Introduction
There are many owner-draw button written using C++ and the MFC classes.
There are also many ActiveX controls, mostly written in Visual Basic.
While with C++ you can easily use both the MFC and the OCX controls, with Visual Basic
you need to convert the MFC objects to an activex control. Unluckily, the class wizard doesn't cover all the messages and the events sent
to an ActiveX control, and some messages are different, so most of the magic
must be hand written.
Because sometimes a piece of code is more clear than the article itself, I included
the porting of CxShadeButton
to an ActiveX control, but it's just an example, in the article I will speak
about a generic AxButtonCtrl.
The MFC Activex Control Wizard & the Class Wizard
With a couple of clicks, the Activex Control Wizard writes for us about 600
lines of commented code that probably we will never read. Just remember to
select "BUTTON" in the combo-box where the wizard ask: "Which
window class, if any, should this control subclass?", and the framework
is ready.
Using the Class Wizard, you can add the member functions to process the basic
messages:
WM_CREATE
WM_ERASEBKGND
WM_KEYDOWN
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_MOUSEMOVE
WM_SIZE
PreSubclassWindow
Some of these messages are optional, but I'm thinking about a custom control,
so there are good chances to handle more messages than a normal button.
The WM_DRAWITEM message is not in the list, it's not a mistake, and I will explain
this later.
PreSubclassWindow
void AxuttonCtrl::PreSubclassWindow()
{
... COleControl::PreSubclassWindow();
ModifyStyle(0, BS_OWNERDRAW|BS_NOTIFY);
}
In this method you can copy the same code used in a MFC control; the difference
is in the last 2 lines: the button is now derived from COleControl
(in place of CButton). You must set the BS_OWNERDRAW
style to paint the button with your custom graphics, and the BS_NOTIFY
style if you need some special notification messages, like BN_DISABLE,
BN_KILLFOCUS, ...
WM_CREATE & WM_SIZE
These messages sometime are not used by MFC controls, but in Visual Basic, the
WYSIWYG philosophy requires that the developer can see the appearance while
she/he builds the GUI.
The control receives the WM_CREATE message and creates the object
with COleControl::OnCreate(lpCreateStruct), after this call the
button exists, and you can use all the windows functions (CWnd
members in MFC) to initialize the graphic objects.
The WM_SIZE message is sent after WM_CREATE, and when
the button size has been changed; here you must build (or rebuild) the position
and/or the dimensions of the graphic objects.
Keyboard & mouse messages
These messages are useful for the tooltips and the hover functionality, see
later.
Drawing the button - Reflected window messages
The Class Wizard provides an AxButtonCtrl::OnDraw member for the
drawing functions.
void AxButtonCtrl::OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
DoSuperclassPaint(pdc, rcBounds);
}
You can leave it as it is. Our button waits the WM_DRAWITEM message,
but the mechanism with activex controls is slightly different: COleControl
creates an extra window called "reflector", in the same position of
the control window. The reflector intercepts certain window messages and sends
them to the control with a different name. The reflected messages are:
| Message sent by control |
Message reflected to control |
| WM_COMMAND |
OCM_COMMAND |
| WM_CTLCOLOR |
OCM_CTLCOLOR |
| WM_DRAWITEM |
OCM_DRAWITEM |
| WM_MEASUREITEM |
OCM_MEASUREITEM |
| WM_DELETEITEM |
OCM_DELETEITEM |
| WM_VKEYTOITEM |
OCM_VKEYTOITEM |
| WM_CHARTOITEM |
OCM_CHARTOITEM |
| WM_COMPAREITEM |
OCM_COMPAREITEM |
| WM_HSCROLL |
OCM_HSCROLL |
| WM_VSCROLL |
OCM_VSCROLL |
| WM_NOTIFY |
OCM_NOTIFY |
| WM_PARENTNOTIFY |
OCM_PARENTNOTIFY |
For these messages you must add the message handlers manually: in the control
class .H file, declare a handler function like this:
class AxButtonCtrl : public COleControl
{
...
protected:
LRESULT OnOcmCommand(WPARAM wParam, LPARAM lParam);
LRESULT OnOcmDrawItem(WPARAM wParam, LPARAM lParam);<br>...
}
In the control class .CPP file, add the ON_MESSAGE entries to the message map:
BEGIN_MESSAGE_MAP(AxButtonCtrl, COleControl)
...
ON_MESSAGE(OCM_COMMAND, OnOcmCommand)
ON_MESSAGE(OCM_DRAWITEM, OnOcmDrawItem)
...
END_MESSAGE_MAP()
And implement the member functions to process the reflected messages. The wParam
and lParam parameters are the same as those of the original window message.
LRESULT AxButtonCtrl::OnOcmDrawItem(WPARAM wParam, LPARAM lParam)
{
UINT nIDCtl = (UINT) wParam;
LPDRAWITEMSTRUCT lpDrawItemStruct = (LPDRAWITEMSTRUCT) lParam;
... return 0;
}
Events
The Class Wizard gives some stock handlers for the standard events, however
the resulting functionality could be limited.
If you add the "Click" event with the stock handler implementation,
when someone clicks the button with the mouse, COleButton automatically
calls the FireClick method, so you don't need to process the OCM_COMMAND
message. But if the button has the focus and someone presses the space bar,
the FireClick method is not called, although the button receives
the BN_CLICKED notification through the OCM_COMMAND
message.
In the end, use the custom event handler implementation, in this way you can
control exactly the behavior of the button, without hidden calls or messages.
LRESULT AxButtonCtrl::OnOcmCommand(WPARAM wParam, LPARAM lParam)
{
...
switch (wNotifyCode)
{
case BN_CLICKED:
FireClick();
break;
case BN_KILLFOCUS:
...
}
return 0;
}
ToolTips
The MSDN article Q141871 (HOWTO: Add Tooltips to ActiveX Controls) describes
the steps to implement tooltips. The resume: add the RelayEvent
method and a CToolTipCtrl m_ttip member variable; create and activate
the tooltip; in the handlers for WM_LBUTTONDOWN, WM_LBUTTONUP, and WM_MOUSEMOVE
call RelayEvent to relay appropriate messages to the ToolTip control.
Hover functionality
You must use a bool m_tracking member variable to track the mouse
position. When the mouse is over the button, the WM_MOUSEMOVE messages are sent,
and here activate the tracking.
...
if (!m_tracking) {
TRACKMOUSEEVENT t = {sizeof(TRACKMOUSEEVENT),TME_LEAVE,m_hWnd,0};
if (::_TrackMouseEvent(&t)) {
m_tracking = true;
Invalidate();
}
}
To detect when the mouse leaves, you must add this message handler in the control
class .H file:
LRESULT OnMouseLeave(WPARAM, LPARAM);
and in the control class .CPP file:
ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave)<br><br>
LRESULT AxButtonCtrl::OnMouseLeave(WPARAM, LPARAM)
{
ASSERT (m_tracking);
m_tracking = false;
Invalidate();
return 0;
}
Conclusion
This article treats how to convert an owner draw button in an ActiveX control, this is just the beginning, to build a complete control I should talk about automation, property sheet, and so on ..., but these topics are out of the scope of the article. For detailed informations about COM, please read these
articles