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
相关产品推荐
相关产品推荐

