14

如何重置 PopupMenu 项目列表的最大宽度?

假设您在运行时将一些 TMenuItems 添加到弹出菜单:

item1: [xxxxxxxxxxxxxxxxxxx]
item2: [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]

菜单会自动调整大小以适应最大的项目。但随后您执行 Items.Clear 并添加一个新项目:

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;
4

3 回答 3

9

tl,dr:附加一个 ImageList。


如果菜单项可以发送WM_MEASUREITEM消息,则将重新计算宽度。

设置OwnerDraw属性来True实现这一点,这是第一个解决方案。但是对于较旧的 Delphi 版本,这将导致菜单项的非默认和非样式绘制。这是不可取的。

幸运的是,TMenu有一种特殊的方式来判断菜单(项目)是否是所有者绘制的:

function TMenu.IsOwnerDraw: Boolean;
begin
  Result := OwnerDraw or (Images <> nil);
end;

因此将Images属性设置为现有的 ImageList 将实现相同的效果。请注意,ImageList 中不需要有图像。如果其中有图像,您不必使用它们并让菜单项ImageIndex成为可能-1。当然,带有图像的 ImageList 也可以。

于 2014-11-05T11:39:25.130 回答
3

有一种解决方法,但它非常非常肮脏:使用破解程序类来获取对TPopupMenu.Items菜单项属性的FHandle私有成员的访问权限。

破解程序类涉及复制目标类的私有存储布局,直到并包括感兴趣的私有成员,并使用类型转换将该类型“覆盖”到上下文中的目标类型实例上,然后允许您访问目标的内部存储。

在这种情况下,目标对象是TPopupMenu的Items属性,它是TMenuItem的一个实例。 TMenuItem派生自TComponent ,因此为TMenuItem提供对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 版本的正确性。

有了这个破解器类,我们就可以提供一个实用程序 proc 来包装我们将使用它提供的访问权限执行的讨厌的技巧。在这种情况下,我们可以像往常一样清除菜单项,但也可以自己调用DestroyMenu()使用cracker cast 用 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相比,您是否认为这更实用或更可取取决于您。类破解是一种可以帮助您摆脱困境的技术,否则可能无法解决,但绝对应该被视为“最后的手段”!

于 2014-11-04T22:04:56.833 回答
1

迟到的答案:但至少在 10.1 Berlin 中,我发现最简单的解决方案是将 OwnerDraw 设置为 true,但不提供 OnDrawItem,只提供 OnMeasureItem。这保留了菜单的样式,但允许您在调用后设置菜单项的宽度canvas.textextent((Sender as Tmenuitem).caption)

由于我必须将项目标题设置为例如“打开:somefilename.txt ”,这允许菜单以最小的努力进行自我定制。

于 2017-08-25T17:19:25.350 回答