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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/723502/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 20:14:45  来源:igfitidea点击:

WPF: Slider with an event that triggers after a user drags

wpfxamlslider

提问by Andreas Grech

I am currently making an MP3 player in WPF, and I want to make a slider that will allow the user to seek to a particular position in an MP3 by sliding the slider to the left or right.

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

I have tried using the ValueChangedevent but that triggers every time it's value is changed, so if you drag it across, the event will fire multiple times, I want the event to only fire when the user has finished dragging the slider and Then get the new value.

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

How can I achieve this?

我怎样才能做到这一点?



[Update]

[更新]

I have found this poston MSDN which basically discusses the same thing, and they came up with two "solutions"; either subclassing the Slider or invoking a DispatcherTimerin the ValueChangedevent that invokes the action after a timespan.

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

Can you come up with anything better then the two mentioned above?

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

采纳答案by YotaXP

You can use the thumb's 'DragCompleted' event for this. Unfortunately, this is only fired when dragging, so you'll need to handle other clicks and key presses separately. If you only want it to be draggable, you could disable these means of moving the slider by setting LargeChange to 0 and Focusable to false.

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

Example:

例子:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />

回答by Alan

Besides using the Thumb.DragCompletedevent you can also use both ValueChangedand Thumb.DragStarted, this way you don't lose functionality when the user modifies the value by pressing the arrow keys or by clicking on the slider bar.

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

Xaml:

Xml:

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

Code behind:

后面的代码:

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

回答by Peter

<Slider PreviewMouseUp="MySlider_DragCompleted" />

works for me.

对我来说有效。

The value you want is the value after a mousup event, either on clicks on the side or after a drag of the handle.

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

Since MouseUp doesn't tunnel down (it is handeled before it can), you have to use PreviewMouseUp.

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

回答by Sinatr

Another MVVM-friendly solution (I was not happy with answers)

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

View:

看法:

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

ViewModel:

视图模型:

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

It's delayed (by 1000ms in given example) operation. New task is created for every change done by slider (either by mouse or keyboard). Before starting task it signals(by using Monitor.PulseAll, perhaps even Monitor.Pulsewould be enough) to running already tasks (if any) to stop. Do somethingpart will only occurs when Monitor.Waitdon't get signal within timeout.

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

Why this solution? I don't like spawning behavior or having unnecessary event handling in the View. All code is in one place, no extra events needed, ViewModelhas choice to either react on each value change or at the end of user operation (which adds tons of flexibility, especially when using binding).

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

回答by Bagerfahrer

My implementation is based on @Alan's and @SandRock's answer:

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

You can apply it in that way:

你可以这样应用它:

...
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>

I've set the UpdateSourceTrigger to explicit, as the behaviour does it. And you are in need of the nuget package Microsoft.Xaml.Behaviors(.Wpf/.Uwp.Managed).

我已经将 UpdateSourceTrigger 设置为显式,因为行为就是这样。并且您需要 nuget 包 Microsoft.Xaml.Behaviors(.Wpf/.Uwp.Managed)。

回答by SandRock

Here is a behavior that handles this problem plus the same thing with the keyboard. https://gist.github.com/4326429

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

It exposes a Command and Value properties. The value is passed as the parameter of the command. You can databind to the value property (and use it in the viewmodel). You may add an event handler for a code-behind approach.

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

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

回答by beefsupreme

My solution is basically Santo's solution with a few more flags. For me, the slider is being updated from either reading the stream or the user manipulation (either from a mouse drag or using the arrow keys etc)

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

First, I had wrote the code to update the slider value from reading the stream:

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

    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
        }
    }

I then added my code that captured when the user was manipulating the slider with a mouse drag:

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

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

And added the code behind:

并在后面添加了代码:

/// <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;
}

When the user updates the slider's value with a mouse drag, the value will still change due to the stream being read and calling UpdateSliderPosition(). To prevent conflicts, UpdateSliderPosition()had to be changed:

当用户通过鼠标拖动更新滑块的值时,由于正在读取和调用流,该值仍会更改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
        }
    }
}

While this will prevent conflicts, we are still unable to determine whether the value is being updated by the user or by a call to UpdateSliderPosition(). This is fixed by yet another flag, this time set from within UpdateSliderPosition().

虽然这将防止冲突,但我们仍然无法确定该值是由用户更新还是通过调用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;
            }
        }
    }

Finally, we're able to detect whether the slider is being updated by the user or by the call to UpdateSliderPosition():

最后,我们能够检测滑块是由用户更新还是通过调用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()
        }
    }

Hope that helps someone!

希望对某人有所帮助!

回答by Vincent

If you want to get the manipulation ended information even if the user is not using the thumb to change the value (ie clicking somewhere in the track bar), you can attach an event handler to your slider for the pointer pressed and capture lost events. You can do the same thing for the keyboard events

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

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

The "magic" here is the AddHandler with the true parameter at the end which allows us to get the slider "internal" events. The event handlers :

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

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

The m_bIsPressed member will be true when the user is currently manipulating the slider (click, drag or keyboard). It will be reset to false once done .

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

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

回答by soan saini

I liked Answer by @sinatr.

我喜欢@sinatr 的回答。

My Solution Based on Answer Above: This solution cleans up the code a lot and encapsulates the mechanism.

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

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

Use it in Your class as:

在您的课程中使用它作为:

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
    }
}

回答by Dominik Palo

This subclassed version of the Slider wokrs as you want:

根据需要,此 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();
    }
}