Skip to article
C# Quick ID: 1008021

Centering Text on a WPF Shape Using a User Control

WPF excels at creating great looking applications.

Updated

WPF excels at creating great looking applications.

One of the ways you can make your application look great is to add some shapes like circles, triangles and rectangles. However, the shapes in WPF do not allow you to add text (or any content) within the shape, as shapes are not containers.

In a WPF application I am building right now, I have a need to create different shapes and center text within those shapes. The shapes I need are rectangles, circles, ellipses and triangles. The problem is WPF shapes are not containers so you cannot add any text inside of them, but WPF is flexible enough that there are often several ways to work around this. In this article, I will show you a few different methods of centering text in each of these shapes.

Drawing an Ellipse/Circle Using a Border

If you wish to put text into an ellipse or a circle, you can simulate this using a Border control and play with the Width, Padding and CornerRadius properties of the Border control. I used the code snippet below to draw text within an ellipse as shown in Figure 1.

Figure 1: Use a Border control to simulate an ellipse.
<Border CornerRadius="50"
        Width="60"
        Margin="10"
        Padding="4"
        Background="Aquamarine"
        BorderBrush="Black"
        BorderThickness="1">
   <TextBlock HorizontalAlignment="Center"
              Text="Test" />
</Border>

You can set the width and height of the Border control to create a circle as well. The XAML in the next snippet creates a circle with text in the middle, as shown in Figure 2.

Figure 2: Use a Border control to simulate a circle.
<Border CornerRadius="50"
        Width="60"
        Height="60"
        Margin="10"
        Padding="0,20,0,0"
        Background="Aquamarine"
        BorderBrush="Black"
        BorderThickness="1">
   <TextBlock HorizontalAlignment="Center"
              Text="Test" />
</Border>

Draw a Triangle Using a Visual Brush

To create a triangle shape in WPF you use a Polygon control. If you remember your high school math, a Polygon is a series of connected lines that have the same start and end point. The XAML below creates a triangle; however, there is no text in it because a Polygon is not a container.

<Polygon Points="25,0 5,30 45,30"
         Fill="LightBlue"
         Stroke="Black"
         StrokeThickness="2" />

To add text like that shown in Figure 3, you need to use the Polygon control as the background of a TextBlock control. The XAML shown in Listing 1 is used to create Figure 3.

Figure 3: Use a Visual Brush to put text in a triangle.

Two key things make this XAML work. First, you use the VisualBrush object of the Background of the TextBlock object to give you a place to put the Polygon control. Second, depending on the text that you fill into the TextBlock control, and the width and height of the TextBlock control, you will need to play with the Padding property to align the text to the correct location. Take the above XAML and put it in a new window and then adjust the height, width and text values. You will see that you need to adjust the Padding property to make the text fall into the correct location within the Polygon.

Listing 2 shows another example of a Visual Brush, this time using an Ellipse control. Figure 4 shows the results of the XAML in Listing 2.

Figure 4: Use a Visual Brush to put text in an ellipse.

Notice the Padding attribute is different than when using the Polygon control. You will need to make minor adjustments based on the shape that you are using and the height and width of the control, and the text you are putting into the TextBlock.

Position a Shape on a Canvas

Using a Visual Brush within a TextBlock control works, but it is not very re-usable code. The ultimate goal of this article is to show you how to move all of this code into one user control that you can use over and over again. Before you can learn that technique, you first need to learn to use a Canvas, a Shape, a TextBlock and MultiBinding converters.

You use a WPF Canvas control to position child controls at a specified point on the Canvas. I created Figure 5 by placing a shape (you can use any control) at a certain point by setting the attached properties “Canvas.Top” and “Canvas.Left”. In Figure 5 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. I used the XAML below to create Figure 5.

Figure 5: Position the circle at a specific point on a Canvas.
<Canvas Name="cnvMain"
        Width="80"
        Height="80">
  <Ellipse Canvas.Top="20"
           Canvas.Left="40"
           Width="40"
           Height="40"
           Fill="Gray" />
</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 as shown in the following XAML.

<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 6.

Figure 6: A circle bound to the same height and width as the Canvas on which it is placed.
<Canvas Name="cnvMain"
        Width="80"
        Height="80">
  <Ellipse 
       Width="{Binding ElementName=cnvMain, 
                       Path=ActualWidth}"
       Height="{Binding ElementName=cnvMain,
                        Path=ActualHeight}"
       Fill="Gray" />
</Canvas>

Using a MultiBinding Converter

Now that you have a Shape within a Canvas you can place a TextBlock in the center of the Canvas to display text in the middle of that 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 height to get the Top property of where to position the TextBlock in relation to the top of the Canvas.

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 MultiBinding converter comes in very handy.

The result of the MultiBinding converters, shown in Listing 3, is to set the “Canvas.Left” and “Canvas.Top” properties of the TextBlock control. If you are familiar with a normal WPFbinding markup extension, you understand how a converter class is used to convert a single input value and produce a single output value that you assign to a property. A MultiBinding converter 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 just like a normal value converter method. 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. Listing 4 shows the MidPointValueConverter 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. You can use this extra value 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 MultiBindingXAML element as shown in Listing 5.

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

Figure 7: Shapes, text and Canvas using MultiBinding Converter.

Creating a User Control for Hosting Shapes

So, up to this point you have now learned how to create shapes and center text within those shapes using a couple of different methods. Now it is time to wrap up all this functionality into a nice, re-usable user control. In Figure 8 you see a couple of shapes that were created using a user control called “ucShape”. The first one draws a circle with the name “Paul” in it while the second one is a triangle.

Figure 8: Use a user control to specify which shapes and text to create.

Listing 6 shows the XAML used to create the circle in Figure 8. Notice each of the properties you can set on the user control. You can set the text to display on the shape using the TextToDisplay property. You use the Shape property to specify which type of shape you wish to display. You can use six different shapes with this user control; Rectangle, Square, Triangle, InvertedTriangle, Ellipse and Circle. Other properties that you can set include the color of the shape, the text, and the height and width of the shape.

Listing 7 shows the XAML code that draws the triangle shown in Figure 8. I’ve sent the ShapeToCreate property in this sample to “Triangle”. Notice the additional properties HeightOffset and WidthOffset. Normally the text is centered directly in the middle of the shape but with a triangle you will want to move the text to the fattest part of the triangle. These two additional properties adjust the text up, down, left or right.

Build a Shape User Control

To create your own shape user control you add a user control named ucShape to your project and add each of the properties shown in Listing 7. You will need to set up an enumeration for each shape as seen in the code below.

public enum ShapeEnums
{
  Rectangle,
  Square,
  Triangle,
  InvertedTriangle,
  Ellipse,
  Circle
}

You create the appropriate property get and set statements within the user control for each of the properties in Listing 7. I am not going to list the code for those here as I assume you know how to create normal properties. You can get the complete source code (in C# or VB) for this user control by downloading the sample code at www.pdsa.com/downloads.

The User Control XAML

Listing 8 is the complete XAML for your ucShape user control. A Canvas control is the container control for the entire user control. You’ll use the Canvas control to center the text at a specific point on the control. Notice that you do not use a shape control in the user control. The code will create the shape at run time by examining the ShapeToCreate property and adding the appropriate shape onto the Canvas at the appropriate location.

Notice the binding extensions on the Canvas control to the ActualWidth and ActualHeight properties of the user control. When you set the Width and Height properties on the user control these values will be used to set the Canvas to the same height.

The TextBlock control is contained within the Canvas control, but no other properties are set on this control. The properties such as where to place the TextBlock control on the Canvas, the Foreground color, the text, etc., will be set dynamically at run time based on the properties you set on the user control.

Loading the User Control

When an instance of the ucShape user control is loaded, the code creates the shape based on the ShapeToCreate property. The appropriate properties on the TextBlock control such as the Text, the Top and Left will also be set at this time. The Top and Left properties of the TextBlock will be set through the MultiBinding converter created in code this time as opposed to in the XAML.

Listing 9 shows the code for the Loaded event on the ucShape user control. This event procedure calls a method named CreateShape which is where the logic for creating each of the shapes is located. If a valid shape is returned from the CreateShape method, then that shape is inserted as the first control on the Canvas control.

The Loaded event procedure sets the Foreground property on the TextBlock control from the TextColor property set when the user control was declared. Finally, the MultiBinding converter for the Left and Top properties on the TextBlock control are created by calling the SetWidthMultiBinding and SetHeightMultiBinding. You will see this code a little later in this article.

The CreateShape Method

The CreateShape method (Listing 10) is fairly straight-forward WPF code. It contains a switch statement that checks the ShapeToCreate property. Based on that property, the appropriate shape is created and appropriate Width and Height properties on the shape are bound to the Width and Height properties on the Canvas control.

Create a MultiBinding in Code

In Listing 5 you saw how to use a MultiBinding converter to pass in two parameters from XAML and get a single value returned. Since you have moved all of the code into the user control, you will now create these MultiBinding converters in code too. Listing 11 shows the code for calculating the Left and Top properties for the TextBlock. Yes, you could have still created the MultiBinding converters in XAML, but I thought it would be good to show you how to perform the same thing using WPF code.

Summary

In this article, you learned how create text centered on a shape using a few different methods. It just goes to prove that there are always several methods to accomplish the same thing in WPF. For a quick and dirty approach, using a VisualBrush within a TextBlock works quite well. Using a user control is the best approach when you want to re-use the shape and text over and over again. You can place the user control and all the associated code into a DLL to reuse in many applications.

Listing 1: Place Text in a Triangle using a Visual Brush

<TextBlock Text="Test"
           Width="50"
           Height="50"
           Padding="13,28,0,0">
   <TextBlock.Background>
     <VisualBrush>
       <VisualBrush.Visual>
          <Polygon Points="25,0 5,30 45,30"
                   Fill="LightBlue"
                   Stroke="Black"
                   StrokeThickness="2" />
       </VisualBrush.Visual>
     </VisualBrush>
   </TextBlock.Background>
</TextBlock>

Listing 2: Place Text in an Ellipse using a Visual Brush

<TextBlock Text="Test"
           Height="40"
           Width="40"
           Padding="8,10,0,0">
  <TextBlock.Background>
    <VisualBrush>
      <VisualBrush.Visual>
         <Ellipse Height="20"
                  Width="20"
                  Fill="LightBlue" />
      </VisualBrush.Visual>
    </VisualBrush>
  </TextBlock.Background>
</TextBlock>

Listing 3: Use MultiBinding converters to perform calculations and assign the result to properties

<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>

Listing 4: The MidPointValueConverter class takes multiple inputs to produce a single output

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 offset = 0;
      double totalMeasure = 0;
      double controlMeasure = 0;
      
      // Make sure enough "values" were passed
      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 measure of the outside control,
             then the total measure of the control to be placed
             inside the outside control.", 
             "values");
      }
      
      // Get the total width/height
      totalMeasure = (double)values[0];
      // Get the width/height of the control
      controlMeasure = (double)values[1];
      
      // check for any offset to the width/height
      if (parameter != null)
        offset = System.Convert.ToDouble(parameter);
      
      return (object)(((totalMeasure - controlMeasure) / 2) 
             + offset);
    }
      
  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 offset As Double = 0
    Dim totalMeasure As Double = 0
    Dim controlMeasure As Double = 0
      
    ' Make sure enough "values" were passed
    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 total measure (width/height) of the 
         outside control, then the total measure (width/height) 
         of the inside control.", 
      "values")
    End If
      
    ' Get the total width/height
    totalMeasure = System.Convert.ToDouble(values(0))
    ' Get the width/height of the control
    controlMeasure = System.Convert.ToDouble(values(1))
      
    If parameter IsNot Nothing Then
      offset = System.Convert.ToDouble(parameter)
    End If
      
    Return DirectCast((((totalMeasure - controlMeasure) / 2) _
                         + offset), 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      

Listing 5: Setting an extra ConverterParameter can help you move the text a little in any direction

<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>

Listing 6: Using your own user control means you can wrap up a lot of code and have something that is truly re-usable across many WPF applications

<src:ucShape Margin="5"
             TextToDisplay="Paul"
             ShapeToCreate="Circle"
             FillColor="LightBlue"
             TextColor="Black"
             StrokeColor="Yellow"
             StrokeThickness="2"
             Width="50"
             Height="50" />

Listing 7: Use a user control to display a Triangle

<src:ucShape Margin="5"
             TextToDisplay="Paul"
             ShapeToCreate="Triangle"
             FillColor="Red"
             TextColor="Black"
             StrokeColor="Black"
             HeightOffset="10"
             WidthOffset="-1"
             StrokeThickness="2"
             Width="50"
             Height="50" />

Listing 8: The XAML for the ucShape user control is actually quite simple

<UserControl x:Class="WPFShapesUserControl_CS.ucShape"
             x:Name="userControlShape"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:src="clr-namespace:WPFShapesUserControl_CS"
 Loaded="UserControl_Loaded">
      
  <Canvas x:Name="cnvMain"
          Width="{Binding ElementName=userControlShape,
                    Path=ActualWidth}"
          Height="{Binding ElementName=userControlShape,
                    Path=ActualHeight}">
    <TextBlock x:Name="tblock" />
  </Canvas>
      
</UserControl>

Listing 9: The Load event of the user control is where you add the Shape and set the left and right properties of the TextBlock control

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
  Shape shape = null;
      
  shape = CreateShape();
      
  if (shape != null)
  {
    // Insert the Shape into the Canvas 
    // Place it before the TextBlock Control
    cnvMain.Children.Insert(0, shape);
      
    // Setup the TextBlock Control Properties
    tblock.Foreground = new SolidColorBrush((Color)ColorConverter.
        ConvertFromString(mTextColor));
    SetWidthMultiBinding();
    SetHeightMultiBinding();
  }
}

Listing 10: Create Shapes and Bindings at runtime

private void CreateShape()
{
  PointCollection pc = new PointCollection();
  Binding heightBinding = new Binding("ActualHeight");
  Binding widthBinding = new Binding("ActualWidth");
  Shape shape = null;
      
  switch (mShapeToCreate)
  {
    case ShapeEnums.Rectangle:
    case ShapeEnums.Square:
      shape = new Rectangle();
      
      heightBinding.Source = cnvMain.ActualHeight;
      shape.SetBinding(Rectangle.HeightProperty, heightBinding);
      
      widthBinding.Source = cnvMain.ActualWidth;
      shape.SetBinding(Rectangle.WidthProperty, widthBinding);
      
      break;
      
    case ShapeEnums.Triangle:
      pc.Add(new Point((cnvMain.ActualWidth / 2), 0));
      pc.Add(new Point(0, cnvMain.ActualWidth));
      pc.Add(new Point(cnvMain.ActualWidth, cnvMain.ActualWidth));
      
      shape = new Polygon();
      ((Polygon)shape).Points = pc;
      
      break;
      
    case ShapeEnums.InvertedTriangle:
      pc.Add(new Point(0, 0));
      pc.Add(new Point((cnvMain.ActualWidth / 2), 
        cnvMain.ActualWidth));
      pc.Add(new Point(cnvMain.ActualWidth, 0));
      
      shape = new Polygon();
      ((Polygon)shape).Points = pc;
      
      break;
      
    case ShapeEnums.Ellipse:
    case ShapeEnums.Circle:
      shape = new Ellipse();
      
      heightBinding.Source = cnvMain.ActualHeight;
      shape.SetBinding(Ellipse.HeightProperty, heightBinding);
      
      widthBinding.Source = cnvMain.ActualWidth;
      shape.SetBinding(Ellipse.WidthProperty, widthBinding);
      
      break;
  }
      
  if (shape != null)
  {
    shape.Fill = new 
      SolidColorBrush((Color)ColorConverter.
        ConvertFromString(mFillColor));
    shape.Stroke = new
      SolidColorBrush((Color)ColorConverter.
        ConvertFromString(mStrokeColor));
    shape.StrokeThickness = mStrokeThickness;
    shape.Height = cnvMain.ActualHeight;
    shape.Width = cnvMain.ActualWidth;
  }
      
  return shape;
}

Listing 11: You can create MultiBindings in C# or Visual Basic code as well as in XAML

private void SetWidthMultiBinding()
{
  MultiBinding mb = new MultiBinding();
  mb.Converter = new MidpointValueConverter();
  mb.ConverterParameter = mWidthOffset;
      
  Binding bind = new Binding("ActualWidth");
  bind.Source = cnvMain;
  bind.Path = new PropertyPath(Canvas.ActualWidthProperty);
  mb.Bindings.Add(bind);
      
  Binding bind2 = new Binding("ActualWidth");
  bind2.Source = tblock;
  bind2.Path = new PropertyPath(TextBlock.ActualWidthProperty);
  mb.Bindings.Add(bind2);
      
  tblock.SetBinding(Canvas.LeftProperty, mb);
}
      
private void SetHeightMultiBinding()
{
  MultiBinding mb = new MultiBinding();
  mb.Converter = new MidpointValueConverter();
  mb.ConverterParameter = mHeightOffset;
      
  Binding bind = new Binding("ActualHeight");
  bind.Source = cnvMain;
  bind.Path = new PropertyPath(Canvas.ActualHeightProperty);
  mb.Bindings.Add(bind);
      
  Binding bind2 = new Binding("ActualHeight");
  bind2.Source = tblock;
  bind2.Path = new PropertyPath(TextBlock.ActualHeightProperty);
  mb.Bindings.Add(bind2);
      
  tblock.SetBinding(Canvas.TopProperty, mb);
}

Open original image