正如 Aero 向导相比传统向导带来了更佳的用户体验那样,任务对话框则是带来了比高龄的消息框更好的用户体验。不过,任务对话框要比低级的消息框提供了多得多的一长串的特性以及可定制性。而随这些而来的则是相当深的复杂度。在此 Windows Vista for Developers 系列的这第二个部分里,我将会为你展示如何通过 C++ 有效地使用任务对话框 API 来既简单又容易地构建出形形色色的对话框。如果你现在就迫不及待,你可以跳到文章的末尾,在那儿你可以找到一份供下载的源代码,里面有对任务对话框 API 的完整 C++ 封装。
在 comctl32.dll 库的内部,隐藏着一个名为 CTaskDialog 的 C++ 类,它负责实现了任务对话框提供的所有功能。由 comctl32.dll 导出的 TaskDialog 和 TaskDialogIndirect 函数会为你调用到它。TaskDialog 函数仅仅是 TaskDialogIndirect 的一个简化版本,它提供了较少的功能,不过也更容易使用一些。由于无论哪一个都并不是即刻就很有用,所以本文将专注于 TaskDialogIndirect,并演示如何使用一个 C++ 的小小辅助就可以使得它相当的便于使用。
以下代码创建一个最简单的任务对话框:
1 2 3 4 5 6 7 8 9 10 |
TASKDIALOGCONFIG config = { sizeof (TASKDIALOGCONFIG) }; int selectedButtonId = 0; int selectedRadioButtonId = 0; BOOL verificationChecked = FALSE; HRESULT result = ::TaskDialogIndirect(&config, &selectedButtonId, &selectedRadioButtonId, &verificationChecked); |
TASKDIALOGCONFIG 结构提供了一大堆你可以填充的成员域以及标志,其中还有一个你可以用来提供对任务对话框发出的事件作出响应的回调函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
struct TASKDIALOGCONFIG { UINT cbSize; HWND hwndParent; HINSTANCE hInstance; TASKDIALOG_FLAGS dwFlags; TASKDIALOG_COMMON_BUTTON_FLAGS dwCommonButtons; PCWSTR pszWindowTitle; union { HICON hMainIcon; PCWSTR pszMainIcon; }; PCWSTR pszMainInstruction; PCWSTR pszContent; UINT cButtons; const TASKDIALOG_BUTTON* pButtons; int nDefaultButton; UINT cRadioButtons; const TASKDIALOG_BUTTON* pRadioButtons; int nDefaultRadioButton; PCWSTR pszVerificationText; PCWSTR pszExpandedInformation; PCWSTR pszExpandedControlText; PCWSTR pszCollapsedControlText; union { HICON hFooterIcon; PCWSTR pszFooterIcon; }; PCWSTR pszFooter; PFTASKDIALOGCALLBACK pfCallback; LONG_PTR lpCallbackData; UINT cxWidth; }; |
正如你能想象到的,恰如其分地填充这个结构简直是个挑战,而出错的机会却相当的大。尽管其中的很多域可以置零,但为了能达到预期的效果,下列的域通常是要进行设置的:
cbSize 域在编译时指定了此结构的大小,这是一种常用技术,用以在 C 中表示数据结构的版本。它使得操作系统可以知道应用程序编译时采用了哪种版本结构,并据此来像应用程序所期望的那样来对其中的域以及功能性作一个确定的推断。
hwndParent 域保存了父窗口的句柄。这就允许结果对话框以模态窗口的方式工作,而且还可以使你对它进行相对于父窗口的定位。
hInstance 域对 C++ 开发人员有用,因为它能使你使用 ID 来指定字符串以及图标资源而不必使用代码来加载或者创建它们。
dwFlags 域保存了若干个允许你控制对话框的行为以及外观的标志。本分的后续篇幅会分别探讨这些标志。
文本标题
TASKDIALOGCONFIG 结构提供了以下在任务对话框上设置不同的文本标题的域:
1 2 3 4 5 6 7 8 |
pszWindowTitle pszMainInstruction pszContent pszVerificationText pszExpandedInformation pszExpandedControlText pszCollapsedControlText pszFooter |
所有的这些域都可以被初始化为字符串指针或者是用 MAKEINTRESOURCE 宏创建的资源标识符。除此之外,你还可以为自定义按钮设置标题,我们会在下一节里涉及这些。
下面的窗口截图展示了不同的文本标题:
“Window title” 可以在对话框创建之前通过 pszWindowTitle 域来指定。一旦创建,就可以使用常规的 SetWindowText 函数更新标题了。
“Main instruction” 可以在对话框创建之前通过 pszMainInstruction 域来指定。一旦创建,你就必须使用 TDM_SET_ELEMENT_TEXT 消息来更新文本。设置 WPARAM 为 TDE_MAIN_INSTRUCTION,设置 LPARAM 为字符串指针或者用 MAKEINTRESOURCE 宏创建的资源标识符。同样的方法也可用在 “Content”、“Verification text”、“Expanded information” 以及 “Footer” 文本标题上,只要为 WPARAM 传递不同的需要更新文本的控件的标识符即可。
“Expanded control text” 和 “Collapsed control text” 仅能在对话框创建之前通过 pszExpandedControlText 和 pszCollapsedControlText 域指定。Windows Vista Build 5456 中,在展开和收缩扩充信息的控件里有一个 bug。如果该控件失去了焦点,文本会恢复到为收缩状态所指定的值。
设置文本标题是否是一个挑战取决于你从哪儿取得文本以及你什么时候要设置它。在后文中,我们会看到如何使用 C++ 来梦幻般地简化这一工作。
按钮
任务对话框支持公用按钮与自定义按钮的任意组合。当前定义的公用按钮有:
1 2 3 4 5 6 |
TDCBF_OK_BUTTON (IDOK) TDCBF_YES_BUTTON (IDYES) TDCBF_NO_BUTTON (IDNO) TDCBF_CANCEL_BUTTON (IDCANCEL) TDCBF_RETRY_BUTTON (IDRETRY) TDCBF_CLOSE_BUTTON (IDCLOSE) |
你可以在 dwCommonButtons 域中指定这些按钮标志的任意组合。括号中的常量标明了当特定按钮被点击之后用以标识该按钮的按钮标识符。
可在文末找到的下载里的 Common Buttons 示例演示了公用按钮的使用:
你不能直接操纵公用按钮的事情之一是对它们重新排序或者改变它们的标题。要完全控制这些按钮,你需要提供一组 TASKDIALOG_BUTTON 结构。下面是指定了两个自定义按钮的简单例子:
1 2 3 4 5 6 7 8 9 10 |
TASKDIALOGCONFIG config = { sizeof (TASKDIALOGCONFIG) }; TASKDIALOG_BUTTON buttons[] = { { 101, L"First Button" }, { 102, L"Second Button" } }; config.pButtons = buttons; config.cButtons = _countof(buttons); |
你也可以使用 MAKEINTRESOURCE 宏来为按钮所用到的字符串表中的字符串指定一个资源标识符。
除按钮外,任务对话框还可以容纳一组单选按钮。它们也可以使用一组 TASKDIALOG_BUTTON 结构来描述:
1 2 3 4 5 6 7 8 |
TASKDIALOG_BUTTON radioButtons[] = { { 201, L"First Radio Button" }, { 202, L"Second Radio Button" } }; config.pRadioButtons = radioButtons; config.cRadioButtons = _countof(radioButtons); |
下面是以上代码的结果:
你还可以指定 TDF_USE_COMMAND_LINKS 标志把自定义按钮显示为命令链接。如果你不愿看到标题边的图标的话可以使用 TDF_USE_COMMAND_LINKS_NO_ICON 标志。
正如你看到的,这些标志仅仅影响自定义按钮。你指定的任何公用按钮仍然显示为常规按钮。
你还可以在你的按钮旁边显示那个声名狼藉的用户帐号控制(User Account Control)的盾形图标,只要给窗口发送 TDM_SET_BUTTON_ELEVATION_REQUIRED_STATE 消息即可。 WPARAM 指定按钮标识符, LPARAM 指定一个指示要显示还是隐藏盾标的 BOOL 值。
无论你的按钮是命令链接还是常规按钮,它都会生效。同样,它还可以工作于像 OK 和 Cancel 这样的公用按钮,尽管通常都不会为这样一个按钮准备一个漂亮的用户体验去获取权限的提升。
图标
任务对话框可以选择性地显示一个“主”图标以及一个“脚注”图标。主图标出现在主指令(main instruction)文本的边上,而且,如果指定了 TDF_CAN_BE_MINIMIZED 标志,也可以显示在标题栏上。当存在脚注文本的时候,脚注图标显示在其附近。
指定图标需要点技巧。在对话框创建之前可以使用 pszMainIcon 域来指定一个使用了 MAKEINTRESOURCE 宏的图标资源标识符。如果你用这种方法,要确保 TDF_USE_HICON_MAIN 标志没有设置。另外,你还可以在 hMainIcon 域里指定一个图标句柄,在这种情况下,你就要确保设置了 TDF_USE_HICON_MAIN 标志。
脚注图标是用同样的方式工作的。在对话框创建之前,可以使用 pszFooterIcon 域指定一个图标资源标识符。此外,还可以在 hFooterIcon 中指定一个图标句柄。对于脚注图标,表明你是要使用一个句柄的标志是 TDF_USE_HICON_FOOTER。
对话框创建后,你可以发送 TDM_UPDATE_ICON 消息来更新图标。把 WPARAM 设置为 TDIE_ICON_MAIN 来更新主图标,设置为 TDIE_ICON_FOOTER 来更新脚注图标。 LPARAM 是要设置成图标资源标识符还是图标句柄则取决于创建时指定的标志是 TDF_USE_HICON_MAIN 还是 TDF_USE_HICON_FOOTER。
如同文本标题一样,把这些全部搞妥也是个挑战,本文稍后献上的 C++ 解决方案会在相当程度上简化这一问题。
进度条
任务对话框的显著特性之一是它提供了一个进度条。只要简单地指定一下 TDF_SHOW_PROGRESS_BAR 标志,你的任务对话框就会包含一个进度条。如果你希望把进度条显示为来回摆动的形式,可以使用 TDF_SHOW_MARQUEE_PROGRESS_BAR 标志。对话框创建之后,你还可以使用 TDM_SET_PROGRESS_BAR_MARQUEE 消息在常规进度条和往返进度条之间切换。把 WPARAM 设置为 TRUE 来显示为往返进度条,设置为 FALSE 来显示常规进度条。 LPARAM 则以毫秒为单位用以控制往返动画的时延。
可以用 TDM_SET_PROGRESS_BAR_RANGE 消息来指定进度条的范围。 LPARAM 中指定了范围的两个值,低位字中是范围的最小值而高位字中为范围的最大致。 TDM_SET_PROGRESS_BAR_POS 消息用来设置进度条的居于范围内的位置。 WPARAM 指定了位置的值。
还可以用 TDM_SET_PROGRESS_BAR_STATE 消息来改变进度条的状态。 WPARAM 可以设置为 PBST_NORMAL、 PBST_PAUSED 或者 PBST_ERROR。
本文下载里的 Progress 示例以及 Progress Effects 示例中演示了进度条的所有功能。
通知
任务对话框提供了一组通知,允许你加入行为以及事件发生时的响应。这些通知会被传递到你在 pfCallback 域中指定的一个回调函数。此回调函数的原型如下:
1 2 3 4 5 |
HRESULT __stdcall Callback(HWND handle, UINT notification, WPARAM wParam, LPARAM lParam, LONG_PTR data); |
不过这一原型有些误导,因为没有任何消息是返回一个 HRESULT。仅有的一个返回了点东西的消息也仅仅是返回一个值为 TRUE或者 FALSE 的布尔值。我希望这个问题能在发布之前解决(译者注:文本成文于 Windows Vista 正式版发布之前)。其中的 handle 参数提供了任务对话框的窗口句柄,你可以保存下来以备其他时间使用,直到收到 TDN_DESTROYED 通知为止。 data 参数提供了你在 lpCallbackData 中指定的指针。这通常用于传递一个 C++ 窗口对象的指针到静态回调函数中。现在我们看一下这些通知。
TDN_DIALOG_CONSTRUCTED 是第一个到达的通知。它在对话框创建完毕而即将要显示的时候触发,因而随之提供了任务对话框的窗口句柄。在这个时候,你可以发送任何你想在显示之前修改对话框的外观的消息。随在本通知之后的是 TDN_CREATED 通知,不过你通常不需要理会它,除非你要执行一些针对特定窗口的初始化操作。这两个通知消息都可以用来执行初始化操作,只不过在导航页面发生时不会发送 TDN_CREATED 而只发送 TDN_DIALOG_CONSTRUCTED。下一节里会讨论导航。
毫无惊奇可言, TDN_BUTTON_CLICKED 通知表示一个按钮被点击过了。这既包括公用按钮也包括自定义按钮。这一通知还会在通过点击右上角的 X 或者按 Esc 键以取消对话框的情况下发出,不过这个功能仅在创建之前指定了 TDF_ALLOW_DIALOG_CANCELLATION 标志才有效。 WPARAM 中为指示哪个按钮被点击了的按钮标识符。在前文里,我已经讨论过了按钮和按钮标识符。回调函数如果对此通知返回 FALSE 则会使对话框关闭,返回 TRUE 则会组织对话框被关闭。
TDN_RADIO_BUTTON_CLICKED 通知表示有一个单选按钮被点击了。 WPARAM 中为指示哪个单选按钮被点击了的按钮标识符。回调函数对此通知的返回值会被忽略掉。
TDN_HELP 通知表示用户在键盘上按下了 F1(帮助)键。试着提供些帮助吧。
TDN_VERIFICATION_CLICKED 通知表示确认复选框的状态发生了改变。 WPARAM 为 FALSE 则为未选中状态,为 TRUE 则为选中状态。
TDN_EXPANDO_BUTTON_CLICKED 通知表示展开或者收缩“扩充信息”区域的控件被点击了。 WPARAM 为 FALSE 则为收缩状态,为 TRUE 则为展开状态。
TDN_HYPERLINK_CLICKED 通知表示任务对话框中的某个文本域内的超链接被点击了。只有“内容”、“扩充信息”和“脚注”文本标题内才支持超链接,而且需要指定 TDF_ENABLE_HYPERLINKS 标志。超链接可以使用 HTML 中的 A(anchor)元素来定义,如下所示:
1 |
<a href="uri">text</a> |
仅支持双引号,所以在必要时你可能需要转义。链接可以出现在大一些的串中间。在 href 属性中指定的值会由 LPARAM 提供,你可以用它来做任何你喜欢做的事,比如打开一个网页。任务对话框没有聪明到可以给它提供任何缺省的行为。本文下载里的 MainWindow 类演示了超链接的使用。
TDN_TIMER 通知用以提供一个定时器,你的对话框可以用它做各种事情,从更新对话框控件到在一段时间后自动关闭对话框。如果指定了 TDF_CALLBACK_TIMER 标志,则定时器通知会大约每 200 毫秒发送一次。本文下载里的 Timer 示例演示了定时器功能的使用:
消息
任务对话框会响应一些消息以便你用来按需模拟特定的动作。
TDM_CLICK_BUTTON 和 TDM_CLICK_RADIO_BUTTON 消息可以分别模拟按钮或者单选按钮的点击。 WPARAM 中指定按钮标识符, LPARAM 参数会被忽略。
TDM_CLICK_VERIFICATION 消息模拟确认复选框的点击。 WPARAM 里指示它应该是选中的( TRUE)还是未选中的( FALSE)。 LPARAM 用以指示它是否应该接受焦点, TRUE 为是 FALSE 为否。
TDM_ENABLE_BUTTON 和 TDM_ENABLE_RADIO_BUTTON 消息分别可以启用或者禁用一个按钮或者单选按钮。 WPARAM 指定按钮标识符, LPARAM 指示是要启用( TRUE)还是禁用( FALSE)。
我在前一节里避免提及的最后一个通知是 TDN_NAVIGATED,这个通知在本文写作之时还没有任何文档。它直接关系到 TDM_NAVIGATE_PAGE 消息,因此我认为应该在这儿讨论它。自从 TDM_NAVIGATE_PAGE 消息出现以来,它也是毫无任何文档。在调试器中经过几分钟的单步追踪汇编(当然有操作系统符号的配合)之后,我就可以把它说出来了。这些消息允许你转换,或者说是导航,从一个任务对话框到另一个,就像一个只能前进的向导一样。“新”的任务对话框有效地接管了前一个对话框的窗口,因此对于新的任务对话框来说,并不会有一个新的窗口创建出来。当我在反汇编器里跟踪到 comctl32.dll 的代码内部时,我发现 TDM_NAVIGATE_PAGE 的消息处理器并不读取 WPARAM,而是预期 LPARAM 中有一个指向描述了将要导航到的下一个对话框的外观和行为的 TASKDIALOGCONFIG 结构。然后 TDN_NAVIGATED 通知被发送到新任务对话框的回调函数里。本文的 Error 示例演示了这个功能。
C++ 的援助
任 务对话框确实很强大,但是带来了使用性上的损失。尽管只暴露了两个函数,但任务对话框的 C API 却相当的复杂。为了解决这个问题,我使用自然 C++ 代码(译者注:自然 C++ 代码,原文为 native C++ code,作者可能是相对于托管 C++ 代码而言)写了 TaskDialog C++ 类来简化对任务对话框的使用。 TaskDialog 类继承于 ATL 的 CWindow 类,封装了决大多数(如果不是全部的话)的任务对话框的功能,简化掉了有关准备 TASKDIALOGCONFIG 结构、发送消息以及响应通知的许多复杂性。本文下载中的所有示例都使用了我的 TaskDialog 类,所以你应该算是有了充足的可靠的例子。
下面是包含在本文的下载中的一个示例任务对话框的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
class TimerDialog : public Kerr::TaskDialog { public: TimerDialog() : m_reset(false) { SetWindowTitle(L"Timer Sample"); SetMainInstruction(L"Time elapsed: 0 seconds"); AddButton(L"Reset", Button_Reset); m_config.dwFlags |= TDF_ALLOW_DIALOG_CANCELLATION | TDF_CALLBACK_TIMER; } private: enum { Button_Reset = 101 }; virtual void OnTimer(DWORD milliseconds, bool&reset) { CString text; text.Format(L"Time elapsed: %.2f seconds", static_cast<double>(milliseconds) / 1000); SetMainInstruction(text.GetString()); reset = m_reset; m_reset = false; } virtual void OnButtonClicked(int buttonId, bool&closeDialog) { switch (buttonId) { case Button_Reset: { m_reset = true; break; } case IDCANCEL: { closeDialog = true; break; } default: { ASSERT(false); } } } bool m_reset; }; |
如你所见,它为任务对话框提供了一个简单的、面向对象的模型。你不需要直接填充各种结构或者管理按钮定义的数组。 TaskDialog 基类考虑到了所有的细节。提供了设置(以及更新)不同的文本标题和图标的方法,也提供了添加按钮以及发送各种消息的方法。最后,还提供了响应通知的虚方法。
使用如上定义的任务对话框是再简单不过了:
1 2 |
TimerDialog dialog; Dialog.DoModal(); |
一旦 DoModal 方法返回,你就可以使用 GetSelectedButtonId、 GetSelectedRadioButtonId 和 VerificiationChecked 方法来获取用户选定的各个按钮。
为了给你一个由 TaskDialog 类隐藏起来的复杂性的概念性认识,可以看一下 SetWindowTitle 方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
void Kerr::TaskDialog::SetWindowTitle(ATL::_U_STRINGorID text) { if (0 == m_hWnd) { m_config.pszWindowTitle = text.m_lpstr; } else if (IS_INTRESOURCE(text.m_lpstr)) { CString string; // Since we know that text is actually a resource Id we can ignore the pointer truncation warning. #pragma warning(push) #pragma warning(disable: 4311) VERIFY(string.LoadString(m_config.hInstance, reinterpret_cast<UINT>(text.m_lpstr))); #pragma warning(pop) VERIFY(SetWindowText(string)); } else { VERIFY(SetWindowText(text.m_lpstr)); } } |
ATL 的 _U_STRINGorID 类使得你可以轻易地指定一个字符串指针或者资源标识符。如果任务对话框还没有创建,则简单地更新内部的 TASKDIALOGCONFIG 结构。否则,就会使用 SetWindowText 函来更新窗口标题。使用这种方法,开发人员就可以在任意位置调用 SetWindowTitle 方法而无需根据窗口标题指定的时间或者数据来提供不同的代码。
示例
下载区中提供的本文的示例生动地演示了文章中讲到的所有特性。
这篇文章的终稿比我预计的要长一些。Windows Vista 任务对话框 API 实在是提供了太多的功能,简直使我无法适时地完成本文。这也是我所知道的关于任务对话框的唯一的完整的文档。我希望它能使许多的读者受益。
我原本计划使用托管代码来介绍任务对话框,可是 Daniel Moth 已经使用 C# 出色地完成了介绍任务对话框的工作。他还创建了一个 webcast,演示了许多创建任务对话框的方案,其中的 Task Dialog Designer 来源于我的 MSDN 杂志文章。我必须指出的是,该 webcast 中不正确地把 Kerr.Vista 配件说成是一个 COM DLL,而其实它仅仅是一个简单使用 C++/CLI 写就的 .NET 配件。
阅读第三部分:桌面窗口管理器