Die Benutzeroberfläche und ihre Grenzen

Neue Denkmuster

Charles Petzold

Herunterladen des Codebeispiels.

Die Leinwand ist eine von mehreren Layoutoptionen, die in WPF (Windows Presentation Foundation) und Silverlight zur Verfügung stehen. Sie ist die Option mit der längsten Tradition. Wenn Sie die Leinwand mit untergeordneten Elementen füllen, positionieren Sie jedes untergeordnete Element durch die Angabe der Koordinaten mithilfe der zugewiesenen Eigenschaften Canvas.Left und Canvas.Top. Dies ist ein ganz anderes Paradigma als bei den anderen Bereichen, in denen die untergeordneten Elemente basierend auf einfachen Algorithmen angeordnet werden, ohne dass der Programmierer die tatsächlichen Positionen herausfinden muss.

Wenn Sie das Wort „Leinwand“ hören, denken Sie wahrscheinlich ans Malen und Zeichnen. Aus diesem Grund neigen die Programmierer, die mit WPF und Silverlight arbeiten, vielleicht dazu, für die Anzeige von Vektorgrafiken auf die Leinwand zu verweisen. Wenn Sie jedoch die Leinwand für die Anzeige von Linien-, Polylinien-, Polygon- und Pfadelementen verwenden, enthalten die Elemente selbst Koordinatenpunkte, die sie innerhalb der Leinwand positionieren. Folglich brauchen Sie sich nicht mit den zugewiesenen Canvas.Left- und Canvas.Top-Eigenschaften zu beschäftigen.

Warum also eine Leinwand verwenden, wenn Sie die zugewiesenen Eigenschaften nicht benötigen, die sie zur Verfügung stellt? Gibt es einen besseren Ansatz?

Leinwand im Vergleich zum Raster

Im Lauf der Jahre lehnte ich die Leinwand für die Anzeige von Vektorgrafiken zunehmend ab und neigte eher zur Verwendung eines Einzelzellenrasters. Ein Einzelzellenraster ist wie ein rechteckiges Raster, nur ohne Zeilen- oder Spaltendefinitionen. Falls das Raster nur eine Zelle enthält, können Sie mehrere Elemente in der Rasterzelle ablegen. Sie verwenden dann keine der zugewiesenen Eigenschaften des Rasters, um Zeilen oder Spalten anzugeben.

Anfänglich scheint die Verwendung eines Leinwand- oder Einzelzellenrasters sehr ähnlich zu sein. Unabhängig davon, welche Sie für Vektorgrafiken verwenden, werden die Linien-, Polygon- und Pfadelemente basierend auf ihren Koordinatenpunkten im Verhältnis zur oberen linken Ecke des Containers positioniert.

Der Unterschied zwischen der Leinwand und dem Einzelzellenraster liegt darin, wie der Container für den Rest des Layoutsystems dargestellt wird. WPF und Silverlight enthalten ein zweiphasiges Layout von oben nach unten, wobei jedes Element die Größe der untergeordneten Elemente abfragt und dann für die Anordnung der untergeordneten Elemente im Verhältnis zu sich selbst verantwortlich ist. Innerhalb dieses Layoutsystems sind die Leinwand und das Einzelzellenraster sehr unterschiedlich:

  • Für die untergeordneten Elemente hat das Raster dieselben Abmessungen wie die des eigenen übergeordneten Elements. Dabei handelt es sich in der Regel um endliche Abmessungen. Die Leinwand scheint jedoch immer unendliche Abmessungen seiner untergeordneten Elemente aufzuweisen.
  • Das Raster meldet die zusammengesetzte Größe seiner untergeordneten Elemente dem übergeordneten Element. Die Leinwand weist jedoch immer eine offensichtliche Größe von null auf, unabhängig von den untergeordneten Elementen, die sie enthält.

Angenommen, Sie verfügen über eine Gruppe von Polygonelementen, die ein cartoonähnliches Vektorgrafikbild bilden. Wenn Sie alle diese Polygonelemente in einem Einzelzellenraster ablegen, basiert die Größe des Rasters auf den maximalen horizontalen und vertikalen Koordinaten der Polygone. Das Raster kann dann innerhalb des Layoutsystems als normales Element mit endlicher Größe behandelt werden, da seine Größe die Größe des zusammengesetzten Bildes ordnungsgemäß widerspiegelt. (Dies funktioniert nur dann ordnungsgemäß, wenn sich die obere linke Ecke des Bildes am Punkt (0, 0) befindet und keine negativen Koordinaten vorhanden sind.)

Wenn Sie jedoch alle diese Polygone auf einer Leinwand ablegen, meldet die Leinwand dem Layoutsystem, dass es eine Größe von null aufweist. Bei der Integration eines zusammengesetzten Vektorgrafikbilds in Ihre Anwendung bevorzugen Sie im Allgemeinen das Verhalten des Einzelzellenrasters gegenüber der Leinwand.

Ist die Leinwand also völlig nutzlos? Absolut nicht. Der Trick besteht darin, die Besonderheiten der Leinwand zu Ihrem Vorteil zu nutzen. In einem ganz konkreten Sinne nimmt die Leinwand nicht am Layout teil. Deshalb können Sie sie verwenden, wenn Sie layoutunabhängig arbeiten möchten, um Grafiken anzuzeigen, die die Grenzen des Layoutsystems überschreiten und außerhalb „schweben“. Standardmäßig schneidet die Leinwand die untergeordneten Elemente nicht zu. Obwohl sie also sehr klein ist, kann sie noch untergeordnete Elemente außerhalb ihrer Grenzen aufnehmen. Die Leinwand ist eher ein Bezugspunkt für die Anzeige von Elementen oder Grafiken als ein Container.

Die Leinwand ist großartig für Methoden, die ich als „neues Denkmuster“ bezeichne. Ich werde zwar die Codebeispiele in Silverlight zeigen, Sie können jedoch dieselben Methoden auch in WPF verwenden. Der herunterladbare Quellcode, der mit diesem Artikel mitgeliefert wird, ist eine Visual Studio-Lösung namens ThinkingOutsideTheGrid. Sie können die Programme unter charlespetzold.com/silverlight/ThinkingOutsideTheGrid testen.

Visuelle Verknüpfung von Steuerelementen

Angenommen, Sie verwenden eine Reihe von Steuerelementen in Ihrer Silverlight- oder WPF-Anwendung und müssen eine visuelle Verknüpfung zwischen zwei oder mehr Steuerelementen bereitstellen. Vielleicht möchten Sie eine Linie von einem Steuerelement zu einem anderen ziehen, und vielleicht kreuzt diese Linie andere Steuerelemente, die dazwischen liegen.

Sicherlich muss diese Linie auf Änderungen im Layout reagieren, vielleicht wenn das Fenster oder die Seite vom Benutzer in der Größe geändert wird. Darüber informiert zu werden, dass ein Layout aktualisiert wird, ist eine hervorragende Anwendung des LayoutUpdated-Ereignisses – ein Ereignis, das ich nie verwenden konnte, bevor ich die Probleme analysiert hatte, die ich in diesem Artikel beschreibe. LayoutUpdated wird von UIElement in WPF und von FrameworkElement in Silverlight definiert. Wie der Name bereits suggeriert, wird das Ereignis ausgelöst, nachdem eine Layoutphase die Elemente auf dem Bildschirm neu angeordnet hat.

Bei der Verarbeitung des LayoutUpdated-Ereignisses möchten Sie nichts tun, was eine weitere Layoutphase verursacht und Sie in eine Endlosrekursion verwickelt. Hier kommt die Leinwand zum Tragen: Da sie immer eine Nullgröße an das übergeordnete Element meldet, können Sie die Elemente auf der Leinwand ohne Auswirkungen auf das Layout ändern.

Die XAML-Datei des ConnectTheElements-Programms ist wie folgt strukturiert:

<UserControl ... >
  <Grid ... >
    <local:SimpleUniformGrid ... >
      <Button ... />
      <Button ... />
      ...
    </local:SimpleUniformGrid>

    <Canvas>
      <Path ... /> 
      <Path ... />
      <Path ... />
    </Canvas>
  </Grid>
</UserControl>

Das Raster enthält ein SimpleUniformGrid-Element, das die Anzahl der Zeilen und Spalten für die Anzeige der untergeordneten Elemente basierend auf der Gesamtgröße und dem Seitenverhältnis berechnet. Wenn Sie die Größe des Fensters ändern, ändert sich die Anzahl der Zeilen und Spalten, und die Zellen sind frei beweglich. Von den 32 Schaltflächen in diesem SimpleUniformGrid-Element haben zwei der Schaltflächen die Namen btnA und btnB. Die Leinwand belegt denselben Bereich wie das SimpleUniformGrid-Element, liegt jedoch darauf. Diese Leinwand enthält Pfadelemente, die vom Programm dazu verwendet werden, Ellipsen um die zwei benannten Schaltflächen herum sowie eine Linie dazwischen zu zeichnen.

Die Code-Behind-Datei führt alle ihre Aktivitäten während des LayoutUpdated-Ereignisses aus. Sie muss die Position von zwei benannten Schaltflächen relativ zur Leinwand finden, die praktischerweise auch am SimpleUniformGrid-Element, dem Raster und dem MainPage-Element selbst ausgerichtet ist.

Um eine Position eines Elements im Verhältnis zu einem beliebigen anderen Element in derselben visuellen Struktur zu finden, verwenden Sie die TransformToVisual-Methode. Diese Methode wird von der Visual-Klasse in WPF und von UIElement in Silverlight definiert, sie funktioniert jedoch in beiden Umgebungen auf dieselbe Art und Weise. Angenommen, das Element el1 befindet sich an einer beliebigen Stelle innerhalb des Bereichs, der von el2 belegt wird. (In ConnectTheElements ist el1 eine Schaltfläche, und el2 ist ein MainPage-Element.) Dieser Methodenaufruf gibt ein Objekt vom Typ GeneralTransform zurück. Dabei handelt es sich um die abstrakte übergeordnete Klasse aller anderen Grafiktransformationsklassen:

el1.TransformToVisual(el2)

Sie können eigentlich nichts mit GeneralTransform machen, außer die Transform-Methode aufrufen, die einen Punkt von einem Koordinatenraum in einen anderen umwandelt.

Angenommen, Sie möchten die Mitte von el1 finden, jedoch im Koordinatenraum von el2. Hier ist der Code:

Point el1Center = new Point(
  el1.ActualWidth / 2, el1.ActualHeight / 2); 
Point centerInEl2 = 
  el1.TransformToVisual(el2).Transform(el1Center);

Falls el2 entweder die Leinwand ist oder an der Leinwand ausgerichtet ist, können Sie diesen centerInEl2-Punkt zum Festlegen einer Grafik auf der Leinwand verwenden, die scheinbar in der Mitte von el1 positioniert wird.

ConnectTheElements führt diese Transformation in der WrapEllipseAroundElement-Methode zum Zeichnen von Ellipsen um die beiden benannten Schaltflächen herum aus und berechnet dann die Koordinaten der Linie zwischen den Ellipsen, basierend auf einer Schnittstelle der Linie zwischen den Mittelpunkten der Schaltflächen. Abbildung 1 zeigt das Ergebnis.

image: The ConnectTheElements Display

Abbildung 1 Die ConnectTheElements-Anzeige

Wenn Sie dieses Programm in WPF testen, ändern Sie das SimpleUniformGrid-Element in ein WrapPanel-Element, um beim Ändern der Größe des Programmfensters eine dynamischere Änderung im Layout zu erhalten.

Überwachen von Schiebereglern

Das Ändern von Grafiken und anderen visuellen Darstellungen als Reaktion auf Änderungen in einer Bildlaufleiste oder einem Schieberegler sind sehr grundlegend. In WPF und Silverlight können Sie dies entweder im Code oder einer XAML-Bindung durchführen. Was aber, wenn Sie die Grafiken genau am tatsächlichen Schieberegler mit Ziehpunkt ausrichten möchten?

Dies ist die Idee hinter dem TriangleAngles-Projekt, das ich als eine Art der interaktiven Trigonometriedemonstration entwarf. Ich ordnete zwei Schieberegler, einen vertikalen und einen horizontalen, im rechten Winkel zueinander an. Die beiden Ziehpunkte der Schieberegler definieren die beiden Scheitelpunkte eines rechtwinkligen Dreiecks, wie in Abbildung 2 dargestellt.

image: The TriangleAngles Display

Abbildung 2 Die TriangleAngles-Anzeige

Beachten Sie, wie das halbdurchsichtige Dreieck auf den beiden Schiebereglern liegt. Wenn Sie die Ziehpunkte des Schiebereglers bewegen, ändern die Seiten des Dreiecks Größe und Proportion, wie von den angegebenen Winkeln und Beschriftungen der vertikalen und horizontalen Säulen angegeben.

Dies ist offensichtlich eine weitere Aufgabe für ein Leinwand-Overlay, jedoch mit einer hinzugefügten Komplexitätsschicht, da das Programm Zugriff auf den Ziehpunkt des Schiebereglers benötigt. Dieser Ziehpunkt des Schiebereglers ist Teil einer Steuerelementvorlage: Den Ziehpunkten werden Namen innerhalb der Vorlage zugewiesen. Auf diese Namen kann jedoch unglücklicherweise außerhalb der Vorlage nicht zugegriffen werden.

Stattdessen kommt die häufig notwendige statische VisualTreeHelper-Klasse zur Hilfe. Mit dieser Klasse können Sie jede visuelle Struktur über die GetParent-, GetChildenCount- und GetChild-Methoden in WPF oder Silverlight durchlaufen (oder eher „durchklettern“). Zur Verallgemeinerung des Vorgangs zum Auffinden eines bestimmten Typs des untergeordneten Elements schrieb ich eine etwas rekursive allgemeine Methode:

T FindChild<T>(DependencyObject parent) 
  where T : DependencyObject

Ich nenne sie folgendermaßen:

Thumb vertThumb = FindChild<Thumb>(vertSlider);
Thumb horzThumb = FindChild<Thumb>(horzSlider);

An diesem Punkt konnte ich das TransformToVisual-Element an den beiden Ziehpunkten verwenden, um ihre Koordinaten relativ zum Leinwand-Overlay zu erhalten.

Dies funktionierte für einen Schieberegler, aber nicht für den anderen. Es dauerte eine Weile, um mich daran zu erinnern, dass die Steuerelementvorlage für den Schieberegler zwei Ziehpunkte enthält: einen für die horizontale Ausrichtung und einen für die vertikale. Je nach der für den Schieberegler festgelegten Ausrichtung ist für die Vorlage die Visibility-Eigenschaft auf Collapsed festgelegt. Ich fügte ein zweites Argument zur FindChild-Methode namens mustBeVisible hinzu und verwendete dieses, um die Suche innerhalb einer beliebigen untergeordneten Verzweigung aufzugeben, in der ein Element nicht sichtbar ist.

Das Festlegen von HitTestVisible in dem Polygon, das das Dreieck bildet, auf „False“ trug dazu bei, zu verhindern, dass es die Mauseingabe am Ziehpunkt des Schiebereglers beeinträchtigte.

Bildlauf außerhalb von ItemsControl

Angenommen, Sie verwenden ein ItemsControl-Element oder ein ListBox-Element mit einem DataTemplate-Element für die Anzeige der Objekte in der Auflistung des Steuerelements. Können Sie eine Leinwand in dieses DataTemplate-Element so einschließen, dass die Informationen hinsichtlich eines bestimmten Elements außerhalb des Steuerelements angezeigt werden können, das Element jedoch beim Bildlauf des Steuerelements zu überwachen scheinen?

Ich habe genau hierfür noch keine geeignete Methode gefunden. Das große Problem scheint in einer Zuschneideregion zu bestehen, die von ScrollViewer auferlegt wurde. Dieser ScrollViewer schneidet jede beliebige Leinwand, die außerhalb ihrer Grenzen zu schweben scheint, und folglich alles auf dieser Leinwand.

Mit ein bisschen zusätzlichem Wissen über das Innenleben des ItemsControl-Elements können Sie etwas machen, was Ihrer Vorstellung nahekommt.

Ich stelle mir dieses Feature als Pop-out-Element vor, insofern, als es etwas ist, das zu einem Element in einem ItemsControl-Element gehört, das aber tatsächlich aus dem ItemsControl-Element selbst herausragt. Das ItemsControlPopouts-Projekt demonstriert die Methode. Um etwas für die Anzeige des ItemsControl-Elements bereitzustellen, erstellte ich eine kleine Datenbank namens ProduceItems.xml, in der sich das Unterverzeichnis „Data“ von „ClientBin“ befindet. ProduceItems besteht aus einer Vielzahl von Elementen mit dem Tagnamen ProduceItem, von denen jedes ein Name-Attribut, ein Photo-Attribut mit Verweis auf ein Bitmap-Bild des Elements sowie eine optionale Nachricht enthält, die aus ItemsControl „herausragend” angezeigt wird. (Die Fotos und die anderen Illustrationen sind Microsoft Office-ClipArt.)

Die ProduceItem- und ProduceItems-Klassen bieten Codeunterstützung für die XML-Datei. ProduceItemsPresenter liest die XML-Datei und deserialisiert sie in ein ProduceItems-Objekt. Dieses ist auf die DataContext-Eigenschaft der visuellen Struktur festgelegt, die ScrollViewer und ItemsControl enthält. ItemsControl enthält ein einfaches DataTemplate zum Anzeigen der Elemente.

Eventuell können Sie jetzt auf ein kleines Problem stoßen. Das Programm fügt Geschäftsobjekte vom Typ ProduceItem in das ItemsControl-Element wirksam ein. Intern bildet ItemsControl für jedes Element basierend auf dem DataTemplate-Element eine visuelle Struktur. Um die Bewegung dieser Elemente zu überwachen, müssen Sie auf die interne visuelle Struktur zugreifen. So finden Sie heraus, wo genau sich die Elemente im Verhältnis zum Rest des Programms befinden.

Diese Informationen sind verfügbar. ItemsControl definiert eine Abrufeigenschaft namens ItemContainerGenerator, die ein Objekt vom Typ ItemContainerGenerator zurückgibt. Dies ist die Klasse, die für das Generieren der visuellen Strukturen, die jedem Element in ItemsControl zugewiesen sind, verantwortlich ist. Sie enthält praktische Methoden, wie z. B. ContainerFromItem, das den Container (was tatsächlich einem ContentPresenter entspricht) für jedes Objekt im Steuerelement enthält.

Wie bei den anderen beiden Programmen bedeckt das ItemsControlPopouts-Programm die gesamte Seite mit einer Leinwand. Erneut ermöglicht das LayoutUpdated- Ereignis, dass das Programm prüft, ob eine Änderung auf der Leinwand erforderlich ist. Der LayoutUpdated-Handler in diesem Programm listet über die ProduceItem-Objekte in ItemsControl auf und prüft, ob eine nicht-leere Message-Eigenschaft oder eine Message-Eigenschaft ungleich null vorhanden ist. Alle diese Message-Eigenschaften sollten einem Objekt vom Typ PopOut auf der Leinwand entsprechen. PopOut ist einfach eine kleine Klasse, die vom ContentControl-Element mit einer Vorlage abgeleitet wird, auf der eine Zeile und der Nachrichtentext angezeigt werden. Wenn kein PopOut-Element vorhanden ist, wird es erstellt und der Leinwand hinzugefügt. Wenn es vorhanden ist, wird es einfach erneut verwendet.

Das PopOut-Element muss dann auf der Leinwand positioniert werden. Das Programm erhält den Container, der dem Datenobjekt entspricht, und wandelt seine Platzierung im Verhältnis zur Leinwand um. Wenn sich diese Stelle zwischen dem oberen und unteren Rand von ScrollViewer befindet, ist die Visibility-Eigenschaft des PopOut-Elements auf Visible festgelegt. Andernfalls ist das PopOut-Element ausgeblendet.

Ausbrechen aus der Zelle

WPF und Silverlight bieten mit Sicherheit den großen Vorteil der Layout-Einfachheit. Das Raster und andere Bereiche stellen die Elemente ordentlich in Zellen zusammen und gewährleisten, dass sie dort bleiben. Es wäre eine Schande, davon auszugehen, dass Benutzerfreundlichkeit Sie in Ihrer Freiheit einschränken muss, die Elemente nach Ihren Wünschen zu platzieren.

Charles Petzold schreibt seit langem redaktionelle Beiträge für das MSDN Magazin*. Sein neuestes Buch heißt „The Annotated Turing: A Guided Tour Through Alan Turing’s Historic Paper on Computability and the Turing Machine“ (Wiley, 2008). Petzold veröffentlicht einen Blog auf seiner Website charlespetzold.com.*

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Arathi Ramani und dem WPF Layout-Team