Reputation: 3503
How can you reset the max width for a PopupMenu's items list?
Say i.e you add a few TMenuItems at runtime to a popupmenu:
item1: [xxxxxxxxxxxxxxxxxxx]
item2: [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]
The menu automatically adjusts the size to fit the largest item. But then you do Items.Clear and add a new item:
item1: [xxxxxxxxxxxx ]
It ends up like that, with a large empty space after the caption.
Is there any workaround besides re-creating the popupmenu?
Here the code for reproducing this anomaly:
procedure TForm1.Button1Click(Sender: TObject);
var
t: TMenuItem;
begin
t := TMenuItem.Create(PopupMenu1);
t.Caption := 'largelargelargelargelargelarge';
PopupMenu1.Items.Add(t);
PopupMenu1.Popup(200, 200);
end;
procedure TForm1.Button2Click(Sender: TObject);
var
t: TMenuItem;
begin
PopupMenu1.Items.Clear;
t := TMenuItem.Create(PopupMenu1);
t.Caption := 'short';
PopupMenu1.Items.Add(t);
PopupMenu1.Popup(200, 200);
end;
Upvotes: 14
Views: 2224
Reputation: 2060
Late answer: but in 10.1 Berlin at least I find that the easiest solution is to set OwnerDraw to true, but do not provide OnDrawItem, only OnMeasureItem. This retains the styling of the menu, but allows you to set the width of the menu items after calling canvas.textextent((Sender as Tmenuitem).caption)
.
Since I have to set item captions to for example 'Open: somefilename.txt' this allows the menu to self-customize with minimal effort.
Upvotes: 1
Reputation: 43649
tl,dr: Attach an ImageList.
If the menu items could get send a WM_MEASUREITEM
message, then the width would be recalculated.
Setting the OwnerDraw
property to True
achieves that, which is the first solution. But for older Delphi versions, this will result in non-default and non-styled drawing of the menu items. That is not desirable.
Fortunately, TMenu
has a extraordinary way of telling whether the menu (items) is (are) owner drawn:
function TMenu.IsOwnerDraw: Boolean;
begin
Result := OwnerDraw or (Images <> nil);
end;
Thus setting the Images
property to an existing ImageList will achieve the same. Note that there need not be images in the ImageList. And if there are images in it, you do not have to use them and let the ImageIndex
be -1
for the menu items. Of course an ImageList with images will do just fine too.
Upvotes: 9
Reputation: 23036
There is workaround, but it is very, very dirty: Use a cracker class to obtain access to the FHandle private member of the TPopupMenu.Items menu item property.
A cracker class involves reproducing the private storage layout of the target class up to and including the private member of interest, and using a type-cast to "overlay" that type onto an instance of the target type in a context that then allows you to access the internal storage of the target.
In this case, the target object is the Items property of TPopupMenu which is an instance of TMenuItem. TMenuItem derives from TComponent so the cracker class to provide access to FHandle for a TMenuItem is:
type
// Here be dragons...
TMenuItemCracker = class(TComponent)
private
FCaption: string;
FChecked: Boolean;
FEnabled: Boolean;
FDefault: Boolean;
FAutoHotkeys: TMenuItemAutoFlag;
FAutoLineReduction: TMenuItemAutoFlag;
FRadioItem: Boolean;
FVisible: Boolean;
FGroupIndex: Byte;
FImageIndex: TImageIndex;
FActionLink: TMenuActionLink;
FBreak: TMenuBreak;
FBitmap: TBitmap;
FCommand: Word;
FHelpContext: THelpContext;
FHint: string;
FItems: TList;
FShortCut: TShortCut;
FParent: TMenuItem;
FMerged: TMenuItem;
FMergedWith: TMenuItem;
FMenu: TMenu;
FStreamedRebuild: Boolean;
FImageChangeLink: TChangeLink;
FSubMenuImages: TCustomImageList;
FOnChange: TMenuChangeEvent;
FOnClick: TNotifyEvent;
FOnDrawItem: TMenuDrawItemEvent;
FOnAdvancedDrawItem: TAdvancedMenuDrawItemEvent;
FOnMeasureItem: TMenuMeasureItemEvent;
FAutoCheck: Boolean;
FHandle: TMenuHandle;
end;
NOTE: Since this technique relies on an exact reproduction of the internal storage layout of the target class, the cracker declaration may need to include $IFDEF variations to cater for changes in that internal layout between different Delphi versions. The declaration above is correct for Delphi XE4 and should be checked against the TMenuItem source for correctness w.r.t other Delphi versions.
With that cracker class we can then provide a utility proc to wrap up the nasty tricks we are then going to perform using the access this provides. In this case, we can clear the menu items as usual, but also call DestroyMenu() ourselves using the cracker cast to overwrite the FHandle member variable with 0 since it is now invalid and needs to be 0 to force the TPopupMenu to recreate the menu when next needed:
procedure ResetPopupMenu(const aMenu: TPopupMenu);
begin
aMenu.Items.Clear;
// Here be dragons...
DestroyMenu(aMenu.Items.Handle);
TMenuItemCracker(aMenu.Items).FHandle := 0;
end;
In your sample code simply replace your call to PopupMenu1.Items.Clear in your Button2Click handler with a call to ResetPopupMenu(PopupMenu1).
It goes without saying that this is dangerous in the extreme. Quite apart from the sheer lunacy of hacking around inside the private storage of a class, no account is taken in this specific case for unmerging merged menus, for example.
But you asked if there was a workaround, and here is at least one. :)
Whether you consider this more or less practical or desirable than simply destroying and recreating the TPopupMenu is up to you. Class cracking is a technique which can be useful for getting you out of a jam which might otherwise be impossible to resolve but should definitely be considered a "last resort" !
Upvotes: 3