WPF:Слайдер с событием, которое срабатывает после перетаскивания пользователем

StackOverflow https://stackoverflow.com/questions/723502

  •  05-09-2019
  •  | 
  •  

Вопрос

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

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

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


[Обновление]

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

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

Это было полезно?

Решение

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

Пример:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />

Другие советы

Помимо использования 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);
}
<Slider PreviewMouseUp="MySlider_DragCompleted" />

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

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

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

Еще одно решение, ориентированное на 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 ms в данном примере) операция.Новая задача создается для каждого изменения, выполняемого ползунком (либо мышью, либо клавиатурой).Перед началом выполнения задачи необходимо сигналы (используя Monitor.PulseAll, возможно , даже Monitor.Pulse было бы достаточно), чтобы уже запущенные задачи (если таковые имеются) остановить. Сделай что - нибудь часть будет происходить только тогда, когда Monitor.Wait не получайте сигнал в течение тайм-аута.

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

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

Он предоставляет свойства команды и значения.Значение передается в качестве параметра команды.Вы можете привязать данные к свойству 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>

И добавил код, лежащий в основе:

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

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

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

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

Мне понравился ответ @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
    }
}
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>

PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));

Эта подклассовая версия слайдера работает так, как вы хотите:

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();
    }
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top