WPF: слайдер с событием, которое запускается после того, как пользователь перетаскивает
В настоящее время я создаю MP3-плеер в WPF, и я хочу создать слайдер, который позволит пользователю искать определенную позицию в MP3, сдвигая ползунок влево или вправо.
Я попытался использовать событие ValueChanged
, но это срабатывает при каждом изменении значения, поэтому, если вы перетащите его, событие будет срабатывать несколько раз, Я хочу, чтобы событие срабатывало только тогда, когда пользователь закончив перетаскивание ползунка, а затем получим новое значение.
Как я могу это достичь?
[Обновление]
Я нашел этот пост на MSDN, который в основном обсуждает одно и то же, и они придумали два "решения"; либо подклассифицируя Slider, либо вызывая a DispatcherTimer
в событии ValueChanged
, которое вызывает действие после периода времени.
Можете ли вы придумать что-нибудь лучше, чем два упомянутых выше?
Ответы
Ответ 1
Для этого вы можете использовать для этого событие "DragCompleted" большого пальца. К сожалению, это происходит только при перетаскивании, поэтому вам придется обрабатывать другие нажатия и нажатия клавиш отдельно. Если вы хотите, чтобы его можно было перетаскивать, вы можете отключить эти средства перемещения ползунка, установив для параметра LargeChange значение 0 и Focusable в false.
Пример:
<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
Ответ 2
Кроме использования события 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);
}
Ответ 3
<Slider PreviewMouseUp="MySlider_DragCompleted" />
работает для меня.
Значение, которое вы хотите, - это значение после события mousup, либо на кликах сбоку, либо после перетаскивания дескриптора.
Так как MouseUp не туннелируется (до тех пор пока он не используется), вы должны использовать PreviewMouseUp.
Ответ 4
Другое решение, совместимое с 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
ms в данном примере). Новая задача создается для каждого изменения, выполняемого слайдером (либо с помощью мыши, либо с клавиатуры). Перед запуском задачи он сигнализирует (используя Monitor.PulseAll
, возможно, даже Monitor.Pulse
будет достаточно) для запуска уже задач (если они есть) для остановки. Что-то происходит, когда Monitor.Wait
не получает сигнал в течение таймаута.
Почему это решение? Мне не нравится поведение нереста или ненужное управление событиями в представлении. Весь код находится в одном месте, никаких дополнительных событий не требуется, ViewModel
имеет возможность либо реагировать на каждое изменение значения, либо в конце работы пользователя (что добавляет тонны гибкости, особенно при использовании привязки).
Ответ 5
Вот поведение, которое справляется с этой проблемой, плюс то же самое с клавиатурой. 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>
Ответ 6
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>
PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));
Ответ 7
Мое решение - это в основном решение 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()
}
}
Надеюсь, что это поможет кому-то!
Ответ 8
Если вы хотите получить информацию о завершении манипуляции, даже если пользователь не использует большой палец для изменения значения (т.е. щелкнув где-то на панели треков), вы можете прикрепить обработчик события к вашему ползунку для нажатия и захвата указателя потерянные события. Вы можете сделать то же самое для событий клавиатуры
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 будет прав, когда пользователь в настоящее время манипулирует слайдером (щелчок, перетаскивание или клавиатура). Это будет reset false после выполнения.
private void OnValueChanged(object sender, object e)
{
if(!m_bIsPressed) { // do something }
}
Ответ 9
Эта подклассовая версия слайдера 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();
}
}