Introduction
I've been working on Windows Mobile applications lately, one of which I'm building in WTL. I chose WTL over MFC or the .NET Compact Framework because of speed, size and dependency limitations of the latter two.
I started out with an SDI (single document interface) WTL wizard-based application, added some control-derived windows and some dialog windows (form views) and only then realized I had to find a way to make the SDI framework dynamically load and unload child windows the same way that you can with MFC or .NET applications � to be honest it's not a whole lot easier to do in MFC.
MDI (multiple document interface) is certainly an option, usually, but the WTL MDI framework doesn't support Windows Mobile/CE. As with most things, there are work-arounds (here's one such example). But even if MDI can be stretched to work, it irks me to have to make all that effort to make one architecture do something that another (SDI) ought to do itself.
This article demonstrates two techniques which can be used to do just that: dynamically switch between views in an SDI application. I'm sure there are other ways to do it but these are the two that I use. Above, I've included full source and a working example for you. I hope this article saves at least one other person the trouble of having to figure this out for themselves.
About the Techniques
- Technique 1: The first method involves destroying and recreating the views instances on demand. It's the easier of the two approaches and works well where you don't mind destroying and recreating the window objects. However, when developing on mobile or constrained devices, or in cases where you want to persist the view(s) between selections, you may not want to pay the cost of repeatedly destroying and recreating the windows or deal with re-initializing them each time the user changes views.
- Technique 2: The second approach I'll describe shows how to create the views on demand and then persist them between subsequent selections by changing an internal identifier directly using the Win32 function
SetWindowLongPtr
. (SetWindowLong
is now deprecated according to MSDN)
Background
The default WTL wizard-built SDI application has a single client, CWindow
derived, window or "view" within a single parent "frame" which usually descends from CFrameWindowImpl
.
{For a fuller discussion of WTL's organization and usage, please see Michael Dunn's excellent series here on CodeProject � particularly "WTL for MFC Programmers, Part II - WTL GUI Base Classes".}
This design philosophy is essentially continued in MDI where there is an owning child frame for each view (that is, it's one-to-one vs one-to-many).
To be clear, WTL doesn't implement anything like MFC's Document/View model so where above and elsewhere I refer to a "view", I mean simply a child window (i.e. a window, dialog or wrapped child control) within the application's main frame.
In many cases, the best way to gain the ability to switch between views is to go with the flow and simply build your application as an MDI application. That said, there are cases where MDI, as mentioned above, isn't available or desirable. Getting back to SDI, the main frame stores a handle (HWND) to its child view in a public variable called m_hWndClient
.
What Not To Do
Knowing how the frame stores a reference to its child view, you might at first be tempted to solve the problem by re-assigning the new child window to the frame's m_hWndClient
and then updating the layout.
this->m_hWndClient = m_hWndNewView;
UpdateLayout();
Unfortunately this doesn't work, principally because the frame doesn't know about the window whose handle you've just given it.
The "Trick"
The trick, if there is one, to solving this problem is to understand that Windows implicitly references the first "pane" within the frame window that is not a control bar � which happens to be the child view. This is also, I should mention, why you need to do the same thing in both MFC and in WTL in order to switch views.
In MFC, this pane is identified as AFX_IDW_PANE_FIRST. If you poke around inside ATL (atlres.h), you'll find a similarly named definition called ATL_IDW_PANE_FIRST. But both have the same value of "0xE900".
As I hinted above, you can either destroy the current child "view," create the new view and then re-assign the new view's handle (implicitly setting the first pane ID) � Technique #1; or you can explicitly change the IDs of the two views so that the current view's ID is no longer ATL_IDW_PANE_FIRST and then assign this ID to the new view using some direct windows calls. (Which I'll show you how to do in a just a bit.)
Interestingly, Technique #1 doesn't require switching the IDs as described � so I'm guessing that when you create the second view, which definitely has a different internal ID, that the frame or windows re-assigns the ID to "0xE900". If you don't switch the IDs but just create the second view with the frame as its parent HWND, the frame will continue to reference the first view as its child as long as it exists. I'll leave it to someone wiser in the ways of Windows to explain further.
Technique #1: Destroy & Recreate the Views
I first saw this technique used in Chris Sell's White Paper "WTL Makes UI Programming a Joy - Part 2" (which you can find on www.sellsbrothers.com. Look for a function called TogglePrintPreview()
in the BitmapView example).
I'll use a slightly different version so that my example code matches the demo and source I've provided. I've also extended it so that I can support an arbitrary number of views. The steps can be broken down roughly into:
- Create the new view
- Hand the new view's HWND to the frame's
m_hWndClient
- Show the new view
- Destroy the old view
- Update the window
- Optional: Update your frame's
PreTranslateMessage
method to include your new view's override
enum VIEW {BASIC, DIALOG, EDIT, NONE};
CBasicView m_view;
CEditView m_edit;
CBasicDialog m_dlg;
...
void SwitchView(VIEW view)
{
CWindow *pOldView, *pNewView;
pOldView = GetCurrentView();
pNewView = GetNewView(view);
if(!pOldView || !pNewView || (pOldView == pNewView))
return;
pOldView->ShowWindow(SW_HIDE);
pNewView->ShowWindow(SW_SHOW);
pOldView->DestroyWindow();
UpdateLayout();
}
GetCurrentView()
is a helper function that compares m_hWndClient
to each view's handle and then returns the matching view, cast to a CWindow*
. Like so:
CWindow* GetCurrentView()
{
if(!m_hWndClient)
return NULL;
if(m_hWndClient == m_view.m_hWnd)
return (CWindow*)&m_view;
else if(m_hWndClient == m_dlg.m_hWnd)
return (CWindow*)&m_dlg;
else if(m_hWndClient == m_edit.m_hWnd)
return (CWindow*)&m_edit;
else
return NULL;
}
GetNewView(VIEW view)
is a helper function that returns the requested view, cast to a CWindow*
. In the process, it creates the view object if necessary and also assigns its handle to the frame's m_hWndClient
. Like so:
CWindow* GetNewView(VIEW view)
{
CWindow* newView = NULL;
switch(view)
{
case BASIC:
if(m_view.m_hWnd == NULL)
m_view.Create(m_hWnd);
m_hWndClient = m_view.m_hWnd;
newView = (CWindow*)&m_view;
break;
case DIALOG:
if(m_dlg.m_hWnd == NULL)
m_dlg.Create(m_hWnd);
m_hWndClient = m_dlg.m_hWnd;
newView = (CWindow*)&m_dlg;
break;
case EDIT:
if(m_edit.m_hWnd == NULL)
m_edit.Create(m_hWnd);
m_hWndClient = m_edit.m_hWnd;
newView = (CWindow*)&m_edit;
break;
}
return newView;
}
- The functions above should be pretty self-explanatory based on the discussion thus far.
SwitchView(VIEW view)
first calls GetCurrentView()
to get a reference to the current view.
- It then calls
GetNewView(VIEW view)
to get a reference to the requested view, creating it if necessary. It also hands the frame m_hWndClient
the new view's handle.
- If either the new or old views are NULL or they equal one another � meaning that the user has asked to change the current view to itself � it does nothing.
SwitchView(VIEW view)
then HIDES the old view and SHOWS the new view
- Finally, it destroys the old view. This last step implicitly changes the internal ID of the new view to ATL_IDW_PANE_FIRST.
As I mentioned above, you should also consider updating the frame's PreTranslateMessage
override to ensure that the views get a chance to execute their own PreTranslateMessage
on messages. PreTranslateMessage
essentially allows the frame and/or your view to preview messages and do something with them before they get translated and dispatched. (Return TRUE
to prevent the message being translated and dispatched.)
Most applications don't override PreTranslateMessage
unless they need to do some special message handling, such as when they're subclassing a lot of controls. That said, the WTL wizard will automatically generate PreTranslateMessage
functions in your CWindowImpl
and CDialogImpl
views and will also add the code necessary to route messages to them from the main frame's PreTranslateMessage
, another reason I considered it mandatory to ensure messages were routed to my views from the main frame.
Here's how I've modified the frame's PreTranslateMessage
to give my views a chance to look at the messages:
virtual BOOL PreTranslateMessage(MSG* pMsg)
{
if(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))
return TRUE;
if(m_hWndClient != NULL)
{
CWindow* pCurrentView = GetCurrentView();
if(m_view.m_hWnd == pCurrentView->m_hWnd)
return m_view.PreTranslateMessage(pMsg);
else if(m_dlg.m_hWnd == pCurrentView->m_hWnd)
return m_dlg.PreTranslateMessage(pMsg);
else if(m_edit.m_hWnd == pCurrentView->m_hWnd)
return m_edit.PreTranslateMessage(pMsg);
}
return FALSE;
}
Here I first ensure that the frame has a valid child handle, and then I call the same GetCurrentView()
function described above to return a CWindow*
. I then use that CWindow*
's HWND member to compare to each of my view's. I do it this way because I need the view in order to call the view's own PreTranslateMessage
. I can't use the CWindow
to call it because it doesn't implement PreTranslateMessage
.
It goes without saying that you don't need to include message routing to any views that don't implement PreTranslateMessage
.
There are probably more elegant ways to do this, such as through run-time type information (RTTI), templates or other forms of inheritance, containment, etc.. Keep in mind that RTTI in particular can be an expensive way to solve this because it will traverse the inheritance hierarchy for each object, for each message. Given the number of messages that'll pass through PreTranslateMessage
and the fact that I wanted to focus on the core problem I'm trying to solve, I'll have to leave more elegant solutions to the reader as a follow-on exercise.
PreTranslateMessage
, by the way, is the sole method in the CMessageFilter
interface � and a method which the main frame implements. It isn't, however, in the inheritance hierarchy of either CWindowImpl
or CDialogImpl
. Meaning that it isn't implicitly available in either and it isn't required of implementors of either.
Technique #2: Destroy & Recreate the Views
This discussion will be much shorter as most of the preparation has already been done. All that's required at this point is a simple change to the SwitchView
method to persist the views between switches instead of destroying them. If you refer back to SwitchView
above, replace:
pOldView->DestroyWindow();
...with...
pOldView->SetWindowLongPtr(GWL_ID, 0);
pNewView->SetWindowLongPtr(GWL_ID, ATL_IDW_PANE_FIRST);
... that's it! As discussed above, the frame uses the first pane ID in order to update its client view so you need to change the current view's GWL_ID
to something other than ATL_IDW_PANE_FIRST
and then change the new view's GWL_ID
to ATL_IDW_PANE_FIRST
.
Using the Code
- You can invoke
SwitchView
anywhere by simply calling it with a VIEW
enum corresponding to the desired view. e.g. SwitchView(BASIC)
or SwitchView(EDIT)
.
- If you want to use my implementation, there are few things you'll need to do:
- Update
enum VIEW {}
to include identifiers for each view � name them whatever will help you keep them straight
- Add a member variable to your main frame for each view. e.g.
CMyView m_myView
.
- Update the switch statement in
GetNewView(VIEW)
to include a case for each of your VIEW
enums and view members.
- Update
GetCurrentView()
to return a CWindow*
reference to each of your view members.
- Optionally update your frame's
PreTranslateMessage
method to invoke your views' own PreTranslateMessage
methods; making sure to also implement PreTranslateMessage
in each view (this is done by default if you generate them with the WTL wizard).
- In the sample code, I've actually updated
SwitchView
's logic to use the same method to handle both scenarios due to the high degree of overlap between the two. In practice, I think you'd usually want to go with one method or the other, but this gives you the option to have both in the same application. The effective changes are:
void SwitchView(VIEW view, BOOL bPreserve = FALSE)
{
...
if(bPreserve)
{
}
else
{
}
}
Wrap-Up
Please consider this but a starting point. There are any number of improvements that can be made to the code accompanying this article but which I deemed non-essential to my main subject or which time and space precluded exploring further. Some specific improvements I might make include:
- Create and store views as pointers (which is what I do myself in practice) � this of course requires a bit more diligence and some changes to the code in a few places due to the differences between stack and pointer semantics. One example being in member comparisons such as in
PreTranslateMessage
. You have to make sure you have a valid pointer before checking the member HWND or you're asking for an ASSERT storm. e.g....
CWindow* CPocketMDFrame::GetCurrentView()
{
if(!m_hWndClient)
return NONE;
if((m_pView) && (m_hWndClient == m_pView->m_hWnd))
return (CWindow*)m_pView;
...
}
- Store the views in an array of
CWindow*
's � doing so could help you remove the dependence on my VIEW
enum but at the cost of some additional complexity; your choice
- Possibly create an interface class or template to consolidate some of the behavior and create a common view interface
- Implement a light-weight Document/View like architecture which would make dealing with the contained views a bit simpler, probably using the Observer design pattern
- Possibly implement a Visitor (design pattern) to make the frame's
PreTranslateMessage
routing to the views cleaner
Copyright and License
This article is copyrighted material, (c) 2007 by Tim Brooks. This article has been researched and written with the intention of helping others benefit from my knowledge and experience just as I've benefited from the knowledge and experience of countless others. If you would like to translate this article please email me to let me know. I would like to know about derivation of this article and also be able to reference said translations here and elsewhere.
The demo code accompanying this article is released to the public domain. This article, however, is not public domain. If you use the code in your own application, I'd appreciate an email telling me about it � but I don't require it. Finally, attribution in your own source code would be appreciated but is likewise not required.
History
September, 17, 2007 - Article First Published