如何实现一个由鼠标移动的 WPF 控件,如滑块控件但 2D?

Posted

技术标签:

【中文标题】如何实现一个由鼠标移动的 WPF 控件,如滑块控件但 2D?【英文标题】:How to implement a WPF control that is moved by the mouse, like a slider control but 2D? 【发布时间】:2021-12-27 22:24:05 【问题描述】:

我想通过其控制点 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>

【问题讨论】:

将 MouseMove 处理程序添加到设置 Ellipse 位置的 Canvas。画布需要具有非空背景,即透明。通过 Canvas.Left 和 Top 而不是 RenderTransform 设置位置。 请注意,在视图模型中实现 x/y 值的双重存储似乎毫无意义。存储 x 和 y,或点,而不是两者。 如果用户可以快速移动鼠标,你会发现他们失去了一个“拇指”,不得不回去重新拿起它。正是出于这个原因,我制作了可拖动的用户控件,并在它们的包含面板中处理预览鼠标向下、移动和鼠标向上以获取可拖动的东西。 【参考方案1】:

如果您将贝塞尔控制点包装在 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 中,这样椭圆才能呈现在 Bezier 段的前面。或者您可以在贝塞尔线段声明之后移动拇指部分。

代码隐藏:

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

【讨论】:

谢谢,Thumb 正是我想要的。我还没有让事件处理程序工作 - DelegateCommand 需要 Prism 吗?但能够移动拇指是拼图中缺失的部分。 可以是棱镜。那里有许多 ICommand 实现。如果您不想使用 Prism,请参阅 gist.github.com/InKahootz/0fb62ad9330c07aec2f89a34e3c3e5a4 以获得简单的方法。一旦你连接了事件处理程序,它应该开始移动/可拖动!

以上是关于如何实现一个由鼠标移动的 WPF 控件,如滑块控件但 2D?的主要内容,如果未能解决你的问题,请参考以下文章

如何自定义滑块控件,如 wpf 中的音量

网格中控件周围的 WPF 可单击区域

C# wpf 实现任意控件拖动

2021-08-14 WPF控件专题 Slider控件详解

MFC中如何让静态控件响应鼠标移动的消息?

WPF 中的 Slider 控件如何捕捉特定值?