WPF:滑块,当用户拖动后触发事件

64

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

我尝试使用ValueChanged事件,但是每次值更改时都会触发该事件,所以如果您在滑块上拖动,事件将多次触发,我希望事件仅在用户完成拖动滑块并获得新值后才触发

我该如何实现这一点?


[更新]

我在MSDN上找到了这篇文章,它基本上讨论了同样的问题,并提出了两种"解决方案":子类化Slider或在ValueChanged事件中调用DispatcherTimer,在时间间隔后调用操作。

你能想出比上述两个解决方案更好的方法吗?

13个回答

87

除了使用 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
@CodyF:你可能可以重写受保护的OnThumbDragCompleted和OnThumbDragStarted方法。 - ravuya
谢谢你,我的Visual Studio甚至没有检测到Thumb.DragStarted是一个可能性! - Ruud van Gaal
我遇到了一个问题,即在页面变量(一个文本框)被初始化之前就调用了ValueChanged事件。因此,请不要忘记进行空值检查... - tgraupmann
1
它给我报错:...不包含Slider_DragCompleted的定义,也没有可访问的扩展方法Slider_DragCompleted接受类型为'MainWindow'的第一个参数 - mrid

61
您可以使用拇指的“DragCompleted”事件来实现此功能。不幸的是,这只在拖动时被触发,因此您需要单独处理其他点击和按键操作。如果您只想让它可拖动,可以通过将LargeChange设置为0并将Focusable设置为false来禁用移动滑块的其他方式。
例子:
<Slider Thumb.DragCompleted="MySlider_DragCompleted" />

谢谢,那个事件完全可以。 - Andreas Grech
5
需要翻译的内容:Careful though, this won't handle the user changing the value by pressing the arrow keys, see santo's post for a solution that will.请注意,这种方法无法处理用户通过按箭头键更改值的情况,可以查看Santo的帖子以获取一个解决方案。 - Omer Raviv
4
很遗憾,WinRT 没有 Thumb 的等效物。 - Freakishly
它在UWP中无法使用。 - lindexi
使用Interactivity.Interaction.Triggers设置调用中继命令时似乎无法正常工作。但是Peter的解决方案(使用PreviewMouseUp事件)在实现为中继命令时确实有效。 - BoiseBaked

21
<Slider PreviewMouseUp="MySlider_DragCompleted" />

对我来说可行。

您想要的值是在鼠标弹起事件之后,无论是在侧面单击还是拖动句柄后,都是该值。

由于MouseUp不会向下隧道(它在处理之前就已经处理了),因此您必须使用PreviewMouseUp。


@minusvoter:很酷,十年后成为唯一一个点踩的人,甚至没有留下评论 :) - Peter
我喜欢Peter的解决方案! - BoiseBaked
仍然不确定为什么必须使用PreviewMouseUp(首先引发的隧道传播事件)而不是MouseUp(冒泡传播事件),但这是另一天要理解的事情。 - BoiseBaked
如果设置了 IsMoveToPointEnabled="True"mySlider.AddHandler(Slider.PreviewMouseDownEvent, new MouseButtonEventHandler(mySlider_PreviewMouseDown), true);,那么当我点击滑块(而不是拇指)时,拇指会跳到鼠标指针位置,然后我就无法拖动拇指。如果我需要拖动拇指,我必须释放鼠标,然后单独进行拖动操作。 - CodingNinja

9

我将提供一个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在超时时间内没有接收到信号时,Do something部分才会发生。
为什么要这样做?我不喜欢在视图中生成行为或处理不必要的事件。所有代码都在一个地方,不需要额外的事件,ViewModel可以选择在每个值更改时或用户操作结束时(特别是在使用绑定时)作出反应,这增加了很多灵活性。

仔细阅读后,显然这是一个很好的方法,唯一的问题是它会在每个Task.Run中创建一个新线程...这样就可以在短时间内创建100个线程,这会有所影响吗? - Juan Pablo Garcia Coello
如果任务正在运行并在更改属性时达到“等待”状态,则脉冲将简单地完成它。这就像总是等待超时,只有在从上次更改开始计时后发生超时时才执行某些操作。您是正确的,没有同步来确保哪个任务正在等待,因此理论上可能会有几个任务(不是100个)等待直到超时。但是,每次只有一个任务能够获得锁并一次执行某些操作。在我的情况下,没有问题,可以在用户完成更新滑块后的一秒钟内连续发生几个 某些操作 - Sinatr
1
绑定有一个 Delay 属性,使用一些小值(100 毫秒?)将使得这种情况(多个任务等待时)很少发生。也许这个属性可以单独用作解决方案。 - Sinatr
好的,只有少量线程将运行,因为PulseAll会“处理”Task.Run线程,因为等待被“取消”,谢谢,这很酷。 - Juan Pablo Garcia Coello
我真的很喜欢这个,我在我正在工作的项目中尝试了一下,它似乎在UWP应用程序中完美地运行。说实话,我以前好像没有使用过Monitor类,所以我会更详细地研究一下它。非常有用。 - Dan Harris
显示剩余2条评论

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


不错的创新,为一个老问题带来了更新的解决方案。 - SandRock

1

这里有一个处理此问题的行为,同时还有键盘相同的事情。https://gist.github.com/4326429

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

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

我已经对你的回答进行了多次下投票,原因如下:你提供的实现方式非常不可靠和不正确。变量applyKeyUpValue没有被使用,keysDown在某些情况下进入无效状态,ValueProperty是不必要的,因为我们可以访问AssociatedObject.Value,而且我在WPF方面并不是很擅长,但我相当确定Value(Property)的绑定在你将NewValue分配给Value(Property)时不会得到更新。我在答案中添加了一种替代行为解决方案。 - Teneko

0

这个子类化版本的滑块可以按照您的要求工作:

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 似乎不明智。 - SandRock

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

0

我对WinUI3 v1.2.2的解决方案如下:

Xaml文件:

                <Slider Margin="10, 0" MinWidth="200" LargeChange="0.5"
                                TickPlacement="BottomRight" TickFrequency="10" 
                                SnapsTo="StepValues" StepFrequency="5"
                                Maximum="719" 
                                Value="{x:Bind Path=XamlViewModel.XamlSliderToDateInt, Mode=TwoWay}">
                </Slider>

截止日期滑块属性:

    private int _sliderToDateInt;
    public int XamlSliderToDateInt
    {
        get { return _sliderToDateInt; }
        set
        {
            SetProperty(ref _sliderToDateInt, value);
            _myDebounceTimer.Debounce(() =>
            {
                this.XamlSelectedTimeChangedTo = TimeSpan.FromMinutes(value);

                // time-expensive methods:
                this.XamlLCModel = _myOxyPlotModel.UpdatePlotModel(_myLCPowerRecList, XamlSliderFromDateInt, XamlSliderToDateInt, _myOxyPlotPageOptions);
                this.XamlTRModel = _myOxyPlotModel.UpdatePlotModel(_myTRPowerRecList, XamlSliderFromDateInt, XamlSliderToDateInt, _myOxyPlotPageOptions);
            },
            TimeSpan.FromSeconds(0.6));
        }
    }

计时器声明:

        private DispatcherQueueTimer _myDebounceTimer;

在构造函数中进行计时器初始化:

        _myDebounceTimer = _dispatcherQueue.CreateTimer();

方法_myOxyPlotModel.UpdatePlotModel()将不会快于每0.6秒调用一次,即使XamlSliderToDateInt属性通过拖动滑条进行更快的更新。
感觉就像拖动到一个位置,然后停止拖动/释放鼠标按钮,停止后计时器计数到0.6秒并调用我的oxyplot方法。 Debounce()方法属于命名空间CommunityToolkit.WinUI.UI。

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

希望这能帮到某个人!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接