Extending Our Full Control with COM Categories

 < Free Open Study > 



One map that is not automatically included by the Object Wizard is the category map. Most ActiveX controls will want to implement the "safe for initialization" and "safe for scripting" categories to play well with a web-based host. If we do not add support for these categories, MS IE will display security warnings when attempting to work with this control. To mark our control as safe, add a category map to the initial generated code (don't forget the CATID constants are defined in <objsafe.h>):

// If we want to support COM categories, we must manually add // the ATL category map by hand. BEGIN_CATEGORY_MAP(CShapesControl)      IMPLEMENTED_CATEGORY(CATID_SafeForScripting)      IMPLEMENTED_CATEGORY(CATID_SafeForInitializing) END_CATEGORY_MAP()

If you open up the OLE/COM viewer utility and look under the Controls category, you will find ShapesControl is now supporting these COM categories:

click to expand
Figure 14-14: Hunting down our control from the OLE/COM Object Viewer.

Adding a Custom Property to CShapesControl

The initial files generated by the ATL Object Wizard do indeed constitute a Full Control. You can compile the project and make use of the current functionality. However, your control does not have any unique support beyond a new Toolbox bitmap and a few COM categories. To build a custom coclass, we must extend the boilerplate code.

CShapesControl will allow the user to pick from a variety of graphical objects, which will be rendered on the control's view area. We will call this property ShapeType. Assume that ShapeType can only accept a small range of valid shapes, which would be best expressed by a custom enumeration. To begin defining the ShapeType property, add the following enumeration to your IDL file:

// A custom enumeration used with our custom property. [uuid(6019BEB1-7307-11d3-B92D-0020781238D4), v1_enum] typedef enum CURRENTSHAPE {      [helpstring("The Circle")] shapeCIRCLE          = 0,      [helpstring("The Square")] shapeSQUARE          = 1,      [helpstring("The Rect")] shapeRECTANGLE         = 2 } CURRENTSHAPE; 

Like any ATL COM object, we may add our new custom property using the Add Property Wizard (Figure 14-15). As for the type of property, specify CURRENTSHAPE (this is not available from the drop-down combo box, so manually enter the correct data type):

click to expand
Figure 14-15: Adding the initial custom ShapeType property.

Now, add a member variable named m_shapeType (of type CURRENTSHAPE) to CShapesControl, and set the default type to shapeCIRCLE in the constructor. The get_ShapeType() method simply returns the current value of the m_shapeType value:

// Set default shape. CShapesControl() {      m_bWindowOnly = TRUE;      m_shapeType = shapeCIRCLE; } // Return the current type of shape. STDMETHODIMP CShapesControl::get_ShapeType(CURRENTSHAPE *pVal) {      // Return the current value to container.      *pVal = m_shapeType;      return S_OK; } 

put_ShapeType() will set the value of m_shapeType (provided it is within bounds) and make a call to FireViewChange() to redraw the control:

// Set the value (if in range), and redraw the image. STDMETHODIMP CShapesControl::put_ShapeType(SHAPETYPE newVal) {      // Check bounds.      if(newVal >= shapeCIRCLE && newVal <= shapeRECTANGLE){           m_shapeType = newVal;           FireViewChange();           return S_OK;      }      return E_FAIL; }

FireViewChange() is defined by CComControl<>, and may be called whenever you need to inform the container it is time to be redrawn. ActiveX controls can be drawn under two circumstances: within a window or as a metafile (if you are an embedded object). The implementation of FireViewChange() tests for each possibility and places a WM_PAINT message into the queue, which is handled by OnDraw():

// As found in <atlctl.h>. inline HRESULT CComControlBase::FireViewChange() {      if (m_bInPlaceActive)      {           // Active           if (m_hWndCD != NULL)                ::InvalidateRect(m_hWndCD, NULL, TRUE);           // Window based           else if (m_spInPlaceSite != NULL)                m_spInPlaceSite->InvalidateRect(NULL, TRUE);      // Windowless      }      else      // Inactive           SendOnViewChange(DVASPECT_CONTENT);      return S_OK; }

Rendering Your Control with OnDraw()

The default implementation of OnDraw() paints a simple string within the boundaries of the control. Of course you will typically delete much of this code as you paint the visual appearance of your custom object. Here is the initial logic behind OnDraw():

// OnDraw() is used when rendering the visual aspect of your control. HRESULT OnDraw(ATL_DRAWINFO& di) {      RECT& rc = *(RECT*)di.prcBounds;      Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);      SetTextAlign(di.hdcDraw, TA_CENTER|TA_BASELINE);      LPCTSTR pszText = _T("ATL 3.0 : ShapesControl");      TextOut(di.hdcDraw,           (rc.left + rc.right)/2,           (rc.top + rc.bottom)/2,           pszText,           lstrlen(pszText));      return S_OK; } 

OnDraw() takes a single parameter of type ATL_DRAWINFO. This structure is filled and formatted by the ATL framework, and contains a number of fields that prove very useful as you render your control. For example, you may obtain a valid device context by accessing the hdcDraw field. The dimensions of your current bounding rectangle are held within the prcBounds field. Under most circumstances, these two fields provide just enough information required to render your control. Just make use of the Win32 GDI functions and "paint the picture."

Many of the remaining fields of ATL_DRAWINFO are useful only if your control is being rendered into a Windows metafile, rather than directly into a window. This last statement illustrates a very important point: ActiveX controls can be rendered under two circumstances. First, the control may be asked to render itself directly into a window. In this case, the ATL framework will set NULL values for the fields relating to metafile rendering, and draws the image using a standard WM_PAINT message (as seen in the implementation of FireViewChange()).

Metafile rendering, on the other hand, takes place when your control is being rendered at design time, such as when you are building a VB solution, as well as when your control is hosted as an embedded object. A metafile is a visual snapshot of your control's visible appearance. When an ActiveX host container requires such a snapshot, it sends in a metafile HDC, a bounding rectangle, and a drawing aspect, which specifies the format the control should draw itself (icon, print preview format, thumbnail) as specified by the DVASPECT enumeration.

In either case, the code you add to the OnDraw() method will be used to render the image. If you ever wish to perform different blocks of drawing logic based on the type of rendering taking place (windowed or metafile), you can perform a simple test using GetDeviceCaps(), and test for a value of DT_METAFILE. The ATL_DRAWINFO structure is defined in <atlctl.h> as the following, with some comments for the more useful fields:

// This structure is formatted by the framework, and sent into OnDraw(). struct ATL_DRAWINFO {      UINT cbSize;               // Holds the size of this structure.      DWORD dwDrawAspect;        // DVASPECT flag.      LONG lindex;               // Amount to render (ignore).      DVTARGETDEVICE* ptd;       // Information about the target device.      HDC hicTargetDev;          // Metafile HDC.      HDC hdcDraw;               // A valid HDC to use during rendering.      LPCRECTL prcBounds;        // Rectangle in which to draw      LPCRECTL prcWBounds;       // Metafile rectangle.      BOOL bOptimize;            // Are we optimized?      BOOL bZoomed;           BOOL bRectInHimetric;      // Size in HIMETRIC coordinates.      SIZEL ZoomNum;       SIZEL ZoomDen; }; 

Customizing OnDraw()

Our ShapesControl needs to render a shape within the view, based off the current value of m_shapeType. To begin refitting OnDraw() to suit our needs, we will test against the current shape, and render the image using a thick blue pen. This task requires that we drop down to the Win32 GDI function level. To create the pen, we need to make a call to CreatePen() and specify the pen style, width, and color (represented by the RGB macro). Once the pen is constructed, we select it into the device context (which we can obtain from ATL_DRAWINFO) and render the correct shape. When we are all finished with this drawing cycle, we restore the old pen (and thus restore the device context) with a second call to SelectObject(). Here is the relevant code:

// Using Win32 GDI calls, draw a circle, square, or rectangle based on // the value of the ShapeType property. HRESULT CShapesControl::OnDraw(ATL_DRAWINFO& di) {      // Let's get the HDC right away and use throughout this code.      HDC hdc = di.hdcDraw;      // Get the size of this view.      RECT& rc = *(RECT*)di.prcBounds;      Rectangle(hdc, rc.left, rc.top, rc.right, rc.bottom);      // Configure a big blue pen.      HPEN oldPen, newPen;      newPen = CreatePen(PS_SOLID, 10, RGB(0, 0, 255));      oldPen = (HPEN) SelectObject(hdc, newPen);      // Which shape?      switch(m_shapeType)      {      case shapeCIRCLE:           Ellipse(hdc, rc.left, rc.top, rc.right, rc.bottom);           break;      case shapeSQUARE:           Rectangle(hdc, rc.left, rc.top, rc.right, rc.bottom);           break;      case shapeRECTANGLE:           Rectangle(hdc, rc.left + 20, rc.top + 30, rc.right - 20, rc.bottom - 30);           break;      }      SelectObject(hdc, oldPen); ...      return S_OK; } 

So far so good! We have a custom property that can control the shape rendered in the control's view. If you wish to run a simple test at this point, launch VB and insert your component using the Components dialog box. Place an instance of your control onto the VB form, select it, and examine the property window. As you can see, the ShapeType property has integrated into the VB IDE (Figure 14-16).


Figure 14-16: Our custom ShapeType property.

Go ahead and change the value a few times. You should see the image rendered in your control update when you commit the changes.

Rigging Up the Caption and Font Stock Properties

When we began this project, we specified support for the stock Caption property. The Object Wizard responded by adding CStockPropImpl<> into our inheritance chain and by placing a CComBSTR member variable (m_bstrCaption) into our class. Because ATL is taking care of the [propput] and [propget] implementation for us, all we need to do is use m_bstrCaption when we see fit. For this control, we will center the caption into the control's view area. Here are the changes we need to make to our current OnDraw() implementation:

// Render the value in the Caption stock property. // Don't forget to define USES_CONVERSION; within scope of OnDraw(). SetTextAlign(di.hdcDraw, TA_CENTER|TA_BASELINE); TextOut(di.hdcDraw,      (rc.left + rc.right)/2,      (rc.top + rc.bottom)/2,      W2A(m_bstrCaption),      m_bstrCaption.Length()); 

With this additional change to OnDraw(), we can now modify the Caption property (at design or run time) and have the view render the correct string. To fully finish up our control's text manipulation logic, we should account for the stock Font property. To represent the current Font selection, we have been provided an IFontDisp data member, wrapped up within an ATL smart pointer. The IFontDisp interface is implemented by a COM font object, which also supports the more general IFont interface. From an IFont interface, we can obtain the underlying HFONT data member and use it to set our device context. Thus, to make use of the Font stock property will require us to query the m_pFont smart pointer for IID_IFont, and from there, grab the HFONT itself. Add the following code (before your call to TextOut()) to OnDraw():

// Draw the Caption stock property with the correct font. HFONT oldFont, newFont; CComQIPtr<IFont, &IID_IFont> pFont(m_pFont); if (pFont != NULL) {      pFont->get_hFont(&newFont);      pFont->AddRefHfont(newFont);      oldFont = (HFONT) SelectObject(hdc, newFont); }

The other font-related detail is to configure an initial font for our ActiveX control to use when first placed into a container. In the constructor of your control, you will need to configure a FONTDESC (font description) structure and create a COM font object using OleCreateFontIndirect(). Here is the updated constructor:

// Set a start-up font. CShapesControl() {      m_bWindowOnly = TRUE;      m_shapeType = shapeCIRCLE;      m_bstrCaption = "Yo!";      m_clrBackColor = RGB(255, 255, 0);      // Create a default font to use with this control.      static FONTDESC _fontDesc =      { sizeof(FONTDESC), OLESTR("Times New Roman"),      FONTSIZE(12), FW_BOLD, ANSI_CHARSET, FALSE, FALSE, FALSE };      OleCreateFontIndirect( &_fontDesc, IID_IFontDisp, (void **)&m_pFont); }

Setting the BackColor

To finish accessing our stock properties, we need to make use of the member variable representing our current BackColor (m_clrBackColor of type OLE_COLOR). This [automation] compliant data type is the standard COM way to represent a combination of red, green, and blue values (the RGB macro can be used to specify the range of each as seen in the constructor code of CShapesControl). As the Win32 GDI functions work with the window's COLORREF type, we must make a call to OleTranslateColor() before calling the GDI functions to render the color of the controls background. Here is the final and complete implementation of OnDraw() given this final adjustment:

// The completed OnDraw(). HRESULT CShapesControl::OnDraw(ATL_DRAWINFO& di) {      USES_CONVERSION;      COLORREF colBack;      HBRUSH oldBrush, newBrush;      HPEN oldPen, newPen;      HFONT oldFont, newFont;          // Let's get the HDC right away and use throughout this code.      HDC hdc = di.hdcDraw;      // Get the size of this view.      RECT& rc = *(RECT*)di.prcBounds;      Rectangle(hdc, rc.left, rc.top, rc.right, rc.bottom);      // Fill the background using the BackColor stock property.      OleTranslateColor(m_clrBackColor, NULL, &colBack);      newBrush = (HBRUSH) CreateSolidBrush(colBack);      oldBrush = (HBRUSH) SelectObject(hdc, newBrush);      FillRect(hdc, &rc, newBrush);      // Draw the correct shape with a big blue pen.      newPen = CreatePen(PS_SOLID, 10, RGB(0, 0, 255));      oldPen = (HPEN) SelectObject(hdc, newPen);      switch(m_shapeType)      {      case shapeCIRCLE:           Ellipse(hdc, rc.left, rc.top, rc.right, rc.bottom);           break;      case shapeSQUARE:           Rectangle(hdc, rc.left, rc.top, rc.right, rc.bottom);           break;      case shapeRECTANGLE:           Rectangle(hdc, rc.left + 20, rc.top + 30, rc.right - 20, rc.bottom - 30);           break;      }      // Draw the Caption stock property with the correct font.      CComQIPtr<IFont, &IID_IFont> pFont(m_pFont);      if (pFont != NULL)      {           pFont->get_hFont(&newFont);           pFont->AddRefHfont(newFont);          // Lock font for use.           oldFont = (HFONT) SelectObject(hdc, newFont);      }      SetTextAlign(hdc, TA_CENTER|TA_BASELINE);      SetBkMode(hdc, TRANSPARENT);      TextOut(hdc, (rc.left + rc.right)/2, (rc.top + rc.bottom)/2,                W2A(m_bstrCaption), m_bstrCaption.Length());      // Reset DC & clean up.      SelectObject(hdc, oldPen);      SelectObject(hdc, oldBrush);      SelectObject(hdc, oldFont);      pFont->ReleaseHfont(newFont);          // Free font.      return S_OK; }

Again, as you can see, building a control with ATL does demand that you gain (or already have) comfort with the Win32 API. Unlike MFC ActiveX control development, you are not provided with numerous utility classes (such as CPen, CBrush, CDC, and so on) that wrap raw handles.

If we load up ShapesControl into VB, we are now able to manipulate the Caption, BackColor, and Font stock properties using the VB property window. Here is one such configuration:


Figure 14-17: Exercising our properties.

Firing a Custom Event

Like any COM object, ActiveX controls may send events to any interested client. In fact, ActiveX controls typically send a number of events, many in response to some user interaction. Adding an event to an ATL control is exactly the same as adding an event to any other coclass. For our purpose, we will keep things simple and fire out a COM event whenever the mouse has clicked ShapesControl. Add the following method to your [default, source] dispinterface:

// We will send out the (x, y) of where we were clicked. dispinterface _IShapesControlEvents {      properties:      methods:      [id(1), helpstring("method ClickedControl")]      HRESULT ClickedControl([in] short X, [in] short Y); };

Next, compile your project, and using the Implement Connection Point Wizard, bring in support for the connection proxy. When you go back to examine the code changes, you will see that you are now derived from CProxy_IShapesControlEvents<>, and your connection map has been updated (probably incorrectly) with a new CONNECTION_POINT_ ENTRY macro. Be sure your entry uses the correct "DIID_" prefix.

As CProxy_IShapesControlEvents<> has defined the Fire_ControlClicked() method, we now need to raise this event when our control receives a WM_LBUTTONDOWN event. Use the Add Windows Message Handler Wizard (as detailed in Chapter 13) to intercept this event. As you know, this will result in an additional MESSAGE_HANDLER macro entry used to route the flow of execution to the function handling this message. In the wizard-supplied stub code, call Fire_ControlClicked(), passing to the container the (x, y) location of the mouse cursor (here is an example of the need to "crack" out some information from the LPARAM parameter):

// Tell the container we have been clicked at location (x, y) LRESULT OnLButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {      // Figure out (x, y) and fire event.      short xPos = LOWORD(lParam);      short yPos = HIWORD(lParam);      Fire_ClickedControl(xPos, yPos);      return 0; }

To extend the VB tester to capture our event, add two Label objects onto the main form. In the handler for the ClickedControl event, add the following (output in Figure 14-18):


Figure 14-18: Responding to the custom event.

' Your event has integrated into the VB IDE, just ' like an intrinsic control. ' Private Sub ShapesControl1_ClickedControl(ByVal X As Integer, ByVal Y As Integer)      LabelX.Caption = "X Position: " & X      LabelY.Caption = "Y Position: " & Y End Sub 

Configuring Property Persistence

If you have been fooling around with our VB test client, you may have noticed a strange pattern. When you set the various properties of your control at design time things seem fine. Your ShapeType setting holds, and you can see the effects of selecting various settings from the VB property window. However, when you run the program, you are always placed back at the default shapeCIRCLE value defined in the constructor of ShapesCon- trol. Even stranger, the BackColor, Caption, and Font properties all seem to be "remembered" at run time.

As pointed out earlier in this chapter, the ATL Object Wizard has added a new item called a property map, which is in charge of saving and loading the values of your custom properties via the COM persistence model. ATL always ensures that any stock property is saved and loaded correctly when asked to do so by a container; however, the job of persisting your custom properties is up to you. We will hold off on the details behind COM persistence and the property map for now; however, adding the correct support to save your custom properties is painless. Simply add a new PROP_ENTRY macro for each custom property. The first parameter is the name of the property you are persisting, followed by the DISPID used to identify it:

// Persisting our custom ShapeType property. BEGIN_PROP_MAP(CShapesControl)      PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)      PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)      PROP_ENTRY("BackColor", DISPID_BACKCOLOR, CLSID_StockColorPage)      PROP_ENTRY("Caption", DISPID_CAPTION, CLSID_NULL)      PROP_ENTRY("Font", DISPID_FONT, CLSID_StockFontPage)      // Example entries      // PROP_ENTRY("Property Description", dispid, clsid)      // PROP_PAGE(CLSID_StockColorPage)      PROP_ENTRY("ShapeType", 1, CLSID_NULL) END_PROP_MAP()

Because we do not yet have a property page used to edit the ShapeType property, we can enter CLSID_NULL as the final parameter to the PROP_ENTRY macro. If you now go back and test your control from within VB, you will see that the values you set at design time are correctly held over into run time, as well as between sessions.

Adding a Custom AboutBox to ShapesControl

I am sure you are proud of your new ActiveX control. Most people who write controls with ATL are exceptionally proud of their work, and it is only fitting that we create a standard About box to show off just a bit. As it turns out, COM defines a (very) small number of stock methods that all ActiveX containers understand. Unlike stock properties, ATL does not provide a default implementation of stock methods. On the other hand, COM always assumes the following DISPIDs for each stock method:

Stock Method

DISPID

Meaning in Life

AboutBox

DISPID_ABOUTBOX

Shows a standard About box.

Refresh

DISPID_REFRESH

Redraws the control.

DoClick

DISPID_DOCLICK

Simulates a mouse click.

To add support for DISPID_ABOUTBOX, begin by adding a new method to your [default] interface, and change the DISPID of the member to DISPID_ABOUTBOX. The name of the method called in response to DISPID_ABOUTBOX is irrelevant, but it makes sense to name our method AboutBox():

// Add the following to your [default] interface. [id(DISPID_ABOUTBOX), helpstring("method AboutBox")] HRESULT AboutBox();

Recall from the last chapter that ATL provides the CSimpleDialog<> template when you need to show a dialog with minimal fuss and bother. About boxes usually have little user interaction, so we will make use of CSimpleDialog<> in the implementation of our AboutBox() method. First, however, we need to insert a new dialog resource. Feel free to be creative when designing your About box, and assign a relevant resource ID. As for the code behind AboutBox(), this is all you need:

// Implementing the stock AboutBox() method. STDMETHODIMP CShapesControl::AboutBox() {      // Show the about box.      CSimpleDialog<IDD_ABOUT> dlg;      dlg.DoModal();      return S_OK; }

Now, how you access this dialog will vary among IDEs. In Visual Basic, you will find that the properties window has a special property named About located at the top of the property list. When you select this option, you will trigger the code above:


Figure 14-19: The custom About box for ShapesControl.

Completing the VB Test Client

The ShapesControl now has enough functionality to build a full test client (we will add support for property pages later in the chapter). Place a group of radio buttons onto the main form to allow the user to select the desired shape at run time. To keep each radio button mutually exclusive, VB demands that you draw the frame object first and then draw each radio button directly into the frame:

click to expand
Figure 14-20: The VB client.

I am sure you can gather what the code behind this GUI looks like. Set the ShapeType property to the correct value based on which radio button was clicked, for example:

' Setting our property at runtime. ' Private Sub OptionSquare_Click()      ShapesControl1.ShapeType = shapeSQUARE End Sub

That wraps up the major code behind the ATL ShapesControl project. Before we move on to take a deeper look as to how all this ATL code works under the hood, the following lab will give you a chance to build another (more interesting) ActiveX control. This new project not only reinforces all the topics presented so far, but also supplies additional logic for bitmap rendering, as well as illustrating some simple animation techniques.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net