Putting a Month Calendar Control on the MDI Client

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Environment: MFC/C++

This technique has been used in Windows NT 4.0, Windows XP and Windows 95, and built in Visual Studio 6.0 and Visual Studio .NET. It is written in C++ using MFC.

There are plenty of articles about subclassing the MDI client window and drawing on it, but I wanted to put a control there — specifically the Month Calendar control, using MFC. Adding the control was easy. Just create it in PreSubclassWindow(), size it in OnSize(), and destroy it in UnsubclassWindow(). Here’s how:

in MDIClient.h

class CMDICalendar : public CWnd
{
public:
  CMDICalendar();
  virtual ~CMDICalendar();
  HWND UnsubclassWindow();

protected:
  DECLARE_MESSAGE_MAP()
  virtual void PreSubclassWindow();
  CMonthCalCtrl m_ctrlCalendar;
  afx_msg void OnSize(UINT nType, int cx, int cy);
};

in MDIClient.cpp

#define ID_CALENDAR    4500

CMDICalendar::CMDICalendar()
{
}

CMDICalendar::~CMDICalendar()
{
}

BEGIN_MESSAGE_MAP(CMDICalendar, CWnd)
  ON_WM_SIZE()
END_MESSAGE_MAP()

void CMDICalendar::PreSubclassWindow()
{
  RECT rc;
  GetClientRect(&rc);
  VERIFY(m_ctrlCalendar.Create(WS_CHILD | WS_VISIBLE |
                               WS_CLIPSIBLINGS |
                               MCS_DAYSTATE | MCS_NOTODAYCIRCLE,
                               rc, this, ID_CALENDAR));
  DWORD colour = ::GetSysColor(COLOR_APPWORKSPACE);
  m_ctrlCalendar.SetColor(MCSC_BACKGROUND, colour);
  m_ctrlCalendar.SetColor(MCSC_MONTHBK, colour);
  CWnd::PreSubclassWindow();
}

void CMDICalendar::OnSize(UINT nType, int cx, int cy)
{
  CWnd::OnSize(nType, cx, cy);
  m_ctrlCalendar.MoveWindow(0, 0, cx, cy, FALSE);
}

HWND CMDICalendar::UnsubclassWindow()
{
  VERIFY(m_ctrlCalendar.DestroyWindow());
  return CWnd::UnsubclassWindow();
}

The calendar control is created in PreSubclassWindow() and destroyed in UnsubclassWindow(), which is an override, rather than a virtual function. This creation and destruction are necessary because we subclass the MDI client to show the Month Calendar control and unsubclass to restore the client to normal from a menu command. The background colour of the control is set to the same colour as the client. Refer to the calendar 1 program in the Downloads section.

This is quite easy, and it works — as long as you use the WS_CLIPSIBLINGS style for the control. But there are three major problems:

  1. When an MDI child window is minimized, it disappears.
  2. When the ‘Next’ command is used in the child’s system menu, or Ctrl+Tab is pressed, the child window disappears.
  3. When the Month Calendar control is clicked with the mouse, the child windows disappear.

Here are the solutions:

  1. Reduce the size of the Month Calendar control to allow one line of icons at the bottom of the client window. The control background is the same colour as the client, so you can’t see the join. Save the height of the icon in a member variable and
    use it like so:

    CMDICalendar::CMDICalendar()
    : m_cyIcon(0)
    {
    }
    
    void CMDICalendar::PreSubclassWindow()
    {
      m_cyIcon = ::GetSystemMetrics(SM_CYMINIMIZED);
      RECT rc;
      GetClientRect(&rc);
      rc.bottom -= m_cyIcon;
      VERIFY(m_ctrlCalendar.Create(WS_CHILD | WS_VISIBLE |
                                   WS_CLIPSIBLINGS |
                                   MCS_DAYSTATE | MCS_NOTODAYCIRCLE,
                                   rc, this, ID_CALENDAR));
      DWORD colour = ::GetSysColor(COLOR_APPWORKSPACE);
      m_ctrlCalendar.SetColor(MCSC_BACKGROUND, colour);
      m_ctrlCalendar.SetColor(MCSC_MONTHBK, colour);
      CWnd::PreSubclassWindow();
    }
    
    void CMDICalendar::OnSize(UINT nType, int cx, int cy)
    {
      CWnd::OnSize(nType, cx, cy);
      m_ctrlCalendar.MoveWindow(0, 0, cx, cy - m_cyIcon, FALSE);
      ArrangeIconicWindows();
    }
    

    The call to ArrangeIconicWindows() keeps the icons in place as the window is sized. Of course, this only allows one row of icons, but that’s enough for most users.

  2. Trapping WM_MDISETMENU and sending the Month Calendar control to the bottom of the Z-order stops the child windows from disappearing. Here’s how:
    BEGIN_MESSAGE_MAP(CMDICalendar, CWnd)
      ON_WM_SIZE()
      ON_MESSAGE(WM_MDISETMENU, OnMDISetMenu)
    END_MESSAGE_MAP()
    
    LRESULT CMDICalendar::OnMDISetMenu(WPARAM wParam, LPARAM lParam)
    {
      m_ctrlCalendar.SetWindowPos(&wndBottom, 0, 0, 0, 0,
      SWP_NOACTIVATE | SWP_NOSIZE |
      SWP_NOMOVE | SWP_NOSENDCHANGING);
      return Default();
    }
  3. Trapping NM_RELEASEDCAPTURE in CMDICalendar allows a mouse click to bring the Month Calendar control to the fore as before, but releasing the mouse puts it behind all the MDI child windows again. This is a method of consulting the calendar, and then switching back to the child windows, assuming the child windows aren’t maximized, of course.

    in MDIClient.cpp

    BEGIN_MESSAGE_MAP(CMDICalendar, CWnd)
      ON_WM_SIZE()
      ON_MESSAGE(WM_MDISETMENU, OnMDISetMenu)
      ON_NOTIFY(NM_RELEASEDCAPTURE, ID_CALENDAR, OnReleasedCapture)
    END_MESSAGE_MAP()
    
    void CMDICalendar::OnReleasedCapture(NMHDR* pNMHDR, LRESULT* pResult)
    {
      m_ctrlCalendar.SetWindowPos(&wndBottom, 0, 0, 0, 0,
      SWP_NOACTIVATE | SWP_NOSIZE |
      SWP_NOMOVE | SWP_NOSENDCHANGING);
    }
    

This sends the Month Calendar control to the bottom, as seen in the Calendar 2 program in the Downloads section.

Further to Point 2 above, this only works if a WM_MDISETMENU command is actually sent to the client window. In one of my applications (a database application), there in no document, and therefore no CMultiDocTemplate because each child window represents a different table of the database. The database connection is kept in a variable in the CWinApp derived class and there is only one menu, whatever is displayed. As a result, no WM_MDISETMENU is sent, and it has to be manufactured like so:

void CChildFrame::OnMDIActivate(BOOL bActivate,
                                CWnd* pActivateWnd,
                                CWnd* pDeactivateWnd)
{
  CMDIChildWnd::OnMDIActivate( bActivate,
                               pActivateWnd,
                               pDeactivateWnd);
  ::SendMessage(GetMDIFrame()->m_hWndMDIClient,
                WM_MDISETMENU,
                0, 0);
  if(!bActivate)
        RedrawWindow(NULL, NULL,
                     RDW_INVALIDATE | RDW_FRAME | RDW_NOCHILDREN);
}

When WM_MDISETMENU is sent with a WPARAM and LPARAM of zero, the result is that the menu is refreshed, but not changed. The RedrawWindow(…) is necessary; otherwise, not all the deactivated window frames are repainted.

Downloads

Download source — 25 Kb

Download source — 25 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read