0% found this document useful (0 votes)
143 views

EDais - Using Device Contexts (DCS) in Visual Basic

The document provides an introduction to using Device Contexts (DCs) in Visual Basic. It explains that a DC acts as an interface between applications and output hardware, holding drawing objects and properties. It discusses creating a memory DC using CreateCompatibleDC, selecting a bitmap from a picture box control into the DC using SelectObject, and drawing the bitmap to another picture box using BitBlt. The document is intended to teach the basic concepts and usage of DCs through simple examples.

Uploaded by

dungde92
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
143 views

EDais - Using Device Contexts (DCS) in Visual Basic

The document provides an introduction to using Device Contexts (DCs) in Visual Basic. It explains that a DC acts as an interface between applications and output hardware, holding drawing objects and properties. It discusses creating a memory DC using CreateCompatibleDC, selecting a bitmap from a picture box control into the DC using SelectObject, and drawing the bitmap to another picture box using BitBlt. The document is intended to teach the basic concepts and usage of DCs through simple examples.

Uploaded by

dungde92
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 48

1 EDais DC tutorial

Using Device Contexts (DCs) in Visual Basic


Written by Mike D Sutton Of EDais Http://www.mvps.org/EDais/ [email protected] - 03.05.2004 -

Http://www.mvps.org/EDais/

2 EDais DC tutorial

Introduction

What is a DC?

A Device Context (DC) is core of the GDI (Graphics Device Interface), Windows graphics library. Behind the scenes the DC is the interface between our applications and the output hardware, however when developing with them we rarely see this aspect and can just think of them as a holder for various drawing objects and properties. As its name suggests, a DC puts all the objects it contains in the context of a specific device which and will work out how to format those objects to be compatible and efficient with the desired device. Note; in this last paragraph, device means any kind of output device such as printer or monitor. Before we start I just want to dispel a popular misconception; this being that a DCs and Bitmaps are synonymous when in fact theyre entirely different GDI objects. Although most API drawing routines take a Device Context as a target parameter, they actually manipulate the Bitmap selected into this DC rather than drawing on the DC itself. Chapter 1 will go through the process in a little more depth. Chapter 1 - A simple introduction to DC's Chapter 2 - Whats in a DC? Chapter 3 - GDI Brush objects Chapter 4 - GDI Pen objects Chapter 5 - GDI Font objects Chapter 6 - GDI Regions Chapter 7 - GDI Paths Chapter 8 - Mapping modes Chapter 9 - Tips and tricks

Http://www.mvps.org/EDais/

3 EDais DC tutorial

Chapter I

A simple introduction to DCs

There are a few different types of DCs, however the one well be looking at is the most common for general use, that being the memory device context. To create a memory device context well need the CreateCompatibleDC() call:
Private Declare Function CreateCompatibleDC Lib "GDI32.dll" (ByVal hDC As Long) As Long

Since the DC is a little more complex that your average GDI object, it has a special destruction routine; DeleteDC():
Private Declare Function DeleteDC Lib "GDI32.dll" (ByVal hDC As Long) As Long

When calling the CreateCompatibleDC() method, it requires a handle to an existing device context created in compatibility with the desired device (every DC must be bound to a device.) A quick look in the MSDN however reveals the following information about the parameter: Quote; if this handle is NULL, the function creates a memory DC compatible with the application's current screen. The call will return us a DC handle or HDC in C-speak, or an invalid handle (0) if for some reason something went wrong. Note; Most objects in GDI programming are managed via GDI handles which arent actually memory pointers, but simply identifiers for the GDI objects held internally by the system. The handle itself generally is nothing more than an ID but on some OS such as Win2K various bits of the handle are used to represent various properties about the object it refers to. As a simple example well create a new DC and make sure weve been passed back a valid handle:
Dim hDC As Long hDC = CreateCompatibleDC(0&) Debug.Print hDC Call DeleteDC(hDC)

Running the above code should print a big number into the immediate window, (this could be negative since it may very well write to the sign bit of the value this is fine though since the bit pattern for the handle is still the same regardless of how VB interprets it,) if not then have a look at the result of Err.LastDllError. You may get an out of memory error if something has a GDI resource leak and has claimed all the available GDI space for instance. Note; Always remember to destroy all GDI objects you create, failure to do so will likely create a GDI resource leak in your application A huge problem in something like a paint routine which will likely get called thousands of times, chewing up additional memory each time!

Http://www.mvps.org/EDais/

4 EDais DC tutorial Now a DC on its own isnt a whole lot of use, generally well need some kind of canvas selected into it that it can use to draw to or from. In GDI, these canvases are in the form of Bitmap objects, which are effectively just a big data storage that holds image data. If youve gone through the DIB or DDB articles on this site then youll be at an advantage here however if not then dont worry since we can cheat and get VB to manage the Bitmap for us by using its StdPicture object. Note; Depending on how familiar with graphics application development in VB you are, you may or may not have come across the StdPicture object which is used throughout VB to encapsulate graphics. Behind the scenes the object is really just a wrapper for the various API calls associated with different types of GDI objects such as Bitmaps, Icons, Metafiles etc., and the .Handle property of the object is the handle to its internal GDI object. There are two ways we could use a StdPicture object here, either by instantiating a local object, loading an image into it and destroying that when were done, or by borrowing one off any of VBs controls that expose a .Picture property. In this case well just use the latter approach, which gives us less work to do and more time to focus on DCs instead. To this end, drop a couple of picture boxs on your form and give the first (Picture1) a picture. Important though, make sure you give it a raster image i.e. Bitmap, JPEG or GIF since Icons, Cursors and Metafiles are stored differently internally and wouldnt work with this technique! Now we have our DC and Bitmap, its time to make them talk to one another. In GDI programming, the way we use an object with a DC is to select it into the DC and since it can only hold one object of each type the old object handle is returned to us. This handle must be re-selected before the DC is destroyed to keep Windows happy. The overall lifespan of a GDI object is as follows:
hObj = CreateXYZ( ... ) hOldObj = SelectObject(hDC, hObj) // Use object here SelectObject(hDC, hOldObj) DeleteObject(hObj)

This is the most important rule in GDI programming and, speaking from bitter experience, one that will save you numerous hours of tracking down resource leaks! One thing I tend to do is right after creating an object I write the corresponding delete object call, and similarly for selection/de-selection which makes the chance of forgetting to write the corresponding call far less likely. Since a resource leak happens at runtime it will hurt your users so if you take only one thing from this article let it be this.

Just remember; Create, Select, Use, De-select, Destroy C.S.U.D.D!

Http://www.mvps.org/EDais/

5 EDais DC tutorial To perform GDI object selection well need the SelectObject() API call:
Private Declare Function SelectObject Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal hObject As Long) As Long

Finally, well need a way of drawing the Bitmap from our DC into the second picture box, for this we can use the BitBlt() API call:
Private Declare Function BitBlt Lib "GDI32.dll" (ByVal hdcDest As Long, _ ByVal nXDest As Long, ByVal nYDest As Long, ByVal nWidth As Long, _ ByVal nHeight As Long, ByVal hdcSrc As Long, ByVal nXSrc As Long, _ ByVal nYSrc As Long, ByVal dwRop As Long) As Long

First up, lets create the DC; well need a handle variable for it and the creation/destruction code:
Dim hDC As Long hDC = CreateCompatibleDC(0&) ... Call DeleteDC(hDC)

Remember that when selecting the Bitmap into the DC well have to retain the handle were passed back to de-select the Bitmap before destroying the DC:
Dim hOldBmp As Long hOldBmp = SelectObject(hDC, Picture1.Picture.Handle) ... Call SelectObject(hDC, hOldBmp)

Finally, we want to draw the bitmap to the second picture box which is where the BitBlt() call comes in:
Call BitBlt(Picture2.hDC, 0, 0, 100, 100, hDC, 0, 0, vbSrcCopy)

I expect most people will already be familiar with BitBlt() however if not then dont worry about the myriad of parameters it takes. Heres the final code which Ive put in the Paint() event of the second picture box so it gets called whenever the control re-draws itself:
Private Sub Picture2_Paint() Dim hDC As Long, hOldBmp As Long hDC = CreateCompatibleDC(0&) hOldBmp = SelectObject(hDC, Picture1.Picture.Handle) Call BitBlt(Picture2.hDC, 0, 0, 100, 100, hDC, 0, 0, vbSrcCopy) Call SelectObject(hDC, hOldBmp) Call DeleteDC(hDC) End Sub

Http://www.mvps.org/EDais/

6 EDais DC tutorial

Chapter I I

Whats in a DC?

In the previous chapter you saw how to create a very simple DC and select a Bitmap into it, however under certain circumstances it may be necessary to go the other way and actually find information on a GDI object selected into the DC. A DC also stores information about the device its bound too and what its capable or rendering, as well as holding various properties that can affect the outcome of various drawing routines. This chapter will cover each of these aspects then drill down into more detail in later chapters. The easiest of the three areas mentioned above is querying the devices capabilities i.e. what its capable of rendering. In many cases though, GDI will be able to emulate various operations even if the manufacturer hasnt added support for them directly in the device driver itself. To query the DC for its devices capabilities we can use the GetDeviceCaps() API call:
Private Declare Function GetDeviceCaps Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nIndex As Long) As Long

There are two different kinds of values this call will return to us, the first are simple single values for example when querying whether the device supports clipping:
Private Const CLIPCAPS As Long = 36 ' Clipping capabilities ... Debug.Print "Device supports clipping: " & CStr(GetDeviceCaps(hDC, CLIPCAPS) = 1)

Since the call will return either 0 if it doesnt support clipping and 1 if it does, checking that the return value is equal to one gives us a True/False result. The second kind of value the call will return is a series of packed flags, for example when querying the raster or curve capabilities of the device driver.
Private Const RASTERCAPS As Long = 38 ' BitBlt capabilities Private Const RC_BITBLT As Long = &H1 ' Can do standard BLT ... If (GetDeviceCaps(hDC, RASTERCAPS) And RC_BITBLT) Then _ Debug.Print vbTab & "Can perform standard BLT"

This example calls queries the devices raster capabilities then checks for a specific flag (Binary bit flag) within that, in this case the flag which indicates the device driver is capable of performing a standard BitBlt() operation. GetDeviceCaps() is the only API call you can use to query the devices capabilities, see the MSDN for more information on exactly what you can query and what they return. The next thing well go through is how to query or change the various drawing settings the DC holds, in this case there is no one call you can use but various Get*/Set*() calls. You can think of a DCs settings as read/write properties of the DC Http://www.mvps.org/EDais/

7 EDais DC tutorial in VB terms, meaning that you can query and set them via the public interface (the GDI API in this case) but dont have direct access to the variables themselves - The device capabilities on the other hand are static and therefore are read only. A simple example of a DC setting is something like the text colour, you can use the GetTextColor() or SetTextColor() API calls to retrieve or change this respectively. To see this actually register a change on the DC, well also need a text drawing routine so grab the TextOut() API function declaration:
Private Declare Function TextOut Lib "GDI32.dll" Alias "TextOutA" ( _ ByVal hDC As Long, ByVal X As Long, ByVal Y As Long, _ ByVal lpString As String, ByVal nCount As Long) As Long

First though, well simply report the current value:


Private Declare Function GetTextColor Lib "GDI32.dll" (ByVal hDC As Long) As Long ... Debug.Print "Current text colour: 0x" & Hex(GetTextColor(hDC))

This can be called anywhere between where the DC is created and where it is destroyed, by default the text colour is set to black though so dont be worried if it returns 0. Well first need a string so declare that, then after the DC has been created and the Bitmap selected change the text colour and draw the text:
Private Declare Function SetTextColor Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal crColor As Long) As Long Const DrawString As String = "Hello, world!" ... Call SetTextColor(hDC, vbRed) Call TextOut(hDC, 10, 10, DrawString, Len(DrawString))

Querying the text colour again at this point will return the colour youve set in the previous SetTextColor() call. Depending on the DC settings of the surface you drew to, you may see that the background of the text string has an ugly white rectangular background obscuring the image behind it. This is because by default the background mode of the DC is set to opaque which is another setting/property of the DC, to change it use the SetBkMode() API call:
Private Declare Function SetBkMode Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nBkMode As Long) As Long Private Const TRANSPARENT As Long = &H1 ... Call SetBkMode(hDC, TRANSPARENT)

Http://www.mvps.org/EDais/

8 EDais DC tutorial Again the GetBkMode() will retrieve the current value, if in doubt you can always find information for what the calls expect or return by checking the MSDN. If instead of removing the background colour you wanted to change its colour then you can use the background colour property of the DC accessible via the Get/SetBkColor() API calls. One final thing to note about DC settings that they you dont have to re-set them before the DC is destroyed, only GDI objects themselves require de-selecting. If youre writing a routine that changes these settings on a public DC however (I.e. one that not only your one routine is using such as one owned by one of VBs controls) then its good practice to restore the DCs settings to the same as when you found it. If youre changing a large number of the DCs settings then it can be a real pain to restore everything back to the same state as you received it, so in these cases you can use another couple of API calls that create and restore snapshots of the DCs settings at any point:
Private Declare Function SaveDC Lib "GDI32.dll" (ByVal hDC As Long) As Long Private Declare Function RestoreDC Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nSavedDC As Long) As Long

When you call SaveDC(), the current state of the DC is pushed to its internal state stack and returns the new states index within this stack. These states can then be restored at any time using a call to RestoreDC() which either takes an explicit index of the state to restore from the stack, or a relative index from the current state. For example, RestoreDC(1) restores the 1st state from the stack, where as RestoreDC(-1) restores the previous state regardless of how many are on the stack. Since this is implemented as a stack then normal LIFO stack rules apply so everything between the current state and the one restored will be discarded after a call to RestoreDC(). There is no function defined by the GDI that allows you to test if a DC has restore states or how many restore states it has, but since SaveDC() returns the new states index in the stack its an easy task to write a little function that provides this functionality:
Private Function GetDCStateCount(ByVal inDC As Long) As Long GetDCStateCount = SaveDC(inDC) - 1 ' Return the number of restore states for this DC If (GetDCStateCount >= 0) Then Call RestoreDC(inDC, -1) ' Pop off this new state End Function

If the function returns -1 then there was something wrong with calling SaveDC() on the target DC, most likely its not been passed a valid DC handle. Anything else (0 and above) is the number of restore states currently defined for this DC. The number of saved states for a DC is effectively unlimited; the only potential limiting factor is how much system memory is available.

Http://www.mvps.org/EDais/

9 EDais DC tutorial Finally now on to the third type of GDI object we can query Its internal GDI objects. There are two ways of retrieving the current object of a specific type selected into a DC; the first is to use the GetCurrentObject() API call with the appropriate flag:
Private Declare Function GetCurrentObject Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal uObjectType As Long) As Long Private Const OBJ_PEN As Long = &H1 ... Dim hCurObj As Long hCurObj = GetCurrentObject(hDC, OBJ_PEN)

The second is to select a new compatible (or stock) GDI object into the DC and grab the return handle:
Private Declare Function GetStockObject Lib "GDI32.dll" (ByVal nIndex As Long) As Long Private Const BLACK_PEN As Long = &H7 ... Dim hCurObj As Long hCurObj = SelectObject(hDC, GetStockObject(BLACK_PEN)) ' Use hCurObj here... Call SelectObject(hDC, hCurObj)

This method is useful when dealing with Bitmap objects, since many API calls that deal with Bitmaps require exclusive access to them. For more on stock objects, see the Tips and tricks section at the end of this article. Note; The stock objects returned by this call are usually the same ones that are originally selected into the DC when its first created. Its also possible to simply reselect stock objects into a DC before its destroyed (to de-select your GDI objects) rather than specifically selecting the _same_ stock object which was originally selected. Usually stick away from this method though; its very easy to get resource leaks when using advanced tricks like these.

Http://www.mvps.org/EDais/

10 EDais DC tutorial

Chapter I I I

The GDI brush object

The GDI Brush object is whats responsible for filling the inside of shapes in Windows with solid colour, hatching or pattern (Bitmap) fills. There are numerous kinds of GDI brush and many ways of creating them depending on what style of fill youre after. The simplest style of brush however is a solid colour brush and the API exposes a routine especially for creating these:
Private Declare Function CreateSolidBrush Lib "GDI32.dll" (ByVal crColor As Long) As Long

The only parameter this call takes is a 24-bit RGB colour value so you can use VBs colour constants such as vbRed, vbGreen, vbBlue etc. or a user defined colour value for which you can either pass the full colour code or use the RGB() function. You cant however use VBs system colour constants here (such as vbButtonFace or vb3DShadow) since the API is expecting a literal colour value rather than a system colour code. If you need to use system colours then have a look at the Tips and tricks chapter of this article, however the preferable way to use a system colour brush is to use the GetSysColorBrush() API call which returns a stock object of a solid fill in the desired system colour. In addition to the system colour stock brushs, there are an additional 7 which define greyscale and hollow/null brushes, these can be got at with the GetStockObject() API call by using the *_BRUSH constants. One final stock object to mention here is the special DC brush, which is only included in Win2K+. When this brush is selected into the DC, it will take whatever colour the DCs brush colour attribute is set to which can be read/written with the GetDCBrushColor()/SetDCBrushColor() API calls respectively. The only benefit of this is that if you need many different solid colour fills, you dont have to go through the whole creation, selection, de-selection, destruction process for each one however since this is not supported on older OS your drawing code wont work properly there. Heres a quick example of how to use the DC brush stock object to draw a set of traffic lights:
' GetDCBrushColor() not used in following code; only included for completeness Private Declare Function GetDCBrushColor Lib "GDI32.dll" ( _ ByVal hDC As Long) As Long Private Declare Function SetDCBrushColor Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal crColor As Long) As Long Private Declare Function Ellipse Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal X1 As Long, ByVal Y1 As Long, _ ByVal X2 As Long, ByVal Y2 As Long) As Long Private Const DC_BRUSH As Long = 18 ... Dim hOldBrush As Long ' Select the DC brush into the DC hOldBrush = SelectObject(hDC, GetStockObject(DC_BRUSH)) ' Set green as the DC colour and draw the first light Call SetDCBrushColor(hDC, &HFF00&)

Http://www.mvps.org/EDais/

11 EDais DC tutorial
Call Ellipse(hDC, 0, 0, 100, 100) ' Set amber as the DC colour and draw the second light Call SetDCBrushColor(hDC, &HC0FF&) Call Ellipse(hDC, 0, 100, 100, 200) ' Set red as the DC colour and draw the last light Call SetDCBrushColor(hDC, &HC0&) Call Ellipse(hDC, 0, 200, 100, 300) ' Re-select the DC's original brush Call SelectObject(hDC, hOldBrush)

Youll see that the brush is only selected once however each of the three circles is actually drawn in a different colour. If you do not see the three colours then you may be running on an older OS that doesnt support this stock object, for this reason only use this feature if you specifically do not wish to include support for older OS and/or you can guarantee that it will never be run there. The next type of brush object is the hatch brush, which fills an area with one of the 6 defined hatch styles and in the given colour. This brush type can be created using the CreateHatchBrush() API call and specifying the desired hatch style constant (HS_*) and colour. Again VBs system colour constants cant be used here but you can use the EvalCol() method listed above to correctly evaluate those. The final brush type well talk about here is the pattern brush, which fills an area with a user defined pattern or bitmap fill. These brushs are created using the CreatePatternBrush() API call, which simply takes the handle to an API Bitmap object. For the sake of a quick demonstration though we can use the same trick as back in chapter 1, and borrow a Bitmap from one of VBs controls that exposes a .Picture property:
Private Declare Function CreatePatternBrush Lib "GDI32.dll" ( _ ByVal hBitmap As Long) As Long Private Declare Function Rectangle Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal X1 As Long, ByVal Y1 As Long, _ ByVal X2 As Long, ByVal Y2 As Long) As Long Private Sub Form_Paint() Dim hBrush As Long, hOldBrush As Long Form1.ScaleMode = vbPixels hBrush = CreatePatternBrush(Picture1.Picture.Handle) hOldBrush = SelectObject(Form1.hDC, hBrush) Call Rectangle(Form1.hDC, 0, 0, Form1.ScaleWidth, Form1.ScaleHeight) Call SelectObject(Form1.hDC, hOldBrush) Call DeleteObject(hBrush) End Sub

Note; if youre still running and/or support Win95 machines then the bitmap must be 8*8 pixels or less, this is a limitation of the API which has been lifted on later OS. One problem you may notice here is that when the form re-paints itself the pattern sometimes shifts a little down and right, this is because the brush origin is being changed due to the forms position on screen. You can however quickly fix this with

Http://www.mvps.org/EDais/

12 EDais DC tutorial a call to the SetBrushOrgEx() API call, which sets the brush origin property for the DC. Heres the API declaration and the call which should be placed somewhere before the Rectangle() call:
Private Declare Function SetBrushOrgEx Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nXOrg As Long, _ ByVal nYOrg As Long, ByRef lpPt As Any) As Long ... Call SetBrushOrgEx(Form1.hDC, 0, 0, ByVal 0&)

If you have a brush handle or HBRUSH and you need to find information out about what that brush contains then we can get the API to give us some information about what it contains. To query a GDI object we can use the GetObject() API call:
Private Declare Function GetObject Lib "GDI32.dll" Alias "GetObjectA" ( _ ByVal hObject As Long, ByVal nCount As Long, ByRef lpObject As Any) As Long

Note; You may want to re-name this function to GetObjectAPI to avoid conflicts with VBs GetObject() method, since its already got an alias declared you can safely re-name the function itself. When called on a GDI brush object, it will expect a LOGBRUSH (Logical brush) structure into which it will write the properties of the brush object its given:
Private Type LogBrush ' 12 bytes lbStyle As Long lbColor As Long lbHatch As Long End Type ... Dim BrushInf As LogBrush If (GetObject(inBrush, Len(BrushInf), ByVal 0&)) Then With BrushInf Debug.Print _ "lbStyle: " & .lbStyle & vbCrLf & _ "lbColor: " & .lbColor & vbCrLf & _ "lbHatch: " & .lbHatch End With End If

Whilst these fields look fairly self-explanatory, depending on what kind of brush weve given it these fields can actually mean completely different things. For instance a hollow or null brush will obviously only use the style field (if nothings being drawn it doesnt require a hatch or colour.) A pattern brush doesnt require a hatch or colour either, so it populates the fields with the Bitmap handle and palette usage flag respectively. For more information on this have a look at the MSDNs page for the LOGBRUSH structure or the accompanying code for this chapter.

Http://www.mvps.org/EDais/

13 EDais DC tutorial

Chapter I V

The GDI pen object

The GDI Pen object is whats responsible for drawing the edges of shapes in Windows and as such theyre used all over the place. The easiest way of creating a Pen is to use the CreatePen() API call:
Private Declare Function CreatePen Lib "GDI32.dll" (ByVal nPenStyle As Long, _ ByVal nWidth As Long, ByVal crColor As Long) As Long

The first parameter of this call defines the pens style, such as whether its broken, solid or invisible (null.) There is however one slightly different pen style, PS_INSIDEFRAME, which rather than defining a pen style (its always rendered as a solid line) defines the positioning of the edge instead. Normally when the edge is drawn, its centred on the outer edge meaning that half the width of the pen is drawn outside the shapes edge, half is inside the shapes edge. When this flag is specified the edge is drawn completely inside the outer edge of the shape, this applies to closed primitives only so lines or polygons are drawn the same as with a normal solid pen. Heres some sample code demonstrating this pen style when drawing circles:
Private Declare Function Ellipse Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal X1 As Long, ByVal Y1 As Long, _ ByVal X2 As Long, ByVal Y2 As Long) As Long ' Pen styles Private Const PS_SOLID As Long = &H0 Private Const PS_INSIDEFRAME As Long = &H6 Private Sub Form_Paint() Dim hPen As Long, hOldPen As Long ' Draw with a normal solid pen hPen = CreatePen(PS_SOLID, 20, vbRed) hOldPen = SelectObject(Form1.hDC, hPen) Call Ellipse(Form1.hDC, 10, 10, 110, 110) Call SelectObject(Form1.hDC, hOldPen) Call DeleteObject(hPen) ' Draw with an inside-frame solid pen hPen = CreatePen(PS_INSIDEFRAME, 20, vbRed) hOldPen = SelectObject(Form1.hDC, hPen) Call Ellipse(Form1.hDC, 130, 10, 230, 110) Call SelectObject(Form1.hDC, hOldPen) Call DeleteObject(hPen) ' Draw circle positions hPen = CreatePen(PS_SOLID, 1, vbBlack) hOldPen = SelectObject(Form1.hDC, hPen) Call Ellipse(Form1.hDC, 10, 10, 110, 110) Call Ellipse(Form1.hDC, 130, 10, 230, 110) Call SelectObject(Form1.hDC, hOldPen) Call DeleteObject(hPen) End Sub

Http://www.mvps.org/EDais/

14 EDais DC tutorial Running this code, youll see that the circle on the right appears smaller because the edge is drawn within the outer edge of the circle, the black lines however show that both circles are the same size only the edge has moved. The patterned pen styles (dot, dash-dot etc.) can only be used with 1-pixel wide pens due to the way GDI draws shapes with wider pens; it expands the line to a 2D vector shape and then draws it as a polygon. In this way, these pens are called Geometric pens where as single pixel wide pens are called Cosmetic pens since theyre generally used to add fine details (or at least thats the reasoning behind the naming convention.) If you do specify any of the patterned lines styles with a wider than 1 pixel pen then the style will be ignored and youll simply be returned a solid pen of the desired thickness. Well see in a second how to overcome this limitation by using the more complex extended pen and custom style. Note; One other point here is that if you want to draw a line with alternate black and white pixels, while the PS_DOT style sounds like it should be what youre after it actually draws small dashs. To draw a properly dotted rectangle have a look at the DrawFocusRect() API call instead, for other shapes then youll most likely need to look into the LineDDA() API call and manage the drawing yourself. Since this pen structure is a bit limited in what it can do, the extended pen object was created to give the developer greater control over how the pen draws lines. Generally a pen and extended pen can be used interchangeably and both are stored and passed as a HPEN. To create an extended pen, youll need the ExtCreatePen() API call:
Private Declare Function ExtCreatePen Lib "GDI32.dll" ( _ ByVal dwPenStyle As Long, ByVal dwWidth As Long, _ ByRef lplb As LogBrush, ByVal dwStyleCount As Long, _ ByRef lpStyle As Long) As Long

Youll see here that the call expects a LOGBRUSH structure since geometric pens are drawn with a brush rather than as a line of pixels, however this method keeps the brush used for the edge separate from the brush used to fill the shape. Most of the information here is very similar to a standard API pen however the last two parameters of the call define an optional user style array, which allows the extended pen to emulate and extend upon the standard pens pattern styles. The style array contains the dash and gap sizes for the desired dash style where the first entry defines the size of the first gap, the next defines the size of the first dot and so on. Once it reaches the end of the array it will loop back around and start back from the start again, so an odd number of entries in the array will have the effect of reversing the style pattern every other iteration. To retrieve information on an existing GDI Pen object, we can again employ the services of the GetObject() API call, which will return us a 16-byte LOGPEN (Logical pen) structure as defined in the MSDN:
Private Type PointAPI X As Long Y As Long End Type

Http://www.mvps.org/EDais/

15 EDais DC tutorial
Private Type LogPen ' 16 bytes lopnStyle As Long lopnWidth As PointAPI lopnColor As Long End Type

Note; For some reason the width of the pen is stored as a point here rather than as just a single DWord which is somewhat odd since the Y member of this point structure isnt even used, however thats what the API is expecting us to provide it so we must follow suit (or just pad the structure with a dummy DWord value.) We can now simply create a LogPen structure and get the API call to fill it with the Pens information via the GetObject() API call:
Dim PenInf As LogPen Call GetObject(inPen, Len(PenInf), PenInf) With PenInf Debug.Print _ "lopnStyle: " & .lopnStyle & vbCrLf & _ "lopnSize: (" & .lopnWidth.X & ", " & .lopnWidth.Y & ")" & vbCrLf & _ "lopnColor: 0x" & Hex(.lopnColor) End With

To find out whether a pen handle is a normal pen or extended pen we can use the GetObject() call again, but send it only the handle itself and get it to tell us how much information it has about the object (and thus how big a buffer we need to create to extract all that information.) An extended pen structure will return us an EXTLOGPEN structure, which is at least 24 bytes but can contain a variable sized DWord array, which defines the optional extended dash style as defined in the ExtCreatePen() API call when the pen is created. The structure is defined as follows:
Private Type ExtLogPen elpPenStyle As Long elpWidth As Long elpBrushStyle As Long elpColor As Long elpHatch As Long elpNumEntries As Long elpStyleEntry() As Long End Type

First off well query the object to see how big it is and get an idea of what type of pen this is:
Dim PenSize As Long PenSize = GetObjectAPI(inPen, 0, ByVal 0&) Select Case PenSize Case 16 ' Logical pen Case Is >= 24 ' Extended logical pen Case Else ' Unknown! End Select

Http://www.mvps.org/EDais/

16 EDais DC tutorial If we do get given an extended pen then well have to take into consideration the variable sized array at the end of the structure. Unfortunately we have a problem here since VBs dynamic arrays are stored with an additional variable sized SAFEARRAY header prefixing the data in memory. The API however expects the style data to directly follow the Num. entries member of the structure; the two structures are shown in the diagram to the right. If we just write directly to this UDT after having allocated enough entries in the style array, the API will overwrite the header structure causing VB all kinds of problems when addressing it! To bypass these problems we can instead use a simple array of DWords (Longs) to get the pen data from the API, and then copy this data to the appropriate places within the VB structure as depicted in the diagram to the right. Note; Copying this data into the UDT is really an optional step here, since all the members of the UDT including the extended style array are DWords already.
Dim ExtPen As ExtLogPen Dim PenBuf() As Long ReDim PenBuf(PenSize \ 4) As Long Call GetObjectAPI(inPen, PenSize, PenBuf(0)) Call RtlMoveMemory(ExtPen, PenBuf(0), 24) If (PenSize >= 28) Then ' Copy the user style array ReDim ExtPen.elpStyleEntry(((PenSize - 24) \ 4) - 1) As Long Call RtlMoveMemory(ExtPen.elpStyleEntry(0), PenBuf(6), (PenSize - 24) And Not 3) End If

API

VB

The rather scary looking two lines at the end are just to copy the user style array, the first allocates the array with enough space to contain the additional number of entries PenSize holds the size of the entire structure, 24 is the size of the fixed part of the structure and each style entry is 4 bytes long. The -1 is there because the array is 0 rather than 1 based. The second line copies the data from the DWord array into the user style array, since each entry in the PenBuf() array is 4 bytes long, well need to start copying from the 6th entry (6*4 == 24 == Size of the fixed section of the header.) The And Not 3 part is a shorthand way of rounding down to next smallest 4 since is masks any bits

Http://www.mvps.org/EDais/

17 EDais DC tutorial below the 3rd bit (3 = binary 0011, so (Not 3) = binary 1100). You could use ((PenSize 24) \ 4) * 4 here if you rather which is a little more readable. After all that we can properly query a pen to see whether its extended or not, and examine all the internal data associated with either type of object, see the code for this chapter for a fully implemented routine.

Http://www.mvps.org/EDais/

18 EDais DC tutorial

Chapter V

The GDI font object

The GDI Font object is whats responsible for drawing the symbols we see when textdrawing calls are made on a device context. Typography is a huge subject on which an entire article could be written on its own (perhaps I may do so at some point), so this chapter will only scratch the surface of the subject for now. As always the MSDN is a good reference to continue youre research into GDI development. To create an API Font object, you can use the CreateFont() API call:
Private Declare Function CreateFont Lib "GDI32.dll" Alias "CreateFontA" ( _ ByVal nHeight As Long, ByVal nWidth As Long, ByVal nEscapement As Long, _ ByVal nOrientation As Long, ByVal fnWeight As Long, ByVal fdwItalic As Long, _ ByVal fdwUnderline As Long, ByVal fdwStrikeOut As Long, _ ByVal fdwCharSet As Long, ByVal fdwOutputPrecision As Long, _ ByVal fdwClipPrecision As Long, ByVal fdwQuality As Long, _ ByVal fdwPitchAndFamily As Long, ByVal lpszFace As String) As Long

Wow, thats a lot of parameters! Luckily though its actually very easy to use since most of the parameters have default values of 0 so just specifying the font size and typeface is all you need to create a valid Font object:
Private Sub Form_Paint() Dim hFont As Long, hOldFont As Long Const DrawString As String = "Hello, world!" hFont = CreateFont(50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "Times New Roman") hOldFont = SelectObject(Form1.hDC, hFont) Call TextOut(Form1.hDC, 10, 10, DrawString, Len(DrawString)) Call SelectObject(Form1.hDC, hOldFont) Call DeleteObject(hFont) End Sub

This creates a 50-pixel high font in the Times new Roman typeface and draws the given string using it. As anyone who has used a word-processor will know, you dont usually specify font size in pixels but in points. To specify a point-size for the given font you can use the following method:
Private Declare Function MulDiv Lib "Kernel32.dll" (ByVal nNumber As Long, _ ByVal nNumerator As Long, ByVal nDenominator As Long) As Long Private Declare Function GetDeviceCaps Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nIndex As Long) As Long Private Const LOGPIXELSY As Long = 90 ' Logical pixels/inch in Y ... Height = -MulDiv(PointSize, GetDeviceCaps(hDC, LOGPIXELSY), 72)

This takes the given point size and converts it to the corresponding pixel size on the given device which you can send as the first parameter of the CreateFont() call.

Http://www.mvps.org/EDais/

19 EDais DC tutorial Note; This technique assumes that the mapping mode of the target DC is set to MM_TEXT (pixels), which is the default mapping mode for a DC but can be retrieved/set with the GetMapMode() and SetMapMode() API calls respectively. Well cover mapping modes and how to convert between them in more detail later in this article. The second parameter (the font width) defines the average width of the characters rather than of any one particular character, but by specifying 0 here the aspect ratio of the original font will be preserved regardless of the height. For more control over this parameter you can use the GetTextMetrics() API call and base the value on the .tmAveCharWidth member of the returned TEXTMETRIC structure. The next two parameters define the rotation of the font; the first defines the rotation of the baseline of text while the second defines the rotation of the individual characters off this baseline. The latter can only be used when the graphics mode of the DC is set to advanced mode which is accessed with the GetGraphicsMode() and SetGraphicsMode() API calls. As you can see from the diagram on the left the output of this character escapement mode isnt particularly great at arbitrary angles, however works quite well at 90, 180 and 270 degrees. The red crosshair in each of the 8 segments to the left is the position the text string was actually rendered at, as you can see the output positioning actually differs quite a lot depending on the font escapement used making it unsuitable for a generic routine requiring absolute positioning of the text. Since the Font object actually takes two rotation parameters you can create a font with baseline rotation, and simply make multiple calls to a text drawing routine to draw each character if you require greater output position precision. Font rotation works pretty much as you may expect, and rotates the text around the given point. Alignment isnt always perfect here so its sometimes necessary to wrap the call to achieve better absolute positioning. Angles are measured anti-clockwise and starting from the right by the GDI font rasteriser. By using both the font rotation and character rotation together, you can get some interesting effects such as text the reads from top to bottom without having to turn the page sideways!

Http://www.mvps.org/EDais/

20 EDais DC tutorial The next parameter of the call defines the weight of the font, although this is usually exposed only through the Bold property of a font. Unlike the bold property which only has one of two defined weights, the API gives a weight of between 0 and 1000 where 400 is what wed commonly know as normal and 700 is bold. The next three parameters are all pretty self-explanatory and define Italic, Underline and Strike-through respectively The next parameter defines the character set the API call should interpret and render the text string its given. The default value will invoke font substitution if the given font doesnt exist, so its advised that you explicitly define the character set if you want predictable results over different machines. The precision parameter gives some hints to the font-mapper about what font it should choose if there are multiple similar ones available. The clipping precision parameter can be largely ignored unless youre working with embedded fonts. The quality parameter specifies how the text is rendered by the font rasteriser renders the text string, including specifying whether anti-aliasing or ClearType font smoothing is performed which can greatly increase the visual appearance of rendered text. The next parameter defines the pitch and family of the text which specifies a very general idea of how the font should look so a similar looking one can be chosen if the specified one doesnt exist. The final parameter is the typeface itself which is the name were familiar with such as Times New Roman, Arial etc. To find a list of the available fonts on a machine we can use a font enumeration routine provided by the API, and provide a call-back function for it to return us the information about each typeface. The API call well use for this is EnumFontFamiliesEx() and since well be using call-backs its easiest to put all of this in a module (the AddressOf operator used with call-back functions can only be used on methods defined within a module.) The EnumFontFamiliesEx() call expects a LOGFONT (logical font) structure defining some properties of the fonts we wish to enumerate but for this example well just set the default properties to enumerate all available fonts:
Private Const LF_FACESIZE As Long = 32 Private Type LogFont lfHeight As Long lfWidth As Long lfEscapement As Long lfOrientation As Long lfWeight As Long lfItalic As Byte lfUnderline As Byte lfStrikeOut As Byte lfCharSet As Byte lfOutPrecision As Byte lfClipPrecision As Byte lfQuality As Byte lfPitchAndFamily As Byte lfFaceName(LF_FACESIZE - 1) As Byte End Type

Http://www.mvps.org/EDais/

21 EDais DC tutorial
... Dim FontInf As LogFont ' Set to enumerate all fonts FontInf.lfCharSet = DEFAULT_CHARSET FontInf.lfPitchAndFamily = 0 FontInf.lfFaceName(0) = 0

Now well need a function matching the call-back function signature for it to call into:
Private Const LF_FULLFACESIZE As Long = 64 Private Type EnumLogFontEx elfLogFont As LogFont elfFullName(LF_FULLFACESIZE - 1) As Byte elfStyle(LF_FACESIZE - 1) As Byte elfScript(LF_FACESIZE - 1) As Byte End Type ... Private Function EnumFontFamExProc(ByRef lpELFX As EnumLogFontEx, _ ByVal lpNTME As Long, ByVal FontType As Long, ByVal lParam As Long) As Long ... End Function

In this case Ive actually simplified the example by ignoring the NEWTEXTMETRICEX structure it passes us back, however if you need it then your function header would look like this instead:
Private Function EnumFontFamExProc(ByRef lpELFX As EnumLogFontEx, _ ByRef lpNTME As NewTextMetricEx, ByVal FontType As Long, _ ByVal lParam As Long) As Long

The information were interested in in this case is contained within the EnumLogFontEx structure, however since all the strings it returns are stored as byte arrays well need to use the StrConv() function to convert them to VB strings to display them:
Debug.Print """" & _ TrimNull(StrConv(lpELFX.elfFullName, vbUnicode)) & """ " & _ TrimNull(StrConv(lpELFX.elfStyle, vbUnicode)) & " (" & _ TrimNull(StrConv(lpELFX.elfScript, vbUnicode)) & ")"

Youll see that Im using a TrimNull() function here to remove any extra junk from the end of the string before displaying it:
Private Function TrimNull(ByRef inString As String) As String Dim NullPos As Long NullPos = InStr(1, inString, vbNullChar) If (NullPos) Then TrimNull = Left$(inString, NullPos - 1) Else TrimNull = inString End Function

Http://www.mvps.org/EDais/

22 EDais DC tutorial Finally well return 1 to indicate to the API that we wish to continue enumeration:
EnumFontFamExProc = 1 ' Return 1 to continue enumeration

All thats left is to kick off the enumeration by calling the API function and passing it the address of the call-back function:
Private Declare Function EnumFontFamiliesEx Lib "GDI32.dll" _ Alias "EnumFontFamiliesExA" (ByVal hDC As Long, _ ByRef lpLogFont As LogFont, ByVal lpEnumFontFamProc As Long, _ ByVal lParam As Long, ByVal dwFlags As Long) As Long ... Call EnumFontFamiliesEx(inDC, FontInf, AddressOf EnumFontFamExProc, 0, 0)

Using our default call-back function listed above, this will simply print the information out to the debug window however you could use this to populate a font list combo for example. Once again, to retrieve the properties of a font object we can use the GetObject() API call, which will return us a LOGFONT structure containing the information about the font. Since these properties are effectively just mirroring those we passed to CreateFont() I wont go through them again, the code example for this chapter includes a demonstration of this. When it comes to text output, there are numerous properties of the DC that affect how the text is displayed. Weve already seen how the graphics mode property of the DC allows for rotation of the individual characters from the baseline, and back in chapter 2 we saw how the text colour and background mode properties of the DC change the output. There are also a few others that will affect the output such as the text alignment and justification. There are numerous functions that perform text output which give different options for output formatting depending on what you require, however the easiest of these is the TextOut() call we used briefly back in chapter 2. TextOut() is used for simple text rendering where you need only draw a single line of text, and its output is affected by the current text alignment mode of the target DC which is retrieved/set via the Get/SetTextAlign() API calls. The majority of the alignment options are reasonably self-explanatory, however this also allows you to specify whether the current position of the DC should be used via the TA_UPDATECP text align flag. If this flag is set then the position you render the string to is ignored and instead the text is draw at the current position of the DC, then after the call the horizontal coordinate of the current position is updated to reflect the width of the string, so a subsequent call to TextOut() will draw the two strings next to one-another. The current position is again a property of the DC, but here there is a discrepancy from the normal pattern as is it retrieved/set with the GetCurrentPositionEx()/MoveToEx() API calls (There is no SetCurrentPositionEx() call.) One downfall of TextOut() is that it will not properly render strings with line break or tab characters in them, so to properly expand the tab characters you can use the Http://www.mvps.org/EDais/

23 EDais DC tutorial TabbedTextOut() API call which operates in much the same way as TextOut() but allows you to specify tab-stop positions to which the text will be aligned to:
Dim Tabs(1) As Long Const DrawString As String = "String" & vbTab & "more" Tabs(1) = 60 Call TabbedTextOut(Form1.hDC, 0, 0, DrawString, Len(DrawString), 2, Tabs(0), 0) Call TabbedTextOut(Form1.hDC, 0, 30, DrawString, Len(DrawString), 2, Tabs(0), 20)

The last parameter here defines an overall offset for the tab-stops, so the second string will appear to have a larger gap between the two words even though the tab-stop distance is still the same. This still doesnt allow us to render strings with line-breaks in them, and for that we have to shift up a gear and call in a more powerful API text rendering call, DrawText(). Rather than taking a single coordinate at which to draw the text, DrawText() takes a rectangle area into which you have various options for how you want the text to be rendered. By default the text is clipped to this rectangle however specifying the DT_NOCLIP flag in the options parameter of this call will prevent this behaviour. The call ignores the current text alignment mode so it too has left/middle/right/top/vertical centre/bottom alignment flags which specify how the text is aligned within the given rectangle, however vertical alignment can only be used with single line strings (specify the DT_SINGLELINE flag.) Line-break characters are supported by this call (as long as DT_SINGLELINE is not set) and word-wrap is also supported by specifying the DT_WORDBREAK flag which will wrap long lines if they extend beyond the width of the rectangle. Tab characters are expanded if the DT_EXPANDTABS flag is set, but in contrast to TabbedTextOut(), you can also optionally specify the tab size from between 0 and 255 characters by specifying the DT_TABSTOP flag and using the high byte of the low word to store the desired tab-stop size (see the code for this chapter for an example.) Prefix characters (also known as keyboard accelerators, most commonly seen on menus) are by default parsed, and specified by the ampersand symbol before a character. To display an ampersand symbol within the string but without having it interpreted as a prefix escape, use a double ampersand. You have various options on how the prefix is interpreted, by default it is pares and rendered however if you dont require prefixes then specify the DT_NOPREFIX flag which turns off prefix parsing. The two other options are to parse but not draw the prefix, and to only draw the prefix, specified by the DT_HIDEPREFIX and DT_PREFIXONLY flags respectively. As mentioned above if the text is larger than the given area it will be clipped, however little indication is given to the user that this text has been clipped. The normal way of indication that there is more text available than what is currently displayed is by the use of ellipses or three period symbols. DrawText() supports three different ellipses Http://www.mvps.org/EDais/

24 EDais DC tutorial modes, the most useful are the word and path modes which are specified by the DT_WORD_ELLIPSIS and DT_PATH_ELLIPSIS flags respectively. Word ellipses is the one youre most likely used to, where the last rendered word is replaced by ellipses if clipped indicating to the user more text is available but not displayed. Path ellipses are slightly more complicated but useful when displaying long file paths, you may have noticed these in use in file copy dialogs and such. In this mode the start and end of the string is displayed specifying the drive/base-path(s) and filename of the path, but sub-folders between them are included only as long as there is available space, at which point ellipses are added to indicate there more that havent been displayed. The final formatting flag well cover with this call is the DT_CLACRECT which is the odd one of the bunch in then when its specified it prevents the call for actually drawing anything but calculates the size of the string and returns it through the rectangle area property. This one can be particularly useful where you wish to find the size of a formatted string however imposes some restrictions upon which other flags can be specified with it. See the MSDN for details on this and the demo code for this chapter for an example for all the formatting flags mentioned here.

Http://www.mvps.org/EDais/

25 EDais DC tutorial

Chapter V I

GDI Regions

GDI Regions hold information about an area and are utilised in numerous places including drawing, clipping and hit-testing. Probably the most common use for regions is for setting the clipping area of a window, whats known as its window region, using the SetWindowRgn() API call. Since this is more of a UI topic it will not be covered here however all the region code presented in this chapter would be compatible with the call, which may open up some interesting possibilities later on. There are numerous calls to create region objects from various primitive shapes, so well dive right in with CreateRectRgn() which creates a region with a single rectangular area defined within it:
Private Declare Function CreateRectRgn Lib "GDI32.dll" (ByVal X1 As Long, _ ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long Private Declare Function DeleteObject Lib "GDI32.dll" (ByVal hObject As Long) As Long Private Declare Function CreateSolidBrush Lib "GDI32.dll" (ByVal crColor As Long) As Long Private Declare Function FillRgn Lib "GDI32.dll" (ByVal hDC As Long, _ ByVal hRgn As Long, ByVal hBrush As Long) As Long Private Sub Form_Paint() Dim hRgn As Long Dim hBrush As Long hRgn = CreateRectRgn(0, 0, 100, 100) hBrush = CreateSolidBrush(vbRed) Call FillRgn(Form1.hDC, hRgn, hBrush) Call DeleteObject(hBrush) Call DeleteObject(hRgn) End Sub

Now you may be looking at this code and thinking that its a bit overkill for just drawing a filled red rectangle and youd be right, however thankfully this isnt all regions have to offer us. There are three types of region the GDI is capable of defining; null, simple and complex: A null region is a valid region handle but contains no area, which you can create using CreateRectRgn(0, 0, 0, 0). For the most part its not very useful and is only usually seen when combining multiple regions which well be looking at in a second. A simple region is one which can be defined by a single rectangle such as in the previous code example, while a complex region is one that must be defined by multiple rectangles. Contrary to how it may appear from a quick look through the various region creation calls, regions are actually stored as raster rather than vector shapes meaning that all regions are actually a sequence of rectangles defining one or more scan-lines each rather than a series of points in 2D space.

Http://www.mvps.org/EDais/

26 EDais DC tutorial Probably the most useful feature about regions is that they can be combined with other regions to create more complex shapes, this is performed using the CombineRgn() API call:
Private Declare Function CombineRgn Lib "GDI32.dll" ( _ ByVal hDestRgn As Long, ByVal hSrcRgn1 As Long, _ ByVal hSrcRgn2 As Long, ByVal nCombineMode As Long) As Long

CombineRgn() takes three input region handles; one for the output region and two source regions which it should combine. All of these handles including the destination handle must be valid GDI region objects, so the destination region is usually specified as either one of the source regions or a null region. The call also takes a combination mode parameter which defines how the final region is calculated from the two source areas, the following table summarises the different modes: Mode RGN_AND RGN_OR RGN_XOR RGN_DIFF Description Calculates the intersection of the two source areas (the area they have in common.) Calculates the union of the two areas (the area either occupy.) Takes the union of the two areas and subtracts their intersection Calculates the difference between the first region and the second (this subtracts the second region from the first so the order in which the two source regions are passed will affect the resulting region.) Returns a copy of the first region, not exactly a region combination mode as such but useful for creating a clone region.

RGN_COPY

Heres a little diagram showing the resulting region when combining a circle and square region using the different combination modes (apart from copy mode which is self explanatory):

The last two are both using difference combination mode, however the order in which the two regions were passed to the call were reversed showing how it affects the resulting region. Unlike other GDI objects the GetObject() method is not used to retrieve data about the region, instead the GetRegionData() call is used which fills a RGNDATA structure with information about the given region handle. Unfortunately we get stung in VB

Http://www.mvps.org/EDais/

27 EDais DC tutorial again in the same way as when using an EXTLOGPEN structure, since RGNDATA is a variable sized structure and VBs dynamic arrays cause problems with the SAFEARRAY header table getting in the way again. To bypass this, Ive written a little wrapper function which uses the same technique as we used back in chapter 4 but just automates the process:
Private Declare Function GetRegionData Lib "GDI32.dll" ( _ ByVal hRgn As Long, ByVal dwCount As Long, ByRef lpRgnData As Any) As Long Private Type RectAPI Left As Long Top As Long Right As Long Bottom As Long End Type Private Type RgnDataHeader dwSize As Long iType As Long nCount As Long nRgnSize As Long rcBound As RectAPI End Type Private Type RgnDataVB rdh As RgnDataHeader Buffer() As RectAPI End Type Private Function GetRegionDataVB(ByVal inRgn As Long) As RgnDataVB Dim RgnData() As Long, DataSize As Long Dim HeadSize As Long Dim RectSize As Long Dim NumRect As Long DataSize = GetRegionData(inRgn, 0, ByVal 0&) If (DataSize > 0) Then ' Get structure sizes HeadSize = Len(GetRegionDataVB.rdh) RectSize = Len(GetRegionDataVB.rdh.rcBound) ReDim RgnData(DataSize \ 4) As Long Call GetRegionData(inRgn, DataSize, RgnData(0)) NumRect = (DataSize - HeadSize) \ RectSize If (NumRect = RgnData(2)) Then ' Populate VB UDT with region data ReDim Preserve GetRegionDataVB.Buffer(NumRect - 1) As RectAPI Call RtlMoveMemory(GetRegionDataVB.rdh, RgnData(0), HeadSize) Call RtlMoveMemory(GetRegionDataVB.Buffer(0), _ RgnData(HeadSize \ 4), NumRect * RectSize) End If End If End Function

This function simply takes a region handle and acts as the interpreter between the API and VB, returning a RgnDataVB structure which is based on the API RGNDATA structure but supports a dynamic array of rectangle structures.

Http://www.mvps.org/EDais/

28 EDais DC tutorial The ExtCreateRegion() API call goes the other way, that is it takes a RGNDATA structure and creates a GDI region object from it, however again there will be problems with VBs dynamic arrays so a second wrapper function is required:
Private Declare Function ExtCreateRegion Lib "GDI32.dll" ( _ ByRef lpXform As Any, ByVal nCount As Long, ByRef lpRgnData As Any) As Long Private Const RDH_RECTANGLES As Long = &H1 Private Function ExtCreateRegionVB(ByRef inData As RgnDataVB, _ Optional ByVal inXFormPtr As Long = &H0, _ Optional ByVal inCalculateBounds As Boolean = True) As Long Dim NumRects As Long Dim LoopRect As Long Dim RgnData() As Long On Error Resume Next ' Get number of defined areas NumRects = UBound(inData.Buffer()) + 1 On Error GoTo 0 If (NumRects > 0) Then ' Fill region data header inData.rdh.dwSize = Len(inData.rdh) inData.rdh.iType = RDH_RECTANGLES inData.rdh.nCount = NumRects inData.rdh.nRgnSize = NumRects * Len(inData.Buffer(0)) If (inCalculateBounds) Then inData.rdh.rcBound = inData.Buffer(0) If (NumRects > 1) Then ' Calculate complex region bounds rect. For LoopRect = 1 To NumRects - 1 With inData.Buffer(LoopRect) If (.Left < inData.rdh.rcBound.Left) Then _ inData.rdh.rcBound.Left = .Left Else _ If (.Right > inData.rdh.rcBound.Right) Then _ inData.rdh.rcBound.Right = .Right If (.Top < inData.rdh.rcBound.Top) Then _ inData.rdh.rcBound.Top = .Top Else _ If (.Bottom > inData.rdh.rcBound.Bottom) Then _ inData.rdh.rcBound.Bottom = .Bottom End With Next LoopRect End If End If ' Create flat data buffer and copy region data to it ReDim RgnData(((inData.rdh.dwSize + inData.rdh.nRgnSize) \ 4) - 1) As Long Call RtlMoveMemory(RgnData(0), inData, inData.rdh.dwSize) Call RtlMoveMemory(RgnData(inData.rdh.dwSize \ 4), _ inData.Buffer(0), inData.rdh.nRgnSize) ExtCreateRegionVB = ExtCreateRegion(ByVal inXFormPtr, _ inData.rdh.dwSize + inData.rdh.nRgnSize, RgnData(0)) End If End Function

This function takes three parameters; the first is the RgnDataVB structure containing information about the region, the second is an optional pointer to a 2D transformation matrix in which to transform the given region data a bit of an odd way to pass a structure to the call but this way it can be made optional and doesnt require the

Http://www.mvps.org/EDais/

29 EDais DC tutorial XFORM type declaration if its not going to be used. The final parameter allows you to skip the potentially quite expensive bounds rectangle checking if its already been calculated. By using these two wrapped methods together, you can get some interesting derived functionality such as this little function:
Private Function TransformRgn(ByVal inRgn As Long, ByRef inTrans As XForm) As Long TransformRgn = ExtCreateRegionVB(GetRegionDataVB(inRgn), VarPtr(inTrans), False) End Function

This one-liner extracts the information about a GDI region, transforms it by a given 2D transformation matrix and returns the new region. For more on transformation matrices and how theyre used, have a look at chapter 8. You may be wondering at this point how this region data actually maps to the region itself, well take the combination region for the square and circle shown earlier in this chapter as an example:

As you can see the resulting complex region is actually stored as 37 separate rectangular scan area's which are coloured in alternating stripes to make it easier to see, while the bounding rectangle for the region as a whole is shown in red.

Http://www.mvps.org/EDais/

30 EDais DC tutorial

Chapter V I I

GDI Paths

GDI Paths are different from most GDI object types in that they have no handle; instead they are always bound to a device context. They are also not created in the usual way i.e. some kind of CreatePath() call, instead they are created by switching the DC into a special path recording mode which intercepts various drawing commands turning them into vector paths. To switch the DC into this path recording mode, we use the BeginPath() API call:
Private Declare Function BeginPath Lib "GDI32.dll" (ByVal hDC As Long) As Long

This call opens whats known as the Path bracket on the target DC and subsequent GDI drawing calls will be interpreted as path content rather than being drawn to the DCs Bitmap object. Not all GDI drawing calls are supported by the path bracket; for a list of those that are and on which OS, consult the MSDN documentation on the BeginPath() call. Once path recording has been completed on the target DC, the EndPath() call closes the path bracket for the DC and returns it to a normal drawing sate:
Private Declare Function EndPath Lib "GDI32.dll" (ByVal hDC As Long) As Long

The path object is still bound to the DC at this point in the same way as a normal GDI object is selected into the DC, however again we dont get direct access to it but use various GDI calls instead. Since a path is just a series of lines which can be drawn using the StrokePath() API call, it makes drawing outlined text very easy:
Private Declare Function StrokePath Lib "GDI32.dll" (ByVal hDC As Long) As Long Private Sub Form_Paint() Const DrawString As String = "Outline" ' Since VB uses GDI behind the scenes, this actually sets the API font for the DC Form1.Font.Size = 60 Form1.Font.Name = "Arial" Call BeginPath(Form1.hDC) Call TextOut(Form1.hDC, 10, 10, DrawString, Len(DrawString)) Call EndPath(Form1.hDC) Call StrokePath(Form1.hDC) End Sub

One downfall of this is that GDI has no anti-aliasing support for shapes and as such you can only draw aliased text outlines using this technique. As we can see in the previous demo, paths can be constructed of many individual shapes which can either be open or closed shapes. The TextOut() call above will write the letter outlines into the DCs path which will more than likely always be closed shapes, however if youre using calls that generate non-closed shapes such as lines and poly-lines then the CloseFigure() API call can be used to close the current shape. Since paths have no GDI object handle to refer to them by we cant use our old friend GetObject() to retrieve information about them, instead GDI exposes another call specifically for retrieving path data, GetPath().

Http://www.mvps.org/EDais/

31 EDais DC tutorial GetPath() retrieves the list for points for the current path and also a list of point types for each of the points in the path. Each point can be either a Move to, Line to or Bezier to drawing command and any of them can also close the current figure by specifying the PT_CLOSEFIGURE flag. The Bezier point style is slightly different from the others because a Bezier curve is defined by 4 points and as such they always occur in sets of three in the path data, not 4 as may be expected since each point is being drawn from the last one so the previous point is the start of the curve. The first two points define the off-line control points from the start and end respectively and the third specifies the curve end point. Calling GetPath() and passing 0 as the number of points to extract returns the number of points currently in the path, so extracting the path data is simply a case of creating a couple of arrays at the right size and calling GetPath() a second time to fill them:
Private Declare Function GetPath Lib "GDI32.dll" (ByVal hDC As Long, _ ByRef lpPoints As Any, ByRef lpTypes As Any, ByVal nSize As Long) As Long ... Dim PointCoords() As PointAPI Dim PointTypes() As Byte Dim NumPoints As Long NumPoints = GetPath(hDC, ByVal 0&, ByVal 0&, 0) If (NumPoints) Then ReDim PointCoords(NumPoints - 1) As PointAPI ReDim PointTypes(NumPoints - 1) As Byte ' Get the path data from the DC Call GetPath(hDC, PointCoords(0), PointTypes(0), NumPoints) <> 0 End If

The following illustration shows a path consisting of two joined lines and an ellipse, then its filled profile and finally the path data stored when creating it The green circles show where individual figures within the path start and the red square shows where they end. The blue handles show the Bezier control handles for the curves making up the ellipse (note; the Ellipse() call is only supported in a path bracket in Win2K+)

As you can see, when a path is filled any non-closed shapes are closed before it works out the fill areas. Sometime its not convenient to work with the Bezier curve data so the FlattenPath() API call can be used to convert these curves into a sequence of straight line segments. Heres the above path data after having flattened it:

Http://www.mvps.org/EDais/

32 EDais DC tutorial

Back in chapter 4 we looked at geometric pens which we found converted the area they draw into a polygon region and filling it, paths give us the inside story on the matter via the WidenPath() API call which has the effect of stroking the path with the currently selected pen and re-creating the path from it. The illustrations below show the contents of the path bracket after widening the path with a couple of different 20-pixel wide brushes:

Square end caps, and bevel joins

Round end caps and joins

As you can see GDIs path widening is fairly primitive, taking the flat version of the path and extruding points out at right angles to it, often causing anomalies where lines meet sharply and end up as two overlapping regions Youll see this a lot on the inside edge of the circles and where the two lines join. This also causes us a problem when filling the shape as seen below, the original path is shown in black:

Alternate polygon fill mode

Winding polygon fill mode

The area where the shape overlaps itself is hollow in the left illustration since the DCs polygon fill mode was set to alternate. This can be changed to fill these overlapping areas by calling the SetPolyFillMode() API call and passing it the WINDING flag which gives the correct result as shown on the right. Paths can be a little annoying to work with since after most calls using the path, it is removed from the DC and as such to perform multiple operations on the path it must

Http://www.mvps.org/EDais/

33 EDais DC tutorial be created multiple times. To get around this limitation we can store the path data locally by using the GetPath() function mentioned earlier then use the PolyDraw() API call to draw this back into the DC while in path recording mode to restore the path data. PolyDraw() takes data in exactly the same format as the path data we receive from the GetPath() call so its relatively painless to draw it back in again:
Private Declare Function PolyDraw Lib "GDI32.dll" (ByVal hDC As Long, _ ByRef lpPt As PointAPI, ByRef lpbTypes As Byte, ByVal cCount As Long) As Long ... Dim PointCoords() As PointAPI Dim PointTypes() As Byte Dim NumPoints As Long ' Create path here and buffer data, see the previous code example ' GetPath( ... ) ' Perform operation on path Call FillPath(hDC) ' At this point the path has been removed from the DC so we'll restore it Call BeginPath(hDC) Call PolyDraw(hDC, PointCoords(0), PointTypes(0), NumPoints) Call EndPath(hDC) ' Now we can perform a second operation on the DC without having to re-create it Call StrokePath(hDC)

See the accompanying code for this chapter for a path class which wraps up this functionality and automates saving and restoring the path in a DC as well as drawing the path data itself.

Http://www.mvps.org/EDais/

34 EDais DC tutorial

Chapter V I I I

Mapping modes

In the code examples presented so far in this article Ive assumed that coordinates are measure in pixels which is the default mapping mode with the API, otherwise known as Text mapping mode. There are 8 different mapping modes available through GDI and two of these allow you to define your own custom mode, the mapping mode is a setting/property of the DC and can be retrieved/set with the Get/SetMapMode() API calls. The mapping mode controls what is known as the logical coordinate space which is later mapped to the coordinate space of the display device itself which, as you may guess, is called device coordinate space. There are two functions defined by the API which allow you to convert between these two coordinate spaces, those being LPtoDP() to convert from logical to device coordinate space, and DPtoLP() to perform the reverse calculation. Unless otherwise stated all drawing coordinates and scales in GDI are in the logical coordinate space so to draw something at a specific scale in device space, the coordinates must be transformed into the corresponding logical space versions in the local mapping mode. The reason for having varying mapping modes is that GDI is designed to be device independent and as such has to cope with drawing to varying devices which may have very different display capabilities. When drawing to the screen for example were generally working at a scale of about 72, 96 or 120 pixels per inch depending on your hardware and display settings. When rendering for display on a printer device on the other hand the pixel granularity is usually a lot finer and with modern devices its not uncommon to work at a resolution of thousands of pixels per inch. By using mapping modes generic code can be written to work in either situation and GDI will perform the mapping mode conversions behind the scenes to work optimally on the given display device. In NT-based OS were given another method of transforming 2D drawing by way of the world transformation matrix which is capable of more complex transformations than the page to device mapping, such as rotation and shearing. On Win9x and WinME the world space isnt supported and effectively maps 1:1 to the page space, so to perform complex transformations on these OS the matrix transformation must be performed on the drawing coordinates before sending them to the GDI, something well look at later in this chapter. When drawing in GDI we work in world coordinate space, this then gets transformed to page space by the world transformation matrix where supported. Page space is then mapped into device space via the mapping mode for the current DC which rescales (sometimes also performing primitive reflection if the axes are reversed into Cartesian coordinate space) and offsets to the new origin. Finally this is mapped to physical device space which is simply a transformation to the origin of the physical media over which we have no control.

Http://www.mvps.org/EDais/

35 EDais DC tutorial World space The original drawing is performed in world space but in logical (page) coordinates. Page space The drawing is transformed into page space by the world transform matrix. Here a 45 rotation and offset to a central origin has been applied by the matrix. This transformation is capable of moving, scaling, rotating, shearing or reflection of the original drawing, and any number of these can be combined into a single operation by using matrix multiplication.

Device space Finally the DCs mapping mode scales the drawing for output to the physical device and offsets it to the devices origin. Here the drawing is re-scaled and offset to a new origin. As mentioned earlier, there are 6 predefined mapping modes implemented by GDI and a further two allowing you to create your own modes with either equally (isotropic) or unequally (anisotropic) scaled axis. Of the 6 predefined ones the one youll be most familiar with is Text mapping mode which simply maps page to device space 1:1 and generally is only used for on-screen graphics. The next two operate in the metric system, which allows you to specify coordinates in millimetres which then get mapped based on the physical size and resolution of the output device. There are two modes defined here with different levels of granularity; the first, low-metric, is measured in 1/10ths of millimetres where as high-metric mode is measured in 1/100ths of millimetres allowing for finer output resolution and both have a negative Y-axis. The next two are similar to the last ones but work in inches instead; lo-English works in 1/100ths of inches and hi-English works in 1/1000ths of inches and again they both have negatively scaled Y-axis. The sixth mode, twips, is one youre likely familiar with if youve worked with VB forms, but works in a slightly different way to the VB scale-mode with the same name since it also has a negatively oriented Y-axis. A twip is measured as 1/1440th of an inch or 1/20th of a point Http://www.mvps.org/EDais/

36 EDais DC tutorial The Isotropic and Anisotropic scale-modes allow you to define your own mapping mode by setting the window and view-port extents. The window extent is essentially the logical space and the view-port extent defines its mapping in device space and these can be got at with the GetWindow[OrgEx/ExtEx]() and GetViewport[OrgEx/ExtEx]() API calls, and set with SetWindow[OrgEx/ExtEx]() and SetViewport[OrgEx/ExtEx](). When setting a custom scale mode you must call these methods in a specific order to get the desired results, that being:
SetMapMode( ... ) SetWindow[Org/Ext]Ex( ... ) SetViewport[Org/Ext]Ex( ... )

The reason for setting the mapping mode first is that the window and view-port origin and extent calls are ignored unless the mapping mode of the DC has been set to something that uses them. The ordering of the window and view-port calls is defined by the API, the ordering of the origin/extent calls for either the view-port or window dont matter but the window must be set up before the view-port. If youre drawing to a shared DC and changing the mapping modes on it for your own drawing try and clean up in the reverse order in which you called them during setup (you may wish to reverse the window and view-port calls in cleanup to comply with the API ordering though), this is a quite common technique in GDI programming in general anyway. This should ensure that the DC remains the same when your method returns which is always good practice. Heres a quick example of how to set up a simple isotropic scale-mode on a DC which reduces the drawing size by half and offsets the origin:
Private Declare Function SetWindowExtEx Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nX As Long, ByVal nY As Long, ByRef lpSize As Any) As Long Private Declare Function SetViewportExtEx Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nX As Long, ByVal nY As Long, ByRef lpSize As Any) As Long Private Declare Function SetMapMode Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nMapMode As Long) As Long Private Declare Function Rectangle Lib "GDI32.dll" (ByVal hDC As Long, _ ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) As Long Private Type PointAPI X As Long Y As Long End Type Private Const MM_ISOTROPIC As Long = 7 Private Sub Form_Paint() Dim OldWnd As PointAPI, OldExt As PointAPI Dim OldMode As Long Dim hPen As Long, hOldPen As Long OldMode = SetMapMode(Me.hDC, MM_ISOTROPIC) Call SetWindowExtEx(Me.hDC, 1, 1, OldWnd) ' Map 1 pixel in logical space... Call SetViewportExtEx(Me.hDC, 2, 2, OldExt) ' ... to 2 pixels in device space hPen = CreatePen(PS_SOLID, 1, vbRed) hOldPen = SelectObject(Me.hDC, hPen) Call Rectangle(Me.hDC, 10, 10, 90, 90) Call SelectObject(Me.hDC, hOldPen) Call DeleteObject(hPen)

Http://www.mvps.org/EDais/

37 EDais DC tutorial
Call SetWindowExtEx(Me.hDC, OldWnd.X, OldWnd.Y, ByVal 0&) Call SetViewportExtEx(Me.hDC, OldExt.X, OldExt.Y, ByVal 0&) Call SetMapMode(Me.hDC, OldMode) End Sub Private Sub Form_Resize() Call Me.Refresh End Sub

Notice that even though weve defined the pen with a width of 1 that the rectangle is still drawn with a 2-pixel border. This, as you may have guessed, is because the pen size is measured in logical coordinate space, so when drawn to the device is also rescaled and converted to a 2-pixel wide pen. The same does not however hold true for brushes which are drawn using their normal scale regardless of the coordinate space, however custom Bitmap pattern brushes can be created based on the current mapping mode scale to simulate the effect. In the above demonstration weve not had to deal with the world transformation matrix, which is only calculated if the graphics mode of the DC is set to advanced mode. The world transform is defined by the XForm structure:
Private Type XForm eM11 As Single eM12 As Single eM21 As Single eM22 As Single eDx As Single eDy As Single End Type

Note; this structure is incorrectly defined in the standard Win32 API viewer that ships with VB6, the above declaration is correct. The XForm structure represents a 2*3 transformation matrix in the following order: eM11 eM21 eDx eM12 eM22 eDy

By default the transformation matrix of a DC is set to a 1:1 mapping between world and page space known as the identity matrix: 1 0 0 1 0 0 If youve never worked with matrices before then dont worry, this isnt a maths lesson and for the most part you need not worry about how they work. If you want to learn more about matrices and transformations then youll find plenty of information online. The identity matrix simply transforms any given point back into itself again and so no visible transformation occurs.

Http://www.mvps.org/EDais/

38 EDais DC tutorial The simplest of matrix operations is a translation which is accomplished by setting the Dx and Dy members of the XForm structure: 1 0 0 1 X Y The X and Y offsets here are measured in logical coordinate space (assuming no scaling is being applied by the matrix) and simply move the drawing relative to its origin. The next type of transformation is a scale which is performed by a matrix similar to the identity matrix: X 0 0 0 Y 0

This may now make the identity matrix seem a little more obvious - its simply a 1:1 scale. By entering 2 for X for example, it will scale the horizontal axis up to double its original scale. So far this can all be performed equally well by the page to device space mapping mode, but here is where the world transforms start to shine; rotation: Cos(Rot) -Sin(Rot) 0 Sin(Rot) Cos(Rot) 0

Rot in the above table defines the angle to rotate about the origin. To convert an angle from degrees to radians simply divide by 180 and multiply by Pi. A shear transformation is defined with the following matrix: 1 X 0 Y 1 0

Reflections can be performed by simply inverting one of other axis; here are horizontal and vertical reflection matrices: -1 0 0 0 1 0 1 0 0 0 -1 0

Horizontal reflection

Vertical reflection

Http://www.mvps.org/EDais/

39 EDais DC tutorial Before getting into combination transforms, lets run through a quick example of how to use a transformation matrix:
Private Declare Function SetGraphicsMode Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal iMode As Long) As Long Private Declare Function GetWorldTransform Lib "GDI32.dll" ( _ ByVal hDC As Long, ByRef lpXform As XForm) As Long Private Declare Function SetWorldTransform Lib "GDI32.dll" ( _ ByVal hDC As Long, ByRef lpXform As XForm) As Long Private Type XForm eM11 As Single eM12 As Single eM21 As Single eM22 As Single eDx As Single eDy As Single End Type Private Const GM_ADVANCED As Long = 2 Private Sub Form_Paint() Dim OldMode As Long Dim OldXForm As XForm, MyXForm As XForm MyXForm.eM11 = 1 MyXForm.eM22 = 1 MyXForm.eDx = 100 ' Create simple translation matrix MyXForm.eDy = 50 ' Set graphics mode to advanced mode to use world transformation OldMode = SetGraphicsMode(Me.hDC, GM_ADVANCED) ' Get current transformation matrix Call GetWorldTransform(Me.hDC, OldXForm) ' Apply new transformation matrix Call SetWorldTransform(Me.hDC, MyXForm) ' Draw rectangle Call Rectangle(Me.hDC, 0, 0, 150, 100) ' Re-set world transformation matrix Call SetWorldTransform(Me.hDC, OldXForm) ' Re-set graphics mode Call SetGraphicsMode(Me.hDC, OldMode) End Sub Private Sub Form_Resize() Call Me.Refresh End Sub

All this does is to apply a simple translation matrix to offset the origin of the drawn rectangle. If all went well the rectangle should be floating away from the top left corner, if its still sitting in the top left corner of the form then either the code or declares are incorrect or its not supported on your OS. Presuming everything went well though you can use the above to test the other transformation matrices if you wish to get a feel for how they modify the drawing.

Http://www.mvps.org/EDais/

40 EDais DC tutorial Since the effects operate about the origin of the surface, it will often mean that the drawing is moves off the surface on a standard DC since the origin is in the top left corner, this is especially apparent for rotation matrices. To get a better feel for how they operate, you may want to offset the origin for the DC into the centre of the form before it draws. To do that well get the size of the window and use the SetViewportOrgEx() API call to set the origin to the middle of this area before drawing with the GDI calls. This is a good example of how world and page space transformations can often be used together to create the final device mapping. First off youll need to get the size of the window which we can do with the GetClientRect() API call and its associated rectangle structure:
Private Declare Function GetClientRect Lib "User32.dll" ( _ ByVal hWnd As Long, ByRef lpRect As RectAPI) As Long Private Type RectAPI Left As Long Top As Long Right As Long Bottom As Long End Type ... Dim WndArea As RectAPI ' Get window size Call GetClientRect(Me.hWnd, WndArea)

Now since weve going to be drawing at different positions depending on the size of the form, well need to clear the background to get rid of any previous drawings which can be accomplished with the FillRect() call. We already have the window area, so all well need is a brush to fill it with so just borrow a stock system colour brush of the button face colour:
Private Declare Function FillRect Lib "User32.dll" ( _ ByVal hDC As Long, ByRef lpRect As RectAPI, ByVal hBrush As Long) As Long Private Declare Function GetSysColorBrush Lib "User32.dll" (ByVal nIndex As Long) As Long Private Const COLOR_BTNFACE As Long = 15 ... ' Clear background Call FillRect(Me.hDC, WndArea, GetSysColorBrush(COLOR_BTNFACE))

Note; Since the stock brush is owned by Windows we dont need to worry about creating and destroying it and can pass the result of the GetSysColorBrush() API call directly into FillRect(). If this was a pen created by us using the CreatePen(), CreatePenIndirect() or ExtCreatePen() API calls then it would have to be explicitly deleted after the call.

Http://www.mvps.org/EDais/

41 EDais DC tutorial You can now offset the origin into the centre of the window and be sure to re-set it afterwards otherwise the window wont refresh properly:
Private Declare Function SetViewportOrgEx Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal nX As Long, ByVal nY As Long, ByRef lpPoint As Any) As Long Private Type PointAPI X As Long Y As Long End Type ' Set view-port origin to centre of window Call SetViewportOrgEx(Me.hDC, WndArea.Right \ 2, WndArea.Bottom \ 2, OldOrg) ... ' Re-set view-port origin Call SetViewportOrgEx(Me.hDC, OldOrg.X, OldOrg.Y, ByVal 0&)

Finally, change the Rectangle() call to draw around the origin:


Call Rectangle(Me.hDC, -75, -50, 75, 50)

At this point rotation and scale matrices look a lot better, for example try changing your transformation matrix to this:
Const Pi As Single = 3.14159 Const RotAng As Single = 15 ' Rotation angle Const RotRad As Single = (RotAng / 180) * Pi MyXForm.eM11 = Cos(RotRad) MyXForm.eM12 = Sin(RotRad) MyXForm.eM21 = -MyXForm.eM12 MyXForm.eM22 = MyXForm.eM11

You should now see the rectangle in the centre of the form rotated by 15 degrees about its mid-point. In case not, heres the full code for this section so far:
Dim WndArea As RectAPI Dim OldOrg As PointAPI Dim OldMode As Long Dim OldXForm As XForm, MyXForm As XForm Const Pi As Single = 3.14159 Const RotAng As Single = 15 ' Rotation angle Const RotRad As Single = (RotAng / 180) * Pi MyXForm.eM11 = Cos(RotRad) MyXForm.eM12 = Sin(RotRad) MyXForm.eM21 = -MyXForm.eM12 MyXForm.eM22 = MyXForm.eM11 ' Get window size Call GetClientRect(Me.hWnd, WndArea) ' Clear background Call FillRect(Me.hDC, WndArea, GetSysColorBrush(COLOR_BTNFACE))

Http://www.mvps.org/EDais/

42 EDais DC tutorial
' Set view-port origin to centre of window Call SetViewportOrgEx(Me.hDC, WndArea.Right \ 2, WndArea.Bottom \ 2, OldOrg) ' Set graphics mode to advanced mode to use world transformation OldMode = SetGraphicsMode(Me.hDC, GM_ADVANCED) ' Get current transformation matrix Call GetWorldTransform(Me.hDC, OldXForm) ' Apply new transformation matrix Call SetWorldTransform(Me.hDC, MyXForm) ' Draw rectangle at origin Call Rectangle(Me.hDC, -75, -50, 75, 50) ' Re-set world transformation matrix Call SetWorldTransform(Me.hDC, OldXForm) ' Re-set graphics mode Call SetGraphicsMode(Me.hDC, OldMode) ' Re-set view-port origin Call SetViewportOrgEx(Me.hDC, OldOrg.X, OldOrg.Y, ByVal 0&)

Since these XForm structures can get a bit tedious to fill out each time, I generally use a function which emulates a constructor for them and allows you to populate and return one in a single line:
Private Function NewXForm( _ ByVal inM11 As Single, ByVal inM12 As Single, _ ByVal inM21 As Single, ByVal inM22 As Single, _ ByVal inDx As Single, ByVal inDy As Single) As XForm With NewXForm ' Set all the members of this structure .eM11 = inM11 .eM12 = inM12 .eM21 = inM21 .eM22 = inM22 .eDx = inDx .eDy = inDy End With End Function

Its then an obvious step to create a constructor for each of the various transformations, see the code for this chapter for an example. As mentioned before, any of these transformations can be combined to create more complex transformations. One common use of combining matrices is to offset the centre of effect of the transformation for example changing the point around which rotations or scales are applied:

Http://www.mvps.org/EDais/

43 EDais DC tutorial

To do this you first offset so that the origin is at the desired centre of effect then apply the transformation you wish and finally translate back to the original origin:

Original shape

Shape is moved so its centre is at the origin

Shape is rotated at origin

Shape is moved back to original position

Of course because of the way matrix concatenation works you never see the shape move to the origin and back again since its all contained within a single operation welcome to the wonderful world of transformation matrixes! In GDI the CombineTransform() API performs the complex task of matrix multiplication for us so all we need to do is create the appropriate XForm structures for the desired effects and set the result as the world transformation. The API function returns us the result matrix as a third parameter of this call where as it would be nice to simply have the new transform returned to us as the result of the method, to accomplish this we can wrap the call with a VB function:
Private Declare Function CombineTransform Lib "GDI32.dll" ( _ ByRef lpXFormResult As XForm, ByRef lpXForm1 As XForm, _ ByRef lpXForm2 As XForm) As Long ... Private Function CombineTransformVB( _ ByRef inA As XForm, ByRef inB As XForm) As XForm Call CombineTransform(CombineTransformVB, inA, inB) End Function

This method doesnt offer us any new functionality; it just makes calling the API call a litter easier and allows it to be performed in-line.

Http://www.mvps.org/EDais/

44 EDais DC tutorial One final thing to note about combination matrix transformations is that the order in which matrices are multiplied or applied is important and can completely change the outcome of the transformation. For example consider the example of applying a 15 rotation and translation of 100 in the X axis to a shape, the steps below show the effect of applying the matrices in different orders: Rotation then translation

Original shape

Rotation is applied Translation then rotation

Translation is applied

Original shape

Translation is applied

Rotation is applied

As you can see in the second example, the resulting shape ends up lower than the first since the rotation moves it over a greater distance (its a greater distance from the origin when applied.) As mentioned before, some of the older OS dont support the world transformation and as such these effects cant be achieved without manually transforming the coordinates before GDI gets them. The mathematics for transforming a point by a given 2*3 transformation matrix is well documented online so heres the function:
Private Sub TransformPoint(ByRef inTransform As XForm, _ ByRef inX As Single, ByRef inY As Single) Dim tX As Single, tY As Single With inTransform tX = (.eM11 * inX) + (.eM21 * inY) + .eDx tY = (.eM22 * inY) + (.eM12 * inX) + .eDy End With inX = tX inY = tY End Sub

If you want to transform a number of points in one go, which is also far more efficient than sending each one to the above method, then you could use something along these lines instead:

Http://www.mvps.org/EDais/

45 EDais DC tutorial
Private Sub TransformPoints(ByRef inTransform As XForm, ByRef inPts() As PointAPI) Dim tX As Single, tY As Single Dim PtsFrom As Long, PtsTo As Long Dim LoopPts As Long On Error Resume Next PtsTo = -1 ' Get array bounds PtsFrom = LBound(inPts()) PtsTo = UBound(inPts()) On Error GoTo 0 If (PtsTo > PtsFrom) Then For LoopPts = PtsFrom To PtsTo With inPts(LoopPts) ' Transform this point tX = (inTransform.eM11 * .X) + (inTransform.eM21 * .Y) + inTransform.eDx tY = (inTransform.eM22 * .Y) + (inTransform.eM12 * .X) + inTransform.eDy .X = tX .Y = tY End With Next LoopPts End If End Sub

Its likely that the CombineTransform() API would also not be available on these older OS so well also need a matrix multiplication routine:
Private Function CombineXForm(ByRef inA As XForm, ByRef inB As XForm) As XForm CombineXForm.eM11 = (inA.eM11 * inB.eM11) + (inA.eM12 * inB.eM21) CombineXForm.eM12 = (inA.eM11 * inB.eM12) + (inA.eM12 * inB.eM22) CombineXForm.eM21 = (inA.eM21 * inB.eM11) + (inA.eM22 * inB.eM21) CombineXForm.eM22 = (inA.eM21 * inB.eM12) + (inA.eM22 * inB.eM22) CombineXForm.eDx = (inA.eDx * inB.eM11) + (inA.eDy * inB.eM21) + inB.eDx CombineXForm.eDy = (inA.eDx * inB.eM12) + (inA.eDy * inB.eM22) + inB.eDy End Function

Armed with these methods it should be possible to emulate 2D affine transformations without relying on support from the API. It will be the case however that some drawing operations wouldnt work quite as expected since simply transforming their bounding coordinates would not result in a correctly transformed shape. In the case of a circle for instance, simply rotating the corner points would stretch/squash the ellipse rather than rotating it:

Desired result using world transform

Actual result using Ellipse() API with rotated corner points

Http://www.mvps.org/EDais/

46 EDais DC tutorial As you can see from the above diagram, while simply transforming the corner points will work fine for scaling and translations, rotations and skews cause it problems since the orientation of the shape must be altered. At this point the shapes need to be emulated as a series of points which can then be transformed as a generic polygon, heres an example of how to do this for an ellipse:
Private Declare Function Polygon Lib "GDI32.dll" (ByVal hDC As Long, _ ByRef lpPoint As PointAPI, ByVal nCount As Long) As Long Private Function TransformEllipse(ByVal inDC As Long, _ ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, _ ByVal Y2 As Long, ByRef inTransform As XForm) As Long Dim EllipsePoints() As PointAPI, NumPts As Long Dim Width As Long, Height As Long Dim MakePts As Long Const Pi As Single = 3.14159 Const TwoPi As Single = Pi * 2 ' Decide how many points in this ellipse NumPts = (Abs(X2 - X1) + Abs(Y2 - Y1)) \ 4 If (NumPts < 4) Then NumPts = 4 ' Get the size of the ellipse Width = (X2 - X1) \ 2 Height = (Y2 - Y1) \ 2 ' Create point buffer ReDim EllipsePoints(NumPts - 1) As PointAPI For MakePts = 0 To NumPts - 1 ' Fill circle points EllipsePoints(MakePts).X = (Cos((MakePts / NumPts) * TwoPi) * Width) + Width + X1 EllipsePoints(MakePts).Y = (Sin((MakePts / NumPts) * TwoPi) * Height) + Height + Y1 Next MakePts ' Transform points based on given transformation matrix and draw Call TransformPoints(inTransform, EllipsePoints()) TransformEllipse = Polygon(inDC, EllipsePoints(0), NumPts) End Function

As you can see, making a generic 2D affine transformation work with newer OS using the world transform matrix and older OS using emulated behaviour is no easy task, but possible if required.

Http://www.mvps.org/EDais/

47 EDais DC tutorial

Chapter I X

Tips and tricks

In no particular order, here are a few tips and tricks I use in my own work that may be of some use to you when working on your own GDI development. This section may be updated from time to time so you may wish to check back at the site from time to time for any updates to this section, also if you have any of your own tips that may be useful to others then by all means send them over and Ill get the article updated. Object creation and use This is more of a reiteration of what was mentioned back in chapter 1, but the most important thing you can possibly remember in GDI development is to always be very careful that you dont get a GDI leak. A leak occurs when you create a GDI object and fail to destroy it or leave an object selected into a DC when it gets destroyed, and if repeated frequently such as in a paint routine will drain the system GDI resources to the point where Windows itself cannot function properly any longer. A good habit to get into is to write the corresponding delete object call right after creating a GDI object, and similarly for selection/de-selection which makes the chance of forgetting to write the corresponding call far less likely. Its sometimes the case that the GDI object is stored within an object and not terminated in the same context as its created; in these situations its not always possible to use the above technique but be sure to destroy any GDI objects the object holds alive when its destroyed. Cache/Re-use existing objects Rather than creating new GDI objects every time something needs to be rendered, its often far more efficient to cache some of the more complex objects so they can be reused for subsequent frames. Of course this will increase the overhead of the application slightly and the strain on the overall system GDI resources so as always theres a fine balance between creating small objects on the fly and storing complex objects for re-use, try coding it both ways and see what the performance/resource use effect is on the application. Reset handles after deletion A GDI handle is just a big number to VB, and as such theres no way of telling just by looking at it that its a valid GDI object, so its often a good idea especially when working with GDI objects within classes to re-set the GDI object handles after deleting the object i.e: Call DeleteDC(hDC) hDC = 0 This way you can easily check the hDC handle elsewhere to see if its already been deleted (and if so potentially re-create it), this is also very useful when debugging GDI code since deleted handles can easily be spotted, otherwise everything still _looks_ valid even though the GDI object referred to by the handle has already been destroyed.

Http://www.mvps.org/EDais/

48 EDais DC tutorial Checking your application for GDI leaks Display the Windows task manager by pressing Ctrl+Shift+Esc and under the processes pane select View -> Select columns and check GDI Objects. The process list will now show the GDI object count for each process where you can watch to see how well your application is managing its GDI resources, if the count keeps going up then youve most likely got a leak somewhere. When running in the IDE, the VB process is called VB6.EXE but the GDI count of both the IDE _and_ your application is displayed here Its also been noted a few times that the IDE either leaks or caches a few GDI objects from time to time so the count may fluctuate a little (by 1 or 2 objects per complication) regardless of what your application is up to. For a better estimate of its GDI object use, compile the application and it will then appear in its own process. System colours If you need to specify a system colour to an API call then have a look at the GetSysColor() API call. One technique I often use is to allow the user to specify any colour and check at runtime whether its a system colour code or not:
Private Declare Function GetSysColor Lib "User32.dll" (ByVal nIndex As Long) As Long ... Private Function EvalCol(ByVal inCol As Long) As Long If ((inCol And &HFFFFFF00) = &H80000000) Then _ EvalCol = GetSysColor(inCol And &HFF&) Else EvalCol = inCol End Function

System colour codes always have the high bit of the high Word set then the low byte of the low Word contains the system colour index, so this function will check for that bit pattern and if found it will query the API for the corresponding system colour. If the given colour is a non-system colour then it is returned intact so its safe to pass any colour value through it. Stock objects Whenever possible use GDIs stock objects instead of creating your own. The reason for this is twofold; creating and deleting new objects incurs some performance overhead and using stock objects wont result in resource leaks if either purposefully or accidentally left selected into a DC when its destroyed (the DC contains default stock objects already when its created and as such these should have also been reselected when its destroyed.) Most of the stock objects are accessible via the GetStockObject() API call which includes Brushs, Pens, Fonts and the system palette, but can sometimes be coerced into returning other hidden stock objects such as the default Bitmap handle which is selected into all new DCs upon creation. As mentioned previously back in chapter 3, the GetSysColorBrush() API call also returns stock objects corresponding to solid colour brushes in the current system colours these are managed by the system and are re-populated when the system colour scheme is re-set but the handles will remain constant. As always comments, criticism or suggestions are always welcome; I hope the article has been of some use! Mike D Sutton (Visual Basic MVP) Http://www.mvps.org/EDais/

You might also like