|
|
In
the final part of this series on VB graphics, Dermot
Hogan explains how to sublass the WM_PAINT
message and 'bitblt'... |
See also: part
one and part two of this series
Take Full Control Over Windows Graphics
Visual Basic 6 (aka 'Classic VB') provides a good comprehensive
framework for writing programs. Using simple Visual Basic
you can write quite complicated applications quickly.
Sometimes you may require a finer control over what the
program is doing and in several places Visual Basic provides
you with these facilities. But you can go further and
take complete control if you wish.
Classic VB is the name given to
the Visual Basic product line culminating in Visual
Basic 6 by the Classic
VB campaigning group. |
When working with the GDI, the combination of standard
Visual Basic and the extensions that are provided is
something of an unsatisfactory compromise. The whole
point of working with the GDI is to get the last ounce
of detailed control that Windows permits. You may wish
to handle the background repaint event, say, to either
suppress it or do something different from the standard.
The tools that Visual Basic provides do not permit you
to do this but, as is common in software, there is a
get-out clause: you can intercept the fundamental Windows
messages that instruct Visual Basic to do things. This
technique is called ‘subclassing’.
Beyond The Event Horizon
The basic operation of Windows involves messages which
are generated by events. An event might be a mouse
click, a key press or mouse move. Roughly speaking,
event messages are directed to an application by Windows
and, unless you pick them up yourself, are processed
by a window’s default ‘window procedure’.
You can substitute your own window procedure by using
an API call, SetWindowLong (the origin of the API name
goes back into Window’s pre-history) like this:
oldproc = SetWindowLong(h, GWL_WNDPROC, AddressOf MyWndProc)
Here, h is the window’s handle and MyWndProc is
the code that will be executed by Windows when a message
arrives for that window. The old – here, the default – window
procedure is returned by SetWindowLong.
We need to hang onto this in order to reset the window
to normal message handling, which is done quite simply
like this:
SetWindowLong h, GWL_WNDPROC, oldproc
The message handling procedure, MyWndProc,
always has the same arguments:
Private Function MyWndProc(ByVal hwnd As Long, _
ByVal iMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
The first argument is the handle of the window that
the message is destined for. You can use this to get
the device context for painting and other graphics operations.
The second parameter is the message number, say, WM_PAINT.
The last two parameters, wParam and lParam just give
information about the message. What they convey and the
format in which they convey it varies a lot from message
to message.
If you decide not to handle the message (which will be
the case for most messages) you must call the default
window procedure and return the value returned by that:
MyWndProc = CallWindowProc(oldproc, hwnd, iMsg, wParam,
lParam)
In general, working with a window procedure is fairly
easy if you don’t try to be too ambitious. If you
try to be clever, you’ll find that it isn’t
too easy to find out what is happening in a window procedure
using Visual Basic. The standard way of setting a breakpoint
and using the debugger doesn’t work because both
your program and the Visual Basic debug system will be
fighting over who gets to process the messages for the
window in question. Incidentally, that’s how the
Visual Basic debugger works – it intercepts messages
for your program by subclassing and hooking inbound messages.
The trick when you do your own subclassing is to use
the Debug.Print method and look at the resulting output
in the Immediate window.
The Window Procedure
When a message is routed to your subclassed window by
the Windows operating system, the window procedure that
you have set up is called. The standard way of handling
messages is to use If Then
Else or a Select
Case statement.
So the code looks something like this:
Select Case iMsg
Case WM_PAINT
Case WM_ERASEBKGND
End Select
You will need a Case clause
for each message you want to handle. The rules for handling
messages in a subclassed window are simple, but they
must be followed to the letter or you’ll end up
with some rather unexpected results. The two basic rules
are:
- if you handle a message, then the procedure must
return zero, and…
- if you don’t handle a message, then you
must call the old window procedure:
Case Else
MyWndProc = CallWindowProc(oldproc, hwnd, iMsg, wParam,
lParam)
End Select
We’ll start by looking at the WM_PAINT message.
This is sent to the window in question whenever Windows
has determined that something needs to be drawn or repainted.
For example, if you click the button Start
subclassing in the sample program, you won’t see any messages
in Visual Basic’s docked Immediate window as you
move the main form around a little. But as soon as you
resize the window – or minimise and maximise the
window, you’ll see a whole raft of them appear
in the Immediate window.
Processing the WM_PAINT message has its own set of
rules. First, you must call the BeginPaint API. This
returns the device context and additionally sets up a
PAINTSTRUCT structure containing information about the
painting operation, should you need to use it. Here,
I’ve just printed
out the data in it. Next, you draw the picture – I’ve
used DrawBox as the drawing routine. This just draws
a rectangle with a thick border. Following that, you
call EndPaint (it’s not clear why this last call
to EndPaint is necessary; it’s
probably historic). Finally, you must set the return
value to zero:
hdc = BeginPaint(hwnd, ps)
Drawx hdc
EndPaint hwnd, ps
MyWndProc = 0
This
is what happens if you don’t handle the WM_ERASEBKGND
message.
Note the Immediate window docked on the
right for debugging purposes.
If you just write code to handle the WM_PAINT message,
you won’t get the result you might have expected
(see the screenshot above). As well has handling the
WM_PAINT message, you must also redraw any background
in the window. Otherwise, you’ll get what was there
before – sort of. Fortunately, this is easy – just
handle the WM_ERASEBKGND message:
GetClientRect hwnd, rc
brush = CreateSolidBrush(vbGreen)
FillRect wParam, rc, brush
MyWndProc = 0
This paints the background a hideous lime-green colour – but
at least you can see that it’s doing something!
Essentially, the trick is to get the dimensions of the
portion of the window to be repainted using the GetClientRect API and then create a brush of a suitable colour. Normally,
this is the boring Windows grey, but you can choose any
colour you like. Next, just ‘fill’ the rectangle
with the brush and you’ll get rid of the random
background.
That’s nearly it, but there’s one final wrinkle – you
must paint the background as you start to subclass. Otherwise,
only the part of the background that Windows thinks needs
repainting will be done. The reason for this is that
Windows tries to minimise the amount of work that needs
to be done by working out what parts of the window have
been overlaid. So the client rectangle area obtained
in the WM_ERASEBKGND message just contains what
is ‘invalid’ and if you haven’t painted
the background (or invalidated the area which is another
way), Windows won’t know this.
Even when you do handle the background repaint messages
correctly, you will find that Windows only wants you
to repaint the part of the window that it thinks needs
to be repainted.
Blitzing The Bits
Now to ‘bitblts’. A bitblt (usually pronounced ‘bit-blit’)
is short for “bit block transfer” – a
technique for copying pixels around on the screen. A
bitblt is a very common operation in graphics programming.
The reason that it’s given a special name is that
the alternative – two nested for loops doing a
pixel by pixel copy - is way too slow. A bitblt is normally
done in the hardware. Graphics adapters have long had
the ability to zap blocks of pixels around and do clever
things with them like overlaying them on the existing
background so that just the bits you want are visible.
This is an example of a basic bitblt. The rectangle
and its border is copied to the same window lower down.
Note that this affects only the current display - if
you resize the window, the copy will be lost.
A bitblt is easy to do too – here’s a simple
example (just press the BitBlt button on the
example program):
r = BitBlt(hdc, 400, 400, 200, 100, hdc, 0, 0, SRCCOPY)
This simply copies the block of pixels of width 200
and height 100 starting at (0,0) into a location at the
same window at the destination location (400, 400). You
can also play around with various modifications other
than a straight copy.
However, bit-blitting is very handy is you want to
do screen capture. This is covered in more detail in
the section on ‘Capturing
the Screen’ later
on.
Here’s
the complete result. In order to get this you must repaint
the entire backround as you want it before subclassing
the window.
Bitmaps are the most fundamental objects that
are manipulated by the GDI
At some point most graphics operations come down to
generating, tweaking and then outputting a bitmap.
The term 'bitmap' is misleading. There’s not much
mapping involved and you certainly need more than one
bit to represent an element of a bitmap these days. Very
early on in the evolution of personal computers (and
probably before), the monitor screen was monochrome and
one bit could indeed represent one pixel efficiently
given the very limited memory then available. These days,
24 or 32 bits per pixel is the norm.
Bitmaps in Windows can also be a good bit (no pun intended)
more complicated than a simple representation of the
colour to be displayed at a pixel location. There are
two types of bitmap – device independent bitmaps
(DIBs) and device dependent bitmaps (DDBs). A DIB represents
pixels not directly as colours but as indexes into a
colour table. A DDB contains colour information but that
colour information is not sufficient by itself to uniquely
determine the colour specified. Instead, colour values
reference the device’s colour ‘palette’.
A device independent bitmap contains two main structures.
Each element in the pixel array is a value in the colour
table.
This then specifies the actual colour used to
the monitor.
You can use bit-block transfer to copy the whole
screen into another bitmap.
Bit-blitting is used when you want to capture a screen.
Essentially, you want to copy the entire screen bitmap,
all 1024x768 pixels say, to somewhere else. This turns
out to be reasonably easy. The first thing to is to get the
screen’s device context, using the CreateDC API:
shdc = CreateDC("DISPLAY", 0, 0, 0)
Next, you create a ‘clone’ DC, compatible
with the screen DC:
chdc = CreateCompatibleDC(shdc)
Now create a bitmap of the right size and select it
into the cloned DC:
hbm = CreateCompatibleBitmap(shdc, 1024, 678)
r = SelectObject(chdc, hbm)
We’re nearly ready to go now, but first we need
to hide the application’s window – unless
you want to copy that as well. This is done by the ShowWindow API:
ShowWindow hwnd, SW_HIDE
Now we can bitblit the screen bitmap from the screen
device context to our clone:
r = BitBlt(chdc, 0, 0, 1024, 768, shdc, 0, 0, SRCCOPY)
Having got the screen bit map, we can restore the application’s
window:
ShowWindow hwnd, SW_SHOW
Just to show that we’ve done something, we can
bitblit from the cloned DC into the application window:
r = BitBlt(hdc, 0, 0, 1024, 768, chdc, 0, 0, SRCCOPY)
That’s more or less all there is to it, though
copying the bitmap to disk or tweaking areas of the copied
bitmap requires a few more API calls.
This
might look like the start of a hall-of-mirrors, but it is a screenshot of a
screenshot of an application.
This
illustrates the use of bitblitting in screen capture.
April 2006 |