Vista 中的 C++ 实用功能:使用 TaskDialogIndirect 构建获取用户输入的对话框






4.91/5 (45投票s)
2006年12月18日
13分钟阅读

137973

933
使用 TaskDialogIndirect API 显示功能丰富的对话框,以帮助用户做出决定。
目录
引言
在这篇 Vista 实用程序文章中,我将介绍新的 TaskDialogIndirect()
API,您可以使用它来创建复杂的对话框,而无需构建自己的对话框资源并编写代码来处理大量控件。虽然 TaskDialogIndirect()
非常复杂,但结果可能非常令人印象深刻,并且您知道对话框在所有 Vista 主题中都会正常显示,并且将继续在未来版本的 Windows 中正常工作。
涵盖 TaskDialogIndirect()
的所有功能将使本文过于冗长,因此在本文中,我将重点介绍当用户需要做出决定时,用于获取用户输入的对话框。此处提供的示例代码与上一篇关于 TaskDialog()
的文章所处的环境相同:我们的应用程序需要知道用户是否要下载更新。
本文是为 Vista 的 RTM 版本编写的,使用了 Visual Studio 2005、WTL 7.5 和 Windows SDK。有关在哪里下载这些组件的更多信息,请参阅第一篇 Vista 实用程序文章中的介绍。
TaskDialogIndirect 的基本用法示例
所有任务对话框的功能都由 TASKDIALOGCONFIG
结构控制。由于这是一个庞大的结构,我将在文章中随着讨论任务对话框的各种功能而逐步介绍它的新部分。
TaskDialogIndirect()
的原型是
HRESULT TaskDialogIndirect ( const TASKDIALOGCONFIG* pTaskConfig, int* pnButton, int* pnRadioButton, BOOL *pfVerificationFlagChecked );
参数如下:
pTaskConfig
- 指向您在调用函数之前填充的
TASKDIALOGCONFIG
结构的指针。此结构控制对话框的大部分外观和行为。 pnButton
- 指向一个
int
的指针,该指针被设置为指示用户单击哪个按钮关闭了对话框。如果用户单击内置按钮,该值可以是以下之一:IDOK
、IDYES
、IDNO
、IDCANCEL
、IDRETRY
、IDCLOSE
。如果用户单击自定义按钮,该值将设置为该按钮的 ID。如果函数失败,该值将设置为 0。如果您不需要知道单击了哪个按钮,可以将此参数传递为NULL
。 pnRadioButton
- 指向一个 int 的指针,该指针被设置为指示对话框关闭时选择了哪个单选按钮。如果对话框没有单选按钮,您可以将此参数传递为
NULL
。 pfVerificationFlagChecked
- 指向一个
BOOL
的指针,该指针指示对话框关闭时复选框的状态。如果对话框没有复选框,您可以将此参数传递为NULL
。
返回值是表示函数是否成功的 HRESULT
。如果无法加载资源或在内存不足的情况下,它可能会失败。
让我们从创建一个与上一篇文章中对话框类似的对话框开始。
void CTheApp::OnUpdateAvailable() { HRESULT hr; TASKDIALOGCONFIG tdc = { sizeof(TASKDIALOGCONFIG) }; int nClickedBtn; LPCWSTR szTitle = L"Mike's AntiFluff Scanner", szHeader = L"An update for Mike's AntiFluff Scanner is available", szBodyText = L"Version 2007.1 of Mike's AntiFluff Scanner has been released. " \ L"Do you want to download the update now?"; tdc.hwndParent = m_hWnd; tdc.dwCommonButtons = TDCBF_YES_BUTTON|TDCBF_NO_BUTTON; tdc.pszWindowTitle = szTitle; tdc.pszMainIcon = TD_INFORMATION_ICON; tdc.pszMainInstruction = szHeader; tdc.pszContent = szBodyText; hr = TaskDialogIndirect ( &tdc, &nClickedBtn, NULL, NULL ); if ( SUCCEEDED(hr) && IDYES == nClickedBtn ) { // download the update... } }
正如您所见,我们通过设置 TASKDIALOGCONFIG
结构中的成员来指示要在对话框中显示的文本。毫不奇怪,这会产生一个与 TaskDialog()
显示的对话框完全相同的对话框。
TaskDialogIndirect 的功能
在本节中,我们将快速浏览一下您的任务对话框可以具有的一些功能。(请记住,本文不会涵盖任务对话框可以做的*所有*事情。)
我并没有有意识地这样计划,但当我在示例任务对话框中添加功能并编写它们时,这一节变成了一种迭代式设计。我希望您喜欢这个深入了解我们 UI 设计师在尝试设计用户真正能够阅读和使用的东西时所经历的过程。:)
第一个要查看的 TASKDIALOGCONFIG
成员是 dwFlags
。虽然许多任务对话框功能仅通过在 TASKDIALOGCONFIG
成员中存储有效值来启用,但有些标志会控制对话框本身或特定功能的行为。您可以始终设置的一些标志,它们会影响对话框本身,是
TDF_ALLOW_DIALOG_CANCELLATION
:如果设置了此标志,用户可以按 Esc 和 Alt+F4 关闭对话框,即使对话框没有 ID 为IDCANCEL
的按钮。TDF_POSITION_RELATIVE_TO_WINDOW
:设置此标志时,任务对话框将相对于hwndParent
窗口居中。没有此标志,对话框将基于hwndParent
所在的监视器居中。TDF_CAN_BE_MINIMIZED
:如果设置了此标志,用户可以通过右键单击标题栏并从菜单中选择“最小化”来最小化任务对话框。您还必须设置TDF_ALLOW_DIALOG_CANCELLATION
标志,或具有 ID 为IDCANCEL
的按钮,此标志才能生效。由于任务对话框始终是模态的,因此此标志对于需要输入的提示不太有用。
控制单个功能的标志将在下面与相应的功能一起提及。
TaskDialog 也具有的功能
TASKDIALOGCONFIG
的这些成员控制的功能与相应的 TaskDialog()
参数相当
hwndParent
:用作任务对话框父窗口的窗口。hInstance
:API 应从中加载资源的HINSTANCE
。dwCommonButtons
:对话框中应显示哪些内置按钮。pszWindowTitle
:要在对话框标题栏中显示的字符串(或字符串资源 ID)。pszMainIcon
:要在对话框中显示的图标资源 ID(或内置图标的 ID)。pszMainInstruction
:要在对话框顶部显示的字符串(或资源 ID)。pszContent
:要在对话框正文中显示的字符串(或资源 ID)。
与 TaskDialog()
一样,任何字符串都可以是 NULL
,并且您必须对资源 ID 使用 MAKEINTRESOURCE
宏。此外,pszMainInstruction
和 pszContent
可以包含 '\n'
来创建换行符。使用这些成员无需设置任何标志。
自定义文本的按钮
使用 TaskDialogIndirect()
,我们不限于六个预定义的按钮。我们可以创建任意数量的按钮,并具有我们喜欢的任何文本,甚至可以将它们与预定义的按钮混合。两个 TASKDIALOGCONFIG
成员控制这些按钮
pButtons
:指向一个TASKDIALOG_BUTTON
结构数组的指针,每个按钮一个。cButtons
:UINT
,表示该数组中有多少个结构。
TASKDIALOG_BUTTON
是一个简单的结构
struct TASKDIALOG_BUTTON { int nButtonID; PCWSTR pszButtonText; };
nButtonID
是按钮的 ID,可以是任何尚未分配给预定义按钮的 ID。pszButtonText
是一个以零结尾的 Unicode 字符串,或字符串资源 ID。此字符串用于按钮的文本,并且可以包含一个 ampersand 来指示助记键。
这是一个任务对话框的新版本,使用两个新字符串替换了“是”和“否”(新代码以粗体显示)
void CTheApp::OnUpdateAvailable() { HRESULT hr; TASKDIALOGCONFIG tdc = { sizeof(TASKDIALOGCONFIG) }; int nClickedBtn; LPCWSTR szTitle = L"Mike's AntiFluff Scanner", szHeader = L"An update for Mike's AntiFluff Scanner is available", szBodyText = L"Version 2007.1 of Mike's AntiFluff Scanner has been released. " \ L"Do you want to download the update now?"; TASKDIALOG_BUTTON aCustomButtons[] = { { 1000, L"Heck &Yeah!" }, { 1001, L"N&o Way Dude" } }; tdc.hwndParent = m_hWnd;tdc.dwCommonButtons = TDCBF_YES_BUTTON|TDCBF_NO_BUTTON;tdc.pButtons = aCustomButtons; tdc.cButtons = _countof(aCustomButtons); tdc.pszWindowTitle = szTitle; tdc.pszMainIcon = TD_INFORMATION_ICON; tdc.pszMainInstruction = szHeader; tdc.pszContent = szBodyText; hr = TaskDialogIndirect ( &tdc, &nClickedBtn, NULL, NULL ); if ( SUCCEEDED(hr) && 1000 == nClickedBtn ) { // download the update... } }
这就是我们的两个自定义按钮!请注意,我们现在将 nClickedBtn
与 1000 比较,而不是与 IDYES
比较,因为 1000 是我们分配给“Heck Yeah!”按钮的 ID。
设置默认按钮
我们可以设置 TASKDIALOGCONFIG
的 nDefaultButton
成员,使特定按钮成为默认按钮。该按钮将在对话框首次显示时获得焦点。如果我们想将“No Way Dude”按钮设为默认按钮,我们将添加以下行
tdc.nDefaultButton = 1001;
要使预定义按钮成为默认按钮,请将 nDefaultButton
设置为其预定义 ID:IDOK
、IDRETRY
等。
使用命令链接
在 Vista 中,按钮控件有一个新的样式 BS_COMMANDLINK
,它将其变成一个命令链接,这是一个更大的按钮,可以带有图标和可选的第二行文本。通过在 dwFlags
中设置 TDF_USE_COMMAND_LINKS
标志,我们的所有自定义按钮都将成为命令链接。如果我们添加以下行
tdc.dwFlags = TDF_USE_COMMAND_LINKS;
对话框将有两个命令链接,而不是两个普通按钮。
只有自定义按钮才能更改为命令链接。如果我们还将预定义的“关闭”按钮放在对话框中,它仍会显示在底部。
命令链接的目标是提供更有帮助和更有意义的标签,因此让我们将文本更改为更用户友好。
void CTheApp::OnUpdateAvailable() { HRESULT hr; TASKDIALOGCONFIG tdc = { sizeof(TASKDIALOGCONFIG) }; int nClickedBtn; LPCWSTR szTitle = L"Mike's AntiFluff Scanner", szHeader = L"An update for Mike's AntiFluff Scanner is available", szBodyText = L"Version 2007.1 of Mike's AntiFluff Scanner has been released." \ L"Do you want to download this update?"; TASKDIALOG_BUTTON aCustomButtons[] = { { 1000, L"&Download and install the update now" }, { 1001, L"Do ¬ download the update" } }; tdc.hwndParent = m_hWnd; tdc.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION|TDF_USE_COMMAND_LINKS; tdc.pButtons = aCustomButtons; tdc.cButtons = _countof(aCustomButtons); tdc.pszWindowTitle = szTitle; tdc.pszMainIcon = TD_INFORMATION_ICON; tdc.pszMainInstruction = szHeader; tdc.pszContent = szBodyText; hr = TaskDialogIndirect ( &tdc, &nClickedBtn, NULL, NULL ); if ( SUCCEEDED(hr) && 1000 == nClickedBtn ) { // download the update... } }
这是带有更新文本的对话框。
UI 设计师会告诉你,UI 的第一条规则是用户不看 UI。(有关更多文本为何更糟的良好描述,请参阅Joel Spolsky 的这篇文章。)我认为这个版本更可取,因为最重要的信息在命令链接上,这是用户在做出决定时*必须*阅读的控件。标题文本很醒目,用户的目光很可能会首先被吸引到那里,因此很有可能被阅读。这三个元素告诉用户他们需要了解的有关情况的所有信息:正在发生什么(有更新可用),以及他们可以做什么(下载或不下载)。
正文文本老实说,现在我们已经将按钮更改为命令链接,这已经不那么重要了。它也位于两个较大的 UI 元素之间,我敢打赌很多人会完全忽略它。让我们删除这些文本,并扩展命令链接上的文本。
命令链接可以显示第二行文本,提供有关按钮将执行的操作的更多详细信息。两行由换行符分隔。这是我们对话框的下一个版本。
void CTheApp::OnUpdateAvailable() { HRESULT hr; TASKDIALOGCONFIG tdc = { sizeof(TASKDIALOGCONFIG) }; int nClickedBtn; LPCWSTR szTitle = L"Mike's AntiFluff Scanner", szHeader = L"An update for Mike's AntiFluff Scanner is available";szBodyText = L"Version 2007.1 of Mike's AntiFluff Scanner has been released." \L"Do you want to download this update?";TASKDIALOG_BUTTON aCustomButtons[] = { { 1000, L"&Download and install the update now\n" L"Update the program to version 2007.1" }, { 1001, L"Do ¬ download the update\n" L"You will be reminded to install the update in one week" } }; tdc.hwndParent = m_hWnd; tdc.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION|TDF_USE_COMMAND_LINKS; tdc.pButtons = aCustomButtons; tdc.cButtons = _countof(aCustomButtons); tdc.pszWindowTitle = szTitle; tdc.pszMainIcon = TD_INFORMATION_ICON; tdc.pszMainInstruction = szHeader;tdc.pszContent = szBodyText;hr = TaskDialogIndirect ( &tdc, &nClickedBtn, NULL, NULL ); if ( SUCCEEDED(hr) && 1000 == nClickedBtn ) { // download the update... } }
现在事情开始成形了!命令链接上较大的文本仍然告诉用户每个按钮的作用,而较小的第二行则提供更多详细信息,如果用户关心阅读它们。请注意,即使用户*只*阅读每个按钮的第一行,他仍然能够做出一个好的决定。
显示附加详细信息
当有更多详细信息可用,但又不想用大量文本使对话框显得杂乱时,另一个有用的功能是展开信息文本区域。如果我们设置 TASKDIALOGCONFIG
的 pszExpandedInformation
成员,对话框将有一个按钮,用户可以单击该按钮来展开对话框并查看 pszExpandedInformation
中的文本。我们可以通过两个更改将此功能添加到我们的示例对话框中。
LPCWSTR szExtraInfo = L"This update was released on December 1, 2006 " \ L"and updates the Scanner to run properly on the Vista RTM build."; tdc.pszExpandedInformation = szExtraInfo;
现在对话框底部有一个“查看详细信息”按钮,它将显示有关我们更新的更详细信息。
单击按钮会显示附加信息。
对话框还可以在页脚区域显示展开后的信息,位于“查看详细信息”按钮下方。要将文本移到那里,请将 TDF_EXPAND_FOOTER_AREA
标志添加到 dwFlags
。然后,展开状态将如下所示。
详细信息按钮的文本可以通过设置 TASKDIALOGCONFIG
的 pszExpandedControlText
和 pszCollapsedControlText
成员来自定义。如果您想在展开和折叠状态下为按钮使用相同的文本,您可以只设置这两个成员中的一个。
高级 UI 功能
现在我们已经了解了如何使用基本 UI 元素构建对话框,让我们来看看一些更高级的功能。
添加复选框
任务对话框还可以显示一个复选框,它通常用于“不再显示此消息”的提示。我们可以通过设置 TASKDIALOGCONFIG
的 pszVerificationText
成员来向我们的对话框添加一个复选框。TaskDialogIndirect()
通过其第 4 个参数返回复选框的状态,因此我们需要添加一个 BOOL
变量并将其地址传递给该参数。
void CTheApp::OnUpdateAvailable() { HRESULT hr; TASKDIALOGCONFIG tdc = { sizeof(TASKDIALOGCONFIG) }; int nClickedBtn; BOOL bCheckboxChecked; LPCWSTR szTitle = L"...", szHeader = L"...", szExtraInfo = L"...", szCheckboxText = L"In&stall future updates automatically, without asking me"; TASKDIALOG_BUTTON aCustomButtons[] = { { 1000, L"..." }, { 1001, L"..." } }; tdc.hwndParent = m_hWnd; tdc.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION|TDF_USE_COMMAND_LINKS; tdc.pButtons = aCustomButtons; tdc.cButtons = _countof(aCustomButtons); tdc.pszWindowTitle = szTitle; tdc.pszMainIcon = TD_INFORMATION_ICON; tdc.pszMainInstruction = szHeader; tdc.pszExpandedInformation = szExtraInfo; tdc.pszVerificationText = szCheckboxText; hr = TaskDialogIndirect ( &tdc, &nClickedBtn, NULL, &bCheckboxChecked );
如果用户单击下载更新的按钮(ID 为 1000 的按钮),我们还会检查复选框的状态。如果已选中,则我们的应用程序将存储一个配置设置,告知它将来自动下载更新。
if ( SUCCEEDED(hr) && 1000 == nClickedBtn ) { // download the update... if ( update_was_installed && bCheckboxChecked ) { // store an option so we don't prompt again } } }
对话框带有复选框的外观如下。
通常,复选框默认是未选中的,但您可以通过将 TDF_VERIFICATION_FLAG_CHECKED
标志添加到 dwFlags
来使其默认处于选中状态。
添加超链接
任务对话框支持在 pszContent
和 pszExpandedInformation
文本元素(以及 pszFooter
,我们稍后将看到的功能)中嵌入超链接。在此示例中,我们将向 pszExpandedInformation
文本添加一个链接,该链接将打开一个网页,提供有关更新的更多信息。要添加链接,我们需要做三件事:
- 将
TDF_ENABLE_HYPERLINKS
标志添加到dwFlags
。 - 指示
pszExpandedInformation
中的文本的哪个部分应该是链接。 - 添加一个回调函数,以便在用户单击链接时收到通知。
在文本中创建链接很简单,我们只需将文本用 <a>
标签括起来,就像在 HTML 中一样。在 <a>
标签中,我们放置了一个 href
属性,其中包含我们希望链接启动的 URL。您可以在下面的 szExtraInfo
文本中看到此标签。
void CTheApp::OnUpdateAvailable() { HRESULT hr; TASKDIALOGCONFIG tdc = { sizeof(TASKDIALOGCONFIG) }; int nClickedBtn; BOOL bCheckboxChecked; LPCWSTR szTitle = L"...", szHeader = L"...", szCheckboxText = L"...", szExtraInfo = L"This update was released on December 1, 2006 " \ L"and updates the Scanner to run properly on the Vista RTM build.\n" \ L"<a href=\"http://www.example.com/\">Full details about this update</a>"; TASKDIALOG_BUTTON aCustomButtons[] = { { 1000, L"..." }, { 1001, L"..." } }; tdc.hwndParent = m_hWnd; tdc.dwFlags = TDF_USE_COMMAND_LINKS|TDF_ENABLE_HYPERLINKS; tdc.pButtons = aCustomButtons; tdc.cButtons = _countof(aCustomButtons); tdc.pszWindowTitle = szTitle; tdc.pszMainIcon = TD_INFORMATION_ICON; tdc.pszMainInstruction = szHeader; tdc.pszExpandedInformation = szExtraInfo; tdc.pszVerificationText = szCheckboxText; tdc.pfCallback = TDCallback; tdc.lpCallbackData = (LONG_PTR) this; hr = TaskDialogIndirect ( &tdc, &nClickedBtn, NULL, &bCheckboxChecked ); }
任务对话框实际上不会对链接单击做任何事情,应用程序负责获取 href
并对其进行操作。这就是我们将在 TDCallback
函数中执行的操作。回调函数具有以下原型。
HRESULT CALLBACK TaskDialogCallbackProc ( HWND hwnd, UINT uNotification, WPARAM wParam, LPARAM lParam, LONG_PTR dwRefData )
hwnd
是任务对话框的 HWND
;我们可以将其用作我们要显示的任何 UI 的父窗口。uNotification
是一个指示已发生事件的常量。wParam
和 lParam
是消息特定的数据。dwRefData
是存储在 TASKDIALOGCONFIG
的 lpCallbackData
成员中的值。
此回调尚未完全文档化,因此目前,我们必须检查参数并确定它们包含什么。当用户单击链接时,uNotification
为 TDN_HYPERLINK_CLICKED
,lParam
是一个 LPCWSTR
,其中包含 href
属性中的文本。这是处理此通知的 TDCallback()
。
HRESULT CALLBACK TDCallback (
HWND hwnd, UINT uNotification, WPARAM wParam,
LPARAM lParam, LONG_PTR dwRefData )
{
switch ( uNotification )
{
case TDN_HYPERLINK_CLICKED:
ShellExecute ( hwnd, _T("open"), (LPCWSTR) lParam,
NULL, NULL, SW_SHOW );
break;
}
return S_OK;
}
做出这些更改后,超链接将出现在展开后的信息文本中。
使用页脚区域
我认为我们已经将这个对话框中所有有用的信息都塞满了,所以最后一个要讲的功能是页脚区域。除了我们已经看到的按钮控件和展开后的信息文本之外,任务对话框还可以显示一个图标和一些始终显示在页脚中的文本。文本也可以包含超链接。
在最后一个示例中,让我们将“完整详细信息”超链接移到页脚,使其始终可见。以下是需要对 TASKDIALOGCONFIG
结构进行的更改,以及新的字符串。
LPCWSTR szFooter = L"<a href=\"http://www.example.com/\">Full details about this update</a>"; tdc.pszFooter = szFooter; tdc.pszFooterIcon = TD_INFORMATION_ICON;
这是结果:
结论
任务对话框无疑是 Vista 的一个受欢迎的补充,并且是构建 UI 的便捷方式,无需担心对话框模板和控件布局的细节。获取输入只是任务对话框功能的一部分,请务必查看“扩展阅读”部分中的链接,了解更多关于其其他功能的信息。
进一步阅读
面向开发者的 Windows Vista - 第二部分 - 深入了解任务对话框。Kenny Kerr 发布了一篇非常长的帖子,讨论了任务对话框的更多功能,以及大量的示例代码和一个 ATL 包装器类。
MSDN 提供了关于新的 Vista UI 指南的冗长页面。对于任务对话框,您应该熟悉的页面是文本和语气。
版权和许可
本文是受版权保护的材料,©2006 Michael Dunn。我知道这不会阻止人们在网络上到处复制它,但我必须这么说。如果您有兴趣翻译本文,请给我发电子邮件告知我。我不认为我会拒绝任何人翻译的许可,我只是想知道翻译的情况,以便我可以在这里发布链接。
随本文提供的演示代码已发布到公共领域。我这样发布是为了让代码使每个人受益。(我不会将文章本身设为公共领域,因为只有在 CodeProject 上提供文章才能同时提高我的知名度和 CodeProject 网站的知名度。)如果您在自己的应用程序中使用演示代码,发送电子邮件告知我将不胜感激(仅为了满足我好奇是否有其他人从我的代码中受益),但不是必需的。在您自己的源代码中注明出处也受到赞赏,但不是必需的。
修订历史
2006 年 12 月 18 日:文章首次发布。
系列导航:« 使用任务对话框显示友好消息