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/操作性を少し整えるだけで使い勝手が大きく向上します。
ディスカッション
コメント一覧
まだ、コメントがありません