Special Edition Using Microsoft® Visual Studio for Enterprise Development

Previous chapterNext chapterContents


- 6 -
Creating Components with Visual C++

by Brad Rhodes and Dave Kincade

Learn about using Visual C++ to create component software and using MFC as a development tool, and explore new Visual C++ enhancements.
Understand MFC history and enhancements added to each version release. Learn about document/view architecture to establish structures to hold, retrieve, store, and change data on request. See how utility classes help in the development of component software.
See COM and DCOM from a C++ programmer's perspective. Learn about the interface concept and how to build COM objects by using Visual C++ and MFC.
Gain an understanding of OLE Automation and see how simple it is to create and use OLE Automation objects with Visual C++ and MFC.
Learn how you can use the Active Template Library (ATL) to create components in the Visual C++ environment.

C ++ has become one of the most powerful and complete programming languages available. Microsoft's Visual C++ extends the C++ programming environment by providing you with a feature-rich development environment and a vast array of extension libraries. Visual C++ follows C++'s design principle by providing you with higher levels of programming extraction without taking away any lower-level access to the computer or operating system. C++'s power is offset by the learning curve that you need to overcome to be effective in Visual C++.

Simpler applications will rarely tap into the power of the Visual C++ environment, yet complex applications will quickly demand the power of the C++ language. Several factors determine an application's complexity:

Flexibility is what gives Visual C++ an edge over other development tools. The more complicated the application, the more it's beneficial to use Visual C++ as the development tool. This flexibility comes from the basis that C++ is the language used to develop most of the Windows NT and Windows 95 operating systems. The APIs for these systems are designed for use from C++.

The recent additions to Visual Basic allowing access to OLE components will greatly improve Visual Basic's capability to access components in the operating system and third-party packages. Visual C++ has access to all the components available to Visual Basic developers and many tools and components that aren't available to VB developers.

In addition to providing a world-class C++ compiler, the Visual C++ development environment delivers a number of reusable components, including the Microsoft Foundation Class (MFC) library and the Active Template Library (ATL). Visual C++ integrates these extension libraries into the development environment and uses wizards to greatly simplify the use of MFC and ATL classes. n

Using Visual C++, an Object-Oriented Language

Because Visual C++ uses the C++ object-oriented programming language, it's well suited to helping you create reusable component objects. These benefits have to do with the language's structure and are independent of the development platform and development tool used. Some specific features account for the advantages of using Visual C++:


See "Object Technology Basics," Chapter 4

Strong Type Checking

C++ is a strongly typed programming language, meaning that it's designed specifically to provide you with compiler error messages whenever you misuse a variable. (First-time programmers find strongly typed languages more difficult to learn because they must be more precise in specifying how a data type is to be handled.) The C++ additions to the C language allow you to take precise control over the use of data types.

Weakly typed languages allow a sloppier approach. In a weakly typed system, the compiler often tries to fix any misuse of a variable by implicitly converting from one type to another. The problems with weakly typed languages begin when the code base is fairly large. After the compiler goes through several layers of software, with each layer potentially going through a type conversion, the data type can easily be mishandled. When this happens, determining the source of the problem is extremely difficult. Strong typing in the language prevents this scenario from ever happening.

Reusability

A key concept of the C programming language was the separation of a module's definition from a module's implementation. In C, a module's definition is placed in a header file (.h), and its implementation is placed in the C file (.c). The header file contains all the externally available interfaces, fully defined. C++ inherits from C this concept of module implementation separate from module definition.

The implementation of a software module can be packaged in an object (.obj) or library (.lib) file. You can reuse a packaged software module over and over again. To use a common module, you include the definition file and link in the implementation. The dynamic link library further propagates this concept by allowing the packaged modules to be linked together at runtime.

C++ enhances the production of componentized software modules by introducing the concepts of inheritance, polymorphism, and encapsulation. These mechanisms provide software components with a more manageable means of extending and enhancing the individual components. The Component Object Module (COM) and Distributed Component Object Module (DCOM) further enhance the production of binary compatible software components.


See "COM and OLE 2.0" Chapter 4

The MFC class library is a great example of an extendible software component library. You can leverage MFC to produce new software components or applications. You can possibly extend any part of MFC so that the software library can be custom-tailored to the requirement.

Later, this chapter shows how you can use Visual C++ to create software components. You'll create a reusable software component with MFC and then create the same reusable software component by using the Active Template Library (ATL).

API Access

C++ was designed to extend the C programming language without cutting off any access to system resources that C contained. Most modern operating systems are written in C or Assembler. The best possible access to a machine's resources is through direct API calls. The C++ language and most of the libraries written for C++ allow you to work at the C++ object level without cutting off access to C's API calls.

Exception Handling

C++ provides many powerful and flexible features. The introduction of exceptions provides a framework for you to create a highly structured error-trapping mechanism. It's only natural that in a component-based application, components are used to perform much of the work. When a component encounters an error condition, the component may not have enough information about the context of the action to provide appropriate error recovery. Exceptions provide a path for the component to communicate back to the application that something unexpected has happened.

Suppose that you have a component used for serial port communications or network communications. If you assume there's a user interface and begin putting up user-interface messages, you'll greatly decrease the component's flexibility. A Windows NT service that runs only in the background can no longer use such a component.

For this reason, exceptions allow the component to detect errors and throw the error condition back out to the higher-level code that's using the component. This higher-level code can catch the error and either put up a user interface message or make an error-log entry. In some cases, a higher-level piece of code may be able to continue despite the error, perhaps after taking corrective action.

Standard Library

C++ provides the standard library. The ANSI specification for this library is the combination of the older C standard library and the new standard template library. These two libraries combine to provide a powerful base of reusable code.

Creating Components with Visual C++

The Visual C++ development environment offers numerous approaches to developing componentized software. You can take these different approaches to develop different component types. Each approach includes a number of positives and negatives, depending on the type of component being developed. It's advantageous to view the different approaches to creating components from a high level before getting into the details of implementing the components.

The first type of componentized software is the development of libraries. Libraries have been used for years to divide up software development efforts. The concept of using software libraries has been extended by the addition of the dynamic link library (DLL). A DLL is very similar to a traditional software library except that DLLs are loaded by an operating system-provided runtime linker rather than statically linked into the executable.

Another type of software component is a class framework or class library. A good example of a class library is the MFC class library described later in the section "Using MFC." A class library is very similar to a normal C library, except that the header file for the class library contains C++ class definitions.

A third type of software components are COM components, which include ActiveX components. COM components extend the binary specification laid out in the DLL concept to include multiple interfaces grouped into classes. Building COM-based components is possible by using just raw C function specifications, the MFC class framework, or the Active Template Library (ATL).


See "COM/DCOM Architecture Basics," Chapter 4

You could build COM components by writing straight to the C API and hand-coding the Interface Definition Language (IDL) files. Thankfully, Visual C++ provides MFC and the ATL class libraries and wizards to greatly simplify the creation of components. Given the availability of MFC and ATL, there's no reason to implement a component by using the straight C API.

The MFC and ATL class libraries are at complete opposites of the design spectrum. The MFC library is a more monolithic group of classes. Implementing classes that extend parts of the library is often difficult without getting involved with other classes in the library. When developing relatively small standalone components, using the MFC library causes quite a bit of overhead. You must statically or dynamically link the MFC implementation modules into your component for a complete delivery. Regardless of your component's size, you need to ship the approximately 1MB mfc42.dll file--a lot of overhead for a small component.

When developing large components with a lot of functionality, the overhead of the MFC library begins to be marginalized. The component may include support for a user interface, drag-and-drop activation, ActiveX document support, and Windows printing. This component type takes advantage of a large amount of the MFC class library. Many of these features would be difficult to implement without MFC. Also, many of these types of components will be used in MFC-developed applications.

Compared with the MFC, the ATL takes a minimalist approach to providing support for component creation. You include only the parts of the ATL that you need. All of the ATL is implemented in header files, and there's no need to link any DLL or static library. For the most part, the ATL components compile down to a very small piece of code that's very near the hand implementation that would be done by writing straight to the C library.

The downside of the ATL is that this is the first version. It doesn't have anywhere near the support or stability that MFC enjoys. Many features aren't yet available in ATL. As with everything in C++, however, it's possible to use the ATL and MFC libraries in combination, taking advantage of ATL's capability to implement COM interfaces and using MFC for more high-level constructs.

Using MFC

By selecting the MFC class library as the basis for a project, you've made the development of Windows-based components an easier job. MFC provides a number of classes that encapsulate most of the conceptual objects used in Windows programming.

Most objects in the MFC world are derived from the abstract base class CObject. It contains the basic reusable functionality built on by other MFC classes. CObject contains the template to access runtime type information (GetRuntimeClass() and IsKindOf()), perform some diagnostic functions (AssertValid() and Dump()), and perform serialization (IsSerializable() and Serialize()).

CObject also contains the capability to dynamically create an instance of an object just by having the runtime type information for that object. This functionality is important to COM because server objects are created dynamically at the client's request. Recent versions of MFC have been enhanced to encapsulate most of the COM concepts.

A Brief History of MFC

In 1989, Microsoft pulled together a number of its brightest minds from many varied areas to form a development group that became known as the application framework development group (AFX). AFX's task was to develop a set of libraries that developers could use as a tool in the development of GUI programs.

The initial design of the AFX class framework contained a deep class inheritance tree, a type of design considered advantageous by object-oriented programmers. The problem is that programmers transitioning from a very flat C API programming environment find it overwhelming to transition to deeply nested class frameworks. Microsoft's later Win32 group was charged with delivering the AFX framework to developers. The Win32 group wanted to protect its investment in the Win32 API and help developers transition to the AFX class library.

The Win32 group took the AFX class framework and flattened the architecture. In some of the inheritance trees, this flattening went from eight classes deep to three classes deep. The resulting version of the framework much more represented a heavy wrapper of the Windows APIs. This new attempt was released in 1992 and was called the Microsoft Foundation Classes (MFC) version 1.0.

With the release of Visual C++ 1.0 in April 1993, Microsoft included the new and improved MFC 2.0, in which you could add popular new features such as context-sensitive help, toolbars, and status bars. It also allowed you to easily conform to Microsoft user-interface standards. Because many developers were worried about having to rewrite applications that used MFC 1.0, Microsoft made MFC 2.0 backward-compatible with version 1.0. This was also the first version of MFC that could be used with Windows NT.

Late in 1993, Microsoft released Visual C++ 1.5 and MFC 2.5. This upgrade added support for OLE 2.0 and ODBC. In late 1994, Microsoft's big addition to MFC version 3.0 was thread safety. With the release of MFC 3.0, Microsoft dropped support for Windows 3.1. The latest version, 5.0, adds substantial new support for the development of COM objects.

Application Framework

The application framework in MFC is a complex yet easy-to-use set of objects that encapsulate everything needed to create the shell of a Windows component. This shell works very well if you're creating an OLE Automation object, a COM object with multiple interfaces, an ActiveX control, or a full standalone Windows application.

The application framework handles all the complexities of writing a Windows component. When a new project is created, Visual C++ builds a new class, derived from the appropriate type, to handle the project type. Then the ClassWizard lets you create new classes or customize existing classes. From a component-development standpoint, the ClassWizard is extremely useful in the creation of OLE Automation components but is somewhat lacking when it comes to creating COM objects not derived from IDispatch.

Document/View Overview  The document/view architecture was initially developed in the SmallTalk object-oriented programming language. This application architecture is based on the concept that you should always separate modules into distinct and well-defined sections. The document/view architecture provides an object-oriented mechanism for separating view-related processing from document-related processing. This is best understood when talking about an application, such as Microsoft Word. View processing is primarily concerned with drawing the representation of a document onscreen and would use the display environment to present the data. For Word, view processing includes drawing the text, scrolling the window, highlighting text, boldfacing text, and so on.

Document processing involves those operations that affect the in-memory representation of the document, such as inserting paragraphs, saving the file, and opening the file. This includes moving the representation to and from the desktop. Many operations that change the representation of the document in memory will result in view changes. The document can always call the view to display the required changes.

As you can see, the document/view architecture is a powerful way of separating modules of a software system. The problem with this architecture, however, is that it's non-intuitive, and working with it the first time is extremely confusing. But after you pass the learning curve, the document/view architecture provides a powerful mechanism for implementing well-defined systems.

This transition can be likened to the transition from a DOS-based programming environment to a Windows-based programming environment. It's a paradigm shift. There's a transition from the traditional concept of having data and displaying it by using the document/view architecture.

The document/view concept is relatively simple: One object (the document) should hold the data, and another object (the view) should know how to display it. The document should have structures to hold the data, know how to retrieve and store the data, and know how to change the data on request. The view should know how to display the data, allow the application's user to modify the data, and tell the document about the changes to the data.

With this architecture, multiple views can display the same data in different ways. For example, a spreadsheet's application document would keep the spreadsheet data in a two-dimensional array. The first view would display the data as a traditional spreadsheet with the data in rows and columns; the second view would display the data as a graph.

Single Document Interface (SDI)  In a Single Document Interface (SDI) application, only one document can be alive at a time. This document can have only one view active at a time but have multiple views defined and allow users to switch between them. A good example of an SDI application is Notepad provided with Microsoft Windows 95 and Windows NT. When it comes to creating component software, SDI can have some advantages. If the component being built is an out-of-process component, by using SDI you can set up the component in a way that each client will create its own instantiation of the component server.

Multiple Document Interface (MDI)  In a Multiple Document Interface (MDI) application, multiple document types can be alive simultaneously, multiple instances of each type of document can be alive simultaneously, and multiple views for each document (regardless of type) can be visible onscreen--all within the same application. Microsoft Word is an example of an MDI application. When creating a component based on the MDI architecture, you can create different interfaces for each document and have one server running that handles multiple clients. Each client would have its own document to hold its data. The data wouldn't cross from one client to another client, unless that functionality was programmed into the server component.

Utility Classes

MFC provides a number of utility classes that can help in the development of a component, application, or any other type of project. These utility classes fall into a number of categories, including file handling, arrays, lists, maps, complex data structures, and data types.

Multithreaded Applications

Because C++ is at a low enough level, it can support the process of creating multiple processing threads in one application. When an application starts executing, a single process thread is running the application. If you need to perform multiple tasks simultaneously, it becomes a simple task to create a second worker or interface thread that will perform any tasks necessary.

On a single-processor machine, a multithreaded program will probably run faster because the computer system has many components. Each component--primarily the CPU and the disk--can become a bottleneck. A multithreaded program can use more resources more efficiently by better using components that aren't bottlenecked. For instance, while one thread waits for the disk to perform an I/O operation, the second thread can use the processor.

On a multiprocessor machine, however, the computation time-saving escalates quite quickly. One processor may be performing the processing for the main application thread while a second processor is performing the computation tasks for the second thread of the application. Many times, developers let the main process thread continue to monitor the application message loop while the child threads monitor other hardware, perform time-expensive computations, read and write files to a disk, or draw objects onscreen.

In multithreading terminology are two types of child (nonprocess) threads: worker and user interface.

Worker threads are designed to perform a task and then terminate at the end of the task. The worker thread and the main application process thread can have two-way communications. They also can use concepts such as semaphores, mutexes, critical sections, events, and locks to perform thread synchronization.

Often, programs are written by using multithreaded techniques to increase throughput. To increase throughput, threads can be moved to separate CPUs, or, on a single-CPU system, one thread can use the CPU while a second thread is stalled, waiting for a read operation on the disk. Threads often need to communicate with each other to coordinate their activities. For instance, two threads processing all the rows in a database would need to communicate which rows they're processing so that each row gets processed only once.

An example of this type of communication is the semaphore, which allows one thread to lock a chunk of memory while it's changing the data. This way, other threads are prevented from reading that memory chunk until it's valid.

User-interface threads enter into their own message loop. In the Windows programming environment, the message loop is the part of the API that allows software to communicate with the operating system that's ready for the next piece of user input. These types of threads are very useful if the application needs to implement multiple top-level windows that are independent of each other.

Using COM

Over the past few years, component software has become a hot topic. With the rapid change in technology, the concept and terminology of component software has changed almost as rapidly as the development tools used to create them. Luckily, the development tools' implementation of COM has been slow enough not to get caught in the changing definitions.

Definition of COM

Chapter 4, "Using Microsoft's Object Technologies," discusses the Component Object Model in detail. As a quick review, COM is the specification of how application components should communicate and control each other in a Windows environment. COM is supported and facilitated by the operating system and is the basic concept in which OLE and ActiveX technology now reside.

The main focus of object-oriented and component-based design is the packaging of code behind a well-designed interface. If a component's user adheres to the interface specification, the component should behave predictably.

An analogy in real life of the importance of good interface design is the electrical outlet. You don't need to concern yourself with much beyond identifying that the electric outlet is a standard electrical outlet before plugging in your computer. The two sides to this commitment are that the electrician commits to wire the outlet only for common voltage and amps, and you commit to plug in only common electrical appliances. A 220-volt electrical outlet purposely has a different interface.


See "COM/DCOM Architecture Basics," Chapter 4

Interfaces

To be direct, an interface is a group of functions that supports one common concept for an object and can be referenced by a GUID (global unique ID). All interfaces start with a capital I. After an interface is designed and published, it can't be changed. If an interface needs to be changed, a new interface must be created and a new GUID assigned.

As a simple example, imagine that you have a bowling ball object and an interface into the bowling ball object called IBowlingBall. This interface can have functions like Aim(), SetSpeed(), SetRotation(), and Throw(). All the functions in the interface would control the bowling ball object.

Now imagine that you have a spaceship object. You might want to have an interface to control the navigation, another interface to control the weapons system, and a third interface to cook food in the galley. Each interface would control only part of your object, but some of the underlying functionality would probably overlap. Maybe your spaceship couldn't engage its warp drive if the shields were raised. Maybe your microwave and lasers are on the same circuit and would blow a fuse if you tried to fry an egg and tried to fry some egg-shaped aliens at the same time. The spaceship object handles these relationships; the interfaces just provide access to the spaceship object.

IUnknown  Already, a number of interfaces have been published. The most important interface already published is IUnknown, which every COM object must implement. The IUnknown interface contains two functions for reference counting (AddRef() and Release()), and one function that allows other applications to access your specific interface (QueryInterface()). Think of an office building as an object and each company in the building as an interface. The IUnknown interface is the guard in the lobby who counts the number of people entering the building (AddRef) and the number of people leaving the building (Release). He also knows the location of every company with an office in the building (QueryInterface). If you ask for a company (an interface) that doesn't exist in the building, he tells you that it's not in the building (not supported). On the other hand, if you asked for a company that did exist in the building, he would tell you its location.

Because every COM object implements the IUnknown interface, Visual C++ has created the C++ definition of the interface and included it with the software, in the Unknwn.H include file (see Listing 6.1).

Listing 6.1  IUnknown, the Standard Set of Interfaces From Which All COM Objects Derive

interface IUnknown
{
public: 
    BEGIN_INTERFACE
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(
        /* [in] */ REFIID riid,
        /* [iid_is][out] */ void __RPC_FAR *__RPC_FAR *ppvObject) = 0;
      
    virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;
      
    virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
      
    END_INTERFACE
};


NOTE: You can find the Unknwn.H file in your Visual C++ Include directory.

Other Interfaces  A number of other interfaces have been defined. Remember that as soon as they're defined, however, they must remain the same forever. If you need to make any change to the interface, you must create a new interface. On the other hand, you can change the underlying implementation and still keep the existing interface, as long as the interface itself doesn't change. If you look in the help file for Visual C++ (specifically, in Platform, SDK, and DDK Documentation\Platform SDK\Object Services\COM\Reference\Interfaces), you'll see a number of other interfaces defined in the Visual C++ header files. Many of these interfaces will become common to component designers, and others will remain as special-use cases.

One interface, IDispatch, doesn't show up in the help file's list of COM interfaces because it's a special implementation of a COM interface used with OLE Automation. That interface is covered in detail later when OLE Automation is discussed in the section "Using Automation."

Visual C++'s Implementation of COM

Because COM is the method for components to communicate, MFC has gone to great lengths to reduce the amount of typing required to get an interface off the ground. The MFC implementation centers around the use of a number of macros, abstract base classes, and inheritance. MFC takes the concept of message maps used for Windows event communication and extends it to implementing COM interfaces.

In MFC, you shouldn't think of an interface as more then a struct or class of virtual functions. Object-oriented designs often include a base class that's nothing but pure virtual functions. MFC provides a couple of macros to help clean up the definition of interfaces--the STDMETHOD_(method) and STDMETHOD_(type, method) macros found in the objbase.h header file:

#define STDMETHOD_(method)
  //virtual HRESULT STDMETHODCALLTYPE method
#define STDMETHOD_(type, method)
  //virtual type STDMETHODCALLTYPE method

With these macros, re-create a simple version of the IUnknown interface. Listing 6.2 shows the code that creates the new IUnknown interface.

Listing 6.2  How the STDMETHOD Set of Macros Are Used

struct IUnknown
{
    STDMETHOD_(ULONG, AddRef) () = 0;
    STDMETHOD_(ULONG, Release)() = 0;
    STDMETHOD(QueryInterface) (REFIID iid, void** ppvObject) = 0;
};

You might have noticed that all functions of the IUnknown interface are pure virtual functions because such functions are part of the definition of the interface. It's up to the implementation to determine how the functions actually work.

Now it's time to create your own interface. Because every interface must implement the IUnknown interface, your interfaces will be derived from IUnknown (refer to Listing 6.1). Listing 6.3 shows the interfaces for the spaceship example discussed earlier in the section "Interfaces."

Listing 6.3  Spaceship Object Interfaces

struct INavigation : public IUnknown
{
    STDMETHOD_(void, SetDirection) (int x, int y, int z) = 0;
    STDMETHOD_(void, SetSpeed)     (int speed) = 0;
    STDMETHOD_(void, EngageWarp)   () = 0;
};
struct IWeapons : public IUnknown
{
    STDMETHOD_(int,  GetShieldStrength) () = 0;
    STDMETHOD_(void, SetShieldStrength) () = 0;
    STDMETHOD_(void, FireLaser) (int x, int y, int z, int power) = 0;
};
struct IGalley : public IUnknown
{
    STDMETHOD_(void, SetMicrowavePower) (int power) = 0;
    STDMETHOD_(void, SetMicrowaveTime)  (int time) = 0;
    STDMETHOD_(void, StartMicrowave)    () = 0;
};

As you can see, all Listing 6.3 does is derive the interfaces from the IUnknown interface, which means that the interfaces include the pure virtual function definitions for AddRef(), Release(), and QueryInterface().


NOTE: Structure definitions are always derived from the IUnknown structure, which provides your classes with the default IUnknown interfaces: QueryInterface(), AddRef(), and Release().

All that's left to do is to create a GUID for each of the preceding three interfaces, implement a class that contains and uses these interfaces, and compile. These steps will be outlined more in the following examples.

Example 1: An InProc COM Server

This InProc COM server is a standard 52-playing-card server that will simulate having a deck of 52 cards (ace through king of all four suits). It has two interfaces:

The following sections outline how to create this application.

Running AppWizard to Generate the InProc Server  To generate the InProc server, follow these steps:

1. Choose File, New from the menu.

2. On the Projects page, select MFC AppWizard (dll) (see Figure 6.1).

FIG. 6.1
Choose from these project options for the MyCards InProc COM server.

3. In the Project Name text box, enter MyCards.

4. Set the proper directory in the Location combo box.

5. When all the options are set correctly, as in Figure 6.1, click OK. The MFC AppWizard - Step 1 of 1 dialog box appears (see Figure 6.2).

FIG. 6.2
The MFC AppWizard - Step 1 of 1 dialog box for the MyCards InProc COM server.

6. Make sure that the Regular DLL Using Shared MFC DLL option and the Automation checkbox are selected, and then click the Finish button.

The New dialog box in Figure 6.1 has several options. The first area controls how the MFC libraries are linked to your project. You can statically link the MFC libraries in their entirety to your DLL, but that adds nearly a megabyte to the size of executable. You can link the MFC libraries as another DLL. Or you can choose to make your DLL an extension to the MFC. (You would used this option when you're extending certain classes from MFC for use by other developers.)

The dialog box in Figure 6.2 includes a second set of options that allows you to include certain MFC extensions in your DLL. MFC supports OLE Automation and Windows sockets, but not all DLLs built will need to access OLE Automation and Windows sockets extensions to the core MFC functionality. Because we're talking mostly about COM objects here, however, you want the support for OLE Automation.

Creating the CardInterface.h Interface Header File  The CardInterface.h file contains all the information about the ICardDeck and ICheat interfaces. Any application (including the COM server) can use this file because it contains only the abstract definition of the interfaces. Follow these steps to create this file:

1. Create a new text file called CardInterface.h and add the code shown in Listing 6.4.

2. Save the file with the filename CardInterface.h.

Listing 6.4  The Interface Definition File for the ICardDeck and ICheat Interfaces

// CardInterface.h - File containing all the information about the ICardDeck and
//                   ICheat Interfaces.
#if !defined( __CARDINTERFACE_H__ )
#define __CARDINTERFACE_H__
// Constants for the ICardDeck Interface
const int SUIT_NONE    = 0;
const int SUIT_HEART   = 1;
const int SUIT_DIAMOND = 2;
const int SUIT_CLUB    = 3;
const int SUIT_SPADE   = 4;
const int CARD_NONE    = 0;
const int CARD_ACE     = 1;
const int CARD_TWO     = 2;
const int CARD_THREE   = 3;
const int CARD_FOUR    = 4;
const int CARD_FIVE    = 5;
const int CARD_SIX     = 6;
const int CARD_SEVEN   = 7; 
const int CARD_EIGHT   = 8;
const int CARD_NINE    = 9;
const int CARD_TEN     = 10;
const int CARD_JACK    = 11;
const int CARD_QUEEN   = 12;
const int CARD_KING    = 13;
// ICardDeck Interface
struct ICardDeck : public IUnknown
{
    STDMETHOD_(void, Shuffle) () = 0;
    STDMETHOD_(void, GetNextCard) (int* pnSuit, int* pnCard) = 0;
    STDMETHOD_(int,  GetUsedCount) () = 0;
    STDMETHOD_(BSTR, GetCardName) (int nSuit, int nCard) = 0;
};
// ICheat Interface
struct ICheat : public IUnknown
{
    STDMETHOD_(int,  GetCurrentPosition) () = 0;
    STDMETHOD_(void, SetCardAtPosition) (int nPos, int nSuit, int nCard) = 0;
    STDMETHOD_(void, LookAtPosition) (int nPos, int* pnSuit, int* pnCard) = 0;
};
#endif        // #if !defined( __CARDINTERFACE_H__ )

Creating New GUIDs for the Two Interfaces  Now you'll use the GUIDGen utility to generate GUIDs for the ICardDeck and ICheat interfaces. This generic utility is supplied with Microsoft Developer Studio. There are four types of GUIDs you can generate with GUIDGen (see Fig- ure 6.3):

FIG. 6.3
Use the GUIDGen utility to create GUIDs for the COM server.
Follow these steps:

1. Run the GUIDGen utility, which you can find in your Visual C++ Bin directory.

2. Select the static const struct GUID = {...} format and click the Copy button.

3. In the CardInterface.h file, insert the copied text just below the const for CARD_KING.

4. Change the text <<name>> to IID_ICardDeck.

5. Click the New GUID button and then repeat steps 2 through 4 for the IID_ICheat interface.

Listing 6.5 shows the text added to CardInterface.h with output from the GUIDGen utility.

Listing 6.5  Text Added to CardInterface.h

...
const int CARD_QUEEN   = 12;
const int CARD_KING    = 13;
// GUID for the ICardDeck Interface
// {35AB55A0-005B-11d1-87A1-444553540000}
static const GUID IID_ICardDeck =
{ 0x35ab55a0, 0x5b, 0x11d1, { 0x87, 0xa1, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };
// GUID for the ICheat Interface
// {35AB55A1-005B-11d1-87A1-444553540000}
static const GUID IID_ICheat =
{ 0x35ab55a1, 0x5b, 0x11d1, { 0x87, 0xa1, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };
// ICardDeck Interface
struct ICardDeck : public IUnknown
...

Creating the CCOMCards Class  To create the CCOMCards class, you need to start with a template generated by the ClassWizard, remove the OLE Automation code (as it's not used), and then include the common set of defines in the CardInterface.h file. To generate the basic COM component template, follow these steps:

1. Choose View, ClassWizard from the menu to start the ClassWizard.

2. Click the Add Class button and select New from the drop-down menu.

3. In the Class Information section, type CCOMCards in the Name text box and select the Base Class of CCmdTarget.

4. In the Automation section, select Creatable by Type ID and use the default type ID of MyCards.COMCards (see Figure 6.4).

FIG. 6.4
The C
reatable by Type ID option allows the Automation client to instate the object by name.

5. Click OK.

These steps create the class that will implement the ICardDeck and ICheat interfaces. The class is derived from CCmdTarget because it has some default implementation that will help you implement the IUnknown interface.

Although you selected that you wanted to use OLE Automation, you don't really want to use it. Choosing to create by type ID does three things:

Now remove the following OLE Automation code from COMCards.h:

// Generated OLE dispatch map functions
//{{AFX_DISPATCH(CCOMCards)
    // NOTE - the ClassWizard will add and remove member functions here.
//}}AFX_DISPATCH
DECLARE_DISPATCH_MAP()

Add the CardInterface common header to the file by using the boldface code shown in Listing 6.6.

Listing 6.6  The CardInterface Common Header

#if !defined(AFX_COMCARDS_H__C293A3D6_0059_11D1_87A1_444553540000__INCLUDED_)
#define AFX_COMCARDS_H__C293A3D6_0059_11D1_87A1_444553540000__INCLUDED_
#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000
// COMCards.h : header file
//
#include "CardInterface.h"
/////////////////////////////////////////////////////////////////////////////
// CCOMCards command target
class CCOMCards : public CCmdTarget
{
    DECLARE_DYNCREATE(CCOMCards)
    CCOMCards();           // protected constructor used by dynamic creation
// Attributes
public:
// Operations
public:
// Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CCOMCards)
    public:
    virtual void OnFinalRelease();
    //}}AFX_VIRTUAL
// Implementation
protected: 
    virtual ~CCOMCards();
    // Attributes
    int          m_nUsedCardCount;
    int          m_nCurrentPosition;
    // The card array where entry contains a number representing a card
    // (0-12 Hearts, 13-25 Diamonds, 26-38 Clubs, 39-51 Spade)
    int          m_aCardArray[52];
    // Generated message map functions
    //{{AFX_MSG(CCOMCards)
        // NOTE - the ClassWizard will add and remove member functions here.
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
    DECLARE_OLECREATE(CCOMCards)
    BEGIN_INTERFACE_PART(CardDeck, ICardDeck)
        STDMETHOD_(void, Shuffle) ();
        STDMETHOD_(void, GetNextCard) (int* pnSuit, int* pnCard);
        STDMETHOD_(int,  GetUsedCount) ();
        STDMETHOD_(BSTR, GetCardName) (int iSuit, int iCard);
    END_INTERFACE_PART(CardDeck)
    BEGIN_INTERFACE_PART(CardCheat, ICheat)
        STDMETHOD_(int,  GetCurrentPosition) ();
        STDMETHOD_(void, SetCardAtPosition) (int nPos, int nSuit, int nCard);
        STDMETHOD_(void, LookAtPosition) (int nPos, int* pnSuit, int* pnCard);
    END_INTERFACE_PART(CardCheat)
    DECLARE_INTERFACE_MAP()
};
/////////////////////////////////////////////////////////////////////////////
//{{AFX_INSERT_LOCATION}}
// Microsoft Developer Studio will insert additional declarations 
// immediately before the previous line.
#endif // !defined(AFX_COMCARDS_H__C293A3D6_0059_ 11D1_87A1_444553540000__INCLUDED_)

Take a quick look at some of the macros used in Listing 6.6: DECLARE_OLECREATE, BEGIN_INTERFACE_PART, STDMETHOD, STDMETHOD_, END_INTERFACE_PART, and DECLARE_INTERFACE_MAP. The first macro, DECLARE_OLECREATE, has the definition

#define DECLARE_OLECREATE(class_name) \
public: \
    static AFX_DATA COleObjectFactory factory; \
    static AFX_DATA const GUID guid; \

This macro, with the accompanying IMPLEMENT_OLECREATE macro used in the implementation file, allows this object to be dynamically created when the client component requests a new object.

The next four macros--BEGIN_INTERFACE_PART, STDMETHOD_, STDMETHOD, and END_INTERFACE_PART--all work together to integrate the interfaces into the class. Listing 6.7 shows their definitions.

Listing 6.7  Common Macros Used in COM Objects

#define BEGIN_INTERFACE_PART(localClass, baseClass) \
    class X##localClass : public baseClass \
    { \
    public: \
        STDMETHOD_(ULONG, AddRef)(); \
        STDMETHOD_(ULONG, Release)(); \
        STDMETHOD(QueryInterface)(REFIID iid, LPVOID* ppvObj); \
#define STDMETHOD(method)       virtual HRESULT STDMETHODCALLTYPE method
#define STDMETHOD_(type,method) virtual type STDMETHODCALLTYPE method
#define END_INTERFACE_PART(localClass) \
    } m_x##localClass; \
    friend class X##localClass; \

These macros create a nested class inside the CCOMCards class that includes the functions in the IUnknown interface. This nested class is then given access to all the protected and private members of the CCOMCards class through a friend statement. Listing 6.8 shows the complete ICardDeck interface.

Listing 6.8  The ICardDeck Interface

class XCardDeck : public ICardDeck
{
public:
    virtual ULONG   STDMETHODCALLTYPE AddRef();
    virtual ULONG   STDMETHODCALLTYPE Release();
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppvObj);
    virtual void STDMETHODCALLTYPE Shuffle();
    virtual void STDMETHODCALLTYPE GetNextCard(int* pnSuit, int* pnCard);
    virtual int  STDMETHODCALLTYPE GetUsedCount();
    virtual BSTR STDMETHODCALLTYPE GetCardName(int iSuit, int iCard);
} m_xCardDeck;
friend class XCardDeck;

The last macro is DECLARE_INTERFACE_MAP. This macro, along with the BEGIN_INTERFACE_MAP, INTERFACE_PART, and END_INTERFACE_MAP macros in the implementation file, allows the CCmdTarget derived class (CCOMCards) to access the proper interface's information when the QueryInterface function of the IUnknown interface is called.

The final step in creating the CCOMCards interface definition is to remove the unneeded OLE Automation code. Remove the code shown in Listing 6.9 from the CCOMCards.h file.

Listing 6.9  Lines to Remove from the CCOMCards.h File

BEGIN_DISPATCH_MAP(CCOMCards, CCmdTarget)
    //{{AFX_DISPATCH_MAP(CCOMCards)
        // NOTE - the ClassWizard will add and remove mapping macros here.
    //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()
// Note: we add support for IID_ICOMCards to support typesafe binding
//  from VBA. This IID must match the GUID that is attached to the
//  dispinterface in the .ODL file.
// {C293A3D4-0059-11D1-87A1-444553540000}
static const IID IID_ICOMCards =
{ 0xc293a3d4, 0x59, 0x11d1, { 0x87, 0xa1, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };
BEGIN_INTERFACE_MAP(CCOMCards, CCmdTarget)
    INTERFACE_PART(CCOMCards, IID_ICOMCards, Dispatch)
END_INTERFACE_MAP()

Adding the COM Implementation Code to the CCOMCards Class  Edit the COMCards.cpp file to add the boldfaced code in Listing 6.10. The first item to add is a call to shuffle the deck of cards when this object is first constructed. The next lines to add are the INTERFACE_MAP entries. The last set of code is the implementation of the object's interfaces.

Listing 6.10  COM Implementation Code

// COMCards.cpp : implementation file
//
#include "stdafx.h"
#include "MyCards.h"
#include "COMCards.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CCOMCards
IMPLEMENT_DYNCREATE(CCOMCards, CCmdTarget)
CCOMCards::CCOMCards()
{
    EnableAutomation();
    // To keep the application running as long as an OLE automation
    // object is active, the constructor calls AfxOleLockApp.
    AfxOleLockApp();
    // Shuffle the cards
    m_xCardDeck.Shuffle();
}
CCOMCards::~CCOMCards()
{
    // To terminate the application when all objects created with
    // with OLE automation, the destructor calls AfxOleUnlockApp.
    AfxOleUnlockApp();
}
void CCOMCards::OnFinalRelease()
{
    // When the last reference for an automation object is released
    // OnFinalRelease is called. The base class will automatically
    // delete the object. Add additional cleanup required for your
    // object before calling the base class.
    CCmdTarget::OnFinalRelease();
}
BEGIN_MESSAGE_MAP(CCOMCards, CCmdTarget)
    //{{AFX_MSG_MAP(CCOMCards)
        // NOTE - the ClassWizard will add and remove mapping macros here.
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()
BEGIN_INTERFACE_MAP(CCOMCards, CCmdTarget)
    INTERFACE_PART(CCOMCards, IID_ICardDeck, CardDeck)
    INTERFACE_PART(CCOMCards, IID_ICheat,    CardCheat)
END_INTERFACE_MAP()
// {C293A3D5-0059-11D1-87A1-444553540000}
IMPLEMENT_OLECREATE(CCOMCards, "MyCards.COMCards",
    0xc293a3d5, 0x59, 0x11d1, 0x87, 0xa1, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0)
/////////////////////////////////////////////////////////////////////////////
// CCOMCards message handlers
/////////////////////////////////////////////////////////////////////////////
// Interface functions for the ICardDeck Interface
STDMETHODIMP_(ULONG) CCOMCards::XCardDeck::AddRef()
{
    TRACE("CCOMCards::XCardDeck::AddRef()\n");
    METHOD_PROLOGUE(CCOMCards, CardDeck)
    return( pThis->ExternalAddRef() );
}
STDMETHODIMP_(ULONG) CCOMCards::XCardDeck::Release()
{
    TRACE("CCOMCards::XCardDeck::Release()\n");
    METHOD_PROLOGUE(CCOMCards, CardDeck)
    return( pThis->ExternalRelease() );
}
STDMETHODIMP CCOMCards::XCardDeck::QueryInterface(REFIID iid, LPVOID* ppvObj)
{
    TRACE("CCOMCards::XCardDeck::QueryInterface()\n");
    METHOD_PROLOGUE(CCOMCards, CardDeck)
    return( pThis->ExternalQueryInterface(&iid, ppvObj) );
}
STDMETHODIMP_(void) CCOMCards::XCardDeck::Shuffle()
{
    // Get the pThis pointer
    METHOD_PROLOGUE(CCOMCards, CardDeck)
    // Reset the count and position variable
    pThis->m_nUsedCardCount   = 0;
    pThis->m_nCurrentPosition = 0;
    // Prepare the bool structure to order the cards
    int  nCardsPicked = 0;
    bool bSelectedCards[52];
    for( int i = 0; i < 52; ++i )
    {
        bSelectedCards[i] = false;
    }
    // Reset the random number generator
    srand( (unsigned)time( NULL ) );
    // Select the 52 cards
    while( nCardsPicked < 52 )
    {
        // Get a card
        int nCard = int(rand() / double(RAND_MAX) * 52);
        // See if that card is used
        if ( !bSelectedCards[nCard] )
        {
            bSelectedCards[nCard] = true;
            pThis->m_aCardArray[nCardsPicked] = nCard;
            ++nCardsPicked;
        }
    }
}
STDMETHODIMP_(void) CCOMCards::XCardDeck::GetNextCard(int* pnSuit, int* pnCard)
{
    // Get the pThis pointer
    METHOD_PROLOGUE(CCOMCards, CardDeck)
    // See if we have cards left
    if ( pThis->m_nUsedCardCount >= 52 )
    {
        *pnSuit = SUIT_NONE;
        *pnCard = CARD_NONE;
    }
    else
    {
        *pnSuit = (pThis->m_aCardArray[pThis->m_nCurrentPosition] / 13 + 1);
        *pnCard = (pThis->m_aCardArray[pThis->m_nCurrentPosition] % 13 + 1);
        ++(pThis->m_nUsedCardCount);
        ++(pThis->m_nCurrentPosition);
    }
}
STDMETHODIMP_(int) CCOMCards::XCardDeck::GetUsedCount()
{
    METHOD_PROLOGUE(CCOMCards, CardDeck)
    return( pThis->m_nUsedCardCount );
}
STDMETHODIMP_(BSTR) CCOMCards::XCardDeck::GetCardName(int nSuit, int nCard)
{
    METHOD_PROLOGUE(CCOMCards, CardDeck)
    CString sCardName;
    // Get the card number
    switch( nCard )
    {
    case CARD_ACE:    sCardName = "Ace";     break;
    case CARD_TWO:    sCardName = "Two";     break;
    case CARD_THREE:  sCardName = "Three";   break;
    case CARD_FOUR:   sCardName = "Four";    break;
    case CARD_FIVE:   sCardName = "Five";    break;
    case CARD_SIX:    sCardName = "Six";     break;
    case CARD_SEVEN:  sCardName = "Seven";   break;
    case CARD_EIGHT:  sCardName = "Eight";   break;
    case CARD_NINE:   sCardName = "Nine";    break;
    case CARD_TEN:    sCardName = "Ten";     break;
    case CARD_JACK:   sCardName = "Jack";    break;
    case CARD_QUEEN:  sCardName = "Queen";   break;
    case CARD_KING:   sCardName = "King";    break;
    default:          sCardName = "Unknown";
    }
    sCardName += " of ";
    // Get the suit
    switch( nSuit )
    {
    case SUIT_DIAMOND: sCardName += "Diamonds";  break;
    case SUIT_HEART:   sCardName += "Hearts";    break;
    case SUIT_CLUB:    sCardName += "Clubs";     break;
    case SUIT_SPADE:   sCardName += "Spades";    break;
    default:           sCardName += "Unknown";
    }
    // Return a BSTR (the client will need to free the BSTR)
    return( sCardName.AllocSysString() );
}
/////////////////////////////////////////////////////////////////////////////
// Interface functions for the ICheat Interface
STDMETHODIMP_(ULONG) CCOMCards::XCardCheat::AddRef()
{
    TRACE("CCOMCards::XCardCheat::AddRef()\n");
    METHOD_PROLOGUE(CCOMCards, CardCheat)
    return( pThis->ExternalAddRef() );
}
STDMETHODIMP_(ULONG) CCOMCards::XCardCheat::Release()
{
    TRACE("CCOMCards::XCardCheat::Release()\n");
    METHOD_PROLOGUE(CCOMCards, CardCheat)
    return( pThis->ExternalRelease() );
}
STDMETHODIMP CCOMCards::XCardCheat::QueryInterface(REFIID iid, LPVOID* ppvObj)
{
    TRACE("CCOMCards::XCardCheat::QueryInterface()\n");
    METHOD_PROLOGUE(CCOMCards, CardCheat)
    return( pThis->ExternalQueryInterface(&iid, ppvObj) );
}
STDMETHODIMP_(int) CCOMCards::XCardCheat::GetCurrentPosition()
{
    METHOD_PROLOGUE(CCOMCards, CardCheat)
    return( pThis->m_nCurrentPosition );
}
STDMETHODIMP_(void) CCOMCards::XCardCheat::SetCardAtPosition(int nPos,
Âint nSuit, int nCard)
{
    METHOD_PROLOGUE(CCOMCards, CardCheat)
    // Check the validity of the position
    if ( nPos >= 0 && nPos < 52 )
    {
        // Compute the card number
        int nCardNumber = (nSuit - 1) * 13 + (nCard - 1);
        // Put the new card at the specified position
        pThis->m_aCardArray[nPos] = nCardNumber;
    }
}
STDMETHODIMP_(void) CCOMCards::XCardCheat::LookAtPosition(int nPos,
Âint* pnSuit, int* pnCard)
{
    METHOD_PROLOGUE(CCOMCards, CardCheat)
    if ( nPos < 0 || nPos >= 52 )
    {
        *pnSuit = SUIT_NONE;
        *pnCard = CARD_NONE;
    }
    else
    {
        *pnSuit = (pThis->m_aCardArray[nPos] / 13 + 1);
        *pnCard = (pThis->m_aCardArray[nPos] % 13 + 1);
    }
}

Almost every function in Listing 6.10 uses the METHOD_PROLOGUE macro and the pThis pointer. The METHOD_PROLOGUE macro actually creates the pThis pointer. The definition of METHOD_PROLOGUE is found in the AFXDISP.H header file in the MFC include directory:

#define METHOD_PROLOGUE(theClass, localClass) \
    theClass* pThis = \
        ((theClass*)((BYTE*)this - offsetof(theClass, m_x##localClass))); \
...

Because the interfaces are actually nested classes, the this pointer points to the interfaces themselves, not the CCOMCards object. The METHOD_PROLOGUE macro computes the offset of the specified object into the CCOMCards object and subtracts that from the this pointer provided. pThis is a new pointer into the CCOMCards object that allows you to access the member variables found in the CCOMCards class.

The code in Listing 6.10 also uses three CCmdTarget functions: ExternalAddRef(), ExternalRelease(), and ExternalQueryInterface(). CCmdTarget provides these functions so that you don't have to do all the reference counting and interface tracking every time you create a component. Just be sure to derive your component from CCmdTarget, and the functionality is provided for you.

Compiling and Registering the New InProc COM Server  Compile the project to produce MyCards.dll and run the command REGSVR32 MYCARDS.DLL to register the COM server. This command calls your DLL to determine the objects and their GUIDs. It then makes entries in the system's Registry so that other programs can use this object.

Example 2: A COM Client That Uses the InProc Server

This example is the client application that would use the preceding COM server and both its interfaces. This example won't need as many tricks and macros as the preceding example. The implementation is rather simple but must have a fair amount of error checking.

Follow these steps to create the CcardClient application:

1. Choose File, New from the menu.

2. Set the options in the New dialog box similar to those shown in Figure 6.5. Click OK to start the AppWizard.

FIG. 6.5
Start setting up your application in the New dialog box.

3. In the MFC AppWizard - Step 1 dialog box, change the application type to Single Document Type.

4. In the Step 2 dialog box, leave the database support set to none and accept the defaults for the rest of the AppWizard buttons by clicking the Finish button. Figure 6.6 shows the results of the CcardClient AppWizard settings.

FIG. 6.6
These settings are used for the CcardClient application sample.

5. Edit the stdafx.h file and add the boldfaced line shown in Listing 6.11.

Listing 6.11  Code to Be Added to the CcardClient Application's stdafx.h File

// stdafx.h : include file for standard system include files,
//  or project specific include files that are used frequently, but
//      are changed infrequently
//
#if !defined(AFX_STDAFX_H__C4D43FC7_006B_11D1_87A1_444553540000__INCLUDED_)
#define AFX_STDAFX_H__C4D43FC7_006B_11D1_87A1_444553540000__INCLUDED_
#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000
#define VC_EXTRALEAN          // Exclude rarely-used stuff from Windows headers
#include <afxwin.h>         // MFC core and standard components
#include <afxext.h>         // MFC extensions
#include <afxdisp.h>        // MFC OLE automation classes
#include <afxole.h>
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h>               // MFC support for Windows Common Controls
#endif // _AFX_NO_AFXCMN_SUPPORT
//{{AFX_INSERT_LOCATION}}
// Microsoft Developer Studio will insert additional declarations
// immediately before the previous line.
#endif // !defined(AFX_STDAFX_H__C4D43FC7_006B_11D1_87A1_444553540000__INCLUDED_)
6. Edit CcardClientApp::InitInstance to initialize COM (see the boldfaced line in List- ing 6.12).

Listing 6.12  The Code That Adds COM Initialization

BOOL CcardClientApp::InitInstance()
{
    AfxOleInit();
    AfxEnableControlContainer();
    // Standard initialization
    // If you are not using these features and wish to reduce the size
    //  of your final executable, you should remove from the following
    //  the specific initialization routines you do not need.
#ifdef _AFXDLL
    Enable3dControls();             // Call this when using MFC in a shared DLL
#else
    Enable3dControlsStatic();     // Call this when linking to MFC statically
#endif
...
7. Add a menu item to the application. Open the resource editor and double-click the IDR_MAINFRAME menu.

8. Add the PopUp menu item named Cards.

9. To the PopUp menu Cards item, add one menu item called Test. This menu should now look like Figure 6.7.

FIG. 6.7
The Test menu item allows you to test the Card COM object.

10. Start the ClassWizard by choosing View, ClassWizard from the menu.

11. In the Object IDs list select ID_CARDS_TEST, select COMMAND from the Messages list, and then click Add Function to create the command handler for this message. Figure 6.8 shows the ClassWizard as it's ready to create the command handler.

FIG. 6.8
The ClassWizard is ready to create a command handler in the view class for the ID_CARDS_TEST command.

12. Add the CardInterface.h header file from Listing 6.13.

Listing 6.13  All the Information About the ICardDeck and ICheat Interfaces

// CardInterface.h - File containing all the information about the ICardDeck and
//                   ICheat Interfaces.
#if !defined( __CARDINTERFACE_H__ )
#define __CARDINTERFACE_H__
// Constants for the ICardDeck Interface
const int SUIT_NONE    = 0;
const int SUIT_HEART   = 1;
const int SUIT_DIAMOND = 2;
const int SUIT_CLUB    = 3;
const int SUIT_SPADE   = 4;
const int CARD_NONE    = 0;
const int CARD_ACE     = 1;
const int CARD_TWO     = 2;
const int CARD_THREE   = 3;
const int CARD_FOUR    = 4;
const int CARD_FIVE    = 5;
const int CARD_SIX     = 6;
const int CARD_SEVEN   = 7;
const int CARD_EIGHT   = 8;
const int CARD_NINE    = 9;
const int CARD_TEN     = 10;
const int CARD_JACK    = 11;
const int CARD_QUEEN   = 12;
const int CARD_KING    = 13;
// GUID for the ICardDeck Interface
// {35AB55A0-005B-11d1-87A1-444553540000}
static const GUID IID_ICardDeck =
{ 0x35ab55a0, 0x5b, 0x11d1, { 0x87, 0xa1, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };
// GUID for the ICheat Interface
// {35AB55A1-005B-11d1-87A1-444553540000}
static const GUID IID_ICheat =
{ 0x35ab55a1, 0x5b, 0x11d1, { 0x87, 0xa1, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };
// ICardDeck Interface
struct ICardDeck : public IUnknown
{
    STDMETHOD_(void, Shuffle) () = 0;
    STDMETHOD_(void, GetNextCard) (int* pnSuit, int* pnCard) = 0;
    STDMETHOD_(int,  GetUsedCount) () = 0;
    STDMETHOD_(BSTR, GetCardName) (int nSuit, int nCard) = 0;
};
// ICheat Interface
struct ICheat : public IUnknown
{
    STDMETHOD_(int,  GetCurrentPosition) () = 0;
    STDMETHOD_(void, SetCardAtPosition) (int nPos, int nSuit, int nCard) = 0;
    STDMETHOD_(void, LookAtPosition) (int nPos, int* pnSuit, int* pnCard) = 0;
};
#endif        // #if !defined( __CARDINTERFACE_H__ )
13. Edit the CcardClientView.cpp file to add the boldfaced line shown in Listing 6.14.

Listing 6.14  Adding the CardInterface.h #include Statement

// CcardClientView.cpp : implementation of the CcardClientView class
//
#include "stdafx.h"
#include "CcardClient.h"
#include "CcardClientDoc.h"
#include "CcardClientView.h"
#include "CardInterface.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
...
14. Modify the CcardClientView::OnCardsTest() function to add the boldfaced code shown in Listing 6.15.

Listing 6.15  Completing the CcardClient Application

void CcardClientView::OnCardsTest()
{
    // Get the ClassID for the Card COM object
    CLSID clsid;
    HRESULT hResult;
    hResult = ::CLSIDFromProgID(L"MyCards.COMCards", &clsid);
    if ( hResult != NOERROR )
    {
        TRACE("Could not get the Class ID!\n");
        return;
    }
    // Get the class factory's pointer
    LPCLASSFACTORY pClassFactory;
    hResult = ::CoGetClassObject(clsid,
        CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void**)&pClassFactory);
    if ( hResult != NOERROR )
    {
        TRACE("Could not get the class factory pointer!\n");
        return;
    }
    // Create a CCOMCards object (on the server) and get its IUnknown pointer
    LPUNKNOWN pUnknown;
    if (pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnknown) !=
    ÂS_OK)
    {
        TRACE("Could not get the IUnknown pointer!\n");
        pClassFactory->Release();
        return;
    }
    // Get a pointer to the ICardDeck interface
    ICardDeck* pCardDeck;
    if ( pUnknown->QueryInterface(IID_ICardDeck, (void**)&pCardDeck) != S_OK)
    {
        TRACE("Could not get the ICardDeck pointer!\n");
        pUnknown->Release();
        pClassFactory->Release();
        return;
    }
    // Get a pointer to the ICheat interface (could also use pUnknown)
    ICheat* pCheat;
    if ( pCardDeck->QueryInterface(IID_ICheat, (void**)&pCheat) != S_OK)
    {
        TRACE("Could not get the ICheat pointer!\n");
        pCardDeck->Release();
        pUnknown->Release();
        pClassFactory->Release();
        return;
    }
    // Test everything out
    int nSuit;
    int nCard;
    while( pCardDeck->GetUsedCount() < 52 )
    {
        int nSuit;
        int nCard;
        pCardDeck->GetNextCard(&nSuit, &nCard);
        BSTR bCardName = pCardDeck->GetCardName(nSuit, nCard);
        TRACE("%s\n", LPCSTR(CString(bCardName)));
        ::SysFreeString(bCardName);
    }
    // Reshuffle
    pCardDeck->Shuffle();
    // Stack the deck for a two handed game of poker
    TRACE("\nTIME TO PLAY POKER\n");
    pCheat->SetCardAtPosition(1, SUIT_DIAMOND, CARD_ACE);
    pCheat->SetCardAtPosition(3, SUIT_DIAMOND, CARD_KING);
    pCheat->SetCardAtPosition(5, SUIT_DIAMOND, CARD_QUEEN);
    pCheat->SetCardAtPosition(7, SUIT_DIAMOND, CARD_JACK);
    pCheat->SetCardAtPosition(9, SUIT_DIAMOND, CARD_TEN);
    // Deal the poker hands
    TRACE("YOU                  ME\n");
    while( pCheat->GetCurrentPosition() < 10 )
    {
        // Get your card
        pCardDeck->GetNextCard(&nSuit, &nCard);
        BSTR bCardName = pCardDeck->GetCardName(nSuit, nCard);
        CString sYourCard(bCardName);
        ::SysFreeString(bCardName);
        // Get my card
        pCardDeck->GetNextCard(&nSuit, &nCard);
        bCardName = pCardDeck->GetCardName(nSuit, nCard);
        CString sMyCard(bCardName);
        ::SysFreeString(bCardName);
        // Display what we got
        TRACE("%-20.20s %-20.20s\n", LPCSTR(sYourCard), LPCSTR(sMyCard));
    }
    // Release everything (order does not matter)
    TRACE("\nReleaseing everything\n");
    pCheat->Release();
    pCardDeck->Release();
    pUnknown->Release();
    pClassFactory->Release();
    // Let the user know we are done
    AfxMessageBox("Done processing ... successfully!");
}

Here are a few notes about the code in Listing 6.15:

Finally, after the application is compiled and tested, the output shown in Figure 6.9 appears.

Using Automation

After seeing the implementation of interfaces that use COM in the preceding examples, you may be looking for something a little simpler. That simplicity comes by the way of Automation. Automation is one of the easiest types of COM to implement, especially because Visual C++ does most of the work for you.

Automation supports the concept of late binding. Normally, DLLs and COM objects are first linked into your application before your application ever runs. This is an early binding mechanism. In late binding, you can query an object for the types of methods that it supports and what those methods may take as arguments. This way, a client can work with servers that it may not fully know about.

FIG. 6.9
The final output of the CcardClient application.

Definition of Automation

Automation is the answer to the late-binding issue. The key to Automation is the IDispatch interface, which has four functions: GetTypeInfo, GetTypeInfoCount, GetIDsOfNames, and Invoke. The most import function, Invoke, performs late binding. When a client application wants to call a function on the server, it provides information to Invoke to tell the server which function to execute and what parameters to use when it executes. Listing 6.16 shows the IDispatch interface definition. Although the definition looks complicated, just remember that Visual C++ hides quite a bit of this complexity when it helps you implement Automation.

Listing 6.16  The IDispatch Portion of the OAIDL.H File

interface IDispatch : public IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
            /* [out] */ UINT __RPC_FAR *pctinfo) = 0;
        virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
            /* [in] */ UINT iTInfo,
            /* [in] */ LCID lcid,
            /* [out] */ ITypeInfo __RPC_FAR *__RPC_FAR *ppTInfo) = 0;
        virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
            /* [in] */ REFIID riid,
            /* [size_is][in] */ LPOLESTR __RPC_FAR *rgszNames,
            /* [in] */ UINT cNames,
            /* [in] */ LCID lcid,
            /* [size_is][out] */ DISPID __RPC_FAR *rgDispId) = 0;
        virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke(
            /* [in] */ DISPID dispIdMember,
            /* [in] */ REFIID riid,
            /* [in] */ LCID lcid,
            /* [in] */ WORD wFlags,
            /* [out][in] */ DISPPARAMS __RPC_FAR *pDispParams,
            /* [out] */ VARIANT __RPC_FAR *pVarResult,
            /* [out] */ EXCEPINFO __RPC_FAR *pExcepInfo,
            /* [out] */ UINT __RPC_FAR *puArgErr) = 0;
    };

The first two functions, GetTypeInfoCount and GetTypeInfo, combine to form one method of informing the client applications of the functions supported by each specific implementation of the IDispatch interface. Because Visual C++ and MFC support the other method--building an Object Description Language (ODL) file and compiling it into a Type Library File (TLB)--this chapter doesn't look in depth at these two functions.

The third function, GetIDsOfNames, translates a number of function names to the IDs that must be used when the functions are called through Invoke. To save time, you can retrieve multiple function IDs in one call to GetIDsOfNames.

After the information to translate a function name to an ID is retrieved, you can use this information in the call to Invoke. The other information supplied has to do with the parameters to be passed to the function, the return value from the function, and exception-handling information. We won't go into any more detail about the Invoke parameters because all this information is nicely encapsulated in MFC.

MFC's Implementation of Automation

When implementing an Automation server or client in Visual C++ and MFC, the system takes care of almost everything except calling and implementing the functions for you. It uses a number of macros to hide the implementation details, but you need to look at how to implement the macros. Because MFC's implementation of Automation is so simple, look at the implementation in the following example.

Example 3: An OutOfProc Automation Server

The following sample program shows how to implement an OutOfProc (DLL) Automation server, taking full advantage of Visual C++ and MFC's encapsulation of Automation. This server creates an Automation class that performs basic math functions.

Creating the InProc Automation Server  Use the AppWizard to create the new Visual C++ project:

1. Choose File, New from the menu.

2. On the Projects page, select MFC AppWizard (DLL).

3. In the Project Name text box, enter AutoMath.

4. Set the proper directory in the Location edit box. When all the options are set correctly, click OK.

5. The MFC AppWizard - Step 1 of 1 dialog box appears. Make sure that the Regular DLL Using Shared MFC DLL option and Automation checkbox are selected, and then click the Finish button.

Creating the CMath Class  By using the ClassWizard, follow these steps:

1. Click the Add Class button and select New.

2. In the Class Information section, type CMath in the Name text box and select the Base Class of CCmdTarget.

3. In the Automation section, select Automation and then click OK.

Selecting the Automation option means that the ClassWizard will do most of the work for you. It will insert a number of macros in the header and implementation file that sets up for the insertion of Automation functions.

Because Automation uses the IDispatch interface, it will have to implement the IUnknown interface just like all other interfaces. For this reason and some IDispatch function implementation reasons, this new class is again derived from CCmdTarget.

Adding the Automation Functions to the CMath Class  Now add the interface definitions that cover the automation portion of the CMath class. Follow these steps:

1. Select the CMath class.

2. On the Automation page, click the Add Method button and type Add in the External Name drop-down combo box.

3. Select long as the Return Type.

4. Add two parameters, 11 and 12, with the type of long. Your Add Method dialog box should look the one in Figure 6.10.

FIG. 6.10
Use the ClassWizard to add the Add() Automation function to the CMath class in the InProc Automation server.

5. Click OK.

6. Repeat steps 2-5 to create the Subtract() function, which takes two longs as parameters and returns a long.

7. Click the Add Property button and type AddCorrect as the External Name.

8. Select a Type of BOOL.

9. In the Implementation section, click Get/Set Methods and add a parameter named bAddCorrect as a Boolean. Figure 6.11 shows what the dialog box should look like.

FIG. 6.11
Use the ClassWizard to add the AddCorrect property to the CMath class in the InProc Automation server.

10. Click OK. This property determines whether the Add method returns the correct value.

11. Again, click the Add Property button. Type SubtractCorrect as the External Name.

12. Select a Type of BOOL.

13. Change Variable Name to m_bSubtractCorrect so that your dialog box looks like the one in Figure 6.12.

FIG. 12
Use the ClassWizard to add the SubtractCorrect property to the CMath class in the InProc Automation server.

14. Click OK. This property will determine whether the Subtract method returns the correct value.

15. Click OK in the ClassWizard dialog box when finished adding Automation methods and properties.

The ClassWizard adds some declarations to the header file and the code shown in Listing 6.17 to the implementation file for the CMath class.

Listing 6.17  Code for the New CMath Class

BEGIN_DISPATCH_MAP(CMath, CCmdTarget)
    //{{AFX_DISPATCH_MAP(CMath)
    DISP_PROPERTY_NOTIFY(CMath, "SubtractCorrect",
        m_bSubtractCorrect, OnSubtractCorrectChanged, VT_BOOL)
    DISP_PROPERTY_EX(CMath, "AddCorrect", GetAddCorrect, SetAddCorrect, VT_BOOL)
    DISP_FUNCTION(CMath, "Add", Add, VT_I4, VTS_I4 VTS_I4)
    DISP_FUNCTION(CMath, "Subtract", Subtract, VT_I4, VTS_I4 VTS_I4)
    //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()

This dispatch map is how MFC keeps track of which function should be called when the Automation server's Invoke function is called.

Also, the ClassWizard has been updating a file called AutoMath.ODL (Object Description Language), which contains the external information needed to be able to interface with this Automation server. When you compile, the ODL file is also compiled and a Type Library file (.tlb) is generated. The .tlb file helps an Automation client properly use this Automation server.

Adding the m_bAddCorrect Member Variable  You need to add a Boolean variable that verifies that your class is adding correctly. Follow these steps:

1. Add a Boolean member variable called m_bAddCorrect to the protected section of the CMath header file.

2. Add the boldfaced code in Listing 6.18 to the constructor of the CMath class in the implementation file.

Listing 6.18  Modifications for Adding the m_AddCorrect Variable

CMath::CMath()
{
    EnableAutomation();
    // To keep the application running as long as an OLE automation
    //     object is active, the constructor calls AfxOleLockApp.
    AfxOleLockApp();
    m_bAddCorrect      = TRUE;
    m_bSubtractCorrect = TRUE;
}

Adding the Implementation Code for the CMath Object  Modify the code to the CMath implementation file so that it resembles the code in Listing 6.19.

Listing 6.19  Implementation of the CMath Class Functions

...
long CMath::Add(long l1, long l2)
{
    if ( m_bAddCorrect )    return(l1 + l2);
    else                    return(l1 * l2);
}
long CMath::Subtract(long l1, long l2)
{
    if ( m_bSubtractCorrect )   return(l1 - l2);
    else                        return(l1 / l2);
}
BOOL CMath::GetAddCorrect()
{
    return m_bAddCorrect;
}
void CMath::SetAddCorrect(BOOL bNewValue)
{
    m_bAddCorrect = bNewValue;
}
void CMath::OnSubtractCorrectChanged()
{
    // We won't do anything here, but we could detect the
    // value changing by placing code in this function.
}

Compiling and Registering the New InProc Automation Server  Compile the project to produce AutoMath.dll and run the command REGSVR32 AUTOMATH.DLL to register the Automation server.

Example 4: An Automation Client That Uses InProc and OutOfProc Servers

Now that you've developed an Automation server, all you need is an Automation client that uses it. Building an Automation client is much easier than building the Automation server (which really wasn't hard at all). Again, MFC and its macros hide all the complicated COM stuff.

In the new version of Visual C++, smart pointers and the #import preprocessor statement have come on the scene for Automation. In this sample Automation client, you will be using both features.

Smart pointers wrap the pointer to an OLE or COM object. Smart pointers use some advance C++ programming techniques to automatically call the AddRef() and Release functions when using an interface. This keeps you from explicitly calling these functions yourself.

The import preprocessor statement saves a step when you're developing a client that uses a COM server. Before having the #import preprocessor statement, you would have to obtain an Interface Definition Language (IDL) or an Object Definition Language (ODL) file to include in your project. These files would define the interfaces for your C++ code. The #import directive allows you to include the Type Library (TLB) file directly. There used to be a tool to create the IDL or ODL file from the TLB; now you just include the TLB directly.

Creating the Automation Client Application  By using the AppWizard, follow these steps:

1. Create an MFC SDI application, using the settings shown in Figure 6.13.

FIG. 6.13
In the AppWizard, use these settings to create the MathUser Automation client application.

2. In the Step 3 of 6 dialog box, select Automation support. Use the default settings for the rest of the selections. Figure 6.14 shows the proper settings for Step 3.

FIG. 6.14
Your settings for the Step 3 of 6 dialog box should match these.

Editing the StdAfx.h File  Edit the StdAfx.h file to add the #import directive by adding the boldfaced lines shown in Listing 6.20.

Listing 6.20  Code to Be Added to the StdAfx.h File in This Project

...
#define VC_EXTRALEAN          // Exclude rarely-used stuff from Windows headers
#include <afxwin.h>         // MFC core and standard components
#include <afxext.h>         // MFC extensions
#include <afxdisp.h>        // MFC OLE automation classes
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h>               // MFC support for Windows Common Controls
#endif // _AFX_NO_AFXCMN_SUPPORT
#import "../AutoMath/Debug/AutoMath.tlb"
using namespace AutoMath;
...

The #import directive, when compiled, creates two files in the Debug directory: AutoMath.tlh and AutoMath.tli. The .tlh (Type Library Header) and .tli (Type Library Implementation) files implement an interface to the Automation server. The advantage of using the #import directive instead of the old method of creating a class based on a type library is that if the type library file changes, the #import directive rebuilds the files on the next compile. Under the old way, the class based on the type library file had to be regenerated.

If you look at the contents of the newly created files, notice some strange-looking code. What this code is doing is creating a smart pointer to the Automation server. It's also implementing all the functions of the Automation server as inline calls to Invoke (through the _com_dispatch_method function implemented by Visual C++).

You may have also noticed that both properties created in the Automation server are implemented both ways in the smart pointer. You'll exploit that advantage when you implement your testing function.

Adding a Menu Item to Allow Testing  With the resource editor, edit the IDR_MAINFRAME menu. Add a POPUP menu item named Math and add one menu item called Test. The menu should look like Figure 6.15.

FIG. 6.15
Use a menu item for testing the AutoMath Automation object.

Adding a Command Handler for the ID_MATH_TEST Command ID  By using the ClassWizard, create a command handler for the ID_MATH_TEST command in the CMathUserView class.

Adding Test Code to the Command-Handler Function  Modify the CMathUserView::OnMathTest() function and add the code shown in Listing 6.21. This test function uses both methods of changing the two Boolean properties: the Put... functions and the direct set method.

Listing 6.21  The New CmathUserView::OnMathTest Implementation

void CMathUserView::OnMathTest()
{
    // Create the "smart pointer" to the math object
     IMathPtr pMath;
    if ( pMath.CreateInstance(__uuidof(Math)) != S_OK )
    {
        AfxMessageBox("Could Not Create Instance!");
        return;
    }
    // Test the math functions
    long lResult;
    lResult = pMath->Add(25, 5);
    TRACE("Add(25, 5) = %ld\n", lResult);
    lResult = pMath->Subtract(25, 5);
    TRACE("Subtract(25, 5) = %ld\n", lResult);
    // Start messing up
    TRACE("Changing the correct flags\n");
    pMath->AddCorrect = FALSE;
    lResult = pMath->Add(25, 5);
    TRACE("Add(25, 5) = %ld\n", lResult);
    pMath->SubtractCorrect = FALSE;
    lResult = pMath->Subtract(25, 5);
    TRACE("Subtract(25, 5) = %ld\n", lResult);
    // Start doing it right again
    TRACE("Doing it right again\n");
    pMath->PutAddCorrect(TRUE);
    lResult = pMath->Add(25, 5);
    TRACE("Add(25, 5) = %ld\n", lResult);
    pMath->PutSubtractCorrect(TRUE);
    lResult = pMath->Subtract(25, 5);
    TRACE("Subtract(25, 5) = %ld\n", lResult);
    // Display done message
    AfxMessageBox("Test Complete. Check the output window.");
}

Implementing Components with ATL

ATL is a template-based library designed to provide developers with the tools to quickly build COM objects, Automation servers, and ActiveX controls. Rather than derive from a class to extend the functionality of the class, you create a new class from the template. Typically, when creating this new class, you pass in a class type that the template uses to construct a new type. The template is then seen as an extension of your class.

What ATL Provides to COM Developers

ATL provides standard implementations for IUnknown, IClassFactory, IClassFactory2, and IDispatch to aid in the creation of COM objects. In addition to these classes, ATL includes support for a window class, which is used to create ActiveX objects.

One area where MFC and ATL vary is that MFC doesn't use multiple inheritance to implement COM interfaces. ATL uses a lot of multiple inheritance, due to the template-based architecture of the library. This means that ATL must do its reference counting at the class level. When you're using multiple inheritance, which base class should do the reference counting isn't clear. MFC does its reference counting at the interface level. Per-interface reference counting is more flexible than per-object.

Reference counting allows the same object to be instantiated and used by multiple clients. The first client calls AddRef() to access the interfaces and calls Release() when it's done. When the reference count of the object goes down to zero, the object can be removed from memory. There are two schemes for implementing reference counting:

Per-interface reference counting is good because each interface is tracked in memory by itself. However, per-interface reference counting is expensive because the client must call QueryInterface, AddRef, and Release for each function to be called. Per-object reference counts allow a group of interfaces to be made available with one QueryInterface, AddRef, and Release set of calls.

MFC implements only the per-interface reference counting. ATL, by default, implements the per-object reference counting. For ATL to support lighter weight interfaces, its implementors have chosen to provide tear-off interfaces, which allow ATL interfaces to be reference counted at the interface level.

Like MFC, ATL implements COM interfaces by inheriting from classes that contain pure virtual functions. Unlike MFC, ATL does this by inheriting from template classes. The code snippet in Listing 6.22 shows how the ATLCard class is constructed.

Listing 6.22  An Example of an ATL-Derived COM Interface Definition

...
 class ATL_NO_VTABLE CATLCard :
     public CComObjectRootEx<CComSingleThreadModel>,
     public CComCoClass<CATLCard, &CLSID_ATLCard>,
     public IDispatchImpl<IATLCard, &IID_IATLCard, &LIBID_ATLCSRVLib>,
     public IDispatchImpl<IATLCheat, &IID_IATLCheat, &LIBID_ATLCSRVLib>
{
...

Here, the CATLCard class inherits the AddRef() and Release() pure virtual functions from the base class of CComObjectRootEx<>, IUnknown. ATL actually implements AddRef in CComObjectRootEx. The IDispatchImpl template classes define the functions necessary for the interfaces, including the Invoke and GetTypeInfo interface definitions.

Example 5: An InProc COM Server with ATL

This example will implement the in-process server for the standard 52-playing-card deck (the same as Example 1 implemented earlier). Implementing this sample in ATL will help you see the differences between component development in MFC and ATL.

Constructing an InProc Server  Start the project by using the ATL COM AppWizard to construct an in-process server:

1. Choose File, New from the menu.

2. On the Projects page, select ATL COM AppWizard.

3. In the Project Name text box, enter ATLCSrv.

4. Set the proper directory in the Location text box. When all the options are set correctly (see Figure 6.16), click OK.

FIG. 6.16
These project options for the ATLCSrv InProc COM Server reflect how your dialog box should look.

5. The ATL COM AppWizard - Step 1 of 1 dialog box appears (see Figure 6.17). Set the Server Type to Dynamic Link Library (DLL). Leave Allow Merging of Proxy/Stub Code and Support MFC deselected.

FIG. 6.17
Use these settings for the ATL COM AppWizard - Step 1 of 1 Options dialog box for the ATLCSrv InProc COM server.

6. Click OK when the New Project Information dialog box appears.

Now that you've constructed the project, you can begin creating the component.

Creating the ATL COM Component  To create the ATL COM component, you use the ATL Object wizard, which allows you to select the type of object you want to create. For instance, if you're creating an object that will be used in Internet Explorer, select the Internet Explorer option to create an object template that includes the calls Internet Explorer will call. The Simple Object is the most basic set of COM interfaces. The Add-In object creates an ATL object that extends the Windows 95 user shell, a shell extension object. The Active Server Component option creates an object for use in Active Server Pages. The MS Transaction Server option creates a Transaction Server-ready object. The Component Registrar object provides just the mechanisms for placing values into the Registry. Follow these steps to create the ATL COM component:

1. Choose Insert, New ATL Object from the menu to open the ATL Object Wizard (see Figure 6.18).

FIG. 6.18
Use this dialog box to create ATL objects and controls.

2. Select Objects in the left pane and Simple Object in the right pane.

3. Click Next to open the ATL Object Wizard Properties dialog box (see Figure 6.19).

FIG. 6.19
With this dialog box, you specify the properties for your new ATL object.

4. Enter the class name ATLCards in the Short Name text box and then let the wizard generate the other fields in the dialog box.

5. Select the Attributes tab (see Figure 6.20).

FIG. 6.20
The ATL Object Wizard Properties dialog box's Attributes page sets more technical aspects of the object.

For the simple object, the Attributes page includes three major sections: Threading Model, Interface, and Aggregation. The Threading Model settings are as follows:

6. Leave the default settings for this example and click OK to create the class.

These steps create the class CATLCards and the first interface IATLCards. You'll put the standard card-handling functions into the IATLCards interface and then create a new interface to create the Cheat functions implemented earlier.

Creating the ICheat Interface  You need to add the new interface to the existing CATLCards class. Unfortunately, the ATL class wizards don't provide support to add an interface to the existing class. This limitation does give you an opportunity to investigate what the wizards are doing by going ahead and implementing the interface by hand. Follow these steps:

1. Add the interface definition to the IDL file, shown in Listing 6.23. (Note that the uuid is generated by using the GUIDGen utility in the bin directory of Visual C++.)

Listing 6.23  Adding the ICheat Interface to the CATLCards.idl File

...
     [
          object,
          uuid(685BB54E-16EA-11D1-8375-000000000000),
          dual,
          helpstring("IATLCard Interface"),
          pointer_default(unique)
     ]
     interface IATLCard : IDispatch
     {
     };
     [
          object,
          uuid(54207220-16F1-11d1-8375-000000000000),
          dual,
          helpstring("IATLCheat Interface"),
          pointer_default(unique)
     ]
     interface IATLCheat : IDispatch
     {
          [id(1), helpstring("method GetCurrentPosition")] HRESULT GetCurrentPosition
([out]int *iCurrentPos);
          [id(2), helpstring("method SetCardAtPosition")] HRESULT SetCardAtPosition
([in] int nPos, [in] int nSuit, [in] int nCard);
          [id(3), helpstring("method LookAtPosition")] HRESULT LookAtPosition
([in] int iPos, [out] int * pnSuit, [out] int * pnCard);
     };
[
     uuid(685BB541-16EA-11D1-8375-000000000000),
     version(1.0),
     helpstring("atlcsrv 1.0 Type Library")
]
...
2. Notice in Listing 6.23 that the methods in the IATLCard interface haven't been defined yet. This listing goes ahead and defines the methods that define the IATLCheat interface.

3. Add the code in Listing 6.24, which derives the implementation object from the idl definition.

Listing 6.24  Code to Derive Implementation Object

...
class ATL_NO_VTABLE CATLCard :
     public CComObjectRootEx<CComSingleThreadModel>,
     public CComCoClass<CATLCard, &CLSID_ATLCard>,
     public IDispatchImpl<IATLCard, &IID_IATLCard, &LIBID_ATLCSRVLib>,
     public IDispatchImpl<IATLCheat, &IID_IATLCheat, &LIBID_ATLCSRVLib>
{
public:
     CATLCard()
     {
     }
DECLARE_REGISTRY_RESOURCEID(IDR_ATLCARD)
BEGIN_COM_MAP(CATLCard)
     COM_INTERFACE_ENTRY(IATLCard)
     COM_INTERFACE_ENTRY(IATLCheat)
     COM_INTERFACE_ENTRY2(IDispatch, IATLCheat)
END_COM_MAP()
// IATLCard
public:
     STDMETHOD(LookAtPosition)(/*[in]*/ int iPos, /*[out]*/ int * pnSuit, /*[out]*/ int * pnCard);
     STDMETHOD(SetCardAtPosition)(/*[in]*/ int nPos, /*[in]*/ int nSuit, /*[in]*/ int nCard);
...
You've made two changes to the ATLCard.h file. The addition of the IdispatchImpl template instantiation in the inheritance of CATLCard implements the interface. Adding the two entries to the COM_MAP allows the new interface to be referenced by the QueryInterface functions.

4. Implement the functions in the ATLCard.cpp file. These changes are straightforward and identical to the implementations earlier in Example 1.

Adding the Methods to the IATLCards Interface  Now that the cheat interface is complete, you can go back to using the ATL COM wizards to implement the methods that make up the IATLCards interface. This section covers creating one of the interfaces; the others are created in the exact same fashion. Follow these steps:

1. Add the GetCardName method by right-clicking the IATLCard interface in the Class View window. Select Add Method from the pull-down menu to open the Add Method to Interface dialog box (see Figure 6.21).

FIG. 6.21
Use the Add Method to Interface dialog box to define methods on an interface.

2. Notice the proper use of the [in] and [out] variable declarations in Listing 6.24 of the preceding section. These let COM properly define the inbound and outbound variables for the method. Entering these correctly in the Add Method to Interface dialog box will propagate these to the three places that need this information and save you time.

3. Implement GetCardName. This implementation is essentially the same as the version in Example 1, with one main difference: Because you're using the ATL to implement the COM object, you want to avoid using MFC altogether. The code in Example 1 makes effective use of the CString object to implement the string handling. You can use the string class provided in the Standard Template Library (STL) to get the same effective string handling.

Listing 6.25 shows the changes made to the CATLCard.cpp file.

Listing 6.25  Changes to the CATLCard.cpp File

...
STDMETHODIMP CATLCard::GetCardName(int iSuit, int iCard, BSTR * bsCardName)
{
    // Get the card number
     std::string sCardName;  // temporary string put our local result in.
    switch( iCard )
    {
          case CARD_ACE:    sCardName = "Ace";     break;
          case CARD_TWO:    sCardName = "Two";     break;
          case CARD_THREE:  sCardName = "Three";   break;
          case CARD_FOUR:   sCardName = "Four";    break;
          case CARD_FIVE:   sCardName = "Five";    break;
          case CARD_SIX:    sCardName = "Six";     break;
          case CARD_SEVEN:  sCardName = "Seven";   break;
          case CARD_EIGHT:  sCardName = "Eight";   break;
          case CARD_NINE:   sCardName = "Nine";    break;
          case CARD_TEN:    sCardName = "Ten";     break;
          case CARD_JACK:   sCardName = "Jack";    break;
          case CARD_QUEEN:  sCardName = "Queen";   break;
          case CARD_KING:   sCardName = "King";    break;
          default:          return S_FALSE;
    }
     sCardName += " of ";
     switch( iCard )
     {
         case SUIT_DIAMOND: sCardName += "Diamonds";  break;
          case SUIT_HEART:   sCardName += "Hearts";    break;
          case SUIT_CLUB:    sCardName += "Clubs";     break;
          case SUIT_SPADE:   sCardName += "Spades";    break;
          default:           return S_FALSE;
     }
     // Copy our result to a BSTR string.
     CComBSTR cbstrCardStr( sCardName.data() );
     *bsCardName = cbstrCardStr;
     return S_OK;
}
...

Listing 6.25 shows the BSTR() conversion operation being called, which automatically does SysAllocString and returns the proper BSTR pointer that you need to return from the function.

The ATL component is designed to have the same functionality as the original MFC component. This way, you can test the ATL component by using the CcardClient application from Example 2. Because the component behaves the same as the MFC component, the output of CcardClient will be identical.

From Here...

In this chapter, you learned how Visual C++ can be a very powerful tool in component development. In particular, you explored many of the issues in creating components by using Visual C++, MFC, and ActiveX.


Previous chapterNext chapterContents


© Copyright, Macmillan Computer Publishing. All rights reserved.