Текст центра в данной точке на холсте WPF

У меня есть Controls.Canvas с несколькими фигурами на нем и хотел бы добавить текстовые метки, которые сосредоточены на заданных точках (я рисую дерево с помеченными вершинами). Каков самый простой способ сделать это программно в WPF?

Я пробовал установку RenderTransform и вызывал Controls.Canvas.SetLeft и т.д., но не помещал метку, где я ее хочу. WPF, похоже, поддерживает позиционирование только при заданных координатах слева, справа, сверху и снизу и не центрируется на заданной координате, а свойство Width - NaN, а свойство ActualWidth - 0.0 при построении Canvas.

Ответы

Ответ 1

Вы могли бы достичь этого, привязав маркер метки к меткам ActualWidth и ActualHeight, и умножив эти значения на -0.5. Это перемещает метку слева на половину ее ширины; и он перемещает метку вверх на половину ее высоты.

Вот пример:

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>

Красный прямоугольник выделяет координату (40, 40), на которой расположена метка "MMMMMM".

Преобразователь

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();
    }
}

Результат выглядит следующим образом:

centered label

Чтобы сделать это программно, определите прикрепленное свойство Mover.MoveToMiddle, например:

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);
        }
    }

}

Настройка Mover.MoveToMiddle - true означает, что край этого элемента структуры автоматически привязан к его фактической ширине и высоте, так что элемент фрейма перемещается в его центральную точку.

Вы бы использовали его в своем коде 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 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>

Альтернативой может быть привязка к RenderTransform вместо Margin. В этом случае конвертер вернет

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

а метод обратного вызова приложенного свойства будет содержать следующие строки:

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

Эта альтернатива имеет то преимущество, что эффект присоединенного свойства виден в дизайнере Visual Studio (это не так, когда вы устанавливаете свойство Margin).

Ответ 2

Это также работает с меньшей привязкой.

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));
  }
}

И вы используете его вот так...

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

Ответ 3

Извините, Джон, я не понял ваш вопрос на всем протяжении вчерашнего дня в Twitter. Вот как я могу попробовать это в F #! @cammcad

#r @ "C:\Program Files (x86)\Reference   Сборки \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

Ответ 4

Чтобы центрировать текст в заданной области (скажем, прямоугольник), вы можете просто обернуть его Grid. Смотрите пример в этом ответе. Сетка может быть расположена в любом месте внутри холста, используя свойства Left, Top, Width и Height. Текст всегда будет оставаться в центре сетки.

Эта логика может быть заключена в пользовательский FrameworkElement, подобный этому.

Чтобы отцентрировать текст в точке (x, y) вы можете рассчитать соответствующий прямоугольник:

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

где (maxWidth, maxHeight) - максимально допустимый размер текста.