Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Control client area minimum size with MFC

0.00/5 (No votes)
14 Feb 2006 1  
Controlling the client view minimum size with WM_GETMINMAXINFO in MFC can be tricky. This article proposes an elegant and reusable solution to this problem.

Contents

Introduction

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.

Background

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) 
{
  // 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();
}

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.

The code

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

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():

/**********************************************************
 *
 * 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;
        }
    }

The visibility state is acquired by handling the view menu item ID_VIEW_STATUS_BAR. This is done in 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;
}

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:

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

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:

/*
 * 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;
}

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.

The demo program

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.

Limitations and suggestions for enhancements

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.

Conclusion

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.

Bibliography

History

  • 02-13-2006
    • Original article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here