Page 1 of 1

WPF Build a Window, with Style

#1 andrewsw  Icon User is online

  • It's just been revoked!
  • member icon

Reputation: 3822
  • View blog
  • Posts: 13,543
  • Joined: 12-December 12

Posted 27 April 2014 - 07:58 AM

Coming from WinForms you may, like me, want to get a workable Form (called a Window in WPF) up and running. This is my main goal with this tutorial. I am talking about a workable design: there is no code (other than XAML) in this tutorial.

Pre-Requisites:

I assume that you have an understanding of what XAML is, or how it is used in WPF, and know what I mean when I mention labels, textboxes and comboboxes. Knowledge of WinForms is not essential but a basic understanding of WPF and common controls is.

Part 2

Windows Presentation Foundation :MSDN

My Objectives:

  • Get a basic Window up and running, with labels, textboxes, a combobox, etc..
  • I use Grid and StackPanel layouts - discuss these (and others)
  • The Window will be adaptable. We can use different controls and make the Window bigger or smaller
  • Use, and introduce, Styles, reducing the amount of XAML we need to type
  • Add a couple of Style Triggers, just to introduce this subject.

Styling and Templating :MSDN

Without further ado, here is the Window:

Attached Image

I do not claim it to be perfect (or very exciting) but it is workable and I hope that you will learn a number of things about WPF in the process of building it.

IMPORTANT NOTE: The Window doesn't do anything.. clicking the buttons won't get you anywhere!

Here is the Designer view with gridlines, for reference:

Attached Image

Brief Intro - Margins or Height/Width?

Create a new (temporary) WPF Application and double-click the Button control in the Toolbox to add it to the Window, or drag-and-drop it onto the Window if you prefer. Look at the XAML this creates:
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75"/>

    </Grid>
</Window>


Notice that you are adding the Button to a default Grid, not to the Window itself.

If you run the application the Button is static and quite dull. (Try resizing the window.) It has a fixed width and is very WinForm-like.

The WPF-Designer is a pale imitation of the WinForm-Designer and doesn't make good choices about default attributes/properties. At a very early stage it is useful to drag and drop controls, look through the added control's Properties Window, and read the generated XAML. This will give you a feel for WPF - an overview. But, very soon after this, we need to start getting used to writing the XAML instead. Don't worry:

  • Adding a control with XAML is easy, and there's intellisense to help us
  • We only need to add a few attributes to a control to get the look that we want
  • Creating Styles for controls will reduce our typing even further
  • The Designer is still useful as it gives us (almost) immediate visual feedback.

There is a small icon between the two panes (it has two arrows) that switches the panes, moving the XAML to the top (if you prefer). There is also one to collapse the Designer pane.

Expression Blend can be used to design WPF. I've read mixed opinions about it, but I haven't explored it so can't comment.


Press Undo to get rid of the Button, or Delete it from the Designer (or the XAML). Instead, drag the Button from the Toolbox and drop it into the XAML, between the Grid tags:
    <Grid>
        <Button />
    </Grid>


Notice in the Designer that the Button now occupies the full extent of the Grid - this is the default behaviour if no width, or other metrics, are given to the button.

Change the XAML to this:
    <Grid>
        <Button Content="My Button" Margin="10" />
    </Grid>


Run this version and resize the Form Window, notice how the Button resizes but maintains the margin that we set. Try this version:
    <Grid>
        <Button Margin="10,20,30,40">Still My Button</Button>
    </Grid>


The text on the button can either be set as the Content-attribute or as the text/value enclosed by the Button-tags. Using Content is the more WPF way. It is called Content because it doesn't have to be Text.. it can be an image, some other Control.. or anything.

We are setting the left, top, right and bottom margins. Note that "10,20" will set the left and right margins to 10, and the top and bottom to 20.

These numbers aren't pixels, they are Device Independent Pixels (DIPs). "A DIP is defined as 1/96th of a logical inch". They are pixels with attitude.

My (personal) Early Conclusions:
  • Work with margins and padding in preference to setting specific widths or heights. This provides us with a flexible (fluid/liquid) layout. The control will fill its available space, keeping the margins intact.
  • Try to avoid setting both margins and widths/heights. These are competing attributes. I haven't investigated the algorithm that is used to resolve these settings but I'm guessing there might occur some odd behaviour if you use both, such as the height of the control(s) suddenly collapsing. Investigate this if it intrigues you*.
  • We can use MinWidth and MinHeight to prevent a Window getting ridiculously small. We should consider MaxWidth and MaxHeight as well, although I prefer to leave this consideration until the Window is built/complete. (A side-note: Why would somebody want to make my Window much bigger than I had intended/designed?)
  • I prefer to set min/max on the Window itself. If you start doing this with individual controls (or control-types) I can foresee it getting a little messy. Just my opinion.

With my Window I demonstrate the behaviour of both Height/Width and MinHeight/MinWidth. You can then decide which approach you prefer. As it turned out, I preferred setting Height/Width (for the Grid-cells). So my personal preference for this specific Window was to use a fixed layout. (I can probably blame this on my WinForm background.)

With a fluid layout the controls will resize with the Window, but the font-sizes won't change - they don't autoscale. A quick search suggests that this isn't as easy to achieve as it could be. It seems to require ViewBoxes. I haven't pursued this topic.

* Layout :MSDN

Let's Get Started - Default Font and Font-Size

Abandon the application and start a new WPF Application named ProgrammersWPF (or whatever).

From the Solution Explorer open the file App.xaml and add this Style as a Resource:
<Application x:Class="ProgrammersWPF.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="Mainwindow.xaml">
    <Application.Resources>
        <Style x:Key="windowStyle" TargetType="{x:Type Window}">
            <Setter Property="FontFamily" Value="Verdana" />
            <Setter Property="FontSize" Value="12" />
            <Setter Property="MinWidth" Value="500" />
            <Setter Property="MinHeight" Value="300" />
        </Style>
    </Application.Resources>
</Application>


WPF Styles are similar to css-stylesheets. They use a form of inheritance - similar to, but not exactly the same as, css-styles. Setting the font and font-size of a Window will cause all child-controls of the Window to inherit these settings. Not every setting is inherited: the MinWidth and MinHeight won't be. (Note: You could omit, or comment out, the MinWidth/Height settings for now, and reinstate them a bit later.)

This is an application-wide resource. The Style is named 'windowStyle' (its Key) and it applies to (is targeted to) Window controls. If we omitted the Key then it would apply to all Window's within this Application. Because we are using a Key we need to modify the Mainwindow.xaml to tell it to apply this style to our Window:
<Window x:Class="ProgrammersWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Programmers Window"  
        Style="{StaticResource windowStyle}">


(The full code is listed at the end of the tutorial.)

If we set a default font and size we will probably want to apply this consistently throughout our application, but we shall leave it as a named-resource for now.

You might initially think of a StaticResource as being similar to an internal or external css-stylesheet (assuming you are familiar with css) and a DynamicResource as an inline stylesheet. This comparison doesn't take us too far though:

SO said:

A StaticResource will be resolved and assigned to the property during the loading of the XAML which occurs before the application is actually run. It will only be assigned once and any changes to resource dictionary ignored.

A DynamicResource assigns an Expression object to the property during loading but does not actually lookup the resource until runtime when the Expression object is asked for the value. This defers looking up the resource until it is needed at runtime. A good example would be a forward reference to a resource defined later on in the XAML. Another example is a resource that will not even exist until runtime. It will update the target if the source resource dictionary is changed.

SO link

I don't explore DynamicResources in this tutorial.

Alternatively, we can create a more general Style without specifying a TargetType. In which case we need to qualify the Properties with their target control-type. For example:
        <Style x:Key="generalStyle">
            <Setter Property="window.FontFamily" Value="Verdana" />
            <Setter Property="window.FontSize" Value="12" />
            <Setter Property="window.MinWidth" Value="500" />
            <Setter Property="window.MinHeight" Value="300" />
        </Style>


(We can include different control-types in this single Style.)

This can create a more general, comprehensive, Style that can be applied to a number of different control-types. This is an interesting option but I don't use it in this tutorial. It requires careful planning (IMO) and is probably best left until we have a good understanding of how Styles work.

You could close this App.xaml file for now.

Laying It Out

Panel Elements and Custom Layout behaviors :MSDN

Nest the default Grid inside a DockPanel. Define the heights and widths of the cells of the Grid:
<Window x:Class="ProgrammersWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Programmers Window"  
        Style="{StaticResource windowStyle}">
    <DockPanel>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="50" />
                <RowDefinition Height="50" />
                <RowDefinition Height="50" />
                <RowDefinition Height="50" />
                <RowDefinition Height="50" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="100" />
            </Grid.ColumnDefinitions>
        </Grid>
    </DockPanel>
</Window>


Notice in the Designer that the last row and column extend beyond this height and width. Add the SizeToContent attribute to the Window to fix this:
        Title="Programmers Window" SizeToContent="WidthAndHeight" 


If you prefer, you could give the Window a specific Height and Width.

A DockPanel is often used as the main container/parent-control. We can position other controls within it to the top, bottom, left, right and optionally in the centre. It is also quite often used without positioning anything within it, so that it just fills the window.

The DockPanel is a little redundant. Part of the reason to include it is that later we may wish to extract the Grid and make it a user/custom control that we insert into the Window as part of the MVVM pattern. (MSDN Magazine article.). For this tutorial though, we just insert it and ignore it.

Once you've added a few controls to the Grid you can change all of the above Height and Width values to MinHeight and MinWidth to obtain a fluid layout.

There is a UniformGrid so, if we are using the same widths and heights, why not use it? I did explore this but it was simply too restrictive.

Grid System

The 960 grid system and similar are popular with css, and I am following similar principles. If we divide the grid into a sensible number of cells then we have a very flexible layout, that can handle most combinations of controls.

My goal is to start with specified, uniform, widths and heights. I will allow controls to span rows and columns and use margins to align them neatly. If I need more room I prefer to add rows and columns (of the same dimensions) rather than modifying individual widths or heights.

Nevertheless, once the Window is complete, we still have the option to change a width or height, for the aesthetics. The problem is, if you start to do this before you've added all the controls, you never get it just right, and the Grid is no longer re-usable.

I will also use a StackPanel layout for the buttons, discussed later.

For Windows with few controls, such as a main switchboard or a Window with a large DataGrid, I would consider a DockPanel, StackPanel, or combinations of these. A Grid is overkill if there are only a few controls on the window.

Controls and their Style

Add the first Label and TextBox:
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Content="First Name" />
        <TextBox Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2" x:Name="txtFirstName" />


I have given the TextBox a Name. In WinForms I name all my controls but in WPF it is not necessary, and perhaps even frowned upon by some. I take the middle-ground: I name all controls that will represent data-values, but I (largely) don't name controls that are simply UI-elements, such as Buttons.

We can't see the TextBox very easily because by default it fills the two cells that it spans. You could add a background-colour to the Window's Style (in App.xaml):
        <Setter Property="Background" Value="LightGray" />


You could add this property to the Grid instead if you prefer:
        <Grid Background="BlanchedAlmond">


.. or whatever colour you fancy! Of course, rather than adding this to the Grid directly, I would rather define a Grid-style.

Add the following style which targets Control rather than any specific type of control.
        Style="{StaticResource windowStyle}">
    <window.Resources>
        <Style x:Key="controlStyle" TargetType="Control">
            <Setter Property="HorizontalContentAlignment" Value="Left" />
            <Setter Property="VerticalContentAlignment" Value="Center" />
        </Style>
    </window.Resources>
    <DockPanel>


Recall that if we didn't give this Style a name (its Key) then these properties would (or should) affect nearly all controls. However, this isn't the one size fits all solution that you might expect. Only a certain number of properties will carry-through - only among control-wide properties. We also cannot include properties that are only relevant to specific control-types.

Besides, it is unlikely that the set of properties that you apply to labels will be the same as those for (all) textboxes, checkboxes, etc.. ContentAlignment is, though, a good example of a property that can apply to a Control.

We could, alternatively, create these styles as Application.Resources (in App.xaml), or even package our styles together as a theme or add-in. This requires some planning because our application is likely to use different kinds of Windows.

ResourceDictionary Class
Creating Resource Dictionaries :msdn blog

Add two more styles for labels and textboxes that are BasedOn our Control-Style:
        </Style>
        <Style TargetType="Label" BasedOn="{StaticResource controlStyle}">
            <Setter Property="Padding" Value="10" />
        </Style>
        <Style TargetType="TextBox" BasedOn="{StaticResource controlStyle}">
            <Setter Property="Padding" Value="3" />
            <Setter Property="Margin" Value="10,12" />
        </Style>
    </window.Resources>


I am a css-minimalist. I aim to get the results I want with as few rules (properties) as possible. Just Margins. Then maybe Padding, then maybe one or two more.

Labels do have Margins, but they also have a default Padding of "5" (units). Rather than playing with both I just overrule the default Padding-value. The significant detail is that the left-margin of textboxes and left-padding of labels are the same so that they will align neatly.

Unless you add a background or border to a label then there is essentially no difference between its padding and margin.

Let's add the other Label, TextBox and the CheckBox, and a style for the CheckBox:
        <Style TargetType="CheckBox" BasedOn="{StaticResource controlStyle}">
            <Setter Property="Margin" Value="10" />
        </Style>


            <Label Grid.Row="1" Grid.Column="0" Content="Last Name" />
            <TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" x:Name="txtLastName" />
            <CheckBox Content="Active?" Grid.Row="0" Grid.Column="3" x:Name="ckbActive" />


Now the DatePicker:
        <Style TargetType="DatePicker" BasedOn="{StaticResource controlStyle}">
            <Setter Property="Margin" Value="10,12" />
        </Style>
    </window.Resources>

            <Label Grid.Row="2" Grid.Column="0" Content="Start Date" />
            <DatePicker Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" x:Name="dpStartDate" />
        </Grid>


The DatePicker was a late addition to the Toolbox and is, let's say, unfinished. It is difficult to control the positioning of the text-region in relation to the calender-icon, and you might even see an odd blue border around it. You can play with Margin, Padding and other properties, or create a ControlTemplate for it. Or, like me, you can accept it as is.

I don't want to waste hours fiddling with the look of the DatePicker. If its look or behaviour bothered me too much I think I would look for a plug-in version; I'm sure someone has already done this hard work for us.

The ComboBox and ListBox:

Spoiler

Eventually, of course, these items wouldn't usually be hard-coded into the XAML, but be retrieved from, and bound to, a data-source (an object).

I added the long text to see how the ListBox behaves. It behaves quite well, with a horizontal scrollbar appearing when needed, but adding another column (and spanning the ListBox across 3-columns) can be considered. The behaviour would be more tightly controlled if the ListBox were given a specific Width, but this is something that I hope to avoid. One, of many, possible approaches is to create a Style giving the list-items a specific width, which will cause the horizontal scrollbar to appear immediately.
        <Style TargetType="ListBoxItem" BasedOn="{StaticResource controlStyle}">
            <Setter Property="Width" Value="200" />
        </Style>


However, if the purpose of this is simply to display the horizontal scrollbar, then it is unnecessary because the ListBox has a ScrollViewer.HorizontalScrollBarVisibility attribute.

StackPanel and Buttons

I put the Buttons in a StackPanel which can lay-out its child controls either horizontally or vertically.
        <Style TargetType="Button">
            <Setter Property="Margin" Value="10" />
            <Setter Property="MinWidth" Value="70" />
        </Style>

            </ListBox>
            <StackPanel Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="5" Orientation="Horizontal"
                HorizontalAlignment="Center">
                <Button Content="First" />
                <Button Content="Previous" />
                <Button Content="Next" />
                <Button Content="Last" />
                <Button Content="New" />
            </StackPanel>


You could, if you want, just put each Button in a separate cell of the Grid. This would enable the buttons to be aligned with the other controls on the Window, but it will become fiddly when the text of one of the buttons is too wide for the cell.

Personally, I do not consider it important that the buttons should align with other controls; they perform a different function than the textboxes, etc.. It is more important that the buttons look neat collectively, as a group.

I do not care that the buttons have slightly different widths. You could give them the same, specific, width if you want but you should anticipate that, at some point, you might need to add a button that needs a larger width.

I do, however, set a minimum-width as I don't want a button to be oddly-narrow in comparison to its brothers.

Another advantage of using a StackPanel is that it makes the button-set more portable.

Introducing Style Triggers

Style.Triggers Property :MSDN

MSDN said:

Essentially, triggers are objects that enable you to apply changes when certain conditions (such as when a certain property value becomes true, or when an event occurs) are satisfied.


Buttons already have a default Trigger: if you move your mouse over a button its Background changes to a window's-grey colour. (To change this colour you need to modify the Button's ControlTemplate.)

The following Trigger makes the button-text Bold when the mouse is over a Button. That is, when "IsMouseOver" has the value "True".
        <Style TargetType="Button">
            <Setter Property="Margin" Value="10" />
            <Setter Property="MinWidth" Value="70" />
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="FontWeight" Value="Bold" />
                </Trigger>
            </Style.Triggers>
        </Style>


Cool. No code required! This is a great aspect of WPF. This kind of effect has nothing to do with the model, or business-layer, behind our application, so it is great, and sensible, that it sits inside the presentation-layer.

This is significantly different to the purely event-based model of WinForms. With WinForms this effect requires code, so it is hardly surprising that WinForm-programmers acquire the habit on integrating UI/effect-code with their business (model) code.

Another notable difference from WinForms is that we don't have to write anything to reverse the property-change. When the mouse is no longer over the button its font-weight automatically reverts to its original value.

Another simple example is to make a listbox-item oblique when selected:
        <Style TargetType="ListBoxItem">
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="FontStyle" Value="Oblique" />
                </Trigger>
            </Style.Triggers>
        </Style>


I found, and borrowed, the following code from stackoverflow. It is a glow effect (an Animation) on textboxes when they gain, and lose, focus.

Spoiler

I shall leave you to study it:
Animation Overview :MSDN
Storyboards Overview :MSDN

A more traditional effect is to make a control's background yellow on-focus:
        <Style x:Key="controlStyle" TargetType="Control">
            <Setter Property="HorizontalContentAlignment" Value="Left" />
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Style.Triggers>
                <Trigger Property="IsFocused" Value="True">
                    <Setter Property="Background" Value="LightYellow" />
                </Trigger>
            </Style.Triggers>
        </Style>


You will notice, though, that it only affects the textboxes. That's fine as it would be slightly odd if it affected, for example, the combobox. Given this, I would probably just move it into the TextBox-Style.

MultiTriggers change property values or perform actions when a set of conditions are met. Here is an example that changes the background for a particular ListBoxItem:
        <Style TargetType="ListBoxItem">
            <Setter Property="Background" Value="Transparent" />
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                    <Setter Property="FontStyle" Value="Oblique" />
                </Trigger>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="Content" Value="Java" />
                    </MultiTrigger.Conditions>
                    <MultiTrigger.Setters>
                        <Setter Property="Background" Value="Aquamarine" />
                    </MultiTrigger.Setters>
                </MultiTrigger>
            </Style.Triggers>
        </Style>




Here is the full code:

Spoiler

The Window includes this additional code:
        Style="{StaticResource windowStyle}" Loaded="Window_Loaded"
        FocusManager.FocusedElement="{Binding ElementName=txtFirstName}">


The second line sets the focus to the firstname-textbox when the application is run.

I said right at the beginning that there is no code-behind in this tutorial. However, I did experiment with this single line of code in Mainwindow.xaml.cs:
        private void Window_Loaded(object sender, RoutedEventArgs e) {
            //this.lstLang.Height = (this.lstLang.ActualHeight / this.lstLang.Items.Count) * 6;
        }


This would cause the ListBox to only display six items, but it disturbs my design. I mention it just in case it interests you.

This post has been edited by andrewsw: 08 May 2014 - 12:56 PM


Is This A Good Question/Topic? 3
  • +

Replies To: WPF Build a Window, with Style

#2 farrell2k  Icon User is offline

  • D.I.C Lover
  • member icon

Reputation: 864
  • Posts: 2,653
  • Joined: 29-July 11

Posted 27 April 2014 - 10:56 PM

Xaml is entirely too complicated.
Was This Post Helpful? 0
  • +
  • -

#3 andrewsw  Icon User is online

  • It's just been revoked!
  • member icon

Reputation: 3822
  • View blog
  • Posts: 13,543
  • Joined: 12-December 12

Posted 28 April 2014 - 10:57 AM

Agreed :)

With that in mind my intention is to reduce the amount of XAML I need to type, using Styles and Resources, and anything else that helps.




Making the ListBox's Margin "10,12" it is now the same as the other controls and means that it lines up neatly with adjacent controls. This also provides the opportunity to move the Margin setting into the main Control-Style:

Spoiler

However, this only reduces the code by a handful of lines. We need to experiment with a variety of other controls in order to determine whether a change like this is beneficial.
Was This Post Helpful? 1
  • +
  • -

#4 farrell2k  Icon User is offline

  • D.I.C Lover
  • member icon

Reputation: 864
  • Posts: 2,653
  • Joined: 29-July 11

Posted 28 April 2014 - 01:17 PM

This is nice. Keep going. You are motivating me to learn it. It's so ubiquitous in the market and you're a great teacher.
Was This Post Helpful? 0
  • +
  • -

#5 andrewsw  Icon User is online

  • It's just been revoked!
  • member icon

Reputation: 3822
  • View blog
  • Posts: 13,543
  • Joined: 12-December 12

Posted 28 April 2014 - 03:00 PM

Thank you very much - nice to hear ;)

I am planning to continue (but am reluctant to promise). I want to break it down into blocks. The books I have mix and combine topics too much IMO, making it seem more overwhelming than it needs to be.

We'll see..
Was This Post Helpful? 0
  • +
  • -

#6 theZ  Icon User is offline

  • New D.I.C Head

Reputation: 0
  • View blog
  • Posts: 3
  • Joined: 18-February 14

Posted 27 November 2014 - 04:18 AM

Thank you very, very much for this detailed and useful tutorial you made. It's awsome!
Respect!
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1