Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

A DELPHI SCREENGRAB UTILITY

First in a series in which we shall program a screen capture application which can take pictures of your entire desktop or a selected window
by Huw Collingbourne

Requirements:
Borland's Delphi 7 or above

 

Download The Source Code:
delphi1src.zip

 

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.

If you are new to Delphi, you may want to view the following Flash Tutorial (animations which pop up in a new window - more Flash Tutorials Here ) which provides a quick guide to Delphi 2005:
An introduction to Delphi 2005 in 2 minutes and 10 seconds!

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.


Going Further...

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

 


Home | Archives | Contacts

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