WTL for MFC Programmers, Part III - Toolbars and Status Bars

原作 :Michael Dunn [英文原文]
翻译 :Orbit(桔皮干了) [http://www.winmsg.com/cn/orbit.htm]

下载演示程序代码

本章内容

对第三部分的介绍

自从作为Windows 95的通用控件出现以来,工具条和状态条就变成了很普遍的事物。由于MFC支持浮动的工具条从而使它们更受欢迎。随着通用控件的更新,Rebars(最初被称为Coollbar)使得工具条有了另一种展示方式。在第三部分,我将介绍WTL对这些控制条的支持和如何在你的程序中使用它们。

主窗口的工具条和状态条

CFrameWindowImpl有三个HWND类型的成员变量在窗口创建时被初始化,我们已经见过m_hWndClient,它是填充主窗口客户区的“视图”窗口的句柄,现在我们遇到了另外两个:

CFrameWindowImpl只支持一个工具条,也没有像MFC那样的可多点停靠的工具条,如果你想使用多个工具条又不想修改CFrameWindowImpl的内部代码,你就需要使用Rebar。我将介绍它们二者并演示如何使用应用程序向导添加工具条和Rebar。

CFrameWindowImpl::OnSize()消息响应函数调用了UpdateLayout(),UpdateLayout()做两件事:从新定位所有控制条和改变视图窗口的大小使之填充整个客户区。实际工作是由UpdateBarsPosition()完成的,UpdateLayout()只是调用了该函数。实现的代码相当简单,向工具条和状态条发送WM_SIZE消息,由这些控制条的默认窗口处理过程将它们定位到主窗口的顶部或底部。

当你告诉应用程序向导给你的窗口添加工具条和状态条时,向导就在CMainFrame::OnCreate()中添加了创建它们的代码。现在我们来看看这些代码,当然是为了再写一个时钟程序。

向导为工具条和状态条生成得代码

我们将开始一个新的工程,让向导为主窗口创建工具条和状态条。首先创建一个名为WTLClock2的新工程,在向导的第一页,选SDI并使“生成CPP文件”检查框被选中:

 [AppWizard pg 1 - 22K]

在第二页,取消Rebar使向导仅仅创建一个普通的工具条:

 [AppWizard pg 2 - 21K]

从第二部分的程序中复制相应的代码,新程序看起来是这样的:

 [default toobar - 5K]

CMainFraCMainFrame 如何创建工具条和状态条


在这个例子中,向导向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宏指定得资源,这里使用的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,向导生成的ATL_IDS_IDLEMESSAGE对应的字符串是“Ready”。
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;
}

这些代码翻转控制条的显示状态,相应的翻转View|Toolbar菜单上的检查标记,然后调用UpdateLayout()重新定位控制条并改变视图窗口的大小。

工具条和状态条的内在特征

MFC的框架提供了很多好的特性,例如工具条按钮的工具提示和菜单项的掠过式帮助。WTL中相对应的功能实现在CFrameWindowImpl类中。下面的屏幕截图显示了工具提示和掠过式帮助。

 [Toolbar button tooltip - 4K]  [Status bar flyby help - 5K]

CFrameWindowImplBase类有两个消息相应函数用来实现这些功能,OnMenuSelect()处理WM_MENUSELECT消息,它像MFC那样查找掠过式帮助的字符串:首先装载与菜单资源ID相同的字符串资源,在字符串中查找 \n 字符,使用\n之前的内容作为掠过帮助的内容。OnToolTipTextA() 和 OnToolTipTextW() 函数分别响应 TTN_GETDISPINFOA消息和TTN_GETDISPINFOW消息,提供工具条按钮的工具提示。这两个处理函数和OnMenuSelect()函数一样装载相应的字符串,只是使用\n后面的字符串。(边注:OnMenuSelect()和OnToolTipTextA()函数对于DBCS字符是不安全的,因为它在查找\n字符时没有检查DBCS字符串的头部和尾部)下面是工具条及其关联的帮助字符串的例子:

 [Toolbar button and help string - 9K]

创建不同样式的工具条

如果你不喜欢在工具条上显示3D按钮(尽管从可用性观点来看平面的界面元素是件糟糕的事情),你可以通过改变CreateSimpleToolBar()函数的参数来改变工具条的样式。例如,你可以在CMainFrame::OnCreate()使用如下代码创建一个IE风格的工具条:

    CreateSimpleToolBar ( 0, ATL_SIMPLE_TOOLBAR_STYLE | 
                               TBSTYLE_FLAT | TBSTYLE_LIST );

如果你使用向导为你的程序添加了manifest文件,它就会在Windows XP系统上使用6.0版的通用控件,你不能选择按钮的类型,工具条会自动使用平面按钮即使你创建工具条时没有添加TBSTYLE_FLAT风格。

工具条编辑器

正如我们前面所见,向导为我们的程序创建了几个默认的按钮,当然只有About按钮有事件处理。你可以像在MFC的工程中一样使用工具条编辑器修改工具条资源,CreateSimpleToolBarCtrl()用这个工具条资源创建工具条。下面是向导生成的工具条在编辑器中的样子:

 [default toolbar in the editor - 7K]

对于我们的时钟程序,我们添加四个按钮,两个按钮用来改变视图窗口的颜色,另外两个用来显示/隐藏工具条和状态条。下面是我们的新工具条:

 [New toolbar buttons - 5K]

这些按钮是:

前两个按钮都有相应的菜单项,它们都调用视图类的一个新函数SetColor(),向这个函数传递前景颜色和背景颜色,视图窗口用这两个参数改变窗口的显示。响应这两个按钮的处理函数与响应相应的菜单项的处理函数在使用COMMAND_ID_HANDLER_EX宏上没有区别,你可以查看例子工程的代码了解这些消息处理的细节。在下一节我将介绍状态条和工具条按钮的UI状态更新,使它们能够反映工具条或状态条当前的状态。

工具条按钮的UI状态更新

向导生成的代码已经为CMainFrame添加了对View|Toolbar和View|Status Bar两个菜单项的Check和Uncheck的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,所以当需要更新按钮的状态时CUpdateUI会向这个窗口发消息。另一个重要的调用位于OnIdle()中:

BOOL CMainFrame::OnIdle()
{
    UIUpdateToolBar();
    return FALSE;
}

当消息队列中没有消息等待时CMessageLoop::Run()就会调用OnIdle(),UIUpdateToolBar()遍历UI更新表,寻找那些带有UPDUI_TOOLBAR标志又被UISetCheck()之类的函数改变了状态的的界面元素(当然是工具条),相应的改变按钮的状态。需要注意得是如果更新弹出式菜单的状态就不需要做以上两步,因为CUpdateUI响应WM_INITMENUPOPUP消息,只有接到此消息时才更新菜单状态。

如果查看例子代码就会发现它也演示了如何更新框架窗口的菜单条上的顶级菜单项的状态。有一个菜单项是执行Start和Stop命令,起到开始和停止时钟的作用,当然这需要做一些不平常的事情:菜单条上的菜单项总是处于弹出状态。为了完整的介绍CUpdateUI我将它们也加进例子代码中,要了解它们可以查找对UIAddMenuBar()和UIUpdateMenuBar()两个函数的调用。

使用Rebar代替简单的工具条

CFrameWindowImpl也支持使用Rebar控件,使你的程序看起来像IE,使用Rebar也是在程序中使用多个工具条的一个方法(译者加:前面讲过,另一个方法就是修改WTL的源代码)。要使用Rebar需要在向导的第二页选上支持Rebar的检查框,如下所示:

 [AppWizard pg 2 with rebar checked - 21K]

第二个例子工程WTLClock3就使用了Rebar控件,如果你正在跟着例子代码学习,那现在就打开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文件中,与ATL_SIMPLE_TOOLBAR_STYLE风格相似,只是附加了一些诸如CCS_NOPARENTALIGN之类的风格,这是使工具条作为Rebar的子窗口能够正常工作所必需的风格。

下一行代码是调用CreateSimpleReBar()函数,该函数创建Rebar控件并将句柄存到m_hWndToolBar中。接下来调用AddSimpleReBarBand()函数为Rebar创建一个条位并告诉Rebar这个条位上是一个工具条。

 [App with rebar - 4K]

CMainFrame::OnViewToolBar()函数也有些不同,它只隐藏Rebar上工具条所在的条位而不是隐藏m_hWndToolBar(如果隐藏m_hWndToolBar将隐藏整个Rebar而不仅仅是工具条)。

如果你使用多个工具条,只需像向导为我们生成的关于第一个工具条的代码那在OnCreate()创建它们并调用AddSimpleReBarBand()添加到Rebar就行了。CFrameWindowImpl使用标准的Rebar控件,不像MFC那样支持可停靠的工具条,你所能作得就是排列这些工具条在Rebar中的位置。

多窗格的状态条

WTL另有一个状态条类实现多窗格的状态条,与MFC的默认的状态条一样有CAPS,LOCK和NUM LOCK指示器,这个类就是CMultiPaneStatusBarCtrl,在WTLClock3例子工程中演示了如何使用这个类。这个类支持有限的UI更新,当弹出式菜单被显示时有“Default”属性的窗格会延伸到整个状态条的宽度用于显示菜单的掠过式帮助。

第一步就是在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
窗格ID数组中元素的个数(译者加:就是窗格数)
bSetText
如果是true,所有的窗格被立即设置文字,这一点将在下面解释。

窗格ID可以是ID_DEFAULT_PANE,此ID用于创建支持掠过式帮助的窗格,窗格ID也可以是字符串资源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,这很不幸,因为如果你重新排列了窗格,你要记得更新UI更新表。

由于我们在调用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()的调用就不会起作用。我没有研究这个问题是否可以被修复,所以如果你打算改变菜单的文本,你需要留意这个地方。

最后,我们需要检查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()时将从字符串资源中装载“CAPS”字符串,但是在CString的构造函数中使用了一个机灵的窍门(有充分的文档说明)。

在完成所有的代码之后,状态条看起来是这个样子:

 [Multipane status bar - 4K]

承上启下:有关对话框的话题

在第四章我将介绍对话框的用法(包括ATL的类和WTL的增强功能),控件的包装类和WTL有关对话框消息处理的改进。

引用和参考

"How to use the WTL multipane status bar control" by Ed Gadziemski 更详细的介绍了CMultiPaneStatusBarCtrl类的用法。

修改记录

2003年4月11日,本文第一次发表。