65.9K
CodeProject 正在变化。 阅读更多。
Home

用 MFC 控制客户区最小尺寸

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.48/5 (9投票s)

2006年2月14日

7分钟阅读

viewsIcon

60982

downloadIcon

919

使用 MFC 中的 WM_GETMINMAXINFO 消息控制客户端视图的最小尺寸可能很棘手。本文提出了一种优雅且可重用的解决方案。

目录

引言

有时候,看似简单的问题反而最难找到解决方案,这是否很奇怪?试图将客户端视图限制在最小尺寸就是这样一类问题。我尝试了许多方法,才找到了一个令人满意的解决方案。最终的解决方案仍然不完美,稍后我会解释原因。在本文中,我将提供一些关于问题的背景信息,并描述我在解决问题的过程中遇到的各种困难,介绍我最初尝试解决问题的方法。然后,我将展示最终解决方案的内部工作原理,并深入探讨其设计思路。之后,我将介绍演示程序,旨在展示如何在您自己的程序中使用这些代码,最后,我将讨论当前代码的局限性和可能的改进之处。

背景

我尝试的第一个简单尝试是在子视图类中直接处理 WM_GETMINMAXINFO 消息,代码如下:

void CChildView::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI) 
{
    lpMMI->ptMinTrackSize.x = DEFAULTMINCLIENTSIZE;
    lpMMI->ptMinTrackSize.y = DEFAULTMINCLIENTSIZE;
}

如果能这么简单就好了!这样做根本无效,因为只有可调整大小的顶级窗口才会接收 WM_GETMINMAXINFO 消息,而 CChildView 并非此类窗口。这是解决问题的第一个困难。用 MFC 解决这个问题的第二个困难是,MFC 将框架的客户端区域分割给不同的 UI 组件(视图、状态栏和工具栏),而该区域的大部分管理由 MFC 在后台进行,并且几乎没有文档记录。我的第二次尝试试图绕过这个困难,代码如下:

CMainFrame::CMainFrame()
{
    minX = DEFAULTMINCLIENTSIZE;
    minY = DEFAULTMINCLIENTSIZE;
}

void CMainFrame::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI) 
{
    CView *pView = GetActiveView();
    if( pView )
    {
        RECT viewRect;
        pView->GetClientRect(&viewRect);
        if( viewRect.right < DEFAULTMINCLIENTSIZE )
        {
            minX += (DEFAULTMINCLIENTSIZE - viewRect.right);
        }
        if( viewRect.bottom < DEFAULTMINCLIENTSIZE )
        {
            minY += (DEFAULTMINCLIENTSIZE - viewRect.bottom);
        }

    }
    lpMMI->ptMinTrackSize.x = minX;
    lpMMI->ptMinTrackSize.y = minY;
}

这能产生一个不错的结果,但并不十分准确。例如,对于一个带有工具栏和状态栏的应用程序,如果您减小框架的垂直尺寸,它会略微低于 DEFAULTMINCLIENTSIZE,然后又会弹回指定的高度。此外,如果您在达到最小尺寸后移除状态栏和工具栏,您会发现即使视图尺寸大于指定的最小尺寸,窗口仍然无法进一步缩小。在第二次尝试之后,我在网上搜索了一下,看看其他人是如何解决这个问题的。我发现了一篇 来自 ovidiucucu 的文章,他提出了以下代码来解决我遇到的问题:

void CChildFrame::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI) 
{
  // the minimum client rectangle (in that is lying the view window)
  CRect rc(0, 0, 200, 150);
  // compute the required size of the frame window rectangle
  // based on the desired client-rectangle size
  CalcWindowRect(rc);

  lpMMI->ptMinTrackSize.x = rc.Width();
  lpMMI->ptMinTrackSize.y = rc.Height();
}

听起来不错,但也没用。正如我之前所说,框架的客户端矩形由状态栏、工具栏和视图共享。所以,如果您的框架客户端区域只有一个视图,它就能工作,否则就不行。现在,通过这些不同的尝试,应该可以清楚地看出,要解决这个问题,唯一的办法就是深入研究 MFC 的内部机制,以便精确地跟踪每个客户端区域组件的大小和位置。这正是我代码所做的,我将在下一节中向您展示如何做到这一点。

代码

这一节是可选的。如果您只对使用代码感兴趣,而不关心实现细节,可以随时跳到下一节。您还在吗?太棒了!优秀的程序员都很好奇。在描述 CMinMaxFrame 类需要完成的各项任务之前,我将用一个小类图向您展示整体代码组织。

每次使用 MFC 时,我都强制自己采用一种小型设计模式,即将所有与 MFC 无关的内容解耦到一个名为 CSomethingLogic 的类中。这样,如果以后将代码移植到不同的框架,移植会更容易。话虽如此,设计还是很简单的。CMinMaxFrame 派生自 CFrameWnd 并使用 CMinMaxLogic。要使用此代码,您需要将主框架类派生自 CMinMaxFrameCMinMaxFrame 的职责包括:

  • 跟踪状态栏的大小和可见状态。
  • 跟踪工具栏的大小、可见状态以及其位置(未停靠、已停靠;如果已停靠,则停靠在哪个框架侧)。
  • 处理 WM_GETMINMAXINFO 消息,并根据前几点的信息计算框架大小,以获得所需的客户端视图大小。

现在您知道了代码必须做什么,我将展示代码本身。代码中包含大量注释,应该可以自学,但我可能会提供更多见解。首先,这是类声明:

/*
 * class CMinMaxLogic
 *
 * It is used with the class CMinMaxFrame. Its purpose is to isolate
 * everything that is not related to MFC to ease an eventual porting
 * to another framework (ie.: WTL).
 *
 * Note: This class assumes that the associated frame has a menu and the
 * following Window Styles:
 *
 * - WS_BORDER
 * - WS_CAPTION
 * - WS_THICKFRAME
 *
 * This condition should always be met since the MFC AppWizard
 * generated code is using WS_OVERLAPPEDWINDOW that includes all 3 styles
 * to create the frame window.
 */
class CMinMaxLogic
{
public:
    CMinMaxLogic(LONG x, LONG y);
    ~CMinMaxLogic(void);

/*********************************************************
 *
 * Name      : setClientMin
 *
 * Purpose   : Compute the minimum frame size
 *             from the provided minimum client
 *             area size. It is called at construction
 *             and can be recalled anytime
 *             by the user.
 *
 * Parameters:
 *     x       (LONG) Minimum client horizontal size.
 *     y       (LONG) Minumum client vertical size.
 *
 * Return value : None.
 *
 ********************************************************/
    void setClientMin(LONG x, LONG y );

/********************************************************
 *
 * Name      : OnGetMinMaxInfo
 *
 * Purpose   : Set the minimum size
 *             to the minimum frame size and make
 *             adjustments based on the toolbar
 *             and status bar visibility
 *             state and their sizes.
 *
 * Parameters:
 *     lpMMI  (MINMAXINFO FAR*) MinMax info 
 *                        structure pointer.
 *
 * Return value : None.
 *
 *******************************************************/
    void OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI);

    BOOL  m_sbVisible; /* Status bar visibility      */
    LONG  m_sbHeight;  /* Status bar height          */
    BOOL  m_tbVisible; /* Toolbar visibility         */
    int   m_tbPos;     /* Toolbar position (left, 
                                 right, top, bottom) */
    int   m_tbSize;    /* Toolbar size               */
private:
    LONG  m_cxMin;     /* Minimum client size        */
    LONG  m_cyMin;
    LONG  m_fyMin;     /* Minimum frame size
                          that includes 
                          borders, the frame, 
                          the toolbar                */
    LONG  m_fxMin;     /* and the status bar to have
                          a client area 
                          of m_cxMin*m_cyMin         */
};

#define DEFAULTMINCLIENTSIZE 350

class CMinMaxFrame : public CFrameWnd
{
public:
    CMinMaxFrame( LONG minX = DEFAULTMINCLIENTSIZE,
                  LONG minY = DEFAULTMINCLIENTSIZE );

/********************************************************
 *
 * Name      : setClientMin
 *
 * Purpose   : Recompute the minimum frame size
 *             from the newly provided minimum
 *             client area size. It can be called
 *             anytime by the user.
 *
 * Parameters:
 *     x       (LONG) Minimum client horizontal size.
 *     y       (LONG) Minumum client vertical size.
 *
 * Return value : None.
 *
 *******************************************************/
    void setClientMin(LONG x, LONG y )
    {
        m_MinMaxLogic.setClientMin(x,y);
    }

/********************************************************
 *
 * Name      : setToolBar
 *
 * Purpose   : Register the toolbar to monitor
 *             for adjusting the minimum frame
 *             size to respect the requested
 *             the minimum client area size.
 *
 * Note      : Currently only 1 toolbar
 *             is supported but more could be
 *             supported with the help of a toolbar list.
 *
 * Parameters:
 *     pTB     (CToolBar *) Toolbar to register.
 *
 * Return value : None.
 *
 *********************************************************/
    void setToolBar( CToolBar *pTB )
    {
        m_pTB = pTB;
        if( pTB )
        {
            m_MinMaxLogic.m_tbPos = TBFLOAT;
        }
        else
        {
            m_MinMaxLogic.m_tbPos = TBNOTCREATED;
        }
    }

/**********************************************************
 *
 * Name      : setStatusBar
 *
 * Purpose   : Register the status bar to monitor
 *             for adjusting the minimum
 *             frame size to respect the requested
 *             the minimum client area
 *             size.
 *
 * Parameters:
 *     pST     (CStatusBar *) Status bar to register.
 *
 * Return value : None.
 *
 *********************************************************/
    void setStatusBar( CStatusBar *pST )
    {
        // Compute the status bar height
        if( pST )
        {
            m_MinMaxLogic.m_sbHeight = 
                  pST->CalcFixedLayout(TRUE,TRUE).cy;
        }
        else
        {
            m_MinMaxLogic.m_sbHeight = 0;
        }
    }

// Overrides
/**********************************************************
 *
 * Name      : RecalcLayout
 *
 * Purpose   : This function is called
 *             by the MFC framework whenever a
 *             toolbar status is changing
 *             (is attached or detached to/from
 *             the frame). It is used as
 *             a hook to maintain this class
 *             internal state concerning
 *             the toolbar position and size.
 *             It should not be called directly.
 *
 * Parameters:
 *     bNotify (BOOL) Not used.
 *
 * Return value : None.
 *
 *********************************************************/
    virtual void RecalcLayout(BOOL bNotify = TRUE);
protected:
    afx_msg void OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI);
    afx_msg BOOL OnBarCheck(UINT nID);
    DECLARE_MESSAGE_MAP()
private:
    CMinMaxLogic m_MinMaxLogic;
    CToolBar    *m_pTB;

    // TB Functions
    void triggerGetMinMaxInfoMsg(void);
    int getTBSize(int pos);
    int findDockSide(void);
};

第一项任务(跟踪状态栏的大小和可见状态)是最简单的,所以我们先解决它。由于状态栏的垂直尺寸通常不会改变,所以只需要通过调用 CStatusBar::CalcFixedLayout() 来存储其值。这在 CMinMaxFrame::setStatusBar() 中完成。

/**********************************************************
 *
 * Name      : setStatusBar
 *
 * Purpose   : Register the status bar
 *             to monitor for adjusting the minimum
 *             frame size to respect the requested
 *             the minimum client area size.
 *
 * Parameters:
 *     pST     (CStatusBar *) Status bar to register.
 *
 * Return value : None.
 *
 *********************************************************/
    void setStatusBar( CStatusBar *pST )
    {
        // Compute the status bar height
        if( pST )
        {
            m_MinMaxLogic.m_sbHeight = 
                  pST->CalcFixedLayout(TRUE,TRUE).cy;
        }
        else
        {
            m_MinMaxLogic.m_sbHeight = 0;
        }
    }

可见状态是通过处理视图菜单项 ID_VIEW_STATUS_BAR 来获取的。这在 CMinMaxFrame::OnBarCheck() 中完成。

/*
 * CMinMaxFrame::OnBarCheck function
 *
 * Purpose   : MFC defined message handler. It is called whenever a toolbar
 *             or a status bar visibility state change. It is used to trigger
 *             a WM_GETMINMAXINFO since the minimum frame size to maintain a
 *             minimum client area size has changed.
 */
BOOL CMinMaxFrame::OnBarCheck(UINT nID) 
{
    BOOL res = CFrameWnd::OnBarCheck(nID);

    // TODO: Add your command handler code here
    if( nID == ID_VIEW_STATUS_BAR )
    {
        m_MinMaxLogic.m_sbVisible = !m_MinMaxLogic.m_sbVisible;
        if( m_MinMaxLogic.m_sbVisible )
        {
            triggerGetMinMaxInfoMsg();
        }
    }
    else if( nID == ID_VIEW_TOOLBAR )
    {
        m_MinMaxLogic.m_tbVisible = !m_MinMaxLogic.m_tbVisible;
        if( m_MinMaxLogic.m_tbVisible )
        {
            triggerGetMinMaxInfoMsg();
        }
    }

    return res;
}

同一个函数也用于跟踪工具栏的可见状态。这里有一个假设。代码假设在启动时,两个栏都可见,但这可能并非总是如此。这方面应该有一天得到改进。如果其中一个栏变得可见,则必须重新计算框架的最小尺寸,这就是 triggerGetMinMaxInfoMsg() 的作用。

/*
 * CMinMaxFrame::triggerGetMinMaxInfoMsg function
 */
void CMinMaxFrame::triggerGetMinMaxInfoMsg()
{
    /*
     * Trigger a WM_MINMAXINFO message by calling the function MoveWindow()
     * with the current frame size. The purpose of generating a call to the
     * WM_GETMINMAXINFO handler is to verify that the new client area size
     * still respect the minimum size.
     */
    RECT wRect;
    GetWindowRect(&wRect);
    MoveWindow(&wRect);
}

现在,最难的部分是跟踪工具栏的位置和大小。即使没有文档记录,CFrameWnd 的虚拟函数 RecalcLayout() 也会在工具栏状态更改时被调用。CMinMaxFrame 利用这一点来获取通知:

/*
 * CMinMaxFrame::RecalcLayout function
 *
 * Purpose   : This function is called by the MFC framework whenever a
 *             toolbar status is changing (is attached or detached to/from
 *             the frame). It is used as a hook to maintain this class
 *             internal state concerning the toolbar position and size.
 *             It should not be called directly.
 */
void CMinMaxFrame::RecalcLayout(BOOL bNotify) 
{    
    CFrameWnd::RecalcLayout(bNotify);

    // TODO: Add your specialized code here and/or call the base class
    if( m_MinMaxLogic.m_tbPos != TBNOTCREATED )
    {
        if( !m_pTB->IsFloating() )
        {
            int newPos = findDockSide();
            if( m_MinMaxLogic.m_tbPos != newPos )
            {
                m_MinMaxLogic.m_tbPos  = newPos;
                m_MinMaxLogic.m_tbSize = getTBSize(m_MinMaxLogic.m_tbPos);

                triggerGetMinMaxInfoMsg();
            }
        }
        else
        {
            m_MinMaxLogic.m_tbPos  = TBFLOAT;
            m_MinMaxLogic.m_tbSize = 0;
        }
    }
}

/*
 * CMinMaxFrame::findDockSide function
 *
 * Note: This function is using AFXPRIV. It might not be working anymore
 *       with a future MFC version.
 */
#include "afxpriv.h"

int CMinMaxFrame::findDockSide()
{
    // dwDockBarMap
    static const DWORD dwDockBarMap[4] =
    {
        AFX_IDW_DOCKBAR_TOP,
        AFX_IDW_DOCKBAR_BOTTOM,
        AFX_IDW_DOCKBAR_LEFT,
        AFX_IDW_DOCKBAR_RIGHT
    };

    int res = TBFLOAT;

    for( int i = 0; i < 4; i++ )
    {
        CDockBar *pDock = (CDockBar *)GetControlBar(dwDockBarMap[i]);
        if( pDock != NULL )
        {
            if( pDock->FindBar(m_pTB) != -1 )
            {
                res = i;
                break;
            }
        }
    }
    return res;
}

/*
 * CMinMaxFrame::getTBSize function
 *
 * Purpose   : Returns the horizontal or the vertical toolbar size based on the
 *             toolbar position.
 */
int CMinMaxFrame::getTBSize(int pos)
{
    int res;

    CSize cbSize = m_pTB->CalcFixedLayout(FALSE,
                                       (pos==TBTOP||pos==TBBOTTOM)?TRUE:FALSE);
    if( pos == TBTOP || pos == TBBOTTOM )
    {
        res = cbSize.cy;
    }
    else
    {
        res = cbSize.cx;
    }

    return res;
}

CMinMaxFrame::findDockSize() 函数中存在一个潜在问题。该函数使用了未文档化的函数和私有类。此解决方案已在 MFC 6 和 MFC 7 中进行了测试,但不能保证在未来的 MFC 版本中继续有效。我仍在寻找执行此任务的“官方”方法,但不知道是否有其他方法。CFrameWnd 为框架的每一侧包含一个 CDockBar 对象,MFC 通过这些对象来知道工具栏的位置。顺便说一句,您可能想知道我是如何获得进行此操作的知识的。我从《 MFC Internals》一书中获得了这些信息。当然,您也可以自己深入研究 MFC 源代码来找出所有信息,但有一本突出 MFC 工作原理重要内容的书可以节省大量时间。您应该认真考虑拥有这本书。当您尝试使用 MFC 完成某些事情而似乎没有明显方法时,这本书将是您极大的帮助。

演示程序

本节的目的是介绍使用 CMinMaxFrame 类的步骤。演示程序只是一个普通的 MFC 向导生成的程序,已修改为使用 CMinMaxFrame。以下是使用 CMinMaxFrame 所需的步骤:

  • 编辑您的框架类头文件和 CPP 文件,将所有 CFrameWnd 实例替换为 CMinMaxFrame
  • 在您的 OnCreate() 处理程序中,在创建完工具栏和状态栏后,调用 CMinMaxFrame::setToolBar()CMinMaxFrame::setStatusBar()
  • CMinMaxFrame 构造函数中指定客户端视图的最小尺寸,或者随时调用 CMinMaxFrame::setClientMin() 函数。

就这样!就是这么简单。

局限性和改进建议

尽管我对最终结果相当满意,但这段代码仍然不完美。以下是可以改进的地方列表:

  • 支持多个工具栏(使用列表)。
  • 支持最大尺寸。
  • 重写 PreCreateWindow() 函数,以确保三个强制性窗口样式标志始终存在。
  • 使用文档化的 MFC 功能,因为当前解决方案可能不再适用于未来的 MFC 版本。

写这篇文章时,我心中有两个目标。首先,帮助那些遇到和我一样问题的程序员朋友。其次,我希望如果您找到了改进代码的方法,能够得到您的反馈。如果这样做,我将更新本文并包含您的改进。

结论

就是这样!希望您喜欢这篇文章,如果您喜欢并觉得它有用,请花几秒钟为其评分。您可以在文章底部进行评分。此外,我邀请您访问我的 网站,查看本文的更新。

参考文献

历史

  • 02-13-2006
    • 原始文章。
© . All rights reserved.