Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
354 views
in Technique[技术] by (71.8m points)

c# - 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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...