ドラッグでスクロールできるScrollViewer

2026年7月13日月曜日

.NET WPF C#

画像ビューアを作っていて、ユーザーが画像をドラッグ操作でスクロールできる機能が欲しかったのでビヘイビアを作りました。

環境

.NET 10、C#、WPF

必要なもの

NuGetパッケージ

  • Microsoft.Xaml.Behaviors.Wpf

コード

using Microsoft.Xaml.Behaviors;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace Common.Behaviors
{
    /// <summary>
    /// DragScrollBehavior は ScrollViewer の拡張です。
    /// DragScrollBehavior を関連付けた ScrollViewer はユーザーがドラッグ操作でコンテンツをスクロールできます。
    /// </summary>
    public class DragScrollBehavior : Behavior<ScrollViewer>
    {
        // スクロールの方向を逆にするかどうかを指定します。
        private const bool ScrollDirectionReversed = true;

        // コンテンツではなくビューポートのスケールを使用するかどうかを指定します。
        private const bool UseViewportScale = true;

        private Point? _startPosition;
        private double _startHorizontalOffset;
        private double _startVerticalOffset;
        private double _scrollDirectionFactor = ScrollDirectionReversed ? -1 : 1;

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.PreviewMouseLeftButtonDown += ScrollViewer_PreviewMouseLeftButtonDown;
            AssociatedObject.PreviewMouseLeftButtonUp += ScrollViewer_PreviewMouseLeftButtonUp;
            AssociatedObject.PreviewMouseMove += ScrollViewer_PreviewMouseMove;
            AssociatedObject.LostMouseCapture += ScrollViewer_LostMouseCapture;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.PreviewMouseLeftButtonDown -= ScrollViewer_PreviewMouseLeftButtonDown;
            AssociatedObject.PreviewMouseLeftButtonUp -= ScrollViewer_PreviewMouseLeftButtonUp;
            AssociatedObject.PreviewMouseMove -= ScrollViewer_PreviewMouseMove;
            AssociatedObject.LostMouseCapture -= ScrollViewer_LostMouseCapture;
        }

        private void ScrollViewer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // スクロールバーの使用を邪魔しないようにスクロールバー上では開始しません。
            if ((e.OriginalSource is not DependencyObject obj)
                || (FindScrollViewerOrScrollBar(obj) != AssociatedObject)
                || ((AssociatedObject.ScrollableWidth == 0) && (AssociatedObject.ScrollableHeight == 0)))
            {
                return;
            }

            _startPosition = e.GetPosition(AssociatedObject);
            _startHorizontalOffset = AssociatedObject.HorizontalOffset;
            _startVerticalOffset = AssociatedObject.VerticalOffset;
        }

        private void ScrollViewer_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (!_startPosition.HasValue)
            {
                return;
            }

            _startPosition = null;

            if (!AssociatedObject.IsMouseCaptured)
            {
                return;
            }

            AssociatedObject.Cursor = Cursors.Arrow;
            AssociatedObject.ReleaseMouseCapture();
        }

        private void ScrollViewer_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            // ドラッグ開始後の場合
            if (AssociatedObject.IsMouseCaptured)
            {
                if (_startPosition is not Point startPositionValue)
                {
                    return;
                }

                // 開始点からの差を求め ScrollViewer のオフセットを設定します。
                Point currentPosition = e.GetPosition(AssociatedObject);
                Vector difference = currentPosition - startPositionValue;
                double horizontalScaleFactor = UseViewportScale ? AssociatedObject.ExtentWidth / AssociatedObject.ViewportWidth : 1;
                double verticalScaleFactor = UseViewportScale ? AssociatedObject.ExtentHeight / AssociatedObject.ViewportHeight : 1;
                AssociatedObject.ScrollToHorizontalOffset(_startHorizontalOffset + difference.X * horizontalScaleFactor * _scrollDirectionFactor);
                AssociatedObject.ScrollToVerticalOffset(_startVerticalOffset + difference.Y * verticalScaleFactor * _scrollDirectionFactor);
            }

            // ドラッグ開始前の場合
            else
            {
                if ((e.LeftButton != MouseButtonState.Pressed)
                    || (_startPosition is not Point startPositionValue))
                {
                    return;
                }

                // 開始点からの差を求め SystemParameters で定義される距離を上回ったらドラッグを開始します。
                Point currentPosition = e.GetPosition(AssociatedObject);
                Vector difference = currentPosition - startPositionValue;

                if ((Math.Abs(difference.X) < SystemParameters.MinimumHorizontalDragDistance)
                    && (Math.Abs(difference.Y) < SystemParameters.MinimumVerticalDragDistance))
                {
                    return;
                }

                AssociatedObject.CaptureMouse();
                AssociatedObject.Cursor = Cursors.ScrollAll;
            }
        }

        private void ScrollViewer_LostMouseCapture(object sender, MouseEventArgs e)
        {
            _startPosition = null;
            AssociatedObject.Cursor = Cursors.Arrow;
        }

        /// <summary>
        /// 指定したオブジェクトからビジュアルツリーの祖先をたどり ScrollViewer か ScrollBar を探します。
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        private DependencyObject? FindScrollViewerOrScrollBar(DependencyObject obj)
        {
            while (true)
            {
                switch (obj)
                {
                    case Window:
                        return null;

                    case ScrollViewer or ScrollBar:
                        return obj;

                    case DependencyObject:
                        obj = VisualTreeHelper.GetParent(obj);
                        break;

                    default:
                        return null;
                }
            }
        }
    }
}
rommxbbu6v6

.NET、C#、WPFの話題が多いです。

人気の投稿 / 新着記事

人気記事 新着記事
  • 読み込み中...

人気の投稿 ※消さないでください

ラベル

.NET (1) C# (1) WPF (1)

ブログ アーカイブ

目次

このブログを検索