「橡皮筋」是指滑鼠拖曳時即時顯示繪圖預覽,滑鼠釋放後確定圖形。核心挑戰:即時預覽不留痕跡 + 確定後永久保留 + 視窗重繪正確還原。
PictureBox 承載繪圖面,Dock = Fill 佔滿視窗ArrayList (或 List<Shape>) 儲存已確定圖形Paint 事件統一處理永久圖形重繪MouseDown/Move/Up 處理互動邏輯
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
public class RubberBandForm : Form
{
private PictureBox canvas;
private List shapes = new List();
private Point startPt = Point.Empty, endPt = Point.Empty;
private bool isDrawing = false;
public RubberBandForm()
{
canvas = new PictureBox
{
Dock = DockStyle.Fill,
BackColor = Color.White
};
canvas.Paint += Canvas_Paint;
canvas.MouseDown += Canvas_MouseDown;
canvas.MouseMove += Canvas_MouseMove;
canvas.MouseUp += Canvas_MouseUp;
Controls.Add(canvas);
Text = "橡皮筋繪圖示範";
ClientSize = new Size(600, 400);
}
// 形狀資料結構
private class EllipseShape
{
public Rectangle Bounds { get; set; }
public Pen Pen { get; set; } = Pens.Black;
}
// 重繪:只畫已確定的形狀
private void Canvas_Paint(object sender, PaintEventArgs e)
{
foreach (var shape in shapes)
e.Graphics.DrawEllipse(shape.Pen, shape.Bounds);
}
// 滑鼠按下:記錄起點
private void Canvas_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
isDrawing = true;
startPt = e.Location;
endPt = Point.Empty;
}
}
// 滑鼠移動:橡皮筋效果
private void Canvas_MouseMove(object sender, MouseEventArgs e)
{
if (!isDrawing) return;
endPt = e.Location;
// 關鍵技巧:使用 ControlPaint.DrawReversibleFrame
// XOR 模式繪製,再次呼叫即可抹除,不需重繪整個畫布
using (Graphics g = canvas.CreateGraphics())
{
// 抹除上一次預覽 (若存在)
if (endPt != Point.Empty)
{
Rectangle prevRect = GetRect(startPt, endPt);
ControlPaint.DrawReversibleFrame(
canvas.RectangleToScreen(prevRect), Color.Black, FrameStyle.Dashed);
}
// 繪製新預覽
Rectangle currRect = GetRect(startPt, e.Location);
ControlPaint.DrawReversibleFrame(
canvas.RectangleToScreen(currRect), Color.Black, FrameStyle.Dashed);
}
}
// 滑鼠釋放:確定圖形
private void Canvas_MouseUp(object sender, MouseEventArgs e)
{
if (!isDrawing) return;
isDrawing = false;
// 抹除最後一次預覽
if (endPt != Point.Empty)
{
Rectangle prevRect = GetRect(startPt, endPt);
ControlPaint.DrawReversibleFrame(
canvas.RectangleToScreen(prevRect), Color.Black, FrameStyle.Dashed);
}
// 建立永久形狀
Rectangle finalRect = GetRect(startPt, e.Location);
if (finalRect.Width > 0 && finalRect.Height > 0)
{
shapes.Add(new EllipseShape { Bounds = finalRect });
canvas.Invalidate(); // 觸發 Paint 事件重繪
}
startPt = Point.Empty;
endPt = Point.Empty;
}
// 輔助:由兩點計算矩形 (處理負寬高)
private Rectangle GetRect(Point p1, Point p2)
{
return new Rectangle(
Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y),
Math.Abs(p1.X - p2.X), Math.Abs(p1.Y - p2.Y));
}
[STAThread]
static void Main() => Application.Run(new RubberBandForm());
}
control.RectangleToScreen(rect)FrameStyle.Dashed 虛線框提示「預覽狀態」Paint 事件中透過 e.Graphics 繪製Invalidate() 觸發重繪,而非直接呼叫 CreateGraphics()
// 在建構函式或 Load 事件啟用
canvas.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer, true);
abstract class Shape
{
public abstract void Draw(Graphics g);
public abstract bool Contains(Point pt); // 點選測試
}
class RectangleShape : Shape
{
public Rectangle Bounds { get; set; }
public Pen Pen { get; set; }
public override void Draw(Graphics g) => g.DrawRectangle(Pen, Bounds);
public override bool Contains(Point pt) => Bounds.Contains(pt);
}
class LineShape : Shape
{
public Point Start { get; set; }
public Point End { get; set; }
public Pen Pen { get; set; }
public override void Draw(Graphics g) => g.DrawLine(Pen, Start, End);
public override bool Contains(Point pt) => /* 線段距離檢測 */ false;
}
| 錯誤做法 | 問題 | 正確做法 |
|---|---|---|
MouseMove 中 CreateGraphics() 直接畫 | 視窗重繪時消失、殘留痕跡 | 用 DrawReversibleFrame 或雙緩衝繪製至位圖 |
| 不儲存形狀資料,只畫在畫布上 | 無法重繪、無法編輯、無法儲存 | 資料模型 (List<Shape>) 與視圖 (Paint) 分離 |
| 用實線畫預覽 | 難以區分預覽與確定圖形 | 虛線框、不同顏色、半透明筆刷 |
DrawReversibleFrame XOR 模式預覽 + Paint 事件永久繪製DoubleBuffer) 消除閃爍Shape 基類支援多圖形、編輯、點選、序列化InkCanvas、Adorner 層提供更豐富的繪圖基礎設施