
by Brad Rhodes and Dave Kincade
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
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
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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).
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."
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.
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."
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.
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:
FIG. 6.1
Choose from these project options for the MyCards InProc COM server.
FIG. 6.2
The MFC AppWizard - Step 1 of 1 dialog box for the MyCards InProc COM server.
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:
// 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:
Listing 6.5 shows the text added to CardInterface.h with output from the GUIDGen utility.
...
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:
FIG. 6.4
The Creatable by Type ID option allows the Automation client
to instate the object by name.
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.
#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.
#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.
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.
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.
// 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.
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:
FIG. 6.5
Start setting up your application in the New dialog box.
FIG. 6.6
These settings are used for the CcardClient application sample.
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_)
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 ...
FIG. 6.7
The Test menu item allows you to test the Card COM object.
FIG. 6.8
The ClassWizard is ready to create a command handler in the view class for the ID_CARDS_TEST
command.
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__ )
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 ...
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.
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.
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.
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.
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.
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:
Creating the CMath Class By using the ClassWizard, follow these steps:
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:
FIG. 6.10
Use the ClassWizard to add the Add() Automation function to the CMath
class in the InProc Automation server.
FIG. 6.11
Use the ClassWizard to add the AddCorrect property to the CMath
class in the InProc Automation server.
FIG. 12
Use the ClassWizard to add the SubtractCorrect property to the
CMath class in the InProc Automation server.
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.
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:
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.
...
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.
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:
FIG. 6.13
In the AppWizard, use these settings to create the MathUser Automation client
application.
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.
... #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.
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.");
}
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.
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.
...
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.
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:
FIG. 6.16
These project options for the ATLCSrv InProc COM Server reflect how your dialog box
should look.
FIG. 6.17
Use these settings for the ATL COM AppWizard - Step 1 of 1 Options dialog box for
the ATLCSrv InProc COM server.
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:
FIG. 6.18
Use this dialog box to create ATL objects and controls.
FIG. 6.19
With this dialog box, you specify the properties for your new ATL object.
FIG. 6.20
The ATL Object Wizard Properties dialog box's Attributes page sets more technical
aspects of the object.
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:
...
[
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")
]
...
...
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);
...
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:
FIG. 6.21
Use the Add Method to Interface dialog box to define methods on an interface.
Listing 6.25 shows the changes made 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.
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.
© Copyright, Macmillan Computer Publishing. All rights reserved.