Archives

Archives / 2010 / January
  • Set the MinWidth and MinHeight based on SizeToContent Property

    We are developing a WPF application for a client where they want to be able to use the SizeToContent=”WidthAndHeight” attribute on the window. This attribute is great because it allows WPF to size the window to the size of the controls on the window. This is especially important as they need the labels to grow and shrink when they localize the labels for English, Spanish and German. Once the initial size is set by WPF, they want that initial size to be the minimum height and minimum width for that window. As it turns out, this is very easy to accomplish in WPF.

    The Form
    Figure 1 shows a sample form that uses a <Grid> control and row and columns for the labels, text boxes and buttons. Using a grid control allows your controls to grow and shrink as you re-size the window.

    Set Initial Width/Height
    Figure 1: Setting the initial MinWidth and MinHeight properties

    Take a look at the XAML for this window.

    <Window x:Class="SetInitialMinWidthHeight.winMain"
            x:Name="winTest"
            xmlns="http://schemas.microsoft.com/winfx/2006
                         /xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Set Initial MinWidth/MinHeight"
            WindowStartupLocation="CenterScreen"
            MinWidth="{Binding ElementName=winTest,
                               Path=InitialMinWidth}"
            MinHeight="{Binding ElementName=winTest,
                                Path=InitialMinHeight}"
            SizeToContent="WidthAndHeight"
            Loaded="Window_Loaded">
      <Window.Resources>
         <!-- SOME RESOURCES HERE -->
      </Window.Resources>
      <Grid Margin="10">
        <Grid.RowDefinitions>
          <!-- ROW DEFINITIONS HERE -->
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
          <!-- COLUMN DEFINITIONS HERE -->
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0"
                   Grid.Column="0"
                   Text="First Name" />
        <TextBox Grid.Row="0"
                 Grid.Column="1"
                 x:Name="txtFirstName" />
        <TextBlock Grid.Row="1"
                   Grid.Column="0"
                   Text="Last Name" />
        <TextBox Grid.Row="1"
                 Grid.Column="1"
                 x:Name="txtLastName" />
        <Button Grid.Row="2"
                Grid.Column="1"
                Content="Submit" />
      </Grid>
    </Window>

    Most of this window is pretty normal grid layout XAML code, but take a look at the MinWidth, MinHeight and SizeToContent properties. Notice that the SizeToContent property is set to WidthAndHeight. This allows the window to size itself based on how much room the child controls take up. Take a look at the binding used for the MinWidth and MinHeight properties. Each of these two properties are binding to Dependency properties that have been created on the window itself. Notice that the binding refers to the “winTest”, which has been set as the x:Name of the Window.

    The Dependency Properties

    In order to data-bind from XAML to a property on your form you need to create a Dependency Property. Both the “InitialMinHeight” and “InitialMinWidth” properties that are being bound are dependency properties. You create dependency properties using the DependencyProperty.Register() static method. To create our InitialMinHeight property you first create a static DependencyProperty with the name InitialMinHeightProperty. Notice the “Property” suffix added at the end.

    C#
    protected static DependencyProperty InitialMinHeightProperty =
      DependencyProperty.Register(
        "InitialMinHeight", typeof(double), typeof(winMain));

    protected static DependencyProperty InitialMinWidthProperty =
      DependencyProperty.Register(
        "InitialMinWidth", typeof(double), typeof(winMain));

    VB
      Protected Shared InitialMinHeightProperty As DependencyProperty _
        = DependencyProperty.Register("InitialMinHeight", _
                              GetType(Double), GetType(winMain))

      Protected Shared InitialMinWidthProperty As DependencyProperty _
        = DependencyProperty.Register("InitialMinWidth", _
                              GetType(Double), GetType(winMain))

    Once you have created these dependency properties, you now create normal properties. However, in the getter and setters you call the GetValue and SetValue methods that are part of the WPF Window class.

    C#
    protected double InitialMinHeight
    {
      get { return (Double)GetValue(InitialMinHeightProperty); }
      set { SetValue(InitialMinHeightProperty, value); }
    }

    protected double InitialMinWidth
    {
      get { return (Double)GetValue(InitialMinWidthProperty); }
      set { SetValue(InitialMinWidthProperty, value); }
    }

    VB
    Protected Property InitialMinHeight() As Double
      Get
        Return DirectCast(GetValue(InitialMinHeightProperty), _
                           Double)
      End Get
      Set(ByVal value As Double)
        SetValue(InitialMinHeightProperty, value)
      End Set
    End Property

    Protected Property InitialMinWidth() As Double
      Get
        Return DirectCast(GetValue(InitialMinWidthProperty), _
                           Double)
      End Get
      Set(ByVal value As Double)
        SetValue(InitialMinWidthProperty, value)
      End Set
    End Property

    Now that you have created these properties, you can now set them during the Loaded event procedure.

    C#
    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
      // Set dependency properties after form's height/width
      // has been set initially
      InitialMinWidth = this.ActualWidth;
      InitialMinHeight = this.ActualHeight;
    }

    VB
    Private Sub Window_Loaded(ByVal sender As Object, _
      ByVal e As RoutedEventArgs)
      ' Set dependency properties after form's width/height
      ' has been set initially
      InitialMinWidth = Me.ActualWidth
      InitialMinHeight = Me.ActualHeight
    End Sub

    The ActualWidth and ActualHeight properties are set with whatever the values are that have been determined by WPF. You retrieve these values and put them into the dependency properties. The dependency properties allow the values to be updated and put into the MinWidth and MinHeight property of the Window.

    Summary

    In this article you learned how to set an initial minimum width and height. Using dependency properties and data binding you can have a WPF window set its initial size using SizeToContent, save those settings, then set the MinWidth and MinHeight on the Window.

    NOTE: You can download the complete sample code at my website. http://www.pdsa.com/downloads. Choose Tips & Tricks, then "WPF Set Initial Min Width and Height" from the drop-down.

    Good Luck With Your Coding,
    Paul Sheriff

    ** SPECIAL OFFER FOR MY BLOG READERS **
    Visit http://www.pdsa.com/Event/Blog for a free eBook on "Fundamentals of N-Tier".

     

    Read more...

  • Centering Text within a WPF Shape using a Canvas

    In my last blog post, I showed you how to use a Shape control, a Border control and a TextBlock to create rectangles, circles, ellipsis, and triangles with text in the middle of the shape. You also learned how to use a VisualBrush in a TextBlock to help you center the text as well. In this post, you will learn another technique for accomplishing the same task. This blog will show you how to use a Canvas, a Shape and a TextBlock. The reason for using a Canvas is to introduce you to using a Multi-Binding converter in WPF.

    Position a Shape on a Canvas

    As you may know, a Canvas control is used to position a canvas’ child controls at a specified point on the Canvas. You place a shape at a certain point by setting the attached properties “Canvas.Top” and “Canvas.Left” as shown in the following XAML.

    <Canvas Name="cnvMain"
            Width="80"
            Height="80">
      <Ellipse Canvas.Top="20"
               Canvas.Left="40"
               Width="40"
               Height="40"
               Fill="Gray" />
    </Canvas>

    In Figure 1 you can see that the Circle is positioned 20 points below the top of the Canvas and 40 points to the right of the canvas’ leftmost border.

    WPF Shapes Canvas 1

    Figure 1: Position the Circle at a specific point on a Canvas

    Place a Shape within a Canvas

    Most of the time, you want a shape to take up the same amount of width and height as the canvas. So, you can simply set the Width and Height properties of both the Canvas control and the Shape control to be exactly the same. Since the Shape control is the child within the Canvas, it will fill up the whole canvas. Note that you do not need to specify the Canvas.Top and Canvas.Left as the default for these is zero (0) for any child control placed in a Canvas.

    <Canvas Name="cnvMain"
            Width="80"
            Height="80">
      <Ellipse Width="80"
               Height="80"
               Fill="Gray" />
    </Canvas>

    Bind Ellipse Width and Height to Canvas

    The problem with the above XAML is that if you wish to change the width and the height of the canvas and you want the shape to be the exact same height, you need to change four numbers. Instead of hard-coding the numbers on the Shape control, you can take advantage of the element-to-element binding features of WPF. Below is an example of binding the Width and Height of the Ellipsis to the Width and Height of the Canvas. The XAML below will produce the circle shown in Figure 2.

    <Canvas Name="cnvMain"
            Width="80"
            Height="80">
      <Ellipse Width="{Binding ElementName=cnvMain,
                               Path=ActualWidth}"
               Height="{Binding ElementName=cnvMain,
                                Path=ActualHeight}"
               Fill="Gray" />
    </Canvas>

    WPF Shapes Canvas 2 
    Figure 2: A circle bound to the same height and width as the Canvas on which it is placed.

    Using a Multi-Binding Converter

    Now that you have learned to place a Shape within a Canvas, you now need to put a TextBlock in the center of the Canvas so you can have words display in the middle of the Shape. To center a TextBlock in the middle of a Canvas you need to take the Width of the Canvas and subtract the Width of the TextBlock, then divide this by 2 to get the value for the Left property. You use the same calculation for the Height to get the Top property of where to position the TextBlock.

    It would be really convenient if you could use an expression in a WPF binding since you could then perform this calculation, but you can’t. So, instead, you need to pass the width of the Canvas and the width of the TextBlock to some code you write, and also the height of the Canvas and TextBlock. This is where a multi-binding converter comes in very handy.

    Consider the following XAML:

    <Canvas Name="cnvMain"
            Width="40"
            Height="40">
      <Rectangle Name="rectSize"
                 Fill="Blue"
                 Width="{Binding ElementName=cnvMain,
                                 Path=ActualWidth}"
                 Height="{Binding ElementName=cnvMain,
                                  Path=ActualHeight}"
                 Stroke="Black"
                 StrokeThickness="2" />
      <TextBlock Name="tbSize"
                 Foreground="White"
                 Text="Test">
        <Canvas.Left>
         <MultiBinding
           Converter="{StaticResource MidValue}">                       
           <Binding ElementName="cnvMain"
                    Path="ActualWidth" />
           <Binding ElementName="tbSize"
                    Path="ActualWidth" />
         </MultiBinding>
        </Canvas.Left>
        <Canvas.Top>
         <MultiBinding Converter="{StaticResource MidValue}">                       
           <Binding ElementName="cnvMain"
                    Path="ActualHeight" />
           <Binding ElementName="tbSize"
                    Path="ActualHeight" />
         </MultiBinding>
        </Canvas.Top>
      </TextBlock>
    </Canvas>

    Notice the code in the above XAML that sets the <Canvas.Left> within the <TextBlock> control. This code, shown below, is responsible for passing data to our multi-binding converter.

    <TextBlock ...>
      <Canvas.Left>
        <MultiBinding Converter="{StaticResource MidValue}">                       
          <Binding ElementName="cnvMain"
                   Path="ActualWidth" />
          <Binding ElementName="tbSize"
                   Path="ActualWidth" />
        </MultiBinding>
      </Canvas.Left>
    </TextBlock>

    Just like a normal binding sets one specific value, a <MultiBinding> allows you to pass more than one value to a converter class. Notice the “Converter={…}” attribute uses a StaticResource called Midvalue. This static resource is defined in the Window.Resources section of this window as follows:

    <Window.Resources>
      <src:MidpointValueConverter x:Key="MidValue" />
    </Window.Resources>

    The MidPointValueConverter class implements the IMultiValueConverter interface. This interface defines the Convert and ConvertBack methods. It is your job to write the code to take the multiple inputs that are passed to the “values” parameter to the Convert method and return a single value that can be used to set the Left property.

    C#

    using System;
    using System.Globalization;
    using System.Windows.Data;

    public class MidpointValueConverter : IMultiValueConverter
    {
      #region Convert/ConvertBack Methods
      public object Convert(object[] values, Type targetType,
        object parameter, CultureInfo culture)
      {
        double extra = 0;

        if (values == null || values.Length < 2)
        {
          throw new ArgumentException("The MidpointValueConverter
                class requires 2 double values to be passed to it.
                First pass the Total Overall Width, then the
                Control Width to Center.", "values");
        }

        double totalMeasure = (double)values[0];
        double controlMeasure = (double)values[1];

        if (parameter != null)
          extra = System.Convert.ToDouble(parameter);

        return (object)(((totalMeasure - controlMeasure) / 2) + extra);
      }

      public object[] ConvertBack(object value, Type[] targetTypes,
        object parameter, CultureInfo culture)
      {
        throw new NotImplementedException();
      }
      #endregion
    }

    Visual Basic

    Imports System
    Imports System.Globalization
    Imports System.Windows.Data

    Public Class MidpointValueConverter
      Implements IMultiValueConverter

    #Region "Convert/ConvertBack Methods"
      Public Function Convert(ByVal values As Object(), _
         ByVal targetType As Type, ByVal parameter As Object, _
         ByVal culture As CultureInfo) As Object _
          Implements IMultiValueConverter.Convert
        Dim extra As Double = 0

        If values Is Nothing OrElse values.Length < 2 Then
          Throw New ArgumentException("The MidpointValueConverter
              class requires 2 double values to be passed to it.
              First pass the TotalWidth, then the Width.", "values")
        End If

        Dim totalWidth As Double = CDbl(values(0))
        Dim width As Double = CDbl(values(1))

        If parameter IsNot Nothing Then
          extra = System.Convert.ToDouble(parameter)
        End If

        Return DirectCast((((totalWidth - width) / 2) + extra), Object)
      End Function

      Public Function ConvertBack(ByVal value As Object, _
        ByVal targetTypes As Type(), ByVal parameter As Object, _
        ByVal culture As CultureInfo) As Object() _
         Implements IMultiValueConverter.ConvertBack
        Throw New NotImplementedException()
      End Function
    #End Region
    End Class

    In the MidPointValueConverter class you grab the two values passed in, which are the total width/height of the Canvas and then the total width/height of the TextBlock control. You can then subtract those two values and divide by 2 to get the location of either the Left or the Top property.

    Notice that you can also pass in an “extra” value in the “parameter” parameter. This extra value can be used if you want to move the TextBlock control down or to the right depending on the shape you use. For example, if you are using a Triangle with the point of the triangle at the top, you might want to move the text a little lower into the widest part of the triangle instead of right in the middle of the triangle. Or, if you have a triangle where the point is to the right, then you might want to move the text a little over to the left. You set this “extra” parameter as the ConverterParameter attribute on the <MultiBinding> as shown in the following XAML.

    <Canvas Name="cnvMain2"
            Width="50"
            Height="50">
      <Polygon Points="25,0 0,40 50,40"
               Fill="LightBlue"
               Stroke="Black"
               StrokeThickness="2"></Polygon>
      <TextBlock Name="tbSize2"
                 Foreground="Black"
                 Text="Avatar">
        <Canvas.Left>
         <MultiBinding Converter="{StaticResource MidValue}"
                       ConverterParameter="1">
           <Binding ElementName="cnvMain2"
                    Path="ActualWidth" />
           <Binding ElementName="tbSize2"
                    Path="ActualWidth" />
         </MultiBinding>
        </Canvas.Left>
        <Canvas.Top>
         <MultiBinding Converter="{StaticResource MidValue}"
                       ConverterParameter="7">
           <Binding ElementName="cnvMain2"
                    Path="ActualHeight" />
           <Binding ElementName="tbSize2"
                    Path="ActualHeight" />
         </MultiBinding>
        </Canvas.Top></TextBlock>
    </Canvas>

    Figure 3 shows the results of the rectangle with text in it, and the result of using a polygon with the ConverterParameter set.

    WPF Shapes Canvas 3 
    Figure 3: Shapes, Text and Canvas using Multi-Binding

    Summary

    This article introduced you to using a Canvas, Shape and TextBlock control to create shapes with text centered within the shape. In addition, you learned the basics of creating a Multi-Binding value converter in WPF to help you perform the centering. You will find a lot of uses for value converters when designing your WPF applications. Sometimes you will just need to convert a single value, but sometimes you will find it necessary to take several values and return a single value. You simply need to create classes that implement either an IValueConverter interface or the IMultiValueConverter interface.

    Read more...