A Keystroke Recorder

Written by Stefan Ruck (January 1999)

[Here is a link to the sources for this article. Ed.]

Introduction
In this article I will show you an easy way to record keystrokes and, of course, how to play back the keys recorded, using IBM's Open Class Library. I'm not covering the handling of these recorded keystrokes as macros, i.e. how to edit and save them.

The Ingredients
What we need is a frame window, a keyboard handler, an edit control, a static text and an ordered collection.

The frame window will do at least all the work which have to be done to record and playback keystrokes. The keyboard handler will help the frame window to do its job. The edit control, as the client of the frame window, is what shows the keys pressed. The static text, as the status line of the frame window, is used to show the state of the recorder. Finally the ordered collection is needed to save the keystrokes in the right sequence.

The Preparation
I will focus here on the frame window, the edit control and the ordered collection. The status line is not of interest, because it is not needed for the recorder handling at all. It is only used to show the user the recorder state. If you leave it out, the recorder will still do its job as well. Please have a look at the source code to see its usage.

Also I'm not explaining the set up of the client area and the menu bar of the frame window here. In this article the only thing of interest is how to record and playback the keystrokes.

The Basics
To be able to handle the keyboard events ourselves, we need to inherit one class from IKeyboardHandler and override its virtual functions Boolean IKeyboardHandler::virtualKeyPress(IKeyboardEvent & event) and Boolean IKeyboardHandler::characterKeyPress(IKeyboardEvent & event).

I've decided to use the frame window to handle the recording, not the edit control. This will easily give us the ability to play back the recorded keystrokes, not only in the control they were recorded, but in any other control which can be reached by the frame window. Because there is only one edit control in the sample application of this article, it makes no difference which object (the frame window or the edit control) is derived from IKeyboardHandler. But when you implement a multiple document interface it will make life much easier to let the frame window do the job.

Since the handling of both (virtual and character key press) keyboard events is the same, we just have to call one function to do it. // handling virtual key presses virtual Boolean virtualKeyPress (IKeyboardEvent &event) { return (handleKeyPress (event)); }

// handling character key presses virtual Boolean characterKeyPress (IKeyboardEvent &event) { return (handleKeyPress (event)); }

//******************************************* // AKeystrokeRecorderApp :: handleKeyPress * // - standard key press handling           * //******************************************* Boolean AKeystrokeRecorderApp :: handleKeyPress (IKeyboardEvent & event)

{  // add the event to the list m_pKeyRecorderList -> addAsLast (event);

// pause from event handling to avoid loop IKeyboardHandler :: stopHandlingEventsFor (m_pEditField);

// send the event to the edit field m_pEditField -> sendEvent (event);

// resume the event handling IKeyboardHandler :: handleEventsFor (m_pEditField);

return (true); } Figure 1: Handling the keyboard events

In the source code above, AKeystrokeRecorderApp is my application class derived from IFrameWindow and IKeyboardHandler.

AKeystrokeRecorderApp::m_pKeyRecorderList is of type ISequence<IKeyboardEvent></tt> and stores all keyboard events. AKeystrokeRecorderApp::m_pEditField</tt> is an IMultiLineEdit</tt> control.

The handling of each key press is as follows. First the event is added as the last member to the list of recorded events. It is important to add it last, because the sequence must be kept for playback. Then we have to stop the keyboard event handling to avoid a loop. Otherwise, we will never come back from this method because it will always call itself, since it sends the event to the edit control, whose keyboard events we are handling... The event itself must be sent, not posted, to the edit control to show the character or whatever. Using IWindow::sendEvent</tt> makes sure to retrieve control when the event is handled completely by the edit control. Posting it we can't tell when it'll be handled and so we would take the risk of losing it in the handling loop. Finally we have to resume the keyboard event handling for the edit control to be able to record the next event.

I've done some execution analysis to see whether it'll be better to use a flag to avoid the message loop or turn the event handling on and off like I did it. The analysis showed me that it makes no difference at all. The execution time and the sum of function calls is the same.

The Steering
I have put three menu items into the frame window's menu to control the recorder. One to start/stop, one to pause/resume and one for playback.

Start/Stop
//********************************************* // AKeystrokeRecorderApp :: recorderStartStop * // - starts or stops the key recorder        * //********************************************* void AKeystrokeRecorderApp :: recorderStartStop

{  // do we already record the keystrokes? if (m_bRecordKeystrokes) {         // yes, so stop it, reset the flags m_bRecordKeystrokes = false; m_bRecordingPaused = false; // reset the menu m_pMenuBar -> setText (MI_RECORDER_RECORD,                                IResourceId (STR_START_RECORDER)); // only enabled if list is not empty m_pMenuBar -> enableItem (MI_RECORDER_PLAY,                                   !m_pKeyRecorderList -> isEmpty ); m_pMenuBar -> enableItem (MI_RECORDER_PAUSE, false); m_pMenuBar -> setText (MI_RECORDER_PAUSE,                                IResourceId (STR_PAUSE_RECORDER)); // stop the keyboard-event handling IKeyboardHandler :: stopHandlingEventsFor (m_pEditField); // clear the text of the status line m_pStatusLine -> setText (""); return; }

// we are not recording, so do it

// clean up the old keystrokes delete m_pKeyRecorderList;

m_pKeyRecorderList = 0L;

// get a new list m_pKeyRecorderList = new ISequence<IKeyboardEvent>;

// update the menu m_pMenuBar -> setText (MI_RECORDER_RECORD,                         IResourceId (STR_STOP_RECORDER));

m_pMenuBar -> enableItem (MI_RECORDER_PLAY, false);

m_pMenuBar -> enableItem (MI_RECORDER_PAUSE, true);

// set the flags to the current values m_bRecordKeystrokes = true; m_bRecordingPaused = false;

// handling the keyboard-events of the edit field // so we can record them IKeyboardHandler :: handleEventsFor (m_pEditField);

// set the text of the status line m_pStatusLine -> setText (IResourceId (STR_RECORDING)); } Figure 2: Start/stop the recording

Since we receive just one command id when the menu item for start/stop recording is selected, we need a flag to indicate what to do. This is AKeystrokeRecorderApp::m_bRecordKeystrokes</tt>. When true, recording is in progress and has to be stopped. Otherwise we should start the recording.

The way to start/stop the recording is to call IKeyboardHandler::handleEventsFor(m_pEditField)</tt> to start and IKeyboardHandler::stopHandlingEventsFor(m_pEditField)</tt> to stop. The rest is to keep the menu and the status line up to date so the user will know what's going on. As soon as IKeyboardHandler::handleEventsFor</tt> is called, any keyboard events for the edit control will be handled by AKeystrokeRecorderApp::handleKeyPress</tt> (see figure 1).

Pause/Resume
//*********************************************** // AKeystrokeRecorderApp :: recorderPauseResume * // - pause/resume the recorder                 * //*********************************************** void AKeystrokeRecorderApp :: recorderPauseResume

{  if (m_bRecordingPaused)  // is the pause active? {         // yes, resume the recording // switch the menu text m_pMenuBar -> setText (MI_RECORDER_PAUSE,                                IResourceId (STR_PAUSE_RECORDER)); // resume the keyboard-event handling IKeyboardHandler :: handleEventsFor (m_pEditField); // set the flag m_bRecordingPaused = false; // set the text of the status line m_pStatusLine -> setText (IResourceId (STR_RECORDING)); }  else {         // no, so pause the recording // switch the menu text m_pMenuBar -> setText (MI_RECORDER_PAUSE,                                IResourceId (STR_RESUME_RECORDER)); // stop the keyboard-event handling IKeyboardHandler :: stopHandlingEventsFor (m_pEditField); // set the flag m_bRecordingPaused = true; // set the text of the status line m_pStatusLine -> setText (IResourceId (STR_PAUSED)); } } Figure 3: Pause/resume the recording

Like for start/stop, we also just receive one command id for pause/resume. So I created another flag to indicate the pause state, AKeystrokeRecorderApp::m_bRecordingPaused</tt>.

The key calls are equivalent to AKeystrokeRecorderApp::recorderStartStop</tt>, IKeyboardHandler::stopHandlingEventsFor(m_pEditField)</tt> to pause and IKeyboardHandler::handleEventsFor(m_pEditField)</tt> to resume. As above, the rest of the code is to keep the menu and the status line up to date.

Play
//****************************************** // AKeystrokeRecorderApp :: recorderPlay  * // - replay the recorded key strokes      * //****************************************** void AKeystrokeRecorderApp :: recorderPlay

{  // do we have recorded key strokes? if (!m_pKeyRecorderList        || m_pKeyRecorderList -> isEmpty ) // no, so return return;

// set the text of the status line m_pStatusLine -> setText (IResourceId (STR_PLAY));

// get a cursor for the list ICursor * pCursor = m_pKeyRecorderList -> newCursor ;

m_pKeyRecorderList -> setToFirst (*pCursor);

// send all the recorded keystrokes to the edit field do     { m_pEditField -> sendEvent (m_pKeyRecorderList -> elementAt (*pCursor)); }// no more events, so break the loop while (m_pKeyRecorderList -> setToNext (*pCursor));

//clean up  delete pCursor;

// clear the text of the status line m_pStatusLine -> setText (""); } Figure 4: Playback the keystrokes

Now this is the easiest part. We just have to check if there is something to playback. If so, we send each member of the list to the edit control. Because we've added the latest event at the end of the list in AKeystrokeRecorderApp::handleKeyPress</tt>, we can take the events in the sequence they are stored.

You may wonder why I didn't call IKeyboardHandler::stopHandlingEventsFor(m_pEditField)</tt> first. But having a closer look at AKeystrokeRecorderApp::recorderStartStop</tt> you will see that as long as recording is enabled, playback is disabled in the menu. So we don't have to worry about that.

Bon Appétit
That's all. As you see, it is really simple to record and play back keystrokes. If you feel the need to filter some events which should not be recorded, you can do this by expanding the virtualKeyPress, characterKeyPress or handleKeyPress method of your application.

If you would like to refine the recorder, write an editor to save, load and change the macros. I would really like to see your enhancements in EDM/2 soon.

In the source files there are two makefiles included. The one ending with .mav was made for IBM's VisualAge C++. The one ending with .mac isn't a MAC file but the C Set++ makefile.