by Zane Thomas
Reprinted with permission from Visual Basic Programmer's Journal, 1/98, Volume 8, Issue 1, Copyright 1998, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange at www.devx.com
Controls that you create with VB5 are always running, even in design mode. As long as you keep things simple, this doesn't complicate life too much. For example, you can create a simple, useful text-box control in VB5 without breaking a sweat. As you continue to add properties and methods with the goal of creating a control that does everything the standard VB text-box control does and more, however, you begin to run into some significant roadblocks.
Let's say you want to add an Appearance property to a control. Adding this property to a control you create with the VC5 Active Template Library 2.0 (ATL) is straightforward. Simply destroy and re-create the control's window in response to a change in the Appearance property. The same approach in a VB5 UserControl, however, requires that you create and handle the text box using the Windows API. You can't use a VB text-box control in the UserControl because the text-box control is in run mode when the UserControl is in design mode.
ATL is a great control--authoring tool-honest!--but the samples provided with it do little to address the difficult areas, and do much to create the impression that ATL is an inadequate and difficult tool. Not only are the controls you create with ATL small and fast, but you can compile them so they require no additional runtimes--a trick that VB can't manage at this time. This combination makes ATL ideal for creating professional, lightweight controls, but ATL's Spartan documentation and minimally functional sample programs obscure many of its virtues.
In this article, I'll show you how to get more out of ATL by demonstrating a commonplace development task: subclassing existing Windows controls, such as the Windows text-box control (see Figure 1). ATL's sample for subclassing these controls is woefully inadequate. The SubEdit control's colors are wrong, the 3-D effect is missing, the font is wrong, and the default Text property isn't the name of the control as it is with VB's text-box control. That isn't all, either, but it's enough to get you started.
You can find the SubEdit control sample on Visual Studio CD 4 at \devstudio\ vc\samples\atl\subedit. If you don't have Visual Studio, but you have the VC5 CD, you should be able to find the sample discussed here in the Samples directory. It would be nice if I could you show you the ATL sample code on the CD side-by-side with the revised code, but the sample for subclassing is copyrighted. Instead, you can download a detailed list of extensive modifications I made from the free, Registered Level of The Development Exchange (see the Code Online box at the end of this column for details).
Overcome 3-D, Color Problems
Let's warm up with something easy and fix the 3-D appearance of SubEdit. Note that the SubEdit project builds a control named AtlEdit instead of SubEdit, as you might reasonably expect. I use the two names interchangeably in this article.
First, add a 3-D border. SubEdit's OnCreate Member function creates the text box. You can add the 3-D border as easily as inserting ", WS_EX_CLIENTEDGE" after ES_AUTOVSCROLL in the call to m_EditCtrl.Create.
Setting the correct foreground and background colors is a little more involved, however. Select the ClassView tab in the Project Explorer, and add ForeColor and BackColor properties. Select OLE_COLOR as the type for each. Most of the "standard" properties have a defined constant you should use as the DispID for such properties. ATL's property-creation tool doesn't provide a drop-down list to select available defined constants for standard properties, and ATL generates an error if you attempt to use the property-creation tool to enter a defined constant. So, you must modify the IDL file after creating the ForeColor and BackColor properties, changing the numeric DispIDs assigned by the tool to DISPID_FORECOLOR and DISPID_BACKCOLOR. Next, add member variables for the two new properties, and call them m_clrBackColor and m_clrForeColor. Their type is OLE_COLOR.
Most properties should have default values that you set within the control's constructor or in the control's OnSetClientSite function. The constructor is sufficient for simple initialization of properties such as ForeColor and BackColor. If you want to initialize a property using a container's ambient properties, however, you need to wait until the container calls the OnSetClientSite function. Note that you should initialize all your property variables to reasonable defaults within the constructor, even when code in OnSetClientSite is being used. This is required because not all containers support all ambient properties.
Add this pair of lines to CAtlEdit( ) in ATLEDIT.H to initialize the color properties:
m_clrBackColor = 0x80000000 |
COLOR_WINDOW;
m_clrForeColor = 0x80000000 |
COLOR_WINDOWTEXT;
Set the upper bit in the color properties so your control can use system colors when values are later passed to OleTranslateColor-and so VB will interpret them as system colors. Without the upper bit, the values would be interpreted as RGB values.
Windows sends the parent of text boxes a WM_CTLCOLOREDIT message when text boxes are about to be painted. The return value should be the handle of the brush that repaints the background. Add a couple lines to the control's constructor to ensure that you'll have a brush ready whenever the WM_CTLCOLOREDIT message arrives:
// Create background brush for edit
// control
m_hbrushEdit = CreateBrush(
TranslateColor(m_clrBackColor));
Your control now has reasonably initialized colors. The ATL support code routes messages through a message map. Next, add a line to the message map to handle the WM_CTLCOLOREDIT message:
MESSAGE_HANDLER(WM_CTLCOLOREDIT,
OnCtlColorEdit)
Finally, add the function, rebuild the control, and verify that it has a 3-D appearance and reasonable colors:
LRESULT OnCtlColorEdit(UINT uMsg, WPARAM
wParam, LPARAM lParam, BOOL&
bHandled)
{
::SetTextColor((HDC)wParam,
TranslateColor(m_clrForeColor));
::SetBkColor((HDC)wParam,
TranslateColor(m_clrBackColor));
if(m_hbrushEdit != NULL)
return (LRESULT)m_hbrushEdit;
else
return (LRESULT)::GetStockObject(
WHITE_BRUSH);
}
Fix the Font, Tab, and Focus
The font problem comes next on the list. The Windows EditBox defaults to a font that doesn't match the standard VB default font. In fact, it stands out like a sore thumb. As you do with the color properties, start by adding an appropriate member variable to the control, and initialize it with a reasonable default value in the control's constructor. Then send the font's handle to the text box after the call to m_EditCtrl.Create.
There's quite a bit of code associated with creating and handling the font, too much to print here. You can download the complete listings for this column from the free, Registered Level of The Development Exchange.
So far, you've corrected SubEdit's 3-D effect look, as well its color and font problems. But SubEdit's problems go beyond its appearance. For example, place an AtlEdit control and a regular VB text box on a form, then run the project. When AtlEdit has the focus, you should see an insertion caret, yet nothing appears to happen. If you start typing, however, the insertion caret appears and the control seems to behave as you would expect it to. But things are not normal. When you press the Tab key, AtlEdit inserts a tab character in the text area rather than shifting the focus to the standard text box, as it should.
You can fix this perception that the control does not have the focus with a minimum of fuss. The problem occurs because the control doesn't set focus to the subclassed text box as it should. Add this line to the message map:
MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
Next, add these lines in the control class definition (ATLEDIT.H):
LRESULT OnSetFocus(UINT uMsg, WPARAM
wParam, LPARAM lParam, BOOL&
bHandled)
{
CComQIPtr <IOleControlSite,
&IID_IOleControlSite>
spSite(m_spClientSite);
if (m_bInPlaceActive && spSite)
spSite->OnFocus(TRUE);
m_EditCtrl.SetFocus();
return 0L;
}
The text box now gets the focus whenever the container sets focus to the AtlEdit control.
Fixing the tabbing problem is a little more complicated. You must override IOleInPlaceActiveObject::TranslateAccel-erator and add appropriate code to handle tabs, or any other special or regular characters in an ATL control (see Listing 1). The _SpecialKeyState function is re- quired because the system doesn't supply the state of the special keys to IOleInPlaceActiveObject.
Listing 1
|
Listing 1 Make the SubEdit Control Tab Correctly. Fixing the tab behavior of the SubEdit control requires you to override the IOleInPlaceActiveObject: :TranslateAccelerator and add appropriate code to handle tabs or other special characters in an ATL control. You must use the SpecialKeyStateFunction because the system doesn't supply the state of the special keys to IOleInPlaceActiveObject. |
You're now ready to test the control again. Build the control, then place it and a standard VB text-box control on a form. The Tab key now works as it should, but you see a new problem. Tab to the standard text box, use the mouse to select the AtlEdit control, then press the Tab key. Bummer, the tab is inserted again!
You see this behavior because nothing lets VB know AtlEdit has the focus when you select AtlEdit with the mouse, so TranslateAccelerator isn't called. This is easy to fix. Add WM_MOUSEACTIVATE to the ATL_MSG_MAP(1) section of the message map, then add the OnMouseActivate function to the control class definition:
LRESULT OnMouseActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
if( !DesignTime() )
InPlaceActivate(OLEIVERB_UIACTIVATE);
return (0);
}
The control should behave as the standard VB text-box control does if you build it now.
There is one more annoying problem with the control that you need to correct. VB's text-box control assigns the control's name as the default Text property when the control is created. The SubEdit sample comes up short here, as well. You can find the fix for this problem in the online listings.
This column explains only what you must do to correct deficiencies in the ATL code sample supplied by Microsoft related to the tab, focus, and appearance of the SubEdit control. Nevertheless, you must tackle each of these issues before you can create a truly professional control with ATL. This article sets you up to implement the cool part: what it is your control does that is truly useful or exciting. From here, you can easily add the functionality that is difficult-if not impossible-to implement with VB5-authored controls. And you'll create smaller, faster controls that don't necessarily require aa runtime besides.
Zane Thomas is vice president of development at Mabry Software and the author of Mabry's Internet Pack, as well as dozens of other controls. Reach Zane through the Mabry Web site at www.mabry.com.