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
{
....
....
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 )
{
for( int i = 0; i < m_arrPanes.GetSize(); i++ )
if( m_arrPanes[ i ]->m_bOpen )
m_arrPanes[ i ]->m_bOpen = FALSE;
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;
}
pPane->m_pwndRubric = pwndRubric;
pPane->m_pwndContent = pwndContent;
pPane->m_bOpen = TRUE;
int iIndex = m_arrPanes.Add( pPane );
RearrangeStack();
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++ )
{
m_arrPanes[ i ]->m_pwndRubric->DestroyWindow();
delete m_arrPanes[ i ]->m_pwndRubric;
m_arrPanes[ i ]->m_pwndContent->DestroyWindow();
delete m_arrPanes[ i ]->m_pwndContent;
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++ )
{
m_arrPanes[ i ]->m_pwndRubric->SetWindowPos( NULL,
0,
rFrame.top,
rFrame.Width(),
m_iRubricHeight,
SWP_NOZORDER | SWP_SHOWWINDOW );
if( m_arrPanes[ i ]->m_bOpen )
{
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 );
rFrame.top = iContentWndBottom;
}
else
m_arrPanes[ i ]->m_pwndContent->ShowWindow( SW_HIDE );
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()
{
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).
#define WM_RUBRIC_WND_CLICKED_ON ( WM_APP + 04100 )
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:
#define WM_RUBRIC_WND_CLICKED_ON ( WM_APP + 04100 )
...
...
protected:
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg LRESULT OnRubricWndClicked(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
...
...
BEGIN_MESSAGE_MAP(CStackedWndCtrl, CStatic)
ON_WM_SIZE()
ON_MESSAGE(WM_RUBRIC_WND_CLICKED_ON, OnRubricWndClicked)
END_MESSAGE_MAP()
...
...
LRESULT CStackedWndCtrl::OnRubricWndClicked(WPARAM wParam, 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 )
{
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();
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.