WPF in action

Although WPF doesn't have as many controls as Windows Forms, it includes standard controls such as check boxes, drop-down lists, and radio buttons. It also includes a range of new controls, both for advanced layout and entirely new user interface components. WPF also covers a wide range of areas beyond traditional user interfaces, including document support and 3D drawing. Even though it doesn't have all the controls that Windows Forms does, it still does an awful lot. It's extremely useful for developing Windows applications from IronPython if you're prepared to target .NET 3.0.7 Although most of the documentation and online tutorials focus on XAML, which often isn't the best way of working with WPF from IronPython, most of the features are as straightforward to use from code as the last example.

In this section, you'll create a WPF application using a selection of controls, both new and old. Although this application itself won't win any design awards, it does show you how to use a useful range of WPF controls. The finished application looks like figure 9.5.

Wxpython Combobox Grid

Figure 9.5 A selection of WPF controls in a grid

6 From Microsoft product page: http://www.microsoft.com/expression/products/overview.aspx?key=blend.

7 Which will be an extra dependency on platforms older than Vista.

Figure 9.5 A selection of WPF controls in a grid

6 From Microsoft product page: http://www.microsoft.com/expression/products/overview.aspx?key=blend.

7 Which will be an extra dependency on platforms older than Vista.

You can see that the controls are laid out in a grid. The code for each of the controls is covered in its own section, starting with the basic framework of the application.

9.2.1 Layout with the Grid

The most important component in this user interface is the Grid. This is contained in a border with curved edges, purely for visual effect, and has visible grid lines so that you can see how the controls are contained within it. Listing 9.4 is the constructor for ControlsExample8 and the methods it calls to create the main window. Controls-Example itself is a subclass of Window.

Listing 9.4 Controls example framework with Grid in Window import clr clr.AddReference("PresentationFramework")

clr.AddReference("PresentationCore")

clr.AddReference("windowsbase")

from System.Windows import ( Window, Thickness, HorizontalAlignment, SizeToContent, CornerRadius

from System.Windows.Controls import ( Grid, ColumnDefinition, RowDefinition, Label, Border

from System.Windows.Media import Brushes class ControlsExample(Window):

def _init_(self): I Sets up grid grid = self .getGrid() <1-'

grid.Background = GetLinearGradientBrush()

self . createControls (grid) <1- Creates rest of UI

border = Border () <1-, border.BorderThickness = Thickness (5) I Creates border border.CornerRadius = CornerRadius(10)

border.BorderBrush = Brushes.Blue border.Background = Brushes.Yellow border.Padding = Thickness(5)

border. Child = grid <1— Puts grid in border self .Content = border <1-,

| Sets border on window self.Title = 'WPF Controls Example'

self.SizeToContent = SizeToContent.Height self .Width = 800

grid.ShowGridLines = True

8 This listing is not the full code. The following section works through more of the code for ControlsExample. You can download it as a single file from the book's website.

grid.ColumnDefinitions.Add(ColumnDefinition()) grid.RowDefinitions.Add(RowDefinition())

label = Label () label.Margin = Thickness(15) label.FontSize = 16 label.Content = "Nothing Yet..."

label.HorizontalAlignment = HorizontalAlignment.Center self.label = label grid.SetColumnSpan(self.label, 3) grid.SetRow(self.label, 0) grid.Children.Add(self.label)

return grid

This code initializes the grid and then calls down to createControls to create the rest of the controls. Note that the grid is contained in the border by setting the Child attribute of the border. Along the way, this code uses a helper function to set a colorful background gradient and another function to place the controls in the grid. These functions, along with the additional imports that they use, are shown in listing 9.5.

] Creates 3 rows | and 3 columns

I Label spans 3 columns

Listing 9.5 Helper functions for the controls example from System.Windows import Point from System.Windows.Controls import ToolTip from System.Windows.Input import Cursors from System.Windows.Media import ( Colors, GradientStop, LinearGradientBrush

def GetLinearGradientBrush():

brush = LinearGradientBrush () <1- Creates brush brush.StartPoint = Point(0,0) brush.EndPoint = Point(1,1) stops = [

(Colors.Yellow, 0.0), (Colors.Tomato, 0.25), (Colors.DeepSkyBlue, 0.75), (Colors.LimeGreen, 1.0)

for color, stop in stops:

brush. GradientStops .Add (GradientStop (color, stop)) <i-, ...

stops def SetGridChild(grid, child, col, row, tooltip): if hasattr(child, 'FontSize'): <-

child.FontSize = 16 child.Margin = Thickness(15) child.Cursor = Cursors.Hand child.ToolTip = ToolTip(Content=tooltip) grid.SetColumn(child, col) grid.SetRow(child, row) grid.Children.Add(child)

Not all elements have FontSize attributes

GetLinearGradientBrush returns a brush (a LinearGradientBrush), which is set as the Background property on the grid. The start and end points of the gradient are set using the Point structure. This isn't the Point from the System.Drawing namespace that we've used before, but a new one. (In fact, if you attempt to use that one, you get the wonderful error message expected Point, got Point.) Although this Point lives in the System.Windows namespace, it's defined in the WindowsBase assembly. This structure is the only reason you need to explicitly add a reference to this assembly at the start of the application. SetGridChild is used to set the controls in the grid. Let's look at how the grid is used. The grid in this application is three-by-three: three rows and three columns. The rows and columns are created by adding RowDefinition and ColumnDefinition to their respective collections on the grid object.

grid.ShowGridLines = True for i in range (3) :

grid.ColumnDefinitions.Add(ColumnDefinition()) grid.RowDefinitions.Add(RowDefinition())

The first row (row zero) contains the top label, spanning across all three columns. The label has to be set in position in the grid and added to the Children collection on the grid.

grid.SetColumnSpan(self.label, 3) grid.SetRow(self.label, 0) grid.Children.Add(self.label)

Later controls are added to the grid by SetGridChild.

grid.SetColumn(child, col) grid.SetRow(child, row) grid.Children.Add(child)

SetGridChild also does a couple of other neat things. It sets a cursor and a tooltip on all the objects it places in the grid. If you move the mouse pointer over the controls, then the mouse pointer becomes a hand and a tooltip for the control is shown.

It's time to look at some of the controls used in this application, starting with a couple of standard controls available in Windows Forms, but have a new implementation for WPF.

CheckBox & ComboBox

9.2.2 The WPF ComboBox and CheckBox

A ComboBox

Figure 9.6 shows the WPF CheckBox and ComboBox.

They're contained in a StackPanel and created by the createComboAndCheck method (listing 9.6).

□CheckBox

Figure 9.6 CheckBox and ComboBox

Listing 9.6 Creating ComboBox and CheckBox from System.Windows.Controls import( StackPanel, CheckBox, ComboBox, ComboBoxItem def createComboAndCheck(self, grid): panel = StackPanel()

label.Content = "CheckBox & ComboBox"

label.FontSize = 16

label.Margin = Thickness(10)

check = CheckBox() check.Content = "CheckBox" check.Margin = Thickness(10) check.FontSize = 16

check. IsChecked = True I C^d™^ , c t. , , _ CheckBox is used def action(s, e) : <-1

checked = check.IsChecked self.label.Content = "CheckBox IsChecked = %s" % checked check.Checked += action check.Unchecked += action combo = ComboBox ()

for entry in ("A ComboBox", "An Item", "The Next One", "Another"):

item.Content = entry | Populates ComboBox item.FontSize = 16 combo.Items.Add(item) combo.SelectedIndex = 0 combo.Height = 2 6 def action(s, e) :

selected = combo.SelectedIndex self.label.Content = "ComboBox SelectedIndex = %s" % selected combo.SelectionChanged += action combo.FontSize = 16 combo.Margin = Thickness(10)

panel.Children.Add(label) panel.Children.Add(c ombo)

panel. Children .Add (check) I .Puts StackPanel

SetGridChild(grid, panel, 0, 1, "ComboBox & CheckBox") <-' in grid

This code is all straightforward. It creates the CheckBox and adds an event handler, called action, to be called when it's checked or unchecked. When action is called, it sets the text on the top label.

Next, the ComboBox is created and populated with ComboBoxItems. Another action event handler is added to the SelectionChanged event, and sets the text on the label when the selection is changed.

Finally, these components are placed in the StackPanel, which is added to the grid in the first column and second row. A StackPanel has no FontSize property, so the Font-Size is set on the individual controls. SetGridChild checks for the presence of the Font-Size property using hasattr, and won't attempt to set the FontSize on the StackPanel.

The ComboBox and CheckBox are basic components in any user interface. WPF also includes other standard controls such as the RadioButton, ListBox, TabControl, TextBox, RichTextBox, TreeView, Slider, ToolBar, and ProgressBar. Although the

API is different than the one in Windows Forms, you can see that the controls are just as easy to use. The best way to start using them is to experiment and read the documentation that provides simple examples. All these controls live in the System. Windows.Controls namespace, and you can find the documentation at http:// msdn2.microsoft.com/en-us/library/system.windows.controls.aspx.

Another old friend is the Image control, which also has its place in the WPF library.

9.2.3 The Image control

One of the reasons we've chosen to show the Image control is to show that, although everything may be shiny and new, it isn't without warts. The first wart is that Image is awkward to use from code. This is largely mitigated by creating the image from XAML, but it doesn't help with the second problem: how you specify the location of the image to be shown. In fact, it makes this problem worse. Before we discuss this, let's look at the code (listing 9.7).

Listing 9.7 Image control import os from System import Uri, UriKind from System.Windows.Controls import Image from System.Windows.Media.Imaging import BitmapImage def createlmage(self, grid): image = Image ( ) Q

image_uri = os.path.join(os.path.dirname(_file_), 'image.jpg') Q

bi.UriSource = Uri(image_uri, UriKind.RelativeOrAbsolute) Q

bi.EndInit()

image.Source = bi Q

SetGridChild(grid, image, 1, 1, "Image")

You can see that this code is verbose. You have to create both a BitmapImage and an Image instance Q. You specify the location of the image file using a Uri Q, which can be done only inside a BeginInit/EndInit block.9 The BitmapImage is set as the Source on the Image instance Q.

It's in specifying the location of the image that the real fun begins. You specify an absolute location on the filesystem Q. This is fine, if a little ugly, from code because you can construct it dynamically. From XAML, it's impossible—unless your application is always going to run from the same location or you dynamically insert the location into the XAML.

One possible alternative is to use the pack URI syntax.10 It's slightly odd, but easy enough to use.

pack://siteoforigin:,,,/directory_name/image.jpg

9 For more detail on the use of images with WPF, this page is a helpful reference: http://msdn2.micro-soft.com/en-us/library/ms748873.aspx.

10 See this page for all the gory details: http://msdn2.microsoft.com/en-us/library/aa970069.aspx.

Unfortunately, this specifies a location relative to the executing assembly of the current application. If you're running the script with IronPython, this means relative to the location of ipy.exe. But creating a custom executable for your own applications is simple, and we explore this topic in chapter 15. If your XAML documents and resources are distributed with your application, then this solution would work. For reference, the equivalent XAML for the image is the following:

<Image Source= "pack: //siteof origin ■.,,,/directory_name/image . jpg" />

Which is a lot simpler than the code in listing 9.7. Later in this chapter, we look at how to use XAML documents from WPF with a high-level document reading control, including fetching images from arbitrary locations.

After the annoying complexity of working with images, let's take a look at one of the new controls, the Expander.

9.2.4 The Expander

The Expander is one of our favorite controls. It can contain other controls, which the user can expand or hide. One use for the Expander is to provide menus as sidebars, as shown in figure 9.7.

The Expander is easy to use; the code from our example application is in listing 9.8.

Listing 9.8 Expander control from System.Windows.Controls import ( Expander, TextBlock, Button

from System.Windows.Media.Effects import ( DropShadowBi tmapE f fec t

def createExpander(self, grid): expander = Expander()

expander.Header = "An Expander Control" <1- Sets title expander.IsExpanded = True contents = StackPanel() textblock = TextBlock(FontSize=16 ,

Text="\r\nSome content for the expander..." "\r\nJust some text you know...\r\n") contents.Children.Add(textblock) button = Button() button. Content = 'Push Me' button.FontSize = 24

button.BitmapEffect = DropShadowBitmapEffect()

Expander Stackpanel
Figure 9.7 Expander controls with contained buttons

button.Margin = Thickness(10) def action(s, e):

self.label.Content = "Button in Expander pressed" button.Click += action <1—

contents.Children.Add(button)

expander.Content = contents <-

Event handler for button

SetGridChild(grid, expander, 2, 1, "Expander")

Sets StackPanel on Expander

The Expander is created and populated with a StackPanel, which contains a TextBlock and a Button. We'll take a closer look at the TextBlock in a moment. The Button has a Click event handler that changes the label, just to prove that it works. Clicking the header hides and shows the contents of the Expander.

NOTE The source code for this application includes another control, which we don't have space to cover here. The InkCanvas is a control that can be drawn on. You can use it to add user annotations, or diagramming capabilities to an application. It's particularly useful on tablet PCs and can be used in conjunction with the InkAnalyzer,11 which provides handwriting recognition.12

Another new WPF component is the ScrollViewer.

9.2.5 The ScrollViewer

The ScrollViewer is a container element. When the children it contains are bigger than the visible area, it provides horizontal and vertical scrollbars as necessary. Figure 9.8 shows it in action.

Listing 9.9 is the code from our example application that uses the ScrollViewer to contain a control and one of the WPF basic shapes with a colored gradient fill. (We thought it looked too good to only use once.)

ZI Playing with the SciollVteweiQQJ Q'öjö

Some Menus

v Some Menus

**

option i

Some M ore Menus

Option 2

Option 1

Option 3

Option 2

Some More Menus

Option :

And Vet More,,.

^ Artd Vet More..,

Option 1

Even More wOOt!

Option 2

Option 1

Option 3

Option 2

Even More wOOt!

Option 3

v

< 1

DGÊ

Figure 9.8 The ScrollViewer control

Figure 9.8 The ScrollViewer control

Listing 9.9 ScrollViewer control with Rectangle and TextBlock from System.Windows import (

HorizontalAlignment, VerticalAlignment, TextWrapping

from System.Windows.Controls import ( ScrollBarVisibility, ScrollViewer

from System.Windows.Shapes import Rectangle def createScrollViewer(self, grid):

11 See http://msdn2.microsoft.com/en-us/library/system.windows.ink.inkanalyzer.aspx.

12 To use handwriting recognition, you need to install the tablet PC SDK. We'd include a URL, but it's horrifi-cally long and easy to find.

scroll = ScrollViewer () <— Creates ScrollViewer scroll.Height = 200

scroll.HorizontalAlignment = HorizontalAlignment.Stretch scroll.VerticalAlignment = VerticalAlignment.Stretch scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto panel = StackPanel() text = TextBlock()

text.TextWrapping = TextWrapping.Wrap text.Margin = Thickness(0, 0, 0, 20)

text.Text = "A ScrollViewer.\r\n\r\nScrollbars appear as and when they are ^ needed...\r\n"

rect = Rectangle()

rect.Fill = GetLinearGradientBrush() rect.Width = 500 rect.Height = 500

panel.Children.Add(text) panel.Children.Add(rect)

scroll.Content = panel

SetGridChild(grid, scroll, 1, 2, "ScrollViewer")

The ScrollViewer contains a StackPanel populated with a TextBlock and a Rectangle. By default, the horizontal scrollbar is disabled, so you enable it by setting HorizontalScrollBarVisibility to ScrollBarVisibility.Auto. So that the Scroll-Viewer fills the available space, you also set the HorizontalAlignment and Vertical-Alignment to the appropriate enumeration member.

It's also worth noting that this code segment sets the margin on the TextBlock, using Thickness created with four arguments rather than a single number (technically doubles—but IronPython casts the integers for you).

The four numbers specify the margins on the left, top, right, and bottom. (The Thickness structure represents a rectangle.) Here you're specifying a bottom margin of 20 pixels.

The last detail from this code is that the colored gradient is set on the rectangle with the Fill attribute. Other brushes are available, like image and tiled drawing brushes,13 and you can add effects like opacity, reflection, and magnification.

One of the most useful features of WPF is its support for text, in both large and small amounts. The next section covers the TextBlock, which is a way of including small amounts of text within your applications.

9.2.6 The TextBlock: a lightweight document control

The TextBlock is designed for displaying small amounts of flow content. Flow content is formatted text that will be automatically reflowed as the container control is resized.

13 As usual, there's a useful page on MSDN providing examples of the different brushes. See http:// msdn2.microsoft.com/en-us/library/aa970904.aspx.

As well as the TextBlock, there are other controls for incorporating whole documents into applications.

You've already used the TextBlock in the ScrollViewer example. There you set the content by setting the Text property, using it as little more than a glorified label. We used a TextBlock rather than a Label so that you could control the TextWrapping.

In listing 9.10, you'll create a TextBlock with flow content, using the programmatic API.14 This example uses classes from the System.Windows.Documents namespace.

Listing 9.10 TextBlock with flow content from System.Windows import TextAlignment from System.Windows.Documents import ( Bold, Hyperlink, Italic, Run

def createTextBlockAndHyperlink (self, grid) : textblock = TextBlock()

textblock.TextWrapping = TextWrapping.Wrap textblock.Background = Brushes.AntiqueWhite textblock.TextAlignment = TextAlignment.Center textblock. Inlines .Add (Bold (Run ("TextBlock") ) ) <— Starts adding content textblock.Inlines.Add(Run(" is designed to be ")) textblock.Inlines.Add(Italic(Run("lightweight")))

textblock.Inlines.Add(Run(", and is geared specifically at integrating ")) textblock.Inlines.Add(Italic(Run("small")))

textblock.Inlines.Add(Run(" portions of flow content into a UI. "))

link = Hyperlink (Run ("A Hyperlink - Click Me"))

self.label.Content = "Hyperlink Clicked" link.Click += action textblock.Inlines.Add(link)

SetGridChild(grid, textblock, 2, 2, "TextBlock")

Documents are another place where XAML is significantly more concise than code. The equivalent XAML for this TextBlock is as follows:

<TextBlock Background="AntiqueWhite" TextWrapping="Wrap" TextAlignment="Center">

<Bold>TextBlock</Bold> is designed to be <Italic>lightweight,</Italic> and is geared specifically at integrating <Italic>small</Italic> portions of flow content into a UI.

<Hyperlink>A Hyperlink - Click Me</Hyperlink> </TextBlock>

Not only is this less work than the code; but, if you're used to creating documents using HTML (or other markups), it's reasonably intuitive. You'll notice that the code has to wrap straight runs of text in the Run class, but this is done automatically in the

14 For a reference to the TextBlock content model, see http://msdn2.microsoft.com/en-us/library/ bb613554.aspx.

Sets up attributes <,_I on TextBlock

1 Click event handler I for Hyperlink

XAML. The only thing the XAML doesn't do for you is set up the Click handler on the hyperlink. This is the same problem that we've already encountered with using XAML from IronPython, and you could solve it using the same trick of setting an x:Name attribute in the XAML. Shortly we'll explore a more general solution to this problem when working with XAML documents from IronPython.

Before we do that, let's look at the other side of the coin—turning WPF objects back into XAML.

9.2.7 The XamlWriter

In the Hello World example, we also looked at the equivalent XAML. The full XAML for the example application we've been using to look at WPF controls would be painful to create by hand. Fortunately, there's an easier way. Listing 9.11 uses a XamlWriter to turn our ControlsExample into XAML.

Listing 9.11 Creating XAML from WPF objects with XamlWriter from System. IO import File from System.Windows.Markup import XamlWriter window = ControlsExample()

text = XamlWriter.Save(window.Content)

File.WriteAllText('out.xaml', text)

The XamlWriter does have some limitations. For example, it can't handle creating XAML for subclasses of WPF objects.15 Our main ControlsExample class is a subclass of Window, so you can only serialize the object tree below the top-level window. This is why you call XamlWriter.Save (a static method) on the window's Content property.

We've already looked at including small amounts of flow content in user interfaces. WPF also provides a powerful way of incorporating whole documents through XPS documents.

+3 -2

Post a comment