Layout der WPF-Statuszeile

Standard

In meinem letzten Artikel habe ich gezeigt, wie man eine einfache Benutzeroberfläche für einen Texteditor mit WPF entwerfen kann. Aber die Statuszeile sieht noch nicht ganz so aus, wie sie soll.

Die Zeilen- und Spaltennummer, in der sich die Einfügemarke momentan befindet, soll ca. im rechten Drittel der Statuszeile angezeigt werden, auch wenn sich die Fenstergröße ändert.

Statuszeile klein

Statuszeile groß

Beim letzten Mal hatten wir die Statuszeile noch so definiert:

        ...
        <StatusBar DockPanel.Dock="Bottom">
            <TextBlock Text="Col 1, Ln 1" />
        </StatusBar>
        ...

Das sieht so aus:

Statuszeile Texteditor

Ein TextBlock-Element befindet sich in der Statuszeile, das links angezeigt wird. Doch wie bekommen wir den Text an die gewünschte Position?

Die StatusBar stellt ihre Steuerelemente normalerweise mit einem DockPanel dar. Allerdings werden alle Steuerelemente, die nicht vom Typ StatusBarItem sind, in einem StatusBarItem-Element eingehüllt, d.h. wir müssen selbst das StatusBarItem-Element angeben, damit wir die DockPanel-Eigenschaften definieren können.

        ...
        <StatusBar DockPanel.Dock="Bottom">
            <StatusBarItem DockPanel.Dock="Right">
                <TextBlock Text="Col 1, Ln 1" />
            </StatusBarItem>
            <StatusBarItem/>
        </StatusBar>
        ...

Statuszeile DockPanel

Der Text wird jetzt zwar rechts dargestellt, aber noch nicht genau dort, wo wir ihn haben wollen. Außerdem mussten wir ein leeres StatusBarItem-Element hinzufügen, das den Hauptteil der Statuszeile ausfüllen soll. Andernfalls würde das StatusBarItem-Element mit dem TextBlock nicht rechts dargestellt. Würden wir einen zweiten Text darstellen wollen, müssten wir es zum leeren StatusBarItem hinzufügen. Somit käme der linke Teil im XAML-Code vor dem rechten. Viel lieber hätten wir an dieser Stelle ein Grid, weil wir dann die Breite der Spalten selbst bestimmen können. Aber wie erreichen wir das?

Die StatusBar gehört wie ListBox, ListView, ComboBox, Menu und einigen anderen zu den sogenannten Items-Controls. Diese stellen ihre Subelemente in einem Panel dar, das durch die Eigenschaft ItemsPanel definiert wird. Normalerweise wird darin ein StackPanel definiert, aber StatusBar hat diese Eigenschaft überschrieben und durch ein DockPanel ersetzt, wie wir oben gesehen haben. Wir können dieser Eigenschaft ein Grid zuweisen und so zu dem gewünschten Ergebnis kommen. In XAML sieht das so aus:

        <StatusBar DockPanel.Dock="Bottom">
            <StatusBar.ItemsPanel>
                <ItemsPanelTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="2*"/>
                            <ColumnDefinition Width="1*"/>
                        </Grid.ColumnDefinitions>
                    </Grid>
                </ItemsPanelTemplate>
            </StatusBar.ItemsPanel>
            <StatusBarItem Grid.Column="1" Grid.Row="0">
                <TextBlock Text="Col 1, Ln 1" />
            </StatusBarItem>
        </StatusBar>

Die Eigenschaft bekommt ein ItemsPanelTemplate, in dem wir mit einem Grid ein Layoutgitter definieren, das aus einer Zeile und zwei Spalten besteht. Die Spalten teilen sich den verfügbaren Platz im Verhältnis 2:1 auf. Die rechte Spalte umfasst also das rechte Drittel der Statuszeile. Den TextBlock geben wir in das StatusBarItem, das wir der rechten Spalte zuweisen. Damit befindet sich der Text immer im rechten Drittel der Statuszeile.

Die WPF-Statuszeile kann auch andere Steuerelemente darstellen, z.B. Bilder, Fortschrittsbalken und viele andere mehr. Wenn man dann mehr Kontrolle über das Layout der Statuszeile braucht, kann das DockPanel jederzeit durch etwas anderes ersetzt werden.

.NET TDD Roundup #1

Standard

This is .NET TDD roundup #1. The .NET TDD roundup aggregates information of interest to .NET developers using TDD/BDD. If you have something interesting you have done or have run across, please send me the URL with a brief description via the contact link on my blog.

Test-Driven Development

Unit Test Frameworks

NUnit

MbUnit

  • XML Assertions in MbUnit v3
    • Yann Trévin writes about new assertions in the upcoming MbUnit 3.2 which will help with testing XML output.

Benutzeroberfläche eines Texteditors mit WPF

Standard

In meinem letzten Artikel habe ich einen Überblick über den Layoutmechanismus in WPF gegeben. In diesem Artikel möchte ich zeigen, wie man eine Benutzeroberfläche für einen Texteditor mit WPF layouten kann.

Ziel

In diesem Artikel möchte ich zeigen, wie man mit dem WPF-Layoutmechanismus eine GUI für einen Notepad-ähnlichen Texteditor entwerfen kann. Das Ergebnis wird in etwa so aussehen:

Texteditor Ergebnis

Oft gibt es mehr als eine Möglichkeit, ein Ziel zu erreichen. So auch hier: in der ersten Variante verwende ich ein Grid und in der zweiten Variante ein DockPanel.

Variante mit Grid

Visual Studio erstellt ein Window immer mit einem Grid:

    <Window x:Class="TextEditor.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
     
        </Grid>
    </Window>

Zuerst müssen wir die Spalten und Zeilen definieren, die das Layoutgitter haben soll, dann können wir die Steuerelemente hinzufügen und einzelnen Gitterzellen zuweisen.

    <Window x:Class="TextEditor.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
     
            <Menu Grid.Column="0" Grid.Row="0" />
            <TextBox Grid.Column="0" Grid.Row="1" />
            <StatusBar Grid.Column="0" Grid.Row="2" />
        </Grid>
    </Window>

Wir definieren drei Zeilen und eine Spalte. Jedes Steuerelement kommt in eine eigene Zeile. Das Ergebnis sieht dann so aus:

Layout mit Grid

Wie wir sehen, sieht das Ergebnis noch gar nicht nach Texteditor aus. Drei gleich große Layoutgitterzeilen beinhalten das Menü, die TextBox und die Statuszeile.

Per Voreinstellung bekommt jede Zeile die Höhe “1*” zugewiesen, d.h. die Höhe wird im Verhältnis 1:1:1 aufgeteilt. Daher sind die drei Zeilen gleich hoch.

Wenn wir die Zeilendefinitionen so abändern, dass die erste Zeile (Menü) und die dritte Zeile (Statuszeile) eine fixe Höhe bekommen und die zweite Zeile (TextBox) den Rest bekommt, dann kommen wir dem Ziel etwas näher.

    ...
            <Grid.RowDefinitions>
                <RowDefinition Height="23"/>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="26"/>
            </Grid.RowDefinitions>
    ...

Die erste Zeile setzen wir auf 23 Pixel und die letzte Zeile auf 26 Pixel. Die zweite Zeile bekommt den Rest. Die Höhenangabe für die zweite Zeile kann weggelassen werden, weil “1*” der voreingestellte Wert ist.

Dem Menü fehlen noch die Hauptmenüpunkte und die Textbox akzeptiert noch nicht die Eingabetaste. Mit folgenden Änderungen sieht unser Fenster beinahe wie ein richtiger Texteditor aus:

    <Window x:Class="TextEditor.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="23"/>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="26"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
     
            <Menu Grid.Column="0" Grid.Row="0">
                <MenuItem Header="_Datei" />
                <MenuItem Header="_Bearbeiten" />
                <MenuItem Header="_Format"/>
                <MenuItem Header="_Ansicht"/>
                <MenuItem Header="_Hilfe"/>
            </Menu>
            <TextBox Grid.Column="0" Grid.Row="1" AcceptsReturn="True" />
            <StatusBar Grid.Column="0" Grid.Row="2" />
        </Grid>
    </Window>

Die WPF-TextBox zeigt mehrzeilige Texte immer mehrzeilig an, aber per Voreinstellung akzeptiert sie die Eingabetaste nicht. Mit der Eigenschaft AcceptsReturn kann dieses Verhalten geändert werden.

Wenn der Benutzer jetzt einen längeren Text in die TextBox eingibt oder einfügt, sollten Schiebeleisten erscheinen. Unser Fenster verhält sich leider noch nicht so. Wenn wir die TextBox in ein ScrollViewer-Element einbetten, erhalten wir das gewünschte Verhalten:

    <Window x:Class="TextEditor.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="23"/>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="26"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
     
            <Menu Grid.Column="0" Grid.Row="0">
                <MenuItem Header="_Datei" />
                <MenuItem Header="_Bearbeiten" />
                <MenuItem Header="_Format"/>
                <MenuItem Header="_Ansicht"/>
                <MenuItem Header="_Hilfe"/>
            </Menu>
            <ScrollViewer Grid.Column="0" Grid.Row="1"
                          HorizontalScrollBarVisibility="Auto"
                          VerticalScrollBarVisibility="Auto">
                <TextBox AcceptsReturn="True" />
            </ScrollViewer>
            <StatusBar Grid.Column="0" Grid.Row="2" />
        </Grid>
    </Window>

Da jetzt ScrollViewer das Kindelement des Grids ist, muss die Zuweisung zu einer Layoutgitterzelle für das ScrollViewer-Element erfolgen. Die TextBox braucht dann keine Zuweisung mehr.

Mit den Eigenschaften HorizontalScrollBarVisibility und VerticalScrollBarVisibility können wir steuern, wann die Schiebeleisten erscheinen.

Variante mit DockPanel

Das Ziel kann auch mit einem DockPanel erreicht werden. Tauschen wir mal das Grid durch ein DockPanel aus und entfernen die Zeilen- und Spaltendefinitionen und die Zuordnung zu den Layoutgitterzellen:

    <Window x:Class="TextEditor.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <DockPanel>
            <Menu DockPanel.Dock="Top">
                <MenuItem Header="_Datei" />
                <MenuItem Header="_Bearbeiten" />
                <MenuItem Header="_Format"/>
                <MenuItem Header="_Ansicht"/>
                <MenuItem Header="_Hilfe"/>
            </Menu>
            <ScrollViewer HorizontalScrollBarVisibility="Auto"
                          VerticalScrollBarVisibility="Auto">
                <TextBox AcceptsReturn="True" />
            </ScrollViewer>
            <StatusBar DockPanel.Dock="Bottom" />
        </DockPanel>
    </Window>

Das Ergebnis sieht noch nicht so aus, wie wir wollen.

Layout mit DockPanel

Das Problem ist, dass bei einem DockPanel jenes Steuerelement, das den Rest der Fläche ausfüllen soll, als letztes angegeben werden muss. Das ist bei uns der ScrollViewer mit der TextBox. Außerdem gibt das DockPanel jedem Element gerade soviel Platz, wie es braucht, um den Inhalt darzustellen. Daher sollten wir etwas in der Statuszeile anzeigen, damit das DockPanel die Statuszeile überhaupt darstellt:

    <Window x:Class="TextEditor.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <DockPanel>
            <Menu DockPanel.Dock="Top">
                <MenuItem Header="_Datei" />
                <MenuItem Header="_Bearbeiten" />
                <MenuItem Header="_Format"/>
                <MenuItem Header="_Ansicht"/>
                <MenuItem Header="_Hilfe"/>
            </Menu>
            <StatusBar DockPanel.Dock="Bottom">
                <TextBlock Text="Col 1, Ln 1" />
            </StatusBar>
            <ScrollViewer HorizontalScrollBarVisibility="Auto"
                          VerticalScrollBarVisibility="Auto">
                <TextBox AcceptsReturn="True" />
            </ScrollViewer>
        </DockPanel>
    </Window>

Das sieht doch gleich besser aus.

Endergebnis

Zusammenfassung

Wir kamen sowohl mit dem Grid als auch mit dem DockPanel an unser Ziel. Beide Varianten haben Vor- und Nachteile.

Beim Grid mussten wir die Definitionen für die Zeilen und Spalten und an den Steuerelementen sowohl die Spalte als auch die Zeile angeben, obwohl unser Layout nur eine Spalte hatte.

Das DockPanel hatte den Nachteil, dass die Reihenfolge geändert und daher die Statuszeile im XAML-Code vor der TextBox angegeben werden musste. Außerdem mussten alle Steuerelemente einen Inhalt haben, damit sie überhaupt angezeigt wurden.

Steuerelemente und Panels in WPF

Standard

Layouting in WPF ist viel flexibler und mächtiger als in Windows Forms, aber auch viel komplexer. In Windows Forms gibt es Anchoring und Docking, um das Verhalten der Steuerelemente bei Änderung der Fenstergröße zu steuern. In WPF beeinflussen viele Faktoren die Anordnung der Steuerelemente, einer davon sind die Panel-Elemente.

In der MSDN-Library findet sich eine Beschreibung des WPF-Layoutsystems. Die wichtigsten Panel-Elemente sind:

Canvas Definiert einen Bereich, in dem Elemente explizit mit Koordinaten
relativ zu diesem Bereich positioniert werden.
StackPanel Die Elemente werden entweder horizontal oder vertikal in einer Linie
angeordnet.
DockPanel Die Elemente werden an den Rändern positioniert oder füllen den inneren Restbereich.
WrapPanel Die Elemente werden sequentiell von links nach rechts angeordnet und in
die nächste Zeile umgebrochen, wenn am Ende einer Zeile kein Platz mehr
ist.
Grid Definiert einen flexiblen Gitterbereich, der aus Spalten und Zeilen
besteht.

Mehr Informationen über die verschiedenen Panel-Elemente gibt es im Artikel Panel Overview.

Nicht nur die richtige Auswahl des Panel-Elements beeinflusst das Layout, einige Steuerelement-Eigenschaften haben ebenfalls Auswirkungen auf die Anordnung der Steuerelemente. Die wichtigsten Eigenschaften mit Einfluss auf das Layout sind:

HorizontalAlignment Definiert, wie das Element horizontal innerhalb des Parent-Elements
positioniert werden soll. Für diese Eigenschaft gibt es vier mögliche
Werte: *Left*, *Center*, *Right*, *Stretch*.
VerticalAlignment Definiert, wie das Element vertikal innerhalb des Parent-Elements
positioniert werden soll. Für diese Eigenschaft gibt es vier mögliche
Werte: *Top*, *Center*, *Bottom*, *Stretch*.
Margin Definiert den Abstand zwischen dem Element und dem Elternelement oder
den Geschwisterelementen.
Padding Definiert den Abstand zwischen dem Rand eines Steuerelements und seinen
Kindelementen.

Detailliertere Informationen über den Bedeutung dieser Eigenschaften gibt es in der MSDN-Library.

Nicht jede Eigenschaft ist in jedem Panel sinnvoll. Folgende Tabelle zeigt die Wirkung bestimmter Kombinationen von Eigenschaften und Panel-Elementen:

Canvas Margin Teilweise: An den zwei Seiten, die für die Positionierung relevant sind,
werden die entsprechenden Elemente des Margin-Werts hinzuaddiert.
HorizontalAlignment Nein
VerticalAlignment Nein
StackPanel Margin Ja
HorizontalAlignment Teilweise: Nur, wenn Orientation Vertical ist.
VerticalAlignment Teilweise: Nur, wenn Orientation Horizontal ist.
DockPanel Margin Ja
HorizontalAlignment Teilweise: Wenn Dock den Wert Left oder Right hat, ist diese Eigenschaft
sinnlos.
VerticalAlignment Teilweise: Wenn Dock den Wert Top oder Bottom hat, ist diese Eigenschaft
sinnlos.
WrapPanel Margin Ja
HorizontalAlignment Teilweise: Wenn Orientation Vertical ist oder ItemWidth genug Extraraum
lässt.
VerticalAlignment Teilweise: Wenn Orientation Horizontal ist oder ItemHeight genug
Extraraum lässt.
Grid Margin Ja
HorizontalAlignment Ja
VerticalAlignment Ja

Welche Vorteile hat der WPF-Layoutmechanismus, der im Vergleich zu Windows Forms viel komplexer ist?

  1. Der Entwickler kann besser kontrollieren, wie sich die Anordnung der Steuerelemente verhalten soll, wenn der Benutzer die Größe des Fensters ändert. Das konnte aber bereits in Windows Forms mit dem Anchoring oder dem Docking erreicht werden.
  2. Die Steuerelemente können ihre Größe und Position automatisch anpassen, wenn bei einer lokalisierten Anwendung die Texte der Übersetzungen länger sind. Darüber gibt es wieder einen Artikel in der MSDN-Library.

Es gibt weitere Panel-Elemente, die ich in diesem Artikel nicht berücksichtigt habe. Diese sind zum größten Teil nur für die Entwicklung eigener Steuerelemente relevant. Außerdem können eigene Panel-Elemente entwickelt werden, wenn die Vorgefertigten nicht ausreichen.