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

WPF:滑动条,带有在用户拖动后触发的事件,wpf,xaml,slider,Wpf,Xaml,Slider,我目前正在用WPF制作一个MP3播放器,我想制作一个滑块,允许用户通过向左或向右滑动滑块来寻找MP3中的特定位置 我曾尝试使用ValueChanged事件,但每次它的值更改时都会触发,因此如果将其拖动,事件将触发多次,我希望事件仅在用户完成拖动滑块并获取新值时才会触发 我怎样才能做到这一点 [更新] 我在MSDN上发现,它基本上讨论了相同的事情,他们提出了两个“解决方案”;将滑块子类化或在ValueChanged事件中调用调度程序,该事件在时间跨度后调用操作 你能想出比上面提到的两个更好的方法

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

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

我怎样才能做到这一点


[更新]

我在MSDN上发现,它基本上讨论了相同的事情,他们提出了两个“解决方案”;将滑块子类化或在
ValueChanged
事件中调用调度程序
,该事件在时间跨度后调用操作


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

你可以使用thumb的“DragCompleted”事件来解决这个问题。不幸的是,这只会在拖动时触发,因此您需要分别处理其他单击和按键操作。如果您只希望它是可拖动的,可以通过将LargeChange设置为0并将Focusable设置为false来禁用这些移动滑块的方法

例如:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />


对我有用

您想要的值是mousup事件后的值,无论是在侧面单击还是在拖动控制柄后

由于MouseUp不能向下移动(它在移动之前已被处理),因此必须使用PreviewMouseUp


<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>

PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));
PositionSlider.LostMouseCapture+=新的MouseEventHandler(Position_LostMouseCapture); PositionSlider.AddHandler(Thumb.DragCompletedEvent,新的DragCompletedEventHandler(Position_DragCompleted));
除了使用
Thumb.DragCompleted
事件外,还可以同时使用
ValueChanged
Thumb.DragStarted
,这样,当用户通过按箭头键或单击滑块来修改值时,不会丢失功能

Xaml:


代码隐藏:

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);
}
/// <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;
}
private bool dragStarted=false;
私有无效滑块\u DragCompleted(对象发送方,DragCompletedEventArgs e)
{
DoWork(((滑块)发送器).Value);
this.dragStarted=false;
}
私有无效滑块\u DragStarted(对象发送器,DragStartedEventArgs e)
{
this.dragStarted=true;
}
私有无效滑块\u值已更改(
对象发送器,
RoutedPropertyChangedEventArgs(e)
{
如果(!dragStarted)
嫁妆(即新价值);
}

以下是一种处理此问题的行为,以及键盘的相同操作

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

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

我的解决方案基本上是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>
虽然这将防止冲突,但我们仍然无法确定该值是由用户还是通过调用
UpdateSliderPosition()
进行更新。这是由另一个标志修复的,这次是从
UpdateSliderPosition()
中设置的


希望这对别人有帮助

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

视图:

它被延迟(在给定的示例中延迟了
1000
ms)操作。对于滑块(鼠标或键盘)所做的每个更改,都会创建新任务。在开始任务之前,它会发出信号(通过使用
Monitor.pulsell
,甚至可能
Monitor.Pulse
就足够了)让正在运行的任务(如果有的话)停止。“执行某些操作”部分仅在
监视时发生。等待
在超时内未收到信号


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

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

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成员将为true。一旦操作完成,它将重置为false

private void OnValueChanged(object sender, object e)
{
    if(!m_bIsPressed) { // do something }
}

滑块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();
    }
}

我喜欢@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
    }
}

我的实施基于@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();
        }
    }
公共类SliderValueChangeByDragBehavior:行为
{
二等兵布尔已经出发了;
/// 
///关于附加的行为。
/// 
受保护的覆盖无效附加()
{
AssociatedObject.AddHandler(Thumb.DragStartedEvent,(DragStartedEventHandler)滑块\u DragStarted);
AssociatedObject.AddHandler(Thumb.DragCompletedEvent,(DragCompletedEventHandler)滑块\u DragCompleted);
AssociatedObject.ValueChanged+=滑块\u值已更改;
base.onatached();
}
/// 
///关于行为分离。
///
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);
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;
}
private void OnValueChanged(object sender, object e)
{
    if(!m_bIsPressed) { // do something }
}
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();
    }
}
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
    }
}
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>