Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WTL
Article

Switch Views in a WTL SDI Application

Rate me:
Please Sign up or sign in to vote.
4.80/5 (11 votes)
19 Sep 200711 min read 59.1K   1.6K   33   8
An article explaining how to switch views in a WTL SDI application.
Screenshot - SDIMultiView.gif

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.

C++
// Somewhere within your frame class -- Doesn't work!!!
this->m_hWndClient = m_hWndNewView; // m_hWndNewView is a handle to the 
                                       // view I want to switch to
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:

  1. Create the new view
  2. Hand the new view's HWND to the frame's m_hWndClient
  3. Show the new view
  4. Destroy the old view
  5. Update the window
  6. Optional: Update your frame's PreTranslateMessage method to include your new view's override
C++
// View is just an enum that makes it more convenient to address views. 
// There's a defined VIEW enum for each view/dialog class you want
// to be able to switch to.
// 
// You could accomplish the same thing with simple integers, member windows 
// handles, or whatever else distinguishes the requested and current views. 

enum VIEW {BASIC, DIALOG, EDIT, NONE};

// Member views
CBasicView m_view; // Basic view derived from wizard
CEditView m_edit; // Basic dialog derived from wizard
CBasicDialog m_dlg; // Basic edit control view derived from wizard

...

void SwitchView(VIEW view)
{
    // Pointers to old and new views
    CWindow *pOldView, *pNewView;

    // Get current window/view
    pOldView = GetCurrentView(); // Defined below

    // Get/create requested view
    pNewView = GetNewView(view); // Defined below

    // Check if requested view is current view or default
    if(!pOldView || !pNewView || (pOldView == pNewView))
        return; // Nothing to do
    
    // Show/Hide
    pOldView->ShowWindow(SW_HIDE); // Hide the old
    pNewView->ShowWindow(SW_SHOW); // Show the new window

    // Delete the old view
    pOldView->DestroyWindow();

     // Ask frame to update client
    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:

C++
// Helper method to get current view ~ MFC GetActiveView() not available!
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:

C++
// Helper method to get/create new view
CWindow* GetNewView(VIEW view)
{
    CWindow* newView = NULL;
    // Now set requested view
    switch(view)
    {
    case BASIC:
        // If doesn't exist, create it and set reference to frame's 
        // m_hWndClient
        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:

C++
// Implemented in CMainFrame
virtual BOOL PreTranslateMessage(MSG* pMsg)
{
    if(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))
    return TRUE;

    if(m_hWndClient != NULL)
    {
        // Call PreTranslateMessage for the current view
        CWindow* pCurrentView = GetCurrentView(); // Get the current view 
        // (cast as a CWindow*) ~ function shown above

        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:

C++
// Delete the old view
pOldView->DestroyWindow();

...with...

C++
// Change the current view's ID so it isn't the first child w/in the frame
 pOldView->SetWindowLongPtr(GWL_ID, 0); 

// Make the new view the frame's first pane/child
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) // Destroy by default
{
    ...
    if(bPreserve)
    {    
        // Use Technique #2
    }
    else
    {
        // Use Technique #1
    }        
}

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....
C++
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

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


Written By
Web Developer
United States United States
Tim lives in sunny Tacoma, WA with a panamoric view of Mt. Rainier in his backyard. (Ok, the sunny part was an exaggeration).
Tim started programming in 1981 using BASIC on a mini-frame on, really, punch cards, TTYs and paper tape. He graduated from the Naval Academy with an English and general engineering degree in 1991, and went on to become a Marine Corps Communications Officer where he administed a Novell/Banyan Vines (remember them?) network and did Perl and HTML scripting on the side.

Since then, he's gone on to teach himself first VBA, C#, Windows, MFC and WTL programming - he also likes swimming against the tide! Tim has developed numerous, mostly ASP/ASP.Net applications for companies he's worked for as well as for family and friends. He's also developed personal tools using MFC, WTL and ATL. He loves the breadth of .Net but also continues to see a future for MFC and WTL as long as people need to build small(er) and fast(er) applications.

His current development passion is embedded programming on constrained devices (mostly PocketPC and Smartphone).

He is also crazy about his wife, step son and dogs; enjoys snowboarding, bike riding, and the occasional Xbox360 game. He speaks enough German to pass as a Czech tourist. And he thinks Mike Blaszczak's "Professional MFC with Visual C++ 6" is the best programming book ever written!

Comments and Discussions

 
QuestionHow to solve this error Pin
Member 117394183-Aug-15 4:17
Member 117394183-Aug-15 4:17 
VB
Error   1   error C1083: Cannot open include file: 'atlapp.h': No such file or directory    d:\sdiapp\sdimultiview_src\sdimultiview\sdimultiview\stdafx.h   14  1   SDIMultiView

Questionhow can i solve this error...Plz help me Pin
Member 117394183-Aug-15 4:08
Member 117394183-Aug-15 4:08 
GeneralProfessional MFC with Visual C++ 6 Pin
ThomasBrownII10-Oct-08 19:28
ThomasBrownII10-Oct-08 19:28 
AnswerRe: Professional MFC with Visual C++ 6 Pin
Tim Brooks25-Oct-08 5:38
Tim Brooks25-Oct-08 5:38 
GeneralIs it possible to switch between views, where all the views are static splitter windows Pin
Venkata Rama Krishna K22-May-08 20:35
Venkata Rama Krishna K22-May-08 20:35 
GeneralCTabView Pin
Alain Rist19-Sep-07 21:18
Alain Rist19-Sep-07 21:18 
AnswerRe: CTabView Pin
Tim Brooks20-Sep-07 4:33
Tim Brooks20-Sep-07 4:33 
GeneralRe: CTabView Pin
Alain Rist20-Sep-07 5:48
Alain Rist20-Sep-07 5:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.