9

I want a popup menu above a button:

enter image description here

Delphi wraps the Win32 menu system in a way that seems to preclude every mode or flag that the underlying Win32 API provides that was not in the VCL author's brain on that day. One such example appears to be the TPM_BOTTOMALIGN which can be passed into TrackPopupMenu but, the Delphi wrapper appears to render this not only impossible in the stock VCL, but by injudicious use of private and protected methods, is impossible (at least seems to me to be impossible) to do either accurately at runtime, or by overrides. The VCL component TPopupMenu is not very well designed either, as it should have had a virtual method called PrepareForTrackPopupMenu that did everything other than the call to TrackPopupMenu or TrackPopupMenuEx, and then allow someone to override a method that actually invokes that Win32 method. But that's too late now. Maybe Delphi XE5 will have this basic coverage of the Win32 API done right.

Approaches I have tried:

Approach A: Use METRICS or Fonts:

Accurately determine height of a popup menu so I can subtract the Y value before calling popupmenu.Popup(x,y). Results: Would have to handle all variants of Windows theming, and make assumptions that I seem unable to be sure about. Seems unlikely to result in good results in the real world. Here's an example of a basic font metrics approach:

   height := aPopupMenu.items.count * (abs(font.height) + 6) + 34;

You can take into account hidden items, and for a single version of windows with a single theme mode setting in effect, you might get close like that, but not exactly right.

Approach B: Let Windows Do It:

Try to pass in TPM_BOTTOMALIGN to eventually reach Win32 API call TrackPopupMenu.

So far, i think I can do it, if I modify the VCL menus.pas.. I am using Delphi 2007 in this project. I am not all that happy about that idea though.

Here is the kind of code I am trying:

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;

Alternatively I wanted to do this:

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

Of course, popupEx is not there in the VCL. Nor any way to pass in more flags to TrackPopupMenu than those that the VCL guys added probably in 1995, in version 1.0.

Note: I believe the problem of estimating the height before showing the menu is impossible, thus we should be actually having the problem solved by TrackPopupMenu not by estimating the height.

Update: Calling TrackPopupMenu directly does not work, because the rest of the steps in the VCL method TPopupMenu.Popup(x,y) are necessary to invoke for my application to paint its menu and have it look correct, however it is impossible to invoke them without evil trickery because they are private methods. Modifying the VCL is a hellish proposition and I don't wish to entertain that either.

Warren P
  • 65,725
  • 40
  • 181
  • 316
  • Why not just call `TrackPopupMenu` directly with `aPopupMenu.Handle` and pass it whatever flags, etc. you want to provide? @Sertac posted an example [here](http://stackoverflow.com/a/11649119/62576) – Ken White May 31 '13 at 15:33
  • That does not appear to do everything that happens when I call TPopupMenu.Popup, though, and there's no VCL prepare-for-call-of-trackpopupmenu but don't call track popup menu. – Warren P May 31 '13 at 15:37
  • i think you can try to get some open-source button with menu or toolbar with buttons component and learn their sources. Like JvArrowButton of JVCL or ToolBar2000 or some other button or toolbar from torry.net - just find the proper component and learn from it – Arioch 'The May 31 '13 at 15:42
  • Sorry, but I can't see the image from here (proxy issue), which is why I'm not posting an answer. However, you say you want to accurately position a `TPopupMenu` above a button, and `TrackPopupMenu` with the `x` and `y` coordinates and the proper flag and menu handle does *exactly* that for you. What functionality are you missing from your requirements? – Ken White May 31 '13 at 15:43
  • Try to do that, with a TPopupMenu and you'll see many problems. Including the fact that most of the submenu item captions are not populated and the submenus are empty. calling TrackPopupMenu assumes that the VCL maintains the state of the items inside in a state where it's ready to call TrackPopupMenu. The VCL does not do that. – Warren P May 31 '13 at 15:45
  • Too bad the equation `A shl 10 = $20` has no solution... – Andreas Rejbrand May 31 '13 at 15:48
  • I think there is a solution to my problem, involving hooking the window message loop. – Warren P May 31 '13 at 15:50
  • 2
    @WarrenP: are you aware that with class helpers you do not only get access to protected members but to private members as well __provided__ you prefix them with `Self`? It makes working around VCL design decisions a heck of a lot easier. – Marjan Venema Jun 01 '13 at 16:22
  • Really? Can I do that in 2007? Oh they ARE in there. Why did I think those were added later. Good point! – Warren P Jun 01 '13 at 16:31

2 Answers2

6

A little bit hacky, but it might solve it.

Declare an interceptor class for TPopupMenu overriding 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;

The trick is to post a cancel message to the menu window which cancels the inherited TrackPopupMenu call.

Uwe Raabe
  • 45,288
  • 3
  • 82
  • 130
  • 2
    Looking at his question I think his trying to get the PopupMenu to appear above the Buttons origin point , not at the mouse cursor. Adding X := MyForm.Button.ClientOrigin.X; Y := MyForm.Button.ClientOrigin.Y - 2; before inherited should do it. +1 for great answer tho! – Peter May 31 '13 at 18:00
  • 1
    So you would get a popup menu appearing then immediately closed, then another one, right? I give this full marks for safety but it might glitch a bit on remote desktop users, who would see the whole thing unfolding in gory detail. Maybe a little hack would be to have it pop up off screen? `inherited Popup(-1000,-1000);` – Warren P May 31 '13 at 19:36
  • If this thing is to be used at several places it might be better to make a custom component with a decent name. This ould also allow for a proper event to adjust the popup coordinates. The out of sight coordinates for the inherited call are a good idea though. – Uwe Raabe May 31 '13 at 21:15
  • I am actually not going to override popup, but I am going to use an interposing class. The method name `PopupWithBottomAlignment` is clear and obvious. I will actually implement the code in a unit which can be reused and then simply interpose it in the unit where I want to use it. – Warren P Jun 01 '13 at 16:01
2

I cannot duplicate your issue with TrackPopupMenu. With a simple test here with D2007, items' captions, images, submenus seem to look and work correctly.

Anyway, the below example installs a CBT hook just before the menu is popped. The hook retrieves the window associated with the menu to be able to subclass it.

If you don't care a possible flashing of the popup menu under stressed conditions, instead of a hook, you can use the PopupList class to handle WM_ENTERIDLE to get to the menu's window.

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;
Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169