话说昨天解决了MFC跨线程操作控件的问题,我满以为今天可以free一回,玩玩Linux、学学Vim、再准备一下毕业论文的事情,但还是有事情要做,然后又是“被”MFC郁闷了一天。

先介绍一下总体的情况。我们项目客户端的开发环境是VS2008+SP1,用的是MFC类库,里面居然用到了CMFCToolBar、CMFCMenuBar以及Appearance变化等的SPI新特性。说“居然”是因为这些东西不是项目必要的,当时可能也以为只是名字变了用法没变,估计在工程创建的时候根本就没有考虑这些,直接按着单文档工程默认配置,next、next直接创建完的,囧!当时做的时候也只是当作测试Demo来用,也没太在意,毕竟我们项目的重点在服务器而非这个MFC客户端。

后来由于项目原因,甲方要求我们把这个客户端尽快修改成一个可以发布版本。不改不知道,一改吓一跳,当准备动手修改工具栏时才发现与以前惯的CToolBar真实差距甚大。CToolBar可以用CImageList把自定义的BMP图片放到工具栏的按钮,详细可看这里,CMFCToolBar根本就不是这样的一个玩法。直接放一个CToolBar上来,在DockControlBar()的时候会出现断言错误(缺少DockBar,貌似是这个名字,汗!)。定位代码到MainFrm的EnableDocking(),现在的MainFrm的继承关系是CMainFrm->CFrameWndEx->CFrameWnd,而以前是CMainFrm->CFrameWnd,CFrameWndEx::EnableDocking()是为DockPane()服务的,而DockControlBar()需要的DockBar并不会被初始化。调用基类的CFrameWnd::EnableDocking()后再DockControlBar()不会出现断言,但是那个工具栏没有显示。而且现在新特性下在工具栏位置能够按出右键菜单,但右键菜单中根本不可能有关于该CToolBar的信息,乍看起来很不和谐~

最后,求助本地MSDN无果,貌似SP1没有包含对MSDN文档的更新;求助MSDN官网,那个真是“言简意赅”。只能说,MS你这次真的“亮”了!

以下为google + vs2008 sp1 sample + 看代码的成果:
  • 创建默认ToolBar外的第二个ToolBar
1 //默认工具栏
2 m_wndToolBar.CreateEx(this, TBSTYLE_FLAT,
3                WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);
4 //自定义工具栏
5 m_mybar.CreateEx(this, TBSTYLE_FLAT,
6                  WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC,
7                  CRect(1,1,1,1), ID_MYBAR);

注意,Wizard生成的工具栏Create时没有带ID,但第二个工具栏Create时最好要带ID。加了ID之后,在工具栏右键菜单才会出现第二个工具栏的CheckBox。否则,不良后果有:1、右键菜单没有该工具栏Checkbox;2、把默认工具栏和该工具栏拖出来(浮动),可以看到名字都是一样的(英文版为Standard);3、后面要提到的UserImage不能作为按钮图标显示。

  • 加载工具栏资源
我们先来看看CMFCToolBar加载工具栏的函数原型:
1 virtual BOOL LoadToolBar(UINT uiResID, UINT uiColdResID = 0, UINT uiMenuResID = 0, BOOL bLocked = FALSE,
2         UINT uiDisabledResID = 0, UINT uiMenuDisabledResID = 0,  UINT uiHotResID = 0);
可以看出,uiResID代表要加载的工具栏资源,理论上只需要这一个参数就能完成工具栏的加载。但是VS的Toolbar Editor只能编辑4bit的工具栏图标,以前CToolBar是用CImagList来加载更多bits的图标的,现在应该怎么做呢?多亏了Explore sample的例子,我发现后面的几个UINT参数就是BMP的资源,最主要的是最后一个uiHotResID,即便其他用默认值,这项赋BMP ID就能按预期的图标显示。Cold、Disable表示的是不同状态下的图标样式,带Menu的是Menu有关的图标,具体可看SP1 Feature的sample。
我的Demo里自定义工具栏的总创建过程:
1     if ( !m_mybar.CreateEx(this, TBSTYLE_FLAT,
2                            WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC,
3                            CRect(1,1,1,1), ID_MYBAR) ||
4          !m_mybar.LoadToolBar( IDR_TOOLBAR1, 00, FALSE, 00, theApp.m_bHiColorIcons?IDB_BITMAP1:0 ) )
5     {
6         TRACE0("Failed to create toolbar\n");
7         return -1;      // fail to create
8     }
9     m_mybar.SetWindowText(_T("abc"));
最后的SetWindowText()设置工具栏的名称。
CMFCToolBar有LoadBitmap的方法,但是测试发现,用LoadToolBar只加载工具栏资源,再用LoadBitmap加载BMP资源,虽然返回值是TRUE,但显示图标为空白,没有实际效果。

  • 工具栏停靠
1   // TODO: Delete these five lines if you don't want the toolbar and menubar to be dockable
2   m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
3   m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
4   m_mybar.EnableDocking(CBRS_ALIGN_ANY);
5   EnableDocking(CBRS_ALIGN_ANY);
6   DockPane(&m_wndMenuBar);
7   DockPane(&m_wndToolBar);
8   DockPane(&m_mybar);
与默认工具栏无异。

  • 用户自定义图标
CMFCToolBar可以让用户自定义工具栏图标,使用静态成员函数SetUserImages()将一个CMFCToolBarImages对象设置进去,由所有CMFCToolBar对象共享。Wizard自动生成代码中有这样的例子:
 1     if (CMFCToolBar::GetUserImages() == NULL)
 2     {
 3         // load user-defined toolbar images
 4         if (m_UserImages.Load(_T(".\\UserImages.bmp")))
 5         {
 6             m_UserImages.SetImageSize(CSize(1616), FALSE);
 7             CMFCToolBar::SetSizes(CSize(16,16), CSize(16,16));
 8             CMFCToolBar::SetUserImages(&m_UserImages);
 9         }
10     }
这个例子加载了工程路径下的一个BMP,其他方法可以查看MSDN,与CImageList有点点类似。
使用CMFCToolBar::ReplaceButton()可以替换已有的工具栏按钮,以下是我的Demo中的代码:
1     m_mybar.ReplaceButton( ID_QTLOGO, CMFCToolBarButton(ID_QTLOGO, 0, _T("123"), TRUE) );
第一个参数ID_QTLOGO为自定义工具栏上的一个按钮,后面是一个CMFCToolBarButton的临时对象。CMFCToolBarButton构造函数第一个参数为替换后的ID,第三个参数为名称,第二个参数为图标的索引(zero-based),第四个参数为m_bUserButton,指明第二个参数是索引工具栏已加载图标(LoadToolBar或LoadBitmap)还是用户自定义图标(SetuserImages),TRUE指用户自定义图标。这里的结果是将ID_QTLOGO上的图标替换为UserImages.bmp上的第一个图标。

GetCmdMgr()->GetCmdImage()可以根据工具栏上图标的ID获取出已加载图标的索引值:
1     m_mybar.ReplaceButton( ID_QTLOGO, CMFCToolBarButton(ID_QTLOGO, GetCmdMgr()->GetCmdImage(ID_PLUS), _T("123")) );
这里将工具栏上ID_QTLOGO的图标替换为ID_PLUS按钮对应的图标。

特别地,如果在你将这些工具栏改来改去但显示结果却没有改变的时候,你可以尝试删除 HKEY_CURRENT_USER\Software\Local AppWizard-Generated Applications\$(你的程序名) 这个键值,当你重启程序后工具栏应该会按你的预想变化的。这是我在查资料时看到的,当时没注意但后来发现挺有用的,出处没有记录下来。

最后,ReplaceButton还可以将按钮替换为其他控件。

  • 其他...
我在自定义工具栏上做了一个有效响应,里面使用静态成员函数CMFCToolBar::ResetAllImages()将所有图标都清空了,此时会发现默认工具栏、自定义工具栏的图标都为空。
 1 void CMainFrame::OnQtLogo()
 2 {
 3     CMFCToolBar::ResetAllImages();
 4 
 5     //CMFCToolBar::AddToolBarForImageCollection(IDR_MENU_IMAGES, theApp.m_bHiColorIcons ? IDB_MENU_IMAGES_24 : 0);
 6 
 7     m_wndToolBar.LoadBitmap(IDB_BITMAP1);
 8     m_mybar.LoadBitmap(IDR_MAINFRAME_256);
 9     m_wndToolBar.RedrawWindow();
10     m_mybar.RedrawWindow();
11 }
更奇妙的是,后面我对两个工具栏重新加载了BMP,而且加载的BMP资源是反了的,此时默认工具栏上出现了原来自定义工具栏的4个图标,余下部分及自定义工具栏则为原来默认工具栏图标。可以想象,RestAllImages只是将图标资源都释放了,工具栏资源依然健在,重新加载BMP的时候,工具栏图标就像一个个顺序排好的空间,加载进来的BMP图标会出现从前往后补位的现象。
注意代码中,默认工具栏图标重新加载时使用的资源是IDR_MAINFRAME_256,是默认的工具栏资源。也就是说,这里用LoadBitmap加载工具栏资源也是有效果的。这样应该可以说明工具栏在创建时LoadToolBar、LoadBitmap分别成功地加载了工具栏、BMP资源,实际上是加载了两套图标资源,这两者是顺序而非重合的,所以只显示原来的工具栏资源。要想指定两者的重合关系,只有在LoadToolBar的时候同时传入工具栏资源及BMP资源的ID。

Demo下载

————————————————————————————————————————————————————————————————
好吧,终于写完了!写得很仓促,不足的地方也很多,欢迎指教!