65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (34投票s)

2005年7月15日

8分钟阅读

viewsIcon

105242

downloadIcon

3019

一个 WTL 扩展,它引入了布局映射来自动更新可调整大小的对话框中的布局。

引言

最近我参与了一个涉及大量对话框的 WTL 项目,其中大部分对话框的布局方案或多或少都比较复杂,无法通过 Visual Studio 的对话框设计器来描述。此外,它们还需要是可调整大小的,甚至在调整大小时要能保留其布局。

试想一个简单的应用程序,你想让一个控件无论对话框大小如何,都能始终“填充”对话框。或者你想创建一个可调整大小的对话框,它始终能将“确定”和“取消”按钮整齐地放置在角落里。通常,这需要你为 WM_SIZEWM_WINDOWPOSCHANGED 或类似的事件编写处理程序,并在对话框类中“手工编码”布局。

对于上述的简单情况,只需一两行代码就可以实现。然而,随着项目中对话框数量的增加——或者其布局的复杂性——你会发现自己一遍又一遍地编写类似的 P代码,或者用布局代码污染你的代码。

布局映射

因此,我提出了一种“半自动”解决方案,它在很大程度上符合 WTL 的精神。这个解决方案称为“布局映射”,并且像所有其他的 ATL/WTL 映射一样,基于宏。尽管我不特别喜欢将大量代码隐藏在看似无害的宏后面,但在这个案例中,我认为它足够了,因为它保持了代码的可读性。

请注意,WTL 已经包含了一个用于此目的的类;它叫做 CDialogResize<T>,并且(出于某种原因)可以在头文件 atlframe.h 中找到。查看其源代码应该能很快揭示如何使用它。它允许为每个控件指定在对话框调整大小时要执行的操作。该操作可以是*移动*或*调整大小*——或者都没有,如果控件未列出,则默认为*没有*。它还可以在对话框的右下角显示一个“抓手”,并指定其大小限制。事实上,这里提出的解决方案与 CDialogResize<T> 非常相似,但更进一步。

要为你的 WTL 对话框添加对话框布局功能,你只需要遵循以下简单步骤:

  1. 让你的对话框类从 CDialogLayout<T> 派生(除了 CDialogImpl<T>)。
  2. 在你的类中添加一个布局映射,使用宏 BEGIN_LAYOUT_MAP()END_LAYOUT_MAP() 以及其他几个宏,如下所述。
  3. CDialogLayout<T> 处理 WM_SIZEWM_INITDIALOG 消息,因此请确保消息处理程序得到正确调用。
    1. 在你的消息映射的末尾调用 CHAIN_MSG_MAP(CDialogLayout<T>)
    2. 如果你自己处理这些消息,请在你的处理程序中调用 SetMsgHandled(FALSE)(或者如果你还在使用旧式映射,则为 bHandled=FALSE)。
  4. 根据你是否想要在右下角有一个“大小抓手”,将 m_bGripper 设置为 TRUEFALSE。默认值为 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_HORIZONTALLAYOUT_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()

下图显示了四个内部容器的排列。

工作原理

尽管这些宏背后有很多代码,但它实际上非常直接。所有宏都映射到以下类之一:CLayoutControlCLayoutContainerCLayoutRule

使用 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 的说明。
    • 修正了文章文本和源代码中的一些小错误。
© . All rights reserved.