Question

{ *
  * AControl: Control handle determined by Spy++ (e.g. 0037064A)
  * ANewText: Text to assign to control
  * AWinTitle: Window Title/Caption
  * }
function ControlSetText(const AControl, ANewText, AWinTitle: string): boolean;
  function EnumChildren(AWindowHandle: HWND; ALParam: lParam): bool; stdcall;
  begin
    ShowMessage(AControl); // if commented out - code works fine
    TStrings(ALParam).Add(IntToStr(GetDlgCtrlID(AWindowHandle)));
    Result := true;
  end;

var
  _MainWindowHandle: HWND;
  _WindowControlList: TStringlist;
  i: integer;
  _ControlHandle: integer;
begin
  Result := false;
  _MainWindowHandle := FindWindow(nil, PWideChar(AWinTitle));
  if _MainWindowHandle <> 0 then
  begin
    _WindowControlList := TStringlist.Create;
    try
      if TryStrToInt('$' + Trim(AControl), _ControlHandle) then
        try
          EnumChildWindows(_MainWindowHandle, @EnumChildren,
            UINT_PTR(_WindowControlList));
          for i := 0 to _WindowControlList.Count - 1 do
          begin
            if (StrToInt(_WindowControlList[i]) = _ControlHandle)
            then
            begin
              SendMessage(StrToInt(_WindowControlList[i]), WM_SETTEXT, 0,
                integer(PCHAR(ANewText)));
              Result := true;
            end;
          end;
        except
          on E: Exception do
            MessageDlg(E.Message, TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0)
        end;
    finally
      FreeAndNil(_WindowControlList);
    end;
  end;
end;

The debugger raises an exception with the message

--------------------------- Debugger Exception Notification ---------------------------

Project Default_project.exe raised exception class $C0000005 with message 'access violation at 0x00406fae: write of address 0x00408dbb'.

It breaks at:

for i := 0 to _WindowControlList.Count - 1 do

I call it like this:

ControlSetText('00070828', 'New TEdit text', 'Delphi_test_app');

I am planning an update, so, not only control handle could be passed, but also control type+identifier e.g. 'Edit1'.

EDIT:

What I am trying is to do is to implement http://www.autohotkey.com/docs/commands/ControlSetText.htm

Was it helpful?

Solution 2

The root cause of your crash is that your are using an inner function as the EnumChildWindows() callback and it is referencing a parameter from its outer function, which will not work (and why it does work when you comment out the access of that parameter). The call stack frame is not what EnumChildWindows() is expecting. You need to make the inner function be a standalone function instead.

With that said, there is another bug in your code. Even if the above worked, your code would still fail because you are storing child Control IDs in your TStringList but then using them as if they were HWND values instead. They are not!

Try something more like this:

uses
  ..., System.Generics.Collections;

{ *
  * AControl: Control handle determined by Spy++ (e.g. 0037064A)
  * ANewText: Text to assign to control
  * AWinTitle: Window Title/Caption
  * }

type
  THWndList = TList<HWND>;

function EnumChildren(AWindowHandle: HWND; AParam: LPARAM): BOOL; stdcall;
begin
  THWndList(AParam).Add(AWindowHandle);
  Result := TRUE;
end;

function TryStrToHWnd(const AStr: String; var Wnd: HWND): Boolean;
begin
  {$IFDEF WIN64}
  Result := TryStrToInt64(AStr, Int64(Wnd));
  {$ELSE}
  Result := TryStrToInt(AStr, Integer(Wnd));
  {$ENDIF}
end;

function ControlSetText(const AControl, ANewText, AWinTitle: String): Boolean;
var
  _MainWindowHandle: HWND;
  _WindowControlList: THWndList;
  i: integer;
  _ControlHandle: HWND;
  EnumInfo: TEnumInfo;
begin
  Result := False;
  _MainWindowHandle := FindWindow(nil, PChar(AWinTitle));
  if _MainWindowHandle <> 0 then
  begin
    _WindowControlList := THWndList;
    try
      if TryStrToHWnd('$' + Trim(AControl), _ControlHandle) then
      try
        EnumChildWindows(_MainWindowHandle, @EnumChildren, LPARAM(_WindowControlList));
        for i := 0 to _WindowControlList.Count - 1 do
        begin
          if (_WindowControlList[i] = _ControlHandle) then
          begin
            Result := SendMessage(_WindowControlList[i], WM_SETTEXT, 0, LPARAM(PChar(ANewText))) = 1;
            Break;
          end;
        end;
      except
        on E: Exception do
          MessageDlg(E.Message, TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0);
      end;
    finally
      FreeAndNil(_WindowControlList);
    end;
  end;
end;

Alternatively:

{ *
  * AControl: Control handle determined by Spy++ (e.g. 0037064A)
  * ANewText: Text to assign to control
  * AWinTitle: Window Title/Caption
  * }

type
  PEnumInfo = ^TEnumInfo;
  TEnumInfo = record
    Control: HWND;
    Found: Boolean;
   end;

function EnumChildren(AWindowHandle: HWND; AParam: LPARAM): BOOL; stdcall;
begin
  PEnumInfo(AParam).Found := (AWindowHandle = PEnumInfo(AParam).Control);
  Result := not PEnumInfo(AParam).Found;
end;

function TryStrToHWnd(const AStr: String; var Wnd: HWND): Boolean;
begin
  {$IFDEF WIN64}
  Result := TryStrToInt64(AStr, Int64(Wnd));
  {$ELSE}
  Result := TryStrToInt(AStr, Integer(Wnd));
  {$ENDIF}
end;

function ControlSetText(const AControl, ANewText, AWinTitle: String): Boolean;
var
  _MainWindowHandle: HWND;
  _ControlHandle: HWND;
  EnumInfo: TEnumInfo;
begin
  Result := False;
  _MainWindowHandle := FindWindow(nil, PChar(AWinTitle));
  if _MainWindowHandle <> 0 then
  begin
    if TryStrToHWnd('$' + Trim(AControl), _ControlHandle) then
    try
      EnumInfo.Control := _ControlHandle;
      EnumInfo.Found := False;
      EnumChildWindows(_MainWindowHandle, @EnumChildren, LPARAM(@EnumInfo));
      if EnumInfo.Found then
      begin
        Result := SendMessage(_ControlHandle, WM_SETTEXT, 0, LPARAM(PChar(ANewText))) = 1;
      end;
    except
      on E: Exception do
        MessageDlg(E.Message, TMsgDlgType.mtError, [TMsgDlgBtn.mbOK], 0);
    end;
  end;
end;

Or just get rid of EnumChilWindows() and let Windows validate the HWND you try to send to:

{ *
  * AControl: Control handle determined by Spy++ (e.g. 0037064A)
  * ANewText: Text to assign to control
  * }

function TryStrToHWnd(const AStr: String; var Wnd: HWND): Boolean;
begin
  {$IFDEF WIN64}
  Result := TryStrToInt64(AStr, Int64(Wnd));
  {$ELSE}
  Result := TryStrToInt(AStr, Integer(Wnd));
  {$ENDIF}
end;

function ControlSetText(const AControl, ANewText: String): Boolean;
var
  _ControlHandle: HWND;
begin
  Result := TryStrToHWnd('$' + Trim(AControl), _ControlHandle) and
    (SendMessage(_ControlHandle, WM_SETTEXT, 0, LPARAM(PChar(ANewText))) = 1);
end;

OTHER TIPS

The problem is that your callback is a local nested function. That is it is nested inside ControlSetText. It must be declared at global scope.

Any extra state information must be passed in through the lParam parameter.

I also find it odd that you store integers and pointers in strings. Store them as integers or pointers.

In fact it is more than odd. You put control ids in the list, as strings, but then use them as window handles. So once you get past the crash the code won't work. I don't want to get into debugging that in this question.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top