如何重置PopupMenu项目列表的最大宽度?
例如,您在运行时将一些TMenuItems添加到弹出菜单:
item1: [xxxxxxxxxxxxxxxxxxx]
item2: [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]
菜单会自动调整大小以适应最大的项目。然后你做项目。清除并添加一个新项目:
item1: [xxxxxxxxxxxx ]
它的结局是这样的,标题后面有一个很大的空白。
除了重新创建弹出菜单之外,还有什么解决办法吗?
这里是再现这种异常的代码:
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;
tl,dr:附加图像列表。
如果菜单项可以发送WM_MEASUREITEM
消息,则宽度将重新计算。
将OwnerDraw
属性设置为True
可以实现这一点,这是第一个解决方案。但对于较旧的Delphi版本,这将导致菜单项的非默认和非样式绘制。这是不可取的。
幸运的是,TMenu
有一种非凡的方式来判断菜单(项目)是否是所有者绘制的:
function TMenu.IsOwnerDraw: Boolean;
begin
Result := OwnerDraw or (Images <> nil);
end;
因此,将Images
属性设置为现有的ImageList将实现相同的效果。请注意,ImageList中不需要有图像。如果其中有图像,则不必使用它们,并让菜单项的ImageIndex
为-1
。当然,带有图像的ImageList也可以。
有一种解决方法,但非常非常脏:使用cracker类访问TPopupMenu.Items菜单项属性的FHandle私有成员。
cracker类包括复制目标类的私有存储布局,直到并包括感兴趣的私有成员,并使用类型转换将该类型"覆盖"到目标类型的实例上,然后允许您访问目标的内部存储。
在这种情况下,目标对象是TPopupMenu的Items属性,该属性是TMenuItem实例TMenuItem派生自TComponent,因此为TM列举项提供FHandle访问权限的破解程序类是:
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;
注意:由于此技术依赖于目标类内部存储布局的精确复制,破解程序声明可能需要包含$IFDEF变体,以满足不同Delphi版本之间内部布局中的更改。上面的声明对于Delphi XE4是正确的,并且应该对照TMenuItem源检查是否与其他Delphi版本正确。
有了这个cracker类,我们就可以提供一个实用程序proc来完成我们将要使用它提供的访问来执行的讨厌的技巧。在这种情况下,我们可以像往常一样清除菜单项,但也可以使用cracker强制转换自己调用DestroyMenu(),用0覆盖FHandle成员变量,因为它现在无效,需要为0才能强制TPopupMenu在下次需要时重新创建菜单:
procedure ResetPopupMenu(const aMenu: TPopupMenu);
begin
aMenu.Items.Clear;
// Here be dragons...
DestroyMenu(aMenu.Items.Handle);
TMenuItemCracker(aMenu.Items).FHandle := 0;
end;
在示例代码中,只需将Button2Click处理程序中对PopupMenu1.Items.Clear的调用替换为对ResetPopupMenu(PopupMenu1)的调用。
不用说,这是非常危险的。例如,除了在类的私人存储中进行疯狂的黑客攻击外,在这种特定情况下,没有考虑到合并菜单的不合并。
但你问是否有变通办法,这里至少有一个。:)
您是否认为这比简单地销毁和重新创建TPopupMenu更实用或更可取,取决于您自己。类破解是一种很有用的技术,可以让你摆脱困境,否则可能无法解决,但绝对应该被视为"最后的手段"!
迟来的答案:但在10.1 Berlin中,至少我发现最简单的解决方案是将OwnerDraw设置为true,但不提供OnDrawItem,只提供OnMeasureItem。这保留了菜单的样式,但允许您在调用canvas.textextent((Sender as Tmenuitem).caption)
后设置菜单项的宽度。
由于我必须将项目标题设置为例如"Open:somefilename.txt",这允许菜单以最小的工作量进行自定义。