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.

10 Comments

  • Great post!

    Any thoughts on how to programmatically center text around an arbitrary position on a canvas?

  • Found it:
    textBlock.Measure(new Size(double.PositiveInfinity,
    double.PositiveInfinity));
    Canvas.SetLeft(textBlock, position -
    textBlock.DesiredSize.Width/2);

    Thanks anyway!

  • Hi,
    Can you upload the sample project.

    thanks

  • You can get the samples at www.pdsa.com/downloads. Then choose Tips & Tricks from the drop down, then WPF Text and Shapes.

  • What should a person do on Windows Phone 7 which doesn't have 'MultiBinding Converter'?

  • Abel,

    Do a web search on "Multibinding Silverlight". A couple of people have created some code to help multi bind in Silverlight.

    Paul

  • Nice article. I wonder if you have ever tried to position one shape by referring to another? Specifically, I have an ellipse in which I specify Canvas.Top. Now I want to draw another ellipse which has the same Canvas.Top value, but I'd really like to set it by binding to the top of the first ellipse, rather than repeating the value. This is basically what you're doing here, but yours works because the ellipse exposes its width and height, but I can't seem to get at its location?

  • Pete,

    Sorry it took so long to get back to you. Yes, you can do this. You just need to name the original Rectangle and TextBlock, then just setup a binding like the following:

    Canvas.Top="{Binding ElementName=rect1, Path=Canvas.Top}"

    Of course, you will need to remove the from these new ones because you don't want to use the multi-binding to position, you want to use this new binding.

    Paul

  • Long time? That was what I call a rapid response! Unfortunately, the answer was what I had already tried, and it doesn't work:(
    The first hint of failure is that intellisense doesn't help; the property grid doesn't offer me the choice in the binding dialog; the designer simply ignores the binding if I type it by hand, and finally so does the runtime.
    I have a tiny example if you want to spend time with me on it, otherwise I'll let you know if I find a solution.
    Pete
    PS The reason I want to do it is that (in my real application) the first ellipse is placed and sized dynamically based on a converter using realtime data. Since I could bind the second ellipse *dimensions* to the first, it seemed sensible to bind the *placement* as well.

  • Pete,

    That's funny, I did not need the parentheses when I did it. Oh well. Glad you got it working.

    Paul

Comments have been disabled for this content.