-
Notifications
You must be signed in to change notification settings - Fork 3
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 xmlns="https://github.com/avaloniaui"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="View.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="View">
<StackPanel>
+ <TextBlock Text="Celsius" />
<TextBox Name="celsiusTextBox" />
+ <TextBlock Text="Fahrenheit" />
<TextBox 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 xmlns="https://github.com/avaloniaui"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="View.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="View">
<StackPanel>
- <TextBlock Text="Celsius" />
+ <TextBlock Text="Celsius" Background="#AAA" />
<TextBox Name="celsiusTextBox" />
- <TextBlock Text="Fahrenheit" />
+ <TextBlock Text="Fahrenheit" Background="#AAA" />
<TextBox 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. Avalonia 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 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 xmlns="https://github.com/avaloniaui"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="View.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="View">
<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="100%,0%">
+ <GradientStop Color="Gray" Offset="0" />
+ <GradientStop Color="White" Offset="1" />
+ </LinearGradientBrush>
+ </TextBlock.Background>
+ </TextBlock>
<TextBox 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="100%,0%">
+ <GradientStop Color="Gray" Offset="0" />
+ <GradientStop Color="White" Offset="1" />
+ </LinearGradientBrush>
+ </TextBlock.Background>
+ </TextBlock>
<TextBox 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,EndPointandOffsetproperties 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:
-
Windowhas aResourcesproperties 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
LinearGradientBrushdefinition 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 Avalonia's answer to your wishes. We show how to group all text blocks' properties in a style:
<Window xmlns="https://github.com/avaloniaui"
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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="View.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="View">
- <Window.Resources>
- <LinearGradientBrush x:Key="labelBackgroundBrush" StartPoint="0,0" EndPoint="1,0">
- <GradientStop Color="Gray" Offset="0" />
- <GradientStop Color="White" Offset="1" />
- </LinearGradientBrush>
- </Window.Resources>
+ <Window.Styles>
+ <Style Selector="TextBlock.label">
+ <Setter Property="Background">
+ <Setter.Value>
+ <LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,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.Styles>
<StackPanel>
- <TextBlock Text="Celsius" FontWeight="Bold" FontSize="16" Padding="2" Background="{StaticResource labelBackgroundBrush}" />
+ <TextBlock Text="Celsius" Classes="label" />
<TextBox Name="celsiusTextBox" HorizontalContentAlignment="Center" FontSize="16" />
- <TextBlock Text="Fahrenheit" FontWeight="Bold" FontSize="16" Padding="2" Background="{StaticResource labelBackgroundBrush}" />
+ <TextBlock Text="Fahrenheit" Classes="label" />
<TextBox 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" Classes="label" />
- <TextBox Name="celsiusTextBox" Classes="textbox" />
<TextBlock Text="Fahrenheit" Classes="label" />
<TextBox Name="fahrenheitTextBox" Classes="textBox" />
- <Button Content="Convert to Celsius" Click="ConvertToCelsius" Classes="button" />
<Button Content="Convert to Fahrenheit" Click="ConvertToFahrenheit" Classes="button" />
</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" Classes="label" />
+ <TextBox Grid.Row="1" Name="celsiusTextBox" Classes="textBox" />
+ <Button Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Content="Convert" Click="ConvertToFahrenheit" Classes="button" />
+ </Grid>
<TextBlock Text="Fahrenheit" Classes="label" />
<TextBox Name="fahrenheitTextBox" Classes="textBox" />
<Button Content="Convert to Celsius" Click="ConvertToCelsius" Classes="button" />
</StackPanel>Notice the following details:
- A
Grid's columns and rows are specified usingGrid.ColumnDefinitionsandGrid.RowDefinitionsrespectively. - To indicate in which grid cell a control belongs, you need to make use of attached properties, namely
Grid.Row,Grid.ColumnandGrid.RowSpanin 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 Avalonia'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" Classes="label" />
<TextBox Grid.Row="1" Name="celsiusTextBox" Classes="textBox" />
<Button Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Content="Convert" Click="ConvertToFahrenheit" Classes="button" />
</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*withna number is interpreted as a relative width. Say for example you have three columns withWidths 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 Widths, rows have Heights.
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
ConvertToFahrenheitandConvertToCelsius. These names won't do anymore, since each method now needs to convert to two scales. UseConvertCelsius,ConvertFahrenheitandConvertKelvininstead, whereConvertCelsiusreads 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