可调整大小对话框的自动布局






4.90/5 (34投票s)
2005年7月15日
8分钟阅读

105242

3019
一个 WTL 扩展,它引入了布局映射来自动更新可调整大小的对话框中的布局。
引言
最近我参与了一个涉及大量对话框的 WTL 项目,其中大部分对话框的布局方案或多或少都比较复杂,无法通过 Visual Studio 的对话框设计器来描述。此外,它们还需要是可调整大小的,甚至在调整大小时要能保留其布局。
试想一个简单的应用程序,你想让一个控件无论对话框大小如何,都能始终“填充”对话框。或者你想创建一个可调整大小的对话框,它始终能将“确定”和“取消”按钮整齐地放置在角落里。通常,这需要你为 WM_SIZE
、WM_WINDOWPOSCHANGED
或类似的事件编写处理程序,并在对话框类中“手工编码”布局。
对于上述的简单情况,只需一两行代码就可以实现。然而,随着项目中对话框数量的增加——或者其布局的复杂性——你会发现自己一遍又一遍地编写类似的 P代码,或者用布局代码污染你的代码。
布局映射
因此,我提出了一种“半自动”解决方案,它在很大程度上符合 WTL 的精神。这个解决方案称为“布局映射”,并且像所有其他的 ATL/WTL 映射一样,基于宏。尽管我不特别喜欢将大量代码隐藏在看似无害的宏后面,但在这个案例中,我认为它足够了,因为它保持了代码的可读性。
请注意,WTL 已经包含了一个用于此目的的类;它叫做 CDialogResize<T>
,并且(出于某种原因)可以在头文件 atlframe.h 中找到。查看其源代码应该能很快揭示如何使用它。它允许为每个控件指定在对话框调整大小时要执行的操作。该操作可以是*移动*或*调整大小*——或者都没有,如果控件未列出,则默认为*没有*。它还可以在对话框的右下角显示一个“抓手”,并指定其大小限制。事实上,这里提出的解决方案与 CDialogResize<T>
非常相似,但更进一步。
要为你的 WTL 对话框添加对话框布局功能,你只需要遵循以下简单步骤:
- 让你的对话框类从
CDialogLayout<T>
派生(除了CDialogImpl<T>
)。 - 在你的类中添加一个布局映射,使用宏
BEGIN_LAYOUT_MAP()
、END_LAYOUT_MAP()
以及其他几个宏,如下所述。 CDialogLayout<T>
处理WM_SIZE
和WM_INITDIALOG
消息,因此请确保消息处理程序得到正确调用。- 在你的消息映射的末尾调用
CHAIN_MSG_MAP(CDialogLayout<T>)
。 - 如果你自己处理这些消息,请在你的处理程序中调用
SetMsgHandled(FALSE)
(或者如果你还在使用旧式映射,则为bHandled=FALSE
)。
- 在你的消息映射的末尾调用
- 根据你是否想要在右下角有一个“大小抓手”,将
m_bGripper
设置为TRUE
或FALSE
。默认值为TRUE
。
锚点
用于布局控件的关键概念是“锚点”,这也用于 .NET Windows 窗体。任何控件都可以锚定到主对话框的四个边缘(左、上、右、下)的任意组合。如果一个控件锚定到一个边缘,控件与该边缘之间的距离将始终保持不变。
因此,控件的通常行为可以看作是“左上角”锚定(当拖动对话框的左上角时,它们会移动,但拖动右下角时不会),这也正是 CDialogLayout
的默认行为。
示例 1:简单对话框
在这个示例中,我想在用户调整大小时自动布局一个简单的对话框。它只在右下角包含两个按钮:“确定”和“取消”。即使对话框被调整大小,它们也应该固定在右下角。为了实现这一点,可以使用以下代码:
#include <DialogLayout.h> class CTestDialog : public CDialogImpl<CTestDialog>, public CDialogLayout<CTestDialog> { public: enum { IDD = IDD_TESTDIALOG }; BEGIN_MSG_MAP(CTestDialog) MSG_WM_INITDIALOG(OnInitDialog) COMMAND_ID_HANDLER_EX(IDOK, OnOK) COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel) CHAIN_MSG_MAP(CDialogLayout<CTestDialog>) END_MSG_MAP() BEGIN_LAYOUT_MAP() LAYOUT_CONTROL(IDOK, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM) LAYOUT_CONTROL(IDCANCEL, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM) END_LAYOUT_MAP() BOOL OnInitDialog(HWND, LPARAM) { // ... m_Gripper = FALSE; SetMsgHandled(FALSE); return TRUE; } void OnOK(UINT, int, HWND) { EndDialog(IDOK); } void OnCancel(UINT, int, HWND) { EndDialog(IDCANCEL); } };
默认锚点是 LAYOUT_ANCHOR_LEFT | LAYOUT_ANCHOR_TOP
(表示固定在左上角),因此你不必显式列出具有此行为的控件。
示例 2:带有 ListBox 的对话框
下一个示例包含一个带有“确定”/“取消”按钮的对话框,就像上面一样,但它还有一个列表框以及“添加”/“删除”按钮。列表框应该始终“填充”对话框窗口,以便它随对话框一起增长或缩小。这是通过 LAYOUT_ANCHOR_ALL
实现的,它实际上是所有四个标志的按位或运算的简写。同样,你可以使用 LAYOUT_ANCHOR_HORIZONTAL
或 LAYOUT_ANCHOR_VERTICAL
来分别只组合左和右或上和下。
当然,我们在这里只关注布局,所以我们不关心按钮的实际功能。我们只需要在布局映射中添加一些条目。
BEGIN_LAYOUT_MAP() LAYOUT_CONTROL(IDC_LIST, LAYOUT_ANCHOR_ALL) LAYOUT_CONTROL(IDC_BUTTON_ADD, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_TOP) LAYOUT_CONTROL(IDC_BUTTON_REMOVE, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_TOP) LAYOUT_CONTROL(IDOK, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM) LAYOUT_CONTROL(IDCANCEL, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM) END_LAYOUT_MAP()
布局容器
到目前为止,控件的锚点始终指向主对话框窗口的边缘。然而,在某些情况下,这种解决方案不够灵活。这时,“布局容器”就派上用场了。事实上,总有一个布局容器环绕着整个对话框,它由 BEGIN_LAYOUT_MAP()
宏隐式创建。
如果需要,你可以定义额外的布局容器,甚至嵌套它们。为此,请使用宏 BEGIN_LAYOUT_CONTAINER()
和 END_LAYOUT_CONTAINER()
。一个布局容器内的所有 LAYOUT_CONTROL
条目都使用容器的边缘而不是主对话框的边缘进行锚定。
BEGIN_LAYOUT_CONTAINER()
接受四个参数,为容器的四个边缘中的每一个指定一个*布局规则*。有不同类型的布局规则,你可以以任何组合使用它们。
ABS()
规则将一个边缘放置在父容器内的绝对位置,以 DLU(对话框单位)为单位。你也可以使用负数从父容器的右边缘或底边缘开始计数。RATIO()
规则接受一个介于 0.0 和 1.0 之间的浮点数,并将边缘放置在,使其始终以该比例划分父容器。LEFT_OF()
、ABOVE()
、RIGHT_OF()
和BELOW()
规则接受一个对话框控件的 ID,并将容器的边缘与其相应控件的边缘对齐。请注意,如果还有一个控件的LAYOUT_CONTROL()
条目,它应该出现在布局映射中*在*布局容器*之前*。LEFT_OF_PAD()
、ABOVE_PAD()
、RIGHT_OF_PAD()
和BELOW_PAD()
规则的工作方式与上述规则相同,但可以指定额外的*填充*(以 DLU 为单位),该填充将添加到容器边缘和控件之间。
一些示例
- 一个始终与其父容器的所有边缘保持 7 DLU 间距的容器。
BEGIN_LAYOUT_CONTAINER( ABS(7), ABS(7), ABS(-7), ABS(-7) ) // ... END_LAYOUT_CONTAINER()
- 一个始终占据其父容器左上四分之一的容器。
BEGIN_LAYOUT_CONTAINER( ABS(0), ABS(0), RATIO(0.5), RATIO(0.5) ) // ... END_LAYOUT_CONTAINER()
请注意,
ABS(0)
和RATIO(0)
具有相同效果。
布局容器不必附加到对话框中的任何控件。然而,这是它们的一个常见应用(例如,与组框一起),所以我添加了方便的 BEGIN_LAYOUT_CONTAINER_AROUND_CONTROL()
宏,它只接受容器控件的 ID 作为参数。如果你查看它的定义,它只是一个简写。
#define BEGIN_LAYOUT_CONTAINER_AROUND_CONTROL(ctrlID) \
BEGIN_LAYOUT_CONTAINER( LEFT_OF(ctrlID), ABOVE(ctrlID), \
RIGHT_OF(ctrlID), BELOW(ctrlID) )
示例 3:两个 ListBox 和“移动”按钮
下一个示例比前面的示例更复杂,并且(毫不奇怪)需要额外的布局容器。有两个列表框和一些用于在列表框之间移动项目的按钮。同样,我们只关心布局。
- 与之前一样,“确定”和“取消”按钮应保持在对话框的右下角。
- 列表框应始终具有相同的大小,并且每个列表框占据对话框区域宽度的大约一半。
- 移动按钮应同时在列表框之间进行水平和垂直居中。
能够满足这些条件的布局映射将是(请注意,可能有几个等效的布局映射):
BEGIN_LAYOUT_MAP() LAYOUT_CONTROL(IDOK, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM) LAYOUT_CONTROL(IDCANCEL, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM) BEGIN_LAYOUT_CONTAINER( ABS(7), ABS(7), ABS(-7), ABOVE_PAD(IDOK, 7) ) BEGIN_LAYOUT_CONTAINER( ABS(0), ABS(0), RATIO(0.5), RATIO(1.0) ) LAYOUT_CONTROL(IDC_LIST1, LAYOUT_ANCHOR_ALL) END_LAYOUT_CONTAINER() BEGIN_LAYOUT_CONTAINER( RATIO(0.5), ABS(0), RATIO(1.0), RATIO(1.0) ) LAYOUT_CONTROL(IDC_LIST2, LAYOUT_ANCHOR_ALL) END_LAYOUT_CONTAINER() BEGIN_LAYOUT_CONTAINER( RIGHT_OF_PAD(IDC_LIST1, 7), RATIO(0.0), LEFT_OF_PAD(IDC_LIST2, 7), RATIO(0.5) ) LAYOUT_CONTROL(IDC_BUTTON_MOVELEFT, LAYOUT_ANCHOR_LEFT | LAYOUT_ANCHOR_BOTTOM) END_LAYOUT_CONTAINER() BEGIN_LAYOUT_CONTAINER( RIGHT_OF_PAD(IDC_LIST1, 7), RATIO(0.5), LEFT_OF_PAD(IDC_LIST2, 7), RATIO(1.0) ) LAYOUT_CONTROL(IDC_BUTTON_MOVERIGHT, LAYOUT_ANCHOR_LEFT | LAYOUT_ANCHOR_TOP) END_LAYOUT_CONTAINER() END_LAYOUT_CONTAINER() END_LAYOUT_MAP()
下图显示了四个内部容器的排列。
工作原理
尽管这些宏背后有很多代码,但它实际上非常直接。所有宏都映射到以下类之一:CLayoutControl
、CLayoutContainer
或 CLayoutRule
。
使用 BEGIN_LAYOUT_MAP()
宏,你实际上定义了一个名为 SetupLayout()
的方法,该方法在 WM_INITDIALOG
处理程序中被调用一次。此方法创建布局容器和布局规则的树状结构,该结构将一直保留在内存中直到对话框被销毁。
然后,WM_SIZE
处理程序调用 DoLayout()
方法,该方法通过树传播。本质上,这就是所有规则实际应用的地方。然后,控件会使用 DeferWindowPos()
调用一次性重新定位。
已知问题
Windows XP 在使用新的通用控件清单时有时会显示奇怪的行为。这尤其适用于组框,当对话框被调整大小时,组框往往会消失。我认为这是一个 Windows 错误,并采取了一个简单的解决方法,即每次重新定位组框时就重绘它们。但这可能会引入一些闪烁。
布局映射的一个缺点是,你需要为布局映射的每个项目提供控件 ID——即使是通常没有自己 ID 的静态控件。然而,如果你自己编写布局代码,你会需要这些 ID。
结论
使用上述布局映射,可以为可调整大小的对话框实现复杂的布局方案。所提出的解决方案既简单又灵活,并且与 WTL 很好地集成。
修订历史
- 07-16-2005
- 原始文章。
- 01-19-2006
- 添加了显示角落“抓手”的功能。
- 添加了一段关于 WTL 的
CDialogResize
的说明。 - 修正了文章文本和源代码中的一些小错误。