MFC and COM

[Previous] [Next]

The primary reason why MFC makes COM, OLE, and ActiveX programming simpler is that it provides canned implementations of common COM interfaces in classes such as COleControl and COleControlSite. COM has been described as an "empty API," which means that Microsoft defines the interfaces and the methods and tells you what the methods are supposed to do but leaves it up to you, the object implementor, to write the code. The good news is that as long as you're writingActiveX controls, Automation servers, or other types of components that MFC explicitly supports, MFC implements the necessary COM interfaces for you.

In the next three chapters, you'll get acquainted with many of the MFC classes that implement COM interfaces. Right now, I want you to understand how MFC classes implement COM interfaces. To do that, you must understand the two techniques that COM programmers use to write C++ classes representing COM objects. The first is multiple inheritance. The second is nested classes. MFC uses only nested classes, but let's look at both techniques so that we can compare the relative merits of each.

Multiple Inheritance

C++ programmers define COM interfaces using the following syntax:

 interface IUnknown {     virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv) = 0;     virtual ULONG __stdcall AddRef () = 0;     virtual ULONG __stdcall Release () = 0; }; 

The keyword interface is an alias for struct. Therefore, to the C++ programmer, an interface definition is a set of pure virtual functions logically bound together as members of a common structure. And because structures and classes are treated almost identically in C++, it's perfectly legal to derive one interface from another, like this.

 interface IMath : public IUnknown {     virtual HRESULT __stdcall Add (int a, int b, int* pResult) = 0;     virtual HRESULT __stdcall Subtract (int a, int b, int* pResult) = 0; }; 

You can take advantage of the fact that interface definitions are merely sets of pure virtual functions when you develop C++ classes that represent COM objects. For example, you can declare a class that implements IMath like this:

 class CComClass : public IMath { protected:     long m_lRef;    // Reference count public:     CComClass ();     virtual ~CComClass ();     // IUnknown methods     virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);     virtual ULONG __stdcall AddRef ();     virtual ULONG __stdcall Release ();     // IMath methods     virtual HRESULT __stdcall Add (int a, int b, int* pResult);     virtual HRESULT __stdcall Subtract (int a, int b, int* pResult); }; 

With this setup, you can implement QueryInterface, AddRef, Release, Add, and Subtract as member functions of class CComClass.

Now, suppose you want CComClass to implement not just one COM interface, but two. How do you do it? One approach is to derive CComClass from both IMath and another interface by using multiple inheritance, like so:

 class CComClass : public IMath, public ISpelling { protected:     long m_lRef;    // Reference count public:     CComClass ();     virtual ~CComClass ();     // IUnknown methods     virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);     virtual ULONG __stdcall AddRef ();     virtual ULONG __stdcall Release ();     // IMath methods     virtual HRESULT __stdcall Add (int a, int b, int* pResult);     virtual HRESULT __stdcall Subtract (int a, int b, int* pResult);     // ISpelling methods     virtual HRESULT __stdcall CheckSpelling (wchar_t* pString); }; 

This approach has a couple of advantages. First, it's simple. To declare a class that implements n interfaces, you simply include all n interfaces in the class's list of base classes. Second, you have to implement IUnknown only once. If each interface were truly implemented separately, you'd have to implement QueryInterface, AddRef, and Release for each one. But with multiple inheritance, all methods supported by all interfaces are essentially merged into one implementation.

One of the more interesting aspects of using multiple inheritance to write COM classes is what happens when a client calls QueryInterface asking for an interface pointer. Let's say that the client asks for an IMath pointer. The proper way to return the interface pointer is to cast the this pointer to an IMath*:

 *ppv = (IMath*) this; 

If the client asks for an ISpelling pointer instead, you cast to ISpelling*:

 *ppv = (ISpelling*) this; 

If you omit the casts, the code will compile just fine but will probably blow up when one of the two interfaces is used. Why? Because a class formed with multiple inheritance contains multiple vtables and multiple vtable pointers, and without the cast, you don't know which vtable the this pointer references. In other words, the two casts shown here return different numeric values, even though this never varies. If a client asks for an ISpelling pointer and you return a plain (uncasted) this pointer, and if this happens to reference IMath's vtable, the client calls ISpelling methods through an IMath vtable. That's a formula for disaster and is why COM classes that use multiple inheritance always cast to retrieve the proper vtable pointer.

Nested Classes

What's wrong with using multiple inheritance to implement COM classes? Nothing—provided that no two interfaces have methods with the same names and signatures. If IMath and ISpelling both contained methods named Init that had identical parameter lists but required separate implementations, you wouldn't be able to use multiple inheritance to define a class that implements both of them. Why? Because with multiple inheritance, the class would have just one member function named Init. It would therefore be impossible to implement Init separately for IMath and ISpelling.

This limitation is the reason MFC uses the nested class approach to implementing COM interfaces. Nested classes are a little more work and slightly less intuitive than multiple inheritance, but they're also suitably generic. You can use the nested class approach to implement any combination of COM interfaces in a single C++ class, regardless of the interfaces' characteristics. Here's how it works.

Suppose that CComClass implements IMath and ISpelling and that both interfaces have a method named Init that accepts no parameters.

 virtual HRESULT __stdcall Init () = 0; 

You can't use multiple inheritance in this case because of C++'s inability to support two semantically identical functions in one class. So instead, you define two subclasses, each of which implements one interface:

 class CMath : public IMath { protected:     CComClass* m_pParent;    // Back pointer to parent public:     CMath ();     virtual ~CMath ();     // IUnknown methods     virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);     virtual ULONG __stdcall AddRef ();     virtual ULONG __stdcall Release ();     // IMath methods     virtual HRESULT __stdcall Add (int a, int b, int* pResult);     virtual HRESULT __stdcall Subtract (int a, int b, int* pResult);     virtual HRESULT __stdcall Init () = 0; }; class CSpelling : public ISpelling { protected:     CComClass* m_pParent;    // Back pointer to parent public:     CSpelling ();     virtual ~CSpelling ();     // IUnknown methods     virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);     virtual ULONG __stdcall AddRef ();     virtual ULONG __stdcall Release ();     // ISpelling methods     virtual HRESULT __stdcall CheckSpelling (wchar_t* pString);     virtual HRESULT __stdcall Init () = 0; }; 

To make CMath and CSpelling nested classes, you declare them inside CComClass. Then you include in CComClass a pair of data members that are instances of CMath and CSpelling:

 class CComClass : public IUnknown { protected:     long m_lRef;        // Reference count     class CMath : public IMath     {         [...]     };     CMath m_objMath;        // CMath object     class CSpelling : public ISpelling     {         [...]     };     CSpelling m_objSpell;    // CSpelling object public:     CComClass ();     virtual ~CComClass ();     // IUnknown methods     virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);     virtual ULONG __stdcall AddRef ();     virtual ULONG __stdcall Release (); }; 

Notice that CComClass now derives only from IUnknown. It doesn't derive from IMath or ISpelling because the nested classes provide implementations of both interfaces. If a client calls QueryInterface asking for an IMath pointer, CComClass simply passes out a pointer to the CMath object:

 *ppv = (IMath*) &m_objMath; 

Similarly, if asked for an ISpelling pointer, CComClass returns a pointer to m_objSpell:

 *ppv = (ISpelling*) &m_objSpell; 

A key point to understand about the nested class approach is that the subobjects must delegate all calls to their IUnknown methods to the equivalent methods in the parent class. Notice that in place of a member variable that stores a reference count, each nested class stores a CComClass pointer. That pointer is a "back pointer" to the subobject's parent. Delegation is performed by calling CComClass's IUnknown methods through the back pointer. Typically, the parent's constructor initializes the back pointers:

 CComClass::CComClass () {     [...]        // Normal initialization stuff goes here.     m_objMath.m_pParent = this;     m_objSpell.m_pParent = this; } 

The nested classes' implementations of IUnknown look like this:

 HRESULT __stdcall CComClass::CMath::QueryInterface (REFIID riid, void** ppv) {     return m_pParent->QueryInterface (riid, ppv); } ULONG __stdcall CComClass::CMath::AddRef () {     return m_pParent->AddRef (); } ULONG __stdcall CComClass::CMath::Release () {     return m_pParent->Release (); } 

Delegation of this sort is necessary for two reasons. First, if a client calls AddRef or Release on an interface implemented by a subobject, the parent's reference count should be adjusted, not the subobject's. Second, if a client calls QueryInterface on one of the subobjects, the parent must field the call because only the parent knows which nested classes are present and therefore which interfaces it implements.

MFC and Nested Classes

If you browse through the source code for MFC classes such as COleControl, you won't see anything that resembles the code in the previous section. That's because MFC hides its nested classes behind macros.

MFC's COleDropTarget class is a case in point. It's one of the simpler MFC COM classes, and it implements just one COM interface—a standard interface named IDropTarget. If you look inside Afxole.h, you'll see these statements near the end of COleDropTarget's class declaration:

 BEGIN_INTERFACE_PART(DropTarget, IDropTarget)     [...]     STDMETHOD(DragEnter)(LPDATAOBJECT, DWORD, POINTL, LPDWORD);     STDMETHOD(DragOver)(DWORD, POINTL, LPDWORD);     STDMETHOD(DragLeave)();     STDMETHOD(Drop)(LPDATAOBJECT, DWORD, POINTL pt, LPDWORD); END_INTERFACE_PART(DropTarget) 

MFC's BEGIN_INTERFACE_PART macro defines a nested class that implements one COM interface. The class is named by prepending a capital X to the first parameter in the macro's parameter list. In this example, the nested class's name is XDropTarget. The END_INTERFACE_PART macro declares a member variable that's an instance of the nested class. Here's the code generated by the preprocessor:

 class XDropTarget : public IDropTarget { public:     STDMETHOD_(ULONG, AddRef)();     STDMETHOD_(ULONG, Release)();     STDMETHOD(QueryInterface)(REFIID iid, LPVOID* ppvObj);     STDMETHOD(DragEnter)(LPDATAOBJECT, DWORD, POINTL, LPDWORD);     STDMETHOD(DragOver)(DWORD, POINTL, LPDWORD);     STDMETHOD(DragLeave)();     STDMETHOD(Drop)(LPDATAOBJECT, DWORD, POINTL pt, LPDWORD); } m_xDropTarget; friend class XDropTarget; 

Do you see the resemblance between the preprocessor output and the nested class example we looked at earlier? Notice that the name of the nested class instance is m_x plus the first parameter in the macro's parameter list—in this case, m_xDropTarget.

The nested class implements the three IUnknown methods plus the methods listed between BEGIN_INTERFACE_PART and END_INTERFACE_PART. IDropTarget has four methods— DragEnter, DragOver, DragLeave, and Drop—hence the methods named in the preceding code listing. Here's an excerpt from the MFC source code file Oledrop2.cpp showing how IDropTarget's methods are implemented in the nested XDropTarget class:

 STDMETHODIMP_(ULONG) COleDropTarget::XDropTarget::AddRef() {     [...] } STDMETHODIMP_(ULONG) COleDropTarget::XDropTarget::Release() {     [...] } STDMETHODIMP COleDropTarget::XDropTarget::QueryInterface(...) {     [...] } STDMETHODIMP COleDropTarget::XDropTarget::DragEnter(...) {     [...] } STDMETHODIMP COleDropTarget::XDropTarget::DragOver(...) {     [...] } STDMETHODIMP COleDropTarget::XDropTarget::DragLeave(...) {     [...] } STDMETHODIMP COleDropTarget::XDropTarget::Drop(...) {     [...] } 

The code inside the method implementations is unimportant for now. The key here is that a few innocent-looking macros in an MFC source code listing turn into a nested class that implements a full-blown COM interface. You can create a class that implements several COM interfaces by including one BEGIN_INTERFACE_PART/END_INTERFACE_PART block for each interface. Moreover, you needn't worry about conflicts if two or more interfaces contain identical methods because the nested class technique permits each interface (and its methods) to be implemented independently.

How MFC Implements IUnknown

Let's go back and look more closely at COleDropTarget's implementation of QueryInterface, AddRef, and Release. Here's the complete, unabridged version:

 STDMETHODIMP_(ULONG) COleDropTarget::XDropTarget::AddRef() {     METHOD_PROLOGUE_EX_(COleDropTarget, DropTarget)     return pThis->ExternalAddRef(); } STDMETHODIMP_(ULONG) COleDropTarget::XDropTarget::Release() {     METHOD_PROLOGUE_EX_(COleDropTarget, DropTarget)     return pThis->ExternalRelease(); } STDMETHODIMP COleDropTarget::XDropTarget::QueryInterface(     REFIID iid, LPVOID* ppvObj) {     METHOD_PROLOGUE_EX_(COleDropTarget, DropTarget)     return pThis->ExternalQueryInterface(&iid, ppvObj); } 

Once more, what MFC is doing is hidden behind a macro. In this case, the macro is METHOD_PROLOGUE_EX_, which creates a stack variable named pThis that points to XDropTarget's parent—that is, the COleDropTarget object of which the XDropTarget object is a member. Knowing this, you can see that XDropTarget's IUnknown methods delegate to COleDropTarget. Which begs a question or two: What do COleDropTarget's ExternalAddRef, ExternalRelease, and ExternalQueryInterface functions do, and where do they come from?

The second question is easy to answer. All three functions are members of CCmdTarget, and COleDropTarget is derived from CCmdTarget. To answer the first question, we need to look at the function implementations inside CCmdTarget. Here's an excerpt from the MFC source code file Oleunk.cpp:

 DWORD CCmdTarget::ExternalAddRef() {     [...]     return InternalAddRef(); } DWORD CCmdTarget::ExternalRelease() {     [...]     return InternalRelease(); } DWORD CCmdTarget::ExternalQueryInterface(const void* iid,     LPVOID* ppvObj) {     [...]     return InternalQueryInterface(iid, ppvObj); } 

ExternalAddRef, ExternalRelease, and ExternalQueryInterface call another set of CCmdTarget functions named InternalAddRef, InternalRelease, and InternalQueryInterface. The Internal functions are a little more complicated, but if you look at them, you'll find that they do just what AddRef, Release, and QueryInterface are supposed to do, albeit in an MFC way. So now we know that the nested class's IUnknown methods delegate to the parent class and that the parent class inherits implementations of these methods from CCmdTarget. Let's keep going.

Interface Maps

The most interesting Internal function is InternalQueryInterface. If you peek at it in Oleunk.cpp, you'll see that it calls a little-known function named GetInterface, which belongs to a little-known class named CUnknown. GetInterface does a table lookup to determine whether this class supports the specified interface. It then retrieves a pointer to the nested class that implements the interface and returns it to InternalQueryInterface. So MFC uses a table-driven mechanism to implement QueryInterface. But where do the tables come from?

Once more, we can look to COleDropTarget for an example. At the very end of COleDropTarget's class declaration is the statement

 DECLARE_INTERFACE_MAP() 

And in COleDropTarget's implementation is this set of related statements:

 BEGIN_INTERFACE_MAP(COleDropTarget, CCmdTarget)     INTERFACE_PART(COleDropTarget, IID_IDropTarget, DropTarget) END_INTERFACE_MAP() 

DECLARE_INTERFACE_MAP is an MFC macro that declares an interface map—a table containing one entry for each interface that a class (in reality, a nested class) implements. BEGIN_INTERFACE_MAP and END_INTERFACE_MAP are also macros. They define the contents of the interface map. Just as message maps tell MFC which messages a class provides handlers for, interface maps tell MFC which COM interfaces a class supports and which nested classes provide the interface implementations. Each INTERFACE_PART macro that appears between BEGIN_INTERFACE_MAP and END_INTERFACE_MAP constitutes one entry in the table. In this example, the INTERFACE_PART statement tells MFC that the interface map is a member of COleDropTarget, that COleDropTarget implements the IDropTarget interface, and that the nested class containing the actual IDropTarget implementation is XDropTarget. INTERFACE_PART prepends an X to the class name in the same manner as BEGIN_INTERFACE_PART.

Because an interface map can contain any number of INTERFACE_PART macros, MFC classes aren't limited to one COM interface each; they can implement several. For each INTERFACE_PART entry that appears in a class's interface map, there is one BEGIN_INTERFACE_PART/END_INTERFACE_PART block in the class declaration. Take a look at COleControl's interface map in Ctlcore.cpp and the numerous BEGIN_INTERFACE_PART/END_INTERFACE_PART blocks in AfxCtl.h and you'll see what I mean.

MFC and Aggregation

Does it seem curious that CCmdTarget has two sets of functions with QueryInterface, AddRef, and Release in their names? When I showed you the source code for the External functions, I omitted (for clarity) the part that explains why. Here it is again, but this time in unabbreviated form:

 DWORD CCmdTarget::ExternalAddRef() {     // delegate to controlling unknown if aggregated     if (m_pOuterUnknown != NULL)         return m_pOuterUnknown->AddRef();     return InternalAddRef(); } DWORD CCmdTarget::ExternalRelease() {     // delegate to controlling unknown if aggregated     if (m_pOuterUnknown != NULL)         return m_pOuterUnknown->Release();     return InternalRelease(); } DWORD CCmdTarget::ExternalQueryInterface(const void* iid,     LPVOID* ppvObj) {     // delegate to controlling unknown if aggregated     if (m_pOuterUnknown != NULL)         return m_pOuterUnknown->QueryInterface(*(IID*)iid, ppvObj);     return InternalQueryInterface(iid, ppvObj); } 

Observe that the External functions call the Internal functions only if m_pOuterUnknown holds a NULL value. m_pOuterUnknown is a CCmdTarget member variable that holds an object's controlling unknown. If m_pOuterUnknown is not NULL, the External functions delegate through the pointer held in m_pOuterUnknown. If you're familiar with COM aggregation, you can probably guess what's going on here. But if aggregation is new to you, the preceding code requires further explanation.

COM has never supported inheritance in the way that C++ does. In other words, you can't derive one COM object from another in the way that you can derive one C++ class from another. However, COM does support two mechanisms— containment and aggregation—for object reuse.

Containment is the simpler of the two. To illustrate how it works, let's say you've written an object that contains a pair of methods named Add and Subtract. Now suppose someone else has written a COM object with Multiply and Divide methods that you'd like to incorporate into your object. One way to "borrow" the other object's methods is to have your object create the other object with CoCreateInstance and call its methods as needed. Your object is the outer object, the other object is the inner object, and if m_pInnerObject holds a pointer to the interface on the inner object that implements Multiply and Divide, you might also include Multiply and Divide methods in your object and implement them like this:

 HRESULT __stdcall CComClass::Multiply (int a, int b, int* pResult) {     return m_pInnerObject->Multiply (a, b, pResult); } HRESULT __stdcall CComClass::Divide (int a, int b, int* pResult) {     return m_pInnerObject->Divide (a, b, pResult); } 

That's containment in a nutshell. Figure 18-5 shows the relationship between the inner and outer objects. Notice that the inner object's interface is exposed only to the outer object, not to the clients of the outer object.

Figure 18-5. Containment.

Aggregation is altogether different. When one object aggregates another, the aggregate object exposes the interfaces of both the inner and the outer objects. (See Figure 18-6.) The client has no idea that the object is actually an aggregate of two or more objects.

Figure 18-6. Aggregation.

Aggregation is similar to containment in that the outer object creates the inner object. But the similarities end there. For aggregation to work, the inner object and the outer object must work together to create the illusion that they're really one object. Both objects must adhere to a strict set of rules governing their behavior. One of those rules says that the outer object must pass its own IUnknown pointer to the inner object. This pointer becomes the inner object's controlling unknown. If a client calls an IUnknown method on the inner object, the inner object must delegate to the outer object by calling QueryInterface, AddRef, or Release through the controlling unknown. That's what happens when CCmdTarget's External functions call QueryInterface, AddRef, or Release through m_pOuterUnknown. If the object is aggregated, m_pOuterUnknown is non-NULL and the External functions delegate to the outer object. Otherwise, the object isn't aggregated and the Internal functions are called instead.

A key difference between containment and aggregation is that any object can be contained by another object, but only objects that specifically support aggregation can be aggregated. MFC makes aggregation easy because it builds in aggregation support for free.

MFC and Class Factories

Any class library that places a friendly wrapper around COM should include support for class factories. COM class factories typically contain a lot of boilerplate code that varies little from one application to the next, so they're perfect candidates to be hidden away inside a C++ class.

MFC provides a canned implementation of COM class factories in COleObjectFactory. MFC's COleObjectFactory class implements two COM interfaces: IClassFactory and IClassFactory2. IClassFactory2 is a superset of IClassFactory; it supports all of IClassFactory's methods and adds licensing methods that are used primarily by ActiveX controls.

When you create a COleObjectFactory, you feed its constructor four critical pieces of information. The first is the CLSID of the object that the class factory creates. The second is a RUNTIME_CLASS pointer identifying the C++ class that implements objects of that type. The third is a BOOL that tells COM whether this server, if it's an EXE, is capable of creating multiple object instances. If this parameter is TRUE and 10 clients call CoCreateInstance on the COM class that this server implements, 10 different instances of the EXE are launched. If the parameter is FALSE, one instance of the EXE serves all 10 clients. The fourth and final parameter to COleObjectFactory's constructor is the ProgID of the object that the class factory creates. ProgID is short for Program ID; it's a human-readable name (for example, "Math.Object") that can be used in lieu of a CLSID to identify a COM class. The following code fragment creates a COleObjectFactory that instantiates CComClass when CLSID_Math is passed to a COM activation function:

 COleObjectFactory cf (     CLSID_Math,                  // The object's CLSID     RUNTIME_CLASS (CComClass),   // Class representing the object     FALSE,                       // Many clients, one EXE     _T ("Math.Object")           // The object's ProgID ); 

Most MFC applications don't explicitly declare an instance of COleObjectFactory; instead, they use MFC's DECLARE_OLECREATE and IMPLEMENT_OLECREATE macros. When the preprocessor encounters

 // In the class declaration DECLARE_OLECREATE (CComClass) // In the class implementation IMPLEMENT_OLECREATE (CComClass, "Math.Object", 0x708813ac,     0x88d6, 0x11d1, 0x8e, 0x53, 0x00, 0x60, 0x08, 0xa8, 0x27, 0x31) 

it outputs this:

 // In the class declaration public:     static COleObjectFactory factory;     static const GUID guid; // In the class implementation COleObjectFactory CComClass::factory(CComClass::guid,     RUNTIME_CLASS(CComClass), FALSE, _T("Math.Object")); const GUID CComClass::guid =     { 0x708813ac, 0x88d6, 0x11d1, { 0x8e, 0x53, 0x00,       0x60, 0x08, 0xa8, 0x27, 0x31} }; 

The one drawback to the OLECREATE macros is that they contain hardcoded references to COleObjectFactory. If you derive a class from COleObjectFactory and want to use it in an application, you must either discard the macros and hand-code the references to the derived class or write your own OLECREATE macros. Programmers occasionally do find it useful to derive their own classes from COleObjectFactory to modify the class factory's behavior. By overriding the virtual OnCreateObject function, for example, you can create a "singleton" class factory—a class factory that creates an object the first time IClassFactory::CreateInstance is called and hands out pointers to the existing object in response to subsequent activation requests.

Internally, MFC maintains a linked list of all the COleObjectFactory objects that an application creates. (Look inside COleObjectFactory's constructor and you'll see the code that adds each newly instantiated object to the list.) COleObjectFactory includes handy member functions for registering all an application's class factories with the operating system and for registering the objects that the class factories create in the system registry. The statement

 COleObjectFactory::UpdateRegistryAll (); 

adds to the registry all the information required to create any object that is served up by this application's class factories. That's powerful, because the alternative is to write low-level code that relies on Win32 registry functions to update the registry yourself.

Putting It All in Perspective

Has this chapter covered everything there is to know about the relationship between MFC and COM? Hardly. There's plenty more, as you'll discover in the next three chapters. But this chapter has set the stage for the ones that follow. Now when you see a diagram like the one in Figure 18-1, you'll understand what you're looking at and have a pretty good idea of how MFC implements it. Plus, when you look over a wizard-generated source code listing or dig down into the MFC source code, you'll know what statements like INTERFACE_PART and IMPLEMENT_OLECREATE mean.

If COM is new to you, you're probably feeling a little overwhelmed right now. Don't despair. Learning COM is a lot like learning to program Windows: You endure the obligatory six months of mental fog before it all begins to make sense. The good news is that you don't have to be an expert on COM to build COM-based applications with MFC. In fact, you don't have to know much about COM at all. But if you believe (as I do) that the best programmers are the ones who understand what goes on under the hood, the information presented in this chapter will serve you well in the long run.



Programming Windows with MFC
Programming Windows with MFC, Second Edition
ISBN: 1572316950
EAN: 2147483647
Year: 1999
Pages: 101
Authors: Jeff Prosise

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net