See also: part
two of this series
You can do far more with ‘Classic VB’ than
just display forms with buttons on them. 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. |
Yes, Sir, I Can Doodle…
The standard graphics available in Visual Basic 6 are
very simple and limited in scope. You can draw lines,
circles, boxes and fill them with colours and patterns.
But you might consider it to be somewhat ambitious to
build a CAD system using the available Visual Basic primitives.
What Microsoft provides in this respect is really not
up to the job. However, you can go behind the scenes
and use more advanced Windows GDI techniques to get better
performance and more sophisticated graphics.
The Graphics Device Interface (GDI) is the part of Windows
that is responsible for the drawing of pictures and text
on your screen. It extends a bit further than that in
practice – it’s also used for dealing with
printing, faxes and so on.
The problem with the GDI is that it’s old. Parts
of it go back to Windows 1.0 and it’s been modified,
tweaked and re-written countless times since. Frankly,
it shows. The general design of the GDI API is, well,
eccentric. For the next couple of months, I’m going
to explore the highways and byways of the GDI and then
move on to look at how it’s done in .NET. Interestingly,
you’ll find that the .NET GDI isn’t that
different from the original GDI. Finally, I’ll
look at how you do the same things in the new Windows
Vista system, WinFX or Avalon (the Windows Presentation
Foundation).
Don’t be put off by the fact that this is using
Visual Basic 6, by the way. If you’ve never used
the Windows API or figured out why Windows works the
way it does, then you’ll still find the explanations
and the underlying principles also apply to VB .NET and
beyond. Additionally, Visual Basic 6 is still a lot easier
to follow, especially if you’re not used to object
orientation. You may find this series to be a good introduction
to object oriented programming as well, since you’ll
be able to compare the old style non-object oriented
way and the newer .NET style.
The GDI
At its core, the GDI divides into two worlds: the ‘logical’ and
the ‘physical’. A program writes to the logical
world where the monitor dimensions are very large,
colours are perfect and almost anything can be done.
The GDI then translates this perfection into grubby reality
of 800x600 (say) pixels on an LCD. The logical
GDI has a set of objects which don’t exist on physical
devices – for example, a ‘brush’, which
is used to fill a background with a colour or pattern.
The GDI then generates the necessary instructions for
filling a rectangle with a colour for the device driver – the
interface to the graphics card - to make it happen.
When you talk to the GDI, you must specify a ‘device
context’. This is a sort of scratchpad of interesting
things that the GDI needs to know about whenever it does
something. For example, when you draw a line, you need
to specify not only its starting and ending locations,
but the colour, the thickness and possibly the style – dashed
or solid. To save having to specify these basic properties
every time you want to draw a line, you specify these
in the device context (‘select into’ in the
GDI jargon) and just pass the device context to the API
call.
The GDI drawing functions really form four basic classes:
lines, filled areas, bitmaps and text, though the last
one probably exceeds all the other three combined in
terms of complexity. We’ll start with a humble
line.
From A to B … and
back again
You’d think that drawing a line from one point
to another is just about the easiest thing in the world … sort
of like falling off the GDI log. It’s not quite
that easy though. There’s the small matter of Visual
Basic that gets in the way. We’ll start off simply
with a form and a single ‘Test’ button on
it (Download Source
Code). In the form’s load method,
we’ll
create a new ‘pen’ and tell Windows to use
it:
Private Sub Form_Load()
redPen = CreatePen(PS_SOLID,
5, vbRed)
DeleteObject SelectObject(hdc, redPen)
End Sub
The CreatePen API makes a new ‘pen’. This
is what you probably think it is – something that
draws a line in a certain style and colour. Here, it’s
solid, 5 pixels wide and a nice red colour. You can also
set it to various combinations of dots and dashes and
even make it invisible. The new pen is then ‘selected
into’ the device context – that is, it replaces
the existing pen in the device context. This is done
by the SelectObject API which takes as arguments the
device context of the form, hdc, and the newly created
pen. A handle to the old pen is returned by SelectObject.
It’s considered good programming practice to delete
the old pen to avoid memory leaks, but for us that’s
not very significant. Still, that’s why DeleteObject is called here.
Now we’ve got a red pen, so let’s draw something
with it:
Sub Draw()
MoveToEx hdc, 0, 0, 0
LineTo hdc, 200, 200
End Sub
The first line of this function uses the MoveToEx API
to move the ‘point’ of the pen to somewhere
definite – here the form’s co-ordinates (0,0).
Next, the LineTo API draws a line from the current pen
point (0,0) to a new pen point (200, 200). All pretty
easy so far, so let’s test it:
Private Sub Test_Click()
Draw
End Sub
You should get a thick red diagonal
line running part way across the form. But now we hit
a problem. If you minimise the form and then expand it
to full size again – the line has vanished!
Getting Visual Basic to draw the graphics is not trouble
free. There is clearly a bug when the form is repainted
which you have to work around.
There
are two ways to get 'home grown' graphics to
display in Visual Basic. One is to use the Paint
method, but it’s simpler
to set the AutoRedraw property.
|
|
If you click Test again, the line
re-appears. It turns out that you get the same
behaviour if you overlap the line with another
window. Clearly, Visual Basic is not redrawing
the form correctly. More precisely, it is redrawing
the form because the Test button is redrawn, but
it’s not redrawing the line.
The obvious solution (well, sort
of … after a good bit of poking around)
is to set the Form’s AutoRedraw property
to True. This should make the form redisplay any
graphics when it is moved or resized. If you set
this, and then press ‘Test’ you’ll
get … nothing! But now try minimising
the form then restoring it. The line will magically
appear. It turns out that this is nothing more
than a good old-fashioned bug and it is indeed
documented as such in the MSDN library. Unfortunately,
it’s only documented as being a problem for
a bit-mapping operation, BitBlt.
This is not a great deal of use when you are searching
for problems with LineTo.
It took me some considerable time to track it down.
The solution suggested by Microsoft does work – just
put in a call to Refresh after
the call to Draw.
However, by the time I’d discovered that,
I was well on the road to plan B. It’s this:
if the AutoRedraw property is set to False, you
must handle the re-painting required yourself.
There are two areas where this is necessary, in
the Paint method
which handles overlaps and maximize/minimize operations
and in the Resize method.
So all we need to do is put a call to Draw in
these two methods. This also works.
|
Square Bashing
Now we’ll move onto more interesting structures
than the humble line - squares for instance. You
can draw a square in a number of different ways. The
first is just to draw four lines that happen to join
up. In the example program, there are four buttons A,
B, C and D which, if you click them in order, will produce
a square. However, while these may look to you like a
square (and to a mathematical topologist they are a square)
to Windows they are merely four lines. They do not have
an ‘interior’. This means that you can’t ‘fill’ the
square with a colour, pattern or anything else.
Drawing a rectangle with
four lines isn’t the
same as creating a 'filled shape'. Four lines (even if
they seem to be connected) do not have an interior and
so can’t be filled
To do this, you have to draw one of Windows’ ‘filled
shapes’ – a rectangle, ellipse or polygon.
You fill rectangles and on so on with a ‘brush’.
A brush is a GDI object that really does work a bit like
a paintbrush. At its simplest, you specify a colour and
possibly a pattern and Windows will then ‘paint’ the
interior of a filled shape with the pattern and colour.
We’ll just use a simple one – a rectangle.
In the Rectangle button’s click code there is just
the call:
RectangleX hdc, 50, 50, 250, 250
Refresh
You can paint the interior of a filled shape with either
a solid colour or a pattern. Several standard patterns
are supplied by default.
I’ve had to call the API RectangleX using the
Alias keyword in
the API’s Declare statement to
avoid a conflict with the Rectangle Button (I could have
called the button something else, of course, but that
would be doing things the easy way). The RectangleX API
call creates a Windows rectangle of size 200 at a starting
position of (50,50). Now if you just have that code you
won’t get something very different from the four
simple lines. This is because Windows is filling the
interior of the rectangle with the current ‘brush’ which
is exactly the same as the grey of the form.
So we have to create a new brush with a different colour
in the Form’s Load method:
blueBrush = CreateSolidBrush(vbBlue)
DeleteObject SelectObject(hdc,
blueBrush)
Now if you click Rectangle, you’ll get a
blue colour filling the interior. You can try variations
on brushes by calling CreateHatchBrush which creates
various simple brush patterns, ‘hatches’.
For example, try clicking the HatchBrush button followed
by the Rectangle Button.
There is a world elsewhere...
The Microsoft GDI isn’t the only way to approach
the problem of drawing ‘high level’ concepts
such as circles onto a ‘low level’ device
like a printer. Adobe designed PostScript in 1984 (or
thereabouts) as a means of enabling computer printers
to produce high quality phototypesetting output. PostScript
was adopted by Steve Jobs in the Apple Mac in 1985 and
became part of the Mac legend – the ability to
produce really good desktop publishing from a (relatively)
cheap personal computer.
Quartz sits below the Mac OS X 'Aqua' interface. Quartz
uses PDF as its graphics language which is arguably a
better way of doing things than the Windows GDI.
PostScript has been through several generations now,
but arguably the most significant innovation was the
design of Display PostScript used in Steve Jobs’ NeXT
computer. The idea here was that instead of using API
calls to draw a circle, or whatever, on a screen, the
program would generate PostScript commands that would
be interpreted directly by a graphics ‘engine’.
There were two advantages to this. First, give or take
small differences in the PostScript engine, what you
saw on the screen really was what you got when it was
printed. And secondly, it was an elegant piece of engineering.
Unfortunately, NeXT did not thrive. But Steve Jobs did.
Via a long and tortuous route, he took over at Apple
and managed to keep it off the rocks. He and his team
have managed to put the company on a stable and prosperous
footing – a feat which had defeated the previous
managements. But times have moved on and PostScript is
not the state-of-the-art graphics processing language
that it once was. Instead, Adobe’s Portable Document
Format (PDF) rules the roost. But PDF is a superset of
PostScript, optimised and enhanced. Instead of Display
PostScript, OS X now uses ‘Quartz’ – the
equivalent to Windows GDI. And guess what? Quartz uses
PDF as its graphics language. In many respects, the Mac
is superior to Windows XP which still uses the
GDI. Even in .NET the GDI is still there – hidden,
it’s true, by a good class interface. But elegant,
it still isn’t.
Paths are another way of defining shapes...
Simple shapes like circles, ellipses and rectangles
are fine for objects like windows, textboxes and the
like. But for really complicated drawings or shapes,
it is better to use a ‘path’.
Here’s the difference between a ‘stroked’ path
and two simple lines. You can see that the ends of the
two simple lines are not joined smoothly whereas the
stroked lines are.
GDI paths are
collections of lines, not necessarily connected, which
the GDI handles as a single entity. You start a path
using BeginPath and
then just add lines as you
would normally, ending with EndPath.
But there are two differences. First, the sequence:
BeginPath hdc
MoveToEx hdc, 10, 10, 0
LineTo hdc, 10, 210
LineTo hdc, 210, 210
LineTo hdc, 210, 10
LineTo hdc, 10, 10
EndPath hdc
...doesn’t display anything. To display the path
you have to issue the StrokePath command.
The second difference lies in the fact that the lines
here are connected; and that has an effect when you specify
the types of line ending and mitre joins that each line
has where it connects to the next line in the sequence.
You can specify the type of join and mitre by using
the ExtCreatePen API which we’ll look at next month.
Next month, we’ll look at further GDI programming with
bit-blitting and other tricks.
February 2006 |