-1
{ *
  * 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

Edijs Kolesnikovičs
  • 1,627
  • 3
  • 18
  • 34
  • related/dupliacate: http://stackoverflow.com/questions/4096081/how-to-use-this-customsort-function-to-sort-listview – Sertac Akyuz Apr 17 '14 at 20:47
  • @sertac I thought you'd have linked to your question where we argued back and forth on this topic ;-) http://stackoverflow.com/questions/10162749/why-cannot-take-address-to-a-nested-local-function-in-64-bit-delphi – David Heffernan Apr 17 '14 at 20:59
  • @David - It was the first thing I recalled. :) But error in this question does not depend on bitness. – Sertac Akyuz Apr 17 '14 at 21:06
  • 1
    **Whenever** you use the `@` operator on a function, alarms should go off in your head. If you need to use that to make your code compile, then there's almost certainly an error somewhere. If you can't find the error yourself, ask for help. – Rob Kennedy Apr 17 '14 at 22:53
  • @rob the declaration of EnumChildWindows in the RTL compells the caller to use @. I wish emba would fix that but I cannot see it happening. – David Heffernan Apr 18 '14 at 04:49
  • 1
    @edijs I don't understand your update. The question has been answered surely. – David Heffernan Apr 18 '14 at 06:11
  • To the question edit. So then [`start here`](https://github.com/AutoHotkey/AutoHotkey/blob/v1.0.48.05/Source/script2.cpp#L2429). – TLama Apr 18 '14 at 06:20

2 Answers2

4

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • He is not storing pointers, he is storing control ID numbers instead. But you are right, a `TStringList` is not the best choice. a `TList` would make more sense. – Remy Lebeau Apr 17 '14 at 18:35
  • @remy actually he is. Some of the strings are window handles and they are pointers. For instance the call to SendMessage. – David Heffernan Apr 17 '14 at 20:09
  • Look again. All of the strings he is storing in the `TStringList` are control IDs from `GetDlgCtrlID()`, but he's using those values with `SendMessage()` as if they were `HWND`s, which they are not. – Remy Lebeau Apr 17 '14 at 21:37
1

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;
TLama
  • 75,147
  • 17
  • 214
  • 392
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thanks. With your help (and others) I almost succeeded writing what I wanted (one function I could pass handle or classname+id). Right now I can pass both, but AutoHotKey starts numbering each classname from 1 (my code goes from 1 to N). Anyway, thanks. – Edijs Kolesnikovičs Apr 19 '14 at 04:19