VC++
MFC Tutorial: ActiveX
Control Example
|
|
|
Navin
Kohli
This article
is somewhat like an extension to POLYGON tutorial
provided in Microsoft’s online documentation. So I would
not be explaining the basic steps involved to create the
project and adding control to the project. I would only
explain the steps where I diverted form POLYGON tutorial.
To completely follow the example code of this article
we will have to do the following steps.
-
Name
your project as ActiveXCtl
-
Name the
control as ShapeCtl.
-
Since
our control is circular and we will fill it with
different kinds of fill patterns, we will add two
custom properties to control, Radius and FillPattern.
To do so follow the steps to add Sides property in
POLYGON tutorial.
-
POLYGON tutorial
has only one event added to control. Following the
same method we will add three methods to our control.
Right click on CShapeCtl class in class view and add
windows message handlers for WM_LBUTTONDOWN, WM_LBUTTONDBLCLK
and WM_MOUSEMOVE. As is clear from these
messages that our control
will fire events for click, double click and mouse
move on the control. The following functions will be
added to CShapeCtl.h file
LRESULT OnLButtonDblClk(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled);
LRESULT OnLButtonDown(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled);
LRESULT OnMouseMove(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled);
-
We will
see the implementation a little later.
-
In the
second step of POLYGON tutorial
where control is configured, from the Stock Properties
tab add all the stock properties to control.
It is not necessary that you have to add all of them.
Since for my learning experience I wanted to see how
all of these get implemented, I added all of these.
Therefore to look your code similar to the example
code, add all the stock properties.
-
Follow
step 6 of POLYGON tutorial to add property pages to
control. We will add two pages and name these as ShapeProp
and ApperanceProp. Add the controls to these
pages as show in the figure
Figure 1
Figure 2
-
We will
not add any MFC
support to our project because we won’t be using it.
Add the
necessary controls and corresponding variables to the
property page classes to manipulate the values
corresponding to these controls. Since at the start of
this article I told you that I didn’t add any MFC
support to the project, therefore we will not be making
use of CPropertPage or other MFC
classes for these property page. We will use Win32 API
calls to manipulate all the controls. I know it sucks
after using the MFC
for our applications. If you don’t want to go my way
then you add the MFC
support to this control
and make use of all the control
classes. Here is part of our code that initializes the
controls.
LRESULT CAppearanceProp::OnInitDialog(UINT uMsg, WPARAM wParam,
LPARAM lParam,
BOOL& bHandled)
{
USES_CONVERSION;
if ((m_hBorderWidthEdit = GetDlgItem (IDC_BORDERWIDTH_EDIT))
!= NULL)
{
::SendMessage (m_hBorderWidthEdit, UDM_SETRANGE, 0,
MAKELONG (10, 0));
}
if ((m_hBorderWidthSpin = GetDlgItem (IDC_BORDERWIDTH_SPIN))
!= NULL)
{
::SendMessage (m_hBorderWidthSpin, UDM_SETRANGE, 0,
MAKELONG (10, 0));
}
m_hAppearanceCombo = GetDlgItem (IDC_APPEARANCE_COMBO);
m_hFillStyleCombo = GetDlgItem (IDC_FILLSTYLE_COMBO);
m_hFillPatternCombo = GetDlgItem (IDC_PATTERN_COMBO);
m_hDrawStyleCombo = GetDlgItem (IDC_DRAWSTYLE_COMBO);
m_hEnableCheck = GetDlgItem (IDC_ENABLE_CHECK);
m_hBorderVisibleCheck = GetDlgItem (IDC_BORDERVISIBLE_CHECK);
m_hTabStopCheck = GetDlgItem (IDC_TABSTOP_CHECK);
.
.
All the
control variables have been defines as HWND.
Control
Implementation
Now we have
done enough work to prepare skeleton structure for our
control. Compile the project and test it by inserting it
in ActiveX Control Test
Container provided in the Tool menu of VC++
IDE. Ofcourse you will see nothing else but a rectangle
with the label "ATL 2.0". Now we will fill the
function bodies to draw the control the way we want it to
be. Before I start discussing these functions, here is one
thing that I did to the source code files. Object wizard
puts most of the function bodies in header file. To
separate the interface definitions and their
implementation I moved all the function implementations to
source files. Therefore if you see any difference between
your files and the example code files, this could be the
reason.
OnDraw ()
An OnDraw
method is automatically added to your control
class when you create your control with the ATL
Object Wizard. POLYGON tutorial has put code for drawing
simple polygon. We will add code to draw circular control,
but a fancy one. Our control implements circle filled with
fill patterns and you can change the font to one of your
choice. Here is part of code in OnDraw function
that fills the control with pattern you specify
switch (m_nFillPattern)
{
case fillBDiagonal:
hBrush = CreateHatchBrush (HS_BDIAGONAL, colorFill);
break;
case fillCrossHatch:
hBrush = CreateHatchBrush (HS_CROSS, colorFill);
break;
case fillDiagXHatch:
hBrush = CreateHatchBrush (HS_DIAGCROSS, colorFill);
break;
case fillFDiagonal:
hBrush = CreateHatchBrush (HS_FDIAGONAL, colorFill);
break;
case fillHorizontal:
hBrush = CreateHatchBrush (HS_HORIZONTAL, colorFill);
break;
case fillVertical:
hBrush = CreateHatchBrush (HS_VERTICAL, colorFill);
break;
}
The font
property is implemented through IFontDisp
interface. So we will make use of it to get, change and
release the font object handle.
CComQIPtr<IFont, &IID_IFont> pFont (m_pFont);
if (pFont != NULL)
{
pFont->get_hFont (&hFont);
pFont->AddRefHfont (hFont);
hOldFont = (HFONT)SelectObject (hdc, hFont);
}
And as a
good Win32 programming practice, do delete al the GDI
objects like HPEN, HBRUSH, HFONT,
etc. after these have been used. And don’t forget to
restore the font, pen and brush in the device context
before you exit the function.
if (pFont != NULL)
{
pFont->ReleaseHfont (hFont);
}
SelectObject (hdc, hOldFont);
SelectObject (hdc, hOldBrush);
DeleteObject (hBrush);
SelectObject (hdc, hOldPen);
DeleteObject (hPen);
Ambient
Properties Change
If you want
your control to respond to any change in the ambient
properties of the container that is hosting the control,
we will have to implement the OnAmbientPropertyChange
method of IOleControl
interface. Our control has already has IObjectControl
in the inheritance list. If its not, then add it to the
list of classes from which our control is derived from.
And add corresponding entry in the BEGIN_COM_MAP.
In the header file you need to add the following lines for
this
class ATL_NO_VTABLE CShapeCtl :
.
public IOleControlImpl<CShapeCtl>,
.
.
BEGIN_COM_MAP(CShapeCtl)
.
COM_INTERFACE_ENTRY(IOleControl)
.
Add the
following interface definition to CShapeCtl.h file.
STDMETHOD(OnAmbientPropertyChange)(DISPID dispid);
.
And in the
source file we will handle the property we want to respond
to. In the example code I have handled the change in
Background color.
STDMETHODIMP CShapeCtl::OnAmbientPropertyChange (DISPID dispid)
{
switch (dispid)
{
case DISPID_AMBIENT_BACKCOLOR:
if (SUCCEEDED (GetAmbientBackColor (m_clrBackColor)))
{
FireViewChange ();
}
break;
case DISPID_AMBIENT_DISPLAYNAME:
case DISPID_AMBIENT_FONT:
.
Control
Redraw
Whenever
some control property changes you would like to redraw the
control. Call FireViewChange method of CComControl
class. The following code shows how the control handles
the change in radius.
STDMETHODIMP CShapeCtl::put_Radius(short newVal)
{
if (newVal <= 0)
{
return (Error (_T ("Radius value can't be zero!")));
}
else if (newVal > RectHt || newVal > RectWd)
{
return (Error (_T ("Radius can't exceed control's extents!")));
}
m_nRadius = newVal;
FireViewChange ();
return S_OK;
}
Event
Firing
While
creating control we added three methods to handle mouse
button down and move events. For firing event for mouse
click we check the location where the click took place. If
its inside the control region, we fire ClickIn
event otherwise we fire ClickOut event.
LRESULT CShapeCtl::OnLButtonDown(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
HRGN rgn;
WORD xPos = LOWORD (lParam);
WORD yPos = HIWORD (lParam);
rgn = CreateEllipticRgn((CenterPt.x - m_nRadius),
(CenterPt.y - m_nRadius),
(CenterPt.x + m_nRadius),
(CenterPt.y + m_nRadius));
if (PtInRegion (rgn, xPos, yPos))
{
Fire_ClickIn (xPos, yPos);
}
else
{
Fire_ClickOut (xPos, yPos);
}
DeleteObject (rgn);
return 0;
}
Use CreateEllipticRgn
API call with the radius of control and then use PtInRegion
API call to check if the click was inside this region or
not. Don’t forget to delete the created region object.
Control
Persistence
Here is the
most important part of COM objects. What do we do with the
control we have created? How should I save the properties
of this control? How will the container, which uses this
control, reload the control in the state it was saved? And
you know this is the part that is not explained in most of
the examples published in books. And this was the part
which took me couple of weeks to get the things straight.
There are different types of persistence interfaces
provided in COM to accomplish this tucancode.net. But the
interfaces which are of main interest to ActiveX controls
are IPersistStream, IPersistStorage, and IPersistPropertyBag.
A container implements only one type of persistence
interface. But if you want to make your control useable by
any kind of container, implement each one of these
interfaces. Why do we have to implement each one on the
interface? And this was the same question I ucancode.neted myself
when I was experimenting with this control and trying to
insert it in different kinds on containers. Here are
couple of hints to decide which interface will be used by
the container.
-
VB 6.0
needs only IPersistStreamInit or IPersistStream.
And when VB6.0 saves the control in a form, it uses IPersistPropertyBag
interface to save and load the control.
-
IE 4.0
will only use IPersistProprtyBag to load the
control.
-
VisualInterDev
uses IPersistPropertyBag to save the state of
the control
-
MS
Office applications use IPersistStorage
interface to save and load the control
-
When you
insert the control in VC++ dialog box, it uses IPersistStreamInit
interface to save and load the Control State.
If you look
in the resource file of the Container App project, you
will see the following code
IDD_CONTAINERAPP_DIALOG DLGINIT
BEGIN
IDC_SHAPECTL, 0x376, 129, 0
0x0000, 0x0000, 0x0060, 0x8080, 0x0000, 0x0080, 0x0080, 0xcdcd,
0xcdcd, 0xcdcd, 0xcdcd, 0x00ff, 0x0000, 0xcdcd, 0xcdcd, 0xffff,
0xffff, 0xcdcd, 0xcdcd, 0x0000, 0x0000, 0x0008, 0x0000, 0xffff,
0xffff, 0x0002, 0x0000, 0x0018, 0x8000, 0xcdcd, 0xcdcd, 0x0002,
0x0000, 0x0022, 0x0000, 0x004e, 0x0061, 0x0076, 0x0065, 0x0065,
0x006e, 0x0027, 0x0073, 0x0020, 0x0043, 0x006f, 0x006e, 0x0074,
0x0072, 0x006f, 0x006c, 0x0000, 0x0001, 0x0600, 0x02bc, 0xd4c0,
0x0001, 0x530e, 0x7263, 0x7069, 0x2074, 0x544d, 0x4220, 0x6c6f,
"\144"
0
END
This is how IPersistStreamInit
interface saved the control sate. Next time you load the
dialog box, the Control State will be loaded from this
stream.
All the
persistence interfaces have same methods, but with
different argument types, to save and load the control
state. For implementation of these three persistence
interfaces, make sure that we have these in the
inheritance list and the PROP_MAP_ENTRY. If not,
then add these.
class ATL_NO_VTABLE CShapeCtl :
.
public IPersistStreamInitImpl<CShapeCtl>,
.
public IPersistStorageImpl<CShapeCtl>,
,
public IPersistPropertyBagImpl<CShapeCtl>,
.
In the
property map add following entries if not present.
BEGIN_COM_MAP(CShapeCtl)
.
COM_INTERFACE_ENTRY(IPersistStreamInit)
COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
.
COM_INTERFACE_ENTRY(IPersistPropertyBag)
.
COM_INTERFACE_ENTRY(IPersistStorage)
.
END_COM_MAP()
And add the
following interface definitions to CShapeCtl.h.
STDMETHOD(Load)(IStream *pStream);
STDMETHOD(Save)(IStream *pStream, BOOL fClearDirty);
STDMETHOD(GetSizeMax)(ULARGE_INTEGER* pcbSize);
STDMETHOD(Load)(IStorage *Storage);
STDMETHOD(Save)(IStorage *pStorage, BOOL fSameAsLoad);
STDMETHOD(Load)(IPropertyBag *pPropBag, IErrorLog* pErrorLog);
STDMETHOD(Save)(IPropertyBag *pPropBag, BOOL fClearDirty,
BOOL fSaveAllProperties);
Now we will
have to provide the interface implementation in
CShapeCtl.cpp file. I wouldn’t be going into details of
implementation of each interface. I have tried to explain
each and every detail of these interfaces in the
CShapeCtl.cpp file While implementing these interfaces
make sure that data is loaded in the same order in which
it was saved. And you use the same stream and storage
names. IPersistPropertyBag persistence has some
problems. The default ATL implementation persist data in
property bag if its of VARIANT type. So we will need some
method to save user-defined data types. Therefore the
simplest way would be to convert custom data types to one
of the types defined in VARIANT type. And then save it in
the property bag. And while loading reconvert it to user
defined data type. For more explanation see
"Professional ATL COM Programming", Wrox Press
Ltd, by Dr. Richard Grimes. The code used in the example
has been taken from this book.
void CShapeCtl::Insert (BSTR *pStream, void *pData, DWORD size)
{
CComBSTR bstr;
bstr.Attach (*pStream);
LPBYTE ptr = static_cast<LPBYTE>(pData);
TCHAR strTemp[3] = {0};
for (DWORD j = 0; j < size; ++j)
{
BYTE ch = *ptr;
strTemp[1] = (ch & 0xf) > 9 ?
(ch & 0xf);
ch = ch >> 4;
strTemp[0] = (ch & 0xf) > 9 ?
(ch & 0xf);
ptr++;
bstr += strTemp;
}
*pStream = bstr.Detach ();
}
void CShapeCtl::Extract (LPCWSTR *pStream, void *pData, DWORD size)
{
LPCWSTR str = *pStream;
LPBYTE ptr = static_cast<LPBYTE>(pData);
for (DWORD j = 0; j < size; ++j)
{
BYTE ch = (str[0] >= L
ch *= 16;
ch += (str[1] >= L
*ptr = ch;
ptr++;
str += 2;
}
*pStream = str;
}
This code
converts your data type to string type and save as VARIANT
and while reloading convert from string to user defined
type. For more explanation see the comments in the code.
Control
Category and Safety
If you
insert this control in a web page and load the page, a
warning message warning about unsafe ActiveX
control gets displayed. It only gets displayed
if you click OK. So we should mark our controls safe or
unsafe so that when o container wants to load it, it knows
about it. There are two ways of doing this. First way is
to include the following category map in your control. I
didn’t use this method. Instead I added the component
category creation code in ActiveXCtl.cpp. During the
registration of control, category gets created and
registered in the registry. And when control is
unregistered, this category is removed form the system
registry. This way you can define your own control
category types. Look in DllRegisterServer and DllUnregisterServer
function bodies for implementation.
BEGIN_CATEGORY_MAP (CshapeCtl)
IMPLEMENT_CATEGORY(CATID_SafeForScripting)
IMPLEMENT_CATEGORY(CATID_SafeForInitializing)
END_CATEGORY_MAP ()
Second way
is to Implement IObjectSafety interface. I have
implemented this interface too. There was no need to
implement both methods, but I did it for sake of getting
experience of implementation of these methods. Add the
following interface definition in the inheritance list,
property map.
class ATL_NO_VTABLE CShapeCtl :
.
public IObjectSafetyImpl<CShapeCtl,
INTERFACESAFE_FOR_UNTRUSTED_CALLER |
INTERFACESAFE_FOR_UNTRUSTED_DATA>
.
BEGIN_COM_MAP(CShapeCtl)
.
COM_INTERFACE_ENTRY(IObjectSafety)
.
END_COM_MAP()
Add these
interface definitions to CShapeCtl.h file
STDMETHOD(GetInterfaceSafetyOptions)
(REFIID riid, DWORD *pdwSupportedOptions,
DWORD *pdwEnabledOptions);
STDMETHOD(SetInterfaceSafetyOptions)
(REFIID riid, DWORD dwOptionSetMucancode.net,
DWORD dwEnabledOptions);
Look in code
for implementation details of the interface.
Testing
Control
Figure 3
I have
included ContainerApp which is a simple dialog based MFC
application. In the dialog box right click and choose
Insert Activex control option. Select ShapeCtl control.
Right click on the control and manipulate the control
properties from the property pages we added.
To include
this control in web page use any of the editor or use the
ActiveXPage.html file included in the ComApps folder.
References:
-
Professional
ATL COM Programming,
Wrox Press Ltd, by Dr. Richard Grimes.
-
Active
Template Library, IDG Books Worldwide Inc, by Tom
Armstrong
-
The
Essence of COM with ActiveX, Prentice Hall, by
David S. Platt
-
Inside
OLE second Ed., Microsoft Press, by Kraig
Brockscmidt
-
POLYGON
tutuorial supplied with VC++
6.0
-
Microsft
knowledgebase articles
Compilation
The example
code has been compiled with VC++
6.0 (SP-1) on Windows NT 4.0 (SP-4). If you compile the
code, you will get 9 warnings. These warnings are there
because I didn’t implement the dispatch interfaces for
font, picture and mouse icon property of the control. I
didn’t do this to keep the implementation of control as
simple as I could. See Microsoft Knowledgebase Article ID
: Q166472 for details on their implementation.
Downloads
activex_tut1.zip - Download
source code -- 58 Kb
activex_tut2.zip - Download Container Application -- 31 Kb
|