Подтвердить что ты не робот

TPopupMenu сохраняет максимальную ширину даже после Items.clear

Как вы можете reset максимальную ширину для списка элементов PopupMenu?

Скажем, вы добавляете несколько TMenuItems во время выполнения в popupmenu:

item1: [xxxxxxxxxxxxxxxxxxx]
item2: [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]

В меню автоматически настраивается размер, соответствующий самому большому элементу. Но затем вы делаете Items.Clear и добавляете новый элемент:

item1: [xxxxxxxxxxxx                    ]

Он заканчивается таким образом, с большим пустым пространством после заголовка.

Есть ли какой-нибудь обходной путь помимо повторного создания popupmenu?

Здесь приведен код для воспроизведения этой аномалии:

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

Ответ 1

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 с изображениями тоже будет очень хорош.

Ответ 2

Существует обходное решение, но оно очень сильно очень. Используйте класс взломщика, чтобы получить доступ к частному члену FHandle TPopupMenu.Items свойства элемента меню.

Класс взлома включает в себя воспроизведение макета частного хранилища целевого класса до и включая частный член, представляющий интерес, и использование типа-cast для "наложения" этого типа на экземпляр целевого типа в контексте, который затем позволяет вам получить доступ к внутреннему хранилищу цели.

В этом случае целевым объектом является свойство Элементы TPopupMenu, которое является экземпляром TMenuItem. TMenuItem происходит от TComponent, поэтому класс взлома для обеспечения доступа к FHandle для TMenuItem:

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 для корректности w.r.t других версий Delphi.

С этим классом взломщика мы можем затем предоставить утилиту proc, чтобы обернуть неприятные трюки, которые мы затем будем выполнять, используя доступ, который предоставляет. В этом случае мы можем очистить элементы меню, как обычно, но также вызывать 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;

В вашем примере кода просто замените ваш вызов на PopupMenu1.Items.Clear в обработчике Button2Click с вызовом ResetPopupMenu (PopupMenu1).

Само собой разумеется, что это опасно в крайнем случае. В отличие от безумного безумства взлома внутри частного хранилища класса, в этом конкретном случае учет не происходит для неприкрепленных объединенных меню.

Но вы спросили, есть ли обходной путь, и вот как минимум один.:)

Считаете ли вы это более или менее практичным или желательным, чем просто уничтожение и воссоздание TPopupMenu, зависит от вас. Взломать класс - это метод, который может быть полезен для того, чтобы вытащить вас из джема, который в противном случае невозможно было бы разрешить, но определенно следует считать "последним средством"!

Ответ 3

Поздний ответ: но в 10.1 Берлине, по крайней мере, я нахожу, что самым простым решением является установить OwnerDraw в true, но не предоставлять OnDrawItem, а только OnMeasureItem. Это сохраняет стиль меню, но позволяет вам устанавливать ширину пунктов меню после вызова canvas.textextent((Sender as Tmenuitem).caption).

Поскольку мне нужно установить подписи к элементу, например, "Открыть: somefilename.txt", это позволяет настраивать меню с минимальными усилиями.