在设计时绘制复杂的 ATL/ActiveX 控件





5.00/5 (3投票s)
一篇讨论在设计时绘制控件的文章
首先,让我们讨论问题,然后我将向您展示解决方案。
问题在于,如果您使用 ATL 创建控件,并且该控件具有子窗口(例如这里的 TreeView),它在运行时会正常工作。通常,它在设计时也会正常工作,但在某些容器中则不行。这是因为某些容器在设计时不会创建您的控件,它们仅实例化它们(通过 CoCreateInstance)并通过 IViewObject 接口进行绘制。它们直接调用 OnDraw(ATL_DRAWINFO& di) 并提供一个设备上下文供您绘制。这可能是一个图元文件、屏幕设备或任何其他设备。
如果调整控件的大小,它可能会在短时间内通过 IOLEObject::DoVerbInPlaceActive(...) 创建,并通过 IOLEObject::Close(...) 销毁。
MS VC++ 中的 ResourceEditor(对话框,见上图)是在设计时以这种方式显示控件的一个容器。
我将向您展示一些图表,以便您了解该怎么做。源代码在文档末尾的示例中。您有两种在设计时绘制控件的可能性(如 OnDraw 的图表中所示)。
![]() |
控件的函数通过调用基类来创建基础控件。子窗口的创建应放在一个单独的函数(CreateChildWindows)中,这样您就可以在不创建控件的情况下创建它们。 |
![]() |
CreateChildWindows 检查是否存在父窗口。如果不存在,它将使用 CreateHiddenWindow 创建一个。如果子窗口不存在,它们将被创建(在新的父窗口上)或我们将在窗口上调用 SetParent。窗口可能已经存在,因为有时控件会被原地激活(例如调整大小),在这种情况下我们只会设置一个新的父窗口。 |
![]() |
如果已经存在一个“HiddenWindow”,我们就返回它,否则我们就创建一个新的,所有参数都设置为 0,窗口类为 _T("Static")。 对于第二个解决方案,我们还必须手动删除 WS_BORDER 和 WS_CAPTION 样式,因为它们是为顶级窗口自动设置的(我们使用 Get / SetWindowLong)。 |
![]() |
我们必须在控件的析构函数中销毁“HiddenWindow”。 |
解决方案一
绘制子窗口,这些子窗口在重载的 WM_PAINT 消息处理程序中进行所有绘制,让系统不做任何事情(没有通用控件!)。
绘制代码应放在一个单独的函数中,例如 OnDraw(HDC hdc, RECT& rc)
,可以从外部调用。因此,WM_PAINT - 消息处理程序仅获取 DC 和客户端矩形并调用 OnDraw,我们也可以从控件的 OnDraw 中这样做。在 OnDraw 函数中,您不得调用任何返回窗口状态或属性的标准函数(如 DC 或 RECT!)!接下来,您可以调用默认的绘制例程(通过 DefWindowProc),因为窗口存在,但它是隐藏的!!!
![]() |
如果控件在调用 OnDraw 时没有窗口(这一定是设计模式,此时 OnDraw 可能通过 IViewObject 调用,否则就会有窗口!)。我们测试窗口是否已存在(子窗口和“HiddenWindow”)。如果不存在,则调用 CreateChildWindows(0) 来创建它们。 然后我们遍历所有子窗口并调用 OnDraw 函数以及参数(我们必须提前计算矩形)。如果您需要任何用于绘制的成员,它们必须提前初始化! |
第二个解决方案
这一点会遇到问题,因为您无法将绘制代码放入一个单独的函数中,因为它不是您自己的。因此,您必须调用树控件的默认处理程序。我们可以使用 ComCtl32.dll 版本 5.80。早期版本也能做到,但并非所有控件都支持。
我们将窗口绘制到位图,因此通常我们只需要将位图blit到容器设备(该位图是控件的一个成员)。只有当旧位图无效时(属性更改、大小更改...),我们才创建一个新位图。
第二个解决方案(a,ComCtl32.dll 版本 >= 5.80)
在此版本的 ComCtl32.dll 中,您可以调用 SendMessage(WM_PAINT, WPARAM(hdc), 0),窗口将绘制到给定的设备上下文。但要小心,它绘制到 (0 / 0),因此您必须移动视口(通过 SetViewportOrgEx),并且窗口不会绘制非客户端区域。这可以通过 DrawEdge(用于边框)完成。
![]() |
如果控件在调用 OnDraw 时没有窗口(这一定是设计模式,此时 OnDraw 可能通过 IViewObject 调用,否则就会有窗口!)。 现在,我们检查是否必须重新创建位图(变量 m_bRecreateBitmap 和 m_sizeBmp)。如果不是,我们就进入 OnDraw 函数的 Blitting 部分。
|
第二个解决方案(b,ComCtl32.dll 版本 < 5.80)
如果您使用的是 ComCtl32.dll 的早期版本,则可能无法正常工作。在这种情况下,您需要显示 HiddenWindow 来将客户端矩形blit到位图。然后您可以隐藏窗口。这有点丑陋,因为窗口会跳到前面,然后在短时间内(约四分之一秒或半秒)隐藏自己。
![]() |
如果控件在调用 OnDraw 时没有窗口(这一定是设计模式,此时 OnDraw 可能通过 IViewObject 调用,否则就会有窗口!)。 现在,我们检查是否必须重新创建位图(变量 m_bRecreateBitmap 和 m_sizeBmp)。如果不是,我们就进入 OnDraw 函数的 Blitting 部分。
|
好了,我们完成了。现在是这个类的源代码(我已经用...替换了无意义的部分,代码是针对解决方案 2 a 的)。
要测试此源代码,您只需创建一个新的 MFC 项目,其中包含一个对话框,并将新控件放在上面。然后您将看到我们的新绘制例程生效。如果您编译项目并运行它,您将看到正常的绘制例程。
DrawingDesignmodeObj.h
class ATL_NO_VTABLE CDesignTimePaintingObj :
public ...
{
public:
/////////////////////////////////////////////////////////////////////////
// ATL - Makros
/////////////////////////////////////////////////////////////////////////
...
/////////////////////////////////////////////////////////////////////////
// protected functions and Members
/////////////////////////////////////////////////////////////////////////
protected:
// the contained control (a tree control)
CContainedWindow m_wndTree; // window - object
// variable for the OnSize Messagehandler
bool m_bInitialised; // Indicates, that the Childwindows are created,
// so we can size them
// variables for designtimepainting
HWND m_hHiddenWnd; // handle of the "HiddenWindow"
HBITMAP m_hBmp; // handle of the Bitmap used for faster painting
bool m_bRecreateBitmap; // indicates, wether the bitmap must be
// recreated or not
SIZE m_sizeBmp; // size of the actual bitmap
/////////////////////////////////////////////////////////////////////////
// helperfunctions (declared in DesignTimePaintingObj.cpp)
/////////////////////////////////////////////////////////////////////////
void SizeViews();
void FillTree(void);
BOOL CreateChildWindows(HWND hParent);
void CreateHiddenWindow();
/////////////////////////////////////////////////////////////////////////
// public functions and members
/////////////////////////////////////////////////////////////////////////
public:
/////////////////////////////////////////////////////////////////////////
// standard functions (declared in DesignTimePaintingObj.cpp)
/////////////////////////////////////////////////////////////////////////
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);
/////////////////////////////////////////////////////////////////////////
// messagehandlers (declared in DesignTimePaintingObj.cpp)
/////////////////////////////////////////////////////////////////////////
LRESULT OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
LRESULT OnSetFocus(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
};
DrawingDesignmodeObj.cpp
...
/////////////////////////////////////////////////////////////////////////
// standard functions
/////////////////////////////////////////////////////////////////////////
CDesignTimePaintingObj::CDesignTimePaintingObj() :
m_wndTree(_T("SysTreeView32"), this, 1)
{
// This is only initialising, nothig important
...
}
CDesignTimePaintingObj::~CDesignTimePaintingObj()
{
// free all members
::DeleteObject(m_hBmp);
::DestroyWindow(m_hHiddenWnd);
}
// In the Create - function, we have to create the ContainedWindows via their
// Create - functions
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;
}
// This function does nothing, if the control was instantiated and created.
// But some containers instantiate controls
// without creating them, and they draw them through the interface IViewObject::Draw.
// So this is the interisting part of the project
HRESULT CDesignTimePaintingObj::OnDraw(ATL_DRAWINFO& di)
{
RECT& rc = *(RECT*)di.prcBounds;
// we are drawn throug IViewObject?
if(m_hWnd == 0)
{
// was there a resize since the last painting?
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;
}
// should we recreate the Bitmap?
if(m_bRecreateBitmap == true)
{
// First, create the Windows, so that they can draw themselves
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;
// Create the Bitmap
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;
// prepare painting the NonClientArea of the TreeView
poiOffset.x = ::GetSystemMetrics(SM_CXEDGE) + 1;
poiOffset.y = ::GetSystemMetrics(SM_CYEDGE) + 1;
// Select the Bitmap as a drawing >Context
HDC hBmpDC = ::CreateCompatibleDC(di.hdcDraw);
HBITMAP hOldBmp = (HBITMAP) ::SelectObject(hBmpDC, m_hBmp);
// Position the Windows in the Hidden one
m_wndTree.SetWindowPos(HWND_TOP, &rc, SWP_NOZORDER);
::SetViewportOrgEx(hBmpDC, poiOffset.x, poiOffset.y, &pt);
// force The temporary Window to Draw itself and copy it into the Bitmap
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);
}
// Copy the Bitmap to the Container
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;
}
/////////////////////////////////////////////////////////////////////////
// messagehandlers
/////////////////////////////////////////////////////////////////////////
// set the focus to the childwindow (the code was created by the wizard)
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;
}
// nothing important
LRESULT CDesignTimePaintingObj::OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
UNUSEDPARAM(uMsg);
UNUSEDPARAM(wParam);
UNUSEDPARAM(lParam);
UNUSEDPARAM(bHandled);
SizeViews();
return 0;
}
/////////////////////////////////////////////////////////////////////////
// helperfunctions
/////////////////////////////////////////////////////////////////////////
// Create the childwindow on the given window. If no window is specified by
// hParent (if it's NULL)
// we create our "HiddenWindow" and put the childs on this one.
BOOL CDesignTimePaintingObj::CreateChildWindows(HWND hParent)
{
// should we create our "HiddenWindow"
if(hParent == 0)
{
CreateHiddenWindow();
hParent = m_hHiddenWnd;
}
// If the Windows already exist, only set a new parent
if(m_wndTree.m_hWnd != NULL)
{
m_wndTree.SetParent(hParent);
}
else
{
RECT rc = {0,0,0,0};
// only create the window
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);
// this is only for this example so you can see something
FillTree();
}
return TRUE;
}
// Creates our HiddenWindow which acts as a parentwindow, which will never be seen.
void CDesignTimePaintingObj::CreateHiddenWindow()
{
// if no HiddenWindow exists, create on
if(m_hHiddenWnd == NULL)
{
long lStyle;
m_hHiddenWnd = ::CreateWindow(_T("STATIC"), _T(" "), 0, 0, 0, 0, 0, 0, 0,
0, NULL);
// remove all Borders, captions and other nonsens
lStyle = ::GetWindowLong(m_hHiddenWnd, GWL_STYLE);
lStyle &= ~(WS_BORDER | WS_CAPTION);
::SetWindowLong(m_hHiddenWnd, GWL_STYLE, lStyle);
}
}
// only resize the childwindows
void CDesignTimePaintingObj::SizeViews()
{
if(m_bInitialised == true)
{
RECT rc;
GetClientRect(&rc);
m_wndTree.SetWindowPos(HWND_TOP, &rc, SWP_NOZORDER | SWP_DRAWFRAME);
}
}
// I put some items in the tree, so you can see something, not important
void CDesignTimePaintingObj::FillTree()
{
...(putting stuff in the tree here)...
}
我认为在控件上放置更多窗口应该没有问题,您只需要在 OnDraw 中绘制更多窗口。
玩得开心
Gerolf