Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

CLASSIC VB : GRAPHICS #2

You can do far more with Visual Basic 6 (aka ‘Classic VB’) than just display simple forms with buttons on them, Dermot Hogan explains …
Requirements:
VB6 ; Windows Millennium/Windows 2000/Windows XP

 

Download The Source Code:
vbgfx2src.zip

 

See also: part one and part three of this series

In this series I’ll look at using the Graphics Device Interface or GDI to do some simple drawing operations. This uses the ‘application programming interfaces’ or APIs to do a lot of the work.

Classic VB is the name given to the Visual Basic product line culminating in Visual Basic 6 by  the Classic VB campaigning group.

Windows Coordination 
…or is it syncopation?

Last month, we looked at the basics of GDI programming – drawing lines, rectangles and paths. We also looked at filling closed paths and rectangles with simple colours and patterns using ‘brushes’. This month we’ll look at more advanced operations such as coordinate transformations and pixel operations.

There is a whole branch of mathematics devoted to moving objects between ‘coordinate spaces’. This is ‘linear algebra’ and it turns out to be very widely used both in industry and computing. Fortunately, it’s also simple. Generally anything that’s ‘linear’ in mathematics is simple; and anything that isn’t is unbelievably complicated.

When you first come across linear algebra, it can look pretty ferocious, but that’s really because of the matrix notation used to represent the transformations. There’s nothing much you can do about the notation, since no-one has come up with a better way of writing out these equations. Once you get used to it, the notation and theory are really very elegant, reaching a beautiful culmination in differential topology and the theory of forms. But that’s a long, long way from the very inelegant Windows GDI!

In the GDI, there are four ‘spaces’ and there is a set of APIs dealing with transformations between these spaces. The first space is the ‘world-space’. This is an optional space if you wish, since there is built in a default transformation between the world coordinate system and the next space, ‘page-space’. This transformation basically makes the world-space and the page-space the same thing, so if you don’t want to do anything fancy, just forget about the world-space.

The second space, page-space, is the one we would normally work in. A better term might be ‘device-independent space’, though this used to be called ‘logical’ space or ‘logical coordinates’. There’s nothing logical about it, though, since logically speaking, it is no better than the next space, the ‘device-space’.  This is nearly the final space, which is the ‘physical-space’. This is the space that truly represents the actual physical device that you are going to draw on. We won’t be concerned about the last one here.

You might think that while a split between device independent coordinates and the physical device coordinates is a good idea, four spaces is over-egging things a bit. But who are we to comment on the inner workings of Microsoft designers? All I will say, is that PostScript manages perfectly well with just two spaces, ‘user’ and ‘device’.

Had We But World Enough and Time…

Let’s start off by looking at the world space. Conceptually, it’s simple. It’s just a Cartesian (that is rectilinear) coordinate grid with 2^32 points in each direction: each point can be represented by a 32 bit signed integer. You can’t specify a fractional point, though of course you can do floating point calculations and then round things off before setting the coordinate. There are a number of transformations available between world and page spaces; you can translate an object – that is move it, scale an object – make it larger or smaller, rotate an object, shear an object – sort of push it over sideways – and, finally, reflect an object.


When you use the ‘advanced’ graphics mode, you will find that rectangles are very slightly bigger on the bottom and right. Read the small print in the API documentation

We can use the test program from last month to try things out. The first thing that you must do is set the graphics mode:

SetGraphicsMode(hdc, GM_ADVANCED)

If you don’t do this, the default mode is GM_COMPATIBLE. This is compatible with Windows 95/98/ME which uses 16 bit coordinate systems and you can’t do any rotations or things like that. So these will only work if you have Windows NT/2000/XP.

If you now run the program and click the Rectangle button, you’ll see a blue rectangle with wide red borders and filled with blue. Now press the button Test 1. This sets the graphics mode to advanced as described above. Now press Rectangle again and you’ll notice a small change in the size. It’s so small as to be almost unnoticeable, but it’s there. This is due to something called ‘rectangle exclusion’. For Windows 95, etc., the system excludes the bottom and rightmost edges when drawing rectangles. If GM_ADVANCED is set in NT/2000/XP, you get the bottom and rightmost edges included.

Now let’s try a transformation. These are achieved mathematically by applying a 3x3 matrix operation to a vector (a point) in a coordinate space. Because the Windows GDI spaces are essentially two-dimensional, the third z-axis is not used (it’s constant), so you can get away with just using 6 elements. These are specified in an XFORM structure which we’ll come to shortly.

Initially, we’ll try a simple 30-degree rotation:

x.eM11 = 0.866
x.eM12 = 0.5
x.eM21 = -0.5
x.eM22 = 0.866
x.eDx = 0
x.eDy = 0
Refresh

and in the Draw code which does the actual drawing:

SetWorldTransform hdc, x
RectangleX hdc, 5, 5, 250, 250
ModifyWorldTransform hdc, x, MWT_IDENTITY

All that’s happening here is that a transformation between world-space and page-space is being performed by the SetWorldTransform API. This applies the rotation specified in the transformation matrix, x, to map between things drawn in the world coordinate system and the page. A rectangle is then drawn and the coordinate system reset using the MWT_IDENTITY parameter in another API ModifyWorldTransfom. This last step is necessary, by the way. You can see what happens if you don’t do it in one of the screen shots.

It looks simple, easily understood and… it didn’t work.

Now, when things don’t work the first place to look is for typing errors – nope, no typing errors. Next, check for correct API parameter passing: all ok. And so on. Well, I won’t bore you with all the things I tried for the next few hours trying to get this thing to work. Suffice to say, that I got as close as I’ve ever come to throwing in the towel with a Visual Basic program. Then I looked again at the API documentation on XFORM (for about the 100th time) – and it clicked. The API documentation specified the XFORM structure members as FLOAT (equivalent to SINGLE in Visual Basic) – that is a 32-bit floating point number. Now, no-one uses 32-bit floats. They are so imprecise that you really can’t do any serious arithmetic with them and internally, in an Intel CPU, floating point operations are 80 bit anyway. So there is absolutely no reason for having these things around. I had assumed that the API documentation meant DOUBLE – a 64-bit number. And I wasn’t alone in this – the person at Microsoft who had entered the XFORM structure into the API Viewer tool had also assumed this: the structure exported from the API Viewer was wrong! And that’s what I had used as the source of my XFORM structure.


Spot the difference! The API viewer says that the members of XFORM are 64-bit numbers while the API documentation, correctly, states that the are 32-bit floating point.

The correct XFORM structure is this:

Private Type XFORM
        eM11 As Single
        eM12 As Single
        eM21 As Single
        eM22 As Single
        eDx As Single
        eDy As Single
End Type

There are five possible world-to-page transformations: translation, rotation, scale, shear and reflection. The last two components are used for translation, eDx specifies the horizontal translation component and eDy, the vertical.

The other transformations, rotation, shear, etc. are done via the first four components, which are the elements of a 2x2 matrix.


Floating on air. Here’s what the rotation is supposed to look like. Using the incorrect XFORM structure, the API calls just failed with a return code of zero. A good five hours wasted!

Unbalanced Scales

The world-to-page transformations do not involve any change of scale. There are the same number of units in the axes of the world coordinate system as the page coordinates. But when you move from page to device spaces, you do have to be aware of scales and also the direction of the axes. It seems odd at first sight that the default coordinate system of the page space is downwards from the top. This is because printers (and old style monitors) started printing or displaying at the top of the page and worked down – and I suppose documents are much the same. However, for a modern monitor or LCD display, this is irrelevant – you can start the addressing at the bottom just as well as at the top. Still, we’re stuck with the situation.

The default page-to-device mapping mode is simple: one unit of page space is the same as one pixel. This is the MM_TEXT mode. There are eight modes you can use, though most of them are really hangovers from older Windows versions. The other problem is that you also have to specify the origin of the ‘viewport’ and ‘window’ – which, just to confuse matters, don’t mean what you think they mean. Even worse, you have to try and second guess what the Visual Basic environment is doing in repainting the background form.

Finally, to confuse things further, the mapping mode, MM_ISOTROPIC, might seem to imply that the aspect ratio is maintained when mapping from page to device. But it turns out that you can squash or extend shapes by setting the extent ratios accordingly.

There’s one more wrinkle to iron out before this will work: you have to set the origin of the mapping. This is done via the SetWindowOrgEx API and if we want the coordinate system to go up the screen, this has to be set to the current bottom of the window.


You must remember to reset the mapping between the world and page coordinate systems when you’ve finished. Otherwise, it doesn’t look right.

Next Month:

There are still quite a few problems to be sorted out before we’re ready to copy images around. Next month, we’ll look at bypassing Visual Basic’s painting and redrawing by sub-classing the form window and trapping the WM_PAINT message.


Upside down

This is how you switch from a coordinate system that points down to one that points up.

Here’s some code that switches the orientation of the y-axis from going down the page to upwards:

r = SetMapMode(hdc, MM_ISOTROPIC)
r = SetWindowExtEx(hdc, 100, 100, sx)
r = SetViewportExtEx(hdc, 100, -100, sx)
r = GetClientRect(hwnd, rx)
r = SetWindowOrgEx(hdc, 0, rx.Bottom, px)

The API call SetMapMode switches the scaling mode. But we then have to set the ‘window’ extents and origin by using SetWindowExt and SetWindowOrg and the ‘viewport’ similarly by using the corresponding APIs. The term ‘viewport’ normally implies some sort of clipping window where lines drawn outside the viewport aren’t displayed. This is not what the GDI means by ‘viewport’. In GDI-speak, ‘window’ refers to coordinates in the page-space and ‘viewport’ to those in the device-space.  All that SetWindowExt and SetViewportExt do is establish the ratio between there two coordinate systems. You can try this by replacing the 100 in the code example by 1 (or -1 as necessary) and you’ll get exactly the same result.


Here’s how the three main coordinate systems work in the Windows GDI.
You start in the world one, move to page, then finally to device.

 

Regional Variations

Regions are useful when you want to detect a mouse click.

Regions are GDI objects similar to filled shapes but with some more interesting properties. You can create elliptical (circular) regions, rectangular regions and irregular polygon shapes.

Besides filled them with a brush or a bitmap, there are three other things you can do with regions: combine them, clip graphics output and, lastly, detect if a mouse has moved or clicked anywhere within the region.

We’ll just look at detecting a mouse click here, so the first thing to do is create a region:

hRegion = CreateRoundRectRgn(300, 100, 500, 300, 10, 10)

Here, I’ve created  a rectangular region with round edges – the first four parameters determine to rectangle’s position and size while the last two specify the ‘roundness’ of the corners. Now,  we’ll create a solid yellow brush and paint the region:

brush = CreateSolidBrush(vbYellow)
DeleteObject SelectObject(hdc, brush)
PaintRgn hdc, hRegion

This region will disappear as soon as the form is minimised since the paint method doesn’t have this code in it, but the region will still be there as we’ll see.

There’s a very useful API, PtInRegion, that detection if a specific point is within a region. This is a trivial operation if the region is rectangular, but it’s not so easy if the region is an irregular shape or, as here, has rounded corners.

The way to detect if a mouse click occurs within the region is to trap all mouse clicks:

Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
  If PtInRegion(hRegion, X, Y) Then
     MsgBox "In region"
  End If
End Sub

You can try this out by pressing the Region button and clicking the mouse around and then in the region. Also, try minimising and restoring the form to make the region invisible and then try clicking.


Regions are a good way to detect mouse clicks in an irregular shape. Might be useful for triangular buttons?

Refreshing the parts that VB cannot reach...

There are problems with refreshing a window when mixing GDI programming with VB

It won’t take you long when working with the GDI from Visual Basic to notice that there can be problems with repainting a window. There are two real trouble spots. the first is the bug in Visual Basic that I mentioned last month, where if you use the AutoRefresh property of a form or control, you must call Refresh after the GDI calls – otherwise you’ll see nothing.

The second one is involved in messing around with the coordinate systems. The problem seems to lie in some code that is clearly going on ‘behind the scenes’ in a Paint method. For example, you sometimes can’t get the background to repaint properly.

The real solution to this lies in sub-classing the WM_PAINT message. This will give you the full control of the repainting of a window just as you have when you write in a language like C++. Very briefly, each window has a default ‘window procedure’ that handles all messages that are destined for that window. It’s quite easy to substitute your own window procedure for the default one – or even the Visual Basic one.

The window procedure is set on a ‘class’ basis – that is, for a defined window class you specify the procedure that will handle all the messages for the windows of that class. Now that’s really all that a Windows window class does. It has nothing whatsoever to do with object orientation. Again, like a lot of the GDI, it’s really a hangover from the pre-history of Windows.

Subclassing works surprisingly well in Visual Basic, but it doesn’t seem to be a widely used technique. There are one or two problems with debugging a subclassed window because the Visual Basic run time environment gets very confused if it finds that it isn’t getting all the messages that it thinks it ought.


More problems. When the window is resized, Visual Basic doesn’t know that we’ve tweaked the coordinate system.

 

March 2006

 


Home | Archives | Contacts

Copyright © 2006 Dark Neon Ltd. :: not to be reproduced without permission