-
Notifications
You must be signed in to change notification settings - Fork 24
layout
Right now, our layout is a bit ugly minimalistic. Let's see how to improve your application's aesthetics.
Note: how nice your GUI looks does not matter for this course. The purpose of this section is to introduce new concepts, such as the different panels, property element syntax and attached properties.
Right now, you should have two text boxes. One shows a temperature in °C, the other in °F, but it is definitely not clear which text box corresponds to which temperature scale. Let's add labels.
<Window x:Class="View.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:View"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
+ <TextBlock Text="Celsius" />
<TextBox x:Name="celsiusTextBox" />
+ <TextBlock Text="Fahrenheit" />
<TextBox x:Name="fahrenheitTextBox" />
<Button Content="Convert to Celsius" Click="ConvertToCelsius" />
<Button Content="Convert to Fahrenheit" Click="ConvertToFahrenheit" />
</StackPanel>
</Window>
A TextBlock
, not to be confused with TextBox
, can be used to add text to your GUI. We can make it stand out a bit more:
<Window x:Class="View.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:View"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
- <TextBlock Text="Celsius" />
+ <TextBlock Text="Celsius" Background="#AAA" />
<TextBox x:Name="celsiusTextBox" />
- <TextBlock Text="Fahrenheit" />
+ <TextBlock Text="Fahrenheit" Background="#AAA" />
<TextBox x:Name="fahrenheitTextBox" />
<Button Content="Convert to Celsius" Click="ConvertToCelsius" />
<Button Content="Convert to Fahrenheit" Click="ConvertToFahrenheit" />
</StackPanel>
</Window>
Snapshot tag: pre-properties-exercise
Find a way to
- put the text blocks' text in bold
- add some padding (2) to the text blocks
- increase both the text blocks' and text boxes' font size to 16
- have the text boxes center their temperature values
Snapshot tag: post-properties-exercise
Say we want to give the text blocks a nicer background, like a nice gradient effect that goes from gray to white. WPF offers many options, among which:
- Linear gradients smoothly change from one color to another along an axis of your own choice: vertically, horizontally, diagonally, …
- Circular gradients
- Multiple colors in the gradient, e.g., from blue to red to green to yellow
There are many possibilities, but how would we fit all that information in a Background="..."
property? It would become quite a complicated string.
Fortunately, these is a cleaner way. First, you need to know that XAML does nothing magical: it merely describes which objects to create. You can create a whole GUI without any XAML, instead creating every control in C#:
<Button x:Name="button" Content="Click me" FontSize="16" />
is equivalent to
var button = new Button();
button.Contents = "Click me";
button.FontSize = 16;
XAML is smart enough to realize that FontSize
ought to be a double
, so it will convert the string
16
to a double
16 for you. But in the end, XAML is nothing more than a description of which objects to create and what values their properties should be set to.
Now we want to create a LinearGradientBrush
that goes from gray to white along a horizontal axis.
In C# code, we could write
var brush = new LinearGradientBrush();
// Define axis as going from left to right
brush.StartPoint = new Point(0, 0);
brush.EndPoint = new Point(1, 0);
// Start with gray
brush.GradientStops.Add(new GradientStop(Colors.Gray, 0));
// End with white
brush.GradientStops.Add(new GradientStop(Colors.White, 1));
Now, we don't want to write this in C#. This kind of information belongs in the XAML. How do we define such a complex object in XAML and assign it to the Background
property of the text blocks?
XAML is no regular XML: it allows you to define properties (attributes in XML-speak) not in one, but in two ways. The simplest syntax is what we've been using until now:
<Element Property="PropertyValue">
...
</Element>
For more complex property values (as is the case of our LinearGradientBrush
) we can use an alternative syntax, called the "Property Element Syntax".
<Element>
<Element.Property>
PropertyValue
</Element.Property>
...
</Element>
For example,
<TextBlock Text="Celsius" Background="#AAA" FontWeight="Bold" FontSize="16" Padding="2" />
and
<TextBlock>
<TextBlock.Text>Celsius</TextBlock.Text>
<TextBlock.Background>#AAA</TextBlock.Background>
<TextBlock.FontWeight>Bold</TextBlock.FontWeight>
<TextBlock.FontSize>16</TextBlock.FontSize>
<TextBlock.Padding>2</TextBlock.Padding>
</TextBlock>
describe the exact same thing. Of course, it does not make much sense to use the property element syntax here, as it is much less readable. But it will certainly come in handy for our fancy background:
<Window x:Class="View.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:View"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
- <TextBlock Text="Celsius" Background="#AAA" FontWeight="Bold" FontSize="16" Padding="2" />
+ <TextBlock Text="Celsius" FontWeight="Bold" FontSize="16" Padding="2">
+ <TextBlock.Background>
+ <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
+ <GradientStop Color="Gray" Offset="0" />
+ <GradientStop Color="White" Offset="1" />
+ </LinearGradientBrush>
+ </TextBlock.Background>
+ </TextBlock>
<TextBox x:Name="celsiusTextBox" HorizontalContentAlignment="Center" FontSize="16" />
- <TextBlock Text="Fahrenheit" Background="#AAA" FontWeight="Bold" FontSize="16" Padding="2" />
+ <TextBlock Text="Fahrenheit" FontWeight="Bold" FontSize="16" Padding="2">
+ <TextBlock.Background>
+ <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
+ <GradientStop Color="Gray" Offset="0" />
+ <GradientStop Color="White" Offset="1" />
+ </LinearGradientBrush>
+ </TextBlock.Background>
+ </TextBlock>
<TextBox x:Name="fahrenheitTextBox" HorizontalContentAlignment="Center" FontSize="16" />
<Button Content="Convert to Celsius" Click="ConvertToCelsius" FontSize="16" />
<Button Content="Convert to Fahrenheit" Click="ConvertToFahrenheit" FontSize="16" />
</StackPanel>
</Window>
In other words, switch to property element syntax if the property value is a complex object.
Snapshot: linear-gradient-brush
Play around a bit with the
StartPoint
,EndPoint
andOffset
properties to understand their effect.
Redundancy is the natural enemy of the programmer, yet we allowed it in our code: the LineairGradientBrush
XAML code has been repeated twice. We need a way to avoid this.
In a regular programming language, if we need the same object twice, we create it once, give it a name, and use the name twice to refer to it:
// Assign it a name
var brush = CreateBrush();
// Refer to it repeatedly
textBlock1.Background = brush;
textBlock2.Background = brush;
Resources are XAML's equivalent of variables: you create an object once, give it a name through the use of resources, and from then on you can simply use the name to refer to the object. Its syntax looks as follows:
<Window>
<Window.Resources>
<LinearGradientBrush x:Key="brush">
...
</LinearGradientBrush>
</Window.Resources>
...
<TextBlock Background="{StaticResource brush}" ...>
</Window>
In other words:
-
Window
has aResources
properties in which you can put all your "shared objects" and assign them a name usingx:Key="..."
. - To refer to a resource, you use the
{StaticResource key}
syntax.
Snapshot: linear-gradient-brush
Remove the duplicate
LinearGradientBrush
definition by turning it into a resource calledlabelBackgroundBrush
.
Snapshot: linear-brush-resource
There's actually still a lot of redundancy: the FontWeight
, FontSize
, etc. are all repeated. We would like to bundle these together and tell a control to "use this set of properties."
Styles are WPF's answer to your wishes. We show how to group all text blocks' properties in a style:
<Window x:Class="View.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:View"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
- <LinearGradientBrush x:Key="labelBackgroundBrush" StartPoint="0,0" EndPoint="1,0">
- <GradientStop Color="Gray" Offset="0" />
- <GradientStop Color="White" Offset="1" />
- </LinearGradientBrush>
+ <Style x:Key="labelStyle" TargetType="{x:Type TextBlock}">
+ <Setter Property="Background">
+ <Setter.Value>
+ <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
+ <GradientStop Color="Gray" Offset="0" />
+ <GradientStop Color="White" Offset="1" />
+ </LinearGradientBrush>
+ </Setter.Value>
+ </Setter>
+ <Setter Property="FontWeight" Value="Bold" />
+ <Setter Property="FontSize" Value="16" />
+ <Setter Property="Padding" Value="2" />
+ </Style>
</Window.Resources>
<StackPanel>
- <TextBlock Text="Celsius" FontWeight="Bold" FontSize="16" Padding="2" Background="{StaticResource labelBackgroundBrush}" />
+ <TextBlock Text="Celsius" Style="{StaticResource labelStyle}" />
<TextBox x:Name="celsiusTextBox" HorizontalContentAlignment="Center" FontSize="16" />
- <TextBlock Text="Fahrenheit" FontWeight="Bold" FontSize="16" Padding="2" Background="{StaticResource labelBackgroundBrush}" />
+ <TextBlock Text="Fahrenheit" Style="{StaticResource labelStyle}" />
<TextBox x:Name="fahrenheitTextBox" HorizontalContentAlignment="Center" FontSize="16" />
<Button Content="Convert to Celsius" Click="ConvertToCelsius" FontSize="16" />
<Button Content="Convert to Fahrenheit" Click="ConvertToFahrenheit" FontSize="16" />
</StackPanel>
</Window>
Snapshot: text-block-style
Define styles for the text boxes and buttons.
Snapshot: styles
Earlier in this tutorial, we mentioned panels such as StackPanel
, Grid
and DockPanel
. Until now, we've solely been relying on the StackPanel
. We will now introduce you to the Grid
.
Right now, our buttons are placed a bit awkwardly, as they are separated from the text box they are associated with. It would make more sense that the "Convert to Fahrenheit" button is placed right to the Celsius text box, and similarly for the other button:
+-------------------------+----------+
| TextBlock | |
+-------------------------+ Button |
| TextBox | |
+-------------------------+--*-------+
We can achieve this layout using a grid:
- The grid should have 2 rows and 2 columns.
- The button is placed in the second column but spans both rows.
First, remove the Celsius related controls:
<StackPanel>
- <TextBlock Text="Celsius" Style="{StaticResource labelStyle}" />
- <TextBox x:Name="celsiusTextBox" Style="{StaticResource textBoxStyle}" />
<TextBlock Text="Fahrenheit" Style="{StaticResource labelStyle}" />
<TextBox x:Name="fahrenheitTextBox" Style="{StaticResource textBoxStyle}" />
- <Button Content="Convert to Celsius" Click="ConvertToCelsius" Style="{StaticResource buttonStyle}" />
<Button Content="Convert to Fahrenheit" Click="ConvertToFahrenheit" Style="{StaticResource buttonStyle}" />
</StackPanel>
and add the following code:
<StackPanel>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition />
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition />
+ <RowDefinition />
+ </Grid.RowDefinitions>
+ <TextBlock Grid.Row="0" Text="Celsius" Style="{StaticResource labelStyle}" />
+ <TextBox Grid.Row="1" x:Name="celsiusTextBox" Style="{StaticResource textBoxStyle}" />
+ <Button Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Content="Convert" Click="ConvertToFahrenheit" Style="{StaticResource buttonStyle}" />
+ </Grid>
<TextBlock Text="Fahrenheit" Style="{StaticResource labelStyle}" />
<TextBox x:Name="fahrenheitTextBox" Style="{StaticResource textBoxStyle}" />
<Button Content="Convert to Celsius" Click="ConvertToCelsius" Style="{StaticResource buttonStyle}" />
</StackPanel>
Notice the following details:
- A
Grid
's columns and rows are specified usingGrid.ColumnDefinitions
andGrid.RowDefinitions
respectively. - To indicate in which grid cell a control belongs, you need to make use of attached properties, namely
Grid.Row
,Grid.Column
andGrid.RowSpan
in our example.
The Grid.Row
, Grid.Column
and Grid.RowSpan
properties should arise suspicion: they clearly belong to Grid
, but they're added as properties to other controls. It's as if you set a field defined as a member of class A
on an object of class B
. Attached properties are WPF's solution to the need to be able to attach extra data to other objects. In our case, the grid wants to be able to iterate through all of its children and ask each "where should I put you?"
While it is allowed to add Grid.Row
, etc. to any control, it only makes sense if you add them to the direct children of a grid. In all other cases, they'll just be ignored:
<Grid>
<Button Grid.Row="0" /> <!-- Makes sense -->
</Grid>
<StackPanel>
<Button Grid.Row="0" /> <!-- Allowed, but useless -->
</StackPanel>
Run the program, and you'll notice that the grid chose to make its column the same width. For our purposes, this makes little sense: we would prefer the button to receive less room, say only 25% of the total available width. This can be accomplished by specifying a Width
for the columns:
<Grid>
<Grid.ColumnDefinitions>
- <ColumnDefinition />
- <ColumnDefinition />
+ <ColumnDefinition Width="3*" />
+ <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Celsius" Style="{StaticResource labelStyle}" />
<TextBox Grid.Row="1" x:Name="celsiusTextBox" Style="{StaticResource textBoxStyle}" />
<Button Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Content="Convert" Click="ConvertToFahrenheit" Style="{StaticResource buttonStyle}" />
</Grid>
A ColumnDefinition
's width can take different kinds of values:
- A regular number, e.g., 250, is interpreted as an absolute width. Try to avoid it.
- If it's set to
auto
, it will make the column as narrow as possible, just wide enough to accomodate its contents. - A
*
orn*
withn
a number is interpreted as a relative width. Say for example you have three columns withWidth
s set to*
,2*
and3*
. The available width will be distributed so that the second column is twice as wide as the first and the third is thrice as wide as the first. In other words, using3*
and*
as above will give the first column 75% of the available width and the second only 25%.
The same functionality is also available on rows, only instead of Width
s, rows have Height
s.
Snapshot: celsius-grid
Give the Fahrenheit related controls the same
Grid
-treatment.Note: do not put the Fahrenheit controls in the same grid (i.e., by adding extra rows to the existing grid). Instead, make a whole new grid.
Snapshot: fahrenheit-grid
Add a third set of controls, this time for the Kelvin scale. Have every button update the text boxes of the two other scales, e.g., if I press "Convert" next to Celsius, both the Fahrenheit and Kelvin temperatures should be updated.
Currently, the Click-handling methods are called
ConvertToFahrenheit
andConvertToCelsius
. These names won't do anymore, since each method now needs to convert to two scales. UseConvertCelsius
,ConvertFahrenheit
andConvertKelvin
instead, whereConvertCelsius
reads in the Celsius text box and converts its value to Fahrenheit and Kelvin, etc.Do not skip this exercise, we need Kelvin for later.
As always, check your work. Test it as follows:
- Enter 0 in the Celsius text box and press the correspondig Convert button. Fahrenheit should now contain 32 and Kelvin 273.15.
- Enter 100 in the Fahrenheit text box and press Convert. Expected values are 37.78°C and 311K.
- Enter 500K, which should be converted to 227°C and 440°F.
Snapshot: kelvin