4

I want to ask the user to input a password. As the password is sometimes needed in a different thread than the main thread where VCL runs, I tried to send a Message to the main window and ask for the password. Then the main window asks the user.

How I ask the user:

procedure TMainForm.WMGetPassword(var Msg: TMessage);
var
  Password: String;
begin
  if QueryPassword(Password) then // function QueryPassword(out Password: String): boolean;
  begin
    Password := Password + #0; // Add #0-Terminator
    Move(Password[1], Msg.wParam, Length(Password) * sizeOf(Char)); // Copy the String in my buffer
    Msg.Result := 1;
  end
  else
  begin
    Msg.Result := 0;
  end;
end;

How I ask the main window:

var
  PasswordBuffer: PChar;
  Password: String;
begin
  PasswordBuffer := AllocMem(100 * sizeof(Char));
  PasswordResult := SendMessage(MainFormHWND, WM_GetPassword, Integer(PasswordBuffer), 0);
  Result := (PasswordResult <> -1);
  if not Result then
    Exit;

  SetString(Password, PasswordBuffer, 100);
  ShowMessage(Password);
end;

But Password and PasswordBuffer are empty afterwards. What am I doing wrong?

BenMorel
  • 34,448
  • 50
  • 182
  • 322
rollstuhlfahrer
  • 3,988
  • 9
  • 25
  • 38
  • What is the type of Msg.wParam in WMGetPassword? Do you need to cast it to a PChar? – Keith Miller Aug 14 '12 at 09:46
  • Msg.wParam is Integer, Msg.lParam too. – rollstuhlfahrer Aug 14 '12 at 09:54
  • @rollstuhlfahrer, no, Msg.wParam is `WPARAM` and Msg.lParam is `LPARAM` type and this way they should be typecasted. Forget on what you've seen in many tutorials on the Internet about message sending where these parameters were typecasted as `Integer(x)`. – TLama Aug 14 '12 at 10:02

3 Answers3

3

Since you are passing Msg.wParam in as the second parameter, it is writing your string to that location, and not the location you are trying to point to. It will be overwriting the values stored in Msg.wParam, Msg.lParam, Msg.Result + probably some other information on the stack too.

Instead of:

Move(Password[1], Msg.wParam, Length(Password) * sizeOf(Char));

You should be using:

Move(Password[1], PChar(Msg.wParam)^, Length(Password) * sizeOf(Char));

or use MoveMemory if you want to use pointers.

Dan Bartlett
  • 826
  • 7
  • 8
3

As long as the thread is in the same process (so it shares the same address space) your code should work. It is however needlessly complicated and has a memory leak (PasswordBuffer is never freed).

You can use a string variable in the thread and pass an address to its internal preallocated buffer to the main thread:

type
  TTestThread = class(TThread)
  private
    fHwnd: HWND;
  protected
    procedure Execute; override;
  public
    constructor Create(AWnd: HWND);
  end;

constructor TTestThread.Create(AWnd: HWND);
begin
  fHwnd := AWnd;
  inherited Create(False);
end;

procedure TTestThread.Execute;
const
  MAXLEN = 1024;
var
  s: string;
begin
  SetLength(s, MAXLEN);
  if SendMessage(fHwnd, WM_GETPASSWORD, MAXLEN, LPARAM(@s[1])) > 0 then begin
    s := PChar(s);
    // don't use VCL here
    Windows.MessageBox(0, PChar('password is "' + s + '"'), 'password',
      MB_ICONINFORMATION or MB_OK);
  end;
end;

In the main thread the password is put into the buffer, length-limited to the buffer size:

procedure TForm1.WMGetPassword(var AMsg: TMessage);
var
  Pwd: string;
begin
  if InputQuery('Password Entry', 'Please enter the password:', Pwd)
    and (Pwd <> '')
  then begin
    StrPLCopy(PChar(AMsg.LParam), Pwd, AMsg.WParam);
    AMsg.Result := 1;
  end else
    AMsg.Result := -1;
end;
mghie
  • 32,028
  • 6
  • 87
  • 129
2

@Dan got it right, plus @mghie spotted the leak.

Here is an alternative way which does not involve pointers:

type
  TMyMessage = class
    msg: string;
  end;

procedure TMainForm.WMGetPassword(var Msg: TMessage);
var
  SMessage: TMyMessage;
  Password: string;
begin
  if QueryPassword(Password) then 
  begin
    SMessage := TMyMessage(msg.WParam);
    SMessage.msg := Password;
    msg.Result := 1;
  end
  else
  begin
    Msg.Result := 0;
  end;
end;


var
  MyMsg: TMyMessage;
begin
  MyMsg := TMyMessage.Create('');
  try
    PasswordResult := SendMessage(FormHandleHWND,WM_GetPassword,WPARAM(MyMsg),0);
    Result := (PasswordResult <> -1);
    if Result
      then Password := MyMsg.msg;
  finally
    MyMsg.Free;
  end;
end;
LU RD
  • 34,438
  • 5
  • 88
  • 296
  • Isn't using a class for just one String-Value breaking a butterfly on a wheel? – rollstuhlfahrer Aug 14 '12 at 10:57
  • Not at all. Since you are allocating memory anyway, why not use a class. – LU RD Aug 14 '12 at 11:00
  • 1
    Uhm, because it looks ponderously :-? – TLama Aug 14 '12 at 11:02
  • @TLama, considering the many ways to go wrong (as the OP did) with pointers and memory allocation, this is a lifesaver, not an anchor. – LU RD Aug 14 '12 at 11:05
  • @rollstuhlfahrer, all my `SendMessage/PostMessage` calls are using this technique when passing information (besides the most simple integer types). It's good because of the strict typing and the possibility to expand how much information to pass. – LU RD Aug 14 '12 at 11:14