Reputation: 48697
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
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
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
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
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
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
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:
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