WPF:带有在用户拖动后触发的事件的滑块

Posted

技术标签:

【中文标题】WPF:带有在用户拖动后触发的事件的滑块【英文标题】:WPF: Slider with an event that triggers after a user drags 【发布时间】:2010-10-17 22:24:28 【问题描述】:

我目前正在 WPF 中制作 MP3 播放器,我想制作一个滑块,允许用户通过向左或向右滑动滑块来寻找 MP3 中的特定位置。

我曾尝试使用ValueChanged 事件,但每次更改它的值时都会触发,因此如果将其拖过,该事件将触发多次,我希望该事件仅在用户触发时触发完成拖动滑块然后获取新值

我怎样才能做到这一点?


[更新]

我在 MSDN 上找到了this post,它基本上讨论了同样的事情,他们提出了两个“解决方案”;将 Slider 子类化或在 ValueChanged 事件中调用 DispatcherTimer,该事件在一段时间后调用操作。

你能想出比上面提到的两个更好的吗?

【问题讨论】:

【参考方案1】:

除了使用Thumb.DragCompleted 事件之外,您还可以同时使用ValueChangedThumb.DragStarted,这样当用户通过按箭头键或单击滑动条修改值时,您不会失去功能。

Xaml:

<Slider ValueChanged="Slider_ValueChanged"
    Thumb.DragStarted="Slider_DragStarted"
    Thumb.DragCompleted="Slider_DragCompleted"/>

后面的代码:

private bool dragStarted = false;

private void Slider_DragCompleted(object sender, DragCompletedEventArgs e)

    DoWork(((Slider)sender).Value);
    this.dragStarted = false;


private void Slider_DragStarted(object sender, DragStartedEventArgs e)

    this.dragStarted = true;


private void Slider_ValueChanged(
    object sender,
    RoutedPropertyChangedEventArgs<double> e)

    if (!dragStarted)
        DoWork(e.NewValue);

【讨论】:

如何以编程方式找到 DragStarted 和 DragCompleted 事件? @CodyF:您也许可以覆盖受保护的 OnThumbDragCompleted 和 OnThumbDragStarted 方法。 谢谢你;我的 Visual Studio 没有检测到 Thumb.DragStarted 甚至是一种可能性! 在初始化页面变量(文本框)之前调用 ValueChanged 事件时遇到问题。所以不要忘记你的空检查...... 它给了我错误... does not contain a definition for Slider_DragCompleted and no accessible extension method Slider_DragCompleted accepting first argument of type 'MainWindow'【参考方案2】:

您可以为此使用拇指的“DragCompleted”事件。不幸的是,这仅在拖动时触发,因此您需要单独处理其他点击和按键。如果您只希望它是可拖动的,您可以通过将 LargeChange 设置为 0 并将 Focusable 设置为 false 来禁用这些移动滑块的方法。

例子:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />

【讨论】:

干杯;那个事件会很好。 小心,这不会处理用户通过按箭头键更改值,请参阅 santo 的帖子以获取解决方案。 不幸的是,WinRT 没有 Thumb 等效项 UWP 中不使用。 在使用 Interactivity.Interaction.Triggers 设置对中继命令的调用时似乎不起作用。但 Peter 的解决方案(使用 PreviewMouseUp 事件)在实现为中继命令时确实有效。【参考方案3】:
<Slider PreviewMouseUp="MySlider_DragCompleted" />

为我工作。

您想要的值是鼠标点击事件后的值,无论是点击侧面还是拖动手柄后。

由于 MouseUp 不会向下隧道(它在它可以之前被处理),因此您必须使用 PreviewMouseUp。

【讨论】:

@minusvoter : 很酷,10 年后是负 1,也是唯一的一个,甚至没有发表评论 :) 我喜欢彼得的解决方案! 仍然不知道为什么你必须使用 PreviewMouseUp(一个首先引发的隧道传播事件)而不是 MouseUp(一个气泡传播事件),但那是另一天来理解的。【参考方案4】:

另一个 MVVM 友好的解决方案(我对答案不满意)

查看:

<Slider Maximum="100" Value="Binding SomeValue"/>

视图模型:

public class SomeViewModel : INotifyPropertyChanged

    private readonly object _someValueLock = new object();
    private int _someValue;
    public int SomeValue
    
        get  return _someValue; 
        set
        
            _someValue = value;
            OnPropertyChanged();
            lock (_someValueLock)
                Monitor.PulseAll(_someValueLock);
            Task.Run(() =>
            
                lock (_someValueLock)
                    if (!Monitor.Wait(_someValueLock, 1000))
                    
                        // do something here
                    
            );
        
    

它被延迟了(在给定的例子中是1000ms)操作。为滑块(通过鼠标或键盘)完成的每个更改创建新任务。在开始任务之前它发出信号(通过使用Monitor.PulseAll,甚至Monitor.Pulse 就足够了)停止运行已经运行的任务(如果有的话)。 做某事部分只会在Monitor.Wait在超时内没有收到信号时发生。

为什么是这个解决方案?我不喜欢在视图中产生行为或进行不必要的事件处理。所有代码都在一个地方,不需要额外的事件,ViewModel 可以选择对每个值更改或在用户操作结束时做出反应(这增加了很多灵活性,尤其是在使用绑定时)。

【讨论】:

详细阅读它,显然是一个非常好的方法,唯一的问题是它在每个 Task.Run 中创建一个新线程......所以它可以在短时间内创建 100 个线程,确实有关系吗? 如果任务开始运行并在属性更改时到达Wait,那么脉冲将简单地完成它。这就像总是等待超时,只有在超时发生时才做某事,从最后一次更改开始计算。你是对的,没有同步来确保正在等待什么任务,所以理论上几个任务(不是 100 个)可能会等待直到超时。但是只有一个人能够一次获得锁定并执行某些操作。在我的情况下,在用户完成更新滑块后的一秒钟内,几个 某事 接二连三地发生并不是问题。 绑定有Delay 属性,使用一些小的值(100 毫秒?)将使这种情况(当多个任务正在等待时)不太可能发生。也许这个属性可以单独用作解决方案。 好的,所以只有少数线程会运行,因为 PulseAll 将“处理”Task.Run 线程,因为等待被“取消”了,谢谢,更酷 我真的很喜欢这个,我在我现在正在做的一个项目中尝试过它,它似乎在 UWP 应用程序中完美运行。老实说,我认为我以前没有使用过 Monitor 类,所以我将更详细地研究它。很有用【参考方案5】:

我的实现基于@Alan 和@SandRock 的回答:

public class SliderValueChangeByDragBehavior : Behavior<Slider>
    
        private bool hasDragStarted;

        /// <summary>
        /// On behavior attached.
        /// </summary>
        protected override void OnAttached()
        
            AssociatedObject.AddHandler(Thumb.DragStartedEvent, (DragStartedEventHandler)Slider_DragStarted);
            AssociatedObject.AddHandler(Thumb.DragCompletedEvent, (DragCompletedEventHandler)Slider_DragCompleted);
            AssociatedObject.ValueChanged += Slider_ValueChanged;

            base.OnAttached();
        

        /// <summary>
        /// On behavior detaching.
        /// </summary>
        protected override void OnDetaching()
        
            base.OnDetaching();

            AssociatedObject.RemoveHandler(Thumb.DragStartedEvent, (DragStartedEventHandler)Slider_DragStarted);
            AssociatedObject.RemoveHandler(Thumb.DragCompletedEvent, (DragCompletedEventHandler)Slider_DragCompleted);
            AssociatedObject.ValueChanged -= Slider_ValueChanged;
        

        private void updateValueBindingSource()
            => BindingOperations.GetBindingExpression(AssociatedObject, RangeBase.ValueProperty)?.UpdateSource();

        private void Slider_DragStarted(object sender, DragStartedEventArgs e)
            => hasDragStarted = true;

        private void Slider_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
        
            hasDragStarted = false;
            updateValueBindingSource();
        

        private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        
            if (!hasDragStarted)
                updateValueBindingSource();
        
    

你可以这样应用它:

...
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:myWhateverNamespace="clr-namespace:My.Whatever.Namespace;assembly=My.Whatever.Assembly"
...

<Slider
                x:Name="srUserInterfaceScale"
                VerticalAlignment="Center"
                DockPanel.Dock="Bottom"
                IsMoveToPointEnabled="True"
                Maximum="x:Static localLibraries:Library.MAX_USER_INTERFACE_SCALE"
                Minimum="x:Static localLibraries:Library.MIN_USER_INTERFACE_SCALE"
                Value="Binding Source=x:Static localProperties:Settings.Default, Path=UserInterfaceScale, UpdateSourceTrigger=Explicit">
                <i:Interaction.Behaviors>
                    <myWhateverNamespace:SliderValueChangeByDragBehavior />
                </i:Interaction.Behaviors>
            </Slider>

我已将 UpdateSourceTrigger 设置为显式,就像行为一样。而且您需要 nuget 包 Microsoft.Xaml.Behaviors(.Wpf/.Uwp.Managed)。

【讨论】:

很好地刷新了一个老问题。【参考方案6】:

这是一个处理此问题的行为以及与键盘相同的事情。 https://gist.github.com/4326429

它公开了一个命令和值属性。该值作为命令的参数传递。您可以将数据绑定到 value 属性(并在 viewmodel 中使用它)。您可以为代码隐藏方法添加事件处理程序。

<Slider>
  <i:Interaction.Behaviors>
    <b:SliderValueChangedBehavior Command="Binding ValueChangedCommand"
                                  Value="Binding MyValue" />
  </i:Interaction.Behaviors>
</Slider>

【讨论】:

出于以下几个原因,我对您的答案投了反对票:您提供的实现非常不可靠且不正确。未使用变量 applyKeyUpValue,keysDown 在某些情况下进入无效状态,ValueProperty 不是必需的,因为我们可以访问 AssociatedObject.Value 并且我在 WPF 中不是那么好,但我很确定 Value 的绑定当您将 NewValue 分配给 Value(Property) 时,(Property) 不会更新。我在答案中添加了另一种行为解决方案。【参考方案7】:

我的解决方案基本上是 Santo 的解决方案,带有更多标志。对我来说,滑块正在通过读取流或用户操作(通过鼠标拖动或使用箭头键等)进行更新

首先,我编写了代码来通过读取流来更新滑块值:

    delegate void UpdateSliderPositionDelegate();
    void UpdateSliderPosition()
    
        if (Thread.CurrentThread != Dispatcher.Thread)
        
            UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
            Dispatcher.Invoke(function, new object[]  );
        
        else
        
            double percentage = 0;  //calculate percentage
            percentage *= 100;

            slider.Value = percentage;  //this triggers the slider.ValueChanged event
        
    

然后我添加了当用户通过鼠标拖动操作滑块时捕获的代码:

<Slider Name="slider"
        Maximum="100" TickFrequency="10"
        ValueChanged="slider_ValueChanged"
        Thumb.DragStarted="slider_DragStarted"
        Thumb.DragCompleted="slider_DragCompleted">
</Slider>

并在后面添加代码:

/// <summary>
/// True when the user is dragging the slider with the mouse
/// </summary>
bool sliderThumbDragging = false;

private void slider_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)

    sliderThumbDragging = true;


private void slider_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)

    sliderThumbDragging = false;

当用户通过鼠标拖动更新滑块的值时,由于正在读取流并调用UpdateSliderPosition(),该值仍会发生变化。为防止冲突,UpdateSliderPosition() 必须更改:

delegate void UpdateSliderPositionDelegate();
void UpdateSliderPosition()

    if (Thread.CurrentThread != Dispatcher.Thread)
    
        UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
        Dispatcher.Invoke(function, new object[]  );
    
    else
    
        if (sliderThumbDragging == false) //ensure user isn't updating the slider
        
            double percentage = 0;  //calculate percentage
            percentage *= 100;

            slider.Value = percentage;  //this triggers the slider.ValueChanged event
        
    

虽然这可以防止冲突,但我们仍然无法确定该值是由用户更新还是通过调用 UpdateSliderPosition() 来更新的。这由另一个标志修复,这次是从UpdateSliderPosition() 中设置的。

    /// <summary>
    /// A value of true indicates that the slider value is being updated due to the stream being read (not by user manipulation).
    /// </summary>
    bool updatingSliderPosition = false;
    delegate void UpdateSliderPositionDelegate();
    void UpdateSliderPosition()
    
        if (Thread.CurrentThread != Dispatcher.Thread)
        
            UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
            Dispatcher.Invoke(function, new object[]  );
        
        else
        
            if (sliderThumbDragging == false) //ensure user isn't updating the slider
            
                updatingSliderPosition = true;
                double percentage = 0;  //calculate percentage
                percentage *= 100;

                slider.Value = percentage;  //this triggers the slider.ValueChanged event

                updatingSliderPosition = false;
            
        
    

最后,我们能够检测滑块是由用户更新还是通过调用UpdateSliderPosition()

    private void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    
        if (updatingSliderPosition == false)
        
            //user is manipulating the slider value (either by keyboard or mouse)
        
        else
        
            //slider value is being updated by a call to UpdateSliderPosition()
        
    

希望对某人有所帮助!

【讨论】:

【参考方案8】:

如果您想获得操作结束信息,即使用户没有使用拇指来更改值(即单击轨迹栏中的某处),您可以将事件处理程序附加到滑块以获取按下的指针并捕获丢失的事件。你可以对键盘事件做同样的事情

var pointerPressedHandler   = new PointerEventHandler(OnSliderPointerPressed);
slider.AddHandler(Control.PointerPressedEvent, pointerPressedHandler, true);

var pointerCaptureLostHandler   = new PointerEventHandler(OnSliderCaptureLost);
slider.AddHandler(Control.PointerCaptureLostEvent, pointerCaptureLostHandler, true);

var keyDownEventHandler = new KeyEventHandler(OnSliderKeyDown);
slider.AddHandler(Control.KeyDownEvent, keyDownEventHandler, true);

var keyUpEventHandler   = new KeyEventHandler(OnSliderKeyUp);
slider.AddHandler(Control.KeyUpEvent, keyUpEventHandler, true);

这里的“魔法”是 AddHandler 最后带有 true 参数,它允许我们获取滑块“内部”事件。 事件处理程序:

private void OnKeyDown(object sender, KeyRoutedEventArgs args)

    m_bIsPressed = true;

private void OnKeyUp(object sender, KeyRoutedEventArgs args)

    Debug.WriteLine("VALUE AFTER KEY CHANGE 0", slider.Value);
    m_bIsPressed = false;


private void OnSliderCaptureLost(object sender, PointerRoutedEventArgs e)

    Debug.WriteLine("VALUE AFTER CHANGE 0", slider.Value);
    m_bIsPressed = false;

private void OnSliderPointerPressed(object sender, PointerRoutedEventArgs e)

    m_bIsPressed = true;

当用户当前正在操作滑块(单击、拖动或键盘)时,m_bIsPressed 成员将为真。一旦完成,它将被重置为 false。

private void OnValueChanged(object sender, object e)

    if(!m_bIsPressed)  // do something 

【讨论】:

【参考方案9】:

你想要的这个 Slider wokrs 的子类版本:

public class NonRealtimeSlider : Slider

    static NonRealtimeSlider()
    
        var defaultMetadata = ValueProperty.GetMetadata(typeof(TextBox));

        ValueProperty.OverrideMetadata(typeof(NonRealtimeSlider), new FrameworkPropertyMetadata(
        defaultMetadata.DefaultValue,
        FrameworkPropertyMetadataOptions.Journal | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
        defaultMetadata.PropertyChangedCallback,
        defaultMetadata.CoerceValueCallback,
        true,
        UpdateSourceTrigger.Explicit));
    

    protected override void OnThumbDragCompleted(DragCompletedEventArgs e)
    
        base.OnThumbDragCompleted(e);
        GetBindingExpression(ValueProperty)?.UpdateSource();
    

【讨论】:

继承 Slider 似乎不明智。【参考方案10】:

我喜欢@sinatr 的回答。

我的解决方案基于上述答案: 这个方案清理了很多代码,封装了机制。

public class SingleExecuteAction

    private readonly object _someValueLock = new object();
    private readonly int TimeOut;
    public SingleExecuteAction(int timeOut = 1000)
    
        TimeOut = timeOut;
    

    public void Execute(Action action)
    
        lock (_someValueLock)
            Monitor.PulseAll(_someValueLock);
        Task.Run(() =>
        
            lock (_someValueLock)
                if (!Monitor.Wait(_someValueLock, TimeOut))
                
                    action();
                
        );
    

在你的课堂上使用它:

public class YourClass

    SingleExecuteAction Action = new SingleExecuteAction(1000);
    private int _someProperty;

    public int SomeProperty
    
        get => _someProperty;
        set
        
            _someProperty = value;
            Action.Execute(() => DoSomething());
        
    

    public void DoSomething()
    
        // Only gets executed once after delay of 1000
    

【讨论】:

【参考方案11】:
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>

PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));

【讨论】:

我很乐意描述您的解决方案。抛出这样的代码没有帮助。 我会添加说明。 ggarber 与@YotaXP 的解决方案相同,但在代码中而不是在 XAML 中。解决方案没有区别。 哦,这种类型的事件声明称为附加事件,这意味着您可以将事件附加到本机不支持该事件的元素。

以上是关于WPF:带有在用户拖动后触发的事件的滑块的主要内容,如果未能解决你的问题,请参考以下文章

如何拖动元素并确保光标不会比它快?

反应:带有自定义钩子的滑块无法正常工作

WPF 滑块仅可拖动

防止拖动时触发点击事件[重复]

jQuery 自定义事件 - 当宽度为 xx 时触发事件

jQuery Mobile Slider 禁用点击事件