1

I'm trying to develop a routine in Delphi XE to output text to the screen to simulate typing text in a game. It's not as simple as concatenating characters to a TMemo for example, as this results in flickering even when using beginupdate/endupdate. I've used the technique here, converted in to Delphi code as best I can and modified it based on comments here but I'm not getting the correct text. I'm getting garbage I guess because the characters are somehow not in the right format. Does anyone see what is wrong here or know a better way to simulate typing?

procedure SendEnter();
var
  KeyInputs: array [0..1] of TInput;
begin
  ZeroMemory(@KeyInputs,sizeof(KeyInputs));
  KeyInputs[0].Itype := INPUT_KEYBOARD;
  KeyInputs[0].ki.wVk := VK_RETURN;
  KeyInputs[0].ki.dwFlags := 0;
  KeyInputs[1].Itype := INPUT_KEYBOARD;
  KeyInputs[1].ki.wVk := VK_RETURN;
  KeyInputs[1].ki.dwFlags := KEYEVENTF_EXTENDEDKEY;
  SendInput(2, KeyInputs[0], SizeOf(TInput));
end;

function TForm2.PrintOutLine(sLine: string): boolean;
var
  i: integer;
  VKRes: SmallInt;
  VK: byte;
  KeyInputsU: array [0..3] of TInput;
  KeyInputsL: array [0..1] of TInput;
  bUppercase: boolean;
begin
  Memo1.SetFocus;
  i:= 1;
  while (i<=Length(sLine)) do begin
    bUppercase:= sLine[i]=UpCase(sLine[i]);
    VKRes:= VkKeyScanEx(sLine[i],GetKeyboardLayout(0));
    VK:= VKRes;
    if (bUppercase) then begin
      ZeroMemory(@KeyInputsU,sizeof(KeyInputsU));
      KeyInputsU[0].Itype := INPUT_KEYBOARD;
      KeyInputsU[0].ki.wVk := VK_LSHIFT;
      //KeyInputsU[0].ki.dwFlags := 0;
      KeyInputsU[1].Itype := INPUT_KEYBOARD;
      KeyInputsU[1].ki.wVk := vk;
      KeyInputsU[1].ki.dwFlags := KEYEVENTF_UNICODE ;
      KeyInputsU[2].Itype := INPUT_KEYBOARD;
      KeyInputsU[2].ki.wVk := vk;
      KeyInputsU[2].ki.dwFlags := KEYEVENTF_UNICODE or KEYEVENTF_KEYUP;
      KeyInputsU[3].Itype := INPUT_KEYBOARD;
      KeyInputsU[3].ki.wVk := VK_LSHIFT;
      KeyInputsU[3].ki.dwFlags := KEYEVENTF_KEYUP;
      SendInput(4, KeyInputsU[0], SizeOf(TInput));
    end
    else begin
      ZeroMemory(@KeyInputsL,sizeof(KeyInputsL));
      KeyInputsL[0].Itype := INPUT_KEYBOARD;
      KeyInputsL[0].ki.wVk := vk;
      KeyInputsL[0].ki.dwFlags := KEYEVENTF_UNICODE;
      KeyInputsL[1].Itype := INPUT_KEYBOARD;
      KeyInputsL[1].ki.wVk := vk;
      KeyInputsL[1].ki.dwFlags := KEYEVENTF_UNICODE or KEYEVENTF_KEYUP;
      SendInput(2, KeyInputsL[0], SizeOf(TInput));
    end;
    Application.ProcessMessages;
    Sleep(80);
    inc(i);
  end;
  SendEnter;
  Form2.SetFocus;
  Result:= True;
end;
fullerm
  • 406
  • 1
  • 8
  • 23
  • 2
    Why do you use this technique for your own control? You can simply append text by concatenating value of your memo's `Text` property. The rest can be about using `UpperCase` function. – Victoria Jun 23 '18 at 14:15
  • @Victoria This is a test routine for something much more complicated. – fullerm Jun 23 '18 at 14:19
  • @Victoria concatenating to a memo even using beginupdate/endupdate results in a flicker while this technique does not. – fullerm Jun 23 '18 at 15:00
  • Aha, I see now. So it's about your own control and the problem is flickering. Well, the reason behind is that `TMemoStrings` locks control drawing (by the `WM_SETREDRAW` message) when you call `BeginUpdate` and refreshes the control when you finish your updates by calling `EndUpdate`. Still, [something like this](https://pastebin.com/1suSVXe7) might be better workaround than sending key strokes, I think. But there must be cleaner way.. – Victoria Jun 23 '18 at 15:07
  • Sorry for ignoring your original question. I know it's not a cure but you might consider enabling for your memo control `DoubleBuffered` property in case you're doing some heavy updates. – Victoria Jun 23 '18 at 15:32
  • DoubleBuffered is usually a bad idea. If you want to add some text then faking input is the wrong way. As far as the question it is always wrong to send input events one at a time. Put them in an array and send them all together. There are so mag examples of how to do this I find it astounding that such questions still get asked. Use websearch and take your time. – David Heffernan Jun 23 '18 at 15:34
  • @David, I know that setting `DoubleBuffered` is no good. So as is sending key strokes to avoid flickering (if that's the underlying problem). – Victoria Jun 23 '18 at 15:39
  • @David Heffernan using arrays gives the same problem as when the shift press wFlags was set to 0 in the original code, some garbage output. At least without using arrays I didn't get garbage characters. – fullerm Jun 23 '18 at 16:10
  • 1
    This question about SendInput is clearly an XY problem. The real problem is adding to a memo in a flicker-free way. – Rudy Velthuis Jun 23 '18 at 16:18
  • fullerm, you keep ignoring the point that sending key strokes is not a good way (I'd say in any case) and your real problem is elsewhere, as @Rudy, me and David said. – Victoria Jun 23 '18 at 16:20
  • 3
    FYI, calling `SendInput()` with `nInputs=1` is almost always a bug, as it defeats the whole purpose of why `SendInput()` was created. Build up an array of inputs and send it all at one time instead. And when dealing with text, it is a lot easier to use the `KEYEVENTF_UNICODE` flag to send actual text characters instead of dealing with shift states and virtual key codes. – Remy Lebeau Jun 23 '18 at 16:20
  • 1
    @fullerm: You clearly didn't get the advice given by David and Victoria: stop using SendInput and be sure to add multiple characters at once (e.g. line-wise or bigger blocks). If you do that properly, you don't get any garbage. – Rudy Velthuis Jun 23 '18 at 16:24
  • 1
    The fact that you have other bugs doesn't make it right to sent events one at a time. Do you want to learn or not? – David Heffernan Jun 23 '18 at 16:29
  • Anyway, why do you think that the ordinal value of a character will be it's virtual key code? – David Heffernan Jun 23 '18 at 17:40
  • @DavidHeffernan virtual key codes for characters `'0'`..`'9'` and `'A'`..`'Z'` are the same as their ASCII numeric values. This is not true for other ASCII characters for punctuation and symbols and such, though. – Remy Lebeau Jun 23 '18 at 23:23
  • @remy So I was right – David Heffernan Jun 24 '18 at 08:00
  • I've tried to implement all suggestions and make things clearer. Capitalization is now working but still getting garbage for non-alpha characters. How do you format the character value properly? – fullerm Jun 24 '18 at 11:07
  • @fullerm: read Remy's last comment. Be sure to send the keycodes, not the char values. Or use KEYEVENTF_UNICODE to send UTF-16 (Delphi Char) values. – Rudy Velthuis Jun 24 '18 at 12:11
  • @Rudy, still we are missing that Y point here (I know, the question is still X :) Sorry for being curious, how would you personally avoid a heavily updated `TMemo` flickering? Sending key strokes sounds like the last thing we would do. Something [like this](https://pastebin.com/1suSVXe7) might be kind of better than sending key strokes, but still no good. `DoubleBuffered` still no good (even though I would use it in this case). What else would you suggest? – Victoria Jun 24 '18 at 12:54
  • 2
    @Victoria you do not need to simulate key shifts when using `KEYEVENTF_UNICODE`, you can [send text characters as-is with it](https://stackoverflow.com/a/31307429/65863). As for sending simulated keystrokes to a `TMemo`, I would simply use `Memo.SelStart := Memo.GetTextLen; Memo.SelText := 'TextToInsert' +sLineBreak;` – Remy Lebeau Jun 24 '18 at 15:27

3 Answers3

1

To answer your modified question, and your latest comment about non-alpha characters still not working correctly:

Your detection of upper/lower case fails on any character not in a .. z. If you look at the documentation for System.UpCase() it states Character values not in the range a..z are unaffected. Therefore, if you feed it a <, you will get back the same <. Your code will interpret that as an upper case character although it's not.

You have been told in a comment to send the keys (actually characters) with dwFlags KEYEVENTF_UNICODE. You seem to have adopted that only partially and also erroneously.

Note that the MSDN documentation says:

wVk: ...If the dwFlags member specifies KEYEVENTF_UNICODE, wVk must be 0.

wScan: ...If dwFlags specifies KEYEVENTF_UNICODE, wScan specifies a Unicode character

and further, for flag KEYEVENTF_UNICODE:

If specified, the system synthesizes a VK_PACKET keystroke. The wVk parameter must be zero. This flag can only be combined with the KEYEVENTF_KEYUP flag.

Ergo, you do not need to detect and separately deal with upper case and lower case characters. You just set wScan to the ordinal of the UTF-16 character you want to send . Therefore your non-alpha characters also works correctly with following code modified from yours:

function TForm3.PrintOutLine(sLine: string): boolean;
var
  i: integer;
  KeyInputsL: array [0..1] of TInput;
begin
  Memo1.SetFocus;
  i:= 1;
  while (i<=Length(sLine)) do begin
    ZeroMemory(@KeyInputsL,sizeof(KeyInputsL));

    KeyInputsL[0].Itype := INPUT_KEYBOARD;
//    KeyInputsL[0].ki.wVk := vk;            // don't use wVk with KEYEVENTF_UNICODE
    KeyInputsL[0].ki.wScan := ord(sLine[i]); // instead use wScan
    KeyInputsL[0].ki.dwFlags := KEYEVENTF_UNICODE;

    KeyInputsL[1].Itype := INPUT_KEYBOARD;
//    KeyInputsL[1].ki.wVk := vk;
    KeyInputsL[1].ki.wScan := ord(sLine[i]);
    KeyInputsL[1].ki.dwFlags := KEYEVENTF_UNICODE or KEYEVENTF_KEYUP;

    SendInput(2, KeyInputsL[0], SizeOf(TInput));

    Application.ProcessMessages;
    Sleep(80);
    inc(i);
  end;
  SendEnter;
  Form3.SetFocus;
  Result:= True;
end;

The above answers your actual question but does not consider surrogate pairs of UTF-16 codepoints. There's a full code (in C++) for that shown here

Also, not a part of your question, but I can't let it pass without a comment: Application.ProcessMessages and Sleep() is not the correct way of sending one character at a time. Use a timer instead to trigger the sending of each character.

Community
  • 1
  • 1
Tom Brunberg
  • 20,312
  • 8
  • 37
  • 54
0

The solution from Remy Lebeau also works well.

function TForm2.PrintOutLine(sLine: string): boolean;
var
  i: integer;
begin
  i:= 1;
  while (i<=Length(sLine)) do begin
    Memo1.SelStart := Memo1.GetTextLen;
    Memo1.SelText := sLine[i];
    Application.ProcessMessages;
    Sleep(80);
    inc(i);
  end;
  Memo1.SelStart := Memo1.GetTextLen;
  Memo1.SelText := #13#10;
  Result:= True;
end;
fullerm
  • 406
  • 1
  • 8
  • 23
0
procedure TypeKey(Key: Cardinal);
const KEYEVENTF_KEYDOWN = 0;
      KEYEVENTF_UNICODE = 4;
var rec: TInput;
    rLen: Integer;
    shift: Boolean;
begin
  rLen:=SizeOf(TInput);

  shift:=(Key shr 8)=1;

  if shift then begin
    rec.Itype:=INPUT_KEYBOARD;
    rec.ki.wVk:=VK_SHIFT;
    rec.ki.dwFlags:=KEYEVENTF_KEYDOWN; // or KEYEVENTF_UNICODE;
    SendInput(1,rec,rLen);
  end;

  rec.Itype:=INPUT_KEYBOARD;
  rec.ki.wVk:=Key;
  rec.ki.dwFlags:=KEYEVENTF_KEYDOWN; // or KEYEVENTF_UNICODE;
  SendInput(1,rec,rLen);

  rec.Itype:=INPUT_KEYBOARD;
  rec.ki.wVk:=Key;
  rec.ki.dwFlags:=KEYEVENTF_KEYUP; // or KEYEVENTF_UNICODE;
  SendInput(1,rec,rLen);

  if shift then begin
    rec.Itype:=INPUT_KEYBOARD;
    rec.ki.wVk:=VK_SHIFT;
    rec.ki.dwFlags:=KEYEVENTF_KEYUP; // or KEYEVENTF_UNICODE;
    SendInput(1,rec,rLen);
  end;
end;

procedure TypeString(Str: String);
var i, sLen: Integer;
begin
  sLen:=Length(Str);
  if sLen>0 then for i:=1 to sLen do TypeKey(vkKeyScan(Str[i]));
  TypeKey(VK_RETURN);
end;