2012/03/07

Delphi 中 SendMessage 使用技巧

作者:吳祐賓



2024/02/19 更新

VCL 最厲害的地方是將 Windows 完美的封裝成易用的框架,又保留 Windows Message 事件機制的處理方式。在 2012 年我還在使用 C++ Builder 就曾為了學習 SendMessage 技巧而吃了很多苦頭。2024 年再回頭看這份【Delphi中SendMessage使用技巧】文件,發現原文來源網址已失效。在網路上搜尋到的資料都沒有標記來源,盜文嚴重程度可見一般。這份文件到現在仍然可以使用,作為 Windows API 的入門仍然很有價值,我整理這份文件的重點如下。

使用 SendMessage 向元件發送 Message

VCL 容器元件如 ListBox, ComboBox 等,其寬度屬於靜度設定。有時畫面在排列時需要將容器元件寬度調得很窄,但呼叫下拉清單時則寬度需要隨 item 最大長度配合調整。這種情況就很適合 SendMessage 處理。

先取得下拉清單項目的最大長度,再使用 SendMessage 對下拉清單傳遞 CB_SETDROPPEDWIDTH 及寬度值,就可以滿足預期顯示的效果。如下圖顯示。

左邊是原始 VCL 元件效果,右邊是搭配 SendMessage 傳遞寬度值的效果

部份程式碼如下所示


begin
  i := 0; // 清單計數器
  MaxWidth := 0;
  //讀 LastName 清單到 ComboBox.Items
  ComboBox1.Clear;
  Table1.First;
  while not Table1.Eof do begin
    ComboBox1.Items.add(Table1LastName.AsString);
    LWidth := ComboBox1.Font.Size * Length(ComboBox1.Items[i]);
    if LWidth > MaxWidth then
      MaxWidth := LWidth; //找出最大值
    Table1.Next;
    i := i + 1;
  end;
  ComboBox1.Text := ComboBox1.Items[0];
  //傳遞 Message 以改變顯示區域的寬度
  SendMessage(ComboBox1.Handle, CB_SETDROPPEDWIDTH, MaxWidth, 0);
end;


把 Button 變成 RadioButton

利用 SendMessage API 還可以實現一些有趣效果,例如在 Button 的 Click 事件添加下列指令,就可以在按下按鈕後看到按鈕 UI 的變化。


SendMessage(TButton(Sender).Handle, BM_SETSTYLE, BS_RADIOBUTTON, 1);





【同場加映】自定接收 Message 方法

對元件的操作大多是不會經過 TForm 傳遞的,而是直接對該元件直接觸發,Windows 這樣的設計目的在於程式操作的體驗會比較好。以 ListBox 元件為例,它沒有封裝清單滾動事件,但內部仍然有滾動事件的處理,此時就可以干涉元件內部的滾動事件處理。步驟如下:

1. 繼承 TListBox 元件為 TMyListBox,並重載 WndProc 方法



    TMyListBox=class(TListBox)
    private
      procedure WndProc(var Msg: TMessage); override; //重載 WndProc,處理所有傳遞到元件的 Message
    end;


TMessage 為 record 型別,包含 Message Code 和 Param



2. WndProc 事件加入對滾動事件的處理



procedure TMyListBox.WndProc(var Msg: TMessage);
begin
  if (Msg.Msg = WM_VSCROLL) and (Msg.WParamLo = SB_ENDSCROLL) then
  begin
    //獲得滑鼠位置對應的列
    ItemIndex := ItemAtPos(LPoint, True);
    Form1.Edit1.Text := IntToStr(ItemIndex);
  end;

  inherited;
end;


程式接收到 WM_VSCROLL Message,且 WParamLo 參數為 SB_ENDSCROLL 時,表示 TMyListBox 已停止滾動,接著就可以用 ItemAtPos 方法確定滑鼠位置所對應的 ItemIndex。ItemAtPos 方法的 Point 參數是一個 TPoint 類別的全域變數,用於儲存滑鼠的位置。



3. 滑鼠移動時,將當前位置儲存在 TPoint 裡



procedure TForm1.ListBoxMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin
  LPoint.X := X;
  LPoint.Y := Y;
end;


4. 建立及初始化 TMyListBox



var
  List: TMyListBox;
  i: Integer;
begin
    LPoint.X := 0;
    LPoint.Y := 0;
    //創建自定義列表框
    List := TMyListBox.Create(Form1);
    List.Parent := Form1;
    List.Left := 250;
    List.Top := 200;
    List.Width := 150;
    List.Height := 200;
    for i := 0 to 300 do
    begin
      List.Items.Add(inttostr(i)); //初始化
    end;
    //指定處理MouseMove事件的方法
    List.OnMouseMove := ListBoxMouseMove;
end;






See also

  • 攝影師:Lukas: https://www.pexels.com/zh-tw/photo/684318/

  • Delphi中SendMessage使用技巧


东南大学 梁云
应用与开发(http://www.etc.edu.cn/application-development14/index.htm)
Windows系统是由消息机制驱动的,每个线程如果建立了一个窗口,则由系统分配一个消息队列用于窗口消息的处理。另外,消息也可以不经过消息队列而利用SendMessage函数直接发送给窗口,窗口过程将处理这个消息,但只有当消息被处理之后,SendMessage才能返回到调用程序。下面结合两个Delphi程序,讨论如何利用SendMessage向控件发送消息和控件对这种消息的响应。
SendMessage向控件发送消息
在编程中,有时需要控件以特殊的风格显示,而这种要求又无法通过设置控件属性实现。例如,读取客户列表并显示在下拉框供用户选择,如果下拉框宽度太窄,则不能全部显示;如果将宽度定得太宽,界面又有不紧凑之感。因此希望能在运行期动态地确定下拉框显示区域的宽度,这种要求如果不用SendMessage函数就很难实现。
解决办法是,在读数据库时计算字符串的显示宽度,用显示宽度的最大值确定下拉框显示区域的宽度。再用SendMessage函数向下拉框发送CB_SETDROPPEDWIDTH消息和宽度值,下拉框根据消息中传来的信息,就可以进行正确显示。
程序运行结果如图1所示:
--連結已失效--
 
部分源程序代码如下:

  i:=0; //計數
  MaxWidth:=0;
  Query1.SQL.Clear;
  Query1.SQL.Add('select Company from Customer');
  Query1.Open;
  //讀客戶列表到下拉框
  while not Query1.Eof do begin
     ComboBox1.Items.add(Query1.FieldByName('Company').AsString);
    Width := ComboBox1.Font.Size * Length(ComboBox1.Items[i]);
   if Width>MaxWidth then
     MaxWidth:=Width; //找出最大值
   Query1.Next;
   i:=i+1;
  end;
  Query1.Close;
  ComboBox1.Text:=ComboBox1.Items[0];
  //發送消息以確定顯示區域的寬度
  SendMessage(ComboBox1.Handle, CB_SETDROPPEDWIDTH, MaxWidth, 0);
利用SendMessage函数还可以实现一些有趣的效果,例如在按钮的Click事件中加入如下语句:
  SendMessage(Button.Handle, BM_SETSTYLE, BS_RADIOBUTTON, 1);

运行后点击按钮,就可以把按钮变成一个收音机按钮。
控件接收SendMessage消息
  上面讨论了用SendMessage向控件发送消息的过程。但凡事有利就有弊,用SendMessage发送的消息在处理上存在着一定困难。因为该消息不经过消息队列,所以无法用OnMessage方式来指定对消息的响应,甚至用HookMainWindow也不行,因为消息直接发送到控件,绕过了主窗体。要对这种类型的消息作出响应,需要重载控件的WndProc方法。
  例如,对于一个列表框,滚动条的滚动消息就是用SendMessage方式发送的,因此该消息不在TlistBox的事件列表中。下面是处理控件响应该滚动消息的具体步骤。
1.首先从TlistBox继承一个TmyListBox类,并重载WndProc方法。在程序中加入下列定义:
  type
    TMyListBox=class(TListBox)
    private
      procedure WndProc(var Msg: TMessage); override;
    //重載WndProc,處理發送到控件的消息
    public
    end;
  其中WndProc方法指定控件对消息的响应,输入参数是TMessage类型,该数据类型是一个记录,包含了消息代码和消息的参数,消息参数可以用Longint或Word方式获得。
  2.对滚动事件做出响应,在WndProc方法中加入如下处理代码:
  if (Msg.Msg=WM_VSCROLL) and (Msg.WParamLo=SB_ENDSCROLL) then
  begin
    //獲得鼠標位置對應的列
    ItemIndex:=ItemAtPos(Point,true);
    Form1.Edit1.Text:=inttostr(ItemIndex);    
  end;
  inherited;
  当程序接收到WM_VSCROLL消息,且WParamLo参数为SB_ENDSCROLL时,表示竖直滚动条停止滚动,就可以用ItemAtPos方法确定与鼠标位置对应的ItemIndex。ItemAtPos方法的Point参数是一个TPoint类型的变量,用来保存鼠标的位置。
  3.定义方法ListBoxMouseMove,在鼠标移动时,将当前位置保存在Point中:
  procedure TForm1.ListBoxMouseMove(Sender: TObject; Shift: TShiftState; X,Y: Integer);
  begin
    Point.X:=X;
    Point.Y:=Y;
  end;
  4.在运行期创建和初始化列表框,并指定列表框的MouseMove事件对应上一步定义的ListBoxMouseMove方法。在主窗体的Create事件中输入下面的代码:
  begin
    Point.X:=0;
    Point.Y:=0;
    //創建自定義列表框
    List:=TMyListBox.Create(Form1);
    List.Parent:=Form1;
    List.Left:=5;
    List.Top:=30;
    List.Width:=150;
    List.Height:=200;
    for i:=0 to 300 do
    begin
      List.Items.Add(inttostr(i)); //初始化
    end;
    //指定處理MouseMove事件的方法
    List.OnMouseMove := ListBoxMouseMove;
  end;
(计算机世界报 26 C13

沒有留言:

張貼留言