声明:
工具栏是一个比较传统的话题,如果您认为本文的内容比较陈旧的话,请您不要阅读,以免浪费你的宝贵精力和时间。
多工具条编程秘技
李安东
2001年10月1日
关键字: 工具栏 真彩色按钮 按钮标签
对于一个应用程序来说,用户界面始终是至关重要的问题之一,因为它不仅决定了用户使用的方便程度,而且还能增加用户对软件的信赖程度。下面就如何利用VC++ 6.0编写带有多个工具条的应用程序的问题,谈一点自己的体会。
一、实现多个工具栏
通常在每个新建的工程项目中,AppWizard已经创建了一个默认的工具栏,其资源标识符ID为IDR_MAINFRAME,并且在主窗口类(如CMainFrame)内添加了一个CToolBar类型的变量m_wndToolBar。而工具栏的实际创建则是在主窗口类的OnCreate()成员函数内进行的。我们在实现多工具栏时,也应遵循这些原则,将多工具栏集中在主窗口类内,进行统一实现和管理,这样做比较方便。
利用MFC提供的CToolBar和CDialogBar类实现多个工具栏,步骤是比较容易的。主要分如下几个步骤:
(一)、创建工具栏资源模板
如果要添加由CToolBar实现的工具栏,在资源编辑器(Resource editor)中:
1、创建一个新的工具栏资源,分配资源标识符ID(例如IDR_MYTOOLBAR);
2、添加并编辑该工具栏上的各个工具按钮。
CToolBar工具栏的特点就是包含多个按钮,但如果要在工具栏上包含其他控件,如编辑框、组合式列表框等,则应使用CDialogBar来实现,步骤如下:
1、单击“Project / Add to Project / Components and Controls...”菜单项;
2、在出现的“Components and Controls Gallery”对话框中,双击“Visual C++ Components”;
3、在出现的列表中选中“Dialog bar”,然后单击“Insert”按钮;
4、最后单击“Close”关闭该对话框。
这样AppWizard就自动为我们在CMainFrame类内产生了该工具栏所需的代码,并为其建立了一个资源模板,其资源ID为CG_IDD_MYDIALOGBAR。
注意Dialog bar的资源是一个对话框模板,而不是工具栏模板。
按上述步骤,为要添加的每个工具栏均添加一个资源,并分配一个资源标识ID。至于如何在工具栏上添加按钮,以及如何建立每个按钮的响应函数,这里就不一一赘述了。
(二)、派生新的工具栏类
一般情况下,可以直接使用MFC中的CToolBar或CDialogBar类实现工具栏。但若要实现某些特定的操作,就必须派生自己的工具栏类。在派生时有一点小技巧,介绍如下:
在由ClassWizard或ClassView派生新类时,其中的基类列表框中找不到CToolBar和CDialogBar这两个类。此时可暂时先选CToolBarCtrl或CDialog作为基类,然后再将基类改为CToolBar或CDialogBar。下面仅介绍一下由CDialogBar派生出CMyDialogbar类的具体步骤:
1、在ClassView中右击第一个项目;
2、在出现的快捷菜单中单击“New Class...”;
3、在出现的“New Class”对话框中,输入新产生的类的名称“CMyDialogbar”;
4、选择基类为CDialog,并指定Dialog ID为在第(一)节中创建的Dialog bar的对话框资源的ID,如CG_IDD_MYDIALOGBAR。
5、打开MyDialogbar.h文件,将CMyDialogbar的基类修改为CDialogBar;
6、打开MyDialogbar.cpp文件,将所有出现的CDialog 均替换为CDialogBar。
注意CDialogBar的构造函数没有参数,所以还要将“: CDialogBar(CMyDialogbar::IDD, pParent)”中的参数删去,改为“: CDialogBar()”。
按类似方法可由CToolBar派生出CMyToolbar类。
然后,在CMainFrame类内添加这两个类的对象,如:
CMyToolbar m_myToolBar;
CMyDialogbar m_wndMyDialogBar;
//This variable is generated by AppWizard formerly. At this point, your work is //simply replacing it’s base class with CMyDialogbar .
为了允许程序中的所有代码均能访问工具栏,最好将这两个变量设为public。当然请不要忘了将上述两个类的头文件用#include指令包含进来。
(三)、创建工具栏控件
打开MainFrm.cpp文件,在CMainFrame类的OnCreate()成员函数内添加如下代码:
//Toolbar2:
if (!m_myToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD|CBRS_GRIPPER
| CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC|CBRS_TOP
,CRect(0, 0, 0, 0), ID_VIEW_MYTOOLBAR) ||
! m_myToolBar.LoadToolBar(IDR_MYTOOLBAR))
{
TRACE0("Failed to create toolbar\n");
return -1; // fail to create
}
m_myToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_myToolBar);
AppWizard已经为我们产生了创建CMyDialogbar 控件的代码,这里就不必添加了。
这里要注意如下几个问题:
1、工具栏的控件标识符ID问题
应当注意工具栏的控件标识符ID与它的资源标识符ID通常是不相同的。AppWizard为程序的第一个默认工具栏分配的控件标识符ID是ID_VIEW_TOOLBAR,我们还必须为每一个新添加的工具栏,分别分配一个控件标识符ID。例如为m_myToolBar工具栏分配控件ID的步骤如下:
(1)、打开Resource.h文件,添加如下行:
#define ID_VIEW_MYTOOLBAR 137
(2)、在CMainFrame类的OnCreate()函数中创建m_myToolBar工具栏控件时,将ID参数值ID_VIEW_MYTOOLBAR传递给CToolBar的CreateEx()函数,而不能使用CreateEx()的缺省参数(见上述代码)。
不需要再为m_wndMyDialogBar 分配ID,因为AppWizard已经为m_wndMyDialogBar分配了控件标识符CG_ID_VIEW_FILTERBAR。
CreateEx()的函数原型如下:
BOOL CreateEx(CWnd* pParentWnd, DWORD dwCtrlStyle = TBSTYLE_FLAT, DWORD dwStyle = WS_CHILD | WS_VISIBLE | CBRS_ALIGN_TOP, CRect rcBorders = CRect(0, 0, 0, 0), UINT nID = AFX_IDW_TOOLBAR);
由此可见ID_VIEW_TOOLBAR的值等于AFX_IDW_TOOLBAR。
2、程序启动时隐藏工具栏问题
有些工具栏在程序启动时,可能并不希望它显示出来,这时只需在CToolBar的CreateEx()函数中,将WS_VISIBLE参数去掉即可(见上述代码)。但对于m_wndMyDialogBar工具栏此法不灵,可在CMainFrame的OnCreate()函数中添加如下行:
m_wndMyDialogBar.ShowWindow(SW_HIDE);//this approach is effective for all toolbars.
(四)、添加消息映射条目
除了默认工具栏以外,我们还应为每一个新添加的工具栏分别添加两个消息映射条目:
1、打开MainFrm.cpp文件,在CMainFrame类的消息映射表中为m_myToolBar工具栏添加如下行:
ON_COMMAND_EX(ID_VIEW_MYTOOLBAR, OnBarCheck)
ON_UPDATE_COMMAND_UI(ID_VIEW_MYTOOLBAR, OnUpdateControlBarMenu)
AppWizard已经为m_wndMyDialogBar添加了如下行:
ON_COMMAND_EX(CG_ID_VIEW_MYDIALOGBAR, OnBarCheck)
ON_UPDATE_COMMAND_UI(CG_ID_VIEW_MYDIALOGBAR, OnUpdateControlBarMenu)
它们用于产生显示和隐藏该工具栏的处理代码。但不需要为默认工具栏添加这两个条目。
(五)、添加菜单项
切换到ResourceView,然后打开适当的菜单资源,
1、在菜单中添加菜单项“我的工具栏”,并指定ID为ID_VIEW_MYTOOLBAR;
2、在菜单中添加菜单项“我的对话框工具栏”,并指定ID为CG_ID_VIEW_FILTERBAR。
不需要为这两个菜单项建立处理函数,因为在第(四)节中添加的消息映射条目,已经为我们产生了处理代码。
单击这两个菜单项,即会显示或隐藏相应的工具栏。
至此,我们的程序中已经有了三个工具栏,它们的类型分别是:
CToolBar 、CMyToolBar和CMyDialogbar。
编译以后发现,这三个工具栏已经可以正常工作了。
二、编写类似Word 97的工具栏快捷菜单问题
即当用户右击主窗口中的菜单栏、工具栏时,显示“工具栏”子菜单。
自然要用OnContextMenu(CWnd* pWnd, CPoint point)函数,但调试跟踪发现,右击菜单栏根本无法进入该函数,即此函数对右击菜单栏无反应。故可采用OnNcRButtonUp( UINT nHitTest, CPoint point )函数,效果很好,举例如下。
假设工具栏子菜单的菜单资源已经建立,其资源ID是IDR_MENU_TOOLBAR,包含菜单项“默认工具栏”、“我的工具栏”和“我的对话框工具栏”;三个工具栏的对象均是CMainFrame类内的变量:
CToolBar m_wndToolBar;
CMyToolbar m_myToolBar;
CMyDialogbar m_wndMyDialogBar;
(一)、则首先按如下步骤实现右击工具栏时的响应:
1、在CMainFrame类内越位OnContextMenu()函数,响应WM_CONTEXTMENU消息;
2、在该函数内加入如下代码:
HWND h0= m_wndToolBar.GetParent()->m_hWnd;
HWND h1= m_myToolBar.GetParent()->m_hWnd;
HWND h2= m_wndMyDialogBar.GetParent()->m_hWnd;
if(h0==pWnd->m_hWnd || h1==pWnd->m_hWnd || h2==pWnd->m_hWnd)
{
CMenu mymenu;
mymenu.LoadMenu(IDR_MENU_TOOLBAR);
CMenu *submenu=mymenu.GetSubMenu(0);
submenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON,
point.x,point.y,this);
}
这里要注意OnContextMenu()函数的第一个参数pWnd既不是主框架窗口,也不是工具栏窗口本身,而是工具栏的父窗口的指针(它可能就是Dock窗口)。另外,不要在工具栏的类内再响应WM_CONTEXTMENU消息了,比如不要在CMyDialogbar类内越位OnContextMenu()函数。否则右击该工具栏时不会再进入CMainFrame类的OnContextMenu()函数。
通过试验发现,有趣的是多个工具栏相邻地靠在一起时,其父窗口是同一个(即句柄是相等的)。但也仍应分别进行判断(如上例),防止用户把它们拉开,不相邻时,它们的父窗口就不同了。
上述方法效果较好,但是如果把工具栏拖到屏幕中间处于漂浮状态时,对鼠标右键就没有反应了,解决方法请参见下面第(三)节介绍。
(二)、但是要实现对右击菜单栏的响应,还需按如下步骤进行:
1、打开MainFrm.h文件,在CMainFrame类中添加函数原型:
afx_msg void OnNcRButtonUp( UINT nHitTest, CPoint point );
2、在CMainFrame的消息映射表中添加:
ON_WM_NCRBUTTONUP()
3、在OnNcRButtonUp()函数中添加如下代码:
if(nHitTest==HTMENU)
{
CMenu mymenu;
mymenu.LoadMenu(IDR_MENU_TOOLBAR);
CMenu *submenu=mymenu.GetSubMenu(0);
submenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON,
point.x,point.y,this);
}
(三)、解决工具条漂浮时响应右击的办法
在工具条的类中也越位OnContextMenu()函数,即也响应WM_CONTEXTMENU消息,然后在OnContextMenu()中显示快捷菜单时,在submenu->TrackPopupMenu()中不要传递this指针,而是主窗口CMainFrame的指针,否则菜单项左边的选中标记显示不出来。
例如每个工具条类的OnContextMenu()函数中添加如下代码:
CMenu mymenu;
mymenu.LoadMenu(IDR_MENU_TOOLBAR);
CMenu *submenu=mymenu.GetSubMenu(0);
submenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON,
point.x,point.y, AfxGetMainWnd( ));
这样做每个工具条就必须使用自己派生的类(包括默认工具栏),派生自CToolBar或CDialogBar。因此将工具条变量的声明修改如下:
CMyToolbar m_wndToolBar;
CMyToolbar m_myToolBar;
CMyDialogbar m_wndMyDialogBar;
但这时CMainFrame的OnContextMenu()仍然需要。因为调试时观察发现,虽然此时右击工具栏按钮区域,不会进入CMainFrame的OnContextMenu();但是CToolBar和其派生类在其工具条不漂浮时,右击按钮以外的工具栏区域,只能进入CMainFrame的OnContextMenu(),并且此时pWnd指向工具栏的父窗口,因此反而不能进入他自己的OnContextMenu()。
至此,右击显示“工具栏”子菜单问题就完全解决了。
三、显示按钮文字
由CToolBar或CToolBar派生类所创建的工具条,还可以为每一个按钮在其下方或右侧显示标签文字。因此可以建立一个菜单项“显示文字”,让用户在显示和隐藏标签文字之间切换。
要实现上述功能,首先在CMainFrame的OnCreate()函数内调用CToolbar的SetButtonText()成员函数即可,随后还要调用它的SetSizes()成员函数。并且还要在响应“显示文字”菜单项的消息处理函数内,调用SetSizes()函数调整按钮尺寸,以便显示或遮挡按钮下面的文字。下面假设每个按钮图形的尺寸为24x23,具体实现步骤如下:
1、打开MainFrm.cpp文件,在OnCreate()函数内的末尾的return 0;语句之前插入如下代码:
char* stext1[]=
{
"老虎",
"鹦鹉",
"",
"鲜花",
"水果"
};
char* stext2[]=
{
"李嘉新",
"朱英",
"",
"小龙女",
"CoCo"
};
for(int i=0;i<5;++i)
{
m_wndToolBar.SetButtonText(i,stext1[i]);
m_myToolBar.SetButtonText(i,stext2[i]);
}
m_wndToolBar.SetSizes(CSize(31,29),CSize(24,23));
m_myToolBar.SetSizes(CSize(31,29),CSize(24,23));
现在文字是被遮挡不可见的,按钮的尺寸是31x29(必须比图形略大一点)。
2、在CMainFrame 类内建立“显示文字”菜单项的响应函数:
void CMainFrame::OnViewText();
void CMainFrame::OnUpdateViewText(CCmdUI* pCmdUI);
3、在OnUpdateViewText(CCmdUI* pCmdUI)函数内添加如下代码:
pCmdUI->SetCheck(m_bTextVisible);
4、在OnViewText()函数内添加如下代码:
m_bTextVisible=!m_bTextVisible;
if(m_bTextVisible)
{//Display text:
m_wndToolBar.SetSizes(CSize(32,43),CSize(24,25));
m_myToolBar.SetSizes(CSize(43,43),CSize(24,25));}
else
{//Make text invisible:
m_wndToolBar.SetSizes(CSize(31,29),CSize(24,23));
m_myToolBar.SetSizes(CSize(31,29),CSize(24,23));
}
//Resize the toolbar:
if(m_wndToolBar.IsWindowVisible( ) && m_wndToolBar.IsFloating( ))
{
PostMessage(WM_COMMAND,ID_VIEW_TOOLBAR);
PostMessage(WM_COMMAND,ID_VIEW_TOOLBAR);
}
if(m_myToolBar.IsWindowVisible( ) && m_myToolBar.IsFloating( ))
{
PostMessage(WM_COMMAND, ID_VIEW_MYTOOLBAR);
PostMessage(WM_COMMAND, ID_VIEW_MYTOOLBAR);
}
if(!m_wndToolBar.IsFloating( ))
{
PostMessage(WM_COMMAND,ID_VIEW_TOOLBAR);
PostMessage(WM_COMMAND,ID_VIEW_TOOLBAR);
return;
}
if(!m_myToolBar.IsFloating( ))
{
PostMessage(WM_COMMAND, ID_VIEW_MYTOOLBAR);
PostMessage(WM_COMMAND, ID_VIEW_MYTOOLBAR);
return;
}
5、在CMainFrame 类中添加一个类型为BOOL的变量m_bTextVisible,并将其初始化为FALSE。
现解说如下:
1、调用SetButtonText()时,按钮的序号 (index)一定要包括工具条上的分隔条(|),其文字用一个空字符串""表示即可(请参见上面的代码)。
2、显示文字时按钮的尺寸,由编程者试验确定。但一个原则就是按钮的宽度和高度必须至少比按钮图形的宽度和高度分别大7和6。
3、调用SetSizes()后,虽然按钮的高度改变了,但是工具条的高度却并不能随之立刻变过来。只有当用户单击该工具条的菜单项或改变主窗口大小时,工具条高度才改变,十分头疼。经反复摸索,我采用了如下的方法,不必改变主窗口大小(参见上面的代码):
PostMessage(WM_COMMAND, ID_VIEW_MYTOOLBAR);
PostMessage(WM_COMMAND, ID_VIEW_MYTOOLBAR);
即让工具栏先隐藏然后恢复为显示状态,或先显示再恢复为隐藏状态,导致工具栏窗口的刷新。这样做仅会引起工具栏抖动,避免了因改变主窗口尺寸而引起的主窗口抖动。
即使有多个工具条,也只需随意选一个按上述处理,所有的工具条都会同时被刷新。对所有停靠(即不飘浮的)的工具条都有作用(即使不相邻)。
但这样处理,对飘浮的工具条却无效,反之若被处理的工具条飘浮,则对停靠的也就无效了。因此,稳妥的办法,就是对所有可见的飘浮工具条分别按上述方法处理一下,然后任选一个不飘浮的工具条(可见与否没关系)处理一下(请参见上面的示例代码)。实际窗口效果如图3所示。
四、显示256种颜色的按钮
通常应用程序的工具按钮都是16色的,但是实际上也可以根据需要使用256种色(8位色),甚至是24位的颜色。尺寸也不一定是16x15,也可以是任意大的尺寸。
实现方法就是利用CImageList类装载24位颜色或256色的位图(共需三个位图),然后分别调用CToolbar类的成员函数SetImageList()、SetDisabledImageList()和SetHotImageList()。
(一)、制作位图
需要为工具条上的每个按钮准备三个位图,分别表示三种状态:正常、加亮(hot)和变灰(disabled),其中后两个也可省略不要。加亮就是当鼠标指针指向该按钮时的状态,变灰就是当该按钮被禁止时的状态。现在假设我们要实现的按钮图形尺寸是24x23。并假设已经为工具栏制作了工具栏模板资源。
制作位图可以用任何图形处理工具,如photoshop等。首先可以制作表示正常状态的位图:
1、分别为每个按钮制作一个24x23的图形;
2、将制作好的按钮图形左右排列,相邻地组合在一块,形成一个位图(如图1和图2);
3、将位图以24位颜色的格式保存为bmp文件,比如toolbarimg0.bmp和toolbarimg.bmp文件。
制作时需要注意的是,按钮图形在位图中的排列顺序,一定要与该工具栏资源模板中的按钮顺序完全相同,一一对应。这里的资源模板仍然是需要的,因为这样我们就不必自己编写构造按钮的具体代码,而由主框架(Framework)去完成。
下一步就是制作加亮和变灰的位图:
1、将上面制作的正常状态的位图,经过纹理化处理(比如用照片编辑器),然后以256色的格式另存为另外一个bmp文件,比如toolbardisabledimg0.bmp和toolbardisabledimg.bmp文件,作为禁止状态的位图,没有必要为其保存24位格式;
2、同样,将上面制作的正常状态的位图,经过加亮处理,然后以256色的格式另存为另外一个bmp文件,比如toolbarhotimg0.bmp和toolbarhotimg.bmp文件,作为加亮状态的位图。
3、将上述制作的六个位图,全部import到工程中来,分别分配它们的资源ID为:
IDB_IMG0
IDB_DISABLED_IMG0
IDB_HOT_IMG0
IDB_IMG
IDB_DISABLED_IMG
IDB_HOT_IMG
注意:对于24位位图,在import时会出现一个警告对话框,可以不必理会它。
(二)、添加实现代码
1、在CMainFrame类内添加如下六个变量(可以设为protected):
CImageList m_img1;
CImageList m_img2;
CImageList m_disabledimg1;
CImageList m_disabledimg2;
CImageList m_hotimg1;
CImageList m_hotimg2;
2、在CMainFrame类的OnCreate()函数末尾的return 0;语句之前插入如下代码:
//Load imagelists for toolbar1:
CBitmap bm;
bm.LoadBitmap(IDB_IMG0);
m_img1.Create(24,23,ILC_COLOR24,4,0);
m_img1.Add(&bm,RGB(0,0,0));
bm.DeleteObject( );
bm.LoadBitmap(IDB_DISABLED_IMG0);
m_disabledimg1.Create(24,23,ILC_COLOR8,4,0);
m_disabledimg1.Add(&bm,RGB(0,0,0));
bm.DeleteObject( );
bm.LoadBitmap(IDB_HOT_IMG0);
m_hotimg1.Create(24,23,ILC_COLOR8,4,0);
m_hotimg1.Add(&bm,RGB(0,0,0));
//Set imagelists for toolbar1:
CToolBarCtrl& ctl1=m_wndToolBar.GetToolBarCtrl( );
ctl1.SetImageList(&m_img1);
ctl1.SetDisabledImageList(&m_disabledimg1);
ctl1.SetHotImageList(&m_hotimg1);
//Load imagelists for toolbar2:
bm.DeleteObject( );
bm.LoadBitmap(IDB_IMG);
m_img2.Create(24,23,ILC_COLOR24,4,0);
m_img2.Add(&bm,RGB(0,0,0));
bm.DeleteObject( );
bm.LoadBitmap(IDB_DISABLED_IMG);
m_disabledimg2.Create(24,23,ILC_COLOR8,4,0);
m_disabledimg2.Add(&bm,RGB(0,0,0));
bm.DeleteObject( );
bm.LoadBitmap(IDB_HOT_IMG);
m_hotimg2.Create(24,23,ILC_COLOR8,4,0);
m_hotimg2.Add(&bm,RGB(0,0,0));
//Set imagelists for toolbar2:
CToolBarCtrl& ctl2=m_myToolBar.GetToolBarCtrl( );
ctl2.SetImageList(&m_img2);
ctl2.SetDisabledImageList(&m_disabledimg2);
ctl2.SetHotImageList(&m_hotimg2);
这里所采用的关键技术就是在调用CImageList类的Create()函数时,将其第二个参数指定为ILC_COLOR24或ILC_COLOR8,我们即可把24位或8位的位图加载进来(实际窗口效果参见图3)。这种方法在显示TreeView和ListView中的图标时同样适用。
五、小结
本文介绍了多工具条的编程方法,对于提高程序的易用性和商业化水平,均具有较高的实用价值。笔者在本人设计开发的软件中使用这些技术,均得到用户的一致好评。示例代码均在VC++ 6.0和Win 98下调试通过。文中一定还存在一些不完善的地方,希望朋友们提出改进意见和建议,并希望本文能起到抛砖引玉的效果。
下面把本文中的重点在此小结一下(此段可省去不要):
1、若想让工具栏在程序启动时不显示,只需在调用Create()创建该工具条时,不指定WS_VISIBLE风格即可。或干脆调用ShowWindow(SW_HIDE)。
2、创建多个工具条时,都应该在主窗口的OnCreate()函数中进行。
3、除了AppWizard自动创建的第一个缺省工具条的ID默认为ID_VIEW_TOOLBAR,其他工具条在创建时都要为它另外指定ID。
4、除了缺省工具条以外,还应为每一个工具条在MainFrm.cpp文件中添加如下两个
消息映射条目(假设该工具条的ID为ID_TOOLBAR),以便响应菜单项:
ON_COMMAND_EX(ID_MYTOOLBAR, OnBarCheck)
ON_UPDATE_COMMAND_UI(ID_MYTOOLBAR, OnUpdateControlBarMenu)