QuickDialogs - 一个用于快速优雅创建对话框的库






4.86/5 (86投票s)
一个用于以声明式方式创建简单对话框的库,开销极小
引言
考虑一下无处不在的 Windows 消息框。它可能是 Windows 中使用最多、最熟悉的 UI 之一,这在很大程度上归因于创建它的简便性。
MessageBox(hwnd, "Hello, World", "Message", MB_OK);
只需花一点时间思考一下这有多么简单。您只需一行代码即可创建一个完整的窗口。然而,使用一段时间后,缺点就显而易见了——灵活性。对于消息框,您只有一段文本、一个可选的图标和一组预定义的按钮可供选择。仅此而已。没有编辑框、没有复选框、没有其他反馈。现在,有相当多的对话框比消息框提供的功能要多一些,但其引人注目的简洁性导致消息框被用在更适合自定义对话框的场景(就像“方钉圆孔综合症”)。一个常见的例子是重新措辞您的问题,使其符合“是”、“否”或“取消”,或者更糟糕的“点击确定执行 x,或点击取消执行 y”。
QuickDialogs 是一个框架,旨在提供更高的对话框构建灵活性,同时尽可能保留 MessageBox
API 调用那种简洁优雅的语法。QuickDialogs 的优势有三点:
- 它很快。创建一个简单的对话框比消息框需要更多的代码,但比自定义对话框需要少得多的代码。
- 它很一致。由于对话框代码为您处理所有布局,因此对话框的外观和感觉一致,无需任何额外努力。
- 它是可定制的。该库基于模板类,许多部分——基础窗口类和布局引擎——可以替换为您自己的自定义类。您可以向对话框添加自己的控件。
需要注意的是,它不是一个通用的窗口框架。它旨在做好一项特定任务(对话框)。如果您想要一个优秀的通用窗口框架,可以查看 WFC、MFC 或 Win32++,或者其他许多优秀的框架。
背景
该库的灵感来源于我们在工作中使用的类似库,该库做着非常类似的事情。然而,该库是用纯 C 编写的,并使用可变参数宏在一次调用中完成对话框的定义和实例化。这意味着没有类型安全,您必须牢记参数——创建对话框时经常会发生崩溃。它也完全没有文档,代码也不是世界上最清晰简洁的代码(我曾在代码深处发现过一个 char****
,这是我见过的生产代码指针上最多的间接引用)。
我一直想按照我自己的想法重构它,但一直想不出一种在保持简洁语法的 \同时\ 使其可读且类型安全的方法。直到我开始玩弄 boost 并看到了 Boost::Format
和 Boost::Spirit
在运算符重载方面的一些卓越之处,我才想出了这里提出的方法。
工作原理
QuickDialogs 使用运算符重载提供一种类型安全机制,将任意数量的控件插入对话框。控件使用流插入 (<<
) 运算符插入。所有控件的属性都可以通过构造函数设置,从而仅用几行代码就可以声明式地编写对话框。
布局是定性的——而不是手动定位控件,您只需添加控件,指定诸如“我希望控件彼此在同一行”、“居中此控件”或“将这些控件放在新列中”之类的要求。布局控制由 | 运算符、特殊的 columnbreak
和 sectionbreak
控件提供,对齐控制由 + 和 * 一元运算符提供。为了实现这一点,控件的大小会自动调整。
大多数类都是模板化的,以便灵活处理 Unicode 并提供定制和扩展性——您可以替换布局引擎、更改基础对话框窗口,并在 \无需修改库的情况下\ 编写自己的控件*。
*话虽如此,总会有人提出一个它无法处理的绝佳想法。到时候再说。
Using the Code
首先,您需要将头文件复制到您的 include 路径中。不需要 CPP 文件——该库是纯头文件。然后,在您的项目中包含 #include <quickdialogs.h>
,然后就可以开始了!好吧,还不完全是。您还需要链接到 user32.lib、gdi32.lib 和 comctl32.lib。
展示其工作原理的最佳方法是给出示例,然后逐步分析它们。让我们从一个基本的对话框开始。
dialog d("My first dialog");
d << "Hello, World!"
<< spacer()
<< *button("OK");
d.show();
这会给我们一个基本的消息框。
这大约是如果使用 MessageBox
API 调用所需代码量的两倍,但仍然相当少。现在让我们逐一看看代码的每个部分。
dialog d("My first dialog");
。这会创建一个带有标题的新对话框。d << "Hello, World!"
。这会添加一个新段落(static
控件),文本为“Hello, World!”。<< spacer()
。这会在上一个控件和下一个控件之间添加一些垂直空间。<< *button("OK");
。这会创建一个OK 按钮。*
运算符是(居中)对齐运算符。默认对齐是拉伸以适应宽度。您可以为按钮的标题指定任何文本。默认情况下,返回值等同于“OK”。d.show();
。我将把这个的含义留给读者作为练习;)。
现在让我们尝试一些更有功能的。
bool check = false;
HICON ico = (HICON)LoadImage(NULL, "ChickenEgg.ico",
IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
dialog d("My second dialog");
d << image(ico)
<< columnbreak()
<< spacer(20)
<< "Which came first, the chicken or the egg?"
<< sectionbreak()
<< spacer()
<< ~*(button("&Chicken", qdYes) | button("&Egg", qdNo) | button("&Don't Care", qdCancel))
<< spacer()
<< check_box("Don't ask me pointless questions again", check);
if (d.show() == qdYes)
{ /*...*/ }
这看起来像这样。
这正是 QuickDialogs 在经典消息框之上显示灵活性的地方。我们添加了自定义按钮和复选框。让我们更详细地看看它们。
- 我们添加了一个图像——从自定义图标文件中加载。任何可以表示为
HBITMAP
或HICON
的内容都是允许的。消息框中看到的所有标准图标都可以使用LoadIcon
函数获得(有关详细信息,请参阅 MSDN)。 - 布局——请注意使用了两个新控件——
columnbreak()
和sectionbreak()
。这些是布局控件。QuickDialogs 中的布局引擎将控件划分为列和部分。如果您想添加新列,请添加columnbreak
,布局引擎会将其放置在合理的位置。同样,如果您想在当前所有列下方重新开始,请添加sectionbreak
。在这里,我们使用它将问题与图标并排对齐,并在它们下方居中按钮。 - 我们添加了一组按钮,由
~*(button("&Chicken", qdYes) | button("&Egg", qdNo) | ...)
表示。分组运算符 (|
) 用于创建出现在同一行中的控件组。和以前一样,我们使用 * 运算符居中该组。~ 运算符是特定于组的特殊运算符——统一分组运算符。它使所有组控件具有相同的宽度和高度(最大控件的宽度/高度)。这是考虑到像这样的按钮而设计的——它使按钮大小一致,就像您在对话框中看到的那样。按钮本身很容易理解,但请注意我们指定了按钮的返回值(qdYes
/qdNo
/qdCancel
)。 - 最后一点值得注意的是复选框——
check_box("Don't ask me pointless questions again", check)
。我们将一个 bool 引用(check)传递进去——这有两个目的。首先,它指定了复选框的初始状态(true
表示选中,false
表示未选中)。其次,当对话框返回时,它将包含复选框的状态。这种简单的“引用传递”绑定用于所有需要输入/输出的控件。
最后,一些更复杂的东西:一个带标签的选项对话框。
int fontsize = 2;
std::string autosaveinterval( "10 ");
char* fontsizecaptions[] = { "Very Small ", "Small ",
"Normal ", "Large ", "Very Large " };
bool bold = false, italic = false, underline = false, strikethough = false;
bool superscript = false, subscript = false, smallcaps = false, normal = true;
bool reloadonstartup = false, multiinstance = false,
splashscreen = true, lockfiles = false;
bool linenums = false, autosave = true;
dialog d( "Options ");
d << (tab_control(0) + (tab("Font ")
<< (paragraph( "Font Size: ") |
combo_box(fontsize, fontsizecaptions, fontsizecaptions + 5))
<< (groupbox( "Styles ") << check_box("Bold", bold)
<< check_box("Italic", italic)
<< check_box("Underline", underline)
<< check_box("Strikethrough", strikethrough)
<< columnbreak()
<< radio_button("Normal", normal)
<< radio_button("Subscript", subscript)
<< radio_button("Superscript", superscript)))
+ (tab("Other options ")
<< check_box("Reload last document at startup", reloadonstartup)
<< check_box("Allow multiple instances to run", multiinstance)
<< check_box("Display Splash screen", splashscreen)
<< check_box("Keep files locked while editing", lockfiles)
<< check_box("Show line numbers", linenums)
<< (check_box("Autosave every", autosave) |
edit(autosaveinterval, esNumber, 25) | paragraph("minutes"))))
<< ~+(button( "&Apply", apply_changes, qdNone)
| button( "&OK", qdOK, true)
| button( "&Cancel", qdCancel));
所有这些代码的结果如下所示。
虽然它看起来比之前的对话框更复杂,但其中大部分与我们之前看到的完全相同。关键的新概念是容器控件。
- 这里使用了两种容器控件——分组框和选项卡控件。分组框的工作方式与对话框相同——您可以使用相同的运算符插入控件。选项卡控件略有不同:选项卡控件只包含选项卡(使用 "+" 运算符添加)。选项卡按照对话框和分组框的方式工作。
- 我们在 **Apply** 按钮中使用了事件——它调用
apply_changes
函数来应用对对话框的任何更改。如果您使用 C++0x,也可以使用函数对象或 lambda。
所有这些对话框都包含在项目中的 quickdialogs.cpp 中,这样您就可以看到它的工作原理。我还包含了一个使用每个控件的测试对话框,以便您可以看到它们如何工作的示例。
关注点
- 在寻找将 C++ 类实例与窗口类实例绑定的最佳方法时,我认为一种简单明了的方法是使用存储在每个窗口类中的 User 数据(即使用
SetWindowLongPtr(hwnd, GWLP_USERDATA, ptr)
)。但是,正如 Raymond Chen 在他的博客 The Old New Thing 中所指出的那样,我通过艰难的方式发现,这些数据实际上是为窗口类保留的私有存储,而不是供类用户使用的(并且 SysLink 控件也使用了它)。一个更好的解决方案是使用GetProp
/SetProp
,并结合一个全局字符串原子来提高查找速度。显然,.NET 使用的是这种方法。 - Unicode。虽然我没有讨论 Unicode,但该库完全兼容 Unicode。每个控件都有一个 Unicode 等效项(通常是名称前面加上 'w')。您可以在非 Unicode 对话框中使用 Unicode 控件,反之亦然。我想不出为什么您会想这样做,但这实现起来确实很酷。
- 在思考如何实现某个功能时,可以参考 .NET Framework。 .NET Framework 在如何实现功能方面极具参考价值,特别是如何自动调整控件大小。在这些情况下,.NET Reflector 是您的朋友。或者说,直到他们删除了免费版本。请改用 ILSpy,这是一个免费的开源替代品,功能非常相似。
- 主题。主题的正确工作是一个非常棘手的难题。花了一些时间和精力才使各种不同的按钮和静态控件在主题选项卡控件上正确绘制。对于任何试图获得相同效果的人来说,可以参考 Windows API 中的
EnableThemeDialogBackground
。请注意,它仅适用于对话框。具有相同窗口类的窗口似乎不起作用。您似乎还需要在WM_INITDIALOG
中设置它——它似乎在其他任何地方都无效。您无法想象修复这个问题的烦恼。
编译器兼容性
代码符合 C++03 标准(除了示例代码中的一个小 lambda 表达式……),但需要 TR1。我在以下平台下获得了干净、无警告的编译:
- Microsoft Visual Studio 2010
- Microsoft Visual Studio 2008 SP1 (TR1 需要 SP1)
- Intel C++ 2011
- GCC 4.5.2 (MinGW) 和 boost TR1 库。
我曾尝试使用 boost 在 CLang 下使其工作,但编译 boost 中的类型特征时遇到了一些错误,并且在将 VS2010 的干净编译转换为 GCC 的干净编译付出了巨大的努力后,我不想再尝试寻找 CLang 和 boost 的正确组合。但它也相差不远了,所以我认为再过几个版本它就会“正常工作”了。
它在所有编译器中都编译为 32 位,在 VS2010 和 VS2008 中编译为 64 位。我没有在任何其他编译器中测试过 64 位。
操作系统兼容性
这已在 Windows XP、Windows Vista 和 Windows 7 上进行了测试。
理论上,它可以在 Windows 2000 上工作,但我没有测试过。如果有人想使用一个 11 年历史的、不再受支持的操作系统,请告诉我它是否有效。
Linux 的爱好者们,如果你们看到这里,做得好,但这是 Windows 专用的。我没有 Linux 编程经验,所以你们运气不好,不过我刚在家里安装了 Ubuntu,所以未来也许……理论上方法仍然适用,所以如果有人为他们喜欢的 Linux 或跨平台小部件工具包实现了一个等效的库,我很想听到。
替代方案
- 久负盛名的 Windows 消息框快速、简单,但完全缺乏灵活性。但它是每个 Windows 版本的一部分,而且易于使用。
- 在 Windows Vista 及更高版本中,您可以使用
TaskDialog
API。它被引入来解决这个库所涵盖的许多相同问题。与 QuickDialogs 相比,它的优点是它是标准的操作系统组件,并提供了进度条和命令链接按钮,而 QuickDialogs 目前不提供这些功能。QuickDialogs 提供更广泛的控件、更灵活的布局,并且还可以在 Windows XP 上使用。根据您的目标,两者都是不错的选择。
未来计划
- 文档——目前就是本文。我想写一些更实质性的内容。我正在考虑撰写关于如何定制框架各个部分的文章,因此欢迎提出意见和建议。
- 可调整大小的对话框。如果对话框可以调整大小,那将非常酷。考虑到整个框架都围绕着自动布局构建,这似乎不会花费太多精力。
- 更多控件——进度条、列表视图和 Vista 命令链接控件。考虑其他请求(例如
ComboBoxEx
、Trackbars
、Scrollbox
)。 TaskDialog
——我想让功能成为TaskDialog
API 的超集,并能够生成外观与TaskDialog
API 相似的对话框。我还没有决定为此采取什么方法,所以任何建议都将不胜感激。- 有几个地方可以指定不同控件的最小尺寸——目前,这些都以像素为单位。我想将其更改为使用对话框单位,这将使它们与分辨率无关。
- 仍然存在一些小的布局 bug。我正在一一解决……
历史
- 2011 年 5 月 8 日:第一个版本!
- 2011 年 5 月 22 日:修复了定义 UNICODE 时编译的问题,并添加了控件的“
t
”typedef
(感谢 _flix01_)。修复了 marl 报告的令人尴尬的复制粘贴错误。