Search code examples
delphikeypressmessagingvcl

Delphi VCL form Arrow Key management solution fires twice


I have a VCL form with many tframes (referred to as tcellFrame) containing components arranged in a grid. I am using mouse clicks and arrow keys for the user to navigate between them. The mouse clicks work fine, but I had trouble with the arrow keys until I discovered this question thread: Delphi XE and Trapping Arrow Key with OnKeyDown. The solution in Sertac Akyuz's answer does handle getting the arrow key messages to the form using

procedure TForm1.DialogKey(var Msg: TWMKey);
begin
  case Msg.CharCode of
    VK_DOWN, VK_UP, VK_RIGHT, VK_LEFT:
      if Assigned(onKeyDown) then
        onKeyDown(Self, Msg.CharCode, KeyDataToShiftState(Msg.KeyData));
    else
      inherited
  end;
end;

but acts upon the form twice for each key stroke. Instead of moving to the left one cellframe, it moves two. Tracing the thread using the debugger demonstrates that the onkeydown event is called twice.

My onKeyDown event is structured as follows:

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  i : integer;
  g, r, c, str : string;
  tmpFrame : tcellFrame;  //frame component containing tlabels
begin
  ...

  case key of
    VK_UP:
      begin
        //calc new cell location values for g,r,c
        str := ('Cell'+g+r+c);
        picklist.Clear;  // picklist is a form-wide tstringlist variable containing currently selected cellframes.
        picklist.add (str);
        TmpFrame := FindComponent(picklist [0]) as TCellFrame;
        tmpframe.Color := pickClr;
      end;
    //VK_DOWN, VK_LEFT, VK_RIGHT: defined similarly to VK_UP

  end;
end;

There is more code in Formkeydown, but it is all internal calculations to determine the proper tcellframe name to place in the picklist.

My questions are:

  • What is causing this repeat to occur?
  • How do I terminate the message implementation after its first instance?

Solution

  • In your CM_DIALOGKEY message handler, return a non-zero value if you handle the key, then it won't be dispatched further.

    procedure TForm1.DialogKey(var Msg: TWMKey);
    begin
      case Msg.CharCode of
        VK_DOWN, VK_UP, VK_RIGHT, VK_LEFT:
          begin
            if Assigned(onKeyDown) then
              onKeyDown(Self, Msg.CharCode, KeyDataToShiftState(Msg.KeyData));
            Msg.Result := 1; // <-- add this
          end;
        else
          inherited;
      end;
    end;
    

    However, if you have KeyPreview=True on the Form and the arrow keys are already being dispatched normally, then there is no need to handle CM_DIALOGKEY at all, just let the Form's OnKey... events get dispatched normally. You should not be firing the Form's OnKey... events from a CM_DIALOGKEY handler.

    See A Key's Odyssey for more details.