web-dev-qa-db-fra.com

WPF: curseur avec un événement qui se déclenche après le glissement d'un utilisateur

Je fabrique actuellement un lecteur MP3 dans WPF et je souhaite créer un curseur qui permettra à l'utilisateur de rechercher une position particulière dans un MP3 en faisant glisser le curseur vers la gauche ou la droite.

J'ai essayé d'utiliser l'événement ValueChanged mais cela se déclenche chaque fois que sa valeur est modifiée. Par conséquent, si vous le faites glisser, l'événement se déclenchera plusieurs fois, Je veux que l'événement ne se déclenche que lorsque l'utilisateur a terminé de tirer le curseur et Ensuite, obtenez la nouvelle valeur.

Comment puis-je atteindre cet objectif?


[Mettre à jour]

J'ai trouvé ce message sur MSDN qui traite essentiellement de la même chose, et ils ont proposé deux "solutions"; soit en sous-classant le curseur, soit en invoquant une variable DispatcherTimer dans l'événement ValueChanged qui appelle l'action après un intervalle de temps.

Pouvez-vous trouver quelque chose de mieux que les deux mentionnés ci-dessus?

48
Andreas Grech

Vous pouvez utiliser l'événement 'DragCompleted' du pouce pour cela. Malheureusement, cela ne se déclenche que lorsque vous faites glisser la souris, vous devez donc gérer les autres clics et pressions sur les touches séparément. Si vous souhaitez uniquement pouvoir le faire glisser, vous pouvez désactiver ces moyens de déplacement du curseur en définissant LargeChange sur 0 et Focusable sur false.

Exemple:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
49
YotaXP

En plus d'utiliser l'événement Thumb.DragCompleted, vous pouvez également utiliser ValueChanged et Thumb.DragStarted pour ne pas perdre de fonctionnalités lorsque l'utilisateur modifie la valeur en appuyant sur les touches de direction ou en cliquant sur le 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);
}
70
Alan
<Slider PreviewMouseUp="MySlider_DragCompleted" />

travaille pour moi. 

La valeur que vous voulez est la valeur après un événement mousup, soit sur un clic sur le côté, soit après un glissement de la poignée. 

Puisque MouseUp ne creuse pas le tunnel (il est manipulé avant de pouvoir le faire), vous devez utiliser PreviewMouseUp. 

12
Peter

Une autre solution compatible avec MVVM (je n'étais pas content 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
                    }
            });
        }
    }
}

Son opération est retardée (de 1000 ms dans l'exemple donné). Une nouvelle tâche est créée pour chaque modification effectuée par le curseur (par la souris ou le clavier). Avant de commencer la tâche, il signale (en utilisant Monitor.PulseAll, peut-être même que Monitor.Pulse suffirait-il) à exécuter déjà des tâches (le cas échéant) pour arrêter. Faire quelque chose une partie ne se produit que lorsque Monitor.Wait ne reçoit pas le signal dans le délai imparti.

Pourquoi cette solution? Je n'aime pas le comportement de frai ni la gestion inutile d'é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 une grande flexibilité, en particulier lors de l'utilisation de la liaison).

5
Sinatr

Voici un comportement qui gère ce problème plus la même chose avec le clavier. https://Gist.github.com/4326429

Il expose les propriétés Command et Value. La valeur est transmise en tant que paramètre de la commande. Vous pouvez associer des données à la propriété value (et l'utiliser dans le modèle de vue). Vous pouvez ajouter un gestionnaire d'événements pour une approche code-behind.

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

Cette version du slider sous-classé fonctionne 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();
    }
}
0
Dominik Palo

Ma solution est fondamentalement la solution de Santo avec quelques drapeaux supplémentaires. Pour moi, le curseur est mis à jour à partir de la lecture du flux ou de la manipulation de l'utilisateur (soit en faisant glisser la souris, soit en utilisant les touches de direction, 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é le code capturé lorsque l'utilisateur manipulait le curseur en faisant glisser la 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 en faisant glisser la souris, la valeur change en raison du flux en cours de lecture 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 évite 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 indicateur, cette fois défini dans 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 en mesure 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!

0
beefsupreme
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>

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

J'ai aimé Answer by @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
    }
}
0
soan saini

Si vous souhaitez obtenir les informations sur la manipulation même si l'utilisateur n'utilise pas le pouce pour modifier la valeur (c.-à-d. En cliquant quelque part dans la barre de suivi), vous pouvez attacher un gestionnaire d'événements au curseur pour le pointeur enfoncé 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" est ici le AddHandler avec le paramètre vrai à la fin qui nous permet d'obtenir le curseur "interne" des événements . 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 membre m_bIsPressed sera true lorsque l'utilisateur manipule actuellement le curseur (cliquez, faites glisser ou utilisez le clavier). Il sera réinitialisé à faux une fois terminé.

private void OnValueChanged(object sender, object e)
{
    if(!m_bIsPressed) { // do something }
}
0
Vincent