介绍 你们有些人可能知道,未来版本的Windows,代号为Vista将包括更丰富的用户体验。作为的一部分,Windows Vista将支持一种新的对话框命名任务对话框。 任务对话框非常类似于普通消息框,但他们支持更多的选项,通常更灵活。任务对话框,我们可以实现非常简单但富有对话框用于提示,以及更复杂的类似于向导的应用程序。我理解,许多操作在Windows Vista将面临的任务序列对话框。 一个任务对话框下面的一般结构: 窗口标题(或者标题)主要指令,显示一个图标和一个texta内容区域显示一个描述和支持hyperlinksa组无线电buttonsa进步巴拉组自定义按钮,以及一些常见的buttonsa复选框,允许简单的场景像“不再显示此对话框”一个脚注区,显示一个小图标和文本,支持超链接 本文介绍了我这个新功能的实现,设计源代码兼容与即将到来的API。实际上,因为这个功能只可在Windows Vista并不意味着你不一定要今天在您的应用程序中使用它。所以我决定试着自己实现这个。 这个定制实现,你也可以使用任务对话框应用程序设计为在Windows 2000和后来的平台上工作。 如何在您的项目中使用它 本文的下载功能的动态库commctrl_taskdialogs.dll暴露组成任务对话框的函数API。它还包含一个头文件,commctrl_taskdialogs。h,包括在你的应用程序访问的声明类型,任务对话框的结构和功能的API。动态库使用ATL和WTL实现,因此没有外部依赖。 在Windows Vista,这个API将由comctl32.dll暴露并通过commctrl.h头文件。因为这个实现是设计为源代码兼容,您应该能够取代包含指令当Vista最终船只。 如果你读的描述任务对话框API,你会发现很容易实现一个简单的提示,如上面所示的图。下面的代码演示如何显示这样一个对话框: 隐藏,复制Code
#include <commctrl_taskdialogs.h> // on Vista, use #include <commctrl.h> directly int button; ::TaskDialog( ::GetActiveWindow() // parent window handle , ::AfxGetResourceHandle() // resource handle , MAKEINTRESOURCE(IDR_MAINFRAME) // window title , MAKEINTRESOURCE(IDS_TASK_WELCOME_INSTRUCTION) // main instruction , MAKEINTRESOURCE(IDS_TASK_WELCOME_CONTENT) // text in the content area , TDCBF_YES_BUTTON | TDCBF_NO_BUTTON // common buttons , MAKEINTRESOURCE(IDR_MAINFRAME) // icon , &button ); if (button == IDYES) { // creates and displays the first task dialog // that will take part in the navigation sequence ... } )
很简单,不是吗?尽管是非常灵活的,任务对话框API暴露只有两个函数。这是正确的。 TaskDialog函数本身相当于对话框API。它不做太多,除了允许的任何组合按钮通常在一个消息框。不过,一个显著特点是任务对话框的可能性从资源加载字符串和图标,使用指定的资源处理结合MAKEINTRESOURCE宏。 这个API的真正威力在于TaskDialogIndirect函数,您可能猜到,TASKDIALOGCONFIG结构+其他参数。这种结构的成员组织的总体结构任务对话框,如上图所示。是什么使它强大的可能性为应用程序指定一个回调函数,有趣的事情发生了在任务时将调用对话框中,如当一个按钮被按下时,当一个单选按钮或复选框被选中时,当点击超链接时,等等。如您所见,这个函数比MessageBoxIndirect方式更强大的API。事实上,加上其内置的支持一个进度条和回调计时器,和从一个任务对话框导航到另一个可能性,TaskDialogIndirect功能很像一个迷你应用程序框架。 TASKDIALOGCONFIG结构包含了无数成员,它可能需要一段时间适应。幸运的是,很容易创建一些包装它,所以你会发现包装器类为ATL和MFC项目在本文的下载。 包装器类 因为它可能是乏味的使用原始TaskDialogIndirect函数和维护直接TASKDIALOGCONFIG结构的所有成员,我提供了一些包装类,您可以使用在你的MFC和ATL项目。使用这些包装类绝不是强制性的,然而,我在头文件包含一组宏使用任务对话框支持从一个普通的Win32 API SDK应用程序。 包装器类使其易于管理和操作任务对话框,和包括支持回调通知。它包含一组虚函数,称为自动的框架,和一组方法,管理信息的传播任务对话框。 例如,任务对话框显示在本文的开始是由下面的代码样例应用程序中实现: 隐藏,收缩,复制鳕鱼e
class CCheckBoxTaskDialog : public CTaskDialog { // construction / destruction public: CCheckBoxTaskDialog() : CTaskDialog( MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_INSTRUCTION) , MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_CONTENT) , MAKEINTRESOURCE(IDR_MAINFRAME) , 0 , MAKEINTRESOURCE(IDR_MAINFRAME)) { // only allow hyperlinks on systems that support it // i.e. check for the version of the common control library if (DllGetVersion(L"comctl32") >= MAKEDLLVERULL(6, 0, 0, 0)) { SetFlags(TDF_ENABLE_HYPERLINKS); SetVerificationText(MAKEINTRESOURCE( IDS_TASK_CHECK_BOX_CHECK_ENABLE_HYPERLINKS)); } SetFlags(TDF_ALLOW_DIALOG_CANCELLATION); SetCommonButtons(TDCBF_CLOSE_BUTTON); AddButton(IDC_TASK_CHECK_BOX_BUTTON_CONTINUE); SetFooter(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_FOOTER)); SetFooterIcon(MAKEINTRESOURCE(IDR_MAINFRAME)); } // overrides private: BOOL OnButtonClicked(UINT uID) { if (uID == IDC_TASK_CHECK_BOX_BUTTON_CONTINUE) { Navigate(ProgressBarTaskDialog_); return TRUE; } return FALSE; } void OnVerificationClicked(BOOL bChecked) { if (bChecked) { SetContent(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_CONTENT_HYPERLINKS)); SetFooter(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_FOOTER_HYPERLINKS)); } else { SetContent(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_CONTENT)); SetFooter(MAKEINTRESOURCE(IDS_TASK_CHECK_BOX_FOOTER)); } } void OnHyperLinkClicked(LPCWSTR wszHREF) { ::ShellExecuteW(GetSafeHwnd(), L"open", wszHREF, L"", L"", SW_SHOWNORMAL); } // data members private: CProgressBarTaskDialog ProgressBarTaskDialog_; };
作为参考,这里是CTaskDialog MFC包装类的声明。ATL包装类非常相似,包含相同的方法和虚函数: 隐藏,收缩,复制Code
#ifndef _INC_AFXTASK #define _INC_AFXTASK class CSimpleTaskDialog : public CWnd { // construction / destruction public: DECLARE_DYNAMIC(CSimpleTaskDialog) CSimpleTaskDialog( LPCTSTR instruction = _T("") , LPCTSTR content = _T("") , LPCTSTR title = _T("") , TASKDIALOG_COMMON_BUTTON_FLAGS buttons = TDCBF_OK_BUTTON , LPCTSTR icon = 0); virtual ~CSimpleTaskDialog(); // attributes public: virtual int GetButton(void) const; virtual void SetWindowTitle(LPCTSTR title); virtual void SetMainInstruction(LPCTSTR instruction); virtual void SetContent(LPCTSTR content); virtual void SetIcon(LPCTSTR icon); // operations public: virtual INT_PTR DoModal(CWnd* pParentWnd = CWnd::GetActiveWindow()); // data members protected: TASKDIALOGCONFIG m_config; int m_button; }; class CTaskDialog : public CSimpleTaskDialog { // construction / destruction public: DECLARE_DYNAMIC(CTaskDialog) CTaskDialog( LPCTSTR instruction = _T("") , LPCTSTR content = _T("") , LPCTSTR title = _T("") , TASKDIALOG_COMMON_BUTTON_FLAGS buttons = TDCBF_OK_BUTTON , LPCTSTR icon = 0); // attributes public: int GetRadioButton(void) const; BOOL IsVerificationChecked(void) const; void SetFlags(TASKDIALOG_FLAGS dwFlags, TASKDIALOG_FLAGS dwExMask); void SetCommonButtons(TASKDIALOG_COMMON_BUTTON_FLAGS buttons); void SetWindowTitle(LPCTSTR title); void SetMainInstruction(LPCTSTR instruction); void SetContent(LPCTSTR content); void SetIcon(LPCTSTR icon); void SetIcon(HICON hIcon); void AddButton(UINT nButtonID, LPCTSTR pszButtonText = 0); void AddRadioButton(UINT nButtonID, LPCTSTR pszButtonText = 0); void SetDefaultButton(UINT uID); void SetDefaultRadioButton(UINT uID); void SetVerificationText(LPCTSTR verification, BOOL bChecked = FALSE); void SetFooterIcon(HICON icon); void SetFooterIcon(LPCTSTR pszIcon); void SetFooter(LPCTSTR content); // operations public: INT_PTR DoModal(CWnd* pParentWnd = CWnd::GetActiveWindow()); void ClickButton(UINT uID); void ClickRadioButton(UINT uID); void ClickVerification(BOOL bState, BOOL bFocus = FALSE); void EnableButton(UINT uID, BOOL bEnabled = TRUE); void EnableRadioButton(UINT uID, BOOL bEnabled = TRUE); void SetProgressBarMarquee(BOOL bMarquee, UINT nSpeed); void SetProgressBarPosition(UINT nPos); void SetProgressBarRange(UINT nMinRange, UINT nMaxRange); void SetProgressBarState(UINT nState); BOOL UpdateElementText(TASKDIALOG_ELEMENTS te, LPCTSTR string); void UpdateIcon(TASKDIALOG_ICON_ELEMENTS tie, LPCTSTR icon); void UpdateIcon(TASKDIALOG_ICON_ELEMENTS tie, HICON hIcon); void Navigate(const TASKDIALOGCONFIG* task_dialog); void Navigate(const CTaskDialog& task_dialog); // overrides public: virtual void OnDialogConstructed(void); virtual void OnCreated(void); virtual void OnDestroyed(void); virtual void OnRadioButtonClicked(UINT uID); virtual BOOL OnButtonClicked(UINT uID); virtual void OnVerificationClicked(BOOL bChecked); virtual void OnHyperLinkClicked(LPCWSTR wszHREF); virtual void OnHelp(void); virtual BOOL OnTimer(DWORD dwTickCount); virtual void OnNavigated(void); // implementation private: static HRESULT __stdcall TaskDialogCallbackProc(HWND hWnd, UINT uCode, WPARAM wParam, LPARAM lParam, LONG_PTR data); // data members private: int m_radiobutton; BOOL m_verification; CArray<TASKDIALOG_BUTTON> m_buttons; CArray<TASKDIALOG_BUTTON> m_radioButtons; }; #endif // _INC_AFXTASK #include "afxtask.inl"
的兴趣点 编写本文附带的代码非常有趣,也很有挑战性。我将尝试在这里介绍开发过程中发生的各种有趣的点: 最初,我用内存中的对话框模板创建任务对话框。不幸的是,我不能使它可靠地工作,而且它被证明不如在空对话框上动态创建控件灵活。无论如何,这种方法对于支持任务对话框的导航特性是必要的,因为在这种情况下,窗口句柄会在主任务对话框和被导航到的任务对话框之间回收。 因此,在运行时创建控件更加棘手,因为与使用资源编辑器设计对话框相比,您需要注意更多的事情。您必须负责设置适当的字体,以z顺序(用于tabstop顺序)定位控件,并计算它将在对话框中占用的空间。算法我使用的位置控制看起来像这样: 计算文本区段 首先,我计算对话框的最大宽度,包括一些边距。我假设主指令和按钮行应该分别显示在一行上。因此,对于初学者,我认为对话框的宽度是这些值与TASKDIALOGCONFIG结构的一个成员中指定的宽度之间的最大值。但是,我想让内容区域看起来也不错,所以我尝试在内容区域的文本执行16:9的高宽比。我通过增加对话框的宽度来达到这个比例。 一个棘手的部分是计算内容区域中文本所占的矩形。据我所知,没有执行这些计算的内置函数,尽管有一些函数可以获得单行上的文本区段和符合指定宽度的字符数。因此,我需要自己执行计算,即在单词换行边界处分割文本的每一行,并在此过程中累积行高。 好吧,事实证明要比这复杂得多,因为我想处理换行字符( ),但Windows认为换行字符根本不占用任何空间!这里是我使用的函数: 隐藏,收缩,复制Code
/// implements GetTextExtentExPoint with additionnal support for newline-characters static BOOL WINAPI GetTextExtentExPointExW(HDC hDC, LPCWSTR szText , int cchString, int nMaxExtent , LPINT lpnFit, LPINT alpDx, LPSIZE lpSize) { // first, check whether there is a newline character. // if there is one, update the character count LPWSTR _szNewLine = ::StrChr(szText, L' '); if (_szNewLine != 0) { int cch = 0; const WCHAR* _szText = szText; while (_szText <= _szNewLine) { _szText = ::CharNextW(_szText); ++cch; } cchString = min(cchString, cch); } // call the real function BOOL bResult = ::GetTextExtentExPoint(hDC, szText, cchString, nMaxExtent, lpnFit, alpDx, lpSize); // a single newline-character should take up // the same space as one line would if (_szNewLine != 0 && lpSize->cy == 0) { SIZE size; ::GetTextExtentExPoint(hDC, L"|", 1, 65536, 0, 0, &size); lpSize->cy = size.cy; } return bResult; }
前面的函数返回适合单行的正确字符数,并考虑了可能的换行字符。同时,以换行字符开始的行会被认为占用一些空间,用一个|字符来模拟。 现在,我们可以计算给定文本的文本范围,考虑到单词的换行位置。我使用以下函数: 隐藏,收缩,复制Code
/// implements GetTextExtent with additionnal support for word wrapped text static BOOL WINAPI GetTextExtentExW(HDC hDC, LPCWSTR szText, int cchString, int nMaxExtent, LPSIZE lpSize) { lpSize->cx = 0; lpSize->cy = 0; int nFit = 0; SIZE extent = { 0, 0 }; const WCHAR* _szText = szText; // calculate the number of characters that fit on a single line // taking into account potential newline characters if (::GetTextExtentExPointExW(hDC, _szText, cchString, nMaxExtent, &nFit, 0, &extent)) { // update the horizontal extent of the text lpSize->cx = min(nMaxExtent, extent.cx); // if the specified text fits on a single line, // update the vertical extent of the text if (nFit == cchString) lpSize->cy = extent.cy; // otherwise, break up each individual line and // accumulate the vertical dimensions else { _szText = GetWordWrapBoundary(_szText, &nFit); while (true) { // update the horizontal and vertical extents of the text lpSize->cx = min(nMaxExtent, max(lpSize->cx, extent.cx)); lpSize->cy += extent.cy; if ((cchString -= nFit) == 0) break; // perform the calculation for the next line // and update the number of characters to consider ::GetTextExtentExPointExW(hDC, _szText, cchString, nMaxExtent, &nFit, 0, &extent); _szText = GetWordWrapBoundary(_szText, &nFit); } } return TRUE; } return FALSE; }
前面的函数使用了一个小实用函数GetWordWrapBoundary,该函数计算单词换行边界中最后一个字符的索引。使用该函数是为了避免使用[]下标操作符,并避免对字符是否具有固定大小做出任何假设。我不知道它是否与UNICODE字符串具有复杂的脚本,但我最好是安全比抱歉: 隐藏,收缩,复制Code
static LPCWSTR WINAPI GetWordWrapBoundary(LPCWSTR szText, int* pAt) { // first go the the last specified character const WCHAR* _szAt = szText; for (int nAt = *pAt; nAt > 0; nAt--) { _szAt = ::CharNextW(_szAt); if (*_szAt == L'