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

Stacked Windows Control Tutorial

0.00/5 (No votes)
10 Jul 2006 1  
Step-by-step development of a stacked-windows control.

StackedWindowsControl demo application

Introduction

The development of custom controls from scratch is often unnecessary as the standard toolset is quite comprehensive and, if not sufficient, subclassing or owner-drawn flavors take care of the job. This is an important point that should not be dismissed. When developing a custom control from scratch, there is a better than good chance that the result will be inferior to the standard.

That said, there are a few controls that are simply missing and, if we want to deploy them in our applications, there is no other solution than to construct them out of thin air. One such case is the "stacked windows control" (or whatever it is called) used by, for example, Spybot or Outlook. Because it is not among the standard controls and because it is an interesting exercise, this tutorial explains how to develop this kind of control, one step at a time.

The intended audience for this tutorial is the rookie programmer and, before I start, I want to challenge you not to read the article and to try to develop the control on your own. Although it may look daunting or you may not know where to start, it is not as hard as you might think. Give it a try, see how far you can get, then come back and check what I have to say. Hint: it is all about resizing and repositioning windows, nothing more.

What is to be Accomplished

The target is a "stacked windows control". That's it. It will be as generic as possible, and will illustrate how to assemble a control of this kind.

The keen reader may like to know that I have written this tutorial as I wrote the demo project. The instructions, explanations, and code below do amount to the development of the stacked window control in the screenshot above (the one on the left, to be precise).

On with the code.

Step-by-Step Procedure

Project Kick-off

The setup is simple. Create a new dialog-based project, and set the warning level to 4 (Project Settings, C/C++ tab). Level 4 will ensure that anything suspicious is brought up to our attention so that it is up to us to decide what to do with 'informational warnings which, in most cases, can be safely ignored' (from the docs).

Let's start working on the control. Create a new MFC class named CStackedWndCtrl that uses CStatic as the base class.

In the resource editor, add a picture control with ID IDC_SWC. Leave the defaults as Frame for Type and Black for Color.

Using the MFC ClassWizard, add a member variable to IDC_SWC named m_StackedWndCtrl, making sure to select Control as the Category and CStackedWndCtrl as the Variable Type.

Upon clicking on OK, a message box warns us to make sure we have included the header file for the class CStackedWndCtrl in our dialog code. Do it now if you haven't already.

The Data Structure

The backbone of any kind of control is a data structure where to keep the information that will be displayed.

Well, what is going to be displayed? The control is made out of panes, where each pane contains two windows, a rubric window and a content window. The following image illustrates the concept.

The mechanics of the control require that only one pane's content window be shown at a time. Clicking on a pane's rubric window will trigger the display of its associated content window, and will also hide the currently shown pane's content window.

The data structure will, therefore, contain a couple of pointers to CWnd objects and a boolean flag to indicate whether to show or hide the pane's content window. No need for anything else.

#include <afxtempl.h>


class CStackedWndCtrl : public CStatic
{
  ....
  ....

// Attributes

protected:

  typedef struct
  {
      CWnd* m_pwndRubric;
      CWnd* m_pwndContent;
      BOOL  m_bOpen;
  } TDS_PANE, *PTDS_PANE;

  CArray<PTDS_PANE, PTDS_PANE> m_arrPanes;

  ....
  ....
}

An array is a convenient and sufficient way to store, retrieve, and work with these structures. Remember that in order to use the array template, we need to include the appropriate header.

The next task is to write a public method that will allow us to add panes to the control. Nothing to it. We make copies of the pointers to the window objects passed as parameters, and set the new pane as the one that is shown.

int CStackedWndCtrl::AddPane( CWnd* pwndRubric, CWnd* pwndContent )
{
  // Hide whatever pane's content window is currently shown

  // We will always show the content window of the last pane added

  for( int i = 0; i < m_arrPanes.GetSize(); i++ )
      if( m_arrPanes[ i ]->m_bOpen )
          m_arrPanes[ i ]->m_bOpen = FALSE;

  // Create a new pane structure

  PTDS_PANE pPane = new TDS_PANE;

  if( pPane == NULL )
  {
      AfxMessageBox( "Failed to add a new pane to" 
                     " the stack.\n\nOut of memory." );
      return -1;
  }

  // Copy the pointers to the rubric and content windows

  // Also, set this pane as open

  pPane->m_pwndRubric     = pwndRubric;
  pPane->m_pwndContent    = pwndContent;
  pPane->m_bOpen          = TRUE;

  // Add the new pane to the end of the stack

  int iIndex = m_arrPanes.Add( pPane );

  // Rearrange the stack

  RearrangeStack();

  // Return the index of the new pane

  return iIndex;
}

Before we worry about arranging and displaying the panes (if you want to test the code, just comment out the call to the method RearrangeStack), it is very important that we make sure that the structure is properly deleted on exit, to prevent memory leaks. We carry out this task in the destructor, as follows:

CStackedWndCtrl::~CStackedWndCtrl()
{
  for( int i = 0; i < m_arrPanes.GetSize(); i++ )
  {
      // Delete the rubric window

      m_arrPanes[ i ]->m_pwndRubric->DestroyWindow();

      delete m_arrPanes[ i ]->m_pwndRubric;

      // Delete the content window

      m_arrPanes[ i ]->m_pwndContent->DestroyWindow();

      delete m_arrPanes[ i ]->m_pwndContent;

      // Delete structure

      delete m_arrPanes[ i ];
  }

  m_arrPanes.RemoveAll();
}

Simple stuff. We loop through the array of panes, destroying each window, then deleting each window object, then deleting each pane object, and finally, removing all pointers from the array.

This functionality is enough to make the CStackedWndCtrl class able to do its work. We can add panes, and these are properly disposed of when the control is destroyed.

The Visual Magic

None of it, I am afraid. The algorithm to arrange and display the control is quite straightforward.

We loop through the panes, offsetting the top of the frame by a predetermined measure, m_iRubricHeight, which has been set in the demo with a default value (feel free to experiment). When we hit upon the pane that is open, we use the number of rubric windows that are left to display, to calculate the dimensions of this pane's content window. Check out the code.

void CStackedWndCtrl::RearrangeStack()
{
  CRect rFrame;

  GetClientRect( &rFrame );

  for( int i = 0; i < m_arrPanes.GetSize(); i++ )
  {
      // Rubric windows are always visible

      m_arrPanes[ i ]->m_pwndRubric->SetWindowPos( NULL,
                                                   0,
                                                   rFrame.top,
                                                   rFrame.Width(),
                                                   m_iRubricHeight,
                                                   SWP_NOZORDER | SWP_SHOWWINDOW );

      // Only the content window of the flagged pane is shown

      // All others are hidden if they aren't already

      if( m_arrPanes[ i ]->m_bOpen )
      {
          // From the bottom of the frame, take off as many rubric

          // window's heights as there are left to display

          int iContentWndBottom = rFrame.bottom - 
              ( ( m_arrPanes.GetSize() - i ) * m_iRubricHeight );

          m_arrPanes[ i ]->m_pwndContent->SetWindowPos(
                           NULL,
                           0,
                           rFrame.top + m_iRubricHeight,
                           rFrame.Width(),
                           iContentWndBottom - rFrame.top,
                           SWP_NOZORDER | SWP_SHOWWINDOW );

          // The next rubric window will be placed right below

          // this pane's content window

          rFrame.top = iContentWndBottom;
      }
      else
          m_arrPanes[ i ]->m_pwndContent->ShowWindow( SW_HIDE );

      // The top of the frame is offset by the height of a rubric window

      rFrame.top += m_iRubricHeight;
  }
}

That takes care of arranging and displaying the control.

Let's now add a call to PreSubclassWindow to get rid of the black frame around the picture control. While it is useful when working in the resource editor, it is unnecessary and unsightly when the application is run.

void CStackedWndCtrl::PreSubclassWindow() 
{
  // Remove the black frame and clip children to reduce flickering

  ModifyStyle( SS_BLACKFRAME, WS_CLIPCHILDREN );

  CStatic::PreSubclassWindow();
}

We also take the opportunity to add the WS_CLIPCHILDREN flag to reduce flickering when resizing the control, which reminds me...

...it is always a good idea to make sure that the control will be able to resize itself if necessary. In this case, the functionality is quite easy to implement. Fire up the Classwizard, add a message handler for WM_SIZE, and make a call to RearrangeStack.

void CStackedWndCtrl::OnSize(UINT nType, int cx, int cy) 
{
  CStatic::OnSize(nType, cx, cy);

  RearrangeStack();
}

We are almost done. If you add some test panes, compile, and run; the stack control will display all rubric windows and the last pane's content window.

Of course, what the control cannot do is respond to user clicks on rubric windows. We haven't written code for it yet. Be that our next and last task on the list.

The Only Requirement of the Rubric Window

As far as our control is concerned, rubric and content windows can be any kind of window. Literally. Dialogs, static controls, list boxes/controls, tree controls, calendar controls, edit/richedit controls, generic windows, even custom controls. If we can get a CWnd pointer to it, the class CStackedWndCtrl will work as intended. The only limit is common sense, not a technical issue. For example, a combo box could be set as either the rubric or content window but its appropriateness is rather questionable.

However, there is one requirement, and it applies to the rubric window. When it is clicked on, it must inform its parent (a CStackedWndCtrl object) so that the associated content window can be displayed. We will accomplish this by sending a message.

For simplicity, I am going to use buttons as rubric windows. They are, after all, the most sensible choice. We will derive a class from CButton, and add this bit of specialized functionality.

Well then, create a class named CTelltaleButton derived from CButton. Add the following message definition to its header, and a message handler for =BN_CLICKED (reflected message).

// In TelltaleButton.h


#define WM_RUBRIC_WND_CLICKED_ON ( WM_APP + 04100 )

// In TelltaleButton.cpp


void CTelltaleButton::OnClicked() 
{
  GetParent()->SendMessage( WM_BUTTON_CLICKED, (WPARAM)this->m_hWnd );
}

The rubric window will send a message that contains, as wParam, its own handle. With this information, its parent control will be able to figure out which rubric window has been clicked on.

Now, we handle the message in CStackedWndCtrl by manually adding a method to its message map as follows:

// In StackedWndCtrl.h


#define WM_RUBRIC_WND_CLICKED_ON ( WM_APP + 04100 )

  ...
  ...

  // Generated message map functions

protected:
  //{{AFX_MSG(CStackedWndCtrl)

  afx_msg void OnSize(UINT nType, int cx, int cy);
  //}}AFX_MSG

  afx_msg LRESULT OnRubricWndClicked(WPARAM wParam, LPARAM lParam);
  DECLARE_MESSAGE_MAP()

// In StackedWndCtrl.cpp


  ...
  ...


BEGIN_MESSAGE_MAP(CStackedWndCtrl, CStatic)
  //{{AFX_MSG_MAP(CStackedWndCtrl)

  ON_WM_SIZE()
  //}}AFX_MSG_MAP

  ON_MESSAGE(WM_RUBRIC_WND_CLICKED_ON, OnRubricWndClicked)
END_MESSAGE_MAP()

  ...
  ...

LRESULT CStackedWndCtrl::OnRubricWndClicked(WPARAM wParam, LPARAM /*lParam*/)
{
  HWND hwndRubric = (HWND)wParam;
  BOOL bRearrange = FALSE;

  for( int i = 0; i < m_arrPanes.GetSize(); i++ )
    if( m_arrPanes[ i ]->m_pwndRubric->m_hWnd == hwndRubric )
    {
      // Rearrange the control only if a rubric window

      // other than the one belonging to the pane that

      // is currently open is clicked on

      if( m_arrPanes[ i ]->m_bOpen == FALSE )
      {
        m_arrPanes[ i ]->m_bOpen = TRUE;
        bRearrange = TRUE;
      }
    }
    else
      m_arrPanes[ i ]->m_bOpen = FALSE;

  if( bRearrange )
    RearrangeStack();

  // In case the rubric window that has sent the message wants to know

  // if the control has been rearranged, return the flag

  return bRearrange;
}

It all comes down to looping through the panes in order to find the rubric window that has been clicked on. If it is different from the one that belongs to the currently open pane, rearrange the control.

Some Eye Candy

Because CStackedWndCtrl is very flexible as to what can be used for its rubric and content windows, it is quite easy to jazz it up. To illustrate how to do this, I have included in the demo project a "plain" control and one that uses Davide Calabro's shaded buttons and Everaldo Coelho's icons. As you can see, by inspecting the code in the demo, not a single line of code in CStackedWndCtrl needs to be modified. As it should.

Our short journey comes to an end here, my friend; I go this way, you go that way. I hope that the sights I've shown you have served to seed your imagination, and that our quiet dealings will be of benefit to you.

Feedback

My intention has been to provide a tutorial that is coded clearly, as simple to understand and follow as possible. I am sure that there are finer solutions to the functionality I have implemented here. Any suggestions that improve, simplify, or better explain the code are welcome.

Acknowledgments

For the demo project, I've used an old version of CResizableDialog by Paolo Messina, that I've become fond of when writing articles for the Code Project. Thanks Paolo.

Another Italian's work, Davide Calabro's appealing CButtonST, has been used in the demo project. Thanks Davide.

I have used some of Everaldo Coelho's icons in the demo project. You can find more of his work here and here. Thanks Everaldo.

I have also used Dan Moulding's Visual Leak Detector to check for memory shenanigans. A very, very handy tool which I recommend to all and sundry. Thanks Dan.

Last, I want to express my gratitude to everyone that shares, or makes it possible to freely share knowledge. Time and again, I see fellow human beings writing articles, tutorials, assisting strangers in the forums, and I am humbled and motivated by their generosity. It is a great pleasure to be able to give something back.

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