如何实现一个由鼠标移动的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);
}