2014-10-18 7 views
13

Wie können Sie die maximale Breite für eine PopupMenu-Artikelliste zurücksetzen?TPopupMenu behält maximale Breite, auch nach Items.clear

Say heißt Sie ein paar TMenuItems zur Laufzeit zu einem popupmenu hinzufügen:

item1: [xxxxxxxxxxxxxxxxxxx] 
item2: [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] 

Das Menü passt automatisch die Größe der größten Posten passen. Aber dann tun Sie Items.Clear und ein neues Element hinzuzufügen:

item1: [xxxxxxxxxxxx     ] 

Es ist wie es endet, mit einem großen leeren Raum nach dem Titel.

Gibt es eine Umgehungslösung neben der Neuerstellung des Popupmenüs?

Hier ist der Code für diese Anomalie reproduzieren:

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

Ich kann das mit nur api Aufrufen, CreatePopupMenu, InsertMenu, TrackPopupMenu, DeleteMenu usw. duplizieren. Es gibt keine "Kontraktion", solange das Handle gültig ist. Daher ist meine Meinung, dass die einzige Lösung darin besteht, das Popup-Menü freizugeben und es zur Laufzeit neu zu erstellen, das ist die einzige Möglichkeit, 'DestroyMenu' aufzurufen. –

+2

@hikari: Danke für die Bearbeitung.Die Frage ist viel nützlicher mit dem verfügbaren Code, insbesondere für zukünftige Leser, die sie in einer Suche finden können. –

Antwort

3

Es Abhilfe ist, aber es ist sehr, sehr schmutzig: eine Cracker-Klasse verwenden, Zugang zum privaten Mitglied fhandle der zu erhalten TPopupMenu.Items Menüpunkt Eigenschaft.

Eine Cracker-Klasse umfasst das Reproduzieren des privaten Speicherlayouts der Zielklasse bis einschließlich des interessierenden privaten Members und das Verwenden einer Typumwandlung zum "Überlagern" dieses Typs auf eine Instanz des Zieltyps in einem Kontext Dann können Sie auf den internen Speicher des Ziels zugreifen.

In diesem Fall wird das Zielobjekt ist die Artikel Eigenschaft TPopupMenu, die eine Instanz von TMenuItem. TMenuItem leitet sich von TComponent so die Cracker-Klasse Zugriff auf fhandle für eine TMenuItem zur Verfügung zu stellen:

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; 

HINWEIS: Da diese Technik beruht auf einer exakte Wiedergabe des internen Speicher Layout der Zielklasse, die Cracker-Erklärung muss möglicherweise enthalten $ IFDEF Variationen für Änderungen in diesem in ternales Layout zwischen verschiedenen Delphi-Versionen. Die obige Deklaration ist korrekt für Delphi XE4 und sollte mit der TMenuItem Quelle auf Korrektheit w.r.t anderen Delphi-Versionen überprüft werden.

Mit dieser Cracker-Klasse können wir dann ein Utility-Proc zur Verfügung stellen, um die bösen Tricks, die wir dann mit dem Zugriff, den dies bietet, auszuführen.In diesem Fall können wir die Menüeinträge wie gewohnt löschen, aber auch DestroyMenu() selbst aufrufen und dabei die FHandle Membervariable mit 0 überschreiben, da sie nun ungültig ist und 0 sein muss, um die zu erzwingen TPopupMenu das Menü neu erstellen, wenn neben benötigt:

procedure ResetPopupMenu(const aMenu: TPopupMenu); 
    begin 
    aMenu.Items.Clear; 

    // Here be dragons... 

    DestroyMenu(aMenu.Items.Handle); 
    TMenuItemCracker(aMenu.Items).FHandle := 0; 
    end; 

in Ihrem Beispielcode einfach auf Ihren Anruf PopupMenu1.Items.Clear in Ihrem Button2Click Handler mit einem Aufruf an ResetPopupMenu (PopupMenu1) ersetzen.

Es ist selbstverständlich, dass dies im Extremfall gefährlich ist. Abgesehen von dem bloßen Wahnsinn des Hackens im privaten Speicher einer Klasse wird in diesem speziellen Fall zum Beispiel nicht darauf geachtet, zusammengeführte Menüs zu trennen.

Aber Sie haben gefragt, ob es einen Workaround gab, und hier ist mindestens einer. :)

Ob Sie dies mehr oder weniger praktisch oder wünschenswert halten, als einfach die TPopupMenu zu zerstören und neu zu erstellen, liegt bei Ihnen. Class Cracking ist eine Technik, die nützlich sein kann, um aus einem Stau herauszukommen, der sonst unmöglich zu lösen wäre, aber definitiv als "letzter Ausweg" betrachtet werden sollte!

+1

Dies ist ein cooler Trick, aber es gibt mir Gänsehaut :-) –

+0

+1 Ich glaube nicht, dass es gefährlich ist, wenn Sie wissen, was Sie tun und die Kontrolle über Ihre eigenen Quellen. zum Beispiel, TNT Unicode-Anzug für ältere Delphi-Versionen stützt sich stark auf diese Technik der Knacken privater Felder (mit richtiger $ IFDEF für jede Version). Dein Code funktioniert gut mit meinem D7 (natürlich habe ich eine brauchbare Cracker-Struktur definiert, die mit D7-Offsets übereinstimmt. Viel besser als 'OwnerDraw', was die Themen ebenfalls deaktiviert und sehr schlecht aussieht. Übrigens könntest du den Offset von' FHandle' berechnen und einfach einen 'Filler [offset bytes] 'vor dieses Feld setzen. – kobik

8

tl, dr: Fügen Sie eine ImageList hinzu.


Wenn die Menüpunkte eine WM_MEASUREITEM Nachricht senden bekommen könnte, dann würde die Breite neu berechnet werden.

Das Setzen der OwnerDraw Eigenschaft auf True erreicht das, was die erste Lösung ist. Bei älteren Delphi-Versionen führt dies jedoch zu einer nicht standardmäßigen und nicht formatierten Zeichnung der Menüelemente. Das ist nicht wünschenswert.

Glücklicherweise TMenu hat eine außergewöhnliche Art und Weise, ob das Menü (Titel) zu sagen, ist (sind) Eigentümer gezeichnet:

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

So die Images Eigenschaft zu einer vorhandenen Abbildungsliste Einstellung wird das gleiche erreichen. Beachten Sie, dass keine Bilder in der ImageList vorhanden sein müssen. Und wenn Bilder darin sind, müssen Sie sie nicht verwenden und lassen Sie die ImageIndex-1 für die Menüpunkte sein. Natürlich wird eine ImageList mit Bilder auch gut tun.

+0

Auf jeden Fall viel sicherer als die Klasse zu knacken :) aber ich finde es merkwürdig, dass jede dieser Techniken ein etwas anderes Aussehen des Menüs ergibt als jedes andere * oder * das "einfache" Menü. Wenn OwnerDraw TRUE gesetzt wird, ergibt sich ein * viel * kleineres Menü und die Dummy ImageList in einem etwas größeren (nicht nur ein Rand für Bilder - auch in einem einfachen Popup vorhanden -, aber auch eine zusätzliche Auffüllung rechts vom Elementtext). Sehr komisch. – Deltics

+0

@Deltics Das viel kleinere Menü mit 'OwnerDraw = True' habe ich auch hier mit D7 bemerkt, aber nicht mit XE2. Ich vermute, dass es ein Bug in älteren Versionen ist. Der Rand auf der rechten Seite ist Platz für Hotkeys. Ich vermute, sobald eine ImageList angehängt ist, kann das Menü vollständig funktionieren. – NGLN

+0

Dies deaktiviert die Themen, und Dose sieht nicht gut aus. Ich denke, die meisten Zeichnungen sollten manuell erstellt werden, wenn die Elemente nativ aussehen sollen. Id besser einfach zerstören und neu erstellen Sie das Popupmenü oder verwenden Sie die Cracker-Klasse. – kobik

1

Späte Antwort: aber in 10.1 Berlin zumindest finde ich, dass die einfachste Lösung ist, OwnerDraw auf true zu setzen, aber OnDrawItem nicht, OnMeasureItem only. Dadurch bleibt das Aussehen des Menüs erhalten, Sie können jedoch die Breite der Menüelemente nach dem Aufruf von canvas.textextent((Sender as Tmenuitem).caption) festlegen.

Da ich Artikelbeschriftungen setzen muss, um zum Beispiel 'Öffnen: somefilename.txt' dies ermöglicht das Menü mit minimalem Aufwand selbst anpassen.