|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Contents
Introduction to Part IIOK, time to actually start talking about WTL! In this part I'll cover the basics of writing a main frame window and cover some of the welcome improvements WTL gives, such as UI updating and better message maps. To get the most out of this part, you should have WTL installed so the header files are in the VC search path, and the AppWizard is in the appropriate directory. The WTL distribution comes with instructions on how to install the AppWizard, so consult the docs. Remember, if you encounter any problems installing WTL or compiling the demo code, read the readme section of Part I before posting questions here. WTL OverviewThe WTL classes can be divided into a few major categories:
This article will cover frame windows in depth and touch on some of the UI features and utility classes. Most of the classes are stand-alone classes, although a few such as Beginning a WTL EXEIf you don't use the WTL AppWizard (we'll get to that later), a WTL EXE begins much like an ATL EXE. The sample code in this article will be another frame window as in Part I, but will be a bit less trivial in order to show some WTL features. For this section, we'll start a new EXE from scratch. The main window will show the current time in its client area. Here is a basic stdafx.h: #define STRICT #define WIN32_LEAN_AND_MEAN #define _WTL_USE_CSTRING #include <atlbase.h> // base ATL classes #include <atlapp.h> // base WTL classes extern CAppModule _Module; // WTL version of CComModule #include <atlwin.h> // ATL GUI classes #include <atlframe.h> // WTL frame window classes #include <atlmisc.h> // WTL utility classes like CString #include <atlcrack.h> // WTL enhanced msg map macros atlapp.h is the first WTL header you include. It contains classes for message handling and (Note that we need a global Next let's define our frame window. SDI windows like ours are derived from // MyWindow.h: class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: DECLARE_FRAME_WND_CLASS(_T("First WTL window"), IDR_MAINFRAME); BEGIN_MSG_MAP(CMyWindow) CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() };
Now let's look at // main.cpp: #include "stdafx.h" #include "MyWindow.h" CAppModule _Module; int APIENTRY WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { _Module.Init ( NULL, hInstance ); CMyWindow wndMain; MSG msg; // Create the main window if ( NULL == wndMain.CreateEx() ) return 1; // Window creation failed // Show the window wndMain.ShowWindow ( nCmdShow ); wndMain.UpdateWindow(); // Standard Win32 message loop while ( GetMessage ( &msg, NULL, 0, 0 ) > 0 ) { TranslateMessage ( &msg ); DispatchMessage ( &msg ); } _Module.Term(); return msg.wParam; }
If you run this now, you'll see the main frame window, but of course it doesn't actually do anything yet. We'll need to add some message handlers to do stuff, so now is a good time to cover the WTL message map macros. WTL Message Map EnhancementsOne of the cumbersome and error-prone things you do when using the Win32 API is unpacking parameters from the WTL's enhanced message map macros are in atlcrack.h. (The name comes from "message cracker", a term used for similar macros in windowsx.h.) The initial step in using these macros differs on VC 6 and VC 7, this note in atlcrack.h explains it:
So if you are using VC 6, you would make the change in MyWindow.h: // MyWindow.h, VC6 only: class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() }; (The For our clock program, we'll need to handle class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() // OnCreate(...) ? }; WTL message handlers look a lot like MFC's, where each handler has a different prototype depending on what parameters are passed with the message. But since we don't have a wizard to write the handler, we'll have to find the prototype ourselves. Fortunately, VC can help out. Put the cursor on the "MSG_WM_CREATE" text and press F12 to go to the definition of that macro. On VC 6, VC will rebuild the project first to build its browse info database. Once that's done, VC will open atlcrack.h at the definition of #define MSG_WM_CREATE(func) \ if (uMsg == WM_CREATE) \ { \ SetMsgHandled(TRUE); \ lResult = (LRESULT)func((LPCREATESTRUCT)lParam); \ if(IsMsgHandled()) \ return TRUE; \ } The underlined code is the important part, it's the actual call to the handler, and it tells us the handler returns an Now we can add an class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() LRESULT OnCreate(LPCREATESTRUCT lpcs) { SetTimer ( 1, 1000 ); SetMsgHandled(false); return 0; } };
We call We'll also need a #define MSG_WM_DESTROY(func) \ if (uMsg == WM_DESTROY) \ { \ SetMsgHandled(TRUE); \ func(); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } So our class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) MSG_WM_DESTROY(OnDestroy) CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() void OnDestroy() { KillTimer(1); SetMsgHandled(false); } }; Next up is our class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) MSG_WM_DESTROY(OnDestroy) MSG_WM_TIMER(OnTimer) CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() void OnTimer ( UINT uTimerID, TIMERPROC pTimerProc ) { if ( 1 != uTimerID ) SetMsgHandled(false); else RedrawWindow(); } }; This handler just redraws the window so the new time appears in the client area. Finally, we handle class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) MSG_WM_DESTROY(OnDestroy) MSG_WM_TIMER(OnTimer) MSG_WM_ERASEBKGND(OnEraseBkgnd) CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() LRESULT OnEraseBkgnd ( HDC hdc ) { CDCHandle dc(hdc); CRect rc; SYSTEMTIME st; CString sTime; // Get our window's client area. GetClientRect ( rc ); // Build the string to show in the window. GetLocalTime ( &st ); sTime.Format ( _T("The time is %d:%02d:%02d"), st.wHour, st.wMinute, st.wSecond ); // Set up the DC and draw the text. dc.SaveDC(); dc.SetBkColor ( RGB(255,153,0) ); dc.SetTextColor ( RGB(0,0,0) ); dc.ExtTextOut ( 0, 0, ETO_OPAQUE, rc, sTime, sTime.GetLength(), NULL ); // Restore the DC. dc.RestoreDC(-1); return 1; // We erased the background (ExtTextOut did it) } }; This handler demonstrates one of the GDI wrappers, So after all that, here's what our window looks like:
The sample code also has If you're using VC 7.1, check out the WTL Helper plug-in by Sergey Solozhentsev, which will do the grunt work of adding message map macros for you. What You Get with the WTL AppWizardThe WTL distribution comes with a very nice AppWizard, so let's see what features it puts in an SDI app. Going through the wizard (VC 6)Click File|New in VC and select ATL/WTL AppWizard from the list. We'll be rewriting the clock app, so enter WTLClock for the project name:
The next page is where you select an SDI, MDI, or dialog-based app, along with some other options. Select the options as shown here and click Next:
The last page is where we can choose to have a toolbar, rebar, and status bar. To keep this app simple, uncheck all those and click Finish.
Going through the wizard (VC 7)Click File|New|Project in VC and select ATL/WTL AppWizard from the list. We'll be rewriting the clock app, so enter WTLClock for the project name and click OK:
When the AppWizard screen comes up, click Application Type. This page is where you select an SDI, MDI, or dialog-based app, along with some other options. Select the options as shown here and click User Interface Features:
The last page is where we can choose to have a toolbar, rebar, and status bar. To keep this app simple, uncheck all those and click Finish.
Examining the generated codeAfter finishing the wizard, you'll see three classes in the generated code: There is also a
Finally, we have class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>, public CMessageFilter, public CIdleHandler { public: DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME) BEGIN_UPDATE_UI_MAP(CMainFrame) END_UPDATE_UI_MAP() BEGIN_MSG_MAP(CMainFrame) // ... CHAIN_MSG_MAP(CUpdateUI<CMainFrame>) CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>) END_MSG_MAP() BOOL PreTranslateMessage(MSG* pMsg); BOOL OnIdle(); protected: CWTLClockView m_view; };
LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, WS_EX_CLIENTEDGE); // register object for message filtering and idle updates CMessageLoop* pLoop = _Module.GetMessageLoop(); pLoop->AddMessageFilter(this); pLoop->AddIdleHandler(this); return 0; }
The generated Our next stop on the WTL tour is CMessageLoop Internals
int Run() { MSG msg; for(;;) { while ( !PeekMessage(&msg) ) CallIdleHandlers(); if ( 0 == GetMessage(&msg) ) break; // WM_QUIT retrieved from the queue if ( !CallPreTranslateMessageFilters(&msg) ) { // if we get here, message was not filtered out TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; }
Notice that there are no calls to CFrameWindowImpl Internals
LRESULT OnSize(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) { if(wParam != SIZE_MINIMIZED) { T* pT = static_cast<T*>(this); pT->UpdateLayout(); } bHandled = FALSE; return 1; } This checks that the window is not being minimized. If not, it delegates to void UpdateLayout(BOOL bResizeBars = TRUE) { RECT rect; GetClientRect(&rect); // position bars and offset their dimensions UpdateBarsPosition(rect, bResizeBars); // resize client window if(m_hWndClient != NULL) ::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER | SWP_NOACTIVATE); } Notice how the code is referencing Back to the Clock ProgramNow that we've seen some of the frame window class details, let's get back to our clock app. The view window can handle the timer and drawing, just as class CWTLClockView : public CWindowImpl<CWTLClockView> { public: DECLARE_WND_CLASS(NULL) BOOL PreTranslateMessage(MSG* pMsg); BEGIN_MSG_MAP_EX(CWTLClockView) MESSAGE_HANDLER(WM_PAINT, OnPaint) MSG_WM_CREATE(OnCreate) MSG_WM_DESTROY(OnDestroy) MSG_WM_TIMER(OnTimer) MSG_WM_ERASEBKGND(OnEraseBkgnd) END_MSG_MAP() }; Note that it's fine to mix the ATL message map macros with the WTL versions, as long as you change
The last thing we'll add to this app is UI updating. To demonstrate this, we'll add a new top-level menu item called Clock that has Start and Stop commands to stop and start the clock. The Start and Stop menu items will be enabled and disabled as appropriate. UI UpdatingSeveral things work together to provide idle-time UI updating: a
To hook up UI updating, we need to do four things:
The AppWizard-generated code takes care of the first three parts for us, so all that's left is to decide which menu items we'll update, and when they will be enabled or disabled. New menu items to control the clockLet's add a new Clock menu to the menu bar, with two items:
Then we add an entry in the class CMainFrame : public ... { public: // ... BEGIN_UPDATE_UI_MAP(CMainFrame) UPDATE_ELEMENT(IDC_START, UPDUI_MENUPOPUP) UPDATE_ELEMENT(IDC_STOP, UPDUI_MENUPOPUP) END_UPDATE_UI_MAP() // ... }; Then whenever we want to change the enabled state of either item, we call This system works differently from MFC's Calling UIEnable()Let's go back to the LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { m_hWndClient = m_view.Create(...); // register object for message filtering and idle updates // [omitted for clarity] // Set the initial state of the Clock menu items: UIEnable ( IDC_START, false ); UIEnable ( IDC_STOP, true ); return 0; } And here's what the Clock menu looks like when the app is started:
The communication could be done through class CMainFrame : public ... { public: BEGIN_MSG_MAP_EX(CMainFrame) // ... COMMAND_ID_HANDLER_EX(IDC_START, OnStart) COMMAND_ID_HANDLER_EX(IDC_STOP, OnStop) END_MSG_MAP() // ... void OnStart(UINT uCode, int nID, HWND hwndCtrl); void OnStop(UINT uCode, int nID, HWND hwndCtrl); }; void CMainFrame::OnStart(UINT uCode, int nID, HWND hwndCtrl) { // Enable Stop and disable Start UIEnable ( IDC_START, false ); UIEnable ( IDC_STOP, true ); // Tell the view to start its clock. m_view.StartClock(); } void CMainFrame::OnStop(UINT uCode, int nID, HWND hwndCtrl) { // Enable Start and disable Stop UIEnable ( IDC_START, true ); UIEnable ( IDC_STOP, false ); // Tell the view to stop its clock. m_view.StopClock(); } Each handler updates the Clock menu, then calls a method in the view, since the view is the class that controls the clock. The One Last Note on Message MapsIf you're using VC 6, you might have noticed that when you change
This happens because ClassView doesn't recognize #if (ATL_VER < 0x0700) #undef BEGIN_MSG_MAP #define BEGIN_MSG_MAP(x) BEGIN_MSG_MAP_EX(x) #endif Next Stop, 1995We've only just begun to scratch the surface of WTL. In the next article, I'll bring our sample clock app up to 1995 UI standards and introduce toolbars and status bars. In the meantime, experiment a bit with the Copyright and licenseThis article is copyrighted material, (c)2003-2005 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here. The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefiting from my code) but is not required. Attribution in your own source code is also appreciated but not required. Revision History
Series Navigation: « Part I (ATL GUI Classes) | » Part III (Toolbars and Status Bars) | ||||||||||||||||||||