首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 圖形開發中級實戰:打造自訂資料網格控件

     編輯:關於C#

前言:邁向圖形開發的進階領域

在「C# 發現之旅」系列的前幾講中,我們已建立起 Windows 圖形開發的基礎認知。本講將進入中級階段,著手開發一個功能完整的資料網格控件。不同於一般的 Web 資料庫開發,這類圖形組件涉及文檔-視圖架構、座標轉換、滾動機制與滑鼠交互等核心圖形編程技術,是提升 C# 綜合開發能力的絕佳實戰題材。

專案需求與設計思路

功能規格

  • 以網格介面呈現資料表的文字內容
  • 支援設定列高,欄寬自動適應文字內容
  • 內容超出可視區域時自動顯示捲軸
  • 支援滑鼠點擊選取單一儲存格,拖曳選取多個儲存格
  • 提供複製選取儲存格文字至剪貼簿功能

文檔-視圖架構模式

面對這類複雜的圖形組件,傳統將資料與繪製邏輯混雜在控件內的做法難以維護。我們採用文檔-視圖模式:建立獨立的文檔物件模型(DOM)來映射資料結構,視圖控件僅負責根據此模型進行繪製與事件響應。這種模式廣泛應用於專業圖形軟件(如 CAD、編輯器)中,能有效分離關注點。

文檔物件模型設計

資料本質是二維表格,自然對應三層物件樹狀結構:

  • Cell:儲存格物件,包含文字、位置、大小、選取狀態
  • CellRow:表格行物件,本質是儲存格集合
  • CellDocument:表格文檔物件,本質是行集合,提供 Cells 屬性快速取得所有儲存格

為簡化設計,未單獨定義欄物件,而是透過遍歷行集合來處理欄相關邏輯。

核心功能實現詳解

1. 資料填充與文檔建構

FillDataTable 方法負責將 DataTable 轉換為文檔模型:

public void FillDataTable(DataTable table) {
    if (table == null) throw new ArgumentNullException("table");
    myDocument.Clear();
    // 建立標題行
    CellRow headerRow = new CellRow();
    myDocument.Add(headerRow);
    foreach (DataColumn col in table.Columns) headerRow.Add(col.ColumnName);
    // 填充資料行
    foreach (DataRow drow in table.Rows) {
        CellRow row = new CellRow();
        foreach (DataColumn col in table.Columns) {
            object v = drow[col];
            string txt = (v == null || DBNull.Value.Equals(v)) ? "[NULL]" : Convert.ToString(v);
            row.Add(txt);
        }
        myDocument.Add(row);
    }
    // 觸發排版與重繪
    using (Graphics g = this.CreateGraphics()) {
        RefreshSize(g);
        this.Invalidate();
    }
}

此處使用 CreateGraphics() 取得繪圖上下文以計算文字寬度,Graphics 物件不可直接 new,必須從控件或影像取得。

2. 智慧排版與捲軸管理

RefreshSize 是排版核心,執行「先算欄寬、再定位置、最後設捲軸」的三步驟:

public void RefreshSize(Graphics g) {
    Size viewSize = Size.Empty;
    int leftAccum = 0;
    // 允許最多 1000 欄
    for (int colIdx = 0; colIdx < 1000; colIdx++) {
        var cellsInCol = new ArrayList();
        // 收集同一欄的所有儲存格
        for (int rowIdx = 0; rowIdx < myDocument.Count; rowIdx++) {
            CellRow row = myDocument[rowIdx];
            if (colIdx < row.Count) {
                Cell cell = row[colIdx];
                cell.intLeft = leftAccum;
                cell.intTop = rowIdx * this.RowHeight;
                cellsInCol.Add(cell);
            }
        }
        if (cellsInCol.Count == 0) break; // 無更多欄位

        // 計算該欄最大寬度
        int maxWidth = 40; // 最小寬度
        foreach (Cell cell in cellsInCol) {
            if (!string.IsNullOrEmpty(cell.Text)) {
                SizeF sz = g.MeasureString(cell.Text, this.Font, 1000, StringFormat.GenericDefault);
                if (maxWidth < (int)sz.Width) maxWidth = (int)sz.Width;
            }
        }
        maxWidth += 10; // 留白

        // 套用欄寬並更新整體視圖大小
        foreach (Cell cell in cellsInCol) {
            cell.intWidth = maxWidth;
            cell.intHeight = this.RowHeight;
            if (cell.Left + cell.Width > viewSize.Width) viewSize.Width = cell.Left + cell.Width;
            if (cell.Top + cell.Height > viewSize.Height) viewSize.Height = cell.Top + cell.Height;
        }
        leftAccum += maxWidth;
    }
    viewSize.Width += 10; viewSize.Height += 10; // 邊距
    if (!this.AutoScrollMinSize.Equals(viewSize)) {
        this.AutoScrollMinSize = viewSize;
        this.Invalidate();
    }
}

關鍵點:

  • 使用 Graphics.MeasureString 精確測量文字寬度
  • 透過 AutoScrollMinSize 與 AutoScroll = true 讓 UserControl 自動管理捲軸顯示
  • 排版完成後呼叫 Invalidate() 觸發重繪

3. 高效繪製:剪裁矩形優化

OnPaint 覆寫實現網格繪製,善用 e.ClipRectangle 跳過不可見儲存格:

protected override void OnPaint(PaintEventArgs e) {
    base.OnPaint(e);
    // 座標轉換:文檔座標 → 控件客戶區座標(抵銷捲動位移)
    Rectangle clipRect = e.ClipRectangle;
    clipRect.Offset(-this.AutoScrollPosition.X, -this.AutoScrollPosition.Y);
    e.Graphics.TranslateTransform(this.AutoScrollPosition.X, this.AutoScrollPosition.Y);

    // 共用繪圖資源(using 確保釋放)
    using (Pen gridPen = (intGridColor.A != 0) ? new Pen(intGridColor) : null)
    using (SolidBrush gridBrush = (intGridBackColor.A != 0) ? new SolidBrush(intGridBackColor) : null)
    using (SolidBrush textBrush = new SolidBrush(this.ForeColor)) {
        StringFormat fmt = new StringFormat {
            Alignment = StringAlignment.Near,
            LineAlignment = StringAlignment.Center,
            FormatFlags = StringFormatFlags.NoWrap
        };

        foreach (CellRow row in myDocument) {
            foreach (Cell cell in row) {
                Rectangle bounds = cell.Bounds;
                // 剪裁優化:不相交則跳過
                if (!clipRect.IntersectsWith(bounds)) continue;

                // 背景:選取態用系統高亮色
                if (cell.Selected) e.Graphics.FillRectangle(SystemBrushes.Highlight, bounds);
                else if (gridBrush != null) e.Graphics.FillRectangle(gridBrush, bounds);

                // 邊框
                if (gridPen != null) e.Graphics.DrawRectangle(gridPen, bounds);

                // 文字
                if (cell.Text != null) {
                    e.Graphics.DrawString(cell.Text, this.Font,
                        cell.Selected ? SystemBrushes.HighlightText : textBrush,
                        new RectangleF(cell.Left, cell.Top, cell.Width, cell.Height), fmt);
                }
            }
        }
    }
}

座標轉換(折射效應)是滾動介面繪圖的關鍵:文檔視圖座標與控件客戶區座標因捲動產生位移,必須用 TranslateTransform 整體偏移繪圖原點,並在點擊測試時反向轉換滑鼠座標。

4. 滑鼠交互:點擊與拖曳選取

三個事件覆寫構成完整選取邏輯:

// 滑鼠按下:單點選取
protected override void OnMouseDown(MouseEventArgs e) {
    base.OnMouseDown(e);
    Point pt = new Point(e.X, e.Y);
    pt.Offset(-this.AutoScrollPosition.X, -this.AutoScrollPosition.Y); // 座標轉換
    LastMousePosition = pt;
    foreach (Cell cell in myDocument.Cells) {
        bool sel = cell.Bounds.Contains(pt);
        if (cell.Selected != sel) { InvalidateCell(cell); cell.Selected = sel; }
    }
}

// 滑鼠移動:拖曳框選
protected override void OnMouseMove(MouseEventArgs e) {
    base.OnMouseMove(e);
    if (LastMousePosition.X < 0) return;
    Point pt = new Point(e.X, e.Y);
    pt.Offset(-this.AutoScrollPosition.X, -this.AutoScrollPosition.Y);
    // 以兩點構成選取矩形
    Rectangle selRect = Rectangle.FromLTRB(
        Math.Min(LastMousePosition.X, pt.X), Math.Min(LastMousePosition.Y, pt.Y),
        Math.Max(LastMousePosition.X, pt.X), Math.Max(LastMousePosition.Y, pt.Y));
    foreach (Cell cell in myDocument.Cells) {
        bool hit = selRect.IntersectsWith(cell.Bounds);
        if (cell.Selected != hit) { cell.Selected = hit; InvalidateCell(cell); }
    }
}

// 滑鼠釋放:結束拖曳
protected override void OnMouseUp(MouseEventArgs e) {
    base.OnMouseUp(e);
    LastMousePosition = new Point(-1, -1);
}

InvalidateCell 會將文檔座標的儲存格轉為客戶區座標再呼叫 Invalidate(rect),實現局部重繪,避免全介面閃爍。

5. 剪貼簿整合

Copy 方法將選取儲存格按行組裝為 Tab 分隔文字,寫入系統剪貼簿:

public void Copy() {
    StringBuilder sb = new StringBuilder();
    foreach (CellRow row in myDocument) {
        bool hasSel = false;
        foreach (Cell cell in row) if (cell.Selected) {
            sb.Append(cell.Text).Append('\t');
            hasSel = true;
        }
        if (hasSel) sb.AppendLine();
    }
    if (sb.Length > 0) {
        DataObject data = new DataObject();
        data.SetData(sb.ToString());
        Clipboard.SetDataObject(data, true);
    }
}

.NET 的 DataObject 支援多種格式同時放入剪貼簿(純文字、RTF、圖片等),目標應用程式可自行挑選可用格式。

系統色彩與視覺一致性

程式碼中大量使用 SystemBrushes.Highlight、SystemBrushes.HighlightText 等系統預定義筆刷,確保控件外觀隨作業系統主題自動調整,提供原生一致的使用者體驗。Color.A 屬性(Alpha 通道)用於判斷顏色是否完全透明(A=0),避免建立無意義的繪圖物件。

部署與延伸

開發期以 WinForms 應用程式模式除錯,完成後將專案輸出類型改為「類別庫」編譯成 DLL,即可作為可重用元件交付客戶整合。

本講小結與下一步

本講完整實作了一個具備資料綁定、自動排版、滾動虛擬化、滑鼠框選、剪貼簿互操作的資料網格控件。關鍵技術點包括:

  • 文檔-視圖架構分離資料與呈現
  • Graphics.MeasureString 驅動的自適應欄寬演算法
  • 捲動座標轉換(折射效應)的統一處理模式
  • 剪裁矩形與 InvalidateCell 的繪製效能優化
  • 系統色彩筆刷實現主題適配

下一講將挑戰更高階的圖形開發主題:向量繪圖編輯器的核心架構、圖層管理、撤銷/重做機制以及複雜圖形的命中測試與編輯。敬請期待!

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