61

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

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

我怎样才能做到这一点?


[更新]

我在 MSDN 上发现这篇文章基本上讨论了同样的事情,他们提出了两个“解决方案”;子类化 Slider 或在时间跨度后调用操作DispatcherTimerValueChanged事件中调用 a。

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

4

11 回答 11

80

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

xml:

<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);
}
于 2009-11-03T07:51:35.957 回答
59

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

例子:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
于 2009-04-06T22:46:38.393 回答
20
<Slider PreviewMouseUp="MySlider_DragCompleted" />

为我工作。

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

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

于 2009-06-30T17:07:29.173 回答
7

另一个 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
                    }
            });
        }
    }
}

它被延迟(1000在给定示例中为毫秒)操作。为滑块(通过鼠标或键盘)完成的每个更改创建新任务。在开始任务之前,它会发出信号(通过使用Monitor.PulseAll,甚至Monitor.Pulse可能就足够了)运行已经运行的任务(如果有的话)停止。仅当在超时内未收到信号时才会执行某些操作。Monitor.Wait

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

于 2014-10-17T08:25:13.830 回答
4

我的实现基于@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)。

于 2019-09-17T06:57:30.750 回答
1

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

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

<Slider>
  <i:Interaction.Behaviors>
    <b:SliderValueChangedBehavior Command="{Binding ValueChangedCommand}"
                                  Value="{Binding MyValue}" />
  </i:Interaction.Behaviors>
</Slider>
于 2012-12-18T09:11:12.300 回答
0

我的解决方案基本上是带有更多标志的 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()
        }
    }

希望对某人有所帮助!

于 2012-12-20T15:15:29.863 回答
0

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

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 }
}
于 2015-08-07T14:39:52.450 回答
0

根据需要,此 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();
    }
}
于 2015-12-16T14:59:26.560 回答
0

我喜欢@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
    }
}
于 2018-09-12T02:02:45.577 回答
-1
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>

PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));
于 2009-07-29T22:10:34.607 回答