Developers Workshop to COM and ATL 3.0
| < Free Open Study > |
|
When you need to create a dialog that must respond to user input, CSimpleDialog<> won't cut the mustard. To develop a more interactive box, you will need to create a new C++ class that derives from either CDialogImpl<> or CAxDialogImpl<>. Recall that the difference between these two templates is in their support (or lack thereof) for hosting ActiveX controls. Unlike the process of building a Win32 window with ATL, interactive dialog boxes are inserted into your ATL projects using the ATL Object Wizard, as seen in Figure 13-7:
This selection from the Miscellaneous category inserts a new dialog into your current ATL project. By default, this class is derived from the full-blown CAxDialogImpl<> template. Therefore, you (by default) have a dialog that is able to host ActiveX controls. This template gathers most of its functionality from CDialogImpl<> and its immediate base class CDialogImplBase<>.
When you use the ATL Object Wizard to insert a new dialog box, you will be given a dialog box resource template to design your GUI as well as a new C++ class derived from CAxDialogImpl<>. Here is a look at the initial header file:
// Your new CAxDialogImpl<> derived class, courtesy of the ATL Object Wizard. class CTestDialog : public CAxDialogImpl<CTestDialog> { public: CTestDialog() {} ~CTestDialog() {} enum { IDD = IDD_TESTDIALOG }; BEGIN_MSG_MAP(CTestDialog) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) COMMAND_ID_HANDLER(IDOK, OnOK) COMMAND_ID_HANDLER(IDCANCEL, OnCancel) END_MSG_MAP() LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { return 1; // Let the system set the focus } LRESULT OnOK(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { EndDialog(wID); return 0; } LRESULT OnCancel(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { EndDialog(wID); return 0; } };
Adding a Custom Handler
The wizard has generated an initial message map, with entries for WM_INITDIALOG, as well as WM_COMMAND handlers for the dialog's initial OK and Cancel buttons via the COMMAND_ID_HANDLER macro. This alternative message map entry is used to route commands from a specific widget to some method in your class. This is much like the MESSAGE_HANDLER macro; however, rather than responding to standard Windows messages (e.g., WM_CREATE), COMMAND_ID_HANDLER responds exclusively to WM_COMMAND messages, given the ID of the widget:
// This MSG_MAP macro is used to respond to WM_COMMAND messages, // using a control's resource ID as a base line. #define COMMAND_ID_HANDLER(id, func) \ if(uMsg == WM_COMMAND && id == LOWORD(wParam)) \ { \ bHandled = TRUE; \ lResult = func(HIWORD(wParam), LOWORD(wParam), (HWND)lParam, bHandled); \ if(bHandled) \ return TRUE; \ }
Because your new dialog class supports an ATL message map, you are able to use the same Add Windows Message Handler Wizard as before. The upside of this story is that as you build the GUI front end of your dialog box, you may respond to numerous control- specific messages, in the same vein as the MFC ClassWizard. To illustrate this, assume we have assembled the following GUI for a new dialog template:
The goal of this dialog box is to generate a string message in response to the clicking of our custom button (IDC_GETMESSAGE). This message will be placed into the dialog's edit control (IDC_MSG).
Using the Windows Message Handler CASE tool, we may select the IDs of the individual controls, and elect to respond to given notifications. If we want to respond to the BN_CLICKED event for IDC_GETMESSAGE, we make the selection in the wizard and type in a method name to handle the event, as shown in Figure 13-9.
If we examine the changes made in our dialog class, we will see an additional entry in our message map, as well as stub code for the command handler. Here is a simple implementation of the OnClickedGetmessage() method, which makes use of the CWindow::SetDlgItemText(). Figure 13-10 shows a test run.
// When the custom button is clicked, send a string to the edit box. LRESULT OnClickedGetmessage(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { // Place a message into IDC_MSG SetDlgItemText(IDC_MSG, "You will never program CORBA"); return 0; }
On a related note, the SetDlgItemText() method of CWindow has a counterpart named GetDlgItemText(), which is used to extract the current text within an edit field based off the resource ID. In fact, CWindow provides a large number of "control-specific" helper functions that allow you to perform basic manipulation of radio buttons, combo boxes, and other basic controls. Keep in mind that you will still be expected to dive headlong into Win32 API function calls to execute more elaborate code.
Note | There is an unsupported set of ATL control wrapper classes that can be used when developing ATL Windows applications. <atlcontrols.h> is included as part of the Atlcon sample program included with MSDN. These C++ classes look and feel much like their MFC equivalents, and are worth checking out if you plan to do any hardcore Windows-based ATL development. |
That covers the basics of how ATL provides you with "just enough" framework support to allow you to create lightweight Windows applications. To close this chapter, the following lab will give you a chance to create a full Windows window as well as a simple dialog box using ATL. Even if you do not plan to use ATL to build traditional Windows applications, the information presented in this chapter is critical when building GUI-based COM objects, such as ActiveX controls.
Lab 13-1: ATL Windowing
Here, you will build a main window with the ATL framework, complete with a working menu and a custom About box. However, rather than building a simple stand-alone window, the ATL window developed in this lab will expose its services to the outside world from a standard COM interface. In this way, you allow COM clients to create, manipulate, and kill a standard Windows window.
On the CD The solution for this lab can be found on your CD-ROM under:Labs\Chapter 13\ATL WindowLabs\Chapter 13\ATL Window\Client
Step One: Create the Initial Project
In this lab you will be creating an ATL window that can be manipulated with a custom COM interface. Begin by creating a new EXE workspace using the ATL COM AppWizard. As we are going to make use of a number of ATL windowing-specific templates, you must specifically include <atlwin.h>. As mentioned, the best place to do so is in your project's precompiled header file. Be sure you add this line after the definition of CExeModule:
// Preparing the project to use the ATL windowing templates. class CExeModule : public CComModule { public: LONG Unlock(); DWORD dwThreadID; HANDLE hEventShutdown; void MonitorShutdown(); bool StartMonitor(); bool bActivity; }; extern CExeModule _Module; // Must add <atlwin.h> after the declaration of CExeModule! #include <atlwin.h>
Step Two: Create Custom Resources
Begin by inserting a new Icon resource into your project using the Insert | Resource menu selection. As this icon will be used to represent your window, render some interesting image and assign this image an ID of IDI_MAINICON. See Figure 13-11 for an example.
Now insert a custom menu (Insert | Resource). We don't need any massive functionality here; a topmost menu item with a single selectable submenu will do just fine. Later we will associate this menu item to a command handler used to show a custom About box. Assign the ID of the menu resource itself to IDR_MAINMENU, and the subitem to IDM_ABOUT. See Figure 13-12.
The final custom resource we need for this lab is a new dialog template. This will be hosted in a later step by the CSimpleDialog<> template, and will serve as a simple About box. Assign a resource ID of IDD_ABOUTBOX.
Now that we have some resources, we can move onto the more interesting world of code.
Step Three: Create a CWindowImpl<> Derived Class
The next step is to create a C++ class derived from CWindowImpl<>. There is no "Insert New Window" Wizard as of ATL 3.0, so we need to create this class by hand. Name this class CMyWindow, and define an empty message map:
// Be sure to include the extra spaces between your template parameters! class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CWinTraits<WS_CAPTION | WS_POPUPWINDOW | WS_VISIBLE, 0> > { public: CMyWindow(); virtual ~CMyWindow(); BEGIN_MSG_MAP(CMyWindow) END_MSG_MAP() };
This window will allow a COM client to paint some text onto the client area and draw a circle of some size and color. All of this behavior can be captured by the following member functions, which you should add to CMyWindow's public sector:
// These are public methods of your new window. void ChangeText(BSTR newString); void DrawACircle(int x, int y, int width, int height); void ChangeTheColor(OLE_COLOR newColor);
To fill out the implementation of each function, we will need to also create some private data members. Add a CComBSTR to represent the text to be drawn, an OLE_COLOR variable to represent the color used to paint the circle, and a RECT to hold the bounding box of the circle itself. Finally, add a COLORREF data member:
// Private data members. private: RECT m_Rect; // Size of circle. CComBSTR m_Text; // Text to paint. OLE_COLOR m_clrFillColor; // OLE color of circle. COLORREF m_colFore; // GDI color of circle.
COLORREF is a Win32 data type that stores the red-green-blue value of some color. The OLE_COLOR data type is an OLE data type used to express color. Given that your client will hand you an OLE_COLOR type, we will need to translate this into a COLORREF for use with the Win32 GDI functions.
Let's begin by implementing ChangeTheColor(), which simply assigns the internal OLE_COLOR and invalidates the client area to redraw the client area:
// Set the OLE_COLOR. void CMyWindow::ChangeTheColor(OLE_COLOR newColor) { m_clrFillColor = newColor; Invalidate(); }
ChangeTheText() performs the same sort of operation on the CComBSTR:
// Set the text. void CMyWindow::ChangeText(BSTR newString) { m_Text = newString; Invalidate(); }
The DrawACircle() method takes four integers which build up the bounding rectangle used to draw our circle. Fill the private m_Rect data member with each value, and call Invalidate() to refresh the image:
// Set the bounding rectangle of this circle. void CMyWindow::DrawACircle(int top, int left, int bottom, int right) { m_Rect.left = left; m_Rect.right = right; m_Rect.top = top; m_Rect.bottom = bottom; Invalidate(); }
Now that we have some initial behavior, we are ready to add some code to construct the window. Call the Create() method of CWindowImpl<>, passing in an initial size as well as our custom menu item and icon. Notice that the menu is attached using a default parameter of Create(), whereas our icon is set by tweaking the WNDCLASSEX structure. Finally, our constructor should assign all state data to some initial values. Here is the lineup:
// Prep our state. CMyWindow::CMyWindow() { // Set the BSTR. m_Text = "Default String"; // Set color. m_colFore = RGB(0, 255, 0); // Set bounding rectangle of the circle. m_Rect.left = 10; m_Rect.right = 70; m_Rect.top = 10; m_Rect.bottom = 70; // Set the custom icon. HICON hCustomIcon = LoadIcon(_Module.GetResourceInstance(), MAKEINTRESOURCE(IDI_MAINICON)); GetWndClassInfo().m_wc.hIcon = hCustomIcon; // Load the custom menu. HMENU hMenu = LoadMenu(_Module.GetResourceInstance(), MAKEINTRESOURCE(IDR_MAINMENU)); // Call the inherited Create() method to get window going. RECT r = {200, 200, 500, 500}; Create(NULL, r, "My ATL Window", 0, 0, (UINT)hMenu); }
Your destructor should clean things up by calling DestroyWindow():
// Kill this window. CMyWindow::~CMyWindow() { if(m_hWnd) DestroyWindow(); }
The next order of business is to add a WM_PAINT message handler. Access the Add Windows Message Handler CASE tool from ClassView, and route this message to your custom CMyWindow class. This will populate your message map with a new MESSAGE_HAN- DLER entry as well as stub code for a method named OnPaint(). In this method, we will need to render the current text, and paint a circle with the correct color and within the correct bounding rectangle. To do so will require that we translate the client-supplied OLE_COLOR into a valid COLORREF using OleTranslateColor(). The remaining code is straight Win32 GDI logic:
// Render the text and circle. LRESULT CMyWindow::OnPaint(UINT msg, WPARAM wparam, LPARAM lparam, BOOL& handled) { PAINTSTRUCT ps; HDC hdc = GetDC(); HBRUSH hOldBrush, hBrush; BeginPaint(&ps); // Draw text. USES_CONVERSION; TextOut(hdc, 70, 70, OLE2A(m_Text.m_str), m_Text.Length()); // Make a filled circle. OleTranslateColor(m_clrFillColor, NULL, &m_colFore); hBrush = CreateSolidBrush(m_colFore); hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); Ellipse(hdc,m_Rect.left,m_Rect.top,m_Rect.right,m_Rect.bottom); // Clean up. SelectObject(hdc, hOldBrush); DeleteObject(hBrush); EndPaint(&ps); ReleaseDC(hdc); return 0; }
The final step is to rig up the selection of your custom menu item to a command handler function. In this case, the ATL CASE tools will not give us too much assistance, so we need to edit our message map by hand. Add in a COMMAND_HANDLER macro, which handles the selection of your submenu:
// Update your message map to handle the selection of your custom menu item. BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_PAINT, OnPaint) COMMAND_HANDLER(IDM_ABOUT, 0, OnAbout) END_MSG_MAP()
Every COMMAND_HANDLER has the exact same signature. In your implementation of OnAbout(), use CSimpleDialog<> to launch your About box:
// Don't forget to include <resource.h>. LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { CSimpleDialog<IDD_ABOUTBOX> d; d.DoModal(); return 0; }
At this point we have a full (but trivial) main window written using the ATL framework. The next step is to allow the outside world to access it.
Step Four: Create the COM Object
Now that we have a custom window object, we will need to expose its functionality to the outside world from a COM coclass. Using the ATL Object Wizard, insert a Simple Object named CoWindow (keeping all defaults).
We will now need to populate our [default] interface with a set of methods, all of which issue commands to an inner CMyWindow object. First, add a private member variable to CoWindow of type CMyWindow, named m_theWnd. Next, add the following methods to the ICoWindow interface:
// This single interface of the coclass allows the outside world to manipulate our // CWindowImpl<> derived object. interface ICoWindow : IDispatch { [id(1), helpstring("Creates the CMyWindow object")] HRESULT CreateMyWindow(); [id(2), helpstring("Kills the CMyWindow object")] HRESULT KillMyWindow(); [id(3), helpstring("Draws the circle")] HRESULT DrawCircle([in] int top, [in] int left, [in] int bottom, [in] int right); [id(4), helpstring("Changes the color")] HRESULT ChangeTheColor([in] OLE_COLOR newColor); [id(5), helpstring("Changes the text")] HRESULT ChangeWindowText([in] BSTR newText); };
The CreateMyWindow() and KillMyWindow() methods allocate and deallocate the private CMyWindow data member maintained by CoWindow:
// User wants a new window. STDMETHODIMP CCoWindow::CreateMyWindow() { m_theWnd = new CMyWindow; m_theWnd->CenterWindow(); return S_OK; } // User is tired of the window. STDMETHODIMP CCoWindow::KillMyWindow() { if(m_theWnd) delete m_theWnd; return S_OK; }
The remaining methods can be implemented by simple delegation. For example, ChangeWindowText() takes the incoming BSTR and sends it into the ChangeText() method of CMyWindow(). As you recall, we set up this method to assign a new BSTR and refresh the window:
// Allow user to change the text. STDMETHODIMP CCoWindow::ChangeWindowText(BSTR newText) { // Change the text. if(m_theWnd) m_theWnd ->ChangeText(newText); else return E_FAIL; return S_OK; }
For completion, here are the remaining methods of ICoWindow:
// Draw! STDMETHODIMP CCoWindow::DrawCircle(int top, int left, int bottom, int right) { if(m_theWnd) m_theWnd -> DrawACircle(top, left, bottom, right); else return E_FAIL; return S_OK; } // Change the color. STDMETHODIMP CCoWindow::ChangeTheColor(OLE_COLOR newColor) { if(m_theWnd) m_theWnd -> ChangeTheColor(newColor); else return E_FAIL; return S_OK; }
And now for the final task-a client to drive the window.
Step Five: A Visual Basic Test Client
For ease of implementation, let's build a VB client. Once you have created a new Standard EXE workspace, set a reference to your new ATL server. Design a GUI which allows the end user to create and kill the window, change the text, and set the dimensions (and color) of the circle. Here is one possible GUI configuration:
The code behind this form is straightforward. Declare a CoWindow variable in your [General][Declarations] section, and use the front end to call each member of the [default] interface. Here is the complete VB code listing:
' [General][Declarations] ' Option Explicit Private w As ATLWINDOWEXAMPLELib.CoWindow Private Sub btnDraw_Click() w.DrawCircle CInt(txtTop), CInt(txtLeft), CInt(txtBottom), CInt(txtRight) End Sub Private Sub btnKill_Click() w.KillMyWindow btnLoad.Enabled = True btnKill.Enabled = False End Sub Private Sub btnLoad_Click() w.CreateMyWindow btnLoad.Enabled = False btnKill.Enabled = True End Sub Private Sub Form_Load() Set w = New CoWindow btnLoad.Enabled = True btnKill.Enabled = False End Sub Private Sub OptionBlue_Click() Dim c As Long c = RGB(0, 0, 255) w.ChangeTheColor c End Sub Private Sub OptionGreen_Click() Dim c As Long c = RGB(0, 255, 0) w.ChangeTheColor c End Sub Private Sub OptionRed_Click() Dim c As Long c = RGB(255, 0, 0) w.ChangeTheColor c End Sub Private Sub Text1_Change() w.ChangeWindowText Text1.Text End Sub
When you run your test client, you are now able to create the window, assign text, and color and size the rendered circle. Here is a test run:
When you select your custom menu item, you will see your dialog box pop up, and as you can see, your custom icon is placed in the upper left-hand corner of the window.
As mentioned earlier in this chapter, the windowing support provided by ATL is a nice middle of the road solution between the raw Win32 API and a full framework such as MFC. This lab illustrated the use of ATL to create a main window. Even if you do not intend to use ATL as a Windows framework, you will find many of these same templates show up when building an ActiveX control.
| < Free Open Study > |
|