Posts for Category: Layout
For some kinds of applications displaying as “maximised” is not enough, either because they want to make use of every single pixel of screen-space possible, or because displaying the task bar and associated window trimmings would be distracting or confusing for users of the application. The PowerPoint slide viewer is a good example of this, starting in “kiosk” mode to create a more immersive presentation and preventing audience members becoming distracted by the task bar and things in the notification area. Fortunately doing this for your own WPF application can be achieved quite easily by setting the WindowStyle and WindowState properties on the Window as shown below.
Xaml Code
<Window x:Class="LearnWPF.KioskMode.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="LearnWPF.KioskMode"
WindowStyle="None"
WindowState="Maximized">
WindowStyle should be set to None (this draws the window without any border, and is the same state you can use to create a WPF splash screen) and the WindowState set to Maximized. These properties can also be set from code. One small issue we have observed is if you try to change the WindowStyle in code after the WindowState has changed to Maximized the resulting window will be drawn without a border, but won’t display over the top of the status bar.
Developers new to WPF are sometimes a little confused when the Width and Height properties of elements report as ‘NaN’ (Not a Number) in code, when these very same elements are clearly visible and do have a width and height. What’s going on?
The short answer is you can interpret ‘NaN’ to mean “not set”. The Width and Height properties of Buttons, TextBoxes and most other controls are inherited from the FrameworkElement superclass. These normal .NET properties in turn backs on to a DependencyProperty (the WidthProperty in the case of Width) that has been registered for FrameworkElement in the class constructor with a default value of ‘NaN’. Until you set that WidthProperty to something, or set the Width .NET property in code or Xaml the Width will stay as ‘NaN’. If you wish to know what the on-screen size for an object is you should instead consult the ActualWidth and ActualHeight properties, which always reflects the actual width and height of the object on screen. These properties are sometimes a little hard to find given the number of public properties and methods on many objects in WPF.
Reflecting another visual in WPF is typically done using a VisualBrush and a ScaleTransform to “flip” the content upside down, as shown in this nice example on xamlog. In order to perform the “flip” the ScaleTransform needs the centre about which this should occur. Unfortunately the centre position of an element is not directly exposed via a property, so the net result of this is that for simple reflections (where you know the size of the source) you typically end up hard-coding the centre of the transform in your XAML . A recent poster in the WPF forums asked if it was possible to avoid this hard-coding. One approach which I’m going to explore here is to use data binding and a fairly generalized “arithmetic converter” capable of multiplying, dividing, adding to or subtracting from values as part of the binding operation . In order to obtain the center for our ScaleTransform we bind the CenterX property of the transform to the ActualWidth of the element we want to reflect, and multiply by 0.5. Similarly we can bind the CentreY property of the transform to the ActualHeight * 0.5. The arithmetic operation is specified to the converter using the ConverterParameter property of the binding. In order to use the converter we first need to add a namespace alias to the CLR namespace that the converter is contained in.
<Window x:Class="LearnWPF.ReflectionWithoutHardCodingWidth.Window1"
xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation
"
xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml
"
xmlns:cvrt="clr-namespace:LearnWPF.Converters"
Title="LearnWPF.ReflectionWithoutHardCodingWidth" Height="300" Width="300"
>
Then we can use the converter like so to perform the binding.
<ScaleTransform ScaleX="1" ScaleY="-1"
CenterX="{Binding ElementName=PhotoBorder,
Path=ActualWidth,
Converter={StaticResource arithConverter},
ConverterParameter=*0.5}"
CenterY="{Binding ElementName=PhotoBorder,
Path=ActualHeight,
Converter={StaticResource arithConverter},
ConverterParameter=*0.5}" />
This generalized arithmetic converter is clearly over-engineered for the task of finding the centre of an element, but it has other applications when using bindings to control layout in WPF.
I’ve created a complete project, using the sample from Xamlog as a starting point which you can download here. Below are screen-shots of the reflection in action with images of different sizes and aspect ratios.
Splitters are a useful UI feature where the width or height of a control on the form can be modified to show more or less information. An Example of this is the splitter between the folder tree-view in and the file and subfolder detail view in Windows Explorer, allowing the size of the folder tree-view on the left-hand side of the form to be sized up to 50% of the form width. Windows Forms 1.1 included a Splitter control, and Windows Forms 2.0 included a SplitContainer. WPF does not have equivalent versions of either of these controls; however it is possible to add splitters to your WPF application using the WPF Grid, and a GridSplitter. The example code below allows the two columns in the grid to be re-sized. This is done by adding a GridSplitter as a child item of the Grid.
Xaml Code (Feb 2006 CTP)
<Window x:Class="LearnWPF.Splitter.Window1"
xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation
"
xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml
"
Title="LearnWPF.Splitter" Height="300" Width="300"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Background="#feca00" Grid.Column="0">
<TextBlock FontSize="35" Foreground="#58290A"
TextWrapping="Wrap">Left Hand Side</TextBlock>
</StackPanel>
<GridSplitter/>
<Border CornerRadius="10" BorderBrush="#58290A"
BorderThickness="5" Grid.Column="1">
<TextBlock FontSize="25" Margin="20" Foreground="#FECA00"
TextWrapping="Wrap">Right Hand Side</TextBlock>
</Border>
</Grid>
</Window>
This gives the following UI behavior:
The splitter appears "docked" on the right edge of the cell that contains it. Like other items contained in a grid, the splitter can be positioned in a column/row other than the first one by setting the Grid.Column and Grid.Row properties. In the above example it defaults to row and column 0 because these have not been explicitly set.
To allow the GridSplitter to resize cells vertically is somewhat non-intuitive as described by Charles Petzold here. The GridSplitter has a ResizeDirection which defaults to Columns but can be set to Rows to allow the splitter to change the height of a row in the grid. To enable the height of row to be resized correctly by the splitter you also need to set the HorizontalAlignment and VerticalAlignment for to align it to the bottom, and across the width of the containing cells. You can also set the Grid.ColumnSpan for the splitter to span across all the cells in the row that it resizes, otherwise the splitter will only fill a single cell.
This example below creates a 2x2 grid of cells, with a splitter spanning both columns and allowing the height of the rows to be changed.
Xaml Code (Feb 2006 CTP)
<Window x:Class="LearnWPF.Splitter.Window1"
xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation
"
xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml
"
Title="LearnWPF.Splitter" Height="300" Width="300"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Background="#feca00" Grid.Column="0" Grid.Row="0">
<TextBlock FontSize="35" Foreground="#58290A"
TextWrapping="Wrap">Left Hand Side</TextBlock>
</StackPanel>
<GridSplitter ResizeDirection="Rows"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"/>
<Border CornerRadius="10" BorderBrush="#58290A" Grid.Column="1" Grid.Row="1"
BorderThickness="5">
<TextBlock FontSize="25" Margin="20" Foreground="#FECA00"
TextWrapping="Wrap">Right Hand Side</TextBlock>
</Border>
</Grid>
</Window>
WPF attempts to bring the best of rich-client technologies like Windows Forms, and layout techniques from the browser to give a high degree of flexibility when positioning elements on forms.
Rich client technologies like VB6 forms, Access Forms etc were based on absolute positioning. As a developer you specified the top and left values for a control to position it on the form. This allows for a fine degree of control, but often required a large amout of code to handle form re-sizing. This was an imperative approach to layout. Controls like Labels and Panels had no awareness of the size of the content inside them - for example if the content of a label was larger than what could be displayed in the label then the content is clipped, and not shown.
Web UI rendered in the browser typically require no re-sizing code. Instead HTML gives a declarative approach to the layout of elements. You specify a serise of containers like <div> elements (or <table>, <tr> and <td> elements if you haven't become convinced of the benefits of positional CSS-based layouts) and position those by either floating them Left or Right, or positioning them absolutely in your page. Elements like <div> are aware of the size of their contents and will stretch to accomodate their content.
Both these approaches could be equally difficult to achieve the desired layout, although the declarative approach had the advantage of requiring much less code to do so. Recently windows forms has added concepts like Docking and Anchoring, adding a more declarative approach to rich-client development. WPF continues this trend with a declarative approach to layout based on the concept of panels.
Most UI elements in WPF can only contain a single child element - for example the following Xaml code fails to compile with the following error: "The 'Button' object already has a child and cannot add 'CheckBox'. 'Button' can accept only one child."
Xaml Syntax (does not compile)
<Button>
<TextBlock>Some Text...</TextBlock>
<CheckBox />
<ComboBox />
</Button>
If the button allowed these two elements to be nested inside it it would be responsible for laying them out somehow. Since there are many possible ways these items could be layed out the button would have to become a much more complicated control to accomodate them all, and would now have two "responsibilities" - allowing the user to click on it, and laying out content. Not only would the button become much more complicated but all the controls in WPF would become more complicated. Instead of taking this approach the WPF team decided to factor out the responsibility for layout to a different control - the panels. There are a number of different types of panels in WPF and each enables a different kind of layout. Panels can be nested wherever regular content can be placed, allowing a fine degree of control over the layout of items. For example to correct the problem in the example above we could nest a StackPanel inside the button, and position the elements inside the StackPanel
<Button>
<StackPanel>
<TextBlock>Some Text...</TextBlock>
<CheckBox />
<ComboBox />
</StackPanel>
</Button>
This give the following layout:
Having briefly introduced the important role panels play in controlling layout in WPF let's look at the most commonly used panel types and their characteristics.
StackPanel
The StackPanel lays out child elements by stacking them one after the other. Elements are "stacked" in the order they appear in the Xaml file (document order in XML terms). Items can either be stacked vertically (the default) or horizontally.
Xaml Syntax for Stacking Vertically (Feb 2006 CTP):
<StackPanel>
<TextBlock FontSize="16"
Foreground="#58290A">
Items inside a StackPanel</TextBlock>
<Button>Item 2</Button>
<Border BorderBrush="#feca00" BorderThickness="2">
<TextBlock>Item 3</TextBlock>
</Border>
</StackPanel>
Xaml Syntax for Stacking Horizontally (Feb 2006 CTP):
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="16"
Foreground="#58290A">
Items inside a StackPanel</TextBlock>
<Button>Item 2</Button>
<Border BorderBrush="#feca00" BorderThickness="2">
<TextBlock>Item 3</TextBlock>
</Border>
</StackPanel>
WrapPanel
The wrap panel lays out items from left to right. When a row of items has filled the horizontal space available to them the panel wraps the next item around onto the next line (in a similar way to how text is layed out).
Xaml Syntax (Feb 2006 CTP):
<WrapPanel>
<TextBlock FontSize="16"
Foreground="#58290A">
Items inside a WrapPanel</TextBlock>
<Button>Item 2</Button>
<Border BorderBrush="#feca00" BorderThickness="2">
<TextBlock>Item 3</TextBlock>
</Border>
</WrapPanel>
DockPanel
The DockPanel allows elements to be docked to the edges of the panel's container, similar to Windows Forms docking. Items are docked in the order they appear in the Xaml file (document order). The last Border element fills all the remaining space since no DockPanel.Dock attribute is specified for it.
Xaml Syntax (Feb 2006 CTP):
<DockPanel>
<TextBlock FontSize="16" DockPanel.Dock="Top"
Foreground="#58290A">
Items inside a DockPanel</TextBlock>
<Button DockPanel.Dock="Left">Item 2</Button>
<Border BorderBrush="#feca00" BorderThickness="2">
<TextBlock>Item 3</TextBlock>
</Border>
</DockPanel>
Canvas
The Canvas panel is similar to the way old rich-client layout worked where you control the absolute position of things by setting a Top and Left property for them. Additionally, instead of setting the Left to position an item you can instead set the Right, or instead of setting the Top you can set the Bottom. If you specify a Left and a Right the Right value is ignored, the element does not change its size to make both these values correct, and similarly Top takes precedence over Bottom. Elements that are declared earlier in the Xaml file can appear behind elements that are declared later (if their positions over-lap).
Xaml Syntax (Feb 2006 CTP):
<Canvas>
<TextBlock FontSize="16" Canvas.Top="10" Canvas.Left="20"
Foreground="#58290A">
Items inside a Canvas</TextBlock>
<Button Canvas.Bottom="25" Canvas.Right="50">Item 2</Button>
<Border BorderBrush="#feca00" BorderThickness="2"
Canvas.Top="20" Canvas.Left="50">
<TextBlock>Item 3</TextBlock>
</Border>
</Canvas>
Grid
The Grid panel is an extremely flexible panel, and can be used to achieve almost everything that can be done with the other panel controls (although often not with the same ease). The grid panel allows you to define rows and columns in the grid using Xaml, and then place controls in calls in the grid using grid-specific attributes. Elements can span multiple rows or columns. The grid will automatically make all its rows and columns the same size (based on the size of the content) but you can specify a proportional size for rows and columns using a star notation, or specify an absolute width or height. The star notation can be seen in the example code below where one of the columns is twice the width of the other column by setting the Width attribute to "2*". The example below also shows the height of one row being set to an absolute value. The differences these introduce can more readily be seen when re-sizing the form containing the grid, as the grid will expand by default to fill the space available to it. The example below also has the ShowGridLines attribute of the grid set to True to help you visualize the space each row and column is taking up. A broader range of layout tricks for the grid panel is beyond the scope of this introduction, but hopefully we'll be able to re-visit this soon in a future article.
Xaml Syntax (Feb 2006 CTP):
<Grid Margin="10" ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition />
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>
<TextBlock FontSize="16"
Foreground="#58290A"
Grid.Column="0" Grid.Row="0"
Grid.ColumnSpan="2"
>
Items inside a Grid</TextBlock>
<Button Grid.Column="0" Grid.Row="1">
Item 2</Button>
<Border BorderBrush="#feca00" BorderThickness="2"
Grid.Column="1" Grid.Row="2">
<TextBlock>Item 3</TextBlock>
</Border>
</Grid>
We can see from the different examples above that the different Panel controls not only influence the position of the elements they're laying out, but also their size.
Implementing a Custom Panel
The WPF layout system is not limited to the layouts that are included by default. If you need a special layout that can't easily be achieved with the built-in panels you can implement your own. WPF Program Manager Kevin Moore has implemented two custom panels - an animating tile panel and a TreeMapPanel . The source for these built with the February CTP (the most recent at the time of writing) is available here.
In a
previous how-to
we looked at how to use a ScaleTransform to allow the user to re-size a region of a form using a slider control. When the value of the slider changed some C# code in the event handler created a new ScaleTransform, set its ScaleX and ScaleY values based on the value of the slider, and then assigned the ScaleTransform to the LayoutTransform of the item we wanted to re-size. It is also possible to do this without writing any "code" by creating a ScaleTransform in XAML and binding the ScaleX and ScaleY properites of the transform to the value of the slider.
WPF has some powerful data binding capabilities - usually people think of data binding as binding the property of a control to some underling data-structure - either relational data, XML or some "business object" from the problem domain. Binding can also be used to bind the value of two UIElements together. In the sample code (shown in full below) we've defined a slider in the same way as the previous example, but without specifying a ValueChanged handler.
<Slider Name="UISizer" Minimum="0.1" Maximum="5" Value="1">
Then for the grid we wish to re-size we add a child LayoutTransform element, and inside that we nest our ScaleTransform. The values for the ScaleX and ScaleY properties are bound to the value of the slider using the Binding markup extension. Markup extensions are defined inside the curly braces, and look similar to the way XPath functions can be used in XSLT. This binding expression tells WPF that the ScaleX and ScaleY properties should be bound to the Value property of the UISizer element.
<Grid Name="ItemToResize" Background="#feca00" Width="500">
<Grid.LayoutTransform>
<ScaleTransform ScaleX="{Binding ElementName=UISizer, Path=Value}"
ScaleY="{Binding ElementName=UISizer, Path=Value}" />
</Grid.LayoutTransform>
The complete XAML (below) does all that the previous demo did, without requiring any C# or VB.NET code.
XAML (For Feb CTP):
<Window x:Class="ScaleTest.Window1"
xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation
"
xmlns:x="
http://schemas.microsoft.com/winfx/2006/xaml
"
Title="ScaleTest" Height="300" Width="300"
>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="12">Smaller</TextBlock>
<Slider Name="UISizer" Minimum="0.1" Maximum="5" Value="1">
</Slider>
<TextBlock FontSize="20">Bigger</TextBlock>
</StackPanel>
<Border BorderThickness="3" BorderBrush="#58290A">
<Grid Name="ItemToResize" Background="#feca00" Width="500">
<Grid.LayoutTransform>
<ScaleTransform ScaleX="{Binding ElementName=UISizer, Path=Value}"
ScaleY="{Binding ElementName=UISizer, Path=Value}" />
</Grid.LayoutTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" FontSize="15">Foo:</TextBlock>
<TextBlock Grid.Column="0" Grid.Row="1" FontSize="15">Bar:</TextBlock>
<TextBox Grid.Column="1" Grid.Row="1" Padding="3"></TextBox>
<ComboBox Grid.Column="1" Grid.Row="0" Padding="3">
<ListBoxItem>
<TextBlock>Value 1</TextBlock>
</ListBoxItem>
<ListBoxItem>
<TextBlock>Value 2</TextBlock>
</ListBoxItem>
<ListBoxItem>
<Ellipse Width="50" Height="35" Fill="Orange" />
</ListBoxItem>
<ListBoxItem>
<TextBlock>Value 4</TextBlock>
</ListBoxItem>
</ComboBox>
</Grid>
</Border>
<TextBlock FontSize="20">This item will not change its size...</TextBlock>
</StackPanel>
</Window>
Many WPF applications allow a region of the form to be re-sized by the user by moving a slider control. This is a great feature for users with poor vision, who can resize regions of the form to a size that suits them, and also shows WPF's vector-based graphics system off to great effect. So how is this achieved? Sizing the UI relies on the ScaleTransform class in WPF. As the name suggests a ScaleTransform allows a specific UIElement and its children to be re-sized, either at the layout or at the render stage.
To apply a ScaleTransform to a UIElement you create a new ScaleTransform object, set the ScaleX and ScaleY properties and then assign the newly created ScaleTransform to the UIElements LayoutTransform or RenderTransform property. If you assign the new ScaleTransform to the LayoutTransform property the scaling happens before the sizes of all the controls have been measured, and the UIElements around the one you are scaling will re-size in accordance with the new size of the UIElement you scaled. If you assign the new ScaleTransform to the RenderTransform property then the scaling happens after the sizes of all the UIElements have been measured, and the new size of the UIElement you are changing won't be taken into account.
In the example application we create a slider, and attach a method to the ValueChanged event of the slider. In the ValueChanged event we create a new ScaleTransform, and assign it to the LayoutTransform of the UIElement we wish to size (in the sample application it is a grid with a text box, combo box and some text blocks nested inside it).
XAML:
<Slider Name="UISizer" Minimum="0.1" Maximum="5" Value="1" ValueChanged="UISizerValueChanged">
C#:
public void UISizerValueChanged(object sender, RoutedEventArgs e)
{
if (ItemToResize != null && UISizer != null)
{
// we could use ItemToResize.RenderTransform here too
ItemToResize.LayoutTransform =
new ScaleTransform(UISizer.Value, UISizer.Value);
}
}
Tip: Don't scale the slider (or other control) that the user will click on to re-size the region of the window. Doing so will cause the slider to change in size, which interferes with (usually magnifying) the changes made by the user. Also set sensible maximum and minimum values for the slider.