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