1. MFC工具条和状态栏

     

       

    1. 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窗口。

       

    1. 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 toolbarn");

return -1; // fail to create

}

//创建状态栏

if (!m_wndStatusBar.Create(this) ||

!m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)))

{

TRACE0("Failed to create status barn");

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如何实现共性,如何支持派生类的特定要求,派生类又如何实现自己的特定需求等。

         

      1. 控制窗口的创建

         

        创建工具条、状态条、对话框工具栏的方法是不同的,所以必须给每个派生类CToolBar、CStatusBar、CDialogBar设计和实现自己的窗口创建函数Create。但是,它们是也是有共性的,共性由CControlBar的PreCreateWindow处理。在窗口创建之后,各个派生类都要进行的处理(共性)由CControlBar的OnCreate完成,特别的处理通过派生类的OnNcCreate完成。

           

        1. 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。

          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_风格。

           

        2. 控制条的窗口创建

           

CControlBar派生类实现了自己的窗口创建函数Create,CControlBar的PreCreateWindow被派生类的Create函数直接或者间接地调用。以CToolBar为例讨论窗口创建函数和创建过程。

     

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

     

  2. 处理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函数。

     

  3. 处理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,省却了消息发送的过程。

    在控制窗口创建之后,对于工具条来说,下一步就是向工具栏添加按钮。

     

  4. 向工具栏添加按钮

     

    通过函数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也是如此,这里不作讨论。

     

  5. 状态栏和对话框工具栏的创建

     

至此,分析了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创建过程的具体实现,这里不作详细讨论了。

         

      1. 控制条的销毁

         

        描述了控制条的创建,顺便考察其销毁的设计。

        工具条、状态栏等这些控制窗口都要使用DestroyWindow来销毁,所有有关操作集中由CControlBar处理。CControlBar覆盖了虚拟函数DestroyWindow、PostNcDestroy和消息处理函数OnDestroy。

        当然,各个派生类的虚拟析构函数被实现。如果成员变量m_bAutoDelete为TRUE,则动态创建的MFC窗口将自动销毁。

         

      2. 处理控制条的位置

         

           

        1. 计算控制条位置的过程和算法

           

          工具条等控制条是作为一个子窗口在父边框窗口内显示的。为了处理控制条的布置(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作一些分析。

           

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

           

        3. 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.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,这里不作进一步的分析。

         

      3. 工具条、状态栏和边框窗口的接口

         

           

        1. 应用程序在状态栏中显示信息

           

MFC内部通过给边框窗口发送消息WM_SETMESSAGESTRING、WM_POPMESSAGESTRING的方式在状态栏中显示信息。这两个消息在afxpriv.h里头定义。

WM_SETMESSAGESTRING消息表示在状态栏中显示和某个ID对应的字符串信息或者指定的字符串信息,消息参数wParam指定了字符串资源ID,消息参数lParam指定了字符串指针,两个消息参数只有一个有用。一般,一个命令ID对应了一个字符串ID,对应的字符串是命令ID的说明。

消息WM_POPMESSAGESTRING用来重新设置状态栏。

这两个消息对应的消息处理函数分别是OnSetMessageString和OnPopMessageString,OnSetMessageString和OnPopMessageString分别实现如下:

     

  1. 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显示,分隔符号是“n”。例如,字符串ID_APP_EXIT(对应“退出”菜单、按钮)是“Exit ApplicationnExit”,当鼠标落在“退出”按钮上时,状态栏显示“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;

    }

     

  2. 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);

}