A Keystroke Recorder

From EDM2
Jump to: navigation, search

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, ie. 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> and stores all keyboard events. AKeystrokeRecorderApp::m_pEditField is an IMultiLineEdit 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 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. 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) to start and IKeyboardHandler::stopHandlingEventsFor(m_pEditField) 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 is called, any keyboard events for the edit control will be handled by AKeystrokeRecorderApp::handleKeyPress (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.

The key calls are equivalent to AKeystrokeRecorderApp::recorderStartStop, IKeyboardHandler::stopHandlingEventsFor(m_pEditField) to pause and IKeyboardHandler::handleEventsFor(m_pEditField) 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, we can take the events in the sequence they are stored.

You may wonder why I didn't call IKeyboardHandler::stopHandlingEventsFor(m_pEditField) first. But having a closer look at AKeystrokeRecorderApp::recorderStartStop 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 for C++. The one ending with .mac isn't a MAC file but the CSet++ makefile.