Hosting a
Control
If
you want to host a control, you can do so with ATL's
control-hosting support. For example, the Ax in
CAxDialogImpl stands for ActiveX control and indicates
that the dialog box is capable of hosting controls. To host a
control in a dialog box, right-click the dialog box resource and
choose Insert ActiveX Control. This produces a dialog box
that lists the controls installed on your system, as shown in
Figure 1.17.
After you insert the control, you can click on
it and set its properties in the Properties window, as shown in
Figure 1.18.
By
clicking the Control Events toolbar button, you also can choose to
handle a control's events, as shown in Figure 1.19.
When the dialog box is shown, the control is
created and initialized based on the properties set at development
time. Figure 1.20 shows an
example of a dialog box hosting a control.
ATL provides support for hosting ATL controls
not only in dialog boxes, but also in other windows, in controls
that have a UI declared as a dialog box resource (called composite
controls), and in controls that have a UI declared as an HTML
resource (called HTML controls). For more information about control
containment, see Chapter
12, "Control Containment."
Being a C++ COM
Client
COM and C++ go hand in handat least,
theoretically. A COM interface maps directly to a C++ abstract
class. All you need to do to use a COM object is run its IDL file
through the MIDL compiler, and you've got a header file with all
the information you need.
This worked well until
the VB team asked if it could play with this COM stuff, too.
VB developers generally neither know nor want to
know C++. And IDL is a language that's very much in the C++
tradition, with lots of support for C/C++-specific things in it
(such as arrays and pointers) VB needed a way to store type
information about COM objects that VB developers could use and
understand easily.
Thus was born the type
library (a.k.a. typelib). A
typelib stores information about a COM object: The classid, the
interfaces that the object supports, the methods on those
interfaces, and so onjust about everything you'd find in an IDL
file (with some unfortunate exceptions, mostly having to do with
C-style arrays). The COM system includes a set of COM objects that
lets you programmatically walk through the contents of a typelib.
Best of all, the typelib can be embedded into a DLL or EXE file
directly, so you never have to worry about the type information
getting lost.
The typelib was so successful for VB developers
that many COM components these days aren't shipped with an IDL
file; the type library includes everything needed to use the
components. Only one thing is missing: How do we use typelibs in
C++?
The C++ language doesn't understand typelibs. It
wants header files. This was such a serious problem that, back in
Visual Studio 6, Microsoft extended the compiler so that it could
use type libraries in much the same way that you use header files.
This extension was the #import statement.
#import is used much like
#include is. The general form is shown here:
#import "pisvr.dll" <options>
The #import statement generates either
one or two C++ header files, depending on the options you use.
These header files have the extensions .tlh (for "typelib
header") and .tli (for "typelib inline") and are generated
into your project output directory (by default, Debug for a debug
build, Release for a release build).
The options on the #import line give
you a great deal of control over the contents of the generated
files. Check the Visual Studio documentation for the full list; we
talk about some of the more commonly used options here.
The no_namespace option tells the
compiler that we don't want the contents of the generated files to
be placed into a C++ namespace. By default, the contents of the
generated files are placed in a C++ namespace named after the type
library.
named_guids instructs the compiler that
we want to have named symbols for the GUIDs in the type library. By
default, this would not compile because the name
CLSID_PISvr would not be defined:
::CoCreateInstance( CLSID_PISvr, ... );
Instead, you have to do this:
::CoCreateInstance( __uuidof( PISvr ), ... );
You also need to use __uuidof( ) to get
the IID for interfaces.
The raw_interfaces_only option requires
the most explanation. By default, when the #import
statement generates the header file, it doesn't just spit out class
definitions for interfaces. It actually generates wrapper classes
that attempt to make a COM interface easier to use. For example,
given the interface:
interface ICalcPi : IDispatch {
[propget, id(1), helpstring("property Digits")]
HRESULT Digits([out, retval] LONG* pVal);
[propput, id(1), helpstring("property Digits")]
HRESULT Digits([in] LONG newVal);
[id(2), helpstring("method CalcPi")]
HRESULT CalcPi([out,retval] BSTR* pbstrPi);
};
Normal use of this interface would be something
like this:
HRESULT DoStuff( long nDigits, ICalcPi *pCalc ) {
HRESULT hr = pCalc->put_Digits( nDigits );
if( FAILED( hr ) ) return hr;
BSTR bstrResult;
hr = pCalc->CalcPi( &bstrResult );
if( FAILED( hr ) ) return hr;
std::cout << "PI to " << nDigits << " digits is "
<< CW2A( bstrResult );
::SysFreeString( bstrResult );
return S_OK;
}
When using the #import statement, on
the other hand, using this interface looks like this:
void DoStuff( long nDigits, ICalcPiPtr spCalc ) {
spCalc->Digits = nDigits;
_bstr_t bstrResults = spCalc->CalcPi();
std::cout << "PI to " << spCalc->Digits << " digits is "
<< ( char * )bstrResults;
}
The
ICalcPiPtr type is a smart pointer expressed as a typedef
for the _com_ptr_t class. This class is not part of ATL; it's part of the
Direct-To-COM extensions to the compiler and is defined in the
system header file comdef.h (along with all the other
types used by the wrapper classes). The smart pointer automatically
manages the reference counting, and the_bstr_t type
manages the memory for a BSTR (which we discuss in
Chapter 2, "Strings
and Text").
The most remarkable thing about the wrapper
classes is that the hrESULT testing is gone. Instead, the
wrapper class translates any failed HRESULTs into a C++
exception (the _com_error class, to be precise). This lets
the generated code use the method's [retval] variable as
the actual return value, which eliminates a lot of temporary
variables and output parameters.
The wrapper classes can immensely simplify
writing COM clients, but they have their downsides. The biggest is
that they require the use of C++ exceptions. Some projects aren't
willing to pay the performance penalties that exception handling
brings, and throwing exceptions means that developers have to pay
very careful attention to exception safety.
Another downside to the wrappers for ATL
developers is that ATL also has wrapper classes for COM interfaces
(see Chapter 3, "ATL
Smart Types") and BSTRs (see Chapter 2). The ATL wrappers are arguably
better than the ones defined in comdef.h; for example, you
can accidentally call the Release() method on an
ICalcPiPtr, but if you use the ATL wrapper, that would be
a compile error.
By default, you get the wrappers when you use
#import. If you decide that you don't want them, or if for
some reason they don't compile (which has been known to happen to
at least one of your humble authors on very complex and strange
typelibs), you can turn off the wrapper classes and just get
straight interface definitions by using the
raw_interfaces_only option.
|