Domanda

Al momento sto facendo un lettore MP3 in WPF, e voglio fare un dispositivo di scorrimento che permetterà all'utente di cercare di una particolare posizione in un MP3 facendo scorrere il cursore verso sinistra o verso destra.

Ho provato con l'evento ValueChanged ma che si innesca ogni volta che il suo valore è cambiato, quindi se lo si trascina attraverso, l'evento verrà generato più volte, Voglio l'evento al fuoco solo quando l'utente ha terminato il trascinamento il dispositivo di scorrimento e quindi ottenere il nuovo valore .

Come posso raggiungere questo obiettivo?


[Aggiornamento]

Ho trovato questo post su MSDN che discute fondamentalmente la stessa cosa, e si avvicinò con due "soluzioni"; sia sottoclasse il cursore o invocando un DispatcherTimer nel caso ValueChanged che richiama l'azione dopo un periodo.

Si può venire con qualcosa di meglio poi i due di cui sopra?

È stato utile?

Soluzione

È possibile utilizzare evento 'DragCompleted' del pollice per questo. Purtroppo, questo viene attivato solo quando il trascinamento, quindi avrai bisogno di gestire altri clic e le pressioni dei tasti separatamente. Se si desidera solo di essere trascinato, è possibile disattivare questi mezzi di spostare il cursore impostando LargeChange a 0 e Focusable su false.

Esempio:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />

Altri suggerimenti

Oltre a utilizzare l'evento Thumb.DragCompleted è anche possibile utilizzare sia ValueChanged e Thumb.DragStarted, in questo modo non si perde la funzionalità quando l'utente modifica il valore premendo i tasti freccia o facendo clic sulla barra di scorrimento.

XAML:

<Slider ValueChanged="Slider_ValueChanged"
    Thumb.DragStarted="Slider_DragStarted"
    Thumb.DragCompleted="Slider_DragCompleted"/>

Codice dietro:

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" />

funziona per me.

Il valore che si desidera è il valore dopo un evento mousup, sia sui clic sul lato o dopo un trascinamento del manico.

Dato MouseUp non lo fa tunnel verso il basso (si handeled prima che si può), è necessario utilizzare PreviewMouseUp.

Un'altra soluzione MVVM-friendly (non ero felice con risposte)

Visualizza:

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

E 'in ritardo (dal ms 1000 in data esempio) il funzionamento. Nuovo incarico viene creata per ogni cambiamento fatto da cursore (o da mouse o tastiera). Prima di iniziare il compito segnali (utilizzando Monitor.PulseAll, forse anche Monitor.Pulse sarebbe sufficiente) alla corsa già compiti (se presente) per interrompere. Fa qualcosa parte sarà solo si verifica quando Monitor.Wait non si ottiene segnale all'interno di timeout.

Perché questa soluzione? Non mi piace la deposizione delle uova comportamento o avere inutili movimentazione in vista dell'evento. Tutto il codice è in un posto, eventi supplementari necessari, ViewModel ha scegliere se reagire a ogni cambio di valore o alla fine del funzionamento di utente (che aggiunge tonnellate di flessibilità, soprattutto quando si utilizza vincolante).

Ecco un comportamento che gestisce questo problema, più la stessa cosa con la tastiera. https://gist.github.com/4326429

Si espone una proprietà di comando e di valore. Il valore viene passato come parametro del comando. È possibile DataBind alla proprietà value (e utilizzarlo nel ViewModel). È possibile aggiungere un gestore di eventi per un approccio code-behind.

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

La mia soluzione è fondamentalmente la soluzione di Santo con un paio di bandiere. Per me, il cursore viene aggiornato sia da leggere il ruscello o la manipolazione utente (sia da un trascinamento del mouse o utilizzando i tasti freccia, ecc)

Per prima cosa, avevo scritto il codice per aggiornare il valore di scorrimento dalla lettura del flusso:

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

Ho poi aggiunto il mio codice che ha catturato quando l'utente stava manipolando il cursore con un trascinamento del mouse:

<Slider Name="slider"
        Maximum="100" TickFrequency="10"
        ValueChanged="slider_ValueChanged"
        Thumb.DragStarted="slider_DragStarted"
        Thumb.DragCompleted="slider_DragCompleted">
</Slider>

E ha aggiunto il codice dietro:

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

Quando l'utente aggiorna il valore del dispositivo di scorrimento con un trascinamento del mouse, il valore sarà ancora cambiare a causa del flusso di essere letto e chiamando UpdateSliderPosition(). Per evitare conflitti, UpdateSliderPosition() doveva essere cambiato:

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

Anche se questo sarà prevenire i conflitti, siamo ancora in grado di determinare se il valore viene aggiornato dall'utente o da una chiamata a UpdateSliderPosition(). Questo è fissato da un'altra bandiera, questa volta insieme all'interno 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;
            }
        }
    }

Infine, siamo in grado di rilevare se il cursore è in fase di aggiornamento da parte dell'utente o la chiamata a 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()
        }
    }

La speranza che aiuta qualcuno!

Se si desidera ottenere la manipolazione conclusa informazioni anche se l'utente non sta usando il pollice per modificare il valore (ad esempio facendo clic da qualche parte nella barra di pista), è possibile collegare un gestore di eventi per il vostro dispositivo di scorrimento per il puntatore pressato e la cattura eventi perduti. Si può fare la stessa cosa per gli eventi della tastiera

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 "magia" qui è l'AddHandler con il vero parametro alla fine, che ci permette di ottenere i cursori eventi "interni". I gestori di eventi:

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

Il membro m_bIsPressed sarà vero quando l'utente sta manipolando il cursore (click, trascinamento o la tastiera). Sarà riportato a falsa volta fatto.

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

Mi è piaciuta la risposta via @sinatr.

La mia soluzione basata sulla risposta di cui sopra: Questa soluzione pulisce il codice molto e incapsula il meccanismo.

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

utilizzarlo nella vostra classe come:

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

Questa versione sottoclasse del cursore funziona come si desidera:

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();
    }
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top