Я пытаюсь (в D7) создать поток с насосом сообщений, который в конечном итоге я хочу перенести в DLL.

Вот соответствующие / нетривиальные части моего кода:

const
  WM_Action1 = WM_User + 1;
  scThreadClassName = 'MyThreadClass';

type
  TThreadCreatorForm = class;

  TWndThread = class(TThread)
  private
    FTitle: String;
    FWnd: HWND;
    FWndClass: WNDCLASS;
    FCreator : TForm;
    procedure HandleAction1;
  protected
    procedure Execute; override;
  public
    constructor Create(ACreator: TForm; const Title: String); 
  end;

  TThreadCreatorForm = class(TForm)
    btnCreate: TButton;
    btnAction1: TButton;
    Label1: TLabel;
    btnQuit: TButton;
    btnSend: TButton;
    edSend: TEdit;
    procedure FormShow(Sender: TObject);
    procedure btnCreateClick(Sender: TObject);
    procedure btnAction1Click(Sender: TObject);
    procedure btnQuitClick(Sender: TObject);
    procedure btnSendClick(Sender: TObject);
    procedure WMAction1(var Msg : TMsg); message WM_Action1;
    procedure FormCreate(Sender: TObject);
  public
    { Public declarations }
    WndThread : TWndThread;
    ThreadID : Integer;
    ThreadHWnd : HWnd;
  end;

var
  ThreadCreatorForm: TThreadCreatorForm;

implementation

{$R *.DFM}

procedure SendStringViaWMCopyData(HSource, HDest : THandle; const AString : String);
var
  Cds : TCopyDataStruct;
  Res : Integer;
begin
  FillChar(Cds, SizeOf(Cds), 0);
  GetMem(Cds.lpData, Length(Astring) + 1);
  try
    StrCopy(Cds.lpData, PChar(AString));
    Res := SendMessage(HDest, WM_COPYDATA, HSource, Cardinal(@Cds));
    ShowMessage(IntToStr(Res));
  finally
    FreeMem(Cds.lpData);
  end;
end;

procedure TThreadCreatorForm.FormShow(Sender: TObject);
begin
  ThreadID := GetWindowThreadProcessId(Self.Handle, Nil);
  Assert(ThreadID = MainThreadID);
end;

procedure TWndThread.HandleAction1;
begin
  //
end;

constructor TWndThread.Create(ACreator: TForm; const Title:String);
begin
  inherited Create(True);
  FTitle := Title;
  FCreator := ACreator;
  FillChar(FWndClass, SizeOf(FWndClass), 0);
  FWndClass.lpfnWndProc := @DefWindowProc;
  FWndClass.hInstance := HInstance;
  FWndClass.lpszClassName := scThreadClassName;
end;

procedure TWndThread.Execute;
var
  Msg: TMsg;
  Done : Boolean;
  S : String;
begin
  if Windows.RegisterClass(FWndClass) = 0 then Exit;
  FWnd := CreateWindow(FWndClass.lpszClassName, PChar(FTitle), WS_DLGFRAME, 0, 0, 0, 0, 0, 0, HInstance, nil);
  if FWnd = 0 then Exit;

  Done := False;
  while GetMessage(Msg, 0, 0, 0) and not done do begin
    case Msg.message of
      WM_Action1 : begin
        HandleAction1;
      end;
      WM_COPYDATA : begin
        Assert(True);
      end;
      WM_Quit : Done := True;
      else begin
        TranslateMessage(msg);
        DispatchMessage(msg)
      end;
    end; { case }
  end;
  if FWnd <> 0 then
    DestroyWindow(FWnd);
  Windows.UnregisterClass(FWndClass.lpszClassName, FWndClass.hInstance);
end;

Создав поток, я нахожу его дескриптор окна с помощью FindWindow, и он отлично работает.

Если я PostMessage , это мое определенное пользователем сообщение WM_Action1, оно получено GetMessage () и перехвачено оператором case в Execute потока, и это отлично работает.

Если я отправлю себе (то есть свою форму хоста) сообщение WM_CopyData с помощью процедуры SendStringViaWMCopyData (), которая работает нормально.

Однако: если я отправлю своему потоку сообщение WM_CopyData, GetMessage и оператор case в Execute никогда не увидят его, а SendMessage в SendStringViaWMCopyData вернет 0.

Итак, мой вопрос: почему сообщение WM_CopyData не получает GetMessage в .Execute? У меня неприятное ощущение, что я чего-то упускаю ...

1
MartynA 21 Авг 2014 в 21:49

2 ответа

Лучший ответ

WM_COPYDATA не является опубликованным сообщением, это отправленное сообщение, поэтому оно не проходит через очередь сообщений, и поэтому цикл сообщений никогда его не увидит. Вам необходимо назначить оконную процедуру вашему классу окна и вместо этого обработать WM_COPYDATA в этой процедуре. Не используйте DefWindowProc() в качестве оконной процедуры.

Кроме того, при отправке WM_COPYDATA поле lpData выражается в байтах , а не в символах , поэтому вам необходимо принять это во внимание. И вы неправильно заполняете COPYDATASTRUCT. Вам необходимо указать значения для полей dwData и cbData. И вам не нужно выделять память для поля lpData, вместо этого вы можете указать его на существующую память вашего String.

Попробуй это:

const
  WM_Action1 = WM_User + 1;
  scThreadClassName = 'MyThreadClass';

type
  TThreadCreatorForm = class;

  TWndThread = class(TThread)
  private
    FTitle: String;
    FWnd: HWND;
    FWndClass: WNDCLASS;
    FCreator : TForm;
    procedure WndProc(var Message: TMessage);
    procedure HandleAction1;
    procedure HandleCopyData(const Cds: TCopyDataStruct);
  protected
    procedure Execute; override;
    procedure DoTerminate; override;
  public
    constructor Create(ACreator: TForm; const Title: String); 
  end;

  TThreadCreatorForm = class(TForm)
    btnCreate: TButton;
    btnAction1: TButton;
    Label1: TLabel;
    btnQuit: TButton;
    btnSend: TButton;
    edSend: TEdit;
    procedure FormShow(Sender: TObject);
    procedure btnCreateClick(Sender: TObject);
    procedure btnAction1Click(Sender: TObject);
    procedure btnQuitClick(Sender: TObject);
    procedure btnSendClick(Sender: TObject);
    procedure WMAction1(var Msg : TMsg); message WM_Action1;
    procedure FormCreate(Sender: TObject);
  public
    { Public declarations }
    WndThread : TWndThread;
    ThreadID : Integer;
    ThreadHWnd : HWnd;
  end;

var
  ThreadCreatorForm: TThreadCreatorForm;

implementation

{$R *.DFM}

var
  MY_CDS_VALUE: UINT = 0;

procedure SendStringViaWMCopyData(HSource, HDest : HWND; const AString : String);
var
  Cds : TCopyDataStruct;
  Res : Integer;
begin
  ZeroMemory(@Cds, SizeOf(Cds));
  Cds.dwData := MY_CDS_VALUE;
  Cds.cbData := Length(AString) * SizeOf(Char);
  Cds.lpData := PChar(AString);
  Res := SendMessage(HDest, WM_COPYDATA, HSource, LPARAM(@Cds));
  ShowMessage(IntToStr(Res));
end;

procedure TThreadCreatorForm.FormShow(Sender: TObject);
begin
  ThreadID := GetWindowThreadProcessId(Self.Handle, Nil);
  Assert(ThreadID = MainThreadID);
end;

function TWndThreadWindowProc(hWnd: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
  pSelf: TWndThread;
  Message: TMessage;
begin
  pSelf := TWndThread(GetWindowLongPtr(hWnd, GWL_USERDATA));
  if pSelf <> nil then
  begin
    Message.Msg := uMsg;
    Message.WParam := wParam;
    Message.LParam := lParam;
    Message.Result := 0;
    pSelf.WndProc(Message);
    Result := Message.Result;
  end else
    Result := DefWindowProc(hWnd, uMsg, wParam, lParam);
end;

constructor TWndThread.Create(ACreator: TForm; const Title:String);
begin
  inherited Create(True);
  FTitle := Title;
  FCreator := ACreator;
  FillChar(FWndClass, SizeOf(FWndClass), 0);
  FWndClass.lpfnWndProc := @TWndThreadWindowProc;
  FWndClass.hInstance := HInstance;
  FWndClass.lpszClassName := scThreadClassName;
end;

procedure TWndThread.Execute;
var
  Msg: TMsg;
begin
  if Windows.RegisterClass(FWndClass) = 0 then Exit;
  FWnd := CreateWindow(FWndClass.lpszClassName, PChar(FTitle), WS_DLGFRAME, 0, 0, 0, 0, 0, 0, HInstance, nil);
  if FWnd = 0 then Exit;
  SetWindowLongPtr(FWnd, GWL_USERDATA, ULONG_PTR(Self));

  while GetMessage(Msg, 0, 0, 0) and (not Terminated) do
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;
end;

procedure TWndThread.DoTerminate;
begin
  if FWnd <> 0 then
    DestroyWindow(FWnd);
  Windows.UnregisterClass(FWndClass.lpszClassName, FWndClass.hInstance);
  inherited;
end;

procedure TWndThread.WndProc(var Message: TMessage);
begin
  case Message.Msg of
    WM_Action1 : begin
      HandleAction1;
      Exit;
    end;
    WM_COPYDATA : begin
      if PCopyDataStruct(lParam).dwData = MY_CDS_VALUE then
      begin
        HandleCopyData(PCopyDataStruct(lParam)^);
        Exit;
      end;
    end; 
  end;

  Message.Result := DefWindowProc(FWnd, Message.Msg, Message.WParam, Message.LParam);
end;

procedure TWndThread.HandleAction1;
begin
  //
end;

procedure TWndThread.HandleCopyData(const Cds: TCopyDataStruct);
var
  S: String;
begin
  if Cds.cbData > 0 then
  begin
    SetLength(S, Cds.cbData div SizeOf(Char));
    CopyMemory(Pointer(S), Cds.lpData, Length(S) * SizeOf(Char));
  end;
  // use S as needed...
end;

initialization
  MY_CDS_VALUE := RegisterWindowMessage('MY_CDS_VALUE');

end.
8
Remy Lebeau 21 Авг 2014 в 23:05
Спасибо. Я понял, что мне нужно сделать SendMessage, но не знал, что мне нужна моя собственная процедура Windows (и на данный момент, что она должна содержать, но я думаю, что могу посмотреть исходный код Delphi). Что меня обмануло, так это то, что я отправляю сообщение WM_COPYDATA точно так же, как мое сообщение WM_Action1, и GetMessage видит это последнее, но не первое.
 – 
MartynA
21 Авг 2014 в 22:20
Вы не показали, как вы отправляете WM_Action1 в окно темы.
 – 
Remy Lebeau
21 Авг 2014 в 22:26
Ах, извините, это было потому, что он, казалось, работал правильно, поэтому я подумал, что это бесполезно. Но теперь, когда я смотрю более внимательно, я не так уверен.
 – 
MartynA
21 Авг 2014 в 22:32
Кроме того, при отправке WM_COPYDATA поле lpData выражается в байтах, а не в символах, поэтому вам необходимо принять это во внимание. Вы имеете в виду cbData.
 – 
David Heffernan
21 Авг 2014 в 23:01
@DavidHeffernan: я имел в виду lpData, но да, это также относится и к cbData. Его код может быть сегодня в D7 и, таким образом, использует AnsiString, что является 8-битным. Но если он когда-нибудь перейдет на D2009+, где String — 16 бит, его исходный код, который выделял и заполнил lpData, потерпит неудачу.
 – 
Remy Lebeau
21 Авг 2014 в 23:06

Сообщение с копией данных отправляется синхронно. Это означает, что он не будет возвращен GetMessage. Поэтому вам необходимо предоставить оконную процедуру для обработки сообщения, потому что отправленные сообщения отправляются непосредственно оконной процедуре их окон, будучи синхронной, а не асинхронной.

Помимо этого, другая проблема заключается в том, что вы не указываете длину данных в структуре копируемых данных, cbData. Это необходимо при отправке сообщения через поток, чтобы система могла упорядочить ваши данные.

Вы должны установить dwData, чтобы получатель мог проверить, обрабатывает ли он предполагаемое сообщение.

Здесь вам вообще не нужно использовать GetMem, вы можете использовать строковый буфер напрямую. Дескриптор окна - это HWND, а не THandle. Окно только для сообщений здесь было бы наиболее подходящим.

3
David Heffernan 21 Авг 2014 в 22:58
Я понял, что мне нужно сделать SendMessage (и сделать это в подпрограмме SendStringViaWMCopyData выше), хотя бы потому, что я предположил, что что-то должно висеть в памяти, используемой Cds, до тех пор, пока оно не будет обработано получателем. Думаю, мне придется поискать, как написать оконную процедуру...
 – 
MartynA
21 Авг 2014 в 22:16
Это достаточно легко. Установка cbData также имеет решающее значение.
 – 
David Heffernan
21 Авг 2014 в 22:19
То же самое относится и к настройке dwData, чтобы различать разные WM_COPYDATA сообщения, чтобы вы обрабатывали только свое собственное сообщение и случайно не обрабатывали чужое сообщение. VCL, например, использует WM_COPYDATA внутри.
 – 
Remy Lebeau
21 Авг 2014 в 22:43
Не так критично с точки зрения отправки и получения сообщения. И это довольно слабая защита от случайной обработки сообщений. Но да, лучше поставить.
 – 
David Heffernan
21 Авг 2014 в 22:57
@DavidHeffernan: Слабо это или нет, но dwData — единственная защита, предлагаемая WM_COPYDATA, и большинство (но не все) реализаций используют RegisterWindowMessage(), чтобы гарантировать, что dwData — уникальное значение.
 – 
Remy Lebeau
21 Авг 2014 в 23:08