自动调整控件大小
在窗口或对话框中自动停靠/锚定控件的机制。
引言
在接下来的文章中,我将描述一种机制,该机制使得在对话框和窗口中使用控件的锚定和停靠成为可能。
控件的停靠和锚定意味着对话框或普通窗口中的某些或所有控件会根据父窗口(例如对话框或视图)的大小调整其大小和位置。您已经看到过很多应用程序中都有这种机制。MS Outlook 中的联系人表单是一个很好的例子。如果您调整表单的大小,输入行、备忘录框和其他控件也会收缩或扩大其高度和/或宽度,以便完美地适应窗口的新区域。
您可能还从 .NET 开发环境中的窗口表单设计器中知道此功能。如果您天生就是一名 .NET 开发人员,并且将或从未打算使用 C 或 C++ 编写 Windows 应用程序,那么本文对您帮助不大。但对于那些仍然编写 ATL/MFC 应用程序或必须维护这些应用程序的人来说,此处提供的代码可能是您应用程序的一个不错的改进。
锚定的思想
停靠和锚定的思想非常简单。您向用户提供一个可调整大小的窗口或对话框,该窗口内的元素会调整其大小和位置,以保持其逻辑位置和大小,或者调整其大小以适应父窗口的区域,这样当窗口变得太小时就不会有浪费的空间或消失的控件。通过这种机制,用户可以调整窗口和控件的大小,并体验到应用程序的更多灵活性。
如果您要为每个对话框和控件编写此代码,并且有很多控件,那么您肯定需要花费一些时间来完成这项任务。此外,在第二个或第三个对话框之后,这项工作会变得枯燥乏味。
BPCtrlAnchorMap
停靠/锚定机制通过初始化一个控件映射表来解决,该映射表包含每个子窗口的初始大小和位置。当父窗口的大小改变时,会调用一个处理函数,该处理函数查找控件映射表,并根据父窗口的相对大小变化来计算控件的新大小和位置。
BPAnchorControlMap(这就是本文的主题)提供了这个控件映射表机制以及所有必需的函数和一些有用的宏。它提供了一种快速简便的方法来在对话框中实现锚定/停靠,并将您的代码编写工作量降至最低。您只需调用两个函数并声明所谓的锚定映射表。
实现 BPCtrlAnchorMap
BPCtrlAnchorMap 可以在任何 MFC 或 ATL 应用程序中实现。通过一些小的修改,它也可以用于没有面向对象框架的纯 win32 C 或 C++ 应用程序。
以下示例基于 MFC 对话框应用程序,并介绍了使之工作的必要步骤。
- 在声明您的类的同一个头文件中包含 bpctrlanchormap.h 文件,或者将其包含在您应用程序的通用头文件中,例如 stdafx.h。
- 在类声明中调用宏
DECLARE_ANCHOR_MAP()
。此宏会生成我们两个必需函数InitAnchors
和HandleAnchors
的声明。我们稍后需要在代码中调用这些函数。 - 在您的窗口的
OnInitDialog
处理程序或OnCreate
处理程序中调用InitAnchors()
函数。重要的是,在调用InitAnchors()
时,您的对话框或窗口的子窗口必须已创建并分配了一个有效的窗口句柄。如果满足此条件,您可以从代码中的任何位置调用InitAnchors()
,但上面提到的两个函数是最佳位置。BOOL CAnchorMapDemoDlg::OnInitDialog() { CDialog::OnInitDialog(); // some initialization code // ... // ... InitAnchors(); return(TRUE); };
可以向
InitAnchors()
函数传递一个标志参数来指定其他功能。ANIF_CALCSIZE (0x0001)
如果在调用
InitAnchors
时指定此标志,则该函数将自行计算窗口所需的尺寸。这对于 FormViews 非常有用,在这些视图中,对话框窗口会在调用InitAnchors
之前调整大小以适应父窗口的区域。(另请参阅“将锚定映射表与 FormViews 结合使用”)ANIF_SIZEGRIP (0x0002)
此标志将指示初始化函数在窗口的右下角添加一个大小调整手柄。大小调整手柄在窗口最大化时会自动隐藏,在窗口处于“正常”或还原状态时会显示。
- 在您的类中添加一个
OnSize(WM_SIZE)
消息处理程序(类向导会为您完成此操作),并从中调用HandleAnchors(&rcWnd)
。&rcWnd
参数是指向RECT
结构的指针,该结构必须包含您的对话框或窗口的实际窗口坐标。您可以通过事先调用GetWindowRect(&rcWnd)
来获取这些坐标。void CAnchorMapDemoDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); CRect rcWnd; GetWindowRect(&rcWnd); HandleAnchors(&rcWnd); // you can alternatively pass NULL for &rcWnd };
您也可以调用
HandleAnchors(NULL)
来让函数为您获取坐标。在这种情况下,您的窗口类必须直接或间接派生自CWnd
(因为需要m_hWnd
)。 - 在您的 C 或 CPP 文件中定义 锚定映射表。(您必须在类外部执行此操作)。锚定映射表定义看起来类似于消息映射定义。我们将在下一节讨论锚定映射表。锚定映射表的简单示例如下。
BEIGN_ANCHOR_MAP(CMyDialog) ANCHOR_MAP_ENTRY(IDC_MYCONTROL, ANF_BOTTOM | ANF_RIGHT) END_ANCHOR_MAP()
- 编译并运行您的应用程序
定义锚定映射表
锚定映射表定义告诉 BPCtrlAnchorMap.h 中的代码,您窗口中的哪些控件应参与停靠/锚定以及这些控件应如何表现。
您使用宏 BEGIN_ANCHOR_MAP(theclass)
开始锚定映射表的定义,其中将 theclass
替换为您窗口类的名称(例如 CMyDialog)。
在此之后,您可以使用宏 ANCHOR_MAP_ENTRY(nIDCtrl, nFlags)
定义一个或多个锚定映射表条目。nIDCtrl
参数是要添加到停靠/锚定机制中的子窗口的 *控件 ID*。nFlags
参数是以下常量的一个或多个组合,用于指定控件的停靠行为。
以下 **停靠标志** 用于将控件停靠到窗口的一个边框。停靠的控件将被移动到其停靠的窗口边框,并会将其宽度或高度调整为与边框相同的宽度或高度,而控件的另一个维度(宽度或高度)将保持不变。请注意,ANF_DOCK
标志不应相互组合,也不应与锚定标志组合。
ANF_DOCK_TOP (0x0001)
此标志将控件停靠在窗口顶部。
ANF_DOCK_BOTTOM (0x0002)
此标志将控件停靠在窗口底部。
ANF_DOCK_LEFT (0x0004)
此标志将控件停靠在窗口左侧。
ANF_DOCK_RIGHT (0x0008)
此标志将控件停靠在窗口右侧。
ANF_DOCK_ALL (0x000F)
此标志将控件停靠在窗口的所有边框。
ANF_DOCK_TOP_EX (0x0200)
将控件停靠在窗口顶部,但保持其原始宽度和高度。
ANF_DOCK_BOTTOM_EX (0x0400)
将控件停靠在窗口底部,但保持其原始宽度和高度。
ANF_DOCK_LEFT_EX (0x0800)
将控件停靠在窗口左侧,但保持其原始宽度和高度。
ANF_DOCK_RIGHT_EX (0x1000)
将控件停靠在窗口右侧,但保持其原始宽度和高度。
以下是 **锚定标志**,用于定义控件的边框与父窗口的边框保持恒定距离。如果父窗口改变其大小,锚定的控件将沿着父窗口的边框移动其边缘。以下标志可以以任何组合方式合并。
ANF_TOP (0x0010)
控件到父窗口顶部的距离将保持恒定。
ANF_BOTTOM (0x0020)
控件到底部父窗口的距离将保持恒定。
ANF_LEFT (0x0040)
控件到父窗口左侧的距离将保持恒定。
ANF_RIGHT (0x0080)
控件到父窗口右侧的距离将保持恒定。
ANF_AUTOMATIC (0x0100)
这是一个特殊标志,它允许代码确定最佳锚定方法。您不应将此标志与任何其他标志组合。
此外,还有一些 **特殊标志**,您可以将其与停靠/锚定标志结合使用。
ANF_ERASE (0x02000)
这是一个特殊标志,它告诉 EraseBackground()
函数擦除控件所占用的区域。EraseBackground()
函数是一个特殊函数,用于减少包含许多控件的窗口中的闪烁。有关此主题的更多信息,请参阅“消除闪烁”。
特殊情况为 NULL
请注意,当您将 **NULL
** 或 0
作为 控件 ID 传递给 ANCHOR_MAP_ENTRY
宏时,有一个特殊情况。控件 ID 为 0
的效果是,父窗口中所有尚未添加到控件映射表中的控件现在都将使用指定的 nFlags 参数添加。定义对话框锚定映射表的快速方法是添加条目 ANCHOR_MAP_ENTRY(NULL, ANF_AUTOMATIC)
。
这将自动添加所有控件,并为每个控件确定最佳锚定方法。
ANCHOR_MAP_ENTRY_RANGE
宏
在最新版本中,我添加了 ANCHOR_MAP_ENTRY_RANGE
宏,可用于将一系列控件 ID 添加到控件映射表中。此宏的使用方式与 ANCHOR_MAP_ENTRY
完全相同,只是您必须传递两个控件 ID 作为参数,一个用于指定范围内的第一个 ID,另一个用于指定最后一个 ID。第三个参数是普通的标志值。
ANCHOR_MAP_ENTRY_RANGE(IDCB_SKILL1, IDCB_SKILL6, ANF_TOP | ANF_LEFT)
现在,在定义完所有锚定映射表条目后,使用宏 END_ANCHOR_MAP()
结束锚定映射表定义。
您现在可以测试并运行您的应用程序了。
将锚定映射表与 FormViews 结合使用
BPControlAnchorMap 机制最初是为对话框设计的,在对话框或父窗口及其控件都具有固定尺寸。当您将此机制与 CFormView
结合使用时会产生一个问题。CFormView
会调整自身大小以适应它所包含的父窗口的区域。由于此过程发生在您调用 InitAnchors()
之前,因此控件映射表将为空,控件也不会正确调整大小。
对此的解决方法是在调用 InitAnchors
函数时使用 ANIF_CALCSIZE
标志。如果您调用 InitAnchors(ANIF_CALCSIZE)
,则 InitAnchors
函数将尝试查找父窗口(您的 CFormView)的原始大小,方法是获取控件映射表中所有控件的右下角坐标。
消除闪烁
在某些情况下,或者当您有包含大量控件的大型对话框时,控件和窗口在调整大小时可能会闪烁。这是因为 WM_ERASBKGND
消息处理程序的默认实现会擦除整个对话框窗口,从而擦除我们的控件。这在控件不移动或改变大小的标准对话框中有效。
为了消除闪烁并将其降至最低,我添加了一个特殊的 EraseBackground
函数,该函数仅擦除未被控件占据的区域。该机制使用一个 Region (HRGN) 对象,该对象初始化为窗口的客户区矩形。初始化区域对象后,EraseBackground
函数会遍历所有子窗口(不仅仅是控件映射表中的那些),并从区域中移除它们的区域。最后,用背景色填充该区域。这可以防止子窗口被背景色覆盖。
我在此技术中遇到一个问题是使用分组框(Group-Boxes)。分组框不绘制其内部背景,并且由于整个分组框区域已从区域中移除,因此其背景未被填充,分组框变得“透明”。为了强制将分组框的区域不从区域中移除,请在使用分组框控件添加到控件映射表时使用 ANF_ERASE
标志。
如果您想使用此自定义 EraseBackground
函数,请在您的窗口中添加一个 WM_ERASEBKGND
处理程序,并在其中调用 m_bpfxAnchorMap.EraseBackground()
来代替默认实现。
BOOL CAnchorMapDemoDlg::OnEraseBkgnd(CDC* pDC) { // Here we call the EraseBackground-Handler from the // anchor-map which will reduce the flicker. return(m_bpfxAnchorMap.EraseBackground(pDC->m_hDC)); }
幕后
以下几行将尝试让您了解所有宏的作用。如果您对代码的工作原理感兴趣,请下载并查看 bpctrlanchormap.h。
整个机制发生在了一个特殊类中,当您调用 DECLARE_ANCHOR_MAP
时,该类将被嵌入到您的对话框类中。bpctrlanchormap.h 文件包含了该类(CBPCtrlAnchorMap
)的声明以及代码。您在类声明中调用的宏 DECLARE_ANCHOR_MAP()
最终会扩展为以下 C++ 代码。
CBPCtrlAnchorMap m_bpfxAnchorMap; void InitAnchors(BOOL bFindCtrlEdges = 0); void HandleAnchors(RECT *pRect);
由于您在类声明中调用 DECLARE_ANCHOR_MAP
,因此成员 m_bpfxAnchorMap
以及函数 InitAnchors
和 HandleAnchors
将添加到您的类中。
InitAnchors
和 HandleAnchors
函数的实现(代码)是通过锚定映射表宏 BEGIN_ANCHOR_MAP
、ANCHOR_MAP_ENTRY
和 END_ANCHOR_MAP
生成的。
BEGIN_ANCHOR_MAP(theclass)
宏最终会扩展为以下代码。
void theclass::HandleAnchors(RECT *pRect) { m_bpfxAnchorMap.HandleAnchors(pRect); } void theclass::InitAnchors(BOOL bFindCtrlEdges) {
请注意,InitAnchors
函数没有以花括号闭合。这是因为该函数的代码尚未完成。它与 ANCHOR_MAP_ENTRY(nCtrlID, nFlags)
一起出现,后者会扩展为以下代码。
m_bpfxAnchorMap.AddControl(nIDCtrl, nFlags);
END_ANCHOR_MAP
宏包含一些额外的 InitAnchors
代码,最后输出函数的闭合花括号。
m_bpfxAnchorMap.Initialize(m_hWnd, bFindCtrlEdges); RECT rcWnd; ::GetWindowRect(m_hWnd, &rcWnd); m_bpfxAnchorMap.HandleAnchors(&rcWnd); };
现在我们已经准备好使用函数了,我们可以从类中调用 InitAnchors
和 HandleAnchors
函数。其余的由 CBPCtrlAnchorMap 的代码完成。
好了,就这样。
我希望您觉得这些代码有用,也许有人能帮助我减少在调整包含许多控件的大型对话框大小时的闪烁问题。
再见,drice!