You can find the wrox.com
code downloads for this chapter at www.wrox.com/go/beginningvisualc#2015programming
on the Download Code tab. The code is in the Chapter 14 download and individually named according to the names throughout the chapter.
The first part of this book has concerned itself with the ins and outs of C#, but now it is time to move away from the details of the programming language and into the world of the graphical user interface (GUI).
Over the past 10 years, Visual Studio has provided the Windows developers with a couple of choices for creating user interfaces: Windows Forms, which is a basic tool for creating applications that target classic Windows, and Windows Presentation Foundations (WPF), which provide a wider range of application types and attempts to solve a number of problems with Windows Forms. WPF is technically platform-independent, and some of its flexibility can be seen in the fact that a subset of WPF called Silverlight is used to create interactive web applications. In this and the next chapter you are going to learn how to use WPF to create Windows applications, and in Chapter 23 you will build on this knowledge when you create Universal Windows Apps.
At the heart of the development of most graphical Windows applications is the Window Designer. You create a user interface by dragging and dropping controls from a Toolbox to your window, placing them where you want them to appear when you run the application. With WPF this is only partly true, as the user interface is in fact written entirely in another language called Extensible Application Markup Language (XAML, pronounced zammel). Visual Studio allows you to do both and as you get more comfortable with WPF, you are likely going to combine dragging and dropping controls with writing raw XAML.
In this chapter, you work with the Visual Studio WPF designer to create a number of windows for the card game that you wrote in previous chapters. You learn to use some of the many controls that ship with Visual Studio that cover a wide range of functionality. Through the design capabilities of Visual Studio, developing user interfaces and handling user interaction is very straightforward — and fun! Presenting all of Visual Studio's controls is impossible within the scope of this book, so this chapter looks at some of the most commonly used controls, ranging from labels and text boxes to menu bars and layout panels.
XAML is a language that uses XML syntax and enables controls to be added to a user interface in a declarative, hierarchical way. That is to say, you can add controls in the form of XML elements, and specify control properties with XML attributes. You can also have controls that contain other controls, which is essential for both layout and functionality.
XAML is designed with today's powerful graphics cards in mind, and as such it enables you to use all the advanced capabilities that these graphics cards offer through DirectX. The following lists some of these capabilities:
One problem that exists with maintaining Windows applications that has been written over the years is that they very often mix the code that generates the user interface and the code that executes based on users' actions. This makes it difficult for multiple developers and designers to work on the same project. WPF solves this in two ways. First, by using XAML to describe the GUI rather than C#, the GUI becomes platform independent, and you can in fact render XAML without any code whatsoever. Second, this means that it feels natural to place the C# code in a different file than you place the GUI code. Visual Studio utilizes something called code-behind files, which are C# files that are dynamically linked to the XAML files.
Because the GUI is separated from the code, it is possible to create tailor-made applications for designing the GUI, and this is exactly what Microsoft has done. The design tool Blend for Visual Studio is the favored tool used by designers when creating GUIs for WPF. This tool can load the same projects as Visual Studio, but where Visual Studio targets the developer more than the designer, the opposite is true in Expression Blend. This means that on large projects with designers and developers, everyone can work together on the same project, using their preferred tool without fear of inadvertently influencing the others.
As stated, XAML is XML, which means that as long as the files are fairly small, it is possible to see immediately what it is describing. Take a look at this small example and see if you can tell what it does:
<Window x:Class="Ch14Ex01.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:WpfApplication1"
mc:Ignorable="d"
Title="Hello World" Height="350" Width="525">
<Grid>
<Button Content="Hello World"
HorizontalAlignment="Left"
Margin="220,151,0,0"
VerticalAlignment="Top"
Width="75"/>
</Grid>
</Window>
The XAML in this example creates a window with a single button on it. Both the window and the button display the text "
". XML allows you to place tags inside other tags as long as you close them properly. When an element in placed inside another in XAML, this element becomes the content of the enclosing element, meaning that the Hello World
could also have been written like this:Button
<Button HorizontalAlignment="Left"
Margin="220,151,0,0"
VerticalAlignment="Top"
Width="75">
Hello World
</Button>
Here, the
property of the Content
has been removed and the text is now a child node of the Button
control. Button
can be just about anything in XAML, which is also demonstrated in this example: The Content
element is the content of the Button
element, which is itself the content of the Grid
element.Window
Most, if not all, controls can have content, and there are very few limits to what you can do to change the appearance of the built-in controls. Chapter 15 explores this in more detail.
The
element of the previous example is the root element of the XAML file. This element usually includes a number of namespace declarations. By default, the Visual Studio designer includes two namespaces that you should be aware of: Window
http://schemas.microsoft.com/winfx/2006/xaml/presentation
and http://schemas.microsoft.com/winfx/2006/xaml
. The first one is the default namespace of WPF and declares a lot of controls that you are going to use to create user interfaces. The second one declares the XAML language itself. Namespaces don't have to be declared on the root tag, but doing so ensures that their content can be easily accessed throughout the XAML file, so there is rarely any need to move the declarations.
When you create a new window in Visual Studio, the presentation namespace is always declared as the default and the language namespace as
. As seen with the xmlns:x
, Window
, and Button
tags, this ensures that you don't have to prefix the controls you add to the window, but the language elements you specify must be prefixed with an Grid
.x
The last namespace that you will see quite often is the system namespace:
"xmlns:sys=
". This namespace allows you to use the built-in types of the .NET Framework in your XAML. By doing this, the markup you write can explicitly declare the types of elements you are creating. For example, it is possible to declare an array in markup and state that the members of the array are strings:clr-namespace:System;assembly=mscorlib
<Window.Resources>
<ResourceDictionary>
<x:Array Type="sys:String" x:Key="localArray">
<sys:String>"Benjamin Perkins"</sys:String>
<sys:String>"Jacob Vibe Hammer"</sys:String>
<sys:String>"Job D. Reid"</sys:String>
</x:Array>
</ResourceDictionary>
</Window.Resources>
Although XAML is a powerful way to declare user interfaces, it is not a programming language. Whenever you want to do more than presentation, you need C#. It is possible to embed C# code directly into XAML, but mixing code and markup is never recommended and you will not see it done in this book. What you will see quite a lot is the use of code-behind files. These files are normal C# files that have the same name as the XAML file, plus a
extension. Although you can call them whatever you like, it's best to stick to the naming convention. Visual Studio creates code-behind files automatically when you create a new window in your application, because it expects you to add code to the window. It also adds the .cs
property to the x:Class
tag in the XAML:Window
<Window x:Class="Ch14Ex01.MainWindow"
This tells the compiler that it can find the code for this window in, not a file, but the class
. Because you can specify only the fully qualified class name, and not the assembly in which the class is found, it is not possible to put the code-behind file somewhere outside of the project in which the XAML is defined. Visual Studio puts the code-behind files in the same directory as the XAML files so you never have to worry about this while working in Visual Studio.Ch14Ex01.MainWindow
Now you know enough about how WPF is constructed to start getting your hands dirty, so it's time to look at the editor. Start by creating a new WPF project by selecting File New Project. From the New Project dialog box, navigate to the Clasic Desktop node under Visual C# Windows and select the project template WPF Application. To be able to reuse this example with the next examples, name the project
.Ch14Ex01
Visual Studio now displays an empty window and a number of panels around it. The greater part of the screen is divided in two sections. The upper section, known as the Design View, displays a WYSIWYG (What You See Is What You Get) representation of the window you are designing and the lower section, known as the XAML View, displays a textual representation of the same window.
To the right of the Design View, you see the Solution Explorer that you have seen in previous projects and a Properties panel that displays information about the current selection in the Design and XAML Views. It is worth noting that the selection in the Properties panel, XAML View, and Design View are always in sync, so if you move the cursor in the XAML View you will see the selection change in the other two.
Collapsed to the left of the Design View are a number of panels, one of which is the Toolbox. This chapter shows you how to use many of the controls from the Toolbox panel to create dialog boxes for the card game, so expand it and pin it open by clicking the pin in the top-right corner. While you are at it, expand the Common WPF Controls node in the panel as well. You will be using most of the controls shown here in this chapter.
Controls combine prepackaged code and a GUI that can be reused to create more complex applications. They can define how they draw themselves by default and a set of standard behaviors. Some controls, such as the
, Label
, and Button
controls are easily recognizable and have been used in Windows applications for about 20 years. Others, such as TextBox
and Canvas
, don't display anything and simply help you create the GUI.StackPanel
Out-of-the-box controls look exactly as you would expect a control to look in a standard Windows application and use the current Windows Theme to draw themselves. All of this is highly customizable and with only a few clicks you can completely change how a control is displayed. This customization is done using properties that are defined on the controls. WPF uses normal properties that you have seen before and adds a new type of property called a dependency property. These are examined in detail in Chapter 15, but for now it is enough to know that many of the properties of WPF do more than just get and set a value; for one, they are able to notify observers of changes.
Besides defining how something looks on the screen, controls also define standard behavior, such as the ability to click on a button and select something in a list. You can change what happens when a user performs an action on a control by “handling” the events that the control defines. When and how you implement the event handler will vary from application to application and from control to control, but generally speaking you will always handle the
event for a button; for a Click
control, you often have to react when the user changes the selection and so the ListBox
event should be handled. On other controls, such as the SelectionChanged
or Label
controls, you will rarely implement any event.TextBlock
You can add controls to a window in a number of ways, but the most common way is to drag and drop them from the Toolbox onto the Design View or the XAML View.
As mentioned, all controls have a number of properties that are used to manipulate the behavior of the control. Some of these are easy to understand such as
and height
, whereas others are less obvious such as width
. All of them can be set using the Properties panel, directly in XAML, or by manipulating the control on the Design View.RenderTransform
Visual Studio will create a default namespace for your classes when you create a new project. That namespace is subsequently used when you add new classes or windows to your project. You can change the namespace by double-clicking Properties in the Solution Explorer. If you find that your classes get a different namespace than given in the examples, it can be helpful to change the default namespace to the namespace from the book. The change will only affect new classes, not anything already in the project.
Return to the previous example and follow these steps. As you change the properties, notice how your changes affect the XAML and Design Views. You are going to change the window to look like Figure 14.1.
Button
control in Design View; this is the button that is currently filling the entire window.rotatedButton
.Content
to 2nd Button
.<Window x:Class="Ch14Ex01.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:Ch14Ex01"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left"
Margin="218,113,0,0" VerticalAlignment="Top" Width="75"/>
<Button x:Name="rotatedButton" Content="2nd Button" Width="75"
Height="22" FontWeight="Bold" Margin="218,138,224,159"
RenderTransformOrigin="0.5,0.5" >
<Button.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="-23.896"/>
<TranslateTransform/>
</TransformGroup>
</Button.RenderTransform>
</Button>
</Grid>
</Window>
Any change that you apply in any of the three views is reflected in the other views, but some things are easier to do in certain views. Changing something trivial like the text displayed on a button can be done quickly in XAML View, but adding the information needed to perform a render transformation is much quicker from Design View.
In this exercise, you began by changing the name of the button, which added the
property to the button. The name of a control must be unique within the scope of the namespace, so you can use the name for only one control.x:Name
Next you changed the
property, set the Content
and Height
of the control, and then changed the font to bold. Doing so changed the way the control displayed itself within the window. It used to fill all the space of its container, but now you have limited it to a specific size.Width
Then you dragged the first button to a specific position on the Design View. As you see later in this chapter, this action will not always yield the same results but is dependent on the container in which the control is placed. In this case, with the
container, the control can be dragged to a specific position. The action sets the Grid
property on the control. Two other properties should be mentioned here: Margin
"HorizontalAlignment=
" and Left
"VerticalAlignment=
". With these two properties set, the margin becomes relative to the top-left corner of the window and thus the control is pushed to the position you placed it in the grid. If you compare the first and second buttons at this point, you will notice that the second control has none of these properties set. By omitting the alignment properties as well as the margin properties, the control is placed at the center of the container, even at runtime. This means that the first button with the margin and alignments set is fixed when the window resizes, but the second button always stays centered.Top
Finally, you performed a little bit of a party trick. By dragging the control when the
mouse pointer is displayed, you can rotate the control. This is a standard feature of XAML and WPF and can be applied to all controls, although there are a few controls that fail to change their content when the control itself is rotated. This includes controls that rely on Windows Forms or old Windows controls to display content.Rotate
The animations that you can do in WPF are covered in Chapter 15, but from the XML that was generated when you dragged the cursor, you can see that you can perform some advanced animation simply by manipulating these properties.
For the most part, normal .NET properties are simple getters and setters, which is fine for most cases. However, when you are working with a dynamic user interface that can and should change when properties change, you have to write a lot of code in these get and set methods that will be repeated many times. A dependency property is a property that is registered with the WPF property system in such a way as to allow extended functionality. This extended functionality includes, but is not limited to, automatic property change notifications. Specifically, dependency properties have the following features:
In practice, because of the way in which dependency properties are implemented, you might not notice much of a difference compared to ordinary properties. However, when you create your own controls, you will quickly find that a lot of functionality suddenly disappears when you use ordinary .NET properties.
Chapter 15 shows how you can implement new dependency properties.
An attached property is a property that is made available to each child object of an instance of the class that defines the property. For example, as you will see later in this chapter, the
control that you used in the previous examples allows you to define columns and rows for ordering the child controls of the Grid
. Each child control can then use the attached properties Grid
and Column
to specify where it belongs in the grid:Row
<Grid HorizontalAlignment="Left" Height="167" VerticalAlignment="Top" Width="290">
<Button Content="Button" HorizontalAlignment="Left" Margin="10,10,0,0"
VerticalAlignment="Top" Width="75" Grid.Column="0" Grid.Row="0"
Height="22"/>
…
</Grid>
Here, the attached property is referred to using the name of the parent element, a period, and the name of the attached property.
In WPF, attached properties serve a variety of uses. You will see a lot of attached properties shortly, when you look at how to position controls in the “Control Layout” section. You will learn how container controls define attached properties that enable child controls to define, for example, which edges of the container to dock to.
In Chapter 13, you learned what events are and how to use them. This section covers particular kinds of events — specifically, the events generated by WPF controls — and introduces routed events, which are usually associated with user actions. For example, when the user clicks a button, that button generates an event indicating what just happened to it. Handling the event is the means by which the programmer can provide some functionality for that button.
Many of the events you handle are common to most of the controls that you work with in this book. This includes events such as
and LostFocus
. This is because the events themselves are inherited from base classes such as MouseEnter
or Control
. Other events such as the ContentControl
event of the CalendarOpened
are more specific and only found on specialized controls. Some of the most used events are listed in Table 14.1.DatePicker
Table 14.1 Common Control Events
Event | Description |
|
Occurs when a control is clicked. In some cases, this event also occurs when a user presses the Enter key. |
|
Occurs when a drag-and-drop operation is completed — in other words, when an object has been dragged over the control, and the user releases the mouse button. |
|
Occurs when an object being dragged enters the bounds of the control. |
|
Occurs when an object being dragged leaves the bounds of the control. |
|
Occurs when an object has been dragged over the control. |
|
Occurs when a key is pressed while the control has focus. This event always occurs before and . |
|
Occurs when a key is released while a control has focus. This event always occurs after event. |
|
Occurs when a control receives focus. Do not use this event to perform validation of controls. Use and instead. |
|
Occurs when a control loses focus. Do not use this event to perform validation of controls. Use and instead. |
|
Occurs when a control is double-clicked. |
|
Occurs when the mouse pointer is over a control and a mouse button is pressed. This is not the same as a event because occurs as soon as the button is pressed and before it is released. |
|
Occurs continually as the mouse travels over the control. |
|
Occurs when the mouse pointer is over a control and a mouse button is released. |
You will see many of these events in the examples in this chapter.
There are two basic ways to add a handler for an event. One way is to use the Events list in the Properties window, shown in Figure 14.2, which is displayed when you click the lightning bolt button.
To add a handler for a particular event, either type the name of the event and press Return, or double-click to the right of the event name in the Events list. This causes the event to be added to the XAML tag. The method signature to handle the event is added to the C# code-behind file.
<Button x:Name="rotatedButton" Content="2nd Button" Width="75"
Height="22" FontWeight="Bold" Margin="218,138,224,159"
RenderTransformOrigin="0.5,0.5"
Click="rotatedButton_Click">
…
</Button>
private void rotatedButton_Click(object sender, RoutedEventArgs e)
{
}
You can also type the name of the event directly in XAML and add the name of the handler there. If you do this, you can right-click on the event and chose Navigate to Event Handler. This will add the event handler to the code-behind file.
WPF uses events that are called routed events. A standard .NET event is handled by the code that has explicitly subscribed to it and it is sent only to those subscribers. Routed events are different in that they can send the event to all controls in the hierarchy in which the control participates.
A routed event can travel up and down the hierarchy of the control on which the event occurred. So, if you right-click a button, the
event will first be sent to the button itself, then to the parent of the control — in the case of the earlier example, the MouseRightButtonDown
control. If this doesn't handle it, then the event is finally sent to the window. If, on the other hand you don't want the event to travel further up the hierarchy, then you simply set the Grid
property RoutedEventArgs
to Handled
, and no additional calls will be made at that point. When an event travels up the control hierarchy like this, it is called a bubbling event.true
Routed events can also travel in the other direction, that is, from the root element to the control on which the action was performed. This is called a tunneling event and by convention all events like this are prefixed with the word
and always occur before their bubbling counterparts. An example of this is the Preview
event.PreviewMouseRightButtonDown
Finally, a routed event can behave exactly like a normal .NET event and only be sent to the control on which the action was made.
Routed commands serve much the same purpose as events in that they cause some code to execute. Where Events are bound directly to a single element in the XAML and a handler in the code, Routed Commands are more sophisticated.
The key difference between events and commands is in their use. An event should be used whenever you have a piece of code that has to respond to a user action that happens in only one place in your application. An example of such an event could be when the user clicks OK in a window to save and close it. A command can be used when you have code that will be executed to respond to actions that happen in many locations. An example of this is when the content of an application is saved. There is often a menu with a Save command that can be selected, as well as a toolbar button for the same purpose. It is possible to use event handlers to do this, but it would mean implementing the same code in many locations — a command allows you to write the code just once.
When you create a command, you must also implement code that can respond to the question, “Should this code be available to the user at the moment?” This means that when a command is associated with a button, that button can ask the command if it can execute and set its state accordingly.
A command is much more complicated to implement than an event, so you are not going to see them in use until Chapter 15, where they will be used with menu items.
This example builds on the example from earlier in the chapter. If you added the rows and columns earlier, you should remove them to match the XAML in this example.
rotatedButton
and add the event KeyDown
. You can do this through the Properties panel or by typing the XAML directly. Name it rotatedButton_KeyDown
.Grid
by clicking on the tag it in the XAML View, and add the same event to it. Name it Grid_KeyDown
.Window
tag in the XAML View and add the event again. Name it Window_KeyDown
.PreviewKeyDown
and change the name of the event to reflect that it is the Preview
handler. The XAML should look like this:<Window x:Class="Ch14Ex01.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:Ch14Ex01"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" KeyDown="Window_KeyDown"
PreviewKeyDown="Window_PreviewKeyDown">
<Grid KeyDown="Grid_KeyDown" PreviewKeyDown="Grid_PreviewKeyDown"> <Button x:Name="button" Content="Button" HorizontalAlignment="Left"
Margin="27,4,0,0" VerticalAlignment="Top" Width="75" Grid.Column="0"
Grid.Row="0"/>
<Button x:Name="rotatedButton" Content="2nd Button" Width="75" Height="22"
FontWeight="Bold" RenderTransformOrigin="0.5,0.5"
KeyDown="rotatedButton_KeyDown"
PreviewKeyDown="rotatedButton_PreviewKeyDown" Grid.Column="1"
Grid.Row="1" >
<Button.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="-23.896"/>
<TranslateTransform/>
</TransformGroup>
</Button.RenderTransform>
</Button>
</Grid>
</Window>
private void Grid_KeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show("Grid handler, bubbling up");
}
private void Grid_PreviewKeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show("Grid handler, tunneling down");
}
private void rotatedButton_KeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show("rotatedButton handler, bubbling up");
}
private void rotatedButton_PreviewKeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show("rotatedButton handler, tunneling down");
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show("Window handler, bubbling up");
}
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
MessageBox.Show("Window handler, tunneling down");
}
Grid_PreviewKeyDown
event handler and add this line below the MessageBox
line:e.Handled = true;
The
and KeyDown
events demonstrate bubbling and tunneling events. When you press a key with PreviewKeyDown
selected, you see each of the event handlers executing, one after another.rotatedButton
First the
events execute, starting with the handler on Preview
, then the Window
, and finally the Grid
. Then the rotatedButton
events execute, but in the opposite order, starting with the event handler on the KeyDown
and finishing with the handler on rotatedButton
.Window
If you use any of the keys explicitly stated not to use in Step 8, you will notice that the
event is fired only on Preview
. This is because these are not considered input keys and are ignored by the grid and buttons.Window
Then you added this line:
e.Handled = true;
This changed the behavior dramatically. By setting the
property of the Handled
you not only caused the execution of the tunneling events, but also of the bubbling events. This is generally true for all events like this. If you stop the execution of either the RoutedEventArgs
or the “normal” version of the event handlers, you stop them both.Preview
As stated, WPF has a lot of controls to choose from. Two types of interest are the Content and Items controls. Content controls, such as the
control, have a Button
property that can be set to any other control. This means that you can determine how the control is displayed, but you can specify only a single control directly in the content. That being said, you can specify an Items control, which is a control that allows you to insert multiple controls as content. An example of an Items control is the Content
control. When you are creating user interfaces, you are continually combining these two control types.Grid
In addition to Content and Items controls, there are a number of other types of controls that don't allow you to use other controls as their content. One example of this is the
control, which is used to display an image. Changing that behavior defeats the purpose of the control.Image
So far in this chapter you have used the
element to lay out a few controls, primarily because that is the control supplied by default when you create a new WPF application. However, you haven't yet examined the full capabilities of this class, nor have you learned about the other layout containers that you can use to achieve alternative layouts. This section looks at control layout in more detail, as it is a fundamental concept of WPF.Grid
All content layout controls derive from the abstract
class. This class simply defines a container that can contain a collection of objects that derive from Panel
. All WPF controls derive from UIElement
. You cannot use the UIElement
class directly for control layout, but you can derive from it if you want to. Alternatively, you can use one of the following layout controls that derive from Panel
Panel:
Canvas
— This control enables you to position child controls any way you see fit. It doesn't place any restrictions on child control positioning, but nor does it provide any assistance in positioning.DockPanel
— This control enables you to dock child controls against one of its four edges. The last child control fills the remaining space.Grid
— This control enables flexible positioning of child controls. You can divide the layout of this control into rows and columns, which enables you to align controls in a grid layout.StackPanel
— This control positions its child controls in a sequential horizontal or vertical layout.WrapPanel
— This control positions its child controls in a sequential horizontal or vertical layout as StackPanel
, but rather than a single row or column of controls, this control wraps its children into multiple rows or columns according to the space available.You'll look at how to use these controls in more detail shortly. First, however, there are a few basic concepts to understand:
Border
controlWhen a container control contains multiple child controls, they are drawn in a specific stack order. You might be familiar with this concept from drawing packages. The best way to think of stack order is to imagine that each control is contained in a plate of glass, and the container contains a stack of these plates of glass. The appearance of the container, therefore, is what you would see if you looked down from the top through these layers of glass. The controls contained by the container overlap, so what you see is determined by the order of the glass plates. If a control is higher up the stack, then it will be the control that you see in the overlap area. Controls lower down may be partially or completely hidden by controls above them.
This also affects hit testing when you click on a window with the mouse. The target control will always be the one that is uppermost in the stack when considering overlapping controls. The stack order of controls is determined by the order in which they appear in the list of children for a container. The first child in a container is placed on the lowest layer in the stack, and the last child on the topmost layer. The children between the first and last child are placed on increasingly higher layers. The stack order of controls has additional implications for some of the layout controls that you can use in WPF, as you will see shortly.
Earlier examples used the
, and Margin, HorizontalAlignment
properties to position controls in a VerticalAlignment
container, but without going into much detail about their use. You have also seen how you can use Grid
and Height
to specify dimensions. These properties, along with Width
, which you haven't looked at yet, are useful for all of the layout controls (or most of them, as you will see), but in different ways. Different layout controls can also set default values for these properties. You'll see a lot of this by example in subsequent sections, but before doing that, it is worth covering the basics.Padding
The two alignment properties,
and HorizontalAlignment
, determine how the control is aligned. VerticalAlignment
can be set to HorizontalAlignment
, Left
, Right
, or Center
. Stretch
and Left
tend to position controls to the left or right edges of the container, Right
positions controls in the middle, and Center
changes the width of the control so that its edges reach to the sides of the container. Stretch
is similar, and has the values VerticalAlignment
, Top
, Bottom
, or Center
.Stretch
and Margin
specify the space to leave blank around the edges of controls and inside the edges of controls, respectively. Earlier examples used Padding
to position controls relative to the edges of a window. This worked because with Margin
set to HorizontalAlignment
and Left
set to VerticalAlignment
, the control is positioned tight against the top-left corner, and Top
inserted a gap around the edge of the control. Margin
is used similarly, but spaces out the content of a control from its edges. This is particularly useful for Padding
, as you will see in the next section. Both Border
and Padding
can be specified in four parts (in the form Margin
, leftAmount
, topAmount
, rightAmount
) or as a single value (a bottomAmount
value).Thickness
Later, you will see how
and Height
are often controlled by other properties. For example, with Width
set to HorizontalAlignment
, the Stretch
property of a control changes as the width of its container changes.Width
The
control is a very simple, and very useful, container control. It holds a single child, not multiple children like the more complicated controls you'll look at in a moment. This child will be sized to completely fill the Border
control. This might not seem particularly useful, but remember that you can use the Border
and Margin
properties to position the Padding
within its container, and the content of the Border
within the edges of the Border
. You can also set, for example, the Border
property of a Background
so that it is visible. You will see this control in action shortly.Border
The
control, as previously noted, provides complete freedom over control positioning. Another thing about Canvas
is that the Canvas
and HorizontalAligment
properties used with a child element will have no effect whatsoever over the positioning of those elements.VerticalAlignment
You can use
to position elements in a Margin
as it was done in earlier examples, but a better way is to use the Canvas
, Canvas.Left
, Canvas.Top
, and Canvas.Right
attached properties that the Canvas.Bottom
class exposes:Canvas
<Canvas…>
<Button Canvas.Top="10" Canvas.Left="10"…>Button1</Button>
</Canvas>
The preceding code positions a
so that its top edge is 10 pixels from the top edge of the Button
, and its left edge is 10 pixels from the left edge of the Canvas
. Note that the Canvas
and Top
properties take precedence over Left
and Bottom
. For example, if you specify both Right
and Top
, then the Bottom
property is ignored.Bottom
Figure 14.3 shows two
controls positioned in a Rectangle
control, with the window resized to two sizes.Canvas
All of the example layouts in this section can be found in the LayoutExamples project in the downloadable code for this chapter. See the “Wrox.com
Code Downloads for this Chapter” section at the beginning of this chapter for information on how to download this chapter's code.
One
is positioned relative to the top-left corner, and one is positioned relative to the bottom-right corner. As you resize the window, these relative positions are maintained. You can also see the importance of the stacking order of the Rectangle
controls. The bottom-right Rectangle
is higher up in the stacking order, so when they overlap this is the control that you see.Rectangle
The code for this example is as follows (you can find it in the downloaded code at
):LayoutExamplesCanvas.xaml
<Window x:Class="LayoutExamples.Canvas"
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:LayoutExamples"
mc:Ignorable="d"
Title="Canvas" Height="300" Width="300">
<Canvas Background="AliceBlue">
<Rectangle Canvas.Left="50" Canvas.Top="50" Height="40" Width="100"
Stroke="Black" Fill="Chocolate"/>
<Rectangle Canvas.Right="50" Canvas.Bottom="50" Height="40" Width="100"
Stroke="Black" Fill="Bisque"/>
</Canvas>
</Window>
The
control, as its name suggests, enables you to dock controls to one of its edges. This sort of layout should be familiar to you, even if you've never stopped to notice it before. It is how, for example, the DockPanel
control in Word remains at the top of the Word window, or how the various windows in Visual Studio are positioned. In Visual Studio you can also change the docking of windows by dragging them around.Ribbon
has a single attached property that child controls can use to specify the edge to which controls dock: DockPanel
. You can set this property to DockPanel.Dock
, Left
, Top
, or Right
.Bottom
The stack order of controls in a
is extremely important, as every time you dock a control to an edge you also reduce the available space of subsequent child controls. For example, you might dock a toolbar to the top of a DockPanel
and then a second toolbar to the left of the DockPanel
. The first control would stretch across the entire top of the DockPanel
display area, but the second control would only stretch from the bottom of the first toolbar to the bottom of the DockPanel
along the left edge.DockPanel
The last child control you specify will (usually) fill the area that remains after all the previous children have been positioned. (You can control this behavior, which is why this statement is qualified.)
When you position a control in a
, the area occupied by the control might be smaller than the area of the DockPanel
that is reserved for the control. For example, if you dock a DockPanel
with a Button
of 100, a Width
of 50, and a Height
of HorizontalAlingment
to the top of a Left
, then there will be space to the right of the DockPanel
that isn't used by other docked children. In addition, if the Button
control has a Button
of 20, then a total of 90 pixels at the top of the Margin
will be reserved (the height of the control plus the top and bottom margins). You need to take this behavior into account when you use DockPanel
for layout; otherwise, you can end up with unexpected results.DockPanel
Figure 14.4 shows a sample
layout.DockPanel
The code for this layout is as follows (you can find it in the downloadable code at
):LayoutExamplesDockPanels.xaml
<Window x:Class="LayoutExamples.DockPanels"
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:LayoutExamples"
mc:Ignorable="d"
Title="DockPanels" Height="300" Width="300">
<DockPanel Background="AliceBlue">
<Border DockPanel.Dock="Top" Padding="10" Margin="5"
Background="Aquamarine" Height="45">
<Label>1) DockPanel.Dock="Top"</Label>
</Border>
<Border DockPanel.Dock="Top" Padding="10" Margin="5"
Background="PaleVioletRed" Height="45" Width="200">
<Label>2) DockPanel.Dock="Top"</Label>
</Border>
<Border DockPanel.Dock="Left" Padding="10" Margin="5"
Background="Bisque" Width="200">
<Label>3) DockPanel.Dock="Left"</Label>
</Border>
<Border DockPanel.Dock="Bottom" Padding="10" Margin="5"
Background="Ivory" Width="200" HorizontalAlignment="Right">
<Label>4) DockPanel.Dock="Bottom"</Label>
</Border>
<Border Padding="10" Margin="5" Background="BlueViolet">
<Label Foreground="White">5) Last control</Label>
</Border>
</DockPanel>
</Window>
This code uses the
control introduced earlier to clearly mark out the docked control regions in the example layout, along with Border
controls to output simple informative text. To understand the layout, you must read it from top to bottom, looking at each control in turn:Label
Border
control is docked to the top of the DockPanel
. The total area taken up in the DockPanel
is the top 55 pixels (Height
+ 2 × Margin
). Note that the Padding
property does not affect this layout, as it is inside the edge of the Border
, but this property does control the positioning of the embedded Label
control. The Border
control fills any available space along the edge it is docked to if not constrained by Height
or Width
properties, which is why it stretches across the DockPanel
.Border
control is also docked to the top of the DockPanel
, and takes up another 55 pixels from the top of the display area. This Border
control also includes a Width
property, which causes the border to take up only a portion of the width of the DockPanel
. It is positioned centrally, as the default value for HorizonalAlignment
in a DockPanel
is Center
.Border
control is docked to the left of the DockPanel
and takes up 210 pixels of the left of the display.Border
control is docked to the bottom of the DockPanel
and takes up 30 pixels plus the height of the Label
control it contains (whatever that is). This height is determined by the Margin
, Padding
, and contents of the Border
control, as it is not specified explicitly. The Border
control is locked to the bottom-right corner of the DockPanel
, as it has a HorizontalAlignment
of Right
.Border
control fills the remaining space.Run this example and experiment with resizing content. Note that the further up the stacking order a control is, the more priority is given to its space. By shrinking the window, the fifth
control can be completely obscured by controls further up the stacking order. Be careful when using Border
control layout to avoid this, perhaps by setting minimum dimensions for the window.DockPanel
You can think of
as being a slimmed down version of StackPanel
, where the edge to which child controls are docked is fixed for those controls. The other difference between these controls is that the last child control of a DockPanel
doesn't fill the remaining space. However, controls will, by default, stretch to the edges of the StackPanel
control.StackPanel
The direction in which controls are stacked is determined by three properties.
can be set to Orientation
or Horizontal
, and Vertical
and HorizontalAlignment
can be used to determine whether control stacks are positioned next to the top, bottom, left, or right edge of the VerticalAlignment
. You can even make the stacked controls stack at the center of the StackPanel
using the StackPanel
value for the alignment property you use.Center
Figure 14.5 shows two
controls, each of which contains three buttons. The top StackPanel
has its StackPanel
property set to Orientation
and the bottom one has Horizontal
set to Orientation
.Vertical
The code used here is as follows (you can find it in the downloaded code at
):LayoutExamplesStackPanels.xaml
<Window x:Class="LayoutExamples.StackPanels"
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:LayoutExamples"
mc:Ignorable="d"
Title="StackPanels" Height="300" Width="300">
<Grid>
<StackPanel HorizontalAlignment="Left" Height="128" VerticalAlignment="Top"
Width="284" Orientation="Horizontal">
<Button Content="Button" Height="128" VerticalAlignment="Top"
Width="75"/>
<Button Content="Button" Height="128" VerticalAlignment="Top"
Width="75"/>
<Button Content="Button" Height="128" VerticalAlignment="Top"
Width="75"/>
</StackPanel>
<StackPanel HorizontalAlignment="Left" Height="128" VerticalAlignment="Top"
Width="284" Margin="0,128,0,0" Orientation="Vertical">
<Button Content="Button" HorizontalAlignment="Left" Width="284"/>
<Button Content="Button" HorizontalAlignment="Left" Width="284"/>
<Button Content="Button" HorizontalAlignment="Left" Width="284"/>
</StackPanel>
</Grid>
</Window>
is essentially an extended version of WrapPanel
; controls that “don't fit” are moved to additional rows (or columns). Figure 14.6 shows a StackPanel
control containing multiple shapes, with the window resized to two sizes.WrapPanel
The code to achieve this effect is shown here (you can find it in the downloaded code at
):LayoutExamplesWrapPanel.xaml
<Window x:Class="LayoutExamples.WrapPanel"
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:LayoutExamples"
mc:Ignorable="d"
Title="WrapPanel" Height="92" Width="260">
<WrapPanel Background="AliceBlue">
<Rectangle Fill="#FF000000" Height="50" Width="50" Stroke="Black"
RadiusX="10" RadiusY="10"/>
<Rectangle Fill="#FF111111" Height="50" Width="50" Stroke="Black"
RadiusX="10" RadiusY="10"/>
<Rectangle Fill="#FF222222" Height="50" Width="50" Stroke="Black"
RadiusX="10" RadiusY="10"/>
<Rectangle Fill="#FFFFFFFF" Height="50" Width="50" Stroke="Black"
RadiusX="10" RadiusY="10"/>
</WrapPanel>
</Window>
controls are a great way to create a dynamic layout that enables users to control exactly how content should be viewed.WrapPanel
controls can have multiple rows and columns that you can use to lay out child controls. You have used Grid
controls several times already in this chapter, but in all cases you used a Grid
with a single row and a single column. To add more rows and columns, you must use the Grid
and RowDefinitions
properties, which are collections of ColumnDefinitions
and RowDefinition
objects, respectively, and are specified using property element syntax:ColumnDefinition
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
…
</Grid>
This code defines a
control with two rows and two columns. Note that no extra information is required here; with this code, each row and column is dynamically resized automatically as the Grid
control resizes. Each row will be a third of the height of the Grid
, and each column will be half the width. You can display lines between cells in a Grid
by setting the Grid
property to Grid.ShowGridlines
.true
You can also define rows and columns in the grid by clicking the edges of the grid in the Design View. If you move the mouse pointer to the edge of the grid, a yellow line is drawn across the Design View; if you click the edge, the necessary XAML is inserted. When you do this, the
and Width
properties of the rows and columns are always set by the designer, but you can delete them or drag the lines to suit your needs.Height
You can control the resizing with the
, Width
, Height
, MinWidth
, MaxWidth
, and MinHeight
properties. For example, setting the MaxHeight
property of a column ensures that the column stays at that width. You can also set the Width
property of a column to Width
, which means “fill the remaining space after calculating the width of all other columns.” This is actually the default. When you have multiple columns with a *
of Width
, then the remaining space is divided between them equally. The *
value can also be used with the *
property of rows. The other possible value for Height
and Height
is Width
, which sizes the row or column according to its content. You can also use Auto
controls to enable users to customize the dimensions of rows and columns by clicking and dragging.GridSplitter
Child controls of a
control can use the attached Grid
and Grid.Column
properties to specify which cell they are contained in. Both these properties default to Grid.Row
, so if you omit them, then the child control is placed in the top-left cell. Child controls can also use 0
and Grid.ColumnSpan
to be positioned over multiple cells in a table, where the upper-left cell is specified by Grid.RowSpan
and Grid.Column
.Grid.Row
Return to the example from the beginning of the chapter with the two buttons and follow these steps.
Grid
control by clicking in the XAML View.Grid.Row
and Grid.Column
properties to the button. Change the Grid.Row
and Grid.Column
attached properties to 0
.Margin
property to make the button fully visible in the cell.Margin
property from the second button.GridSplitter
control to the XAML View just before the closing tag of the Grid
control and set its properties like this:<GridSplitter Grid.RowSpan="2" Width="3" BorderThickness="2" BorderBrush="Black"/>
<Window x:Class="Ch14Ex01.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:Ch14Ex01"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" KeyDown="Window_KeyDown"
PreviewKeyDown="Window_PreviewKeyDown">
<Grid KeyDown="Grid_KeyDown" PreviewKeyDown="Grid_PreviewKeyDown">
<Grid.RowDefinitions>
<RowDefinition Height="109*"/>
<RowDefinition Height="210*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="191*"/>
<ColumnDefinition Width="326*"/>
</Grid.ColumnDefinitions>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left"
Margin="27,4,0,0" VerticalAlignment="Top" Width="75" Grid.Column="0"
Grid.Row="0"/>
<Button x:Name="rotatedButton" Content="2nd Button" Width="75" Height="22"
FontWeight="Bold" RenderTransformOrigin="0.5,0.5"
KeyDown="rotatedButton_KeyDown"
PreviewKeyDown="rotatedButton_PreviewKeyDown" Grid.Column="1"
Grid.Row="1" >
<Button.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="-23.896"/>
<TranslateTransform/>
</TransformGroup>
</Button.RenderTransform>
</Button>
<GridSplitter Grid.RowSpan="2" Width="3" BorderThickness="2"
BorderBrush="Black"/>
</Grid>
</Window>
Figure 14.7 shows the application running with the splitter pushed to two positions.
By dividing the grid into two columns and two rows, you have changed how the controls can be positioned in the grid. When you set the
and Grid.Row
to Grid.Column
for the first button, you move it from its previous position on the form to the top-left section.0
The second button more or less stays put, but when you drag the
slider, you see that the margin of the button is now relative to the left edge of the column in which it is placed, meaning that it slides across the window as you move the slider.GridSplitter
Now that you know the basics of what it means to work with WPF and Visual Studio, it is time to start working with the controls to create something useful. The remaining sections of this chapter and Chapter 15 are dedicated to writing a game client for the card game you have been developing over the previous chapters. You are going to use a lot of controls to write the game client, and you are even going to write one yourself.
In this chapter you are going to write the supporting dialog boxes of the game — this includes the About, Options, and New Game windows.
An About window, or About box as it's sometimes called, is used to display information about the developer of the application and the application itself. Some About windows are quite complex, like the one found in Microsoft Office applications and Visual Studio, and display version and licensing information. By convention, the About window can be accessed from the Help menu where it is usually the last item on the list.
Figure 14.8 shows a screenshot of the finished dialog box that you are about to create.
An About window is not something that the user is going to see very often. In fact, the reason that it is usually located on the Help menu is that it is very often only used when the user needs to find information about the version of the application or who to contact when something is wrong. But this also means that it is something the user has a specific purpose for visiting and if you include such a window in your application, you should treat it as important.
Whenever you are designing an application, you should strive to keep the look and feel as consistent as possible. This means that you should stick to a few select colors and use the same styling of controls everywhere in the application. In the case of Karli Cards, you are going to work with three main colors — red, black, and white.
If you look at Figure 14.8 you will see that the top-left corner of the window is occupied by a Wrox Press logo. You have not used images before, but adding a few select images to your applications can make the user interface look more professional.
is a very simple control that can be used to great effect. It allows you to display a single image and to resize this image as you see fit. The control exposes two properties, as shown in Table 14.2.Image
Table 14.2 Image Control
Property | Description |
|
Use this property to specify the location of the image. This can be a location on disk or somewhere on the web. As you will see in Chapter 15, it is also possible to create a static resource and use it as the source. |
|
It's actually pretty rare to have an image that is exactly the right size for your purpose, and sometimes the size of the image must change as the application window is resized. You can use this property to control how the image behaves. There are four possibilities: — The image doesn't resize. — The image resizes to fill the entire space. This may contort the image. — The image keeps its aspect ratio and doesn't fill the available space if this would change the aspect ratio. — The image keeps its aspect ratio and fills the available space. If keeping the ratio means that some of the image is too large for the space available, the image is clipped to fit. |
You have already seen this most simple of controls used in some of the previous examples. It displays simple text information to the user and in some cases relays information about shortcut keys. The control uses the
property to display its text. The Content
control displays text on a single line. If you prefix a letter with an underscore “_” character, the letter will become underlined and it will then be possible to access the control directly by using the prefixed letter and Alt. For example, Label
assigns the shortcut Alt+N to any control directly following the label._Name
Like
, this control displays simple text without any complicated formatting. Unlike the Label
, the Label
control is capable of displaying multiple lines of text. It is not possible to format individual parts of the text.TextBlock
The
displays the text even if it will not fit in the space granted to the control. The control itself does not provide any scrollbars in this case, but it can be wrapped in a handy view control when needed: the TextBlock
.ScrollViewer
Like the
control, you have already seen quite a bit of the Label
control. This control is used everywhere and is easily recognized on a user interface. Your users will expect that they can left-click it to perform an action — no more and no less. Altering this behavior will most likely lead to bad interface design and frustrated users.Button
By default, the button displays itself with a single short line of text or an image that describes what happens when you click on it.
The button does not contain any properties to display images or text, but you can use the
property to display simple text or embed an Content
control in the content to display an image. You can find this code in the downloaded code at Image
:Ch14Ex01ImageButton.xaml
<Button HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Margin="10" >
<StackPanel Orientation="Horizontal">
<Image Source=".ImagesDelete_black_32x32.png" Stretch="UniformToFill"
Width="16" Height="16"/>
<TextBlock>Delete</TextBlock>
</StackPanel>
</Button>
The image for the button is included in the code download in
.Ch14Ex01Images
Figure 14.9 shows the Delete button with text and an image.
To complete the following example, you need an image for a banner. This image is included in the download for this chapter in
.KarliCards GuiImagesBanner.png
Before you can start the About window, you need a project to work on. This is just one of many windows you are going to make in this and the next chapter, so go ahead and create a new WPF application project and name it
. Name the solution KarliCards Gui
.KarliCards
About.xaml
.Height=
"300
" Width=
"434
" MinWidth=
"434
" MinHeight=
"300
"
ResizeMode=
"CanResizeWithGrip
"
Grid
and create four rows by clicking at the edges of the grid. Don't worry too much about the exact positioning of the rows; instead change the values like this:<Grid.RowDefinitions>
<RowDefinition Height="58"/>
<RowDefinition Height="20"/>
<RowDefinition/>
<RowDefinition Height="42"/>
</Grid.RowDefinitions>
Canvas
control from the Toolbox into the top-most row. Remove any properties inserted by Visual Studio and add this:Grid.Row="0" Background="#C40D42"
Height="56" Canvas.Left="0" Canvas.Top="0" Stretch="UniformToFill"
Source=".ImagesBanner.png"
Images
.Canvas
and drag a Label
control onto it. Change its properties like this:Canvas.Right="10" Canvas.Top="25" Content="Karli Cards" Foreground="#FFF7EFEF"
FontFamily="Times New Roman"
Grid
and drag a new canvas control onto it. Change its properties to:Grid.Row="1" Background="Black"
Canvas
control and drag a Label
onto it. Change its properties like this:Canvas.Left="5" Canvas.Top="0" FontWeight="Bold" FontFamily="Arial"
Foreground="White"
Content="Karli Cards (c) Copyright 2012 by Wrox Press and all readers"
Grid
again, and drag the last Canvas
into the bottom-most row. Change its properties like this:Grid.Row="3"
Canvas
control and drag a Button
onto it. Change its properties to this:Content="_OK" Canvas.Right="12" Canvas.Bottom="10" Width="75"
Grid
again, and drag a StackPanel
into the last center row. Change its properties to:Grid.Row="2"
StackPanel
and drag two Label
controls and one TextBlock
into it, in that order.Label
like this:Content="CardLib and Idea developed by Karli Watson" HorizontalAlignment="Left"
VerticalAlignment="Top" Padding="20,20,0,0" FontWeight="Bold"
Foreground="#FF8B6F6F"
Label
like this:Content="Graphical User Interface developed by Jacob Hammer"
HorizontalAlignment="Left" Padding="20, 0,0,0" VerticalAlignment="Top"
FontWeight="Bold" Foreground="#FF8B6F6F"
TextBlock
like this:Text="Karli Cards developed with Visual C# 6 for Wrox Press.
You can visit Wrox Press at http://www.wrox.com."
Margin="0, 10,0,0" Padding="20,0,0,0" TextWrapping="Wrap"
HorizontalAlignment="Left" VerticalAlignment="Top" Height="39"
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
App.xaml
file and change the name MainWindow.xaml
to About.xaml
.You begin by setting some properties on the window. By setting
and MinWidth
, you prevent the user from resizing the window to a point where it obscures the content. The MinHeight
is set to ResizeMode
, which displays a small grip section in the bottom-right corner of the window that indicates to the user that the window can be resized.CanResizeWithGrip
Next you add four rows to the grid. By doing this, you define the basic structure of the window. By setting rows 1, 2, and 4 to fixed heights, you ensure that only the third row can change height; this is the row that holds the content.
Then you add the first
control. This provides you with a handy place to set the background color of the first row. By ensuring that the canvas has no specific size, you force the canvas to fill the top row in the grid.Canvas
The
control that is added to the canvas is fixed to the left and top edges of the canvas. This ensures that as the window resizes, the image stays put. You also gave the image a fixed height, but left the width open. With the Image
property set to Stretch
, this allows the UniformToFill
control to use the height as a guide for the aspect ratio. The control simply changes its width to match the scale specified by the height and aspect ratio.Image
For the final part of the first row you add a single
control and bind it to the top-right edge of the canvas, ensuring that when the window resizes, the Label
moves with the right edge.Label
Then you start on the second row, which is filled by another
control that has a Canvas
added to it.Label
The bottom
is more of the same, but this time you add a button to it and bind that button to the bottom-right side of the canvas. This ensures that when the window is resized, the button sticks to the bottom-right side of the window. The underscore “_” before the text OK creates a Alt+O shortcut for the button.Canvas
Finally, you add a
to the third row and add StackPanel
and a Labels
control to it. By setting the TextBlock
of the first label to 20, 20, 0, 0, you push the content of the control down from the row above by 20 pixels and out from the left edge, also by 20 pixels.Padding
The padding of the next label is set to 20,0,0,0, which pushes the content out from the edge because the space between the two labels is fine and doesn't need any extra space.
The
was then introduced. The property TextBlock
is set to TextWrapping
, which causes the text to wrap if it can't fit on a single line. As the window resizes and the line becomes longer, the text is automatically fitted into as few lines as needed. Both the Wrap
and Margin
properties are used here. The Padding
property is set so it pushes the entire control down 10 pixels from the labels above, and the Margin
is set so it pushes the content of the control in by 20 pixels from the left edge.Padding
The code in the event handler closes the window. In this case, this is the same as closing the entire application, because in Step 19 you changed the startup window to be the About window, so closing it is the same as closing the application.
The next window you are going to create is the Options window. This window will allow the players to set a number of parameters that will alter the game play. It will also allow you to use some controls that you haven't used yet: the
, CheckBox
, RadioButton
, ComboBox
, and TextBox
controls.TabControl
Figure 14.10 shows the window with the first tab selected. At first glance the window looks much like the About window, but there is a lot more to do on this window.
Previously in this chapter you used the
and Label
controls. These controls are designed exclusively for displaying text to the user. The TextBlock
control allows the user to type text into the application. Although it can just display text as well, you should not use it for this purpose unless the user is allowed to edit the displayed text. If you decide that you want to display text using a textbox, be sure to set its TextBox
property to IsEnabled
to prevent users from being able to edit it.false
You control how the text is displayed and can be entered into the
using a number of properties shown in Table 14.3.TextBox
Table 14.3 TextBox Properties
Property | Description |
|
The text currently displayed in the control. |
|
When this is set to , the user can edit the text in the . When it is , the text is grayed out and the user cannot give focus to the control. |
|
Sometimes you want the to display only a single line of text. In this case, you can set this property to . This is the default. If you want your text to be displayed on multiple lines, you can set it to either or . will cause the text that extends beyond the edge of the box to be moved to the line below. will in some cases allow very long words to extend beyond the edge if no suitable breakpoint can be determined. |
|
If your allows the user to enter multiple lines of text, then the user can potentially type text that will disappear below the lower edge of the box. In that case, it's a good idea to display a scrollbar. Set this to if you want the scrollbar to appear only if the text is too long to be displayed. Set it to to always display it, and or to never display a scrollbar. |
|
This property controls how text can be entered into the control. If you set this to , which is the default, then the user can't break the line with a Return. |
present the users with options that they can select or clear. You should use a CheckBoxes
if you have want to present an option to the users that can be turned on or off, or want the users to answer yes or no to a question. For example, in the Options dialog box, you want the user to answer to decide whether they should play against the computer. To this end a CheckBox
with the text “Play Against Computer” is used.CheckBox
A
is designed to be used as a single entity that is unaffected by other CheckBox
on the view. You will sometimes see CheckBoxes
used in a way that links them together so that selecting one causes another to become cleared, but this is not the intended use for this control. If you want this functionality, you should use a CheckBoxes
, described in the next section.RadioButton
can also display a third state, which is known as “indeterminate” and is supposed to indicate that the yes/no answer could not be answered. This state is commonly used when a CheckBoxes
is used to show information about something else. For example, CheckBox
are sometimes used to indicate whether all child nodes in a Tree View are selected. In this case, the CheckBoxes
will be selected if all nodes are selected, cleared if none are, and indeterminate if some, but not all, are selected.CheckBox
Table 14.4 lists the properties commonly used to control the
control.CheckBox
Table 14.4 CheckBox Properties
Property | Description |
|
The is a control and its display can therefore be heavily customized. Adding a text to the property yields the default view. |
|
Used to indicate if the control can have two or three states. The default is , meaning that only two possible values exist. |
|
This is either or . By default, setting it to displays a checkmark. If is , null is possible and indicates that the state is indeterminate. |
are used with other RadioButtons
to allow users to choose between multiple options where only one can be selected at any time. You should use RadioButtons
when you want the users to answer a question that has a very limited number of possible values. If there are more than four or five possible values, you should consider using a RadioButtons
or a ListBox
instead. In the Options window you will create shortly, the user can choose the skill level of the computer player. There are three options: Dumb, Good, and Cheats. Only one should ever be selected at any given time.ComboBox
When more than one
is displayed in the same view they will by default know about each other and as soon as any one of them is selected, all the others are cleared. If you have multiple unrelated RadioButton
on the same view, they can be grouped together to avoid controls clearing the values of unrelated controls.RadioButtons
You can control
with the properties listed in Table 14.5.RadioButtons
Table 14.5 RadioButton Properties
Property | Description |
|
are controls and can therefore have their display modified. By default, you enter a text in the . |
|
This is either or . If is , is possible and indicates that the state is indeterminate. |
|
The name of the group the control belongs to. By default this is empty and any without a is considered in the same group. |
Like the
and RadioButton
controls, CheckBox
allow users to select exactly one option. However, ComboBoxes
are fundamentally different from the other two in two ways:ComboBoxes
ComboBoxes
display the possible choices in a drop-down list.
are commonly used to display long lists of values, such as country or state names, but they can be used for many purposes. In the Options dialog box, a ComboBoxes
is used to display a list from which the user can choose the number of players. Although this could just as well have been done using ComboBox
, the use of a RadioButtons
saves space in the view.ComboBox
A
can be changed to display itself with a ComboBox
at the top that allows the users to type any values that they feel are missing. One of the exercises of this chapter asks you to add a TextBox
to the Options dialog box from which the users can either type their name or select it from a list.ComboBox
The two properties —
and IsReadOnly
— are very important for the behavior of the control and work together to provide four possible ways for the user to select the value of the IsEditable
using the keyboard (see Table 14.6):ComboBox
Table 14.6 IsReadOnly and IsEditable Combinations
IsReadOnly is true | IsReadOnly is false | |
is
|
The is displayed but the control does not react to key presses. If a selection is made in the list, the text can be selected in the . |
The is displayed and the user can type anything she wishes. If something is typed that is in the list, it is selected. The control will display the best possible match as the user is typing. |
is
|
When is , no longer has any effect because the is not displayed. When the control is selected, the user can select a value from the list by typing but it is not possible to type a value that isn't in the list. |
A
is an Items control, which means that you can add multiple items to it. Table 14.7 shows additional properties for the ComboBox control.ComboBox
Table 14.7 Other ComboBox Properties
ComboBox Property | Description |
|
The property represents the text displayed at the head of the . It is either an element of the list or a new text typed by the user. |
|
Represents the index of the selected item in the list. If this is –1 then no selection is made. This is also the case if the user has typed something that was not in the list. |
|
Represents the actual item of the list, not just the index or the text. If nothing is selected or the user has typed something new, this returns null. |
The
is radically different than the other controls presented this section. It is a layout control that is used to group controls on pages that can be selected by clicking on them.TabControl
Tab controls are used when you want to display a lot of information in a single window but don't want to clutter the view too much. In this case, you should divide the information into groups of related items and create a single page for each group. Generally speaking, you should never allow controls on one page to affect controls on another page. If you do so anyway, the user will not realize that something has changed on another page and will be confused when settings change behind her back.
By default each page is constructed of
that, by default, are populated by a single TabItems
control, but you can change the Grid
to any other control as you see fit. On each tab, you can lay out your UI and, by selecting the Grid
, you can change between the tabs. Each TabItems
has a TabItem
that can be used to display the tab itself. This can be used as a Header
control, meaning that you can customize how the header is displayed so that it can be more than just a text.Content
The first thing that you probably notice when you see the Options window is that it looks remarkably like the About window, and that is true. Because of that, it is possible to reuse at least some of the code from the previous example.
Options.xaml
.Grid
control that is inserted by default.About.xaml
window described earlier, copy the Grid
control and all its content, and paste it into the new Options.xaml
file.Title="Options" Height="345" Width="434" ResizeMode="NoResize"
StackPanel
and all of its content.Canvas
control with the Grid.Row
property set to 3
and all of its content.Label
control from the Canvas
control with the Grid.Row
property set to 1
.Label
control in the Canvas
with the Grid.Row
property set to 0
like this:<Label Canvas.Right="10" Canvas.Top="13" Content="Options" Foreground="#FFF7EFEF"
FontFamily="Times New Roman" FontSize="24" FontWeight="Bold"/>
StackPanel
into the bottom row and set its properties to this:Grid.Row="3" Orientation="Horizontal" FlowDirection="RightToLeft"
StackPanel
like this: <Button Content="_Cancel" Height="22" Width="75" Margin="10,0,0,0"
Name="cancelButton"/>
<Button Content="_OK" Height="22" Width="75" Margin="10,0,0,0"
Name="okButton"/>
TabControl
into the second row and set its properties like this:Grid.RowSpan="2" Canvas.Left="10" Canvas.Top="2" Width="408" Height="208"
Grid.Row="1"
Header
property of each of the two TabItem
controls to Game
and Computer Player
, respectively.
Your window now looks like Figure 14.11 and it is time to insert some content into the tab items.
TabItem
and drag a CheckBox
control onto it. Set its properties like this:Content="Play against computer" HorizontalAlignment="Left" Margin="11,33,0,0"
VerticalAlignment="Top" Name="playAgainstComputerCheck"
Label
control and then a ComboBox
control into the TabItem
and set their properties like this: <Label Content="Number of players" HorizontalAlignment="Left"
Margin="10,54,0,0" VerticalAlignment="Top"/>
<ComboBox HorizontalAlignment="Left" Margin="196,58,0,0"
VerticalAlignment="Top" Width="86" Name="numberOfPlayersComboBox"
SelectedIndex="0" >
<ComboBoxItem>2</ComboBoxItem>
<ComboBoxItem>3</ComboBoxItem>
<ComboBoxItem>4</ComboBoxItem>
</ComboBox>
TabItem
with the header Computer Player
. Drag a Label
and three RadioButtons
onto the Grid
and set their properties like this: <Label Content="Skill Level" HorizontalAlignment="Left"
Margin="10,10,0,0" VerticalAlignment="Top"/>
<RadioButton Content="Dumb" HorizontalAlignment="Left"
Margin="37,41,0,0" VerticalAlignment="Top" IsChecked="True"
Name="dumbAIRadioButton"/>
<RadioButton Content="Good" HorizontalAlignment="Left"
Margin="37,62,0,0" VerticalAlignment="Top" Name="goodAIRadioButton"/>
<RadioButton Content="Cheats" HorizontalAlignment="Left"
Margin="37,83,0,0" VerticalAlignment="Top"
Name="cheatingAIRadioButton"/>
App.xaml
file and change StartupUri
to Options.xaml
.The window's
is set to ResizeMode
. You can therefore position the controls without regard to what happens if the window changes size, because the user can no longer resize the window.NoResize
The
in Step 9 has a new property, StackPanel
, which is set to FlowDirection
. This causes the two buttons that are added to it to cling to the right edge of the dialog box rather than the left edge that is the default. Interestingly, this also changes the meaning of the RightToLeft
property of the two buttons, causing Margin
and Left
to be swapped.Right
The
on the second tab are set up without specifying a RadioButtons
, which causes them to be grouped together. You set the GroupName
property to IsChecked
on the first one, which makes this the default selection.true
The window looks fine at this point, and there are even a few things users can do with it, although nothing happens when a setting is changed. Users expect that the options they choose are stored and used by the application. You could do this by storing the values of the controls in the window, but this is not very flexible and mixes the data of the application with the GUI, which is not a good idea. Instead, you should create a class to hold the selections made by the users.
In this example, you will add a new class to the project that will contain the selections made by the user and handle events that happen as the user changes selections.
GameOptions.cs
.using System;
namespace KarliCards_Gui
{
[Serializable]
public class GameOptions
{
public bool PlayAgainstComputer { get; set; }
public int NumberOfPlayers { get; set; }
public int MinutesBeforeLoss { get; set; }
public ComputerSkillLevel ComputerSkill { get; set; }
}
[Serializable]
public enum ComputerSkillLevel
{
Dumb,
Good,
Cheats
}
}
Options.xaml.cs
code-behind file and add a private
field to hold the GameOptions
instance: private GameOptions _gameOptions;
using System.IO;
using System.Windows;
using System.Xml.Serialization;
namespace KarliCards_Gui
{
/// <summary>
/// Interaction logic for Options.xaml
/// </summary>
public partial class Options : Window
{
private GameOptions _gameOptions;
public Options()
{
if (_gameOptions == null)
{
if (File.Exists("GameOptions.xml"))
{
using (var stream = File.OpenRead("GameOptions.xml"))
{
var serializer = new XmlSerializer(typeof(GameOptions));
_gameOptions = serializer.Deserialize(stream) as GameOptions;
}
}
else
_gameOptions = new GameOptions();
}
InitializeComponent();
}
RadioButtons
to add the Checked
event handler to the code-behind file. Change the handlers like this: private void dumbAIRadioButton_Checked(object sender, RoutedEventArgs e)
{
_gameOptions.ComputerSkill = ComputerSkillLevel.Dumb;
}
private void goodAIRadioButton_Checked(object sender, RoutedEventArgs e)
{
_gameOptions.ComputerSkill = ComputerSkillLevel.Good;
}
private void cheatingAIRadioButton_Checked(object sender, RoutedEventArgs e)
{
_gameOptions.ComputerSkill = ComputerSkillLevel.Cheats;
}
TextBox
on the Game tab. Click the lightning icon on the Properties panel and double-click the GotFocus
event to add the handler to the code-behind file. private void timeAllowedTextBox_GotFocus(object sender, RoutedEventArgs e)
{
timeAllowedTextBox.SelectAll();
}
TextBox
again in Design View and add the PreviewMouseLeftButtonDown
event handler to the code-behind file. private void timeAllowedTextBox_PreviewMouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
var control = sender as TextBox;
if (control == null)
return;
Keyboard.Focus(control);
e.Handled = true;
}
The new class is currently just a number of properties that store the values from the Options window. It is marked as
to make it possible to save it to a file.Serializable
The
event of a Checked
is raised whenever the user selects it. You handle this event in order to set the value of the RadioButton
property of the ComputerSkillLevel
instance.GameOptions
Data binding is a way of declaratively connecting controls with data. In the Options window, you handled the
event of the Checked
in order to set the value of the RadioButtons
property in the ComputerSkillLevel
class. This works well, and you can use code and event handling to set all the values you have in a window, but very often it is better to bind the properties of your controls directly to the data.GameOptions
A binding consists of four components:
You don't always set all of these elements explicitly; particularly the binding target is very often implicitly specified by the fact that you are setting a binding to a property on a control.
The binding source is always set in order to make a binding work, but it can be set in several ways. In the following sections and in Chapter 15, you are going to see several ways of binding data from sources.
A
control defines a data source that can be used for data binding on all child elements of an element. You will often have a single instance of a class that holds most of the data that is used in a view. If this is the case you can set the DataContext
of the window to the instance of that object, which makes you able to bind properties from that class in your view. This is demonstrated in the “Dynamic Binding to External Objects” section.DataContext
You can bind to any .NET object that has the data you need as long as the compiler can locate the object. If the object is found in the same context, that is the same XAML block, as the control using the object, you can specify the binding source by setting the
property of the binding. Take a look at this changed ElementName
from the Options window:ComboBox
<ComboBox HorizontalAlignment="Left" Margin="196,58,0,0" VerticalAlignment="Top"
Width="86" Name="numberOfPlayersComboBox" SelectedIndex="0"
IsEnabled="{Binding ElementName=playAgainstComputerCheck, Path=IsChecked}" >
Notice the
property. Instead of specifying IsEnabled
or true
, there is now lengthy text within a couple of curly brackets. This way of specifying property values is called markup extension syntax, and is a shorthand for specifying properties. The same could have been written like this:false
<ComboBox HorizontalAlignment="Left" Margin="196,58,0,0"
VerticalAlignment="Top" Width="86" Name="numberOfPlayersComboBox"
SelectedIndex="0" >
<ComboBox.IsEnabled>
<Binding ElementName="playAgainstComputerCheck"
Path="IsChecked"/>
</ComboBox.IsEnabled>
Both examples set the binding source to the playAgainstComputerCheck
. The source property is specified in the CheckBox
to be the Path
property.IsChecked
The binding target is set to the
property. Both examples do this by the specifying the binding as the content of the property — they just do it using different syntax. Finally, the binding target is implicitly specified by the fact that the binding is done on the IsEnabled
.ComboBox
The binding in this example causes the
property of the IsEnabled
to be set or cleared depending on the value of the ComboBox
property of the IsChecked
. The result is that without any code, the CheckBox
is enabled and disabled when the user changes the value of the ComboBox
.CheckBox
It is possible to create object instances on the fly by specifying that a class is used as a resource in the XAML. This is done by adding a namespace to the XAML to allow the class to be located, and then declaring the class as a resource on an element in the XAML.
You can create resource references on parent elements of the object that you want to data bind.
In this example you create a new class to hold the data for the
in the Options window and bind it to the control.ComboBox
NumberOfPlayers.cs
.using System.Collections.ObjectModel;
namespace KarliCards_Gui
{
public class NumberOfPlayers : ObservableCollection<int>
{
public NumberOfPlayers()
: base()
{
Add(2);
Add(3);
Add(4);
}
}
}
Options.xaml
file's Design View and select the Window
root element.Canvas
element that contains the ComboBox
and add this code below it, and above the TabControl
declaration. <Canvas.Resources>
<local:NumberOfPlayers x:Key="numberOfPlayersData"/>
</Canvas.Resources>
ComboBox
and remove the three ComboBoxItems
from it.ItemsSource="{Binding Source={StaticResource numberOfPlayersData}}"
There is a lot happening in this example. The class
derives from a special collection named NumberOfPlayers
. This base class is a collection that has been extended to make it work better with WPF. In the constructor of the class, you add the values to the collection.ObservableCollection
Next you create a new resource on the
. You could have created this resource on any parent element of the Canvas
. When a resource is specified on an element, all child elements can use it.ComboBox
Finally you set the
to a binding. The ItemsSource
property is specifically designed to allow you to specify a binding for the collection of items on an Items control. In the binding you just need to specify the binding source. The binding target, target property, and source property settings are handled by the ItemsSource
property.ItemsSource
Now you can bind to objects that are created on the fly as they are needed in order to provide some data. What if you already have an instantiated object that you want to use for data binding? In that case, you need to do a little plumbing in the code.
In the case of the Options window, you don't want the options to be cleared every time the window is opened, and you want the selections the user made to persist and be used in the rest of the application.
You can do this in code by setting the value of the
property to the instance.DataContext
In this example you bind the remaining controls to the
instance in the Options window.GameOptions
Options.xaml.cs
code-behind file.InitializeComponent()
, add this line:DataContext = _gameOptions;
GameOptions
class and change it like this:using System;
using System.ComponentModel;
namespace KarliCards_Gui
{
[Serializable]
public class GameOptions : INotifyPropertyChanged
{
private bool _playAgainstComputer = true;
private int _numberOfPlayers = 2;
private ComputerSkillLevel _computerSkill = ComputerSkillLevel.Dumb;
public int NumberOfPlayers
{
get { return _numberOfPlayers; }
set
{
_numberOfPlayers = value;
OnPropertyChanged(nameof(NumberOfPlayers));
}
}
public bool PlayAgainstComputer
{
get { return _playAgainstComputer; }
set
{
_playAgainstComputer = value;
OnPropertyChanged(nameof(PlayAgainstComputer));
}
}
public ComputerSkillLevel ComputerSkill
{
get { return _computerSkill; }
set
{
_computerSkill = value;
OnPropertyChanged(nameof(ComputerSkill));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
[Serializable]
public enum ComputerSkillLevel
{
Dumb,
Good,
Cheats
}
}
Options.xaml
and select the CheckBox
. Add the IsChecked
property like this:IsChecked="{Binding Path=PlayAgainstComputer}"
ComboBox
and change it like this, removing the SelectedIndex
property and changing the ItemsSource
and SelectedValue
properties:<ComboBox HorizontalAlignment="Left" Margin="196,58,0,0" VerticalAlignment="Top"
Width="86" Name="numberOfPlayersComboBox"
ItemsSource="{Binding Source={StaticResource numberOfPlayersData}}"
SelectedValue="{Binding Path=NumberOfPlayers}"/>
Click
event handler to the code-behind file. Do the same with the Cancel button and add this code to the handlers: private void okButton_Click(object sender, RoutedEventArgs e)
{
using (var stream = File.Open("GameOptions.xml", FileMode.Create))
{
var serializer = new XmlSerializer(typeof(GameOptions));
serializer.Serialize(stream, _gameOptions);
}
Close();
}
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
_gameOptions = null;
Close();
}
Setting the
of the window to an instance of DataContext
allows you to bind to this instance simply by specifying the property to use in the binding. This is done in Steps 4 and 5. Note that the GameOptions
is filled with items from a static resource, but the selected value is set in the ComboBox
instance.GameOptions
The
class is changed quite a bit. It now implements the GameOptions
interface, which means that the class is now able to inform WPF that a property has changed. In order for this notification to work, you have to call the subscribers to the INotifyPropertyChanged
event defined by the interface. For this to happen, the property setters have to actively call them, which is done using the helper method PropertyChanged
.OnPropertyChanged
When the
method is called, we use a new expression introduced by C# 6: OnPropertyChanged
. When we call nameof
…nameof(
with an expression, it will retrieve the name of the final identifier. This is particularly useful in the case of the )
method, because it takes the name of the property that is being changed as a string.OnPropertyChanged
The OK button event handler saves the settings to disk using an
. The XmlSerializer
event handler sets the game options field to null, ensuring that the selections made by the user are cleared. Both event handlers close the window.Cancel
You are now only one window short of having created all the supporting windows in the game. The last window before creating the game board is a window where the player can add new players and select the players who will be participating in a new game. This window will use a
to display the names of the players.ListBox
and ListBoxes
can often be used for the same purpose, but where a ComboBoxes
normally allows you to select only a single entry, ComboBox
often allows the user to select multiple items. Another key difference is that a ListBoxes
will display its content in a list that is always expanded. This means that it takes up more real estate on the window, but it allows the user to see the options available right away.ListBox
Table 14.8 lists a few particularly interesting properties for the ListBox control.
Table 14.8 Interesting ListBox Properties
Property | Description |
|
This property controls how the user can select items from the list. There are three possible values: , which allows the user to select only one item, , which allows the user to select multiple items without holding down the Ctrl key, and , which allows the user to select multiple consecutive items by holding down the Shift key, and non-consecutive items by holding down the Ctrl key. |
|
Gets or sets the first selected item or null if nothing is selected. Even if multiple items are selected, only the first item is returned. |
|
Gets a list containing the items that are currently selected. |
|
Works like , but returns the index instead of the item itself and –1 instead of null if nothing is selected. |
This window is displayed to the players when a new game starts. It will allow the players to enter their names and select them from a list of known players.
StartGame.xaml
.Grid
element from the window and copy the main Grid
and its content from the Options.xaml
window instead.Canvas
control that has its Grid.Row
property set to 1
.Height="345" Width="445" ResizeMode="NoResize"
GameOptions.cs file
and add these fields at the top of the class: private ObservableCollection<string> _playerNames =
new ObservableCollection<string>();
public List<string> SelectedPlayers { get; set; }
System.Collections.Generic
and the System.Collections.ObjectModel
namespaces, so include these:using System.Collections.Generic;
using System.Collections.ObjectModel;
SelectedPlayers
collection: public GameOptions()
{
SelectedPlayers = new List<string>();
}
public ObservableCollection<string> PlayerNames
{
get
{
return _playerNames;
}
set
{
_playerNames = value;
OnPropertyChanged("PlayerNames");
}
}
public void AddPlayer(string playerName)
{
if (_playerNames.Contains(playerName))
return;
_playerNames.Add(playerName);
OnPropertyChanged("PlayerNames");
}
StartGame.xaml
window.ListBox
, two Labels, a TextBox
, and a Button
to the grid below the Canvas
in grid row 1 and change the controls to look like those shown in Figure 14.12.
Name
property of the controls as shown in Table 14.9.
Table 14.9 The Name Property
Control | Name |
|
|
|
|
|
|
ItemsSource
of the ListBox
like this:ItemsSource="{Binding Path=PlayerNames}"
ListBox
's SelectionChanged
event handler to the code-behind file and add this code: private void playerNamesListBox_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (_gameOptions.PlayAgainstComputer)
okButton.IsEnabled = (playerNamesListBox.SelectedItems.Count == 1);
else
okButton.IsEnabled = (playerNamesListBox.SelectedItems.Count ==
_gameOptions.NumberOfPlayers);
}
IsEnabled
property of the OK button to false
. private GameOptions _gameOptions;
Options.xaml.cs
code-behind (though not the name) and add these lines to the end after InitializeComponent
(Note: You will need to add using declarations for System.IO
and System.Xml.Serialization
): if (_gameOptions.PlayAgainstComputer)
playerNamesListBox.SelectionMode = SelectionMode.Single;
else
playerNamesListBox.SelectionMode = SelectionMode.Extended;
Click
event handler. Add this code: private void addNewPlayerButton_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(newPlayerTextBox.Text))
_gameOptions.AddPlayer(newPlayerTextBox.Text);
newPlayerTextBox.Text = string.Empty;
}
Options.xaml.cs
code-behind files to this code-behind. foreach (string item in playerNamesListBox.SelectedItems)
{
_gameOptions.SelectedPlayers.Add(item);
}
App.xaml
file and change the StartupUri
to StartGame.xaml
.You started by adding code to the
class that holds information about all the known players and the current selection made in the GameOptions
window.StartGame
The
's ListBox
property is the same as you saw on the ItemsSource
earlier. But where you were able to bind the selected value of the ComboBox
directly to a value, it is more complicated with a ComboBox
. If you try to bind the ListBox
property you will find that it is read-only and therefore can't be used for data binding. The work-around used here is to use the OK button to store the values through code. Note that the cast to SelectedValues
works here because the content of the IList<string>
is strings at the moment, but if you decided to change the default behavior and display something else, then this selection of items must be changed as well.ListBox
The
's ListBox
event is raised whenever something happens that changes the selection. In this case you want to handle this event to check if the number of items selected is correct. If the game is to be played against a computer, then there can only be one human player; otherwise the correct number of human players must be selected.SelectionChanged
Chapter 15 discusses the Styles, Control, and Item templates and shows why you can't always know what type the content of a control is.
14.1 A
control can be used to display large amounts of text, but the control does not provide any way to scroll the text itself if the text extends beyond the viewport. By combining the TextBlock
with another control, create a window that contains a TextBlock
with a lot of text that can be scrolled and where the scrollbar appears only if the text extends beyond the viewport.TextBlock
14.2 The
and Slider
controls have a few things in common, such as a minimum, maximum, and current value. Using only data binding on the Progress
, create a window with a slider and a progress bar, where the ProgressBar
control controls the minimum, maximum, and current value of the progress bar.Slider
14.3 Change the
in the previous question to display itself diagonally from the bottom-left corner to the top-right corner of the window.ProgressBar
14.4 Create a new class with the name
and three properties: PersistentSlider
, MinValue
, and MaxValue
. The class must be able to participate in data binding and all the properties must be able to notify bound controls of changes.CurrentValue
PersistentSlider
and initialize it with some default values.Minimum
, Maximum
, and Value
properties to the data source.Answers to the exercises can be found in Appendix A.
Key Concept | Description |
XAML | XAML is a language that uses XML syntax and enables controls to be added to a user interface in a declarative, hierarchical way. |
Data binding | You can use data binding to connect properties of controls to the value of other controls. You can also define resources and use code defined in classes outside your views as a data source for both values of properties and as content for controls. can be used to specify the binding source of existing object instances and thereby allow you to bind to instances that are created in other parts of your application. |
Routed events | Routed events are special events used in WPF. They come in two flavors: bubbling and tunneling. Bubbling events are first called on the control on which they are activated and then bobble up through the view tree to the root element. Tunneling events move the other way, from the root element to the control that was activated by the user. Both bubbling and tunneling can be stopped by setting the property of the event arguments to . |
INotifyPropertyChanged | The interface is implemented by a class that will be used from a WPF view. When property setters of the class are called, they raise the event with the name of the property that changed its value. Any control property that is bound to the property that raised the event will be notified of the change and can update itself accordingly. |
ObservableCollections | An is a collection that, among others, implement the interface. You use this specialized collection when you want to provide properties or values that are lists to a WPF view for data binding. |
Content controls | Content controls can contain a single control in their content. An example of such a control is . This control can be or ; they allow you to create complex customizations. |
Items controls | Items controls can contain a list of controls in their content. An example of such a control is the . Each control in the list can be customized. |
Layout controls | You learned to use a number of controls that are used to help you create the view:
|
UI controls | UI controls display themselves on the view, often using the layout controls to guide their positions. These controls were used:
|