用 MFC 控制客户区最小尺寸






4.48/5 (9投票s)
2006年2月14日
7分钟阅读

60982

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
。要使用此代码,您需要将主框架类派生自 CMinMaxFrame
。CMinMaxFrame
的职责包括:
- 跟踪状态栏的大小和可见状态。
- 跟踪工具栏的大小、可见状态以及其位置(未停靠、已停靠;如果已停靠,则停靠在哪个框架侧)。
- 处理
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 版本。
写这篇文章时,我心中有两个目标。首先,帮助那些遇到和我一样问题的程序员朋友。其次,我希望如果您找到了改进代码的方法,能够得到您的反馈。如果这样做,我将更新本文并包含您的改进。
结论
就是这样!希望您喜欢这篇文章,如果您喜欢并觉得它有用,请花几秒钟为其评分。您可以在文章底部进行评分。此外,我邀请您访问我的 网站,查看本文的更新。
参考文献
- ovidiucucu,如何防止可调整大小的窗口小于...?
- George Shepherd, Scot Wingo, MFC Internals, Addison Wesley, 1996.
- Jeff Prosise, Programming Windows with MFC, Second Edition Microsoft Press, 1999.
- Charles Petzold, Programming Windows, Fifth Edition, Microsoft Press, 1999.
历史
- 02-13-2006
- 原始文章。