WPF: слайдер с событием, которое срабатывает после перетаскивания пользователя - PullRequest
49 голосов
/ 07 апреля 2009

В настоящее время я делаю MP3-плеер в WPF и хочу создать слайдер, который позволит пользователю искать определенную позицию в MP3, сдвигая слайдер влево или вправо.

Я пытался использовать событие ValueChanged, но оно срабатывает при каждом изменении его значения, поэтому, если вы перетаскиваете его, событие будет запускаться несколько раз, Я хочу, чтобы событие запускалось только тогда, когда пользователь имеет закончил перетаскивание ползунка и затем получил новое значение .

Как мне этого добиться?


[Update]

Я нашел этот пост на MSDN, в котором в основном обсуждается одно и то же, и они придумали два "решения"; либо создайте подкласс Slider, либо вызовите DispatcherTimer в событии ValueChanged, которое вызывает действие после промежутка времени.

Можете ли вы придумать что-нибудь лучше, чем два упомянутых выше?

Ответы [ 10 ]

69 голосов
/ 03 ноября 2009

Помимо использования события Thumb.DragCompleted вы также можете использовать как ValueChanged, так и Thumb.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);
}
49 голосов
/ 07 апреля 2009

Для этого вы можете использовать событие большого пальца DragCompleted. К сожалению, это срабатывает только при перетаскивании, поэтому вам придется обрабатывать другие нажатия и нажатия клавиш отдельно. Если вы хотите, чтобы он был только перетаскиваемым, вы можете отключить эти средства перемещения ползунка, установив LargeChange в 0 и Focusable в false.

Пример:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
11 голосов
/ 30 июня 2009
<Slider PreviewMouseUp="MySlider_DragCompleted" />

работает на меня.

Требуемое значение - это значение после события mousup, либо при щелчках сбоку, либо после перетаскивания ручки.

Так как MouseUp не туннелирует (он обрабатывается раньше, чем может), вы должны использовать PreviewMouseUp.

4 голосов
/ 17 октября 2014

Еще одно дружественное MVVM решение (я не был доволен ответами)

Вид:

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

Задержка (на 1000 мс в данном примере) операции. Новая задача создается для каждого изменения, выполненного с помощью ползунка (с помощью мыши или клавиатуры). Перед запуском задачи сигнализирует (с помощью Monitor.PulseAll, возможно, даже Monitor.Pulse будет достаточно) для запуска уже выполненных задач (если они есть) для остановки. Сделать что-то часть будет происходить только тогда, когда Monitor.Wait не получит сигнал в течение тайм-аута.

Почему это решение? Мне не нравится нерестовое поведение или ненужная обработка событий в представлении. Весь код находится в одном месте, никаких дополнительных событий не требуется, ViewModel имеет возможность реагировать на каждое изменение значения или в конце пользовательской операции (что добавляет гибкости, особенно при использовании связывания).

2 голосов
/ 18 декабря 2012

Вот поведение, которое решает эту проблему, плюс то же самое с клавиатурой. https://gist.github.com/4326429

Предоставляет свойства Command и Value. Значение передается в качестве параметра команды. Вы можете привязать данные к свойству value (и использовать его в модели представления). Вы можете добавить обработчик событий для подхода с выделенным кодом.

<Slider>
  <i:Interaction.Behaviors>
    <b:SliderValueChangedBehavior Command="{Binding ValueChangedCommand}"
                                  Value="{Binding MyValue}" />
  </i:Interaction.Behaviors>
</Slider>
0 голосов
/ 12 сентября 2018

Мне понравилось Ответить @ 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 голосов
/ 16 декабря 2015

Эта подклассовая версия 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();
    }
}
0 голосов
/ 07 августа 2015

Если вы хотите получить информацию об окончании манипуляции, даже если пользователь не использует большой палец для изменения значения (т. Е. Щелкает где-то на панели треков), вы можете прикрепить обработчик событий к вашему ползунку для нажатого указателя и захвата потерянные события. Вы можете сделать то же самое для событий клавиатуры

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 с истинным параметром в конце, который позволяет нам получить ползунок «внутренние» события. Обработчики событий:

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 }
}
0 голосов
/ 20 декабря 2012

Мое решение - это в основном решение Санто с несколькими флагами. Для меня ползунок обновляется либо из чтения потока, либо из-за манипуляций пользователя (либо путем перетаскивания мышью, либо с помощью клавиш со стрелками и т. Д.)

Сначала я написал код для обновления значения ползунка после чтения потока:

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

Надеюсь, это кому-нибудь поможет!

0 голосов
/ 30 июля 2009
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>

PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...