Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

A DELPHI SCREENGRAB UTILITY #3

This month we adapt our screen grab tool by giving it hotkeys to let you grab a screen at a moment’s notice
by Huw Collingbourne

Requirements:
Borland's Delphi 7 or above

 

Download The Source Code:
delphi3src.zip

 

See also Part Two - 'How To Grab A Selected Area'

Part Three :: Hotkeys and Saving Images

In the previous versions of the screen grabber (see Part Two of this series), most grab operations occurred after a fixed and predefined interval. This meant that a user who wanted to grab a window would have to locate the window and make any adjustments to it in the fixed interval before the grab was made. It would, of course, be more useful if the user could spend an arbitrary amount of time arranging the screen and then initiate the grab by pressing a hotkey. Better still would be the ability to use different hotkeys for different types of screen grab - for example, Ctrl+Shift+S for a full screen grab and Ctrl+Shift+A for a grab of a selected area.


This month's screengrab tool can capture the screen, a selected window, a window's client area or a selected area. It can also save and load images in a variety of graphic formats.

It turns out that this is quite easy to do with the help of a couple of Windows API functions. To define a hotkey you need to use the RegisterHotKey() function. This takes four arguments, hWnd, id, fsModifiers and vk. The first argument, hWnd, is the handle to the window that will receive WM_HOTKEY messages generated by the hotkey. The second argument is an integer which is used to identify a hotkey. The third argument is a key modifier derived from the WM_HOTKEY message. This argument may be decomposed into wParam and lParam values as explained in the Win32 SDK help. For our purpose, only the low-order word, lParam, is of interest. This may have one or more values indicating the four special keys, MOD_ALT, MOD_CONTROL, MOD_SHIFT and MOD_WIN. Finally, the fourth argument to RegisterHotKey() is either a virtual key code as defined in Delphi’s Windows.pas unit or the integer value of an alphanumeric key as returned by the Ord() function.


If a hotkey has already been registered (possibly by some other application), an error message is displayed

This is how you would register the HotKey Alt+Shift+F2, which is identified by the integer, 2, and which sends messages to the current Form whose handle is self.Handle:

RegisterHotkey(self.Handle, 2, MOD_ALT or MOD_SHIFT , VK_F2)

The RegisterHotKey() function returns a Boolean value of True if it succeeds. It will fail if the hotkey has already been registered. To see an example of registering three hotkeys, load my HotKey.dpr project and find the RegHotKeys() procedure. This registers three hotkeys, Ctrl+X, Alt+Shift+F2 and Ctrl+Alt+F. Notice that it tests the return value of the RegisterHotKey() function and displays an error message if this value is not True.

I register these hotkeys when the application begins in Form.Create(). Every hotkey that has been registered should be unregistered subsequently. I unregister the hotkeys when the application ends in FormClose(). There are also two buttons on the form which can register and unregister the hotkeys. You will find that our error messages are shown if you attempt to re-register existing hotkeys. It is perhaps less important to display an error message if an attempt has been made to unregister a non-existent hotkey. However, if you wish to do this, you will find an example for HotKey 3 in my UnRegHotKeys() procedure.

Receiving Hotkey Messages


When you press a key combination which has been registered as a hotkey, your program can respond with some predefined action. Here it just shows a message. In our screengrab tool, however, it will initiate a specific type of capture operation

Once you’ve registered some hotkeys you need some way of responding to them when they are pressed. In order to be able to do this, you need a procedure that will specifically process the WM_HOTKEY message. For a Delphi procedure to respond to a windows message its declaration must be followed by the keyword message and an integer constant which identifies the message. This is how I declare this procedure in my code:

procedure WMHotkey( Var msg: TWMHotkey );message WM_HOTKEY;

Here TWMHotKey is a record type which is declared in Delphi’s Messages unit. This record includes a field named hotkey which provides the integer number which was used to identify the hotkey in RegisterHotKey(). In the implementation of WMHotKey() I use this value to index a Case statement and display a different message for each hotkey.

Vanishing Hotkeys

At this point it was my intention to say that it would be trivial to add hotkeys to my existing screen grabbing project, ScreenGrab.dpr. Little did I suspect the problems that I was about to run into. When I added the hotkey code to the screen grabber, everything went pear shaped. Whenever I grabbed a selected area of the screen, all the hotkeys went AWOL. One moment they worked, the next moment they didn’t.

I’ll save you the details of the frustrating hours that followed as I tried to debug this problem. Since it was only when I grabbed a selected area of the screen that the problem arose, I initially assumed that there must be something dodgy about the mouse-handling methods responsible for marking off a rectangular selection area. After much testing, however, I was able to give these methods a clean bill of health.

Eventually, I tracked down the problem to this line of code in the MaximizeForm() method which is called by GrabSelectedArea():

Form1.FormStyle := fsStayOnTop;

In the initial version of this project, as soon as that code executed, the hotkeys vanished. It looks a harmless enough assignment and I was surprised that it caused any problems. The only explanation I could think of was that in some way this assignment might change the handle to the main form. You will recall that the Handle is required in the RegisterHotKey() function. I wrote some code to display the Handle (an integer value) before and after the FormStyle is changed and, sure enough, the Handle changes too!

Lost Handles

This, I must admit, struck me as bizarre. Why on earth would the handle of a window change like this? Fortunately, I have the Architect version of Delphi which includes the source code of its runtime libraries. On searching for the SetFormStyle() method which executes when the FormStyle property is changed I found that it does indeed destroy the window handle and recreates the window with a new handle. One way to get around this problem is to use the following API call instead of setting the FormStyle property to fsStayOnTop:

SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE);

In my code, the window is returned to its non-topmost state in the RestoreForm() method. This can be accomplished with the following API call:

SetWindowPos(Handle, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE);

But there is one more problem. Setting the BorderStyle property also changes the window handle. The BorderStyle has to be changed in MaximizeForm() in order to create a borderless full-screen image and in RestoreForm() in order to return the form to its default state. A glance through Borland’s source code confirms that an assignment to BorderStyle recreates a window. A glance through Microsoft’s Win32 documentation confirms that this is the expected behaviour!

My initial thought was to create a separate borderless form at design time on which the image could be displayed. In fact, the solution which I eventually adopted is much simpler. Instead of registering the hotkeys at the time of form creation, I now register them when the Grab menu (or the Ctrl+G shortcut) is pressed and unregister them before changes are made to FormStyle and BorderStyle. I then register them anew (that is, to the Handle of the newly recreated window) after these changes are made. This ensures that the hotkeys are always associated with the current window handle. This is done before an after the call to MaximizeForm() in the GrabSelectedArea() event-handler.

The Great Escape

The actual techniques of assigning and using hotkeys are much the same as in my previous project. The principal difference is that the Case statement in WMHotKey() now invokes the various screen grabbing routines rather than merely displaying message boxes. To grab the whole screen press Ctrl+G...


Click Ctrl+G or select the Grab menu to start the screen capture process

Then, when you are ready, press Ctrl+Shift+S. To grab the active Window, press Ctrl+Shift+W. To grab the window’s client area, press Ctrl+Shift+C and to grab an area of the screen press Ctrl+Shift+A then use the mouse to drag out a selection rectangle.


Using techniques from last month's project, you can drag out a selection rectangle to grab a rectangular area

In the previous version of the program, there were menu items for each separate type of screen grab operation with delays built in to allow the user to find a window to grab. This is no longer necessary. Now any type of grab is initiated with Ctrl+G which executes the GrabMenuClick() method. This registers the hotkeys and hides the form. The user can now use any hotkey at any time to perform the appropriate capture operation. If you change your mind, you can press the ESCAPE key. This has also been registered as a hotkey. All it does is redisplay the form without performing any type of screen grab.


Here, the selected area of the screen has been grabbed and copied into the screengrabber

Having now implemented four screen capture options and assigned hotkeys to each of them, all I next needed to do was to provide a way of saving and loading my grabs to and from disk. Fortunately, Delphi provides all the routines needed to save bitmaps and other graphics formats including - with a little extra effort - JPEGs.

Saving Images

When you want to save and load files into a TImage object, you need to make use of its Picture property. It is the Picture which provides the SaveToFile() and LoadFromFile() methods. By default, it can save and load images in Bitmap, icon and Windows metafile formats. These are indicated by the extensions, .BMP, .ICO and .EMF or .WMF. You can save and load images in code or with the help of standard file dialogs. Delphi 7 and later also provides two special dialogs, SavePictureDialog and OpenPictureDialog which have some useful features such as the ability to display a mini preview of the selected image and an automatic ability to construct a list of file ‘filters’ as a drop-down selection list.


Special Picture dialogs make it easy to save and load images in a variety of graphics formats

As with standard file dialogs the Picture dialogs are displayed by calling the Execute() method. If the user selects a file name and clicks the ‘OK’ button, this method returns a True value and the dialog’s FileName property will be set to the selected file. So this is all the code you need to load a graphics file using an OpenPictureDialog:

if OpenPictureDialog1.Execute then
   image1.Picture.LoadFromFile(OpenPictureDialog1.FileName); 

The procedure for saving a graphic is similar apart from the fact that you would need to execute a SavePictureDialog and use the image1.Picture.SaveToFile() method to save the image to disk. In fact, in my Save1Click() method, you will see that I have added some extra code. First I check that there is a picture to save. If image1.Picture.Graphic equals nil there is no picture currently in the image control and an error message will be displayed:

if Image1.Picture.Graphic = nil then
ShowMessage('No picture to save!')

Next I check the file extension of the file being saved. If it is ‘.JPG’ then I call a separate method to save the picture in JPEG format, Otherwise, the picture is automatically saved in one of the standard formats according to its extension.

Saving JPEGs

If you want to be able to save graphics in JPEG format, you will need to add the Jpeg unit to the uses section of your code. Once you’ve done this, the JPG and JPEG extensions become available to the dialogs used to open and save pictures. The JPEG format also becomes available to your code. In order to save a file in JPEG, you need to convert it from the default bitmap format by creating an object of the TJPEGImage type and assigning the bitmap to it. In my project this is done by assigning the bitmap to the object, jp, in the SaveJPG() method:

with jp do
begin
Assign(Form1.Image1.Picture.Bitmap);
SaveToFile(fn); end;

The JPEGImage class contains numerous routines to scale, colour and compress images. You can view documentation via the help file but the source of the Jpeg unit is, unfortunately, not supplied.

Adjusting JPEG Quality

In some versions of Delphi such as Delphi 7 (though not, as far as I can see, Delphi 2005), there is a sample program, JPEGProj.dpr which is located in the \Help\Examples\Jpeg subdirectory beneath your main Delphi directory. This program shows how you can change the size, quality and resolution of a JPEG image, make a colour image greyscale and create progressive JPEGs which load gradually into a Web browser. This can be useful when displaying large images with a slow Internet connection.

When you examine the code you will see that image quality is easily adjusted using properties such as PixelFormat to switch between 8-bit and 24-bit format, ProgressiveDisplay and GrayScale which have simple Boolean values and Performance which can be set to either jpBestQuality or jpBestSpeed

PixelFormat := TJPEGPixelFormat(Self.PixelFormat.ItemIndex);
Grayscale := Boolean(Colorspace.ItemIndex);
Performance := TJPEGPerformance(Self.Performance.ItemIndex);
ProgressiveDisplay := Self.ProgressiveDisplay.Checked;

Notice how the code deals with the problem of attempting to load a file which is not a valid graphic. This could be due to the fact that a non-JPEG file has been given the ‘.jpg’ extension or that the file is corrupt or has an unrecognised format. The TForm1.OpenFile() method encloses the call to OpenFile() within a try..except exception handling block. If an exception occurs, the code following except is executed (unlike a try..finally statement, this code is not executed when no exception occurs). If the exception is identified as EInvalidGraphic then the graphic that failed to load is set to nil so that no attempt is made to display it:

Image1.Picture.Graphic := nil;

 Finally, if you want to be able to print images that fit correctly onto the paper being used, be sure to study the code in Print1Click(). This shows you how to use the Printer.PageWidth and Printer.PageHeight properties to scale the image prior to printing.


What Next?

In its present form, my screen grabbing program has very elementary error handling. For example, in the DrawCursor() method, I enclose the code which creates and assigns an Icon object inside a try..finally statement. When an exception occurs following try, the code following finally is executed. This code frees the TIcon object and it will always execute whether or not an exception has occurred. You will find similar exception handling in Grab() and SaveJPG(). One place where it is difficult to trap an exception is when the File Save or Open dialogs are displayed. This is because the code has no direct control until after the dialogs are closed. If the user selects a file with a graphic extension such as .jpg or .bmp but that file is of the incorrect format an exception will occur. This is because the dialog attempts to display the graphic. This exception is more of an annoyance than a threat and may be disabled prior to your final project compilation by selecting Tools, Debugger Options, Language Exceptions. Then uncheck ‘Stop on Delphi Exceptions’.

There are plenty more things that you could so to improve this application. You might want to prompt before grabbing a new picture over an existing one. You could create a new MDI (multiple document interface) ‘child’ window for each new grab. If you plan to save JPEG files regularly, you can experiment with the compression features of Delphi’s JPEG library. And, for added flexibility, you could have user-definable hotkeys.

July 2005

 


Home | Archives | Contacts

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