随笔- 313  评论- 12176  文章- 1 

Windows Vista for Developers——第二部分:深入分析任务对话框

作者:Kenny Kerr

翻译:Dflying Chen

原文:http://weblogs.asp.net/kennykerr/archive/2006/07/18/Windows-Vista-for-Developers-_1320_-Part-2-_1320_-Task-Dialogs-in-Depth.aspx

请同时参考《Windows Vista for Developers》系列

 

正如Aero向导比传统的向导更加友好一样,替代原有消息窗口的任务对话框(task dialog)也能够带来更好的用户体验。与消息窗口相比,任务对话框提供了很多新的功能,并大大增强了自定义能力。当然,随着这些功能上的增强,复杂性也有所提高。在《Windows Vista for Developers》系列的第二部分中,我将用原生C++演示如何有效地使用任务对话框API创建各种各样的对话框。如果你没有耐心,那么请直接跳到本文的最后找到下载链接,这个链接包含有一个完整的、用C++封装好的任务对话框API的源代码。

comctl32.dll库中的一个名为CTaskDialog 的C++类实现了所有任务对话框所提供的功能。你可以通过调用comctl32.dll 中的TaskDialog TaskDialogIndirect 函数来使用任务对话框。TaskDialog 其实就是个简单版本的TaskDialogIndirect ,它精简了TaskDialogIndirect 的很多功能,但也稍微易于使用一些。实际上TaskDialog TaskDialogIndirect 都不是那么简单,所以本文将在简要介绍TaskDialogIndirect 之后,用C++其进行封装,让其更加易于使用。

下面的这段代码将创建一个最简单的任务对话框:

TASKDIALOGCONFIG config = { sizeof (TASKDIALOGCONFIG) };
int selectedButtonId = 0;
int selectedRadioButtonId = 0;
BOOL verificationChecked = FALSE;
HRESULT result = ::TaskDialogIndirect(&config,
                                      &selectedButtonId,
                                      &selectedRadioButtonId,
                                      &verificationChecked);

TASKDIALOGCONFIG 结构包含了创建任务对话框所必需的字段和标记,同时也提供了回调函数,用来响应任务对话框所触发的事件:

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;
};

可以想象,搞出这样一个结构出来显然不是件容易的事情,难免会出现这样或那样的错误。虽然我们可以忽略其中的很多字段,但如下一些字段却是要得到预期效果所必需的:

  1. cbSize 字段在编译期指定了该结构的大小,在C语言中,这种技术常用于区分结构体的版本。操作系统可以通过该字段知晓该结构的版本,以便基于该版本信息对结构体中的数据字段作以假设并使用。
  2. hwndParent 字段保存了父窗口的句柄。这样即可让该对话框以模态的形式显示出来,并可以设定其相对于父窗口的位置。
  3. hInstance 字段对C++开发者来说非常有用,允许我们在资源文件中通过标识符指定其字符串和图标资源,而不用繁琐地在代码中手工加载或创建。
  4. dwFlags 字段保存了各种不同的标记,用来控制对话框的行为和样式。本文将在接下来详细介绍。

 

文本说明(Text Captions)

TASKDIALOGCONFIG 结构包含了如下的字段,用来分别设置任务对话框中的各个文本说明。

pszWindowTitle
pszMainInstruction
pszContent
pszVerificationText
pszExpandedInformation
pszExpandedControlText
pszCollapsedControlText
pszFooter

这些字段均可以用字符串指针,或是用MAKEINTRESOURCE 宏创建的字符串标识符进行初始化。除此之外,我们还可以为自定义按钮设置文本说明,将在下一节中详细介绍。

下图就演示了各种不同文本说明的位置:

在对话框创建之前,我们可以用pszWindowTitle 设定图中的“window title”。在对话框创建之后,则必须使用SetWindowText 函数对其进行修改。

在对话框创建之前,我们即可用pszMainInstruction 设定“main instruction”。在对话框创建之后,则必须用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 字段设置。版本号为5456的Windows Vista在展开/折叠扩展信息时还有一个Bug——若该控件失去输入焦点,这部分文本将被重置为折叠状态时的文字。

设定这些文本说明并不是件容易的事,取决于文本从何处取得以及你希望的设定实际。在本文的稍后部分,我们将用C++对其进行简化。

 

按钮

任务对话框支持各种普通按钮以及自定义按钮的组合。目前普通按钮有如下几个:

  1. TDCBF_OK_BUTTON (IDOK)
  2. TDCBF_YES_BUTTON (IDYES)
  3. TDCBF_NO_BUTTON (IDNO)
  4. TDCBF_CANCEL_BUTTON (IDCANCEL)
  5. TDCBF_RETRY_BUTTON (IDRETRY)
  6. TDCBF_CLOSE_BUTTON (IDCLOSE)

我们可以以任何的方式组合这些标记,并设置到dwCommonButtons 字段中。括号中的常量是按钮的标识符,用来甄别用户点击了哪个按钮。

本文结尾部分源代码下载中的Common Buttons Sample演示了这些普通按钮:

我们不能直接改变这些普通按钮的顺序以及其文本说明。若想完全控制这些按钮,则需要提供一个TASKDIALOG_BUTTON结构的数组。下面这段代码就指定了两个自定义按钮:

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结构的数组指定:

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

可以看到,这些标识只能影响到自定义按钮。普通按钮将仍显示为常规样式。

我们也可以发送TDM_SET_BUTTON_ELEVATION_REQUIRED_STATE消息至该窗口,这样即可在链接旁显示出那个“声名狼藉的”User Account Control盾形图标(为啥声名狼藉?——译者问)。

这条消息适用于任何自定义按钮,无论是命令链接形式还是常规按钮形式。顺便说一句,对于OK和Cancel这类的普通按钮,该消息同样适用,虽然这样做让用户看上去并不怎么友好。

 

图标

我们还可以让任务对话框显示出一个“主”图标和一个“页脚”图标。主图标显示在对话框中主要文本的旁边,若是设定了TDF_CAN_BE_MINIMIZED 标记,那么也会在窗口的标题栏中显示出来。页脚图标将显示在页脚文字旁。

设定图标则有些技巧。在对话框创建之前,我们可以把由MAKEINTRESOURCE 宏创建的图标资源标志符设定到pszMainIcon 字段上。如果你选用这种方法,请不要设置TDF_USE_HICON_MAIN 标记。或者也可以把图标句柄设置到hMainIcon 字段中,若采用这种方法,则要确保设置了TDF_USE_HICON_MAIN标记。

页脚图标也类似。pszFooterIcon 字段用来在创建对话框之前指定图标资源的标识符。或者也可以将图标句柄设置到hFooterIcon 字段中(类似地,要确保设置了TDF_USE_HICON_FOOTER标记)。

在对话框创建完成之后,我们可以通过发送TDM_UPDATE_ICON 消息来更新这些图标。若想更新主图标,那么将WPARAM 设置为 TDIE_ICON_MAIN ,若想更新页脚图标,则把WPARAM 设置为TDIE_ICON_FOOTERLPARAM 要设置为图标资源标志符或者图标句柄,这取决于创建对话框时TDF_USE_HICON_MAINTDF_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_NORMALPBST_PAUSEDPBST_ERROR

本文下载代码中的Progress Sample Progress Effects Sample 演示了所有的进度条功能。

 

通知(Notifications)

任务对话框提能够通知外部程序其当前状态,可以用来为该对话框添加行为或是响应对话框中发生的事件。这些通知可以由指定于pfCallback 字段的回调函数得到。回调函数的原型如下:

HRESULT __stdcall Callback(HWND handle, 
                           UINT notification, 
                           WPARAM wParam, 
                           LPARAM lParam, 
                           LONG_PTR data);

这个原型似乎有些误导的倾向,因为这些消息都不返回HRESULT。有些消息有返回值,但只是布尔值(TRUEFALSE)而已。handle参数提供了任务对话框窗口的句柄,在TDN_DESTROYED通知到来之前,我们可以随心所欲地对该对话框进行操控。data参数提供了指定于lpCallbackData 字段中值的指针。常用于将某个C++窗口对象的指针传递到静态回调函数中。接下来我们看看这些通知。

TDN_DIALOG_CONSTRUCTED是第一个到来的通知。除了提供该任务对话框的句柄之外,该通知还说明对话框创建完毕,即将显示出来。此时我们即可发送任何用来在对话框显示出来之前修改其样式的消息。随之而来的就是TDN_CREATED 通知,不过通常我们并不需要关心这个东西,除非你要做一些窗口特定的初始化工作。在这两个通知中做一些初始化工作都是合法的,虽然在页导航(page navigation )时不会提供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通知表示用户点击了用来折叠/展开“expanded information”内容的区域。若处于折叠状态,则WPARAM FALSE,若处于展开状态,则为TRUE

TDN_HYPERLINK_CLICKED 通知表示用户点击了位于任务对话框中的某个超链接。只有“content”、“expanded information”和“footer”部分才支持超链接,且需要设定TDF_ENABLE_HYPERLINKS 标记。超链接由HTML锚标记(A)定义,例如:

<a href="uri">text</a>

注意这里只支持双引号,所以其中的字符可能需要转义。长字符串中同样可以使用链接。链接的href属性可通过LPARAM 访问,然后我们即可随心所欲地进行处理,例如打开一个网页等。任务对话框并没有为链接提供任何默认的行为。下载代码中的MainWindow 类演示了超链接的使用方法。

TDN_TIMER通知提供了一个定时器,我们可以用这个定时器实现很多不同的功能,例如更新对话框中的内容,在一段时间后关闭该对话框等。若是设置了TDF_CALLBACK_TIMER 标记,那么每隔大约200毫秒就会发送一个TDN_TIMER通知。下载代码中的Timer Sample演示了定时器的功能:

 

消息(Messages)

任务对话框可以响应一系列的消息,我们可以使用这种机制控制并实现某些需要的行为。

TDM_CLICK_BUTTONTDM_CLICK_RADIO_BUTTON消息相应地用来模拟用户点击某个按钮或单选按钮。WPARAM 指定了该按钮或单选按钮的标识符,LPARAM 在这里没什么用处。

TDM_CLICK_VERIFICATION消息用来模拟用户点击了确认复选框。WPARAM 表示该复选框是否被选中,LPARAM 表示该复选框是否需要得到输入焦点。

TDM_ENABLE_BUTTONTDM_ENABLE_RADIO_BUTTON消息相应地用来启用/禁用某个按钮或单选按钮。WPARAM 指定了该按钮或单选按钮的标识符,LPARAM 表示该按钮或单选按钮是否被启用。

上一节中我没有提到的一个通知就是TDN_NAVIGATED,因为现在还没什么相关的文档。该通知与TDM_NAVIGATE_PAGE消息密切相关,所以我觉得把它放在这里说或许会更好一些。TDM_NAVIGATE_PAGE 同样也没什么相关文档说明。经过一段时间的单步调试(当然,这里要使用操作系统的symbol),我终于明白了它的用处。这些消息可以让我们从一个任务对话框切换,或者叫做导航至另一个,就像一个只能向前的向导。“新的”任务对话框将得到“老的”任务对话框的所有权,也就是说并不需要重新创建一个任务对话框。我在反编译器中跟踪comctl32.dll 代码时发现,TDM_NAVIGATE_PAGE 消息的处理程序并未使用WPARAM ,但却期待LPARAM 提供一个TASKDIALOGCONFIG 结构,用来描述将要导航至的对话框。然后TDN_NAVIGATED 通知就和那个新的任务对话框的回调函数关联了起来。下载代码中的Error Sample 演示了这个功能。

 

用C++让一切变得简单

任务对话框的功能自然强大,但对于开发者而言,使用起来却并不是那么的容易。尽管只暴露出了两个函数,任务对话框的C语言API仍就显得复杂。为了解决这个问题,我特意编写了这个TaskDialog C++类,简化我们在C++中操作任务对话框的过程。TaskDialog 类继承于ATL的CWindow 类,它封装了大部分任务对话框的功能,将很多准备TASKDIALOGCONFIG 时所需要的复杂工作抽象了出来,还可以发送消息并响应通知。下载代码中的所有程序都使用了该TaskDialog 类,所以你不愁没有示例代码。

下面就是某个示例任务对话框的源代码,在下载代码中也可以找到:

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 类提供了一种简单的、面向对象的操作任务对话框的方式。你再也不需要直接生成那冗长复杂的结构体,也不需要手工定义按钮数组。这些细节统统都由TaskDialog 类替你搞定。TaskDialog 类提供了设置/修改任务对话框中文本说明和图标的方法,还提供了添加按钮、发送各种消息的方法,响应通知的功能则用一系列的虚方法提供。

使用上面定义的这个任务对话框是件再简单不过的事了:

TimerDialog dialog;
Dialog.DoModal();

DoModal 方法返回之后,我们即可使用GetSelectedButtonIdGetSelectedRadioButtonIdVerificiationChecked方法取得用户选择的按钮。

若想知道TaskDialog 类到底隐藏了那些复杂性,看看SetWindowTitle 方法的代码吧:

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 方法都没有问题,不用再关心文字的来源如何,或是对话框是否已经被创建。

 

示例程序

本文提到的实例程序可以在此下载,其中演示了所有文中提到的功能:http://www.kennyandkarin.com/kenny/vista/taskdialogsamplecpp.zip

 

嗯……似乎这篇文章的长度远远超过我的预料(也超出了我的预料,好累——译者注)。Windows Vista的任务对话框API提供了这么多的功能,我实在是不能再压缩了。突然意识到,这也是目前唯一一份完整的任务对话框参考文档。希望能够让尽可能多的读者受益。

本来我想在托管代码中演示任务对话框功能的,但Daniel Moth已经用C#做出了一个不错的版本。他也曾讲过一堂webcast ,其中演示了创建任务对话框的若干种办法,包括我在MSDN Magazine文章的中提到的Task Dialog Designer。还要提到的一点就是这个webcast中有处错误:其中说Kerr.Vista是一个COM组件,而事实上这只是个用C++/CLI编写的简单.NET程序集。

posted on 2007-03-15 21:36  Dflying Chen  阅读(...)  评论(...编辑  收藏