如何将TPopupMenu排列起来,以便准确地将其定位在按钮上方?

9
我想让一个按钮上方弹出一个弹出菜单:
Delphi 将 Win32 菜单系统包装起来,以一种方式似乎排除了每种模式或标志,这些模式或标志在底层 Win32 API 中提供,但不在 VCL 作者的大脑中。其中一个例子似乎是 TPM_BOTTOMALIGN,它可以传递到 TrackPopupMenu,但 Delphi 包装器似乎不仅在股票 VCL 中无法实现这一点,而且通过不慎使用私有和受保护的方法,在运行时或重写方面都是不可能的(至少对我来说似乎是不可能的)。VCL 组件 TPopupMenu 的设计也不太好,因为它应该有一个名为 PrepareForTrackPopupMenu 的虚拟方法,该方法除了调用 TrackPopupMenu 或 TrackPopupMenuEx 外其他都做了,然后允许某人覆盖实际调用该 Win32 方法的方法。但现在为时已晚。也许 Delphi XE5 会正确地完成 Win32 API 的基本覆盖率。
我尝试过的方法:
方法 A:使用 METRICS 或字体:
精确确定弹出菜单的高度,以便在调用 popupmenu.Popup(x,y) 之前减去 Y 值。结果:必须处理所有变量的 Windows 主题,并做出我似乎无法确定的假设。在现实世界中似乎不太可能产生良好的结果。这是一个基本字体度量方法的示例:
   height := aPopupMenu.items.count * (abs(font.height) + 6) + 34;

您可以考虑隐藏的项目,对于一个单一版本的Windows,并且只有一个主题模式设置生效,您可能会接近这样的效果,但不是完全正确的。
方法B:让Windows来完成:
尝试传递`TPM_BOTTOMALIGN`以最终到达Win32 API调用`TrackPopupMenu`。
到目前为止,我认为如果修改VCL menus.pas,我就可以做到。在这个项目中,我使用的是Delphi 2007。但我对这个想法并不是很满意。
这是我正在尝试的代码类型:
procedure TMyForm.ButtonClick(Sender: TObject);
var
  pt:TPoint;
  popupMenuHeightEstimate:Integer;
begin
   // alas, how to do this accurately, what with themes, and the OnMeasureItem event
   // changing things at runtime.
      popupMenuHeightEstimate := PopupMenuHeight(BookingsPopupMenu); 

      pt.X := 0;
      pt.Y := -1*popupMenuHeightEstimate;
      pt := aButton.ClientToScreen(pt);  // do the math for me.
      aPopupMenu.popup( pt.X, pt.Y );

end;

另外,我想要这样做:

  pt.X := 0;
  pt.Y := 0;
  pt := aButton.ClientToScreen(pt);  // do the math for me.
  aPopupMenu.popupEx( pt.X, pt.Y, TPM_BOTTOMALIGN);

当然,在VCL中没有popupEx。也没有任何方法可以传递比那些VCL人员在1995年版本1.0中添加的更多标志到TrackPopupMenu中。注意:我认为在显示菜单之前估算高度的问题是不可能的,因此我们实际上应该通过TrackPopupMenu解决问题,而不是估算高度。更新:直接调用TrackPopupMenu无法正常工作,因为需要调用VCL方法TPopupMenu.Popup(x,y)中的其他步骤,以便我的应用程序可以绘制其菜单并使其看起来正确,但是不可能在没有恶意策略的情况下调用它们,因为它们是私有方法。修改VCL是一个非常困难的任务,我也不希望考虑这个方案。

似乎那并没有执行我调用TPopupMenu.Popup时发生的一切,而且也没有 VCL 准备调用 TrackPopupMenu 但不调用 TrackPopupMenu。 - Warren P
我认为你可以尝试获取一些具有菜单或带有按钮组件的开源按钮,并学习它们的源代码。比如JVCL的JvArrowButton、ToolBar2000或torry.net上的其他按钮或工具栏-只要找到合适的组件并从中学习。 - Arioch 'The
抱歉,由于代理问题,我无法在此处查看图像,因此我没有发布答案。但是,您说您想准确地将 TPopupMenu 定位在按钮上方,并使用 xy 坐标以及适当的标志和菜单句柄来跟踪弹出菜单,这正是 TrackPopupMenu 为您提供的功能。您的要求缺少哪些功能? - Ken White
尝试使用TPopupMenu实现这个功能,你会遇到很多问题。其中包括大部分子菜单项标题未填充且子菜单为空的情况。调用TrackPopupMenu假定VCL维护了内部项目的状态,以便在准备调用TrackPopupMenu时使用。但实际上VCL并没有这样做。 - Warren P
2
@WarrenP:你是否知道,使用类助手不仅可以访问受保护的成员,还可以访问私有成员,__前提是__你在它们前面加上Self。这使得绕过VCL设计决策变得更加容易。 - Marjan Venema
显示剩余4条评论
2个回答

6

有点巧妙,但它可能会解决它。

声明一个拦截器类用于重写TPopupMenu的Popup方法:

type
  TPopupMenu = class(Vcl.Menus.TPopupMenu)
  public
    procedure Popup(X, Y: Integer); override;
  end;

procedure TPopupMenu.Popup(X, Y: Integer);
const
  Flags: array[Boolean, TPopupAlignment] of Word =
    ((TPM_LEFTALIGN, TPM_RIGHTALIGN, TPM_CENTERALIGN),
     (TPM_RIGHTALIGN, TPM_LEFTALIGN, TPM_CENTERALIGN));
  Buttons: array[TTrackButton] of Word = (TPM_RIGHTBUTTON, TPM_LEFTBUTTON);
var
  AFlags: Integer;
begin
  PostMessage(PopupList.Window, WM_CANCELMODE, 0, 0);
  inherited;
  AFlags := Flags[UseRightToLeftAlignment, Alignment] or
    Buttons[TrackButton] or
    TPM_BOTTOMALIGN or
    (Byte(MenuAnimation) shl 10);
  TrackPopupMenu(Items.Handle, AFlags, X, Y, 0 { reserved }, PopupList.Window, nil);
end;

技巧在于向菜单窗口发布一个取消消息,以取消继承的TrackPopupMenu调用。

2
看他的问题,我认为他试图让PopupMenu出现在按钮原点上方,而不是鼠标光标处。在inherited之前添加X:= MyForm.Button.ClientOrigin.X; Y:= MyForm.Button.ClientOrigin.Y - 2;应该可以解决问题。虽然答案很好,但还是要加1分! - Peter
1
所以您会看到一个弹出菜单窗口打开然后立即关闭,接着又出现了另一个对吧?虽然这样做非常安全,但远程桌面用户可能会在屏幕上看到整个过程,从而产生一些小故障。也许一个小技巧就是让它在屏幕外弹出?inherited Popup(-1000,-1000); - Warren P
如果这个东西要在多个地方使用,最好制作一个具有体面名称的自定义组件。这也将允许适当的事件来调整弹出窗口的坐标。继承调用的视线外坐标是个好主意。 - Uwe Raabe
我实际上不会覆盖弹出窗口,而是将使用一个中间类。方法名“PopupWithBottomAlignment”清晰明确。我将在一个可重用的单元中实现代码,然后只需在要使用它的单元中插入即可。 - Warren P

2

我无法复制您使用的TrackPopupMenu问题。在我的D2007上进行简单测试,菜单项的标题、图片和子菜单似乎都能正常显示和工作。

不管怎样,下面的示例会在弹出菜单之前安装一个CBT钩子。钩子检索与要关联菜单的窗口,以便能够对其进行子类化。

如果您不关心在压力情况下可能会闪烁的弹出菜单,可以使用PopupList类来处理WM_ENTERIDLE以进入菜单的窗口。

type
  TForm1 = class(TForm)
    Button1: TButton;
    PopupMenu1: TPopupMenu;
    ...
    procedure PopupMenu1Popup(Sender: TObject);
  private
    ...
  end;

  ...

implementation

{$R *.dfm}

var
  SaveWndProc: Pointer;
  CBTHook: HHOOK;
  ControlWnd: HWND;
  PopupToMove: HMENU;

function MenuWndProc(Window: HWND; Message, WParam: Longint;
    LParam: Longint): Longint; stdcall;
const
  MN_GETHMENU   = $01E1;  // not defined in D2007
var
  R: TRect;
begin
  Result := CallWindowProc(SaveWndProc, Window, Message, WParam, LParam);

  if (Message = WM_WINDOWPOSCHANGING) and
      // sanity check - does the window hold our popup?
      (HMENU(SendMessage(Window, MN_GETHMENU, 0, 0)) = PopupToMove) then begin

    if PWindowPos(LParam).cy > 0 then begin 
      GetWindowRect(ControlWnd, R);
      PWindowPos(LParam).x := R.Left;
      PWindowPos(LParam).y := R.Top - PWindowPos(LParam).cy;
      PWindowPos(LParam).flags := PWindowPos(LParam).flags and not SWP_NOMOVE;
    end else
      PWindowPos(LParam).flags := PWindowPos(LParam).flags or SWP_NOMOVE;
  end;
end;

function CBTProc(nCode: Integer; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall;
const
  MENUWNDCLASS = '#32768';
var
  ClassName: array[0..6] of Char;
begin
  Result:= CallNextHookEx(CBTHook, nCode, WParam, LParam);

  // first window to be created that of a menu class should be our window since
  // we already *popped* our menu
  if (nCode = HCBT_CREATEWND) and
      Bool(GetClassName(WParam, @ClassName, SizeOf(ClassName))) and
      (ClassName = MENUWNDCLASS) then begin
    SaveWndProc := Pointer(GetWindowLong(WParam, GWL_WNDPROC));
    SetWindowLong(WParam, GWL_WNDPROC, Longint(@MenuWndProc));
    // don't need the hook anymore...
    UnhookWindowsHookEx(CBTHook);     
  end;
end;


procedure TForm1.PopupMenu1Popup(Sender: TObject);
begin
  ControlWnd := Button1.Handle;         // we'll aling the popup to this control
  PopupToMove := TPopupMenu(Sender).Handle;  // for sanity check above
  CBTHook := SetWindowsHookEx(WH_CBT, CBTProc, 0, GetCurrentThreadId); // hook..
end;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接