目次
- はじめに
- 実装例 (WindowsForms)
- 各メソッドの詳しい解説
- Offsetの解説
- さいごに
はじめに
WindowsFormsを使った画像の拡大縮小やドラッグによる操作(パン操作)を紹介します!
今回は、マウスカーソルが拡大縮小の中心点になるようにしています。
実装例 (WindowsForms)
以下のコードを用いて、拡大縮小とパン操作を実装することができます。
ImageViewerPictureBoxに画像を表示し、マウスホイールを使ってズームしたり、Altキーと左クリックを使って画像をドラッグすることができます。
public bool isDraggingImage = false; // 画像がドラッグ中かどうか
private bool isSpecialButtonActive = false;
private float zoomLevel = 1.0f; // 初期倍率
private const float zoomIncrement = 0.1f; // 拡大縮小の速度
private const float maxZoomLevel = 20.0f; // 最大倍率
private const float minZoomLevel = 1.0f; // 最小倍率
private PointF imagePositionOffset = new PointF(0, 0); // 画像のオフセット(座標)
public Bitmap baseImage; // 元の画像を保持するフィールド
private System.Drawing.Image scaledImage; // リサイズされた画像
private SizeF baseImageSize; // 画像の元のサイズ
private Point lastMousePoint; // 前回のマウス位置
public ImageViewerForm()
{
InitializeComponent();
// マウスホイールイベントを追加
ImageViewerPictureBox.MouseWheel += ImageViewerPictureBox_MouseWheel;
ImageViewerPictureBox.Paint += ImageViewerPictureBox_Paint; // カスタム描画イベント追加
ImageViewerPictureBox.SizeMode = PictureBoxSizeMode.StretchImage; // ピクチャーボックスのサイズを維持
// ピクチャーボックスのサイズ変更イベントを追加
ImageViewerPictureBox.SizeChanged += ImageViewerPictureBox_SizeChanged;
ImageViewerPictureBox.MouseDown += ImageViewerPictureBox_MouseDown;
ImageViewerPictureBox.MouseUp += ImageViewerPictureBox_MouseUp;
ImageViewerPictureBox.MouseMove += ImageViewerPictureBox_MouseMove;
// 画像をロードして PictureBox に設定
ImageViewerPictureBox.Image = System.Drawing.Image.FromFile("path/to/your/image.bmp");
baseImage = (Bitmap)ImageViewerPictureBox.Image;
// 初期画像のサイズを保存
baseImageSize = new SizeF(baseImage.Width, baseImage.Height);
// 画像を PictureBox に合わせて表示
ImageViewerPictureBox.SizeMode = PictureBoxSizeMode.StretchImage;
// 初期画像のオフセットを設定
imagePositionOffset = new Point(ImageViewerPictureBox.Width / 2, ImageViewerPictureBox.Height / 2);
ImageViewerPictureBox.Invalidate(); // 再描画を促す
}
private void ImageViewerPictureBox_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && ModifierKeys == Keys.Alt)
{
isDraggingImage = true;
lastMousePoint = e.Location; // 現在のマウス位置を保存
}
}
private void ImageViewerPictureBox_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
isDraggingImage = false; // ドラッグを終了
}
}
private void ImageViewerPictureBox_MouseMove(object sender, MouseEventArgs e)
{
if (zoomLevel != 1.0f)
{
if (isDraggingImage)
{
// マウスの移動量を計算
int deltaX = e.Location.X - lastMousePoint.X;
int deltaY = e.Location.Y - lastMousePoint.Y;
// オフセットを更新
imagePositionOffset.X += deltaX;
imagePositionOffset.Y += deltaY;
// リサイズされた画像のサイズを計算
float scaledWidth = ImageViewerPictureBox.Width * zoomLevel;
float scaledHeight = ImageViewerPictureBox.Height * zoomLevel;
// 画像の左上と右下の座標を計算
float left = imagePositionOffset.X - (scaledWidth / 2);
float top = imagePositionOffset.Y - (scaledHeight / 2);
float right = left + scaledWidth;
float bottom = top + scaledHeight;
// ピクチャーボックスの描画領域の大きさ
int pictureBoxWidth = ImageViewerPictureBox.ClientSize.Width;
int pictureBoxHeight = ImageViewerPictureBox.ClientSize.Height;
// ピクチャーボックスの範囲内に画像を制限するための処理
if (left > 0)
{
imagePositionOffset.X = scaledWidth / 2;
}
if (right < pictureBoxWidth)
{
imagePositionOffset.X = pictureBoxWidth - (scaledWidth / 2);
}
if (top > 0)
{
imagePositionOffset.Y = scaledHeight / 2;
}
if (bottom < pictureBoxHeight)
{
imagePositionOffset.Y = pictureBoxHeight - (scaledHeight / 2);
}
lastMousePoint = e.Location;
ImageViewerPictureBox.Invalidate(); // 再描画を促す
}
}
}
private void ImageViewerPictureBox_SizeChanged(object sender, EventArgs e)
{
imagePositionOffset = new Point(ImageViewerPictureBox.Width / 2, ImageViewerPictureBox.Height / 2);
ImageViewerPictureBox.Invalidate(); // 再描画を促す
}
private void ImageViewerPictureBox_MouseWheel(object sender, MouseEventArgs e)
{
var previousZoom = zoomLevel;
if (e.Delta > 0 && zoomLevel < maxZoomLevel)
{
zoomLevel *= (1 + zoomIncrement);
}
else if (e.Delta < 0 && zoomLevel > minZoomLevel)
{
zoomLevel *= (1 - zoomIncrement);
}
if (zoomLevel != previousZoom)
{
AdjustImageOffset(e.Location, previousZoom);
if (zoomLevel <= minZoomLevel)
{
zoomLevel = minZoomLevel;
imagePositionOffset = new PointF(ImageViewerPictureBox.Width / 2, ImageViewerPictureBox.Height / 2);
}
ImageViewerPictureBox.Invalidate();
}
}
private void ImageViewerPictureBox_Paint(object sender, PaintEventArgs e)
{
if (baseImage == null) return;
Graphics graphics = e.Graphics;
int pictureBoxWidth = ImageViewerPictureBox.Width;
int pictureBoxHeight = ImageViewerPictureBox.Height;
int scaledWidth = (int)(pictureBoxWidth * zoomLevel);
int scaledHeight = (int)(pictureBoxHeight * zoomLevel);
using (var resizedImage = new Bitmap(baseImage, new System.Drawing.Size(scaledWidth, scaledHeight)))
{
Point drawLocation = new Point(
(int)(imagePositionOffset.X - (scaledWidth / 2)),
(int)(imagePositionOffset.Y - (scaledHeight / 2))
);
graphics.DrawImage(resizedImage, new Rectangle(drawLocation, new System.Drawing.Size(scaledWidth, scaledHeight)));
}
}
private void AdjustImageOffset(Point mousePosition, float previousZoom)
{
float scaleRatio = zoomLevel / previousZoom;
int offsetX = (int)((mousePosition.X - imagePositionOffset.X) * (scaleRatio - 1));
int offsetY = (int)((mousePosition.Y - imagePositionOffset.Y) * (scaleRatio - 1));
imagePositionOffset.X -= offsetX;
imagePositionOffset.Y -= offsetY;
}
各メソッドの詳しい解説
各メソッドの動作について詳しく解説します!
- ImageViewerForm()
コンストラクタでは、マウスホイールやマウスイベントを追加し、画像の初期位置やサイズを設定します。 - ImageViewerPictureBox.MouseWheel()
マウスホイールの回転を監視し、zoomLevel(拡大縮小率)を更新します。ズーム倍率の制限もここで行い、過度な拡大や縮小を防いでいます。また、AdjustImageOffset()メソッドを呼び出すことで、ズーム時に画像が画面中心ではなくマウスカーソル位置を基準に拡大縮小されるように調整します。 - ImageViewerPictureBox.MouseDown(), ImageViewerPictureBox.MouseMove()
マウスドラッグによるパン操作を実現するためのメソッドです。Altキーを押しながら画像をドラッグすると、画像が現在のマウス位置に追従するように移動します。画像の範囲がピクチャーボックス外にはみ出さないように制限をかけている点もポイントです。 - ImageViewerPictureBox_SizeChanged()
ピクチャーボックスのサイズが変更された際に画像のオフセットを更新し、適切に画像が再配置されるようにします。 - ImageViewerPictureBox_Paint()
画像を拡大縮小してピクチャーボックスに描画するメソッドです。Graphicsオブジェクトを利用して画像をリサイズし、オフセットを考慮した位置に描画します。 - AdjustImageOffset()
ズーム時のマウス位置を基準に画像のオフセットを調整するメソッドです。ズーム前と後で倍率差が生じた際、画像のオフセットを計算し、拡大縮小による位置ズレを防ぎます。
Offsetの解説
今回のコードでは「Offset」の概念が非常に重要です!
オフセットとはそもそも、基準との差やズレを相殺する・埋め合わせるといったような意味です。拡大縮小やドラッグ操作において、画像がピクチャーボックス内の正しい位置に描画されるよう調整していると覚えておけば大丈夫です
少し詳しく言うと..
通常、画像を表示する際は、画像の描画位置が (0, 0) などの固定された座標で始まります。しかし、画像をドラッグしたり拡大縮小したりすると、画像の描画位置を任意の座標に移動させる必要があります。この際、 Offset を使用して動的に調整しているんです!
- Offsetの役割
Offset は PointF 型の変数で、画像の描画位置の基準点を保持します。以下のように、拡大縮小やドラッグ操作によって画像の表示位置が変更される場合、この Offset が調整されることで、画像を適切な位置に表示することができます。 - Offsetの計算
ドラッグ操作中は、以下のようにマウスの移動量を使用して Offset が更新されます。
// マウスの移動量を計算
int deltaX = e.Location.X - lastMousePoint.X;
int deltaY = e.Location.Y - lastMousePoint.Y;
// オフセットを更新
imagePositionOffset.X += deltaX;
imagePositionOffset.Y += deltaY;
この処理では、deltaX と deltaY というマウスの前回位置と現在位置の差分を計算し、それを imageOffset に加えることで、画像がマウスの移動に追随するように描画位置を更新しています。
- 拡大縮小時のOffset調整
拡大縮小時にも Offset の調整が必要です。特に、拡大縮小によって画像のサイズが変わると、画像の描画基準位置がずれるため、マウスの位置を基準に Offset を再調整する必要があります。
private void AdjustImageOffset(Point mousePosition, float previousZoom)
{
float scaleRatio = zoomLevel / previousZoom;
int offsetX = (int)((mousePosition.X - imagePositionOffset.X) * (scaleRatio - 1));
int offsetY = (int)((mousePosition.Y - imagePositionOffset.Y) * (scaleRatio - 1));
imagePositionOffset.X -= offsetX;
imagePositionOffset.Y -= offsetY;
}
AdjustImageOffset メソッドは、マウスの位置を基準に Offset を再計算することで、拡大縮小後も画像の中心位置がユーザーの意図する場所で保たれるようにします。
- Offsetを用いた画像の描画
画像の描画位置は Offset を使用して決定されます。描画時には、Offset を適用して画像の開始位置を調整し、ピクチャーボックス内の適切な位置で表示されるようにします。
using (var resizedImage = new Bitmap(baseImage, new System.Drawing.Size(scaledWidth, scaledHeight)))
{
Point drawLocation = new Point(
(int)(imagePositionOffset.X - (scaledWidth / 2)),
(int)(imagePositionOffset.Y - (scaledHeight / 2))
);
graphics.DrawImage(resizedImage, new Rectangle(drawLocation, new System.Drawing.Size(scaledWidth, scaledHeight)));
}
drawLocation に Offset を適用することで、画像の中心をオフセットし、表示位置を決定しています。
さいごに
しっかりとした解説を書くのは初めてなので、至らないことだらけだと思います。間違いや良く分からないところがあれば、気軽にコメントしていただきたいです!
よろしくお願いします