如何实现一个由鼠标移动的WPF控件,像一个滑块控件但是是2D的?
How to implement a WPF control that is moved by the mouse, like a slider control but 2D?
我想通过其控制点 Q(Segment 的 Point1 属性)在运行时控制 QuadraticBezierSegment 的渲染。我可以使用针对该点的 X 和 Y 值的单独滑块控件来执行此操作。但我最终希望能够使用拖动控制点来重塑线段。在下面的代码中,我可以绘制控制点和线段,它们都会响应滑块。但是我不知道如何拖动点来控制线段(然后我会省去滑块)。
目前没有代码隐藏,我试图将所有内容都保留在 XAML/MVVM 中,但不确定是否可行。谢谢
这是视图模型:
namespace BezierDemo
{
class MainViewModel : INotifyPropertyChanged
{
private System.Windows.Point _q;
private double _qy;
private double _qx;
public MainViewModel()
{
_q.X = 50;
_q.Y = 0;
}
// https://www.danrigby.com/2015/09/12/inotifypropertychanged-the-net-4-6-way/
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
public double QX
{
get { return _q.X; }
set { Q = new System.Windows.Point(value, Q.Y); SetProperty(ref this._qx, value); }
}
public double QY
{
get { return _q.Y; }
set { Q = new System.Windows.Point(Q.X, value); SetProperty(ref this._qy, value); }
}
public System.Windows.Point Q
{
get { return _q; }
set { SetProperty(ref this._q, value); }
}
}
}
...这里是 XAML:
<Window x:Class="BezierDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BezierDemo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="506">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400*"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<!-- Bezier Control Point -->
<Canvas Grid.Column="0">
<Ellipse Width="5" Height="5" Fill="Indigo" Stroke="Indigo" Cursor="Hand" >
<Ellipse.RenderTransform>
<TranslateTransform X="{Binding Path=QX}" Y="{Binding Path=QY}"/>
</Ellipse.RenderTransform>
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Ellipse.MouseMove">
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
</Canvas>
<!-- QuadraticBezierSegment -->
<Path Stroke="Black" Fill="Gray" Grid.Column="0">
<Path.Data>
<PathGeometry>
<PathFigure>
<PathFigure.StartPoint>
<Point X="0" Y="100" />
</PathFigure.StartPoint>
<QuadraticBezierSegment Point1="{Binding Path=Q}" Point2="100, 100" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<!-- X & Y Slider Controls -->
<Grid Grid.Column="2" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<Slider Name="X" Orientation="Vertical" Maximum="100" HorizontalAlignment="Center" VerticalAlignment="Center" Height="234" Value="{Binding Path=QX}" Minimum="0.0"/>
<Label HorizontalAlignment="Center">X</Label>
</StackPanel>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<Slider Name="Y" Orientation="Vertical" Maximum="100" HorizontalAlignment="Center" VerticalAlignment="Center" Height="234" Value="{Binding Path=QY}" Minimum="0.0"/>
<Label HorizontalAlignment="Center">Y</Label>
</StackPanel>
</Grid>
</Grid>
如果您将贝塞尔曲线控制点包裹在 Thumb
元素中,那么您可以很容易地完成您想要的。
<!-- Bezier Control Point -->
<Canvas Grid.Column="0">
<Thumb DragDelta="Thumb_DragDelta" Canvas.Left="{Binding QX, Mode=TwoWay}" Canvas.Top="{Binding QY, Mode=TwoWay}">
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="5" Height="5" Fill="Indigo" Stroke="Indigo" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
注意:我必须将 Panel.ZIndex="-1"
添加到 QuadraticBezierSegment,这样椭圆就会呈现在贝塞尔曲线段的前面。或者您可以在贝塞尔曲线段声明之后移动拇指部分。
隐藏代码:
private void Thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
UIElement thumb = e.Source as UIElement;
Canvas.SetLeft(thumb, Canvas.GetLeft(thumb) + e.HorizontalChange);
Canvas.SetTop(thumb, Canvas.GetTop(thumb) + e.VerticalChange);
}
您可以使用 Microsoft.Xaml.Behaviors.Wpf nuget 包将隐藏代码转换为视图模型中的事件处理程序。
看起来像这样
<Canvas Grid.Column="0">
<Thumb Canvas.Left="{Binding QX, Mode=TwoWay}" Canvas.Top="{Binding QY, Mode=TwoWay}">
<b:Interaction.Triggers>
<b:EventTrigger EventName="DragDelta">
<b:InvokeCommandAction Command="{Binding HandleDragDelta}" PassEventArgsToCommand="True" />
</b:EventTrigger>
</b:Interaction.Triggers>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="5" Height="5" Fill="Indigo" Stroke="Indigo" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
其中 HandleDragDelta
是某种 ICommand
实现,可以采用 DragDeltaEventArgs
参数,因为您需要它。
private DelegateCommand<DragDeltaEventArgs> handleDragDelta;
public ICommand HandleDragDelta => handleDragDelta ??= new DelegateCommand<DragDeltaEventArgs>(PerformHandleDragDelta);
private void PerformHandleDragDelta(DragDeltaEventArgs e)
{
UIElement thumb = e.Source as UIElement;
Canvas.SetLeft(thumb, Canvas.GetLeft(thumb) + e.HorizontalChange);
Canvas.SetTop(thumb, Canvas.GetTop(thumb) + e.VerticalChange);
}
我想通过其控制点 Q(Segment 的 Point1 属性)在运行时控制 QuadraticBezierSegment 的渲染。我可以使用针对该点的 X 和 Y 值的单独滑块控件来执行此操作。但我最终希望能够使用拖动控制点来重塑线段。在下面的代码中,我可以绘制控制点和线段,它们都会响应滑块。但是我不知道如何拖动点来控制线段(然后我会省去滑块)。
目前没有代码隐藏,我试图将所有内容都保留在 XAML/MVVM 中,但不确定是否可行。谢谢
这是视图模型:
namespace BezierDemo
{
class MainViewModel : INotifyPropertyChanged
{
private System.Windows.Point _q;
private double _qy;
private double _qx;
public MainViewModel()
{
_q.X = 50;
_q.Y = 0;
}
// https://www.danrigby.com/2015/09/12/inotifypropertychanged-the-net-4-6-way/
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
public double QX
{
get { return _q.X; }
set { Q = new System.Windows.Point(value, Q.Y); SetProperty(ref this._qx, value); }
}
public double QY
{
get { return _q.Y; }
set { Q = new System.Windows.Point(Q.X, value); SetProperty(ref this._qy, value); }
}
public System.Windows.Point Q
{
get { return _q; }
set { SetProperty(ref this._q, value); }
}
}
}
...这里是 XAML:
<Window x:Class="BezierDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:BezierDemo"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="506">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400*"/>
<ColumnDefinition Width="100*"/>
</Grid.ColumnDefinitions>
<!-- Bezier Control Point -->
<Canvas Grid.Column="0">
<Ellipse Width="5" Height="5" Fill="Indigo" Stroke="Indigo" Cursor="Hand" >
<Ellipse.RenderTransform>
<TranslateTransform X="{Binding Path=QX}" Y="{Binding Path=QY}"/>
</Ellipse.RenderTransform>
<Ellipse.Triggers>
<EventTrigger RoutedEvent="Ellipse.MouseMove">
</EventTrigger>
</Ellipse.Triggers>
</Ellipse>
</Canvas>
<!-- QuadraticBezierSegment -->
<Path Stroke="Black" Fill="Gray" Grid.Column="0">
<Path.Data>
<PathGeometry>
<PathFigure>
<PathFigure.StartPoint>
<Point X="0" Y="100" />
</PathFigure.StartPoint>
<QuadraticBezierSegment Point1="{Binding Path=Q}" Point2="100, 100" />
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<!-- X & Y Slider Controls -->
<Grid Grid.Column="2" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<Slider Name="X" Orientation="Vertical" Maximum="100" HorizontalAlignment="Center" VerticalAlignment="Center" Height="234" Value="{Binding Path=QX}" Minimum="0.0"/>
<Label HorizontalAlignment="Center">X</Label>
</StackPanel>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<Slider Name="Y" Orientation="Vertical" Maximum="100" HorizontalAlignment="Center" VerticalAlignment="Center" Height="234" Value="{Binding Path=QY}" Minimum="0.0"/>
<Label HorizontalAlignment="Center">Y</Label>
</StackPanel>
</Grid>
</Grid>
如果您将贝塞尔曲线控制点包裹在 Thumb
元素中,那么您可以很容易地完成您想要的。
<!-- Bezier Control Point -->
<Canvas Grid.Column="0">
<Thumb DragDelta="Thumb_DragDelta" Canvas.Left="{Binding QX, Mode=TwoWay}" Canvas.Top="{Binding QY, Mode=TwoWay}">
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="5" Height="5" Fill="Indigo" Stroke="Indigo" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
注意:我必须将 Panel.ZIndex="-1"
添加到 QuadraticBezierSegment,这样椭圆就会呈现在贝塞尔曲线段的前面。或者您可以在贝塞尔曲线段声明之后移动拇指部分。
隐藏代码:
private void Thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
UIElement thumb = e.Source as UIElement;
Canvas.SetLeft(thumb, Canvas.GetLeft(thumb) + e.HorizontalChange);
Canvas.SetTop(thumb, Canvas.GetTop(thumb) + e.VerticalChange);
}
您可以使用 Microsoft.Xaml.Behaviors.Wpf nuget 包将隐藏代码转换为视图模型中的事件处理程序。
看起来像这样
<Canvas Grid.Column="0">
<Thumb Canvas.Left="{Binding QX, Mode=TwoWay}" Canvas.Top="{Binding QY, Mode=TwoWay}">
<b:Interaction.Triggers>
<b:EventTrigger EventName="DragDelta">
<b:InvokeCommandAction Command="{Binding HandleDragDelta}" PassEventArgsToCommand="True" />
</b:EventTrigger>
</b:Interaction.Triggers>
<Thumb.Template>
<ControlTemplate>
<Ellipse Width="5" Height="5" Fill="Indigo" Stroke="Indigo" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
其中 HandleDragDelta
是某种 ICommand
实现,可以采用 DragDeltaEventArgs
参数,因为您需要它。
private DelegateCommand<DragDeltaEventArgs> handleDragDelta;
public ICommand HandleDragDelta => handleDragDelta ??= new DelegateCommand<DragDeltaEventArgs>(PerformHandleDragDelta);
private void PerformHandleDragDelta(DragDeltaEventArgs e)
{
UIElement thumb = e.Source as UIElement;
Canvas.SetLeft(thumb, Canvas.GetLeft(thumb) + e.HorizontalChange);
Canvas.SetTop(thumb, Canvas.GetTop(thumb) + e.VerticalChange);
}