Contents
Isn't it strange that sometimes, problems that seem easy at first are the ones causing the most trouble to find a solution for? Trying to restrict a client view to a minimum size is one of those problems. I have tried many approaches before reaching a satisfactory solution. The final solution is still not perfect and I will explain why later. In this article, I will give some background on the problem and describe the various difficulties that I have encountered on the road, by presenting my first attempts to solve the problem. Then I will present the inner working of the final solution and give insights on why it has been designed like it is. I will then continue by presenting the demo program whose purpose is to show how to use the code in your own programs, and finally, I will discuss the limitations and the possible enhancements to the current code.
The first nave attempt I tried was to handle the WM_GETMINMAXINFO
message directly in the child view class, and it looked like this:
void CChildView::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI)
{
lpMMI->ptMinTrackSize.x = DEFAULTMINCLIENTSIZE;
lpMMI->ptMinTrackSize.y = DEFAULTMINCLIENTSIZE;
}
If only it was that simple! It does not work at all because only resizable top most windows receive the WM_GETMINMAXINFO
message, and CChildView
is not such a window. This is the first difficulty to solve the problem. The second difficulty to solve this problem with MFC is that MFC splits the frame client area among different UI components (views, status bar, and the toolbars), and most of the management of that area is done by MFC under the hood and is practically not documented. My second attempt tried to workaround that difficulty and it looked like this:
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;
}
It gives a descent result but it is not very accurate. For instance, with an application having a toolbar and a status bar, if you were to reduce the vertical size of the frame, it would go a little below DEFAULTMINCLIENTSIZE
and then snaps back to the specified minimum height. Also, if you were to remove the status bar and the toolbar after reaching the minimum size, you would find that the window refuses to minimize further even if the view size is above the specified minimum size. After that second attempt, I searched a little bit on the net to see what solutions others have come up with the same problem. I found one article from ovidiucucu and he proposes the following code for the problem that I am having:
void CChildFrame::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI)
{
CRect rc(0, 0, 200, 150);
CalcWindowRect(rc);
lpMMI->ptMinTrackSize.x = rc.Width();
lpMMI->ptMinTrackSize.y = rc.Height();
}
It sounds good but it does not work either. As I previously said, the frame client rectangle is shared among the status bar, the toolbars, and the view. So if your frame client area only has a view, it will work, otherwise it will not. Now with these different attempts, it should start to become clear that the only way to solve this problem is to put our hands in the MFC guts to be able to exactly keep track of every client area component's size and position. This is exactly what my code is doing, and I will show you how in the next section.
This section is optional. If you are only interested in using the code and do not care about the implementation, you can feel free to skip to the next section. Are you still there? Excellent! Good programmers are curious. Before describing the different tasks that the class CMinMaxFrame
needs to fulfill, I am going to present you the overall code organization with a small class diagram.
Whenever I work with MFC, I impose myself this small design pattern where I decouple everything that is not related to MFC in a class called CSomethingLogic
. That way, if I ever port the code to a different framework, it should make the port easier. That being said, the design is simple. CMinMaxFrame
is derived from CFrameWnd
and is using CMinMaxLogic
. In order to use the code, you have to derive your main frame class from CMinMaxFrame
. CMinMaxFrame
responsibilities are:
- Keep track of the status bar size and visibility state.
- Keep track of the toolbar size, visibility state, and its position (undocked, docked; if docked, on which frame side it is docked on).
- Handle the
WM_GETMINMAXINFO
message and compute the frame size to have the requested client view size based on the information from the previous points.
Now that you know what the code must do, I will present the code itself. The code contains plenty of comments and should be self-explanatory but I will probably have some more insights to provide as well. First, here are the class declarations:
class CMinMaxLogic
{
public:
CMinMaxLogic(LONG x, LONG y);
~CMinMaxLogic(void);
void setClientMin(LONG x, LONG y );
void OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI);
BOOL m_sbVisible;
LONG m_sbHeight;
BOOL m_tbVisible;
int m_tbPos;
int m_tbSize;
private:
LONG m_cxMin;
LONG m_cyMin;
LONG m_fyMin;
LONG m_fxMin;
};
#define DEFAULTMINCLIENTSIZE 350
class CMinMaxFrame : public CFrameWnd
{
public:
CMinMaxFrame( LONG minX = DEFAULTMINCLIENTSIZE,
LONG minY = DEFAULTMINCLIENTSIZE );
void setClientMin(LONG x, LONG y )
{
m_MinMaxLogic.setClientMin(x,y);
}
void setToolBar( CToolBar *pTB )
{
m_pTB = pTB;
if( pTB )
{
m_MinMaxLogic.m_tbPos = TBFLOAT;
}
else
{
m_MinMaxLogic.m_tbPos = TBNOTCREATED;
}
}
void setStatusBar( CStatusBar *pST )
{
if( pST )
{
m_MinMaxLogic.m_sbHeight =
pST->CalcFixedLayout(TRUE,TRUE).cy;
}
else
{
m_MinMaxLogic.m_sbHeight = 0;
}
}
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;
void triggerGetMinMaxInfoMsg(void);
int getTBSize(int pos);
int findDockSide(void);
};
The first task (keep track of the status bar size and visibility state) is the easiest task, so let's get rid of this one first. Since a status bar vertical size does not usually change, all that is needed is to store its value by calling CStatusBar::CalcFixedLayout()
. This is done in CMinMaxFrame::setStatusBar()
:
void setStatusBar( CStatusBar *pST )
{
if( pST )
{
m_MinMaxLogic.m_sbHeight =
pST->CalcFixedLayout(TRUE,TRUE).cy;
}
else
{
m_MinMaxLogic.m_sbHeight = 0;
}
}
The visibility state is acquired by handling the view menu item ID_VIEW_STATUS_BAR
. This is done in CMinMaxFrame::OnBarCheck()
:
BOOL CMinMaxFrame::OnBarCheck(UINT nID)
{
BOOL res = CFrameWnd::OnBarCheck(nID);
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;
}
The same function is also used for tracking the toolbar visibility state. There is an assumption that is made here. The code assumes that at startup, both bars are visible which might not always be the case. This aspect should be improved someday. In the case that one of the bars becomes visible, the frame minimum size must be recomputed and this is what triggerGetMinMaxInfoMsg()
is doing:
void CMinMaxFrame::triggerGetMinMaxInfoMsg()
{
RECT wRect;
GetWindowRect(&wRect);
MoveWindow(&wRect);
}
Now, the hardest part that is to keep track of the position and size of the toolbar. Even if it is not documented, the CFrameWnd
virtual function RecalcLayout()
is called every time that a toolbar state is changing. CMinMaxFrame
is using this knowledge to get notified when this happens:
void CMinMaxFrame::RecalcLayout(BOOL bNotify)
{
CFrameWnd::RecalcLayout(bNotify);
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;
}
}
}
#include "afxpriv.h"
int CMinMaxFrame::findDockSide()
{
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;
}
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;
}
There is a potential problem in the function CMinMaxFrame::findDockSize()
. It is that the function is using a non documented function and a private class. This solution has been tested with MFC 6 and MFC 7 but there is no guarantee that it will continue to work with future versions of MFC. I am still looking for an "official" way to perform this task but I am not aware of another way to do it. CFrameWnd
contains one CDockBar
object for each side of the frame, and it is with these objects that MFC knows where the toolbar is located. By the way, you might wonder how I got the knowledge to pull off this stunt. I got this information from the book MFC Internals. You could, of course, just dig into the MFC source code and find everything yourself, but having a book that highlights important stuff in how MFC is working is a big timesaver. You should seriously consider having this book. It will be a blessing when you try to achieve something with MFC and there does not seem to be an obvious way to do it.
This section's purpose is to present the recipe for using the CMinMaxFrame
class. The demo program is just a plain vanilla MFC wizard generated program that has been modified to use CMinMaxFrame
. Here are the steps needed to use CMinMaxFrame
:
- Edit your frame class header file and CPP file to replace every
CFrameWnd
occurrence for CMinMaxFrame
.
- Call
CMinMaxFrame::setToolBar()
and CMinMaxFrame::setStatusBar()
in your OnCreate()
handler once the bars have been created.
- Specify the minimum size of your client view to the
CMinMaxFrame
constructor, or call the function CMinMaxFrame::setClientMin()
anytime you want.
This it it! It is that simple.
Even if I am quite happy with the end result, this code is still not perfect. Here is a list of things that could be done:
- Support more than one toolbar with a list.
- Support maximum size.
- Override the
PreCreateWindow()
function to make sure that the three mandatory window style flags are always present.
- Use documented MFC features as the current solution might not work anymore with future MFC releases.
When I wrote this article I had two goals in mind. First, help my fellow programmers that have encountered the same problem I did. Secondly, I hope to receive feedback from you if you find ways to improve the code. If you do so, I will update this article with your improvements.
That is it! I hope you enjoyed this article, and if you did and found it useful, please take a few seconds to rank it. You can do so right at the bottom of the article. Also, I invite you to visit my website to check for any updates of this article.
- ovidiucucu, How to prevent a resizable window to be smaller than...?
- 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.