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

Sizers:一个可扩展的布局管理库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2000年1月17日

viewsIcon

169050

downloadIcon

1282

一篇关于可扩展布局管理类的文章。

Sample Image - sizer.gif

引言

Java 程序员早就知道窗口管理器的用处。如果一个对话框或视图是可调整大小的,你需要某种方式来移动和调整子窗口的大小。Win32 API 或 MFC 在这项工作上都没有提供帮助。人们已经进行了无数次尝试来纠正这个问题,但所有这些尝试都缺乏以下一个或多个方面:

  1. 完全依赖 MFC 基类。如果库包含一个 CResizableDlg,而你需要为依赖于其他基类的对话框添加功能,你基本上就束手无策了。有时,解决方案提供了一些方法来修改你自己的 CWnd 类以使其可调整大小,但这种方法总是需要大量的编码。
  2. 完全依赖 MFC、ATL 或其他框架。这方面的问题较小,但我们有些人并非所有项目都使用 MFC(或 ATL 等)。我们有些人使用其他框架、直接使用 Win32 API 等。
  3. C API。这更不是问题。然而,使用面向对象的方法通常更容易。
  4. 封闭式架构。无法扩展库以使用独特的算法来布局子窗口。这实际上是最糟糕的缺点之一,因为通常提供的算法只提供了最简单的窗口布局方式。
  5. 非开源。你必须为使用该库付费,而且通常价格不菲。布局管理类通常只是一个更大库的子集,因此为项目添加布局管理的成本可能高达数千美元。

我在这里提供的库,希望是第一个没有这些缺点的 Win32 窗口布局管理库。该库完全面向对象,用 C++ 编写,不依赖 MFC。该库是可扩展的,允许你定义自己的布局机制。为窗口添加布局管理既快速又简单,不依赖其他库。典型的窗口布局代码只需几行代码即可提供,无需实现任何消息处理程序。

通用用法

本节描述了尺寸限制对象的通用用法。

CSizer 是“sizer”的基类,sizer 是一种根据特定约束机制约束其他子 sizer 的类。派生类可以指定自己的约束机制。每个 sizer 都可以有一个关联的窗口,该窗口将随 sizer 自动调整大小。如果你熟悉 Java,你可以将 sizer 视为等同于 LayoutManager、Container 和 Component,所有这些都同时存在。该库中定义了几个 CSizer 派生类来处理不同的约束机制,并且可以很容易地派生你自己的类来处理其他约束机制。

有关可用尺寸器及其使用方法的完整讨论,请参阅尺寸器 API 部分。目前,我将仅提供使用尺寸器布局可动态调整大小的复杂对话框的一般指南。

1. 添加源文件

首先,你需要使用菜单选项“项目 > 添加到项目 > 文件”将源文件添加到你的项目中。你需要的文件是

  • Sizer.h
  • Sizer.cpp
  • DeferPos.h
  • DeferPos.cpp
  • SubclassWnd.h
  • SubclassWnd.cpp

你可能希望将这些文件放在 LIB 或 DLL 中以便于重用。

2. 创建对话框

创建一个示例如下所示的对话框

sample sizer image

3. 创建 MFC 对话框类

从示例对话框创建一个 MFC 对话框类。你应该知道 sizer 库不要求使用 MFC。我们在这个示例中使用它仅仅是为了展示该库如何与 MFC 共存。

4. 修改 OnInitDialog 方法

我们将修改 OnInitDialog 方法以实际实现布局约束。你的代码应该类似于这样

BOOL CSizerTestDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // Set the icon for this dialog.  The framework does this automatically
    //  when the application's main window is not a dialog
    SetIcon(m_hIcon, TRUE);            // Set big icon
    SetIcon(m_hIcon, FALSE);        // Set small icon
    
    // TODO: Add extra initialization here

    // Create the "root" sizer for this dialog to be a vertical "row sizer"
    CRowSizer* pRoot = new CRowSizer(this, CRowSizer::VERTICAL);

    // Add the static text to the root
    pRoot->AddChild(new CFixedSizer(GetDlgItem(IDC_STATIC1), 
                                           CFixedSizer::HEIGHT), 0);

    // Create a new sizer to hold some controls
    // in a horizontal "row sizer" and add
    // the row to the root.
    CRowSizer* pRow = new CRowSizer(CRowSizer::HORIZONTAL);
    pRow->AddChild(new CFixedSizer(GetDlgItem(IDC_NEW_ITEM_EDT), 
                                            CFixedSizer::HEIGHT), 1);
    pRow->AddChild(new CFixedSizer(GetDlgItem(IDC_ADD_BTN), 
                                             CFixedSizer::WIDTH), 0);
    pRoot->AddChild(pRow, 0);

    // Add some more controls to the root.
    pRoot->AddChild(new CFixedSizer(GetDlgItem(IDC_STATIC2), 
                                          CFixedSizer::HEIGHT), 0);
    pRoot->AddChild(new CSizer(GetDlgItem(IDC_ITEM_LST)), 1);

    // Create another sizer to hold some more
    // controls in a horizontal "row sizer" and
    // add the row to the root.
    pRow = new CRowSizer(CRowSizer::HORIZONTAL);
    pRow->AddChild(new CSizer(), 1);
    pRow->AddChild(new CFixedSizer(GetDlgItem(IDOK)), 0);
    pRow->AddChild(new CFixedSizer(GetDlgItem(IDCANCEL)), 0);
    pRoot->AddChild(pRow, 0);

    pRoot->LayoutChildren();

    return TRUE;  // return TRUE  unless you set the focus to a control
}

你需要做的第一件事是创建一个与对话框关联的尺寸器。请注意,尺寸器必须始终通过调用 new 在堆上创建。内存管理将自动为你处理,因为尺寸器要么由其父级删除,要么在关联窗口销毁时删除。在我们的示例中,我们将对话框的尺寸器创建为 CRowSizer,它以垂直或水平行(在此示例中我们指定垂直)布局其子级。添加到尺寸器的每个子级将首先调整到其最小尺寸,然后放置在行中。之后,计算剩余空间,并根据添加时指定的“权重”将子级扩展以填充所有可用空间(保持行布局)。如果所有添加的子级的总权重为 9,并且单个子级的权重为 1,它将扩展以填充可用空间的 1/9。这是更灵活的尺寸器之一。

我们添加到对话框的尺寸器的第一个子项是用于静态“新建项目:”标签的尺寸器。这个尺寸器不会有任何子项,但我们希望确保它保持其固定高度,因此我们使用 CFixedSizerCFixedSizer 可以保持固定高度、宽度或两者。它永远不会有自己的子项,但非常适合像这种情况,我们需要保持固定大小,而不管父项想对我们做什么。

我们接下来要添加到约束系统的是编辑框和“添加”按钮,但这些需要并排放置,而不是垂直排列。这可以通过嵌套尺寸器轻松实现。因此,我们创建另一个 CRowSizer,这次是水平的。编辑控件与具有固定高度的 CFixedSizer 相关联,按钮与具有固定宽度的 CFixedSizer 相关联。这些尺寸器被添加到水平 CRowSizer 中,而水平 CRowSizer 又被添加到对话框的垂直 CRowSizer 中。

另一个静态标签使用具有固定高度的 CFixedSizer 添加到对话框的 CRowSizer 中。

接下来,我们将添加一个与列表控件关联的通用 CSizer。我们使用通用 CSizer 是因为我们不关心以任何方式约束控件,并且它不会有任何子级。对话框尺寸器将可以随意调整此子级的大小。

最后,我们将再次使用嵌套尺寸器技术,为“确定”和“取消”按钮添加另一排水平尺寸器。使它们右对齐的诀窍是,首先向此 CRowSizer 添加一个不与任何窗口关联的通用 CSizer。由于按钮使用 CFixedSizer 对象(在两个维度上都固定),通用尺寸器将接收所有可用的额外空间,有效地将按钮推到最右侧。

这是一个相对简单的示例,但希望它足以演示如何使用尺寸器为任何窗口定义布局约束。

尺寸器 API

此库定义了以下尺寸器类

CSizer
通用尺寸器。此尺寸器没有子级,除了任何关联所有者的最小/最大尺寸约束之外,不指定任何约束。
CFixedSizer
此尺寸器没有子级,但将其自身约束为固定大小,适用于宽度、高度或两者。
CRowSizer
此尺寸器约束其子级以垂直或水平行的方式填充其客户端空间。子级最初调整到其最小尺寸,然后根据其关联的权重调整大小以填充任何剩余空间。如果所有子级的总权重为 9,并且单个子级的权重为 1,则它将获得剩余空间的 1/9。如果子级在未占用所有分配的额外空间的情况下达到其最大尺寸,则剩余空间可能会多次计算。
CFillSizer
此尺寸器约束其所有子级完全填充其客户区。如果有多个子级,它们将重叠。

在描述上述类的 API 之前,应该定义一些通用术语。

Sizer
Sizer 是一个对象,它定义尺寸约束以及任何可能的子 sizer 的可能约束机制。并非所有 sizer 都能拥有子级。sizer 可能有一个“所有者”。
Owner
所有者是关联的窗口。所有者随尺寸器调整大小。
约束
一组指定窗口如何调整大小和移动的规则。
内边距
内边距定义了 sizer 边框和其所有者边框之间的空间。每个边框都可以有自己的内边距尺寸。
区域 (Areas)
区域是矩形规范(使用 RECT 数据类型)用于尺寸器的各个部分。尺寸器的 rect 是外部区域。所有者区域紧随其后,定义为尺寸器区域减去尺寸器的内边距。客户端区域紧随其后,并根据所有者窗口的客户端区域定义。通常,子级将被约束(或裁剪)到客户端区域。如果尺寸器没有关联的所有者,则仍将有所有者和客户端区域,它们将完全相同,并定义为尺寸器区域减去内边距。

尺寸器类的接口定义如下

CSizer

CSizer()
CSizer(HWND hWndOwner)
CSizer(CWnd* pWndOwner)    // MFC only

构造一个带或不带所有者的 CSizer 对象。

HWND GetOwner()

获取关联所有者的 HWND,如果没有则返回 NULL

HWND GetParentWindow()

获取父窗口的 HWND。即使尺寸器没有关联的所有者,也可以通过调用父尺寸器的 GetParentWindow() 来实现。

void SetParent(CSizer* pParent)

设置尺寸器的父级。这应仅由派生类在其将尺寸器添加到其子列表后调用。此方法确保尺寸器从任何现有父级列表中移除。

CSizer* GetParent()
const CSizer* GetParent() const

获取指向父尺寸器的指针。

void SetRect(RECT rect)

将尺寸器的区域设置为指定的 RECT。请注意,所有者不会调整大小/移动,直到调用 OnRealizeLayout,该方法通常由框架调用。

void GetRect(LPRECT pRect)

获取尺寸器的区域。

void GetOwnerRect(LPRECT pRect)

获取所有者的区域。请注意,如果尺寸器的布局尚未实现,此区域可能与在所有者上调用 GetWindowRect 返回的区域不同。即使没有所有者,此方法也有效。

void GetClientRect(LPRECT pRect)

获取客户端区域。请注意,如果尺寸器的布局尚未实现,此区域可能与在所有者上调用 GetClientRect 返回的区域不同。即使没有所有者,此方法也有效。

void SetInsets(int nInsets)
void SetInsets(RECT insets)

设置尺寸器的内边距。

void GetInsets(LPRECT pInsets)

获取尺寸器的内边距。

void GetMinMaxInfo(MINMAXINFO* pMMI)

获取尺寸器的最小和最大尺寸。这通常基于所有者的最小/最大信息,并考虑了内边距。派生类还可以考虑其任何子级的最小/最大信息。

void LayoutChildren()

根据尺寸器的约束机制(如果有)布局子尺寸器。

void OnRemoveChild(CSizer* pSizer)

这应由派生类重写,以从其子列表中移除子级。不应直接调用此方法。

void OnGetMinMaxInfo(MINMAXINFO* pMMI)

这应由派生类重写,以计算尺寸器的最小/最大尺寸。此计算不应考虑尺寸器的内边距。派生尺寸器应调用此方法的基类版本。不应直接调用此方法。而是调用 GetMinMaxInfo

void OnLayoutChildren()

这应由派生类重写,以根据尺寸器的约束机制布局任何子尺寸器。不应直接调用此方法。而是调用 LayoutChildren

void OnRealizeLayout(CDeferPos& dp)

这应由派生类重写,以实现布局(换句话说,通过移动所有者窗口并调用任何子尺寸器的 OnRealizeLayout 来最终确定区域)。不应直接调用此方法。如果需要,由 LayoutChildren 调用。

CFixedSizer

CFixedSizer(RECT rect, FixedType nType=BOTH)
CFixedSizer(HWND hWndOwner, FixedType nType=BOTH)
CFixedSizer(HWND hWndOwner, RECT rect, FixedType nType=BOTH)
CFixedSizer(CWnd* pWndOwner, FixedType nType=BOTH)
CFixedSizer(CWnd* pWndOwner, RECT rect, FixedType nType=BOTH)

构造一个在任何维度上都受约束的 CFixedSizer。类型可以是 HORIZONTALVERTICALBOTH。如果提供,尺寸器最初设置为 rect 大小。

CFillSizer

CFillSizer()
CFillSizer(HWND hWndOwner)
CFillSizer(CWnd* pWndOwner)

构造一个 CFillSizer,它约束其子级完全填充其客户端区域。

CSizer* AddChild(CSizer* pChild)

将子级添加到子列表。

CRowSizer

CRowSizer(RowType nType)
CRowSizer(HWND hWndOwner, RowType nType)
CRowSizer(CWnd* hWndOwner, RowType nType)

构造一个 CRowSizer,它约束其子级以垂直或水平行填充其客户端区域。类型可以是 VERTICALHORIZONTAL

CSizer* AddChild(CSizer* pChild, int nWeight)

将子级添加到尺寸器的子列表,并带有关联的权重。权重决定了分配给子级的额外空间量。如果所有子级的总权重为 9,并且单个子级的权重为 1,它将获得额外空间的 1/9。

程序员注意事项

这些类是根据它们运行良好的示例编写和测试的。但是,我不能保证没有错误。如果你发现任何错误,我将不胜感激,以便我可以将它们添加到未来的更新中。我也很乐意看到其他人提出的任何新尺寸器,并可能将它们添加到未来的更新中。我希望这些类具有可扩展性且易于使用,我非常乐意听取你的意见。

© . All rights reserved.