引言:好几年前就读过这个系列了,也曾经有过翻译的念头,都因种种原因作罢。前些日子在网上看到了一位网友对此系列的翻译,虽然看起来要比看原文省劲,但却发现许多处不忠实原文的地方,而且还有一些翻译上的错误,所以就生出了重新翻译的念头。这是第三章,敬请大家指正。
特别注 1:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
特别注 2:本文为第一版,要浏览第二版请点击这里。
第三部分 – 工具栏和状态栏
内容
- 第三部分介绍
- 框架中的工具栏和状态栏
- AppWizard 为工具栏和状态栏生成的代码
- CMainFrame 如何创建栏
- 显示或者隐藏栏
- 栏的内建特性
- 使用不同的风格创建工具栏
- 工具栏编辑器
- 工具栏按钮的 UI 更新
- 启用工具栏 UI 更新
- 用复用栏代替平实的工具栏
- 多窗格状态栏
- 窗格的 UI 更新
- 下一步:关于对话框的一切
- 参考资料
- 修订历史
第三部分介绍
从在 Windows 95 中被作为通用控件开始,使用工具栏和状态栏已经成为了平常事。MFC 对多浮动工具栏的支持也对他们的流行起了推波助澜的作用。在后来的通用控件升级中,复用栏(Rebar,或者作出被称为的酷栏,coolbar)又为如何呈现工具栏增添了新的途径。在这一部分里,我会涵盖以下知识,WTL 如何对这些种类的栏进行支持以及如何在自己的应用中使用它们。
框架中的工具栏和状态栏
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:
在下一页,不要选 Rebar 以使向导生成普通的工具栏:
从第二部分的应用中把时钟相关的代码复制过来之后,这个新的应用看起来应该是这样:
CMainFrame 如何创建栏
在这个工程里,AppWizard 向 CMainFrame::OnCreate() 函数添加了更多的代码。其作用是创建所有栏并告诉 CUpdateUI 更新工具栏的按钮。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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() 的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 中。代码如下:
1 2 3 4 5 6 7 8 9 10 |
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() 会调用一个重载版本的同名函数来做实际工作:
1 2 3 4 5 6 7 8 9 |
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() 处理器:
1 2 3 4 5 6 7 8 9 |
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 类中也实现了相同的特性。下面是正显示着工具提示和动态帮助的屏幕截图。
CFrameWindowImplBase 有两个处理器用以实现这两个特性。 OnMenuSelect() 处理 WM_MENUSELECT,而且它还会像 MFC 那样寻找动态帮助 – 加载与当前选中的菜单项相同 ID 的字符串资源,在字符串中寻找 \n 字符,并使用 \n 前的文字作为动态帮助。 OnToolTipTextA() 和 OnToolTipTextW() 相应地处理 TTN_GETDISPINFOA 和 TTN_GETDISPINFOW 来为工具栏按钮提供工具提示。这两个处理器和 OnMenuSelect() 加载相同的字符串,不过会使用 \n 之后的文字。(附注: OnMenuSelect() 和 OnToolTipTextA() 并非双字节字符集安全的,也就是说,它们在查找 \n 时不会检查双字节字符集的前导或者结尾字节)。以下是一个工具栏按钮及其关联的帮助字符串的例子:
使用不同的风格创建工具栏
如果你不喜欢工具栏按钮具有三维效果(尽管平面 UI 元素从可用性角度来讲比较糟糕),你可以向 CreateSimpleToolBar() 传递参数以更改工具栏的风格。例如,要创建类似于 Internet Explorer 的工具栏,可以在 CMainFrame::OnCreate() 中使用如下代码:
1 2 |
CreateSimpleToolBar ( 0, ATL_SIMPLE_TOOLBAR_STYLE | TBSTYLE_FLAT | TBSTYLE_LIST ); |
注意,如果你让 AppWizard 为应用添加了 manifest 文件,那它在 XP 系统上就会使用版本 6 的通用控件。那么你就没有选择余地了 – 工具栏总是使用平面按钮,即使在创建工具栏时没有指定 TBSTYLE_FLAT 风格。
工具栏编辑器
正如在前面所看到的,AppWizard 会创建好几个缺省的按钮,不过只有 About 按钮会被处理。你可以像在 MFC 工程中一样编辑工具栏,编辑器可以修改 CreateSimpleToolBarCtrl() 要用到的工具栏资源。下面是 AppWizard 在编辑器中生成的工具栏的样子:
对我们的时钟应用来说,我们要添加两个按钮,用来改变视图窗口的颜色,另外两个按钮来显示或者隐藏工具栏以及状态栏。下面是我们的新工具栏:
按钮:
- 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 更新宏:
1 2 3 4 |
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 标志:
1 2 3 4 |
BEGIN_UPDATE_UI_MAP(CMainFrame) UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP <b>| UPDUI_TOOLBAR</b>) UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP <b>| UPDUI_TOOLBAR</b>) END_UPDATE_UI_MAP() |
要处理工具栏按钮的更新,还要调用另外的两个函数,不过幸好 AppWizard 生成的代码里应经有了。所以,如果你现在就编译工程,菜单项和工具栏按钮都可以更新。
启用工具栏 UI 更新
如果你看 CMainFrame::OnCreate() 函数,你会看到一段新代码,它设置了两个 View 菜单项的初始状态:
1 2 3 4 5 6 7 8 9 10 |
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() 里:
1 2 3 4 5 |
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 框,如下图所示:
我们的第二个例子,WTLClock3,就选中了这一选项。如果你要一路追踪示例代码,那现在就打开 WTLClock3 工程。
你注意到的第一个不同之处应该是创建工具栏的代码。这是有道理的,因为我们是要在应用中使用复用栏。下面是相关的代码:
1 2 3 4 5 6 7 8 9 10 |
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() 向复用栏添加一个复用条,并告诉复用栏该复用条内要包容工具栏。
CMainFrame::OnViewToolBar() 也有不同,它将隐藏包含工具栏的复用条而不是隐藏 m_hWndToolBar,因为那样将隐藏整个复用栏,而不仅仅是一个工具栏。
如果需要多个工具栏,你可以在 OnCreate() 中分别创建它们并调用 AddSimpleReBarBand(),就像向导为第一个工具栏生成的代码一样。因为 CFrameWindowImpl 使用的是标准的复用栏控件,并没有像 MFC 一样对可停靠工具栏进行支持,用户能做的只能是重新排列复用栏上的工具栏的位置。
多窗格状态栏
WTL 还有另外一个状态栏类,实现了在一个栏内包括多个窗格,就像 MFC 里那种有 CAPS LOCK 和 NUM LOCK 指示器的默认状态栏那样。这个类叫做 CMultiPaneStatusBarCtrl,在 WTLClock3 例子里演示了其用法。这个类支持了有限的 UI 更新,而且当弹出菜单显示出来的时候 “缺省” 窗格会拉伸到完整宽度以显示动态帮助。
第一步在 CMainFrame 中声明一个 CMultiPaneStatusBarCtrl 成员变量:
1 2 3 4 5 6 |
class CMainFrame : public ... { //... protected: CMultiPaneStatusBarCtrl m_wndStatusBar; }; |
然后在 OnCreate() 函数中,我们创建状态栏并为它做好 UI 更新的准备工作:
1 2 |
m_hWndStatusBar = m_wndStatusBar.Create ( *this ); UIAddStatusBar ( m_hWndStatusBar ); |
注意,和 CreateSimpleStatusBar() 一样,我们把状态栏句柄保存到了 m_hWndStatusBar 里。
下一步则是通过调用 CMultiPaneStatusBarCtrl::SetPanes() 来设置窗格:
1 |
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() 的代码:
1 2 3 4 5 |
// 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 更新表中新添入口:
1 2 3 4 5 |
BEGIN_UPDATE_UI_MAP(CMainFrame) //... UPDATE_ELEMENT(1, UPDUI_STATUSBAR) // clock status UPDATE_ELEMENT(2, UPDUI_STATUSBAR) // CAPS indicator END_UPDATE_UI_MAP() |
宏的第一个参数是窗格的索引,而非 ID。这很不幸,如果你重新排列了窗格,你需要记得更新表中的数字。
由于我们把 SetPanes() 的第三个参数设置为了 false,所以窗格开始的时候是空着的。我们的下一步就是将时钟状态窗格的初始文本设置为 “Running”。
1 2 |
// Set the initial text for the clock status pane. UISetText ( 1, _T("Running") ); |
和上面一样,第一个参数也是窗格的索引。 UISetText() 是唯一能工作于状态栏的 UI 更新函数。
最后,我们还需要在 CMainFrame::OnIdle() 中加入对 UIUpdateStatusBar() 的调用,以使状态栏窗格在空闲时间能被更新:
1 2 3 4 5 6 |
BOOL CMainFrame::OnIdle() { UIUpdateToolBar(); <b>UIUpdateStatusBar();</b> return FALSE; } |
当你使用 UIUpdateStatusBar() 函数时, CUpdateUI 里的一个问题就会显露出来 – 菜单项的文本在调用 UISetText() 后并未更新!如果你查看 WTLClock3 工程,会发现时钟的开始/停止菜单项移到了 Clock 菜单下,而且是由命令的处理器来设置菜单项的文本。但是,如果对 UIUpdateStatusBar() 的调用存在,则 UISetText() 调用不会起作用。我还不清楚是否可以修正此问题,所以如果你计划更新菜单项的文本的话,你应该把这件事记在脑子里。
最后,我们需要检查 CAPS LOCK 键的状态并相应更新第二个窗格。代码放到 OnIdle() 函数里,从而应用每次进入空闲时都会检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
BOOL CMainFrame::OnIdle() { <b>// 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("") );</b> UIUpdateToolBar(); UIUpdateStatusBar(); return FALSE; } |
第一次调用 UISetText() 时,会通过在 CString 构造函数中使用一个优雅的技巧来从字符串表中加载 “CAPS” 字符串。
完成了这些代码后,状态栏看起来应该是这样:
下一步:对话框的一切
在第四部分里,我会介绍对话框(包括 ATL 的类以及 WTL 的增强),控件封装类,以及更多的与对话框和控件相关的 WTL 消息处理的改进。
参考资料
“How to use the WTL multipane status bar control” ,Ed Gadziemski,其中有 CMultiPaneStatusBarCtrl 类更为详尽的细节知识。
修订历史
2003 年 4 月 11 日:首次发布