画像を表示するImageコントロールをベースにスクロール、Ctrl+マウスホイールによるズーム、マウスドラックによるパン機能を付与した、ユーザーコントロールのサンプルコードになります。
ソースコード
・ユーザーコントロール本体
ファイル名:ZoomImageControl.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace ZoomImageUC;
public class ZoomImageControl : UserControl
{
private readonly ScrollViewer _scroll;
private readonly Border _border;
private readonly Image _image;
private readonly ScaleTransform _scale;
// ----- 依存プロパティ -----
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register(
nameof(Source),
typeof(ImageSource),
typeof(ZoomImageControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
public ImageSource? Source
{
get => (ImageSource?)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
// ----- ズーム設定 -----
public double WheelZoomStep { get; set; } = 1.1; // 1ノッチで±10%
public double MinZoom { get; set; } = 0.05;
public double MaxZoom { get; set; } = 20.0;
/// <summary>この修飾キーが押されているときだけホイールでズーム(既定: Ctrl)。None にすると常時ズーム。</summary>
public ModifierKeys ZoomModifierKey { get; set; } = ModifierKeys.Control;
public double Zoom
{
get => _scale.ScaleX;
set
{
var clamped = Math.Max(MinZoom, Math.Min(MaxZoom, value));
_scale.ScaleX = clamped;
_scale.ScaleY = clamped;
}
}
// ----- ドラッグ(パン)状態 -----
private bool _isDragging;
private Point _dragStartPos;
private Point _dragStartOffset;
public ZoomImageControl()
{
_scroll = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
CanContentScroll = false,
PanningMode = PanningMode.None,
Focusable = true
};
_border = new Border
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
_border.SetBinding(Border.BackgroundProperty,
new System.Windows.Data.Binding(nameof(Background)) { Source = this });
_image = new Image
{
Stretch = Stretch.None,
SnapsToDevicePixels = true
};
_image.SetBinding(Image.SourceProperty,
new System.Windows.Data.Binding(nameof(Source)) { Source = this });
_scale = new ScaleTransform(1.0, 1.0);
_image.LayoutTransform = _scale;
_border.Child = _image;
_scroll.Content = _border;
Content = _scroll;
if (Background == null) Background = Brushes.Transparent;
// 入力イベント
PreviewMouseWheel += OnPreviewMouseWheel; // 修飾キーありのときだけズーム
_image.MouseLeftButtonDown += OnMouseLeftButtonDown; // ドラッグ or ダブルクリック
_image.MouseLeftButtonUp += OnMouseLeftButtonUp;
_image.MouseMove += OnMouseMove;
_image.MouseLeave += (_, __) => { if (_isDragging) EndDrag(); };
}
// ===== ズーム =====
private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
// 修飾キー判定(None の場合は常時ズーム)
if (!IsZoomModifierSatisfied()) return;
if (_image.Source == null) return;
double factor = e.Delta > 0 ? WheelZoomStep : (1.0 / WheelZoomStep);
ZoomAroundMouse(factor, e);
e.Handled = true; // ScrollViewer の通常スクロールを抑止
}
private bool IsZoomModifierSatisfied()
{
if (ZoomModifierKey == ModifierKeys.None) return true;
return (Keyboard.Modifiers & ZoomModifierKey) == ZoomModifierKey;
}
private void ZoomAroundMouse(double factor, MouseWheelEventArgs e)
{
// ScrollViewer 内のマウス座標(表示座標)
Point mouseInScroll = e.GetPosition(_scroll);
// 画像要素内の座標(拡大後座標)
Point mouseOnImage = e.GetPosition(_image);
double oldScale = Zoom;
double newScale = Math.Max(MinZoom, Math.Min(MaxZoom, oldScale * factor));
if (Math.Abs(newScale - oldScale) < 1e-6) return;
// 論理座標(拡大前の座標系)
double logicalX = mouseOnImage.X / oldScale;
double logicalY = mouseOnImage.Y / oldScale;
Zoom = newScale;
_scroll.UpdateLayout();
double targetOffsetX = logicalX * newScale - mouseInScroll.X;
double targetOffsetY = logicalY * newScale - mouseInScroll.Y;
var maxX = Math.Max(0.0, _scroll.ExtentWidth - _scroll.ViewportWidth);
var maxY = Math.Max(0.0, _scroll.ExtentHeight - _scroll.ViewportHeight);
_scroll.ScrollToHorizontalOffset(Math.Max(0.0, Math.Min(maxX, targetOffsetX)));
_scroll.ScrollToVerticalOffset (Math.Max(0.0, Math.Min(maxY, targetOffsetY)));
}
// ===== ドラッグ(パン) =====
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (_image.Source == null) return;
// ダブルクリック相当
if (e.ClickCount == 2)
{
ResetZoomAndCenter();
e.Handled = true;
return;
}
_isDragging = true;
_dragStartPos = e.GetPosition(_scroll);
_dragStartOffset = new Point(_scroll.HorizontalOffset, _scroll.VerticalOffset);
_image.CaptureMouse();
_image.Cursor = Cursors.SizeAll;
e.Handled = true;
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (!_isDragging) return;
Point current = e.GetPosition(_scroll);
Vector delta = current - _dragStartPos;
double targetX = _dragStartOffset.X - delta.X;
double targetY = _dragStartOffset.Y - delta.Y;
var maxX = Math.Max(0.0, _scroll.ExtentWidth - _scroll.ViewportWidth);
var maxY = Math.Max(0.0, _scroll.ExtentHeight - _scroll.ViewportHeight);
_scroll.ScrollToHorizontalOffset(Math.Max(0.0, Math.Min(maxX, targetX)));
_scroll.ScrollToVerticalOffset (Math.Max(0.0, Math.Min(maxY, targetY)));
}
private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isDragging) { EndDrag(); e.Handled = true; }
}
private void EndDrag()
{
_isDragging = false;
_image.ReleaseMouseCapture();
_image.Cursor = Cursors.Arrow;
}
// ===== 便利操作 =====
public void ResetZoom() => Zoom = 1.0;
public void ResetZoomAndCenter()
{
Zoom = 1.0;
_scroll.UpdateLayout();
// 画像(拡大後)の実表示サイズ
double w = _image.ActualWidth;
double h = _image.ActualHeight;
// ビューポートとの差から中央寄せ(はみ出すときはスクロール中央へ)
double targetX = Math.Max(0.0, (w - _scroll.ViewportWidth) / 2.0);
double targetY = Math.Max(0.0, (h - _scroll.ViewportHeight) / 2.0);
_scroll.ScrollToHorizontalOffset(targetX);
_scroll.ScrollToVerticalOffset (targetY);
}
public void ZoomIn (double factor = 1.1) => Zoom = Zoom * factor;
public void ZoomOut(double factor = 1.1) => Zoom = Zoom / factor;
// 内部要素にアクセスしたい場合
public Image InnerImage => _image;
public ScrollViewer InnerScrollViewer => _scroll;
}
・使う側のコード
ファイル名:MainWindow.xaml
<Window x:Class="ZoomImageUC.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ZoomImageUC"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<local:ZoomImageControl
Background="Black"
Source="C:\Users\karet\Pictures\IMG_20251009_180913471_MFNR.jpg"/>
</Grid>
</Window>
感想
ユーザーコントロールZoomImageControlは一つのクラスにまとまっています。このクラスを別のアプリ(プロジェクト)に追加するだけで、ズーム機能を持ったImageコントロールを使うことが出来ます。
動作確認した感じ、ホイールによる挙動が怪しい部分がありますが、とりあえずは動作を確認。
コメント