web-dev-qa-db-ja.com

WPF:ユーザーがドラッグした後にトリガーされるイベントを含むスライダー

現在、WPFでMP3プレーヤーを作成していますが、スライダーを左右にスライドさせることで、ユーザーがMP3の特定の位置をシークできるスライダーを作成したいと考えています。

ValueChangedイベントを使用してみましたが、値が変更されるたびにトリガーされるため、ドラッグすると、イベントが複数回発生しますユーザーがスライダーのドラッグを終了してから新しい値を取得したときにのみ起動します

どうすればこれを達成できますか?


[更新]

基本的に同じことを説明しているMSDNで この投稿 を見つけましたが、2つの「解決策」を思い付きました。 Sliderをサブクラス化するか、タイムスパン後にアクションを呼び出すDispatcherTimerイベントでValueChangedを呼び出します。

上記の2つよりも良いものを思い付くことができますか?

50
Andreas Grech

これには、サムの「DragCompleted」イベントを使用できます。残念ながら、これはドラッグ時にのみ発生するため、他のクリックとキーの押下を個別に処理する必要があります。ドラッグのみが必要な場合は、LargeChangeを0に、Focusableをfalseに設定して、スライダーを移動するこれらの手段を無効にすることができます。

例:

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

Thumb.DragCompletedイベントの使用に加えて、ValueChangedThumb.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);
}
69
Alan
<Slider PreviewMouseUp="MySlider_DragCompleted" />

私のために働く。

必要な値は、マウスの横のクリック時またはハンドルのドラッグ後のマウスアップイベント後の値です。

MouseUpはトンネリングしません(できる前に処理されます)ので、PreviewMouseUpを使用する必要があります。

12
Peter

別の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与えられた例では)操作。スライダー(マウスまたはキーボード)によって行われるすべての変更に対して、新しいタスクが作成されます。タスクを開始する前にsignalsMonitor.PulseAll、おそらくMonitor.Pulseで十分です)既にタスク(存在する場合)を実行して停止します。 何かをする partは、Monitor.Waitタイムアウト内に信号を受信しません。

なぜこの解決策なのか?ビューでのスポーン動作や不要なイベント処理が好きではありません。すべてのコードは1か所にあり、余分なイベントは必要ありません。ViewModelは、各値の変更またはユーザー操作の終了時に反応する選択肢があります(特にバインディングを使用する場合は、柔軟性が大幅に向上します)。

4
Sinatr

この問題に加えてキーボードで同じことを処理する動作を次に示します。 https://Gist.github.com/4326429

CommandおよびValueプロパティを公開します。値はコマンドのパラメーターとして渡されます。 valueプロパティにデータバインドできます(さらに、ビューモデルで使用できます)。コードビハインドアプローチのイベントハンドラーを追加できます。

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

必要に応じて、このスライダークラスのサブクラスバージョン:

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

私のソリューションは、基本的にいくつかのフラグを追加したサントのソリューションです。私にとって、スライダーはストリームの読み取りまたはユーザー操作(マウスのドラッグまたは矢印キーなどの使用)から更新されています

最初に、ストリームの読み取りからスライダーの値を更新するコードを作成しました。

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

それが誰かを助けることを願っています!

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

私は@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
    }
}
0
soan saini

ユーザーが値を変更するために親指を使用していない場合でも(つまり、トラックバーのどこかをクリックして)操作終了情報を取得する場合は、押されたポインターのスライダーにイベントハンドラーをアタッチして、失われたイベントをキャプチャできます。キーボードイベントでも同じことができます

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

ここでの「魔法」とは、スライダーの「内部」イベントを取得できるようにする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メンバーはtrueになります。完了するとfalseにリセットされます。

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