See also Part Two -
'How To Grab A Selected Area'
Part One :: A Simple Way to Grab A Screen
There are many ways of grabbing pictures from the screen. Ultimately all of them require functions provided by the Windows API. This reliance on the API may not always be obvious since Delphi also provides functions that ‘wrap up’ API calls, thereby effectively hiding them from the programmer, as we shall see.
Load the ScreenGrab1.dpr project from this month's source
code file. Notice that I have placed a TImage control, image1, onto the
form. This will provide the target bitmap in which to display the screen
grab. Let’s begin with a fairly simple way of grabbing a picture of the entire screen and saving it as a bitmap. This is what happens when you click the ‘BitBlt Screen’ button.
This simply calls GrabScreen(), passing to it the Bitmap from image1. The
GrabScreen() procedure assigns to the Bitmap a picture of the screen. Image1
is then resized to match the height and width of that bitmap.
My program lets you capture the Screen, a specific window or a window's client
area. Here it has grabbed a picture of the RealPlayer window
Let’s now see how the screen grabbing itself is done. Scroll up the code to find the GrabScreen() procedure. As you will see, this contains just a few lines of code. First it calls the API function, GetDesktopWindow(), passes its return value to another API function, GetWindowDC(), and assigns the result to a handle, hdcSrc. Here, GetDesktopWindow() returns a handle to the Windows desktop which covers the entire screen and GetWindowDC() returns the handle of the ‘device context’ for the Windows desktop. A device context is a graphics structure which contains information on the display of text and graphics on a particular device. So, after this single line of code executes, the hdcSrc variable provides a handle to the graphics display of the entire screen.
Before assigning a picture of the screen to the
bitmap, bm, the code resizes the bitmap to match the
size of the screen itself. The Width and Height properties
of Delphi’s
Screen variable provide the necessary size information.
Finally, I call the API’s BitBlt() function to
copy a picture of the screen into the Bitmap, bm:
BitBlt(bm.Canvas.Handle,0,0,bm.Width,bm.Height,hdcSrc,0,0,SRCCOPY);
In order to understand what is going on here, you need
to examine BitBlt() in some detail. In brief, BitBlt()
copies an image (strictly speaking, it performs a ‘bit
block transfer’) contained within a source rectangle
to a target device context. The first parameter here,
bm.Canvas.Handle, is the handle of the destination device
context. In other words, the Bitmap, bm, is our target
and it will receive the picture. The next two parameters
are the X and Y coordinates defining the upper left corner
of the target. I want the bitmap to be completely filled
so its X and Y coordinates are both 0. Then come two
coordinates defining the bottom right of the target.
Here these are given by the Width and Height properties
of the bitmap. Next comes the handle to the source device
context. As explained earlier, I have initialised the
variable, hdcSrc to the handle of the desktop window’s
device context. The next two parameters specify the source
rectangle's upper left corner and once again these are
0. The final parameter is a predefined constant which
specifies the operation we want to perform. The SRCCOPY
constant instructs BitBlt() to copy the source rectangle
to the destination rectangle. Other constants are explained
in the Windows SDK help entry for BitBlt().
An Alternative Screen Grabber
Now, while the BitBlt() method of screen grabbing is
pretty simple, it may take you some time to wade through
the SDK help and figure out what all those API functions
and parameters do. In fact, there is another way of grabbing
a screen, using the CopyRect() method of Delphi’s
TCanvas class. I have implemented this technique in the
GrabDesktop() method which executes when you click the ‘CopyRect
Screen’ button.
The GrabDesktop() method creates a canvas, c, and sets its Handle to the DesktopWindow’s device context. A THandle variable, h, is assigned a handle to the desktop window and the GetWindowRect() function initialise the TRect structure, SourceRect, with the desktop window coordinates:
c := TCanvas.Create;
c.Handle := GetWindowDC(GetDesktopWindow);
h := GetDesktopWindow;
if h <> 0 then
GetWindowRect(h, SourceRect);
Both the bitmap and a destination rectangle, DestRect, are resized to the dimensions of the Screen. The image copying process is then performed by the CopyRect() method of the bitmap’s canvas:
bm.Canvas.CopyRect(DestRect, c, SourceRect);
This simply copies the graphics from the SourceRect
rectangle on the Canvas object, c, which defines the
desktop, to the destination rectangle, DestRect, on the
bitmap canvas, bm.Canvas. Finally, I need to release
the Device Context, c.Handle, which I assigned earlier
with GetWindowsDC() and I also free the Canvas object,
c.
This may look very different from the BitBlt() method
of screen grabbing which I used earlier. In fact, under
the surface, it is quite similar. It turns out that Delphi’s
CopyRect() method makes use of an API function called
StretchBlt(). This is essentially the same as BitBlt()
apart from the fact that it has two extra parameters
for the width and height of the source rectangle. If
the width and height of the source rectangle differ from
the width and height of the destination rectangle, the
bitmap will be stretched or compressed to fit. As I don’t
need to do this when copying the desktop image, I could
use either BitBlt() or StretchBlt() to perform the same
operation as CopyRect(). I have included the code needed
to do this. To try it, comment out the line that calls
executes bm.Canvas.CopyRect() and uncomment one of the
two alternative code blocks where indicated.
How To Grab The Active Window
It doesn’t take too much effort to adapt the above
techniques to capture a specific window rather than the
entire desktop. The ‘CopyRect Window’ button
calls my GrabActiveWindow() method to capture a picture
of the currently selected window with the help of Delphi’s
CopyRect() method. The code here is broadly similar to
that in my GrabDesktop() method. The principal difference
is that I call the API’s GetForeGroundWindow()
function rather than GetDesktopWindow() in order to return
a handle to the window which supplies our source rectangle.
I also have to do a little more work in order to calculate
the Width and Height of the destination rectangle and
bitmap. This is because the coordinates returned by the
API functions are relative to the desktop and I need
them to be relative to the specific window being captured.
An alternative way of grabbing the active window is
shown in GrabActiveWindow_Alt(). Instead of using the
CopyRect() method of a Delphi Canvas, this goes straight
to the API, using the StretchBlt() method which I mentioned
earlier. To give the user time to find a specific window,
the Click event handlers for both the ‘CopyRect
Window’ and ‘StretchBlt Window’ buttons
hide the application’s main form and wait a few
seconds before grabbing a window. The GrabScreen() method
can also be invoked by double-clicking the main form.
In this case, the form is not hidden and you are given
five seconds to arrange windows on the screen prior to
the capture.
Having experimented with these alternative ways of
grabbing windows and screens, I decided that I need to
simplify the code by writing a more generic screen-capture
method. I have made a first attempt at this in the ScreenGrab2.dpr project.
Here I have coded one method, named Grab(), which captures
either the screen or the active window. You can find
a detailed explanation of this code in the box, ‘An
All Purpose Screen Grabber’ (below) .
I shall continue to develop this very basic application next month by adding additional features to enhance the screen capture process by giving more control to the user over the precise part of a window which is captured. I shall also add the ability to grab the mouse pointer which, in this month’s projects, is not captured. In the final part of this series I shall go on to develop this application still further in order to create a genuinely useful screen grabber that can grab, scale and save images to disk.
An All Purpose Screen Grabber
Let’s rewrite that code to handle grabs of all types
In the project, ScreenGrab1.dpr, I
treated each type of screen grab operation as a ‘special
case’ which has its own specific method. In future,
I would prefer to have more generic screen capture methods
whenever possible. I have begun work on this in the ScreenGrab2.dpr project.
This time, you will see that I have written a single
method, TForm1.Grab(), to capture either the entire desktop
or the active window. In order to control the type of
screen-capture operation, this procedure takes a second
argument, gt, which is of the type, GrabType. I have
defined GrabType earlier in the unit as an enumerated
type comprising two constants, GTSCREEN and GTWINDOW.
When Grab() receives the GTSCREEN constant it calls the
GetForeGroundWindow() function to initialise the handle,
h, otherwise it calls the GetDesktopWindow() function.
This is all that is needed to switch between screen and
window grabbing.
There are, of course, other types of capture operations that you may what to do. For example, you might want to allow the user to grab specific areas on the screen or the client area (minus the caption bar and frame) of a window. Grabbing the client area is fairly easy to do. You may want to see if you can figure it out for yourself. Here are a few clues. The API function you need to do the job is Windows.GetClientRect(). Be sure to qualify GetClientRect() with Windows as indicated as Delphi also has another implementation of GetClientRect() which differs from the Windows API function. You should initialise the handle, hdcSrc using GetDC() rather than GetWindowDC():
hdcSrc := GetDC(h);
Windows.GetClientRect(h,SourceRect);
You will need to add a third constant, GTCLIENT to the GrabType type. Having done that you can rewrite the code of the Grab() method. There are one or two additional complications to watch out for. In particular, pay careful attention to the arguments passed to StretchBlt(). You may want to see if you can work out the details for yourself. You can compare your solution with mine which you will find in the ScreenGrab3.dpr project. For an explanation of the code, refer to ‘Capturing The Client Area’ below .
Viewing The Full Screen
Having grabbed an image from the screen, you may want
to be able to view it freed from the constraints of
the screen capture application’s form. A simple
(though, as we shall discover next month, not necessarily
the best) way of doing this is to maximise the form.
In the ScreenGrab3.dpr project, I
do this in the MaximizeForm() method by setting the
form’s
WindowState property to wsMaximized. I also remove
the form’s border by setting its BorderStyle
property to bsNone. Finally, I hide the menu by setting
its Menu property to nil. Notice that, before I do
any of this, I save the form’s size by setting
four variables in the SaveFormSize() method. The user
can maximise the form using an item from the View menu
or the Shift+Ctrl+F shortcut. There is no menu item
to restore the maximised form for the simple reason
that, when in the maximised state, the form has no
menu. Instead, I restore the form when the user
presses Escape.
When the form is maximised in the ScreenGrab3.dpr project,
there is no menu available (it has been set to nil)
and no associated shortcuts. This leaves me with the
problem of finding some way of responding to a keystroke
in order to restore the form to its previous size and
style and give it its menu back. I do this by setting
form’s KeyPreview property to True so that it
receives key events even if they occur within some
other control such as image1. Now, after a keystroke
has occurred, TForm1.KeyUp() executes. Here I test
if the Key with the numerical values 27 was pressed.
This is the Escape key. If it was and the form is currently
maximised, I call the RestoreForm() method. This restores
the default window state and border style. It calls
RestoreFormSize() to size and position the form using
coordinates saved earlier. Finally, it reassigns MenuMenu1
to its Menu property. The effect is that the user can
press Shift+Ctrl+F to make the form occupy the entire
screen and can press Escape to restore the form to
its normal size, position and style.
Capturing The Client Area
There may be times when you want to take a picture
of the contents of a window but not the window frame
and caption bar. The contents of a window are found in
the ‘client’ area and there is a special
API call, GetClientRect(), which returns the coordinates
of a client area. Load the ScreenGrab3.dpr project
and find the Grab() method. When I want to grab the
client area of the active window, I pass to this method
the constant, GTCLIENT. First I get the handle of the
foreground window using GetForeGroundWindow(). Then I
get the handle to the device context using GetDC() which,
unlike GetWindowDC(), returns a handle to the client
area only rather than to the entire window. To obtain
the dimensions of the client area I call Windows.GetClientRect(),
passing the handle to the foreground window as the first
argument and a TRect structure, SourceRect, as the second
argument. Note that it is important to qualify GetClientRect()
with the Windows identifier in order to call the API
function of this name as Delphi defines a different GetClientRect()
method.
Finally, when you call StretchBlt() to handle a client
area, you must pass to it different argument values than
when handling the screen or a full window. As you can
see in my code, the first six arguments are identical
in both cases. These define the destination device context,
bm.Canvas.Handle, the coordinates of the destination
rectangle and the source device context. The next four
arguments define the source rectangle. These begin with
0,0, at the top left. The right edge is calculated by
subtracting the right hand edge of the source rectangle
from the value of its left hand edge. The bottom is similarly
calculated by subtracting the bottom edge coordinate
from the top. In fact, this provides a generic
way of calculating the appropriate dimensions and we
shall use this in future.
Commercial screen-capture programs generally
let you select an area of the screen by drawing a rectangle
using the mouse. I shall be adding this feature to my
program next month...
June 2005 |