Windows (Windows95或者以上版本) 提供了系列通用控制窗口,其中包括工具条(ToolBar)、状态栏(StatusBar)、工具条提示窗口(ToolTip)。
Windows在一个DLL加载时注册个控制窗口的“窗口类”。例如,工具条的“窗口类”是 “ToolbarWindow32”,状态栏的“窗口类”是“msctls_statusbar32”,工具条提示窗口的“窗口类”是 “tooltips_class32”。为了保证该DLL被加载,使用控制“窗口类”前,应该首先调用函数InitCommonControl。MFC在窗口注册函数AfxDeferRegisterClass中实现了这一点。见2.2.1节MFC下窗口的注册。
创建通用控制窗口,可以使用专门的创建函数,如创建工具条的函数::CreateToolBarEx,创建状态栏的函数::CreateStatusBarEx。也可以调用窗口创建函数::CreateWindowEx,但是需要指定预定义的“窗口类”,必要的话还要其他步骤,如使用“ToolbarWindow32”“窗口类”创建工具栏后,还需要在工具栏中添加或者插入按钮。
一般,通用控制可以指定控制窗口风格(Style)。例如,具备风格CCS_TOP,表示该控制窗口放到父窗口客户区的顶部,具备CCS_BOTTOM,表示该控制窗口在客户区的底部。具体的控制窗口类可以有特别的适合于自己的风格,例如, TTS_ALWAYSTIP表示只要光标落在工具栏的按钮上,ToolTip窗口不论激活与否都会显示出来。
每一控制窗口类都有自己的窗口过程来处理自己的窗口消息,实现特定的功能。控制窗口类的窗口过程由Windows提供。
工具条的窗口过程处理了必要的消息,提供了标准工具条的功能,例如,工具条对客户化特征提供内在的支持,用户可以通过一个客户化对话框来添加、修改、删除或者重新安排工具条按钮。这些特征是否可以被用户所用或者用到什么地步是可以由程序控制的。
工具条的窗口过程将自动设置工具条的尺寸大小和位置,如果指定了控制窗口风格CCS_TOP或者 CCS_BOTTOM,则窗口过程把工具条放到父窗口客户区的顶部或者底部。窗口过程任何时候只要收到WM_SIZE或者TB_AUTOSIZE消息就自动地调整工具条的大小和位置。
工具条的按钮被选中后,会产生一个命令消息,它的窗口过程把该消息送给父窗口的窗口过程处理。
工具条中的按钮并不以子窗口的形式出现,而是以字符或者位图按钮的方式显示,每个按钮大小相同,缺省是24*22个像素。每个按钮都有一个索引,索引编号从0开始。每个按钮包括如下属性:
按钮的字符串索引,位图索引,风格,状态,命令ID
按钮可以有两种风格TBSTYLE_BUTTON和TBSTYLE_CHECK,前者像一个标准按钮那样响应用户的按击,后者响应每一次按击,在按下和跳起两种状态之间切换。按钮响应用户的动作,给父窗口发送一个包含了该按钮对应命令ID的命令消息。一般一个按钮的命令ID对应一个菜单项。
工具条维护两个列表,分别用来存放工具条按钮使用的字符串或者位图,列表中的位图或者字符串从0开始编号,编号和按钮的索引相对应。
工具条可以是Dockable(泊位)或者Floatable(漂浮)的。
工具条可以有TBSTYLE_TOOLTIPS风格,如果具有这种风格,则创建和管理一个Tooltip控制,这是一个小的弹出式窗口,用来显示描述按钮的文本,平时该窗口隐藏,当鼠标落到按钮上面并停留约一秒后才弹出,在鼠标附近显示。
由于Tooltip窗口平时是隐藏的,所以不能接收鼠标消息来决定何时显示本窗口。这样,接收鼠标的窗口必须把鼠标消息送给Tooltip窗口,这是通过给Tooptip窗口发送消息TTM_RELAYEVENT来实现的。
状态栏类似于工具条,有自己的窗口过程,可以泊位、漂浮。不过,习惯上状态栏都位于屏幕底部。每个状态条分成若干格(Status bar panes),每格从0开始编号,编号作为格的索引。每一个格,如同工具条的按钮一样,并不是一个Windows窗口。
MFC使用CToolBarCtrl、CStatusBarCtrl和CToolTipCtrl窗口类分别对工具条、状态栏、Tooltip控制窗口进行了封装。
但是,直接使用这些类还不是很方便。MFC提供了CToolBar、CStatusBar来处理状态栏和工具条,CToolBar、CStatusBar功能更强大,灵活。这两个类都派生于CControlBar。
在MFC下,建议这些控制条子窗口ID介于AFX_IDW_TOOLBARFIRST(0xE800)和AFX_IDW_CONTROLBAR_LAST(0Xe8FF)之间。这256个ID中,前32个又有其特殊性,用于MFC的打印预览中。
CControlBar派生于CWnd类,是控制条窗口类的基类,它派生出CToolBar、CStatusBar、CDockBar、CDialogBar、COleResizeBar类。CControlBar实现了以下功能:
CStatusBar和CControlBar一方面建立在CControlBar的基础之上,另一方面以 Windows的通用控制状态栏和工具条为基础。它们继承了CControlBar类的特性,但是所封装的窗口句柄是相应的Windows控制窗口的句柄,如同CFormView继承了CSrcollView的视类特性,但是其窗口句柄是无模式对话框窗口句柄一样。
典型地,如果在使用AppWizard生成应用程序时,指定了要求工具条和状态栏的支持,则在主边框窗口的OnCreate函数中包含一段如下的代码,用来创建工具条、状态栏和设置一些特性。
//创建工具栏
if (!m_wndToolBar.Create(this) ||!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("Failed to create toolbar/n");
return -1; // fail to create
}
//创建状态栏
if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)))
{
TRACE0("Failed to create status bar/n");
return -1; // fail to create
}
// TODO: Remove this if you don't want tool tips or a resizeable toolbar
//对工具栏设置Tooltip特征
m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() |
CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);
//使得工具栏可以泊位在边框窗口
// TODO: Delete these three lines if you don't want the toolbar to
// be dockable
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
工具条除了Tooltip,Resizeable,Dockable特性外,还可以是Floatable。应用程序可以使用CFrameWnd::SaveBarState保存边框窗口的控制条的有关信息到INI文件或者Windows Register库,使用LoadBarSate从INI文件或者Register库中读取有关信息并恢复各个控制条的设置。
下文,将讨论工具条等的创建、销毁,从中分析CControlBar和派生类的关系,讨论CControlBar如何实现共性,如何支持派生类的特定要求,派生类又如何实现自己的特定需求等。
创建工具条、状态条、对话框工具栏的方法是不同的,所以必须给每个派生类CToolBar、CStatusBar、CDialogBar设计和实现自己的窗口创建函数Create。但是,它们是也是有共性的,共性由CControlBar的PreCreateWindow处理。在窗口创建之后,各个派生类都要进行的处理(共性)由CControlBar的OnCreate完成,特别的处理通过派生类的OnNcCreate完成。
首先,讨论CControlBar 类的PreCreateWindow的实现。
BOOL CControlBar::PreCreateWindow(CREATESTRUCT& cs)
{
if (!CWnd::PreCreateWindow(cs))
return FALSE;
//修改窗口风格,强制适用clipsliblings,以防重复绘制
cs.style |= WS_CLIPSIBLINGS;
//default border style translation for Win4
//(you can turn off this translation by setting CBRS_BORDER_3D)
if (afxData.bWin4 && (m_dwStyle & CBRS_BORDER_3D) == 0)
{
DWORD dwNewStyle = 0;
switch (m_dwStyle & (CBRS_BORDER_ANY|CBRS_ALIGN_ANY))
{
case CBRS_LEFT: //控制条在边框窗口的左边显示
dwNewStyle = CBRS_BORDER_TOP|CBRS_BORDER_BOTTOM;
break;
case CBRS_TOP://控制条在边框窗口的顶部显示
dwNewStyle = CBRS_BORDER_TOP;
break;
case CBRS_RIGHT://控制条在边框窗口的右边显示
dwNewStyle = CBRS_BORDER_TOP|CBRS_BORDER_BOTTOM;
break;
case CBRS_BOTTOM://控制条在边框窗口的底部显示
dwNewStyle = CBRS_BORDER_BOTTOM;
break;
}
// set new style if it matched one of the predefined border types
if (dwNewStyle != 0)
{
m_dwStyle &= ~(CBRS_BORDER_ANY);
m_dwStyle |= (dwNewStyle | CBRS_BORDER_3D);
}
}
return TRUE;
}
其中,afxData是一个全局变量,MFC用它来记录系统信息,如版本信息等。这里afxData.bWin4表示Windows版本是否高于4.0。
CToolBar的PreCreateWindow函数修改了窗口风格,也修改状态栏、工具栏等的CBRS_风格。CBRS_风格的改变不会影响窗口风格。因为这些CBRS_风格被保存在成员变量m_dwStyle中。
除了上述在程序中用到的影响工具条、状态栏等显示位置的CBRS_风格外,还有和泊位相关的CBRS_风格,CBRS_ALIGN_LEFT、 CBRS_ALIGN_RIGHT、CBRS_ALIGN_BOTTOM、CBRS_ALIGN_TOP、CBRS_ALIGN_ANY,分别表示工具条可以在停泊在边框窗口的左边、右边、底部、顶部或者所有这些位置;和漂浮相关的CBRS_风格CBRS_FLOAT_MULTI,表示多个工具条可以漂浮在一个微型边框窗口中;和Tooltips相关的CBRS_风格CBRS_TOOLTIPS和CBRS_FLYBY。
派生类如果没有特别的要求,可以不覆盖PreCreateWindow函数。CStatusBar因为有更具体和特殊的风格要求,所以它覆盖了PreCreateWindow。CStatusBar的覆盖实现调用了CControlBar的实现。
派生类也可以在覆盖实现中修改PreCreateWindow参数cs,改变窗口风格;修改m_dwStyle,改变CBRS_风格。
CControlBar派生类实现了自己的窗口创建函数Create,CControlBar的PreCreateWindow被派生类的Create函数直接或者间接地调用。以CToolBar为例讨论窗口创建函数和创建过程。
Create函数实现如下:
BOOL CToolBar::Create(CWnd* pParentWnd, DWORD dwStyle, UINT nID)
{
ASSERT_VALID(pParentWnd); // must have a parent
ASSERT (!((dwStyle & CBRS_SIZE_FIXED) &&
(dwStyle & CBRS_SIZE_DYNAMIC)));
// 保存dwStyle指定的CBRS_风格
m_dwStyle = dwStyle;
if (nID == AFX_IDW_TOOLBAR)
m_dwStyle |= CBRS_HIDE_INPLACE;
//去掉参数dwStyle包含的CBRS_风格
dwStyle &= ~CBRS_ALL;
//设置窗口风格
dwStyle |=
CCS_NOPARENTALIGN|CCS_NOMOVEY|CCS_NODIVIDER|CCS_NORESIZE;
//初始化通用控制,可以导致InitCommonControl的调用
VERIFY(AfxDeferRegisterClass(AFX_WNDCOMMCTLS_REG));
//创建窗口,将调用PreCreateWindow,OnCreate, OnNcCreate等
CRect rect; rect.SetRectEmpty();
if (!CWnd::Create(TOOLBARCLASSNAME, NULL, dwStyle,
rect, pParentWnd, nID))
return FALSE;
// Note: Parent must resize itself for control bar to be resized
return TRUE;
}
其中:
Create 函数的参数1表示工具条的父窗口。参数2指定窗口风格和CBRS_风格,缺省值为 WS_CHILD | WS_VISIBLE | CBRS_TOP,其中WS_CHILD和WS_VISIBLE是窗口风格,CBRS_TOP是CBRS_风格。参数3指定工具条ID,缺省值为 AFX_IDW_TOOLBAR(0X0E800或者59392)。如果还有多个工具栏要显示,在创建它们时则必须给每个工具栏指明ID。
首先,Create函数把参数2(dwStyle)指定的窗口风格和CBRS_风格分离出来,窗口风格保留在dwStyle中,CBRS_风格保存到成员变量m_dwStyle中。CToolBar::PreCreateWindow将进一步修改这些风格。
接着,Create函数调用了函数AfxDeferRegisterClass。它如果没有注册TOOLBARCLASSNAME表示的“窗口类”,就注册该类;否则,返回TRUE,表示已经注册。TOOLBARCLASSNAME表示的字符串是“ToolbarWindow32”,即“窗口类”名称。
然后,调用CWnd::Create(7个参数)使用“ToolbarWindow32”“窗口类”创建工具栏。
Create在创建窗口的过程中,用MFC的标准窗口过程取代原来的窗口过程,如同CFormView和CDialog窗口创建时窗口过程被取代一样,并发送WM_CREATE和WM_NCCREATE消息。
至于添加向工具栏添加按钮,则由函数LoadToolBar完成。在分析LoadToolBar函数之前,先讨论OnCreate、OnNcCreate等函数。
CControlBar提供了消息处理函数OnCreate来处理WM_CREATE消息。
int CControlBar::OnCreate(LPCREATESTRUCT lpcs)
{
//调用基类的实现
if (CWnd::OnCreate(lpcs) == -1)
return -1;
//针对工具栏,是否有Tooltip特性
if (m_dwStyle & CBRS_TOOLTIPS)
EnableToolTips();
//得到父窗口,并添加自身到其控制条列表中
CFrameWnd *pFrameWnd = (CFrameWnd*)GetParent();
if (pFrameWnd->IsFrameWnd())
{
m_pDockSite = pFrameWnd;
m_pDockSite->AddControlBar(this);
}
return 0;
}
如果需要支持Tooltips,则OnCreate调用EnableTooltips。
m_pDockSite是CControlBar的和泊位相关的成员变量,这里把它初始化为拥有工具栏的父边框窗口,该边框窗口把控制条加入其控制条列表m_listControlBars中。
在处理WM_CREATE之前,派生类先处理消息WM_NCCREAE。例如,CToolBar覆盖了OnNcCreate函数。
CToolBar对WM_NCCREATE消息的处理如下:
BOOL CToolBar::OnNcCreate(LPCREATESTRUCT lpCreateStruct)
{
if (!CControlBar::OnNcCreate(lpCreateStruct))
return FALSE;
// if the owner was set before the toolbar was created, set it now
if (m_hWndOwner != NULL)
DefWindowProc(TB_SETPARENT, (WPARAM)m_hWndOwner, 0);
DefWindowProc(TB_BUTTONSTRUCTSIZE, (WPARAM)sizeof(TBBUTTON), 0);
return TRUE;
}
CToolBar 覆盖CcontrolBar的该函数用来设置工具条的所属窗口和描述工具条按钮结构的大小,这两个动作都是通过给工具条窗口发送消息来实现的。因为这些消息被送给控制窗口类的窗口过程(Windows提供的)来处理,所以直接调用DefWindowProc,省却了消息发送的过程。
在控制窗口创建之后,对于工具条来说,下一步就是向工具栏添加按钮。
通过函数LoadToolBar完成向工具栏添加按钮的任务,其实现如下:
BOOL CToolBar::LoadToolBar(LPCTSTR lpszResourceName)
{
ASSERT_VALID(this);
ASSERT(lpszResourceName != NULL);
//查找并确认按钮位图、字符串等资源的位置
HINSTANCE hInst = AfxFindResourceHandle(lpszResourceName, RT_TOOLBAR);
HRSRC hRsrc = ::FindResource(hInst, lpszResourceName, RT_TOOLBAR);
if (hRsrc == NULL)
return FALSE;
//锁定资源
HGLOBAL hGlobal = LoadResource(hInst, hRsrc);
if (hGlobal == NULL)
return FALSE;
CToolBarData* pData = (CToolBarData*)LockResource(hGlobal);
if (pData == NULL)
return FALSE;
ASSERT(pData->wVersion == 1);
//复制与各个位图对应的命令ID到数组pItem
UINT* pItems = new UINT[pData->wItemCount];
for (int i = 0; i < pData->wItemCount; i++)
pItems[i] = pData->items()[i];
//添加按钮到工具栏,指定各个按钮对应的ID
BOOL bResult = SetButtons(pItems, pData->wItemCount);
delete[] pItems;
//设置按钮的位图
if (bResult)
{
// set new sizes of the buttons
CSize sizeimage(pData->wWidth, pData->wHeight);
CSize sizeButton(pData->wWidth + 7, pData->wHeight + 7);
SetSizes(sizeButton, sizeimage);
// load bitmap now that sizes are known by the toolbar control
bResult = LoadBitmap(lpszResourceName);
}
UnlockResource(hGlobal);
FreeResource(hGlobal);
return bResult;
}
LoadToolBar函数的参数指定了资源。ToolBar资源的类型是RT_TOOLBAR,ToolBar位图资源的类型是RT_BITMAP。
在RT_TOOLBAR类型的资源读入内存之后,可以用CToolBarData结构描述。一个这样的结构包括了ToolBar资源的如下信息:
工具条位图的版本,宽度,高度,个数,各个位图对应的命令ID。
然后,LoadToolBar把这些命令ID被复制到数组pItem中;根据位图宽度、高度形成按钮尺寸sizeButton和位图尺寸sizeimage。
接着,调用SetBottons添加按钮到工具栏,把各个按钮和命令ID对应起来;调用SetSizes设置按钮和位图的尺寸大小;调用 LoadBitmap添加或者取代工具条的位图列表。这些动作都是调用工具栏“窗口类”的窗口过程完成的。例如,SetButtons的实现:
BOOL CToolBar::SetButtons(const UINT* lpIDArray, int nIDCount)
{
ASSERT_VALID(this);
ASSERT(nIDCount >= 1); // must be at least one of them
ASSERT(lpIDArray == NULL ||
AfxIsValidAddress(lpIDArray, sizeof(UINT) * nIDCount, FALSE));
//首先,删除工具条中现有的按钮
int nCount = (int)DefWindowProc(TB_BUTTONCOUNT, 0, 0);
while (nCount--)
VERIFY(DefWindowProc(TB_DELETEBUTTON, 0, 0));
if (lpIDArray != NULL)//命令ID数组非空
{
//添加新按钮
TBBUTTON button; memset(&button, 0, sizeof(TBBUTTON));
int iimage = 0;
for (int i = 0; i < nIDCount; i++)
{
button.fsState = TBSTATE_ENABLED;
if ((button.idCommand = *lpIDArray++) == 0)
{
//按钮之间分隔
button.fsStyle = TBSTYLE_SEP;
//按钮之间隔8个像素
button.iBitmap = 8;
}
else
{
//有位图和命令ID的按钮
button.fsStyle = TBSTYLE_BUTTON;
button.iBitmap = iimage++;//设置位图索引
}
//添加按钮
if (!DefWindowProc(TB_ADDBUTTONS, 1, (LPARAM)&button))
return FALSE;
}
}
else//命令ID数组空,添加空按钮
{
TBBUTTON button; memset(&button, 0, sizeof(TBBUTTON));
button.fsState = TBSTATE_ENABLED;
for (int i = 0; i < nIDCount; i++)
{
ASSERT(button.fsStyle == TBSTYLE_BUTTON);
if (!DefWindowProc(TB_ADDBUTTONS, 1, (LPARAM)&button))
return FALSE;
}
}
//记录按钮个数到成员变量m_nCount中
m_nCount = (int)DefWindowProc(TB_BUTTONCOUNT, 0, 0);
//稍后放置按钮
m_bDelayedButtonLayout = TRUE;
return TRUE;
}
函数的参数1是一个数组,数组的各个元素就是命令ID;参数2是按钮的个数。首先,SetButtons删除工具条原来的按钮;然后,添加新的按钮,若命令ID数组非空,则把每一个按钮和命令ID对应并分配位图索引,否则设置空按钮并返回FALSE;最后,记录按钮个数。
从SetButtons的实现可以看出,对工具条的所有操作都是通过工具条“窗口类”的窗口过程完成的,SetSizes、LoadBitmap也是如此,这里不作讨论。
至此,分析了MFC创建工具条窗口的过程。对于状态栏和对话框工具栏有类似的步骤,但也有不同之处。
CStatusBar的Create使用“msctls_statusbar32”“窗口类”创建状态栏,窗口 ID为AFX_IDW_STATUS_BAR(0XE801),然后通过成员函数SetIndictors给状态栏分格,类似于给工具条添加按钮的过程,它实际上是通过状态栏“窗口类”的窗口过程完成的。
CDialogBar的Create使用CreateDlg创建对话框工具栏,类似于CFormView的过程。在工具栏窗口创建之后,要添加到父窗口的工具栏列表中,这通过CControlBar::OnCreate完成。这样创建的结果导致窗口过程使用MFC的统一的窗口过程,相应“窗口类”的窗口过程也将在缺省处理中被调用,这一点如同CFormView和CDialog中所描述的。在初始化对话框的时候完成了各个控制按钮的添加。
CStatusBar和CdialogBar都没有处理消息WM_NCCREATE。
关于CStautsBar和CDialogBar创建过程的具体实现,这里不作详细讨论了。
描述了控制条的创建,顺便考察其销毁的设计。
工具条、状态栏等这些控制窗口都要使用DestroyWindow来销毁,所有有关操作集中由CControlBar处理。CControlBar覆盖了虚拟函数DestroyWindow、PostNcDestroy和消息处理函数OnDestroy。
当然,各个派生类的虚拟析构函数被实现。如果成员变量m_bAutoDelete为TRUE,则动态创建的MFC窗口将自动销毁。
工具条等控制条是作为一个子窗口在父边框窗口内显示的。为了处理控制条的布置(Layout),首先需要计算出控制条的尺寸大小,这个工作被委派给工具条等控制窗口自己来完成。为此,CControlBar提供了两个函数来达到这个目的:CalcFixLayout,CalcDynamicLayout。这两个函数都是虚拟函数。各个派生类都覆盖了这两个或者其中一个函数,用来计算自身的尺寸大小。这些计算比较琐碎,在此不作详细讨论。其次,在父窗口位置或者大小变化时,控制条的大小和位置要作相应的调整。
下面,描述MFC确定或者更新工具条、状态栏等位置的步骤:
(1)边框窗口在必要的时候调用虚拟函数RecalcLayout来重新放置它的控制条和客户窗口,例如在创建窗口时、响应消息WM_SIZE时(见5.3.3.5节)边框窗口的初始化)。
(2)CFrameWnd::RecalcLayout调用CWnd的成员函数RepositionBars完成控制条窗口的重新放置。
(3)CWnd::RepositionBars作如下的处理:
RepositionBars首先给各个控制子窗口发送(Send)MFC内部使用的消息WM_SIZEPARENT,把窗口客户区矩形指针传递给它们,给它们一个机会来确认自己的尺寸。
然后,各个控制子窗口用OnSizeParent响应WM_SIZEPARENT消息;ControlBar实现了消息处理函数OnSizeParent,它调用CalcDynamicLayout等函数确定本窗口的大小,并从客户区矩形中减去自己的尺寸。
在所有的控制子窗口处理了OnSizeParent消息之后,RepositonBars利用返回的信息调用函数CalcWindowRect计算客户区窗口(MDI客户窗口、View等)的大小。
最后,调用::EndDeferWindowPos或者::SetWindowPos放置所有的窗口(控制子窗口和客户窗口)。
在窗口被放置的时候,发送消息WM_WINDOWPOSCHANGING和WM_WINDOWPOSCHANGED。MFC的实现中,控制窗口响应了前一个消息,消息处理函数是OnWindowPosChanging。CControlBar、CToolBar和CStatusBar等实现了消息处理函数 OnWindowPosChanging。
上述处理过程所涉及的这些函数中,RecalcLayout是CFrameWnd定义的虚拟函数;RepostionBars是CWnd的成员函数; CalcaWindowRect是CWnd的虚拟函数;OnSizeParent是CControlBar定义的消息处理函数; OnWindowPosChanging是CToolbar、CStatusBar、CDockBar等CControlBar派生类定义的消息处理函数。
下面,对其中两个函数RecalcLayout和RepositionBars作一些分析。
RecalcLayout的实现如下:
void CFrameWnd::RecalcLayout(BOOL bNotify)
{
//RecalcLayout是否正在被调用
if (m_bInRecalcLayout)
return;
m_bInRecalcLayout = TRUE;
// clear idle flags for recalc layout if called elsewhere
if (m_nIdleFlags & idleNotify)
bNotify = TRUE;
m_nIdleFlags &= ~(idleLayout|idleNotify);
//与OLE相关的处理
#ifndef _AFX_NO_OLE_SUPPORT
// call the layout hook -- OLE support uses this hook
if (bNotify && m_pNotifyHook != NULL)
m_pNotifyHook->OnRecalcLayout();
#endif
//是否包含浮动(floating)控制条的边框窗口(CMiniFrameWnd类)
if (GetStyle() & FWS_SNAPTOBARS)
{
//计算控制条和边框窗口的位置、尺寸并设置它们的位置
CRect rect(0, 0, 32767, 32767);
RepositionBars(0, 0xffff, AFX_IDW_PANE_FIRST, reposQuery,
&rect, &rect, FALSE);
RepositionBars(0, 0xffff, AFX_IDW_PANE_FIRST, reposExtra,
&m_rectBorder, &rect, TRUE);
CalcWindowRect(&rect);
SetWindowPos(NULL, 0, 0, rect.Width(), rect.Height(),
SWP_NOACTIVATE|SWP_NOMOVE|SWP_NOZORDER);
}
else
//是普通边框窗口,则设置其所有子窗口的位置、尺寸
RepositionBars(0, 0xffff, AFX_IDW_PANE_FIRST,
reposExtra, &m_rectBorder);
//本函数处理完毕
m_bInRecalcLayout = FALSE;
}
该函数主要的目的是调用RepositionBars函数,它分两种情况来调用RepositionBars函数。一种情况是当前边框窗口为浮动控制条的包容窗口(微型边框窗口)时;另一种情况是当前边框窗口为普通边框窗口时。
RepositionBars的实现如下:
void CWnd::RepositionBars(UINT nIDFirst, UINT nIDLast, UINT nIDLeftOver,
UINT nFlags, LPRECT lpRectParam, LPCRECT lpRectClient, BOOL bStretch)
{
ASSERT(nFlags == 0 || nFlags == reposQuery || nFlags == reposExtra);
AFX_SIZEPARENTPARAMS layout;
HWND hWndLeftOver = NULL;
layout.bStretch = bStretch;
layout.sizeTotal.cx = layout.sizeTotal.cy = 0;
if (lpRectClient != NULL)
layout.rect = *lpRectClient; //从参数6得到客户区
else
//参数lpRectClient空,得到客户区域
GetClientRect(&layout.rect);
if (nFlags != reposQuery)
//准备放置各个子窗口(layout)
layout.hDWP = ::BeginDeferWindowPos(8); // reasonable guess
else
layout.hDWP = NULL; // not actually doing layout
//按一定顺序给各个控制条发送父窗口resize的消息;
//各个控制条窗口收到消息后,从客户区中扣除自己使用的区域;
//并且必要的话每个控制窗口调用::DeferWindowPos
//剩下的区域留给nIDLeftOver子窗口
for (HWND hWndChild = ::GetTopWindow(m_hWnd); hWndChild != NULL;
hWndChild = ::GetNextWindow(hWndChild, GW_HWNDNEXT))
{
UINT nIDC = _AfxGetDlgCtrlID(hWndChild);
CWnd* pWnd = CWnd::FromHandlePermanent(hWndChild);
//如果是指定的nIDLeftOver子窗口,则保存其窗口句柄;
//否则,是控制条窗口,给它们发送WM_SIZEPARENT消息
if (nIDC == nIDLeftOver)
hWndLeftOver = hWndChild;
else if (nIDC >= nIDFirst && nIDC <= nIDLast && pWnd != NULL)
//如果layout->hDWP非空, OnSizeParent则将执行窗口布置的操作
::SendMessage(hWndChild, WM_SIZEPARENT, 0, (LPARAM)&layout);
}
//如果是reposQuery,则得到客户区矩形,返回
if (nFlags == reposQuery)
{
ASSERT(lpRectParam != NULL);
if (bStretch)
::CopyRect(lpRectParam, &layout.rect);
else
{
lpRectParam->left = lpRectParam->top = 0;
lpRectParam->right = layout.sizeTotal.cx;
lpRectParam->bottom = layout.sizeTotal.cy;
}
return;
}
//其他情况下(reposDefault、reposExtra),则需要执行Layout操作
//处理hWndLeftOver(nIDLeftOver子窗口)
if (nIDLeftOver != 0 && hWndLeftOver != NULL)
{
CWnd* pLeftOver = CWnd::FromHandle(hWndLeftOver);
// allow extra space as specified by lpRectBorder
if (nFlags == reposExtra)
{
ASSERT(lpRectParam != NULL);
layout.rect.left += lpRectParam->left;
layout.rect.top += lpRectParam->top;
layout.rect.right -= lpRectParam->right;
layout.rect.bottom -= lpRectParam->bottom;
}
//基于layout.rect表示的客户尺寸计算出窗口尺寸
pLeftOver->CalcWindowRect(&layout.rect);
//导致函数::DeferWindowPos的调用
AfxRepositionWindow(&layout, hWndLeftOver, &layout.rect);
}
//给所有的窗口设置尺寸、位置(size and layout)
if (layout.hDWP == NULL || !::EndDeferWindowPos(layout.hDWP))
TRACE0("Warning: DeferWindowPos failed - low system resources./n");
}
RepositionBars用来改变客户窗口中控制条的尺寸大小或者位置,其中:
参数1和参数2定义了需要重新放置的子窗口ID的范围,一般是0到0xFFFF。
参数3指定了一个子窗口ID,它拥有客户窗口剩下的空间,一般是AFX_IDW_PANE_FIRST,表示视的窗口ID。
参数4指定了操作类型,缺省是CWnd::ReposDefault,表示执行窗口放置操作,参数5不会用到;若取值CWnd::ReposQuery,则表示尝试进行窗口放置(Layout) ,但最后不执行这个操作,只是把参数5初始化成客户区的尺寸大小;若取值CWnd::ReposExtra,则把参数5的值加到参数2表示的子窗口的客户区域,并执行窗口放置操作。
参数6表示传递给函数的可用窗口客户区的尺寸,如果空则使用窗口客户区尺寸。
如果执行layout操作的话,该函数的核心处理就是:
首先,调用::BeginDeferWindowPos初始化一个Windows内部的多窗口位置结构(Multiple-window - position structure)hDWP;
然后,让各个子窗口逐个调用::DeferWindowPos,更新hDWP。在调用::DeferWindowPos之前,要作一个确定子窗口大小的工作。这些工作通过给各个控制子窗口发送消息WM_SIZEPARENT来完成。
控制子窗口通过函数OnSizeParent响应WM_SIZEPARENT消息,先确定自己的尺寸,然后,如果需要进行窗口布置 (WM_SIZEPARENT消息参数lParam包含了一个非空的HDWP结构(lpLayout->hDWP)),则OnSizeParent 将调用AfxRepositionWindow函数计算本控制窗口的位置,结果保存到hDWP中。
在所有的控制窗口尺寸确定之后,剩下的留给窗口hWndLeftOver(如果存在的话)。确定了hWndLeftOver的大小之后,调用AfxRepositionWindow函数计算其位置,结果保存到hDWP中。
上面提到的函数AfxRepositionWindow间接调用了::DeferWindowPos。
最后,::EndDeferWindowPos,使用hDWP安排所有子窗口的位置和大小。
至于其他函数,如OnSizeparent、OnWindowPosChanging、CalcWindowRect,这里不作进一步的分析。
MFC内部通过给边框窗口发送消息WM_SETMESSAGESTRING、WM_POPMESSAGESTRING的方式在状态栏中显示信息。这两个消息在afxpriv.h里头定义。
WM_SETMESSAGESTRING消息表示在状态栏中显示和某个ID对应的字符串信息或者指定的字符串信息,消息参数wParam指定了字符串资源ID,消息参数lParam指定了字符串指针,两个消息参数只有一个有用。一般,一个命令ID对应了一个字符串 ID,对应的字符串是命令ID的说明。
消息WM_POPMESSAGESTRING用来重新设置状态栏。
这两个消息对应的消息处理函数分别是OnSetMessageString和OnPopMessageString,OnSetMessageString和OnPopMessageString分别实现如下:
LRESULT CFrameWnd::OnSetMessageString(WPARAM wParam, LPARAM lParam)
{
//最近一次被显示的消息字符串IDS(一个消息对应的字符串)
UINT nIDLast = m_nIDLastMessage;
m_nFlags &= ~WF_NOPOPMSG;
//得到状态栏
CWnd* pMessageBar = GetMessageBar();
if (pMessageBar != NULL)
{
LPCTSTR lpsz = NULL;
CString strMessage;
//设置状态栏文本
if (lParam != 0) //指向一个字符串
{
ASSERT(wParam == 0); // can't have both an ID and a string
lpsz = (LPCTSTR)lParam; // set an explicit string
}
else if (wParam != 0)//一个字符串资源IDS
{
//打印预览时映射SC_CLOSE成AFX_IDS_PREVIEW_CLOSE;
if (wParam == AFX_IDS_SCCLOSE && m_lpfnCloseProc != NULL)
wParam = AFX_IDS_PREVIEW_CLOSE;
//得到资源ID所标识的字符串
GetMessageString(wParam, strMessage);
lpsz = strMessage;
}
//在状态栏中显示文本
pMessageBar->SetWindowText(lpsz);
// 根据最近一次选择的消息更新状态条所属窗口的有关记录
CFrameWnd* pFrameWnd = pMessageBar->GetParentFrame();
if (pFrameWnd != NULL)
{
//记录最近一次显示的消息字符串
pFrameWnd->m_nIDLastMessage = (UINT)wParam;
//记录最近一次Tracking的命令ID和字符串IDS
pFrameWnd->m_nIDTracking = (UINT)wParam;
}
}
m_nIDLastMessage = (UINT)wParam; // new ID (or 0)
m_nIDTracking = (UINT)wParam; // so F1 on toolbar buttons work
return nIDLast;
}
OnSetMessageString函数直接或者从ID从字符串资源中得到字符串指针。如果是从ID得到字符串指针,则函数GetMessageString被调用。
和命令ID对应的字符串由两部分组成,前一部分用于在状态栏显示,后一部分用于Tooltip显示,分隔符号是“/n”。例如,字符串ID_APP_EXIT(对应“退出”菜单、按钮)是“Exit Application/nExit”,当鼠标落在“退出”按钮上时,状态栏显示“Exit Application”,Tooltip显示“Exit”。根据这种格式,GetMessageString分离出第一部分的文本信息。至于第二部分的用途将在讨论Tooltip的章节将用到。
得到了字符串之后,OnSetMessageString调用状态栏的SetWindowText函数。SetWindowText导致消息 WM_SETTEXT消息发送给状态栏,状态栏的消息处理函数OnSetText被调用,实际上等于调用了SetPaneText(0, lpsz),即在状态栏的第0格中显示字符串lpsz的信息。对于工具栏来说,SetWindowText可以认为是SetPaneText(0, lpsz)的简化版本。
顺便指出,pMessageBar->GetParentFrame()返回主边框窗口,即使pMessageBar指向漂浮的工具条。关于泊位和漂浮,见后面13.2.5节的描述。
关于OnSetText,其实现如下:
LRESULT CStatusBar::OnSetText(WPARAM, LPARAM lParam)
{
ASSERT_VALID(this);
ASSERT(::IsWindow(m_hWnd));
int nIndex = CommandToIndex(0); //返回0
if (nIndex < 0)
return -1;
return SetPaneText(nIndex, (LPCTSTR)lParam) ? 0 : -1;
}
LRESULT CFrameWnd::OnPopMessageString(WPARAM wParam,
LPARAM lParam)
{
//WF_NOPOPMSG表示边框窗口不处理WM_POPMESSAGESTRING
if (m_nFlags & WF_NOPOPMSG)
return 0;
//调用OnSetMessageString
return SendMessage(WM_SETMESSAGESTRING, wParam, lParam);
}
一般,在清除状态栏消息时,发送WM_POPMESSAGESTRING,通过消息参数wParam指定一个字符串资源,其ID 为AFX_IDS_IDLEMESSAGE,对应的字符串是“Ready”。
状态栏的一个重要作用是显示菜单命令或者工具条按钮的提示信息。本节讨论如何显示菜单命令的提示信息,关于工具条按钮在这方面的讨论见后面13.2.4.4章节。
显示菜单命令的提示信息,就是每当一个菜单项被选中之后,在状态栏显示该菜单的功能、用法等信息。这些信息以字符串资源的形式保存,字符串ID对应于菜单项的命令ID。
所以,必须处理菜单选择消息WM_MENUSELECT。CFrameWnd实现了消息处理函数OnMenuSelect,其实现如下:
void CFrameWnd::OnMenuSelect(UINT nItemID,
UINT nFlags, HMENU /*hSysMenu*/)
{
CFrameWnd* pFrameWnd = GetTopLevelFrame();
ASSERT_VALID(pFrameWnd);
//跟踪被选中的菜单项
if (nFlags == 0xFFFF)
{
//取消菜单操作
m_nFlags &= ~WF_NOPOPMSG;
if (!pFrameWnd->m_bHelpMode)
m_nIDTracking = AFX_IDS_IDLEMESSAGE;
else
m_nIDTracking = AFX_IDS_HELPMODEMESSAGE;
//在状态栏显示
SendMessage(WM_SETMESSAGESTRING, (WPARAM)m_nIDTracking);
ASSERT(m_nIDTracking == m_nIDLastMessage);
// update right away
CWnd* pWnd = GetMessageBar();
if (pWnd != NULL)
pWnd->UpdateWindow();
}
else
{
//选中分隔栏、Popup子菜单或者没有选中一个菜单项
if (nItemID == 0 || nFlags & (MF_SEPARATOR|MF_POPUP))
{
// nothing should be displayed
m_nIDTracking = 0;
}
else if (nItemID >= 0xF000 && nItemID < 0xF1F0) // max of 31 SC_s
{
//系统菜单的菜单项被选中
m_nIDTracking = ID_COMMAND_FROM_SC(nItemID);
ASSERT(m_nIDTracking >= AFX_IDS_SCFIRST &&
m_nIDTracking < AFX_IDS_SCFIRST + 31);
}
else if (nItemID >= AFX_IDM_FIRST_MDICHILD)
{
//如果选中的菜单项表示一个MDI子窗口
m_nIDTracking = AFX_IDS_MDICHILD;
}
else
{
//选中了一个菜单项
m_nIDTracking = nItemID;
}
pFrameWnd->m_nFlags |= WF_NOPOPMSG;
}
// when running in-place, it is necessary to cause a message to
// be pumped through the queue.
if (m_nIDTracking != m_nIDLastMessage && GetParent() != NULL)
PostMessage(WM_KICKIDLE);
}
OnMenuSelect的作用在于跟踪当前选中的菜单项,把菜单项对应的ID保存在CFrameWnd的成员变量m_nIDTracking中。
如果菜单项没有选中,或者选中的是一个子菜单,则设置nIDTracking为0。
如果选中的是系统菜单,则把系统菜单ID转换成一个对应的命令ID;保存该值到nIDTracking中。
如果选中的菜单是MDI子窗口创建时添加的(用来表示MDI子窗口),则转换菜单ID为AFX_IDS_MDICHILD,所有对应MDI子窗口的菜单项都使用AFX_IDS_MDICHILD,保存该值到nIDTracking中。
其他情况,就是选中菜单项的ID,把它保存到nIDTracking中。
跟踪被选择的菜单项并保存其ID在m_nIDTracking中,OnEnterIdle将用到m_nIDTracking。OnEnterIlde是消息WM_ENTERIDLE的处理函数,CFrameWnd的实现如下。
void CFrameWnd::OnEnterIdle(UINT nWhy, CWnd* pWho)
{
CWnd::OnEnterIdle(nWhy, pWho);
//若不是因为菜单选择进入该函数
//或者当前跟踪到的菜单项ID是最近一次处理的,则返回
if (nWhy != MSGF_MENU || m_nIDTracking == m_nIDLastMessage)
return;
//将发送消息WM_SETMESSAGETEXT
//在状态栏显示m_nIDTracking对应的字符串
SetMessageText(m_nIDTracking);
ASSERT(m_nIDTracking == m_nIDLastMessage);
}
当一个对话框或者菜单被显示的时候,Windows发送WM_ENTERIDLE消息。消息参数wParam取值为MSGF_DIALOGBOX或者 MSGF_MENU。前者表示显示对话框时发送该消息,这时消息参数lParam表示对话框的句柄;后者表示显示菜单时发送该消息,这时消息参数 lParam表示菜单的句柄。
经过消息映射,wParam的值传递给 OnEnterIdle的参数nWhy,参数lParam的值传给参数who。如果参数1取值为MSGF_MENU,并且OnEnterIdle最近一次在菜单显示被调用时的菜单ID不同于这一次,则调用SetMessageText在状态栏显示对应ID命令的字符串,并且记录当前菜单ID到变量 m_nIDTracking中(见消息处理函数OnSetMessageText)。
这样,在菜单选择期间,用户选择的菜单项ID被OnMenuSelect记录,在消息WM_ENTERIDLE处理时在状态栏显示ID命令的提示。
工具条(包括对话框工具条)是一个子窗口,它们可以响应各种消息。如果按标准的Windows消息和命令消息的分发途径,一些消息不能送到拥有工具条的边框窗口,因为这些消息都将被工具条(对话框工具条)处理掉。所以,CControlBar覆盖了虚拟函数 PreTranslateMessage和WindowProc以便实现特定的消息分发路径。
CControlBar 的WindowProc实现了如下的消息分发路径:
用户对控制条的输入消息或者分发给CControlBar及其派生类处理,或者送给拥有控制条的边框窗口处理,或者送给Windows控制“窗口类”的窗口过程处理。
WindowProc的实现如下:
LRESULT CControlBar::WindowProc(UINT nMsg,
WPARAM wParam, LPARAM lParam)
{
ASSERT_VALID(this);
LRESULT lResult;
switch (nMsg)
{
//本函数处理以下消息
case WM_NOTIFY:
case WM_COMMAND:
case WM_DRAWITEM:
case WM_MEASUREITEM:
case WM_DELETEITEM:
case WM_COMPAREITEM:
case WM_VKEYTOITEM:
case WM_CHARTOITEM:
//首先,工具条处理上述消息,如果没有处理,则接着给所属边框窗口处理
if (OnWndMsg(nMsg, wParam, lParam, &lResult))
return lResult;
else
return GetOwner()->SendMessage(nMsg, wParam, lParam);
}
}
// 最后,给基类CWnd,按缺省方式处理
lResult = CWnd::WindowProc(nMsg, wParam, lParam);
return lResult;
}
从上述实现可以看出,对于case范围内的一些消息,如WM_COMMAND、WM_NOTIFY等,控制条如果不能处理,则优先分发给其父窗口(边框窗口)处理,然后进入缺省处理,对于其他消息直接调用基类CWnd的实现(缺省处理)。基于这样的机制,可以把用户对工具条按钮或者对话框工具条内控制的操作解释成相应的命令消息,执行对应的命令处理。
对于工具条,当用户选中某个按钮时(鼠标左键弹起,消息是WM_LBUTTONUP),工具条窗口接收到WM_LBUTTONUP消息,该消息不在CControlBar::WindowProc特别处理的消息范围内,于是进行缺省处理,也就是说,把该消息派发给控制条对应的Windows控制的窗口过程处理(即被MFC统一窗口过程所取代的原窗口过程),该窗口过程则把该消息转换成一条命令消息WM_COMMAND,命令ID就是选中按钮对应的ID,然后,发送该命令消息给拥有工具条的边框窗口,导致相应的命令处理函数被调用。
对于对话框工具条,当工具条的某个控制子窗口被选中之后,则产生一条命令通知消息WM_COMMAND,wParam是控制子窗口的ID。CControlBar::WindowProc处理该消息。 WindowProc首先调用OnWndMsg把消息发送给对话框工具条或者对话框工具条的基类处理,如果没有被处理的话,则OnWndMsg返回 FALSE。接着,WindowPoc把命令消息传递给父窗口(边框窗口)处理。由于工具条的控制窗口的ID对应的是命令ID,所以,这条 WM_COMMAND消息传递给边框窗口时,被解释成一个命令消息,相应的命令处理函数被调用。
CControlBar覆盖PreTranslateMessage函数,主要是为了在光标落在工具条按钮上时显示FLYBY信息,并且让对话框工具条过滤Dialog消息。
BOOL CControlBar::PreTranslateMessage(MSG* pMsg)
{
ASSERT_VALID(this);
ASSERT(m_hWnd != NULL);
//过滤Tooltip消息
if (CWnd::PreTranslateMessage(pMsg))
return TRUE; //是Tooltip消息,已经被处理
UINT message = pMsg->message;
//控制条的父窗口,对工具条和对话框工具条,总是创建它的边框窗口
CWnd* pOwner = GetOwner();
//必要的话,在状态条显示工具栏按钮的提示
if (((m_dwStyle & CBRS_FLYBY) ||
message == WM_LBUTTONDOWN || message == WM_LBUTTONUP) &&
((message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) ||
(message >= WM_NCMOUSEFIRST &&
message <= WM_NCMOUSELAST)))
{
_AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
//确认鼠标在工具栏的哪个按钮上
CPoint point = pMsg->pt;
ScreenToClient(&point);
TOOLINFO ti; memset(&ti, 0, sizeof(TOOLINFO));
ti.cbSize = sizeof(TOOLINFO);
int nHit = OnToolHitTest(point, &ti);
if (ti.lpszText != LPSTR_TEXTCALLBACK)
free(ti.lpszText);
BOOL bNotButton =
message == WM_LBUTTONDOWN && (ti.uFlags & TTF_NOTBUTTON);
if (message != WM_LBUTTONDOWN && GetKeyState(VK_LBUTTON) < 0)
nHit = pThreadState->m_nLastStatus;
//更新状态栏的提示信息
if (nHit < 0 || bNotButton)
{
if (GetKeyState(VK_LBUTTON) >= 0 || bNotButton)
{
SetStatusText(-1);
KillTimer(ID_TIMER_CHECK);
}
}
else
{
if (message == WM_LBUTTONUP)
{
SetStatusText(-1);
ResetTimer(ID_TIMER_CHECK, 200);
}
else
{
if ((m_nStateFlags & statusSet) || GetKeyState(VK_LBUTTON) < 0)
SetStatusText(nHit);
else if (nHit != pThreadState->m_nLastStatus)
ResetTimer(ID_TIMER_WAIT, 300);
}
}
pThreadState->m_nLastStatus = nHit;
}
// don't translate dialog messages when in Shift+F1 help mode
CFrameWnd* pFrameWnd = GetTopLevelFrame();
if (pFrameWnd != NULL && pFrameWnd->m_bHelpMode)
return FALSE;
//在IsDialogMessage之前调用边框窗口的PreTranslateMessage,
//给边框窗口一个处理快捷键的机会
while (pOwner != NULL)
{
// allow owner & frames to translate before IsDialogMessage does
if (pOwner->PreTranslateMessage(pMsg))
return TRUE;
// try parent frames until there are no parent frames
pOwner = pOwner->GetParentFrame();
}
//过滤给对话框的消息和来自子窗口的消息
return PreTranslateInput(pMsg);
}
函数PreTranslateMessage主要是针对工具栏的,用来处理工具栏的CBRS_FLYBY特征。
对于对话框工具栏,也可以有CBRS_FLYBY特征。但在这种情况下,还需要把一些用户键盘输入解释成对话框消息。为了防止快捷键被解释成对话框消息,在调用函数PreTranslateInput之前,必须调用所有父边框窗口的 PreTranslateMessage,给父边框窗口一个机会处理用户的输入消息,判断快捷键是否被按下。
关于Tooltip和本PreTranslateMessage函数处理Tooltip的详细解释见下一节的讨论。
工具条(或对话框工具条)如果指定了CBRS_TOOLTIPS风格(创建时指定或者创建后用SetBarStyle设置),则当鼠标落在某个按钮上(或者对话框的子控制窗口)时,在鼠标附近弹出一个文本框──Tooltip提示窗口。
如果还指定了CBRS_FLYBY风格,则还在状态栏显示和按钮(或子控制窗口)ID对应的字符串信息。当然,鼠标左键在某个按钮(或子控制窗口)按下时,也要在状态栏显示按钮的提示信息,当左键弹起时,则重置状态栏的状态。
如前所述,Tooltip窗口是Windows控制窗口。MFC使用了CToolTipCtrl类封装 Tooltip的HWND窗口。在一个线程的生存期间,至多拥有一个Tooltip窗口,该窗口对象的指针保存在线程状态的成员变量m_pToolTip 中。线程状态类AFX_THREAD_STATE的析构函数如果检测到m_pToolTip,则销毁MFC窗口对象和相应的Windows窗口对象。
为了支持Tooltip显示,CWnd提供了以下函数:EnableTooltip,CancelTooltip,PreTranslateMessage,FilterTooltipMessage,OnToolHitTest。
EnableTooltip设置CBRS_TOOLTIP风格,相反CancelTootip取消这种风格。
PreTranslateMessage调用了FilterTooltipMessage过滤Tooltip消息。
OnToolHitTest是一个由CWnd定义的虚拟函数。CToolBar通过覆盖该函数,来检测对话框工具栏的控制子窗口或者工具栏按钮是否被选中、哪个被选中。
CWnd的PreTranslateMessage在4.5节讨论过,它的实现如下:
BOOL CWnd::PreTranslateMessage(MSG* pMsg)
{
//处理Tooltip消息
AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();
if (pModuleState->m_pfnFilterToolTipMessage != NULL)
//导致调用FilterTooltipMessage
(*pModuleState->m_pfnFilterToolTipMessage)(pMsg, this);
//不是Tooltip消息
return FALSE;
}
至于为什么MFC在模块状态中保存一个处理Tooltip消息的函数地址,通过该函数调用FilterTooltipMessage,是因为Tooltip窗口是模块线程局部有效的。
FilterTooltipMessage检测是否是Tooltip消息。如果是,在必要时创建一个CTooltipCtrl对象和对应的HWND,调用OnToolHitTest确定被选中的按钮或者控制的ID,接着弹出Tooltip窗口。
其他函数和CTooltipCtrl这里不作详细论述了。
Tooltip窗口在弹出之前,它给工具条(或者对话框工具栏)的父窗口发送通知消息TTN_NEEDTEXT,请求得到要显示的文本。
CFrameWnd类处理了TTN_NEEDTEXT通知消息,消息处理函数是OnToolTipText。
消息映射的定义:
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipText)
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnToolTipText)
这里,使用了扩展消息映射宏把子窗口ID在0和0xFFFF之间的控制条窗口的通知消息TTN_NEEDTEXTA和TTN_NEEDTEXTW映射到函数OnToolTipText。
消息映射的实现:
BOOL CFrameWnd::OnToolTipText(UINT, NMHDR* pNMHDR, LRESULT* pResult)
{
ASSERT(pNMHDR->code == TTN_NEEDTEXTA ||
pNMHDR->code == TTN_NEEDTEXTW);
//让上一层的边框窗口优先处理该消息
if (GetRoutingFrame() != NULL)
return FALSE;
//分ANSI and UNICODE两个处理版本
TOOLTIPTEXTA* pTTTA = (TOOLTIPTEXTA*)pNMHDR;
TOOLTIPTEXTW* pTTTW = (TOOLTIPTEXTW*)pNMHDR;
TCHAR szFullText[256];
CString strTipText;
UINT nID = pNMHDR->idFrom;
//如果idFrom是一个子窗口,则得到其ID。
if (pNMHDR->code == TTN_NEEDTEXTA &&
(pTTTA->uFlags & TTF_IDISHWND) ||
pNMHDR->code == TTN_NEEDTEXTW &&
(pTTTW->uFlags & TTF_IDISHWND))
{
//idFrom是工具条的句柄
nID = _AfxGetDlgCtrlID((HWND)nID);
}
if (nID != 0) //若是0,为一分隔栏,不是按钮
{
//得到nID对应的字符串
AfxLoadString(nID, szFullText);
//从上面得到的字符串中取出Tooltip使用的文本
AfxExtractSubString(strTipText, szFullText, 1, '/n');
}
//复制分离出的文本
#ifndef _UNICODE
if (pNMHDR->code == TTN_NEEDTEXTA)
lstrcpyn(pTTTA->szText, strTipText, _countof(pTTTA->szText));
else
_mbstowcsz(pTTTW->szText, strTipText, _countof(pTTTW->szText));
#else
if (pNMHDR->code == TTN_NEEDTEXTA)
_wcstombsz(pTTTA->szText, strTipText, _countof(pTTTA->szText));
else
lstrcpyn(pTTTW->szText, strTipText, _countof(pTTTW->szText));
#endif
*pResult = 0;
//显示Tooltip窗口
::SetWindowPos(pNMHDR->hwndFrom, HWND_TOP, 0, 0, 0, 0,
SWP_NOACTIVATE|SWP_NOSIZE|SWP_NOMOVE);
return TRUE; //消息处理完毕
}
OnToolTipText是一个扩展映射宏定义的消息处理函数,所以有一个UINT参数并且返回BOOL类型的值。不过,由于第二个参数(NMHDR)的idFrom域包含了有关信息,所以第一个UINT类型的参数没有用上。
OnToolTipText也是一个处理通知消息的例子。其中,通知参数wParam的结构如4.4.4.2节所述,具体如下:
typedef struct {
NMHDR hdr; //WM_NOTIFY消息要求的头
LPTSTR lpszText; //接收工具条按钮对应文本的缓冲区
WCHAR szText[80]; //接收Tooltip显示文本的缓冲区
HINSTANCE hinst; //包含了szText的实例句柄
UINT uflags; //标识了NMHDR的idFrom成员的意义
} TOOLTIPTEXT, FAR *LPTOOLTIPTEXT;
uflags如果等于TTF_IDISHWND,则表示通知消息来自对话框工具条的一个子窗口,而不是包含工具条按钮。
OnToolTipText根据子窗口ID或者工具条按钮对应的ID,得到字符串ID。如前所述,字符串ID由两部分组成,第二部分用于Tooltip显示,分隔符号是“/n”。根据这种格式OnToolTipText分离出Tooltip文本。
得到了Tooltip文本之后,可以有三种方法返回文本信息:把文本信息复制到szText缓冲区;把文本地址复制到lpszText;复制字符串资源的ID到lpszText、复制包含资源的实例句柄到hint。本函数采用了第一种方法。
在得到了返回的Tooltip文本之后,该文本在Tooltip窗口中被显示出来。
其他的OnToolHist等函数的实现不作详细的解释了。下面,讨论CBRS_FLYBY风格的实现。
CBRS_FLYBY是MFC提供的特征。当鼠标落在工具条按钮(或者对话框工具条的子窗口)上且稳定300ms 后,在状态栏显示对应的提示信息。如果选中某个按钮或者子窗口(鼠标左键按下),则在相应命令消息处理之前在状态栏显示有关提示信息,之后(鼠标左键弹起),重新设置状态栏的状态信息。
为了支持这种特征,CControlBar覆盖虚拟函数PreTranslateMessage来处理和CBRS_FLYBY相关的消息,该函数前面已经讨论过,这里解释它如何处理CBRS_FLYBY特征。
如果同时具备
条件1:控制条具有CBRS_FLYBY特征或者当前消息是WM_LBUTTONUP或者WM_LBUTTONDOWN。
条件2:当前消息是鼠标消息(在WM_MOUSEFIRST和WM_MOUSELAST之间或者在WM_NCMOUSEFIRST和WM_NCMOUSELAST之间)。
则进行FLYBY处理。
首先,调用OnToolHitTest测试用户是否选中了工具条的按钮或者子窗口;
如果没有按钮或者子窗口被选中,则重新设置状态栏的状态,取消曾经设置的Check定时器。重置状态栏的状态时调用了SetStatusText(int nHit)函数,它是CControlBar内部使用的函数,若nHit等于-1,它向父窗口发送WM_POPMESSAGETEXT,消息参数是 AFX_IDS_IDLEMESSAGE,结果导致状态栏显示“Ready”字样;否则发送WM_SETMESSAGETEXT消息,wParm设置为 nHit,结果导致在状态栏显示ID为nHit的字符串。
如果有按钮或者子窗口被选中,若左键弹起,则重新设置状态栏信息,取消Wait定时器,并重新设置Check定时器,定时是200ms;若左键按下,则在状态栏显示消息ID对应的提示信息;若是其他鼠标消息,如果当前鼠标所在按钮(子窗口)不同于最近一次,则取消 Check定时器,重新设置Wait定时器,定时300毫秒。
CControlBar覆盖了消息处理函数OnTimer,在指定时间之后,检查鼠标位置,如果鼠标还在某个按钮或者子窗口上,则在状态条显示提示信息。Wait定时器在等待之后准备在状态条显示信息,触发一次后被取消;Check定时器在等待之后,判断是否需要取消状态条当前显示的信息,重新设置状态条,若这样的话,同时也取消Check定时器。
注意,这些鼠标消息被处理之后,并没有终止,它们将继续被发送给控制条的窗口过程处理。
至此,CBRS_FLYBY特征的支持实现描述完毕。
在MFC下,工具条、状态条还有一个重要的特征,就是自动地根据条件禁止或者允许使用某个按钮、窗格等。在4.4.5节命令消息的处理中,曾详细讨论了其实现原理,现在,详细地分析所涉及函数是如何实现的。有关的消息处理函数和虚拟函数如下。
处理WM_INITIALUPDATE消息的OnInitialUpdate;
处理WM_IDLEUPDATECMDUI消息的OnIdleUpdateCmdUI;
虚拟函数OnUpdateCmdUI。
回顾5.3.3.5节,在边框窗口的创建之后,给所有的子窗口发送初始化消息,控制子窗口用OnInitialUpdate响应它,调用OnIdleUpdateCmdUI完成状态的初始化。
OnIdleUpdateCmdUI还在IDLE处理时进行状态的更新处理,它生成用于处理状态更新消息的命令目标pTarget,然后调用虚拟函数OnUpdateCmdUI(pTarget,…)来更新工具栏或者状态栏的状态。
CControlBar的子类都实现了自己的OnUpdateCmdUI函数,用该函数生成适当的CCmdUI对象state,然后调用CCmdUI的DoUpdate(pTarget,…)给pTarget所指对象发送状态更新消息。为了完成具体的状态更新,从 CCmdUI派生出CToolCmdUI和CStatusCCmUI,它们实现了自己的Enable、SetCheck等等。
CControlBar使用OnInitialUpdate消息处理函数初始化控制窗口的状态。
void CControlBar::OnInitialUpdate()
{
//在窗口显示之前,更新状态
OnIdleUpdateCmdUI(TRUE, 0L);
}
CControlBar实现了OnInitialUpdate函数,通过它来处理WM_INITIALUPDATE消息。各个子类不必覆盖该消息处理函数。
CControlBar使用OnIdleUpdateCmdUI消息处理函数处理IDLE消息。
LRESULT CControlBar::OnIdleUpdateCmdUI(WPARAM wParam, LPARAM)
{
// handle delay hide/show
BOOL bVis = GetStyle() & WS_VISIBLE;
UINT swpFlags = 0;
if ((m_nStateFlags & delayHide) && bVis)
swpFlags = SWP_HIDEWINDOW;
else if ((m_nStateFlags & delayShow) && !bVis)
swpFlags = SWP_SHOWWINDOW;
m_nStateFlags &= ~(delayShow|delayHide);
if (swpFlags != 0)
{
SetWindowPos(NULL, 0, 0, 0, 0, swpFlags|
SWP_NOMOVE|SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE);
}
// the style must be visible and if it is docked
// the dockbar style must also be visible
if ((GetStyle() & WS_VISIBLE) &&
(m_pDockBar == NULL || (m_pDockBar->GetStyle() & WS_VISIBLE)))
{
//得到父边框窗口,状态更新消息将发送给它
CFrameWnd* pTarget = (CFrameWnd*)GetOwner();
if (pTarget == NULL || !pTarget->IsFrameWnd())
pTarget = GetParentFrame();
if (pTarget != NULL)
OnUpdateCmdUI(pTarget, (BOOL)wParam);
}
return 0L;
}
OnIdleUpdateCmdUI或者在初始化时被OnInitialUpdate调用,或者作为消息处理函数来处理WM_IDLEUPDATECMDUI消息。
CControlBar实现了OnIdleUpdateCmdUI函数,把具体的用户界面更新动作委托给虚拟函数OnUpdateCmdUI完成。
由于各个用户界面的特殊性,所以CControlBar本身没有实现OnUpdateCmdUI,而是留给各个派生类去实现。例如,CToolBar覆盖了OnUpdateCmdUI,其实现如下:
void CToolBar::OnUpdateCmdUI(CFrameWnd* pTarget, BOOL bDisableIfNoHndler)
{
//定义一个CCmdUI对象,CToolCmdUI派生于CCmdUI
CToolCmdUI state;
//给CCmdUI的各个成员赋值
state.m_pOther = this;
//得到总的按钮数目
state.m_nIndexMax = (UINT)DefWindowProc(TB_BUTTONCOUNT, 0, 0);
//逐个按钮进行状态更新
for (state.m_nIndex = 0; state.m_nIndex < state.m_nIndexMax; state.m_nIndex++)
{
//获取按钮状态信息
TBBUTTON button;
_GetButton(state.m_nIndex, &button);
//得到按钮的ID
state.m_nID = button.idCommand;
// ignore separators
if (!(button.fsStyle & TBSTYLE_SEP))
{
//优先让CToolBar对象处理状态更新消息
if (CWnd::OnCmdMsg(state.m_nID,
CN_UPDATE_COMMAND_UI, &state, NULL))
continue;//处理了更新消息,更新下一个按钮
//CToolBar没有处理,将发送给pTarget处理状态更新消息
//第二个参数bDisableIfNoHndler往下传
state.DoUpdate(pTarget, bDisableIfNoHndler);
}
}
//更新加到控制条中的对话框控制的状态
UpdateDialogControls(pTarget, bDisableIfNoHndler);
}
CToolBar 的OnUpdateCmdUI函数完成工具条按钮的状态更新。它接受两个参数,参数1表示接收状态更新命令消息的对象,由CControlBar的函数 OnIdleUpdateCmdUI传递过来,一般是边框窗口对象;参数2表示如果某条命令消息没有处理函数时,对应的用户接口对象是否被禁止。
OnUpdateCmdUI通过发送状态更新通知消息,逐个更新按钮的状态。更新消息首先让工具条对象处理,如果没有处理的话,送给边框窗口对象处理,导致状态更新命令消息的处理函数被调用,参见4.4.5节。
CStatusBar的OnUpdateCmdUI类似于此。
CDialogBar的OnUpdateCmdUI则调用了虚拟函数UpdateDialogControls来进行状态更新,CWnd提供了该函数的实现,过程类似于CToolBar的函数OnUpdateCmdUI。
那么,菜单项的自动更新如何实现的呢?OnInitMenuPopup在菜单项状态的自动更新中曾经被提到,其实现如下:
void CFrameWnd::OnInitMenuPopup(CMenu* pMenu, UINT, BOOL bSysMenu)
{
AfxCancelModes(m_hWnd);
if (bSysMenu)
return; // don't support system menu
ASSERT(pMenu != NULL);
// check the enabled state of various menu items
CCmdUI state;
state.m_pMenu = pMenu;
ASSERT(state.m_pOther == NULL);
ASSERT(state.m_pParentMenu == NULL);
//判断菜单是否在顶层菜单(top level menu)中弹出,如果这样
//则设置m_pParentMenu指向顶层菜单,否则m_pParentMenu
//为空,表示它是一个二级弹出菜单
HMENU hParentMenu;
//是否是浮动式的弹出菜单(floating pop up menu)
if (AfxGetThreadState()->m_hTrackingMenu == pMenu->m_hMenu)
state.m_pParentMenu = pMenu; // parent == child for tracking popup
else if ((hParentMenu = ::GetMenu(m_hWnd)) != NULL)//
{
CWnd* pParent = GetTopLevelParent();
// child windows don't have menus -- need to go to the top!
//得到顶层窗口的菜单
if (pParent != NULL &&
(hParentMenu = ::GetMenu(pParent->m_hWnd)) != NULL)
{
int nIndexMax = ::GetMenuItemCount(hParentMenu);
//确定顶层窗口的菜单是否包含本菜单项
for (int nIndex = 0; nIndex < nIndexMax; nIndex++)
{
if (::GetSubMenu(hParentMenu, nIndex) == pMenu->m_hMenu)
{
//顶层窗口菜单是本菜单的父菜单
state.m_pParentMenu = CMenu::FromHandle(hParentMenu);
break;
}
}
}
}
//本菜单的菜单项(menu item)数量
state.m_nIndexMax = pMenu->GetMenuItemCount();
//对所有菜单项逐个进行状态更新
for (state.m_nIndex = 0; state.m_nIndex < state.m_nIndexMax;
state.m_nIndex++)
{
state.m_nID = pMenu->GetMenuItemID(state.m_nIndex);
if (state.m_nID == 0)
continue; // menu separator or invalid cmd - ignore it
ASSERT(state.m_pOther == NULL);
ASSERT(state.m_pMenu != NULL);
if (state.m_nID == (UINT)-1)
{
// 可能是一个popup菜单,得到其第一个子菜单项目
state.m_pSubMenu = pMenu->GetSubMenu(state.m_nIndex);
if (state.m_pSubMenu == NULL ||
(state.m_nID = state.m_pSubMenu->GetMenuItemID(0)) == 0 ||
state.m_nID == (UINT)-1)
{
continue; // 找不到popup菜单的子菜单项
}
//popup菜单不会被自动的禁止
state.DoUpdate(this, FALSE);
}
else
{
//正常的菜单项,若边框窗口的m_bAutoMenuEnable设置为
//TURE且菜单项非系统菜单,则自动enable/disable该菜单项
state.m_pSubMenu = NULL;
state.DoUpdate(this, m_bAutoMenuEnable && state.m_nID < 0xF000);
}
//经过菜单状态的更新处理,可能增加或删除了一些菜单项
UINT nCount = pMenu->GetMenuItemCount();
if (nCount < state.m_nIndexMax)
{
state.m_nIndex -= (state.m_nIndexMax - nCount);
while (state.m_nIndex < nCount &&
pMenu->GetMenuItemID(state.m_nIndex) == state.m_nID)
{
state.m_nIndex++;
}
}
state.m_nIndexMax = nCount;
}
}
菜单弹出之前,发送WM_INITMENUPOPUP消息,OnInitMenuPopup消息处理函数被调用,逐个更新菜单项目(menu item)的状态。程序员可以处理它们对应的状态更新消息,禁止/允许菜单项目被使用(disable/enable),在菜单项目上打钩或者取消(checked/unchecked),等等。
这里讨论显示或者隐藏工具栏、状态栏的操作,以及工具栏、状态栏被显示/隐藏时,相关的两个菜单项 ID_VIEW_STATUS_BAR、ID_VIEW_TOOLBAR的状态更新。这两个菜单命令及对应的状态更新命令是标准命令消息所包含的。MFC 边框窗口实现了菜单命令消息的处理和菜单项状态的更新。
CFrameWnd提供了OnBarCheck来响应与ID_VIEW_STATUS_BAR、ID_VIEW_TOOLBAR菜单项对应的命令。
消息映射:
ON_COMMAND_EX(ID_VIEW_STATUS_BAR, OnBarCheck)
ON_COMMAND_EX(ID_VIEW_TOOLBAR, OnBarCheck)
这里,使用了扩展命令消息映射宏把ID_VIEW_STATUS_BAR和ID_VIEW_TOOLBAR命令映射给同一个函数OnBarCheck处理。
OnBarCheck函数的实现:
BOOL CFrameWnd::OnBarCheck(UINT nID)
{
ASSERT(ID_VIEW_STATUS_BAR == AFX_IDW_STATUS_BAR);
ASSERT(ID_VIEW_TOOLBAR == AFX_IDW_TOOLBAR);
//得到工具条或者状态条
CControlBar* pBar = GetControlBar(nID);
if (pBar != NULL)
{
//若控制条可见,则隐藏它;否则,显示它
ShowControlBar(pBar, (pBar->GetStyle() & WS_VISIBLE) == 0, FALSE);
//处理完毕
return TRUE;
}
//可以让下一个命令目标继续处理
return FALSE;
}
由于是扩展映射宏定义的消息处理函数,所以OnBarCheck函数有一个UINT类型的参数和一个BOOL返回值。
当用户从“View”菜单选择打了钩的“Toolbar”时,消息处理函数OnBarCheck被调用,参数就是菜单项的ID号ID_VIEW_TOOLBAR,它等于工具条的子窗口IDAFX_IDW_TOOLBAR。处理结果,工具条被隐藏;当再次选择该菜单项则工具条被显示。
处理状态条的过程类似于工具条的处理。
ShowControlBar是CFrameWnd的成员函数,参数1表示控制条对象指针,参数2表示显示(TRUE)或者隐藏(FALSE),参数3表示是立即显示(FALSE)或者延迟显示(TRUE)。
如果工具条或者状态条被隐藏,则相应的菜单项ID_VIEW_STATUS_BAR 或者ID_VIEW_TOOLBAR 变成uncheked(菜单项被标记为没有选择),否则,checked(菜单项被标记选择)。CFrameWnd实现了这两个菜单项的状态更新处理,列举其中一个如下:
声明处理ID_VIEW_TOOLBAR的状态更新消息:
ON_UPDATE_COMMAND_UI(ID_VIEW_TOOLBAR, OnUpdateControlBarMenu)
函数的实现:
void CFrameWnd::OnUpdateControlBarMenu(CCmdUI* pCmdUI)
{
ASSERT(ID_VIEW_STATUS_BAR ==
AFX_IDW_STATUS_BAR);
ASSERT(ID_VIEW_TOOLBAR == AFX_IDW_TOOLBAR);
CControlBar* pBar = GetControlBar(pCmdUI->m_nID);
//存在工具栏
if (pBar != NULL)
{
//工具条窗口被显示则checked,被隐藏则uncheked
pCmdUI->SetCheck((pBar->GetStyle() & WS_VISIBLE) != 0);
return;
}
pCmdUI->ContinueRouting();
}
GetControlBar是CFrameWnd的成员函数,用来返回边框窗口的指定ID的控制条对象(指定ID是控制条的子窗口ID)。
工具条可以泊位在边框窗口的任一边(上、下、左、右),或者漂浮在屏幕上的任何地方。
首先,边框窗口调用CFrameWnd::EnableDocking函数使控制条泊位在边框窗口中有效,指明在边框窗口的哪边接受泊位。如果想在任何边都可以泊位,则使用参数CBRS_ALIGN_ANY。
然后,工具条调用ControlBar::EnableDocking使泊位对工具条有效,如果在调用ControlBar::EnableDocking时指定的泊位目的边和边框窗口能够泊位的边不符合,那么工具条不能泊位,它将漂浮。
最后,边框窗口调用CFrameWnd::DockControlBar泊位工具条。
边框窗口、泊位条、工具条的包含关系如下:
边框窗口
泊位条1
工具条1
工具条2
…
泊位条2
…
边框窗口包含1到4个泊位条子窗口,每个泊位条包含若干个控制条子窗口。
CFrameWnd:: EnableDocking指定哪边接受泊位,则为泊位准备一个泊位条。泊位条用CDockBar描述,派生于CControlBar。如果指定任何边都可以泊位,则创建四个CDockBar对象和对应的HWND窗口。然后,调用ControlBar::EnableDocking在对应的泊位条内安置工具条。
MFC设计了CDockBar类和CFrameWnd的一些函数来实现泊位,具体代码实现在此不作详细讨论。
边框窗口调用FloatControlBar实现工具条的漂浮。
首先,创建一个微型漂浮边框窗口,该边框窗口有一个泊位条。
然后,在微型边框窗口的泊位条内放置工具条。
MFC设计了微型边框类CMiniFrameWnd,在此基础上派生出微型泊位边框窗口类CMiniDockFrameWnd。CMiniDockFrameWnd增加了一个CDockBar类型成员变量m_wndDockBar,即泊位条。
在CMiniDockFrameWnd对象被创建时,创建泊位条m_wndDockBar。泊位条 m_wndDockBar的父窗口如同CMiniDockFrameWnd的父窗口一样,是调用FloatControlBar的边框窗口,而不是微型泊位边框窗口。微型边框窗口和泊位条创建完成之后,调用ControlBar::DockControlBar泊位工具条在 CMiniDockFrameWnd窗口。
具体的代码实现略。