J D
J D

Reputation: 48697

Center text at a given point on a WPF Canvas

I have a Controls.Canvas with several shapes on it and would like to add textual labels that are centered on given points (I'm drawing a tree with labelled vertices). What is the simplest way to do this programmatically in WPF?

I have tried setting RenderTransform and calling Controls.Canvas.SetLeft etc. but neither position the label where I want it. WPF seems to support positioning only at given left, right, top and bottom coordinates and not centered on a given coordinate and the Width property is NaN and the ActualWidth property is 0.0 when I construct the Canvas.

Upvotes: 19

Views: 18008

Answers (6)

StayOnTarget
StayOnTarget

Reputation: 13048

Here's another variant of yetsta's excellent answer. One small flaw was that if the point value is set while the target element is NOT visible, then it's ActualHeight & ActualWidth may be zero. Thus the center point doesn't end up quite correct.

This can lead to some weird inconsistencies depending on the exact order of operations (sometimes visibility set before the point value... other times set after it ... etc.)

So this code includes catching of IsVisibleChanged to handle that.

public class CenterOnPoint
{
    public static readonly DependencyProperty CenterPointProperty =
       DependencyProperty.RegisterAttached("CenterPoint", typeof(Point), typeof(CenterOnPoint),
       new PropertyMetadata(default(Point), OnPointChanged));

    public static void SetCenterPoint(UIElement element, Point value)
    {
        element.SetValue(CenterPointProperty, value);
    }

    public static Point GetCenterPoint(UIElement element)
    {
        return (Point)element.GetValue(CenterPointProperty);
    }

    private static void OnPointChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var element = (FrameworkElement)d;
        element.SizeChanged -= OnSizeChanged;
        element.SizeChanged += OnSizeChanged;
        element.IsVisibleChanged -= OnIsVisibleChanged;
        element.IsVisibleChanged += OnIsVisibleChanged;
        var newPoint = (Point)e.NewValue;
        element.SetValue(Canvas.LeftProperty, newPoint.X - (element.ActualWidth / 2));
        element.SetValue(Canvas.TopProperty, newPoint.Y - (element.ActualHeight / 2));
    }

    private static void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var element = (FrameworkElement)sender;
        var point = GetCenterPoint(element);
        element.SetValue(Canvas.LeftProperty, point.X - (element.ActualWidth / 2));
        element.SetValue(Canvas.TopProperty, point.Y - (element.ActualHeight / 2));
    }

    private static void OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        var element = (FrameworkElement)sender;
        var newPoint = GetCenterPoint(element);
        element.SetValue(Canvas.LeftProperty, newPoint.X - (e.NewSize.Width / 2));
        element.SetValue(Canvas.TopProperty, newPoint.Y - (e.NewSize.Height / 2));
    }
}

Upvotes: 0

HHenn
HHenn

Reputation: 319

I have extended yetsta´s answer to include the possibility of horizontal and verical alignment:

  public class CanvasMover : DependencyObject
  {
    public enum Horizontal
    {
      Left,
      Center,
      Right
    }

    public enum Vertical
    {
      Top,
      Center,
      Bottom
    }

    public Point Position
    {
      get { return (Point)GetValue(PositionProperty); }
      set { SetValue(PositionProperty, value); }
    }

    public static Horizontal GetHorizontalAlignment(DependencyObject obj)
    {
      return (Horizontal)obj.GetValue(HorizontalAlignmentProperty);
    }

    public static Point GetPosition(DependencyObject obj)
    {
      return (Point)obj.GetValue(PositionProperty);
    }

    public static Vertical GetVerticalAlignment(DependencyObject obj)
    {
      return (Vertical)obj.GetValue(VerticalAlignmentProperty);
    }

    public static void SetHorizontalAlignment(DependencyObject obj, Horizontal value)
    {
      obj.SetValue(HorizontalAlignmentProperty, value);
    }

    public static void SetPosition(DependencyObject obj, Point value)
    {
      obj.SetValue(PositionProperty, value);
    }

    public static void SetVerticalAlignment(DependencyObject obj, Vertical value)
    {
      obj.SetValue(VerticalAlignmentProperty, value);
    }

    public static readonly DependencyProperty HorizontalAlignmentProperty =
        DependencyProperty.RegisterAttached("HorizontalAlignment", typeof(Horizontal), typeof(CanvasMover),
          new PropertyMetadata(Horizontal.Center, DependencyPropertyChanged));

    public static readonly DependencyProperty PositionProperty =
                DependencyProperty.RegisterAttached("Position", typeof(Point), typeof(CanvasMover),
          new PropertyMetadata(default(Point), DependencyPropertyChanged));

    public static readonly DependencyProperty VerticalAlignmentProperty =
        DependencyProperty.RegisterAttached("VerticalAlignment", typeof(Vertical), typeof(CanvasMover),
          new PropertyMetadata(Vertical.Center, DependencyPropertyChanged));

    private static void DependencyPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
      if (dependencyObject is not FrameworkElement element)
      {
        return;
      }
      element.SizeChanged -= OnSizeChanged;
      element.SizeChanged += OnSizeChanged;
      SetCanvasPosition(element);
    }

    private static void OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
      var element = (FrameworkElement)sender;
      SetCanvasPosition(element);
    }

    private static void SetCanvasPosition(FrameworkElement element)
    {
      var position = CanvasMover.GetPosition(element);
      var horizontalAlignment = CanvasMover.GetHorizontalAlignment(element);
      var verticalAlignment = CanvasMover.GetVerticalAlignment(element);

      var left = position.X - (element.ActualWidth / 2);
      var top = position.Y - (element.ActualHeight / 2);

      if (horizontalAlignment == Horizontal.Left)
        left = position.X - element.ActualWidth;
      else if (horizontalAlignment == Horizontal.Right)
        left = position.X;

      if (verticalAlignment == Vertical.Top)
        top = position.Y - element.ActualHeight;
      else if (verticalAlignment == Vertical.Bottom)
        top = position.Y;

      element.SetValue(Canvas.LeftProperty, left);
      element.SetValue(Canvas.TopProperty, top);
    }
  }

The usage looks like this:

   <Label behaviors:CanvasMover.HorizontalAlignment="Right"
          behaviors:CanvasMover.Position="288,384"
          behaviors:CanvasMover.VerticalAlignment="Center"
          Content="Sample"
          Foreground="White" />

Upvotes: 1

Pavel
Pavel

Reputation: 5876

To center text in given area (say, rectangle) you can simply wrap it by Grid. See example in this answer. The grid can be positioned anywhere inside the canvas using Left, Top, Width and Height properties. The text will always stay in the center of grid.

This logic can be encapsulated in a custom FrameworkElement like this.

To center text at point (x, y) you can calculate appropriate rectangle:

var text = new CenteredTextBlock
{
    Text = "Hello",
    Width = maxWidth,
    Height = maxHeight,
};
Canvas.SetLeft(text, x - maxWidth / 2);
Canvas.SetTop(text, y - maxHeight / 2);
Canvas.Children.Add(text);

where (maxWidth, maxHeight) is the maximum allowed size for text.

Upvotes: 1

cameron
cameron

Reputation: 91

Sorry Jon, I didn't understand your question all the way yesterday on Twitter. Here's how I might try it in F#! @cammcad

#r @"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll" #r @"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll" #r @"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll"

open System
open System.IO
open System.Windows
open System.Windows.Shapes
open System.Windows.Media
open System.Windows.Controls
open System.Windows.Markup
open System.Xml

(* Add shape and label to canvas at specific location *)
let addShapeAndLabel_at_coordinate (label: string) (coordinate: float * float) (c:    Canvas) = 
  let btn = Button(Content=label,Foreground=SolidColorBrush(Colors.White))
  let template = 
     "<ControlTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
        TargetType=\"Button\">" +
        "<Grid>" +
        " <Ellipse Width=\"15\" Height=\"15\" Fill=\"Orange\" HorizontalAlignment=\"Center\"/>" +
        " <ContentPresenter HorizontalAlignment=\"Center\" " + "VerticalAlignment=\"Center\"/> " + 
        "</Grid>" +
        "</ControlTemplate>"

  btn.Template <- XamlReader.Parse(template) :?> ControlTemplate
  c.Children.Add(btn) |> ignore
  let textsize =  
       FormattedText(label,CultureInfo.GetCultureInfo("enus"),
       FlowDirection.LeftToRight,Typeface("Verdana"),32.0,Brushes.White)
       |> fun x -> x.MinWidth, x.LineHeight
  let left,top = coordinate
  let middle_point_width = fst(textsize) / 2.0
  let middle_point_height = snd(textsize) / 2.0
  Canvas.SetLeft(btn,left - middle_point_width)
  Canvas.SetTop(btn,top - middle_point_height)

let shell = new Window(Width=300.0,Height=300.0)
let canvas = new   Canvas(Width=300.0,Height=300.0,Background=SolidColorBrush(Colors.Green))

addShapeAndLabel_at_coordinate "Tree Node 1" (100.0,50.0) canvas
addShapeAndLabel_at_coordinate "TreeNode 2" (150.0, 75.) canvas
shell.Content <- canvas

[<STAThread>] ignore <| (new Application()).Run shell

Upvotes: 1

yetsta
yetsta

Reputation: 121

This also works, with less binding.

public class CenterOnPoint
{
  public static readonly DependencyProperty CenterPointProperty =
     DependencyProperty.RegisterAttached("CenterPoint", typeof (Point), typeof (CenterOnPoint),
     new PropertyMetadata(default(Point), OnPointChanged));

  public static void SetCenterPoint(UIElement element, Point value)
  {
     element.SetValue(CenterPointProperty, value);
  }

  public static Point GetCenterPoint(UIElement element)
  {
     return (Point) element.GetValue(CenterPointProperty);
  }

  private static void OnPointChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
     var element = (FrameworkElement)d;
     element.SizeChanged -= OnSizeChanged;
     element.SizeChanged += OnSizeChanged;
     var newPoint = (Point)e.NewValue;
     element.SetValue(Canvas.LeftProperty, newPoint.X - (element.ActualWidth / 2));
     element.SetValue(Canvas.TopProperty, newPoint.Y - (element.ActualHeight / 2));
  }

  private static void OnSizeChanged(object sender, SizeChangedEventArgs e)
  {
     var element = (FrameworkElement) sender;
     var newPoint = GetCenterPoint(element);
     element.SetValue(Canvas.LeftProperty, newPoint.X - (e.NewSize.Width / 2));
     element.SetValue(Canvas.TopProperty, newPoint.Y - (e.NewSize.Height / 2));
  }
}

And you use it like this...

label.SetValue(CenterOnPoint.CenterPointProperty, new Point(100, 100));

Upvotes: 12

user128300
user128300

Reputation:

You could achieve this by binding the margin of the label to the ActualWidth and ActualHeight of the label, and multiplying these values with -0.5. This moves the label left by half its width; and it moves the label upwards by half its height.

Here is an example:

XAML:

<Window x:Class="CenteredLabelTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:CenteredLabelTest"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:CenterConverter x:Key="centerConverter"/>
    </Window.Resources>
    <Canvas>
        <TextBlock x:Name="txt" Canvas.Left="40" Canvas.Top="40" TextAlignment="Center" Text="MMMMMM">
            <TextBlock.Margin>
                <MultiBinding Converter="{StaticResource centerConverter}">
                        <Binding ElementName="txt" Path="ActualWidth"/>
                        <Binding ElementName="txt" Path="ActualHeight"/>
                </MultiBinding>
            </TextBlock.Margin>
        </TextBlock>
        <Rectangle Canvas.Left="39" Canvas.Top="39" Width="2" Height="2" Fill="Red"/>
    </Canvas>
</Window>

The red rectangle highlights the coordinate (40, 40) on which the label "MMMMMM" is centered.

Converter:

public class CenterConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue)
        {
            return DependencyProperty.UnsetValue;
        }

        double width = (double) values[0];
        double height = (double)values[1];

        return new Thickness(-width/2, -height/2, 0, 0);
    }

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

The result looks like this:

centered label

In order to do that programmatically, define an attached property Mover.MoveToMiddle, like this:

public class Mover : DependencyObject
{
    public static readonly DependencyProperty MoveToMiddleProperty =
        DependencyProperty.RegisterAttached("MoveToMiddle", typeof (bool), typeof (Mover),
        new PropertyMetadata(false, PropertyChangedCallback));

    public static void SetMoveToMiddle(UIElement element, bool value)
    {
        element.SetValue(MoveToMiddleProperty, value);
    }

    public static bool GetMoveToMiddle(UIElement element)
    {
        return (bool) element.GetValue(MoveToMiddleProperty);
    }

    private static void PropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement element = sender as FrameworkElement;
        if (element == null)
        {
            return;
        }

        if ((bool)e.NewValue)
        {
            MultiBinding multiBinding = new MultiBinding();
            multiBinding.Converter = new CenterConverter();
            multiBinding.Bindings.Add(new Binding("ActualWidth") {Source = element});
            multiBinding.Bindings.Add(new Binding("ActualHeight") {Source = element});
            element.SetBinding(FrameworkElement.MarginProperty, multiBinding);
        }
        else
        {
            element.ClearValue(FrameworkElement.MarginProperty);
        }
    }

}

Setting Mover.MoveToMiddle to true means that the margin of that framework element is automatically bound to its actual width and height such that the framework element is moved to its center point.

You would use it in your XAML code like this:

<Window x:Class="CenteredLabelTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:CenteredLabelTest"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:CenterConverter x:Key="centerConverter"/>
    </Window.Resources>
    <Canvas>
        <TextBlock Canvas.Left="40" Canvas.Top="40" TextAlignment="Center" Text="MMMMMM"
              local:Mover.MoveToMiddle="True"/>
        <Rectangle Canvas.Left="39" Canvas.Top="39" Width="2" Height="2" Fill="Red"/>
    </Canvas>
</Window>

An alternative would be to bind to RenderTransform instead of Margin. In this case, the converter would return

return new TranslateTransform(-width / 2, -height / 2);

and the attached property's callback method would contain these lines:

if ((bool)e.NewValue)
{
    ...
    element.SetBinding(UIElement.RenderTransformProperty, multiBinding);
}
else
{
    element.ClearValue(UIElement.RenderTransformProperty);
}

This alternative has the advantage that the effect of the attached property is visible in the Visual Studio designer (which is not the case when setting the Margin property).

Upvotes: 16

Related Questions