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

14
你如何重置弹出菜单项列表的最大宽度?
比如,你在运行时向弹出菜单添加了几个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;

2
我可以只使用API调用来复制这个,包括CreatePopupMenu、InsertMenu、TrackPopupMenu、DeleteMenu等。只要句柄有效,就没有“收缩”问题。因此,我的观点是,唯一的解决方案是在运行时释放弹出菜单并重新创建它,这是调用“DestroyMenu”的唯一方法。 - Sertac Akyuz
2
@hikari:感谢您的编辑。有了可用的代码,这个问题对于未来可能在搜索中找到它的读者来说会更有用。 - Ken White
3个回答

9

简而言之:添加一个ImageList。


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

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

幸运的是,TMenu有一种特殊的方法来判断菜单(项)是否是自绘的:

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

因此,将 Images 属性设置为现有的 ImageList 将实现相同的效果。请注意,ImageList 中不需要有图像。如果其中有图像,您也无需使用它们,并且可以使菜单项的 ImageIndex-1。当然,拥有图像的 ImageList 也能很好地实现这一功能。

绝对比破解类更安全 :) 但我觉得有趣的是,每种技术都会产生一个略微不同于彼此或“普通”菜单的外观。将OwnerDraw设置为TRUE会导致菜单变得小得多,而虚拟ImageList则会使其稍微变大(不仅仅是图像边距 - 在普通弹出式菜单上也存在,但还有额外的填充在项目文本右侧)。非常奇怪。 - Deltics
@Deltics 我也在 D7 中注意到了 OwnerDraw=True 的小得多的菜单,但在 XE2 中没有。我怀疑这是旧版本中的一个错误。右侧的边距是热键的空间。我怀疑一旦附加了 ImageList,那么菜单就可以完全正常工作了。 - NGLN
@NGLN,使用或不使用ImageList效果相同。在D7/Win7上进行了测试。主题已经损坏(焦点渐变变为蓝色)。 - kobik
@kobik 那么对于旧版本来说,这个答案就不够令人满意了。 - NGLN
@NGLN,我理解你关于图像列表的观点:“但是哪个菜单不使用ImageList?”另一方面,如果您不使用图像列表,Windows将为焦点使用主题,但保留一个空的占位符(在每个项目的左侧)用于图像单选/复选项,如果您使用图像列表,这也看起来不太自然。所以这可能是一个“品味”的问题。+1 - kobik
显示剩余2条评论

3
有一个解决方法,但是非常、非常繁琐:使用一个cracker类来获取TPopupMenu.Items菜单项属性的FHandle私有成员的访问权限。
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 变化以适应不同 Delphi 版本之间的内部布局更改。上面的声明适用于 Delphi XE4,在正确性方面应根据其他 Delphi 版本的 TMenuItem 源代码进行检查。

有了这个破解器类,我们可以提供一个实用程序过程来包装我们即将执行的恶意操作。在本例中,我们可以像往常一样清除菜单项,但是也可以使用破解转换来调用 DestroyMenu(),以覆盖 FHandle 成员变量为0,因为它现在无效并且需要为 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)的调用即可。
毋庸置疑,这是非常危险的。除了在类的私有存储内进行hack的疯狂之外,特别是在这种情况下没有考虑取消合并菜单等因素。
但是您问是否有解决方法,这里至少有一个。 :)
无论您认为这比仅销毁和重新创建TPopupMenu更实用或更可取都由您决定。类破解是一种技术,可以帮助您摆脱可能否则无法解决的困境,但应该明确考虑为“最后的手段”!

+1 如果你知道自己在做什么,并且掌控自己的源代码,我觉得这不会有危险。例如,对于旧版本的 Delphi,TNT Unicode suit 就大量依赖于破解私有字段的技术(每个版本都使用适当的 $IFDEF)。你的代码在我的 D7 上运行得很好(当然,我定义了一个与 D7 偏移匹配的合适的破解结构)。比 OwnerDraw 好多了,后者会禁用主题并且看起来非常糟糕。顺便说一句,你可以计算出 FHandle 的偏移量,然后在这个字段之前加上一个 Filler[offset bytes] - kobik

1
晚回答了:但是在至少10.1 Berlin版本中,我发现最简单的解决方案是将OwnerDraw设置为true,但不提供OnDrawItem,仅提供OnMeasureItem。这会保留菜单的样式,但允许您在调用canvas.textextent((Sender as Tmenuitem).caption)后设置菜单项的宽度。
由于我必须将项目标题设置为例如“打开:somefilename.txt”,因此这使得菜单可以自行自定义,而只需付出最小的努力。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接