COMP 585 Noteset #12 Note: many examples in this section taken or adapted from Pro WPF 4.5 C#, Matthew MacDonald, apress, 2012, pp. 46-48. WPF: More Code vs. Markup The apparently recommended way to use WPF places heavy emphasis on putting as much of the GUI design into XAML as possible, and only using the code-behind files to implement occasional delegates for buttons and menus. Code-only WPF is tedious, but gives the implementer full control over the behavior of the app. Dynamically Loaded XAML One unique aspect of a markup language like XAML is that it can be parsed dynamically or on the fly. Imagine a WPF app that during initialization reads in a XAML file that defines a GUI element in markup. The XAML can be parsed on the fly, converted to executable code, and dynamically linked into the already running app. Code-only app: using System; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.IO; public class Window1 : Window { private Button button1; public Window1() { public Window1(string xamlfile) { this.width = this.height = 285; this.top = this.left = 100; this.title = "Dynamically loaded XAML"; DependencyObject rootelement; using (FileStream fs = new FileStream(xamlfile, FileMode.Open)) { rootelement = (DependencyObject)XamlReader.Load(fs); this.content = rootelement; button1 = (Button) LogicalTreeHelper. FindLogicalNode(rootElement,"button1"); button1.click += button1click; private void button1click(object obj, RoutedEventArgs args) { button1.content = "Thank You!"; [STAThread] public static void Main() { Window1 w = new Window1("DP.xaml"); Application app = new Application(); app.run(w); XAML file "DP.xaml" that defines a DockPanel with Button: <DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Button Name="button1" Margin="30">Please click me.</button> </DockPanel>
The code has for generality chosen to cast the object built from the.xaml file as a DependencyObject. It could also have chosen to cast it as a Panel, which is a more immediate ancestor of DockPanel, or DockPanel itself. See the WPF inheritance hierarchy at the end of noteset #11. A quick summary: Object: root of all C# classes Dispatcher Object: classes that are modified on the same thread that created them. Dependency Object: classes that support Dependency properties. Visual: classes that have a 2D visual representation UIElement: visuals that support layout, input, focus, events (LIFE) FrameworkElement: small expansion of UIElement The using statement (which is distinct from the using directive for importing namespaces) is used for objects from certain classes like File and Font that implement the IDisposable interface and have unmanaged resources that must be disposed of explicitly. The using statement takes care of the dispose. The compiler converts it into an equivalent try/catch. See the MSDN documentation on the C# using statement. The FileStream class is responsible for parsing the XAML content of the file. The System.Windows.LogicalTreeHelper class contains a collection of static methods that are useful for navigating the logical tree of the WPF app. It exposes to the developer some of the DOM (document object model) structure of the app. More on the WPF Build Process: Partial Classes and XAML vs. BAML The sources for a C# WPF app are a mixture of markup and C# code. The end product is an executable. In between are some intermediate steps that require the XAML to be converted to a C# partial class, then compiled and linked with existing C# source. In more detail: XAML is compiled into BAML (binary XML). For example, Window1.xaml is compiled into a temporary file Window1.baml. Also at this step, a Window1.g.cs file is also created, which contains a partial class def for the Window1 class and a constructor that calls the InitializeComponent() method. The compiler then compiles all.cs files (both original source and generated source) and links them into a final executable. At run time, the Window1 constructor is called and the InitializeComponent() method is called, which loads the BAML file dynamically.
Layout The Window class is an example of a WPF Content control. This is part of the class hierarchy for class Window: FrameworkElement Control ContentControl Window Content controls have a Content property that can have only one value. This is in contrast to an ItemsControl like a TreeView: FrameworkElement Control ItemsControl TreeView The WPF layout philosophy is to provide classes that derive from class Panel to implement layout. These Panel objects can be assigned as the value of the Content property of their parent Window object. Panel is a separate derived class path below the FrameworkElement and is therefore a sibling to the Control class. Panels StackPanel: provides a single row or column of elements WrapPanel: provides multiple rows of elements that automatically wrap to the next row DockPanel: docks elements to the edges Grid: provides a flexible table arrangement UniformGrid: like Grid, but all rows have same height, all columns have the same width Canvas: provides absolute positioning, in other words no layout In XAML, the top-level layout of the Window would look like this: <Window x:class="mainwindow" > <Grid> </Grid> </Window> In between the opening and closing Window tags, there can appear exactly one nested tag. That tag implicitly becomes the Content property of the Window. You may have noticed that in Visual Studio, a default WPF app starts with a Window that has a Grid for its Content property, this can be edited to any of the other Panel tags. In C#, the top-level layout would look like this: class MainWindow : Window { private Grid grid; public MainWindow() { grid = new Grid(); this.content = grid; public static void Main() { MainWindow w = new MainWindow();
StackPanel Example Code equivalent: <Window > <StackPanel> <Label>A Button Stack</Label> <Button>Button 1</Button> <Button>Button 2</Button> <Button>Button 3</Button> <Button>Button 4</Button> </StackPanel> </Window> StackPanel p = new StackPanel(); Label x = new Label(); x.content = ; p.children.add(x); Button b = new Button(); b.content = ; p.children.add(b); w.content = p; General Layout Properties Here are a few Control properties that can be set on any FrameworkElement object. When that object is added to a panel with layout logic, the logic can use the value of the property to implement the layout for that Control appropriately. HorizontalAlignment VerticalAlignment Margin MinWidth, MinHeight MaxWidth, MaxHeight Width, Height The general mapping between code and markup is that an object reference in code becomes a tag in XAML, and a property of the object in code becomes an attribute of the opening tag in XAML. XAML Example <Button HorizontalAlignment="Center">Button 1</Button> Code Example Button b = new Button(); b.content = ; b.horizontalalignment = HorizontalAlignment.Center;
Grid The Grid panel is a good general choice for most layouts. It s the default in Visual Studio for a WPF application. It corresponds to a container in Java Swing with a GridBagLayout. There are two parts to defining a Grid panel. First, you define the structure of the Grid by creating row and column definition objects or tags. These are similar to constraints in the Java Swing GridBagLayout. This step implicitly defines the number of rows and columns and the number of positions that are available in the Grid. Next, you add elements to specific positions in the Grid. XAML Example: <Grid> <Grid.RowDefinitions> <RowDefinition> </RowDefinition> <RowDefinition> </RowDefinition> <RowDefinition> </RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition> </ColumnDefinition> <ColumnDefinition> </ColumnDefinition> </Grid.ColumnDefinitions> <Button Grid.Row="0" Grid.Column="0">Top Left</Button> <Button Grid.Row="0" Grid.Column="1">Top Right</Button> <Button Grid.Row="1" Grid.Column="0">Mid Left</Button> <Button Grid.Row="1" Grid.Column="1">Mid Right</Button> <Button Grid.Row="2" Grid.Column="0">Btm Left</Button> <Button Grid.Row="2" Grid.Column="1">Btm Right</Button> </Grid> The content of the RowDefinition and ContentDefinition tags can be empty. You can add settings values here to change the way space is allocated between the rows and columns. After defining the Grid structure, individual controls can be placed into the Grid at specific row-column indexes, using the Grid.Row and Grid.Column attached properties.
Equivalent Code Example: using System; using System.Windows; using System.Windows.Controls; public class LayoutDemo : Window { private Button[,] btns; private String[,] btntxt = { {"Top Left", "Top Right", {"Mid Left", "Mid Right", {"Btm Left", "Btm Right" ; public LayoutDemo() { Grid g = new Grid(); g.rowdefinitions.add(new RowDefinition()); g.rowdefinitions.add(new RowDefinition()); g.rowdefinitions.add(new RowDefinition()); g.columndefinitions.add(new ColumnDefinition()); g.columndefinitions.add(new ColumnDefinition()); this.content = g; btns = new Button[3,2]; for (int i=0; i<btns.getlength(0); i++) { for (int j=0; j<btns.getlength(1); j++) { btns[i,j] = new Button(); btns[i,j].content = btntxt[i,j]; Grid.SetRow(btns[i,j],i); Grid.SetColumn(btns[i,j],j); g.children.add(btns[i,j]); [STAThread] public static void Main() { LayoutDemo w = new LayoutDemo(); Application app = new Application(); app.run(w); Note that row and column values for a Button added to a Grid are examples of attached properties, meaning that they are not native properties of the Button, but are needed when the Button is added to the GUI in the context of a Grid. In the code example, note that since row and column are not native properties, it is not possible to write: Button b = new Button(); b.row = 1; b.column = 0; Instead you must write: Button b = new Button(); Grid.SetRow(b,1); Grid.SetColumn(b,0);
More Control over Grid Rows and Columns You can add attributes to the RowDefinition and ColumnDefinition tags or objects to control absolute or relative sizes. Absolute Sizing: set Width and Height attributes to specific fixed values. Automatic Sizing: set Width and Height to Auto, this is similar to the WinForms AutoSize property. The content of the row or column is examined and the width or height is set to what it needs to be to display its content. Proportional Sizing: Newly available space is divided proportionately across rows and columns. You can also mix/match the three types of sizing. Absolute Sizing: <ColumnDefinition Width="100"></ColumnDefinition> Automatic: <RowDefinition Height="Auto"></RowDefinition> Proportional: <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="2*"></RowDefinition> Span If an element in the grid needs to start at one grid position but span multiple rows/columns, use the RowSpan and ColumnSpan attached properties.
Canvas All of the other panels are layout panels. Canvas is a special panel specifically for no layout. This makes it useful for positioning widgets at fixed predetermined positions, or as a basis for displaying computer graphics based info in terms of graphics primitives like points, lines, shapes, colors, etc. Widgets attached to a Canvas also make use of attached properties Canvas.Left and Canvas.Top to indicate absolute positioning, similar to the row and column attached properties in the case of the Grid panel. Since there is no layout manager, there is no enforcement of any constraints including non-overlap of controls. <Canvas> <Button Canvas.Left="10" Canvas.Top="10">(10,10)</Button> <Button Canvas.Left="120" Canvas.Top="30">(120,30)</Button> <Button Canvas.Left="60" Canvas.Top="80" Width="50" Height="50">(60,80)</Button> <Button Canvas.Left="70" Canvas.Top="120" Width="100" Height="50">(70,120)</Button> </Canvas> Note that Width and Height are native Control properties, not attached properties. As an exercise, convert this layout to code. Also note (as was mentioned earlier), getting a Canvas to respond to mouse events requires giving the Canvas a background color. Special Case: InkCanvas This is a predefined customized Canvas object that implements something similar to the Scribble app that was assigned as a practice problem.