2

I found some weirdness in how the standard Windows edit control works.

When word wrapping is on, it accepts whole logical lines, but returns screen lines on EM_GETLINE request. However, it behaves correctly on window resizing and re-splits the text regarding the original CR-LFs.

So, my idea to find out the original logical lines was to query the screen lines with EM_GETLINE, one after other, and detect CR-LF at the end of the last screen line in a block.

Unfortunately, lines requested by EM_GETLINE do not contain CR-LFs at all.

It seems that the control is storing the CR-LFs internally, but doesn't return them on EM_GETLINE. They could be got only when requesting the whole control text with WM_GETTEXT.

Is there probably some other way to request text pieces between adjacent CR-LFs except getting the whole text and splitting it?

program WindowsEditControl;

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils;

const
  IDM_EXIT = 100;
  GAP = 10;

var
  hEdit: HWND;
  hFnt: HFONT;

function GetLine(hEdit: HWND; Index: Integer): string;
var
  Text: array[0..4095] of Char;
begin
  Word((@Text)^) := Length(Text);
  SetString(Result, Text, SendMessage(hEdit, EM_GETLINE, Index, LPARAM(@Text)));
end;

function GetTxt(hEdit: HWND): string;
var
  Len: Integer;
begin
  Len := SendMessage(hEdit, WM_GETTEXTLENGTH, 0, 0);
  SetString(Result, PChar(nil), Len);
  if Len <> 0 then
  begin
    Len := Len - SendMessage(hEdit, WM_GETTEXT, Len + 1, LongWord(PChar(Result)));
    if Len > 0 then
      SetLength(Result, Length(Result) - Len);
  end;
end;

function WndFunc(h: HWND; iMessage: UINT; w: WPARAM; l: LPARAM): LRESULT; stdcall;
var
  nWidth, nHeight: NativeUInt;
  i, j, lineLength: Integer;
  s: string;
begin
  case iMessage of
    WM_CREATE: begin
      hEdit := CreateWindowEx(WS_EX_NOPARENTNOTIFY, 'edit', 'Control #1',
        WS_BORDER or WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or ES_READONLY or WS_VSCROLL, GAP, GAP,
        810 - GAP*2 - 15, 260, h, 0, hInstance, nil);
      hFnt := CreateFont(20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'Calibri');
      SendMessage(h, WM_SETFONT, hFnt, MakeLong(1, 0));
      SendMessage(hEdit, WM_SETFONT, hFnt, MakeLong(1, 0));
      for i := 0 to 10 do begin
        for j := 0 to 20 do s := s + 'Item' + IntToStr(i) + '.' + IntToStr(j) + ' ';
        s := s + #13#10;
      end;
      SendMessage(hEdit, WM_SETTEXT, 0, LongWord(PWideChar(s)));

      //Attempts to query the edit control:
      //This gets the first SCREEN line of the edit control. CR-LFs aren't there.
      MessageBox(h, PWideChar('|'+GetLine(hEdit, 0)+'|'), 'Screen line 1', 0);
      //This gets the second SCREEN line of the edit control. CR-LFs aren't there.
      MessageBox(h, PWideChar('|'+GetLine(hEdit, 1)+'|'), 'Screen line 2', 0);
      //In a whole text are all the CR-LFs there
      MessageBox(h, PWideChar(GetTxt(hEdit)), 'Whole text', 0);
    end;
    WM_DESTROY: begin
      DeleteObject(hFnt);
      PostQuitMessage(0);
      Result := 0;
    end;
    WM_SIZE: begin
      nWidth := LOWORD(l);
      nHeight := HIWORD(l);
      SetWindowPos(hEdit, 0, GAP, GAP, nWidth - GAP*2, nHeight - GAP*2, 0);
      Result := 0;
    end;
    WM_COMMAND: if w = IDM_EXIT then PostMessage(h, WM_CLOSE, 0, 0);
    else
      Result := DefWindowProc(h, iMessage, w, l);
  end;
end;

var
  wndClass: TWndClass;
  h: HWND;
  msg: TMsg;

begin
  wndClass.style          := CS_HREDRAW or CS_VREDRAW;
  wndClass.lpfnWndProc    := @WndFunc;
  wndClass.cbClsExtra     := 0;
  wndClass.cbWndExtra     := 0;
  wndClass.hInstance      := hInstance;
  wndClass.hIcon          := 0;
  wndClass.hCursor        := LoadCursor(0, IDC_ARROW);
  wndClass.hbrBackground  := GetStockObject(WHITE_BRUSH);
  wndClass.lpszMenuName   := nil;
  wndClass.lpszClassName  := 'EditTest';

  if RegisterClass(wndClass) = 0 then Halt(0);

  h := CreateWindow(wndClass.lpszClassName, 'Edit Test', WS_OVERLAPPEDWINDOW, 35, 35, 810, 320, 0, 0, hInstance, nil);
  ShowWindow(h, SW_SHOW);

  while GetMessage(msg, 0, 0, 0) do begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;
  Halt(msg.wParam);
end.
Paul
  • 25,812
  • 38
  • 124
  • 247
  • Note that you have a potential buffer overflow with size of one char in GetTxt. – Sertac Akyuz Apr 26 '19 at 14:46
  • @Sertac Akyuz: Then it is a Delphi VCL bug. I copied the code from TControl. – Paul Apr 26 '19 at 14:54
  • The API copies the null terminator but reports non-null character count, that's a bug. I'd prefer not to replicate it. – Sertac Akyuz Apr 26 '19 at 16:31
  • 1
    @Paul `EM_GETLINE` is the only way to retrieve individual lines from an edit control, otherwise you would have to use `WM_GETTEXT` to get the whole text and then parse out the lines yourself. During word wrapping, the edit control simply inserts its own "soft" breaks into the text by default, and removes them as needed. `EM_GETLINE` and `EM_GETLINECOUNT` treat those "soft" breaks like any other line break. See [Handling Wordwrap and Line Breaks](https://docs.microsoft.com/en-us/windows/desktop/controls/edit-controls-text-operations#handling-wordwrap-and-line-breaks) on MSDN for more details. – Remy Lebeau Apr 26 '19 at 20:22
  • @SertacAkyuz I see no buffer overflow. A `string` is being used for the buffer, and a `string` always has a null terminator, even though it is not counted by the string's `Length`. `SetLength()` will allocate room for `length+1` chars. "*The API copies the null terminator but reports non-null character count, that's a bug*" - no, it is not. Most APIs that deal with null-terminated strings do that. It is perfectly normal. – Remy Lebeau Apr 26 '19 at 20:23
  • @Remy - What I said about this API is accurate. – Sertac Akyuz Apr 26 '19 at 20:24
  • @SertacAkyuz you claim the API behavior is a bug. It is not a bug, it is documented behavior. `WM_GETTEXTLENGTH` is documented to not include the null terminator in the count of chars. `SetLength()` includes extra room for a null terminator. `WM_GETTEXT` is documented to output a null terminator in the buffer but to not report the null terminator in the returned count ofchars. – Remy Lebeau Apr 26 '19 at 20:26
  • @Remy - I don't claim the API is buggy, it does what the documentation states. – Sertac Akyuz Apr 26 '19 at 20:27
  • You are right about the null terminator for a Delphi string though. I thought it shouldn't be the case because Delphi strings can include null characters. [Docs](http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Internal_Data_Formats_(Delphi)#Long_String_Types) say otherwise though. So, OK, no overflow there. – Sertac Akyuz Apr 26 '19 at 20:27
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/192453/discussion-between-remy-lebeau-and-sertac-akyuz). – Remy Lebeau Apr 26 '19 at 20:28
  • On a side note: when `GetTxt()` sends `WM_GETTEXT`, `LongWord` needs to be changed to `LPARAM` instead, or the code will not work in a 64bit app. Also, there is no need to calculate the new buffer length manually after `WM_GETTEXT` returns, it tells you exactly how many chars are valid: `function GetTxt(hEdit: HWND): string; begin SetString(Result, PChar(nil), SendMessage(hEdit, WM_GETTEXTLENGTH, 0, 0)); if Result <> '' then begin SetLength(Result, SendMessage(hEdit, WM_GETTEXT, Length(Result) + 1, LPARAM(PChar(Result))); end; end;` – Remy Lebeau Apr 26 '19 at 20:35

0 Answers0