|
|||||||||||||||||||||||||
|
|||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThe 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 AccomplishedThe 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 ProcedureProject Kick-offThe 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
In the resource editor, add a picture control with ID
Using the MFC ClassWizard, add a member variable to
Upon clicking on OK, a message box warns us to make sure we have included the header file for the class The Data StructureThe 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 #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 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 The Visual MagicNone 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, 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 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 ...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 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 WindowAs 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 However, there is one requirement, and it applies to the rubric window. When it is clicked on, it must inform its parent (a 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 Well then, create a class named // 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 Now, we handle the message in // 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 CandyBecause 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. FeedbackMy 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. AcknowledgmentsFor 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.
|
||||||||||||||||||||||||