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

C++ 无对话框模板的 Windows 对话框设计。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (61投票s)

2015 年 5 月 30 日

CPOL

49分钟阅读

viewsIcon

111785

downloadIcon

4652

一种替代方法,其中对话框完全由 C++ 代码指定,既不需要 IDE 支持来编写,也不需要 IDE 生成的资源来执行。由 C++ 类型系统驱动。

背景,  简介, 
代码布局设计基础
构建对话框类
使用自己的宏减少布局定义的冗余
控件方法和通知
控制颜色和字体
超越通知
事件处理摘要和更多关于美学指标结构,
绘制控件
更复杂的示例,  选项卡式对话框, 
以多种语言显示文本
快速参考, 工作原理, 
值得关注的点, 历史

代码更新 2015 年 6 月 5 日 - 语言文件 会导致多个编译单元出现链接错误 - 现在已通过仅修改 autodlg.h 来修复。

背景

Visual Dialog Editor、Class Wizard 和 MFC 已经成为许多人(包括我自己)进入 C++ Windows 编程的入口点。如果所有向导都能正常工作,那么在事件驱动和组件提供的环境中开始编写代码是一种舒适的方式。然而,坚持将其作为设计和定义对话框的唯一方式的智慧是值得怀疑的。特别是

  • 拖放控件仍然是体力活,尤其是当需要高标准的对齐和呈现时。如果必须响应频繁变化的审美指令,那将是大量的体力活。
  • 视觉设计需要为控件 ID 进行繁琐的命名分配,这必须通过 IDE 手动完成。
  • 视觉设计需要编写或生成大量代码才能使其执行任何操作,其中许多代码根本不适合程序员。补偿此问题的向导可能会出错,并且可能不可用或未针对您正在使用的类库进行调整。
  • 在不同应用程序中重用对话框需要将其对话框模板尴尬地合并到主机应用程序的 .rc 文件中。代码库无法提供对话框,而无需要求完成此合并过程。

我决定需要一种新方法,其基本要求是

  • 整个对话框(包括布局)必须由代码定义,而无需 IDE 工具的协助即可编写,也无需 IDE 生成的资源即可执行。

这里介绍了一种实现此目的的方法,它被封装为 Windows 对话框的 C++ 基类。它是“Win 32”,因为它既不需要 MFC 也不需要 ATL/WTL。然而,它可以很好地与任何类库配合使用,并支持在其对话框定义中合并它们的控件类包装器。

简介

这里提出的方法最根本的区别在于,您的代码会完成创建对话框的所有工作,而不是让 Windows 从您附加代码的对话框模板资源创建对话框。无需对话框或控件 ID,因为不存在代码与对话框模板的运行时映射。相反,存在每个控件到 C++ 变量的编译时映射。此外,这些变量中的每一个都有自己唯一的数据类型。这种丰富的类型可能很激进,但它带来了许多好处,并且是设计的关键。它允许 C++ 语言通过类型解析在编译时解决许多问题,从而减少和简化您必须编写的代码。实践中,这意味着对于代表控件的每个变量,例如 btnCancel,将生成一个名称相同但前缀为下划线_btnCancel的唯一数据类型。正如您将看到的,有时您需要通过其数据类型_btnCancel而不是其变量名btnCancel来引用控件。

编码布局而不是拖放布局的不熟悉感将在下一节中处理,之后,大多数事情将比您可能习惯的更简单、更干净、更简洁。

还有一些其他创新,包括

  • 所有控件变量都带有一个动态文本缓冲区as_text,通过它可以读取和写入控件的窗口文本,并且在对话框关闭后会保留。
  • 支持非 Windows 控件,这些控件只是绘制在对话框上并响应鼠标事件。(在某些情况下,这更有意义的示例已提供)。
  • 支持在应用程序级别将美学指标(按钮大小、间距、颜色、控件样式等)强加于所有托管的对话框。
  • Expand_XExpand_Y样式替换“锚点”的概念,以供可以从对话框窗口放大中受益的控件使用。
  • 一些适度但有效的 Win 32 控件编程简化。

代码布局设计的基石

这可以被描述为困难的部分,因为它取代了使用视觉对话框编辑器。当您的对话框工作正常时,您将编写更少的代码,但代码布局指令需要比拖动框直到看起来正确所需更多的思考。这种思考会有回报,如果您使用视觉编辑,您将不会获得回报。它是这样工作的:

使用以下宏完全定义、声明和定位每个控件

注意:冗长的AUTODLG标识符会为所有宏添加前缀,以免它们与全局命名空间中的任何其他内容发生冲突。

AUTODLG_CONTROL( 
     variable_name, locator_verb, Dx, Dy, width, height, 
     control_type, control_styles, extended_styles)

或者,如果您希望它有一个附带的标签

AUTODLG_CONTROL_WITH_LABEL( 
     variable_name, locator_verb, Dx, Dy, width, height, 
     control_type, control_styles, extended_styles,
     label_locator, label_height, label_type, label_style)

每个控件的位置由参数locator_verb, Dx, Dy, width, height决定,相关标签由参数label_locator, label_height. 决定。

locator_verb可以是以下之一:

  • at - 其中 DxDy 是从对话框左上角的绝对位置。
  • to_right_of<_control> - 其中 DxDy 是从该位置的偏移量。
  • under<_control> - 其中 DxDy 是从该位置的偏移量。

label_locator可以是以下任何一项:

  • label_left<by> - 其中“by”是标签延伸到控件左侧的量。
  • label_left_align<_control> - 其中标签的左边缘与另一个控件的左边缘对齐。
  • label_above<int by>- 其中“by”是标签悬挂在控件上方的量。
  • label_top_align<_control>- 其中标签的顶部边缘与另一个控件的顶部边缘对齐。

请注意,在引用先前声明的控件时,我们通过其数据类型名称_btnActivate而不是变量名btnActivate来引用它们。

数据类型名称也可用于访问控件的(在编译时)计算位置和大小。

例如:_btnActivate::left

top, right, bottom, widthheight 也可使用。将它们与 at 动词一起使用可能很有用,其中 to_right_ofunder 不是您想要的。

例如:没有“在…左侧”或“在…上方”的动词。这是因为需要新创建控件的大小来计算位置。但是,您作为程序员知道要添加的控件的大小,并可以如下将控件定位在另一个控件的左侧:

AUTODLG_CONTROL( btnOK, at , _btnCancel::left-BWidth-hGap, _btbCancel::top, BWidth, BHeight, ..... 

以及将控件定位在先前声明的控件上方

AUTODLG_CONTROL( btnOK, at , _btnCancel::left, _btbCancel::top-BHeight-vGap, BWidth, BHeight, ..... 

widthheight 参数通常会填充标准宽度和高度,但它们也可以引用其他控件。为这些参数专门提供了两个宏。

  • AUTODLG_WIDTH_TO(x_coord)
  • AUTODLG_HEIGHT_TO(y_coord)

它们对于指示控件使用可用空间直到另一个控件的边缘很有用。

AUTODLG_WIDTH_TO(_btnCancel::right)

其余参数:control_type, control_styles, extended_styles描述控件本身。控件类型可以是具有窗口类名的原始控件(BUTTON, EDIT, LISTBOX等),也可以是控件包装类,例如 **MFC** 和 **WTL** 的 CButton, CEditCListBox

构建对话框类

最好从一个视觉计划开始。您可以将其记在脑海中,用笔和纸绘制,使用 Paint.exe 甚至视觉对话框编辑器(任何旧的都可以)。我用 Paint 为一个捕获用户输入代码的对话框创建了这个。

 

编码任何布局总有方法,甚至有很多方法,但您想要的是一种在按钮宽度等参数发生变化时能保持其完整性并且易于修改的方法。关键在于识别哪些分组和对齐是您关心的,以及涉及的依赖链。在此示例中,我们只有一个组,但我们确实关心一些对齐方式,如下图所示。

我们希望 **Cancel** 按钮对齐在 **Reset** 按钮下方,**OK** 按钮在 **Cancel** 按钮的右侧,相隔标准间距,并且我们希望编辑框的右边缘与 **OK** 按钮的右边缘对齐,其标签左对齐在 **Reset** 按钮的左边缘。然后,编辑框左边缘的更任意的精确位置可以调整,而不会影响其他任何内容。

对话框类定义将一个指标结构作为模板参数,并使用autodlg::def_metrics作为默认值。现在,您需要知道autodlg::def_metrics 定义了以下枚举。

enum
    {
        BWidth = 110, //Standard button size
        BHeight = 25,
        hGap = 12,        //Standard spacing
        vGap = 18,
    };

最好参考这些来编码您的布局,因为您可以通过传递不同的metrics结构来调整这些值。

我们现在可以开始编码对话框布局,参考参数hGapvGapBWidthBHeight

template <class metrics = autodlg::def_metrics>
class EnterCodeDlg : public autodlg::dialog < metrics >
{
   
public:
    AUTODLG_DECLARE_CONTROLS_FOR(EnterCodeDlg)

        AUTODLG_CONTROL(btnReset, at, hGap, vGap, BWidth, BHeight,
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnCancel, under<_btnReset>, 0, BHeight + 2 * vGap,
        BWidth, BHeight, 
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnOK, to_right_of<_btnCancel>, hGap, 0, BWidth, BHeight,
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL_WITH_LABEL(edtCode, under<_btnReset>, BWidth / 2, vGap,
        AUTODLG_WIDTH_TO(_btnOK::right), BHeight,
           EDIT, WS_TABSTOP, 0,
        label_left_align<_btnReset>, BHeight, STATIC, SS_CENTER)

    AUTODLG_END_DECLARE_CONTROLS
  
    AUTODLG_BEGIN_TABLIST
        &btnCancel, &btnReset, &edtCode, &btnOK
    AUTODLG_END_TABLIST

}

它与 .rc 文件中的文本有些相似。这是因为它提供了相同的信息。区别在于,这文本不是从文件读取并在运行时解析的。它只是编译的代码。因此,它遵循我们熟悉的 C++ 编译器和预处理器的语法规则。

所有控件都必须在AUTODLG_DECLARE_CONTROLS_FORAUTODLG_END_DECLARE_CONTROLS宏之间声明,并且在此空间中不应出现其他任何内容,除了publicprivateprotected关键字,您可以根据设计需求自由应用它们。

请注意,btnCancel声明为位于btnReset下方,但具有额外的 y 偏移量(DyBHeight + 2*vGap。这是为了在将嵌套在它们之间的编辑控件留出空间。同样,edtCode声明为位于btnReset下方,但具有 x 偏移量(Dx)为半个按钮宽度(为标签留出空间),其宽度声明为扩展以与btnOK的右边缘对齐,并且其标签左边缘与btnReset的左边缘对齐。

最后,我们定义这些控件的稍微循环的方式并不代表我们想要的制表顺序,所以我们使用AUTODLG_BEGIN_TABLISTAUTODLG_END_TABLIST显式指定。

注意:一些旧的编译器可能无法编译此代码,必须使用一种稍微笨拙一些的替代方法来设置制表顺序。

AUTODLG_BEGIN_SET_TABS //alternative for older compilers
   AUTODLG_SET_TAB(btnCancel)
   AUTODLG_SET_TAB(btnReset)
   AUTODLG_SET_TAB(edtCode)
   AUTODLG_SET_TAB(btnOK)
AUTODLG_END_SET_TABS

以下代码将显示对话框。

EnterCodeDlg<> dlg;
dlg.DoModal();

dlg已声明为EnterCodeDlg<>类型,带有空括号<>。这意味着它将使用定义它的默认指标结构。可以通过将其作为模板参数传递来强制使用不同的指标结构(但具有相同的参数结构)。例如:

EnterCodeDlg<MyAppMetrics> dlg;
dlg.DoModal();

这是它显示的对话框:

此时,对话框没有任何作用 - 就像在视觉编辑器中刚创建的一个一样。然而,已经完成的工作量比视觉编辑器要多得多。每个控件都已绑定到命名变量,已进行子类化,因此您可以访问发生的任何事情,并且已与内置文本缓冲区同步,您可以在控件创建之前初始化它,并在其销毁后保留。您可能还注意到变量名(去除btnedt前缀)已作为控件的显示文本出现。随着我们添加代码使其功能齐全,我们将看到其他工作的优势。

使用自己的宏减少布局定义的冗余

AUTODLG_CONTROL宏在技术上是最优的,因为它以最简洁的方式捕获所需信息(除了冗长的宏名称),但九个参数,其中一些可以是长表达式(您的程序员赋权),并不容易阅读。九个参数为您定义控件提供了完全的灵活性,但您可能并不总是需要所有这些灵活性。例如,您所有的按钮可能大小相同,并且几乎可以肯定都需要BS_NOTIFYWS_TABSTOP样式。与其每次都输入相同的参数,不如定义一个INHOUSE_BUTTON宏,它调用AUTODLG_CONTROL并预填充一些参数:

#define INHOUSE_BUTTON( name, locator, Dx, Dy) \
AUTODLG_CONTROL( name, locator, Dx, Dy, BWidth, BHeight,\
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

然后可以更简洁地定义和声明按钮,使它们的布局更具可读性。

AUTODLG_DECLARE_CONTROLS_FOR(EnterCodeDlg)

        INHOUSE_BUTTON(btnReset, at, hGap, vGap)
        INHOUSE_BUTTON(btnCancel, under<_btnReset>, 0, BHeight + 2 * vGap)
        INHOUSE_BUTTON(btnOK, to_right_of<_btnCancel>, hGap, 0)

        AUTODLG_CONTROL_WITH_LABEL(edtCode, under<_btnReset>, BWidth / 2, vGap,
        AUTODLG_WIDTH_TO(_btnOK::right), BHeight,
           EDIT, WS_TABSTOP, 0,
        label_left_align<_btnReset>, BHeight, STATIC, SS_CENTER)

 AUTODLG_END_DECLARE_CONTROLS

我没有将此类宏包含在库中,因为它们很容易根据您的内部需求创建,我无法合理地预测它们是什么。它们也可能取决于您为指标参数选择的名称,这超出了库的范围。我也未在接下来的示例代码中定义或使用任何宏,因为这将隐藏直接使用库宏,而库宏需要成为教程介绍的重点。

控件方法和通知

首先,如果 Reset 和 OK 按钮在未输入文本时无法执行任何操作。所以让我们从禁用它们开始。

void OnInitDialog(HWND hWnd)
{
    btnOK.enable(FALSE);
    btnReset.enable(FALSE);
}

OnInitDialog在对话框及其所有控件创建后被调用。正如您所习惯的那样。

控件使用它们的enable方法禁用。这是可以对所有控件调用的通用方法组之一。在下面的快速参考部分中可以找到通用方法列表。

现在让我们处理控件,因为它们变得启用,从始终启用并且始终执行相同操作的取消按钮开始。我们只需为btnCancel创建一个通知处理程序。无需创建消息映射条目或任何代码来调用此处理程序。它将通过简单地存在而被自动调用。

void OnNotificationsFrom(_btnCancel*, UINT NotifyCode, LPARAM lParam)
{
     if (BN_CLICKED == NotifyCode)
         EndDialog(IDCANCEL);
}

注意OnNotificationsFrom的第一个参数类型为指向btnCancel数据类型的指针_btnCancel*。这就是确定只有btnCancel会调用处理程序的因素。我们对传递的指针不感兴趣,因为我们知道它只指向btnCancel

EndDialog()的作用与您所习惯的一样,但可用于结束模态和无模式对话框,因此无需对它们进行不同的编码。通常,其参数将是IDOKIDCANCEL,它们始终被定义。您将无法传递控件 ID,因为您没有任何控件 ID。当然,您可以传递对您有意义的自己的数字。

现在对在编辑控件中输入文本的响应

void OnNotificationsFrom(_edtCode*, UINT NotifyCode, LPARAM lParam)
{
        if(EN_CHANGE==NotifyCode)
        {
            btnOK.enable(TRUE);
            btnReset.enable(TRUE);
        }
}

以及对现在已启用的 Reset 按钮的响应

void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)
{
        if(BN_CLICKED==NotifyCode)
        {
            edtCode.as_text = _T("");
            btnOK.enable(FALSE);
            edtCode.set_focus();
        }
}

这显示了所有控件都支持的as_text成员的使用。它是一个动态文本缓冲区,保证与控件的主要显示文本(由 Windows 确定)同步。即:

  • 如果控件在创建之前用文本初始化,那么这将是控件的初始显示文本。
  • 如果读取,它将始终提供控件的显示文本。
  • 如果写入,它将更新控件的显示文本。
  • 控件销毁后,只要控件有标签,它就会保留最后的显示文本。通常,文本是数据的控件都有一个单独的标签。对于没有标签的控件(例如按钮),除非您明确使用它,否则as_text缓冲区将为空。

这可以作为传统数据交换机制的替代。有关auto_stringas_text的类型)的更多信息,请参阅快速参考部分。

最后是 OK 按钮

void OnNotificationsFrom(_btnOK*, UINT NotifyCode, LPARAM lParam)
{
     if (BN_CLICKED == NotifyCode)
            EndDialog(IDOK);
}

此时对话框已准备好如下使用:

EnterCodeDlg<> dlg;
if (IDOK == dlg.DoModal())
{
     TCHAR* szCode=dlg.edtCode.as_text;
}

输入的确切代码可在 dlg.edtCode.as_text 中找到,即使控件本身已被销毁。

控制颜色

 和字体

现在我们有一个允许您输入任何代码的对话框。现在让我们使 OK 按钮拒绝不符合标准的代码并产生适当的视觉响应。首先,我们需要在类定义中添加一个布尔值来标记错误条件。

bool bErrorInCode;

并在构造函数中初始化它

EnterCodeDlg()
{
      bErrorInCode = false;
}

现在我们可以更改处理 OK 按钮的代码

void OnNotificationsFrom(_btnOK*, UINT NotifyCode, LPARAM lParam)
{
        if (BN_CLICKED == NotifyCode)
        {
            if (_tcsstr(edtCode.as_text, _T("1234")) == edtCode.as_text)
            {
                EndDialog(IDOK);
            }
            else
            {
                btnOK.enable(FALSE);
                bErrorInCode = true; 
                edtCode.set_focus();
                edtCode.invalidate();
            }
        }
}

需要edtCode.invalidate();,因为我们将更改edtCode持有不符合代码时如何显示。方法如下:

LRESULT OnCtlColorFrom(_edtCode*, UINT nCtlColor, HDC hDC, bool bMouseOver)
{
        if (bErrorInCode)
        {
            ::SetTextColor(hDC, RGB(255, 0, 0));
            ::SetBkMode(hDC, TRANSPARENT);
            SelectFont(hDC, GetStockObject(SYSTEM_FONT));
            return (LRESULT)GetStockBrush(LTGRAY_BRUSH);
        }
        return NULL;
}

为完成新设计,我们还需要对edtCodebtnReset的行为进行一些更改。

void OnNotificationsFrom(_edtCode*, UINT NotifyCode, LPARAM lParam)
{
        if(EN_CHANGE==NotifyCode)
        {
            if (false == bErrorInCode)
                btnOK.enable(TRUE);
            btnReset.enable(TRUE);
            if (wcslen(edtCode.as_text) < 1)
            {
                bErrorInCode = false;
                edtCode.invalidate();
            }
        }
}
void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)
{
        if (BN_CLICKED == NotifyCode)
        {
            edtCode.as_text = _T("");
            btnOK.enable(FALSE);
            edtCode.set_focus();
            bErrorInCode = false;
            edtCode.invalidate();
        }
}

输入不符合代码时的对话框外观如下:

超越通知

控件被设计用来通知对话框您可能想要响应的特定事件,并且它们通过我们可以方便处理的通知来实现。有时,我们需要知道或需要干预的需求超出了控件设计所预期的,但可以通过拦截原始 Windows 消息到达控件来满足。这里我将编造一个具有此要求的示例,并展示如何轻松完成。

在某些网页上,您应该在其中输入文本的编辑字段会以一个灰色的提示(例如“在此处输入文本”)初始化,但当您单击它时,提示会消失,您会输入文本到一个空的编辑字段中。我不是说我喜欢这种效果,但您可能会被要求这样做。

我们已经知道如何让它显示初始的灰色文本:

添加另一个布尔值来指示编辑框是否被触摸过。

bool bErrorInCode;
bool bEditTouched;

初始化它并设置edtCode的初始文本。

EnterCodeDlg()
{
      bErrorInCode = false;
      bEditTouched = false;
      edtCode.as_text = _T("Enter code here");
}

并向edtCodectlcolor处理程序添加更多条件代码。

LRESULT OnCtlColorFrom(_edtCode*, UINT nCtlColor, HDC hDC, bool bMouseOver)
{
        if (bErrorInCode)
        {
            ::SetTextColor(hDC, RGB(255, 0, 0));
            ::SetBkMode(hDC, TRANSPARENT);
            SelectFont(hDC, GetStockObject(SYSTEM_FONT));
            return (LRESULT)GetStockBrush(LTGRAY_BRUSH);
        }
        if (false==bEditTouched)
        {
            ::SetTextColor(hDC, RGB(0, 0, 0));
            ::SetBkMode(hDC, TRANSPARENT);
            return (LRESULT)GetStockBrush(WHITE_BRUSH);
        }
        return NULL;
}

我们遇到的问题是,edtCode是 EDIT 控件,单击时不会发出任何通知。它通常不是您想要的。然而,所有控件在单击时都会收到WM_LBUTTONDOWNWM_LBUTTONUP消息。所以我们处理到达edtCode的消息来获取我们的单击事件。

LRESULT OnMessageAt(_edtCode*, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
        if (WM_LBUTTONUP==message)
        {
            if (false == bEditTouched)
            {
                bEditTouched = true;
                edtCode.as_text = _T("");
                btnOK.enable(FALSE);
            }
        }
        return 0;
}

零返回允许WM_LBUTTONUP消息继续处理并执行其应有的操作。在这种情况下,我们不想干扰该消息,只想知道它。

现在只需要更改btnResetBN_CLICKED处理程序以重置新提示。

void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)
{
        if(BN_CLICKED==NotifyCode)
        {
            edtCode.as_text = _T("Click here to enter code");
            btnOK.enable(FALSE);
            bErrorInCode = false;
            bEditTouched = false;
            edtCode.invalidate();
        }
}

事件处理摘要和更多关于美学指标结构

到目前为止,我们已经看到了可以为任何单个控件定义的三种事件处理程序:

void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)

LRESULT OnCtlColorFrom(_edtCode*, UINT nCtlColor, HDC hDC, bool bMouseOver)

LRESULT OnMessageAt(_edtCode*, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

这些都有一个版本可以处理特定类型(如BUTTONEDIT)的所有控件的事件,而不是单个控件。

void OnNotificationsByControlType(BUTTON* pC, UINT NotifyCode, LPARAM lParam)
//You may want to detect if any button has been pressed

LRESULT OnCtlColorByControlType(EDIT* pC, UINT nCtlColor, HDC hDC, bool bMouseOver)
//You may want to set the font or background for all edits

LRESULT OnMesageByControlType(EDIT* pC, UINT message, WPARAM wParam, LPARAM lParam)
//You may want to detect if any edit has been clicked on

还有一对类似的用于处理所有者绘制消息族。

LRESULT OnItemMsgFrom(_edtCode*, DRAWITEMSTRUCT* pInfo, bool bMouseOver)

LRESULT OnItemMsgByControlType(BUTTON* pC, MEASUREITEMSTRUCT* pInfo, bool bMouseOver)

有了这些,pInfo的类型决定了正在处理的消息。例如,DRAWITEMSTRUCT* pInfo决定它处理WM_DRAWITEM,而MEASUREITEMSTRUCT* pInfo决定它处理WM_MEASUREITEM

最后,还有一个处理到达对话框的消息的处理程序。

LRESULT OnDialogMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

在描述这些处理程序的调用顺序之前,我们需要了解更多关于美学指标结构的信息。

这是def_metrics结构的定义。这是**指标结构**的最小规模。

struct def_metrics
{
    AUTODLG_METRICS_DEF_HANDLERS

    enum
    {
        BWidth = 110, //Standard button size
        BHeight = 25,
        hGap = 12,        //Standard spacing
        vGap = 18,
    };
    static HBRUSH OnCtlColor(UINT nCtlColor, HDC hDC, bool bMouseOver)
    {
        return 0;
    }
};

指标结构可以作为模板参数传递给对话框声明,旨在提供托管应用程序的对话框美学方面的集中控制。除了我们已经使用的大小和间距参数外,它还可以用于对控件施加样式、控制它们的颜色和字体以及处理所有者绘制。

您将看到它已经有一个OnCtlColor处理程序,设置为什么都不做并被忽略。此外,我们可以向指标结构添加以下处理程序,它们将被调用。

LRESULT OnCtlColorByControlType(EDIT* pC, UINT nCtlColor, HDC hDC, bool bMouseOver) 

LRESULT OnItemMsgByControlType(BUTTON* pC, DRAWITEMSTRUCT* pInfo, bool bMouseOver)

现在我们可以查看每种处理程序的调用顺序。它们并不都相同。

当控件发出**通知**时,所有定义用于处理它的通知都将被调用。首先,它会调用任何匹配其控件类型的OnNotificationsByControlType处理程序,然后它会调用特定控件的任何OnNotificationsFrom处理程序。控件类型处理程序首先获得发言权,特定控件处理程序获得最终决定权。控件通知只能由包含它们的对话框处理。

当控件发送**CtlColor**消息时,处理要求有所不同。CtlColor 处理程序可以更改字体、文本颜色和背景画笔,但它们通常只想控制其中一两个,而将其他保持原样。为此,CtlColor 处理程序是分层构建的。首先,它会传递给 Windows 以获取默认画笔、字体和文本颜色。然后,它调用**指标结构**提供的任何OnCtlColorByControlType处理程序。如果没有匹配的控件类型处理程序,它将调用指标结构的裸OnCtlColor处理程序。接下来,它调用对话框提供的任何匹配的OnCtlColorByControlType处理程序,最后是对话框中的任何匹配的OnCtlColorFrom处理程序。每次调用时,如果返回的画笔非零,它将采用该画笔,否则将保留其已有的画笔。无论是否返回非零画笔,对传递的设备上下文的操作都是有效的。

当控件发送**DrawItem**消息或项目消息系列的任何其他消息时,只会调用最特定的处理程序,而其他所有处理程序都将被忽略。处理程序的搜索遵循以下顺序,并在找到返回非零值的处理程序时立即结束:首先是对话框中的OnItemMsgFrom,然后是对话框中的OnItemMsgByControlType,最后是指标结构中的OnItemMsgByControlType。对于按钮控件,如果您不提供所有者绘制处理程序,则会提供一个默认处理程序。它绘制普通的 3D 边缘按钮,这些按钮将响应 CtlColor - 标准按钮则不行。

如果您使用对话框中的OnMessageAtOnMessageByControlType处理程序拦截**到达控件的消息**,那么您正在以较低级别工作,并且有能力进行粗暴干预。这些处理程序首先被调用。首先是OnMessageByControlType,然后是OnMessageAt。每个处理程序都有权返回非零值,而不允许任何其他处理程序(包括 Windows 默认处理程序)被调用。通过这些处理程序,您首先获得发言权,并可以坚持最终决定权。

如果您使用OnDialogMessage拦截**到达对话框的消息**,您也将首先获得发言权,并可以坚持最终决定权。

总结:

  • 所有**通知**处理程序都将被调用。
  • **CtlColor**处理程序全部调用,从 Windows 默认值开始,到最具体的结束。
  • 只调用最具体的**DrawItem**处理程序。
  • **指标结构**中定义的**CtlColor 处理程序和 DrawItem 处理程序**也将被调用。
  • 原始**窗口消息处理程序**有权终止消息处理。

在我们的示例中,我们为编辑控件添加了一个OnCtlColorFrom处理程序。这是为了给用户一个动态的视觉响应。也就是说,它的目的主要是功能性的,而不是美学性的。任何美学性质的东西最好由传入的指标结构来处理。

这里有一个更完善的**指标结构**,我们将用于接下来的示例;

struct my_app_metrics 
       : public autodlg::styles //This gives access to special autodlg styles
{
    AUTODLG_METRICS_DEF_HANDLERS
    enum
    {
        BWidth = 110, //Standard button size
        BHeight = 25,
        hGap = 12,        //Standard spacing
        vGap = 18,
    };

    AUTODLG_IMPOSE_STYLE_FOR(EDIT, WS_BORDER, 0) //#1
    AUTODLG_IMPOSE_STYLE_FOR(BUTTON, BS_OWNERDRAW | MOUSE_OVER_STYLE, 0) //#2

    static HBRUSH OnCtlColor(UINT nCtlColor, HDC hDC, bool bMouseOver)
    {
        if (WM_CTLCOLORLISTBOX == nCtlColor) //#3
        {
            ::SetTextColor(hDC, RGB(255, 255, 255));
            ::SetBkMode(hDC, TRANSPARENT);
            return GetStockBrush(DKGRAY_BRUSH);
        }
        if (WM_CTLCOLORBTN == nCtlColor) //#4
        {
            ::SetTextColor(hDC, RGB(0, 0, 0));
            ::SetBkMode(hDC, TRANSPARENT);
            if (bMouseOver)
                return GetStockBrush(WHITE_BRUSH);
            return 0;// GetStockBrush(LTGRAY_BRUSH);
        }
        return NULL;
    }
    //#5
    static LRESULT OnCtlColorByControlType(BannerLabel* pCtrl, UINT nCtlColor, HDC hDC, bool bMouseOver)
    {
        ::SetTextColor(hDC, RGB(255, 255, 255));
        ::SetBkMode(hDC, TRANSPARENT);
        SelectFont(hDC, GetStockObject(SYSTEM_FONT));
        return (LRESULT)GetStockBrush(GRAY_BRUSH);
    }
    static LRESULT OnCtlColorByControlType(STATIC_BANNER* pCtrl, UINT nCtlColor, HDC hDC, 
            bool bMouseOver)
    {
        ::SetTextColor(hDC, RGB(255, 255, 255));
        ::SetBkMode(hDC, TRANSPARENT);
        SelectFont(hDC, GetStockObject(SYSTEM_FONT));
        return (LRESULT)GetStockBrush(GRAY_BRUSH);
    }
};

在这里,我们在应用程序级别、子应用程序级别或任何您喜欢的地方决定:

  1. 所有编辑框都有边框。
  2. 所有按钮都有所有者绘制和检测鼠标悬停样式 - 没有提供 OwnerDraw 处理程序,因此绘制了默认的 CtlColor 敏感按钮。
  3. 列表框具有白色文本和深色背景。
  4. 鼠标悬停在按钮上时,按钮用白色画笔绘制。
  5. 特定控件类型BannerLabelSTATIC_BANNER具有白色文本和深色背景。

在**指标结构**中处理 **CtlColor** 的首选方法是在其通用OnCtlColor处理程序中通过切换nCtlColor参数来实现,如LISTBOXBUTTON所示。这使其对所有人都可见,包括希望采用特定控件类型颜色的派生控件。然而,这种处理方法在区分上仅限于nCtlColor枚举的控件类型。当您需要一个针对非常特定控件类型的处理程序时,您需要定义OnCtlColorByControlType ,例如BannerLabelSTATIC_BANNER ,它们是此处发明的控件类型(见下文),而不是nCtlColor枚举的一部分。

如果您希望使用不同的指标参数名称集,那么您应该在**autodlg_metrics_config.h**中注册它们。

绘制控件

我一直对创建STATIC控件窗口以放置在对话框上并给人一种标签直接绘制在对话框上的印象感到有些不自在。为什么不直接在对话框上绘制标签呢?

出于各种原因,我已经这样做了多年。标签很容易,按钮也不是太麻烦,我也做过漂亮的列表框。但不是编辑框,这时编辑控件窗口就充满了您不想复制的功能,而且由于它已经是 Windows 的一部分,创建窗口的开销就得到了很好的证明。尽管如此,典型的对话框上充满了许多控件窗口,这些窗口可以直接绘制在对话框上,在某些情况下会更有意义。

支持此类绘制(无窗口)控件,定义它们相对容易。这是可用于显示标签而不是使用STATIC控件的PaintedLabel控件的所有代码。它不像STATIC控件那样能做所有事情,但它能完全完成绘制标签的工作。

class PaintedLabel : public autodlg::painted_control
{
protected:
    bool OnPaint(HDC& hDC, RECT& r, bool bMouseOver)
    {
        if ((style & WS_VISIBLE) == 0)
            return false;
        
        HBRUSH hBrush = GetCtlColor(WM_CTLCOLORSTATIC, hDC);
        
        if (hBrush)
            ::FillRect(hDC, &r, hBrush);

        if (style & WS_DISABLED)
        {
            RECT rr = r;
            int i = 1;
            ::SetTextColor(hDC, RGB(240, 240, 240));
            ::DrawText(hDC, as_text, wcslen(as_text), &rr, style);
            rr.top += i; rr.left += i;
            rr.right += i; rr.bottom += i;
            ::SetTextColor(hDC, RGB(180, 180, 180));
            ::DrawText(hDC, as_text, wcslen(as_text), &rr, style);
        }
        else
            ::DrawText(hDC, as_text, _tcslen(as_text), &r, style);
        return true;
    }
};

PaintedLabel控件将在后续示例中代替STATIC控件使用。还使用了一个派生自PaintedLabel的类。

class BannerLabel : public PaintedLabel
{};

BannerLabelPaintedLabel的区别仅在于具有不同的类型名称,这仅仅是为了能够通过my_app_metrics结构中定义的OnCtlColorByControlType(BannerLabel*, ...)处理程序进行识别。如果您决定继续使用STATIC控件作为标签,那么您可以使用STATIC_BANNER,它也将接收来自my_app_metrics中提供的OnCtlColorByControlType(STATIC_BANNER*, ...)处理程序的相同特殊处理。

库中包含的其他绘制控件包括SpacerControl,它是一个隐藏的参考框架,其他控件可以挂在其上(它没有代码);GroupBox,它是 Windows 组框的替代品;以及PaintedTabFrame,您必须使用它而不是 Windows 选项卡控件。这些代码可以在autodlg.h的底部找到。其他零散的示例,如StopButtonStartupButtonPaintedSmiler ,可以在misc_painted_controls.h中找到。

如果绘制的控件仅用于显示并且不需要鼠标交互,则应从painted_control派生,并且只需要实现bool OnPaint(HDC& hDC, RECT& r, bool bMouseOver)来处理绘制消息。这些是它将收到的唯一消息,除非您使用do_msg发送一些消息给它。传递的RECT r是以对话框坐标表示的,因为hDC是对话框的。

如果需要鼠标交互,则必须从painted_mouse_control派生,并且还必须提供处理程序void OnNonPaintMessage(UINT message, WPARAM wParam, LPARAM lParam)。这将接收鼠标消息,可以使用mouse_parms结构进行解码。在这种情况下,x、y 坐标是相对于您的控件,而不是对话框。

mouse_parms mouse(wParam, lParam);
mouse.x; //coordinates with respect to the control
mouse.y;
mouse.vKeys;

当控件具有键盘焦点时,它将接收键盘消息,以及您可能选择使用do_msg发送的任何消息。

对于大多数绘制的控件,实现这两个处理程序就足够了。然而,在设计更复杂的绘制控件(如PaintedTabFrame)时,有更多的资源可用。

您还可以实现void OnControlCreated(),它将在控件创建后被调用,以及OnDialogCreated(),它将在对话框中的所有控件创建后被调用。PaintedTabFrame需要后者来适应其内容,这些内容可能会随着创建而扩展。

您还可以访问并覆盖为所有控件提供的通用方法。例如,PaintedTabFrame 覆盖了invalidatesizemoveshow

关于设计绘制控件,就库支持而言,这就是您需要知道的。要了解如何着手,请研究上面列出的示例的源代码。

更复杂的示例 

这是一个更复杂的对话框,更能体现其功能:

您输入访问代码,选择一个星期几和一个时间,查看报告,然后以模态或无模式对话框启动一个虚构的进程。本节将展示其所有代码。

注意:注释用于阐明控件的工作原理,此处使用粗体突出显示此处首次引入并详细解释的内容。

对话框定义和布局

template <class metrics = autodlg::def_metrics>
class SelectDataDlg
    : public autodlg::dialog
        < metrics, autodlg::auto_size, WS_OVERLAPPEDWINDOW > //#1
{
public:
    auto_string sTimePeriod; //#2
    LaunchDlg<def_sizes3> dlgLaunch; //used for modeless dialog

    AUTODLG_DECLARE_CONTROLS_FOR(SelectDataDlg)

        AUTODLG_CONTROL_WITH_LABEL(dlgEnter_code, at, hGap, BHeight+vGap,
        BWidth * 2 + hGap * 3, BHeight * 2 + vGap * 4,
        EnterCodeDlg<metrics>, WS_TABSTOP, 0,
        label_above<BHeight>, BHeight, BannerLabel, SS_LEFT | SS_CENTER) //#3

        AUTODLG_CONTROL_WITH_LABEL(edtUser, to_right_of<_dlgEnter_code>,
        hGap, 0, BWidth, BHeight,
        PaintedLabel, SS_CENTER , 0,
        label_above<BHeight>, BHeight, PaintedLabel, SS_CENTER) //#4

        AUTODLG_CONTROL(spacer1, to_right_of<_edtUser>, 0, 0,
        hGap, BHeight,
        SpacerControl, EXPAND_X_STYLE, 0) //#5

        AUTODLG_CONTROL_WITH_LABEL(lbSelect_day_of_week,
        to_right_of<_spacer1>, hGap * 2, 0,
        BWidth * 3 / 2 + hGap,
        AUTODLG_HEIGHT_TO(this_dlg_type::_dlgEnter_code::bottom),
        LISTBOX, LBS_NOTIFY | WS_TABSTOP, 0,
        label_above<BHeight>, BHeight, BannerLabel, SS_CENTER)

        AUTODLG_CONTROL(lblDay_of_week, under<_dlgEnter_code>, 0, vGap,
        AUTODLG_WIDTH_TO(_lbSelect_day_of_week::right), BHeight,
        BannerLabel, SS_CENTER | EXPAND_X_STYLE, 0) //#6

        AUTODLG_CONTROL(btnMorning, under<_lblDay_of_week>, 0, vGap,
        BWidth, BHeight,
        RADIOBUTTON, WS_GROUP | BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnAfternoon, under<_btnMorning>, 0, vGap,
        BWidth, BHeight,
        RADIOBUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnEvening, under<_btnAfternoon>, 0, vGap,
        BWidth, BHeight,
        RADIOBUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL_WITH_LABEL(edtReport, to_right_of<_btnMorning>,
        BWidth * 2 / 3, 0,
        AUTODLG_WIDTH_TO(_lbSelect_day_of_week::left),
        AUTODLG_HEIGHT_TO(_btnEvening::bottom),
        EDIT, WS_GROUP | WS_TABSTOP | ES_READONLY | EXPAND_X_STYLE
            | EXPAND_Y_STYLE, 0,
        label_left<BWidth * 2 / 3>, BHeight, PaintedLabel, SS_CENTER) //#7

        AUTODLG_CONTROL(btnModal, to_right_of<_edtReport>,
        BWidth/2, vGap, BWidth, BHeight,
        BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(btnModeless, under<_btnModal>, 0, vGap,
        BWidth, BHeight,
        BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

        AUTODLG_CONTROL(groupProcess_control, at,
        _btnModal::left - hGap, _btnModal::top - BHeight,
        AUTODLG_WIDTH_TO(_btnModal::right + hGap),
        AUTODLG_HEIGHT_TO(_btnModeless::bottom + vGap),
        GroupBox, 0, 0) //#8

    AUTODLG_END_DECLARE_CONTROLS

    AUTODLG_BEGIN_DEFINE_NOTIFICATIONS_TO_PARENT
        OKToLaunch //significant event that parent may want to know about //#9
    AUTODLG_END_DEFINE_NOTIFICATIONS_TO_PARENT
    .....
}

1. 在本例中,类定义将三个模板参数传递给autodlg::dialog基类。这是它可以接受的模板参数的完整列表:

  • 指标结构- 默认:autodlg::def_metrics,您可以传递任何符合要求的指标结构。
  • 初始大小策略- 默认:autodlg::auto_size(大小以适应布局),或者,如果您想明确固定初始大小,可以传递autodlg::explict_size<Width, Height>
  • 窗口样式- 默认:WS_POPUPWINDOW | WS_CAPTION在本例中,而是传递了WS_OVERLAPPEDWINDOW以使对话框可由用户调整大小。

2. 声明了一个类型为autodlg::auto_string的成员变量。我们只需要一个字符串来存储一些文本。通常您会使用 MFC 或 WTL 的CStringstd::string,但我不想在示例中引入其他库依赖项,所以我使用了此库附带的基础动态字符串。

3. 第一个控件的类型是EnterCodeDlg<metrics>。也就是说,它是我们之前定义的对话框。对话框将显示在布局中指定的尺寸或其自身的初始尺寸中,以较大的者为准。这意味着您不必精确指定布局中的尺寸。虽然这可能意味着您可以指定零尺寸并且它仍会显示,但那样将无法在布局中放置任何控件在其右侧或下方。您应该指定一个接近对话框自身初始显示尺寸的布局尺寸,并将其用作可以容纳在其右侧或下方的控件数量的基础。第一个控件还有一个类型为BannerLabel的标签,这是在绘制控件部分提到的PaintedLabel的变体。

4. 第二个控件称为edtUser,因为它最初是一个带标签的编辑框,但编辑框从未被编辑过,所以它也可以是一个标签。所以您看到了;一个标签(显示值)和一个附带的标签。

5. 第三个控件是SpacerControl。它是不可见的,有两个功能:一个用于其他控件的参考,并且可以扩展,以便其右侧或下方的控件被推向右侧或下方。它在此处用于在对话框扩展时将lbSelect_day_of_week推向右侧。

横跨整个对话框的长横幅标签lblDay_of_week被赋予了EXPAND_X_STYLE。这意味着当对话框调整大小时,它将水平扩展,因此它会一直横跨整个对话框。

多行编辑框edtReport被赋予了EXPAND_X_STYLE | EXPAND_Y_STYLE,以便它可以充分利用对话框被调整为更大尺寸的机会。

8. 最后一个控件是GroupBox类型。这是一个 PaintedControl,其功能与 Windows 的 Group Box 相同。如果您希望继续使用 Windows Group Box,则类型为GROUP_BOX

9. 夹在AUTODLG_BEGIN_DEFINE_NOTIFICATIONS_TO_PARENTAUTODLG_END_DEFINE_NOTIFICATIONS_TO_PARENT宏之间的是一个通知代码OKToLaunch,此对话框可以使用它来通知其父对话框(如果存在)一个重要事件。

OnInitDialog

void OnInitDialog(HWND hWnd)
{
        //Disable all controls in this dialog except dlgEnter_code
        control_cursor cursor;
        if (cursor.set_first_ctrl(*this))
        do
        {
            if (cursor.get_control() != &dlgEnter_code 
                    && cursor.get_control() != &dlgEnter_code_label)
                cursor.get_control()->enable(FALSE);
        } while (cursor.move_to_next_ctrl());  //#1

        //Populate list box
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Monday")); //#2
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Tuesday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Wednesday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Thursday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Friday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Saturday"));
        lbSelect_day_of_week.do_msg(LB_ADDSTRING, 0, _T("Sunday"));
        
        //A child dialog's Cancel button won't do anything so hide it
        dlgEnter_code().btnCancel.show(SW_HIDE); //#3
        
        edtUser.as_text = _T("---");
 }

1. 在OnInitDialog中,创建了一个control_cursor来枚举对话框中的所有控件。

2. 使用do_msg填充列表框,它只不过是SendMessage的包装器,但避免了编写丑陋的类型转换或填写您不使用的参数。这只是一个小细节,但它能使 Win 32 编程几乎和使用控件类包装器的接口一样易读。

3.如果您还记得,EnterCodeDlg的 OK 和 Cancel 按钮调用EndDialog来关闭对话框。那么,如果对话框是另一个对话框中的控件(它具有WS_CHILD样式),那么EndDialog将不会关闭对话框,而是会向其父对话框发送一个带有传递给EndDialog的代码的通知。由于 Cancel 的唯一目的是关闭对话框,而这不允许发生,所以它实际上没有存在的意义。所以我们隐藏它。

请注意,控件变量后面跟着括号dlg_Enter_code()是访问其作为对话框的成员所必需的。

OnNotificationsFrom(_dlgEnter_code*, ...

void OnNotificationsFrom(_dlgEnter_code*, 
        UINT NotifyCode, LPARAM lParam)  //#1
    {
        if(IDOK==NotifyCode)
        {
            //User code is entry code without the secret first 4 digits
            edtUser.as_text = dlgEnter_code().edtCode.as_text.from(4);
            
            //Obscure first 4 digits showing in edit because dlgEnter_code
            //will not close and will still be visible
            dlgEnter_code().edtCode.as_text.overwrite(0, _T("****"));
            
            //Enable all controls in this dialog
            control_cursor cursor;
            if (cursor.set_first_ctrl(*this))
            do{
                cursor.get_control()->enable(TRUE);
            } while (cursor.move_to_next_ctrl());  //#2

            //Disable all controls in dlgEnter_code
            if (cursor.set_first_ctrl(dlgEnter_code()))
            do{
                cursor.get_control()->enable(FALSE);
            } while (cursor.move_to_next_ctrl());  //#3
            
            //These two should not be available until day and time period are set
            btnModal.enable(FALSE);
            btnModeless.enable(FALSE);
            
            UpdateReport();
            lbSelect_day_of_week.set_focus();
        }
    }

1. 这是嵌入式EnterCodeDlg的任何通知的处理程序。EnterCodeDlg中没有设计父通知,但由于它嵌入为子对话框,OK 和 Cancel 按钮中的EndDialog调用会发送通知而不是关闭对话框。IDCANCEL通知永远不会到达,因为我们隐藏了 Cancel 按钮,但IDOK通知对我们很重要,因为它告诉我们已成功输入了符合要求的代码。

2. 输入了正确的代码后,将创建一个control_cursor来枚举主机对话框的控件并启用它们。

3. 将同一个control_cursor设置为EnterCodeDlg 以枚举其控件并禁用它们。

OnNotificationsFrom(_lbSelect_day_of_week*, ...

void OnNotificationsFrom(_lbSelect_day_of_week*, 
        UINT NotifyCode, LPARAM lParam)
    {
        if(LBN_SELCHANGE==NotifyCode)
        {
            int iSel = lbSelect_day_of_week.do_msg(LB_GETCURSEL);
            if (iSel > -1)
            {
                //read selected text straight into lblDay_of_week
                lbSelect_day_of_week.do_msg(LB_GETTEXT, //action
                    iSel,                                //selection
                    //buffer
                    lblDay_of_week.as_text.get_buf_set_length(
                        lbSelect_day_of_week.do_msg(LB_GETTEXTLEN, iSel))//buffer len //#1
                    );
            }
            UpdateReport();
        }
    }

1. 当有人单击列表框中的某一天时,我们只想在lblDay_of_week(横跨屏幕的横幅标签)中显示它。

粗体部分对许多程序员来说可能并不陌生。

首先发送LB_GETTEXTLEN消息,并使用get_buf_set_length将结果用于初始化as_text的缓冲区以达到所需长度,它返回一个缓冲区,该缓冲区与LB_GETTEXT一起传递以进行填充。

令同一批程序员惊讶的是,代表lblDay_of_week 的控件将自动更新以在其as_text成员中显示新文本。这是由于get_buf_set_length中的一些魔法,稍后将进行解释。

其余代码

void OnNotificationsFrom(_btnMorning*, UINT NotifyCode, LPARAM lParam)
    {
        if (BN_CLICKED == NotifyCode)
        {
            sTimePeriod = _T("morning");
            UpdateReport();
        }
    }
    void OnNotificationsFrom(_btnAfternoon*, UINT NotifyCode, LPARAM lParam)
    {
        if (BN_CLICKED == NotifyCode)
        {
            sTimePeriod = _T("afternoon");
            UpdateReport();
        }
    }
    void OnNotificationsFrom(_btnEvening*, UINT NotifyCode, LPARAM lParam)
    {
        if (BN_CLICKED == NotifyCode)
        {
            sTimePeriod = _T("evening");
            UpdateReport();
        }
    }
    
    void OnNotificationsFrom(_btnModal*, UINT NotifyCode, LPARAM lParam)
    {
        if(BN_CLICKED==NotifyCode)
        {
            LaunchDlg<def_sizes3> dlg;
            dlg.DoModal();
        }
    }
    void OnNotificationsFrom(_btnModeless*, UINT NotifyCode, LPARAM lParam)
    {
        if (BN_CLICKED == NotifyCode)
        {
            dlgLaunch.Create();
        }
    }
    void UpdateReport()
    {
        //Update edtReport with day and time period
        //Return early if either is not set
        if (lbSelect_day_of_week.do_msg(LB_GETCURSEL) < 0)
        {
            edtReport.as_text = _T("SELECT DAY AND TIME PERIOD");
            return;
        }
        edtReport.as_text = lblDay_of_week.as_text;
        edtReport.as_text.append(_T(" - "));
        if (sTimePeriod)
            edtReport.as_text.append(sTimePeriod);
        else
        {
            edtReport.as_text.append(_T("SELECT TIME PERIOD"));
            return;
        }

        //We only want this called once so use disabled state of 
        //btnModal as a condition
        if (btnModal.get_style() & WS_DISABLED)
        {
            //dialog may be used as a child and this is a significant event
            NotifyParent(notify::OKToLaunch, 0);  //#1
            //ready to launch
            btnModal.enable(TRUE);//this block won't get called again
            btnModeless.enable(TRUE);
        }
    }

1. 只要星期几或时间的选择发生变化,就会调用UpdateReport。它会更新edtReport中显示的内容,并确定btnModalbtnModeless是否应启用。它只执行一次启用它们的代码,并且在执行时还会调用NotifyParent(notify::OKToLaunch, 0);

这不是此对话框功能的一部分。它是礼貌地预期它将被嵌入到另一个对话框中。这是主机对话框最可能需要的事件。

btnModalbtnModeless启动的对话框使用各种绘制控件以及 Windows 进度控件。

对话框的代码可在example_dialogs.h中找到,绘制控件的代码可在misc_painted_controls.h 中找到。在这种情况下,绘制控件是获得所需行为最直接的方法。Go 按钮必须以非常慎重的方式按下,Stop 按钮一旦触摸就会关闭,而笑脸有其自身的特殊行为。

选项卡式对话框

您无法用这些对话框做的其中一件事是,在标准的 Windows 选项卡控件中使用它们。这是因为标准 Windows 选项卡控件使用对话框模板,而这些对话框没有任何对话框模板。因此,提供了一个无窗口的PaintedTabFrame控件。它之所以称为PaintedTabFrame,是因为它不是拥有选项卡内容的窗口。它只是框住选项卡内容并控制可见性。内容直接由宿主对话框拥有。

template <class metrics = def_sizes>
class TabbedDlg : public autodlg::dialog 
    < metrics, autodlg::auto_size, WS_OVERLAPPEDWINDOW >
{
    AUTODLG_DECLARE_CONTROLS_FOR(TabbedDlg)
private:
        AUTODLG_CONTROL_WITH_LABEL(tabSelect, at, 6, hGap + BHeight,
        350, 200,
        PaintedTabFrame<5>, EXPAND_X_STYLE | EXPAND_Y_STYLE, NO_STYLE,
        label_above<BHeight>, BHeight, BannerLabel, SS_CENTER)
public:
        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgEnter_code, EnterCodeDlg<metrics>, WS_TABSTOP, 0)
        
        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgSelectDataDlg, SelectDataDlg<metrics>, WS_TABSTOP, 0)

        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgLaunch, LaunchDlg<metrics>, WS_TABSTOP, 0)

        AUTODLG_TAB_BAR_MEMBER(_tabSelect, ctrlCalendar, SysMonthCal32, WS_TABSTOP, 0)

        AUTODLG_TAB_BAR_MEMBER(_tabSelect, edtRichEdit, RICHEDIT, 
                        WS_TABSTOP | ES_MULTILINE | ES_WANTRETURN, 0)
    AUTODLG_END_DECLARE_CONTROLS

}

PaintedTabFrame控件将 5 作为模板参数,表示将有 5 个选项卡,接下来的五个声明的控件必须是这些选项卡,每个都使用AUTODLG_TAB_BAR_MEMBER声明。这里显示了人口最多的选项卡。

在这种情况下,宿主对话框TabbedDlg专门用于显示tabSelect(即PaintedTabFrame)及其内容,因为这通常是您想要的,但这不一定。tabSelect可以同样是同一个对话框上的众多控件之一。与窗口选项卡控件不同,所有选项卡都一起创建为宿主对话框的成员,而不是在选择每个选项卡时创建。正如我们将看到的,这极大地促进了宿主对话框可能需要的任何编码。

目前,选项卡框架允许您在任何地方进行选项卡(通常是您想要的),而不管您是否输入了有效代码或选择了日期和时间段。所以让我们开始进行一些约束,这样您就必须遵循某种正确的程序,从OnInitDialog开始。

void OnInitDialog(HWND hWnd)
{
        tabSelect.lock(true); //#1
        dlgEnter_code().btnCancel.show(false); //#2
        tabSelect.set_tab_text(4, _T("Rich edit field")); //#3
}
  1. 默认情况下,它将选择第一个选项卡,即我们的EnterCodeDlg。我们不希望在输入了有效代码之前选择其他选项卡,因此我们锁定选项卡框架。这会禁用所有其他选项卡,使它们无法被选择。
  2. 正如在我们的SelectDataDlg中一样,EnterCodeDlg的 Cancel 按钮不会起任何作用,所以我们将其隐藏。注意括号后缀dlgEnterCode(),访问嵌入式对话框的成员所必需的。
  3. 选项卡按钮上显示的文本默认是它们显示的对话框或控件的窗口文本。在大多数情况下,这能达到我们想要的效果,但在 Rich Edit 控件的情况下,它会尝试显示其内容作为选项卡按钮文本。因此,我们显式设置该选项卡的选项卡文本。

当成功输入代码后,dlgEnterCode 将发送一个代码为IDOK的通知,因此我们处理它。

void OnNotificationsFrom(_dlgEnter_code*, UINT NotifyCode, LPARAM lParam)
{
        if (IDOK == NotifyCode)
        {
            dlgSelectData().dlgEnter_code().edtCode.as_text = dlgEnter_code().edtCode.as_text; //#1
            tabSelect.select_tab(1);  //#2
            dlgEnter_code.enable(false);  //#3
            dlgSelectData().dlgEnter_code().btnOK.notify(BN_CLICKED);  //#4
        }
}

在输入了正确的代码后,我们现在想显示下一个选项卡dlgSelectData,但这又显示了它自己的dlgEnterCode。我们不想让用户也填写这个,所以

  1. dlgEnterCode(第一个选项卡)的代码复制到dlgSelectData中的dlgEnterCode中。注意访问每个嵌入式对话框的成员时需要空括号 ().
  2. 选择下一个选项卡。锁定不会阻止程序化选择,但会阻止用户选择。现在用户会发现它锁定在新选项卡上。
  3. 禁用第一个选项卡。它不再可见,这可以防止它再次被看到。
  4. 触发dlgSelectData中的dlgEnterCodeOK按钮的BN_CLICKED通知。再次注意访问每个级别的嵌入式对话框的成员时需要空括号().

如果您还记得,我们为SelectDataDlg提供了OKToLaunch通知,以防它发现自己嵌入到另一个对话框中,就像它在这里一样。所以我们在这里处理它并解锁选项卡框架。这将使所有选项卡(除了第一个dlgEnterCode)都可以访问,但它仍然被禁用。

void OnNotificationsFrom(_dlgSelectData*, UINT NotifyCode, LPARAM lParam)
{
        if (_dlgSelectData::notify::OKToLaunch == NotifyCode)
            tabSelect.lock(false);
}

请注意,控件类型名称_dlgSelectData和限定符::notify是访问其自定义通知所必需的。

最后,LaunchDlg在开始处理时会礼貌地发出ProcessStarted通知,在停止处理时会发出ProcessStopped通知。如果需要,我们可以在处理过程中使用它们来锁定选项卡框架。

 void OnNotificationsFrom(_dlgLaunch*, UINT NotifyCode, LPARAM lParam)
 {
        switch (NotifyCode)
        {
        case _dlgLaunch::notify::ProcessStarted:
            tabSelect.lock(true);
            break;
        case _dlgLaunch::notify::ProcessStopped:
            tabSelect.lock(false);
            break;
        }
}

我希望这已经证明了通过稍微修改行为来构建现有对话框有很多范围,但如果您希望它能很好地工作,那么最好将您的控件设为 public,并让对话框定义并发出一些有用的通知。

以多种语言显示文本

控件显示的文本

您可能已经注意到,您不必在对话框中指定控件和标签的文本。相反,它会自动从您的变量名中提取 - 删除任何小写前缀并将下划线转换为空格。这只是一个方便的默认值,用于避免在原型开发期间进行不必要的工作。您还可以使用每个控件的as_text成员显式设置其文本,可以在对话框的构造函数或OnInitDialog处理程序中进行。

您还可以将每个控件的文本指定在一个单独的文件中,该文件可以有多种语言版本,格式如下:

AUTODLG_CTRL_TEXT(EnterCodeDlg, btnOK, Aceptar)
AUTODLG_CTRL_TEXT(EnterCodeDlg, btnReset, Resetear)
AUTODLG_CTRL_TEXT(EnterCodeDlg, btnCancel, Cancelar)
AUTODLG_CTRL_TEXT(EnterCodeDlg, edtCode_label, Codigo:)

AUTODLG_CTRL_TEXT(SelectDataDlg, dlgEnter_code_label, Entrega codigo)
AUTODLG_CTRL_TEXT(SelectDataDlg, edtUser_label, Usuario:)
AUTODLG_CTRL_TEXT(SelectDataDlg, lbSelect_day_of_week_label, Dia de semana)
AUTODLG_CTRL_TEXT(SelectDataDlg, lblDay_of_week, Dia de semana)
AUTODLG_CTRL_TEXT(SelectDataDlg, btnMorning, mañana)
AUTODLG_CTRL_TEXT(SelectDataDlg, btnAfternoon, tarde)
AUTODLG_CTRL_TEXT(SelectDataDlg, btnEvening, noche)
AUTODLG_CTRL_TEXT(SelectDataDlg, edtReport_label, Informe:)
AUTODLG_CTRL_TEXT(SelectDataDlg, groupProcess_control, Gestión processo)

AUTODLG_CTRL_TEXT(LaunchDlg, btnGo, Arranque)
AUTODLG_CTRL_TEXT(LaunchDlg, btnStop, STOP)
AUTODLG_CTRL_TEXT(LaunchDlg, pbarProcessing, Procesando)
AUTODLG_CTRL_TEXT(LaunchDlg, ctrlMeanwhile_stroke_me_label, Mientras tocarme!)

并通过在对话框定义之后(它们所指的)包含以下内容来激活它。

#define AUTODLG_APP_METRICS app_metrics 
#include "Spanish_for_controls.h" //the file above
#undef AUTODLG_APP_METRICS

可以为不同的语言创建这样的文件,也可以为比开发人员选择的变量名更好的原生语言文本创建。不必为您的控件包含语言文件(它们只会从变量名中提取默认值),并且语言文件不必指定每个控件,对于未列出的控件将使用默认值。

代码中使用的字符串

除了要控制单个控件的文本外,您还将要控制代码中使用的字符串的文本。您可以通过替换文字文本来实现:

pbarProcessing_label.as_text = _T("Press firmly on Go to resume process");
               

使用它在AUTODLG_TRANSLATE宏中链接下划线版本:

pbarProcessing_label.as_text =
                AUTODLG_TRANSLATE(Press_firmly_on_Go_to_resume_process);

您可以从一开始就这样做,而无需包含匹配条目的字符串翻译文件。如果未包含字符串翻译文件,它将简单地返回带有将下划线转换回空格的原始字符串。

要激活字符串翻译,您必须提供另一个具有以下格式的文件:

AUTODLG_SET_TRANSLATE(Monday, lunes)
AUTODLG_SET_TRANSLATE(Tuesday, martes)
AUTODLG_SET_TRANSLATE(Wednesday, miercoles)
AUTODLG_SET_TRANSLATE(Thursday, jueves)
AUTODLG_SET_TRANSLATE(Friday, viernes)
AUTODLG_SET_TRANSLATE(Saturday, sabado)
AUTODLG_SET_TRANSLATE(Sunday, domingo)
AUTODLG_SET_TRANSLATE(Click_here_to_enter_code, Click aqui para entrega codigo)
AUTODLG_SET_TRANSLATE(Press_firmly_on_Go_to_start_process, Apreta en Arranque con deliberación)
AUTODLG_SET_TRANSLATE(Processing, En processo)
AUTODLG_SET_TRANSLATE(Press_firmly_on_Go_to_resume_process, Apreta en Arranque para resumir);
AUTODLG_SET_TRANSLATE(SELECT_DAY_AND_TIME_PERIOD, Selecciona DIA y TEMPORADA)
AUTODLG_SET_TRANSLATE(SELECT_TIME_PERIOD, Selecciona TEMPORADA)

并在将要使用它的对话框定义之前包含以下内容:

#undef AUTODLG_TRANSLATE
#define AUTODLG_TRANSLATE(name) autodlg_string_##name
#include "Spanish_translate_table.h" //the file above

您不必包含字符串翻译表,但如果包含,则必须是完整的。也就是说,它必须包含您代码中AUTODLG_TRANSLATE宏的每个用法的条目。否则,它将无法编译。

快速参考

文件命名空间对话框定义头对话框布局定义样式, 
命名约定, 对话框公共成员对话框保护成员, 
可以在对话框定义中定义的事件处理程序
可以在指标结构中定义的事件处理程序
控件方法, 控件枚举auto_string支持的 Windows 控件列表, 
库提供的绘制控件列表 ,PaintedTabFrame, 
库配置文件, 多语言

文件 

要使用该库,您需要将 autodlg.hautodlg_controls.hautodlg_metrics_config.h 放在同一目录中。只需包含 autodlg.h 即可进行编译。

要使用示例代码,您还需要 example_dialogs.hmisc_painted_controls.h 并编译 example dialogs.cpp 

注意:对于旧的编译器,您必须使用 example_dialogs_alt.h 代替 example_dialogs.h。 它使用一种更笨拙的方式定义制表顺序,并填充do_msg调用中的所有参数。

命名空间

库代码包含在 autodlg 命名空间中,专门用于避免名称冲突,因此不要这样做

using namespace autodlg; //DON'T DO THIS

您只需要在类定义的头文件中使用命名空间,如下面的对话框定义所示。

对话框定义头

template <class metrics = autodlg::def_metrics>
class SelectDataDlg : public autodlg::dialog < metr​ics, autodlg::auto_size, WS_OVERLAPPEDWINDOW >

您的类必须派生自 autodlg::dialog,它接受以下模板参数:

  • 指标结构 - 默认:autodlg::def_metrics,您可以传递任何符合要求的指标结构。

  • 初始大小策略 - 默认:autodlg::auto_size (大小以适应布局),或者,如果您想明确固定初始大小,可以传递autodlg::explict_size<Width, Height>

  • 窗口样式 - 默认:WS_POPUPWINDOW | WS_CAPTION。 在本例中,而是传递了WS_OVERLAPPEDWINDOW 以使对话框可由用户调整大小。

如果您想像上面示例中那样将这些参数传递给派生类,这取决于您的设计选择。

对话框布局定义

所有这些都应写在派生对话框定义的声明部分。

布局必须以AUTODLG_DECLARE_CONTROLS_FOR开始,以AUTODLG_END_DECLARE_CONTROLS结束,并且这些宏之间除了控件声明以及publicprivate关键字外,不应出现任何其他内容,您可以根据设计需求自由使用它们。

控件使用以下宏声明:

AUTODLG_CONTROL( 
     variable_name, locator_verb, Dx, Dy, width, height, 
     control_type, control_styles, extended_styles)

或者,如果您希望它有一个附带的标签

AUTODLG_CONTROL_WITH_LABEL( 
     variable_name, locator_verb, Dx, Dy, width, height, 
     control_type, control_styles, extended_styles,
     label_locator, label_height, label_type, label_style)
有关可用定位器动词的详细信息,请参阅代码布局设计基石部分。
 
可以使用以下宏分别表示**宽度**和**高度**参数:
AUTODLG_WIDTH_TO(_btnCancel::right)
AUTODLH_HEIGHT_TO(_btnCancel::bottom
AUTODLG_BEGIN_TABLIST
        &btnCancel, &btnReset, &edtCode, &btnOK
AUTODLG_END_TABLIST

或使用旧编译器

AUTODLG_BEGIN_SET_TABS //alternative for older compilers
   AUTODLG_SET_TAB(btnCancel)
   AUTODLG_SET_TAB(btnReset)
   AUTODLG_SET_TAB(edtCode)
   AUTODLG_SET_TAB(btnOK)
AUTODLG_END_SET_TABS

可以为 NotifyParent 调用定义通知代码。

AUTODLG_BEGIN_DEFINE_NOTIFICATIONS_TO_PARENT
        OKToLaunch //significant event that parent may want to know about
AUTODLG_END_DEFINE_NOTIFICATIONS_TO_PARENT

样式

autodlg 库定义了三个额外的样式:

  • MOUSE_OVER_STYLE,它会导致控件在鼠标进入和离开其矩形时无效。
  • EXPAND_X_STYLEEXPAND_Y_STYLE,它们标记一个控件,如果对话框调整大小到大于其设计尺寸,则会进行扩展。

 

命名约定这些名称是自动生成的

  • 您对话框中控件的数据类型名称是控件名称前加上下划线。
    例如,btnOK的数据类型是_btnOK
  • 控件标签的名称是控件名称后加上 _label 
    例如,edtCode的标签称为edtCode_label

对话框公共成员

HWND m_hWnd;
UINT DoModal(TCHAR* szTitle = 0, DWORD Style = 0)
bool Create(HWND hWnd, TCHAR* szTitle = 0, DWORD Style = 0, RECT* pRect = 0)
void CenterWindow()
void EndDialog(UINT id)
UINT GetEndDialogValue()
static void RunAppMsgLoop(HACCEL hAccelTable)

对话框保护成员 - 可以在您的派生对话框类定义中使用,以及公共成员。

void NotifyParent(int NotifyCode, LPARAM lParam = NULL)
static bool DrawCtlColorButton(LPDRAWITEMSTRUCT lpDrawItemStruct)

可以在对话框定义中定义的事件处理程序

void OnNotificationsFrom(_btnReset*, UINT NotifyCode, LPARAM lParam)
void OnNotificationsByControlType(BUTTON* pC, UINT NotifyCode, LPARAM lParam)

LRESULT OnCtlColorFrom(_edtCode*, UINT nCtlColor, HDC hDC, bool bMouseOver)
LRESULT OnCtlColorByControlType(EDIT* pC, UINT nCtlColor, HDC hDC, bool bMouseOver)

LRESULT OnMessageAt(_edtCode*, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
LRESULT OnMessageByControlType(EDIT* pC, UINT message, WPARAM wParam, LPARAM lParam)

LRESULT OnItemMsgFrom(_edtCode*, DRAWITEMSTRUCT* pInfo, bool bMouseOver)
LRESULT OnItemMsgByControlType(BUTTON* pC, MEASUREITEMSTRUCT* pInfo, bool bMouseOver)

LRESULT OnDialogMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

有关事件处理程序如何调用的更多信息,请参阅事件处理摘要和更多关于美学指标结构

可以在指标结构中定义的事件处理程序 - 它们必须声明为 static

static HBRUSH OnCtlColor(UINT nCtlColor, HDC hDC, bool bMouseOver)

static LRESULT OnCtlColorByControlType(EDIT* pC, UINT nCtlColor, HDC hDC, bool bMouseOver)
static LRESULT OnItemMsgByControlType(BUTTON* pC, MEASUREITEMSTRUCT* pInfo, bool bMouseOver)

控件方法 - 您还将拥有您选择使用的任何控件包装类的接口,但这些接口可用于所有控件,无论它们如何被包装。

//methods that can be called on all controls//
BOOL invalidate()         
BOOL enable(BOOL bEnable) // when called on a control will also act on its label
BOOL show(int nShow)      // when called on a control will also act on its label
HWND set_focus()
HWND set_capture(bool bSet)
DWORD get_style()
DWORD set_style(DWORD _style)
RECT get_rect()
void move(int left, int top)
void move_by(int x, int y)
void size(int width, int height)
void size_by(int x, int y)
template <class W = WPARAM, class L = LPARAM> LRESULT do_msg(UINT message, W wParam = 0, L lParam = 0)

basic_window(内置原始控件包装类的基类,如BUTTONEDIT)的公共成员是

HWND m_hWnd;
operator HWND() const;

控件枚举

//Enable all controls in this dialog
control_cursor cursor(*this);
if (cursor.set_first_ctrl(*this))
do{
    cursor.get_control()->enable(TRUE);
} while (cursor.move_to_next_ctrl());

set_first_ctrl可以传递写入它的对话框的引用,如上面的示例所示,或者它可以传递另一个对话框。

cursor.set_first_ctrl(dlgEnter_code()) 

或特定的控件。

cursor.set_first_ctrl(_btnOK) 

如果它被初始化为对话框,它将枚举其所有控件,从第一个开始。如果它被初始化为控件,它将从该控件枚举到列表的末尾。get_control()返回的control*可用于访问所有控件的通用方法,并且control_cursor还包含一个公共数据成员CreateInfo,其中填充了从派生类查询的更多信息。

auto_string 

auto_string是用于每个控件提供的as_text变量的动态文本缓冲区类型。它的特殊之处在于写入它也会写入控件,而从中读取则会从控件读取。否则,它是一个非常基础的动态文本缓冲区。您可以执行以下操作:

as_text = _T("some text"); //assign zero terminated string
as_text = btnOK.as_text //assign from another auto_string
TCHAR* pzText = as_text; //read into a zero terminated string
as_text.append(_T("some more text")); //append extra text
as_text.overwrite(5, _T("***"));  //overwrite existing text from a given position
TCHAR* pzText = as_text.from(5); //copy all the text after a giving position

//get a buffer of the required length and write into it - thi still updates the control
lbSelect_day_of_week.do_msg(LB_GETTEXT, iSel,  
               lblDay_of_week.as_text.get_buf_set_length(
                                     lbSelect_day_of_week.do_msg(LB_GETTEXTLEN, iSel))

有关auto_string性质的更多信息,请参阅此处

支持的 Windows 控件列表 - 可以通过修改 autodlg_controls.h 来添加更多控件。

BUTTONCHECKBOXRADIOBUTTONGROUPBOXEDITRICHEDITLISTBOXCOMBOBOX
SCROLLBARSTATICSTATIC_BANNERSysMonthCal32PROGRESSBAR
SysListView32SysTreeView32 

库提供的绘制控件列表

PaintedLabelPaintedBannerSpacerControlGroupBoxPaintedTabFrame

有关这些控件的更多信息以及如何设计绘制控件,请参阅绘制控件部分。

PaintedTabFrame

PaintedTabFrame应在您的布局中声明为控件,并将选项卡数量作为模板参数指定。

AUTODLG_CONTROL(tabSelect, at, 6, hGap + BHeight,
        350, 200,
        PaintedTabFrame<5>, EXPAND_X_STYLE | EXPAND_Y_STYLE, NO_STYLE)

然后,它应该后面跟着使用其特殊AUTODLG_TAB_BAR_MEMBER宏定义的控件内容。

        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgEnter_code, 
            EnterCodeDlg<metrics>, WS_TABSTOP, 0)
        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgSelectData, 
            SelectDataDlg<metrics>, WS_TABSTOP, 0)
        AUTODLG_TAB_BAR_MEMBER(_tabSelect, dlgLaunch, 
            LaunchDlg<metrics>, WS_TABSTOP, 0)
        AUTODLG_TAB_BAR_MEMBER(_tabSelect, ctrlCalendar, 
            SysMonthCal32, WS_TABSTOP, 0)
        AUTODLG_TAB_BAR_MEMBER(_tabSelect, edtRichEdit, 
            RICHEDIT, WS_TABSTOP | ES_MULTILINE | ES_WANTRETURN, 0)

内容通常是嵌入式对话框,但也可能包括普通控件,如最后两项所示。

PaintedTabFrame的公共接口除了通用控件接口外还有:

bool select_tab(int NewTab)
void set_tab_text(int i, TCHAR* pText) //only needed when you don't get what you want automatically
void lock(bool _bLock) //prevents user from changing the tab displayed

选项卡式对话框提供了更多关于使用PaintedTabFrame可以做什么的详细示例。

库配置文件

该库读取两个您可以修改的配置文件:

  • autodlg_metrics_config.h - 在此处定义美学指标结构使用的参数名称。
  • autodlg_controls.h - 在此处您可以非常简洁地注册标准控件和常用控件以供使用,还可以指定要使用的控件包装库的 hWnd 成员名称和创建要求。这些已为 MFC 和 WTL 的控件包装器提供。

多语言

在对话框定义之前包含字符串翻译表。

#undef AUTODLG_TRANSLATE
#define AUTODLG_TRANSLATE(name) autodlg_string_##name
#include "Spanish_translate_table.h"

在对话框定义之后包含控件文本规范。

#define AUTODLG_APP_METRICS app_metrics
#include "Spanish_for_controls.h"
#undef AUTODLG_APP_METRICS

两者都不是必需的,但如果包含字符串翻译表,则必须是完整的。

有关字符串翻译和控件文本规范文件的格式信息,请参阅以多种语言显示文本

工作原理

关键在于丰富的类型。也就是说,每个控件都有一个唯一的数据类型。这使得所有控件的唯一数据(控件类型、位置、大小和样式)都可以在声明期间捕获在控件的数据类型中,就像使用AUTODLG_CONTROL宏所实现的那样。

AUTODLG_CONTROL(btnReset, at, hGap, vGap, BWidth, BHeight,
        BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

此处显示其扩展:

//macro to define and declare a control within a layout definition 
#define AUTODLG_CONTROL(name, Locator, X,Y, W,H, CtrlType, Style, ExtStyle)\
struct _##name##_label;\
    /***define the unique type for this control***/\
struct _##name : public control_dlglink\
    <this_dlg_type, typename wrap_selector<CtrlType>::type >\
{\
    template <class C, class T> friend static C* ctrlq::get_label(T* pT);\
    enum {left=Locator::x+X, top=Locator::y+Y, width=W, height=H, \
        right=left+width, bottom=top+height, \
        _style=Style | autodlg::control_specification<CtrlType>::defstyle \
            | metrics::add_style<CtrlType>::style, \
        _extstyle=ExtStyle\
            | autodlg::control_specification<CtrlType>::defextstyle \
            | metrics::add_style<CtrlType>::extstyle};\
        typedef CtrlType control_type;\
        typedef dialog_element label_of_type;\
private:\
    typedef dialog_element label_type;\
    /***virtual method implementation - generates run-time code***/\
    void autodlg_on_control_query(ctrlq::query_base& query)\
    {\
        if(ctrlq::INFO==query.use)\
            static_cast<ctrlq::createinfo&>(query).DoQuery\
              <_##name, CtrlType, dialog_element, metrics>(this, _T(#name));\
        else\
            DoTypedQuery(this, query);\
    }\
};\
    /***declare the control variable***/\
_##name name;\

该宏被传递了创建和显示控件并将其绑定到变量所需的所有信息。

首先定义一个新的结构,其名称是通过预处理器##连接符在前缀下划线形成的。

  • 传递的CtrlType被间接用作其基类control_dlglink的模板参数。
  • 位置和大小信息Locator, X, Y, W, H用于初始化类枚举left, top, width, height, rightbottomcontrol_type typedef 定义为传递的CtrlType 
  • _style _extStyle 枚举以更复杂的方式初始化,具体取决于CtrlType 以及传递的样式。
  • label_of_type label_type typedefs 定义为dialog_element (无特定内容),以指示它不是标签,也没有标签。

到目前为止,所有这些都只是编译时记账,然而成员函数

  • autodlg_on_control_query是一个虚拟函数实现,它确实生成将在运行时调用的代码。

最后,声明变量为新定义的类型。

将布局声明为 static const 类信息的一个直接好处是,定义和声明了一个控件后,

 AUTODLG_CONTROL(btnReset, at, hGap, vGap, BWidth, BHeight,
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

您可以定义和声明下一个控件,并引用其类型。

 AUTODLG_CONTROL(btnCancel, under<_btnReset>, 0, BHeight + 2 * vGap,
        BWidth, BHeight, 
           BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

这有助于通过相对定位将控件链接在一起。

这是under动词的定义。

template<class C> struct under
{
        enum { x = C::left, y = C::top + C::height };
};

以下是它如何在AUTODLG_CONTROL 宏的扩展中使用:
(注意:under在此情况下是定位器)。

enum {left=Locator::x+X, top=Locator::y+Y,.....

这里发生的是,您尽可能地用更友好的术语(动词、相对定位和小的参数化位移)来表达您的布局,然后由编译器将其转换为原始的 x,y 坐标。

 

以下示意图说明了由AUTODLG_CONTROL宏定义的控件背后的类层次结构。

正如我们在AUTDLG_CONTROL 宏的扩展中已经看到的,会创建一个新的类型,即结构体_##name,并且大部分参数信息(在编译时)以其const static类信息的形式存储。如果你有类名,那么你可以在任何地方引用它的类信息,编译器会将其简单地写入你的代码中。 

结构体_##name派生自control_dlglink,它接受两个模板参数。第一个是父对话框的确切类型,由AUTODLG_DECLARE_CONTROLS_FOR宏提供,该宏必须始终在任何控件声明列表之前。另一个是控件类型,它必须派生自内部的control类型,而wrap_selector<Ctrl>结构体用于确保这一点。为此系统创建的如PaintedLabel PaintedTabFrame 等绘制控件,因此已经继承了内部control类型,所以wrap_selector不做任何事情,只选择Ctrl。然而,你可能想使用的MFC或WTL中的任何控件包装器类都不会派生自内部control类型,你也无法让它们派生。本库提供的原始控件BUTTONEDIT等遵循类似的模式,派生自内部的basic_window。因此,在这些情况下,wrap_selector会使用win_control<CtrlType>来包装控件。

在窗口控件的情况下(wrap_selector产生win_control<CtrlType>),我们需要win_control派生自control,以便系统知道如何处理它,但我们也需要它派生自Ctrl,以便Ctrl的方法被继承并可供方便的编码使用。因此,我们使用多重继承。它继承自Ctrl,这可能是一个BUTTONEDIT,或者一个CButtonCEdit,并且还继承自win_control_base,而win_control_base又继承自control

在绘制控件的情况下,Ctrl本身已经直接或通过mouse_control(如果它处理鼠标事件)继承自paint_control,而paint_control又继承自control

由此可知,如果你正在处理一个控件,比如定义为CButtonbtnOK,那么btnOK的公共方法将包括win_control中实现的通用方法以及CButton提供的特定方法。

control是最成熟的通用可枚举基类。也就是说,它是唯一一个声明自己是控件但对控件类型完全不具体的类。control又派生自dialog_element,它比control的层级低,有两个目的;提供虚函数autodlg_on_control_query,所有派生自dialog_element的类都可以调用它,以及当它独立存在时提供一个空控件标记。

控件被声明为不同大小的相邻变量。没有指针列表或数组需要遍历,因此必须找到另一种方法。使用的方法是通过增加指针的类大小来从一个控件移动到下一个控件。唯一的问题是,完整的类大小对于基类control来说并不立即可用。它只能通过调用一个在完整派生类中实现的虚函数来找到。唯一的一个虚函数是autodlg_on_control_query,它用于所有此类查询,包括AUTODLG_CONTROL宏注册的类信息。查询是一个具有代码、适当数据成员和执行查询的函数的类。它可以请求一个操作或信息。查询被初始化,传递给autodlg_on_control_query,并在最派生类中由autodlg_on_control_query执行。最重要的查询代码是INFO,它会填充所有类信息。

struct ctrlq //struct that embraces all queries
{
        enum { INFO, CREATE, NOTIFICATION, CTLCOLOR, ITEM_MSG, MESSAGE, MOUSEOVER, CTRLMETHOD, SHOW, 
               ENABLE, PAINT, MOUSE, MOVE, CREATED, DLG_CREATED };
        struct query_base //base class for all queries
        {
            int use;
            query_base() : use(INFO) {}
        };
        struct createinfo : public ctrlq::query_base
        {
        private:
            createinfo(createinfo const& c){}
        public:
            createinfo(){} //inherits INFO as the value of the use member
            DWORD class_size;
            RECT rect;
            DWORD style;
            DWORD extstyle;
            TCHAR* pzInitText;
            DWORD label_class_size;
            DWORD label_of_class_size;
            DWORD control_family;
            dialog_base* m_pParent;
            HWND* pHWnd;
            template <class T, class Ctrl, class LabelType, class m> void DoQuery(T* pT, TCHAR* pzText)
            {
                class_size = sizeof(T);
                rect.left = pT->left;
                rect.top = pT->top;
                rect.right = pT->right;
                rect.bottom = pT->bottom;
                style = pT->_style;
                extstyle = pT->_extstyle;
                pzInitText = pzText;
                label_class_size = sizeof(LabelType);
                label_of_class_size = sizeof(T::label_of_type);
                control_family = control_family_selector<T>::family;
                pHWnd = hWnd_func_selecter<Ctrl>::selected::get_hWnd_pointer(pT);
            }
            void set_to_create(dialog_base* pParent)
            {
                m_pParent = pParent;
                use = CREATE; //once filled out, the same struct is used as a CREATE query
            }
        };
........
}

control_cursor用于枚举控件,它使用INFO查询来获取类大小。因为INFO查询返回的信息非常有用,control_cursorINFO查询保留为一个公共成员,可以在枚举过程中使用。通过在枚举所有控件时使用这些信息,控件可以以正确的类型、大小、位置和样式创建。

查询还用于允许基类control支持在paint_controlwin_control<Ctrl>中实现的方法。例如,如果你调用controlenable方法,它会将一个查询传递给autodlg_on_control_query ,这将导致最派生类调用其enable方法,该方法将是paint_control中的方法,或者win_control<Ctrl>中的方法,具体取决于它派生自哪个类。

这是ENABLE查询

struct enable : public ctrlq::query_base
{
            int bEnable;
            enable(bool _bEnable) : bEnable(_bEnable)
            {
                use = ENABLE;
            }
            template <class T> void DoQuery(T* pT)
            {
                pT->OnEnable(bEnable);
                control* pLabel = get_label<control>(pT);
                if (pLabel)
                    pLabel->enable(bEnable);
            }
};

在这种情况下,查询还会作用于控件的标签(如果它有标签)。

有了这些,我们就可以定义一个布局,创建它,并在控件上调用方法。现在我们需要处理事件,例如通知。控件通知会到达对话框,并直接反射回控件,然后控件会以Notify查询调用autodlg_on_control_query 

这是Notify查询的DoQuery方法

template<class T> void DoQuery(T* pT)
            {
                pT->GetDlg()->OnNotificationsByControlType(
                    (typename T::control_type*)pT, *this, lParam);

                pT->GetDlg()->OnNotificationsFrom(pT, *this, lParam);
            }

T是由其autodlg_on_control_query 实现传递的最派生类。会调用对话框上的两个方法。在一般情况下,它们将由AUTODLG_DECLARE_CONTROLS_FOR宏提供的以下接收器处理,这些接收器不做任何事情,也不编译任何代码。

template <class C>    inline void OnNotificationsByControlType\
    (C* pC, UINT NotifyCode, LPARAM lParam){}\
template <class C>    inline void OnNotificationsFrom\
    (C* pC, UINT NotifyCode, LPARAM lParam){}\

它们对第一个参数的类型不敏感。任何类型都可以接受。但是,如果你为对话框提供一个版本,将第一个参数的类型指定为调用它的控件,

void OnNotificationsFrom(_btnCancel*, UINT NotifyCode, LPARAM lParam)
{ ....

那么将被调用的是那个版本。所以你只需要提供一个第一个参数类型化为控件的处理程序,它就会被调用。所有事件处理都使用这种机制。

这些是其工作方式的主要动脉。想了解更多,你必须根据你的好奇心检查代码。其中大部分位于dialog_base类中。它必须被封装在某个地方,并将控件类定义放在对话框基类中消除了两者之间许多优先级问题,并使代码更清晰。

要点

我从之前的一个项目Units of measurement types in C++中已经知道,丰富类型可以使编译器在没有任何运行时成本的情况下检查许多事情并做出更好的决定(因为它已经在编译期间完成),但它仍然让我惊讶的是,它能为对话框设计带来如此多看似神奇的好处。这并非真正神奇。它看起来如此,是因为作为设计者,我们将对话框布局视为可变的东西,因为我们的工作就是改变它。这使我们忽略了这样一个事实:对于任何给定的编译,它都是一个在编译时完全已知的常量。对话框资源模板的使用加剧了这种忽视,因为它看起来(而且可能确实是)一个在运行时加载的变量。将布局表示为纯C++代码提供了机会,让对话框布局的常量性能够被语言正确识别,并通过为每个控件提供一个信息丰富的类型来实现这一点。明显的奇迹源于在编译时可获得布局的完整信息。

大部分代码都有非常清晰的逻辑可循,我希望它能在你想要的情况下以任何方式工作。发展最不充分的部分是随着对话框大小的改变而改变和重新排列控件。尽管如此,你可能会发现它对你来说效果很好,而且不难避免那些会使它出错的边缘情况。我已经决定按原样发布它,并将在我方便的时候稍后进行完善。

对我来说,最大的成就是编码你的布局赋予你作为程序员的力量,这项技能你很擅长也很聪明,它使你摆脱了不得不放下那些精湛的技能去从事绘图员和IDE操作员的劳动。

我完成了这项工作并发布它,是因为我希望看到别人使用它。我比使用库更喜欢编写库,在某些方面,我写它是为了治愈我多年来在传统对话框设计中遭受的痛苦。我想为所有人终结这种痛苦。我非常感兴趣的是,任何使用这里提供的东西的人,无论是快乐还是沮丧,都能给我反馈。

最后,我必须提到由撰写的优秀系列文章Custom Controls in Win32 API,我在做这个项目时一直将它作为圣经。它为许多我找不到其他地方的疑问提供了清晰的解释。

历史

首次发布 2015年5月29日
代码更新 2015年6月3日 - 添加了“Display text in multiple languages”并修复了小错误。
代码更新 2015年6月5日 - 语言文件在多个编译单元中会导致链接错误 - 现在通过修改autodlg.h仅修复。

© . All rights reserved.