Skip to content
Frederic Vogels edited this page Oct 14, 2019 · 5 revisions

Making it look better

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.

Labels and their 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

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

Property Element Syntax

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

Exercise

Play around a bit with the StartPoint, EndPoint and Offset properties to understand their effect.

Resources

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 a Resources properties in which you can put all your "shared objects" and assign them a name using x:Key="...".
  • To refer to a resource, you use the {StaticResource key} syntax.

Snapshot: linear-gradient-brush

Exercise

Remove the duplicate LinearGradientBrush definition by turning it into a resource called labelBackgroundBrush.

Snapshot: linear-brush-resource

Styles

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

Exercise

Define styles for the text boxes and buttons.

Snapshot: styles

Grid panel

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 using Grid.ColumnDefinitions and Grid.RowDefinitions respectively.
  • To indicate in which grid cell a control belongs, you need to make use of attached properties, namely Grid.Row, Grid.Column and Grid.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 * or n* with n a number is interpreted as a relative width. Say for example you have three columns with Widths set to *, 2* and 3*. 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, using 3* 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 Widths, rows have Heights.

Snapshot: celsius-grid

Exercise

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

Exercise

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 and ConvertToCelsius. These names won't do anymore, since each method now needs to convert to two scales. Use ConvertCelsius, ConvertFahrenheit and ConvertKelvin instead, where ConvertCelsius 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