WTL MFC 程序员指南 第三部分 - 工具栏和状态栏






4.94/5 (79投票s)
2003年4月12日
14分钟阅读

457910

4360
在 WTL 应用程序中使用工具栏和状态栏的基础知识。
目录
- 引言
- 框架中的工具栏和状态栏
- 工具栏和状态栏的 AppWizard 代码
- 工具栏编辑器
- 工具栏按钮的 UI 更新
- 使用 Rebar 而不是普通工具栏
- 多窗格状态栏
- 下一篇:关于对话框的一切
- 参考文献
- 版权和许可
- 修订历史
第三部分简介
自从工具栏和状态栏成为 Windows 95 的通用控件以来,它们已经变得无处不在。MFC 支持多个浮动工具栏也助长了它们的流行。在后来的通用控件更新中,rebar(或最初称为 coolbar)提供了另一种呈现工具栏的方式。在第三部分,我将介绍 WTL 如何支持这些类型的控件栏以及如何在您自己的应用程序中使用它们。
请记住,如果您在安装 WTL 或编译演示代码时遇到任何问题,请在在此处提问之前阅读第一部分的自述文件。
框架中的工具栏和状态栏
CFrameWindowImpl
有三个 HWND
成员,它们在框架窗口创建时被设置。我们已经看到了 m_hWndClient
,它是框架客户区中的“视图”窗口的句柄。现在我们将遇到另外两个
m_hWndToolBar
:工具栏或 rebar 的HWND
m_hWndStatusBar
:状态栏的HWND
CFrameWindowImpl
只支持一个工具栏,并且没有与 MFC 的多个可停靠工具栏系统相对应的功能。如果您需要多个工具栏,并且不想修改 CFrameWindowImpl
的内部结构,那么您将需要使用 rebar。我将介绍这两种选项,并在 AppWizard 中展示如何从中选择。
CFrameWindowImpl::OnSize()
处理程序调用 UpdateLayout()
,它执行两件事:定位栏,并将视图窗口调整到填充客户区。UpdateLayout()
调用 UpdateBarsPosition()
来执行实际工作。代码非常简单,它只是向工具栏和状态栏发送一个 WM_SIZE
消息,前提是这些栏已经被创建。栏的默认窗口过程负责将栏移动到框架窗口的顶部或底部。
当您告诉 AppWizard 为您的框架提供工具栏和状态栏时,向导会将创建这些栏的代码放在 CMainFrame::OnCreate()
中。现在,让我们仔细看看这段代码,我们将再次编写一个时钟应用程序。
工具栏和状态栏的 AppWizard 代码
我们将启动一个新项目,并让向导为我们的框架创建一个工具栏和状态栏。启动一个名为 WTLClock2 的新 WTL 项目。在第一个 AppWizard 页面上,选择一个 SDI 应用程序并勾选“生成 CPP 文件”
在下一页上,取消勾选“Rebar”,以便向导创建一个普通的工具栏
复制第二部分应用程序的时钟代码后,新应用程序如下所示
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
加上通常的子窗口和可见样式。这使得工具栏创建一个工具提示控件,用于在光标悬停在按钮上时显示。 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
,在右下角添加一个调整大小的抓手。 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
还有一个视图菜单,其中有两个命令用于显示/隐藏工具栏和状态栏。这些命令的 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; }
这会切换栏的可见状态,切换视图|工具栏菜单项旁边的复选标记,然后调用 UpdateLayout()
来定位栏(如果它正在变得可见)并调整视图窗口的大小。
栏的内置功能
MFC 的框架在其工具栏和状态栏中提供了一些不错的功能,例如工具栏按钮的工具提示和菜单项的即时帮助。WTL 在 CFrameWindowImpl
中实现了类似的功能。下面是显示工具提示和即时帮助的屏幕截图。
CFrameWindowImplBase
有两个用于这些功能的 असतात消息处理程序。OnMenuSelect()
处理 WM_MENUSELECT
,它查找即时帮助字符串,就像 MFC 一样 - 它加载一个与当前选定菜单项 ID 相同的字符串资源,查找字符串中的 \n
字符,并将 \n
前面的文本用作即时帮助。OnToolTipTextA()
和 OnToolTipTextW()
分别处理 TTN_GETDISPINFOA
和 TTN_GETDISPINFOW
以提供工具栏按钮的工具提示文本。这些处理程序加载与 OnMenuSelect()
相同的字符串,但使用 \n
后面的文本。(顺便说一句,在 WTL 7.0 和 7.1 中,OnMenuSelect()
和 OnToolTipTextA()
不是 DBCS 安全的,因为它们在查找 \n
时不检查 DBCS 前导/尾随字节。)下面是一个工具栏按钮及其相关帮助字符串的示例
创建具有不同样式的工具栏
您可以通过将样式位作为第二个参数传递给 CreateSimpleToolBar()
来更改工具栏的样式。例如,要使用 3D 按钮并使工具栏类似于 Internet Explorer 中的工具栏,请在 CMainFrame::OnCreate()
中使用以下代码
CreateSimpleToolBar ( 0, ATL_SIMPLE_TOOLBAR_STYLE |
TBSTYLE_FLAT | TBSTYLE_LIST );
请注意,如果您在应用程序中使用通用控件 v6 manifest,那么在 Windows XP 或更高版本上运行时,您在按钮方面没有选择 - 即使您在创建工具栏时未指定 TBSTYLE_FLAT
样式,工具栏也始终使用平面按钮。
工具栏编辑器
如前所述,AppWizard 会创建几个默认按钮。然而,只有关于按钮被连接了。您可以像处理 MFC 项目一样编辑工具栏;编辑器会修改 CreateSimpleToolBarCtrl()
用于构建栏的工具栏资源。以下是 AppWizard 生成的工具栏在编辑器中的外观
对于我们的时钟应用程序,我们将添加两个按钮来更改视图窗口中的颜色,以及两个用于显示/隐藏工具栏和状态栏的按钮。这是我们新的工具栏
按钮是
IDC_CP_COLORS
:将视图更改为 CodeProject 颜色IDC_BW_COLORS
:将视图更改为黑白ID_VIEW_STATUS_BAR
:显示或隐藏状态栏ID_VIEW_TOOLBAR
:显示或隐藏工具栏
前两个按钮在视图菜单上也有对应的项。它们调用视图类中的一个新函数 SetColors()
。每个函数都传递一个前景色和背景色,视图将使用这些颜色显示时钟。处理这两个按钮与处理带有 COMMAND_ID_HANDLER_EX
宏的菜单项没有区别;如果您想查看消息处理程序的详细信息,请查看示例项目。在下一节中,我将介绍 UI 更新视图状态栏和视图工具栏按钮,以便它们反映栏的当前状态。
工具栏按钮的 UI 更新
AppWizard 生成的 CMainFrame
已经有了 UI 更新处理程序,它们会检查并取消勾选视图|工具栏和视图|状态栏菜单项。这与第二部分中的应用程序一样进行 - 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()
还有其他两个函数需要调用以连接工具栏按钮的更新,但向导生成的代码会调用它们,因此如果您在此处构建项目,菜单项和工具栏按钮都将得到更新。
启用工具栏 UI 更新
如果您查看 CMainFrame::OnCreate()
,您会看到一个新代码块,它设置了两个视图菜单项的初始状态
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 更新框架菜单栏中的顶级菜单项。有一个项执行启动和停止命令来启动和停止时钟。虽然这不是一个常见(甚至不推荐)的做法 - 菜单栏中的项应始终是弹出菜单 - 但我为了完整性包含了它,以便涵盖 CUpdateUI
。查找对 UIAddMenuBar()
和 UIUpdateMenuBar()
的调用。
使用 Rebar 而不是普通工具栏
CFrameWindowImpl
也支持使用 rebar 来让您的应用程序看起来类似于 Internet Explorer。如果您需要多个工具栏,使用 rebar 也是最佳选择。要使用 rebar,请在 AppWizard 第二页上勾选 Rebar 复选框,如下图所示
第二个示例项目 WTLClock3 是勾选此选项创建的。如果您正在跟随示例代码,请现在打开 WTLClock3。
您首先注意到的将是创建工具栏的代码有所不同。这很有意义,因为我们在该应用程序中使用 rebar。这是相关代码
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 in
,它类似于 ATL_SIMPLE_TOOLBAR_STYLE
,但具有额外的样式,如 CCS_NOPARENTALIGN
,这些样式对于工具栏作为 rebar 的子项正常工作是必需的。
下一行调用 CreateSimpleReBar()
,它创建一个 rebar 并将其 HWND
保存在 m_hWndToolBar
中。接下来,AddSimpleReBarBand()
向 rebar 添加一个带,并告诉 rebar 该工具栏将包含在该带中。
CMainFrame::OnViewToolBar()
也有所不同。它不隐藏 m_hWndToolBar
(这将隐藏整个 rebar,而不仅仅是一个工具栏),而是隐藏包含工具栏的带。
如果您想拥有多个工具栏,您可以在 OnCreate()
中创建它们并调用 AddSimpleReBarBand()
,就像向导生成的代码为第一个工具栏所做的那样。由于 CFrameWindowImpl
使用标准的 rebar 控件,因此不支持像 MFC 那样的可停靠工具栏;用户唯一能做的就是重新排列工具栏在 rebar 中的位置。
多窗格状态栏
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 );
请注意,我们将状态栏句柄保存在 m_hWndStatusBar
中,就像 CreateSimpleStatusBar()
会做的那样。
下一步是通过调用 CMultiPaneStatusBarCtrl::SetPanes()
来设置窗格
BOOL SetPanes(int* pPanes, int nPanes, bool bSetText = true);
参数如下:
pPanes
- 窗格 ID 的数组
nPanes
pPanes
中的元素数量bSetText
- 如果为 true,则所有窗格的文本都会立即设置。这将在下面解释。
窗格 ID 可以是 ID_DEFAULT_PANE
以创建即时帮助窗格,或者是字符串表中的字符串 ID。对于非默认窗格,WTL 会加载具有匹配 ID 的字符串并计算其宽度,然后将相应的窗格设置为相同的宽度。这与 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
宏中的数字以匹配新顺序。
由于我们将 false 作为第三个参数传递给 SetPanes()
,因此窗格最初是空的。我们的下一步是将时钟状态窗格的初始文本设置为“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 项目,时钟启动/停止菜单项已移至“时钟”菜单,并且该命令的处理程序会设置菜单项的文本。但是,如果存在对 UIUpdateStatusBar()
的调用,则 UISetText()
调用将无效。此 bug 在 WTL 7.1 中仍然存在;一些人已经调查并找到了修复方法 - 有关更多详细信息,请参阅文章末尾的讨论论坛。
最后,我们需要检查 CAPS LOCK 键的状态并相应地更新窗格 2。此代码可以直接放在 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”字符串。
经过所有这些代码,状态栏看起来是这样的
下一篇:关于对话框的一切
在第四部分,我将介绍对话框(ATL 类和 WTL 增强功能)、控件包装器,以及更多与对话框和控件相关的 WTL 消息处理改进。
参考文献
Ed Gadziemski 的“如何使用 WTL 多窗格状态栏控件” 详细介绍了 CMultiPaneStatusBarCtrl
类。
版权和许可
本文是版权材料,(c)2003-2005 Michael Dunn。我知道这无法阻止人们到处复制它,但我还是得说。如果您有兴趣翻译本文,请通过电子邮件与我联系。我不认为会拒绝任何人翻译的许可,我只是想知道翻译情况,以便在此处发布链接。
本文附带的演示代码已发布到公共领域。我以这种方式发布它,以便代码能够惠及所有人。(我不使本文本身进入公共领域,因为只在 CodeProject 上提供本文有助于我个人的知名度和 CodeProject 网站。)如果您在自己的应用程序中使用演示代码,发送一封电子邮件让我知道将不胜感激(只是为了满足我对人们是否受益于我的代码的好奇心),但不是必需的。在您自己的源代码中注明出处也同样受到赞赏,但不是必需的。
修订历史
- 2003 年 4 月 11 日:文章首次发布。
- 2005 年 12 月 20 日:更新以涵盖 VC 7.1 中的更改。
系列导航:« 第二部分 (WTL GUI Base Classes) | » 第四部分 (Dialogs and Controls)