WinFormsで「パレット連動・ドラッグ描画」入門
— 円/長方形/直線をリアルタイムに描く
この技術ブログでは、提示いただいたコードをベースに、パレット(色・線幅・図形種)を別フォームで操作しつつ、メインフォームでドラッグ中に図形をプレビュー描画する最小構成を解説します。
そのうえで、実用に向けた**改善点(矩形の正規化・資源解放・ちらつき対策・Shiftで正円/正方形)**も反映した完成版コードを提示します。
目次
完成仕様(今回のゴール)
- パレット(Pallet フォーム)で図形種(円/長方形/直線)、色、線幅を選択
- メインフォーム(Form1)でマウス左ドラッグすると、ドラッグ範囲に図形をリアルタイム描画
- アンチエイリアスで滑らかに表示
- ちらつきを抑えるためダブルバッファ有効化
- 逆方向ドラッグ(左上へ向けてなど)でも正しく描画(矩形の正規化)
- Shiftキーで正円/正方形に拘束(直線は除外)
注意:本記事のサンプルは「今描いている1つの図形」を都度再描画する最小構成です。履歴を残して複数個を積み上げたい場合は、末尾の拡張アイデアをご覧ください。
1. 元コードのポイント整理
- Paintイベント(ここでは DrawFigures)で常に現在のドラッグ範囲だけを描く。
- MouseDownで始点を記録、MouseMove(左ボタン押下中)で終点を更新して Invalidate() → 再描画。
- パレット Pallet から図形種/色/線幅を取得。
シンプルでわかりやすい構造ですが、いくつか改善余地があります。
2. よくある改善ポイント
- 矩形の正規化FillEllipse / FillRectangle は幅や高さが負数だと期待通りになりません。→ startPos と endPos から 左/上/幅/高さを正規化して Rectangle を作る関数を用意しましょう。
- GDI+資源解放Pen や Brush は IDisposable。using を使って確実に解放しましょう。
- ちらつき対策this.DoubleBuffered = true; を有効化(フォーム内なら protected プロパティにアクセス可)。
- Shiftで比率拘束ドラッグ中に Shift を押していたら、正方形/正円になるよう幅・高さを揃えると使い勝手が上がります。
- 安全なパレット参照pallet が閉じられたときの扱い(例:再生成、または null チェック)を備えておくと堅牢です。(ここでは簡潔さを優先し割愛)
3. 改訂版 Form1(完成コード)
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace DrawApp
{
public partial class Form1 : Form
{
private Point startPos, endPos;
private Pallet pallet;
public Form1()
{
InitializeComponent();
// ちらつき対策(フォーム内なら protected の DoubleBuffered にアクセス可)
this.DoubleBuffered = true;
// パレットを生成・表示
this.pallet = new Pallet();
this.pallet.Show();
// イベント配線(デザイナでも可)
this.Paint += DrawFigures;
this.MouseDown += MousePressed;
this.MouseMove += MouseDragged;
this.MouseUp += MouseReleased;
}
private void MousePressed(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
startPos = endPos = e.Location;
Invalidate(); // 念のため
}
}
private void MouseDragged(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
endPos = e.Location;
Invalidate(); // 再描画トリガー(リアルタイム更新)
}
}
private void MouseReleased(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
endPos = e.Location;
Invalidate();
}
}
private void DrawFigures(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
if (pallet == null || pallet.IsDisposed) return;
int type = pallet.GetFigureType(); // 1: 円, 2: 長方形, 3: 直線
Color color = pallet.GetColor();
int penSize = pallet.GetPenSize();
// Shift で正方形・正円拘束(直線は対象外)
Rectangle rect = MakeNormalizedRect(startPos, endPos,
constrainSquare: (type == 1 || type == 2) && ModifierKeys.HasFlag(Keys.Shift));
switch (type)
{
case 1: // 円(実際は楕円)
using (var brush = new SolidBrush(color))
{
e.Graphics.FillEllipse(brush, rect);
}
break;
case 2: // 長方形
using (var brush = new SolidBrush(color))
{
e.Graphics.FillRectangle(brush, rect);
}
break;
case 3: // 直線
using (var pen = new Pen(color, penSize))
{
pen.StartCap = LineCap.Round;
pen.EndCap = LineCap.Round;
e.Graphics.DrawLine(pen, startPos, endPos);
}
break;
}
}
/// <summary>
/// 2点(start, end)から左上原点・正の幅高さの矩形を作る。
/// constrainSquare=true なら幅と高さを同じにして正方形化する。
/// </summary>
private static Rectangle MakeNormalizedRect(Point start, Point end, bool constrainSquare)
{
int x1 = Math.Min(start.X, end.X);
int y1 = Math.Min(start.Y, end.Y);
int x2 = Math.Max(start.X, end.X);
int y2 = Math.Max(start.Y, end.Y);
int w = x2 - x1;
int h = y2 - y1;
if (constrainSquare)
{
int size = Math.Min(w, h);
// ドラッグ方向を保ったまま size に調整
if (end.X < start.X) x1 = start.X - size;
if (end.Y < start.Y) y1 = start.Y - size;
w = h = size;
}
return new Rectangle(x1, y1, w, h);
}
}
}
4. シンプルな Pallet フォーム(最小実装例)
下記は 最小限 の参照実装です。
- 図形種:ComboBox(Items: 円=1, 長方形=2, 直線=3)
- 色:Button から ColorDialog を開く
- 線幅:NumericUpDown(直線のみ使用)
UI配置はデザイナで行い、comboFigure, btnColor, nudPenSize, panelColorPreview を配置してください。
using System;
using System.Drawing;
using System.Windows.Forms;
namespace DrawApp
{
public partial class Pallet : Form
{
private int figureType = 1; // 1:円, 2:長方形, 3:直線
private Color currentColor = Color.DeepSkyBlue;
private int penSize = 3;
public Pallet()
{
InitializeComponent();
comboFigure.Items.AddRange(new object[] { "円", "長方形", "直線" });
comboFigure.SelectedIndex = 0;
nudPenSize.Minimum = 1;
nudPenSize.Maximum = 50;
nudPenSize.Value = penSize;
panelColorPreview.BackColor = currentColor;
// イベント
comboFigure.SelectedIndexChanged += (s, e) =>
{
figureType = comboFigure.SelectedIndex switch
{
0 => 1,
1 => 2,
2 => 3,
_ => 1
};
};
btnColor.Click += (s, e) =>
{
using var dlg = new ColorDialog();
dlg.Color = currentColor;
if (dlg.ShowDialog() == DialogResult.OK)
{
currentColor = dlg.Color;
panelColorPreview.BackColor = currentColor;
}
};
nudPenSize.ValueChanged += (s, e) => penSize = (int)nudPenSize.Value;
}
public int GetFigureType() => figureType;
public Color GetColor() => currentColor;
public int GetPenSize() => penSize;
}
}
メモ:円/長方形は塗りつぶしなので線幅は無関係ですが、応用で「枠線描画(DrawEllipse / DrawRectangle)に切り替える」メニューを足すと活きてきます。
5. イベント配線(デザイナ派・コード派)
- デザイナで配線
- Form1 を選択 → プロパティウィンドウ(稲妻アイコン)→ Paint に DrawFigures を割り当て
- 同様に MouseDown・MouseMove・MouseUp へ各ハンドラを割り当て
- コードで配線
- 本記事の Form1 コンストラクタ内の this.Paint += …; のように記述
6. よくあるつまずきと対処
- 逆方向ドラッグで描けない→ MakeNormalizedRect のように矩形を正規化する。
- 描画がカクつく・ちらつく→ this.DoubleBuffered = true; を設定。→ 必要に応じて ResizeRedraw = true;(リサイズ時も再描画)
- 毎回1つしか描けない→ 今回は「現在の1図形のみ」をリアルタイム表示する最小構成。→ 履歴を残すには「List<IFigure> などで図形オブジェクトを保持し、Paint で全件を描く」設計にする(拡張案参照)。
- ペン/ブラシの解放忘れ→ using を徹底。
7. 拡張アイデア(課題に最適)
- 図形履歴の管理
- abstract class Figure { public abstract void Draw(Graphics g); }
- 円/長方形/直線の派生クラスを作り、MouseUp 時に List<Figure> に追加 → Paint で全描画。
- 塗りつぶし/枠線の切替
- パレットでトグルし、FillRectangle / DrawRectangle を使い分け。
- Undo/Redo
- Stack<Figure> を2つ運用(履歴とやり直し)。
- 保存/読み込み
- ラスタ保存:using var bmp = new Bitmap(Width, Height); DrawToBitmap(bmp, ClientRectangle); bmp.Save(“out.png");
- ベクタ保存:図形リストを JSON 化して保存→復元。
- ガイド・拘束機能
- Shift:正円/正方形(今回対応)
- Alt:中心基準ドラッグ など
8. まとめ
- Paint駆動のリアルタイム描画は、WinFormsの基本イベント(MouseDown / MouseMove / MouseUp / Paint)だけで構築できます。
- 実用性を上げるには、矩形の正規化・資源解放・ダブルバッファの3点をまず押さえるのが近道。
- 学習課題としては、図形履歴のオブジェクト化や保存・Undo/Redoに広げると、OOP設計の良い練習になります。
訪問数 2 回, 今日の訪問数 1回
ディスカッション
コメント一覧
まだ、コメントがありません