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.
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
|