浙林龙哥

   :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

 

By Richard Grimes

From http://www.devx.com/free/mgznarch/vcdj/1999/febmag99/usingatl1.asp

This is the first installment of an ATL column I will be writing for VCDJ. I’ll be showing you some neat tricks you can do with ATL and keeping you up-to-date on any new developments and technologies associated with the library. There are some exciting things ahead for ATL, so keep watching this space!

This month I will develop a Win32 application with ATL, using no COM whatsoever. Like me, you may have often wanted to write a small graphical Win32 application without the overhead of the document-view architecture of MFC. Don’t get me wrong—MFC is good at what it is designed to do, but it can be overkill for many small applications.

In this article I’ll describe how you can create a Win32 application that creates windows using the ATL windowing classes. In addition, I will introduce some little-known ATL wrapper classes that make using Win32 common controls a simple task.

Create your application

Let’s explore how to create an application that will allow you to use the ATL windowing classes. You have two options: You can create an ATL project and strip out the COM ATL code the wizard generates for you, or you can start with a Win32 project and add ATL support. I will take the second approach because it provides insight into how the ATL Object Wizard works.

You have probably wanted to add a COM object to a non-ATL application at one time or another, and decided that ATL would be the best way to do this. The easiest way to add ATL code is to use the Object Wizard, so select New ATL Object from the Insert menu in the Visual C++ IDE. However, unless you have a project created by the ATL or MFC AppWizard, you will get a dialog telling you that you cannot insert ATL objects into the current project. The message reported is not quite true: While you cannot use Object Wizard to insert an ATL object, you can add ATL support and new ATL classes by hand.

The Object Wizard does a lot of work for you, so it would be nice to be able to use it anyway. The wizard checks for certain project settings and files to see if the application is suitable, so how can you fool it into thinking that the project is an ATL project?

The first criterion is that the project must be a Win32 EXE or DLL project (that is, the project should have the /subsystem:windows linker option); Object Wizard will not work with console applications. If I want to create a Win32 application I usually use the AppWizard Win32 Simple Application option.

The project must have a cpp file that has the same name as the project and this should have an ATL object map (the map can be empty, but it must be present). Finally, the project must have an IDL file with the same name as the project and this file must have a Library block. Again, this file does not have to have any declared interfaces or coclasses, nor does it have to be part of the build, but it has to be present. For example:

library DummyLib
{
};

This is the entire IDL file, and it will not compile, so when you add it to the project you should use the project settings to exclude it from the build.

Of course, to be able to compile a project you will need to include appropriate ATL headers in your stdafx.h file. At the minimum, you should include atlbase.h (you will need to make sure that stdafx.h does not also include windows.h), and if you want to use the windowing classes you will need to include atlwin.h and atlcom.h, and declare _Module in your main cpp file. This is necessary because the classes in atlcom.h and atlwin.h will access this global variable to get hold of module level data. Thus you need to add:

CComModule _Module;

and add an extern forward reference to this variable in stdafx.h before including atlcom.h, so that all source files in your project will have access to it. This object will have to be initialized in your module’s entry point code by calling CComModule::Init() with NULL for the object map pointer.

The bare minimum code in the main cpp file for an EXE is:

#include "stdafx.h"
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
int APIENTRY WinMain(HINSTANCE hInstance,
				HINSTANCE hPrevInstance,
   LPSTR lpCmdLine, int nCmdShow)
{
   return 0;
}

A DLL project will, of course, have a DllMain() instead. Once you have done all this to your project you will be able to run the Object Wizard.

Why go through all this effort to have an existing Win32 EXE or DLL project work with Object Wizard? There are three principal situations when this makes sense. The first is if you want your project to use an OLE DB data source. Visual C++ 6.0 introduced a new Object Wizard type that will generate wrapper classes to allow you to access a table or perform a command on a data source. Although these classes use COM and ATL, they do not implement COM objects and hence you do not need an ATL project. Although you can write these consumer classes by hand, it is far easier to let the Object Wizard do all the work.

The second situation is if you want to implement an object to sink events from a connection point. Such an object will not be created by any other process so object registration and class factory support for the object aren’t needed. Instances of this coclass are created in your client process and passed to the connection point via a call to IConnectionPoint::Advise(). Again, although you can implement this by hand, Object Wizard will do most of the work for you. Note that by default it will make the coclass externally createable, so you still have the additional work of stripping away code (in particular, you’ll need to remove the interface definition from the IDL file, change the object map entry to make the object noncreateable, and, possibly, remove the CComCoClass<> base class).

The final situation, and the subject of this article, is if you want to use ATL to provide lightweight Win32 windowing classes. These classes do all the work of providing a window procedure, registering a window class, and creating the window using suitable styles. What’s more, if you have an ATL windowing class or an ATL dialog with a dialog resource, you can use the ATL message wizard to add message handlers.

ATL windowing classes

One key ATL 3.0 windowing class is CWindow. This is a simple class that wraps an HWND. Most of its methods merely check for the validity of the HWND data member and then call the appropriate Win32 APIs, although there are some other methods like CenterWindow() that do more work.

CWindow works well if you have an HWND of an existing window. However, to change the window’s behavior or add data to it you need to send it messages, and this means you have to delve back into the pre-MFC days when Windows programming meant you had to understand how to squeeze the appropriate information into the parameters of SendMessage(). For example, if you have a list box you can add a string to it using the following code:

CWindow lst;
lst = GetDlgItem(IDC_ITEMS);
lst.SendMessage(LB_ADDSTRING, 0, (LPARAM)str);

Here, GetDlgItem() is a member function of the class used to implement the window or dialog that contains the list box. Writing code like this can become quite tedious; to help you the ATL team has produced wrapper classes for most Win32 common controls.

The header for these classes, called ATLControls.h, can be found in the ATLCon example shipped with Visual C++. I find this header so useful that I have copied it to my Atl\Include folder; however, note that although this header looks like it ought to be part of the core ATL library, it is completely undocumented and is not supported as part of ATL. The classes are all defined in the ATLControls namespace, so you should either import this in the global namespace through a using statement or fully qualify the name of the class. In the code accompanying this article, which is on www.vcdj.com, I have used the former approach.

The following code uses the ATLControls::CListBox wrapper class:

CListBox lst;
lst = GetDlgItem(IDC_ITEMS);
lst.AddString(W2T(bstr));

As you can see, using these wrapper classes makes your code much more readable. Table 1 shows the wrapper classes provided in ATLControls.h. Some of these are standard Win32 controls, some are common controls, and some are controls made available by IE4, which should therefore be installed before using them. If your project uses common controls, remember to call InitCommonControls() in WinMain().

Table 1: Wrapper classes
Class Win32 class name Description
CStatic STATIC Static label
CButton BUTTON Command button
CListBox LISTBOX List box
CDragListBox LISTBOX List box with support for dragging items
CComboBox COMBOBOX Combo box
CEdit EDIT Text box
CScrollBar SCROLLBAR Scroll bar
CRichEditCtrl RICHEDIT Rich edit control
CListViewCtrl SysListView32 List view, like the right-hand pane of Windows Explorer
CTreeViewCtrl SysTreeView32 Tree view, like the left-hand pane of Windows Explorer
CTreeViewCtrlEx SysTreeView32 Same as CTreeViewCtrl but uses the wrapper class CTreeItem rather than the raw HTREEITEM
CHeaderCtrl SysHeader32 Headers with resizable columns
CToolBarCtrl ToolbarWindow32 Tool bar positioned below a menu
CStatusBarCtrl msctls_statusbar32 Status bar positioned at the bottom of a window
CTabCtrl SysTabControl32 Tabs, like those used in a tabbed dialog
CToolTipCtrl tooltips_class32 Tool tip
CTrackBarCtrl msctls_trackbar32 A slider control used to set a value
CUpDownCtrl msctls_updown32 A control used to increment and decrement values
CProgressBarCtrl msctls_progress32 Visual progress indication
CHotKeyCtrl msctls_hotkey32 Used to specify that a key combination will perform some action
CAnimateCtrl SysAnimate32 Control used to play an AVI
CReBarCtrl ReBarWindow32 The fancy toolbars used by IE4
CComboBoxEx ComboBoxEx32 A combo box that can have images for the items
CDateTimePickerCtrl SysDateTimePick32 An edit box-like control through which you can pick a date or time
CMonthCalendarCtrl SysMonthCal32 Control that shows a month’s dates
CIPAddressCtrl SysIPAddress32 An edit box-like control with four fields separated by dots
CPagerCtrl SysPager A control that allows you to ”scroll” another window

In addition, the header defines some utility classes, which are shown in Table 2.

Table 2: Utility classes
Class Description
CImageList Wraps an HIMAGELIST for use with list views and tree views
CTreeItem Wraps a HTREEITEM, used with CTreeViewCtrlEx
CToolInfo Wraps a TOOLINFO, used with CToolTipCtrl
CDragListNotifyImpl Class used to handle notifications from a drag list box
CFlatScrollBar Changes a window to use flat control bars
CCustomDraw Class used to add custom draw support

The example code for this article shows a RegEdit-like utility (Figure 1) used to view items in the registry (it does not allow you to change items, but the code to do so is not difficult to implement).


Figure 1 The Registry Viewer sample application

The example on www.vcdj.com has a tree view, a list view, and a status bar common control. You will find the majority of the code in RegistryDlg.h where I use CTreeViewCtrl, CListViewCtrl, and CStatusbarCtrl from ATLControls to do the work of passing information to, and getting data from, these controls. For example, to set the image list for the tree view (to specify the bitmaps used when branches are open or closed) I used:

// from OnInitDialog()
// m_imageTV is a data member of CImageList
m_imageTV.Create(IDB_NODE, 16, 0, CLR_NONE);
CTreeViewCtrl tv = GetDlgItem(IDC_TREEVEW);
tv.SetImageList(m_imageTV, TVSIL_NORMAL);

Creating windows

Although CWindow and the classes in ATLControls.h allow you to manipulate an existing window, they do not provide support for message handling. If you remember the old days of SDK Windows programming, you’ll realize that to do this you need to register a window class that specifies the address of a function, the window procedure that will handle all the messages sent to the window.

This procedure was typically implemented with a large and unreadable C switch.

MFC made Windows programming much simpler with the ClassWizard and message maps, and ATL has borrowed this idea with ATL message maps and the Visual C++ message map wizard. When you create a window through CWindowImpl<>, it will register a window class for you, specifying a static member of CWindowImpl<> as the window class. This method has access to the this pointer of your CWindowImpl<> object through the clever use of a thunk, and thus is able to call a member function called ProcessWindowMessage() that contains the message handler code.

To implement this method your class declares a message map. For example:

BEGIN_MSG_MAP(CMyWindow)
   //add message handlers
BEGIN_MSG_MAP()

Once you have added a message map to a class, you can add message handlers using the Visual C++ message map wizard. This wizard is accessed through the Class View context menu; note, however, that the wizard only knows about a subset of Windows messages and it doesn’t allow you to add range handlers (that is, handle a range of windows messages with a single method).

In addition, the wizard makes little attempt to “crack” messages. This is important if you are moving from MFC to ATL because the ClassWizard adds handlers that have recognizable parameters. Take as an example the WM_SIZE message; with raw Windows message handling the wParam has a flag to indicate the reason for resizing a window, while the IParam holds the new size of the window with the height in the top 16 bits and the width in the bottom 16 bits.

If you add a handler for this method with the ClassWizard for an MFC project, you will get an OnSize() method added to your class that has three parameters: one for the flag and the other two for the new sizes. There is no confusion about which parameter means what. If you add a handler using the ATL message map wizard you will get a method that is passed the wParam and the IParam; it is your responsibility to obtain the width and height from the IParam.

The example code has the following map:

BEGIN_MSG_MAP(CRegistryDlg)
   MESSAGE_HANDLER(WM_INITDIALOG,
					OnInitDialog)
   MESSAGE_HANDLER(WM_CLOSE, OnClose)
   MESSAGE_HANDLER(WM_SIZE, OnSize)
   NOTIFY_HANDLER(IDC_REGTREE,
			TVN_SELCHANGED, OnClickNode)
   NOTIFY_HANDLER(IDC_REGTREE, TVN_ITEMEX
				PANDING, OnExpandingNode)
   NOTIFY_HANDLER(IDC_REGTREE, TVN_SELCHANG
					ING, OnSelNodeChanging)
   COMMAND_HANDLER(ID_FILE_EXIT, 0, OnExit)
   COMMAND_HANDLER(ID_HELP_ABOUTREGVIEW, 0, OnAbout)
END_MSG_MAP()

The first entry was added by the Object Wizard and the following five were added using the message map wizard. The final two are used to handle WM_COMMAND messages sent when a user clicks on a menu item. Although you can add menus to ATL windows, there is no support for them in the wizard and therefore I had to add these macros, and the handler methods, by hand.

CWindowImpl<> also allows you to sub- and super-class, that is, replace the window procedure of a registered window class, or create a new class based on an existing one. This is integrated with the Object Wizard, which allows you to base an ATL control on a Win32 control. The class also supports parent windows with child windows, as well as routing the child notification messages through a single message map using a facility known as alternate message maps. I won’t cover that topic here; you can find information about it in the various ATL books on the market, including mine.

Dialogs

The ATL class CDialogImpl<> implements a simple modal or modeless dialog from a dialog template resource, which is associated with the class through an anonymous enumeration that has a single member called IID. Client code that uses this class should create an instance of the class and then call either DoModal() to create a modal dialog or Create() to create a modeless dialog.

CDialogImpl<> derives from CWindowImplRoot, so if you derive a class from it you should implement a message map. When you run the message map wizard, it reads the dialog template, determines the controls on the template, and allows you to handle notification messages sent by these controls. However, as mentioned earlier, the wizard only gives a subset of the possible notification messages, but it is simple to add them by hand.

Such a dialog class will be able to use Win32 controls on the dialog template, but if you want to be able to use ActiveX controls you should use CAxDialogImpl<>. This is essentially the same as CDialogImpl<>, except during the dialog creation it scans the dialog template for ActiveX controls and ensures that they are created and initialized correctly. The ATL Object Wizard allows you to add a dialog class using the Dialog type in the Miscellaneous category, and this will add a class derived from CAxDialogImpl<>. In the example code I have used the Object Wizard to add a dialog, but since the dialog does not use ActiveX controls I have changed the base class from CAxDialogImpl<> to CDialogImpl<>. This change releases the project from a dependence on atlhost.h. In addition to adding code, the Object Wizard will also add a dialog template so all you need to do is add controls using the resource editor and then use the message map wizard to add message handlers. In the example code I provide a handler for the WM_SIZE message sent to the dialog when a user resizes or maximizes it. The handler was added with the message map wizard for the dialog and looks like this:

LRESULT OnSize(UINT uMsg, WPARAM wParam,
					LPARAM lParam, BOOL&)
{
   CStatusBarCtrl stat;
   stat = GetDlgItem(IDC_STATUS);
   RECT rect;
   stat.GetWindowRect(&rect);
   int statHeight = rect.bottom - rect.top;
   stat.SetWindowPos(NULL, 0, HIWORD(lParam)
							- statHeight,
   LOWORD(lParam), statHeight, SWP_NOZORDER | SWP_SHOWWINDOW);

   CTreeViewCtrl tv;
   tv = GetDlgItem(IDC_REGTREE);
   tv.SetWindowPos(NULL, 0, 0,
				LOWORD(lParam)/2,
       HIWORD(lParam) - statHeight,
       SWP_NOMOVE | SWP_NOZORDER |
					SWP_SHOWWINDOW);

   CListViewCtrl lv;
   lv = GetDlgItem(IDC_LISTPANE);
   lv.SetWindowPos(NULL, LOWORD(lParam)/2, 0,
						LOWORD(lParam)/2,
      HIWORD(lParam) - statHeight,
			SWP_NOZORDER | SWP_SHOWWINDOW);
   lv.SetColumnWidth(0, LOWORD(lParam)/4);
   lv.SetColumnWidth(1, LOWORD(lParam)/4);
   return 0;
}

CDialogImpl<> derives from CWindow so I can call GetDlgItem() to obtain an HWND for the three controls. I first calculate the height of the status bar and then change the size according to the new size of the dialog, then I calculate the sizes of the tree view and list view controls taking into account the status bar and the new sizes passed to the method. Finally, in the case of the list view, I specify a default width for the two columns that it shows.

You do not have to write a large amount of code to implement a dialog box because ATL provides a class, CSimpleDialog<>, that can be used for simple dialogs. The example project has a dialog resource called IDD_ABOUTDIALOG, which has a copyright message and is shown when the user selects the About menu item. This is handled by the following code:

LRESULT OnAbout(WORD, WORD, HWND, BOOL&)
{
   CSimpleDialog dlg;
   dlg.DoModal(m_hWnd);
   return 0;
}

CSimpleDialog<> derives from CDialogImpl<> but it has a message map so I can create an instance of it. Of course, this class cannot be used with dialogs that accept input values or have controls that are initialized through code. If you do have such a dialog then you should derive from CDialogImpl<> and implement your WM_INITDIALOG handler and a method to handle the message when the OK button is clicked. To do this your class also needs to declare a message map like this:

BEGIN_MSG_MAP(CMyDialog)
   MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
   COMMAND_HANDLER(IDOK, BN_CLICKED, OnOK)
END_MSG_MAP()

Summary

In this article I have shown you two important techniques. The first is how to prepare a Win32 project to allow Object Wizard to run. This is useful if you want to add a dialog or a simple sink object to a project because ATL will generate the basic code for you. However, it also allows you to run the Object Wizard OLE DB consumer generator, which allows you to generate OLE DB consumer classes quickly. Using the steps I have recommended, you can use the Object Wizard with more than just ATL and MFC projects.

The second technique I demonstrated was how to write a Win32 windowed application using the ATL windowing classes. Visual C++ has a message map wizard that allows you to add message handlers with almost the ease of MFC’s ClassView. However, what you lose in this tool’s flexibility you gain in the implementation: ATL is designed to produce lean code, and this coupled with the wrapper classes in ATLControls.h make it the choice when you want to write a small Win32 application. You will be surprised at how easy it is to produce windows code with ATL and how rich the support is. Remember, ATL is not only for ActiveX!

posted on 2004-09-08 19:12  浙林龙哥  阅读(1933)  评论(0)    收藏  举报