Interpreting Delphi IDE Keyboard Macros (Updated)

Keyboard macros have been part of the Delphi IDE since basically forever (I remember using them in Delphi 5 but I wouldn’t rule it out that they already existed in Delphi 1 which I never used.)

GExperts also has had the Macro Library Expert since I know about it to overcome the Delphi IDE’s shortcoming of having only one keyboard macro.

This expert handles the macros as a black box, storing them in a binary stream which is used to get them from and pass them to the IDE and to save and load them from files.

So far I haven’t seen any documentation of the format these macros are stored in. That’s a real shame because it prevented the Macro Library Expert to become more useful, by allowing to edit existing macros.

I have had a look at data stored in its configuration files and tried to interpret them. Here is what I found:

A keyboard macro is a stream of bytes. It always seems to start with the four bytes 54 50 45 52 (ASCII “TPOR”, no idea what it means. TP looks suspiciously like Turbo Pascal). This seems to be a kind of signature. I have not tried to modify it to see what the IDE makes of it, I just want to understand the actual macros. This signature is followed by a single byte that is either 00 or 01. 00 seems to mark the end of the macro. 01 is followed by 4 bytes, which seem to belong together as two separate words, e.g. 61 00 00 00, and describe a key press. Each such 4 byte sequence is again followed by a single byte that is either 00 or 01. 00 again, marks the end of the macro, 01 means that another another key press follows etc.

54 50 54 52
01  61 00  00 00
00

would be a keyboard macro that simply types the lower case letter “a”.

54 50 54 52
01  61 00  00 00
01  62 00  00 00
00

would be “ab”.

Let’s have a closer look at the four bytes describing a key press:

The first word describes the main key, the second word describes modifier keys. The modifier word can have the following values:

  • 00 00 – meaning the main key is a normal character
  • 88 00 – meaning the main key is a special key like the arrow keys or insert / del / page up/down etc.
  • 10 00 added to it means that the Ctrl key is being pressed
  • 20 00 added to it means that the Shift key is being pressed
  • 40 00 added to it means that the Alt key is being pressed

I have not seen any other values.

Please keep in mind that I represent the bytes in the order they show up in the stream. When interpreting two consecutive bytes as words, you have to switch them around according to Intel convention, so the bytes 10 00 actually mean the the number $0010, and 88 00 is the number $0088.

If the modifier word is $0000, the main key is in its simplest form the ASCII code of a character, e.g. like you can see in GExperts ASCII Chart:

I have yet to check whether the value is actually a WideChar.
update: It definitely is a WideChar, so the code below won’t work correctly for input that uses the high byte of the character.

If the modifier value is <> $0000 it describes a special key, e.g.
$0020 is the Space key, $0021 is PgUp, $0022 is PgDn, $002D is Ins key, $002E is Del etc.

update: The above applies to all Delphi versions that I checked. That includes Delphi 6, 7, 2005, 2006, 2007 and 10.1. Note that Delphi 6 and 7 apparently do not support Unicode characters in keyboard macros. I tried to enter some Arabic characters (Unicode $30xx) and they were converted to characters in $00xx range.

Based on my observations I have so far written the following code to interpret a keyboard macro:

type
  TMenuKeyCap = (
    mkcBkSp, mkcTab, mkcEsc, mkcEnter, mkcSpace, mkcPgUp,
    mkcPgDn, mkcEnd, mkcHome, mkcLeft, mkcUp, mkcRight,
    mkcDown, mkcIns, mkcDel, mkcShift, mkcCtrl, mkcAlt);

var // these resource strings are declared in unit Consts
  MenuKeyCaps: array[TMenuKeyCap] of string = (
    SmkcBkSp, SmkcTab, SmkcEsc, SmkcEnter, SmkcSpace, SmkcPgUp,
    SmkcPgDn, SmkcEnd, SmkcHome, SmkcLeft, SmkcUp, SmkcRight,
    SmkcDown, SmkcIns, SmkcDel, SmkcShift, SmkcCtrl, SmkcAlt);

function KeyCodeToText(Code: Word; Modifier: Word): string;
var
  LoByte: Byte;
begin
  Result := Format('unkown (%.4x)', [Code]);
  if (Modifier and $88) <> 0 then begin
    // special keys
    Result := '';
    if (Modifier and $10) <> 0 then
      Result := Result + MenuKeyCaps[mkcCtrl];
    if (Modifier and $40) <> 0 then
      Result := Result + MenuKeyCaps[mkcShift];
    if (Modifier and $20) <> 0 then
      Result := Result + MenuKeyCaps[mkcAlt];
    LoByte := (Code and $FF);
    case LoByte of
    // $00..$07
      $08, $09: // backspace / tab
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcBkSp) + LoByte - $08)];
      $0D: Result := Result + MenuKeyCaps[mkcEnter];
      $1B: Result := Result + MenuKeyCaps[mkcEsc];
    // $1B..$1F ?
      $20..$28: // space and various special characters
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcSpace) + LoByte - $20)];
    // $29..$2C ?
      $2D..$2E: // Ins, Del
        Result := Result + MenuKeyCaps[TMenuKeyCap(Ord(mkcIns) + LoByte - $2D)];
    // $2F ?
      $30..$39: // 0..9
        Result := Result + Chr(LoByte - $30 + Ord('0'));
      $41..$5A: // A..Z
        Result := Result + Chr(LoByte - $41 + Ord('A'));
      $60..$69: Result := Result + Chr(LoByte - $60 + Ord('0'));
      $70..$87: Result := Result + 'F' + IntToStr(LoByte - $6F);
    end;
  end else begin
    LoByte := (Code and $FF);
    case LoByte of
      // $00..$07
      $08, $09: // backspace / tab
        Result := MenuKeyCaps[TMenuKeyCap(Ord(mkcBkSp) + LoByte - $08)];
      $0D: Result := MenuKeyCaps[mkcEnter];
      $1B: Result := MenuKeyCaps[mkcEsc];
      // $1B..$1F ?
      $20..$7E: Result := Chr(LoByte);
      $7F: Result := MenuKeyCaps[mkcDel];
      $80..$FF: Result := Chr(LoByte);
    end;
  end;
end;

function TMacroInfo.TryDecode(AStrings: TStrings): Boolean;

  function Read(var Buffer; Count: Longint): Boolean;
  begin
    Result := (FStream.Read(Buffer, Count) = Count);
  end;

var
  Magic: Longword;
  Flag: Byte;
  HiWord: Word;
  LoWord: Word;
  s: string;
begin
  Result := False;
  FStream.Position := 0;
  if not Read(Magic, SizeOf(Magic)) or (Magic <> $524F5054) then
    Exit;
  if not Read(Flag, SizeOf(Flag)) then
    Exit;
  while Flag = $01 do begin
    if not Read(LoWord, SizeOf(LoWord)) then
      Exit;
    if not Read(HiWord, SizeOf(HiWord)) then
      Exit;
      s := KeyCodeToText(LoWord, HiWord);
    AStrings.Add(s);
    if not Read(Flag, SizeOf(Flag)) then
      Exit;
  end;
  Result := True;
end;

It’s not finished and might never be. It’s also far from perfect. I am posting this here in the hope that it might be useful to somebody. I might use it to extend the Macro Library Expert in GExperts to allow limited editing of keyboard macros.