Полезная информация

Chapter 10
Using BaseCtl to Create a Basic ActiveX Control


Using BaseCtl to Create a Basic ActiveX Control

The BaseCtl framework was originally created by Marc Wandschneider, a member of the Visual Basic (VB) team, to address the need for small, fast OCXs that could be used within VB without adversely affecting VB's performance. The original implementation was referred to lovingly as the MarcWan framework in honor of its primary author.

Along with the desire for a compact control framework that could be used to create ActiveX controls came the mandate to remove the framework's dependence on MFC, which in our mind is both the BaseCtl's strength and weakness. Removing the BaseCtl's dependence on MFC solved several key problems: code overhead and control load time performance. Since the BaseCtl framework is lean, the controls tend to execute faster, and because the MFC DLLs are not required, the amount of time it takes to load the control into memory is effectively reduced. However, writing a control from scratch using the BaseCtl takes significantly more time since you do not have AppWizards and ClassWizards at your disposal to speed up your implementation. The lack of general class support, for example drawing classes and storage classes, can also increase your development effort. With the coming of ActiveX, the need for small, fast OCXs suddenly became an industry concern. Microsoft's answer to that demand was to publish the BaseCtl as an alternative method (as opposed to MFC) for developing controls. Several versions of the BaseCtl framework are floating around. The basic version, which ships with the ActiveX SDK, consists of a number of source files and several samples. A more thorough version consisting of more samples and even an AppWizard written in VB has been available to the members of the Visual Basic 5 (VB 5) beta testing group.

The BaseCtl Framework is intended merely as a sample application and does not have the same support and backing of Microsoft as do its other development products. It was an immediate solution to an immediate problem. The BaseCtl framework has one very distinct disadvantage in that Microsoft considers it a sample application only and does not support it directly as a product. A growing number of developers on the Internet are using BaseCtl, so you should be able to find support easily if you run into a problem.

This chapter focuses on creating ActiveX controls using the BaseCtl sample that ships with the ActiveX SDK. Be warned, though, that this kind of control development is not for the timid. Be prepared to roll your sleeves up and get dirty.

Creating a Basic Control Project

The version of the BaseCtl sample that ships with the ActiveX SDK does not have an AppWizard for creating a basic control like its MFC counterpart. Also, the documentation is minimal and not much help. To speed up your development, we've included a sample project called BCFBasicControl that is based on the original BaseCtl sample files. Using this sample control, you can create a new project by copying the files and changing the names of the files and classes.

First in Table 10.1 examine the files that are needed for creating a basic control with the BaseCtl framework.
Table 10.1 BaseCtl Basic Project Files
File name Description
BCFBasicControl.dsw VC++ build file.
BCFBasicControl.dsp VC++ build file.
BCFBasicControl.opt VC++ build file.
BCFBasicControl.cpp Main control application file.
BCFBasicControl.def Application definition file.
BCFBasicControl.odl Object Definition Language file for describing the and interfaces contained in the control.
BCFBasicControl.rc Application resource file.
BCFBasicControlCtl.bmp Basic control bitmap that appears in the tool browser.
BCFBasicControlCtl.cpp Primary control source file.
BCFBasicControlCtl.h Primary control header file.
BCFBasicControlPPG.cpp Property page source file.
BCFBasicControlPPG.h Property page header file.
Dispids.h Header file that contains all of the method, property, event dispids. Add all dispids to this file.
Dwinvers.h Version information header file. Change this file to the version information specific to the control.
Guids.cpp Source file for the GUIDs defined in the application.
Guids.h Header file for the GUID defined in the application.
LocalObj.h Header file for the OBJECT_TYPE constants that are used in the g_ObjectInfo table to identify the controls and property pages contained in the application.
Resource.h Header file that contains all of the resource ID constants.
project_nameInterfaces.H This file is not created until the project is compiled. The mktylib (or midl) compiler automatically generates this file from the ODL file. This file contains the C++style declarations for the interfaces of the ActiveX component. Do not modify this file directly; it will be recreated every time the ODL file is compiled.


In this chapter, you will create three controls: a standard control, a windowless control, and a subclassed control. All three will be combined into a single control module to give you a feel for how it is done. None of the sample controls is meant to be a fully functional control. They are used only to give you an understanding of how to implement specific features and functionality with a minimum of effort. Again, since you do not have an application wizard at your disposal, you must create your project by hand. To create a new BaseCtl project, you need to perform the following steps:

After you modify all of the files, you are ready to use your new project. Open the Visual C++ development environment, and from the File menu, select the Open Workspace menu item. In the Open Workspace dialog, change to the directory of your newly created project (\Que\ActiveX\BCFControl), and open the BCFControl.dsw file (see fig. 10.1).

FIG. 10.1
Open the new project with the Open Workspace dialog.

Unlike with the MFC and ATL projects, to support more than one control within the application, you must add all of the code and files by hand since you do not have an AppWizard. The simplest way to create additional controls is to repeat the steps described earlier and copy the appropriate code and files into your base project. You will create two additional controls named BCFControlNoWinControl, which is a windowless control, and BCFControlSubWinControl, which will subclass a BUTTON window.

After creating the two new projects, as described in the preceding paragraph, copy the following files to the BCFControl directory.

Ensure that the BCFControl project is open within the VC++ IDE, and from the Project menu, select the Add to Project menu item and the Files submenu. In the Insert Files into Project dialog, select the files BCFControlNoWinCtl.Cpp, BCFControlNoWinPPG.Cpp, BCFControlSubWinCtl.Cpp, and BCFControlSubWinPPG.Cpp, and click the OK button (see fig. 10.2).

FIG. 10.2
Use the Insert Files into Project dialog to add the files of the BCFControlNoWin and BCFControlSubWin control to the base project.

After you add all of the files that you need to your project, you still have to do some cut and paste operations from the remaining files to complete the integration of the three controls.

You need to include the header files from your new control projects to the main application file (see Listing 10.1).

The BaseCtl framework supports a globally declared array for describing all of the OLE components included within the application. The OBJECTINFO array, as it is called, should contain an entry for each control, property page, and automation server you want to declare within your module. You need to add your additional control and property page declarations to the OBJECTINFO array.

Listing 10.1 BCFCONTROL.CPP--Include New Control Header Files and Control Declarations

//=--------------------------------------------------------------------------=

// BCFControl.Cpp //=--------------------------------------------------------------------------=
...
#include "BCFControlCtl.H"
#include "BCFControlPPG.H"
#include "BCFControlNoWinCtl.H"

#include "BCFControlNoWinPPG.H"
#include "BCFControlSubWinCtl.H"
#include "BCFControlSubWinPPG.H"
...
//=--------------------------------------------------------------------------=
// This Table describes all the automatible objects in your automation server.
// See AutomationObject.H for a description of what goes in this structure
// and what it's used for.
//
OBJECTINFO g_ObjectInfo[] = {
CONTROLOBJECT(BCFControl),
PROPERTYPAGE(BCFControlGeneral),
CONTROLOBJECT(BCFControlNoWin),
PROPERTYPAGE(BCFControlNoWinGeneral),
CONTROLOBJECT(BCFControlSubWin),
PROPERTYPAGE(BCFControlSubWinGeneral), EMPTYOBJECT
};

...

The ODL compiler will generate a C++ header file for accessing the interfaces declared in the application. Since you are combining all three controls into a single project, you will have a single interface file to deal with. The two additional control implementations must be changed to reflect the new file. In the BCFControlNoWinCtl.h header file, you need to change the include file

#include "BCFControlNoWinInterfaces.H"

to

#include "BCFControlInterfaces.H"

The same must be done for the BCFControlSubWinCtl.h header file.

Each of the individual projects contains a Guids.h file. You combine all three into a single file (see Listing 10.2).

Listing 10.2 GUIDS.H--Combined Guids.h

#ifndef _GUIDS_H_
// for each property page this server will have, put the guid definition for it
// here so that it gets defined ...
//
DEFINE_GUID(CLSID_BCFControlGeneralPage, 0x317512F4, 0x3E75, 0x11d0,
0xBE, 0xBE, 0x00, 0x40, 0x05, 0x38, 0x97, 0x7D);
DEFINE_GUID(CLSID_BCFControlNoWinGeneralPage, 0xcf395064, 0x3fb6, 0x11d0,
0xbe, 0xc1, 0x00, 0x40, 0x05, 0x38, 0x97, 0x7d);
DEFINE_GUID(CLSID_BCFControlSubWinGeneralPage, 0x02456be4, 0x3fb7, 0x11d0,
0xbe, 0xc1, 0x00, 0x40, 0x05, 0x38, 0x97, 0x7d);
#define _GUIDS_H_
#endif // _GUIDS_H_

Next you need to combine all of the ODL files into a single file. While it is possible for an application to contain more than one type library resource, for simplicity, you will have one. Copy only the interface entries from each of the ODL files, and insert them into the BCFControl.odl file (see Listing 10.3).

Listing 10.3 BCFCONTROL.ODL--Combined Control ODL Files

//=--------------------------------------------------------------------------=
// BCFControl.ODL
//=--------------------------------------------------------------------------=
// Copyright 1995 Microsoft Corporation. All Rights Reserved.
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//=--------------------------------------------------------------------------=
//
// ODL file for the control(s) and automation object(s) in this inproc server
//
#include <olectl.h>
#include "dispids.h"
// can't include oaidl.h, so this will have to do
//
#define DISPID_NEWENUM -4 //=--------------------------------------------------------------------------=
// the libid for this type libray
//
[
uuid(317512F0-3E75-11d0-BEBE-00400538977D),
helpstring("BCFControl Control Library"),
lcid(0x0000),
version(1.0)
]
library BCFControlObjects {
// standard imports
//
importlib("STDOLE32.TLB");
importlib(STDTYPE_TLB);
// primary dispatch interface for CBCFControl control
//
[
uuid(317512F1-3E75-11d0-BEBE-00400538977D),
helpstring("BCFControl Control"),
hidden,
dual,
odl
]
interface IBCFControl : IDispatch {
// properties
//
// methods
//
[id(DISPID_ABOUTBOX)]
void AboutBox(void);
};
// event interface for CBCFControl controls ...
//
[
uuid(317512F2-3E75-11d0-BEBE-00400538977D),
helpstring("Event interface for BCFControl control"),
hidden
]
dispinterface DBCFControlEvents {
properties:
methods:
};
// coclass for CBCFControl controls
//
[
uuid(317512F3-3E75-11d0-BEBE-00400538977D),
helpstring("BCFControl control")
]
coclass BCFControl {
[default] interface IBCFControl;
[default, source] dispinterface DBCFControlEvents;
};
// primary dispatch interface for CBCFControlNoWin control
//
[
uuid(cf395061-3fb6-11d0-bec1-00400538977d),
helpstring("BCFControlNoWin Control"),
hidden,
dual,
odl
]
interface IBCFControlNoWin : IDispatch {
// properties
//
// methods
//
[id(DISPID_ABOUTBOX)]
void AboutBox(void);
};
// event interface for CBCFControlNoWin controls ...
//
[
uuid(cf395062-3fb6-11d0-bec1-00400538977d),
helpstring("Event interface for BCFControlNoWin control"),
hidden
]
dispinterface DBCFControlNoWinEvents {
properties:
methods:
};
// coclass for CBCFControlNoWin controls
//
[
uuid(cf395063-3fb6-11d0-bec1-00400538977d),
helpstring("BCFControlNoWin control")
]
coclass BCFControlNoWin {
[default] interface IBCFControlNoWin;
[default, source] dispinterface DBCFControlNoWinEvents;
};
// primary dispatch interface for CBCFControlSubWin control
//
[
uuid(02456be1-3fb7-11d0-bec1-00400538977d),
helpstring("BCFControlSubWin Control"),
hidden,
dual,
odl
]
interface IBCFControlSubWin : IDispatch {
// properties
//
// methods
//
[id(DISPID_ABOUTBOX)]
void AboutBox(void);
};
// event interface for CBCFControlSubWin controls ...
//
[
uuid(02456be2-3fb7-11d0-bec1-00400538977d),
helpstring("Event interface for BCFControlSubWin control"),
hidden
]
dispinterface DBCFControlSubWinEvents {
properties:
methods:
};
// coclass for CBCFControlSubWin controls
//
[
uuid(02456be3-3fb7-11d0-bec1-00400538977d),
helpstring("BCFControlSubWin control")
]
coclass BCFControlSubWin {
[default] interface IBCFControlSubWin;
[default, source] dispinterface DBCFControlSubWinEvents;
};
};

CAUTION:
When combining several OCXs, you must delete any .tlb files that may have already been created to update the dependencies of the project. Occasionally, Visual C++ will compile more that one type library into different directories and use the incorrect version when compiling the resources for the control. In the case where you have new information that is added to the ODL file, the result will be that the type library has not appeared to have changed, which can be a major problem for applications, such as VB, that depend on the type library for information about the component being used.



Open the BCFControlNoWin.rc and BCFControlSubWin.rc resource files. Copy the property page dialogs and the string and bitmap resources to the BCFControl resource file. Rename the bitmaps to RESID_TOOLBOX_BITMAP1, RESID_TOOLBOX_BITMAP2, and RESID_TOOLBOX_BITMAP3, respectively.

NOTE: If you experience problems opening the resource files because of dependencies on the type library, you can either compile the ODL files into the type library or open the resource files in text mode and edit the files manually.



After combining the resources, you need to combine your Resource.h files as well (see Listing 10.4). Make sure that the constant values match their corresponding resources.

Listing 10.4 RESOURCE.H--Combined Resource.h Files

//=--------------------------------------------------------------------------=

// Resource.H
//=--------------------------------------------------------------------------=
// Copyright 1995 Microsoft Corporation. All Rights Reserved.
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//=--------------------------------------------------------------------------=
//
// resource IDs.
//
#ifndef _RESOURCE_H_
#define RESID_TOOLBOX_BITMAP1 1
#define RESID_TOOLBOX_BITMAP2 2
#define RESID_TOOLBOX_BITMAP3 3
//=--------------------------------------------------------------------------=
// Strings
//
// this must be defined for any server that has propety pages. it must be one
// thousand.
//
#define IDS_PROPERTIES 1000
// this is defined for all inproc servers that use satellite localization. it
// must be 1001
//
#define IDS_SERVERBASENAME 1001
#define IDS_BCFCONTROL_GENERALPAGETITLE 2003
#define IDS_BCFCONTROL_GENERALDOCSTRING 2004
#define IDS_BCFCONTROLNOWIN_GENERALPAGETITLE 2005
#define IDS_BCFCONTROLNOWIN_GENERALDOCSTRING 2006
#define IDS_BCFCONTROLSUBWIN_GENERALPAGETITLE 2007
#define IDS_BCFCONTROLSUBWIN_GENERALDOCSTRING 2008
#define IDS_BCFCONTROL_ABOUTBOXVERB 2009
#define IDS_BCFCONTROLNOWIN_ABOUTBOXVERB 2010
#define IDS_BCFCONTROLSUBWIN_ABOUTBOXVERB 2011
//=--------------------------------------------------------------------------=
// Dialog Stuff
//
#define IDD_PROPPAGE_BCFCONTROLGENERAL 2000
#define IDD_PROPPAGE_BCFCONTROLNOWINGENERAL 2001
#define IDD_PROPPAGE_BCFCONTROLSUBWINGENERAL 2002
#define _RESOURCE_H_
#endif // _RESOURCE_H_

Combine all of the LocalObj.h files (see Listing 10.5). Be sure to renumber the constants so that they are not the same since they are used to identify each control and property page in your application.

Listing 10.5 LOCALOBJ.H--Combined LocalObj.h File

//=--------------------------------------------------------------------------=
// LocalObjects.H
//=--------------------------------------------------------------------------=
...
// **** ADD ALL NEW OBJECTS TO THIS LIST ****
//
#define OBJECT_TYPE_CTLBCFCONTROL 0
#define OBJECT_TYPE_PPGBCFCONTROLGENERAL 1
#define OBJECT_TYPE_CTLBCFCONTROLNOWIN 2
#define OBJECT_TYPE_PPGBCFCONTROLNOWINGENERAL 3
#define OBJECT_TYPE_CTLBCFCONTROLSUBWIN 4
#define OBJECT_TYPE_PPGBCFCONTROLSUBWINGENERAL 5
#define _LOCALOBJECTS_H_
#endif // _LOCALOBJECTS_H_

Last you update the bitmap resource constant for each of your control header files. Listing 10.6 shows the change that needs to be made to each of the control header files.

Listing 10.6 BCFCONTROLCTL.H--Update the Bitmap Source

extern const GUID *rgBCFControlPropPages [];
DEFINE_CONTROLOBJECT(BCFControl,
&CLSID_BCFControl,
"BCFControlCtl",
CBCFControlControl::Create,
1,
&IID_IBCFControl,
"BCFControl.HLP",
&DIID_DBCFControlEvents,
OLEMISC_SETCLIENTSITEFIRST | OLEMISC_ACTIVATEWHENVISIBLE |
OLEMISC_RECOMPOSEONRESIZE | OLEMISC_CANTLINKINSIDE | OLEMISC_INSIDEOUT,
0, // no IPointerInactive policy by default
RESID_TOOLBOX_BITMAP1,
"BCFControlWndClass",
1,
rgBCFControlPropPages,
0,
NULL);

Be sure to make the same change to the BCFControlNoWinCtl.h and BCFControlSubWinCtl.h header files using the ID's RESID_TOOLBOX_BITMAP2 and RESID_TOOLBOX_BITMAP3, respectively.

All of the basic source files are now added to the control project. The next step in any control project is to ensure that the project contains registration support. Without registration, the control cannot be used by any application.

Control Registration

Control registration support is handled completely by the BaseCtl framework and is hidden from the developer. Some of the registration information is part of the DEFINE_CONTROLOBJECT structure (see Listing 10.7), which you looked at earlier in this chapter. See the BaseCtl documentation about the specific information that can be changed.

Listing 10.7 BCFCONTROLCTL.H--DEFINE_CONTROLOBJECT Structure

// TODO: if you have an array of verbs, then add an extern here with the name


// of it, so that you can include it in the DEFINE_CONTROLOBJECT.
// ie. extern VERBINFO m_BCFControlCustomVerbs [];
//
extern const GUID *rgBCFControlPropPages [];
DEFINE_CONTROLOBJECT(BCFControl,
&CLSID_BCFControl,
"BCFControlCtl",
CBCFControlControl::Create,
1,
&IID_IBCFControl,
"BCFControl.HLP",
&DIID_DBCFControlEvents,
OLEMISC_SETCLIENTSITEFIRST |
OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_RECOMPOSEONRESIZE |
OLEMISC_CANTLINKINSIDE | OLEMISC_INSIDEOUT,
0, // no IPointerInactive policy by default
RESID_TOOLBOX_BITMAP1,
"BCFControlWndClass",
1,
rgBCFControlPropPages,
0,
NULL);

NOTE: The BaseCtl basic project does not by default contain support for registering the control automatically when the project is compiled. You must ensure that the control is properly registered before using it, especially if you recently switched between debug and release versions.



You can now compile and register the control you've created, but it won't be of much use because it doesn't contain any methods, properties, or events, which are the basis of every ActiveX control.

Creating Methods

Now that you've successfully created your ActiveX control project, you can add a method, which is one of the basic aspects of component development.

For the purposes of the sample control, you add a method called CaptionMethod. The method accepts two parameters, the second being optional. The first parameter is a string that the control displays within its client area, and the second, optional parameter is the alignment of the caption within the client area, either left, right, or center.

When creating methods, properties, and events for your control, you always start with the ODL file. When the ODL file is compiled, a C++ header file is created that defines all of the interfaces and methods of the control. From the header file, you cut and paste the method, property, and event prototypes into your control implementation. The one thing about your BaseCtl (and ATL) implementation that is different from your MFC implementation is that your control supports dual-interfaces by default. All methods, properties, and events must be written using dual-interface syntax rules, which you get into a little later in this chapter.

First you need to add an entry to the Dispids.h file. Dispids are the constant values used by the IDispatch routines to locate the correct method or property being invoked through the IDispatch interface. Dispids can be any unique number including negative numbers. Be careful when using negative values, however, since the system-defined dispids are all negative. You are better off staying with only positive values. Listing 10.8 shows the dispid entry that you added for your CaptionMethod.

Listing 10.8 DISPIDS.H--Dispid for CaptionMethod

#define dispidCaptionMethod 2

Next you need to add the new method to your ODL file. From the Project Workspace window, select the File View, and open the BCFControl.odl file. Listing 10.9 shows the method that you added to the BCFControl's primary dispatch interface.

The dispidCaptionMethod constant is used for the id of the method, the return type is HRESULT--because it's dual-interface, and the name is CaptionMethod. The parameters are de-fined as a BSTR, named lpctstrCaption, and a VARIANT, named varAlignment. Your last parameter is actually your return value and is defined as a long pointer, named RetVal. You also added the direction that the parameters flow in the form of [in] and [out] parameter attributes. (See Table 10.2 for a complete description of the possible attributes that can be used.)

Parameter attributes are used to aid development tools, such as VB, in determining how parameters are used within a function call. A tool like VB will hide the details of how parameters are handled--for example, creating and destroying memory--based on the parameter direction attributes and other attributes in the type library. The type library is the description of your component to the tools that will use it and is why it is so important to ActiveX component development.
Table 10.2Parameter Flow Attributes
Direction Description
in Parameter is passed from caller to callee.
out Parameter is returned from callee to caller.
in, out Parameter is passed from caller to callee, and the callee returns a parameter.
out, retval Parameter is the return value of the method and is returned from callee to the caller.

Next compile the type library to create a new C++ header file defining your control and its interfaces. After compiling, open the BCFControlInterfaces.h file, and copy the CaptionMethod line.

Listing 10.9 BCFCONTROLINTERFACES.H--Interface File Created from the ODL File

interface DECLSPEC_UUID("317512F1-3E75-11d0-BEBE-00400538977D")
IBCFControl : public IDispatch
{
public:
virtual /* [id] */ HRESULT STDMETHODCALLTYPE CaptionMethod(
/* [in] */ BSTR bstrCaption,
/* [optional][in] */ VARIANT varAlignment,
/* [retval][out] */ long __RPC_FAR *lRetVal) = 0;

};

Open the BCFControlCtl.h file, and paste the line into your class header file, making sure that you remove the = 0 from the prototype. To aid your implementation, you also need to add an enumeration and two member variables to your class definition (see Listing 10.10).

Listing 10.10 BCFCONTROLCTL.H--Updated Control Class Header File

...
#include "BCFControlInterfaces.H"
#include "Dispids.H"
#include "alignmentenums.h"
typedef struct tagBCFCONTROLCTLSTATE
{
long lCaptionLength;
long lAlignment;
} BCFCONTROLCTLSTATE;
//=--------------------------------------------------------------------------=
// CBCFControlControl
//=--------------------------------------------------------------------------=
// our control.
//
class CBCFControlControl : public COleControl, public IBCFControl,
public ISupportErrorInfo
{
public:
// IUnknown methods
//
DECLARE_STANDARD_UNKNOWN();
// IDispatch methods
//
DECLARE_STANDARD_DISPATCH();
// ISupportErrorInfo methods
//
DECLARE_STANDARD_SUPPORTERRORINFO();
// IBCFControl methods
//
// TODO: copy over the method declarations from BCFControlInterfaces.H
// don't forget to remove the PURE from them.
//
STDMETHOD(CaptionMethod)(THIS_ BSTR bstrCaption, VARIANT varAlignment,
long FAR* lRetVal);
STDMETHOD_(void, AboutBox)(THIS);
// OLE Control stuff follows:
//
CBCFControlControl(IUnknown *pUnkOuter);
virtual ~CBCFControlControl();
// static creation function. all controls must have one of these!
//
static IUnknown *Create(IUnknown *);
private:
// overridables that the control must implement.
//
STDMETHOD(LoadBinaryState)(IStream *pStream);
STDMETHOD(SaveBinaryState)(IStream *pStream);
STDMETHOD(LoadTextState)(IPropertyBag *pPropertyBag, IErrorLog *pErrorLog);
STDMETHOD(SaveTextState)(IPropertyBag *pPropertyBag, BOOL fWriteDefault);
STDMETHOD(OnDraw)(DWORD dvAspect, HDC hdcDraw, LPCRECTL prcBounds,
LPCRECTL prcWBounds, HDC hicTargetDev, BOOL fOptimize);
virtual LRESULT WindowProc(UINT msg, WPARAM wParam, LPARAM lParam);
virtual BOOL RegisterClassData(void);
virtual HRESULT InternalQueryInterface(REFIID, void **);
virtual BOOL BeforeCreateWindow(DWORD *pdwWindowStyle,
DWORD *pdwExWindowStyle, LPSTR pszWindowTitle);
// private state information.
//
BCFCONTROLCTLSTATE m_state;
BCFCONTROLCTLSTATE m_DefaultState;
protected:
// storage variable for the caption
LPTSTR m_lptstrCaption;
};
...

The enumeration, which is in the include file Alignmentenums.h, is your list of supported alignment styles (see Listing 10.11).

Listing 10.11 ALIGNMENTENUMS.H--Alignment Styles Enumeration

typedef enum tagAlignmentEnum
{
EALIGN_LEFT = 0,
EALIGN_CENTER = 1,
EALIGN_RIGHT = 2,
}EALIGNMENT;
#define EALIGN_LEFT_TEXT "Left"
#define EALIGN_CENTER_TEXT "Center"
#define EALIGN_RIGHT_TEXT "Right"

You added the lAlignment member variable into the BCFCONTROLCTLSTATE structure, which is a member variable declared as m_state, in your control class. Define the m_lptstrCaption variable as a member of the control class rather than a member of the state structure--for persistence reasons; for now, suffice it to say that you will make your life a lot easier by declaring your member variables this way. You will address persistence and the use of the state structures in more detail a little later in this chapter in the section "Persistence." Also, declare the member variable, lCaptionLength, which is related to the m_lptstrCaption variable, which is needed for persistence reasons; for now, however, it will serve no purpose.

In addition to the m_state variable, you need to add another member of the same type called m_DefaultState. This structure is initialized in the constructor to all of the default values that your control supports (see Listing 10.12). These values will be used later in your persistence implementation to determine whether the properties have changed from their default values, and if not, the properties will not be persisted. See the section entitled "Persistence" for more information.

Listing 10.12 BCFCONTROLCTL.CPP--Member Variable Initialization

//=--------------------------------------------------------------------------=
// CBCFControlControl::CBCFControlControl
//=--------------------------------------------------------------------------=
// "Being born is like being kidnapped. And then sold into slavery."
// - andy warhol (1928 - 87)
//
// Parameters:
// IUnknown * - [in]
//
// Notes:
//
#pragma warning(disable:4355) // using `this' in constructor
CBCFControlControl::CBCFControlControl
(
IUnknown *pUnkOuter
)
: COleControl(pUnkOuter, OBJECT_TYPE_CTLBCFCONTROL, (IDispatch *)this)
{
// initialize anything here ...
//
memset(&m_state, 0, sizeof(BCFCONTROLCTLSTATE));
memset(&m_DefaultState, 0, sizeof(BCFCONTROLCTLSTATE));
// NULL terminate the string reference
m_lptstrCaption = new TCHAR[1];
m_lptstrCaption[0] = `\0';
// set the alignment to the default of left
m_DefaultState.lAlignment = m_state.lAlignment = EALIGN_LEFT;
}
#pragma warning(default:4355) // using `this' in constructor

NOTE: If you like interesting quotations, you will find that the BaseCtl code is sprinkled with them in appropriate places, as can be seen in Listing 10.12.



The CaptionMethod contains all of the code for getting the caption and the alignment style and, like your MFC and ATL implementations, deals correctly with the optional parameter.

Listing 10.13 shows the implementation of the CaptionMethod. Since the method is used both for your IDispatch implementation and your custom interface, it is implemented slightly different from its MFC counterpart. First you declare an HRESULT return type. This return value is used by OLE to determine whether the method call succeeded. The string parameter is passed in differently also. All strings are passed as UNICODE in OLE. This is true even for MFC; the only difference is that MFC hides from the developer the implementation details of how the strings are managed; the developer simply uses the appropriate string data type based on the target application and platform, that is, Win32 ANSI versus Win32 UNICODE.

Listing 10.13 BCFCONTROLCTL.CPP--CaptionMethod Implementation

STDMETHODIMP CBCFControlControl::CaptionMethod(BSTR bstrCaption,
VARIANT varAlignment, long FAR * lRetVal)
{
HRESULT hResult = S_OK;
// return value initialized to failure result
*lRetVal = FALSE;
MAKE_ANSIPTR_FROMWIDE(lpctstrCaption, bstrCaption);
// if the variant is a long just use the value
if(VT_I4 == varAlignment.vt)
{
// assign the value to our member variable
m_state.lAlignment = varAlignment.lVal;
// set the return value
*lRetVal = TRUE;
}
// if the user didn't supply an alignment parameter we will assign the default
else if(VT_ERROR == varAlignment.vt || VT_EMPTY == varAlignment.vt)
{
// assign the value to our member variable
m_state.lAlignment = EALIGN_LEFT;
// set the return value
*lRetVal = TRUE;
}
else
{
// get a variant that we can use for conversion purposes
VARIANT varConvertedValue;
// initialize the variant
::VariantInit(&varConvertedValue);
// see if we can convert the data type to something useful
// VariantChangeTypeEx() could also be used
if(S_OK == ::VariantChangeType(&varConvertedValue,
(VARIANT *) &varAlignment, 0, VT_I4))
{
// assign the value to our member variable
switch(varConvertedValue.lVal)
{
case EALIGN_CENTER:
m_state.lAlignment = EALIGN_CENTER;
break;
case EALIGN_RIGHT:
m_state.lAlignment = EALIGN_RIGHT;
break;
default:
m_state.lAlignment = EALIGN_LEFT;
break;
}
// set the return value
*lRetVal = TRUE;
}
else
{
// at this point we could either throw an error
// indicating there was a problem converting
// the data or change the return type of the method
// and return the HRESULT value from the
// the "VariantChangeType" call.
}
}
// if everything was OK
if(TRUE == *lRetVal)
{
// if we have a string
if(lpctstrCaption != NULL)
{
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lpctstrCaption) + 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lpctstrCaption);
}

// did they pass us bad data?
if(m_state.lAlignment < EALIGN_LEFT || m_state.lAlignment > EALIGN_RIGHT)
// sure did, lets fix their little red wagon
m_state.lAlignment = EALIGN_LEFT;
// force the control to repaint itself
this->InvalidateControl(NULL);
// this->InvalidateControl(); <== MFC Version
}
// return the result of the function call
return hResult;
}

Use the BaseCtl utility macro MAKE_ANSIPTR_FROMWIDE to convert the BSTR to an ANSI string. The file UTIL.H contains a number of helper functions and macros to aid in your control development. This file, along with the BaseCtl header files for the control and automation object implementations, deserves to be reviewed just to note the differences between the standard MFC implementation and the BaseCtl implementation for ActiveX controls. In addition, since documentation is nonexistent, reviewing the header files will give you a feel for the types of functions and functionality that are available to you for doing BaseCtl development.

NOTE: A number of wrapper functions are defined in the COleControl header file for performing other common functions, such as invalidating a rectangle. The wrapper functions allow the control to be created as either windowed or windowless, without requiring separate code implementations to deal with each unique state. When writing your control, do not use the control's window handle directly. Cases like these are why the BaseCtl code should be reviewed carefully prior to making large implementation investments. And due to the lack of documentation, the little differences are also why a BaseCtl implementation can be a little more difficult.



Aside from the string conversion, the CaptionMethod varies little from its MFC and ATL counterpart.

One thing to note though is the use of the method InvalidateControl to force the control's UI to redraw. This is a very good example of the kind of differences between BaseCtl and MFC in terms of certain basic functionality. In this case, the function InvalidateControl requires that a parameter be passed, thus differing from the MFC implementation, which has a default parameter of NULL. All differences of this kind are pointed out in the source code.

Creating Properties

Properties can be categorized as user defined, stock, or ambient.

User defined properties are properties that are implementation- specific and have meaning only to the component that contains them. User defined properties can be further broken into those properties that are defined only as their specific data type (normal properties) and those with additional parameters (parameterized properties).

Stock properties are a set of properties that are already defined by OLE in terms of the basic meaning. Stock properties are not implemented in the control of the container by default. They still require implementation by the control developer. They are predefined only to imply a certain level of uniformity between various control implementations.

Ambient properties, on the other hand, are properties that are supported by the container to provide a default value to the control that uses them.

In the remainder of this chapter, you will create all three types of properties: normal, parameterized, and stock. You will also learn how to use ambient properties.

Creating Normal User Defined Properties

A normal property is a property that is declared as a single type, for example, long or BSTR, and has no parameters. You will expose your control's Alignment member variable through a property.

Properties are added in much the same way as methods. First add the dispid for the property to the Dispids.h file.

#define dispidAlignment 1

Next add the property declaration to the ODL file (see Listing 10.14). The property declaration is really a pair of methods sharing the same dispid. The propget and propput attributes denote the direction with which data is moved to and from the property. The propget method contains a single parameter defined as the return value of the method, using the [out, retval] attributes. The propput method correspondingly accepts a single parameter of the same type.

Listing 10.14 BCFCONTROL.ODL--Normal User Defined Property ODL Declaration

// primary dispatch interface for CBCFControl control
//
[uuid(317512F1-3E75-11d0-BEBE-00400538977D), helpstring("BCFControl Control"),
hidden, dual, odl]
interface IBCFControl : IDispatch
{
// properties
[id(dispidAlignment), propget] HRESULT Alignment([out, retval] long * Value);
[id(dispidAlignment), propput] HRESULT Alignment([in] long Value);
// methods
[id(dispidCaptionMethod)] HRESULT CaptionMethod([in] BSTR bstrCaption,
[in, optional] VARIANT varAlignment, [out, retval] long *lRetVal);
[id(DISPID_ABOUTBOX)] void AboutBox(void);
};

As with your method implementation, you need to compile the ODL file in order to create the C++ interface prototypes that you add to your class definition. After the ODL is compiled, copy the two new function prototypes from the ODL-generated header file to the BCFControl.h header file (see Listing 10.15).

Listing 10.15 BCFCONTROLCTL.H--Property Function Prototypes

// IBCFControl methods
//
STDMETHOD(get_Alignment)(THIS_ long FAR* Value);
STDMETHOD(put_Alignment)(THIS_ long Value);
STDMETHOD(CaptionMethod)(THIS_ BSTR bstrCaption, VARIANT varAlignment,
long FAR* lRetVal);
STDMETHOD_(void, AboutBox)(THIS);

Don't forget to remove the = 0 from the function prototypes when you copy them over.

As you can see, Listing 10.16 takes advantage of the member variable m_state.lAlignment, which you added earlier, and uses it to get and set the property value.

The get_Alignment function returns only the value stored in the m_state.lAlignment member variable.

The put_Alignment function does a little more. This function checks to see if the value is within the valid ranges of values and, if so, stores the value in the m_state.lAlignment member variable. The function then sets the m_fDirty flag and calls PropertyChanged functions (note the MFC equivalent functions still in the code) to notify the control and the container, respectively, that the value of the property has changed. PropertyChanged has the effect of forcing the container to refresh its property browser to reflect the new value. This effect is very important because the value of the property could change without the container's knowledge, either through the control's property sheet or, in some cases, in response to another function call.

You might be asking, "Why didn't I add PropertyChanged to the CaptionMethod?" Well, you could have, but it wouldn't do much because the CaptionMethod can never be executed while the control is in design mode, which is the purpose of PropertyChanged. The PropertyChanged function is where you make use of the dispid constants that were defined earlier. The last thing the put_Alignment method does is invalidate the control's UI so it will repaint using the new information.

Listing 10.16 BCFCONTROLCTL.CPP--Property Function Implementation

STDMETHODIMP CBCFControlControl::get_Alignment(long FAR* Value)
{
HRESULT hResult = S_OK;
// return our current setting
*Value = m_state.lAlignment;
return hResult;
}
STDMETHODIMP CBCFControlControl::put_Alignment(long Value)
{
HRESULT hResult = S_OK;
// if we are in the valid range for the property
if(Value >= EALIGN_LEFT && Value <= EALIGN_RIGHT)
{
// set the new property value
m_state.lAlignment = Value;
// let the control know that the property has changed
m_fDirty = TRUE;
// this->SetModifiedFlag(); <== MFC version
// refresh the property browser
this->PropertyChanged(dispidAlignment);
// this->BoundPropertyChanged(dispidAlignment); <== MFC Version
}
return hResult;
}

The implementation simply sets or returns the value in your member variable m_state.lAlignment. Again, note the differences between your BaseCtl and your MFC implementation.

Creating Parameterized User Defined Properties

A parameterized property is a property that, besides being of a specific type (for example, BSTR or long), accepts one or more additional parameters to further define the data of the property. Parameterized properties can be useful for properties that represent collections of data where the additional parameter is the index into the collection.

You expose your m_lptstrCaption member variable as a parameterized property in addition to your CaptionMethod function.

First add the new dispid for the dispidCaptionProp (see Listing 10.17).

Listing 10.17 DISPIDS.H--Dispid for CaptionProp Property

// properties & methods
//
#define dispidAlignment 1
#define dispidCaptionMethod 2
#define dispidCaptionProp 3
// events

Next you add your ODL definition (see Listing 10.18) and compile the type library to create the header file.

Listing 10.18 BCFCONTROL.ODL--ODL Entry for Caption Property

// properties
[id(dispidAlignment), propget] HRESULT Alignment([out, retval] long * Value);
[id(dispidAlignment), propput] HRESULT Alignment([in] long Value);
// methods
[id(dispidCaptionMethod)] HRESULT CaptionMethod([in] BSTR bstrCaption,
[in, optional] VARIANT varAlignment, [out, retval] long * lRetVal);
[id(dispidCaptionProp), propget] HRESULT CaptionProp(
[in, optional] VARIANT varAlignment, [out, retval] BSTR * bstrRetVal);
[id(dispidCaptionProp), propput] HRESULT CaptionProp(
[in] VARIANT varAlignment, [in] BSTR lpszNewValue);
[id(DISPID_ABOUTBOX)] void AboutBox(void);

Copy the new function prototypes from the header file, BCFControlInterfaces.h, and paste them into the control header file (see Listing 10.19).

Listing 10.19 BCFCONTROLCTL.H--CaptionProp Function Prototypes

// IBCFControl methods
//
STDMETHOD(get_Alignment)(THIS_ long FAR* Value);
STDMETHOD(put_Alignment)(THIS_ long Value);
STDMETHOD(CaptionMethod)(THIS_ BSTR bstrCaption, VARIANT varAlignment,
long FAR* lRetVal);
STDMETHOD(get_CaptionProp)(THIS_ VARIANT varAlignment, BSTR FAR* bstrRetVal);
STDMETHOD(put_CaptionProp)(THIS_ VARIANT varAlignment, BSTR lpszNewValue);
STDMETHOD_(void, AboutBox)(THIS);

Finally you add the implementation of the CaptionProp function pair (see Listing 10.20). The method get_CaptionProp is called to return data from the property. In your implementation, you ignore the alignment parameter because it is of no use to you in this context; you simply return the caption. You need to make sure that the string variable, BSTR FAR * bstrRetVal, which is passed to the get_CaptionProp function does not already point to another string. If it does, you need to destroy it. Next get_CaptionProp uses the function SysAllocString to create a BSTR that is returned from the function call. Note that it is first necessary to convert the ANSI string to an OLECHAR string, with the OLESTRFROMANSI macro, and then allocate a BSTR from that.

put_CaptionProp defers to the CaptionMethod implementation because the CaptionMethod already does everything that you need.

Listing 10.20 BCFCONTROLCTL.CPP--CaptionProp Property Implementation

STDMETHODIMP CBCFControlControl::get_CaptionProp(VARIANT varAlignment,
BSTR FAR* bstrRetVal)
{
// if there is a string
if(*bstrRetVal);
{
// free the string because we are going to replace it
::SysFreeString(*bstrRetVal);
// clear the reference just to be safe
*bstrRetVal = NULL;
}
// return the caption as a BSTR
*bstrRetVal = ::SysAllocString(OLESTRFROMANSI(m_lptstrCaption));
return S_OK;
}
STDMETHODIMP CBCFControlControl::put_CaptionProp(VARIANT varAlignment,
BSTR lpszNewValue)
{
long lRetVal;
HRESULT hResult = this->CaptionMethod(lpszNewValue, varAlignment, &lRetVal);
// use the "CaptionMethod" implementation
if(hResult == S_OK && lRetVal)
// let the container know that the property has changed
m_fDirty = TRUE;
// this->SetModifiedFlag(); <== MFC version
return hResult;
}

Creating Stock Properties

A stock property is a property that is understood by both a control and its container and that has a predefined meaning to both. Stock properties are intended to provide basic uniform functionality to all the controls and containers that implement them. Stock properties do not require you to implement a lot of code; you just hook into the existing property.

You are going to support the stock property BackColor. For stock properties, you do not need to add dispids; rather, you take advantage of the constants defined by OLE. A complete list of system-defined dispids can be found in OLECTL.H, which is one of the files included with VC++. Listing 10.21 shows your ODL implementation of the BackColor property.

Listing 10.21 BCFCONTROL.ODL--BackColor Stock Property Support

// properties
[id(dispidAlignment), propget] HRESULT Alignment(
[out, retval] long * Value);
[id(dispidAlignment), propput] HRESULT Alignment([in] long Value);
[id(DISPID_BACKCOLOR), propget] HRESULT BackColor(
[out, retval] OLE_COLOR * Value);
[id(DISPID_BACKCOLOR), propput] HRESULT BackColor([in] OLE_COLOR Value);
// methods

Listing 10.22 shows the function prototype that was generated from the type library and added to your control header file.

Listing 10.22 BCFCONTROLCTL.H--BackColor Function Prototype

// IBCFControl methods
//
STDMETHOD(get_Alignment)(THIS_ long FAR* Value);
STDMETHOD(put_Alignment)(THIS_ long Value);
STDMETHOD(get_BackColor)(THIS_ OLE_COLOR FAR* Value);
STDMETHOD(put_BackColor)(THIS_ OLE_COLOR Value);
STDMETHOD(CaptionMethod)(THIS_ BSTR bstrCaption, VARIANT varAlignment,
long FAR* lRetVal);
STDMETHOD(get_CaptionProp)(THIS_ VARIANT varAlignment, BSTR FAR* bstrRetVal);
STDMETHOD(put_CaptionProp)(THIS_ VARIANT varAlignment, BSTR lpszNewValue);
STDMETHOD_(void, AboutBox)(THIS);

You also need to add a member variable to your control to store the value of the BackColor. You add the member to the BCFCONTROLCTLSTATE structure, as in Listing 10.23.

Listing 10.23 BCFCONTROLCTL.H--BackColor Member Variable

typedef struct tagBCFCONTROLCTLSTATE
{
long lCaptionLength;
long lAlignment;
OLE_COLOR ocBackColor;
} BCFCONTROLCTLSTATE;

Also, do not forget to add the member variable initialization to your constructor (see Listing 10.24).

Listing 10.24 BCFCONTROLCTL.CPP--Member Initialization

#pragma warning(disable:4355) // using `this' in constructor
CBCFControlControl::CBCFControlControl
(
IUnknown *pUnkOuter
)
: CInternetControl(pUnkOuter, OBJECT_TYPE_CTLBCFCONTROL, (IDispatch *)this)
{
// initialize anything here ...
//
memset(&m_state, 0, sizeof(BCFCONTROLCTLSTATE));
memset(&m_DefaultState, 0, sizeof(BCFCONTROLCTLSTATE));
// NULL terminate the string reference
m_lptstrCaption = new TCHAR[1];
m_lptstrCaption[0] = `\0';
// set the alignment to the default of left
m_DefaultState.lAlignment = m_state.lAlignment = EALIGN_LEFT;
// set the backcolor to the system default
m_DefaultState.ocBackColor = m_state.ocBackColor = 0x80000000 | COLOR_WINDOW;
. . .

Finally you must update the get_BackColor/put_BackColor functions to return and store the BackColor property (see Listing 10.25).

Listing 10.25 BCFCONTROLCTL.CPP--BackColor Property Implementation

STDMETHODIMP CBCFControlControl::get_BackColor(OLE_COLOR * Value)
{
// make sure that we have a good pointer
' CHECK_POINTER(Value);
// set the return value
*Value = m_state.ocBackColor;
// return the result
return S_OK;
}
STDMETHODIMP CBCFControlControl::put_BackColor(OLE_COLOR Value)
{
// if the value is the same
if (m_state.ocBackColor == Value)
// exit
return S_OK;
// save the value
m_state.ocBackColor = Value;
// let the container know that the value has changed
this->PropertyChanged(DISPID_BACKCOLOR);
// this->AmbientPropertyChanged(DISPID_BACKCOLOR); <== MFC Version
// redraw the control
this->InvalidateControl(NULL);
// this->InvalidateControl(); <== MFC Version
// set the dirty flag
m_fDirty = TRUE;
// this->SetModifiedFlag(); <== MFC Version
// exit
return S_OK;
}

The last type of property that you are going to look at is Ambient.

Using Ambient Properties

Ambient properties are properties implemented in the container in which the control resides, as opposed to stock properties, which are implemented in the control and not the container. Ambient properties share the same set of predefined meanings and dispids as those of stock properties. To use an ambient property, the control need request only the property value from the container and apply it in whatever manner is appropriate for the property type. The use of ambient properties allows the control to conform to the same settings as those of the container in which it resides. Using ambient properties provides much better integration between the control and its container.

The BaseCtl framework defines these two methods for retrieving Ambient properties:

BOOL GetAmbientProperty(DISPID, VARTYPE, void *);
BOOL GetAmbientFont(IFont **ppFontOut);

GetAmbientProperty is a general method for retrieving properties based on the dispid supplied. The function will not perform any type of dispid validation when the call is made, so theoretically any dispid can be passed. When supporting nonstock properties, use this routine to check the return value to guarantee that it executed successfully. Also, remember that not all containers support the same set of properties, so as a general rule, the return value should be checked.

GetAmbientFont is a specialized form of the same routine that retrieves the interface pointer to an IFont object. The font object can then be selected into the control and used for the control's drawing operations.

Once the property value is retrieved, it can be used any way the developer sees fit.

Creating Property Sheets

Property sheets are a way for a control to display its properties for review and editing using a tabbed dialog format. The original intent of property sheets was for cases when the control container did not support property browsing facilities. While property sheets have their purpose, they probably are not necessary for all implementations. Your specific requirements will determine whether your control should contain a property sheet. The official OLE line is that all controls should have property sheets, which though correct for commercially developed and distributed controls, is probably not the case for in-house implementations. The majority of development environments already have excellent property browsing facilities. Implement property sheets only if you feel that you absolutely have to.

Removing the property sheets and their corresponding implementation infrastructure definitely reduces the size of your control and should not take away from its implementation in any way.

Since property sheets are tabbed dialogs, some of your work will be done with the dialog editor. Select the Resource View in the Project Workspace window. From the list of dialogs, select IDD_PROPPAGE_BCFCONTROLGENERAL, and double-click the entry to open the resource editor.

Using the resource editor, place a static text control and a combo box on the dialog. Select the label control on the form, and click the right mouse button. In the menu that appears, select the Properties menu item. On the General tab, set the ID of the control to IDC_ALIGNMENTLABEL, and set the Caption to Alignment; you see the control in Figure 10.3. Select the Styles tab, and set the Align Text property to Right. Close the dialog to save the information.

Again, using the mouse, select the combo box, use the right mouse to click the control, and in the menu that appears, select the Properties menu item. On the General tab, set the ID of the control to IDC_ALIGNMENTCOMBO. On the Styles tab, set the T_ype to Drop down, and uncheck the Sort check box. Close the dialog to save the information.

You have placed your two controls on the property sheets and successfully modified their properties. Now you need to add some code to complete the implementation. Close the resource editor, and open the file BCFControlPPG.cpp. Listing 10.26 shows your property page implementation. For developers who have done Windows programming since the "old days," before class libraries and C++, this code will look familiar. We're hopeful that the nightmares won't last too long.

What you have is a standard Windows callback implementation. The first message you look for is WM_INITDIALOG, which is where you set up the Combo Box control with the appropriate data. The next several messages are defined by the BaseCtl framework.

PPM_NEWOBJECTS is the message that is sent when the property page is first connected to the control. This message is where you get all of the property data from the control and set it into the controls on the property page.

PPM_APPLY is the message that is sent when the data in the property page needs to be transferred back to the control. Note the loop that notifies all of the objects that the data has been changed.

The last messages you look for are standard windows control notification messages that let the property page know the data in the control has changed. When you receive the correct messages, you set the dirty flag for the property page. If the dirty flag is set when the Apply button is pressed or if the property dialog is closed, the PPM_APPLY message will be sent to the dialog, thus causing your code that will update the control with the new property values to execute.

Listing 10.26 BCFCONTROLPPG.CPP--Property Page Implementation

//=--------------------------------------------------------------------------=
// CBCFControlGeneralPage::DialogProc
//=--------------------------------------------------------------------------=
// our dialog proc.
//
// Parameters:
// - see win32sdk docs on DialogProc
//
// Notes:
//
BOOL CBCFControlGeneralPage::DialogProc
(
HWND hwnd,
UINT msg,
WPARAM wParam,
LPARAM lParam
)
{
switch (msg)
{
case WM_INITDIALOG:
{
// get the window handle of the combobox control
HWND hTempWnd = ::GetDlgItem(hwnd, IDC_ALIGNMENTCOMBO);
// make sure that the control is empty
::SendMessage(hTempWnd, CB_RESETCONTENT, 0, 0);
// set the selection strings in the control - it is important that the control
// be unsorted since the entries index will relate to the property setting
::SendMessage(hTempWnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR) EALIGN_LEFT_TEXT);
::SendMessage(hTempWnd, CB_ADDSTRING, 0,
(LPARAM)(LPCTSTR) EALIGN_CENTER_TEXT);
::SendMessage(hTempWnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR) EALIGN_RIGHT_TEXT);
}
return TRUE;
// we've been given some new objects, so go and re-set up the dialog page.
case PPM_NEWOBJECTS:
{
HRESULT hr;
IBCFControl *pBCFControl;
IUnknown *pUnk;
DWORD dwDummy;
LONG lAlignment;
// get the IUnknown of the control
pUnk = FirstControl(&dwDummy);
// if we didn't get a pointer then exit
if (!pUnk) return FALSE;
// get the controls custom interface
hr = pUnk->QueryInterface(IID_IBCFControl, (void **)&pBCFControl);
// if it failed then exit
if (FAILED(hr)) return FALSE;
// get the alignment from the control
pBCFControl->get_Alignment(&lAlignment);
// set the alignment selection in the control
::SendMessage(::GetDlgItem(hwnd, IDC_ALIGNMENTCOMBO),
CB_SETCURSEL, lAlignment, 0);
// release the interface
pBCFControl->Release();
}
return TRUE;
case PPM_APPLY:
{
IBCFControl *pBCFControl;
IUnknown *pUnk;
HRESULT hr;
DWORD dwCookie;
// get all the controls we have to update.
//
for(pUnk = FirstControl(&dwCookie); pUnk; pUnk = NextControl(&dwCookie))
{
// QI for the controls custom interface
hr = pUnk->QueryInterface(IID_IBCFControl, (void **)&pBCFControl);
// if it failed then continue to the next "for" iteration
if (FAILED(hr)) continue;

// get the alignment selection in the dialog control
long lAlignment = ::SendMessage(
::GetDlgItem(hwnd, IDC_ALIGNMENTCOMBO), CB_GETCURSEL, 0,0);
// set the alignment in the control
pBCFControl->put_Alignment(lAlignment);
// release the interface pointer
pBCFControl->Release();
}
}
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_ALIGNMENTCOMBO:
if(HIWORD(wParam) == CBN_SELCHANGE)
this->MakeDirty();
}
break;
}

return FALSE;
}

Adding Events

Properties and methods are a way for a programmer to communicate with a control from within the container application. Events are a way for the control to communicate with the container. For ActiveX controls, events are nothing more than IDispatch interfaces that are implemented on the container side of the container/control relationship.

The mechanism that events are based on is known as a connection point. A connection point is simply a description of the type of interface that is required in order to communicate with the container. Connection points are not restricted to only IDispatch interfaces; rather, they can be of any COM interface that is understood by both components. For that matter, connection points/events are not restricted to only controls, they can be used in any COM implementation. Controls were simply the first to take advantage of them. For more information regarding connection points, refer to the documentation in the OLE online help or to Kraig Brockschmidt's Inside OLE, Second Edition, from Microsoft Press.

For the BaseCtl Framework, events are fairly easy to implement.

You add an event for notifying the user that the data in your control has changed. Your event will contain two parameters: a string and a long. The parameters are your caption and alignment property values and are passed by reference so that the programmer of the control can allow or disallow the change that may be taking place. This type of event may not be practical but is used to demonstrate the fact that events are nothing more than methods and have the same level of functionality and flexibility.

The first step is to add the dispid that you use to identify the event (see Listing 10.27).

Listing 10.27 DISPIDS.H--Change Event Dispid

...
#define dispidCaptionProp 3
// events
//
#define eventidChange 5
#define _DISPIDS_H_
#endif // _DISPIDS_H_

As with every other aspect of your control implementation, you must add an entry to the ODL file. Events are added in the same fashion as methods; however, they are added to the event dispatch interface in the ODL file (see Listing 10.28).

Note that the event interface differs slightly from your primary dispatch interface. The most obvious difference being that the interface is of type dispinterface and does not support dual-interface. For these reasons, your event methods are declared in C/C++ fashion and do not require the parameter attributes that your primary dispatch interface did.

NOTE: Event (or rather Source) interfaces cannot be dual-interface.

Listing 10.28 BCFCONTROL.ODL--Event ODL Entry

// event interface for CBCFControl controls ...
//
[
uuid(317512F2-3E75-11d0-BEBE-00400538977D),
helpstring("Event interface for BCFControl control"),
hidden
]
dispinterface DBCFControlEvents {
properties:
methods:
[id(eventidChange)] void Change(BSTR* cstrCaption, long* lAlignment);
};

When you implemented your methods and properties, you had to copy the function prototype from the C++ header file that was generated from the ODL file to your class definition. For events, the implementation works a little differently. The function for the event is actually located in the container, so instead of implementing the function, you need to create some utility structures for calling the event (see Listing 10.29).

You added an enumeration to identify the event that you want to fire. You also declared two arrays: one of VARTYPE and the other EVENTINFO.

The VARTYPE array is an array of variant data type constants that are passed to the FireEvent function. The array is used to identify and give the order of the parameters (if any) that need to be passed to the event method when it is called.

The EVENTINFO array is an array of structures. The EVENTINFO is defined by the BaseCtl framework and is used for declaring the event, the number of parameters it has, and the VARTYPE array of parameter types that it uses.

NOTE: When assigning the values to the enumeration, it is important that they correspond to the position in the EVENTINFO structure array of the actual event being referenced. The first event is at position 0 in the array, the second is at position 1 in the array, and so forth. The enumeration value is used in the event firing method to retrieve information about the event and how it is called.

Listing 10.29 BCFCONTROLCTL.H--Event Handling Structures--Header File

...
#include "alignmentenums.h"
typedef enum
{
BCFControlEvent_Change = 0,
} BCFCONTROLEVENTS;
VARTYPE rgPI4PBSTR [];
EVENTINFO m_rgBCFControlEvents[];
typedef struct tagBCFCONTROLCTLSTATE
{
...

You've added your header declarations; now you need to add your source file implementation (see Listing 10.30). Here you initialize the two arrays that you declare in your header file. The VARTYPE array is initialized with two elements: a BSTR pointer type and a long pointer type. These are the data types of the two parameters that are passed to the event method.

The EVENTINFO structure is initialized to a single element identifying the event ID, the number of parameters, and the parameter array descriptor.

Listing 10.30 BCFCONTROLCTL.CPP--Event Structures Implementation--Source File

...
//=--------------------------------------------------------------------------=
// all the events in this control
//
// TODO: add events here ...
//
VARTYPE rgPI4PBSTR [] = { VT_BSTR | VT_BYREF, VT_I4 | VT_BYREF };
EVENTINFO m_rgBCFControlEvents [] =
{
{ eventidChange, 2, rgPI4PBSTR }
};
...

Your event interface is now completely implemented. The final step is only a matter of adding the FireEvent method calls wherever appropriate in your control implementation. Before adding the FireEvent calls to your code, though, you add a simple helper function to aid your implementation. Since your implementation of the FireEvent method allows the user of the control to change the data that is passed to the event, you will find it easier to maintain the code by implementing the simple helper function, FireChange (with no parameters), that encapsulates the data management associated with the method and its parameters (see Listing 10.31).

Listing 10.31BCFCONTROLCTL.H--FireChange Event--Header File

...
// private state information.
//
BCFCONTROLCTLSTATE m_state;
protected:
void FireChange(void);
LPTSTR m_lptstrCaption;
};
...

Now add your implementation (see Listing 10.32). The BaseCtl framework defines the FireEvent function for calling event functions. The first parameter of the function is the EVENTINFO structure that you defined, and the remaining parameters are based on the parameters you declared for the event.

Listing 10.32 BCFCONTROLCTL.CPP--FireChange Event Implementation

void CBCFControlControl::FireChange(void)
{
// get a BSTR that can be passed via the event
BSTR bstrCaption = ::SysAllocString(OLESTRFROMANSI(m_lptstrCaption));
// fire the change event
this->FireEvent(&(m_rgBCFControlEvents[BCFControlEvent_Change]),
&bstrCaption, &m_state.lAlignment);
// create an ANSI string
MAKE_ANSIPTR_FROMWIDE(lpctstrCaption, bstrCaption);
// free the data that was passed back
::SysFreeString(bstrCaption);
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lpctstrCaption) + 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lpctstrCaption);
}

Now you add the event firing to your control, which is just a matter of adding the

this->FireChange();

function call wherever appropriate. In your case, you added it to your put_Alignment and CaptionMethod functions. Last you compile the control and test your implementation.

TIP: If any of the methods, properties, or events don't appear in your container after you have added them, remember to do the following:

Occasionally, the project will point to an incorrect version of the type library or the registry may point to the incorrect version of the control, which can be a little confusing when you're testing your control and it doesn't behave the way you expect.

Persistence

Persistence refers to the capability of a component to retain its state across execution lifetimes. In other words, regardless of the number of times that the control is started and stopped, it remembers that you changed its background color from white to mauve.

Persistence in a BaseCtl implementation is broken into two major parts consisting of a total of four aspects.

The first part is described as text persistence, that is the persistence of the properties to some form of permanent storage such as a file. The second part, known as binary persistence, is when the control is being used in some form of development environment and the state of the container is switching between a design-time mode and a runtime mode for testing purposes. In this case, the persistence is performed using a temporary storage device such as the computer's memory.

Text persistence of the control's properties are broken into two parts, also. The first is the function LoadTextState, which is called the first time the control is instantiated. Note that this function will not be called when switching from a design-time mode to a runtime mode, even though the control is re-created when switching between modes. The last function to be called is SaveTextState, which is called when the control instance is being terminated. For an application that is switching between design-time and runtime mode, this function will not be called, even though the control is destroyed and re-created.

Binary persistence also consists of two parts: LoadBinaryState and SaveBinaryState. SaveBinaryState is called when a control, in design-mode, is destroyed with the intent of switching to runtime mode. LoadBinaryState is called when the control is being loaded in design-time mode after being in runtime mode, and vice versa. The hierarchy of events looks like this:

Runtime mode only:

LoadText Data
... (some action)
SaveTextState

Design-time mode and runtime mode:

LoadTextState - Design Mode
...
SaveBinaryState (Container is changed to Run-Time mode)
LoadBinaryState (Container is changed to Run-Time mode)
...
LoadBinaryState (Container is changed to Design mode)
...
SaveTextState - Design Mode

First you implement your text persistence, and then you implement your binary persistence.

Text Persistence

Text persistence is the actual storage of your data to some form of data store, such as a file. For your implementation, you persist the caption, alignment, text data path, and background color.

LoadTextState
Listing 10.33 shows the implementation for your loading of the properties. The control simply reads the property from the stream and, if successful, loads the property into the control.

NOTE: Do not exit this function if the control was unable to retrieve a property. It may be that the property was not persisted in the first place and does not really represent an error.

Listing 10.33 BCFCONTROLCTL.CPP--LoadTextState Implementation

STDMETHODIMP CBCFControlControl::LoadTextState
(
IPropertyBag *pPropertyBag,
IErrorLog *pErrorLog
)
{
HRESULT hr;
VARIANT v;
::VariantInit(&v);
v.vt = VT_BSTR;
hr = pPropertyBag->Read(OLESTRFROMANSI("Caption"), &v, pErrorLog);
if(SUCCEEDED(hr))
{
// get a ANSI string from the BSTR
MAKE_ANSIPTR_FROMWIDE(lpctstrCaption, v.bstrVal);
// free the BSTR that was passed in
::SysFreeString(v.bstrVal);
// if we have a string
if(lpctstrCaption != NULL)
{
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lpctstrCaption) + 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lpctstrCaption);
}
}
::VariantInit(&v);
v.vt = VT_I4;
hr = pPropertyBag->Read(OLESTRFROMANSI("Alignment"), &v, pErrorLog);
if(SUCCEEDED(hr))
m_state.lAlignment = v.lVal;

::VariantInit(&v);
v.vt = VT_I4;
hr = pPropertyBag->Read(OLESTRFROMANSI("BackColor"), &v, pErrorLog);
if(SUCCEEDED(hr))
m_state.ocBackColor = v.lVal;
this->InvalidateControl(NULL);
this->m_fDirty = TRUE;
// always return S_OK
return S_OK;
}

SaveTextState Listing 10.34 shows your persistence to permanent storage implementation. Note that the values are persisted only if they have not changed from their default value or are required to by the container. The use of the default state structure definitely improves your overall control load and save times, which is critical to having a control that is useful.

Listing 10.34 BCFCONTROLCTL.CPP--SaveTextState Implementation

STDMETHODIMP CBCFControlControl::SaveTextState
(
IPropertyBag *pPropertyBag,
BOOL fWriteDefaults
)
{
HRESULT hr = S_OK;
VARIANT v;
if(lstrlen(m_lptstrCaption) || fWriteDefaults)
{
::VariantInit(&v);
v.vt = VT_BSTR;
v.bstrVal = ::SysAllocString(OLESTRFROMANSI(m_lptstrCaption));
if (!v.bstrVal) return E_OUTOFMEMORY;
hr = pPropertyBag->Write(OLESTRFROMANSI("Caption"), &v);
::SysFreeString(v.bstrVal);
RETURN_ON_FAILURE(hr);
}
if(m_DefaultState.lAlignment != m_state.lAlignment || fWriteDefaults)
{
::VariantInit(&v);
v.vt = VT_I4;
v.lVal = m_state.lAlignment;
hr = pPropertyBag->Write(OLESTRFROMANSI("Alignment"), &v);
RETURN_ON_FAILURE(hr);
}
if(m_DefaultState.ocBackColor != m_state.ocBackColor || fWriteDefaults)
{
::VariantInit(&v);
v.vt = VT_I4;
v.lVal = m_state.ocBackColor;
hr = pPropertyBag->Write(OLESTRFROMANSI("BackColor"), &v);
RETURN_ON_FAILURE(hr);
}
return hr;
}

You've implemented the persistence of the control's data for the start and end of its lifetime. Now you need to implement the persistence of the control when it transitions from a design-time mode to a runtime mode and back again.

Binary Persistence

Binary persistence is the streaming of the control's data to a temporary storage device, which is done for performance and storage reasons. Loading the data of the control in and out of the binary persistence is much faster than writing to and from the primary storage. In addition, until the container of the control is completely closed, the persistent data of the control should be considered transitive and volatile and should not be persisted to permanent storage. You probably don't want to save the properties of the control to a file every time you start or stop your application while in design mode.

Throughout the implementation of various other aspects of your control, you have been adding your data members to a structure called m_state. Now that structure becomes of real use.

SaveBinaryState
Listing 10.35, shows your implementation of the SaveBinaryState method. First you determine the length of the string m_lptstrCaption and store it in your state structure m_state. Next you persist the state structure in its entirety. Finally you persist the two strings. You do this in separate operations because the strings can be of variable length, and you do not want to restrict the user in any way by truncating the strings or enforcing a size limit.

Listing 10.35 BCFCONTROLCTL.CPP--SaveBinaryState Implementation

STDMETHODIMP CBCFControlControl::SaveBinaryState

(
IStream *pStream
)
{
HRESULT hr = S_OK;
// store the length of the string and the NULL
m_state.lCaptionLength = lstrlen(m_lptstrCaption) + 1;
// write the state of the data to the stream
hr = pStream->Write(&m_state, sizeof(m_state), NULL);
RETURN_ON_FAILURE(hr);
// write the string and the NULL
hr = pStream->Write(m_lptstrCaption, m_state. lCaptionLength, NULL);
RETURN_ON_FAILURE(hr);
return S_OK;
}

After you persist the data, all that remains is to read the data back in when the time is right.
LoadBinaryState
When the container changes its state from runtime mode to design-time mode (and vice versa), the control is destroyed and re-created. However, in the interests of performance, the control will read its persistence from a local stream set up by the container, rather than from the normal storage used by the container when persisting the properties across execution boundaries. LoadBinaryState receives an IStream pointer as its only parameter, from which you can read your persisted data. Listing 10.36 shows your implementation of the LoadBinaryState function.

First you read in your m_state structure, which allows you to determine the length of the two strings that were persisted with the SaveBinaryState function. After you read in your strings, you force the control to redraw itself to reflect the new information.

Listing 10.36 BCFCONTROLCTL.CPP--LoadBinaryState Implementation

STDMETHODIMP CBCFControlControl::LoadBinaryState
(
IStream *pStream
)
{
HRESULT hr = S_OK;
// read the state of the control
hr = pStream->Read(&m_state, sizeof(m_state), NULL);
// create a string of the appropriate size this includes the NULL
m_lptstrCaption = new TCHAR[m_state.lCaptionLength];
// read the string and NULL
hr = pStream->Read(m_lptstrCaption, m_state.lCaptionLength, NULL);
// redraw the control
this->InvalidateControl(NULL);
this->m_fDirty = TRUE;
return S_OK;
}

Although persistence support requires a little more work when using the BaseCtl framework, it is not too difficult. It is more tedious and time consuming than anything. The BaseCtl sample code contains examples of persisting other type of properties, including fonts, which differ slightly from other built-in data types, such as long or BSTR.

Drawing the Control

You draw the control's UI similarly to the way you draw it in your MFC and ATL implementations. For developers accustomed to MFC, the most difficult aspect of drawing the UI is the lack of utility class support. All those nice classes and functions that create brushes and convert colors, for example, are not available with the BaseCtl framework. Most MFC classes and functions have Win32 equivalents, so you shouldn't find it too difficult to convert between the two.

As we point out in Chapters 6 and 8, there are two types of drawing: standard and optimized. Your drawing implementation will support both methods. This chapter will describe standard drawing, and Chapter 11 will focus on optimized drawing.

Standard Drawing

Standard drawing is just that: standard. You have complete freedom to draw the control any way you see fit, using any method that is appropriate. You can use pens and brushes, rectangles and circles. Remember that drawing smart is the goal of any application with UI.

First you need to add a number of member variables and functions to aid in your drawing implementation (see Listing 10.37).

Listing 10.37 BCFCONTROLCTL.H--Drawing Implementation Member Variables and Functions

...
virtual HRESULT InternalQueryInterface(REFIID, void **);
virtual BOOL BeforeCreateWindow(DWORD *pdwWindowStyle,
DWORD *pdwExWindowStyle, LPSTR pszWindowTitle);
// OnData is called asynchronously as data for an object
// or property arrives...
virtual HRESULT OnData(DISPID propId, DWORD bscfFlag, IStream * strm,
DWORD dwSize);
// private state information.
//
BCFCONTROLCTLSTATE m_state;
BCFCONTROLCTLSTATE m_DefaultState;
protected:
void FireChange(void);
LPTSTR m_lptstrCaption;
IFont * m_pFont;
void LoadFont(void);
HBRUSH hBrush, hOldBrush;
COLORREF TranslateColor(OLE_COLOR clrColor, HPALETTE hpal = NULL){
COLORREF cr = RGB(0x00,0x00,0x00);OleTranslateColor(clrColor, hpal, &cr);return cr;}
void FillSolidRect(HDC hDC, LPCRECT lpRect, COLORREF clr){
::SetBkColor(hDC, clr);::ExtTextOut(hDC, 0, 0, ETO_OPAQUE, lpRect,
NULL, 0, NULL);}
void GetTextExtent(HDC hDC, LPCTSTR lpctstrString, int & cx, int & cy);
BOOL bRetrievedDimensions;
int iCharWidthArray[256];
int iCharacterSpacing, iCharacterHeight;
};
...

You need to initialize the member variables to a valid state, which you do in your constructor (see Listing 10.38).

Listing 10.38 BCFCONTROLCTL.CPP--Member Initialization

#pragma warning(disable:4355) // using `this' in constructor
CBCFControlControl::CBCFControlControl
(
IUnknown *pUnkOuter
)
: CInternetControl(pUnkOuter, OBJECT_TYPE_CTLBCFCONTROL, (IDispatch *)this)
{
// initialize anything here ...
//
. . .
// clear the font
m_pFont = NULL;
// clear the brush
hOldBrush = hBrush = NULL;
// clear the flag
bRetrievedDimensions = FALSE;
}
#pragma warning(default:4355) // using `this' in constructor

Next you add the helper functions, GetTextExtent and LoadFont, to your implementation (see Listing 10.39). You also add a default FONTDESC structure in the event that you can't retrieve the ambient font from the container.

GetTextExtent is a function that is supported in MFC but not in Win32, so we've created our own implementation. The function determines the width and height of the font of the current Device Context (DC) and then calculates the size in points of the string that was supplied to the function. This function is used for displaying the text with the correct alignment: left, right, or center. We've optimized the method so as to retrieve the information only once. If your control supports fonts for properties, it is a simple matter to clear the flag bRetrievedDimensions to refresh the width and height of the new font when the control redraws itself.

The function LoadFont tries to load the ambient font from the container and, if unable, creates a new font from your default settings.

Listing 10.39 BCFCONTROLCTL.CPP--Drawing Helper Functions

static FONTDESC _fdDefault =
{
sizeof(FONTDESC),
L"MS Sans Serif",
FONTSIZE(8),
FW_NORMAL,
DEFAULT_CHARSET,
FALSE,
FALSE,
FALSE
};
void CBCFControlControl::LoadFont(void)
{
// if there isn't a font object
if(!m_pFont)
// get the font from the container
this->GetAmbientFont(&m_pFont);
// if there still isn't a font object
if(!m_pFont)
// create a default font object
::OleCreateFontIndirect(&_fdDefault, IID_IFont, (void **) &m_pFont);
}
void CBCFControlControl::GetTextExtent(HDC hDC, LPCTSTR lpctstrString,
int & cx, int & cy)
{
// if we haven't gotten the dimensions yet
if(!bRetrievedDimensions)
{
// get all of the widths for all of the chars
::GetCharWidth(hDC, 0, 255, &iCharWidthArray[0]);
// get the spacing between the chars
iCharacterSpacing = ::GetTextCharacterExtra(hDC);
// make sure that this only executes once
bRetrievedDimensions = TRUE;
// get the metrics of this DC
TEXTMETRIC tmMetrics;
::GetTextMetrics(hDC, &tmMetrics);
// get the height
iCharacterHeight = tmMetrics.tmHeight;
}
// return the height
cy = iCharacterHeight;
// set the initial value to 0
int iTextWidth = 0;
// get the number of characters in our string
long lTextLength = lstrlen(lpctstrString);
// if we have a character
if(lTextLength)
{
long lEndCharPos = lTextLength - 1;
// add up the widths of the characters and the spacing
for(long lCount = 0; lCount <= lEndCharPos; lCount++)
iTextWidth += (iCharWidthArray[(BYTE)
(lpctstrString[lCount])] + iCharacterSpacing);
}
// return the width
cx = iTextWidth;
}

Next you add your OnDraw implementation (see Listing 10.40). First you try to load and select the font into the DC. Then you get the colors you draw with. For the background color, you rely on the stock property that you implemented in the section entitled "Creating Stock Properties." For the rest of the colors, you rely on the system definitions.

Next you draw the background of the control. The sample takes the brute force method here by drawing the entire background every time. A better implementation would be to draw in only the area between the border and the text to prevent unnecessary flicker when the control repaints itself.

After the background is drawn, you draw the text and place it within the control's UI, relative to the alignment property setting. And, finally, you draw a nice 3-D sunken border around the text.

Now comes the cleanup. You reset the colors and the font, and you select the original brush back into the DC and destroy the brush that you created.

Listing 10.40 BCFCONTROLCTL.CPP--OnDraw Implementation

HRESULT CBCFControlControl::OnDraw
(
DWORD dvAspect,
HDC hdcDraw,
LPCRECTL prcBounds,
LPCRECTL prcWBounds,
HDC hicTargetDevice,
BOOL fOptimize
)
{
// ****** Get the text font ******
// **
HFONT hFont = NULL, hOldFont = NULL;
// if there isn't a font object
if(!m_pFont)
// try to load one
this->LoadFont();
if(m_pFont)
{
// get a font handle
m_pFont->get_hFont(&hFont);
// increment the ref count so the font doesn't drop
// out from under us
m_pFont->AddRefHfont(hFont);
::SelectObject(hdcDraw, hFont);
}
// **
// ****** Get the text font ******
// ****** Get the colors ******
// **
// use the window color as the background color
OLE_COLOR tColor;
this->get_BackColor(&tColor);
COLORREF clrTextBackgroundColor = this->TranslateColor(tColor);
// then use the normal windows color for the text
COLORREF clrTextForegroundColor =
this->TranslateColor(::GetSysColor(COLOR_WINDOWTEXT));
// set to the system color
COLORREF clrEdgeBackgroundColor = ::GetSysColor(COLOR_3DFACE);
COLORREF clrEdgeForegroundColor = ::GetSysColor(COLOR_3DFACE);
// **
// ****** Get the colors ******
// ****** Draw the background ******
// **
// set the text color
COLORREF clrOldBackgroundColor = ::SetBkColor(hdcDraw,clrTextBackgroundColor);
COLORREF clrOldForegroundColor = ::SetTextColor(hdcDraw, clrTextForegroundColor);
// if we don't have a brush
if(hBrush == NULL)
// create a solid brush
hBrush = ::CreateSolidBrush(clrTextBackgroundColor);
// select the brush and save the old one
hOldBrush = ::SelectObject(hdcDraw, hBrush);
// draw the background
::Rectangle(hdcDraw, prcBounds->left, prcBounds->top,
prcBounds->right, prcBounds->bottom);
// **
// ****** Draw the background ******
// ****** Draw the text ******
// **
int iHor, iVer;
// get the size of the text for this DC
int cx = 0, cy = 0;
this->GetTextExtent(hdcDraw, m_lptstrCaption, cx, cy);
switch(m_state.lAlignment)
{
case EALIGN_CENTER:
iHor = (prcBounds->right - cx) / 2;
iVer = prcBounds->top + 3;
break;
case EALIGN_RIGHT:
iHor = prcBounds->right - cx - 3;
iVer = prcBounds->top + 3;
break;
// case EALIGN_LEFT:
default:
iHor = prcBounds->left + 3;
iVer = prcBounds->top + 3;
break;
}
// output our text
::ExtTextOut(hdcDraw, iHor, iVer, ETO_CLIPPED | ETO_OPAQUE,
(LPCRECT) prcBounds, m_lptstrCaption, lstrlen(m_lptstrCaption), NULL);
// **
// ****** Draw the text ******
// ****** Draw the border ******
// **
// set the edge style and flags
UINT uiBorderStyle = EDGE_SUNKEN;
UINT uiBorderFlags = BF_RECT;
// set the edge color
::SetBkColor(hdcDraw, clrEdgeBackgroundColor);
::SetTextColor(hdcDraw, clrEdgeForegroundColor);
// draw the 3D edge
::DrawEdge(hdcDraw, (LPRECT)(LPCRECT) prcBounds, uiBorderStyle, uiBorderFlags);
// **
// ****** Draw the border ******
// ****** Reset the colors ******
// **
// restore the original colors
::SetBkColor(hdcDraw, clrOldBackgroundColor);
::SetTextColor(hdcDraw, clrOldForegroundColor);
// **
// ****** Reset the colors ******
// ****** release the text font ******
// **
if(hOldFont)
// select the old object
::SelectObject(hdcDraw, hOldFont);
// increment the ref count so the font doesn't drop
// out from under us
if(m_pFont && hFont)
m_pFont->ReleaseHfont(hFont);
// **
// ****** Get the text font ******
// select the old brush back
::SelectObject(hdcDraw, hOldBrush);
// destroy the brush we created
::DeleteObject(hBrush);
// clear the brush handles
hBrush = hOldBrush = NULL;
return S_OK;
}

From Here...

This chapter focused on creating a basic control implementation. You added methods, properties, and events, which are the backbone of every control implementation. Adding these basic features to a BaseCtl ActiveX control implementation is fairly straightforward. The one major drawback, though, is the lack of IDE support such as you have with MFC and ATL.

This chapter also addressed the issues of persistence and drawing without which a control implementation is definitely incomplete. Chapter 11 expands upon what you have learned and adds new features and functions to your control implementation to make your control truly unique and interesting.

The BaseCtl framework is fairly complete and robust, and as you progress through Chapter 11, you will find that a lot of the new features you add will be implemented in a fashion similar to the features you implemented in this chapter. The one thing that BaseCtl has going for it is that it is all based on COM, which is the foundation of any ActiveX component.