WinFormsで「パレット連動お絵かき」最小実装

— 円・長方形・直線をドラッグで描く(コード解説+実用改善)

この記事は、教科書の Form1(描画側)と Pallet(ツールパレット側)のコードをベースに、

  • 仕組みの要点
  • 実務で困りがちなポイント
  • “ちょい足し”の改良(矩形の正規化・資源解放・ダブルバッファ・Shiftで正円/正方形)を整理します。最後に安全・快適に動く改訂版コード(抜粋)も載せます。

1) 全体像:どういう構成?

  • Pallet フォームで 図形の種類(円/長方形/直線)・色・線幅 を選ぶ
  • Form1(描画面)で 左ドラッグ
    • MouseDown で始点 startPos を記録
    • MouseMove で終点 endPos を更新 → Invalidate() で再描画
    • Paint(ここでは DrawFigures)で、パレットの設定を参照し、現在のドラッグ範囲に図形を描く

現状は「いま描いている1個」だけをリアルタイム表示する最小構成です。履歴を残して複数の図形を積み上げるのは拡張編で触れます。


2) まずはベースコードのカギ

Form1 側

  • startPos/endPos を使って、Paint で図形を描画
  • MouseMove(左ボタン押下中)で endPos を更新し Invalidate()(再描画トリガ)

Pallet 側

  • figureType(1=円, 2=長方形, 3=直線)をボタンで切替
  • 色は ColorDialog の選択を colorButton.BackColor に保持
  • 線幅は TextBox penSizeBox を int.TryParse で数値化(失敗時は 1)

3) “ここでつまずく” を先回りで解消

A. 逆方向ドラッグで幅や高さが負になる

FillEllipse/FillRectangle は幅や高さが負数だとうまく描けません。

始点・終点から「左上基準・正の幅高さ」の矩形を作る関数を用意しましょう。

private static Rectangle MakeNormalizedRect(Point a, Point b, bool square)
{
    int x1 = Math.Min(a.X, b.X);
    int y1 = Math.Min(a.Y, b.Y);
    int x2 = Math.Max(a.X, b.X);
    int y2 = Math.Max(a.Y, b.Y);
    int w = x2 - x1, h = y2 - y1;

    if (square)
    {
        int size = Math.Min(w, h);
        if (b.X < a.X) x1 = a.X - size;
        if (b.Y < a.Y) y1 = a.Y - size;
        w = h = size;
    }
    return new Rectangle(x1, y1, w, h);
}

ついでに Shift キーで正円/正方形拘束できるように square を使います。


B. Pen や Brush の破棄(GDI+ リソース管理)

Pen や Brush は IDisposable。using で確実に破棄しましょう。

using (var pen = new Pen(color, penSize))
{
    e.Graphics.DrawLine(pen, startPos, endPos);
}

C. ちらつき対策(ダブルバッファ)

フォームの DoubleBuffered を true にすると、ドラッグ中の描画がなめらかになります。

public Form1()
{
    InitializeComponent();
    this.DoubleBuffered = true; // 追加
    ...
}

D. “描いている間だけ表示したい”

現在のコードは、最後にドラッグした図形が常時描かれ続けます。

ドラッグ中だけ描くなら、フラグを追加します。

private bool _isDrawing;

private void MousePressed(object s, MouseEventArgs e)
{
    if (e.Button != MouseButtons.Left) return;
    _isDrawing = true;
    startPos = endPos = e.Location;
}

private void MouseDragged(object s, MouseEventArgs e)
{
    if (!_isDrawing) return;
    endPos = e.Location;
    Invalidate();
}

private void MouseReleased(object s, MouseEventArgs e)
{
    if (e.Button != MouseButtons.Left) return;
    _isDrawing = false;
    Invalidate();
}

Paint 側で _isDrawing を見て描く/描かないを切り替えます。


E. Pallet の入力を堅牢に

  • penSizeBox は NumericUpDown に替えると入力エラーが消え、UI も分かりやすいです。
  • colorButton の初期色を設定しておくと安心です。
  • Pallet を TopMost にすると、ツールが背面に隠れにくくなります。

4) 改訂版コード(要点抜粋)

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 bool _isDrawing;
        private Pallet pallet;

        public Form1()
        {
            InitializeComponent();
            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) return;
            _isDrawing = true;
            startPos = endPos = e.Location;
            Invalidate();
        }

        private void MouseDragged(object sender, MouseEventArgs e)
        {
            if (!_isDrawing) return;
            endPos = e.Location;
            Invalidate();
        }

        private void MouseReleased(object sender, MouseEventArgs e)
        {
            if (e.Button != MouseButtons.Left) return;
            _isDrawing = false;
            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 = Math.Max(1, pallet.GetPenSize());

            // Shift押下で正方形/正円拘束(直線は対象外)
            bool square = (type == 1 || type == 2) && ModifierKeys.HasFlag(Keys.Shift);
            Rectangle rect = MakeNormalizedRect(startPos, endPos, square);

            // 「描画中だけ表示」にするなら以下を有効化
            // if (!_isDrawing && type != 3) return;

            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;
            }
        }

        private static Rectangle MakeNormalizedRect(Point a, Point b, bool square)
        {
            int x1 = Math.Min(a.X, b.X), y1 = Math.Min(a.Y, b.Y);
            int x2 = Math.Max(a.X, b.X), y2 = Math.Max(a.Y, b.Y);
            int w = x2 - x1, h = y2 - y1;

            if (square)
            {
                int size = Math.Min(w, h);
                if (b.X < a.X) x1 = a.X - size;
                if (b.Y < a.Y) y1 = a.Y - size;
                w = h = size;
            }
            return new Rectangle(x1, y1, w, h);
        }
    }
}

Pallet(最小修正案)

penSizeBox をそのまま使う前提で、初期色を設定しておきます。

(可能なら NumericUpDown に置き換えるのが最良)

using System;
using System.Drawing;
using System.Windows.Forms;

namespace DrawApp
{
    public partial class Pallet : Form
    {
        int figureType; // 1:円, 2:長方形, 3:直線

        public Pallet()
        {
            InitializeComponent();
            this.figureType = 1;

            // 初期色(見やすい色を設定)
            colorButton.BackColor = Color.DeepSkyBlue;

            // ツールが背面に隠れにくいように
            this.TopMost = true;
        }

        public int GetFigureType() => figureType;

        public int GetPenSize()
        {
            if (int.TryParse(this.penSizeBox.Text, out int size))
                return Math.Max(1, size);
            return 1;
        }

        public Color GetColor() => colorButton.BackColor;

        private void CircleButtonClicked(object sender, EventArgs e) => this.figureType = 1;
        private void RectButtonClicked(object sender, EventArgs e) => this.figureType = 2;
        private void LineButtonClicked(object sender, EventArgs e) => this.figureType = 3;

        private void ColorButtonClicked(object sender, EventArgs e)
        {
            using (var dlg = new ColorDialog { Color = colorButton.BackColor })
            {
                if (dlg.ShowDialog() == DialogResult.OK)
                    colorButton.BackColor = dlg.Color;
            }
        }
    }
}

UIヒント:penSizeBox は NumericUpDown(最小1, 最大50)に替えると、数値以外の入力ミスが消えます。


5) 発展:実務で使うならここまで

  • 図形履歴の保持:List
     に MouseUp 時の確定図形を追加し、Paint で全件描画
    • abstract class Figure { public abstract void Draw(Graphics g); } を作って円/長方形/直線の派生クラスに分離
  • 塗り/枠の切替:FillRectangle と DrawRectangle をトグル可能に
  • Undo/Redo:Stack<Figure> 2本持ち
  • 保存
    • 画像保存 → DrawToBitmap で PNG/JPEG
    • ベクタ保存 → 図形リストを JSON 化して読み書き

6) まとめ

  • イベント駆動 + Paint再描画が WinForms の基本パターン。
  • 逆方向ドラッグ対応の矩形正規化using での GDI+ 解放ダブルバッファは“最初の3点セット”。
  • Shift での正円/正方形拘束や NumericUpDown への置換など、UI/操作性を少し整えるだけで使い勝手が大きく向上します。

訪問数 3 回, 今日の訪問数 1回