You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF MVVM模式下获取Canvas面板中心坐标的实现方案

MVVM模式下获取Canvas尺寸并居中绘制图形的解决方案

下面给出三种WPF中MVVM友好的实现方案,适配你的现有代码结构:


方案一:利用XAML Behaviors传递Canvas尺寸到ViewModel

你的XAML已经引用了Behaviors命名空间,这个方案可以直接复用现有依赖:

1. 补充ViewModel逻辑

添加尺寸属性、命令及位置更新方法,同时实现RelayCommand(WPF常用的命令实现):

// 先实现一个基础的RelayCommand
public class RelayCommand<T> : ICommand
{
    private readonly Action<T> _execute;
    private readonly Func<T, bool> _canExecute;

    public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter) => _canExecute?.Invoke((T)parameter) ?? true;
    public void Execute(object parameter) => _execute((T)parameter);

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }
}

// 修改MainViewVM
public class MainViewVM : INotifyPropertyChanged
{
    public ObservableCollection<mShape> Shapes { get; } = new ObservableCollection<mShape>();
    public ICommand CanvasSizeChangedCommand { get; }

    private double _canvasWidth;
    public double CanvasWidth
    {
        get => _canvasWidth;
        set
        {
            _canvasWidth = value;
            UpdateShapePosition();
            OnPropertyChanged(nameof(CanvasWidth));
        }
    }

    private double _canvasHeight;
    public double CanvasHeight
    {
        get => _canvasHeight;
        set
        {
            _canvasHeight = value;
            UpdateShapePosition();
            OnPropertyChanged(nameof(CanvasHeight));
        }
    }

    public MainViewVM()
    {
        CanvasSizeChangedCommand = new RelayCommand<Canvas>(canvas =>
        {
            CanvasWidth = canvas.ActualWidth;
            CanvasHeight = canvas.ActualHeight;
        });

        Shapes.Add(new mShape
        {
            Stroke = new SolidColorBrush(Color.FromArgb(0xFF, 0x66, 0x66, 0x66)),
            Geometry = new EllipseGeometry { RadiusX = 50, RadiusY = 50 },
            Fill = (SolidColorBrush)new BrushConverter().ConvertFrom("#D3D3D3")
        });
    }

    private void UpdateShapePosition()
    {
        if (Shapes.Count == 0 || CanvasWidth == 0 || CanvasHeight == 0) return;
        var shape = Shapes[0];
        // 计算中点:Canvas尺寸一半减去图形自身半径
        shape.XPos = (CanvasWidth / 2) - 50;
        shape.YPos = (CanvasHeight / 2) - 50;
    }
    
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

2. 修改XAML绑定行为

给Canvas添加尺寸变化监听行为:

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <Canvas ClipToBounds="True" Background="Transparent">
            <i:Interaction.Behaviors>
                <i:EventTriggerBehavior EventName="SizeChanged">
                    <i:InvokeCommandAction Command="{Binding CanvasSizeChangedCommand}" 
                                           CommandParameter="{Binding RelativeSource={RelativeSource Self}}"/>
                </i:EventTriggerBehavior>
            </i:Interaction.Behaviors>
        </Canvas>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

方案二:自定义附加属性传递Canvas尺寸

如果不想依赖Behaviors库,可以自己实现附加属性来追踪Canvas尺寸:

1. 创建附加属性类

public static class CanvasSizeTracker
{
    public static readonly DependencyProperty TrackSizeProperty =
        DependencyProperty.RegisterAttached(
            "TrackSize",
            typeof(bool),
            typeof(CanvasSizeTracker),
            new PropertyMetadata(false, OnTrackSizeChanged));

    public static readonly DependencyProperty CanvasWidthProperty =
        DependencyProperty.RegisterAttached(
            "CanvasWidth",
            typeof(double),
            typeof(CanvasSizeTracker),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public static readonly DependencyProperty CanvasHeightProperty =
        DependencyProperty.RegisterAttached(
            "CanvasHeight",
            typeof(double),
            typeof(CanvasSizeTracker),
            new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    public static bool GetTrackSize(DependencyObject obj) => (bool)obj.GetValue(TrackSizeProperty);
    public static void SetTrackSize(DependencyObject obj, bool value) => obj.SetValue(TrackSizeProperty, value);

    public static double GetCanvasWidth(DependencyObject obj) => (double)obj.GetValue(CanvasWidthProperty);
    public static void SetCanvasWidth(DependencyObject obj, double value) => obj.SetValue(CanvasWidthProperty, value);

    public static double GetCanvasHeight(DependencyObject obj) => (double)obj.GetValue(CanvasHeightProperty);
    public static void SetCanvasHeight(DependencyObject obj, double value) => obj.SetValue(CanvasHeightProperty, value);

    private static void OnTrackSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is Canvas canvas)
        {
            if ((bool)e.NewValue)
            {
                canvas.SizeChanged += OnCanvasSizeChanged;
                UpdateCanvasSize(canvas);
            }
            else
            {
                canvas.SizeChanged -= OnCanvasSizeChanged;
            }
        }
    }

    private static void OnCanvasSizeChanged(object sender, SizeChangedEventArgs e)
    {
        if (sender is Canvas canvas) UpdateCanvasSize(canvas);
    }

    private static void UpdateCanvasSize(Canvas canvas)
    {
        SetCanvasWidth(canvas, canvas.ActualWidth);
        SetCanvasHeight(canvas, canvas.ActualHeight);
    }
}

2. 修改XAML使用附加属性

<!-- 先在Window的xmlns里添加local引用(如果没加的话) -->
xmlns:local="clr-namespace:CanvasSampleMvvm"

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <Canvas ClipToBounds="True" Background="Transparent"
                local:CanvasSizeTracker.TrackSize="True"
                local:CanvasSizeTracker.CanvasWidth="{Binding CanvasWidth}"
                local:CanvasSizeTracker.CanvasHeight="{Binding CanvasHeight}">
        </Canvas>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

3. ViewModel复用方案一的逻辑

ViewModel中的CanvasWidth、CanvasHeight属性及UpdateShapePosition方法无需修改,尺寸变化时会自动同步并更新图形位置。


方案三:XAML转换器直接计算居中位置(无需ViewModel处理尺寸)

这个方案完全通过XAML和转换器实现居中,不需要ViewModel处理任何尺寸逻辑,最符合MVVM关注点分离原则:

1. 创建多值转换器

public class CenterPositionConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length != 2 || !(values[0] is double canvasSize) || !(values[1] is double shapeSize))
            return 0;

        // 居中公式:Canvas尺寸的一半 减去 图形尺寸的一半
        return (canvasSize / 2) - (shapeSize / 2);
    }

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

2. 修改XAML绑定

先在Window资源中添加转换器:

<Window.Resources>
    <vm:MainViewVM x:Key="vm"/>
    <local:CenterPositionConverter x:Key="CenterPositionConverter"/>
</Window.Resources>

然后修改ItemContainerStyle,用多绑定实现居中:

<ItemsControl.ItemContainerStyle>
    <Style TargetType="ContentPresenter">
        <Setter Property="Canvas.Left">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource CenterPositionConverter}">
                    <Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType=Canvas}"/>
                    <Binding Path="DataContext.Geometry.Bounds.Width" RelativeSource="{RelativeSource Self}"/>
                </MultiBinding>
            </Setter.Value>
        </Setter>
        <Setter Property="Canvas.Top">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource CenterPositionConverter}">
                    <Binding Path="ActualHeight" RelativeSource="{RelativeSource AncestorType=Canvas}"/>
                    <Binding Path="DataContext.Geometry.Bounds.Height" RelativeSource="{RelativeSource Self}"/>
                </MultiBinding>
            </Setter.Value>
        </Setter>
    </Style>
</ItemsControl.ItemContainerStyle>

此时ViewModel中可以完全移除尺寸相关属性,只保留图形数据即可。


内容的提问来源于stack exchange,提问作者Gokhan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 03:12:33