n00b pro

08. layout

Dit hoofdstuk is onderdeel van de cursus C#. Andere cursussen in dezelfde reeks: HTML, CSS, Javascript, Ontwikkelomgeving.

Youtube: WPF layout

https://www.youtube.com/watch?v=B6VQzjlFd70

Vensters

Een nieuw WPF project heeft standaard een 800x450 Window (met daarin een Grid control):

<Window x:Class="WpfTest.MainWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:local="clr-namespace:WpfTest"
      mc:Ignorable="d"
      Title="MainWindow" Height="450" Width="800">
   <Grid>
   </Grid>
</Window>

Properties en methodes

Property Omschrijving
Height hoogte van het venster
MaxHeight maximumhoogte van het venster (indien herschaalbaar)
MaxWidth maximumbreedte van het venster (indien herschaalbaar)
MinHeight minimumhoogte van het venster (indien herschaalbaar)
MinWidth minimumbreedte van het venster (indien herschaalbaar)
ResizeMode hoe kan het venster herschalen, b.v. niet herschaalbaar: NoResize, CanResize...
Title titel in de titelbalk bovenaan
Width breedte van het venster
WindowStyle stijl van het venster, b.v. vereenvoudigd venster: ToolWindow
Property Omschrijving
Open() open een venster
Close() sluit een venster

Voorbeelden

Minimum en maximum afmetingen

Beperk de afmetingen met Width, Height, MinWidth, MinHeight, MaxWidth en MaxHeight:

<Window x:Class="WpfTest.MainWindow"
   ...
   Title="Test Window" Height="250" Width="400" MinHeight="250" MinWidth="400" MaxHeight="450" MaxWidth="800">
   <Grid>
   </Grid>
</Window>
venster is nu maar beperkt aanpasbaar in breedte/hoogte

Niet herschaalbaar maken

Niet herschaalbaar venster met ResizeMode="NoResize":

<Window x:Class="WpfTest.MainWindow"
   ...
   Title="Test Window" Height="250" Width="400" ResizeMode="NoResize">
   <Grid>
   </Grid>
</Window>

Vereenvoudigd

Maak een vereenvoudigd venster zonder icoon linksboven of minimize knoppen rechtsboven met WindowStyle="ToolWindow":

<Window x:Class="WpfTest.MainWindow"
   ...
   Title="Test Window" Height="250" Width="400" Height="250" Width="400" WindowStyle="ToolWindow">
   <Grid>
   </Grid>
</Window>
vereenvoudigd venster

Nieuw openen

Een nieuw venster openen kan met de Show() methode. Nemen we als voorbeeld een PopupWindow; voeg het toe aan je project via rechtermuisknop add, Window...:

Voeg dan b.v. een Button toe aan MainWindow, waaromee je de PopupWindow opent:

<Window x:Class="WpfTest.MainWindow" ...>
   <Grid>
      <Button Padding="10,5" Content="open popup" Click="BtnOpenPopup_Click" .../>
   </Grid>
</Window>

De event handler in code-behind:

private void BtnOpenPopup_Click(object sender, RoutedEventArgs e)
{
   new MyPopupWindow().Show();
}

Voor de popup window is een niet herschaalbaar vereenvoudigd venster ideaal:

<Window x:Class="WpfTest.PopupWindow" Height="150" Width="300" WindowStyle="ToolWindow" ResizeMode="NoResize" ...>
    <Grid>
    </Grid>
</Window>

Je kan bij het openen gegevens als b.v. gebruikersnaam meegeven van MainWindow naar PopupWindow door de constructor van die laatste uit te breiden met een parameter.

In PopupWindow.xaml.cs:

public MyPopupWindow(string name) // breid uit met "name" parameter
{
   InitializeComponent();
   lblWelcome.Content = $"hello {name}";
}

In MainWindow.xaml.cs:

private void BtnOpenPopup_Click(object sender, RoutedEventArgs e) {
    new MyPopupWindow("Rogier").Show(); // geef waarde voor "name" mee
}

Sluiten

Maak b.v. een button om het venster te sluiten

<Button Content="sluiten" Click="BtnClose_Click" .../>

De event handler in code-behind:

private void BtnClose_Click(object sender, RoutedEventArgs e)
{
   this.Close();
}

Positionering van controls

Margin en Padding

Slepen we in designer een Button control op de grid, en voegen we wat Padding toe:

<Grid>
   <Button Content="Button" Margin="35,48,10,20" Padding="10,5,10,5" HorizontalAlignment="Left" VerticalAlignment="Top" />
</Grid>
volgorde van margins en paddings in WPF: links, boven, rechts, onder

VerticalAlignment en HorizontalAlignment

De attributen VerticalAlignment en HorizontalAlignment bepalen hoe de control gepositioneerd wordt t.o.v. de parent. Als afstanden worden de marges genomen. Stel je ze b.v. in op Right resp. Bottom, dan worden de rechter- en onderwaarden van Margin genomen:

<Grid>
   <Button Content="Button" Margin="35,48,10,20" Padding="10,5" HorizontalAlignment="Right" VerticalAlignment="Bottom" />
</Grid>
rechstonder gepositioneerd

De standaardwaarden voor VerticalAlignment en HorizontalAlignment zijn Stretch, dus als je het weglaat wordt de control uitgerokken, b.v. VerticalAlignment weglaten rekt verticaal uit:

<Grid>
   <Button Content="Button" Margin="35,48,10,20" Padding="10,5" HorizontalAlignment="Left" />
</Grid>
zonder VerticalAlignment: uitgerokken
→ speficieer dus altijd waarden voor VerticalAlignment en HorizontalAlignment voor elke control!

VerticalContentAlignment en HorizontalContentAlignment

De attributen VerticalContentAlignment en HorizontalContentAlignment bepalen de alignering van de content van de control. De XAML voor twee tekstvakken, verticaal gecentreerd en horizontaal links resp. rechts:

<TextBox Text="tekst links..." HorizontalContentAlignment="Left" VerticalContentAlignment="Center" ... />
<TextBox Text="tekst rechts..." HorizontalContentAlignment="Right" VerticalContentAlignment="Center"... />
verticaal gecentreerd, horizontaal links resp. rechts

WPF panels

overzicht

Control Omschrijving Voorbeeld
Canvas positionering met coördinaten, al dan niet overlappend
DockPanel control waarbinnen panels aan de vier zijden gedocked ("gekleefd") kunnen worden
Grid rijen en kolommen waarbinnen controls geplaatst worden
goede controle over breedtes/hoogtes rijen en kolommen
goede controle over positionering items binnen de cellen
StackPanel eenvoudige stapeling per rij of kolom zonder wrapping
alle items worden tegen elkaar gestapeld, dus "spreiden" is niet mogelijk
WrapPanel als StackPanel, maar met wrapping

Canvas

Canvas wordt hoofdzakelijk gebruikt om (vooral grafische) elementen een vaste plaats te geven. De positie bepaal je met de properties Canvas.Left, Canvas.Top enz...; voor de stapelvolgorde gebruik je Canvas.ZIndex. Een voorbeeld:

<Canvas Name="cvCanvas">
   <Ellipse Fill="Gainsboro" Canvas.Left="25" Canvas.Top="25" Width="200" Height="200" Panel.ZIndex="1" />
   <Rectangle Fill="LightBlue" Canvas.Left="87" Canvas.Top="40" Width="50" Height="50"  Panel.ZIndex="3" />
   <Rectangle Fill="LightCoral" Canvas.Left="50" Canvas.Top="50" Width="50" Height="50" Panel.ZIndex="4" />
   <Rectangle Fill="LightCyan" Canvas.Left="74" Canvas.Top="75" Width="100" Height="100" Panel.ZIndex="2" />
</Canvas>
canvas voorbeeld

DockPanel

Docking is een term gebruikt voor waar en hoe child elements “plakken” in een venster dat van grootte kan veranderen. Een DockPanel wordt typisch gebruikt voor de globale layout van een Windows venster. Een voorbeeld:

<DockPanel LastChildFill="True"> <!-- LastChildFill: laatste element vult resterende middenruimte -->
   <TextBox DockPanel.Dock="Top" Text="Top" />
   <TextBox DockPanel.Dock="Bottom" Text="Bottom" />
   <TextBox DockPanel.Dock="Left" Text="Left1" />
   <TextBox DockPanel.Dock="Left" Text="Left2" />
   <TextBox DockPanel.Dock="Right" Text="Right" />
   <TextBox Text="Center" AcceptsReturn="True" TextWrapping="Wrap" />
</DockPanel>
dockpanel voorbeeld

Grid

Bij Grid kan je een grid van rijen en kolommen definiëren met Grid.ColumnDefintions en Grid.RowDefinitions. Daarna plaats je elementen in de juiste rij/kolom met Grid.Row en Grid.Column.

<Grid Margin="10">
   <Grid.ColumnDefinitions>
      <ColumnDefinition Width="80" />
      <ColumnDefinition Width="*" />
   </Grid.ColumnDefinitions>
   <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
   </Grid.RowDefinitions>
   <Label>Name:</Label>
   <TextBox Grid.Column="1" Margin="0,5,0,10" />
   <Label Grid.Row="1">E-mail:</Label>
   <TextBox Grid.Row="1" Grid.Column="1" Margin="0,5,0,10" />
   <Label Grid.Row="2">Comment:</Label>
   <TextBox Grid.Row="2" Grid.Column="1" Margin="0,5,0,10" AcceptsReturn="True" />
</Grid>

De Width waarden van kolommen worden als volgt verwerkt (hetzelfde principe geldt voor Height waarden van rijen):

  1. teken eerst kolommen met vaste breedtes
  2. teken dan kolommen met breedte Auto, waarbij ze niet meer ruimte innemen dan hun content nodig heeft
  3. verdeel de resterende waarden proportioneel; als er b.v. twee kolommen 2* en 3* zijn, krijgt de eerste kolom 2/5 en de tweede 3/5
Voorbeeld Betekenis Anders gezegd...
Width="80" exact 80 px breed vaste waarde
Width="Auto" neem niet meer ruimte dan nodig fit content
Width="3*" neemt een gewicht 3 resterende ruimte
Width="*" (standaardwaarde) is hetzelfde als Width="1*" resterende ruimte
grid voorbeeld

StackPanel

Bij StackPanel stapel je controls in één rij of kolom:

<StackPanel Orientation="Horizontal">
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">1</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">2</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">3</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">4</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">5</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">6</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">...</TextBlock>
</StackPanel>
stackpanel voorbeeld

Om verticaal te stapelen gebruik je Orientation="Vertical":

<StackPanel Orientation="Vertical">
   ...
</StackPanel>
      
StackPanel verticaal voorbeeld

WrapPanel

Bij WrapPanel stapel je elementen in meerdere rijen of kolommen:

<WrapPanel>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">1</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">2</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">3</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">4</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">5</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">6</TextBlock>
   <TextBlock FontSize="48" Width="100" Height="70" TextAlignment="Center" Background="LightGray" Margin="10,10,10,10">...</TextBlock>
</WrapPanel>
stackpanel voorbeeld

Je kan ook stapelen van rechts naar links met FlowDirection="RightToLeft", maar dat zijn de enige aligneermogelijkheden. Items centraal plaatsen of ver uit elkaar zetten kan bijvoorbeeld niet, zoals met CSS FlexBox wel kan. Voor complexere layouts gebruik je best altijd Grid.

Om verticaal te stapelen gebruik je Orientation="Vertical":

<WrapPanel Orientation="Vertical">
   ...
</WrapPanel>
      
WrapPanel verticaal voorbeeld

UI componenten

overzicht

Control Omschrijving Voorbeeld
Menu een klassiek Windows menu
ScrollViewer voeg scrollbars toe aan panels en andere controls
StatusBar een statusbar onderaan het venster
Tabcontrol een control met tabs

Een menu bouw je met Menu en MenuItem controls, typisch in combinatie met een DockPanel:

<DockPanel LastChildFill="True">
   <Menu DockPanel.Dock="Top">
      <MenuItem Header="_File">
         <MenuItem Header="_Open..." />
         <MenuItem Header="_Save" />
         <MenuItem Header="Save _As..." />
         <Separator />
         <MenuItem Header="E_xit" Click="ExitItem_Click" />
      </MenuItem>
      <MenuItem Header="_Help">
         <MenuItem Header="_About" />
      </MenuItem>
   </Menu>
   <Grid></Grid>
</DockPanel>

→ de underscore in de XAML duidt aan welke letter als shortcut gebruikt wordt

Voorbeeld van een event handler voor het exit menu item:

private void ExitItem_Click(object sender, RoutedEventArgs e)
{
   // 0 wilt zeggen dat er niets is fout gelopen
   Environment.Exit(0);
}
Menu voorbeeld

ScrollViewer

Sommige controls als ListBox hebben van zichzelf scrollbars, sommige controls als TextBox of panels niet. Ze scrollbars geven doe je — misschien wat vreemd — niet met een property, maar met een ScrollViewer control:

  1. wrap de control in een <ScrollViewer> ... </ScrollViewer>
  2. verhuis layout properties als Height, HorizontalAlignment, Margin, VerticalAlignment, Width enz... van de control naar de ScrollViewer

Een TextBox zonder scrollbars:

<TextBox
   x:Name="txt1"
   AcceptsReturn="True"
   Height="150"
   HorizontalAlignment="Left"
   Margin="35,39,0,0"
   Padding="20,10,20,30"
   Text="Bacon ipsum dolor amet prosciutto tail fatback, pancetta sausage meatball swine rump salami pig alcatra ..."
   TextWrapping="Wrap"
   VerticalAlignment="Top"
   Width="200"
/>
      
TextBox zonder ScrollViewer

Dezelfde TextBox met scrollbars:

<ScrollViewer
   Height="150"
   HorizontalAlignment="Left"
   Margin="35,39,0,0"
   VerticalAlignment="Top"
   Width="200">
   <TextBox
      x:Name="txt1"
      AcceptsReturn="True"
      Padding="20,10,20,30"
      Text="Bacon ipsum dolor amet prosciutto tail fatback, pancetta sausage meatball swine rump salami pig alcatra ..."
      TextWrapping="Wrap"
   />
</ScrollViewer>
TextBox met ScrollViewer

StatusBar

Een StatusBar gebruik je typisch in combinatie met een DockPanel, onderaan gedocked:

<DockPanel LastChildFill="True">
   ...
   <StatusBar DockPanel.Dock="Bottom">
      <StatusBarItem Padding="10,5">
         <TextBlock Text="all ready" FontSize="10" />
      </StatusBarItem>
   </StatusBar>
   <Grid></Grid>
</DockPanel>
StatusBar voorbeeld

TabControl

Een TabControl voorbeeld:

<TabControl SelectedIndex="2" >
   <TabItem Header="Tab 1" Margin="0" Padding="10,5">
      <TextBox Padding="10">tab 1 content here...</TextBox>
   </TabItem>
   <TabItem  Header="Tab 2" Margin="0" Padding="10,5">
      <TextBox Padding="10">tab 2 content here...</TextBox>
   </TabItem>
   <TabItem Header="Tab 3" Margin="0" Padding="10,5">
      <TextBox Padding="10">tab 3 content here...</TextBox>
   </TabItem>
   <TabItem Header="Tab 4" Margin="0" Padding="10,5">
      <TextBox Padding="10">tab 4 content here...</TextBox>
   </TabItem>
</TabControl>
TabControl voorbeeld

Page/Frame

In WPF kan je werken met meerdere “schermen” zonder meerdere vensters te openen. Dat doe je met een Frame in je MainWindow, waarin je verschillende Page-objecten laadt.

Typische toepassingen: adminpanelen, wizards, ...

Download de volledige code van de uitleg hieronder: SlnDemoFrame.zip

Frame control in MainWindow

Plaats in MainWindow een Frame; dat is de “placeholder” waarin pagina’s geladen worden. We voegen meteen navigatiebuttons en click event handlers toe:

<Grid Background="#FFF3EBEB">
   <Grid.ColumnDefinitions>
       <ColumnDefinition Width="150" />
       <ColumnDefinition Width="1*" />
   </Grid.ColumnDefinitions>
   <Frame x:Name="frmMain" Grid.Column="2" NavigationUIVisibility="Hidden" Margin="10" Background="White"/>
   <StackPanel Margin="20,10">
      <Button x:Name="btnHome" Content="🏠 Home" Padding="5" Margin="0,0,0,20" Click="BtnHome_Click" />
      <Button x:Name="btnProducts" Content="🛒 Producten" Padding="5" Margin="0,0,0,20" Click="BtnProducts_Click" />
      <Button x:Name="btnAbout" Content="💡Over" Padding="5" Margin="0,0,0,20" Click="BtnAbout_Click" />
   </StackPanel>
</Grid>
MainWindow met Frame en drie Buttons

Met NavigationUIVisibility="Hidden" verberg je de standaard navigatiebalk, die er wat ouderwets uitziet.

Pages toevoegen aan project

Voeg elke pagina toe via Add, Page (WPF)..., bijvoorbeeld in een nieuwe submap Pages:

Page toevoegen

Doen we dit met HomePage, ProductsPage en AboutPage:

elke Page heeft een eigen .xaml en code-behind

Om de code georganiseerd te houden, gebruik je best een submap Pages en geef je elke Page de suffix “Page”, dus b.v. HomePage, ProductsPage en AboutPage.

XAML code van ProductPage (de andere pagina's zijn gelijkaardig):

<Page x:Class="WpfDemoFram.Pages.ProductsPage"
   ...			
   Title="ProductsPage">
   <Grid Margin="20">
      <Grid.RowDefinitions>
         <RowDefinition Height="auto" />
         <RowDefinition Height="*" />
         <RowDefinition Height="auto" />
      </Grid.RowDefinitions>
      <TextBlock FontSize="28" FontWeight="Bold" Margin="0,0,0,20">Productpagina</TextBlock>
      <TextBlock Grid.Row="1">
         Productpagina content hier...
      </TextBlock>
      <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
         <Button x:Name="btnPrev" Padding="10,5" Margin="20,10,0,10" Click="btnPrev_Click">&lt;&lt; vorige</Button>
         <Button x:Name="btnNext" Padding="10,5" Margin="20,10,0,10" Click="btnNext_Click">volgende &gt;&gt;</Button>
      </StackPanel>
   </Grid>
</Page>

Navigatie naar een Page

Vanuit MainWindow gebruik Content property van Frame:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        frmMain.Content = new HomePage();
    }
    private void BtnHome_Click(object sender, RoutedEventArgs e)
    {
        frmMain.Content = new HomePage();
    }
    private void BtnProducts_Click(object sender, RoutedEventArgs e)
    {
        frmMain.Content = new ProductsPage();
    }
    private void BtnAbout_Click(object sender, RoutedEventArgs e)
    {
        frmMain.Content = new AboutPage();
    }
}

Vanuit een Page, bv ProductsPage, gebruik navigationService.Navigate(...):

public partial class ProductsPage : Page
{
   public ProductsPage()
   {
      InitializeComponent();
   }
   private void btnPrev_Click(object sender, RoutedEventArgs e)
   {
      this.NavigationService.Navigate(new HomePage());
   }
   private void btnNext_Click(object sender, RoutedEventArgs e)
   {
      this.NavigationService.Navigate(new AboutPage());
   }
}

Gegevens delen tussen Pages

Er zijn veel manieren om gegevens te delen tussen Pages en Windows, maar de eenvoudigste manier is door de constructor van Page uit te breiden met een parameter. Aanpassing van de code-behind van MainWindow:

public partial class MainWindow : Window
{
   protected const string Username = "Stan"; // dit wordt gedeeld met de pages
   public MainWindow()
   {
      InitializeComponent();
      frmMain.Content = new HomePage(Username); // geef mee met de constructor van de page
   }
   ...
}

Aanpassing van de code-behind van b.v. de HomePage:

public partial class HomePage : Page
{
   private string userName; // lokale hulpvariabele
   public HomePage(string uName) // breid constructor uit met een parameter
   {
      InitializeComponent();
      this.userName = uName; // kopieer lokaal
      txtContent.Text = $"Hallo {this.userName}! Dit is de home pagina.";
   }
   ...
}

Eindresultaat

Animatie van het eindresultaat:

Download de volledige code: SlnDemoFrame.zip