MFC工具条和状态栏

http://51dev.com/program/cpp/20070818/7548.html
Windows控制窗口

  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工具条和状态栏类

  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实现了以下功能:

  和父窗口(边框窗口)顶部或者底部或者其他边对齐。

  可以包含子条目,这些条目或者是基于HWND子窗口,或者是基于非HWND条目。负责分配条目数组。

  支持CBRS_TOP(缺省,控制条放在顶部),CBRS_BOTTOM(放在底部),CBRS_NOALIGN(父窗口大小变化时不重新放置控制条)等几种控制风格。


  支持派生类实现。几个派生类有一定共性,或者其中两个有一定共性,这样CControlBar实现函数一部分只适用于某个派生类,一部分适用于两个或者多个派生类,还有一部分适用于所有派生类。所谓适用,这里指派生类直接继承了CControlBar实现,或者覆盖了其实现但是建立在扩展其实现基础上。类似地,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
");
return -1; // fail to create
}
//创建状态栏
if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)))
{
TRACE0("Failed to create status bar
");
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。但是,它们是也是有共性,共性由CControlBarPreCreateWindow处理。在窗口创建之后,各个派生类都要进行处理(共性)由CControlBarOnCreate完成,特别处理通过派生类OnNcCreate完成。

  PreCreateWindow

  首先,讨论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。

  CToolBarPreCreateWindow函数修改了窗口风格,也修改状态栏、工具栏等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,CControlBarPreCreateWindow被派生类Create函数直接或者间接地调用。以CToolBar为例讨论窗口创建函数和创建过程。

  CToolBar窗口创建函数Create

  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等函数。

  处理WM_CREATE消息

  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函数。

  处理WM_NCCREATE消息

  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创建工具条窗口过程。对于状态栏和对话框工具栏有类似步骤,但也有不同之处。

  CStatusBarCreate使用“msctls_statusbar32”“窗口类”创建状态栏,窗口ID为AFX_IDW_STATUS_BAR(0XE801),然后通过成员函数SetIndictors给状态栏分格,类似于给工具条添加按钮过程,它实际上是通过状态栏“窗口类”窗口过程完成

  CDialogBarCreate使用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作一些分析。

  CFrameWnd虚拟函数RecalcLayout

  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函数。一种情况是当前边框窗口为浮动控制条包容窗口(微型边框窗口)时;另一种情况是当前边框窗口为普通边框窗口时。


  CWnd成员函数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.
");
}

  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分别实现如下:

OnSetMessageString
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显示,分隔符号是“ ”。例如,字符串ID_APP_EXIT(对应“退出”菜单、按钮)是“Exit Application Exit”,当鼠标落在“退出”按钮上时,状态栏显示“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;
}
OnPopMessageString
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以便实现特定消息分发路径。

  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消息传递给边框窗口时,被解释成一个命令消息,相应命令处理函数被调用。


  PreTranslateMessage

  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详细解释见下一节讨论。

  Tooltip

  工具条(或对话框工具条)如果指定了CBRS_TOOLTIPS风格(创建时指定或者创建后用SetBarStyle设置),则当鼠标落在某个按钮上(或者对话框子控制窗口)时,在鼠标附近弹出一个文本框──Tooltip提示窗口。

  如果还指定了CBRS_FLYBY风格,则还在状态栏显示和按钮(或子控制窗口)ID对应字符串信息。当然,鼠标左键在某个按钮(或子控制窗口)按下时,也要在状态栏显示按钮提示信息,当左键弹起时,则重置状态栏状态。

  如前所述,Tooltip窗口是Windows控制窗口。MFC使用了CToolTipCtrl类封装TooltipHWND窗口。在一个线程生存期间,至多拥有一个Tooltip窗口,该窗口对象指针保存在线程状态成员变量m_pToolTip中。线程状态类AFX_THREAD_STATE析构函数如果检测到m_pToolTip,则销毁MFC窗口对象和相应Windows窗口对象。

  CWnd对Tooltip消息预处理

  为了支持Tooltip显示,CWnd提供了以下函数:EnableTooltip,CancelTooltip,PreTranslateMessage,FilterTooltipMessage,OnToolHitTest。

  EnableTooltip设置CBRS_TOOLTIP风格,相反CancelTootip取消这种风格。

  PreTranslateMessage调用了FilterTooltipMessage过滤Tooltip消息。

  OnToolHitTest是一个由CWnd定义虚拟函数。CToolBar通过覆盖该函数,来检测对话框工具栏控制子窗口或者工具栏按钮是否被选中、哪个被选中。

  CWndPreTranslateMessage在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这里不作详细论述了。

  处理TTN_NEEDTEXT通知消息

  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, '
');
}
//复制分离出文本
#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; //标识了NMHDRidFrom成员意义
} TOOLTIPTEXT, FAR *LPTOOLTIPTEXT;

  uflags如果等于TTF_IDISHWND,则表示通知消息来自对话框工具条一个子窗口,而不是包含工具条按钮。

  OnToolTipText根据子窗口ID或者工具条按钮对应ID,得到字符串ID。如前所述,字符串ID由两部分组成,第二部分用于Tooltip显示,分隔符号是“ ”。根据这种格式OnToolTipText分离出Tooltip文本。

  得到了Tooltip文本之后,可以有三种方法返回文本信息:把文本信息复制到szText缓冲区;把文本地址复制到lpszText;复制字符串资源ID到lpszText、复制包含资源实例句柄到hint。本函数采用了第一种方法。

  在得到了返回Tooltip文本之后,该文本在Tooltip窗口中被显示出来。

  其他OnToolHist等函数实现不作详细解释了。下面,讨论CBRS_FLYBY风格实现。

  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,然后调用CCmdUIDoUpdate(pTarget,…)给pTarget所指对象发送状态更新消息。为了完成具体状态更新,从CCmdUI派生出CToolCmdUI和CStatusCCmUI,它们实现了自己Enable、SetCheck等等。

  初始化控制窗口

  CControlBar使用OnInitialUpdate消息处理函数初始化控制窗口状态。

void CControlBar::OnInitialUpdate()
{
//在窗口显示之前,更新状态
OnIdleUpdateCmdUI(TRUE, 0L);
}

  CControlBar实现了OnInitialUpdate函数,通过它来处理WM_INITIALUPDATE消息。各个子类不必覆盖该消息处理函数。


  处理Idle消息更新工具条状态

  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);
}

  CToolBarOnUpdateCmdUI函数完成工具条按钮状态更新。它接受两个参数,参数1表示接收状态更新命令消息对象,由CControlBar函数OnIdleUpdateCmdUI传递过来,一般是边框窗口对象;参数2表示如果某条命令消息没有处理函数时,对应用户接口对象是否被禁止。


  OnUpdateCmdUI通过发送状态更新通知消息,逐个更新按钮状态。更新消息首先让工具条对象处理,如果没有处理话,送给边框窗口对象处理,导致状态更新命令消息处理函数被调用,参见4.4.5节。

  CStatusBarOnUpdateCmdUI类似于此。

  CDialogBarOnUpdateCmdUI则调用了虚拟函数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窗口。

 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值