Anda di halaman 1dari 48

1

EDais DC tutorial

1 EDais DC tutorial Using Device Contexts (DCs) in Visual Basic Written by Mike D Sutton

Using Device Contexts (DCs) in Visual Basic

Written by Mike D Sutton Of EDais

- 03.05.2004 -

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

2

IntroductionIntroduction

What is a DC?

EDais DC tutorial

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 they’re 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 - What’s 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

Chapter I IChapter

EDais DC tutorial

A simple introduction to DC’s

There are a few different types of DC’s, however the one we’ll be looking at is the most common for general use, that being the memory device context. To create a memory device context we’ll 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 aren’t 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 we’ll create a new DC and make sure we’ve 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 isn’t a whole lot of use, generally we’ll 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 you’ve gone through the DIB or DDB articles on this site then you’ll be at an advantage here however if not then don’t worry since we can cheat and get VB to manage the Bitmap for us by using it’s 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 we’re done, or by ‘borrowing’ one off any of VB’s controls that expose a .Picture property. In this case we’ll 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 wouldn’t work with this technique!

Now we have our DC and Bitmap, it’s 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 we’ll need the SelectObject() API call:

Private Declare Function SelectObject Lib "GDI32.dll" ( _ ByVal hDC As Long, ByVal hObject As Long) As Long

Finally, we’ll 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; we’ll 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 we’ll have to retain the handle we’re 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 don’t worry about the myriad of parameters it takes. Here’s the final code which I’ve 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

Chapter I I I IChapter

What’s in a DC?

EDais DC tutorial

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 it’s bound too and what it’s 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 device’s capabilities i.e. what it’s capable of rendering. In many cases though, GDI will be able to emulate various operations even if the manufacturer hasn’t added support for them directly in the device driver itself. To query the DC for it’s 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 doesn’t 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 device’s 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 we’ll 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 DC’s 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 don’t 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 call’s to retrieve or change this respectively. To see this actually register a change on the DC, we’ll 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, we’ll 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 don’t be worried if it returns 0. We’ll 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 you’ve 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 it’s colour then you can use the background colour property of the DC accessible via the Get/SetBkColor() API call’s. One final thing to note about DC settings that they you don’t have to re-set them before the DC is destroyed, only GDI objects themselves require de-selecting. If you’re 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 VB’s controls) then it’s good practice to restore the DC’s settings to the same as when you found it. If you’re changing a large number of the DC’s 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 DC’s 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 it’s internal state stack and returns the new state’s 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 1’st 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 state’s index in the stack it’s 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 it’s 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 – It’s 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 it’s first created. It’s also possible to simply re- select stock objects into a DC before it’s 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; it’s very easy to get resource leaks when using advanced tricks like these.

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

10

Chapter I I I I I IChapter

The GDI brush object

EDais DC tutorial

The GDI Brush object is what’s 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 you’re 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 VB’s 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 can’t however use VB’s 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 brush’s, 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 DC’s 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 don’t 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 won’t work properly there. Here’s 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)

You’ll 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 doesn’t 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 VB’s system colour constants can’t be used here but you can use the EvalCol() method listed above to correctly evaluate those.

The final brush type we’ll talk about here is the pattern brush, which fills an area with a user defined pattern or bitmap fill. These brush’s 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 VB’s 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 you’re 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 form’s 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 set’s the brush origin property for the DC. Here’s 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 VB’s GetObject() method, since it’s 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 it’s 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 we’ve 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 nothing’s being drawn it doesn’t require a hatch or colour.) A pattern brush doesn’t 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 MSDN’s page for the LOGBRUSH structure or the accompanying code for this chapter.

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

13

VChapter

Chapter I I V

The GDI pen object

EDais DC tutorial

The GDI Pen object is what’s responsible for drawing the edges of shapes in Windows and as such they’re 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 pen’s style, such as whether it’s broken, solid or invisible (null.) There is however one slightly different pen style, PS_INSIDEFRAME, which rather than defining a pen style (it’s always rendered as a solid line) defines the positioning of the edge instead. Normally when the edge is drawn, it’s centred on the outer edge meaning that half the width of the pen is drawn outside the shape’s edge, half is inside the shape’s 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. Here’s 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/

130, 10, 230, 110) Call SelectObject(Form1.hDC, hOldPen) Call DeleteObject(hPen) End Sub Http://www.mvps.org/EDais/
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, you’ll 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 they’re generally used to add fine details (or at least that’s 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 you’ll simply be returned a solid pen of the desired thickness. We’ll 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 you’re after it actually draws small dash’s. To draw a properly dotted rectangle have a look at the DrawFocusRect() API call instead, for other shapes then you’ll 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, you’ll 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

You’ll 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 pen’s 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

Private Type LogPen ' 16 bytes lopnStyle As Long lopnWidth As PointAPI lopnColor As Long End Type

EDais DC tutorial

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 isn’t even used, however that’s 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 Pen’s 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 we’ll 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

Case Is >= 24 ' Extended logical pen

Case Else

' Logical pen

' Unknown!

End Select

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

16

EDais DC tutorial

If we do get given an extended pen then we’ll have to take into consideration the variable sized array at the end of the structure. Unfortunately we have a problem here since VB’s 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 DWord’s 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)

API

PenBuf(0)) Call RtlMoveMemory(ExtPen, PenBuf(0), 24) API VB If (PenSize >= 28) Then ' Copy the user

VB

Call RtlMoveMemory(ExtPen, PenBuf(0), 24) API VB If (PenSize >= 28) Then ' Copy the user style
Call RtlMoveMemory(ExtPen, PenBuf(0), 24) API VB If (PenSize >= 28) Then ' Copy the user style

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

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, we’ll need to start copying from the 6’th entry (6*4 == 24 == Size of the fixed section of the header.) The “And Not 3part 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 3’rd 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 it’s 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

VChapter

Chapter V

The GDI font object

EDais DC tutorial

The GDI Font object is what’s responsible for drawing the symbols we see when text- drawing 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 you’re 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, that’s a lot of parameters! Luckily though it’s 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 don’t 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 call’s respectively. We’ll 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.

rotation of the individual characters off this baseline. The latter can only be used when the

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 isn’t 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 isn’t always perfect here so it’s 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!

effects such as text the reads from top to bottom without having to turn the page

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 we’d 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 it’s given. The default value will invoke font substitution if the given font doesn’t exist, so it’s 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 you’re 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 doesn’t exist.

The final parameter is the typeface itself which is the name we’re 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 we’ll use for this is EnumFontFamiliesEx() and since we’ll be using call-backs it’s 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 font’s we wish to enumerate but for this example we’ll 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

Dim FontInf As LogFont

EDais DC tutorial

' Set to enumerate all fonts FontInf.lfCharSet = DEFAULT_CHARSET FontInf.lfPitchAndFamily = 0 FontInf.lfFaceName(0) = 0

Now we’ll 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 I’ve 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 we’re interested in in this case is contained within the EnumLogFontEx structure, however since all the strings it returns are stored as byte arrays we’ll 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)) & ")"

You’ll see that I’m 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 we’ll return 1 to indicate to the API that we wish to continue enumeration:

EnumFontFamExProc = 1 ' Return 1 to continue enumeration

All that’s 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 won’t 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. We’ve 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 it’s 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 string’s 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 doesn’t allow us to render string’s 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 don’t 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 you’re 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 path’s, 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 haven’t been displayed.

The final formatting flag we’ll cover with this call is the DT_CLACRECT which is the odd one of the bunch in then when it’s 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

V I IChapter

Chapter V

GDI Regions

EDais DC tutorial

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, what’s 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 call’s to create region objects from various primitive shapes, so we’ll 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 it’s a bit overkill for just drawing a filled red rectangle and you’d be right, however thankfully this isn’t 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 it’s not very useful and is only usually seen when combining multiple regions which we’ll 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

Description

RGN_AND

Calculates the intersection of the two source areas (the area they have in common.)

RGN_OR

Calculates the union of the two areas (the area either occupy.)

RGN_XOR

Takes the union of the two areas and subtracts their intersection

RGN_DIFF

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.)

RGN_COPY

Returns a copy of the first region, not exactly a region combination mode as such but useful for creating a clone region.

Here’s 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):

modes (apart from copy mode which is self explanatory): The last two are both using difference

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, I’ve 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 VB’s 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 doesn’t require the

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

29

EDais DC tutorial

XFORM type declaration if it’s not going to be used. The final parameter allows you to skip the potentially quite expensive bounds rectangle checking if it’s 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 they’re used, have a look at chapter 8.

You may be wondering at this point how this region data actually maps to the region itself, we’ll take the combination region for the square and circle shown earlier in this chapter as an example:

and circle shown earlier in this chapter as an example: As you can see the resulting

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

V I I I IChapter

Chapter V

GDI Paths

EDais DC tutorial

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 what’s 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 DC’s 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 don’t 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 DC’s path which will more than likely always be closed shapes, however if you’re 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 path’s have no GDI object handle to refer to them by we can’t 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 set’s 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 it’s 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+)

call is only supported in a path bracket in Win2K+) As you can see, when a

As you can see, when a path is filled any non-closed shapes are closed before it work’s out the fill area’s. Sometime it’s 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. Here’s the above path data after having flattened it:

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

32

EDais DC tutorial

32 EDais DC tutorial Back in chapter 4 we looked at geometri c pens which we

Back in chapter 4 we looked at geometric pens which we found converted the area they draw into a polygon region and filling it, path’s 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:

the path with a couple of different 20-pixel wide brushes: Square end caps, and bevel joins

Square end caps, and bevel joins

20-pixel wide brushes: Square end caps, and bevel joins Round end caps and joins As you

Round end caps and joins

As you can see GDI’s 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 – You’ll 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:

shape as seen below, the original path is shown in black: Alternate polygon fill mode Winding

Alternate polygon fill mode

original path is shown in black: Alternate polygon fill mode Winding polygon fill mode The area

Winding polygon fill mode

The area where the shape overlaps itself is hollow in the left illustration since the DC’s polygon fill mode was set to alternate. This can be changed to fill these overlapping area’s by calling the SetPolyFillMode() API call and passing it the WINDING flag which gives the ‘correct’ result as shown on the right. Path’s 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 it’s 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

V I I I I I IChapter

Chapter V

Mapping modes

EDais DC tutorial

In the code examples presented so far in this article I’ve 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 we’re 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 it’s 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’ we’re 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 isn’t 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 we’ll 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 re- scales (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

35 EDais DC tutorial World space The original drawing is performed in world space but in

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.

into a single operation by using matrix multiplication. Device space Finally the DC’s mapping mode scales
into a single operation by using matrix multiplication. Device space Finally the DC’s mapping mode scales

Device space

Finally the DC’s mapping mode scales the drawing for output to the physical device and offsets it to the device’s 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 you’ll 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/10’ths of millimetres where as high-metric mode is measured in 1/100’ths 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/100’ths of inches and hi-English works in 1/1000’ths of inches and again they both have negatively scaled Y-axis. The sixth mode, twips, is one you’re likely familiar with if you’ve 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/1440’th of an inch or 1/20’th 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 don’t matter but the window must be set up before the view-port. If you’re 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. Here’s 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 we’ve 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 re- scaled 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 we’ve 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

eM12

eM21

eM22

eDx

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
1
0
0
1
0
0

If you’ve never worked with matrices before then don’t worry, this isn’t 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 you’ll 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
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 Y 0 0
X
0
0
Y
0
0

This may now make the identity matrix seem a little more obvious - it’s 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)

-Sin(Rot)

Cos(Rot)

0

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 Y X 1 0 0
1
Y
X
1
0
0

Reflection’s can be performed by simply inverting one of other axis; here are horizontal and vertical reflection matrices:

-1 0 0 1 0 0
-1
0
0
1
0
0

Horizontal reflection

1 0 0 -1 0 0 Vertical reflection
1
0
0
-1
0
0
Vertical reflection

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

39

EDais DC tutorial

Before getting into combination transforms, let’s 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 it’s still sitting in the top left corner of the form then either the code or declares are incorrect or it’s 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 we’ll 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 you’ll need to get the size of the window which we can do with the GetClientRect() API call and it’s 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 we’ve going to be drawing at different positions depending on the size of the form, we’ll 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 we’ll 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 don’t 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 won’t 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, here’s 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

It’s 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

43 EDais DC tutorial To do this you first offset so that the origin is at
43 EDais DC tutorial To do this you first offset so that the origin is at

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:

you wish and finally tr anslate back to the original origin: Original shape Shape is moved

Original shape

tr anslate back to the original origin: Original shape Shape is moved so it’s centre is

Shape is moved so it’s centre is at the origin

shape Shape is moved so it’s centre is at the origin Shape is rotated at origin

Shape is rotated at origin

it’s centre is at the origin Shape is rotated at origin Shape is moved back to

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 it’s 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 doesn’t 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

the matr ices in different orders: Rotation then translation Original shape Rotation is applied Translation is

Original shape

different orders: Rotation then translation Original shape Rotation is applied Translation is applied Translation then

Rotation is applied

Rotation then translation Original shape Rotation is applied Translation is applied Translation then rotation Original

Translation is applied

Translation then rotation

is applied Translation is applied Translation then rotation Original shape Translation is applied Rotation is applied

Original shape

is applied Translation then rotation Original shape Translation is applied Rotation is applied As you can

Translation is applied