即使在Items.clear之后,TPopupMenu仍保留最大宽度

时间:2014-10-18 02:41:19

标签: delphi menuitem popupmenu delphi-xe7

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

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

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;

3 个答案:

答案 0 :(得分:8)

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 也可以。

答案 1 :(得分:3)

有解决方法,但它非常非常脏:使用cracker类来获取对 TPopupMenu.Items <的 FHandle 私有成员的访问权限/ strong>菜单项属性。

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

在这种情况下,目标对象是 TPopupMenu Items 属性,它是 TMenuItem 的一个实例。 TMenuItem 来自 TComponent ,因此为 TMenuItem 提供对 FHandle 的访问的cracker类是:

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 < / strong>用于满足不同Delphi版本之间内部布局中更改的变体。上面的声明对于 Delphi XE4 是正确的,应该根据 TMenuItem 来检查w.r.t其他Delphi版本的正确性。

通过这个破解者类,我们可以提供一个实用程序proc来结束我们将要使用它提供的访问执行的讨厌的技巧。在这种情况下,我们可以像往常一样清除菜单项,但也可以使用破解器强制转换 DestroyMenu()来覆盖 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;

在您的示例代码中,只需通过调用 ResetPopupMenu(PopupMenu1)替换您对 Button2Click 处理程序中 PopupMenu1.Items.Clear 的调用

不言而喻,这在极端情况下是危险的。除了在类的私有存储内部进行黑客攻击之外,例如,在这种特定情况下,没有考虑用于取消合并菜单的合并。

但你问是否有解决方法,这里至少有一个。 :)

您是否认为这或多或少是实用或可取的,而不仅仅是破坏和重新创建 TPopupMenu 取决于您。类破解是一种技术,可以帮助你摆脱干扰,否则可能无法解决,但绝对应该被视为“最后的手段”!

答案 2 :(得分:1)

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

由于我必须将项目标题设置为例如“打开: somefilename.txt ”,因此菜单可以轻松自定义。