WPF: curseur avec un événement qui se déclenche après un déplacement d'un utilisateur
Je fais actuellement un lecteur MP3 en WPF, et je veux faire un curseur qui permettra à l'utilisateur de chercher une position particulière dans un MP3 en faisant glisser le curseur vers la gauche ou la droite.
J'ai essayé d'utiliser le ValueChanged
événement, mais qui se déclenche à chaque fois qu'une valeur est modifiée, de sorte que si vous le faites glisser sur, l'événement se déclenche plusieurs fois, je veux l'événement déclenche uniquement lorsque l'utilisateur a fini de glisser le curseur, Puis obtenir la nouvelle valeur.
Comment puis-je atteindre cette?
[mise à Jour]
J'ai trouvé ce post sur MSDN qui discute essentiellement de la même chose, et ils ont trouvé deux "solutions" ; soit sous-classer le curseur ou invoquer un DispatcherTimer
dans l'événement ValueChanged
qui invoque l'action après un laps de temps.
Pouvez-vous trouver quelque chose de mieux que les deux mentionnés ci-dessus?
10 réponses
Vous pouvez utiliser L'événement' DragCompleted ' du pouce pour cela. Malheureusement, cela n'est déclenché que lorsque vous faites glisser, vous devrez donc gérer les autres clics et les pressions sur les touches séparément. Si vous voulez seulement qu'il soit déplaçable, vous pouvez désactiver ces moyens de déplacer le curseur en définissant LargeChange sur 0 et Focusable sur false.
Exemple:
<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
En plus d'utiliser l'événement Thumb.DragCompleted
, vous pouvez également utiliser à la fois ValueChanged
et Thumb.DragStarted
, de cette façon, vous ne perdez pas la fonctionnalité lorsque l'utilisateur modifie la valeur en appuyant sur les touches fléchées ou en cliquant sur la barre de curseur.
Xaml:
<Slider ValueChanged="Slider_ValueChanged"
Thumb.DragStarted="Slider_DragStarted"
Thumb.DragCompleted="Slider_DragCompleted"/>
Code derrière:
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" />
Fonctionne pour moi.
La valeur que vous voulez est la valeur après un événement mousup, soit sur les clics sur le côté ou après un glisser de la poignée.
Puisque MouseUp ne tunnel pas vers le bas (il est handeled avant qu'il puisse), vous devez utiliser PreviewMouseUp.
Une autre solution MVVM-friendly (Je n'étais pas satisfait des réponses)
Vue:
<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
}
});
}
}
}
Il est retardé (par 1000
ms dans l'exemple donné) opération. Nouvelle tâche est créée pour chaque changement effectué par curseur (soit par la souris ou le clavier). Avant de commencer la tâche, signale (en utilisant Monitor.PulseAll
, peut-être même Monitor.Pulse
serait suffisant) pour exécuter déjà des tâches (le cas échéant) pour s'arrêter. faire quelque chose la partie ne se produit que lorsque Monitor.Wait
ne reçoit pas de signal dans timeouts.
Pourquoi cette solution? Je n'aime pas le comportement de frai ou avoir une gestion inutile des événements dans la vue. Tout le code est au même endroit, aucun événement supplémentaire n'est nécessaire, ViewModel
a le choix de réagir à chaque changement de valeur ou à la fin de l'opération de l'utilisateur (ce qui ajoute des tonnes de flexibilité, en particulier lors de l'utilisation de la liaison).
Voici un comportement qui gère ce problème plus la même chose avec le clavier. https://gist.github.com/4326429
Il expose une commande et des propriétés de valeur. La valeur est passée comme paramètre de la commande. Vous pouvez databind à la propriété value (et l'utiliser dans le viewmodel). Vous pouvez ajouter un gestionnaire d'événements pour une approche de code-behind.
<Slider>
<i:Interaction.Behaviors>
<b:SliderValueChangedBehavior Command="{Binding ValueChangedCommand}"
Value="{Binding MyValue}" />
</i:Interaction.Behaviors>
</Slider>
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>
PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));
Ma solution est essentiellement la solution de Santo avec quelques drapeaux de plus. Pour moi, le curseur est mis à jour à partir de la lecture du flux ou de la manipulation de l'utilisateur (soit à partir d'un glisser-souris ou en utilisant les touches fléchées, etc.)
Tout d'abord, j'avais écrit le code pour mettre à jour la valeur du curseur à partir de la lecture du flux:
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
}
}
J'ai ensuite ajouté mon code capturé lorsque l'utilisateur manipulait le curseur avec un glisser-souris:
<Slider Name="slider"
Maximum="100" TickFrequency="10"
ValueChanged="slider_ValueChanged"
Thumb.DragStarted="slider_DragStarted"
Thumb.DragCompleted="slider_DragCompleted">
</Slider>
Et ajouté le code derrière:
/// <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;
}
Lorsque l'utilisateur met à jour la valeur du curseur avec un glisser-souris, la valeur change toujours en raison de la lecture du flux et de l'appel de UpdateSliderPosition()
. Pour éviter les conflits, UpdateSliderPosition()
a dû être changé:
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
}
}
}
Bien que cela prévienne les conflits, nous ne pouvons toujours pas déterminer si la valeur est mise à jour par l'utilisateur ou par un appel à UpdateSliderPosition()
. Ceci est corrigé par un autre drapeau, cette fois défini à partir de 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;
}
}
}
Enfin, nous sommes capables de détecter si le curseur est mis à jour par l'utilisateur ou par l'appel à 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()
}
}
J'espère que ça aide quelqu'un!
Si vous souhaitez obtenir les informations de manipulation terminée même si l'utilisateur n'utilise pas le pouce pour changer la valeur (c'est-à-dire en cliquant quelque part dans la barre de piste), vous pouvez attacher un gestionnaire d'événements à votre curseur pour le pointeur pressé et capturer les événements perdus. Vous pouvez faire la même chose pour les événements de clavier
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);
La" magie " ici est L'AddHandler avec le vrai paramètre à la fin qui nous permet d'obtenir le curseur événements "internes". Les gestionnaires d'événements :
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;
}
Le m_bispressed member sera true lorsque l'utilisateur manipule actuellement le curseur (clic, glisser ou clavier). Il sera réinitialisé à false une fois fait .
private void OnValueChanged(object sender, object e)
{
if(!m_bIsPressed) { // do something }
}
Cette version sous-classée du curseur wokrs comme vous le souhaitez:
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();
}
}
J'ai aimé la réponse de @ sinatr.
Ma Solution basée sur la réponse ci-dessus: Cette solution nettoie beaucoup le code et encapsule le mécanisme.
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();
}
});
}
}
Utilisez-le dans votre classe comme:
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
}
}