WinFormsで「パレット連動・ドラッグ描画」入門

— 円/長方形/直線をリアルタイムに描く

この技術ブログでは、提示いただいたコードをベースに、パレット(色・線幅・図形種)を別フォームで操作しつつ、メインフォームでドラッグ中に図形をプレビュー描画する最小構成を解説します。

そのうえで、実用に向けた**改善点(矩形の正規化・資源解放・ちらつき対策・Shiftで正円/正方形)**も反映した完成版コードを提示します。


完成仕様(今回のゴール)

  • パレット(Pallet フォーム)で図形種(円/長方形/直線)、色、線幅を選択
  • メインフォーム(Form1)でマウス左ドラッグすると、ドラッグ範囲に図形をリアルタイム描画
  • アンチエイリアスで滑らかに表示
  • ちらつきを抑えるためダブルバッファ有効化
  • 逆方向ドラッグ(左上へ向けてなど)でも正しく描画(矩形の正規化
  • Shiftキーで正円/正方形に拘束(直線は除外)

注意:本記事のサンプルは「今描いている1つの図形」を都度再描画する最小構成です。履歴を残して複数個を積み上げたい場合は、末尾の拡張アイデアをご覧ください。


1. 元コードのポイント整理

  • Paintイベント(ここでは DrawFigures)で常に現在のドラッグ範囲だけを描く。
  • MouseDownで始点を記録、MouseMove(左ボタン押下中)で終点を更新して Invalidate() → 再描画。
  • パレット Pallet から図形種/色/線幅を取得。

シンプルでわかりやすい構造ですが、いくつか改善余地があります。


2. よくある改善ポイント

  1. 矩形の正規化FillEllipse / FillRectangle は幅や高さが負数だと期待通りになりません。→ startPos と endPos から 左/上/幅/高さを正規化して Rectangle を作る関数を用意しましょう。
  2. GDI+資源解放Pen や Brush は IDisposable。using を使って確実に解放しましょう。
  3. ちらつき対策this.DoubleBuffered = true; を有効化(フォーム内なら protected プロパティにアクセス可)。
  4. Shiftで比率拘束ドラッグ中に Shift を押していたら、正方形/正円になるよう幅・高さを揃えると使い勝手が上がります。
  5. 安全なパレット参照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. 拡張アイデア(課題に最適)

  1. 図形履歴の管理
    • abstract class Figure { public abstract void Draw(Graphics g); }
    • 円/長方形/直線の派生クラスを作り、MouseUp 時に List<Figure> に追加 → Paint で全描画。
  2. 塗りつぶし/枠線の切替
    • パレットでトグルし、FillRectangle / DrawRectangle を使い分け。
  3. Undo/Redo
    • Stack<Figure> を2つ運用(履歴とやり直し)。
  4. 保存/読み込み
    • ラスタ保存:using var bmp = new Bitmap(Width, Height); DrawToBitmap(bmp, ClientRectangle); bmp.Save(“out.png");
    • ベクタ保存:図形リストを JSON 化して保存→復元。
  5. ガイド・拘束機能
    • Shift:正円/正方形(今回対応)
    • Alt:中心基準ドラッグ など

8. まとめ

  • Paint駆動のリアルタイム描画は、WinFormsの基本イベント(MouseDown / MouseMove / MouseUp / Paint)だけで構築できます。
  • 実用性を上げるには、矩形の正規化・資源解放・ダブルバッファの3点をまず押さえるのが近道。
  • 学習課題としては、図形履歴のオブジェクト化保存・Undo/Redoに広げると、OOP設計の良い練習になります。
訪問数 2 回, 今日の訪問数 1回