首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計疑難解答

C# WinForms 繪圖完全指南:Paint 事件、雙緩衝、圖形拖曳與存檔

     編輯:程式設計疑難解答

核心概念:為什麼必須在 Paint 事件繪圖?

Windows 採用**保留模式**繪圖:視窗被遮擋、最小化、恢復、調整大小時,系統會發送 WM_PAINT 訊息要求重繪。若僅在 Click 事件中呼叫 CreateGraphics().DrawXxx(),繪製內容僅存在於螢幕快取,下一次 WM_PAINT 到來時會被清除(重繪背景),導致圖形消失。

正確做法:在 PictureBox.Paint 或 Form.Paint 事件中,根據資料模型(List<Shape>)繪製所有圖形。使用者操作只修改資料模型,再呼叫 Invalidate() 觸發重繪。

資料模型設計

public abstract class Shape {
    public Rectangle Bounds { get; set; }
    public Color Color { get; set; } = Color.Black;
    public abstract void Draw(Graphics g);
    public abstract bool Contains(Point p); // 用於點選/拖曳判斷
}

public class RectShape : Shape {
    public override void Draw(Graphics g) => g.DrawRectangle(new Pen(Color), Bounds);
    public override bool Contains(Point p) => Bounds.Contains(p);
}

public class EllipseShape : Shape {
    public override void Draw(Graphics g) => g.DrawEllipse(new Pen(Color), Bounds);
    public override bool Contains(Point p) => Bounds.Contains(p); // 簡易矩形碰撞
}

public class LineShape : Shape {
    public Point EndPoint { get; set; }
    public override void Draw(Graphics g) => g.DrawLine(new Pen(Color), Bounds.Location, EndPoint);
    public override bool Contains(Point p) => /* 線段距離判斷 */ false;
}

PictureBox 完整範例

public partial class DrawForm : Form {
    private List<Shape> shapes = new List<Shape>();
    private Shape currentShape;
    private Point startPoint;
    private Shape selectedShape;
    private Point dragOffset;

    public DrawForm() {
        InitializeComponent();
        pictureBox1.Paint += PictureBox1_Paint;
        pictureBox1.MouseDown += PictureBox1_MouseDown;
        pictureBox1.MouseMove += PictureBox1_MouseMove;
        pictureBox1.MouseUp += PictureBox1_MouseUp;
        // 雙緩衝
        pictureBox1.GetType().GetProperty("DoubleBuffered", 
            System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
            ?.SetValue(pictureBox1, true);
    }

    private void PictureBox1_Paint(object sender, PaintEventArgs e) {
        foreach (var s in shapes) s.Draw(e.Graphics);
        // 繪製正在拖曳建立中的形狀
        if (currentShape != null) currentShape.Draw(e.Graphics);
    }

    private void PictureBox1_MouseDown(object sender, MouseEventArgs e) {
        startPoint = e.Location;
        // 檢查是否點選現有形狀(從最上層往下找)
        for (int i = shapes.Count - 1; i >= 0; i--) {
            if (shapes[i].Contains(e.Location)) {
                selectedShape = shapes[i];
                dragOffset = new Point(e.X - selectedShape.Bounds.X, e.Y - selectedShape.Bounds.Y);
                return;
            }
        }
        // 未點中 → 建立新形狀(依工具列選擇)
        currentShape = CreateShapeByTool(); // 回傳 RectShape/EllipseShape...
        currentShape.Bounds = new Rectangle(startPoint, Size.Empty);
    }

    private void PictureBox1_MouseMove(object sender, MouseEventArgs e) {
        if (selectedShape != null) {
            // 拖曳現有形狀
            selectedShape.Bounds = new Rectangle(e.X - dragOffset.X, e.Y - dragOffset.Y, 
                                                 selectedShape.Bounds.Width, selectedShape.Bounds.Height);
            pictureBox1.Invalidate(); // 觸發 Paint 重繪
        } else if (currentShape != null) {
            // 調整新形狀大小
            int w = Math.Abs(e.X - startPoint.X);
            int h = Math.Abs(e.Y - startPoint.Y);
            currentShape.Bounds = new Rectangle(
                Math.Min(startPoint.X, e.X), Math.Min(startPoint.Y, e.Y), w, h);
            pictureBox1.Invalidate();
        }
    }

    private void PictureBox1_MouseUp(object sender, MouseEventArgs e) {
        if (currentShape != null) {
            if (currentShape.Bounds.Width > 5 && currentShape.Bounds.Height > 5)
                shapes.Add(currentShape);
            currentShape = null;
        }
        selectedShape = null;
        pictureBox1.Invalidate();
    }

    // 存檔為圖片
    private void btnSave_Click(object sender, EventArgs e) {
        using (var bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height)) {
            pictureBox1.DrawToBitmap(bmp, pictureBox1.ClientRectangle);
            bmp.Save("drawing.png", System.Drawing.Imaging.ImageFormat.Png);
        }
    }
}

關鍵技術點總結

問題解決方案
圖形被遮擋後消失所有繪圖邏輯集中在 Paint 事件,靠 Invalidate() 觸發重繪
DrawToBitmap 存出白圖啟用 DoubleBuffered = true,並確保在 Paint 中繪製,而非 CreateGraphics()
拖曳殘影/閃爍雙緩衝 + Invalidate() 整個區域,或計算舊/新矩形聯集只重繪變更區
刪除形狀從 shapes 移除項目 → Invalidate()

進階建議

  • 使用 GraphicsPath 實現精確點選(含旋轉、彎曲形狀)。
  • 引入命令模式支援 Undo/Redo。
  • 大量圖形時考慮虛擬化繪製(只繪製可視區域)。

總結

WinForms 繪圖核心在於**「資料驅動視圖」**:維護形狀集合,Paint 事件遍歷繪製,互動只修改集合並 Invalidate()。配合雙緩衝即可獲得流暢、無殘影、可存檔的繪圖體驗。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved