|
|
12/7/2007 好久没有贴代码了,今天贴一个。目的和作用如题。
// copied from cryptuiapi.h
typedef struct _CRYPTUI_CERT_MGR_STRUCT
{
DWORD dwSize;
HWND hwndParent;
DWORD dwFlags;
LPCWSTR pwszTitle;
LPCSTR pszInitUsageOID;
} CRYPTUI_CERT_MGR_STRUCT, *PCRYPTUI_CERT_MGR_STRUCT;
typedef const CRYPTUI_CERT_MGR_STRUCT *PCCRYPTUI_CERT_MGR_STRUCT;
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR pszCmdLine, int nCmdShow)
{
HINSTANCE hCryptui = LoadLibrary(TEXT("cryptui.dll"));
if(!hCryptui)
return 0;
typedef BOOL (WINAPI* fnCryptUIDlgCertMgr)(PCCRYPTUI_CERT_MGR_STRUCT pCryptUICertMgr);
fnCryptUIDlgCertMgr CryptUIDlgCertMgr = NULL;
((FARPROC&)CryptUIDlgCertMgr) = GetProcAddress(hCryptui, TEXT("CryptUIDlgCertMgr"));
if(CryptUIDlgCertMgr)
{
CRYPTUI_CERT_MGR_STRUCT ccm = { sizeof(CRYPTUI_CERT_MGR_STRUCT) };
CryptUIDlgCertMgr(&ccm);
}
FreeLibrary(hCryptui);
return 0;
}
3/8/2007
正如 Aero 向导相比传统向导带来了更佳的用户体验那样,任务对话框则是带来了比高龄的消息框更好的用户体验。不过,任务对话框要比低级的消息框提供了多得多的一长串的特性以及可定制性。而随这些而来的则是相当深的复杂度。在此 Windows Vista for Developers 系列的这第二个部分里,我将会为你展示如何通过 C++ 有效地使用任务对话框 API 来既简单又容易地构建出形形色色的对话框。如果你现在就迫不及待,你可以跳到文章的末尾,在那儿你可以找到一份供下载的源代码,里面有对任务对话框 API 的完整 C++ 封装。
在 comctl32.dll 库的内部,隐藏着一个名为 CTaskDialog 的 C++ 类,它负责实现了任务对话框提供的所有功能。由 comctl32.dll 导出的 TaskDialog 和 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; };
正如你能想象到的,恰如其分地填充这个结构简直是个挑战,而出错的机会却相当的大。尽管其中的很多域可以置零,但为了能达到预期的效果,下列的域通常是要进行设置的:
cbSize 域在编译时指定了此结构的大小,这是一种常用技术,用以在 C 中表示数据结构的版本。它使得操作系统可以知道应用程序编译时采用了哪种版本结构,并据此来像应用程序所期望的那样来对其中的域以及功能性作一个确定的推断。
hwndParent 域保存了父窗口的句柄。这就允许结果对话框以模态窗口的方式工作,而且还可以使你对它进行相对于父窗口的定位。
hInstance 域对 C++ 开发人员有用,因为它能使你使用 ID 来指定字符串以及图标资源而不必使用代码来加载或者创建它们。
dwFlags 域保存了若干个允许你控制对话框的行为以及外观的标志。本分的后续篇幅会分别探讨这些标志。
文本标题
TASKDIALOGCONFIG 结构提供了以下在任务对话框上设置不同的文本标题的域:
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++ 来梦幻般地简化这一工作。
按钮
任务对话框支持公用按钮与自定义按钮的任意组合。当前定义的公用按钮有:
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 结构。下面是指定了两个自定义按钮的简单例子:
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 标志。
正如你看到的,这些标志仅仅影响自定义按钮。你指定的任何公用按钮仍然显示为常规按钮。
你还可以在你的按钮旁边显示那个声名狼藉的用户帐号控制(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 域里指定一个图标句柄,在这种情况下,你就要确保设置了 the 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 域中指定的一个回调函数。此回调函数的原型如下:
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(nchor)元素来定义,如下所示:
<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 类,所以你应该算是有了充足的可靠的例子。
下面是包含在本文的下载中的一个示例任务对话框的源代码:
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 基类考虑到了所有的细节。提供了设置(以及更新)不同的文本标题和图标的方法,也提供了添加按钮以及发送各种消息的方法。最后,还提供了响应通知的虚方法。
使用如上定义的任务对话框是再简单不过了:
TimerDialog dialog; Dialog.DoModal();
一旦 DoModal 方法返回,你就可以使用 GetSelectedButtonId、GetSelectedRadioButtonId 和 VerificiationChecked 方法来获取用户选定的各个按钮。
为了给你一个由 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 方法而无需根据窗口标题指定的时间或者数据来提供不同的代码。
示例
下载区中提供的本文的示例生动地演示了文章中讲到的所有特性。

这篇文章的终稿比我预计的要长一些。Windows Vista 任务对话框 API 实在是提供了太多的功能,简直使我无法适时地完成本文。这也是我所知道的关于任务对话框的唯一的完整的文档。我希望它能使许多的读者受益。
我原本计划使用托管代码来介绍任务对话框,可是 Daniel Moth 已经使用 C# 出色地完成了介绍任务对话框的工作。他还创建了一个 webcast,演示了许多创建任务对话框的方案,其中的 Task Dialog Designer 来源于我的 MSDN 杂志文章。我必须指出的是,该 webcast 中不正确地把 Kerr.Vista 配件说成是一个 COM DLL,而其实它仅仅是一个简单使用 C++/CLI 写就的 .NET 配件。
阅读第三部分:桌面窗口管理器
原文地址:http://weblogs.asp.net/kennykerr/archive/2006/07/18/Windows-Vista-for-Developers-_1320_-Part-2-_1320_-Task-Dialogs-in-Depth.aspx
3/7/2007
Aero 向导体现了向导界面自从在 Windows 95 操作系统家族中首次推广以来的演变。它们为普通的向导界面提供了崭新的外观,而且被设计为可以为用户提供更专注的体验。在本 Windows Vista for Developers 系列的这第一个部分里,我将向你展示如何你就可以使用少量的代码把一个简单的向导转换为一个 Aero 向导。
属性表
扼要地讲,向导其实只是属性表的一个变体,属性表由 PROPSHEETHEADER 结构来定义而由 PropertySheet 函数实现。ATL 提供了 CPropertySheetImpl 类模板来生成使用 PropertySheet 函数牵涉到的许多代码。我们从一个简单的属性表开始,然后再看怎样把它改变为一个 Aero 向导:
class SampleWizard : public CPropertySheetImpl<SampleWizard> { public: BEGIN_MSG_MAP(SampleWizard) CHAIN_MSG_MAP(__super) END_MSG_MAP() SampleWizard() : CPropertySheetImpl<SampleWizard>(IDS_TITLE) { VERIFY(AddPage(m_page)); } private: SamplePage m_page; };
SampleWizard 类派生于上述的 CPropertySheetImpl 类模板,该类模板提供了属性表的大部分功能。消息映射简单地把消息导向基类。构造函数调用了基类的构造函数以设置向导的标题,并使用继承自 CPropertySheetImpl 的 AddPage 方法向向导中添加了一个页面. SamplePage 类具有如下定义:
class SamplePage : public CPropertyPageImpl<SamplePage> { public: BEGIN_MSG_MAP(SamplePage) CHAIN_MSG_MAP(__super) END_MSG_MAP() enum { IDD = IDD_SAMPLE_PAGE }; };
SamplePage 类派生于 CPropertyPageImpl 类模板,该类模板提供了属性页的大部分功能。它也有一个简单的消息映射把消息都导向基类。一个 enum 定义了基类中所期望的 IDD 常量,用以标识属性页用到的对话框资源。
现在,你就可以用下列简单代码创建并显示一个模态属性表了:
SampleWizard sampleWizard; sampleWizard.DoModal();
第一行创建了 SampleWizard 对象,其中,各个基类会负责构造调用 PropertySheet 函数所需的结构。SampleWizard 继承自 CPropertySheetImpl 类模板的 DoModal 方法则负责调用 PropertySheet 函数。其结果就是一个简单的属性表:
经典向导
要把属性表变为一个经典的向导,你所需的全部工作就是把下列语句添加到 SampleWizard 的构造函数中:
m_psh.dwFlags |= PSH_WIZARD97;
它所做的就是把 PSH_WIZARD97 标志合并到已经在 PROPSHEETHEADER 结构里已经设置过的标志中。由于向导提供了一个额外的顶头区域,所以还要更新 SamplePage 类来指定此页的标头文字:
class SamplePage : public CPropertyPageImpl<SamplePage> { public: BEGIN_MSG_MAP(SamplePage) CHAIN_MSG_MAP(__super) END_MSG_MAP() enum { IDD = IDD_BLANK_PAGE }; SamplePage() { VERIFY(m_title.LoadString(IDS_PAGE_TITLE)); SetHeaderTitle(m_title); } private: CString m_title;
};
SamplePage 的构造函数使用继承于 CPropertyPageImpl 的 SetHeaderTitle 方法来设置页面的标头标题。要记住的是在页面的 PROPSHEETPAGE 结构里存储的是一个字符串指针,因此,字符串的生命期必须要超出构造函数。
经过这些适当的小改动,结果发生了相当引人注目的不同:
正如你可能注意到的,先前出现在标签上的文字已经把窗口标题替换了。
Aero 向导
现在,将 SampleWizard 构造函数中的 PSH_WIZARD97 替换为 PSH_AEROWIZARD,然后你就得到了新式的 Aero 向导界面:
你应该注意到窗口标题又变了回来而原来出现在属性表标签上的对话框标题已经不再使用了。
新消息
Aero 向导支持几个新的消息,用以更好地控制由向导提供的标准控件。
PSM_SHOWWIZBUTTONS 消息显示或者隐藏修饰向导的任意标准按钮。PropSheet_ShowWizButtons 宏可以简化此消息的发送。使用它并不是特别的自然,不过一旦掌握了它,你就不该再有任何问题了。尽管只是一个宏,不过把它想象为一个如下定义的函数更有用(宏易于误解):
void PropSheet_ShowWizButtons(HWND handle, DWORD buttons, DWORD mask);
handle 参数标识了向导窗口,buttons 参数指示要显示哪些按钮,而 mask 参数指示要操作哪些按钮。如果一个按钮标志同时出现于 buttons 和 masks 参数中则会被显示。如果一个按钮仅出现于 mask 参数中则会被隐藏。共有下列按钮标志可以使用:
PSWIZB_BACK PSWIZB_NEXT PSWIZB_FINISH PSWIZB_CANCEL
例如,下面的调用将显示 Next 按钮而隐藏 Back 按钮:
PropSheet_ShowWizButtons(handle, PSWIZB_NEXT, PSWIZB_BACK | PSWIZB_NEXT);
PSM_ENABLEWIZBUTTONS 消息启用或者禁用任意标准按钮。PropSheet_EnableWizButtons 宏简化了此消息的发送:
void PropSheet_EnableWizButtons(HWND handle, DWORD buttons, DWORD mask);
在如何标识按钮上此消息与 PSM_SHOWWIZBUTTONS 的工作方式是一致的。buttons 参数指示那些按钮要启用或者禁用而 mask 参数指示要操作哪些按钮。例如,下列调用将启用 Next 按钮并禁用 Back 按钮。
PropSheet_EnableWizButtons(handle, PSWIZB_NEXT, PSWIZB_BACK | PSWIZB_NEXT);
PSM_SETBUTTONTEXT 消息可以修改 Next、Finish 以及 Cancel 按钮的文字。PropSheet_SetButtonText 宏简化了此消息的发送:
void PropSheet_SetButtonText(HWND handle, DWORD button, PCWSTR text);
示例
为了让你能简单地试验 Aero 向导提供的各种选项,我创建了一个简单的向导工程你可以下载并用它进行实践:
阅读第二部分:深入任务对话框
原文地址:http://weblogs.asp.net/kennykerr/archive/2006/07/12/Windows-Vista-for-Developers-_1320_-Part-1-_1320_-Aero-Wizards.aspx
1/30/2007 微软为编制 Windows Live Messenger 的插件提供了良好的支持。要使插件可以工作,需要以下三个步骤:
1、开启 Windows Live Messenger 的插件功能 2、编制插件 3、注册插件
1、开启 Windows Live Messenger 的插件功能。 这个非常简单,打开注册表编辑器,展开 HKEY_CURRENT_USER\Software\Microsoft\MSNMessenger,查看 AddInFeatureEnabled 键值是否存在,不在则创建之,数据类型为 DWORD。将键值置为 1。然后重新启动 MSN Live Messenger。
2、编制插件 用 C# 或者任意其他的 .NET 语言新建工程,向工程中添加对 MessengerClient.dll 的引用。为了避免总是使用完整规格的名称,你可以添加 using Microsoft.Messenger 语句。 接下来创建一个类,名字随便,此处以 AddIn 代之。该类需要实现一个名为 IMessengerAddIn 的接口。该接口中的 Initialize 方法需要实现,因为我们仅是要实现自动回复,仅需如下这样注册 IncomingTextMessage 事件: Client.IncomingTextMessage += new EventHandler<IncomingTextMessageEventArgs>(Client_IncomingTextMessage); 然后再实现这个事件处理器: public void Client_IncomingTextMessage(Object Sender, IncomingTextMessageEventArgs e) { User userFrom = e.UserFrom; Client.SendNudgeMessage(userFrom); Client.SendTextMessage("Hey, I m busy. Please leave your message.", userFrom); } 编译得到 DLL,DLL 的默认名字为 名字空间.DLL,把它改为 名字空间.类名.DLL。而且,类名要注意大小写。
3、注册插件 打开 Windows Live Messenger,打开选项对话框,左侧列表的最下方就会多出一项“加载项”,选中它,然后点击“添加至 Messenger”按钮,找到你上面生成的 DLL 即可。
4、友情提示 不工作不要找我。
12/19/2006 //
// Integrated browser mode - package up a bunch of data into a COPYDATASTRUCT,
// and send it to the desktop window via SendMessage(WM_COPYDATA).
//
void LaunchInternetExplorerWithoutProcess()
{
#define MAX_IEEVENTNAME 32
// First piece of data is a wide string version of the command line.
WCHAR wsz[MAX_IEEVENTNAME] = L"";
COPYDATASTRUCT cds = { SW_NORMAL, sizeof(WCHAR), wsz };
// Second piece of data is the event to fire when the browser window reaches WM_CREATE.
static DWORD dwNextId = 0;
TCHAR szEvent[MAX_IEEVENTNAME + 1];
wsprintf(szEvent, TEXT("IE-%08X-%08X"), GetCurrentThreadId(), dwNextId++);
HANDLE hEventReady = CreateEvent(NULL, FALSE, FALSE, szEvent);
if(hEventReady)
{
// Put the (UNICODE) event name at the end of the cds data
LPWSTR pwszEvent = &wsz[1];
#ifdef UNICODE
lstrcpy(pwszEvent, szEvent);
#else
MultiByteToWideChar(CP_ACP, 0, szEvent, -1, pwszEvent, sizeof(szEvent) / sizeof(szEvent[0]));
#endif
cds.cbData += (lstrlenW(pwszEvent) + 1) * sizeof(WCHAR);
// Send the message
HWND hwndDesktop = GetShellWindow();
int iRet = (int)SendMessage(hwndDesktop, WM_COPYDATA, (WPARAM)hwndDesktop, (LPARAM)&cds);
if(iRet)
{
// Wait for the browser window to hit WM_CREATE.
// When this happens, all DDE servers will have been registered.
DWORD dwRet = WaitForSingleObject(hEventReady, 1000 * 10); // 10 seconds
#ifdef ASSERT
ASSERT(dwRet == WAIT_OBJECT_0);
#endif // ASSERT
}
CloseHandle(hEventReady);
}
#undef MAX_IEEVENTNAME
}
11/19/2006 在很多情况下,我们需要去掉 Windows 窗口的默认标题栏和边框,但又希望能够像原来一样对窗口进行正常的改变位置或者大小的操作。许多程序员采用响应 WM_LBUTTONDOWN/WM_LBUTTONUP 消息,鼠标拖动时自己绘制拖动框(在系统的“拖动时显示窗口内容”选项关闭的情况下)或者不停调用 MoveWindow()/SetWindowPos() API 对窗口的位置或者大小进行更新(在系统的“拖动时显示窗口内容”选项开启的情况下)。这样做虽然可以达到效果,但是相较老汉下面将要描述的方法,稍微麻烦了一些,而且出于用户体验的完整性考虑,需要自己检测拖动过程中用户是否按下了 Esc 键,或者是正在拖动的窗口突然被其他窗口夺取了焦点等等小概率事件。
老汉介绍的方法,整个拖动操作仍然使用系统自身的功能,所以不存在上述问题。其核心思想就是改变系统在对窗口进行点击测试是返回的值,对系统进行一定程度上的欺骗,使其认为鼠标依旧是点击到了默认的标题栏或者边框,从而执行相应的动作。
下面是示例代码。使用向导生成一个 MFC 对话框应用,假定对话框类的名字为 CMvszDlg,向该类添加对 WM_PAINT 和 WM_NCHITTEST 消息的处理函数(默认在 ClassWizard 中是看不到 WM_NCHITTEST 消息的,需要切换到 ClassWizard 的“Class Info”标签,将“Message filter”选择为“Window”)。然后把下面的代码分别复制到对应的消息处理函数中,编译运行即可看到效果。其中,OnPaint() 的实现是为了演示目的,在真实的应用中,显然可以使用更漂亮的效果。
#define BORDER_SIZE 4
#define CAPTION_SIZE 24
void CMvszDlg::OnPaint()
{
Invalidate();
CPaintDC dc(this);
CRect rc;
GetClientRect(&rc);
CBrush* pbr = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pbr);
dc.Rectangle(&rc);
dc.MoveTo(0, BORDER_SIZE + CAPTION_SIZE);
dc.LineTo(rc.right, BORDER_SIZE + CAPTION_SIZE);
dc.MoveTo(0, rc.bottom - (BORDER_SIZE + CAPTION_SIZE));
dc.LineTo(BORDER_SIZE, rc.bottom - (BORDER_SIZE + CAPTION_SIZE));
dc.MoveTo(rc.right - BORDER_SIZE, rc.bottom - (BORDER_SIZE + CAPTION_SIZE));
dc.LineTo(rc.right, rc.bottom - (BORDER_SIZE + CAPTION_SIZE));
dc.MoveTo(BORDER_SIZE + CAPTION_SIZE, 0);
dc.LineTo(BORDER_SIZE + CAPTION_SIZE, BORDER_SIZE);
dc.MoveTo(BORDER_SIZE + CAPTION_SIZE, rc.bottom - BORDER_SIZE);
dc.LineTo(BORDER_SIZE + CAPTION_SIZE, rc.bottom);
dc.MoveTo(rc.right - (BORDER_SIZE + CAPTION_SIZE), 0);
dc.LineTo(rc.right - (BORDER_SIZE + CAPTION_SIZE), BORDER_SIZE);
dc.MoveTo(rc.right - (BORDER_SIZE + CAPTION_SIZE), rc.bottom - BORDER_SIZE);
dc.LineTo(rc.right - (BORDER_SIZE + CAPTION_SIZE), rc.bottom);
rc.DeflateRect(BORDER_SIZE, BORDER_SIZE);
dc.Rectangle(&rc);
}
UINT CMvszDlg::OnNcHitTest(CPoint point)
{
ScreenToClient(&point);
CRect rc;
GetClientRect(&rc);
CRect rcTest = rc;
rcTest.DeflateRect(BORDER_SIZE, BORDER_SIZE);
int iBottom = rcTest.bottom;
rcTest.bottom = rcTest.top + CAPTION_SIZE;
if(rcTest.PtInRect(point))
return HTCAPTION;
rcTest.top = rcTest.bottom;
rcTest.bottom = iBottom;
if(rcTest.PtInRect(point))
return HTCLIENT;
rcTest.SetRect(0, 0, BORDER_SIZE + CAPTION_SIZE, BORDER_SIZE + CAPTION_SIZE);
if(rcTest.PtInRect(point))
return HTTOPLEFT;
rcTest.SetRect(BORDER_SIZE + CAPTION_SIZE, 0, rc.right - (BORDER_SIZE + CAPTION_SIZE), BORDER_SIZE + CAPTION_SIZE);
if(rcTest.PtInRect(point))
return HTTOP;
rcTest.SetRect(rc.right - (BORDER_SIZE + CAPTION_SIZE), 0, rc.right, BORDER_SIZE + CAPTION_SIZE);
if(rcTest.PtInRect(point))
return HTTOPRIGHT;
rcTest.SetRect(0, BORDER_SIZE + CAPTION_SIZE, BORDER_SIZE, rc.bottom - (BORDER_SIZE + CAPTION_SIZE));
if(rcTest.PtInRect(point))
return HTLEFT;
rcTest.SetRect(rc.right - BORDER_SIZE, BORDER_SIZE + CAPTION_SIZE, rc.right, rc.bottom - (BORDER_SIZE + CAPTION_SIZE));
if(rcTest.PtInRect(point))
return HTRIGHT;
rcTest.SetRect(0, rc.bottom - (BORDER_SIZE + CAPTION_SIZE), BORDER_SIZE + CAPTION_SIZE, rc.bottom);
if(rcTest.PtInRect(point))
return HTBOTTOMLEFT;
rcTest.SetRect(BORDER_SIZE + CAPTION_SIZE, rc.bottom - BORDER_SIZE, rc.right - (BORDER_SIZE + CAPTION_SIZE), rc.bottom);
if(rcTest.PtInRect(point))
return HTBOTTOM;
rcTest.SetRect(rc.right - (BORDER_SIZE + CAPTION_SIZE), rc.bottom - (BORDER_SIZE + CAPTION_SIZE), rc.right, rc.bottom);
if(rcTest.PtInRect(point))
return HTBOTTOMRIGHT;
return HTERROR;
}
10/21/2006 几天前在 CSDN 上有人问及一个问题:如何使得一个半透明窗口上,控件所占据的位置是不透明的?
开始有人提到使用 SetLayeredWindowAttributes() API,但考虑到该 API 会将整个窗口设置为一致的透明度,显然不可能满足问题中的要求。所以老汉认为应该使用 UpdateLayeredWindow() API 来完成这一工作,为了不重复输入,把我当时的回复抄录如下:“
真正可以使用的 API 是 UpdateLayeredWindow()。此函数可以根据一幅选入到 DC 中去的 32 位位图的每个像素的 Alpha 通道值设置窗口上对应像素的半透明度。其实现在楼主要做得就是,总是生成一幅和窗口大小一样的 32 位位图,把控件占据的区域的位图像素的 Alpha 通道值全部设置为 255,即不透明,而其余地方的像素则可以根据需要设置为适当的半透明,然后再调用本函数即可。需要注意的是,如果窗口的大小可以改变的话,显然每次都需要动态生成此位图,并调用本函数对窗口进行更新。
”。为了验证这一想法,我特意完成了一个类 CWindowUpdater,代码附后。使用者仅需在顶级窗口的 WM_SIZE 消息的响应中调用 CWindowUpdater::Update(hwnd) 即可。
需要说明的是,这种方法虽然在视觉上解决了上述问题,但是带来了另外一个问题。经过 UpdateLayeredWindow() 作用的窗口,其内容会因此而变成一幅静态图像,所以后果会成为,虽然控件是不透明的,但是用户与控件交互(例如鼠标点击,键盘输入等)时原本应该有的视觉反馈(例如按钮成为下压状态)却全部不可见了,这使得这种方法基本上是鸡肋,并没有真正意义上的实用价值。如果有哪位实现了更完美的解决方案,望不吝赐教。不过,它至少在技术上演示了如何设置窗口上每个像素的透明度。
class CWindowUpdater
{
typedef BOOL (WINAPI *fnUpdateLayeredWindow)(HWND hWnd,
HDC hdcDst, LPPOINT pptDst, LPSIZE psize,
HDC hdcSrc, LPPOINT pptSrc, COLORREF crKey, PBLENDFUNCTION pblend, DWORD dwFlags);
static int BytesPerLine(int iWidth, int iBitsPerPixel)
{
return ((iWidth * iBitsPerPixel + 31) & (~31)) >> 3;
}
public:
static BOOL Update(HWND hwnd)
{
static fnUpdateLayeredWindow _fnUpdateLayeredWindow = (fnUpdateLayeredWindow)-1;
if(_fnUpdateLayeredWindow == (fnUpdateLayeredWindow)-1)
{
(FARPROC&)_fnUpdateLayeredWindow = GetProcAddress(GetModuleHandle(_T("USER32.DLL")),
"UpdateLayeredWindow");
}
if(!_fnUpdateLayeredWindow)
return FALSE;
RECT rc;
if(!GetWindowRect(hwnd, &rc))
return FALSE;
SIZE size = { rc.right - rc.left, rc.bottom - rc.top };
HDC hdcMem = CreateCompatibleDC(NULL);
if(!hdcMem)
return FALSE;
// 1. create a 32-bit memory bitmap according to the window's size
BITMAPINFOHEADER bmih = { 0 };
// Populate BITMAPINFO header
bmih.biSize = sizeof(BITMAPINFOHEADER);
bmih.biWidth = size.cx;
bmih.biHeight = size.cy;
bmih.biPlanes = 1;
bmih.biBitCount = 32;
bmih.biCompression = BI_RGB;
bmih.biClrUsed = 0;
bmih.biSizeImage = BytesPerLine(size.cx, 32) * size.cy;
PVOID pvBits = NULL;
HBITMAP hbmpMem = CreateDIBSection(NULL, (PBITMAPINFO)&bmih, DIB_RGB_COLORS, &pvBits, NULL, 0);
if(hbmpMem)
{
HGDIOBJ hbmpOld = SelectObject(hdcMem, hbmpMem);
// 2. get window's content
SendMessage(hwnd, WM_PRINT, (WPARAM)hdcMem, (LPARAM)PRF_NONCLIENT | PRF_CLIENT | PRF_CHILDREN | PRF_CHECKVISIBLE);
// 3. get rid of the areas occupied by children
// SetWindowTransparency(hwnd, hdcMem, hbmpMem, 128, FALSE);
SetWindowTransparency(hwnd, pvBits, size.cx, size.cy, 128, FALSE);
// 4. pre-multiply rgb channels with alpha channel
// PreMultiplyRGBChannels(hdcMem, hbmpMem);
PreMultiplyRGBChannels(pvBits, size.cx, size.cy);
// 5. ensure the WS_EX_LAYERED extended style is there
SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE) | 0x00080000); // WS_EX_LAYERED
// 6. update the window
POINT ptSrc = { 0, 0 }; // start point of the copy from memory DC to screen DC
BLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
_fnUpdateLayeredWindow(hwnd, NULL, (LPPOINT)&rc, &size, hdcMem, &ptSrc, 0, &bf, 0x00000002); // ULW_ALPHA
// clean up
SelectObject(hdcMem, hbmpOld);
DeleteObject(hbmpMem);
}
DeleteDC(hdcMem);
return TRUE;
}
static BOOL PreMultiplyRGBChannels(HDC hdc, HBITMAP hbmp)
{
if(!hdc || !hbmp)
return FALSE;
BITMAPINFO bmi = { 0 };
bmi.bmiHeader.biSize = sizeof(bmi);
if(!GetDIBits(hdc, hbmp, 0, 1, 0, &bmi, DIB_RGB_COLORS) || bmi.bmiHeader.biBitCount != 32)
return FALSE;
PBYTE pBuff = new BYTE[bmi.bmiHeader.biSizeImage + 0x20];
if(!pBuff)
return FALSE;
BOOL bRet = FALSE;
bmi.bmiHeader.biSize = sizeof(bmi);
bmi.bmiHeader.biCompression = BI_RGB;
if(GetDIBits(hdc, hbmp, 0, bmi.bmiHeader.biHeight, (PVOID)pBuff, &bmi, DIB_RGB_COLORS))
{
PreMultiplyRGBChannels(pBuff, bmi.bmiHeader.biWidth, bmi.bmiHeader.biHeight);
bRet = SetDIBits(hdc, hbmp, 0, bmi.bmiHeader.biHeight, (PVOID)pBuff, &bmi, DIB_RGB_COLORS);
}
delete[] pBuff;
return bRet;
}
static BOOL PreMultiplyRGBChannels(PVOID pvBits, int cx, int cy)
{
if(!pvBits || cx <= 0 || cy <= 0)
return FALSE;
// pre-multiply rgb channels with alpha channel
for(int y=0; y<cy; y++)
{
PBYTE pPixel = ((PBYTE)pvBits) + cx * 4 * y;
for(int x=0; x<cx; x++)
{
pPixel[0] = pPixel[0] * pPixel[3] / 255;
pPixel[1] = pPixel[1] * pPixel[3] / 255;
pPixel[2] = pPixel[2] * pPixel[3] / 255;
pPixel += 4;
}
}
return TRUE;
}
static BOOL SetWindowTransparency(HWND hwnd, HDC hdc, HBITMAP hbmp, int iAlpha, BOOL bExcludeChildren)
{
if(!hwnd || !IsWindow(hwnd) || !hdc || !hbmp || iAlpha < 0 || iAlpha > 255)
return FALSE;
BITMAPINFO bmi = { 0 };
bmi.bmiHeader.biSize = sizeof(bmi);
if(!GetDIBits(hdc, hbmp, 0, 1, 0, &bmi, DIB_RGB_COLORS) || bmi.bmiHeader.biBitCount != 32)
return FALSE;
PBYTE pBuff = new BYTE[bmi.bmiHeader.biSizeImage + 0x20];
if(!pBuff)
return FALSE;
BOOL bRet = FALSE;
bmi.bmiHeader.biSize = sizeof(bmi);
bmi.bmiHeader.biCompression = BI_RGB;
if(GetDIBits(hdc, hbmp, 0, bmi.bmiHeader.biHeight, (PVOID)pBuff, &bmi, DIB_RGB_COLORS))
{
SetWindowTransparency(hwnd, pBuff, bmi.bmiHeader.biWidth, bmi.bmiHeader.biHeight, iAlpha, bExcludeChildren);
bRet = SetDIBits(hdc, hbmp, 0, bmi.bmiHeader.biHeight, (PVOID)pBuff, &bmi, DIB_RGB_COLORS);
}
delete[] pBuff;
return bRet;
}
static BOOL SetWindowTransparency(HWND hwnd, PVOID pvBits, int cx, int cy, int iAlpha, BOOL bExcludeChildren)
{
if(!hwnd || !IsWindow(hwnd) || !pvBits || cx <= 0 || cy <= 0 || iAlpha < 0 || iAlpha > 255)
return FALSE;
RECT rcWindow;
GetWindowRect(hwnd, &rcWindow);
MapWindowPoints(HWND_DESKTOP, hwnd, (LPPOINT)&rcWindow, 2);
// 1. get all children
HRGN hrgnChildren = CreateRectRgn(0, 0, 0, 0);
RECT rc;
HWND hwndChild = GetWindow(hwnd, GW_CHILD);
while(hwndChild)
{
GetWindowRect(hwndChild, &rc); // in screen coord'
MapWindowPoints(HWND_DESKTOP, hwnd, (LPPOINT)&rc, 2); // in client coord'
OffsetRect(&rc, -rcWindow.left, -rcWindow.top); // in window coord'
HRGN hrgnChild = CreateRectRgnIndirect(&rc);
CombineRgn(hrgnChildren, hrgnChildren, hrgnChild, RGN_OR);
DeleteObject(hrgnChild);
hwndChild = GetWindow(hwndChild, GW_HWNDNEXT);
}
for(int y=0; y<cy; y++)
{
PBYTE pPixel = ((PBYTE)pvBits) + cx * 4 * y;
for(int x=0; x<cx; x++)
{
if(PtInRegion(hrgnChildren, x, cy - y))
pPixel[3] = 255;
else
pPixel[3] = iAlpha;
pPixel += 4;
}
}
DeleteObject(hrgnChildren);
return TRUE;
}
};
8/3/2006
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分
第十部分 - 实现一个拖放源
内容
其他 UI Goodies
版权和许可
修订历史
简介
拖放是许多流行应用的特性之一。尽管实现一个放下目标相当简单,但拖动源却要复杂的多。MFC 中有两个类 COleDataObject 和 COleDropSource 可以帮助管理拖动源所必须提供的数据,但 WTL 中没有这种辅助类。对于我们这些 WTL 用户来说,幸运的是,Raymond Chen 在 2000 年的时候在 MSDN 上写过一篇文章(“The Shell Drag/Drop Helper Object Part 2”),其中有 IDataObject 的纯 C++ 实现,这对于为 WTL 应用编制一个完整的拖放源提供了巨大的帮助。
本文的示例工程是一个 CAB 文件查看器,可以使你从 CAB 中提取文件,只要把它们从查看器里拖到资源浏览器窗口中即可。本文还会讨论几个新的框架窗口话题,例如对文件打开的处理以及类似于 MFC 的文档/视图框架的数据管理。我还会演示 WTL 的 MRU(most-recently-used,最近使用)文件列表类,以及第六版的列表视图控件的几个新的 UI 特性。
重要提示:你需要从微软下载并安装 CAB SDK 来编译示例代码。在 KB 文章 Q310618 中有此 SDK 的链接。示例工程假定 SDK 位于和源代码相同的路径下的名为“cabsdk”的目录中。
记住,如果你在安装 WTL 或者编译示例代码时遇到了问题,请在这儿提问之前先阅读第一部分的 readme 一节。
开始工程
要开始我们的 CAB 查看器应用,需要运行 WTL AppWizard 并创建一个名为 WTLCabView 的工程。它应该是一个 SDI 应用,所以在第一页中选择 SDI Application:

在下一页里,去掉 Command Bar,并把 View Type 改为 List View。向导会为我们的视图窗口创建一个派生自 CListViewCtrl 的 C++ 类。

视图窗口类看起来就是这样: class CWTLCabViewView :
public CWindowImpl<CWTLCabViewView, CListViewCtrl>
{
public:
DECLARE_WND_SUPERCLASS(NULL, CListViewCtrl::GetWndClassName())
// Construction
CWTLCabViewView();
// Maps
BEGIN_MSG_MAP(CWTLCabViewView)
END_MSG_MAP()
// ...
};
就像我们在第二部分里使用视图类一样,我们可以使用 CWindowImpl 的第三个模板参数设置缺省的窗口风格: #define VIEW_STYLES \
(LVS_REPORT | LVS_SHOWSELALWAYS | \
LVS_SHAREIMAGELISTS | LVS_AUTOARRANGE )
#define VIEW_EX_STYLES (WS_EX_CLIENTEDGE)
class CWTLCabViewView :
public CWindowImpl<CWTLCabViewView, CListViewCtrl,
CWinTraitsOR<VIEW_STYLES,VIEW_EX_STYLES> >
{
//...
};
由于在 WTL 中没有文档/视图框架,视图类需要做双份的工作,既是 UI,也是存放有关 CAB 信息的地方。在拖放操作中传递的数据结构为 CDraggedFileInfo: struct CDraggedFileInfo
{
// Data set at the beginning of a drag/drop:
CString sFilename; // name of the file as stored in the CAB
CString sTempFilePath; // path to the file we extract from the CAB
int nListIdx; // index of this item in the list ctrl
// Data set while extracting files:
bool bPartialFile; // true if this file is continued in another cab
CString sCabName; // name of the CAB file
bool bCabMissing; // true if the file is partially in this cab and
// the CAB it's continued in isn't found, meaning
// the file can't be extracted
CDraggedFileInfo ( const CString& s, int n ) :
sFilename(s), nListIdx(n), bPartialFile(false),
bCabMissing(false)
{ }
};
视图类中还有如下方法:初始化、管理文件列表,以及在拖放操作开始的时候准备一个 CDraggedFileInfo 列表。由于本文是讲关于拖放的,所以我不会深入到 UI 工作的内部去,需要了解所有细节的话可以检视示例工程中的 WTLCabViewView.h。
文件打开处理
要查看一个 CAB 文件,用户可以使用 File-Open 命令并选择一个 CAB 文件。向导为 CMainFrame 生成的代码包含了 File-Open 菜单项的一个处理器: BEGIN_MSG_MAP(CMainFrame)
COMMAND_ID_HANDLER_EX(ID_FILE_OPEN, OnFileOpen)
END_MSG_MAP()
OnFileOpen() 使用了 CMyFileDialog 类,该类是在第九部分里介绍到的 WTL 的 CFileDialog 的增强版本,用以显示一个标准的文件打开对话框。
void CMainFrame::OnFileOpen (
UINT uCode, int nID, HWND hwndCtrl )
{
CMyFileDialog dlg ( true, _T("cab"), 0U,
OFN_HIDEREADONLY|OFN_FILEMUSTEXIST,
IDS_OPENFILE_FILTER, *this );
if ( IDOK == dlg.DoModal(*this) )
ViewCab ( dlg.m_szFileName );
}
OnFileOpen() 调用了辅助函数 ViewCab():
void CMainFrame::ViewCab ( LPCTSTR szCabFilename )
{
if ( EnumCabContents ( szCabFilename ) )
m_sCurrentCabFilePath = szCabFilename;
}
EnumCabContents() 相当的复杂,它使用 CAB SDK 调用来枚举在 OnFileOpen() 中选中的文件的内容,并填充视图窗口。不过 ViewCab() 现在还不完善,我们后面会给它加入支持 MRU 列表的代码。下面是查看器的样子,其中显示着 Windows 98 的 某个 CAB 文件的内容:

EnumCabContents() 使用了视图类中的两个方法来填充 UI:AddFile() 和 AddPartialFile()。AddPartialFile() 在当一个文件被部分存储在 CAB 中时被调用,因为其头部是在前面的 CAB 里。在上面的截图里,列表中的第一个文件就是一个部分文件。其余的文件是通过 AddFile() 添加的。这两个方法都会为要添加的文件分配一个数据结构,从而视图可以知道其显示的每个文件的所有相关细节。
如果 EnumCabContents() 返回真,则代表所有的枚举以及 UI 设置工作成功完成。如果我们只是写一个简单的 CAB 查看器,我们就可以收手了,尽管此应用不那么有趣。为了使它真正地有用,我们将对它添加拖放支持,以使用户可以从 CAB 中提取文件。
拖动源
拖放源是一个 COM 对象,它实现了两个接口:IDataObject 和 IDropSource。IDataObject 用来存放在拖放操作中客户端需要传递的任意数据,在我们这种情况下,此数据应该是一个 HDROP 结构,其中列出了要从 CAB 中提取的文件。IDropSource 的方法会由 OLE 调用,用以在拖放操作中向源通知事件。
拖动源接口
实现了我们的拖放源的 C++ 类为 CDragDropSource。该类以我在简介中提到过的 MSDN 文章中的 IDataObject 实现为起始。在该文中你可以找到所有代码相关的细节,因此我在这儿就不重复了。然后我们再向类中加入 IDropSource 及其两个方法: class CDragDropSource :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDragDropSource>,
public IDataObject,
public IDropSource
{
public:
// Construction
CDragDropSource();
// Maps
BEGIN_COM_MAP(CDragDropSource)
COM_INTERFACE_ENTRY(IDataObject)
COM_INTERFACE_ENTRY(IDropSource)
END_COM_MAP()
// IDataObject methods not shown...
// IDropSource
STDMETHODIMP QueryContinueDrag (
BOOL fEscapePressed, DWORD grfKeyState );
STDMETHODIMP GiveFeedback ( DWORD dwEffect );
};
用于调用者的辅助方法
CDragDropSource 使用几个辅助方法封装了 IDataObject 的管理以及拖放的通信。一个拖放操作遵循以下模式:
- 主框架得到用户开始拖放操作的通知。
- 主框架调用视图窗口来创建一个被拖动的文件的列表。视图在一个
vector<CDraggedFileInfo> 向量中返回此信息。
- 主框架创建一个
CDragDropSource 对象并将上述向量传递给它,以使它得知要从 CAB 中提取哪些文件。
- 主框架开始拖放操作。
- 如果用户在一个适当的拖放目标上放下,则
CDragDropSource 对象提取文件。
- 主框架更新 UI 以标示不能被提取的文件。
步骤 3 到 6 由辅助方法处理。初始化在 Init() 方法中完成: bool Init(LPCTSTR szCabFilePath, vector<CDraggedFileInfo>& vec);
Init() 将数据复制到保护成员中,填入到一个 HDROP 结构,并使用 IDataObject 方法将该结构保存到数据对象中。Init() 还作了另一个重要的步骤:它在 TEMP 目录下为每个拖动的文件创建了一个零字节的文件。例如,如果用户从 CAB 文件中拖动 buffy.txt 和 willow.txt,Init() 将在 TEMP 目录下使用这两个名字创建两个文件。这是为了预防,万一拖放目标要验证从 HDROP 读入的文件名,如果文件不存在,则目标有可能会拒绝放下。
接下来的方法是 DoDragDrop(): HRESULT DoDragDrop(DWORD dwOKEffects, DWORD* pdwEffect);
DoDragDrop() 接受 dwOKEffects 中的一组 DROPEFFECT_* 标志,这些标志表明了源所允许的那些动作。它会查询必要的接口,然后调用 DoDragDrop() API。如果拖放成功,*pdwEffect 就被设置为用户希望执行的 DROPEFFECT_* 值。
最后一个方法是 GetDragResults(): const vector<CDraggedFileInfo>& GetDragResults();
CDragDropSource 对象维护的 vector<CDraggedFileInfo> 会在拖放操作过程中被更新。如果某个文件被发现还连着另一个 CAB,或者是不能被提取,则 CDraggedFileInfo 结构会被执行必要的更新。主框架调用 GetDragResults() 来获取此向量,查找错误并相应更新 UI。
IDropSource 的方法
第一个 IDropSource 方法是 GiveFeedback(),它通知源,用户是想采取哪种操作(移动、复制或者链接)。如果愿意的话源可以改变光标。CDragDropSource 对操作保持了跟踪,并告诉 OLE 要使用缺省的拖放光标。 STDMETHODIMP CDragDropSource::GiveFeedback(DWORD dwEffect)
{
m_dwLastEffect = dwEffect;
return DRAGDROP_S_USEDEFAULTCURSORS;
}
另一个 IDropSource 方法是 QueryContinueDrag()。OLE 在用户把光标移来移去时调用此方法,并告诉源哪个鼠标键,以及键盘键,被按下了。下边是大多数 QueryContinueDrag() 的实现所采用的样板代码: STDMETHODIMP CDragDropSource::QueryContinueDrag (
BOOL fEscapePressed, DWORD grfKeyState )
{
// If ESC was pressed, cancel the drag.
// If the left button was released, do drop processing.
if ( fEscapePressed )
return DRAGDROP_S_CANCEL;
else if ( !(grfKeyState & MK_LBUTTON) )
{
// If the last DROPEFFECT we got in GiveFeedback()
// was DROPEFFECT_NONE, we abort because the allowable
// effects of the source and target don't match up.
if ( DROPEFFECT_NONE == m_dwLastEffect )
return DRAGDROP_S_CANCEL;
// TODO: Extract files from the CAB here...
return DRAGDROP_S_DROP;
}
else
return S_OK;
}
当我们发现左键被释放了,就到了我们要从 CAB 中提取选中的文件的地方了。 STDMETHODIMP CDragDropSource::QueryContinueDrag (
BOOL fEscapePressed, DWORD grfKeyState )
{
// If ESC was pressed, cancel the drag.
// If the left button was released, do the drop.
if ( fEscapePressed )
return DRAGDROP_S_CANCEL;
else if ( !(grfKeyState & MK_LBUTTON) )
{
// If the last DROPEFFECT we got in GiveFeedback()
// was DROPEFFECT_NONE, we abort because the allowable
// effects of the source and target don't match up.
if ( DROPEFFECT_NONE == m_dwLastEffect )
return DRAGDROP_S_CANCEL;
// If the drop was accepted, do the extracting here,
// so that when we return, the files are in the temp dir
// and ready for Explorer to copy.
if ( ExtractFilesFromCab() )
return DRAGDROP_S_DROP;
else
return E_UNEXPECTED;
}
else
return S_OK;
}
CDragDropSource::ExtractFilesFromCab() 是另一个复杂点的代码,它使用 CAB SDK 把文件提取到 TEMP 目录下,覆盖掉我们先前创建的零字节的文件。当 QueryContinueDrag() 返回 DRAGDROP_S_DROP 时,也即告诉了 OLE 完成此拖放操作。如果拖放目标是一个资源浏览器窗口,资源浏览器会把文件从 TEMP 目录复制到发生拖放的目录。
从查看器中拖放
我们已经看过了实现拖放操作逻辑的类,现在,我们来看一下我们的查看器应用是怎样使用这个类的。当主框架窗口接收到 LVN_BEGINDRAG 通知消息时,它会调用视图以获取选中文件的列表,而后设置 CDragDropSource 对象: LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
vector<CDraggedFileInfo> vec;
CComObjectStack<CDragDropSource> dropsrc;
DWORD dwEffect = 0;
HRESULT hr;
// Get a list of the files being dragged (minus files
// that we can't extract from the current CAB).
if ( !m_view.GetDraggedFileInfo(vec) )
return 0; // do nothing
// Init the drag/drop data object.
if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) )
return 0; // do nothing
// Start the drag/drop!
hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
return 0;
}
第一个调用的是视图的 GetDraggedFileInfo() 方法,用以得到选中文件的列表。此方法返回一个 vector<CDraggedFileInfo>,我们要用它来初始化 CDragDropSource 对象。GetDraggedFileInfo() 在选定的文件都不能被提取的情况下(例如文件被分块存放在不同的 CAB 文件中)有可能失败。如果发生了这种情况,则 OnListBeginDrag() 也静静地失败,不做任何事情就返回。最后,我们调用 DoDragDrop() 来开始操作,并让 CDragDropSource 处理剩余的事情。
上面列出的步骤 6 提到了拖放结束后对 UI 的更新。因为有可能在 CAB 末尾的一个文件仅仅是部分存储于此 CAB 中,而剩余的则在后续的一个 CAB 里。(这在 Windows 9x 的安装文件里非常普遍,在那儿 CAB 需要能符合软盘的大小)当我们试图提取这样的一个文件时,CAB SDK 会告诉我们含有该文件剩余部分的 CAB 的名字。它还会在原始 CAB 所在的相同目录下寻找那个 CAB,如果存在的话则从中提取文件的剩余部分。
因为我们要在视图窗口中标示分块文件,所以 OnListBeginDrag() 会检查拖放的结果,看是否找到了分块文件: LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
//...
// Start the drag/drop!
hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
if ( FAILED(hr) )
ATLTRACE("DoDragDrop() failed, error: 0x%08X\n", hr);
else
{
// If we found any files continued into other CABs, update the UI.
const vector<CDraggedFileInfo>& vecResults = dropsrc.GetDragResults();
vector<CDraggedFileInfo>::const_iterator it;
for ( it = vecResults.begin(); it != vecResults.end(); it++ )
{
if ( it->bPartialFile )
m_view.UpdateContinuedFile ( *it );
}
}
return 0;
}
我们调用 GetDragResults() 来获取反映了拖放操作结果的更新过的 vector<CDraggedFileInfo>。如果结构中的 bPartialFile 成员为 true,则表示该文件仅部分存在于此 CAB 中。我们再调用视图方法 UpdateContinuedFile(),并将信息结构传递给它,因而它可以相应地文件列表视图中的项。下面就是当发现有后续 CAB 时,应用程序如何标示分块文件:

如果找不到后续的 CAB,应用会通过设置 LVIS_CUT 风格来标示该文件不可以被提取,所以图标看起来是虚的:

出于安全考虑,应用把提取出的文件留在了 TEMP 目录里,而不是在拖放操作结束后立刻清除它们。在 CDragDropSource::Init() 创建零字节的临时文件时,它同时也把文件名添加到了全局向量 g_vecsTempFiles 中。临时文件在主框架窗口关闭时才删除。
加入 MRU 列表
我们将要看到的另一个文档/视图风格的特性是最近使用文件列表。WTL 的 MRU 实现为一个模板类 CRecentDocumentListBase。如果你不需要覆盖 MRU 任何的缺省行为(当然缺省通常已经足够用了),你可以使用派生类 CRecentDocumentList。
CRecentDocumentListBase 模板具有以下参数:
template <class T, int t_cchItemLen = MAX_PATH,
int t_nFirstID = ID_FILE_MRU_FIRST,
int t_nLastID = ID_FILE_MRU_LAST> CRecentDocumentListBase
T
- 特化
CRecentDocumentListBase 的派生类的名字。
t_cchItemLen
- 以
TCHAR 为单位的存储在 MRU 项中的字符串长度。至少必须为 6。
t_nFirstID
- 用于 MRU 项的 ID 范围的最小 ID。
t_nLastID
- 用于 MRU 项的 ID 范围的最大 ID。此值必须大于
t_nFirstID。
要把 MRU 特性添加到我们的应用里,需要做以下几步:
- 在我们希望 MRU 菜单项出现的地方插入一个 ID 为
ID_FILE_MRU_FIRST 的菜单项。此项的文本当列表为空时会显示出来。
- 添加一个 ID 为
ATL_IDS_MRU_FILE 的字符串表项。此字符串用作 MRU 项被选中时的动态帮助。如果你使用 WTL AppWizard,则此字符串已经帮你创建好了。
- 添加一个
CRecentDocumentList 对象到 CMainFrame 中。
- 在
CMainFrame::Create() 里初始化该对象。
- 处理命令 ID 介于
ID_FILE_MRU_FIRST 和 ID_FILE_MRU_LAST 之间(含)的 WM_COMMAND 消息。
- 当打开一个 CAB 文件时更新 MRU 列表。
- 应用关闭时保存 MRU 列表。
记住,如果 ID_FILE_MRU_FIRST 和 ID_FILE_MRU_LAST 不适合你的应用的话,你还是可以更改 ID 范围的,只要生成 CRecentDocumentListBase 的一个新的特化版本就可以。
设置 MRU 对象
第一步就是添加一个菜单项,以标明 MRU 项应该处于什么位置。通常是在 File 菜单下,这也是我们的应用用到的。下面就是占位菜单项:

AppWizard 已经把 ATL_IDS_MRU_FILE 字符串添加到了字符串表里,我们将之改为读作“打开此 CAB 文件”。接下来,我们向 CMainFrame 中加入一个名为 m_mru 的 CRecentDocumentList 类型成员变量并在 OnCreate() 中初始化它: #define APP_SETTINGS_KEY \
_T("software\\Mike's Classy Software\\WTLCabView");
LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
HWND hWndToolBar = CreateSimpleToolBarCtrl(...);
CreateSimpleReBar ( ATL_SIMPLE_REBAR_NOBORDER_STYLE );
AddSimpleReBarBand ( hWndToolBar );
CreateSimpleStatusBar();
m_hWndClient = m_view.Create ( m_hWnd, rcDefault );
m_view.Init();
// Init MRU list
CMenuHandle mainMenu = GetMenu();
CMenuHandle fileMenu = mainMenu.GetSubMenu(0);
m_mru.SetMaxEntries(9);
m_mru.SetMenuHandle ( fileMenu );
m_mru.ReadFromRegistry ( APP_SETTINGS_KEY );
// ...
}
前两个方法设置了我们想在维持的项的数目(缺省为 16)以及包含占位项的菜单句柄。ReadFromRegistry() 从注册表中把 MRU 列表读出。它接收我们传递给它的键名,并在其下创建一个新键来保存列表。当我们这里,键为 HKCU\Software\Mike's Classy Software\WTLCabView\Recent Document List。
加载文件列表之后,ReadFromRegistry() 调用另一个 CRecentDocumentList 方法,即 UpdateMenu(),该方法会找到占位菜单项病将之以实际的 MRU 项代替。
处理 MRU 命令并更新列表
当用户选择了某个 MRU 项时,主框架会收到一个 WM_COMMAND 消息,命令 ID 等于菜单项的 ID。我们可以在消息映射中用一个宏来处理这些命令: BEGIN_MSG_MAP(CMainFrame)
COMMAND_RANGE_HANDLER_EX(
ID_FILE_MRU_FIRST, ID_FILE_MRU_LAST, OnMRUMenuItem)
END_MSG_MAP()
消息处理其从 MRU 对象处获取该项的全路径,然后调用 ViewCab() 以使应用显示该文件的内容。 void CMainFrame::OnMRUMenuItem (
UINT uCode, int nID, HWND hwndCtrl )
{
CString sFile;
if ( m_mru.GetFromList ( nID, sFile ) )
ViewCab ( sFile, nID );
}
如上文提到的,我们要扩展 ViewCab() 以使之感知 MRU 对象,并在必要时更新文件列表。新的原型为: void ViewCab ( LPCTSTR szCabFilename, int nMRUID = 0 );
如果 nMRUID 为 0,则 ViewCab() 是被 OnFileOpen() 调用的,否则的话,则是用户选择了某个 MRU 菜单项,nMRUID 就是 OnMRUMenuItem() 接收到的命令 ID。下面是更新过的代码: void CMainFrame::ViewCab ( LPCTSTR szCabFilename, int nMRUID )
{
if ( EnumCabContents ( szCabFilename ) )
{
m_sCurrentCabFilePath = szCabFilename;
// If this CAB file was already in the MRU list,
// move it to the top of the list. Otherwise,
// add it to the list.
if ( 0 == nMRUID )
m_mru.AddToList ( szCabFilename );
else
m_mru.MoveToTop ( nMRUID );
}
else
{
// We couldn't read the contents of this CAB file,
// so remove it from the MRU list if it was in there.
if ( 0 != nMRUID )
m_mru.RemoveFromList ( nMRUID );
}
}
当 EnumCabContents() 成功了,我们就根据 CAB 文件是如何被选中的以不同的方式更新 MRU。如果是通过 File-Open 选中的,我们就调用 AddToList() 来把文件名加入到 MRU 列表里;如果是通过 MRU 菜单项选中的,我们就用 MoveToTop() 把该项移动到列表的顶部。如果 EnumCabContents() 失败,我们就用 RemoveFromList() 把文件名从 MRU 列表中移除。所有这几个方法都会在内部调用 UpdateMenu(),因此 File 菜单会自动被更新。
保存 MRU 列表
在应用关闭时,我们把 MRU 列表保存回注册表中。这事简单,只需要一行: m_mru.WriteToRegistry ( APP_SETTINGS_KEY );
这行放到了 CMainFrame 对 WM_DESTROY 和 WM_ENDSESSION 的消息处理器里。
其他 UI Goodies
透明的拖动图像
Windows 2000 及以后有一个内建的 COM 对象,叫做拖放助手,其目的是在拖放操作中提供良好的透明拖动图像。拖动源通过 IDragSourceHelper 接口使用此对象。下面是我们加入到 OnListBeginDrag() 中的使用此助手对象的额外代码,用粗体标示: LRESULT CMainFrame::OnListBeginDrag(NMHDR* phdr)
{
NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr;
CComPtr<IDragSourceHelper> pdsh;
vector<CDraggedFileInfo> vec;
CComObjectStack<CDragDropSource> dropsrc;
DWORD dwEffect = 0;
HRESULT hr;
if ( !m_view.GetDraggedFileInfo(vec) )
return 0; // do nothing
if ( !dropsrc.Init(m_sCurrentCabFilePath, vec) )
return 0; // do nothing
// Create and init a drag source helper object
// that will do the fancy drag image when the user drags
// into Explorer (or another target that supports the
// drag/drop helper interface).
hr = pdsh.CoCreateInstance ( CLSID_DragDropHelper );
if ( SUCCEEDED(hr) )
{
CComQIPtr<IDataObject> pdo;
if ( pdo = dropsrc.GetUnknown() )
pdsh->InitializeFromWindow ( m_view, &pnmlv->ptAction, pdo );
}
// Start the drag/drop!
hr = dropsrc.DoDragDrop(DROPEFFECT_COPY, &dwEffect);
// ...
}
我们从创建此拖放助手 COM 对象开始。如果成功,我们就调用 InitializeFromWindow() 并传递三个参数:拖动源的窗口 HWND,光标位置,和一个基于我们的 CDragDropSource 对象的 IDataObject 接口。拖放助手使用此接口保存其数据,而且,如果拖放目标也使用助手对象的话,此数据用来生成拖动图像。
要使 InitializeFromWindow() 可以工作,拖放源的窗口需要处理 DI_GETDRAGIMAGE 消息,而且在对该消息的效应里,要创建一个用作拖动图像的位图。对我们而言幸运的是,列表视图控件支持这一特性,因此我们仅需很少的工作就可以得到拖动图像。下面是拖动图像的样子:

如果我们使用一些别的不处理 DI_GETDRAGIMAGE 的窗口做我们的视图类,我们就要自己来创建拖动图像并调用 InitializeFromBitmap() 来把图像保存到拖放助手对象中。
透明的选择矩形
从 Windows XP 开始,列表视图控件可以显示一个透明的选择框。此特性缺省是关闭的,但通过给控件设置 LVS_EX_DOUBLEBUFFER 风格即可启用。我们的应用把这件事在 CWTLCabViewView::Init() 中作为视图窗口初始化工作的一部分来做。下面是成果:

如果没有显示出透明选择框,那就需要检查你的系统属性,确保下面的特性是启用了的:

标示排序的列
在 Windows XP 及之后,详细信息模式的列表视图控件有一个被选中的列,以不同的背景颜色显示。这一特性通常用来标示那一列是被排序了的,而这也正是我们的 CAB 查看器要做的。标头控件也有了两个新的格式风格,使得在一列中标头可以显示一个向上或者向下的箭头。这通常用来显示排序的方向。
视图类在 LVN_COLUMNCLICK 处理器中处理了排序。显示排序列的代码用粗体作了加亮: LRESULT CWTLCabViewView::OnColumnClick ( NMHDR* phdr )
{
int nCol = ((NMLISTVIEW*) phdr)->iSubItem;
// If the user clicked the column that is already sorted,
// reverse the sort direction. Otherwise, go back to
// ascending order.
if ( nCol == m_nSortedCol )
m_bSortAscending = !m_bSortAscending;
else
m_bSortAscending = true;
if ( g_bXPOrLater )
{
HDITEM hdi = { HDI_FORMAT };
CHeaderCtrl wndHdr = GetHeader();
// Remove the sort arrow indicator from the
// previously-sorted column.
if ( -1 != m_nSortedCol )
{
wndHdr.GetItem ( m_nSortedCol, &hdi );
hdi.fmt &= ~(HDF_SORTDOWN | HDF_SORTUP);
wndHdr.SetItem ( m_nSortedCol, &hdi );
}
// Add the sort arrow to the new sorted column.
hdi.mask = HDI_FORMAT;
wndHdr.GetItem ( nCol, &hdi );
hdi.fmt |= m_bSortAscending ? HDF_SORTUP : HDF_SORTDOWN;
wndHdr.SetItem ( nCol, &hdi );
}
// Store the column being sorted, and do the sort
m_nSortedCol = nCol;
SortItems ( SortCallback, (LPARAM)(DWORD_PTR) this );
// Indicate the sorted column.
if ( g_bXPOrLater )
SetSelectedColumn ( nCol );
return 0;
}
加亮代码的第一小节去除了先前的排序列的排序箭头。如果没有排序的列的话,就省略这一步。然后,把箭头添加到用户刚刚点击的列上。如果以升序排序则箭头朝上,降序则朝下。排序结束后,我们调用 SetSelectedColumn() —— 对 LVM_SETSELECTEDCOLUMN 消息的一个封装 —— 来把选中列设置为我们刚刚排序的列。
以下是文件以大小排序后的列表控件的样子:

使用平铺视图模式
在 Windows XP 及之后,列表视图控件还有一种新的风格称为平铺视图模式。作为视图窗口初始化的一部分,如果应用是运行于 XP 或之后上,它就会使用 SetView() (对 LVM_SETVIEW 消息的一个封装)把列表视图的模式设置为平铺模式。然后再填充一个 LVTILEVIEWINFO 结构来设置一些控制如何平铺绘制的属性。cLines 属性被设为了 2,表示要在图标旁边显示两行附加文本。dwFlags 成员设置为了 LVTVIF_AUTOSIZE,这使得控件自身的大小改变时同时也改变平铺区域的大小。 void CWTLCabViewView::Init()
{
// ...
// On XP, set some additional properties of the list ctrl.
if ( g_bXPOrLater )
{
// Turning on LVS_EX_DOUBLEBUFFER also enables the
// transparent selection marquee.
SetExtendedListViewStyle ( LVS_EX_DOUBLEBUFFER,
LVS_EX_DOUBLEBUFFER );
// Default to tile view.
SetView ( LV_VIEW_TILE );
// Each tile will have 2 additional lines (3 lines total).
LVTILEVIEWINFO lvtvi = { sizeof(LVTILEVIEWINFO),
LVTVIM_COLUMNS };
lvtvi.cLines = 2;
lvtvi.dwFlags = LVTVIF_AUTOSIZE;
SetTileViewInfo ( &lvtvi );
}
}
设置平铺视图的图像列表
在平铺视图模式下,我们会使用特大系统图形列表(在缺省的显示设置下图标为 48x48 大小)。我们使用 SHGetImageList() API 获取此图像列表。SHGetImageList() 不同于 SHGetFileInfo() 的是它返回一个基于图像列表对象的 COM 接口。视图窗口有两个成员变量用来管理此图像列表: CImageList m_imlTiles; // the image list handle
CComPtr<IImageList> m_TileIml; // COM interface on the image list
视图窗口在 InitImageLists() 中获取特大图像列表: HRESULT (WINAPI* pfnGetImageList)(int, REFIID, void**);
HMODULE hmod = GetModuleHandle ( _T("shell32") );
(FARPROC&) pfnGetImageList = GetProcAddress(hmod, "SHGetImageList");
hr = pfnGetImageList ( SHIL_EXTRALARGE, IID_IImageList,
(void**) &m_TileIml );
if ( SUCCEEDED(hr) )
{
// HIMAGELIST and IImageList* are interchangeable,
// so this cast is OK.
m_imlTiles = (HIMAGELIST)(IImageList*) m_TileIml;
}
如果 SHGetImageList() 成功,我们可以把 IImageList* 接口转型为一个 HIMAGELIST,再像使用其它图像列表一样使用它。
使用平铺视图的图像列表
由于列表控件并不能为平铺视图模式持有单独的图像列表,所以我们需要在运行时,当用户选择大图标或者平铺视图模式时改变图像列表。视图类有一个 SetViewMode() 方法来处理改变图像列表和视图风格的事宜: void CWTLCabViewView::SetViewMode ( int nMode )
{
if ( g_bXPOrLater )
{
if ( LV_VIEW_TILE == nMode )
SetImageList ( m_imlTiles, LVSIL_NORMAL );
else
SetImageList ( m_imlLarge, LVSIL_NORMAL );
SetView ( nMode );
}
else
{
// omitted - no image list changing necessary on
// pre-XP, just modify window styles
}
}
如果控件即将进入平铺视图模式,那我们就把控件的图像列表设置为 48x48 的那个,否则设置为 32x32 的那个。
设置附加的文本行
在初始化时,我们将平铺效果设置为要显示附加的两行文本。第一行总是项的文字,就像在大图标和小图标模式里一样。显示在两行附加的文本里的文字取自于子项,类似于详细信息模式下的列。我们可以为每个图标设置单独的子项。下面就是在 AddFile() 中视图是怎样设置文本的: // Add a new list item.
int nIdx;
nIdx = InsertItem ( GetItemCount(), szFilename, info.iIcon );
SetItemText ( nIdx, 1, info.szTypeName );
SetItemText ( nIdx, 2, szSize );
SetItemText ( nIdx, 3, sDateTime );
SetItemText ( nIdx, 4, sAttrs );
// On XP+, set up the additional tile view text for the item.
if ( g_bXPOrLater )
{
UINT aCols[] = { 1, 2 };
LVTILEINFO lvti = { sizeof(LVTILEINFO), nIdx,
countof(aCols), aCols };
SetTileInfo ( &lvti );
}
aCols 数组保存有子项,其文本将会被显示出来,在这儿我们显示子项 1 (文件类型)以及 2 (文件大小)。下面是在平铺视图模式下查看器的样子:

注意,附加行当你在详细信息模式下排序某列之后会改变。当使用 LVM_SETSELECTEDCOLUMN 设置了选中列时,该子项的文字总是最先显示,覆盖了我们传递到 LVTILEINFO 结构中的子项设置。
Copyright and license
This article is copyrighted material, ©2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.
The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.
修订历史
2006 年 6 月 16 日:首次发布 | 链接: 上一部分 7/27/2006
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分;下一部分
第九部分 - GDI 类,公用对话框以及工具类
内容
- 简介
- GDI 封装类
- 封装类里的公用函数
- 使用 CDCT
- 与 MFC 封装类的差异
- 资源加载函数
- 使用公用对话框
- CFileDialog
- CFolderDialog
- 其他有用的类和全局函数
- Struct 的封装
- 处理双类型参数的类
- 其他工具类
- 全局函数
- 宏
示例工程
版权和许可
修订历史
简介
WTL 里包含了好多封装类和工具类,而直到现在也还没有在本系列里进行过全面的介绍,比如说 CString 和 CDC。WTL 具有一个封装 GDI 对象的良好体系,一些用以加载资源的便利函数,以及更便于使用某些 Win32 公用对话框的类。在此第九部分里,我将介绍一些使用最广泛的类。
本文讨论了四类特性:
- GDI 封装类
- 资源加载函数
- 使用打开文件公用对话框以及文件夹选择公用对话框
- 其他有用的类以及全局函数
GDI 封装类
相较于 MFC,WTL 为其 GDI 封装类使用了一种相当不同的方法。WTL 的方法是,为每种类型的 GDI 对象准备一个模板类,每个模板都有一个名为 t_bManaged 的 bool 参数。此参数控制着该类的实例是否“管理”着(或者说是拥有)被封装的 GDI 对象。如果 t_bManaged 为 false,则 C++ 对象不会管理 GDI 对象的生命期,C++ 对象仅是围绕 GDI 对象句柄的一个简单封装层。如果 t_bManaged 为 true,则有两件事情会改变:
- 析构函数会对所封装的非空句柄调用
DeleteObject()。
Attach() 在将 C++ 对象关联到新的句柄之前,也会对所封装的非空句柄调用 DeleteObject()。
此设计与 ATL 窗口类的设计是一致的,CWindow 是对 HWND 的一个简单的封装层,而 CWindowImpl 管理了一个窗口的生命期。
GDI 封装类定义于 atlgdi.h 中,不过 CMenuT 是个例外,它定义于 atluser.h 中(译者注:此处作者的说法欠妥,菜单在 Windows 里原本就是属于 USER32 的,而不是 GDI32)。你自己不必包含这些头文件,因为 atlapp.h 总会为你包含它们。每个类还有一个易于记忆的 typedef 名字:
|
封装的 GDI 对象 |
模板类 |
管理对象的 typedef 名 |
普通封装的 typedef 名 |
|
Pen |
CPenT
|
CPen
|
CPenHandle
|
|
Brush |
CBrushT
|
CBrush
|
CBrushHandle
|
|
Font |
CFontT
|
CFont
|
CFontHandle
|
|
Bitmap |
CBitmapT
|
CBitmap
|
CBitmapHandle
|
|
Palette |
CPaletteT
|
CPalette
|
CPaletteHandle
|
|
Region |
CRgnT
|
CRgn
|
CRgnHandle
|
|
Device context |
CDCT
|
CDC
|
CDCHandle
|
|
Menu |
CMenuT
|
CMenu
|
CMenuHandle
|
与 MFC 传递对象指针相比较,我更喜欢这种方法。你永远也不必担心收到了一个 NULL 指针(封装的句柄可能会是 NULL,但那是另一回事),也不会有这种特殊的情况,你有一个临时对象,但你却不能把它保存下来然后对它进行不止一次的函数调用。创建这些类的实例代价也是很低的,因为它们只有一个成员变量,即所封装的句柄。就像 CWindow 一样,在线程之间传递封装类的对象也不会有问题,因为 WTL 并没有像 MFC 那样的线程相关的映射。
另外还有几个设备上下文的封装类,用于特定的绘制场景:
CClientDC:封装了对 GetDC() 和 ReleaseDC() 的调用,用于在窗口的客户区内绘制
CWindowDC:封装了对 GetWindowDC() 和 ReleaseDC() 的调用,用于在窗口内的任意地方绘制
CPaintDC:封装了对 BeginPaint() 和 EndPaint() 的调用,用于 WM_PAINT 处理器中
这些类中的任一个都是在构造函数中接受一个 HWND,而行为与其在 MFC 中的同名类相似。所有这三个类都派生于 CDC,因此它们都管理着自己的设备上下文。
封装类里的公用函数
GDI 封装类都遵循相同的设计。简明起见,在这儿我只介绍 CBitmapT 里的方法,但其它类工作起来都很相似。
- 封装的 GDI 对象句柄
- 每个类都持有一个公用成员变量,其中保留了此 C++ 对象关联着的 GDI 对象的句柄。
CBitmapT 则具有一个名为 m_hBitmap 的 HBITMAP 成员。
- 构造函数
- 构造函数有一个参数,为一个
HBITMAP,其缺省值为 NULL。m_hBitmap 被初始化为此参数的值。
- 析构函数
- 如果
t_bManaged 为 true,而且 m_hBitmap 不是 NULL,则析构函数会调用 DeleteObject() 以销毁位图。
Attach() 和 operator =
- 这两个方法都接受一个
HBITMAP 句柄。如果 t_bManaged 为 true,而且 m_hBitmap 不是 NULL,这些方法会先调用 DeleteObject() 以销毁 CBitmapT 对象管理着的位图,然后将 m_hBitmap 设置为作为参数传入的 HBITMAP。
Detach()
Detach() 会将 m_hBitmap 设置为 NULL,然后再返回原来在 m_hBitmap 中的值。当 Detach() 返回后,CBitmapT 对象就不再与任何 GDI 有关联了。
- 创建 GDI 对象的方法
CBitmapT 对 Win32 中创建位图的 API 也有封装:LoadBitmap()、LoadMappedBitmap()、CreateBitmap()、CreateBitmapIndirect()、CreateCompatibleBitmap()、CreateDiscardableBitmap()、CreateDIBitmap() 以及 CreateDIBSection()。如果 m_hBitmap 不是 NULL 时,这些方法都会产生断言。如果要为另一个 GDI 位图重用本 CBitmapT 对象,就要先调用 DeleteObject() 或者 Detach()。
DeleteObject()
DeleteObject() 销毁 GDI 位图对象并将 m_hBitmap 置为 NULL。这一方法仅当 m_hBitmap 不为 NULL 时才应该调用,否则会产生断言。
IsNull()
IsNull() 在 m_hBitmap 为 NULL 时返回 true,否则返回 false。这一方法用来测试 CBitmapT 对象当前是否关联着一个 GDI 位图。
operator HBITMAP
- 这个转换器会返回
m_hBitmap,这就使你可以把 CBitmapT 对象传递给一个接收 HBITMAP 句柄的函数或者 Win32 API。这个转换器在以下情况下也会被调用,或者是一个 CBitmapT 在布尔值的上下文环境下被求值,或者是对 IsNull() 的逻辑取反求值。因而,这两个 if 语句是等效的: CBitmapHandle bmp = /* some HBITMAP value */;
if ( !bmp.IsNull() ) { do something... }
if ( bmp ) { do something more... }
GetObject() 的封装
CBitmapT 具有一个对于 Win32 API GetObject()的类型安全的封装:GetBitmap()。有两个重载形态:一个是接收 LOGBITMAP* 参数而直接调用 GetObject(),另一个则接收 LOGBITMAP& 参数并返回一个表示成功与否的 bool 值。后者更为易用些。例如: CBitmapHandle bmp2 = /* some HBITMAP value */;
LOGBITMAP logbmp = {0};
bool bSuccess;
if ( bmp2 )
bSuccess = bmp2.GetLogBitmap ( logbmp );
- 操作 GDI 对象的 API 的封装
CBitmapT 对接收 HBITMAP 参数的 Win32 API 也进行了封装:GetBitmapBits()、SetBitmapBits()、GetBitmapDimension()、SetBitmapDimension()、GetDIBits() 以及 SetDIBits()。这些方法在 m_hBitmap 为 NULL 时都会产生断言。
- 其它辅助方法
CBitmapT 还有两个作用于 m_hBitmap 上的有用的方法:LoadOEMBitmap() 和 GetSize()。
使用 CDCT
CDCT 与其它类有一点不同,因此我要单独介绍其差别。
方法上的差别
销毁一个 DC 的方法叫做 DeleteDC() 而不是 DeleteObject()。
将对象选入 DC
MFC 的 CDC 在把对象选入到其中时有一个容易导致错误的地方。MFC 的 CDC 有若干个重载的 SelectObject() 函数,每个都是接收一个指向不同类型的 GDI 封装类的指针(CPen*,CBitmap*,等等)作为参数。如果你把一个 C++ 对象而不是指向该对象的指针传递给 SelectObject(),代码最终会调用一个未文档化的,接收一个 HGDIOBJ 句柄为参数的重载,这样就会导致问题。
WTL 的 CDCT 使用了一种更好的方法,它有好几个选择函数,每一个都只作于特定类型的 GDI 对象: HPEN SelectPen(HPEN hPen)
HBRUSH SelectBrush(HBRUSH hBrush)
HFONT SelectFont(HFONT hFont)
HBITMAP SelectBitmap(HBITMAP hBitmap)
int SelectRgn(HRGN hRgn)
HPALETTE SelectPalette(HPALETTE hPalette, BOOL bForceBackground)
在调试模式的构建中,每个方法都会断言 m_hDC 为非 NULL 值,而且参数是恰当类型的 GDI 对象的句柄。然后再调用 SelectObject() API,并将其返回值转型为合适的类型。
还存在着几个辅助函数,它们先用给定的常量调用 GetStockObject(),然后再把对象选入到 DC: HPEN SelectStockPen(int nPen)
HBRUSH SelectStockBrush(int nBrush)
HFONT SelectStockFont(int nFont)
HPALETTE SelectStockPalette(int nPalette, BOOL bForceBackground)
与 MFC 封装类的区别
较少的构造函数:WTL 的封装类缺少可以创建新的 GDI 对象的构造函数。比如,MFC 的 CBrush 有一个构造函数可以创建一个实心画刷或者模式(patterne)画刷。而使用 WTL 的类,你必须使用某个方法来创建该 GDI 对象。
用更好的方法把对象选入 DC:参看上面使用 CDCT 节
没有 m_hAttribDC:WTL 的 CDCT 没有 m_hAttribDC 成员
一些方法里有少许的参数差异:例如,CDC::GetWindowExt() 在 MFC 里返回一个 CSize 对象,而在 WTL 里返回一个 bool,而大小通过输出参数来返回。
资源加载函数
WTL 里的若干个全局函数是极具帮助的加载不同类型的资源的捷径。在接触这些函数之前,我们需要了解一个工具类:_U_STRINGorID。
在 Win32 中,大多数资源可以用一个字符串(LPCTSTR)或者一个无符号整数(UINT)来标识。接收资源标识符的 API 需要接收一个 LPCTSTR 参数,如果要传递一个 UINT 的话,你需要使用 MAKEINTRESOURCE 宏来将之转换为一个 LPCTSTR。_U_STRINGorID,当作为资源标识符参数类型来使用的时候,会隐藏这一不同,因而调用者就可以直接传递 UINT 或者 LPCTSTR。如果需要的话,函数可以使用一个 CString 来加载字符串: void somefunc ( _U_STRINGorID id )
{
CString str ( id.m_lpstr );
// use str...
}
void func2()
{
// Call 1 - using a string literal
somefunc ( _T("Willow Rosenberg") );
// Call 2 - using a string resource ID
somefunc ( IDS_BUFFY_SUMMERS );
}
这能够工作的原因是,CString 接收 LPCTSTR 的构造函数会检查参数是否是一个字符串 ID。如果是的话,就会从字符串表中加载该字符串并赋予该 CString。
在 VC 6 里,_U_STRINGorID 由 WTL 在 atlwinx.h 中提供;在 VC 7 里,_U_STRINGorID 是 ATL 的一部分。无论哪种,这个类的定义总会被其他的 ATL/WTL 头文件包含进来。
此节中的函数都是从保存在 _Module 全局变量(VC 6)或者 _AtlBaseModule (VC 7)中的资源实例句柄加载资源。使用其他模块的资源超出了本文的范围,因此我在这儿不会提及。只需要记住,缺省情况下,这些函数总是在代码正运行于其中的 EXE 或者 DLL 中寻找。这些函数并没有比调用 API 多做什么,它们的作用在于由 _U_STRINGorID 所提供的资源标识符的处理简化上。
HACCEL AtlLoadAccelerators(_U_STRINGorID table)
调用流向 LoadAccelerators()。 HMENU AtlLoadMenu(_U_STRINGorID menu)
调用流向 LoadMenu()。 HBITMAP AtlLoadBitmap(_U_STRINGorID bitmap)
调用流向 LoadBitmap()。 HCURSOR AtlLoadCursor(_U_STRINGorID cursor)
调用流向 LoadCursor()。 HICON AtlLoadIcon(_U_STRINGorID icon)
调用流向 LoadIcon()。注意一下这个函数,和 LoadIcon() 一样,仅可以加载 32x32 图标。 int AtlLoadString(UINT uID, LPTSTR lpBuffer, int nBufferMax)
bool AtlLoadString(UINT uID, BSTR& bstrText)
调用流向 LoadString()。字符串可以返回至一个 TCHAR 缓冲区,或者是赋与一个 BSTR,取决于你使用哪一个重载。注意这两个函数仅接收 UINT 作为其资源 ID,因为字符串表项不能有字符串标识符。
下面这组函数封装了对 LoadImage() 的调用,而且还接受传递给 LoadImage() 的附加参数。
HBITMAP AtlLoadBitmapImage(
_U_STRINGorID bitmap, UINT fuLoad = LR_DEFAULTCOLOR)
使用 IMAGE_BITMAP 类型调用 LoadImage(),并将 fuLoad 标志传入。 HCURSOR AtlLoadCursorImage(
_U_STRINGorID cursor,
UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE,
int cxDesired = 0, int cyDesired = 0)
使用 IMAGE_CURSOR 类型调用 LoadImage(),并将 fuLoad 标志传入。由于一个光标资源可以包含若干个不同尺寸的光标,因而你可以把尺寸传递给 cxDesired 和 cyDesired 参数以加载一个特定大小的光标。 HICON AtlLoadIconImage(
_U_STRINGorID icon,
UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE,
int cxDesired = 0, int cyDesired = 0)
使用 IMAGE_ICON 类型调用 LoadImage(),并将 fuLoad 标志传入。cxDesired 和 cyDesired 参数的用法同 AtlLoadCursorImage()。
下面这一组函数封装了加载系统定义的资源(例如,标准的手状光标)的调用。其中的一些资源 ID(主要是位图的那些)缺省是不被包含的,需要在你的 stdafx.h 文件中 #define OEMRESOURCE 符号来引用它们。
HBITMAP AtlLoadSysBitmap(LPCTSTR lpBitmapName)
使用 NULL 资源句柄调用 LoadBitmap()。使用此函数可以加载任何在 LoadBitmap() 的文档中列出的 OBM_* 位图。 HCURSOR AtlLoadSysCursor(LPCTSTR lpCursorName)
使用 NULL 资源句柄调用 LoadCursor()。使用此函数可以加载任何在 LoadCursor() 的文档中列出的 IDC_* 光标。 HICON AtlLoadSysIcon(LPCTSTR lpIconName)
使用 NULL 资源句柄调用 LoadIcon()。使用此函数可以加载任何在 LoadIcon() 的文档中列出的 IDI_* 图标。不过要注意此函数 —— 就像 LoadIcon() —— 只能加载 32x32 的图标。 HBITMAP AtlLoadSysBitmapImage(
WORD wBitmapID, UINT fuLoad = LR_DEFAULTCOLOR)
使用 NULL 资源句柄以及 IMAGE_BITMAP 类型调用 LoadImage()。你可以使用此函数来加载与 AtlLoadSysBitmap() 相同的位图。 HCURSOR AtlLoadSysCursorImage(
_U_STRINGorID cursor,
UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE,
int cxDesired = 0, int cyDesired = 0)
使用 NULL 资源句柄以及 IMAGE_CURSOR 类型调用 LoadImage()。你可以使用此函数来加载与 AtlLoadSysCursor() 相同的光标。 HICON AtlLoadSysIconImage(
_U_STRINGorID icon,
UINT fuLoad = LR_DEFAULTCOLOR | LR_DEFAULTSIZE,
int cxDesired = 0, int cyDesired = 0)
使用 NULL 资源句柄以及 IMAGE_ICON 类型调用 LoadImage()。你可以使用此函数来加载与 AtlLoadSysIcon() 相同的图标,不过可以指定一个不同的尺寸,例如 16x16。
最后的这组函数是对 GetStockObject() API 的类型安全的封装。
HPEN AtlGetStockPen(int nPen)
HBRUSH AtlGetStockBrush(int nBrush)
HFONT AtlGetStockFont(int nFont)
HPALETTE AtlGetStockPalette(int nPalette)
每个函数都会检查你是不是传入了恰当的值(比方说,AtlGetStockPen() 只接受 WHITE_PEN,BLACK_PEN 等值),然后再调用 GetStockObject()。
使用公用对话框
WTL 还有些类,可以使 Win32 的公用对话框使用起来更容易些。每个类都会处理公用对话框发送的消息以及回调,并依次调用那些可覆盖的函数。这和用于属性表的设计是一样的,只不过在那儿你是为各个被 CPropertyPageImpl 在必要时调用的属性表通知编写处理器,例如 OnWizardNext(),用来处理 PSN_WIZNEXT。
WTL 为每种公用对话框都包含了两个类,例如,选择文件夹对话框由 CFolderDialogImpl 和 CFolderDialog 封装。如果需要改变缺省行为,或者是要为某个消息编写处理器,你可以从 CFolderDialogImpl 派生一个新类并在其中作改动。如果 CFolderDialogImpl 的缺省行为已经足够了,则你可以使用 CFolderDialog。
公用对话框及其相应的 WTL 类有:
|
公用对话框 |
相应的 Win32 API |
实现类 |
不可定制类 |
|
文件打开和文件保存 |
GetOpenFileName(),
GetSaveFileName()
|
CFileDialogImpl
|
CFileDialog
|
|
选择文件夹 |
SHBrowseForFolder()
|
CFolderDialogImpl
|
CFolderDialog
|
|
选择字体 |
ChooseFont()
|
CFontDialogImpl,
CRichEditFontDialogImpl
|
CFontDialog,
CRichEditFontDialog
|
|
选择颜色 |
ChooseColor()
|
CColorDialogImpl
|
CColorDialog
|
|
打印及打印设置 |
PrintDlg()
|
CPrintDialogImpl
|
CPrintDialog
|
|
打印(Windows 2000 及之后) |
PrintDlgEx()
|
CPrintDialogExImpl
|
CPrintDialogEx
|
|
页面设置 |
PageSetupDlg()
|
CPageSetupDialogImpl
|
CPageSetupDialog
|
|
文本查找及替换 |
FindText(),
ReplaceText()
|
CFindReplaceDialogImpl
|
CFindReplaceDialog
|
把这些类全写出来将会使本文相当的长,因此我只选择了前两个,也正是你在使用中经常用到的两个。
CFileDialog
CFileDialog 及其基类 CFileDialogImpl 用于显示文件打开和文件保存对话框。在 CFileDialogImpl 中最重要的两个数据成员是 m_ofn 和 m_szFileName。m_ofn 是一个 OPENFILENAME,CFileDialogImpl 会为你对它设置一些有意义的缺省值,就像在 MFC 中一样,必要时你可以直接改变此结构中的数据。m_szFileName 是一个 TCHAR 数组,其中为选中文件的名字。由于 CFileDialogImpl 仅有此一个字符串用于保存文件名,当你使用多选模式的打开文件对话框时你需要自己提供缓冲区。
使用 CFileDialog 的基本步骤为:
- 构造一个
CFileDialog 对象,将初始化数据传递给构造函数。
- 调用
DoModal()。
- 如果
DoModal() 返回了 IDOK,则可以从 m_szFileName 获取选定的文件。
这就是 CFileDialog 的构造函数: CFileDialog::CFileDialog (
BOOL bOpenFileDialog,
LPCTSTR lpszDefExt = NULL,
LPCTSTR lpszFileName = NULL,
DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
LPCTSTR lpszFilter = NULL,
HWND hWndParent = NULL )
bOpenFileDialog 为 true 将创建一个文件打开对话框(CFileDialog 会调用 GetOpenFileName() 以显示该对话框),为 false 则创建一个文件保存对话框(CFileDialog 将调用 GetSaveFileName())。其余的参数会直接保存到 m_ofn 结构的对应成员中,不过它们都是可选参数,因为你可以在调用 DoModal() 之前直接访问 m_ofn。
与 MFC 的 CFileDialog 相比,一个显著的不同在于 lpszFilter 参数必须是一个以 NULL 字符分割的字符串列表(也这是在 OPENFILENAME 的文档中公开的格式),而不是使用管道字符分割的列表。
下面是一个使用 CFileDialog 的例子,应用了选择 Word 12 文件(*.docx)的过滤器: CString sSelectedFile;
CFileDialog fileDlg ( true, _T("docx"), NULL,
OFN_HIDEREADONLY | OFN_FILEMUSTEXIST,
_T("Word 12 Files\0*.docx\0All Files\0*.*\0") );
if ( IDOK == fileDlg.DoModal() )
sSelectedFile = fileDlg.m_szFileName;
CFileDialog 的本地化并不到位,因为构造函数使用了 LPCTSTR 参数。一眼望去,过滤器字符串还会有一点阅读上的困难。有两种解决办法,要么在调用 DoModal() 之前设置 m_ofn,要么从 CFileDialogImpl 派生一个新类,在其中作我们所希望的改进。在这儿我们介绍第二种,生成一个作了如下改动的新类:
- 构造函数中的字符串参数用
_U_STRINGorID 取代 LPCTSTR。
- 像 MFC 那样,过滤器字符串使用管道符分割各个域,而不是 NULL 字符。
- 对话框相对于其父窗口自动居中。
我们从写一个构造函数的接收参数类似于 CFileDialogImpl 的构造函数的类开始: class CMyFileDialog : public CFileDialogImpl<CMyFileDialog>
{
public:
// Construction
CMyFileDialog ( BOOL bOpenFileDialog,
_U_STRINGorID szDefExt = 0U,
_U_STRINGorID szFileName = 0U,
DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
_U_STRINGorID szFilter = 0U,
HWND hwndParent = NULL );
protected:
LPCTSTR PrepFilterString ( CString& sFilter );
CString m_sDefExt, m_sFileName, m_sFilter;
};
构造函数初始化这三个 CString 成员,必要时加载字符串: CMyFileDialog::CMyFileDialog (
BOOL bOpenFileDialog, _U_STRINGorID szDefExt, _U_STRINGorID szFileName,
DWORD dwFlags, _U_STRINGorID szFilter, HWND hwndParent ) :
CFileDialogImpl<CMyFileDialog>(bOpenFileDialog, NULL, NULL, dwFlags,
NULL, hwndParent),
m_sDefExt(szDefExt.m_lpstr), m_sFileName(szFileName.m_lpstr),
m_sFilter(szFilter.m_lpstr)
{
}
请注意,字符串参数在对基类的构造函数的调用里全部为 NULL。这是因为基类的构造函数总是在成员的初始化之前调用。为了能设置 m_ofn 中的字符串数据,我们加入了一些代码,这些代码重复了 CFileDialogImpl 的构造函数做的初始化步骤: CMyFileDialog::CMyFileDialog(...)
{
m_ofn.lpstrDefExt = m_sDefExt;
m_ofn.lpstrFilter = PrepFilterString ( m_sFilter );
// setup initial file name
if ( !m_sFileName.IsEmpty() )
lstrcpyn ( m_szFileName, m_sFileName, _MAX_PATH );
}
PrepFilterString() 是一个辅助方法,它接收一个以管道符分割的过滤器字符串,将管道符改为 NULL 字符,并返回一个指向字符串起始位置的指针。其结果就是一个可以用在 OPENFILENAME 中的具有正确格式的字符串列表。
LPCTSTR CMyFileDialog::PrepFilterString(CString& sFilter)
{
LPTSTR psz = sFilter.GetBuffer(0);
LPCTSTR pszRet = psz;
while ( '\0' != *psz )
{
if ( '|' == *psz )
*psz++ = '\0';
else
psz = CharNext ( psz );
}
return pszRet;
}
这些改变使得字符串的处理更加容易。为了实现自动居中,我们还要覆盖 OnInitDone() 通知。这需要我们添加一个消息映射(以使我们可以把通知消息串接到基类上),以及我们自己的 OnInitDone() 处理器: class CMyFileDialog : public CFileDialogImpl<CMyFileDialog>
{
public:
// Construction
CMyFileDialog(...);
// Maps
BEGIN_MSG_MAP(CMyFileDialog)
CHAIN_MSG_MAP(CFileDialogImpl<CMyFileDialog>)
END_MSG_MAP()
// Overrides
void OnInitDone ( LPOFNOTIFY lpon )
{
GetFileDialogWindow().CenterWindow(lpon->lpOFN->hwndOwner);
}
protected:
LPCTSTR PrepFilterString ( CString& sFilter );
CString m_sDefExt, m_sFileName, m_sFilter;
};
关联到 CMyFileDialog 对象的窗口实际上是文件打开对话框的一个子窗口,由于我们需要顶级的窗口,所以我们调用了 GetFileDialogWindow() 来获得它。
CFolderDialog
CFolderDialog 及其基类 CFolderDialogImpl 用于显示一个浏览文件夹对话框。尽管该对话框支持浏览外壳名字空间内的任何地方,但 CFolderDialog 仅具有在文件系统内浏览的能力。CFolderDialogImpl 中两个最重要的数据成员是 m_bi 和 m_szFolderPath。m_bi 是一个由 CFolderDialogImpl 管理并将之传递到 SHBrowseForFolder() API 中去的 BROWSEINFO,如果必要,你可以直接改变该结构中的数据。m_szFolderPath 是一个 TCHAR 数组,用来保存选定文件夹的名字。
使用 CFolderDialog 的基本步骤是:
- 构造一个
CFolderDialog 对象,传递一些初始化数据到构造函数中去。
- 调用
DoModal()。
- 如果
DoModal() 返回 IDOK,从 m_szFolderPath 中获取选定的文件夹的路径。
下面是 CFolderDialog 的构造函数: CFolderDialog::CFolderDialog (
HWND hWndParent = NULL,
LPCTSTR lpstrTitle = NULL,
UINT uFlags = BIF_RETURNONLYFSDIRS )
hWndParent 是浏览对话框的属主窗口。你既可以在构造函数中指定,也可以在 DoModal() 调用时指定。lpstrTitle 是一个显示于对话框里的树控件之上的字符串。uFlags 是控制对话框行为的标志,应该总是包括 BIF_RETURNONLYFSDIRS 以使树仅显示文件系统目录。在 BROWSEINFO 的文档里列出了其他可用于 uFlags 的值,不过要记住,有的标志可能会产生不太好的结果,比如 BIF_BROWSEFORPRINTER。UI 相关的标志,如 BIF_USENEWUI,可以良好工作。注意,就像在 CFileDialog 的构造函数里的字符串一样,lpstrTitle 参数也有相同的使用性问题。
下面是使用 CFolderDialog 选择一个目录的例子: CString sSelectedDir;
CFolderDialog fldDlg ( NULL, _T("Select a dir"),
BIF_RETURNONLYFSDIRS|BIF_NEWDIALOGSTYLE );
if ( IDOK == fldDlg.DoModal() )
sSelectedDir = fldDlg.m_szFolderPath;
为了演示定制的 CFolderDialog,我们将从 CFolderDialogImpl 派生一个类并设定其初始的选择。此对话框的回调不使用窗口消息,因此我们代之以覆盖 OnInitialized() 方法,该方法会在基类接收到 BFFM_INITIALIZED 通知时被调用。OnInitialized() 调用了 CFolderDialogImpl::SetSelection() 来改变对话框中的选择。 class CMyFolderDialog : public CFolderDialogImpl<CMyFolderDialog>
{
public:
// Construction
CMyFolderDialog ( HWND hWndParent = NULL,
_U_STRINGorID szTitle = 0U,
UINT uFlags = BIF_RETURNONLYFSDIRS ) :
CFolderDialogImpl<CMyFolderDialog>(hWndParent, NULL, uFlags),
m_sTitle(szTitle.m_lpstr)
{
m_bi.lpszTitle = m_sTitle;
}
// Overrides
void OnInitialized()
{
// Set the initial selection to the Windows dir.
TCHAR szWinDir[MAX_PATH];
GetWindowsDirectory ( szWinDir, MAX_PATH );
SetSelection ( szWinDir );
}
protected:
CString m_sTitle;
};
其他有用的类和全局函数
Struct 的封装
WTL 中的 CSize,CPoint 和 CRect 分别封装了 SIZE,POINT 和 RECT。它们与其 MFC 等价类的工作是一样的。
处理双类型参数的类
正如前面提到的,你可以使用 _U_STRINGorID 类型作为一个函数的参数,该参数既可以是数字也可以是字符串资源 ID。另外还有两个做类似工作的类:
_U_MENUorID:此类型可以从一个 UINT 或者 HMENU 构造得来,主要用于 CreateWindow() 的封装层里。CreateWindow() 的 hMenu 参数在即将创建的窗口是一个子窗口时实际上是一个窗口 ID,而 _U_MENUorID 则隐藏了这两种用法的区别。_U_MENUorID 有一个成员 m_hMenu,它可以被作为 hMenu 参数传递给 CreateWindow() 或者 CreateWindowEx()。
_U_RECT: 此类型可以从一个 LPRECT 或者 RECT& 构造得来,使得调用者可以传递一个 RECT,或者一个指向 RECT 的指针,再或者一个像 CRect 这样的封装类,只要它提供了到 RECT 的转换。
就像 _U_STRINGorID 一样,_U_MENUorID 和 _U_RECT 总会被你所使用的其他头文件包进来。
其他工具类
CString
WTL 的 CString 工作起来和 MFC 的 CString 一样,因此我在这儿不会详细介绍它。当你在定义了 _ATL_MIN_CRT 的情况下编译时,WTL 的 CString 使用了好多个额外的方法。这些方法,诸如 _cstrchr(),_cstrstr(),是那些当 _ATL_MIN_CRT 被定义时缺失的 CRT 函数的对应替代品。
CFindFile
CFindFile 封装了 FindFirstFile() 和 FindNextFile() API,而且比 MFC 的 CFileFind 更好用一点。常见的使用模式是这样的:
CFindFile finder;
CString sPattern = _T("C:\\windows\\*.exe");
if ( finder.FindFirstFile ( sPattern ) )
{
do
{
// act on the file that was found
}
while ( finder.FindNextFile() );
}
finder.Close();
如果 FindFirstFile() 返回 true,则至少有一个文件匹配了条件。在 do 循环的内部,你可以访问公用的 CFindFile 成员 m_fd,那是一个 WIN32_FIND_DATA 结构,其中保存有找到的文件相关的信息。循环会一直持续到 FindNextFile() 返回 false,这表示所有的文件已经枚举完成。
CFindFile 还有以更易于使用的形式从 m_fd 返回数据的方法。这些方法仅当在对 FindFirstFile() 或者 FindNextFile() 进行了成功调用之后才能返回有意义的值。
ULONGLONG GetFileSize()
以 64 位无符号整数的形式返回文件的大小。 BOOL GetFileName(LPTSTR lpstrFileName, int cchLength)
CString GetFileName()
返回找到文件的文件名和扩展名(从 m_fd.cFileName 中复制)。 BOOL GetFilePath(LPTSTR lpstrFilePath, int cchLength)
CString GetFilePath()
返回找到文件的全路径。 BOOL GetFileTitle(LPTSTR lpstrFileTitle, int cchLength)
CString GetFileTitle()
仅返回找到文件的文件标题,也即不带扩展名的文件名。 BOOL GetFileURL(LPTSTR lpstrFileURL, int cchLength)
CString GetFileURL()
创建一个包含文件全路径的 file:// URL。 BOOL GetRoot(LPTSTR lpstrRoot, int cchLength)
CString GetRoot()
返回包含文件的目录。 BOOL GetLastWriteTime(FILETIME* pTimeStamp)
BOOL GetLastAccessTime(FILETIME* pTimeStamp)
BOOL GetCreationTime(FILETIME* pTimeStamp)
这些方法分别从 m_fd 中返回 ftLastWriteTime、ftLastAccessTime 和 ftCreationTime 成员。
CFindFile 还有一些辅助方法,用于检测找到文件的属性。
BOOL IsDots()
如果找到的文件是“.”或者“..”目录则返回 true。 BOOL MatchesMask(DWORD dwMask)
把 dwMask 中的位(都应该是 FILE_ATTRIBUTE_* 常量)与找到文件的属性进行比较。如果 dwMask 中所有的位也都在文件属性中则返回 true。 BOOL IsReadOnly()
BOOL IsDirectory()
BOOL IsCompressed()
BOOL IsSystem()
BOOL IsHidden()
BOOL IsTemporary()
BOOL IsNormal()
BOOL IsArchived()
这些方法是使用某一特定 FILE_ATTRIBUTE_* 位对 MatchesMask() 进行调用的捷径。例如,IsReadOnly() 即 MatchesMask(FILE_ATTRIBUTE_READONLY)。
全局函数
WTL 还有好几个有用的全局函数,你可以用来做诸如 DLL 版本检查以及显示消息框之类的事情。 bool AtlIsOldWindows()
如果操作系统为 Windows 95、98、NT 3 或者 NT 4 则返回真。 HFONT AtlGetDefaultGuiFont()
返回 GetStockObject(DEFAULT_GUI_FONT) 的返回值。在英文(以及其他的使用拉丁字母的单字节语言)Windows 2000 及之后,此字体的字面名称为“MS Shell Dlg”。这在用作对话框字体时是可以的,但如果你在 UI 中要使用自己创建的字体就未必是最佳选择。MS Shell Dlg 是 MS Sans Serif 的一个别名,而不是新的 UI 字体 Tahoma 的别名。为了避免使用 MS Sans Serif,你可以用以下代码获取消息框使用的字体: NONCLIENTMETRICS ncm = { sizeof(NONCLIENTMETRICS) };
CFont font;
if ( SystemParametersInfo ( SPI_GETNONCLIENTMETRICS, 0, &ncm, false ) )
font.CreateFontIndirect ( &ncm.lfMessageFont );
另一个可选的方法是检查由 AtlGetDefaultGuiFont() 返回的字体的字面名称。如果名字为“MS Shell Dlg”,你可以将之改为“MS Shell Dlg 2”,这是关联到 Tahoma 的一个别名。 HFONT AtlCreateBoldFont(HFONT hFont = NULL)
此函数创建一个给定字体的粗体版本。如果 hFont 为 NULL,AtlCreateBoldFont() 创建由 AtlGetDefaultGuiFont() 返回的字体的粗体版本。 BOOL AtlInitCommonControls(DWORD dwFlags)
这是对 InitCommonControlsEx() API 的一个封装。它使用给定的标志初始化一个 INITCOMMONCONTROLSEX 结构,然后调用该 API。 HRESULT AtlGetDllVersion(HINSTANCE hInstDLL, DLLVERSIONINFO* pDllVersionInfo)
HRESULT AtlGetDllVersion(LPCTSTR lpstrDllName, DLLVERSIONINFO* pDllVersionInfo)
这两个函数到给定的模块中寻找名为 DllGetVersion() 的导出函数,如果找到了,就调用之。如果 DllGetVersion() 调用成功,它把版本信息返回到 DLLVERSIONINFO 结构里。 HRESULT AtlGetCommCtrlVersion(LPDWORD pdwMajor, LPDWORD pdwMinor)
返回 comctl32.dll 的主版本和次版本。 HRESULT AtlGetShellVersion(LPDWORD pdwMajor, LPDWORD pdwMinor)
返回 shell32.dll 的主版本和次版本。 bool AtlCompactPath(LPTSTR lpstrOut, LPCTSTR lpstrIn, int cchLen)
截短文件路径使之小于 cchLen 个字符的长度,如果路径太长则在其尾部添加省略号。作用与 shlwapi.dll 中的 PathCompactPath() 和 PathSetDlgItemPath() 函数相仿。 int AtlMessageBox(HWND hWndOwner, _U_STRINGorID message,
_U_STRINGorID title = NULL,
UINT uType = MB_OK | MB_ICONINFORMATION)
像 MessageBox() 一样显示一个消息框,不过使用了 _U_STRINGorID 参数,因此你可以传入字符串资源 ID。AtlMessageBox() 在必要时处理字符串的加载。
宏
在 WTL 的头文件中你可以看到,引用了许多预处理宏。其中大多数可以在编译设置中设置以改变 WTL 代码的行为。
这些宏由构建设置预定义或者预设置,你可以在整个 WTL 代码中看到它们:
_WTL_VER
- 对于 WTL 7.1 被定义为
0x0710。
_ATL_MIN_CRT
- 如果定义了的话,ATL 不会链接 C 运行时库。因为有的 WTL 类(特别是
CString)通常需要使用 CRT 函数,因此会有用以代替从 CRT 导入的代码的特定代码被编译进来。
_ATL_VER
- 对于 VC 6 被预定义为
0x0300,VC 7 为 0x0700,VC 8 为 0x0800。
_WIN32_WCE
- 如果当前的编译是用于 Windows CE 则被定义。有的 WTL 代码在相应的特性在 CE 上不可用时会被禁用。
下列宏缺省并不定义。要使用它们的话,应该在 stdafx.h 文件中所有的 #include 语句前 #define。
_ATL_NO_OLD_NAMES
- 此宏仅在你维护 WTL 3 的代码时有用。它添加了几个编译器的指令以识别两个旧的类名:
CUpdateUIObject 成为 CIdleHandler,而 DoUpdate() 成为 OnIdle()。
_ATL_USE_CSTRING_FLOAT
- 定义此符号可以启用
CString 中的浮点支持,此时 _ATL_MIN_CRT 必须没有被定义。如果你计划在传递给 CString::Format() 的格式化字符串中使用 %I64 前缀的话你就需要定义此符号。定义 _ATL_USE_CSTRING_FLOAT 会导致 CString::Format() 调用 _vstprintf(),后者可以理解 %I64 前缀。
_ATL_USE_DDX_FLOAT
- 定义此符号可以启用 DDX 代码中的浮点支持,
_ATL_MIN_CRT 也必须没有被定义。
_ATL_NO_MSIMG
- 定义此符号可以阻止编译器看到
#pragma comment(lib, "msimg32") 指令,同时也禁用了 CDCT 中使用了 msimg32 以下函数 —— AlphaBlend()、TransparentBlt()、GradientFill() —— 的代码。
_ATL_NO_OPENGL
- 定义此符号可以阻止编译器看到
#pragma comment(lib, "opengl32") 指令,同时也禁用了 CDCT 中使用了 OpenGL 的代码。
_WTL_FORWARD_DECLARE_CSTRING
- 已经过时,使用
_WTL_USE_CSTRING 代替之。
_WTL_USE_CSTRING
- 定义此符号可以前向声明
CString。这样,那些通常包含于 atlmisc.h 之前的头文件之中的代码就也可以使用 CString 了。
_WTL_NO_CSTRING
- 定义此符号可以禁止使用
WTL::CString。
_WTL_NO_AUTOMATIC_NAMESPACE
- 定义此符号以阻止自动执行
using namespace WTL 指令。
_WTL_NO_AUTO_THEME
- 定义此符号可以禁止
CMDICommandBarCtrlImpl 使用 XP 主题。
_WTL_NEW_PAGE_NOTIFY_HANDLERS
- 定义此符号可以在
CPropertyPage 中使用更新的 PSN_* 通知处理器。由于老的 WTL 3 处理器已经过时,因此应该总是定义此符号,除非你在维护不能更新 WTL 3 代码。
_WTL_NO_WTYPES
- 定义此符号可以禁止定义 WTL 版本的
CSize,CPoint 和 CRect。
_WTL_NO_THEME_DELAYLOAD
- 当使用 VC 6 编译时,定义此符号可以禁止 uxtheme.dll 被自动标记为延迟加载的 DLL。
注意:如果既没有定义 _WTL_USE_CSTRING 也没有定义 _WTL_NO_CSTRING,那么 CString 可以在包含了 atlmisc.h 之后的任何地方使用。
示例工程
本文的示例工程为一个名为 Kibbles 的下载器应用程序,它演示了本文介绍到的许多个类。它使用了在 Windows 2000 及其之后的系统中可以得到的 BITS(后台智能传输服务)组件。由于此应用只能运行在基于 NT 的操作系统上,所以我把它做成了一个 Unicode 工程。
应用的一个视图窗口显示了下载进度,使用了若干个 GDI 调用,包括绘制了饼状图表的 Pie()。应用首次运行时,你可以看到其初始状态的 UI:
![[Kibbles initial state - 20K]](http://tkfiles.storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwcTPRS5bJTqUPoCoRX108Ddbr5JQumtPsOuahOrxzeVMex2bwyn6o5DK5wVAR-6P2xlj6AshU2YTnvRqPPX2C-m0b0etIqp6Bzff4elKWO_A)
你可以从浏览器中拖动一个链接到窗口中以创建一个新的 BITS 任务,该任务将把链接的目标下载到你的“我的文档”文件夹。你也可以点击第三个工具栏按钮来添加你想用的任意 URL。第四个按钮让你可以改变缺省的下载目录。
当下载任务正在进行时,Kibbles 会显示此任务的一些细节,并像如下这样显示下载进度:
![[Kibbles downloading - 22K]](http://tkfiles.storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJyp0WE_02pRJDnwOEP10AdHz1blD6tk_9P6FBNiTH0Jicd5a-A3G8QEAJ9d5gMEJcHLHOl3gXUxBQuTZeLYHjeAYPlGf8bHWTUKAnpyVttC1A)
工具栏上的前两个按钮可以让你改变进度显示中使用的颜色。第一个按钮打开一个选项对话框,在那儿你可以为显示的不同部分分别设置颜色:
![[Colors options dlg - 13K]](http://tkfiles.storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwELTmquC7Vt57ytEMADRKadcfjQXBKkvE5y-Yzf_TGEv91PzqiOpqx_shqGUFMHDBNUJA0WmXhHtcKJ-EDmK5zoo8efXlg8J5JSttpTc9B9A)
对话框使用的精品按钮类来自于 Tim Smith 的文章 Color Picker for WTL with XP themes。查看 Kibbles 工程中的 CChooseColorsDlg 类可以看到其工作原理。Text color 按钮是一个普通按钮,OnChooseTextColor()处理器演示了如何使用 WTL 的 CColorDialog 类。第二个工具栏按钮会把所有的颜色改变为随机值。
第五个按钮可以让你设置一个背景图片,它会被绘制到显示已经下载了多少的饼图里。缺省的图片是作为资源包含的,但如果你的“我的图片”文件夹中有 BMP 文件,你也可以从中选择一个。
![[Danish duck background - 57K]](http://tkfiles.storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwxwkPDVqpmwWZCGLWEclyuobaoEkcMa0T174Fo281a3ylOL-nHlrhuGO2VVsncfpOMkMx3A4VQYb0S3Id3uCrrzbFp8pAkEgjXWEI05H5wvA)
CMainFrame::OnToolbarDropdown() 包含了处理按钮按下事件并显示一个弹出菜单的代码。此函数还使用 CFindFile 来枚举“我的图片”目录下的内容。你可以到 CKibblesView::OnPaint() 中去看完成绘制 UI 的各种 GDI 操作。
关于工具栏的一个重要提示:工具栏使用了一个 256 色的位图,但是 VC 的工具栏编辑器仅能针对 16 色的位图工作。如果你使用编辑器编辑工具条,VC 会把位图减少到 16 色。我的建议是,把高色版本的位图存放到另一个目录里,用图形程序直接对它进行修改,然后保存一个 256 色的版本到 res 目录。
Copyright and License
This article is copyrighted material, (c)2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.
With the exception of ColorButton.cpp and ColorButton.h, the demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.
The files ColorButton.cpp and ColorButton.h come from Color Picker for WTL with XP themes by Tim Smith. They are not covered by the above license statement; see the comments in those files for their license.
修订历史
2006 年 1 月 8 日,首次发布。 | 链接: 上一部分; 下一部分 4/28/2006 加载位图在 Windows 平台下是最稀松平常不过的事情。在最开始的设计中,Windows 希望可执行文件是一个自包容的个体,所以设计的 API 几乎都是只针对模块体内自带的资源的。后来终于发现大家对界面美化的需求日益迫切,尤其是经常需要对外部的位图文件进行加载,所以又另外增加了一个 API,既可以加载可执行模块内部的资源,又可以加载外部的资源,资源类型限于图标、光标和位图,这个 API 就是 LoadImage。
前两天在对 32 位位图进行研究时发现,LoadImage 竟然不能加载 32 位的位图文件(不过加载 32 位的位图资源是可以的)。网上也有人提出了此问题(http://groups.google.com/group/microsoft.public.win32.programmer.gdi/browse_thread/thread/9651a931dedd4acf/935122f52374326c%23935122f52374326c),一个来自于微软的的技术人员声称“应该”是可以的,但事实是老汉又遇到了同样的问题。好像以前也曾经遇到过 32 位位图的加载问题,不过姑且不论解决办法,连是否找到了解决办法都已经忘怀了。
幸好,32 位位图是位图格式里最简单的一个,所以自力更生也不需大费周折,于是有了下面的代码。为了方便地进行文件读写,还写了一个简单的文件映射类,该映射类的用于共享内存目的的构造函数没有写,谁要用的话,请自行补充完整。
注意,代码没有对位图文件中可能存在的 ICM 信息进行处理。
class CFileMapping
{
protected:
HANDLE _hFile;
HANDLE _hFileMapping;
PVOID _pvView;
public:
// Ctor for real file mapping object
CFileMapping(LPCTSTR pszFile, DWORD dwAccess = FILE_GENERIC_READ | FILE_GENERIC_WRITE,
DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE, PSECURITY_ATTRIBUTES psa = NULL,
BOOL bMap = TRUE, LPCTSTR pszObject = NULL)
: _hFile(NULL), _hFileMapping(NULL), _pvView(NULL)
{
_hFile = CreateFile(pszFile, dwAccess, dwShareMode, psa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(!_hFile)
return;
_hFileMapping = CreateFileMapping(_hFile, psa, PAGE_READWRITE, 0, 0, pszObject);
if(_hFileMapping && bMap)
_pvView = Map();
}
// Ctor for shared memory object, not implemented, your duty? :-)
CFileMapping(DWORD dwMaximumSizeLow, DWORD dwMaximumSizeHigh = 0, LPCTSTR pszObject = NULL)
: _hFile(NULL), _hFileMapping(NULL), _pvView(NULL)
{
}
~CFileMapping()
{
Unmap();
if(_hFileMapping)
CloseHandle(_hFileMapping);
if(_hFile && _hFile != INVALID_HANDLE_VALUE)
CloseHandle(_hFileMapping);
}
PVOID Map(SIZE_T dwNumberOfBytesToMap = 0, DWORD dwFileOffsetLow = 0, DWORD dwFileOffsetHigh = 0,
DWORD dwAccess = FILE_MAP_ALL_ACCESS, PVOID pvBaseAddress = NULL)
{
Unmap();
if(_hFileMapping == NULL)
return NULL;
_pvView = MapViewOfFileEx(_hFileMapping, dwAccess, dwFileOffsetHigh, dwFileOffsetLow,
dwNumberOfBytesToMap, pvBaseAddress);
return _pvView;
}
void Unmap()
{
if(_pvView)
{
UnmapViewOfFile(_pvView);
_pvView = NULL;
}
}
BOOL IsMapped()
{
return _pvView != NULL;
}
PVOID GetView()
{
return _pvView;
}
};
HBITMAP WINAPI LoadBitmap32Bits(LPCTSTR pszFile)
{
CFileMapping fm(pszFile);
PBYTE pBytes = (PBYTE)fm.GetView();
if(!pBytes)
return NULL;
PBITMAPFILEHEADER pbfh = (PBITMAPFILEHEADER)pBytes;
PBITMAPINFOHEADER pbih = (PBITMAPINFOHEADER)(pBytes + sizeof(BITMAPFILEHEADER));
if(pbih->biBitCount != 32)
return NULL;
PBYTE pBits = NULL;
HBITMAP hbmp = CreateDIBSection(NULL, (PBITMAPINFO)pbih, DIB_RGB_COLORS, (PVOID*)&pBits, NULL, 0);
if(!hbmp || !pBits)
return NULL;
pBytes += (pbfh->bfOffBits ? pbfh->bfOffBits : (sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)));
SIZE_T size = pbih->biWidth * pbih->biHeight * 4;
CopyMemory(pBits, pBytes, size);
return hbmp;
}3/8/2006
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分;下一部分
第八部分 - 属性表和向导
|
内容
- 简介
- WTL 属性表类
- WTL 属性页类
- CPropertyPageWindow 的方法
- CPropertyPageImpl 的方法
- 处理通知消息
- 创建一个属性表
- 永远最简单的属性表
- 创建一个有用的属性页
- 创建一个更好的属性表类
创建一个向导
其他的 UI 考虑
下一步
修订历史
简介
甚至于在 Windows 95 把属性表引入为公用控件之前,它就已经成为了呈现选项的一种颇为流行的方法了。向导通常用于指导用户通历软件的安装过程或者其他的复杂工作。WTL 对创建这两种类型的属性表都提供了良好的支持,并允许你使用前 面介绍过的所有的那些对话框相关的特性,比如说 DDX 和 DDV。在本章里,我会演示创建一个基本的属性表和向导,以及如何处理由表发送出的事件和通知消息。
WTL 属性表类
有两个类,CPropertySheetWindow 和 CPropertySheetImpl,它们组合起来实现了属性表。它们都定义在 atldlgs.h 头文件中。CPropertySheetWindow 是一个窗口接口类,也就是说,它派生于 CWindow,而 CPropertySheetImpl 具有消息映射并实际上实现了窗口功能。这与基本的 ATL 窗口类是一致的,在其中 CWindow 和 CWindowImpl 也被一起使用。
CPropertySheetWindow 包括了对多个 PSM_* 消息的封装,比如 SetActivePageByID() 就封装了 PSM_SETCURSELID。CPropertySheetImpl 管理着一个 PROPSHEETHEADER 结构以及一组 HPROPSHEETPAGE。CPropertySheetImpl 中还有一些方法,用于设置某些 PROPSHEETHEADER 域,添加以及删除页。你也可以通过访问 m_psh 成员变量来获得对 PROPSHEETHEADER 的直接访问。
最后要说明的是,CPropertySheet 是 CPropertySheetImpl 的一个特化,如果你根本不需要对属性表进行定制的话,那你就可以使用它。
CPropertySheetImpl 的方法
下面是 CPropertySheetImpl 的一些重要的方法。因为许多方法仅仅是对窗口消息的封装,我不会在这儿列一个详尽的清单,但你可以到 atldlgs.h 里查看方法的完整列表。 CPropertySheetImpl(_U_STRINGorID title = (LPCTSTR) NULL,
UINT uStartPage = 0, HWND hWndParent = NULL)
CPropertySheetImpl 的构造函数允许你当下就指定一些常用的属性,这样你就不必在以后调用别的方法来设置它们。title 指定了在属性表的标题上用到的文字。_U_STRINGorID 是一个 WTL 辅助类,可以使你传递 LPCTSTR 或者字符串资源 ID。例如,如果 IDS_SHEET_TITLE 是字符串表中某个字符串的 ID 的话,那么下面这两行就都可以工作:
CPropertySheetImpl mySheet ( IDS_SHEET_TITLE );
CPropertySheetImpl mySheet ( _T("My prop sheet") );
uStartPage 是从零开始的页面索引值,当属性表第一次显示时,该页就会被激活。hWndParent 设置了属性表的父窗口。
BOOL AddPage(HPROPSHEETPAGE hPage)
BOOL AddPage(LPCPROPSHEETPAGE pPage)
向属性表中添加一个属性页。如果该页已经创建的话,你可以将其句柄(一个 HPROPSHEETPAGE)传递给第一个重载版本的函数。更常见的方法是使用第二个重载版本。使用这一版本,你可以先设置一个 PROPSHEETPAGE 结构(此事可以由 CPropertyPageImpl 来做,后文会有介绍),而 CPropertySheetImpl 会为你创建并管理该页。 BOOL RemovePage(HPROPSHEETPAGE hPage)
BOOL RemovePage(int nPageIndex)
从属性表中删除一个属性页。你既可以传递属性页的句柄也可以传递基于零的索引值。 BOOL SetActivePage(HPROPSHEETPAGE hPage)
BOOL SetActivePage(int nPageIndex)
设置属性表的活动属性页。你既可以传递要激活的页的句柄,也可以传递基于零的索引。你也可以在创建属性表之前调用此方法,以设置属性表首次显示时要激活哪一页。 void SetTitle(LPCTSTR lpszText, UINT nStyle = 0)
设置属性表标题使用的文字。nStyle 可以是 0 或者 PSH_PROPTITLE。如果是 PSH_PROPTITLE,则此风格会被加到属性表上,就会使“Properties for”字样附加到你在 lpszText 参数里传入的文本之前。 void SetWizardMode()
设置 PSH_WIZARD 风格,这会使得属性表变成一个向导。你必须在显示属性表之前调用此方法。 void EnableHelp()
设置 PSH_HASHELP 风格,它会为属性表加入帮助按钮。注意,你还需要在为此按钮提供帮助的各个页中启用帮助才能有效。 INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow())
创建并显示一个模态属性表。正的返回值表示成功,对于返回值的完整描述可以参看 PropertySheet() API 的文档。如果有错误发生且属性表没有创建成功,DoModal() 会返回 -1。 HWND Create(HWND hWndParent = NULL)
创建并显示一个非模态的属性表,并返回其窗口句柄。如果发生了错误,属性表就创建不出来,Create() 会返回 NULL。
WTL 属性页类
和属性表类相似,此 WTL 类封装了属性页的相关工作,也是既有一个窗口接口类,CPropertyPageWindow,又有一个实现类 CPropertyPageImpl。CPropertyPageWindow 非常小,其包含的大部分辅助函数都调用了父表中的方法。
CPropertyPageImpl 派生于 CDialogImplBaseT,这是因为一个属性页是用一个对话框资源来构建的。这也意味着,我们在对话框里所用到的所有 WTL 特性在属性表中都是可用的,比如说 DDX 和 DDV。CPropertyPageImpl 有两个主要的目的:管理保存在成员变量 m_psp 中的 PROPSHEETPAGE 结构,并处理 PSN_* 通知消息。对于非常简单的属性页,你可以使用 CPropertyPage 类。这仅仅适用于那些根本不与用户交互的页,比方说一个 About 页,或者是向导里的简介页面。
你还可以创建掌控 ActiveX 控件的页。首先,你要把 atlhost.h 包含到 stdafx.h 中去,而对于该页,你要使用 CAxPropertyPageImpl 代替 CPropertyPageImpl。对于要掌控 ActiveX 控件的简单页,你可以使用 CAxPropertyPage 来代替 CPropertyPage。
CPropertyPageWindow 的方法
CPropertyPageWindow 最重要的方法是 GetPropertySheet():
CPropertySheetWindow GetPropertySheet()
此方法获取属性页的父窗口(也即属性表)的 HWND 并将之关联到一个 CPropertySheetWindow 上。再把新的 CPropertySheetWindow 返回给调用者。请注意,此处只是创建一个临时对象,它并不是返回用来创建属性表的实际的 CPropertySheet 或者 CPropertySheetImpl 对象的指针或者是引用。如果你是使用自己的 CPropertySheetImpl 派生类,而又要在属性表对象中访问数据成员的话,这一点就很重要。
剩下的成员仅仅是调用封装了 PSM_* 消息的 CPropertySheetWindow 函数: BOOL Apply()
void CancelToClose()
void SetModified(BOOL bChanged = TRUE)
LRESULT QuerySiblings(WPARAM wParam, LPARAM lParam)
void RebootSystem()
void RestartWindows()
void SetWizardButtons(DWORD dwFlags)
例如,在 CPropertyPageImpl 派生类里,你可以调用: SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH );
用以代替: CPropertySheetWindow wndSheet;
wndSheet = GetPropertySheet();
wndSheet.SetWizardButtons ( PSWIZB_BACK | PSWIZB_FINISH );
CPropertyPageImpl 的方法
CPropertyPageImpl 管理着一个 PROPSHEETPAGE 结构,即其公用成员 m_psp。CPropertyPageImpl 还有一个 operator PROPSHEETPAGE* 转换器,因此你可以把一个 CPropertyPageImpl 传递到接受 LPPROPSHEETPAGE 或者 LPCPROPSHEETPAGE 参数的方法里,比如 CPropertySheetImpl::AddPage()。
CPropertyPageImpl 的构造函数允许你设置页的标题,也即出现在页的标签上的文字:
CPropertyPageImpl(_U_STRINGorID title = (LPCTSTR) NULL)
如果你需要手动创建一个页,而不是让属性表来干这件事的话,你可以调用 Create(): HPROPSHEETPAGE Create()
Create() 只不过是使用 m_psp 作为参数调用了 CreatePropertySheetPage()。你只有在以下情况下才需要调用 Create(),或者是在属性表创建之后需要向其上添加一个页,或者是要把创建的页传递给其他不受你控制的属性表,例如,一个属性表处理器的外壳扩展。
还有三个方法,可以设置页面内的几处标题文字: void SetTitle(_U_STRINGorID title)
void SetHeaderTitle(LPCTSTR lpstrHeaderTitle)
void SetHeaderSubTitle(LPCTSTR lpstrHeaderSubTitle)
第一个函数用于改变该页的标签上的文字。其他的两个用在 Wizard97 风格的向导中,用于设置属性页上方的题头区域内的文字。 void EnableHelp()
在 m_psp 中设置 PSP_HASHELP 标志,当页面激活时即可以启用 Help 按钮。
处理通知消息
CPropertyPageImpl 中有一个消息映射,它处理了 WM_NOTIFY。如果通知代码是一个 PSN_* 值,OnNotify() 会调用用于此通知的特定的处理器。这由编译时虚函数技术完成,因而在派生类里可以很轻易地覆盖该处理器。
共有两组通知处理器,这是由于 WTL 3 和 7 之间出现了设计上的变化。在 WTL 3 里,通知处理器会返回一个不同于 PSN_* 消息的返回值的值。比如,WTL 3 中 PSN_WIZFINISH 的处理器: case PSN_WIZFINISH:
lResult = !pT->OnWizardFinish();
break;
OnWizardFinish() 希望返回 TRUE 以允许向导结束,或者返回 FALSE 来阻止向导的关闭;但是,由于 IE 5 的公用控件添入了可以从 PSN_WIZFINISH 的处理器返回一个窗口句柄的能力用以设置焦点,所以这就行不通了。WTL 3 的应用不能使用此特性,因为所有的非零值都被认为是一样的。
在 WTL 7 里,OnNotify() 不会改变从 PSN_* 处理器返回的任何值。处理器可以返回任何文档化的合法值,因而其行为也就完全正常了。但是,出于后向兼容的考虑,WTL 3 的处理器仍然存在并且被缺省使用。要使用 WTL 7 的处理器,你必须把下列行添加到 stdafx.h 中,而且位于 atldlgs.h 的包含语句之前: #define _WTL_NEW_PAGE_NOTIFY_HANDLERS
在写新代码的时候,显然没有什么理由不使用 WTL 7 的处理器,所以在这就不介绍 WTL 3 的处理器了。
CPropertyPageImpl 对所有的通知都有缺省的处理器,所以你可以只覆盖和你的程序相关的那些处理器。缺省处理器及其行为如下:
int OnSetActive() - 允许属性页成为活动的
BOOL OnKillActive() - 允许属性页成为非活动的
int OnApply() - 返回表示应用操作已成功的 PSNRET_NOERROR
void OnReset() - 无操作
BOOL OnQueryCancel() - 允许取消操作
int OnWizardBack() - 到上一页
int OnWizardNext() - 到下一页
INT_PTR OnWizardFinish() - 允许向导结束
void OnHelp() - 无操作
BOOL OnGetObject(LPNMOBJECTNOTIFY lpObjectNotify) - 无操作
int OnTranslateAccelerator(LPMSG lpMsg) - 返回表示消息没有被处理的 PSNRET_NOERROR
HWND OnQueryInitialFocus(HWND hWndFocus) - 返回 NULL 以把焦点设置到 Tab 顺序里的第一个控件上
创建一个属性表
现在,我们有关类的介绍就结束了,我们需要有一个程序来演示如何使用它们。本章的示例工程是一个简单的 SDI 应用,它在其客户区要显示一个图片,并使用颜色填充背景。图片和颜色可以通过选项对话框(一个属性表)以及一个向导(后文叙述)来更改。
永远最简单的属性表
使用 WTL AppWizard 生成一个 SDI 工程后,我们就可以开始创建用于 About 框的属性表了。我们首先从向导为我们生成的 about 对话框开始,要改变其风格才能使它像一个属性页一样工作。
第一步是移除 OK 按钮,在属性表里它没有任何意义。在对话框的属性里,将 Style 改为 Child,将 Border 改为 Thin,并选中 Disabled。
第二步,也是最后一步,是在 OnAppAbout() 处理器中创建属性表。我们可以使用不可定制的 CPropertySheet 和 CPropertyPage 来做这件事: void CMainFrame::OnAppAbout(...)
{
CPropertySheet sheet ( _T("About PSheets") );
CPropertyPage<IDD_ABOUTBOX> pgAbout;
sheet.AddPage ( pgAbout );
sheet.DoModal ( *this );
}
结果看起来是这样的:
![[Simple sheet - 27K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU1PwIumtjwl4rtlDj8D3XQS4c1RkPKdz51snBn6R2PMuyhWbBI36O1QqdboJrqxk4jilE8CXHnrP2Qn1PLE8oWW4rzE78th8fWt8CqIWE2KmieIYelem9-A)
创建一个有用的属性页
因为并不是每个属性表的属性页都和 About 框一样简单,所以大部分的页都会需要是一个 CPropertyPageImpl 的派生类,所以我们现在就来看一下这样的一个类。我们要创建一个新的属性页,其中包含了显示在客户区背景中的图象的设置。对话框如下:
![[Background options - 4K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxBJPooIMo1RJRYumfkj2Is8FjY_WhBQC6nktyNjnhDxw3KAjusQ8Mol9_RNSp-gGmg7GlAMi2PPxHNqu-VcZmkMN3Qdkv3Q2rpz4FxOS4OJA)
此对话框和 About 页的风格一样。对于此页,我们需要一个新类,将其命名为 CBackgroundOptsPage。此类派生于 CPropertyPageImpl,因为它毕竟是一个属性页,同时也派生于 CWinDataExchange,这样可以启用 DDX。 class CBackgroundOptsPage :
public CPropertyPageImpl<CBackgroundOptsPage>,
public CWinDataExchange<CBackgroundOptsPage>
{
public:
enum { IDD = IDD_BACKGROUND_OPTS };
// Construction
CBackgroundOptsPage();
~CBackgroundOptsPage();
// Maps
BEGIN_MSG_MAP(CBackgroundOptsPage)
MSG_WM_INITDIALOG(OnInitDialog)
CHAIN_MSG_MAP(CPropertyPageImpl<CBackgroundOptsPage>)
END_MSG_MAP()
BEGIN_DDX_MAP(CBackgroundOptsPage)
DDX_RADIO(IDC_BLUE, m_nColor)
DDX_RADIO(IDC_ALYSON, m_nPicture)
END_DDX_MAP()
// Message handlers
BOOL OnInitDialog ( HWND hwndFocus, LPARAM lParam );
// Property page notification handlers
int OnApply();
// DDX variables
int m_nColor, m_nPicture;
};
此类中需要注意的有:
- 其中有一个名为
IDD 的公用成员,里面存放着相关联的对话框资源 ID。
- 其消息映射与
CDialogImpl 类相似。
- 消息映射会把消息串联到
CPropertyPageImpl,从而可以处理属性表相关的消息。
- 其中有一个
OnApply() 处理器,当用户点击属性表上的 OK 时可以保存用户的选择。
OnApply() 相当简单,它调用 DoDataExchange() 来更新 DDX 变量,然后再返回一个代码,指示属性表是否可以关闭:
int CBackgroundOptsPage::OnApply()
{
return DoDataExchange(true) ? PSNRET_NOERROR : PSNRET_INVALID;
}
添加一个 Tools|Options 菜单项,让它来把属性表搬出来,我们把此命令的处理器放到视图类中。此处理器像前面一样创建属性表,不过要把新的 CBackgroundOptsPage 添加到属性表里。 void CPSheetsView::OnOptions ( UINT uCode, int nID, HWND hwndCtrl )
{
CPropertySheet sheet ( _T("PSheets Options"), 0 );
CBackgroundOptsPage pgBackground;
CPropertyPage<IDD_ABOUTBOX> pgAbout;
pgBackground.m_nColor = m_nColor;
pgBackground.m_nPicture = m_nPicture;
sheet.m_psh.dwFlags |= PSH_NOAPPLYNOW|PSH_NOCONTEXTHELP;
sheet.AddPage ( pgBackground );
sheet.AddPage ( pgAbout );
if ( IDOK == sheet.DoModal() )
SetBackgroundOptions ( pgBackground.m_nColor,
pgBackground.m_nPicture );
}
sheet 的构造函数的第二个参数现在是 0,这表示在开始的时候应该看到索引为 0 的页。你可以把此值改为 1 使得属性表出现时首先看到的是 About 页。因为这仅仅是演示代码,我计划偷个懒,让 CBackgroundOptsPage 中连接到单选按钮的变量成为共有的。视图会把当前的选项存放在这些变量里,如果用户点击了属性表的 OK,那就把这些新的值保存起来。
如果用户点击了 OK,DoModal() 会返回 IDOK,于是视图就会使用的新的图片和颜色重绘自己。下面是不同视图的屏幕截图:
![[Strong Bad background - 9K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUzJ6bbJLVOWuxMYFSH-34E4p1LqX5a80u_5FM_obVP_Qt35FWBO4aY-kO51nFErTbW5ZHH4qgEFw0witMFWS5tFC6Rns8i2EBVvZylb7WssN60yU_AfD3Uo)
创建一个更好的属性表类
OnOptions() 处理器创建的属性表确实不错,但是那一大堆设置和初始化代码,却不应该是 CMainFrame 的职责。更好的方法是从 CPropertySheetImpl 派生一个类,由它来处理这些工作。
#include "BackgroundOptsPage.h"
class COptionsSheet : public CPropertySheetImpl<COptionsSheet>
{
public:
// Construction
COptionsSheet ( _U_STRINGorID title = (LPCTSTR) NULL,
UINT uStartPage = 0, HWND hWndParent = NULL );
// Maps
BEGIN_MSG_MAP(COptionsSheet)
CHAIN_MSG_MAP(CPropertySheetImpl<COptionsSheet>)
END_MSG_MAP()
// Property pages
CBackgroundOptsPage m_pgBackground;
CPropertyPage<IDD_ABOUTBOX> m_pgAbout;
};
有了这个类,我们就把诸如表中有哪些页之类的细节移到了属性表自身里。构造函数处理以下事宜:把属性页添加到属性表里,并设置其它必要的标志: COptionsSheet::CAppPropertySheet (
_U_STRINGorID title, UINT uStartPage, HWND hWndParent ) :
CPropertySheetImpl<COptionsSheet> ( title, uStartPage, hWndParent )
{
m_psh.dwFlags |= PSH_NOAPPLYNOW|PSH_NOCONTEXTHELP;
AddPage ( m_pgBackground );
AddPage ( m_pgAbout );
}
这样就使 OnOptions() 处理器变得简单了一点: void CPSheetsView::OnOptions ( UINT uCode, int nID, HWND hwndCtrl )
{
COptionsSheet sheet ( _T("PSheets Options"), 0 );
sheet.m_pgBackground.m_nColor = m_nColor;
sheet.m_pgBackground.m_nPicture = m_nPicture;
if ( IDOK == sheet.DoModal() )
SetBackgroundOptions ( sheet.m_pgBackground.m_nColor,
sheet.m_pgBackground.m_nPicture );
}
创建一个向导
创建一个向导,没什么可惊讶的,和创建属性表很相仿。有一点要多做的工作是启用 Back 和 Next 按钮;像在 MFC 的属性页里那样,你要覆盖 OnSetActive() 并调用 SetWizardButtons() 来启用适当的按钮。我们由一个简单的简介页开始,其 ID 设置为 IDD_WIZARD_INTRO:
![[Intro page - 3K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJywL-quggIHVmDchWcWb3oyN8F-GHBNXleGmP4R6sFjUHOVwsXe8AgqPP-UM80xKoU-L29n7EnsvmgZJeFkjpnQEg_YccUgtAjbDMM-P9TbEw)
留意一下,此页没有标题文字。由于向导中的每个页通常都具有相同的标题,我建议在 CPropertySheetImpl 的构造函数里设置标题,并让每一页都使用相同的字符串资源。这样,就可以只改动一个字符串而使每一页都能反映出来。
此页的实现都在 CWizIntroPage 类里: class CWizIntroPage : public CPropertyPageImpl<CWizIntroPage>
{
public:
enum { IDD = IDD_WIZARD_INTRO };
// Construction
CWizIntroPage();
// Maps
BEGIN_MSG_MAP(COptionsWizard)
CHAIN_MSG_MAP(CPropertyPageImpl<CWizIntroPage>)
END_MSG_MAP()
// Notification handlers
int OnSetActive();
};
其构造函数通过引用一个字符串资源 ID 来设置页的标题: CWizIntroPage::CWizIntroPage() :
CPropertyPageImpl<CWizIntroPage>( IDS_WIZARD_TITLE )
{
}
在本页成为当前页的时候,字符串 IDS_WIZARD_TITLE(“PSheets Options Wizard”)就会出现在向导的标题栏内。OnSetActive() 仅仅启用 Next 按钮: int CWizIntroPage::OnSetActive()
{
SetWizardButtons ( PSWIZB_NEXT );
return 0;
}
为了实现此向导,我们需要创建一个 COptionsWizard 类,并在应用的菜单里添加一个 Tools|Wizard 菜单选项。COptionsWizard 的构造函数与 COptionsSheet 的很相似,在其中,它设置了所有必须的风格位或者标志,并向表中添加了页。 class COptionsWizard : public CPropertySheetImpl<COptionsWizard>
{
public:
// Construction
COptionsWizard ( HWND hWndParent = NULL );
// Maps
BEGIN_MSG_MAP(COptionsWizard)
CHAIN_MSG_MAP(CPropertySheetImpl<COptionsWizard>)
END_MSG_MAP()
// Property pages
CWizIntroPage m_pgIntro;
};
COptionsWizard::COptionsWizard ( HWND hWndParent ) :
CPropertySheetImpl<COptionsWizard> ( 0U, 0, hWndParent )
{
SetWizardMode();
AddPage ( m_pgIntro );
}
接下来,为 Tools|Wizard 菜单写就的处理器如下: void CPSheetsView::OnOptionsWizard ( UINT uCode, int nID, HWND hwndCtrl )
{
COptionsWizard wizard;
wizard.DoModal( GetTopLevelParent() );
}
下面就是运行着的向导:
![[Wizard on intro page - 10K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU80biCXQ0lzd0-pP-VRBNkpW8510_9TxH4c7UoxYZ3fnQBwv1mxG_82ah0IDYbte7ffyYQ4HpAoJo8oOt6uzvC8QMo2w7n60Cf9seAYV9LrA6rDM3ArJ3fs)
添加更多的页,处理 DDV
为了让它成为一个有点用处的向导,我们将给它添加一个新的页,可以设置视图的背景颜色。这一页上还有一个复选框,用于演示处理 DDV 失败并阻止用户继续下去的情况。下面就是这个新页,其 ID 为 IDD_WIZARD_BKCOLOR:
![[Color selection wizard page - 4K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzpnvcH1XGCZK-QK903Flt1lPT4nwCl31EonHmfo7QAO1KyfQddtxDIqq0Dy8IB1Qp2KoSzehfAZu9CLWQBGXn6nZ9UByLifDWp5-Du_tAwfQ)
此页的实现是在 CWizBkColorPage 类里。下面是相关的部分代码: class CWizBkColorPage :
public CPropertyPageImpl<CWizBkColorPage>,
public CWinDataExchange<CWizBkColorPage>
{
public:
// ...
BEGIN_DDX_MAP(CWizBkColorPage)
DDX_RADIO(IDC_BLUE, m_nColor)
DDX_CHECK(IDC_FAIL_DDV, m_bFailDDV)
END_DDX_MAP()
// Notification handlers
int OnSetActive();
BOOL OnKillActive();
// DDX vars
int m_nColor;
protected:
bool m_bFailDDV;
};
OnSetActive() 的工作方式与简介页一样,不过它会把 Back 和 Next 按钮全部启用。OnKillActive() 是一个新的处理器,它首先执行 DDX,然后再检查 m_bFailDDV 的值,如果为 true,也即复选框是被选中的,OnKillActive() 会阻止向导运行到下一页。
int CWizBkColorPage::OnSetActive()
{
SetWizardButtons ( PSWIZB_BACK | PSWIZB_NEXT );
return 0;
}
int CWizBkColorPage::OnKillActive()
{
if ( !DoDataExchange(true) )
return TRUE; // prevent deactivation
if ( m_bFailDDV )
{
MessageBox (
_T("Error box checked, wizard will stay on this page."),
_T("PSheets"), MB_ICONERROR );
return TRUE; // prevent deactivation
}
return FALSE; // allow deactivation
}
注意,在 OnKillActive() 里的逻辑当然也可以放到 OnWizardNext() 里,这两个处理器都可以将向导停留在当前页上。其不同之处在于 OnKillActive() 是在用户点击了 Back 或者 Next 之时被调用,而 OnWizardNext(),正如其名字所昭示的,仅在用户点击 Next 时被调用。OnWizardNext() 还可以用作其它用途,如果某些页是可以跳过的,则它可以将向导直接导向另一个不同的页而不是顺序里的下一页。
示例工程里的向导还有另外的两个页 CWizBkPicturePage 和 CWizFinishPage。因为它们和上述的两页类似,故此处不再赘述,要想得到全部的细节可以参看示例代码。
其他的 UI 考虑
居中属性表
属性页和向导的缺省行为会显示到接近其父窗口的左上角的位置:
![[sheet position - 19K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUzUrc0kfDe_jhwkDwMWeOrsFIba9i-iFSJgLHKIbxnzM_jahI4zbgeb-D7-s2tKTlo9pbHU9_Q7giIHzEM5LRcA6lwVnIa8HPA_cdppjJ27fVB8u6XU0Nc8)
这看起来很随意,幸好我们还有补救的办法。感谢那些在论坛里提供做这件事情的代码的人们,本文的前一个版本使用了一种更为复杂的方法。
属性表类或者向导类可以处理 WM_SHOWWINDOW 消息。WM_SHOWWINDOW 的 wParam 参数是一个布尔值,表示窗口是否要被显示出来。如果 wParam 为 true,而且窗口又是第一次显示,那就调用 CenterWindow()。
下面就是我们可以加入到 COptionsSheet 类里来居中属性表的代码。m_bCentered 成员用于跟踪属性表是否已经居中过了。 class COptionsSheet : public CPropertySheetImpl<COptionsSheet>
{
//...
BEGIN_MSG_MAP(COptionsSheet)
MSG_WM_SHOWWINDOW(OnShowWindow)
CHAIN_MSG_MAP(CPropertySheetImpl<COptionsSheet>)
END_MSG_MAP()
// Message handlers
void OnShowWindow(BOOL bShowing, int nReason);
protected:
bool m_bCentered; // set to false in the ctor
};
void COptionsSheet::OnShowWindow(BOOL bShowing, int nReason)
{
if ( bShowing && !m_bCentered )
{
m_bCentered = true;
CenterWindow ( m_psh.hwndParent );
}
}
为属性页添加图标
要使用尚未被成员函数封装起来的属性表和属性页的特性,你就需要直接访问相关的结构:CPropertySheetImpl 里的 PROPSHEETHEADER 类型的成员 m_psh,或者 CPropertyPageImpl 里的 PROPSHEETPAGE 类型的成员 m_psp。
例如,要为选项属性表的 Background 页添加图标的话,我们就需要添加一个标志,并在该页的 PROPSHEETPAGE 结构里设置几个其他的成员: CBackgroundOptsPage::CBackgroundOptsPage()
{
m_psp.dwFlags |= PSP_USEICONID;
m_psp.pszIcon = MAKEINTRESOURCE(IDI_TABICON);
m_psp.hInstance = _Module.GetResourceInstance();
}
下面是结果:
![[Tab icon - 5K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU792ZnWRvvC-yP0LY3hpVxCu7QANQAdnWz5CDeVgekBh5uRLMk274ANbQggSfVnetve2k18GeDz30sA_noEpQ4teFwcIgNkT2rtBsO7AOkKqirLx7IHw3os)
下一步
在第九部分里,我会介绍 WTL 的辅助类,以及 GDI 对象和公用对话框的封装类。
修订历史
2003 年 9 月 13 日:首次发布 2006 年 1 月 13 日:把选项属性表/向导的代码移到了视图类中。更新了居中属性表一节。
| 链接: 上一部分; 下一部分
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分;下一部分
第七部分 - 分割条窗口
|
内容
- 简介
- WTL 分割条窗口
- 开始示例工程
- 在窗格中创建窗口
- 消息路由
- 窗格容器
- 类
- 基本方法
- 在分割条窗口中使用窗格容器
- 关闭按钮以及消息处理
高级分割条特性
- 嵌套分割条
- 在窗格中使用 ActiveX 控件
- 特殊绘制
窗格容器中的特殊绘制
奖励:状态栏中的进度条
下一步
参考资料
修订历史
简介
自从 Windows 95 的资源管理器以其文件系统的双窗格视图粉墨登场以来,分割条窗口就成了一个流行的 UI 元素。MFC 中有一个复杂而强大的分割条窗口类,但是学会如何使用它却有点困难,而且它关联于文档/视图框架。在本部分里,我将探讨 WTL 的分隔条窗口,与 MFC 的分割条窗口相比没那么复杂。尽管 WTL 分隔条的实现没有 MFC 的特性丰富,但它却极其易于使用和扩展。
本章的示例工程是对 ClipSpy 的重新实现,当然,是使用了 WTL 而不是 MFC。如果你对该程序不熟悉,现在可以先浏览一下相关的文章,因为在这儿我会复制 ClipSpy 中的功能,但不会提供其工作原理的深入解释。本文主要聚焦于分割条窗口而不是剪贴板。
WTL 分割条窗口
在头文件 atlsplit.h 中包括了所有的 WTL 分割条窗口类。共有三个类:CSplitterImpl、CSplitterWindowImpl 和 CSplitterWindowT。下列会解释这些类及其基本的方法。
类
CSplitterImpl 是一个接收两个模板参数的模板类,一个是窗口接口类的名字,一个是表示分隔条方向的布尔值:true 为垂直方向,false 为水平方向。CSplitterImpl 中含有分割条的几乎所有的实现,许多方法都是可覆盖的,因此你可以为分割条提供定制绘制或者其他的效果。CSplitterWindowImpl 从 CWindowImpl 以及 CSplitterImpl 派生而来,不过并没有太多代码,它有一个空的 WM_ERASEBKGND 处理器以及一个改变分割条窗口的大小的 WM_SIZE 处理器。
最后,CSplitterWindowT 派生于 CSplitterImpl 并且提供了窗口类名。如果你不需要任何的定制,则有另外的两个 typedef 可供平常使用而不必用上面的三个类:CSplitterWindow 用于垂直分割条,CHorSplitterWindow 用于水平分割条。
创建一个分割条
因为 CSplitterWindow 派生于 CWindowImpl,所以你可以像创建其它子窗口一样创建分割条。如果分割条在主框架的生命期内一直存在,好比在 ClipSpy 中的那样,那你就可以在 CMainFrame 中添加一个 CSplitterWindow 类型的成员变量。在 CMainFrame::OnCreate() 中,先把分割条作为框架的子窗口创建出来,再把它设置为主框架的客户窗口: LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
// ...
m_wndSplit.Create ( *this, rcDefault );
m_hWndClient = m_wndSplit;
}
创建分割条之后,你可以把窗口关联到它的窗格上,并进行其它必要的初始化工作。
基本方法bool SetSplitterPos(int xyPos = -1, bool bUpdate = true)
int GetSplitterPos()
调用 SetSplitterPos() 可以设置分隔条的位置。位置使用像素单位表示,对于垂直分割条是相对于分割条窗口的顶部边缘,对于水平分割条是相对于分割条窗口的左部边缘。使用缺省值 -1 可以将分割条置于正中,使得两个窗格具有相同的大小。对于 bUpdate 通常应该传入 true 以便在分割条移动之后立刻相应地改变两个窗格的大小。GetSplitterPos() 返回分割条相对于分割条窗口的顶部边缘或者左部边缘的当前位置。(如果是单窗格模式的分割条,GetSplitterPos() 会返回两个窗格同时显示出来后分割条回到的位置)。 bool SetSinglePaneMode(int nPane = SPLIT_PANE_NONE)
int GetSinglePaneMode()
调用 SetSinglePaneMode() 可以在单窗格和双窗格之间改变分割条的模式。在单窗格模式下,仅有一个窗格是可见的,并且分割条也隐藏了起来,这和 MFC 动态分割条的工作方式是一样的(但是没有用于重新调整分割条的小握柄(little gripper handle))。nPane 允许的值有:SPLIT_PANE_LEFT、SPLIT_PANE_RIGHT、SPLIT_PANE_TOP、SPLIT_PANE_BOTTOM 以及 SPLIT_PANE_NONE。前四个表示要显示哪个窗格,例如,传入 SPLIT_PANE_LEFT 会显示左边的窗格并隐藏右边的窗格。传入 SPLIT_PANE_NONE 会把两个窗格全显示出来。GetSinglePaneMode() 返回表示了当前模式的这五个 SPLIT_PANE_* 值之一。 DWORD SetSplitterExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
DWORD GetSplitterExtendedStyle()
分割条窗口还有扩展的风格位,用来在整个分割条窗口改变大小时控制分割条的移动方式。可用的风格有:
SPLIT_PROPORTIONAL:分隔条中的两个窗格一起改变大小
SPLIT_RIGHTALIGNED:当整个分割条改变大小时右窗格保持大小不变,左窗格改变大小
SPLIT_BOTTOMALIGNED:当整个分割条改变大小时下窗格保持大小不变,上窗格改变大小
如果没有指定上述的三个风格之一,那分割条缺省就是左或者上对齐的。如果你将 SPLIT_PROPORTIONAL 和 SPLIT_RIGHTALIGNED/SPLIT_BOTTOMALIGNED 一起传入,则 SPLIT_PROPORTIONAL 优先。
还有一个另外的风格可以控制用户是否可以移动分割条:
SPLIT_NONINTERACTIVE:分隔条既不可移动也不响应鼠标
缺省的扩展风格是 SPLIT_PROPORTIONAL。 bool SetSplitterPane(int nPane, HWND hWnd, bool bUpdate = true)
void SetSplitterPanes(HWND hWndLeftTop, HWND hWndRightBottom, bool bUpdate = true)
HWND GetSplitterPane(int nPane)
调用 SetSplitterPane() 可以为分割条的某个窗格关联一个子窗口。nPane 是 SPLIT_PANE_* 值之一,表示要设置哪一个窗格。hWnd 是子窗口的窗口句柄。你可以通过调用 SetSplitterPanes() 为两个窗格一次性同时设置好子窗口。你通常会采用 bUpdate 的缺省值,告诉分隔条立即改变子窗口的大小以适应窗格的大小。SetSplitterPane() 会返回一个 bool 值,但它仅在你为 nPane 参数传递了非法值的时候才会返回 false。
你可以用 GetSplitterPane() 获取窗格中窗口的 HWND 值。如果没有为窗格关联窗口的话,GetSplitterPane() 会返回 NULL。 bool SetActivePane(int nPane)
int GetActivePane()
SetActivePane() 会把焦点设置到分割条中的窗口之一上。nPane 是 SPLIT_PANE_* 值之一,表示你要把哪一个窗格设置为活动的。它还设置了缺省的活动窗格(后文会解释)。GetActivePane() 检查拥有焦点的窗口,如果该窗口是一个窗格窗口或者是窗格窗口的子窗口,则会返回一个具体代表了是哪一个窗格的 SPLIT_PANE_* 值。如果拥有焦点的窗口不是窗格的子,GetActivePane() 会返回 SPLIT_PANE_NONE。
bool ActivateNextPane(bool bNext = true)
如果分隔条处于单窗格模式下,焦点会被设置到可见窗格上。否则,ActivateNextPane() 使用 GetActivePane() 检查拥有焦点的窗口,如果某个窗格(或者窗格的子)拥有焦点,那么分割条会把焦点设置到另外一个窗格上。否则,ActivateNextPane() 在 bNext 为 true 时会激活左/上窗格,bNext 为 false 时会激活右/下窗格。 bool SetDefaultActivePane(int nPane)
bool SetDefaultActivePane(HWND hWnd)
int GetDefaultActivePane()
使用 SPLIT_PANE_* 值之一或者一个窗口句柄调用 SetDefaultActivePane() 会将该窗格设置为缺省的活动窗格。如果分隔条自身得到了焦点,它接着就会调用 SetFocus() 把焦点设置到缺省的活动窗格上。GetDefaultActivePane() 会返回一个指明了缺省活动窗格的 SPLIT_PANE_* 值。 void GetSystemSettings(bool bUpdate)
GetSystemSettings() 会读取若干的系统设置并相应地设置数据成员。为 bUpdate 参数传入会使分隔条立刻使用新的设置重绘自身。
分割条在创建时会自动调用此函数,所以你不必亲自调用它。不过,你的主框架应该处理 WM_SETTINGCHANGE 消息并将其传递到分隔条,CSplitterWindow 在其 WM_SETTINGCHANGE 处理器中会调用 GetSystemSettings()。
数据成员
分割条的另外一些特性是通过设置 CSplitterWindow 的公用成员来控制的。在 GetSystemSettings() 调用时这些值会被重置。
m_cxySplitBar:对于垂直分割条:此值控制分割条的宽度,缺省值是由 GetSystemMetrics(SM_CXSIZEFRAME) 返回的;对于水平分割条:此值控制水平分割条的高度,缺省值由 GetSystemMetrics(SM_CYSIZEFRAME) 返回。
m_cxyMin:对于垂直分割条:此值控制各窗格的最小宽度。分割条会禁止使窗格的大小小于此像素值的拖动操作。如果分隔条窗口具有 WS_EX_CLIENTEDGE 扩展风格的话,缺省值为 0,否则的话,缺省值为 2*GetSystemMetrics(SM_CXEDGE);对于水平分割条:此值控制各窗格的最小高度,如果分隔条窗口具有 WS_EX_CLIENTEDGE 扩展风格的话,缺省值为 0,否则的话,缺省值为 2*GetSystemMetrics(SM_CYEDGE)。
m_cxyBarEdge:对于垂直分隔条:此值控制绘制于分隔条周边的三维边框的宽度,如果分隔条窗口具有 WS_EX_CLIENTEDGE 扩展风格的话,缺省值为 2*GetSystemMetrics(SM_CXEDGE),否则的话,缺省值为 0;对于水平分隔条:此值控制绘制于分隔条周边的三维边框的高度,如果分隔条窗口具有 WS_EX_CLIENTEDGE 扩展风格的话,缺省值为 2*GetSystemMetrics(SM_CYEDGE),否则的话,缺省值为 0。
m_bFullDrag:如果此成员设置为 true,当拖动分隔条时窗格会立即改变大小,如果为 false,则拖动时仅绘制分隔条的一个影像(ghost image),只要用户释放分隔条后才会改变窗格的大小。缺省值为 SystemParametersInfo(SPI_GETDRAGFULLWINDOWS) 的返回值。
开始示例工程
我们已经有了非常坚实的基础了,现在来看看怎样才能搞出一个包含分隔条的框架窗口来。用 WTL AppWizard 开始一个新的工程,在第一页里,选中 SDI Application 并点击 Next,在第二页,不要选择 Toolbar,也不要选择 Use a view window,就象这样:
![[AppWizard pg 2 - 22K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJznCRpVZMIVDlLfX7judRMiRe4EtZNXnepZwPMaBSa7WmubNl_QD8b6_wx9I9J86M5SvTqd8uCNCiLjdaQA-V8FvtfE7alg8uORcdqj9cHEwg)
![[VC7 AppWizard - 23K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUweMqGCZkibjCvWusqpwWCSJZ_pC6I__M0z_oSiGsn20yeJEtkfgMqaQDHOjbJi_msFtz_qhkn0h8NJZ5mNt38SPwBNo7BMQIblom8s5ybXpMjrsCtgb8aY)
我们不需要视图窗口,因为分割条以及其窗格会成为“视图”。在 CMainFrame 里添加一个 CSplitterWindow 成员: class CMainFrame : public ...
{
//...
protected:
CSplitterWindow m_wndVertSplit;
};
然后在 OnCreate() 里,创建该分隔条并将其设置为视图窗口: LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
// Create the splitter window
m_wndVertSplit.Create ( *this, rcDefault, NULL,
0, WS_EX_CLIENTEDGE );
// Set the splitter as the client area window, and resize
// the splitter to match the frame size.
m_hWndClient = m_wndVertSplit;
UpdateLayout();
// Position the splitter bar.
m_wndVertSplit.SetSplitterPos ( 200 );
return 0;
}
请注意,在设置分隔条的位置之前你需要设置 m_hWndClient 并调用 CFrameWindowImpl::UpdateLayout()。UpdateLayout() 会把分隔条窗口的大小调整为自己的初始尺寸。如果你忽略了此步骤,分割条的大小就失去了控制,其宽度可能会比 200 个像素还小,最终导致 SetSplitterPos() 起不到你想要的效果。
取代调用 UpdateLayout() 的另一个方法是先获取框架窗口的 RECT,在创建分隔条时使用此取得的 RECT 而不是 rcDefault。这样,你就在正确的初始位置上创建了分隔条,而后续需要处理位置的方法(例如 SetSplitterPos())也都可以正常工作。
如果你现在就运行此应用,你就可以看到分割条在工作。即使不在窗格里创建任何东西,基本的功能也仍然可用。你可以拖动分割条,或者通过双击把分割条移到正中。
![[Empty splitter - 4K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwhFSSLM0TgMJvGZ8Ii6gdLfTnWBQ6UEohl6JmPygboeEkG-WUzoF8y8CU5DSgTqPLRAaAqPEUJqjTwbYCvRw0X-HjHsNmadpdlzU0JtDLZXA)
为了演示管理窗格窗口的不同方法,我将使用一个 CListViewCtrl 的派生类以及一个普通的 CRichEditCtrl。下面是从我们用作左窗格的 CClipSpyListCtrl 类中取得的一个片断: typedef CWinTraitsOR<LVS_REPORT | LVS_SINGLESEL | LVS_NOSORTHEADER>
CListTraits;
class CClipSpyListCtrl :
public CWindowImpl<CClipSpyListCtrl, CListViewCtrl, CListTraits>,
public CCustomDraw<CClipSpyListCtrl>
{
public:
DECLARE_WND_SUPERCLASS(NULL, WC_LISTVIEW)
BEGIN_MSG_MAP(CClipSpyListCtrl)
MSG_WM_CHANGECBCHAIN(OnChangeCBChain)
MSG_WM_DRAWCLIPBOARD(OnDrawClipboard)
MSG_WM_DESTROY(OnDestroy)
CHAIN_MSG_MAP_ALT(CCustomDraw<CClipSpyListCtrl>, 1)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
//...
};
如果你从之前的系列文章一路读来,那你阅读此类就不应该有任何问题。它处理 WM_CHANGECBCHAIN 以得知其他剪贴板浏览器的来去,处理 WM_DRAWCLIPBOARD 以得知何时剪贴板的内容发生了变化。
由于窗格窗口在应用的生命期内一直存在,所以我们也可以为它们在 CMainFrame 中使用成员变量: class CMainFrame : public ...
{
//...
protected:
CSplitterWindow m_wndVertSplit;
CClipSpyListCtrl m_wndFormatList;
CRichEditCtrl m_wndDataViewer;
};
在窗格中创建窗口
现在我们已经有了用于分隔条和窗格的成员变量,填充分隔条就是一件简单的事情了。分隔条窗口创建之后,我们就可以把分隔条作为父窗口来创建两个子窗口了: LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
// Create the splitter window
m_wndVertSplit.Create ( *this, rcDefault, NULL,
0, WS_EX_CLIENTEDGE );
// Create the left pane (list of clip formats)
m_wndFormatList.Create ( m_wndVertSplit, rcDefault );
// Create the right pane (rich edit ctrl)
DWORD dwRichEditStyle =
WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |
ES_READONLY | ES_AUTOHSCROLL | ES_AUTOVSCROLL | ES_MULTILINE;
m_wndDataViewer.Create ( m_wndVertSplit, rcDefault,
NULL, dwRichEditStyle );
m_wndDataViewer.SetFont ( AtlGetStockFont(ANSI_FIXED_FONT) );
// Set the splitter as the client area window, and resize
// the splitter to match the frame size.
m_hWndClient = m_wndVertSplit;
UpdateLayout();
m_wndVertSplit.SetSplitterPos ( 200 );
return 0;
}
可以看到,两个 Create() 调用都把 m_wndVertSplit 作为了父窗口。RECT 参数不重要,因为分隔条在需要时会重新改变两个窗格窗口的大小,所以我们可以使用 CWindow::rcDefault。
最后一步是把窗格的 HWND 传递给分隔条。这也需要放在 UpdateLayout() 之前,以使所有的窗口都可以得到正确的大小。 LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
m_wndDataViewer.SetFont ( AtlGetStockFont(ANSI_FIXED_FONT) );
// Set up the splitter panes
m_wndVertSplit.SetSplitterPanes ( m_wndFormatList, m_wndDataViewer );
// Set the splitter as the client area window, and resize
// the splitter to match the frame size.
m_hWndClient = m_wndVertSplit;
UpdateLayout();
m_wndVertSplit.SetSplitterPos ( 200 );
return 0;
}
下面就是结果,列表控件里已经添加了几列:
![[Splitter w/panes - 4K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwHOYRdaDM_c0VzIJKdx8-o31BHmVNbDKCqVBmXKVLLOj3zwJCdmiBBXILkz0tAJdJqxI7AdySu4Fu2WpemLklS2zEUb6V2lMR95N1REXTFSQ)
注意,分隔条并不限制什么窗口才能放到窗格里,不像 MFC 总是假定你使用的是 CView。窗格窗口至少要有 WS_CHILD 风格,但除此之外再使用什么东西,你是有相当大的自由空间的。
WS_EX_CLIENTEDGE 的影响
接下来的少许不太重要的话题是关于把 WS_EX_CLIENTEDGE 风格应用到分割条和窗格窗口上时的影响的。我们可以把此风格应用到三个地方:主框架、分割条窗口或者分割条窗格中的窗口。每种情况下 WS_EX_CLIENTEDGE 都会创建出一个不同的样子来,因此我在下面用图示来说明。
WS_EX_CLIENTEDGE 应用于框架窗口: 这是最不起眼的一种选择,因为分割条窗口(splitter)的边框(border)具有了沿起(edge),而分割条(bar)却没有。
![[Client edge style on main frame (XP) - 7K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU_QN-mv_30zF4rJ1LXKUdSqAzIVrpOpO3KVEawuZISp7FM9ZhVeLa9EgKQbg5gmk0QiKj1v80GfVrlayhtUneZB6jLz2zV2BRThPBjcVz9toI7LeKHoHhHk)
WS_EX_CLIENTEDGE 应用于分割条窗口: 当一个 CSplitterWindow 拥有 WS_EX_CLIENTEDGE 风格时,其绘制代码执行了额外的步骤,沿着分割条(bar)的每条边绘制一个边框。因此,就像整个分割条窗口的周围一样,每个窗格附近都有一个沿起(edge)。
![[Client edge style on splitter (XP) - 7K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUwalycXvk2HrcEzDfYfKG8um3YeMnFBuVJZJHj-4eFqWBFVVvJlhmxSQOrdtGIXWL272em9wtHIp9Z_ClIDG84jqnzOdmOTmE2PC-ya_pWLf2gvyMltJkkU)
WS_EX_CLIENTEDGE 应用于窗格窗口: 每个窗格窗口都有边框,分割条并入到了框架窗口的菜单和边框中,没有任何的破绽。这在 XP 之前的 Windows 上(或者关闭了主题的 XP)更为显眼,在开着主题的 XP 上,很难说那儿有一个分割条,除非你用鼠标去试。
![[Client edge style on pane (XP) - 7K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU4q8wGDo4P3CNMo1FMdvcHTg_D42-oCDLjcffnSTd-xlMhLGPV7I4_gg_J-wo0LIP4JDosYIyKICqYjGpkpfu3sTQZmpxp-9K4CdrFpOnEfZamDFkw4Z2cs)
消息路由
现在,因为我们在主框架和窗格窗口之间又有了另外的一个窗口,你可能会惊讶通知消息是怎么工作的。尤其是,主框架怎样才能接收到 NM_CUSTOMDRAW 通知而又反射回列表?答案在 CSplitterWindowImpl 的消息映射中可以找到: BEGIN_MSG_MAP()
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
MESSAGE_HANDLER(WM_SIZE, OnSize)
CHAIN_MSG_MAP(baseClass)
FORWARD_NOTIFICATIONS()
END_MSG_MAP()
最后面的 FORWARD_NOTIFICATIONS() 宏很重要。回忆一下第四部分, 有好几个通知消息,总是会发送到子窗口的父窗口。FORWARD_NOTIFICATIONS() 所做的事情就是把消息重新发送给分隔条的父窗口。所以,在列表把 WM_NOTIFY 消息发送给分隔条(也即列表的父窗口)的时候,分隔条则依次把 WM_NOTIFY 消息发送给主框架(分隔条的父窗口)。在主框架窗口反射消息的时候,消息也会被发送回最初产生 WM_NOTIFY 消息的窗口里,也就是列表自己,所以分隔条并不干涉反射。
所有这些的结果就是发送于主框架和列表间的通知消息并不会因为有分隔条窗口的存在而受到影响。这就使得添加或者去处分隔条都相当的容易,因为子窗口类根本不需要为了能使其消息处理可以继续工作而作任何改动。
窗格容器
WTL 还支持一种窗口部件(widget),就好比在资源管理器的左窗格中的那个一样,称为窗格容器。此控件提供了一个带有文字的题头区域,以及一个可选的关闭按钮:
![[Explorer pane container - 3K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzJEo2noELjtRXINWM8nCHUj2D53MShIuVRl9rbY2VM_ELci1xAon1R_PiSUUbO71U-RBroM8x2uGnzQmUqf1996Omb6nqsmsuqVMxWa4Hg2g)
如同分隔条窗口管理着两个窗格窗口一样,窗个容器管理了一个子窗口。在容器被改变大小时,其子窗口也自动改变大小以匹配容器内部的空间。
类
窗格容器的实现有两个类,都在 atlctrlx.h 里:CPaneContainerImpl 和 CPaneContainer。CPaneContainerImpl 是一个 CWindowImpl 的派生类,其中包含了全部的实现,CPaneContainer 仅仅提供了一个窗口的类名。除非你要覆盖某些方法以改变容器的绘制,通常只要用 CPaneContainer 即可。
基本方法HWND Create(
HWND hWndParent, LPCTSTR lpstrTitle = NULL,
DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
DWORD dwExStyle = 0, UINT nID = 0, LPVOID lpCreateParam = NULL)
HWND Create(
HWND hWndParent, UINT uTitleID,
DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
DWORD dwExStyle = 0, UINT nID = 0, LPVOID lpCreateParam = NULL)
创建 CPaneContainer 与创建其它子窗口一样。有两个 Create() 方法,不过仅有第二个参数不同。在第一个版本里,你传递的是一个字符串,用于题头中显示的标题文字,在第二个版本里,要传递的是一个字符串表项的 ID。至于其余的参数,缺省值通常就足够了。 DWORD SetPaneContainerExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
DWORD GetPaneContainerExtendedStyle()
CPaneContainer 还有其他的扩展风格,可以控制关闭按钮以及容器的布局:
PANECNT_NOCLOSEBUTTON:设置此风格可以从题头上把关闭按钮去掉
PANECNT_VERTICAL:设置此风格会使得题头区域为沿着容器窗口的左边缘垂直放置
此扩展风格的缺省值为 0,也即意味着是一个带有关闭按钮的水平容器。 HWND SetClient(HWND hWndClient)
HWND GetClient()
调用 SetClient() 可以把一个子窗口关联到窗格容器。这与 CSplitterWindow 的 SetSplitterPane() 方法很类似。SetClient() 的返回值是旧的客户窗口的 HWND。调用 GetClient() 可以得到当前客户窗口的 HWND。 BOOL SetTitle(LPCTSTR lpstrTitle)
BOOL GetTitle(LPTSTR lpstrTitle, int cchLength)
int GetTitleLength()
调用 SetTitle() 可以改变显示在容器的题头区域内的文字。调用 GetTitle() 可以得到当前的题头文字,而调用 GetTitleLength() 则可以得到当前题头文字以字符为单位的长度(不包括 null 结束符)。 BOOL EnableCloseButton(BOOL bEnable)
如果窗格容器具有关闭按钮,你可以使用 EnableCloseButton() 来启用或者禁用它。
在分割条窗口中使用窗格容器
为了演示如何向一个现存的分割条中添加窗格容器,我们来向 ClipSpy 的分割条的左窗格里添加一个容器。我们不把列表控件关联到左窗格上,而代之以窗格容器。然后再把列表关联到窗格容器上。下面是在 CMainFrame::OnCreate() 中改变了的代码,用以设置窗格容器。 LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...
m_wndVertSplit.Create ( *this, rcDefault );
// Create the pane container.
m_wndPaneContainer.Create ( m_wndVertSplit, IDS_PANE_CONTAINER_TEXT );
// Create the left pane (list of clip formats)
m_wndFormatList.Create ( m_wndPaneContainer, rcDefault );
//...
// Set up the splitter panes
m_wndPaneContainer.SetClient ( m_wndFormatList );
m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );
注意,列表控件的父是 m_wndPaneContainer,而且,m_wndPaneContainer 被设置为了分割条的左窗格。下面就是改动后的左窗格的样子。
![[Pane container - 5K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxDFNuVMRY5bOIHWvDNnm-QDaXAOyAjSzMO2PT3AqcQgdFw5g7xP5-TgwN7oavbDH-uTVcPO8RZwdHNtD1kJ9gdOO032ShYzdeOfyn070bARg)
关闭按钮以及消息处理
当用户点击了关闭按钮时,窗格容器会向其父窗口发送 WM_COMMAND 消息,命令 ID 为 ID_PANE_CLOSE(为定义在 atlres.h 中的一个常量)。如果你在分割条中使用了窗格容器,则通常的动作是调用 SetSinglePaneMode() 来隐藏窗格容器所在的分割条窗格。不过要记住,还应该给用户提供再把它显示出来的方法。
CPaneContainer 的消息映射中也有 FORWARD_NOTIFICATIONS() 宏,就像 CSplitterWindow 一样,所以容器会把通知消息从其客户窗口传递到其父窗口。在 ClipSpy 中,尽管在列表控件和主框架之间有两个窗口,即窗格容器和分割条,但是 FORWARD_NOTIFICATIONS() 宏却使得从列表中发出的所有通知都可以到达主框架。
高级分割条特性
在这一节里,我会介绍如何使用 WTL 的分割条来做一些常见的高级 UI 技巧。
嵌套分割条
如果你计划写一个类似于邮件客户端或者 RSS 阅读器的应用,最终你就可能会使用到嵌套分割条,一个水平的和一个垂直的。用 WTL 的分割条来做这件事相当容易 - 只需把一个分割条创建为另一个的子窗口。
出于演示目的,我们要为 ClipSpy 添加一个水平分割条。水平分割条处于最顶级,垂直分割条将嵌套于其中。在添加完名为 m_wndHorzSplitter 的 CHorSplitterWindow 类型成员之后,我们要像创建 m_wndVertSplitter 一样把它创建出来。因为要使 m_wndHorzSplitter 作为顶级分隔条,m_wndVertSplitter 只好作为 m_wndHorzSplitter 的子窗口创建。最后,m_hWndClient 设置为 m_wndHorzSplitter,从而使得此窗口现在占据了主框架的客户区。 LRESULT CMainFrame::OnCreate()
{
//...
// Create the splitter windows.
m_wndHorzSplit.Create ( *this, rcDefault );
m_wndVertSplit.Create ( m_wndHorzSplit, rcDefault );
//...
// Set the horizontal splitter as the client area window.
m_hWndClient = m_wndHorzSplit;
// Set up the splitter panes
m_wndPaneContainer.SetClient ( m_wndFormatList );
m_wndHorzSplit.SetSplitterPane ( SPLIT_PANE_TOP, m_wndVertSplit );
m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );
//...
}
下面就是成果:
![[Horz splitter w/empty pane - 5K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzfk-Cbab6MoZqUjE0j-uBbZeqwPv-Bbi1yLIJ-GJWcsg1mJd8d40zpJ34E_umcdpiHt8Yg3AOk7yVCfVSkM_6EWg2nlRd2UrUrC-m7rWSxgQ)
在窗格中使用 ActiveX 控件
在分割条窗格中掌控 ActiveX 控件与在对话框中掌控控件类似。你可以在运行时使用 CAxWindow 的方法创建控件,然后再将此 CAxWindow 关联到分割条的一个窗格上。下面显示了如何把浏览器控件添加到水平分割条的下窗格中: // Create the bottom pane (browser)
CAxWindow wndIE;
const DWORD dwIEStyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN |
WS_HSCROLL | WS_VSCROLL;
wndIE.Create ( m_wndHorzSplit, rcDefault,
_T("http://www.codeproject.com"), dwIEStyle );
// Set the horizontal splitter as the client area window.
m_hWndClient = m_wndHorzSplit;
// Set up the splitter panes
m_wndPaneContainer.SetClient ( m_wndFormatList );
m_wndHorzSplit.SetSplitterPanes ( m_wndVertSplit, wndIE );
m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );
特殊绘制
如果你要为分割条提供不同的外观,比如说要在其上绘制纹理,你可以从 CSplitterWindowImpl 派生一个类并覆盖 DrawSplitterBar()。如果你只是想改变一下外观,你可以将 CSplitterWindowImpl 中现成的函数复制过来,再做一些所希望的小小的改动。下面是一个例子,在分割条上绘制了斜纹图案。 template <bool t_bVertical = true>
class CMySplitterWindowT :
public CSplitterWindowImpl<CMySplitterWindowT<t_bVertical>, t_bVertical>
{
public:
DECLARE_WND_CLASS_EX(_T("My_SplitterWindow"),
CS_DBLCLKS, COLOR_WINDOW)
// Overrideables
void DrawSplitterBar(CDCHandle dc)
{
RECT rect;
if ( m_br.IsNull() )
m_br.CreateHatchBrush ( HS_DIAGCROSS,
t_bVertical ? RGB(255,0,0)
: RGB(0,0,255) );
if ( GetSplitterBarRect ( &rect ) )
{
dc.FillRect ( &rect, m_br );
// draw 3D edge if needed
if ( (GetExStyle() & WS_EX_CLIENTEDGE) != 0)
{
dc.DrawEdge(&rect, EDGE_RAISED,
t_bVertical ? (BF_LEFT | BF_RIGHT)
: (BF_TOP | BF_BOTTOM));
}
}
}
protected:
CBrush m_br;
};
typedef CMySplitterWindowT<true> CMySplitterWindow;
typedef CMySplitterWindowT<false> CMyHorSplitterWindow;
下面就是结果,分隔条被拓宽了,以便能更容易地看到效果:
![[custom drawn bars - 14K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwo3-6837UZJMx1hVitBqP6viKY-f_zb4ejoghGhfmpbh0GQFITFlcIH7JXmJcJnZWkjqZCmcrKES3LHd0JQmGhcsOA0JcC7aTIXgunpXcIFQ)
窗格容器中的特殊绘制
CPaneContainer 有几个可以覆盖的方法,用以改变窗格容器的外观。你可以从 CPaneContainerImpl 派生新类并覆盖你想要覆盖的方法,例如:
class CMyPaneContainer :
public CPaneContainerImpl<CMyPaneContainer>
{
public:
DECLARE_WND_CLASS_EX(_T("My_PaneContainer"), 0, -1)
//... overrides here ...
};
更有意思的几个方法是: void CalcSize()
CalcSize() 的目的只是用来设置 m_cxyHeader,它控制着容器的题头区域的宽度(或者是高度)。不过,在 SetPaneContainerExtendedStyle() 中有一个小错误,该错误会导致当窗格在水平和垂直模式间切换时派生类的 CalcSize() 不会被调用到。你可以通过把 CalcSize() 调用改为 pT->CalcSize() 来修正此错误,位置在 atlctrlx.h 的 2215 行。
HFONT GetTitleFont()
此方法返回一个用于绘制题头文字的 HFONT。缺省值由 GetStockObject(DEFAULT_GUI_FONT) 返回,其实就是 MS Sans Serif 字体。如果你希望使用看起来更现代一些的字体 Tahoma,你可以覆盖 GetTitleFont() 并返回一个你所创建的 Tahoma 字体的句柄。 BOOL GetToolTipText(LPNMHDR lpnmh)
覆盖此方法可以提供鼠标悬停于关闭按钮时所需的工具提示文字。此方法其实是 TTN_GETDISPINFO 的处理器,因而你可以将 lpnmh 转型为 NMTTDISPINFO* 并相应设置该结构中的成员。需要记住的是你应该检查通知代码 - 它既可能是 TTN_GETDISPINFO 也可能是 TTN_GETDISPINFOW - 然后再据此访问该结构。 void DrawPaneTitle(CDCHandle dc)
你可以覆盖此方法以提供题头区域的自绘制。可以使用 GetClientRect() 和 m_cxyHeader 来计算题头区域的 RECT。下面是一段示例代码,在一个水平容器的题头区域里作了渐变填充绘制: void CMyPaneContainer::DrawPaneTitle ( CDCHandle dc )
{
RECT rect;
GetClientRect(&rect);
TRIVERTEX tv[] = {
{ rect.left, rect.top, 0xff00 },
{ rect.right, rect.top + m_cxyHeader, 0, 0xff00 }
};
GRADIENT_RECT gr = { 0, 1 };
dc.GradientFill ( tv, 2, &gr, 1, GRADIENT_FILL_RECT_H );
}
示例工程演示了覆盖这些方法中的几个,其结果显示在这儿:
![[Custom drawing in a pane cont. - 6K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwX0dhYXf9apv3Dpp1AruK5Ng-p3h4e_B_f6GmWck2l8U1P921ak1XXA6iF0oz2sN5wfyq1jJL-YpyqYuDvvTHMEWoAevqnrERqdwY3x6xIOg)
如上所示,示例工程有一个 Splitters 菜单,允许你切换分隔条以及窗格容器的几种特殊绘制,以便你可以看到其间的区别。你还可以锁定分割条,这是通过切换 SPLIT_NONINTERACTIVE 扩展风格来完成的。
奖励:状态栏中的进度条
正如我早在上几篇文章之前所允诺过的,这一新的 ClipSpy 会演示如何在状态栏上创建进度条。此工作与 MFC 版本一样 - 所需的步骤有:
- 得到第一个状态栏窗格的
RECT
- 创建一个进度条控件,作为状态栏的子窗口,将其
RECT 设置为上述窗格的 RECT
- 在填充编辑器控件的同时更新进度条的位置
可以到 CMainFrame::CreateProgressCtrlInStatusBar() 中查看代码。
下一步
在第八部分里,我将介绍有关属性表和向导的话题。
参考资料
WTL Splitters and Pane Containers - Ed Gadziemski
修订历史
2003 年 7 月 9 日:首次发布 2006 年 1 月 12 日:主要修正了文中不清楚或者用词不当的地方。更新了一些屏幕截图。添加了有关 WS_EX_CLIENTEDGE 的一节。 | 链接: 上一部分; 下一部分 3/7/2006
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分;下一部分
第六部分 - 掌控 ActiveX 控件
|
内容
- 简介
- 以 AppWizard 开始
- 使用资源编辑器添加控件
- 用于掌控控件的 ATL 类
- CAxDialogImpl
- AtlAxWin 和 CAxWindow
调用控件的方法
接收控件激发的事件
- 在 VC 6 里添加处理器
- 在 VC 7 里添加处理器
- 事件的知会
示例工程概述
运行时创建 ActiveX 控件
键盘处理
下一步
修订历史
简介
在这第六部分里,我将介绍 ATL 对在对话框中掌控(hosting)ActiveX 控件的支持。由于 ActiveX 控件是 ATL 的专项,所以这儿并没有相关的 WTL 类。不过,因为 ATL 掌控控件的方式与 MFC 迥异,所以这是我们要介绍的一个重要主题。我会介绍如何掌控控件以及接收(sink)事件,并开发一个相比用 MFC 的 ClassWizard 写就的应用毫无功能损失的应用程序。当然,你可以在你写的 WTL 应用中使用 ATL 对控件掌控的支持。
本文的示例工程演示了如何掌控 IE 的 Web 浏览器控件。我选择浏览器控件是基于以下两个不错的理由:
- 每个人的机器上都有它,而且
- 它有很多方法并会激发(fire)很多事件,因此用于演示目的,它是确是个很好的控件。
我肯定比不上那些花了很多时间使用 IE 的 Web 浏览器控件编写定制浏览器的人们。但是,通读本文之后,你就会有足够的知识开始编写你自己的定制浏览器了!
以 AppWizard 开始
创建工程
WTL 的 AppWizard 可以为我们创建马上就能掌控 ActiveX 控件的应用。下面我们要创建一个称为 IEHoster 的新工程。像上一章一样,我们要使用一个非模态对话框,只不过这次要把 Enable ActiveX Control Hosting 复选框选中,就象这样:
![[AppWizard - 22K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwwCymbtcuNaxJBXd9QPxzZXefL63l1q5-Tgzu6D7Qbp3UZ35b8zDO2a-s06pbWahaQm1DcoRDq8ISObzxhaE71jLnzNb69rRf9_sLhhxuNlg)
![[AppWizard - 25K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU3RymYYiiMFJycwy6PxJfwlfkNJ7K66gEzPZXHhIvwMZM90IiNKGFc18CDVzPb_AWj55ca4PEHjwrwhPM8s6ROWeWCUFTolkezzqjEvv22vT3vbdBFctSqw)
选中这个复选框会使得我们的主对话框从 CAxDialogImpl 中派生,因此能够掌控 ActiveX 控件。在 VC 6 的向导里,在第二页上还有另外一个复选框,其文字为 Host ActiveX Controls,但是选中它对结果代码没有任何影响,所以在第一页里就可以点击 Finish 按钮完成了。
生成的代码
在这一节里,我会先介绍一些原来没有见过的由 AppWizard 生成的代码片断;下一节里,我再详细介绍 ActiveX 掌控类。
第一个需要检视的文件是 stdafx.h,其中的包含文件有: #include <atlbase.h>
#include <atlapp.h>
extern CAppModule _Module;
#include <atlcom.h>
#include <atlhost.h>
#include <atlwin.h>
#include <atlctl.h>
// .. other WTL headers ...
atlcom.h 和 atlhost.h 相对重要。它们包括了一些 COM 相关的类(比如智能指针 CComPtr),以及用来掌控控件的窗口类。
接下来,再看 maindlg.h 中 CMainDlg 的声明: class CMainDlg : public CAxDialogImpl<CMainDlg>,
public CUpdateUI<CMainDlg>,
public CMessageFilter, public CIdleHandler
CMainDlg 现在是派生于 CAxDialogImpl,后者是使对话框能够掌控 ActiveX 控件的第一步。
最后,是 WinMain() 中的一行新代码: int WINAPI _tWinMain(...)
{
//...
_Module.Init(NULL, hInstance);
AtlAxWinInit();
int nRet = Run(lpstrCmdLine, nCmdShow);
_Module.Term();
return nRet;
}
AtlAxWinInit() 注册了一个名为 AtlAxWin 的窗口类。该类在 ATL 为 ActiveX 控件创建宿主窗口时使用。
由于 ATL 7 的一个改动,你必须给 _Module.Init() 传递一个 LIBID。论坛中的一些人建议在 VC 7 中使用如下代码: _Module.Init(NULL, hInstance, &LIBID_ATLLib);
这个改动在我这儿工作的很好。
使用资源编辑器添加控件
ATL 允许你象在 MFC 应用中一样使用资源编辑器向对话框上添加 ActiveX。首先,在对话框编辑器中右击,选择 Insert ActiveX control:
![[Insert menu - 8K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzTyQjJyyjhSQHXnF5z12QgsfB_TbhdqJCezHz-YSVhT2IoJskfDwTnRzEc4DubiSOONuZ_NewVfIRIRYcFgdeARLFVM7OWHzI_nNKuT9-lPw)
VC 会显示一个你的系统上所安装的控件的列表。向下滚动到 Microsoft Web Browser 并点击 OK,可以将该控件插入到对话框中。查看一下新控件的属性并将其 ID 设置为 IDC_IE。对话框看起来应该象下面这样,在编辑器中控件也是可见的:
![[ IE control in editor - 6K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxIYP4JM2xKycP3B4NrbmmySGkPOVAkIWOgSv88hq_rlJzzLOS2MTLAuWw1_uLJkjagSjgr21dEXoW6AcAB8jODhK0vWHbLieRUmUu9cRKF4g)
如果你现在就编译并运行这个应用,你就可以在对话框中看到 Web 浏览器控件。由于我们还没有告诉它应该导航到何处,所以它显示的是一个空白页。
在下一节里,我将介绍有关创建和掌控 ActiveX 控件的 ATL 类,然后我们再看如果使用这些类来和浏览器进行通讯。
用于掌控控件的 ATL 类
在对话框中掌控一个 ActiveX 控件的时候,会有两个类协同工作:CAxDialogImpl 和 CAxWindow。它们处理控件容器必须实现的所有接口,并为常见的操作(比如对 COM 控件查询一个特定的接口)提供一些辅助函数。
CAxDialogImpl
第一个要介绍的就是 CAxDialogImpl。在你写对话框类的时候,你应该从 CAxDialogImpl 而不是 CDialogImpl 派生,这样才能掌控控件。CAxDialogImpl 覆盖了 Create() 和 DoModal(),这两个函数由全局函数 AtlAxCreateDialog() 和 AtlAxDialogBox() 分别调用。因为 IEHost 对话框是由 Create() 创建的,所以我们应该仔细打量一下 AtlAxCreateDialog()。
AtlAxCreateDialog() 先加载对话框资源,并使用辅助类 _DialogSplitHelper 遍历所有的控件,以寻找那些由资源编辑器生成的并标明是一个需要创建的 ActiveX 控件的项。例如,下面是 IEHost.rc 文件中为 Web 浏览器生成的项:
CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}",
WS_TABSTOP,7,7,116,85
第一个参数是窗口标题(一个空串),第二个是控件 ID,第三个是窗口类名。_DialogSplitHelper::SplitDialogTemplate() 一看到窗口类是由 '{' 开头就知道这是一个 ActiveX 控件项,它会在内存中创建一个新的对话框模板,在新模板里,那些特殊的 CONTROL 项由创建 AtlAxWin 窗口的项所替代。内存中的新项相当于如下定义: CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}",
IDC_IE, "AtlAxWin", WS_TABSTOP, 7, 7, 116, 85
其结果是一个 AtlAxWin 窗口会使用相同的 ID 被创建出来,而且其窗口标题就是 ActiveX 控件的 GUID。因此,如果你调用 GetDlgItem(IDC_IE),则返回的 HWND 值是 AtlAxWin 窗口的,而不是 ActiveX 控件自己的。
一旦 SplitDialogTemplate() 返回,AtlAxCreateDialog() 再调用 CreateDialogIndirectParam() 以使用修改过的模板来创建对话框。
AtlAxWin 和 CAxWindow
正如上述指出的,AtlAxWin 是用来作为一个 ActiveX 控件的容器窗口的。随 AtlAxWin 使用的还有一个特殊的窗口接口类,名叫 CAxWindow。当从一个对话框模板中创建 AtlAxWin 时,AtlAxWin 的窗口过程,即 AtlAxWindowProc(),会处理 WM_CREATE 并在消息的响应中创建 ActiveX 控件。也可以在运行时创建 ActiveX 控件而不必在对话框模板中,不过我们在后面才会介绍。
WM_CREATE 处理器会调用全局的 AtlAxCreateControl(),将 AtlAxWin 的窗口标题传递给它,我们知道,窗口标题已经被设置成了 Web 浏览器的 GUID。AtlAxCreateControl() 再调用更多的函数,但最后代码会到达 CreateNormalizedObject() 处,它会把窗口标题转换为 GUID 并最终调用 CoCreateInstance() 来创建 ActiveX 控件。
因为 ActiveX 控件是 AtlAxWin 的一个子窗口,所以对话框就不能直接访问控件了。但是,CAxWindow 具有与控件通讯的方法。最常用的方法之一是 QueryControl(),它又会调用到控件的 QueryInterface()。比方说,你可以使用 QueryControl() 来从 Web 浏览器控件中得到一个 IWebBrowser2 接口,并使用该接口把浏览器导航到某个 URL。
调用控件的方法
现在,我们的对话框里就有一个 Web 浏览器了,我们可以使用它的 COM 接口来和它交互。我们要做的第一件事情是使用它的 IWebBrowser2 接口导航到一个新的 URL。在 OnInitDialog() 处理器里,我们可以把掌控着浏览器的 AtlAxWin 附着到一个 CAxWindow 变量上。 CAxWindow wndIE = GetDlgItem(IDC_IE);
接下来,我们声明一个 IWebBrowser2 接口指针并使用 CAxWindow::QueryControl() 向浏览器控件查询该接口: CComPtr<IWebBrowser2> pWB2;
HRESULT hr;
hr = wndIE.QueryControl ( &pWB2 );
QueryControl() 调用 Web 浏览器的 QueryInterface(),如果成功的话,IWebBrowser2 就会返回给我们。然后我们可以调用 Navigate():
if ( pWB2 )
{
CComVariant v; // empty variant
pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"),
&v, &v, &v, &v );
}
接收控件激发的事件
从 Web 浏览器获取一个接口是相当简单的,而且这还可以允许我们从一个方向 - 即向控件进行通讯,还有很多的通讯,是以事件的形式从控件而来。ATL 中具有封装了连接点和事件接收的类,使得我们可以接收到由浏览器激发的事件。要使用这一支持,我们要做四件事:
- 将
IDispEventSimpleImpl 添加到 CMainDlg 的继承列表
- 写一个事件接收映射以表明我们要处理哪些事件
- 为这些事件编写处理器
- 把控件连接到接收映射上(这一过程称为知会(advising))(译注:对于 advise/advising 在 COM 方面的使用,业界尚没有一个被广泛接受的统一译法,此处译者姑且译为知会,之所以没有译为通知,是因为在文中很难与 notify/notification 区别开来)
VC 的 IDE 在此过程中可以提供极大的帮助 - 它会为你对 CMainDlg 进行改动,还可以查询 ActiveX 控件的类型库,显示控件可以激发的事件的列表。由于 VC 6 和 VC 7 中添加处理器的用户界面不同,下面我分开来介绍。
在 VC 6 里添加处理器
有两种方法可以调出添加处理器的界面:
- 在 ClassView 窗格里,右击
CMainDlg 并选择菜单中的 Add Windows Message Handler。
- 在查看
CMainDlg 的代码时,或者在资源编辑器中查看相关的对话框时,点击 WizardBar 上 Action 按钮的下拉箭头,并选择菜单中的 Add Windows Message Handler。
选择该命令之后,VC 会显示一个对话框,其中有一个题为 class or object to handle 的控件列表。在列表中选中 IDC_IE,则 VC 会把 WebBrowser 控件可以激发的事件填充到 New Windows message/events 列表中。
![[Event list in VC6 - 21K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU-g2aXJ3PoTlFqAez73C3mVxGu_ItUqiW5iQ74Vgb4Ow6guQ_IhXpjKdOPqfvxJgigt7rGiVZRsvHvv7L1RRF_tzuzhB0b8EJiBt9h44srOSizbzQurBy40)
因为我们要为 DownloadBegin 事件添加处理器,所以要选中该事件并点击 Add and Edit 按钮。VC 就会提示你要求给出方法名:
![[Setting method name - 8K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU9V6ODG0Xj_CYGsdXq14Sl6L9fsUBPw7_nYZDq3B4g9b26GYi5JZkUxYZYY32iFlxG-fjVRVVkeNQipEVQfzYaCs0VqHIe7f8lyoZ0HcEz7OMkl-2h6M6S4)
在你第一次添加事件处理器时,VC 会对 CMainDlg 做一点改动,使其可以成为一个事件接收器。头文件中的改动有点零散,汇总起来就是下面的这些代码: #import "C:\WINNT\System32\shdocvw.dll"
class CMainDlg : public CAxDialogImpl<CMainDlg>,
public CUpdateUI<CMainDlg>,
public CMessageFilter, public CIdleHandler,
public IDispEventImpl<IDC_IE, CMainDlg>
{
// ...
public:
BEGIN_SINK_MAP(CMainDlg)
SINK_ENTRY(IDC_IE, 0x6a, OnDownloadBegin)
END_SINK_MAP()
void __stdcall OnDownloadBegin()
{
// TODO: Add Code for event handler.
}
};
#import 语句是一个编译器指令,用以读取 shdocvw.dll (WebBrowser ActiveX 控件的实现就在此文件中)中的类型库,并为能使用控件中的组件类和接口创建封装类。通常你会把此指令放到 stdafx.h 中,不过在本例中,我们其实根本不需要它,因为 Platform SDK 中已经有了含有 WebBrowser 的接口和方法的头文件了。
继承列表中现在已经有了 IDispEventImpl。它有两个模板参数,第一个是我们指派给 ActiveX 控件的 ID,即 IDC_IE,第二个是派生于 IDispEventImpl 的类的名字。
接收映射由 BEGIN_SINK_MAP 和 END_SINK_MAP 宏隔起来。每一个 SINK_ENTRY 宏列出了一个 CMainDlg 要处理的事件,宏的参数分别为控件 ID(又是 IDC_IE),事件的分派 ID,以及事件到达时要调用的函数的名字。VC 会从 ActiveX 控件的类型库中读取分派 ID,所以不必担心应该指定什么数值(exdispid.h 头文件中列出了 IE 和资源浏览器发送的许多事件的 ID,如果你到其中查看,你可以看到 0x6A 对应着常量 DISP_DOWNLOADBEGIN)。
最后面是一个新方法,OnDownloadBegin()。对那些有参数的事件,VC 会为方法设置正确的原型。所有的事件处理器都是 __stdcall 调用规范,因为它们是 COM 方法。
在 VC 7 里添加处理器
也有两种方法可以添加事件处理器。你可以在对话框编辑器中的 ActiveX 控件上右击,并在菜单上选择 Add Event Handler。你可以在显示出的对话框里选择事件名并设置处理器的名字。
![[Event list in VC7 - 24K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU03MrKm9gPioaF6Xgsr88IBcFO8-2W6qFN9C8mnIKQi_5LAz4r3c0e8dz_Xowq25Fq6BO-Si7gNTtN0Jdnlr0NATrKPLx5kt-2LHKl1Gbco4QRzf1n8abW0)
点击 Add and Edit 按钮将添加该处理器,对 CMainDlg 做必要的更改,并打开 maindlg.cpp 文件,高亮显示着新添加的处理器。
另一个方法是查看 CMainDlg 的属性页,展开 Controls 结点,然后是 IDC_IE 结点。在 IDC_IE 结点下,你可以找到控件激发的事件。
![[Adding event handler thru properties list - 12K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU21YcSZmR4YGD9H2Gwai1mya8sAHJ1P6dK2HZoXZz_xHcEqBDxltYL4F7OyMonBEzA1i-2jkEycsvPhS-SPVUppImN39acEMA0ghkYnJ6IMgzqd4DuSf3IE)
你可以点击事件名边上的箭头,选择菜单上的 <Add> [MethodName] 来添加处理器。你还可以稍后修改处理器的名字,当然还是在属性页里改。
VC 7 对 CMainDlg 的修改和 VC 6 几乎一样,一个例外是并不添加 #import 指令。
事件的知会
最后一步是知会到控件,CMainDlg 想要接收由 WebBrowser 控件激发的事件。此过程在 VC 6 和 VC 7 里仍然不一样,所以还需要分别介绍。相同的是,知会都发生在 OnInitDialog() 里,反知会(unadvising)发生在 OnDestroy() 里。
VC 6 中的知会
VC 6 的 ATL 里有一个全局函数 AtlAdviseSinkMap()。该函数接收一个具有接收映射的 C++ 对象的指针(通常为 this 指针),以及一个布尔值。如果布尔值为 true,则对象是希望开始接收事件,如果为 false,则对象希望停止接收事件。AtlAdviseSinkMap() 知会对话框中所有的控件开始或者停止向 C++ 对象发送事件。
要使用此函数,就要为 WM_INITDIALOG 和 WM_DESTROY 添加处理器,然后再像这样调用 AtlAdviseSinkMap(): BOOL CMainDlg::OnInitDialog(...)
{
// Begin sinking events
AtlAdviseSinkMap ( this, true );
}
void CMainDlg::OnDestroy()
{
// Stop sinking events
AtlAdviseSinkMap ( this, false );
}
AtlAdviseSinkMap() 返回一个 HRESULT 表示知会成功与否。如果 AtlAdviseSinkMap() 在 中失败了,那么你就不能从有的(或者是全部的) ActiveX 控件处得到事件。
VC 7 中的知会
在 VC 7 里,CAxDialogImpl 有一个名为 AdviseSinkMap() 的方法封装了 AtlAdviseSinkMap()。AdviseSinkMap() 接收一个布尔参数,其意义与 AtlAdviseSinkMap() 的第二个参数相同。AdviseSinkMap() 检查到类里有一个接收映射,就会调用 AtlAdviseSinkMap()。
相对于 VC 6,最大的不同在于 CAxDialogImpl() 已经有了为你调用 AdviseSinkMap() 的 WM_INITDIALOG 和 WM_DESTROY 的处理器。要想受益于此特性,就要在 CMainDlg 消息映射的开头添加一个 CHAIN_MSG_MAP 宏,就像这样: BEGIN_MSG_MAP(CMainDlg)
CHAIN_MSG_MAP(CAxDialogImpl<CMainDlg>)
// rest of the message map...
END_MSG_MAP()
示例工程概述
我们已经知道了事件接收是怎么工作的,现在我们来看看整个 IEHost 工程。正像我们所讨论的,它掌控了 Web 浏览器控件,并处理了六个事件。它还显示了一个事件的列表,这样你就可以知道定制浏览器是怎样使用这些事件来在 UI 上提供进度的。应用处理的事件有:
BeforeNavigate2 和 NavigateComplete2:这两个事件可以使应用监测到 URL 导航。如果你愿意,你可以在 BeforeNavigate2 的响应里取消导航。
DownloadBegin 和 DownloadComplete:应用程序使用这两个事件来控制表示浏览器正在工作的“等待”信息。更精致的程序还会像 IE 一样使用个动画。
CommandStateChange:此事件告诉应用什么时候可以使用“后退”和“前进”导航命令。应用会据此相应地启用或者禁止后退和前进按钮。
StatusTextChange:好几种情况下都会激发此事件,例如当鼠标光标移动到超链接上时。此事件会发送一个字符串,应用会响应此事件,将字符串显示到浏览器窗口下面的一个静态控件里。
应用里还有四个控制浏览器的按钮:后退、前进、停止以及重新加载。这些按钮会调用到相应的 IWebBrowser2 方法。
事件以及伴随事件的数据都被记录到了列表控件里,所以事件一激发你就能看到。你可以关闭任一事件的日志,这样你就可以只观测其中的一两个。为了演示一些实质性的事件处理,在 BeforeNavigate2 处理器中会检查 URL,如果其中包含了“doubleclick.net”,则本次导航会被取消。作为 IE 的插件而不是 HTTP 代理运行的广告和弹出窗口拦截器使用的正是这个方法。下面是作此检查的代码。 void __stdcall CMainDlg::OnBeforeNavigate2 (
IDispatch* pDisp, VARIANT* URL, VARIANT* Flags,
VARIANT* TargetFrameName, VARIANT* PostData,
VARIANT* Headers, VARIANT_BOOL* Cancel )
{
CString sURL = URL->bstrVal;
// You can set *Cancel to VARIANT_TRUE to stop the
// navigation from happening. For example, to stop
// navigates to evil tracking companies like doubleclick.net:
if ( sURL.Find ( _T("doubleclick.net") ) > 0 )
*Cancel = VARIANT_TRUE;
}
下面是我们的应用在浏览论坛时的样子:
![[Sample app - 33K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUz4Il1Q3k3Tcg5WtlggJcA-pih5bggBJ5lGLNYhOh9hHdEZBF-IyLoTzIUsQUygoO3p4f3X2wn9qF4YYzTMig4Lzdbndsa9rIA00XeqerYQTHEhaD6rFWLo)
IEHost 还演示了另外好几个在前文中介绍过的 WTL 特性:CBitmapButton(用于浏览器控制按钮),CListViewCtrl(用于事件记录),DDX(用于跟踪复选框的状态),以及 CDialogResize。
运行时创建 ActiveX 控件
在运行时而不是在资源编辑器中创建 ActiveX 控件也是可以的。About 对话框演示了这一技术。对话框资源包含了一个占位用的分组框,表明了浏览器控件该在什么位置:
![[About box in editor - 5K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU65f0CPoVRM55HGlmHvIQazBCKF_Jw5UsB1NDGU5Xmck2LBosBWwE_PPOP-YNbzkcatvnpiWytrIW4igEVA1R3vBCIkkPP5zrgVcbzB9CIbKnCLZVNSTdkU)
在 OnInitDialog() 中,我们使用 CAxWindow 来创建一个新的 AtlAxWin,它会与占位控件使用相同的 RECT,而占位控件随即被销毁: LRESULT CAboutDlg::OnInitDialog(...)
{
CWindow wndPlaceholder = GetDlgItem ( IDC_IE_PLACEHOLDER );
CRect rc;
CAxWindow wndIE;
// Get the rect of the placeholder group box, then destroy
// that window because we don't need it anymore.
wndPlaceholder.GetWindowRect ( rc );
ScreenToClient ( rc );
wndPlaceholder.DestroyWindow();
// Create the AX host window.
wndIE.Create ( *this, rc, _T(""),
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN );
接下来,我们使用 CAxWindow 的一个方法来创建 ActiveX 控件。可供我们选择的两个方法是 CreateControl() 和 CreateControlEx()。CreateControlEx() 有一个附加的参数可以返回接口指针,这样你就不必再另行调用 QueryControl()。我们感兴趣的两个参数,其一是第一个参数,它是 Web 浏览器控件的 GUID 的字符串版本,其二是第四个参数,它是指向 IUnknown* 的一个指针。此指针将会被填充为 ActiveX 控件的 IUnknown。控件创建之后,我们再查询 IWebBrowser2 接口,与前文类似,再将控件导航到一个 URL。 CComPtr<IUnknown> punkCtrl;
CComQIPtr<IWebBrowser2> pWB2;
CComVariant v; // empty VARIANT
// Create the browser control using its GUID.
wndIE.CreateControlEx ( L"{8856F961-340A-11D0-A96B-00C04FD705A2}",
NULL, NULL, &punkCtrl );
// Get an IWebBrowser2 interface on the control and navigate to a page.
pWB2 = punkCtrl;
pWB2->Navigate ( CComBSTR("about:mozilla"), &v, &v, &v, &v );
}
对于具有 ProgID 的 ActiveX 控件,你还可以将其 ProgID 传递给 CreateControlEx() 以代替 GUID。例如,我们可以使用以下调用来创建浏览器控件: // Use the control's ProgID: Shell.Explorer:
wndIE.CreateControlEx ( L"Shell.Explorer", NULL,
NULL, &punkCtrl );
CreateControl() 和 CreateControlEx() 还有专门用于 Web 浏览器的重载函数。如果你的应用把网页作为 HTML 类型的资源包含进来,你就可以将其资源 ID 作为第一个参数,ATL 会创建一个 Web 浏览器控件并导航至该资源。IEHost 包含了一个 ID 为 IDR_ABOUTPAGE 的页面,因而我们可以用以下代码在 About 对话框中显示它:
wndIE.CreateControl ( IDR_ABOUTPAGE );
下面是成果:
![[About box browser ctrl - 12K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUxUDfBNEHI82fmtzXM_DxGfEnjf-fiOeEkLOaviLuPH4jjx9JaX8f_ieJkJAyLnR2Kia_8aZusJ_2k1740BRFvmXlh_oD6XaAFD64xFhb1FbThZjZp6WWqM)
示例工程中包括了上述三种技术的所有代码,查点 CAboutDlg::OnInitDialog() 并注释或者取消注释其中的代码,可以看每种方法的运作。
键盘处理
最后,却也非常重要的一个细节是键盘消息。ActiveX 控件的键盘处理相当复杂,因为宿主和控件必须一起合作来保证控件能够看到它感兴趣的消息。例如,浏览器控件可以让你使用 TAB 键在链接之间移动。MFC 自己会处理所有的这些,所以你可能从未意识到要使键盘支持能够正常工作所需的工作量。
不幸的是,AppWizard 不会为基于对话框的应用生成键盘处理的代码。不过,如果你创建一个 SDI 应用并使用窗体视图(form view)作为视图窗口,那么你就可以在 PreTranslateMessage() 中看到所需的代码。每当从消息队列里读出一个鼠标或者键盘消息,前述代码就会获取拥有焦点的控件并使用 ATL 中的 WM_FORWARDMSG 消息将读出的消息转发给控件。通常,一个窗口在接收到 WM_FORWARDMSG 时什么也不做,因为它对该消息一无所知。但是当一个 ActiveX 控件拥有焦点时,WM_FORWARDMSG 消息最终会被送到掌控控件的 AtlAxWin 处。AtlAxWin 会识别出 WM_FORWARDMSG 并采取必要的步骤来看控件自己是否想处理该消息。
如果拥有焦点的窗口不认识 WM_FORWARDMSG ,则 PreTranslateMessage() 会调用 IsDialogMessage() 以使诸如 TAB 这样的标准对话框导航键可以正常工作。
示例工程中包含了 PreTranslateMessage() 中的必要的代码。因为 PreTranslateMessage() 仅工作于非模态对话框,所以如果你想有正确的键盘处理的话就必须使用一个非模态对话框。
下一步
在下一篇文章里,我们要返回到框架窗口,介绍关于使用分割条窗口的话题。
修订历史
2003 年 5 月 20 日:首次发布 2006 年 1 月 5 日:重写了接收映射、事件处理器以及协商几节。旧代码比真正所需更为复杂。 |
链接:上一部分;下一部分 |
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分;下一部分
第五部分 - 高级对话框 UI 类
|
内容
- 第五部分简介
- 属主绘制(Owner Draw)以及定制绘制(Custom Draw)的专用类
- 新的 WTL 控件
- CBitmapButton
- CCheckListViewCtrl
- CTreeViewCtrlEx 和 CTreeItem
- CHyperLink
对话框控件的 UI 更新
DDV
改变对话框的大小
下一步
参考资料
修订历史
第五部分简介
在上一部分里,我们了解了一些关于对话框和控件的 WTL 特性,其工作方式与 MFC 中的对应类很相似。在本部分里,我们会介绍几个新的 WTL 类,它们实现了一些更加高级的 UI 特性:属主绘制(Owner draw)和定制绘制(Custom draw),新的 WTL 控件,UI 更新,以及对话框数据验证(DDV)。
属主绘制以及定制绘制的专用类
由于属主绘制和定制绘制在 GUI 工作中已经变得相当的普遍,于是 WTL 提供几个嵌入类来处理这些烦人的事。接下来我们会逐一介绍它们,作为我们的上一个示例工程的续集,现在我们从 ControlMania2 开始。如果你是随着 AppWizard 来创建工程,就要确保你的对话框是非模态的。为了使 UI 更新能正常工作,这是必须的。在 UI 更新一节中,我会给出更多的细节。
COwnerDraw
属主绘制包括对四个消息的处理:WM_MEASUREITEM、WM_DRAWITEM、WM_COMPAREITEM 和 WM_DELETEITEM。而在 atlframe.h 中定义的 COwnerDraw 类则可以简化你的代码。这是因为你不必再需要为这些消息写处理器了,而只需把消息串联到 COwnerDraw,后者就会调用在你的类中实现的覆盖函数。
如何串联消息取决于你是否把消息反射回了控件。下面是 COwnerDraw 的消息映射,它清楚地显示出了其中的差异: template <class T> class COwnerDraw
{
public:
BEGIN_MSG_MAP(COwnerDraw<T>)
MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)
MESSAGE_HANDLER(WM_MEASUREITEM, OnMeasureItem)
MESSAGE_HANDLER(WM_COMPAREITEM, OnCompareItem)
MESSAGE_HANDLER(WM_DELETEITEM, OnDeleteItem)
ALT_MSG_MAP(1)
MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
MESSAGE_HANDLER(OCM_MEASUREITEM, OnMeasureItem)
MESSAGE_HANDLER(OCM_COMPAREITEM, OnCompareItem)
MESSAGE_HANDLER(OCM_DELETEITEM, OnDeleteItem)
END_MSG_MAP()
};
映射的主体节来处理 WM_* 消息,而 ALT_MSG_MAP(1) 节处理消息的反射版本即 OCM_*。属主绘制通知就像 WM_NOTIFY 一样,你既可以在控件的父窗口中处理,也可以将之反射回控件本身。如果你选择了前者,你就可以将消息直接串联到 COwnerDraw: // C++ class for a dialog that contains owner-drawn controls
class CSomeDlg : public CDialogImpl<CSomeDlg>,
public COwnerDraw<CSomeDlg>, ...
{
BEGIN_MSG_MAP(CSomeDlg)
//...
CHAIN_MSG_MAP(COwnerDraw<CSomeDlg>)
END_MSG_MAP()
void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};
不过如果你希望控件处理消息,你就要使用 CHAIN_MSG_MAP_ALT 宏把消息串联到 ALT_MSG_MAP(1) 节: // C++ class that implements an owner-drawn button
class CMyButton : public CWindowImpl<CMyButton, CButton>,
public COwnerDraw<CMyButton>, ...
{
BEGIN_MSG_MAP(CMyButton)
//...
CHAIN_MSG_MAP_ALT(COwnerDraw<CMyButton>, 1)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};
COwnerDraw 拆解随消息发送来的参数,然后调用你的类中的实现函数。在上例中,该类实现了 DrawItem(), 那么它在 WM_DRAWITEM 或者 OCM_DRAWITEM 串联到 COwnerDraw 的时候就会被调用到。你可以覆盖的方法有:
void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);
int CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct);
void DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct);
如果出于某些原因你不想处理某个消息,你可以调用 SetMsgHandled(false) 从而消息会传递下去,到达其后的消息映射中可能存在的某个处理器。
对于 ControlMania2,我们将从 ControlMania1 中的树控件开始,并添加一个属主绘制按钮,然后在按钮类中处理反射的 WM_DRAWITEM。下面就是资源编辑器中的新按钮:
![[Owner-drawn button 1 - 7K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJztuigDwMMQ8oMjplyrdMOMFdzQtnQFF7VcVHY8a3xuLEGmWrZXlYF3uSWuJB3bDytexIWM4ZaCfzI6HEFjgJGTRZkOxajX_jNYRGCENAF6uw)
现在,我们还需要一个实现此按钮的类: class CODButtonImpl : public CWindowImpl<CODButtonImpl, CButton>,
public COwnerDraw<CODButtonImpl>
{
public:
BEGIN_MSG_MAP_EX(CODButtonImpl)
CHAIN_MSG_MAP_ALT(COwnerDraw<CODButtonImpl>, 1)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};
DrawItem() 使用诸如 BitBlt() 这样的 GDI 调用来在按钮表面上绘制一个图片。代码应该很容易理解,因为 WTL 的类名和方法又一次类似于 MFC。
void CODButtonImpl::DrawItem ( LPDRAWITEMSTRUCT lpdis )
{
// NOTE: m_bmp is a CBitmap init'ed in the constructor.
CDCHandle dc = lpdis->hDC;
CDC dcMem;
dcMem.CreateCompatibleDC ( dc );
dc.SaveDC();
dcMem.SaveDC();
// Draw the button's background, red if it has the focus, blue if not.
if ( lpdis->itemState & ODS_FOCUS )
dc.FillSolidRect ( &lpdis->rcItem, RGB(255,0,0) );
else
dc.FillSolidRect ( &lpdis->rcItem, RGB(0,0,255) );
// Draw the bitmap in the top-left, or offset by 1 pixel if the button
// is clicked.
dcMem.SelectBitmap ( m_bmp );
if ( lpdis->itemState & ODS_SELECTED )
dc.BitBlt ( 1, 1, 80, 80, dcMem, 0, 0, SRCCOPY );
else
dc.BitBlt ( 0, 0, 80, 80, dcMem, 0, 0, SRCCOPY );
dcMem.RestoreDC(-1);
dc.RestoreDC(-1);
}
下面是按钮看起来的样子:
![[Owner-drawn button - 19K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU39I1YFRcUdFfrCww55GEKRwI0ZlZXUAOlDJohqIjuIox8GfRcoizef03fs8rl_IxSt95DLTkGFuZGAsBcSyMPbIxLyADhWCLOUed16QavJU4y4IcSA2d88)
CCustomDraw
CCustomDraw 和 COwnerDraw 的工作方法类似,不过它是让你处理并串联 NM_CUSTOMDRAW 消息的。CCustomDraw 针对每个定制绘制的步骤都有一个可覆盖的方法:
DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPostEraset(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnSubItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
它们的缺省处理全都返回 CDRF_DODEFAULT,因此,如果你需要进行自己的绘制或者是返回其他不同的值,你覆盖其一即可。
你可能在最后一个截图上已经注意到了,“Dawn” 是用绿色显示的。这是由于使用了一个从 CTreeCtrl 派生来的新类(源代码中名为 CBuffyTreeCtrl)把消息串联到了 CCustomDraw 并覆盖了 OnPrePaint() 和 OnItemPrePaint() 方法。在树填充之后,该节点的附加数据被设置成了 1,而 OnItemPrePaint() 会检查此值,并且一旦发现就会改变文本的颜色。 DWORD CBuffyTreeCtrl::OnPrePaint(
int idCtrl, LPNMCUSTOMDRAW lpNMCD)
{
return CDRF_NOTIFYITEMDRAW;
}
DWORD CBuffyTreeCtrl::OnItemPrePaint(
int idCtrl, LPNMCUSTOMDRAW lpNMCD)
{
if ( 1 == lpNMCD->lItemlParam )
pnmtv->clrText = RGB(0,128,0);
return CDRF_DODEFAULT;
}
和在 COwnerDraw 中的一样,在定制绘制的消息处理器中你也可以调用 SetMsgHandled(false) 函数以使得消息可以传递到消息映射中的其它处理器那儿。
新的 WTL 控件
WTL 有它自己的几个新控件,有的是对其他封装类的增强(比如 CTreeViewCtrlEx),有的则是提供了内建控件所没有的新功能(比如 CHyperLink)。
CBitmapButton
WTL 的 CBitmapButton,声明于 atlctrlx.h 中,较之于 MFC 版本更加易用。WTL 版本使用了图像列表控件而非四个独立的位图资源,这就意味着你可以在一个位图中保存多个按钮的图像,从而多少降低了一些 GDI 资源占用。如果你的应用有很多的图像而又正好运行于 Windows 9x,那这一点就尤为注目,因为使用大量的独立图像会很快耗尽 GDI 资源并使系统完蛋。
CBitmapButton 是一个 CWindowImpl 派生类,拥有众多特性:自动调整控件大小,自动生成三维边框,热追踪(hot-tracking)支持,每个按钮可以使用多个图像用以区别按钮所处的不同状态。
在 ControlMania2 中,我们将在前面所创建的属主绘制按钮旁边创建一个 CBitmapButton。首先,我们向 CMainDlg 添加一个名为 m_wndBmpBtn 的 CBitmapButton 成员。然后再用常用的方法把它连接到新按钮上,既可以调用 SubclassWindow() 也可以使用 DDX。把一幅位图加载到图像列表中,再告诉按钮将要使用此列表。然后告诉按钮列表中的那个图像对应哪一种控件状态。下面是从 OnInitDialog() 中取来的一段设置按钮的代码: // Set up the bitmap button
CImageList iml;
iml.CreateFromImage ( IDB_ALYSON_IMGLIST, 81, 1, CLR_NONE,
IMAGE_BITMAP, LR_CREATEDIBSECTION );
m_wndBmpBtn.SubclassWindow ( GetDlgItem(IDC_ALYSON_BMPBTN) );
m_wndBmpBtn.SetToolTipText ( _T("Alyson") );
m_wndBmpBtn.SetImageList ( iml );
m_wndBmpBtn.SetImages ( 0, 1, 2, 3 );
缺省情况下,按钮会假定它拥有该图像列表,因此 OnInitDialog() 千万不能删除它所创建的图像列表。下面是缺省状态下的新按钮。可以看到该控件的大小已经改变,正好适合图像的大小。
![[WTL bitmap button - 19K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU4JXS9uX1ivYdUo21IkHKFkjmPWCRInkoJqNLlJrwucZcFqy-oQBiwQRwqmNscbO_v9QK4erT5GjyCwpIl0rDgs8IR66uEZyy034Uf10D7qULLhIbk5arG0)
因为 CBitmapButton 是一个非常有用的类,所以我在这儿会介绍它的公用方法。
CBitmapButton 的方法
CBitmapButtonImpl 类包含了实现一个按钮的所有代码,不过除非你要覆盖某个方法或者消息处理器,你可以为你的控件使用 CBitmapButton 类。
CBitmapButtonImpl 构造函数CBitmapButtonImpl(DWORD dwExtendedStyle = BMPBTN_AUTOSIZE,
HIMAGELIST hImageList = NULL)
构造函数设置按钮的扩展风格(不要与他的窗口风格混淆),而且还可以关联一个图像列表。通常使用缺省值就足够了,因为你可以用其他的方法来设置这些属性。 BOOL SubclassWindow(HWND hWnd)
SubclassWindow() 是一个覆盖版本的方法,用来执行子类化并初始化本类拥有的内部数据。
DWORD GetBitmapButtonExtendedStyle()
DWORD SetBitmapButtonExtendedStyle(DWORD dwExtendedStyle,
DWORD dwMask = 0)
CBitmapButton 支持一些影响其外观或者操作的扩展风格:
BMPBTN_HOVER
- 启用热追踪。当鼠标光标位于按钮上的时候,按钮会呈现为其聚焦状态。
BMPBTN_AUTO3D_SINGLE, BMPBTN_AUTO3D_DOUBLE
- 自动围绕图像生成一个三维边框,以及拥有焦点时的聚焦矩形。另外,如果你没有提供按下状态的图像,则也会为你生成一个。
BMPBTN_AUTO3D_DOUBLE 生成一个略为厚实一点的边框。
BMPBTN_AUTOSIZE
- 使按钮的大小自动匹配图像的大小。此风格是缺省风格。
BMPBTN_SHAREIMAGELISTS
- 如果设置了此风格,按钮对象不会销毁用以容纳按钮图像的图像列表。如果没有设置,则图像列表会在
CBitmapButton 的析构函数中销毁。
BMPBTN_AUTOFIRE
- 如果设置了此风格,点击按钮并保持鼠标键为按下状态会连续产生
WM_COMMAND 消息。
当调用 SetBitmapButtonExtendedStyle() 时,dwMask 参数控制了受影响的风格,缺省值 0 会使新的风格完全取代旧的风格。 HIMAGELIST GetImageList()
HIMAGELIST SetImageList(HIMAGELIST hImageList)
调用 SetImageList() 和 GetImageList() 可以把图像列表关联到按钮上,或者取得当前关联于按钮的图像列表。 int GetToolTipTextLength()
bool GetToolTipText(LPTSTR lpstrText, int nLength)
bool SetToolTipText(LPCTSTR lpstrText)
CBitmapButton 支持当鼠标悬停在按钮上时显示一个工具提示。调用 SetImageList() 和 GetToolTipText() 来指定或者获取工具提示上显示的文字。
void SetImages(int nNormal, int nPushed = -1,
int nFocusOrHover = -1, int nDisabled = -1)
调用 SetImages() 来告诉按钮图像列表中的哪个图像用于某种按钮状态。所有参数都是基于 0 的图像列表中的索引值。nNormal 是必需的,其他的都是可选的。传递 -1 表示对于该状态没有图像。
CCheckListViewCtrl
CCheckListViewCtrl,定义于 atlctrlx.h 中,是 CWindowImpl 的一个派生类,它实现了带有复选框的列表视图控件。此类不同于 MFC 的 CCheckListBox,后者使用的是列表框而不是列表视图。CCheckListViewCtrl 相当简单,因为它只添加了少许属于自己的功能。不过,它还引入了一个新的辅助类,名为 CCheckListViewCtrlImplTraits,该类与 CWinTraits 相似但有第三个模板参数,该参数表示控件要使用的扩展列表视图风格。如果你没有定义自己的 CCheckListViewCtrlImplTraits 组合,那么该类将会使用这些缺省值:LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT。
下面是一个特点(traits)定义的示例,使用了不同的扩展视图列表风格,以及一个使用这些特点的新类。(注意,必须在扩展列表视图风格中包括 LVS_EX_CHECKBOXES,否则会得到一个断言失败的消息)。 typedef CCheckListViewCtrlImplTraits<
WS_CHILD | WS_VISIBLE | LVS_REPORT,
WS_EX_CLIENTEDGE,
LVS_EX_CHECKBOXES | LVS_EX_GRIDLINES | LVS_EX_UNDERLINEHOT |
LVS_EX_ONECLICKACTIVATE> CMyCheckListTraits;
class CMyCheckListCtrl :
public CCheckListViewCtrlImpl<CMyCheckListCtrl, CListViewCtrl,
CMyCheckListTraits>
{
private:
typedef CCheckListViewCtrlImpl<CMyCheckListCtrl, CListViewCtrl,
CMyCheckListTraits> baseClass;
public:
BEGIN_MSG_MAP(CMyCheckListCtrl)
CHAIN_MSG_MAP(baseClass)
END_MSG_MAP()
};
CCheckListViewCtrl 方法BOOL SubclassWindow(HWND hWnd)
当你子类化一个现有的列表视图控件时,SubclassWindow() 查看相关的 CCheckListViewCtrlImplTraits 中的扩展列表视图风格,并将之应用到控件上。前两个模板参数(窗口风格以及扩展窗口风格)并未使用。 BOOL GetCheckState(int nIndex)
BOOL SetCheckState(int nItem, BOOL bCheck)
这两个方法实际上在 CListViewCtrl 中。SetCheckState() 接受一个条目索引以及指示是否勾选该条目的布尔值。GetCheckState() 只接受一个索引,而后返回该条目的当前勾选状态。 void CheckSelectedItems(int nCurrItem)
此方法接受一个条目索引。它切换该条目的勾选(Check)状态,该条目必须已经被选定(Select),并且同时改变其他所有已选定条目的选中状态。你自己通常不会使用此方法,因为 CCheckListViewCtrl 在复选框被点击或者用户按下空格键时会处理条目的选中事宜。
下面是 CCheckListViewCtrl 在 ControlMania2 中的样子:
![[Check list ctrl - 22K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU3jdmcbF9QqWeLdfHJExiWglr17BXdaiLsfs3jCcBP0STV84nsUTg7UhpVcOR4CROSjAqLROO-oLuJwaVL21-HMQSA0bSb0fGrt1KPrpm74QEFtOcYjVBdQ)
CTreeViewCtrlEx 和 CTreeItem
通过封装 HTREEITEM,这两个类使得使用树控件的功能更为方便。一个 CTreeItem 对象中保存了一个 HTREEITEM 和一个指向包含此项的树控件的指针,这样你就只需使用 CTreeItem 来操纵该项而不必每次都引用树控件。CTreeViewCtrlEx 和 CTreeViewCtrl 相像,但它的方法是处理 CTreeItem 而非 HTREEITEM。因此,假如你调用 InsertItem(),则它会返回一个 CTreeItem 而不是 HTREEITEM,然后你就可以使用 CTreeItem 操控此新插入的项。下面是一个例子: // Using plain HTREEITEMs:
HTREEITEM hti, hti2;
hti = m_wndTree.InsertItem ( "foo", TVI_ROOT, TVI_LAST );
hti2 = m_wndTree.InsertItem ( "bar", hti, TVI_LAST );
m_wndTree.SetItemData ( hti2, 37 );
// Using CTreeItems:
CTreeItem ti, ti2;
ti = m_wndTreeEx.InsertItem ( "baz", TVI_ROOT, TVI_LAST );
ti2 = ti.AddTail ( "yen", 0 );
ti2.SetData ( 42 );
CTreeItem 对于每个接受 HTREEITEM 的 CTreeViewCtrl 方法都有一个对应的方法,就像 CWindow 对应每个接受 HWND 的 API 都有方法一样。在 ControlMania2 的代码里有对 CTreeViewCtrlEx 和 CTreeItem 的更多方法的演示。
CHyperLink
CHyperLink 是一个 CWindowImpl 派生类,可以子类化一个静态文本控件使之成为可点击的超链接。CHyperLink 根据用户的 IE 的颜色设置自动处理超链接的绘制,而且还支持键盘导航。类 CHyperLinkImpl 是 CHyperLinkImpl 的基类,其中包含了实现一个链接的所有代码,不过除非你要覆盖其方法或者消息处理器,否则你只要对你的控件使用 CHyperLink 就可以了。
CHyperLink 的缺省行为是当链接被点击时在缺省浏览器里启动一个 URL。如果子类化了的静态控件具有 WS_TABSTOP 风格,你还可以用 TAB 键转到该控件并按下空格键或者回车键来点击此链接。CHyperLink 还可以在光标悬停于链接上的时候显示一个工具提示。缺省情况下,CHyperLink 使用静态控件的文本作为 URL 和工具提示的缺省文字,不过你也可以使用后文介绍的方法来改变这些属性。
在 WTL 7.1 里,为 CHyperLink 添加了很多特性,这些新特性可以使用扩展风格来启用。方法列表之后解释了这些风格及其用法。
CHyperLink 方法
这些是常用的 CHyperLink 方法,另外的一些用于计算控件的大小,解析链接文本等等,可以到 atlctrlx.h 里查看此类的完整方法列表。用以执行子类化,并且初始化类中的内部数据。CHyperLink 的构造函数没有参数,下面是其余的公有方法。 CHyperLinkImpl( DWORD dwExtendedStyle = HLINK_UNDERLINED )
CHyperLink()
CHyperLinkImpl 的构造函数接收用于控件的扩展风格。CHyperLink 缺少相对应的构造函数,不过你可以使用 SetHyperLinkExtendedStyle() 来设置这些风格。
BOOL SubclassWindow(HWND hWnd)
SubclassWindow() 是一个覆盖后的方法,用以执行子类化,并且初始化类中的内部数据。如果你使用 DDX_CONTROL 把一个超链接变量关联到了静态控件上,那么此方法会被自动调用;当然你也可以自己手动调用来子类化一个控件。
DWORD GetHyperLinkExtendedStyle()
DWORD SetHyperLinkExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
获取或者设置控件的扩展风格。你必须在调用 SubclassWindow() 或者 Create() 之前设置扩展风格,这样控件才能知道怎么绘制文本。 bool GetLabel(LPTSTR lpstrBuffer, int nLength)
bool SetLabel(LPCTSTR lpstrLabel)
获取或者设置控件要用的文本。如果你不设置标签文本,控件会把标签文本设置为静态控件的窗口文字。 bool GetHyperLink(LPTSTR lpstrBuffer, int nLength)
bool SetHyperLink(LPCTSTR lpstrLink)
获取或者设置关联到控件的 URL。如果你不设置超链接,控件会把超链接设置为静态控件的窗口文字。 bool GetToolTipText(LPTSTR lpstrBuffer, int nLength)
bool SetToolTipText(LPCTSTR lpstrToolTipText)
获取或者设置光标悬停于链接之上时显示在工具提示里的文本。不过,只有链接具有 HLINK_COMMANDBUTTON 或者 HLINK_NOTIFYBUTTON 扩展风格时才能使用这些方法。后文有关于工具提示的更多信息。
下面是一个“普通的”超链接控件在 ControlMania2 对话框中看起来的样子:
![[WTL hyperlink - 21K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUyapy0rcbb9PG57m97DJFY1I9Y9XAtKKZtxPOd3G8mwaa5Od8nd_N6NYGpp71rFyIvyIzpoFNuyDkDeOyyqM4mvpEGV0cp7VIbKA2REwKZFN3VUx97t-Uwk)
其 URL 在 OnInitDialog() 中通过以下调用做了设置: m_wndLink.SetHyperLink ( _T("http://www.codeproject.com/") );
CHyperLink 扩展风格
新的 WTL 7.1 的特性可以通过设置相应的扩展风格位来启用。这些风格有:
HLINK_UNDERLINED
- 链接文本具有下划线。这是缺省行为。
HLINK_NOTUNDERLINED
- 链接文本从不使用下划线。
HLINK_UNDERLINEHOVER
- 链接文本只有在光标悬停在链接上时才有下划线。
HLINK_COMMANDBUTTON
- 链接被点击时,控件向其父窗口发送一个
WM_COMMAND 消息(命令码设置为 BN_CLICKED)。
HLINK_NOTIFYBUTTON
- 链接被点击时,控件向其父窗口发送一个
WM_NOTIFY 消息(通知码设置为 NM_CLICK)。
HLINK_USETAGS
- 控件仅将
<a> 标签内的文字认为是链接,其它文本正常绘制。
HLINK_USETAGSBOLD
- 和
HLINK_USETAGS 一样,但是 <a> 标签内的文本以粗体绘制。当设置了此风格时,下划线扩展风格会被忽略,链接文本总不会出现下划线。
HLINK_NOTOOLTIP
- 控件不显示工具提示。
如果既没有设置 HLINK_COMMANDBUTTON 风格也没有设置 HLINK_NOTIFYBUTTON 风格,则 CHyperLink 对象在被点击后调用其 Navigate() 方法。Navigate() 调用 ShellExecuteEx() 在缺省浏览器里启动一个 URL。如果你想链接被点击时执行其他的动作,那就设置 HLINK_COMMANDBUTTON 或者 HLINK_NOTIFYBUTTON,然后再处理控件发出的通知消息。
CHyperLink 的其它细节
你可以为静态控件设置 SS_CENTER 或者 SS_RIGHT 风格以使得超链接的文本可以居中或者右对齐。但是如果控件具有 HLINK_USETAGS 或者 HLINK_USETAGSBOLD 风格的话,这些位就会被忽略,而文本总是左对齐的。
如果你使用 CHyperLink 来打开一个 URL,也就是说,没有设置 HLINK_COMMANDBUTTON 或者 HLINK_NOTIFYBUTTON,你就不能用 SetToolTipText() 来改变工具提示的文本。但是,你可以通过 CHyperLink 的成员 m_tip 来直接访问工具提示控件,并使用 AddTool() 来设置文本: m_wndLink.m_tip.AddTool ( m_wndLink, _T("Clickety!"), &m_wndLink.m_rcLink, 1 );
请注意,这儿有一个和 WTL 7.0 不兼容的改变:CHyperLink 在 WTL 7.1 里使用了值是 1 的工具 ID。在 WTL 7.0 里,该 ID 与窗口句柄相同,你可以使用 m_tip.UpdateTipText() 来更改文字。在 WTL 7.1 里没有运气用 UpdateTipText() 了,上面的代码重复了 CHyperLink::Init() 所做的事情,重新设置了工具提示。
由于一些绘制上的问题,HLINK_USETAGS 和 HLINK_USETAGSBOLD 风格最好是在链接的文本总在一行上的时候使用。绘制代码寻找位于 <a> 标签内的文本,并把整个文本分为三个部分:标签之前、标签之内、标签之后。但是,如果某一部分的文字需要断词(word-breaking),那么回绕(wrap)就会不正确。我在 ControlMania2 里的一个单独的对话框里展示了这个问题:
![[Link drawing problems - 13K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU-1eSdQ-kzJCbjZJkmDUlG0TXip6wclBnIwPgPSObmrGlIUkS94PvP64zdweXIs2E8l_AVKtVJl6xqEgRZVokN3APG_i9JzdNwWoFKd_l-27qrC-nw9ZykU)
你应该确保 HLINK_UNDERLINEHOVER 不会和 HLINK_USETAGSBOLD 一起设置,因为那会导致一些空白出现在链接文本之后,就好像上图中的第一个超链接一样。
对话框控件的 UI 更新
在对话框里进行控件的 UI 更新比在 MFC 中要简单得多。在 MFC 里,你必须知道未文档化的 WM_KICKIDLE 消息,如何去处理它以及如何触发控件更新。在 WTL 中完全用不着这些窍门,不过 AppWizard 中有一个小错误会需要你添加一行代码。
第一件需要记住的事情是对话框必须是非模态的。这是因为要使 CUpdateUI 能工作,你的应用就必须控制着消息循环。如果你把对话框做成了模态的,那么系统就会处理消息循环,因此空闲处理器就不会被调用。而 CUpdateUI 是在空闲时间内工作,所以没有了空闲处理也就意味着不会有 UI 更新。
ControlMania2 的对话框是非模态的,其类定义的起始部分很像是一个框架窗口类: class CMainDlg : public CDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>,
public CMessageFilter, public CIdleHandler
{
public:
enum { IDD = IDD_MAINDLG };
BOOL PreTranslateMessage(MSG* pMsg);
BOOL OnIdle();
BEGIN_MSG_MAP_EX(CMainDlg)
MSG_WM_INITDIALOG(OnInitDialog)
COMMAND_ID_HANDLER_EX(IDOK, OnOK)
COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel)
COMMAND_ID_HANDLER_EX(IDC_ALYSON_BTN, OnAlysonODBtn)
END_MSG_MAP()
BEGIN_UPDATE_UI_MAP(CMainDlg)
END_UPDATE_UI_MAP()
//...
};
可以看到 CMainDlg 派生于 CUpdateUI 并有一个 UI 更新映射。OnInitDialog() 中的下列代码,你会再次觉得和先前的框架窗口例子一样熟悉: // register object for message filtering and idle updates
CMessageLoop* pLoop = _Module.GetMessageLoop();
ATLASSERT(pLoop != NULL);
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
UIAddChildWindowContainer(m_hWnd);
这一次,我们调用的既非 UIAddToolbar() 也非 UIAddStatusBar(),而是 UIAddChildWindowContainer()。这会告诉 CUpdateUI 我们的对话框包含有需要更新的子窗口。如果你看一下 OnIdle(),说不定你会怀疑漏掉了什么东西: BOOL CMainDlg::OnIdle()
{
return FALSE;
}
你可能期望这里会有另外的一个 CUpdateUI 的方法调用去做实际的更新。你想的很对,的确应该有,但 AppWizard 漏掉了一行代码。你需要把它补入到 OnIdle() 中去: BOOL CMainDlg::OnIdle()
{
UIUpdateChildWindows();
return FALSE;
}
出于演示 UI 更新的目的,当你点击左边的位图按钮时,右边的按钮会被启用或者禁用。所以首先,我们在 UI 更新映射中添加一个入口,使用 UPDUI_CHILDWINDOW 标志以表明此入口用于一个子窗口: BEGIN_UPDATE_UI_MAP(CMainDlg)
UPDATE_ELEMENT(IDC_ALYSON_BMPBTN, UPDUI_CHILDWINDOW)
END_UPDATE_UI_MAP()
然后在左边按钮的处理器中,我们调用 UIEnable() 以切换另一个按钮的启用状态: void CMainDlg::OnAlysonODBtn ( UINT uCode, int nID, HWND hwndCtrl )
{
UIEnable ( IDC_ALYSON_BMPBTN, !m_wndBmpBtn.IsWindowEnabled() );
}
DDV
WTL 中对话框数据验证(DDV)的支持较 MFC 而言要简单一点。在 MFC 里,你要为 DDX(把数据传送到变量)和 DDV(验证数据)分别创建单独的宏,而在 WTL 里,一个宏就把这些全搞定。WTL 通过在 DDX 映射里使用下面的宏来提供基本的 DDV 支持:
DDX_TEXT_LEN
- 像
DDX_TEXT 一样作 DDX 并验证字符串的长度(不包括空结束符)是小于还是等于指定的限制。
DDX_INT_RANGE 和 DDX_UINT_RANGE
- 它们像
DDX_INT 和 DDX_UINT 一样作 DDX,并验证数字是在给定的最小值和最大值之间。
DDX_FLOAT_RANGE
- 像
DDX_FLOAT 一样作 DDX 并验证数字是在给定的最小值和最大值之间。
DDX_FLOAT_P_RANGE (WTL 7.1 中新加)
- 像
DDX_FLOAT_P 一样作 DDX 并验证数字是在给定的最小值和最大值之间。
这些宏的参数与对应的无验证功能的宏很像,附带上了一两个表明可接受范围的参数。DDX_TEXT_LEN 接收一个参数,是允许的最大长度;其它的都是接收两个参数,表示允许的最小值和最大值。
ControlMania2 有一个 ID 为 IDC_FAV_SEASON 的编辑框,并绑定到了成员变量 m_nSeason 上。
![[Season selector edit box - 26K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUxz5lcSsAOXMpFyD1bFe1W3gLpnukMkE2bnkYKJg5yzEdh2KA2XBnYj8iwKQJc-AlBb2TRTerB29ZSKm2wXWt52m9DkM89IZMIx0h5i3aey1JnnLva7aIvU)
由于 Buffy 共有七季,故其合法值是 1 到 7(译者注:Buffy,全称“Buffy the Vampire Slayer”,是一部美国电视连续剧,中译名《捉鬼者巴菲》),故 DDV 宏看起来是这样: BEGIN_DDX_MAP(CMainDlg)
//...
DDX_INT_RANGE(IDC_FAV_SEASON, m_nSeason, 1, 7)
END_DDX_MAP()
OnOK() 调用 DoDataExchange() 来验证季的数值,而且作为 DoDataExchange() 所完成的工作的一部分,m_nSeason 被赋予了值。
处理 DDV 失败
如果一个控件的数据验证失败了,CWinDataExchange 会调用可覆盖函数 OnDataValidateError() 而且 OnDataValidateError() 会返回 false。由于缺省的实现只是在扬声器里发出哔响,所以你可能会希望提供更为友好的错误提示。OnDataValidateError() 的原型是: void OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data );
_XData 是一个结构,CWinDataExchange 会把输入的数据以及允许的范围等细节填入其中。下面是此结构的定义:
struct _XData
{
_XDataType nDataType;
union
{
_XTextData textData;
_XIntData intData;
_XFloatData floatData;
};
};
nDataType 标示着三个成员中的哪一个是有意义的。其可能的值为:
enum _XDataType
{
ddxDataNull = 0,
ddxDataText = 1,
ddxDataInt = 2,
ddxDataFloat = 3,
ddxDataDouble = 4
};
在我们当前的情况下,nDataType 应该是 ddxDataInt,说明 _XData 中的 _XIntData 成员被填入了内容。_XIntData 是一个简单的结构: struct _XIntData
{
long nVal;
long nMin;
long nMax;
};
我们覆盖后的 OnDataValidateError() 会显示一个错误消息来告诉用户允许的范围是什么: void CMainDlg::OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data )
{
CString sMsg;
sMsg.Format ( _T("Enter a number between %d and %d"),
data.intData.nMin, data.intData.nMax );
MessageBox ( sMsg, _T("ControlMania2"), MB_ICONEXCLAMATION );
GotoDlgCtrl ( GetDlgItem(nCtrlID) );
}
查看 atlddx.h 可以看到 _XData 结构里的其他数据类型 - _XTextData 和 _XFloatData。
改变对话框的大小
WTL 引起我注意的首要的几件事情之一就是其对可变大小的对话框的内建支持。此前,我曾就此主题写过一篇文章,欲知详情请参考该文。概要地讲,你要做的就是把 CDialogResize 类添加到对话框的继承列表中,在 OnInitDialog() 中调用 DlgResize_Init(),然后把消息串联到 CDialogResize。
下一步
在下一篇文章中,我们来看一下在对话框中掌控 ActiveX 控件,以及如何处理控件激发的事件。
参考资料
Using WTL's Built-in Dialog Resizing Class - Michael Dunn
Using DDX and DDV with WTL - Less Wright
修订历史
2003 年 4 月 28 日:首次发布 2005 年 12 月 31 日:更新,包括了 WTL 7.1 中的改动 | 链接: 上一部分; 下一部分 3/2/2006
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分;下一部分
第四部分 - 对话框和控件
|
内容
- 简介
- 重温 ATL 对话框
- 通用控件封装类
- 使用 AppWizard 创建基于对话框的应用
- 使用控件封装类
- ATL 方法 1 - 附着到 CWindow
- ATL 方法 2 - CContainedWindow
- ATL 方法 3 - 子类化
- WTL 方法 1 - DDX_CONTROL
- WTL 方法 2 - DDX_CONTROL_HANDLE
- 更多 DDX 的内容
- DDX 宏
- 关于 DoDataExchange() 的更多信息
- 使用 DDX
处理来自控件的通知
拾零
修订历史
第四部分简介
对话框和控件是 MFC 确确实实节省你时间和精力的一个地方。如果没有 MFC 的控件类,你就会被淹没在填充结构以及写下成吨的 SendMessage 调用以管理控件的琐事中。而且 MFC 还提供了对话框数据交换(DDX),可以在控件和变量之间传递数据。WTL 也支持所有这些特性,并且在其通用控件的封装类里还加入了一些改进。在本文中,我们致力于一个基于对话框的应用,它演示了你所使用过的 MFC 特性,以及一些 WTL 在消息处理上的增强。高级 UI 特性以及 WTL 中的新控件将在第五部分里介绍。
重温 ATL 对话框
回忆一下第一部分,ATL 有两个对话框类,CDialogImpl 和 CAxDialogImpl。CAxDialogImpl 用于掌控 ActiveX 控件的对话框。我们在本文中不包括 ActiveX 控件的内容,因而示例代码使用的是 CDialogImpl。
创建一个新的对话框类,要做三件事:
- 创建对话框资源
- 写一个派生于
CDialogImpl 的新类
- 创建一个名为
IDD 的 public 成员变量并将其设置为对话框的资源 ID
然后你就可以像在框架窗口中那样添加消息处理器了。WTL 并没有改变此过程,但确实添加了可用于对话框的附加特性。
控件封装类
WTL 有许多的控件封装类,对于它们,你应该感到熟悉,因为 WTL 类通常与其在 MFC 中的对应类使用相同的(或者极其相似的)名字。通常方法的名字也是一致的,因此当你使用 WTL 的封装类时你可以使用 MFC 的文档。不过当你需要跳转到某个类的定义时,F12 键就派不上用场了。
下面是内建控件的封装类:
- 用户控件:
CStatic、CButton、CListBox、CComboBox、CEdit、CScrollBar、CDragListBox
- 通用控件:
CImageList、CListViewCtrl(MFC 中为 CListCtrl)、CTreeViewCtrl(MFC 中为 CTreeCtrl)、CHeaderCtrl、CToolBarCtrl、CStatusBarCtrl、CTabCtrl、CToolTipCtrl、CTrackBarCtrl(MFC 中为 CSliderCtrl)、CUpDownCtrl(MFC 中为 CSpinButtonCtrl)、CProgressBarCtrl、CHotKeyCtrl、CAnimateCtrl、CRichEditCtrl、CReBarCtrl、CComboBoxEx、CDateTimePickerCtrl、CMonthCalendarCtrl、CIPAddressCtrl
- MFC 中没有的通用控件封装类:
CPagerCtrl、CFlatScrollBar、CLinkCtrl(可点击的超链接,仅在 XP 中可用)
还有一些 WTL 特有的类:CBitmapButton、CCheckListViewCtrl(具有复选框的列表视图)、CTreeViewCtrlEx 和 CTreeItem (两个类一起使用,CTreeItem 封装了 HTREEITEM)、CHyperLink(可点击的超链接,在所有操作系统上均可用)。
需要注意的是大多数的封装类都是窗口接口类,就像 CWindow 一样。它们封装了 HWND 并提供了围绕消息的封装层(例如,CListBox::GetCurSel() 封装了 LB_GETCURSEL)。因而像 CWindow 一样,创建一个临时的控件封装对象并将之关联到现存的控件上,其代价是很低的。仍然像 CWindow 一样,当控件封装对象析构时控件并不会被销毁,不过 CBitmapButton、CCheckListViewCtrl 和 CHyperLink 例外。
因为本系列面向有经验的 MFC 程序员,我不会在与 MFC 中的对应类相似的封装类的细节上花费太多的时间。不过,我会介绍 WTL 中的新类。CBitmapButton 与 MFC 中的同名类有很大的不同,而 CHyperLink 则是全新的。
使用 AppWizard 创建基于对话框的应用
启动 VC 并打开 WTL AppWizard。我敢确信你和我一样,对于时钟程序已经厌倦了,所以我们不妨把下一个应用叫做 ControlMania1(译者注:意思是控件狂)。在 AppWizard 的第一页,点击 Dialog Based。我们还需要在制作模态还是非模态对话框之间作一个选择。其差异很重要,我将会在第五部分里介绍,不过现在我们可以拣一个简单一点的来,模态的吧。象下面一样选中 Modal Dialog 和 Generate .CPP Files:
![[AppWizard page 1 - 21K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzj6QXubirve7J1pI2psz1FkbOgAGNU5eXz7ZW_Xah_kCIof5C5tbkiT83CJR5fv2eOM8EBn7znJs1LiqTiBA4tlW9XcmFp14NySWAzx3KE5w)
![[AppWizard page 1 - 25K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU7g6dwX0uzGfzjou_XCVraNmRB2Bk3hiFQ6qgqLe_xFitVpqRvkwx17gDTu9ZiwMt5zUL7w6rcbaKq9tIRWZK4S_qMOCJrTmxUDjr7zZo637xnbx8bRD74E)
VC 6 的向导的第二页或者 VC 7 的向导的“User Interface Features”标签中,所有的选项仅当主窗口是框架窗口时才有意义,所以它们全部都被禁止掉了。点击 Finish,完成整个向导。
正如你所希望的,AppWizard 对于对话框应用生成的代码是相当简单的。ControlMania1.cpp 有一个 _tWinMain() 函数,下面是其主要部分: int WINAPI _tWinMain (
HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/,
LPTSTR lpstrCmdLine, int nCmdShow )
{
HRESULT hRes = ::CoInitialize(NULL);
AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES);
hRes = _Module.Init(NULL, hInstance);
int nRet = 0;
// BLOCK: Run application
{
CMainDlg dlgMain;
nRet = dlgMain.DoModal();
}
_Module.Term();
::CoUninitialize();
return nRet;
}
代码首先初始化 COM 并创建一个单线程单元(single-threaded apartment)。这对于要掌控 ActiveX 控件的对话框的是必需的,如果你的应用不使用 COM,你可以安全地把 CoInitialize() 和 CoUninitialize() 调用移除。接下来,调用了 WTL 辅助函数 AtlInitCommonControls(),这只是 InitCommonControlsEx() 的一个封装。全局 _Module 初始化完毕后,显示主对话框(注意,用 DoModal() 创建的 ATL 对话框的确是模态的,不像 MFC,所有的对话框都是非模态的,而 MFC 通过禁止掉对话框的父窗口来模拟模态)。最后,_Module 以及 COM 被逆初始化,DoModal() 的返回值用作应用的退出码。
包围着 CMainDlg 变量的代码块(译者注:即代码中的左右花括号)很重要,因为 CMainDlg 可能有使用了 ATL 和 WTL 特性的成员。而这些成员有可能在其析构函数中也使用了 ATL/WTL 特性。如果没有代码块,CMainDlg 的析构函数(以及成员的析构函数)将会在 _Module.Term()(此函数逆初始化了 ATL/WTL)调用之后运行,并试图使用 ATL/WTL 特性,这就可能导致一个难于诊断的崩溃。(作为一个历史问题,WTL 3 的 AppWizard 生成的代码没有此代码块,我的一些程序因而崩溃了)。
尽管此对话框还相当的赤贫,但你还是可以立即编链并运行它:

CMainDlg 中的代码处理了 WM_INITDIALOG、WM_CLOSE 以及全部三个按钮。如果愿意的话浏览一下代码,你应该能理解 CMainDlg 的声明,它的消息映射和它的消息处理器了。
此示例工程将演示如何把变量挂接到控件上。下面是应用的样子,又多了几个控件。你在后续的讨论中可以回来参看此图。
![[Add'l controls - 12K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUy2KjYa6h63ueEAxO7IvVn0vRzCuUyeJ3VQBlQoCHwhame4my24m7-farMCOMTHgChfVXp3epKbrulMbeZh-7j3-HO48OzFgVIBvHFxYAQCitgZJrc6TjBk)
由于应用使用了一个列表视图(list view)控件,所以需要改动一下对 AtlInitCommonControls() 的调用。将之改为: AtlInitCommonControls ( ICC_WIN95_CLASSES );
这会比需要的多注册几个类,但它使我们在添加不同类型的控件时省却了记忆 ICC_* 常量之苦。
使用控件封装类
把一个成员变量联系到控件有好几种方法。有的使用了简单的 CWindow 之属(或者其他的窗口接口类,如 CListViewCtrl),另外的使用了 CWindowImpl 的派生类。如果你只是需要一个临时变量,那么使用 CWindow 就相当不错,而如果你需要子类化一个控件并处理发给它的消息,那就会需要 CWindowImpl 了。
ATL 方法 1 - 附着一个 CWindow
最简单的方法是声明一个 CWindow 或者其他的窗口接口类,然后调用其 Attach() 方法。你也可以使用 CWindow 的构造函数或者赋值操作符把变量关联到控件的 HWND。
下面的代码演示了把变量关联到列表控件的全部三种方法: HWND hwndList = GetDlgItem(IDC_LIST);
CListViewCtrl wndList1 (hwndList); // use constructor
CListViewCtrl wndList2, wndList3;
wndList2.Attach ( hwndList ); // use Attach method
wndList3 = hwndList; // use assignment operator
记住,CWindow 析构函数并不销毁窗口,所以在变量离开作用域前并不需要将之与控件脱离。如果愿意,你也可以对成员变量使用此方法 - 你可以在 OnInitDialog() 处理器中关联变量。
ATL 方法 2 - CContainedWindow
CContainedWindow 是使用 CWindow 和 CWindowImpl 的一个折中。它允许你子类化一个控件,并在其父窗口中处理控件的消息。这就允许你把所有的消息处理器置于对话框类里,而不必再为每个控件写独立的 CWindowImpl 类。注意,不要使用 CContainedWindow 来处理 WM_COMMAND、WM_NOTIFY 或者其他通知消息,因为这些消息总是发送给控件的父窗口。
至于实际的类,CContainedWindowT,是一个模板类,它接受一个窗口接口类名作为模板参数。一个经过特化的 CContainedWindowT<CWindow> 类像简单的 CWindow 一样地工作,并被 typedef 成了一个更短的名字 CContainedWindow。要使用不同窗口接口类,可以把其名字作为模板参数,例如 CContainedWindowT<CListViewCtrl>。
想搞定一个 CContainedWindow,有四件事情要做:
- 在对话框中创建一个
CContainedWindowT 成员变量
- 将处理器放到对话框消息映射的一个
ALT_MSG_MAP 节中
- 在对话框的构造函数里,调用
CContainedWindowT 构造函数并告诉它应该把消息路由到哪一个 ALT_MSG_MAP 节中
- 在
OnInitDialog() 函数中,调用 CContainedWindowT::SubclassWindow() 方法把变量关联到控件上
在 ControlMania1 里,我们为 OK 按钮和 Exit 按钮各使用一个 CContainedWindow。对话框将处理发送到每个按钮的 WM_SETCURSOR 消息并改变光标。
现在我们来实践这些步骤。首先,向 CMainDlg 中添加 CContainedWindow 成员。 class CMainDlg : public CDialogImpl<CMainDlg>
{
// ...
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
};
其次,添加 ALT_MSG_MAP 节。OK 按钮将使用第一节,Exit 按钮使用第二节。这意味着发送到 OK 按钮的所有消息都会被路由到 ALT_MSG_MAP(1) 节而发送到 Exit 按钮的所有消息会被路由到 ALT_MSG_MAP(2) 节。 class CMainDlg : public CDialogImpl<CMainDlg>
{
public:
BEGIN_MSG_MAP_EX(CMainDlg)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)
COMMAND_ID_HANDLER(IDOK, OnOK)
COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
ALT_MSG_MAP(1)
MSG_WM_SETCURSOR(OnSetCursor_OK)
ALT_MSG_MAP(2)
MSG_WM_SETCURSOR(OnSetCursor_Exit)
END_MSG_MAP()
LRESULT OnSetCursor_OK(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);
LRESULT OnSetCursor_Exit(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);
};
第三,为每个成员调用 CContainedWindow 构造函数并把要使用的 ALT_MSG_MAP 节告诉它。 CMainDlg::CMainDlg() : m_wndOKBtn(this, 1),
m_wndExitBtn(this, 2)
{
}
构造函数的参数是一个 CMessageMap* 和一个 ALT_MSG_MAP 节号。第一个参数通常是 this,表示将使用对话框自己的消息映射,第二个参数告诉对象应该把它的消息发送到哪一个 ALT_MSG_MAP 节。
重要注意事项:如果你在使用 VC 7.0/7.1 以及 WTL 7.0/7.1,而一个 CWindowImpl 或者 CDialogImpl 的派生类又同时做了以下工作,那你就会运行到一个失败的断言上:
- 消息映射使用了
BEGIN_MSG_MAP 而不是 BEGIN_MSG_MAP_EX。
- 映射里包括一个
ALT_MSG_MAP 节。
- 某一
CContainedWindowT 变量将消息路由到了该 ALT_MSG_MAP 节。
- 该
ALT_MSG_MAP 节使用了新的 WTL 消息处理器宏。
详情请参看本文论坛内的此帖。解决方案是使用 BEGIN_MSG_MAP_EX 而不是 BEGIN_MSG_MAP。
最后,为每个 CContainedWindow 关联控件。 LRESULT CMainDlg::OnInitDialog(...)
{
// ...
// Attach CContainedWindows to OK and Exit buttons
m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
return TRUE;
}
下面是新的 WM_SETCURSOR 处理器: LRESULT CMainDlg::OnSetCursor_OK (
HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg )
{
static HCURSOR hcur = LoadCursor ( NULL, IDC_HAND );
if ( NULL != hcur )
{
SetCursor ( hcur );
return TRUE;
}
else
{
SetMsgHandled(false);
return FALSE;
}
}
LRESULT CMainDlg::OnSetCursor_Exit (
HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg )
{
static HCURSOR hcur = LoadCursor ( NULL, IDC_NO );
if ( NULL != hcur )
{
SetCursor ( hcur );
return TRUE;
}
else
{
SetMsgHandled(false);
return FALSE;
}
}
如果你要使用 CButton 的特性,你可以把变量声明为: CContainedWindowT<CButton> m_wndOKBtn;
然后 CButton 的方法就可用了。
当你把光标移动到按钮上的时候,你就可以看到 WM_SETCURSOR 处理器在起作用:
![[Exit button cursor - 10K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU-oS2ZeSAkUTx1DrPa6gcOfSSRj3zHGEMdX4BFQwBmEW0A2bpDuLMYVT9WN9tsBjp2MBqUSM-nKWe3bT6-mx7GIFh-JoVAAB6ly4KEei39TJpV4inwKYWXo)
ATL 方法 3 - 子类化
方法 3 致力于创建一个 CWindowImpl 派生类并使用它来子类化控件。这与方法 2 相似,但是消息处理器在 CWindowImpl 类中而不是在对话框类中。
ControlMania1 使用此方法来子类化主对话框中的 About 按钮。下面是 CButtonImpl 类,派生于 CWindowImpl 并处理了 WM_SETCURSOR: class CButtonImpl : public CWindowImpl<CButtonImpl, CButton>
{
BEGIN_MSG_MAP_EX(CButtonImpl)
MSG_WM_SETCURSOR(OnSetCursor)
END_MSG_MAP()
LRESULT OnSetCursor(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg)
{
static HCURSOR hcur = LoadCursor ( NULL, IDC_SIZEALL );
if ( NULL != hcur )
{
SetCursor ( hcur );
return TRUE;
}
else
{
SetMsgHandled(false);
return FALSE;
}
}
};
然后在主对话框里声明一个 CButtonImpl 成员变量: class CMainDlg : public CDialogImpl<CMainDlg>
{
// ...
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
CButtonImpl m_wndAboutBtn;
};
最后在 OnInitDialog() 里子类化按钮。 LRESULT CMainDlg::OnInitDialog(...)
{
// ...
// Attach CContainedWindows to OK and Exit buttons
m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
// CButtonImpl: subclass the About button
m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );
return TRUE;
}
WTL 方法 1 - DDX_CONTROL
WTL DDX(对话框数据交换)工作起来很像 MFC,而且可以相当轻松地把变量连接到控件上。首先,你需要像在前一个例子中一样,有一个 CWindowImpl 的派生类。这次我们要使用一个新类,CEditImpl,这是因为这个例子里我们要子类化编辑框控件。你还需要在 stdafx.h 中 #include atlddx.h 以得到 DDX 支持代码。
为了向 CMainDlg 添加 DDX 支持,把 CWinDataExchange 加入到继承列表中: class CMainDlg : public CDialogImpl<CMainDlg>,
public CWinDataExchange<CMainDlg>
{
//...
};
接下来在类中创建一个 DDX 映射,它与 MFC 应用中 ClassWizard 生成的 DoDataExchange() 函数相似。针对不同类型的数据,存在着好多个 DDX_* 宏,我们在这儿要使用的是 DDX_CONTROL,以把变量连接到控件上。这一次,当你在控件上右击时,我们用 CEditImpl 处理 WM_CONTEXTMENU 消息来做一些事情。 class CEditImpl : public CWindowImpl<CEditImpl, CEdit>
{
BEGIN_MSG_MAP_EX(CEditImpl)
MSG_WM_CONTEXTMENU(OnContextMenu)
END_MSG_MAP()
void OnContextMenu ( HWND hwndCtrl, CPoint ptClick )
{
MessageBox("Edit control handled WM_CONTEXTMENU");
}
};
class CMainDlg : public CDialogImpl<CMainDlg>,
public CWinDataExchange<CMainDlg>
{
//...
BEGIN_DDX_MAP(CMainDlg)
DDX_CONTROL(IDC_EDIT, m_wndEdit)
END_DDX_MAP()
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
CButtonImpl m_wndAboutBtn;
CEditImpl m_wndEdit;
};
最后,在 OnInitDialog() 中,我们调用继承于 CWinDataExchange 的 DoDataExchange() 函数。在 DoDataExchange() 被第一次调用的时候,它会按需子类化控件。在本例中,DoDataExchange() 会子类化 ID 为 IDC_EDIT 的控件,并把它连接到变量 m_wndEdit 上。 LRESULT CMainDlg::OnInitDialog(...)
{
// ...
// Attach CContainedWindows to OK and Exit buttons
m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
// CButtonImpl: subclass the About button
m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );
// First DDX call, hooks up variables to controls.
DoDataExchange(false);
return TRUE;
}
DoDataExchange() 的参数和 MFC UpdateData() 函数的参数具有相同的含义。我们在下一节中讨论其更多细节。
如果你运行 ControlMania1 工程,你会看到所有的这些子类化都在起作用。在编辑框上右击会弹出消息框,在按钮上的光标像在前面显示的一样会改变其形状。
WTL 方法 2 - DDX_CONTROL_HANDLE
WTL 7.1 中添加的一个新特性是 DDX_CONTROL_HANDLE 宏。在 WTL 7.0 里,如果你想对一个单纯的窗口接口类(像 CWindow,CListViewCtrl 等)使用 DDX,你是不能使用 DDX_CONTROL 的,因为 DDX_CONTROL 只能和 CWindowImpl 的派生类一起工作。除了需要基类不同之外,DDX_CONTROL_HANDLE 和 DDX_CONTROL 一样地工作。
如果你还在使用 WTL 7.0,你可以使用以下宏定义一个可以和 DDX_CONTROL 一起工作的 CWindowImpl 派生类: #define DDX_CONTROL_IMPL(x) \
class x##_ddx : public CWindowImpl<x##_ddx, x> \
{ public: DECLARE_EMPTY_MSG_MAP() };
然后你就可以这样写: DDX_CONTROL_IMPL(CListViewCtrl)
这样你就有了一个名为 CListViewCtrl_ddx 的类,它的功能与 CListViewCtrl 一样,但是可以被 DDX_CONTROL 接受。
更多 DDX 的内容
DDX 可以,当然,实际上也确实是作数据交换的。WTL 支持在编辑框和字符串变量间交换字符串数据。它也可以将字符串解析为一个数字,并将该数据在整数型或者浮点型变量间传送。它还支持向/从 int 中传输复选框或者单选框组的状态。
DDX 宏
每个 DDX 宏都会展开为一个真正工作的对 CWinDataExchange 方法的调用。这些宏都有统一的形式:DDX_FOO(controlID, variable)。每个宏接受一种不同的变量类型,不过有的宏,例如 DDX_TEXT,被重载为可以接受多个类型。
DDX_TEXT
- 向/从编辑框中传送文本数据。变量可以是
CString、BSTR、CComBSTR,或者静态分配的字符数组。但用 new 分配的数组不能工作。
DDX_INT
- 在编辑框和
int 间传输数字数据。
DDX_UINT
- 在编辑框和
unsigned int 间传输数字数据。
DDX_FLOAT
- 在编辑框和
float 或者 double 间传输数字数据。
DDX_CHECK
- 向/从
int 中传输复选框的状态。
DDX_RADIO
- 向/从
int 中传输单选按钮组的状态。
DDX_CHECK 既可以接受 int 变量也可以接受 bool 变量。int 版本接收/返回 0、1 或者 2,也即相当于 BST_UNCHECKED、BST_CHECKED 和 BST_INDETERMINATE。bool 版本是 WTL 7.1 中加入的,可以用于没有中间状态的复选框,此版本在复选框被选中时接收/返回 true,未选中时接收/返回 false。如果复选框发生了出现中间状态的情况,则 DDX_CHECK 返回 false。
WTL 7.1 中还加入了另外一个用于浮点类型的宏:
DDX_FLOAT_P(controlID, variable, precision)
- 和
DDX_FLOAT 相似,但是在设置编辑框的文本时,数值被格式化为可以显示由 precision 指示的最高精度。
注意,如果在你的应用中使用了 DDX_FLOAT 或者 DDX_FLOAT_P,你需要在 stdafx.h 中添加如下的一个 #define,并且需要位于所包含的所有 WTL 头文件之前: #define _ATL_USE_DDX_FLOAT
这是因为出于代码大小的优化,缺省对于浮点的支持是被禁止掉了的。
关于 DoDataExchange() 的更多信息
你可以象在 MFC 中调用 UpdateData() 一样调用 DoDataExchange() 方法。DoDataExchange() 的原型为: BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE,
UINT nCtlID = (UINT)-1 );
参数:
bSaveAndValidate
- 指示数据传递方向的标志。传入
TRUE 为从控件传递数据到变量,传入 FALSE 为从变量传递数据到控件。注意此参数的缺省值是 FALSE,但 MFC 中 UpdateData() 函数的缺省值是 TRUE。你还可以使用符号 DDX_SAVE 和 DDX_LOAD(相应地被定义为 TRUE 及 FALSE)作为参数,如果你觉得更便于记忆的话。
nCtlID
- 传入 -1 更新所有控件。否则的话,如果你仅仅是要对某一个控件使用 DDX,则应该传入控件的 ID。
如果控件被成功更新,DoDataExchange() 返回 TRUE,否则返回 FALSE。在对话框中你可以覆盖两个函数以处理错误。一个是 OnDataExchangeError(),任何原因导致的数据交换失败都会调用到它。在 CWinDataExchange 中的缺省实现会发出哔响并将焦点设置到导致错误的控件上。另一个函数是 OnDataValidateError(),不过要到第五部分里介绍 DDV 的时候再讨论它。
使用 DDX
为了使用 DDX,我们向 CMainDlg 中添加两个变量。 class CMainDlg : public ...
{
//...
BEGIN_DDX_MAP(CMainDlg)
DDX_CONTROL(IDC_EDIT, m_wndEdit)
DDX_TEXT(IDC_EDIT, m_sEditContents)
DDX_INT(IDC_EDIT, m_nEditNumber)
END_DDX_MAP()
protected:
// DDX variables
CString m_sEditContents;
int m_nEditNumber;
};
在 OK 按钮的处理器中,我们首先调用 DoDataExchange() 将数据从编辑框中传输到我们刚刚添加的两个变量中,然后再把结果显示到列表控件中。 LRESULT CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl )
{
CString str;
// Transfer data from the controls to member variables.
if ( !DoDataExchange(true) )
return;
m_wndList.DeleteAllItems();
m_wndList.InsertItem ( 0, _T("DDX_TEXT") );
m_wndList.SetItemText ( 0, 1, m_sEditContents );
str.Format ( _T("%d"), m_nEditNumber );
m_wndList.InsertItem ( 1, _T("DDX_INT") );
m_wndList.SetItemText ( 1, 1, str );
}
![[DDX results - 11K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU9skpnH8E_B5hq-HrsD_STwtoUhPZ0Z-sdod51j7ustdqOy6DbLiIpgZ_Hm-Edi0uufydUUoryr5JAN8hNXJ6u7TjH4KJu3Ks9GsqtqdM6rGvM93tr3EOYs)
如果你在编辑框中输入了非数字文本,DDX_INT 就会失败,并调用 OnDataExchangeError()。CMainDlg 覆盖了 OnDataExchangeError() 以显示一个消息框: void CMainDlg::OnDataExchangeError ( UINT nCtrlID, BOOL bSave )
{
CString str;
str.Format ( _T("DDX error during exchange with control: %u"), nCtrlID );
MessageBox ( str, _T("ControlMania1"), MB_ICONWARNING );
::SetFocus ( GetDlgItem(nCtrlID) );
}
![[DDX error msg - 18K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU08fGU6uK1tJpNwT2Moahg9RUGIlzObhHTJq3j78PGheYCeD9W8clNlHV0SXLAoAwijvnibI1Kj44KXWlT3HkfI_3lrmoR3RmFbt_xEyw9grcd-iltLx7UM)
作为最后的 DDX 例子,我们添加一个复选框来演示 DDX_CHECK 的用法:
![[Msg checkbox - 12K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUwwp5Sd2q25k17mTkikWD1KJJV8JMCxSdWvkMs0bXNKhuLqrWhz8aAfwpm8Azt-N_XzvZYbemkOVsxZVJMYpmIqNZPWq4Cby02AFXNc60X3Cx7zSU7q4c4g)
此 DDX_CHECK 没有中间状态,所以我们可以为 DDX_CHECK 使用一个 bool 变量。下面是为复选框使用 DDX 而作的改动: class CMainDlg : public ...
{
//...
BEGIN_DDX_MAP(CMainDlg)
DDX_CONTROL(IDC_EDIT, m_wndEdit)
DDX_TEXT(IDC_EDIT, m_sEditContents)
DDX_INT(IDC_EDIT, m_nEditNumber)
DDX_CHECK(IDC_SHOW_MSG, m_bShowMsg)
END_DDX_MAP()
protected:
// DDX variables
CString m_sEditContents;
int m_nEditNumber;
bool m_bShowMsg;
};
在 OnOK() 的末尾,我们检查 m_bShowMsg 来看复选框是否被选中。 void CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl )
{
// Transfer data from the controls to member variables.
if ( !DoDataExchange(true) )
return;
//...
if ( m_bShowMsg )
MessageBox ( _T("DDX complete!"), _T("ControlMania1"),
MB_ICONINFORMATION );
}
示例工程中还有使用其他 DDX_* 宏的例子。
处理来自控件的通知
在 WTL 中处理通知与 API 级编程类似。控件以 WM_COMMAND 或者 WM_NOTIFY 消息的形式向其父窗口发送通知,其父窗口负责处理。另外还有几个消息也可以视作为通知,例如 WM_DRAWITEM,该消息在属主绘制控件需要绘制的时候发送。父窗口既可以自己处理通知消息,也可将消息反射回控件。反射像在 MFC 中一样工作 - 控件可以自己处理通知,使得代码具有自包容的形态,易于移到其他的工程中。
在父窗口中处理通知
以 WM_NOTIFY 和 WM_COMMAND 发送的通知包含有很多信息。WM_COMMAND 消息的参数中既有发送消息的控件的 ID,又有控件的 HWND,还有通知代码。WM_NOTIFY 消息除此之外还有一个指向 NMHDR 数据结构的指针。ATL 和 WTL 有若干的消息映射宏用于处理通知。在这儿我只介绍 WTL 的宏,毕竟这是一篇关于 WTL 的文章。注意,所有的这些宏都需要在消息映射中使用 BEGIN_MSG_MAP_EX 宏,并要在 stdafx.h 中 #include atlcrack.h。
消息映射宏
要处理 WM_COMMAND 通知,可以使用 COMMAND_HANDLER_EX 若干宏之一:
COMMAND_HANDLER_EX(id, code, func)
- 处理来自于一个特定控件的特定代码的通知
COMMAND_ID_HANDLER_EX(id, func)
- 处理特定代码的所有通知,不管是哪个控件发出的
COMMAND_CODE_HANDLER_EX(code, func)
- 处理特定控件的所有通知,不管通知代码
COMMAND_RANGE_HANDLER_EX(idFirst, idLast, func)
- 处理来自于 ID 处于 idFirst 到 idLast 范围之内的控件的所有通知,不管通知代码
COMMAND_RANGE_CODE_HANDLER_EX(idFirst, idLast, code, func)
- 处理来自于 ID 处于 idFirst 到 idLast 范围之内的控件的特定代码的通知
示例:
COMMAND_HANDLER_EX(IDC_USERNAME, EN_CHANGE, OnUsernameChange):处理来自于 ID 为 IDC_USERNAME 的编辑框的 EN_CHANGE 通知
COMMAND_ID_HANDLER_EX(IDOK, OnOK):处理来自于 ID 为 IDOK 的控件的所有通知
COMMAND_RANGE_CODE_HANDLER_EX(IDC_MONDAY, IDC_FRIDAY, BN_CLICKED, OnDayClicked):处理来自于 ID 介于 IDC_MONDAY 和 IDC_FRIDAY 之间的控件的 BN_CLICKED 通知
也有处理 WM_NOTIFY 消息的宏。它们像上述宏一样工作,但是它们的名字以“NOTIFY_”开头而不是“COMMAND_”。
WM_COMMAND 处理器的原型:
void func ( UINT uCode, int nCtrlID, HWND hwndCtrl );
WM_COMMAND 通知不使用返回值,所以处理器是返回 void。WM_NOTIFY 处理器的原型:
LRESULT func ( NMHDR* phdr );
处理器的返回值用作消息的结果。这与 MFC 不同,MFC 中处理器接受一个 LRESULT* 参数并通过该变量来设置消息的结果。通知代码和发送通知的控件的 HWND 在 NMHDR 结构中,分别为 code 和 hwndFrom 成员。和在 MFC 中一样,如果随通知发送的结构不是一个简单的 NMHDR,你的处理器应该把 phdr 参数转型为正确的类型。
我们要为 CMainDlg 添加一个通知处理器来处理由列表控件发送的 LVN_ITEMCHANGED,并在对话框中显示当前选中的条目。我们从添加消息映射宏以及消息处理器开始: class CMainDlg : public ...
{
BEGIN_MSG_MAP_EX(CMainDlg)
NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)
END_MSG_MAP()
LRESULT OnListItemchanged(NMHDR* phdr);
//...
};
下面是消息处理器: LRESULT CMainDlg::OnListItemchanged ( NMHDR* phdr )
{
NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr;
int nSelItem = m_wndList.GetSelectedIndex();
CString sMsg;
// If no item is selected, show "none". Otherwise, show its index.
if ( -1 == nSelItem )
sMsg = _T("(none)");
else
sMsg.Format ( _T("%d"), nSelItem );
SetDlgItemText ( IDC_SEL_ITEM, sMsg );
return 0; // retval ignored
}
此处理器并未使用 phdr 参数,但出于演示的目的,我还是将之转型为了 NMLISTVIEW*。
反射通知
如果你有一个 CWindowImpl 派生类,像先前我们的 CEditImpl 一样实现了一个控件,你就可以在此类中而不是父对话框中处理通知。这称作反射通知,与 MFC 的消息反射类似。所不同的是父窗口和控件都参与了反射,而 MFC 中仅有控件参与。
如果你要将通知反射回控件类,你只需向对话框的消息映射中添加一个宏 REFLECT_NOTIFICATIONS(): class CMainDlg : public ...
{
public:
BEGIN_MSG_MAP_EX(CMainDlg)
//...
NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)
REFLECT_NOTIFICATIONS()
END_MSG_MAP()
};
这一宏添加了一些代码到消息映射中,可以处理任何先前的宏都没有处理的通知消息。代码将检查消息的 HWND 并将消息发送到该窗口,但是消息的值会被改变为 OLE 控件所使用的值,OLE 控件具有相似的消息反射系统。新的值被称为 OCM_xxx 而不是 WM_xxx,不过在其他方面和其他非反射消息一样处理。
反射的消息共有 18 个:
- 控件通知:
WM_COMMAND、WM_NOTIFY、WM_PARENTNOTIFY
- 属主绘制:
WM_DRAWITEM、WM_MEASUREITEM、WM_COMPAREITEM、WM_DELETEITEM
- 列表框键盘消息:
WM_VKEYTOITEM、WM_CHARTOITEM
- 其他:
WM_HSCROLL、WM_VSCROLL、WM_CTLCOLOR*
在控件类中,你可以仅为感兴趣的反射消息添加处理器,然后在最后加上 DEFAULT_REFLECTION_HANDLER()。DEFAULT_REFLECTION_HANDLER() 确保未处理的消息能正确地路由到 DefWindowProc()。下面是一个简单的属主绘制按钮类,处理了反射的 WM_DRAWITEM。 class CODButtonImpl : public CWindowImpl<CODButtonImpl, CButton>
{
public:
BEGIN_MSG_MAP_EX(CODButtonImpl)
MSG_OCM_DRAWITEM(OnDrawItem)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
void OnDrawItem ( UINT idCtrl, LPDRAWITEMSTRUCT lpdis )
{
// do drawing here...
}
};
用于处理反射消息的 WTL 宏
我们仅仅看到了一个用于反射消息的 WTL 宏,即 MSG_OCM_DRAWITEM。对于其他的 17 个消息也有对应的 MSG_OCM_* 反射宏。由于 WM_NOTIFY 和 WM_COMMAND 具有需要拆解的参数,所以 WTL 除 MSG_OCM_COMMAND 和 MSG_OCM_NOTIFY 之外还为它们提供了特殊的宏。这些宏像 COMMAND_HANDLER_EX 和 NOTIFY_HANDLER_EX 一样地工作,但是具有 “REFLECTED_” 前缀。例如,树控件可以有这样的消息映射: class CMyTreeCtrl : public CWindowImpl<CMyTreeCtrl, CTreeViewCtrl>
{
public:
BEGIN_MSG_MAP_EX(CMyTreeCtrl)
REFLECTED_NOTIFY_CODE_HANDLER_EX(TVN_ITEMEXPANDING, OnItemExpanding)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
LRESULT OnItemExpanding ( NMHDR* phdr );
};
如果你检点一下示例代码中的 ControlMania1 对话框,就会发现有一个像上面一样处理了 TVN_ITEMEXPANDING 的树控件。CMainDlg 的成员 m_wndTree 通过 DDX 连接到树控件,而且 CMainDlg 反射了通知消息。树的 OnItemExpanding() 处理器看起来是这样的: LRESULT CBuffyTreeCtrl::OnItemExpanding ( NMHDR* phdr )
{
NMTREEVIEW* pnmtv = (NMTREEVIEW*) phdr;
if ( pnmtv->action & TVE_COLLAPSE )
return TRUE; // don't allow it
else
return FALSE; // allow it
}
如果你运行 ControlMania1 并点击树中的 +/- 按钮,你就可以看到处理器在工作 - 一旦你展开了一个节点,就再也不能折叠回去了。
拾零
对话框字体
如果你和我一样对用户界面吹毛求疵而又正好在使用 Win 2000 或者 XP,你就可能会对对话框为什么使用的是 MS Sans Serif 字体而不是 Tahoma 字体而感到奇怪。其实是因为 VC 6 实在是太古老了,它生成的资源文件在 NT 4 上可以很好地工作,但对于 NT 的后续版本却不能。你可以修正这一问题,不过需要手动编辑资源文件。
你需要对资源文件中存在的每个对话框做三样改动:
- 对话框类型:把
DIALOG 改为 DIALOGEX
- 窗口风格:添加
DS_SHELLFONT
- 对话框字体:把 MS Sans Serif 改为 MS Shell Dlg
不幸的是,如果你又改动并保存(译者注:在集成环境的资源编辑器里)了资源,前两项改动会丢失,你需要再次修改。下面是对话框改动之前的一个示例: IDD_ABOUTBOX DIALOG DISCARDABLE 0, 0, 187, 102
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Sans Serif"
BEGIN
...
END
下面是之后的样子: IDD_ABOUTBOX DIALOGEX DISCARDABLE 0, 0, 187, 102
STYLE DS_SHELLFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Shell Dlg"
BEGIN
...
END
做完这些改动之后,对话框在新的操作系统上会使用 Tahoma 字体,在旧的操作系统上(在需要时)仍然会使用 MS Sans Serif 字体。
在 VC 7 里,要使用正确的字体你只需要在对话框编辑器里改变一个设置:
![[VC7 dlg editor setting - 8K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU8hyi4OC4VAwQAzMeAxjOv3qystX-rRAtf1HtkEhG6g6kbeKH5o-Adk0ad564nPea1-wDAWQaVGquo5DVqY5Csid57P9_ovr5ltSbcrmIflSUZ1WUUbHODo)
当你把 Use System Font 改为 True 时,编辑器会帮你把字体改成 MS Shell Dlg。
_ATL_MIN_CRT
正如在 VC Forum FAQ 中讲到的,ATL 具有一个优化功能,可以使你创建无需链接 C 运行时库(CRT)的应用程序。这一优化通过在预处理选项中添加 _ATL_MIN_CRT 符号来启用。AppWizard 生成的应用在 Release 配置中包含了这一符号。由于我从来没有写过任何一个有价值而又不需要使用 CRT 中任何东西的应用程序,所以我总是去掉这一符号。而且在任何情况下,如果你在 CString 或者 DDX 中使用了浮点特性,你都需要去掉它。
下一步
在第五部分里,会涵盖以下知识,对话框数据验证(DDV)、WTL 中的新控件以及诸如属主绘制(owner draw)和定制绘制(custom draw)这样的高级用户界面特性。
修订历史
2003 年 4 月 27 日:首次发布 2005 年 12 月 20 日:更新,包括了 WTL 7.1 中的改变
| 链接: 上一部分; 下一部分
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分;下一部分
第三部分 - 工具栏和状态栏
|
内容
- 第三部分介绍
- 框架中的工具栏和状态栏
- AppWizard 为工具栏和状态栏生成的代码
- CMainFrame 如何创建栏
- 显示或者隐藏栏
- 栏的内建特性
- 使用不同的风格创建工具栏
- 工具栏编辑器
- 工具栏按钮的 UI 更新
- 用复用栏代替平实的工具栏
- 多窗格状态栏
- 下一步:关于对话框的一切
- 参考资料
- 修订历史
第三部分介绍
从在 Windows 95 中被作为通用控件开始,使用工具栏和状态栏已经成为了平常事。MFC 对多浮动工具栏的支持也对他们的流行起了推波助澜的作用。在后来的通用控件升级中,复用栏(Rebar,或者最初被称为的酷栏,coolbar)又为如何呈现工具栏增添了新的途径。在这一部分里,我会涵盖以下知识,WTL 如何对这些种类的栏进行支持以及如何在自己的应用中使用它们。
记住,如果你安装 WTL 或者编译示例代码时遇到了任何问题,请在张贴你的问题之前阅读第一部分的 ReadMe 一节。
框架中的工具栏和状态栏
CFrameWindowImpl 有三个 HWND 成员会在框架窗口创建的时候被设置妥当。我们已经见过了 m_hWndClient,它是框架窗口客户区的“视图”窗口的句柄。现在我们会遇到另外的两个:
m_hWndToolBar:工具栏或者复用栏的 HWND
m_hWndStatusBar:状态栏的 HWND
CFrameWindowImpl 仅支持一个工具栏,而且没有 MFC 中可停靠多工具栏的等价物。如果你需要不止一个工具栏,又不想去力劈 CFrameWindowImpl 的内部实现,那就应该使用一个复用栏。我会兼顾这两种情形并演示如何在 AppWizard 中选择其一。
CFrameWindowImpl::OnSize() 处理器会调用 UpdateLayout(),而后者会做两件事情:定位所有的栏,并改变视图窗口的大小以填充客户区。UpdateLayout() 调用 UpdateBarsPosition() 去做实际的工作。代码是相当简单的,只是发送一个 WM_SIZE 消息给工具栏和状态栏,当然前提是它们已经创建完毕了。这些栏的缺省窗口过程来料理把它们移动到框架窗口的顶部或者底部这些事。
如果你要求 AppWizard 给你的框架窗口工具栏和状态栏,那么向导会向 CMainFrame::OnCreate() 函数中放置生成这些栏的代码。在我们要写另一个时钟应用的同时,正好可以来仔细瞅瞅这些代码。
AppWizard 为工具栏和状态栏生成的代码
我们来开始一个新工程并让向导为框架窗口生成工具栏和状态栏。创建一个名为 WTLClock2 的 WTL 工程。在 AppWizard 的第一页,选择 SDI 应用,并选中 Generate CPP files:
![[AppWizard pg 1 - 22K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxk4i62pbp9jYJb01ACSTjjIitMSpTfi0negY7Fg7gAzhu5Iw-cLpdxzTSfGURw_LtdmHYZTdvtuehGQDHzKw-mE6lm9VTxkClME_WeVinUVg)
![[VC7 AppWizard pg 1 - 24K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU2CTyeUJ-ielFAvvV8gEdhHsMpRpd-thqE5LvmjCzuQ4ID-TPYEcbMBlX-2d180DqTFSsqZDxDGCfDKysf5zOO9vujZb-hk7Bf0gaVAA4Dy-PUqOMz1vg8A)
在下一页,不要选 Rebar 以使向导生成普通的工具栏:
![[AppWizard pg 2 - 21K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzBZN9uWLFCk_Myl9ILzX2risQSRPs9fOu7Kl-_lCP0A3f2Uk7zh3XvakMI9cMGKU5oSY0j1X8axgKaO_zUwIFtGCeWTQo9RuJe7voJ0OFvKA)
![[VC7 AppWizard pg 2 - 22K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUw59nqktGnKPdNw-vJtTYUEZmOAWaaysbSK6LVSQiog0zEn8CjAp6qZrgYylF4I2aF9ujY7onhlZLx_sbq_M4rEriRvl4kKToDNKCo4aMzKaq_1xaSBowAQ)
从第二部分的应用中把时钟相关的代码复制过来之后,这个新的应用看起来应该是这样:
![[default toobar - 5K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJyxmNdGL8IiBpT2I2FOkfrl5b_wZ_tZQNoJJppEOCiGuSX4f2BPzxmlLQ8HalldRxiv6t99WgHydfBQJxmJZibB4kjqiPANFwkccmNF1FGJBg)
CMainFrame 如何创建栏
在这个工程里,AppWizard 向 CMainFrame::OnCreate() 函数添加了更多的代码。其作用是创建所有栏并告诉 CUpdateUI 更新工具栏的按钮。 LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,
LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
CreateSimpleToolBar();
CreateSimpleStatusBar();
m_hWndClient = m_view.Create(...);
// ...
// register object for message filtering and idle updates
CMessageLoop* pLoop = _Module.GetMessageLoop();
ATLASSERT(pLoop != NULL);
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
return 0;
}
新代码在函数的开头。CFrameWindowImpl::CreateSimpleToolBar() 使用工具栏资源 IDR_MAINFRAME 创建一个工具栏并将其句柄保存至 m_hWndToolBar。这儿是 CreateSimpleToolBar() 的代码: BOOL CFrameWindowImpl::CreateSimpleToolBar(
UINT nResourceID = 0,
DWORD dwStyle = ATL_SIMPLE_TOOLBAR_STYLE,
UINT nID = ATL_IDW_TOOLBAR)
{
ATLASSERT(!::IsWindow(m_hWndToolBar));
if(nResourceID == 0)
nResourceID = T::GetWndClassInfo().m_uCommonResourceID;
m_hWndToolBar = T::CreateSimpleToolBarCtrl(m_hWnd, nResourceID, TRUE, dwStyle, nID);
return (m_hWndToolBar != NULL);
}
参数:
nResourceID
- 使用的工具栏资源的 ID。缺省值 0 表示使用在
DECLARE_FRAME_WND_CLASS 宏里指定的 ID,在向导生成的代码中,这个值是 IDR_MAINFRAME。
dwStyle
- 工具栏风格。缺省值
ATL_SIMPLE_TOOLBAR_STYLE 被定义为 TBSTYLE_TOOLTIPS 再加上通常的子窗口风格以及可见风格。这使得光标停留在按钮上的时候工具栏会创建要用到的工具提示(tooltip)控件。
nID
- 工具栏的窗口 ID,通常你都会使用缺省值。
CreateSimpleToolBar() 函数如果检查到工具栏还没有创建就会调用 CreateSimpleToolBarCtrl() 来做实际的创建工作。由 CreateSimpleToolBarCtrl() 返回的句柄保存在 m_hWndToolBar 里。CreateSimpleToolBarCtrl() 读取资源并据之创建工具栏按钮,然后返回工具栏窗口的句柄。由于代码相当长,这里就不再赘述。感兴趣的话可以在 atlframe.h 中找到。
OnCreate() 中的下一个调用是 CFrameWindowImpl::CreateSimpleStatusBar()。这将会创建一个状态栏并把句柄保存到 m_hWndStatusBar 中。代码如下:
BOOL CFrameWindowImpl::CreateSimpleStatusBar(
UINT nTextID = ATL_IDS_IDLEMESSAGE,
DWORD dwStyle = ... SBARS_SIZEGRIP,
UINT nID = ATL_IDW_STATUS_BAR)
{
TCHAR szText[128]; // max text lentgth is 127 for status bars
szText[0] = 0;
::LoadString(_Module.GetResourceInstance(), nTextID, szText, 128);
return CreateSimpleStatusBar(szText, dwStyle, nID);
}
这会从字符串表中加载一个字符串,它会被显示到状态栏中。参数:
nTextID
- 显示到状态栏中的初始文字的资源 ID。AppWizard 会生成字符串“Ready”,ID 为 ATL_IDS_IDLEMESSAGE。
dwStyle
- 状态栏的风格。缺省值包括了
SBARS_SIZEGRIP 以便在右下角能够有一个用来改变窗口大小的握柄(gripper)。
nID
- 状态栏的窗口 ID,通常使用缺省值即可。
CreateSimpleStatusBar() 会调用自己的一个重载版本来做实际工作:
BOOL CFrameWindowImpl::CreateSimpleStatusBar(
LPCTSTR lpstrText,
DWORD dwStyle = ... SBARS_SIZEGRIP,
UINT nID = ATL_IDW_STATUS_BAR)
{
ATLASSERT(!::IsWindow(m_hWndStatusBar));
m_hWndStatusBar = ::CreateStatusWindow(dwStyle, lpstrText, m_hWnd, nID);
return (m_hWndStatusBar != NULL);
}
这个版本的函数如果检查到状态栏尚未创建,则会调用 CreateStatusWindow() 来创建状态栏。然后把状态栏句柄存放至 m_hWndStatusBar。
显示或者隐藏栏
CMainFrame 还有一个 View 菜单,有两条命令来显示或者隐藏工具栏和状态栏。这两个命令的 ID 是 ID_VIEW_TOOLBAR 和 ID_VIEW_STATUS_BAR。CMainFrame 类有这两个命令的处理器,用以相应地显示或者隐藏指定栏。下面是 OnViewToolBar() 处理器:
LRESULT CMainFrame::OnViewToolBar(WORD /*wNotifyCode*/, WORD /*wID*/,
HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
BOOL bVisible = !::IsWindowVisible(m_hWndToolBar);
::ShowWindow(m_hWndToolBar, bVisible ? SW_SHOWNOACTIVATE : SW_HIDE);
UISetCheck(ID_VIEW_TOOLBAR, bVisible);
UpdateLayout();
return 0;
}
此函数切换指定栏的可视状态,切换 View|Toolbar 菜单项旁边的选中标志,然后再调用 UpdateLayout() 来定位指定栏(如果是要变为可视的话)并改变视图窗口的大小。
栏的内建特性
MFC 为其工具栏和状态栏提供了一些很好的特性,比如作用于工具栏按钮的工具提示和菜单项的动态帮助。WTL 在 CFrameWindowImpl 类中也实现了相同的特性。下面是正显示着工具提示和动态帮助的屏幕截图。
![[Status bar flyby help - 5K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxhGAoYdG0DR5NTOcPLcM34N3knOS-oYAFOAyNC4kDSSpIs4poHPpG8p3hvowS3xYQGQr8RACQT08tSS5O7ZDlRA-mpOAYpDqsrwGgPlobOLA)
CFrameWindowImplBase 有两个处理器用以实现这两个特性。OnMenuSelect() 处理 WM_MENUSELECT,而且它还会像 MFC 那样寻找动态帮助 - 加载与当前选中的菜单项相同 ID 的字符串资源,在字符串中寻找 \n 字符,并使用 \n 前的文字作为动态帮助。OnToolTipTextA() 和 OnToolTipTextW() 相应地处理 TTN_GETDISPINFOA 和 TTN_GETDISPINFOW 来为工具栏按钮提供工具提示。这两个处理器和 OnMenuSelect() 加载相同的字符串,不过会使用 \n 之后的文字。(附注:在 WTL 7.0 和 WTL 7.1 里,OnMenuSelect() 和 OnToolTipTextA() 并非双字节字符集安全的,因为它们在查找 \n 时不会检查双字节字符集的前导或者结尾字节)。以下是一个工具栏按钮及其关联的帮助字符串的例子:
![[Toolbar button and help string - 9K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxTfmMrLsoGAYyMgFwNK5YM23Cuq0o15T_OhlvSH_bX7r1hOdgLsxZJhVJ9fWJoT1bvjwtT1Iw6FDqzOv50c3QO7KHMhYCgVMK8PiL1jVOoMw)
使用不同的风格创建工具栏
你可以向 CreateSimpleToolBar() 传递风格位作为第二个参数以更改工具栏的风格。例如,要使用三维按钮并创建类似于 Internet Explorer 的工具栏,可以在 CMainFrame::OnCreate() 中使用如下代码: CreateSimpleToolBar ( 0, ATL_SIMPLE_TOOLBAR_STYLE |
TBSTYLE_FLAT | TBSTYLE_LIST );
注意,如果你在应用里使用了第六版通用控件的 manifest 文件,那么当运行在 Windows XP 以及后续的系统上时,你对按钮就没有选择余地了 - 工具栏总是使用平面按钮,即使在创建工具栏时没有指定 TBSTYLE_FLAT 风格。
工具栏编辑器
正如在前面所看到的,AppWizard 会创建好几个缺省的按钮,不过只有 About 按钮会被处理。你可以像在 MFC 工程中一样编辑工具栏,编辑器可以修改 CreateSimpleToolBarCtrl() 要用到的工具栏资源。下面是 AppWizard 在编辑器中生成的工具栏的样子:
![[default toolbar in the editor - 7K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxVxTaGMwrB3Ho4JT4471DNPcHOOIozNwOP64I99zJmhDmW1jZ_W0vbZy5e1CwE8icxOXskAxIjlNiWcbEoNHevbGTnT8tHFyL7E86vn8vDlA)
对我们的时钟应用来说,我们要添加两个按钮,用来改变视图窗口的颜色,另外两个按钮来显示或者隐藏工具栏以及状态栏。下面是我们的新工具栏:
![[New toolbar buttons - 5K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJyuIQ90gYkvBS8KHzzZhfw0Gd-Ix2zokXTnpzAD--AqKbWULloQIt7NTCciPglfAuXCdbhFFiZY5Hbiu8wcLKNyL8Azjrt21tTxSdjzH5G_nQ)
按钮:
IDC_CP_COLORS:将视图改变为 CodeProject 所使用的颜色
IDC_BW_COLORS:将试图改回为黑白色
ID_VIEW_STATUS_BAR:显示或者隐藏状态栏
ID_VIEW_TOOLBAR:显示或者隐藏工具栏
前两个按钮在 View 菜单上也有其对应的菜单项。它们都会调用视图类的一个新函数,名为 SetColors()。调用时传入视图窗口用来显示时钟的前景色和背景色。处理这两个按钮与使用 COMMAND_ID_HANDLER_EX 宏处理菜单项没有任何区别,如果你要看消息处理的细节的话可以查看示例工程。在下一节里,会提及对视图状态栏以及视图工具栏按钮的 UI 更新,这样就可以反映这些栏的当前状态。
工具栏按钮的 UI 更新
AppWizard 生成的 CMainFrame 已经带有 UI 更新的处理器了,它们可以选中或者去选 View|Toolbar 以及 View|Status Bar 菜单项。这些事情和在第二部分的应用里做的一模一样 - 就是 CMainFrame 中针对每个命令的 UI 更新宏: BEGIN_UPDATE_UI_MAP(CMainFrame)
UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP)
UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP)
END_UPDATE_UI_MAP()
由于我们的时钟应用的工具栏有 ID 相同的按钮,所以第一步就是为每个宏添加 UPDUI_TOOLBAR 标志: BEGIN_UPDATE_UI_MAP(CMainFrame)
UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)
UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)
END_UPDATE_UI_MAP()
要处理工具栏按钮的更新,还要调用另外的两个函数,不过幸好 AppWizard 生成的代码里应经有了。所以,如果你现在就编译工程,菜单项和工具栏按钮都可以更新。
启用工具栏 UI 更新
如果你看 CMainFrame::OnCreate() 函数,你会看到一段新代码,它设置了两个 View 菜单项的初始状态: LRESULT CMainFrame::OnCreate( ... )
{
// ...
m_hWndClient = m_view.Create(...);
UIAddToolBar(m_hWndToolBar);
UISetCheck(ID_VIEW_TOOLBAR, 1);
UISetCheck(ID_VIEW_STATUS_BAR, 1);
// ...
}
UIAddToolBar() 告诉 CUpdateUI 我们工具栏的 HWND,所以当需要更新按钮状态的时候它知道应该向哪个窗口发送消息。另一个重要的调用在 OnIdle() 里:
BOOL CMainFrame::OnIdle()
{
UIUpdateToolBar();
return FALSE;
}
当消息队列里没有等待的消息时,CMessageLoop::Run() 函数会对 OnIdle() 进行调用。 UIUpdateToolBar() 遍历 UI 更新表,寻找带有 UPDUI_TOOLBAR 标志同时又被 UISetCheck() 等调用改变了的元素,然后相应地改变按钮的状态。请注意,当我们仅更新弹出菜单项时并不需要这两个步骤,因为 CUpdateUI 会处理 WM_INITMENUPOPUP 并在该消息发送时更新菜单。
如果你查看示例工程的代码,你还会看到 UI 更新是如何更新顶级菜单项的。有一个菜单项,是执行 Start 和 Stop 命令来开始或停止时钟的。当然,这不是常见的做法(也不推荐这样做) -- 菜单栏上的项应该都是弹出菜单 -- 我之所以这么做是出于完整演示 CUpdateUI 的目的。具体可以查看对 UIAddMenuBar() 和 UIUpdateMenuBar() 的调用。
使用复用栏替代平实的工具栏
CFrameWindowImpl 也支持使用复用栏以使应用程序看起来和 Internet Explorer 差不多。而且,使用复用栏也是同时使用多个工具栏的方法。要使用复用栏,可以在 AppWizard 的第二页选中 Rebar 框,如下图所示:
![[AppWizard pg 2 with rebar checked - 21K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwFG2uZr6_ieJLxv45QXnL_WaUblhCqj2L4otv_5Bfcp3ojqI62RNSPzCwy3Wt4jLvdKlsyXz32zfiAqqhhAz8CjTyTUvD57Xtqaedo0XDqrQ)
![[VC7 AppWizard pg 2 with rebar checked - 22K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU3Ylmeea776tAZelKHn8Cp44QvkiUQOfrouwQ0SW-b4N_IpGQMLD60IuOSRmMIcsHGm_AinF5BTw9OdjC2KqwwPK2fKgv9923izhgd0aBA0tri3LGM-LUyM)
我们的第二个例子,WTLClock3,就选中了这一选项。如果你要一路追踪示例代码,那现在就打开 WTLClock3 工程。
你注意到的第一个不同之处应该是创建工具栏的代码。这是有道理的,因为我们是要在应用中使用复用栏。下面是相关的代码: LRESULT CMainFrame::OnCreate(...)
{
HWND hWndToolBar = CreateSimpleToolBarCtrl ( m_hWnd,
IDR_MAINFRAME, FALSE,
ATL_SIMPLE_TOOLBAR_PANE_STYLE );
CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);
AddSimpleReBarBand(hWndToolBar);
// ...
}
代码是以创建工具栏开始的,但是使用了不同的风格 ATL_SIMPLE_TOOLBAR_PANE_STYLE。这是在 atlframe.h 的一个 #define,与 ATL_SIMPLE_TOOLBAR_STYLE 很相似,但是附加有像 CCS_NOPARENTALIGN 这样的风格,这是使工具栏能够作为复用栏的子窗口正常地工作所必须的。
下一行是对 CreateSimpleReBar() 的调用,创建一个复用栏并将其 HWND 保存到 m_hWndToolBar 中。接下来,AddSimpleReBarBand() 向复用栏添加一个复用条,并告诉复用栏该复用条内要包容工具栏。
![[App with rebar - 4K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJyVORTXXFHbgK_pZKZJasi3gU35Y-N-lGlxUEbGRW_Vn_5dn5-o8hq0x8vXQbDZb4t0ffazpFlBAb69uU1VpShLbgd9UUoVe51_-0ZfjUy46A)
CMainFrame::OnViewToolBar() 也有不同,它将隐藏包含工具栏的复用条而不是隐藏 m_hWndToolBar,因为那样将隐藏整个复用栏,而不仅仅是一个工具栏。
如果需要多个工具栏,你可以在 OnCreate() 中分别创建它们并调用 AddSimpleReBarBand(),就像向导为第一个工具栏生成的代码一样。因为 CFrameWindowImpl 使用的是标准的复用栏控件,并没有像 MFC 一样对可停靠工具栏进行支持,用户能做的只能是重新排列复用栏上的工具栏的位置。
多窗格状态栏
WTL 还有另外一个状态栏类,实现了在一个栏内包括多个窗格,就像 MFC 里那种有 CAPS LOCK 和 NUM LOCK 指示器的默认状态栏那样。这个类叫做 CMultiPaneStatusBarCtrl,在 WTLClock3 例子里演示了其用法。这个类支持了有限的 UI 更新,而且当弹出菜单显示出来的时候 “缺省” 窗格会拉伸到完整宽度以显示动态帮助。
第一步在 CMainFrame 中声明一个 CMultiPaneStatusBarCtrl 成员变量: class CMainFrame : public ...
{
//...
protected:
CMultiPaneStatusBarCtrl m_wndStatusBar;
};
然后在 OnCreate() 函数中,我们创建状态栏并为它做好 UI 更新的准备工作: m_hWndStatusBar = m_wndStatusBar.Create ( *this );
UIAddStatusBar ( m_hWndStatusBar );
注意,和 CreateSimpleStatusBar() 一样,我们把状态栏句柄保存到了 m_hWndStatusBar 里。
下一步则是通过调用 CMultiPaneStatusBarCtrl::SetPanes() 来设置窗格: BOOL SetPanes(int* pPanes, int nPanes, bool bSetText = true);
参数:
pPanes
- 窗格 ID 的数组
nPanes
pPanes 中元素的数量
bSetText
- 如果为 true,所有的窗格文字会被立即设置。后面会进一步解释。
窗格 ID 既可以是 ID_DEFAULT_PANE 来创建用于动态帮助的窗格,也可以是字符串表中的字符串 ID。对于非缺省窗格,WTL 加载该字符串并计算其宽度,然后将对应的窗格设置为该宽度。这与 MFC 的逻辑是一致的。
bSetText 用来控制是否立即显示字符串。如果设置为 true,SetPanes() 在每个窗格中显示字符串,否则留空。
下面是调用 SetPanes() 的代码: // Create the status bar panes.
int anPanes[] = { ID_DEFAULT_PANE, IDPANE_STATUS,
IDPANE_CAPS_INDICATOR };
m_wndStatusBar.SetPanes ( anPanes, 3, false );
字符串 IDPANE_STATUS 为“@@@@”,这样就应该(希望如此)有足够的空间来显示时钟的两个状态字符串“Running”和“Stopped”了。像 MFC 一样,你需要估计窗格占用空间的多少。字符串 IDPANE_CAPS_INDICATOR 则是 “CAPS”。
窗格的 UI 更新
为了能够更新窗格的文本,我们要在 UI 更新表中新添入口: BEGIN_UPDATE_UI_MAP(CMainFrame)
//...
UPDATE_ELEMENT(1, UPDUI_STATUSBAR) // clock status
UPDATE_ELEMENT(2, UPDUI_STATUSBAR) // CAPS indicator
END_UPDATE_UI_MAP()
宏的第一个参数是窗格的索引,而非 ID。如果你要重新排列窗格,那你就要关注此事,如果在映射里并非每个窗格都有入口的话,你需要更新 UPDATE_ELEMENT 宏里的数字来匹配新的顺序。
由于我们把 SetPanes() 的第三个参数设置为了 false,所以窗格开始的时候是空着的。我们的下一步就是将时钟状态窗格的初始文本设置为“Running”。 // Set the initial text for the clock status pane.
UISetText ( 1, _T("Running") );
和上面一样,第一个参数也是窗格的索引。UISetText() 是唯一能工作于状态栏的 UI 更新函数。
最后,我们还需要在 CMainFrame::OnIdle() 中加入对 UIUpdateStatusBar() 的调用,以使状态栏窗格在空闲时间能被更新: BOOL CMainFrame::OnIdle()
{
UIUpdateToolBar();
UIUpdateStatusBar();
return FALSE;
}
当你使用 UIUpdateStatusBar() 函数时,CUpdateUI 里的一个问题就会显露出来 - 菜单项的文本在调用 UISetText() 后并未更新!如果你查看 WTLClock3 工程,会发现时钟的开始/停止菜单项移到了 Clock 菜单下,而且命令的处理器设置了菜单项的文本。但是,如果对 UIUpdateStatusBar() 的调用存在,则 UISetText() 调用不会起作用。此问题在 WTL 7.1 里仍然存在,有些已经研究并提出了解决方案 - 详情请参看文末的讨论。
最后,我们需要检查 CAPS LOCK 键的状态并相应更新第二个窗格。代码放到 OnIdle() 函数里,从而应用每次进入空闲时都会检查。 BOOL CMainFrame::OnIdle()
{
// Check the current Caps Lock state, and if it is on, show the
// CAPS indicator in pane 2 of the status bar.
if ( GetKeyState(VK_CAPITAL) & 1 )
UISetText ( 2, CString(LPCTSTR(IDPANE_CAPS_INDICATOR)) );
else
UISetText ( 2, _T("") );
UIUpdateToolBar();
UIUpdateStatusBar();
return FALSE;
}
第一次调用 UISetText() 时,会通过在 CString 构造函数中使用一个优雅然而却是完全文档化了的技巧来从字符串表中加载 “CAPS” 字符串。
完成了这些代码后,状态栏看起来应该是这样:
![[Multipane status bar - 4K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJyxoCb0cdLFBhsvK242Udxjyf8bSaVzv54uobCQcw8i5DlWGDHWj1p9ShFDJ0AU3Fh8oqtdCGzyH0DYv1Mwy2ZedD_tHPwfwANPltDGXFTVGA)
下一步:对话框的一切
在第四部分里,我会介绍对话框(包括 ATL 的类以及 WTL 的增强),控件封装类,以及更多的与对话框和控件相关的 WTL 消息处理的改进。
参考资料
“How to use the WTL multipane status bar control” ,Ed Gadziemski,其中有 CMultiPaneStatusBarCtrl 类更为详尽的细节知识。
修订历史
2003 年 4 月 11 日:首次发布 2005 年 12 月 20 日:更新,包括了 VC 7.1 的改动
|
链接:上一部分;下一部分
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:上一部分;下一部分
第二部分 - WTL 中的 GUI 基础类
|
内容
- 第二部分介绍
- WTL 综述
- 开始一个 WTL EXE
- WTL 消息映射的增强
- 使用 WTL AppWizard 可以得到什么
- 通历向导(VC 6)
- 通历向导(VC 7)
- 检查生成的代码
- CMessageLoop 内幕
- CFrameWindowImpl 内幕
- 回到时钟程序
- UI 更新
关于消息映射的最后注意事项
下一站,1995
修订历史
第二部分介绍
好,是实实在在地讲述 WTL 的时候了!在这部分里,我会介绍写一个主框架窗口的基础知识,以及 WTL 引入的比较受欢迎的改进,比如 UI 更新和更好的消息影射。为了最大程度地掌握本部分的内容,你应该安装 WTL 以使其头文件处于 VC 的搜索路径中,而且 AppWizard 也在适当的目录下。WTL 的分发包中附有如何安装 AppWizard 的说明,请参考该文档。
记住,如果你安装 WTL 或者编译示例代码时遇到了任何问题,请在张贴你的问题之前阅读第一部分的 ReadMe 一节。
WTL 综述
WTL 的类可以分为几个主要的类别:
- 框架窗口的实现 -
CFrameWindowImpl, CMDIFrameWindowImpl
- 控件封装 -
CButton, CListViewCtrl
- GDI 封装 -
CDC, CMenu
- 特殊的 UI 特性 -
CSplitterWindow, CUpdateUI, CDialogResize, CCustomDraw
- 工具类以及宏 -
CString, CRect, BEGIN_MSG_MAP_EX
本文将深入到框架窗口中去,顺便提及一些 UI 特性和工具类。大多数的类都是独立的,不过也有一些像 CDialogResize 这样的嵌入类(mix-in)。
开始一个 WTL EXE
如果你不使用 WTL AppWizard (稍后我们就会提到它),那么一个 WTL EXE 一开始会很像一个 ATL EXE。如同第一部分中的那样,本文中的示例代码是另一个框架窗口,不过为了展示一些 WTL 的特性,较之前者不再那么微不足道。
在本节里,我们会从头开始一个新的 EXE。主窗口会在其客户区显示当前的时间。下面是一个基本的 stdafx.h: #define STRICT
#define WIN32_LEAN_AND_MEAN
#define _WTL_USE_CSTRING
#include <atlbase.h> // base ATL classes
#include <atlapp.h> // base WTL classes
extern CAppModule _Module; // WTL version of CComModule
#include <atlwin.h> // ATL GUI classes
#include <atlframe.h> // WTL frame window classes
#include <atlmisc.h> // WTL utility classes like CString
#include <atlcrack.h> // WTL enhanced msg map macros
atlapp.h 是要包含的第一个 WTL 头文件。它包含了用于消息处理的类和一个继承自 CComModule 的类 CAppModule。如果你计划使用 CString 那就还应该定义 _WTL_USE_CSTRING,因为 CString 定义在 atlmisc.h 里,而在 atlmisc.h 包含的其他头文件里有的特性会使用到 CString。定义 _WTL_USE_CSTRING 使得 atlapp.h 会前向声明 CString 类,从而使其他的这些头文件知道一个 CString 究竟是什么。
(注意,我们需要一个全局的 CAppModule 变量尽管在第一部分里这不是必需的。CAppModule 的一些特性与我们所需的空闲处理以及 UI 更新相关,所以我们需要 CAppModule 的存在)
接下来我们来定义我们的框架窗口。像我们这样的 SDI 窗口继承自 CFrameWindowImpl。窗口类名是使用 DECLARE_FRAME_WND_CLASS 而不是 DECLARE_WND_CLASS 来定义。这儿是 MyWindow.h 里我们窗口定义的开头: // MyWindow.h:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
DECLARE_FRAME_WND_CLASS(_T("First WTL window"), IDR_MAINFRAME);
BEGIN_MSG_MAP(CMyWindow)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
};
DECLARE_FRAME_WND_CLASS 有两个参数,窗口类名(可以为 NULL,ATL 会替你生成一个类名),和一个资源 ID。WTL 会根据此 ID 去寻找图标、菜单以及加速键表,并在窗口创建时加载它们。还会根据此 ID 寻找一个字符串,然后使用该串作为窗口的标题。我们还把消息串联到 CFrameWindowImpl,因为它有自己的一些消息处理器,尤其是 WM_SIZE 和 WM_DESTROY。
现在我们来看 WinMain()。它和第一部分中的 WinMain() 极其类似,只是创建主窗口的调用存在差异。 // main.cpp:
#include "stdafx.h"
#include "MyWindow.h"
CAppModule _Module;
int APIENTRY WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow )
{
_Module.Init ( NULL, hInstance );
CMyWindow wndMain;
MSG msg;
// Create the main window
if ( NULL == wndMain.CreateEx() )
return 1; // Window creation failed
// Show the window
wndMain.ShowWindow ( nCmdShow );
wndMain.UpdateWindow();
// Standard Win32 message loop
while ( GetMessage ( &msg, NULL, 0, 0 ) > 0 )
{
TranslateMessage ( &msg );
DispatchMessage ( &msg );
}
_Module.Term();
return msg.wParam;
}
CFrameWindowImpl 的 CreateEx() 方法采用了最常用的缺省值,因而我们不需要指定任何参数。CFrameWindowImpl 还会处理前文提到的资源加载事宜,所以现在你应该使用 IDR_MAINFRAME 这一 ID 生成一些伪资源,或者使用随本文附带的示例代码。
如果你马上运行,就可以看到主框架窗口了,当然,它实际上还没有做任何事情。我们需要加入一些消息处理器来干活儿,所以现在是研究 WTL 消息映射宏的好时机。
WTL 消息映射的增强
在使用 Win32 API 时,既令人讨厌又易于出错的事情之一就是从随消息一起发送过来的 WPARAM 和 LPARAM 数据中拆封参数。不幸的是,ATL 并未提供更多的帮助,除去 WM_COMMAND 和 WM_NOTIFY 之外,我们仍然需要从其他所有的消息中拆封数据。不过,WTL 正好在这儿对我们施以援手!
WTL 的增强消息映射宏在 atlcrack.h 文件中(此名字来源于 “message cracker”,是一个应用于 windowsx.h 中类似的宏的术语)。要使用这些宏的第一个步骤在 VC 6 和 VC 7 里是不一样的,在 atlcrack.h 中的以下提示解释了这一不同:
对于 ATL 3.0,使用了解拆处理器的消息映射必须使用 BEGIN_MSG_MAP_EX。
对于 ATL 7.0/7.1,你可以为 CWindowImpl/CDialogImpl 的派生类使用 BEGIN_MSG_MAP,但是对于不是派生于 CWindowImpl/CDialogImplbut 的类则必须使用 BEGIN_MSG_MAP_EX。
所以,如果你在使用 VC 6,你需要这样改动你的 MyWindow.h: // MyWindow.h, VC6 only:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
};
(_EX 宏对于 VC 6 来讲是必需的,因为包含于其中的某些代码是消息处理器宏需要使用的。出于可读性的原因,这里就不列出 VC 6 和 VC 7 版本的头文件了,因为它们仅仅是一个宏上面的不同。只需记住_EX 宏在 VC 7 里是不需要的即可。)
对我们的时钟程序来说,我们需要处理 WM_CREATE 并设置一个定时器。WTL 把针对一个消息的消息处理器命名为 MSG_ 后随消息名,比如 MSG_WM_CREATE。这些宏仅接受处理器的名字。我们来为 WM_CREATE 添加一个处理器: class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
// OnCreate(...) ?
};
WTL 的消息处理器看起来很像 MFC,每个处理器都根据随消息传入的参数有一个不同的原型。不过,由于没有向导来写处理器,我们不得不自己来查找原型。幸运的是 VC 可以帮上忙。将光标(注:此处原文错误,不应该是光标[cursor],而应该是插入符[caret])放在 “MSG_WM_CREATE” 文本上再按 F12 会转到宏的定义处。在 VC 6 里,VC 会先重新编译工程以构建浏览信息数据库。这一工作一旦完成,VC 就会在 MSG_WM_CREATE 的定义处打开 atlcrack.h: #define MSG_WM_CREATE(func) \
if (uMsg == WM_CREATE) \
{ \
SetMsgHandled(TRUE); \
lResult = (LRESULT)func((LPCREATESTRUCT)lParam); \
if(IsMsgHandled()) \
return TRUE; \
}
带下划线的是最重要的一行,那是对处理器的实际调用,它告诉我们处理器会返回一个 LRESULT 并接受一个 LPCREATESTRUCT 类型的参数。注意,没有像 ATL 的宏所使用的 bHandled 参数。SetMsgHandled() 函数替代了该参数,很快我们就要解释这件事情。
现在我们可以为窗口类添加一个 OnCreate() 处理器: class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
LRESULT OnCreate(LPCREATESTRUCT lpcs)
{
SetTimer ( 1, 1000 );
SetMsgHandled(false);
return 0;
}
};
CFrameWindowImpl 间接地从 CWindow 派生而来,因此它具有所有 CWindow 的函数,例如 SetTimer()。这使得调用窗口 API 看起来很像 MFC 代码,在 MFC 里你可以使用许多封装了 API 的 CWnd 方法。
我们调用 SetTimer() 来创建一个每秒(1000 毫秒)激发的定时器。因为我们还想让 CFrameWindowImpl 也能处理 WM_CREATE,所以调用了 SetMsgHandled(false) 从而消息可以通过 CHAIN_MSG_MAP 宏串联到基类。这一调用代替了 ATL 宏所使用的 bHandled 参数。(即使是 CFrameWindowImpl 不处理 WM_CREATE,在使用了基类的时候调用 SetMsgHandled(false) 也是一个好习惯,这样你可以不去记基类处理了哪些消息。与 ClassWizard 生成的代码类似,大部分的处理器在开始或者结束都有对基类处理器的调用。)
我们还需要一个 WM_DESTROY 处理器来停止定时器。执行以上相同的流程,可以找到 MSG_WM_DESTROY 宏,看起来就是这样: #define MSG_WM_DESTROY(func) \
if (uMsg == WM_DESTROY) \
{ \
SetMsgHandled(TRUE); \
func(); \
lResult = 0; \
if(IsMsgHandled()) \
return TRUE; \
}
因此我们的 OnDestroy() 处理器既没有参数也没有返回值。CFrameWindowImpl 的确也处理了 WM_DESTROY,所以这儿仍然需要调用 SetMsgHandled(false): class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
MSG_WM_DESTROY(OnDestroy)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
void OnDestroy()
{
KillTimer(1);
SetMsgHandled(false);
}
};
接着是每秒钟调用一次的 WM_TIMER 处理器。现在你应该已经对 F12 这一技巧很熟悉了,所以我们只呈现处理器本身: class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
MSG_WM_DESTROY(OnDestroy)
MSG_WM_TIMER(OnTimer)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
void OnTimer ( UINT uTimerID, TIMERPROC pTimerProc )
{
if ( 1 != uTimerID )
SetMsgHandled(false);
else
RedrawWindow();
}
};
这一处理器仅仅重绘窗口以使新的时间显示在客户区内。最后,我们来处理 WM_ERASEBKGND,在相应的处理器中,把当前时间绘制在客户区的左上角。 class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
MSG_WM_DESTROY(OnDestroy)
MSG_WM_TIMER(OnTimer)
MSG_WM_ERASEBKGND(OnEraseBkgnd)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
LRESULT OnEraseBkgnd ( HDC hdc )
{
CDCHandle dc(hdc);
CRect rc;
SYSTEMTIME st;
CString sTime;
// Get our window's client area.
GetClientRect ( rc );
// Build the string to show in the window.
GetLocalTime ( &st );
sTime.Format ( _T("The time is %d:%02d:%02d"),
st.wHour, st.wMinute, st.wSecond );
// Set up the DC and draw the text.
dc.SaveDC();
dc.SetBkColor ( RGB(255,153,0) );
dc.SetTextColor ( RGB(0,0,0) );
dc.ExtTextOut ( 0, 0, ETO_OPAQUE, rc, sTime,
sTime.GetLength(), NULL );
// Restore the DC.
dc.RestoreDC(-1);
return 1; // We erased the background (ExtTextOut did it)
}
};
此处理器演示了 GDI 的封装类之一,CDCHandle,以及 CRect 和 CString。关于 CString 我想说的是,它和 MFC 的 CString 其实是一样的。我将在稍后讲到这些封装类,不过现在你可以将 CDCHandle 仅仅视为对 HDC 的一个简单封装,就像 MFC 的 CDC 那样。只是当 CDCHandle 离开作用域时,它不会销毁内含的设备上下文。
最后,这就是我们的窗口:
![[clock window - 4K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzxJ5YGsyrC8C_zkOHFcYAxfqt9UpuosaXyhPjrJ3s_QJrsTiMaiNXMr9YFMT7piuEbcZWLzO5NI6I35V_nbpjH6sD6jdkmmZ0sHhO55Ua_Tg)
示例代码中还有为菜单项加入的 WM_COMMAND 处理器,在这儿我不会讲它们,但是你可以打开示例工程,看一下 WTL 的 COMMAND_ID_HANDLER_EX 宏是怎么运作的。
如果你在使用 VC 7.1,可以去找 Sergey Solozhentsev 的 WTL Helper,它会为你处理添加消息映射宏的这种麻烦事。
使用 WTL AppWizard 可以得到什么
WTL 分发包带了一个相当棒的 AppWizard。我们来看一下它可以向 SDI 应用中添加哪些特性。
通历向导(VC 6)
点击 VC 的 File|New 并在列表里选择 ATL/WTL AppWizard。我们来重写时钟程序,输入 WTLClock 作为工程名字:
![[AppWiz screen 1 - 14K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJwvnTKUo14nuS6T-Xw4AjJ6LHilEeSlIAKLu2A13mzpMY37QwOU9alT3e-9Y0zHcCr6Bie4jqFkmDtiA5ca8ylcKFAguqzMccBIbt-x93Neig)
在接下来的页面里,可以选择是 SDI、MDI 还是基于对话框的应用,以及一些其他的选项。选择下面显示的选项并点击 Next:
![[AppWiz screen 2 - 22K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJyAC9yPpEhsJ0Whj8XDX1425YMOaoSFQq5Ce_4IANY4pKn3hR5ROBasBhqAjtSfhlRRF41rfoE6zBGxQSMbC2mQCTmZ325ck8_QltmhEG75sA)
最后一页里我们可以选择拥有工具栏,复用栏以及状态栏。为了保证应用的简单,去掉所有这些选择并点击 Finish。
![[AppWiz screen 3 - 21K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzmvj2d073nW6fept8MqNt8dCzsSmiMrixGT0lFvm2IbyRbiAi4STUpJ_r8sV8HlQyGRQqd68EKBi15KtZcjhenw5aHjO73U10lvIBGXLPjig)
通历向导(VC 7)
点击 VC 的 File|New 并在列表里选择 ATL/WTL AppWizard。我们来重写时钟程序,输入 WTLClock 作为工程名字:
![[AppWiz screen 1 - 14K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU6qTyWpHB024Z4nhZ8Rw-KQw6bwWVwz94DkTjuqwseNLIJWg36nLXdcI2JxmlmdOfahDUPy0ntNyIEfO3_dTQ9OoSxMh5Ul35hTELH_Jy_WqeIl3E8OG_VU)
当 AppWizard 界面出来后,点击 Application Type。在本页里,你可以选择是 SDI、MDI 还是基于对话框的应用,以及一些其他的选项。选择下面显示的选项并点击 User Interface Features:
![[AppWiz screen 2 - 22K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU2HIVZOv6-17kYqqH6PiXTmFX9USoSmtgT2yEVYYBwdqgyjGna-dh1ze3D4Nr_NQWrkJ5PIMOWhqwkI2XbxgssaV7Lc5lNzlpRiEYSiOSpgFWcSBtdX669Q)
最后一页里我们可以选择拥有工具栏,复用栏以及状态栏。为了保证应用的简单,去掉所有这些选择并点击 Finish。
![[AppWiz screen 3 - 21K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU_ST_hO8lTEq4j3LpPqj7ls5q-5pJpkHeYm_tRPRJbw8GEYyNO9z0pyFxWlJcOEu_TQ0YUCQGJlEfkD8cF8OX9ppJmhAurEXXHY2N-H1cAs55VnNXYPdtPI)
检查生成的代码
向导结束后,在生成的代码里你会看到三个类:CMainFrame、CAboutDlg 和 CWTLClockView。从名字里你就可以猜出每个类的作用。尽管有一个“view”类,不过它却是从 CWindowImpl 派生而来的一个“普通”窗口,而没有像 MFC 的文档/视图架构中的框架窗口。
还有一个函数是 _tWinMain(),它初始化 COM、公用控件以及 _Module,之后再调用一个全局的 Run() 函数。Run() 会创建主窗口并开始消息泵,它还使用了一个新类 CMessageLoop。Run() 调用 CMessageLoop::Run(),确切地说是后者包含了消息泵。在下一节里我们将了解 CMessageLoop 的更多细节。
CAboutDlg 是一个简单的 CDialogImpl 派生类,它关联到一个 ID 为 IDD_ABOUTBOX 的对话框上。我在第一部分里谈到了对话框,所以你应该能够理解 CAboutDlg 的代码。
CWTLClockView 是我们这一应用的“view”类。它工作起来像是一个 MFC 视图,没有标题栏,占据着主框架的客户区。CWTLClockView 有一个 PreTranslateMessage() 函数,它工作起来也像是 MFC 中的同名函数。再有就是 WM_PAINT 处理器。目前还没有哪个函数在做举足轻重的事情,但我们即将填写 OnPaint() 方法来显示时间。
最后,我们还有 CMainFrame,它有许多有趣的新东西。下面是类定义的一个简化版本: class CMainFrame : public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter,
public CIdleHandler
{
public:
DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)
BEGIN_UPDATE_UI_MAP(CMainFrame)
END_UPDATE_UI_MAP()
BEGIN_MSG_MAP(CMainFrame)
// ...
CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)
CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)
END_MSG_MAP()
BOOL PreTranslateMessage(MSG* pMsg);
BOOL OnIdle();
protected:
CWTLClockView m_view;
};
CMessageFilter 是一个提供了 PreTranslateMessage() 的嵌入类,CIdleHandler 是另一个嵌入类,它提供了 OnIdle()。CMessageLoop、CIdleHandler 以及 CUpdateUI 一起工作以提供像 MFC 的 ON_UPDATE_COMMAND_UI 那样的 UI 更新功能。
CMainFrame::OnCreate() 创建视图窗口并保存了其窗口句柄,所以当主窗口的大小变化时视图窗口的大小也随之变化。OnCreate() 还把 CMainFrame 对象添加到由 CAppModule 维护的消息过滤器列表和空闲处理列表中。稍后会介绍这些内容。
LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,
LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, |
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |
WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
// register object for message filtering and idle updates
CMessageLoop* pLoop = _Module.GetMessageLoop();
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
return 0;
}
m_hWndClient 是 CFrameWindowImpl 的一个成员,也就是在框架窗口大小改变时要相应改变大小的窗口。
生成的 CMainFrame 还有对 File|New、File|Exit 以及 Help|About 的处理器。对于我们的时钟程序来说,大多数缺省菜单项都不需要,不过留着也没有什么害处。现在可以编译并运行向导生成的代码了,尽管此应用还不是很有用。你可能会对逐步执行全局 Run() 里的 CMainFrame::CreateEx() 函数感兴趣,可以精确地看到框架窗口及其资源是如何被加载和创建的。
我们的 WTL 游览的下一站是 CMessageLoop,它负责消息泵和空闲处理。
CMessageLoop 内幕
CMessageLoop 为我们的应用程序提供了消息泵。除标准的 DispatchMessage/TranslateMessage 循环之外,它还通过 PreTranslateMessage() 提供了消息过滤功能,通过 OnIdle() 提供了空闲处理功能。以下是 Run() 逻辑伪代码:
int Run()
{
MSG msg;
for(;;)
{
while ( !PeekMessage(&msg) )
CallIdleHandlers();
if ( 0 == GetMessage(&msg) )
break; // WM_QUIT retrieved from the queue
if ( !CallTranslateMessageFilters(&msg) )
{
// if we get here, message was not filtered out
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
CMessageLoop 知道要调用哪一个 PreTranslateMessage() 函数是因为每个需要过滤消息的类都要像 CMainFrame::OnCreate() 所做的那样调用 CMessageLoop::AddMessageFilter()。与之相仿,需要进行空闲处理的类要调用 CMessageLoop::AddIdleHandler()。
注意,在消息循环中没有对 TranslateAccelerator() 或者 IsDialogMessage() 进行调用。CFrameWindowImpl 处理了前者,不过,如果你要在应用里添加任何非模态对话框,你就需要在 CMainFrame::PreTranslateMessage() 中增加对 IsDialogMessage() 的调用。
CFrameWindowImpl 内幕
CFrameWindowImpl 及其基类 CFrameWindowImplBase 提供了许多 MFC 的 CFrameWnd 具有的特性:工具栏、复用栏(Rebar)、状态栏、用于工具栏按钮的工具提示(Tooltip)以及针对菜单项的动态帮助。我会逐渐将到这些特性,因为完整地讨论 CFrameWindowImpl 类需要占用整整两篇文章!至于眼下,看看 CFrameWindowImpl 是如何处理 WM_SIZE 和客户区就足够了。在此,请记住 m_hWndClient 是 CFrameWindowImplBase 的一个成员,用来存放位于框架中的“view”的 HWND。
CFrameWindowImpl 有 WM_SIZE 的一个处理器:
LRESULT OnSize(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)
{
if(wParam != SIZE_MINIMIZED)
{
T* pT = static_cast<T*>(this);
pT->UpdateLayout();
}
bHandled = FALSE;
return 1;
}
此函数检查了窗口是不是要被最小化。如果不是,它就派发到 UpdateLayout()。下面是 UpdateLayout(): void UpdateLayout(BOOL bResizeBars = TRUE)
{
RECT rect;
GetClientRect(&rect);
// position bars and offset their dimensions
UpdateBarsPosition(rect, bResizeBars);
// resize client window
if(m_hWndClient != NULL)
::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
SWP_NOZORDER | SWP_NOACTIVATE);
}
注意代码是如何引用 m_hWndClient 的。由于 m_hWndClient 是一个普通的 HWND,实际上它可以是任何窗口。此处没有窗口种类的限制,不像 MFC 的某些特性(例如分割窗口)需要 CView 的派生类。如果你回到 CMainFrame::OnCreate(),可以看到它创建了一个视图窗口并将其句柄保存到 m_hWndClient 中,以确保视图可以被正确地改变大小。
回到时钟程序
现在,在看完了框架窗口类的一些细节之后,让我们回到时钟程序上来。就像前例中的 CMyWindow 一样,视图窗口可以处理定时器和绘制。以下是类的部分定义: class CWTLClockView : public CWindowImpl<CWTLClockView>
{
public:
DECLARE_WND_CLASS(NULL)
BOOL PreTranslateMessage(MSG* pMsg);
BEGIN_MSG_MAP_EX(CWTLClockView)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MSG_WM_CREATE(OnCreate)
MSG_WM_DESTROY(OnDestroy)
MSG_WM_TIMER(OnTimer)
MSG_WM_ERASEBKGND(OnEraseBkgnd)
END_MSG_MAP()
};
注意,只要把 BEGIN_MSG_MAP 改成了 BEGIN_MSG_MAP_EX,那你就可以将 ATL 的消息映射宏和 WTL 版本的混合起来使用。OnPaint() 里使用了前例中在 OnEraseBkgnd() 里的所有绘制代码。下面是新窗口的样子:
![[Clock app w/view window - 3K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxJkJB4MVCyiPR4GH9CQmKOCdUIWds_pNsdJ0T0CFO1oH-iIZb5yGB_rCdDDcTGF-CCoOvoUxiKEcSoUOCO7o0uwCECZriCmxtxNP0Z2MtW-g)
我们要加到应用中的最后一样东西是 UI 更新。出于演示目的,我们要添加一个 Clock 顶级菜单项,并具有 Start 和 Stop 两个命令以开始或者停止时钟。Start 和 Stop 菜单项将被适时地启用或者禁用。
UI 更新
空闲时的 UI 更新是由好几件东西一起工作来提供的:一个 CMessageLoop 对象,CMainFrame 从之继承的嵌入类 CIdleHandler 和 CUpdateUI,以及 CMainFrame 里的 UPDATE_UI_MAP。CUpdateUI 能够操纵五种不同类型的元素:位于菜单栏中的顶级菜单项、弹出菜单中的菜单项、工具栏按钮、状态栏窗格,还有子窗口(比如对话框控件)。每种类型的元素在 CUpdateUIBase 中都有一个对应的常量:
- 菜单栏项:
UPDUI_MENUBAR
- 弹出菜单项:
UPDUI_MENUPOPUP
- 工具栏按钮:
UPDUI_TOOLBAR
- 状态栏窗格:
UPDUI_STATUSBAR
- 子窗口:
UPDUI_CHILDWINDOW
CUpdateUI 可以设置启用状态、勾选状态,还有项目的文本(不过并非所有的项都支持所有的状态,显然你不能勾选一个编辑框子窗口)。它还可以把一个菜单项设置为缺省项而使之文本以粗体显示。
要挂接 UI 更新,我们需要作四件事情:
- 将框架窗口从
CUpdateUI 和 CIdleHandler 继承
- 从
CMainFrame 向 CUpdateUI 串联消息
- 把框架窗口添加到模块的空闲处理列表中
- 填充框架窗口的
UPDATE_UI_MAP
AppWizard 生成的代码已经为我们照顾到了前三项,剩下的事情就是决定哪个菜单项要更新,以及要在什么时候启用或者禁用。
控制时钟的新菜单项
我们来在菜单栏上添加一个新的 Clock 菜单,包括两项:IDC_START 和 IDC_STOP:
![[Clock menu - 2K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJznf0TIbl89mwTPf6wiM-10i8LBttJPVRj9EWMsqZSsFDoaPDFIeuAwAHKlSfNCVys2dFZLTmuqvPr3RSxMBXkLmPHtRXOJriW0R3-CrBIN_w)
然后我们为每一项在 UPDATE_UI_MAP 中添加一个入口: class CMainFrame : public ...
{
public:
// ...
BEGIN_UPDATE_UI_MAP(CMainFrame)
UPDATE_ELEMENT(IDC_START, UPDUI_MENUPOPUP)
UPDATE_ELEMENT(IDC_STOP, UPDUI_MENUPOPUP)
END_UPDATE_UI_MAP()
// ...
};
之后无论何时我们要改变任一项的启用状态,我们就调用 CUpdateUI::UIEnable()。UIEnable() 接受项的 ID,还有一个指示启用状态的 bool 值,true 为启用,false 为禁用。
这套系统工作起来不同于 MFC 的 ON_UPDATE_COMMAND_UI 系统。在 MFC 里,我们为需要更新其状态的 UI 元素写 UI 更新的处理器,MFC 在空闲的时候,或者即将显示菜单的时候对处理器进行调用。在 WTL 里,我们在项要改变的时候调用 CUpdateUI 的方法,CUpdateUI 跟踪 UI 元素及其状态,并且在空闲的时候,或者即将显示菜单的时候对元素进行更新。
调用 UIEnable()
让我们回到 OnCreate() 函数,看一下如何设置 Clock 菜单项的初始状态。 LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,
LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
m_hWndClient = m_view.Create(...);
// register object for message filtering and idle updates
// [omitted for clarity]
// Set the initial state of the Clock menu items:
UIEnable ( IDC_START, false );
UIEnable ( IDC_STOP, true );
return 0;
}
下面是 Clock 菜单在应用刚开始时后的样子:
![[Start item disabled - 4K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJzgXPQa78iXmsJPNpoCE_E2yFSkvNFMYqTY1AverOrJeCeR7C-6g5FvCEzQBy0Sf7M1mvnGWNoidJ5i55KtT0320OQhyqCKlgd_uwEEyMV6hA)
CMainFrame 现在需要这两个新项的处理器。处理器会倒换菜单项的状态,然后再调用视图类的方法开始或者停止时钟。此处是 MFC 的内建消息路由严重遗漏的领域,如果这是一个 MFC 应用,所有的 UI 更新和命令处理可能会被完全放到视图类中。但是在 WTL 里,框架和视图类必须用某种方法相互通讯,菜单为框架所有,因此框架会收到菜单相关的消息并且有责任响应它们,要么自己处理,要么发给视图类。
通讯可以通过 PreTranslateMessage() 完成,不过 UIEnable() 的调用还必须由 CMainFrame 完成。CMainFrame 可以通过将自己的 this 指针传递给视图类而逃避责任,于是视图类可以使用该指针来调用 UIEnable()。在本例中,我选择的方案会导致框架与视图类的紧密耦合,但是我发现它既易于理解又易于解释! class CMainFrame : public ...
{
public:
BEGIN_MSG_MAP_EX(CMainFrame)
// ...
COMMAND_ID_HANDLER_EX(IDC_START, OnStart)
COMMAND_ID_HANDLER_EX(IDC_STOP, OnStop)
END_MSG_MAP()
// ...
void OnStart(UINT uCode, int nID, HWND hwndCtrl);
void OnStop(UINT uCode, int nID, HWND hwndCtrl);
};
void CMainFrame::OnStart(UINT uCode, int nID, HWND hwndCtrl)
{
// Enable Stop and disable Start
UIEnable ( IDC_START, false );
UIEnable ( IDC_STOP, true );
// Tell the view to start its clock.
m_view.StartClock();
}
void CMainFrame::OnStop(UINT uCode, int nID, HWND hwndCtrl)
{
// Enable Start and disable Stop
UIEnable ( IDC_START, true );
UIEnable ( IDC_STOP, false );
// Tell the view to stop its clock.
m_view.StopClock();
}
每个处理器都会先更新 Clock 菜单,然后调用视图的方法,因为视图是控制时钟的类。StartClock() 和 StopClock() 方法没有显示在这里,但可以在示例工程中找到。
关于消息映射的最后注意事项
如果你在使用 VC 6,你可能会注意到:当你把 BEGIN_MSG_MAP 改为 BEGIN_MSG_MAP_EX 后,ClassView 会变得一团糟:
![[Messed-up ClassView - 6K]](http://storage.msn.com/x1pFc-mCH7GfbNMnp589Sm-UWx8N3MSPZCsFd0o5Z46ZJxc_uQvrlErUTzS3TCdtiUEOqQbS90Y8C3DiWMmLv29ZS6a_i0epeUAQLd0OoOSe9JjGDP3stE9MkMu0huo9yMdUOc_D7k2kKzYcYkTACMkHQ)
这是因为 ClassView 不能像理解别的一些它能够特殊分析的东西一样理解 BEGIN_MSG_MAP_EX,因而它把所有的 WTL 消息映射宏当成了实际的函数。通过把宏改回到 BEGIN_MSG_MAP 可以改正这一问题,只要把以下几行加到 stdafx.h 的末尾即可: #if (ATL_VER < 0x0700)
#undef BEGIN_MSG_MAP
#define BEGIN_MSG_MAP(x) BEGIN_MSG_MAP_EX(x)
#endif
下一站,1995
我们仅仅触及到 WTL 的皮毛。在下一篇文章中,我将给我们的示例时钟程序带来 1995 UI 标准并介绍工具栏和状态栏。同时,对 CUpdateUI 的方法做一些实验,比如尝试调用 UISetCheck() 而不是 UIEnable() 来看看改变菜单项的不同方法。
修订历史
2003 年 3 月 26 日:首次发布 2005 年 12 月 15 日:更新,包括了 VC 7.1 里 ATL 的改动 | 链接: 上一部分; 下一部分
特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
链接:下一部分
第一部分 - ATL 中的 GUI 类
|
本章内容
- README.TXT
- 本系列介绍
- 第一部分介绍
- ATL 背景知识
- ATL 窗口类
- 定义窗口实现
- 高级消息映射链和嵌入(Mix-in)类
- ATL EXE 的结构
- ATL 中的对话框
- 就要到 WTL 了,我保证!
- 修订历史
README.TXT
在继续或者在本文的讨论板块中发布帖子之前,我希望你能先阅读以下内容。
本系列原来是为 VC 6 用户写的,介绍 WTL 7.0 的内容。现在 VC 8 已经出来了,我觉得也到了更新本系列来介绍 VC 7.1 的时候了。;)(不过,VC 7.1 的从 6 到 7 的自动转换工作并不是总能平滑地完成,所以 VC 7.1 的用户在试着使用示例源代码的时候可能会遭遇失败)因而,我将继续下去,持续更新本系列。文章将更新到可以反映 WTL 7.1 的特性,并会在下载的源代码中包括 VC 7.1 的工程。
针对 VC 2005 用户的重要提示:VC 2005 的 Express 版本并不附带 ATL 或者 MFC,因此不能使用此版本编译 ATL 或者 WTL 工程。
如果你在使用 VC 6,那你就需要有 Platform SDK。没有它你将不能使用 WTL。你可以使用Web 安装版本或者下载 CAB 文件或者是 ISO 映像,然后在本地运行安装程序。请使用工具把 SDK 的 include 以及 lib 目录加入到 VC 的搜索路径中,该工具可以在 Platform SDK 程序组中的 Visual Studio Registration 文件夹下找到。即使你在用 VC 7,使用最新的 Platform SDK 仍然是一个好主意,因为你可以得到最新的头文件和库。
你需要有 WTL。可以从微软下载版本 7。在文章 "Introduction to WTL - Part 1" 以及 "Easy installation of WTL" 中有一些关于安装的提示。这些文章已经很老了,不过还是有一些不错的信息。WTL 分发包里也有一个 readme 文件,里面有安装指令。我认为在这些文章中没有提到的一件事情是如何把 WTL 的文件加入到 VC 的包含路径里。在 VC 6 里,点击 Tools|Options 并切换到 Directories 标签页,在 Show directories for 组合框中,选中 Include files,然后添加一个新项,使其指向你放置 WTL 头文件的目录。在 VC 7 里,点击 Tools|Options,再点击 Projects,然后是 VC++ Directories,在 Show directories for 组合框中,选中 Include files,然后添加一个新项,使其指向你放置 WTL 头文件的目录。
重要:我们正在提及 VC 7 的包含路径这一话题,如果你还没有更新 Platform SDK,你必须对缺省的目录列表做一个改动。请确保 $(VCInstallDir)PlatformSDK\include 在列表的第一位,优先于 $(VCInstallDir)include,如下图所示:
![[VC7 include path list - 26K]](http://tk.files.storage.msn.com/x1pC47KWjv0VYmhru0KKrOfUz33pz4_0gzSOOhw-rgyZ6mnQ6gi2a84XbbzgmIGpFJdmJ7h6pvP-syztPsROPc7KvCbxshpShHzOidoGQVGAskFQmQVGvVq6Nr91LQD2IJw4x_hrjODFJo)
你应该了解 MFC,并且要了解到你知道消息映射宏的实质是什么,而且能够编辑那些被标记为“DO NOT EDIT”的代码而不出问题。
你需要了解 Win32 API 编程,而且是很好地了解。如果你是直接通过 MFC 学习 Windows 编程而没有学习在 API 级消息是如何工作的,那很不幸,你会在使用 WTL 时遇到麻烦。如果你不知道一个消息的 WPARAM 和 LPARAM 是什么意思,你应该阅读其他的 API 级编程的文章(CodeProject 上就有很多)以使你能够了解。
你需要了解 C++ 模板的语法,在 VC Forum FAQ 上有 C++ FAQ 和模板 FAQ 的链接。
因为我还没有使用 VC 8,所以我不知道示例代码在 8 上是不是可以编译,希望 7 到 8 的升级过程能比 6 到 7 的强。如果在 VC 8 上有任何问题,请张贴到本文的论坛里。
本系列介绍
WTL 确实震动了所有人。它具有许多 MFC GUI 类的强大功能,但是可以生成相当小的可执行代码。如果你和我一样,用 MFC 学习 GUI 编程,对 MFC 所提供的控件封装感到相当舒服,并且对 MFC 内建的灵活的消息处理也有同感;如果你和我一样,不喜欢好几百 K 的 MFC 框架附着到自己的程序上,WTL 正适合你。 不过,还是有一些我们必须跨越的障碍:
- ATL 风格的模板乍看起来很怪异。
- 没有 ClassWizard 支持,所以写消息映射成了手工劳动。
- 在 MSDN 里没有文档,需要到其他地方去找,甚至需要去看 WTL 源程序。
- 没有能买到并放到书架上的参考书。
- 它具有“不被微软官方支持”的污名
- ATL/WTL 窗口非常不同于 MFC 窗口,并非你所有的知识都能够对应过来
另一方面,WTL 的好处有:
- 不需学习或者使用复杂的文档/视图框架
- 具有源于 MFC 的一些基本 UI 特性,例如 DDX/DDV 和“更新命令 UI”功能
- 增强了的一些 MFC 特性(例如,更灵活的分割条窗口)
- 与静态链接 MFC 的应用相比,可执行代码非常小
- 你自己可以改正 WTL 的错误而不影响现存的应用(相比之下,一个应用替换掉 MFC/CRT 的 DLL 来改正错误将引起其他应用崩溃)
- 如果仍然需要 MFC,MFC 和 ATL/WTL 窗口可以和平共处(在我工作的一个原型中,我创建了一个包含有 WTL
CSplitterWindow 的 MFC CFrameWnd ,而前者中又包含有 MFC CDialog。-- 并不是我卖弄,只不过是修改了 MFC 代码而使用了更好的 WTL 分割条)
在本系列中,我将先介绍 ATL 窗口类。毕竟 WTL 是一组 ATL 的附加类,所以对 ATL 窗口有很好的理解相当重要。介绍完 ATL 之后我将介绍 WTL 的特性并展示它如何使界面编程变得轻而易举。
第一部分介绍
WTL 令人震惊。不过在知道为什么之前,我们首先需要了解 ATL。WTL 是一组 ATL 的附加类,如果过去你是一名仅使用 MFC 的程序员,你可能从来没有遇到过 ATL 的 GUI 类。所以请原谅我没有立即涉及 WTL,到 ATL 那儿绕些弯路是有必要的。
在第一部分里,我将给出一些 ATL 的背景知识,包括在写 ATL 代码之前需要知道的一些要点,迅速的解释那些令人胆寒的 ATL 模板,并涵盖了基本的 ATL 窗口类。
ATL 背景知识
ATL 和 WTL 的历史
活动(Active)模板库是一个古怪的名字,不是吗?年长点的可能会记得它原来的名字是 ActiveX 模板库,这是一个更准确的名字,因为 ATL 的目标就是要让 COM 对象和 ActiveX 控件写起来更轻松。(ATL 是在微软将新产品命名为“ActiveX-什么什么”的时候开发的,就像现在微软的新产品被称作“什么什么 .NET”一样)因为 ATL 只是用来写 COM 对象的,所以它只有 GUI 类中最基本的部分,即 MFC 中 CWnd 和 CDialog 的等价物。幸运的是,这些 GUI 类很灵活,可以让像 WTL 这样的东西构筑于其上。
作为微软所有的一个项目,WTL 有两个大的修订版,3 和 7。(选定的版本号是为了与 ATL 的版本号匹配,所以不是 1 和 2。)版本 3.1 已经相当古老了,本系列中将不再涉及。版本 7 是版本 3 的一个重要升级,而版本 7.1 仅仅加入了一些纠错和少许的特性。
在版本 7.1 之后,微软将 WTL 作为了一个开源工程,托管于 Sourceforge 上。此站点上最新的版本是 7.5(译者注:目前已经是 8.0 了),我还没有看 7.5,所以现在本系列不会涵盖 7.5 的内容(我总是会落后两个版本,而且会周期性地赶上来!)
ATL 风格的模板
即使你可以毫不头痛的阅读 C++ 模板,但一开始 ATL 还是会有两件事可能成为拦路虎。比如说: class CMyWnd : public CWindowImpl<CMyWnd>
{
...
};
这样做是合法的,因为 C++ 规范中声称紧随 class CMyWnd 部分之后,名字 CMyWnd 即被定义;并且可以被用在继承列表中。之所以把类名作为模板参数是因为要让 ATL 能做第二件技巧性的工作 - 编译期虚函数调用。
如果要看一下实际运作,可以看一下这几个类: template <class T>
class B1
{
public:
void SayHi()
{
T* pT = static_cast<T*>(this); // HUH?? I'll explain this below
pT->PrintClassName();
}
protected:
void PrintClassName() { cout << "This is B1"; }
};
class D1 : public B1<D1>
{
// No overridden functions at all
};
class D2 : public B1<D2>
{
protected:
void PrintClassName() { cout << "This is D2"; }
};
main()
{
D1 d1;
D2 d2;
d1.SayHi(); // prints "This is B1"
d2.SayHi(); // prints "This is D2"
}
这儿的 static_cast<T*>(this) 是一个技巧。它把 B1* 类型的 this ,通过被调用的特化转型为 D1* 或者 D2* 。因为模板代码在编译时生成,所以保证了此转型是安全的,只要正确的书写了继承列表。 (如果你写成 class D3 : public B1<D2> 你就会遇到麻烦。)转型是安全的是因为 this 对象只能是类型 D1* 或者 D2* (相应的),而不是其他。注意,这和正常的 C++ 多态几乎一样,只不过 SayHi() 方法不是虚拟的。
要解释这是如何工作的,我们来看一下 SayHi() 的每个调用。在第一个调用中,特化 B1<D1> 被采用,所以 SayHi() 代码展开为: void B1<D1>::SayHi()
{
D1* pT = static_cast<D1*>(this);
pT->PrintClassName();
}
由于 D1 没有覆盖 PrintClassName(),所以会搜索 D1 的基类。B1 有 PrintClassName() 方法,所以就是被调到的那个。
现在,看 SayHi() 的第二次调用。这一次使用了特化 B1<D2>,于是 SayHi() 展开为: void B1<D2>::SayHi()
{
D2* pT = static_cast<D2*>(this);
pT->PrintClassName();
}
这次 D2 确实包含了一个 PrintClassName() 方法,所以它是被调用到的那个。
这种技术的好处是:
- 不需要使用指向对象的指针
- 由于不需要 vtbl 而节省内存
- 不会因为未初始化的 vtbl 而在运行时通过空指针调用虚函数
- 所有函数调用在编译时被解析,所以可以被优化
虽然在这个例子里 vtbl 的节约看起来并不明显(只不过 4 个字节),不过可以考虑在有 15 个基类,有的类有 20 个方法的情况下,累计的节省有多少。
ATL 窗口类
很好,背景知识足够了!是钻研 ATL 的时候了。ATL 采用严格的接口/实现相分离来设计,这在窗口类中也很明显。这和 COM 类似,接口的定义与实现完全分离(或者可能有多个实现)。
ATL 有一个类定义了窗口的“接口”,也就是对一个窗口可以做什么。这个类就是 CWindow。它只是对 HWND 的一个封装,提供了几乎所有的以 HWND 作为第一个参数的 User32 API,如 SetWindowText() 和 DestroyWindow()。 CWindow 有一个当你需要原始 HWND 时可以访问的公用成员 m_hWnd。CWindow 也有一个 operator HWND 方法,于是你可以传递 CWindow 对象到接受 HWND 的函数中。没有 CWnd::GetSafeHwnd()的等价物。
CWindow 和 MFC 的 CWnd 很不同。创建 CWindow 对象不需多少代价,因为它只有一个数据成员,而且也没有 MFC 内部用以保存 HWND 到 CWnd 对象的对应关系的对象映射表。还与 CWnd不一样的是,当一个 CWindow 对象离开作用域,关联的窗口不会被销毁。这意味着你不须总是记住要把你创建的临时 CWindow 对象与关联的窗口脱离。
ATL 中窗口的实现类是 CWindowImpl。CWindowImpl 包含了做这些事情的代码,比如窗口类注册,窗口子类化,消息映射,以及一个基本的 WindowProc()。再次不同于 MFC,在MFC 中,所有这些在同一个类里:CWnd。
还有两个独立的类,包含了对对话框的实现,CDialogImpl 和 CAxDialogImpl。CDialogImpl 用于普通的对话框,而 CAxDialogImpl 用于要掌控 ActiveX 控件的对话框。
定义窗口实现
任何要创建的非对话框窗口应该从 CWindowImpl 派生。新类里需要包含三样:
- 窗口类定义
- 消息映射
- 窗口使用的默认风格,称作窗口修饰
窗口类的定义使用 DECLARE_WND_CLASS 或者 DECLARE_WND_CLASS_EX 宏来完成。它们都定义了一个封装了 WNDCLASSEX 结构的 ATL 结构 CWndClassInfo。 DECLARE_WND_CLASS 允许指定新窗口类的名字,其余成员使用缺省值;而 DECLARE_WND_CLASS_EX 还允许指定类风格和窗口背景色。类名也可以用 NULL,ATL 将生成一个。
我们从一个新的类的定义开始,随后的章节里我会逐步向其中增加内容。 class CMyWindow : public CWindowImpl<CMyWindow>
{
public:
DECLARE_WND_CLASS(_T("My Window Class"))
};
接下来是消息映射。ATL 的消息映射比 MFC 的映射简单的多。一个 ATL 映射被展开为一个大的 switch 语句。switch 查找合适的处理器并调用相应的函数。消息映射的宏是 BEGIN_MSG_MAP 和 END_MSG_MAP 。向窗口中添加一个空的映射。 class CMyWindow : public CWindowImpl<CMyWindow>
{
public:
DECLARE_WND_CLASS(_T("My Window Class"))
BEGIN_MSG_MAP(CMyWindow)
END_MSG_MAP()
};
我会在下一节中讲解如何向映射中添加处理器。最后,我们要为我们的类定义窗口修饰。窗口修饰是窗口风格和窗口扩展风格的组合,这些风格在创建窗口时会被用到。这些风格作为模板参数被指定,所以调用者在创建窗口时可以不被如何得到正确的风格而烦恼。这是一个示例的修饰定义,使用了 ATL 类 CWinTraits: typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
WS_EX_APPWINDOW> CMyWindowTraits;
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CMyWindowTraits>
{
public:
DECLARE_WND_CLASS(_T("My Window Class"))
BEGIN_MSG_MAP(CMyWindow)
END_MSG_MAP()
};
调用者可以在 CMyWindowTraits 定义中覆盖这些风格,不过通常没有必要。ATL 还有一些专用的预定义 CWinTraits ,其中适用于像我们这样的顶级窗口的一个是 CFrameWinTraits: typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN |
WS_CLIPSIBLINGS,
WS_EX_APPWINDOW | WS_EX_WINDOWEDGE>
CFrameWinTraits;
填充消息映射
ATL 的消息映射对开发人员来讲是一个缺乏友好性的地方,同时也是 WTL 极大的增强了的地方。ClassView 提供了添加消息处理器的功能,但 ATL 没有类似于 MFC 的特定消息相关的宏和自动化参数解析。在 ATL 里,只有三种类型的消息处理器,一个针对 WM_NOTIFY,一个针对 WM_COMMAND,一个针对其他所有的消息。我们从向窗口添加 WM_CLOSE 和 WM_DESTROY 的消息处理器开始。 class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>
{
public:
DECLARE_WND_CLASS(_T("My Window Class"))
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_CLOSE, OnClose)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
DestroyWindow();
return 0;
}
LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
PostQuitMessage(0);
return 0;
}
};
你会注意到处理器接收原始的 WPARAM 和 LPARAM 值,当消息使用这些参数时你需要自己去解析它们。还有第四个参数,bHandled。这个参数在处理期调用之前被 ATL 设为 TRUE。如果你希望 ATL 的缺省 WindowProc() 在你的处理器返回之后也能处理消息,你可以将 bHandled 设为 FALSE。这和 MFC 不一样,MFC 中必须显式调用消息处理器的基类实现。
我们再添加 WM_COMMAND 的处理器。假定我们窗口的菜单有一个 ID 为 IDC_ABOUT 的 About 项: class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>
{
public:
DECLARE_WND_CLASS(_T("My Window Class"))
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_CLOSE, OnClose)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)
END_MSG_MAP()
LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
DestroyWindow();
return 0;
}
LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
PostQuitMessage(0);
return 0;
}
LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
MessageBox ( _T("Sample ATL window"), _T("About MyWindow") );
return 0;
}
};
注意,COMMAND_HANDLER 宏为你做了解析消息参数的工作。NOTIFY_HANDLER 宏类似地解析 WM_NOTIFY 的消息参数。
高级消息映射和嵌入(Mix-in)类
ATL 中一个最大的不同是任何 C++ 类都可以处理消息,不像 MFC 里消息处理的任务在 CWnd 和 CCmdTarget 间分割开来,再加上几个有 PreTranslateMessage() 方法的类。这一能力允许我们写通常被称作嵌入类的东西,以便通过向继承列表中添加类就可以向我们的窗口中增加特性。
带有消息映射的基类通常是一个以派生类名作为模板参数的模板,这样就能访问派生类中像 m_hWnd(CWindow 中的 HWND 成员)这样的成员。我们来看一下这个嵌入类,它通过处理 WM_ERASEBKGND来绘制窗口背景。 template <class T, COLORREF t_crBrushColor>
class CPaintBkgnd : public CMessageMap
{
public:
CPaintBkgnd() { m_hbrBkgnd = CreateSolidBrush(t_crBrushColor); }
~CPaintBkgnd() { DeleteObject ( m_hbrBkgnd ); }
BEGIN_MSG_MAP(CPaintBkgnd)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
END_MSG_MAP()
LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
T* pT = static_cast<T*>(this);
HDC dc = (HDC) wParam;
RECT rcClient;
pT->GetClientRect ( &rcClient );
FillRect ( dc, &rcClient, m_hbrBkgnd );
return 1; // we painted the background
}
protected:
HBRUSH m_hbrBkgnd;
};
我们来审视一下这个新类。首先,CPaintBkgnd 有两个模板参数:派生类的名字:CPaintBkgnd,和背景的颜色(t_ 前缀通常用于值类型的模板参数)
构造函数和析构函数相当简单,它们创建/销毁了一个 t_crBrushColor 颜色的画刷。接下来的消息映射处理了 WM_ERASEBKGND。最后,OnEraseBkgnd() 处理器使用构造函数中创建的画刷来填充窗口。OnEraseBkgnd() 中有两件事值得注意。首先,它使用了派生类的窗口函数(名为 GetClientRect())。我们怎么知道派生类里恰好有一个 GetClientRect() 函数?但如果没有的话,代码根本不能编译!编译器确保派生类 T 派生于 CWindow。其次,OnEraseBkgnd() 从 wParam 处得到设备上下文。
在我们的窗口中使用这一嵌入类,要做两件事。首先,我们把它加到继承列表中: class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>,
public CPaintBkgnd<CMyWindow, RGB(0,0,255)>
然后,我们要使 CMyWindow 传递消息到 CPaintBkgnd。这叫做串联消息映射。在 CMyWindow 的消息映射里,加入 CHAIN_MSG_MAP 宏: class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>,
public CPaintBkgnd<CMyWindow, RGB(0,0,255)>
{
...
typedef CPaintBkgnd<CMyWindow, RGB(0,0,255)> CPaintBkgndBase;
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_CLOSE, OnClose)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
COMMAND_HANDLER(IDC_ABOUT, OnAbout)
CHAIN_MSG_MAP(CPaintBkgndBase)
END_MSG_MAP()
...
};
任何到达 CMyWindow 映射而没有处理的消息将被传递到 CPaintBkgnd 的映射中。注意,WM_CLOSE,WM_DESTROY,和 IDC_ABOUT 不会被 串联,因为只要它们一被处理,对消息映射的搜索就会终止。typedef 是必要的,因为 CHAIN_MSG_MAP 是个接收单个参数的预处理宏;如果我们以 CPaintBkgnd<CMyWindow, RGB(0,0,255)> 作为参数,其中的逗号会使预处理器认为我们以不止一个参数来调用该宏。
你可以在继承列表中放心的使用多个嵌入类,对每一个类使用 CHAIN_MSG_MAP 宏以使消息能够传入。这不同于 MFC,每个 CWnd 派生类只能有一个基类,而且 MFC 自动向基类传递未处理的消息。
ATL EXE 的结构
现在我们有了一个完整的(虽然不怎么有用)主窗口,我们来看看如何在程序中使用它。ATL 的可执行程序里有一个或者多个大致对应于 MFC 程序中的全局 CWinApp (通常名为 theApp)的全局变量。这一领域在 VC6 和 VC7 之间从根本上被改变了,所以我需要分别介绍这两个版本。
VC 6 的情形
ATL 的可执行程序包含一个全局的 CComModule 变量,这个变量必须命名为 _Module。我们的 stdafx.h 以此开始: // stdafx.h:
#define STRICT
#define VC_EXTRALEAN
#include <atlbase.h> // Base ATL classes
extern CComModule _Module; // Global _Module
#include <atlwin.h> // ATL windowing classes
atlbase.h 会包含基本的 Windows 头文件,所以不必再包含 windows.h,tchar.h 等。在 CPP 文件里,声明(译者注:应该为定义)_Module 变量: // main.cpp:
CComModule _Module;
CComModule 中有我们需要在 WinMain() 中调用的显式初始化/退出函数,所以我们以此为始:
// main.cpp:
CComModule _Module;
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
LPSTR szCmdLine, int nCmdShow)
{
_Module.Init(NULL, hInst);
_Module.Term();
}
传给 Init() 的第一个参数仅在 COM 服务器中使用。而我们的 EXE 不是 COM 服务器,所以我们只需要传入 NULL。ATL 并不像 MFC 那样提供自己的 WinMain() 或者消息泵,所以要使我们的程序运行起来,需要创建 CMyWindow 对象并添加消息泵。 // main.cpp:
#include "MyWindow.h"
CComModule _Module;
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
LPSTR szCmdLine, int nCmdShow)
{
_Module.Init(NULL, hInst);
CMyWindow wndMain;
MSG msg;
// Create & show our main window
if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault,
_T("My First ATL Window") ))
{
// Bad news, window creation failed
return 1;
}
wndMain.ShowWindow(nCmdShow);
wndMain.UpdateWindow();
// Run the message loop
while ( GetMessage(&msg, NULL, 0, 0) > 0 )
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
_Module.Term();
return msg.wParam;
}
以上代码中唯一与众不同的是 CWindow::rcDefault,它是 CWindow 的一个 RECT 成员。将它作为窗口的初始 RECT 就像在 CreateWindow() API 里用 CW_USEDEFAULT 表示宽和高(译者注:此处有误,CW_USEDEFAULT 是用以表示左坐标和宽的)。
ATL 在底下使用了一些汇编语言的把戏来把主窗口的句柄和与之相应的 CMyWindow 对象联系起来。在此之上就是我们可以把 CWindow 对象在线程间传来传去而不出问题,而如果在 MFC 里对 CWnd 这样干会死得很惨。
VC 7 的情形
ATL 7 把模块管理代码分散到了好几个类中。出于兼容的目的,CComModule 仍然存在,在 VC 6 里写的代码经过 VC 7 转换后并不是总能干干净净地编译过去,如果不是根本编译不过去的话。因此在这我介绍一下新的类。
在 VC 7 里,ATL 的头文件自动声明所有模块类的全局实例,而且还为你调用了 Init() 和 Term() 方法,因此这些手动步骤就不再需要了。于是我们的 stdafx.h 看起来就是这样: // stdafx.h:
#define STRICT
#define WIN32_LEAN_AND_MEAN
#include <atlbase.h> // Base ATL classes
#include <atlwin.h> // ATL windowing classes
WinMain() 函数不调用任何 _Module 方法,就像这样:
// main.cpp:
#include "MyWindow.h"
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev,
LPSTR szCmdLine, int nCmdShow)
{
CMyWindow wndMain;
MSG msg;
// Create & show our main window
if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault,
_T("My First ATL Window") ))
{
// Bad news, window creation failed
return 1;
}
wndMain.ShowWindow(nCmdShow);
wndMain.UpdateWindow();
// Run the message loop
while ( GetMessage(&msg, NULL, 0, 0) > 0 )
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
我们的窗口看起来像是这样:
![[First ATL window - 4K]](http://storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU7wjxEaYrKmqlV7AiSH-9ZYcyddefkwThJMF59hAh1WSKAud4SxN0LjJOAvjwsrDIfK7VY4gLLhMZMF_CzZN0ura46QYlQ-FnbUlgwW3Ewdb)
我得承认,没什么特别激动人心的事情。为了能增添些情趣,我们会加一个能显示对话框的 About 菜单项。
ATL 中的对话框
正如已经提到的,ATL 有两个对话框类。我们将为我们的对话框使用 CDialogImpl。创建一个新的对话框类与创建一个新的框架窗口类大致相当,只不过有两处不同:
- 基类是
CDialogImpl 而不是 CWindowImpl
- 需要定义一个名为
IDD 的公用成员,其中包含有对话框的资源 ID
这是新的 About 对话框类的初始定义: class CAboutDlg : public CDialogImpl<CAboutDlg>
{
public:
enum { IDD = IDD_ABOUT };
BEGIN_MSG_MAP(CAboutDlg)
END_MSG_MAP()
};
ATL 没有针对 OK 和 Cancel 按钮的内建处理器,所以我们需要自己写代码,顺便还有 WM_CLOSE 的处理器,当用户点击标题条上的关闭按钮时此处理器会被调用。我们还需要处理 WM_INITDIALOG 以使对话框出现时能正确的设置键盘焦点。这是带有消息处理器的完整的类定义。 class CAboutDlg : public CDialogImpl<CAboutDlg>
{
public:
enum { IDD = IDD_ABOUT };
BEGIN_MSG_MAP(CAboutDlg)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
MESSAGE_HANDLER(WM_CLOSE, OnClose)
COMMAND_ID_HANDLER(IDOK, OnOKCancel)
COMMAND_ID_HANDLER(IDCANCEL, OnOKCancel)
END_MSG_MAP()
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
CenterWindow();
return TRUE; // let the system set the focus
}
LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
EndDialog(IDCANCEL);
return 0;
}
LRESULT OnOKCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
EndDialog(wID);
return 0;
}
};
我们为 OK 和 Cancel 使用同一个处理器以演示 wID 参数,该参数的值被设为 IDOK 或 IDCANCEL,这取决于哪个按钮被点击。
显示对话框和 MFC 相似,创建新类的一个对象并调用 DoModal()。回到我们的主窗口并添加一个拥有 About 菜单项的菜单,该项将显示我们的 About 对话框。我们需要添加两个消息处理器,一个是为 WM_CREATE 而另一个是为新的菜单项 IDC_ABOUT。 class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>,
public CPaintBkgnd<CMyWindow,RGB(0,0,255)>
{
public:
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)
// ...
CHAIN_MSG_MAP(CPaintBkgndBase)
END_MSG_MAP()
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
HMENU hmenu = LoadMenu ( _Module.GetResourceInstance(), // _AtlBaseModule in VC7
MAKEINTRESOURCE(IDR_MENU1) );
SetMenu ( hmenu );
return 0;
}
LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
CAboutDlg dlg;
dlg.DoModal();
return 0;
}
// ...
};
模态对话框的一个小小的不同是在什么地方指定对话框的父窗口。MFC 里是向 CDialog 的构造函数传递父窗口,而在 ATL 中应该把父窗口作为 DoModal() 的第一个参数传递。如果像上面的代码一样不指定,ATL 使用 GetActiveWindow()(将会是我们的框架窗口 )的返回结果作为父窗口。
LoadMenu() 调用也演示了 CComModule 的一个方法:GetResourceInstance()。它返回一个含有资源的模块的 HINSTANCE,就像 AfxGetResourceHandle() 一样,默认的行为是返回 EXE 的 HINSTANCE(还有一个 CComModule::GetModuleInstance(),此函数与 AfxGetInstanceHandle()相仿 )。
注意,由于模块管理类的不同,VC6 和 7 的 OnCreate() 是不同的,GetModuleInstance() 现在是在 CAtlBaseModule 里,而我们调用的是 ATL 为我们准备好的 _AtlBaseModule 对象。
这是我们修改后的主窗口和 About 对话框:
![[About box - 5K]](http://storage.msn.com/x1pC47KWjv0VYmhru0KKrOfU5jT7ImfqGnffJXWY5DtrQ49g1EtioGuGXkGe3xx4m6SfO_QJ6LBfav5S-wEJwnJqUymRp8zrDD1_782W3kyfEyfH3ujC5M0scInvIpOg-_T)
就要到 WTL 了,我保证!
不过会是在第二部分里。因为我是在为 MFC 开发人员写这些文章,所以我认为在进入 WTL 之前,最好对 ATL 先做个介绍。如果这是你对 ATL 的第一次亲密接触,那现在也许是你自己写一些简单应用的好机会,从而可以熟悉消息映射以及嵌入类的使用。你也可以实践一下 ClassView 对 ATL 消息映射的支持,它可以为你添加消息处理器。想要在 VC 6 里开始,请右击 CMyWindow 项并选择关联菜单中的 Add Windows Message Handler。在 VC 7 里,请右击 CMyWindow 项并选择关联菜单中的 Properties。在属性窗口里,点击工具栏上的 Message 按钮可以看到一个窗口消息的列表。要为消息添加处理器,可以到消息对应的行上,点击右面的一列,使之改变为一个组合框,点击组合框的箭头,然后再点击下拉列表中的 <Add> 项。
在第二部分里,我将讲解基本的 WTL 窗口类、WTL AppWizard,以及更好的消息映射宏。
修订历史
2003 年 3 月 22 日:首次发布 2005 年 12 月 15 日:更新,包括了 VC 7.1 中 ATL 的改变
| 链接: 下一部分 1/17/2006 Converting between LCIDs and RFC 1766 language codes
在 LCID 和 RFC 1766 语言代码之间转换
Occasionally, I see someone ask for a function that converts between LCIDs (such as 0x0409 for English-US) and RFC 1766 language identifiers (such as "en-us"). The rule of thumb is, if it's something a web browser would need, and it has to do with locales and languages, you should look in the MLang library. In this case, the IMultiLanguage::GetRfc1766FromLcid method does the trick.
For illustration, here's a program that takes US-English and converts it to RFC 1766 format. For fun, we also convert "sv-fi" (Finland-Swedish) to an LCID.
为了演示,下面的程序会把美国英语(US-English)转换为 RFC 1766 格式。为了活跃气氛,我们还把“sv-fi”(芬兰-瑞典语)转换成了对应的 LCID。 #include <stdio.h>
#include <ole2.h>
#include <oleauto.h>
#include <mlang.h>
int __cdecl main(int argc, char **argv)
{
HRESULT hr = CoInitialize(NULL);
if (SUCCEEDED(hr)) {
IMultiLanguage * pml;
hr = CoCreateInstance(CLSID_CMultiLanguage, NULL,
CLSCTX_ALL,
IID_IMultiLanguage, (void**)&pml);
if (SUCCEEDED(hr)) {
// Let's convert US-English to an RFC 1766 string
BSTR bs;
LCID lcid = MAKELCID(MAKELANGID(LANG_ENGLISH,
SUBLANG_ENGLISH_US), SORT_DEFAULT);
hr = pml->GetRfc1766FromLcid(lcid, &bs);
if (SUCCEEDED(hr)) {
printf("%ws\n", bs);
SysFreeString(bs);
}
// And a sample reverse conversion just for good measure
bs = SysAllocString(L"sv-fi");
if (bs && SUCCEEDED(pml->GetLcidFromRfc1766(&lcid, bs))) {
printf("%x\n", lcid);
}
SysFreeString(bs);
pml->Release();
}
CoUninitialize();
}
return 0;
}
When you run this program, you should get
在你运行此程序时,你会得到如下结果:
en-us 81d
"en-us" is the RFC 1766 way of saying "US-English", and 0x081d is MAKELCID(MAKELANGID(LANG_SWEDISH, SUBLANG_SWEDISH_FINLAND), SORT_DEFAULT).
“en-us”是 RFC 1766 方式的对美国英语的称呼,而 0x081d 就是 MAKELCID(MAKELANGID(LANG_SWEDISH, SUBLANG_SWEDISH_FINLAND), SORT_DEFAULT)。
If you browse around, you'll find lots of other interesting functions in the MLang library. You may recall that earlier we saw how to use MLang to display strings without those ugly boxes.
如果你浏览相邻的内容,你还可以在 MLang 库中找到很多其他感兴趣的功能。你还可能会回忆起早些时候我们如何使用 MLang 来显示字符串而避免出现那些难看的方框。
|