|Products Purchase Publishing Articles Support Company Contact|
Articles > COM > Create Scalable Controls
By Dan Appleman
In this article you'll see how to use SpyWorks ActiveX extensions to create VB ActiveX controls that can scale and print correctly. And, given that these controls need not even be visible, this opens the door to some interesting rendering applications. This article is intended primarily for SpyWorks users though it should prove of interest to everyone. The sample program require SpyWorks in order to run.
I receive many Visual Basic and Windows API questions. One of the most common relates to printing ActiveX controls or entire forms. The "easy" way to print a form is to copy its bitmap to the printer. The problem with this approach is that the quality can be very poor. A printer's resolution is far better than that of a screen display. For a control that is intended to generate graphic output, the results may be unacceptable. The PrintForm method is better, but offers no opportunities for scaling the image or printing anything other than the entire form.
Both approaches are is also limited in that only controls that are displayed on the screen can be printed. If you have a control that is capable of rendering an image, it seems reasonable that you should be able to dump that image directly to the printer without displaying it on screen. This feature would allow you to create ActiveX controls that are truly generic display components.
Puzzle #28 "Drawing OLE Objects" from my book "Dan Appleman's Win32 API Puzzle Book and Tutorial for Visual Basic Programmers" (APress: ISBN 1-893115-01-1) describes a technique for drawing controls that is unfamiliar to most Visual Basic programmers. It shows how you can use the OleDraw API function to display the contents of an ActiveX control on a form or the printer, even if the control is not visible. It also demonstrates the ability to scale the display to any size - allowing the control to take full advantage of the resolution of the output device.
There are just two catches to that approach.
To understand why the OleDraw function does not always work, you need to understand a little bit about the IViewObject interface.
The IViewObject Interface
Every ActiveX control is a COM object. Like any COM object, it supports a number of interfaces. If you don't understand these concepts, I strongly encourage you to read my book "Developing COM/ActiveX Components with Visual Basic 6.0: A Guide to the Perplexed". Space does not permit me to cover those fundamental concepts in this article.
In fact, the definition of an ActiveX control is a COM object implemented by an ActiveX DLL server that supports a certain set of interfaces that are required by any ActiveX control. In other words - if you create a COM object that supports certain interfaces, it becomes an ActiveX control.
One of the interfaces that must be implemented by every ActiveX control is the IViewObject interface. This is the interface that is used by a container to request that a control draw its contents into a device context. The interface has only one method that is of real concern - the Draw function, which in turn has a great many parameters. The method is defined thus in the Win32 API documentation:
HRESULT Draw( DWORD dwAspect, //Aspect to be drawn LONG lindex, //Part of the object of interest in the draw //operation void * pvAspect, //Pointer to DVASPECTINFO structure or NULL DVTARGETDEVICE * ptd, //Pointer to target device in a //structure HDC hicTargetDev,//Information context for the target device HDC hdcDraw,//Device context on which to draw const LPRECTL lprcBounds, //Pointer to the rectangle in // which the object is drawn const LPRECTL lprcWBounds,//Pointer to the window extent // and window origin when drawing a metafile BOOL (*) (DWORD) pfnContinue,//Pointer to the callback //function for canceling or continuing the drawing DWORD dwContinue//Value to pass to the callback function );
A complete description of this method can be found in the online Win32 API documentation from Microsoft. An ActiveX control is required to display its contents when this method is called using the device context provided by the method in the hdcDraw parameter. Why is this important? Most ActiveX controls have windows associated with them (unless they are windowless controls), so it is tempting for control developers to just draw to the window or draw the contents of the control based on the size of the window. Doing so, however, is a serious mistake. The Draw method specifies the device context and the device rectangle into which the control should draw its contents. That device context might be a printer, and the device rectangle might be an arbitrary location on the printer. The Draw method is expected to work even if the control has no window or is invisible. In short, the Draw method makes possible the very features that I mentioned earlier - allowing controls to render their contents at the maximum device resolution and a specified location on a device regardless of whether the control is visible.
Given that every control is required to implement the IViewObject interface, and that the Draw method of this interface supports flexible rendering of a control on any device regardless of whether the control is visible, and the OleDraw API function uses the IViewObject interface to draw a control - why is it that the OleDraw function does not work with every ActiveX control. And why is it that ActiveX controls authored in Visual Basic will not scale or print properly?
Why? Because most controls do not implement the Draw method according to the OLE specifications. Many controls ignore the device rectangle specified by the lprcBounds parameter and base their drawing on the size of the control itself. These controls will display correctly on a form, but will fail to print in some cases and will not scale properly, making it impossible to render a high quality image from the control.
Behind the scenes in a Visual Basic control
The behavior of a control authored using Visual Basic's is perplexing (to say the least). The control's Paint event can be raised in two ways: via the WM_PAINT message and from the IViewObject::Draw method. Based on extensive experimentation, here is what I believe happens under these two situations. The following applies to window based controls only. Windowless controls never receive the WM_PAINT message and rely exclusively on IViewObject.
The WM_PAINT message.
This message is received by a window as an indication from the operating system that the window needs to be drawn. When this message arrives, the hDC property of the UserControl is set to the control's window. The ScaleWidth and ScaleHeight properties can be used to determine the width and height of the area that may be drawn by the control.
The IViewObject::Draw Method.
This method is called by the OleDraw API. A typical case where this method is called is during an invocation of the PrintForm method. The hDC property of the UserControl is set to the output device (in the case of the PrintForm method, this will be the printer, in the case of the OleDraw API it will be the hdcDraw parameter of the IViewObject::Draw method). The device context will be scaled so that the upper left corner of the drawing area is 0,0 (even though the control probably is not located at location 0,0 on the device). There is no way for you to determine the width or height of the target area, so any scaling requested by the OleDraw API will fail.
Even when no scaling is requested, the results are not always satisfactory. Try using the PrintForm method with the OleDraw2 sample application and you'll see that the printed image does not match that displayed on the form.
Desaware's ActiveX Extension technology allows you to override the behavior of the IViewObject interface by intercepting calls to the IViewObject::Draw method. The OleDraw2 sample application illustrates how this is accomplished.
First, you must add a reference to the "Desaware ActiveX Extension Library" using the Project-References command.
You'll need to define a dwControlHook object for your control, and your control needs to implement the IdwViewObject interface.
Implements IdwViewObject Dim ctlhook As dwControlHook
Next, the control creates a dwControlHook object during the UserControl's Initialize event as shown here:
Private Sub UserControl_Initialize() Set ctlhook = New dwControlHook ctlhook.Initialize Me End Sub
The dwControlHook Initialize method detects that your control implements the IdwViewObject interface, and performs the necessary operations to intercept the Draw method. In the case of a standalone ActiveX control, you would also use the constituent licensing model to allow use of the ActiveX extensions in redistributable controls during the Initialize event.
The OleDraw2 sample program demonstrates how you can add scaling to your control. The IViewObject::Draw method is actually divided into three different methods to make it easier to use for Visual Basic developers. The DrawBounds method allows you to retrieve the coordinates of the lprcBounds rectangle. The MetaFileBounds method allows you to retrieve the coordinates of the lprcWBounds rectangle. As with any interface, all of the interface methods must be implemented. As usual with the Desaware ActiveX extensions, the default behavior of an empty method (with no Visual Basic code) is for the control to continue its default behavior.
In this example, the lprcBounds rectangle values are stored in the DrawingBounds rectangle. These values are used during the Paint event to draw 16 vertical bars scaled to the true size of the target area.
Private Sub IdwViewObject_DrawBounds(ByVal _ Left As Long, ByVal Top As Long, ByVal Right _ As Long, ByVal Bottom As Long) DrawingBounds.Left = Left DrawingBounds.Top = Top DrawingBounds.Right = Right DrawingBounds.Bottom = Bottom RectSet = True End Sub
Private Sub UserControl_Paint() Dim x As Long Dim bandsize As Long
UserControl.ScaleMode = vbPixels
If Not RectSet Then DrawingBounds.Left = 0 DrawingBounds.Top = 0 DrawingBounds.Right = ScaleWidth DrawingBounds.Bottom = ScaleHeight End If
bandsize = (DrawingBounds.Right - _ DrawingBounds.Left + 1) / 16
For x = 0 To 15 Line (bandsize * x, 0)-(bandsize * _ (x + 1), DrawingBounds.Bottom - _ DrawingBounds.Top), QBColor(x Mod 16), BF Next x End Sub
The OleDraw3 example demonstrates that it is possible to completely take over the drawing of the control by overriding the Draw method itself. Setting the CancelDefault parameter to True prevents the default processing by the control, so the control's Paint event will only be raised after receipt of the WM_PAINT message.
Private Function IdwViewObject_Draw(ByVal _ Aspect As Long, ByVal hDCDraw As Long, ByVal _ ptd As Long, ByVal hicTargetDev As Long, _ pfnContinue As Long, ByVal dwContinue As Long, _ CancelDefault As Boolean) As Long DrawTheControl hDCDraw CancelDefault = True End Function
The Paint event calls the same DrawTheControl function, passing the device context of the control.
Private Sub UserControl_Paint() Dim x As Long Dim bandsize As Long Dim pt As POINTAPI
UserControl.ScaleMode = vbPixels
DrawingBounds.Left = 0 DrawingBounds.Top = 0 DrawingBounds.Right = ScaleWidth DrawingBounds.Bottom = ScaleHeight UserControl.ScaleMode = vbPixels DrawTheControl UserControl.hdc End Sub
The DrawTheControl function uses API drawing exclusively. This is necessary because during IViewObject processing the hDC property of the control is not yet assigned to the correct device. Thus Visual Basic drawing operations will go to the wrong device context.
Private Sub DrawTheControl(ByVal hdc As Long) Dim x As Long Dim bandsize As Long Dim pt As POINTAPI Dim newbrush As Long Dim priorbrush As Long Dim originalpen As Long
bandsize = (DrawingBounds.Right - _ DrawingBounds.Left + 1) / 16
' Select null pen originalpen = SelectObject(hdc, _ GetStockObject(NULL_PEN))
'For x = 0 To 15 ' Line (DrawingBounds.Left + bandsize * x, _ DrawingBounds.Top)-(DrawingBounds.Left + _ bandsize * (x + 1), DrawingBounds.Bottom), _ QBColor(x Mod 16), BF 'Next x
For x = 0 To 15 newbrush = CreateSolidBrush(QBColor(x Mod 16)) priorbrush = SelectObject(hdc, newbrush) Call Rectangle(hdc, DrawingBounds.Left + _ bandsize * x, DrawingBounds.Top, _ DrawingBounds.Left + bandsize * (x + 1) _ + 1, DrawingBounds.Bottom + 1)
SelectObject hdc, priorbrush DeleteObject newbrush Next x
Call SelectObject(hdc, GetStockObject(BLACK_PEN))
MoveToEx hdc, DrawingBounds.Left, _ DrawingBounds.Top, pt LineTo hdc, DrawingBounds.Right, DrawingBounds.Bottom
SelectObject hdc, originalpen End Sub
SpyWorks 6 adds IViewObject support to the previously implemented IObjectSafety, IOleObject, and IPerPropertyBrowsing extensions to allow VB programmers to create truly professional quality ActiveX controls. By detecting the target rectangle, this feature makes it easy to create scalable controls. It also allows total flexibility to programmers who wish to create controls that can be accurately rendered under any circumstances.
You can download the OleDraw2 and OleDraw3 sample applications from Desawares web site at ftp.desaware.com. The sample programs require SpyWorks 6 or later in order to run.
For notification when new articles are available, sign up for Desaware's Newsletter.
|Products Purchase Articles Support Company Contact