First let's discuss the problem, then let me show you a solution.
The problem is, that if you create a control with ATL, and this control has childwindows (like the TreeView here), it will work fine in runmode. Normally it will also work fine in designmode, but in some containers it won't. That is because some containers don't create your control at designtime, they only instantiate them (via CoCreateInstance) and draw it through the interface IViewObject. They call OnDraw(ATL_DRAWINFO& di) directly and give you a devicecontext to draw on. That can be a metafile or a screen device, or some other device.
If you resize the control, it is possible that it will be created for a short time through IOLEObject::DoVerbInPlaceActive(...) and will be destroyed by IOLEObject::Close(...).
One container that shows controls at designtime this way is the ResourceEditor in MS VC++ (Dialogs, see picture above).
I'll show you some diagrams so you can see what to do. The sourcecode is in the example at the end of the document. You have two posibilities of drawing the control at designtime (shown in the diagramm for OnDraw):
|
The function of the control creates the basecontrol by calling to the baseclass. The Creation of the Childwindows should be put to an extra function (CreateChildWindows) so you can create them without creating the control. |
|
CreateChildWindows checks if there is a parentwindow. If not, it creates one with CreateHiddenWindow. If the childs doesn't exist they are now created (on the new parentwindow) or we call SetParent on the windows. It is possible that the windows exist because sometimes the control is put inplace active (p.e. resizing it) and in this case we will only set a new parent. |
|
If there already exists a "HiddenWindow", we return it, else we create a new one with all parameters set to 0 and the windowclass to _T("Static").
For the second solution, we also have to remove the WS_BORDER and WS_CAPTION style by hand, because they are set automatically for a toplevel window (we use Get / SetWindowLong).
|
|
We have to destroy the "HiddenWindow" in the destructor of the controls. |
First Solution
Drawing childwindows which do all drawing in an overloaded WM_PAINT message handler and let the system do nothing (no common controls!)
The painting code should be put to an extra function like OnDraw(HDC hdc, RECT& rc)
that can be called from outer. So the WM_PAINT - messagehandler only gets the DC and the clientrect and calls OnDraw and we can do so from out OnDraw of the Control. In the OnDraw function, you must not call any standard functions that return states or properties of the window (like a DC or a RECT)! Next, you are allowed to call the default painting routines (via DefWindowProc), because the window exists, but is Hidden!!!
|
If the control dosn't have a window, when the OnDraw is called (this must be designmode, where the OnDraw could be called through IViewObject, elsewhere there would be a window!) We test, if the windows already exist (childs and "HiddenWindow"). If not, call CreateChildWindows(0) to create them.
Then we iterate through all childwindows and call the OnDraw functions with the parameters (we have to calculate the rects before). If you need any members for painting, they must be initialized before!
|
Second Solution
Drawing childwindows which call their WM_PAINT messagehandler in order to do standard painting (like a treecontrol)
This is the point where you get to problems because you can't put the drawing code into an extra function because it is not yours. So you have to call the default handler of the treecontrol. We can use this with ComCtl32.dll in Version 5.80. Earlier versions are able to do so, but not with all controls.
We draw the Windows to a Bitmap, so normally we only have to blit the Bitmap to the container device (this Bitmap is a member of the control). We only create a new bitmap, if the old one is invalid (properties change, size changes, ...)
Second Solution (a, ComCtl32.dll version >= 5.80)
In this version on ComCtl32.dll, you can call SendMessage(WM_PAINT, WPARAM(hdc), 0) and the window will draw to the given devicecontext. but be careful, it draws to (0 / 0), so you have to move the viewport (via SetViewportOrgEx) and the window won't draw the nonclientarea. This could be done by DrawEdge (for Borders).
|
If the control doesn't have a window, when the OnDraw is called (this must be designmode, where the OnDraw could be called through IViewObject, elsewhere there would be a window!). Now, we check if the bitmap must be recreated (variables m_bRecreateBitmap and m_sizeBmp). If not, we got the the Blitting part of the OnDraw function.
- if recreating is neccassary, we check, if the childwindows already exist (childs and "HiddenWindow"). If not, call CreateChildWindows(0) to create them.
- destroy the old bitmap
- create the new one (with the new size, located in di.prcBounds)
- calculate all offsets of the windows,like you create them on the control, so you can draw the windows to the correct position
- now draw all windows to their positions via
- ::SetWindowPos(..., rcThisChild, ...)
- ::SetViewportOrgEx(...)
- ::SendMessage(hwndChild, WM_PAINT, (WPARAM) hdc, 0)
- next, draw the nonclientarea of the windows, like borders, Scrollbars, ...
- ::SetViewportOrgEx(hdc, (0/0) of the control (save at start of OnDraw),...)
- ::DrawEdge(...)
- draw scrollbars as dummies (use FillRect and DrawEdge, there will be no arrows, but that doesn't matter!)
Now, blit the Bitmap to the container device (di.hdcDraw). |
Second Solution (b, ComCtl32.dll version < 5.80)
If you use an earlier version of ComCtl32.dll, it could be possible that this doesn't work. In this case, you need to show the HiddenWindow to Blit the clientrect to the bitmap. Then you can hide the window. This is a bit ugly, because a window jumps to the front and hides itself short time later (about fourth second, or a half).
|
If the control doesn't have a window, when the OnDraw is called (this must be designmode, where the OnDraw could be called through IViewObject, elsewhere there would be a window!). Now, we check if the bitmap must be recreated (variables m_bRecreateBitmap and m_sizeBmp). If not, we got the the Blitting part of the OnDraw function.
- if recreating is neccassary, we check, if the childwindows already exist (childs and "HiddenWindow"). If not, call CreateChildWindows(0) to create them.
- destroy the old bitmap
- create the new one (with the new size, located in di.prcBounds)
- show the "HiddenWindow" (via ::SetWindowPos(..., HWND_TOP; ..., ... | SWP_SHOWWINDOW))
- call ::SetWindowPos for all clientwindows
- Call UpdateWindow(m_hHiddenWindow) to force repainting (including clientarea)
- BitBlt from the HiddenWindow to the local bitmap
- Hide the HiddenWindow
Now, blit the Bitmap to the containerdevice (di.hdcDraw). |
That's it, we have finished. Now the sourcecode for this class (I replaced nonsense by ..., the code is for solution 2 a):
To test this sourcecode, you only need to create a new MFC project with a dialog and put the new control on it. Then you see our new painting routines work. If you compile the project and start it, you see the normal painting routines.
DrawingDesignmodeObj.h
class ATL_NO_VTABLE CDesignTimePaintingObj :
public ...
{
public:
...
protected:
CContainedWindow m_wndTree;
bool m_bInitialised;
HWND m_hHiddenWnd;
HBITMAP m_hBmp;
bool m_bRecreateBitmap;
SIZE m_sizeBmp;
void SizeViews();
void FillTree(void);
BOOL CreateChildWindows(HWND hParent);
void CreateHiddenWindow();
public:
CDesignTimePaintingObj();
~CDesignTimePaintingObj();
HRESULT OnDraw(ATL_DRAWINFO& di);
virtual HWND Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName = NULL,
DWORD dwStyle = WS_CHILD | WS_VISIBLE, DWORD dwExStyle = 0,
UINT nID = 0);
LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
LRESULT OnSetFocus(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
};
DrawingDesignmodeObj.cpp
...
CDesignTimePaintingObj::CDesignTimePaintingObj() :
m_wndTree(_T("SysTreeView32"), this, 1)
{
...
}
CDesignTimePaintingObj::~CDesignTimePaintingObj()
{
::DeleteObject(m_hBmp);
::DestroyWindow(m_hHiddenWnd);
}
HWND CDesignTimePaintingObj::Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName,
DWORD dwStyle, DWORD dwExStyle, UINT nID)
{
m_bInitialised = false;
HWND hWnd = CWindowImpl<CDesignTimePaintingObj>::Create(hWndParent, rcPos,
szWindowName, dwStyle, dwExStyle, nID);
if(hWnd != NULL)
{
CreateChildWindows(hWnd);
}
m_bInitialised = true;
SizeViews();
return hWnd;
}
HRESULT CDesignTimePaintingObj::OnDraw(ATL_DRAWINFO& di)
{
RECT& rc = *(RECT*)di.prcBounds;
if(m_hWnd == 0)
{
SIZE newSize = {abs(rc.right - rc.left), abs(rc.bottom - rc.top)};
if((newSize.cx != m_sizeBmp.cx) || (newSize.cy != m_sizeBmp.cy))
{
m_bRecreateBitmap = true;
}
if(m_bRecreateBitmap == true)
{
CreateChildWindows(0);
if(m_hBmp != NULL)
{
::DeleteObject(m_hBmp);
m_hBmp = NULL;
}
m_bRecreateBitmap = false;
m_sizeBmp.cx = newSize.cx;
m_sizeBmp.cy = newSize.cy;
m_hBmp = ::CreateCompatibleBitmap(di.hdcDraw, m_sizeBmp.cx, m_sizeBmp.cy);
if(m_hBmp == NULL)
{
m_bRecreateBitmap = true;
return S_OK;
}
POINT pt;
POINT poiOffset;
poiOffset.x = ::GetSystemMetrics(SM_CXEDGE) + 1;
poiOffset.y = ::GetSystemMetrics(SM_CYEDGE) + 1;
HDC hBmpDC = ::CreateCompatibleDC(di.hdcDraw);
HBITMAP hOldBmp = (HBITMAP) ::SelectObject(hBmpDC, m_hBmp);
m_wndTree.SetWindowPos(HWND_TOP, &rc, SWP_NOZORDER);
::SetViewportOrgEx(hBmpDC, poiOffset.x, poiOffset.y, &pt);
m_wndTree.SendMessage(WM_PAINT, (WPARAM)hBmpDC, 0);
::SetViewportOrgEx(hBmpDC, pt.x, pt.y, NULL);
::DrawEdge(hBmpDC, &rc, EDGE_BUMP, BF_RECT);
::SelectObject(hBmpDC, hOldBmp);
::DeleteDC(hBmpDC);
}
HDC hBmpDC = ::CreateCompatibleDC(di.hdcDraw);
HBITMAP hOldBmp = (HBITMAP) ::SelectObject(hBmpDC, m_hBmp);
::BitBlt(di.hdcDraw, rc.left, rc.top, m_sizeBmp.cx, m_sizeBmp.cy, hBmpDC, 0,
0, SRCCOPY);
::SelectObject(hBmpDC, hOldBmp);
::DeleteDC(hBmpDC);
}
return S_OK;
}
LRESULT CDesignTimePaintingObj::OnSetFocus(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
LRESULT lRes = CComControl<CDesignTimePaintingObj>::OnSetFocus(uMsg, wParam, lParam,
bHandled);
if (m_bInPlaceActive)
{
DoVerbUIActivate(&m_rcPos, NULL);
if(!IsChild(::GetFocus()))
m_wndTree.SetFocus();
}
return lRes;
}
LRESULT CDesignTimePaintingObj::OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
UNUSEDPARAM(uMsg);
UNUSEDPARAM(wParam);
UNUSEDPARAM(lParam);
UNUSEDPARAM(bHandled);
SizeViews();
return 0;
}
BOOL CDesignTimePaintingObj::CreateChildWindows(HWND hParent)
{
if(hParent == 0)
{
CreateHiddenWindow();
hParent = m_hHiddenWnd;
}
if(m_wndTree.m_hWnd != NULL)
{
m_wndTree.SetParent(hParent);
}
else
{
RECT rc = {0,0,0,0};
UINT styles = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | TVS_HASLINES |
TVS_HASBUTTONS | TVS_LINESATROOT | WS_BORDER;
m_wndTree.Create(hParent, rc, NULL, styles, WS_EX_CLIENTEDGE);
FillTree();
}
return TRUE;
}
void CDesignTimePaintingObj::CreateHiddenWindow()
{
if(m_hHiddenWnd == NULL)
{
long lStyle;
m_hHiddenWnd = ::CreateWindow(_T("STATIC"), _T(" "), 0, 0, 0, 0, 0, 0, 0,
0, NULL);
lStyle = ::GetWindowLong(m_hHiddenWnd, GWL_STYLE);
lStyle &= ~(WS_BORDER | WS_CAPTION);
::SetWindowLong(m_hHiddenWnd, GWL_STYLE, lStyle);
}
}
void CDesignTimePaintingObj::SizeViews()
{
if(m_bInitialised == true)
{
RECT rc;
GetClientRect(&rc);
m_wndTree.SetWindowPos(HWND_TOP, &rc, SWP_NOZORDER | SWP_DRAWFRAME);
}
}
void CDesignTimePaintingObj::FillTree()
{
...(putting stuff in the tree here)...
}
I think it will be no problem to put more windows on the controls, you only have to draw more windows in the OnDraw.
Have much fun
Gerolf