Chevrons


In diesem Kapitel wollen wir uns noch die so genannten "Chevrons" anschauen. Dabei handelt es sich um die kleinen Buttons mit dem Doppelpfeil am rechten Rand eines Bandes, die nur zu sehen sind, wenn das im Band enthaltene Control nicht vollständig dargestellt werden kann. Bekannt ist dieses Verhalten von Toolbars, Menüs u.ä. Elementen.

Allerdings setzt es min. die Version 5.80 der "comctl32.dll" voraus. Im Zweifelsfall hilft hier ein Update des Internet Explorer. Natürlich muss auch Ihre Delphi-Version aktuell genug sein und die neuen Konstanten, Nachrichten und Records kennen. Für Besitzer von Delphi 5 liegt eine spezielle Unit ("CommCtrl_Fragment.pas") bei, die diese Angaben enthält. Besitzer älterer Versionen können diese Unit testweise einsetzen, und Besitzer neuerer Versionen benötigen sie wahrscheinlich gar nicht mehr. Ich gehe also davon aus, dass Sie (auf die eine oder andere Weise) über die entsprechenden Deklarationen verfügen.

Aus dem Grund rüsten wir unser Beispielprogramm mit einer zusätzlichen Toolbar aus. Das bietet gleich einen Vorteil: ich kann und muss Sie auf eine Besonderheit hinweisen. Diese Besonderheit findet sich ein bisschen versteckt im PSDK:

 PSDK 

The toolbar default sizing and positioning behaviors can be turned off by setting the CCS_NORESIZE and CCS_NOPARENTALIGN common control styles. Toolbar controls that are hosted by rebar controls must set these styles because the rebar control sizes and positions the toolbar.

Auf gut Deutsch: Toolbars, die sich im Band einer Rebar befinden, müssen die Stilattribute CCS_NORESIZE und CCS_NOPARENTALIGN benutzen, weil das Rebar-Control die Position und Größe der Toolbar kontrolliert. Wenn Sie diese Attribute vergessen oder weglassen, erhalten Sie alle möglichen Ergebnisse, nur wird die Toolbar nie im Rebar-Control erscheinen. Oder sagen wir: nicht fehlerfrei! Unser Aufruf sieht daher so aus:

hwndChild := CreateWindowEx(0,TOOLBARCLASSNAME,nil,WS_CHILD or
  WS_VISIBLE or CCS_NODIVIDER or CCS_NORESIZE or CCS_NOPARENTALIGN or
  TBSTYLE_FLAT,0,0,0,0,hwndRebar,IDC_TOOLBAR,hInstance,nil);

Als Parent wird wieder, wie gehabt, das Rebar-Control angegeben. Es macht aber auch keinen Unterschied wenn Sie direkt das Handle des Hauptfensters angeben ("hwndParent" im Beispielprogramm).
Das Erzeugen der Buttons und Bitmaps einer Toolbar soll hier ebenfalls nicht das Thema sein. Dafür möchte ich Sie auf das Toolbar-Tutorial verweisen. Wir widmen uns lieber den Chevrons -

Um den kleinen Button anzeigen zu können, müssen wir zuerst eine Idealgröße für das Band festlegen. Wird diese Idealgröße unterschritten, erscheint rechts der Chevron und weist den Benutzer darauf hin, dass einige Elemente verborgen sind. Um die Idealgröße aber überhaupt beim Erzeugen des Bandes angeben zu können, ist das zusätzliche Flag RBBIM_IDEALSIZE erforderlich:

rbbi.fMask      := { ... } or RBBIM_IDEALSIZE;

Damit steht uns die Membervariable cxIdeal zum Befüllen zur Verfügung. Ich habe die Idealgröße der Toolbar vorher in einer Schleife berechnen lassen, so dass die tatsächliche Breite jedes Buttons (auch von Separatoren, die ja naturgemäß schmaler sind) berücksichtigt wird:

iIdeal := 0;
for i  := 0 to SendMessage(hwndChild,TB_BUTTONCOUNT,0,0) - 1 do
begin
  SendMessage(hwndChild,TB_GETITEMRECT,WPARAM(i),LPARAM(@rc));
  inc(iIdeal,(rc.Right - rc.Left));
end;

Diesen Wert übergeben wir nun an die Membervariable des Records:

rbbi.cxIdeal    := iIdeal;

Zu guter Letzt geben wir ein zusätzliches Stilattribut an, damit der Chevron auch tatsächlich erscheint:

rbbi.fStyle     := { ... } or RBBS_USECHEVRON;

Das Band wird nun wie bereits beschrieben eingefügt. Wenn wir das Programm nun einmal starten und die Breite der Toolbar verringern, -voilą- präsentiert sie sich wie folgt:



Zum Popup-Menü komme ich gleich, doch zuvor noch ein Hinweis, wieder speziell für die Toolbar: Sie können die Toolbar ja ganz nach Belieben vergrößern oder verkleinern. Dabei kann es passieren, dass einige Buttons nur teilweise zu sehen sind, wenn die Toolbar (eigentlich eher das Band der Rebar) zu klein wird. Da das merkwürdig aussieht, sorgen wir mit dem erweiterten Stilattribut TBSTYLE_EX_HIDECLIPPEDBUTTONS dafür, dass solche "abgeschnittenen" Buttons komplett verschwinden:

SendMessage(hwndChild,TB_SETEXTENDEDSTYLE,0,
  TBSTYLE_EX_HIDECLIPPEDBUTTONS);


Praktischer Nutzen

Wie kann man den Chevron nun verwenden? Die Anwendung muss ermitteln, welche der Toolbar-Buttons nicht sichtbar sind und deren Befehle in dem Popup-Menü anbieten. Microsofts PSDK bietet unter dem Indexeintrag "Creating an Internet Explorer-style Toolbar" eine kurze Anleitung ("Handling Chevrons") dafür. Allerdings ist anzumerken, dass sie bei mir nur eingeschränkt funktioniert hat. Ich war sozusagen zu kleinen "Bocksprüngen" gezwungen, bevor ich am Ziel ankam. Doch sehen wir es uns an -

  1. Microsoft empfiehlt, zuerst die Maße des Bandes herauszufinden. Das macht Sinn; schließlich müssen wir irgendwie erfahren, welche Buttons der Toolbar sichtbar sind und welche nicht. Es gibt eine Toolbar-Nachricht namens "TB_ISBUTTONHIDDEN", die uns hier leider nicht hilft. Unsere Buttons sind zwar nicht zu sehen, allerdings sind sie nicht versteckt (im Sinne der Definition der genannten Nachricht). Daher müssen wir anders an die Sache herangehen.
    Microsoft empfiehlt, mit Hilfe der Nachricht "RB_GETRECT" die Abmessungen des Bandes herauszufinden, das den Chevron anzeigt. Das tun wir:
    SendMessage(hRB,RB_GETRECT,WPARAM(PNMRebarChevron(lp)^.uBand),
      LPARAM(@rc1));
  2. Dann soll man, gemäß der Angabe im PSDK, die Anzahl der Toolbar-Buttons ermitteln. Dafür wird die Nachricht "TB_BUTTONCOUNT" verwendet, die sich der Einfachheit halber in einer for-Schleife benutzen lässt. Die nächsten Schritte spielen sich also innerhalb dieser Zeilen ab:
    for i := 0 to SendMessage(hTB,TB_BUTTONCOUNT,0,0) - 1 do
    begin
      { ... }
    end;
  3. Dann werden die Maße jedes einzelnen Buttons in der Toolbar bestimmt.
    SendMessage(hTB,TB_GETITEMRECT,WPARAM(i),LPARAM(@rc2));
  4. Die beiden Rechtecke (das vom Rebar-Control und das der Toolbar) werden nun mit der Funktion "IntersectRect" verglichen. Das Funktionsergebnis interessiert uns hierbei nicht, uns geht es um den ersten Parameter der Funktion: er liefert ein Rechteck (TRect) zurück, das sozusagen den gemeinsam genutzten Teil der beiden angegebenen Rechtecke enthält
    IntersectRect(vis,rc1,rc2);
    Machen wir ein Beispiel. Im folgenden Bild sehen Sie, dass der "Speichern"-Button nicht vollständig sichtbar ist. Der Übersichtlichkeit wegen habe ich in der unteren Toolbar den Button komplett dargestellt, so dass man auch sieht, welche Bereiche des Bandes er normalerweise überschreitet:



    Die Ermittlung des Button-Rechtecks liefert nun die kompletten Maße zurück, inkl. des nicht sichtbaren Teils (also so wie in der unteren Toolbar). Durch den Vergleich dieses Button-Rechtecks mit dem Rechteck des Rebar-Bandes mit der Funktion "IntersectRect", würde aber nur der sichtbare Teil (wie in der oberen Toolbar) zurückgeliefert werden.
  5. Dieses zurückgelieferte Rechteck mit dem gemeinsam genutzten Bereich wird nun mit dem originalen Button-Rechteck verglichen, wozu die Funktion "EqualRect" benutzt wird. Hierbei interessiert uns der Rückgabewert allerdings wieder, denn wenn die beiden Rechtecke unterschiedlich sind (Ergebnis false), bedeutet das, der Button ist nicht oder nicht vollständig zu sehen und muss als Item in das Popup-Menü eingetragen werden. Sind beide Rechtecke identisch (Ergebnis true), heißt das selbstverständlich, der Button ist vollständig sichtbar:
    if(not EqualRect(vis,rc2)) and (tb.fsStyle <> BTNS_SEP) then
      AppendMenu(hm,MF_STRING,tb.idCommand,pchar(pText));
    Sie sehen hier auch gleich noch die Kontrollabfrage, ob es sich bei dem Button um einen Separator handelt. Der Grund liegt auf der Hand: Separatoren müssen nun wirklich nicht ins Menü. Theoretisch wäre das aber auch kein Problem, da es bei Menüs ja vergleichbares gibt. :o)

Soweit die Anleitung von Microsoft im PSDK. Wenn Sie sie nachvollziehen, dann werden Sie (vielleicht) bemerken, dass die Anzeige der Menüeinträge nicht wirklich funktioniert. Das Beispielprogramm enthält dazu den Compilerschalter PSDK, der alle meine zusätzlichen Schritte ausklammert, so dass Sie es gern probieren können.

Folgendes ist zu sagen: die Koordinaten der Toolbar-Buttons werden sich in diesem Experiment nie ändern. Das liegt ganz einfach daran, dass sie relativ zur Toolbar zurückgeliefert werden. Egal wohin Sie die Anwendung auch schieben, der erste Button wird immer die Maße (0,0,54,36) besitzen. (Die Werte hängen natürlich von den benutzten Stilattributen ab und können bei Ihnen anders sein.)

Da die Toolbar nun standardmäßig das dritte erzeugte Control ist und sich daher auch im dritten Band (und in der zweiten Zeile) befindet, werden die Abmessungen der Toolbar-Buttons in den meisten Fällen scheinbar außerhalb des Bandes liegen. Die Funktion "IntersectRect" würde in solchen Fällen als gemeinsam genutztes Rechteck immer (0,0,0,0) liefern.
Für einigermaßen brauchbare Ergebnisse müssten Sie die Toolbar nach ganz oben (an den Anfang der Rebar) schieben, so dass sie zum ersten Control wird. Dann kann es aber passieren (und bei mir ist es passiert!), dass ein Button nicht mehr sichtbar war und trotzdem nicht im Popup-Menü auftauchte. Das lag daran, dass bei der Ermittlung des Band-Rechtecks auch der Platz für den Text und die Breite des Chevrons berücksichtigt wurden. Das heißt also:

  1. Nachdem wir im ersten Schritt die Abmessungen des Bandes ermittelt haben, sollten wir herausfinden, welche tatsächlich nutzbare Fläche uns davon zur Verfügung steht. Bedenken Sie, dass die Rebar des Beispielprogramms ja auch noch zusätzlichen Text auf der linken Seite anzeigt. Dieser Bereich ist für Controls nicht nutzbar, wird aber durch Schritt 1 der obigen Anleitung berücksichtigt. Für die Ermittlung der tatsächlich nutzbaren Fläche verwenden wir die Nachricht "RB_GETBANDBORDERS"
    SendMessage(hRB,RB_GETBANDBORDERS,WPARAM(PNMRebarChevron(lp)^.uBand),
      LPARAM(@vis));
    Leider können wir diese Nachricht nicht allein verwenden, da sie wirklich nur die Grenzwerte liefert. Diese Werte kombinieren wir aber mit dem in Schritt 1 ermittelten Band-Rechteck. Dabei werden die Werte der linken oberen Ecke addiert, die der rechten unteren allerdings subtrahiert, wodurch wir das Band-Rechteck verkleinern:
    inc(rc1.Left,vis.Left);
    inc(rc1.Top,vis.Top);
    dec(rc1.Right,vis.Right);
    dec(rc1.Bottom,vis.Bottom);
    Und weil der Chevron auch Platz belegt, der fälschlich als verfügbar betrachtet wird, ziehen wir seine Breite ebenfalls ab:
    dec(rc1.Right,
      PNMRebarChevron(lp)^.rc.Right - PNMRebarChevron(lp)^.rc.Left);
  2. Im dritten Schritt der o.g. Anleitung haben wir dann die Maße der einzelnen Toolbar-Buttons ermittelt. Wie ich aber sagte, sind diese relativ zur Toolbar und können sich daher scheinbar außerhalb des Rebar-Bandes befindet. Insbesondere im Fall des Standards, wenn sich die Toolbar in der zweiten Reihe des Rebar-Controls befindet. Also müssen wir als einzigen noch erforderlichen Schritt die linke obere Ecke unseres verfügbaren Band-Rechtecks berücksichtigen und auf das Button-Rechteck übertragen:
    OffsetRect(rc2,rc1.Left,rc1.Top);

Glauben Sie es ruhig: nach diesen beiden Ergänzungen funktioniert das Popup-Menü des Chevrons wie gewünscht und zeigt die Toolbar-Buttons an, die momentan nicht zu sehen sind.

Und damit sind wir beim Thema: der Anzeige des Popup-Menüs. Nachdem ich ein bisschen abgeschweift bin, nun zum eigentlichen Kern. Wird der Chevron angeklickt, löst er die Benachrichtigung "RBN_CHEVRONPUSHED" aus, die als Teil von "WM_NOTIFY" bearbeitet werden kann. Der lparam zeigt dabei auf ein Record vom Typ TNMRebarChevron. Sie haben in den obigen Zeilen bereits Bekanntschaft damit gemacht. Wir benötigen hier aber noch die Koordinaten des Chevron, die ebenfalls in dem Record mitgeliefert werden. Zur Anzeige des Menüs interessiert uns aber nur die untere linke Ecke.
Die Koordinaten sind allerdings Client-abhängig und müssen erst in gültige Bildschirmwerte umgewandelt werden. Also weisen wir sie der Einfachheit halber einer TPoint-Variablen zu:

p.X := PNMRebarChevron(lp)^.rc.Left;
p.Y := PNMRebarChevron(lp)^.rc.Bottom;

Umgewandelt werden Sie dann mit der Funktion "ClientToScreen", von der wir u.a. auch die Anzeige des Menüs abhängig machen:

if(ClientToScreen(wnd,p)) and (GetMenuItemCount(hm) > 0) then
  TrackPopupMenu(hm,TPM_LEFTALIGN,p.X,p.Y,0,wnd,nil);