Windows Forms 自訂控制項是豐富 UI 元件庫的核心技能。本文以建立一個類比時鐘控制項為例,完整演示:專案結構建立、Win32 API 呼叫取得系統時間、GDI+ 繪製表盤/指針、屬性系統整合(分類、描述、預設值)、重寫 OnPaint、資源管理、設計時支援、測試宿主兩種方式。代碼基於 .NET Framework 1.1 SDK 範本,原理通用於後續所有版本。
...\Samples\QuickStart\winforms\samples\Cs\WritingControls\helloworldcontrol → MyControlHelloWorldControl.cs → MyControl.cs、.resx 同步Makefile、HostApp.cs 內類別名稱nmake all 建立 MyControl.dll、MyControl.exe (宿主測試)
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public class SYSTEMTIME {
public ushort wYear, wMonth, wDayOfWeek, wDay;
public ushort wHour, wMinute, wSecond, wMilliseconds;
}
public static class NativeMethods {
[DllImport("kernel32.dll")]
public static extern void GetLocalTime(out SYSTEMTIME lpSystemTime);
}
為何不用 DateTime.Now?教學展示 P/Invoke;實務中 DateTime.Now 精度足夠(~15ms),GetLocalTime 同樣受系統時鐘解析度限制。
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Timers;
public class MyControl : Control {
// 成員欄位
private Color _handColor = Color.White;
private Color _faceColor = Color.Blue;
private bool _active = false;
private Timer _timer;
// 建構函數
public MyControl() {
SetStyle(ControlStyles.Opaque, false);
SetStyle(ControlStyles.ResizeRedraw, true);
_timer = new Timer(1000); // 1 秒
_timer.Elapsed += OnTimerTick;
}
// 屬性系統整合
[Category("Clock"), Description("指針顏色"), DefaultValue(typeof(Color), "White")]
public Color HandColor {
get => _handColor;
set { _handColor = value; Invalidate(); Update(); }
}
[Category("Clock"), Description("表盤顏色"), DefaultValue(typeof(Color), "Blue")]
public Color FaceColor {
get => _faceColor;
set { _faceColor = value; Invalidate(); Update(); }
}
[Category("Clock"), Description("是否運行")]
public bool Active {
get => _active;
set { _active = value; _timer.Enabled = value; }
}
// 重寫 OnPaint:核心繪製邏輯
protected override void OnPaint(PaintEventArgs pe) {
base.OnPaint(pe);
Graphics g = pe.Graphics;
Rectangle rc = ClientRectangle;
// 1. 繪製表盤背景
using (Brush faceBrush = new SolidBrush(_faceColor)) {
g.FillEllipse(faceBrush, rc);
}
// 2. 取得系統時間
NativeMethods.GetLocalTime(out SYSTEMTIME st);
// 3. 繪製指針 (時/分/秒)
Point center = new Point(rc.Width / 2, rc.Height / 2);
int radius = Math.Min(rc.Width, rc.Height) / 2 - 5;
DrawHand(g, center, radius * 0.5f, (st.wHour % 12) * 30 + st.wMinute * 0.5f, _handColor, 3); // 時針
DrawHand(g, center, radius * 0.8f, st.wMinute * 6 + st.wSecond * 0.1f, _handColor, 2); // 分針
DrawHand(g, center, radius * 0.9f, st.wSecond * 6, Color.Red, 1); // 秒針
}
// 輔助:繪製單一指針
private void DrawHand(Graphics g, Point center, float length, float angleDeg, Color color, int width) {
using (Pen pen = new Pen(color, width)) {
double rad = (angleDeg - 90) * Math.PI / 180.0; // 12 點鐘方向為 -90 度
PointF end = new PointF(
center.X + length * (float)Math.Cos(rad),
center.Y + length * (float)Math.Sin(rad));
g.DrawLine(pen, center, end);
}
}
// Timer 事件:觸發重繪
private void OnTimerTick(object sender, ElapsedEventArgs e) {
if (InvokeRequired) {
Invoke(new Action(Invalidate));
} else {
Invalidate();
}
}
// 資源清理
protected override void Dispose(bool disposing) {
if (disposing) {
_timer?.Dispose();
}
base.Dispose(disposing);
}
}
ResizeRedraw 尺寸變化自動 Invalidate;Opaque 告知不透明,減少閃爍[Category]、[Description]、[DefaultValue] 讓屬性在設計器屬性視窗正確顯式、支援重設Invalidate() 標記需重繪,Update() 強制同步重繪(屬性設值時即時反饋)System.Timers.Timer 在 ThreadPool 執行緒觸發 → 必須 InvokeRequired/Invoke 回 UI 執行緒呼叫 InvalidateGraphics、Pen、Brush 實作 IDisposable,及時釋放 GDI 物件,防止句柄洩漏
// HostApp.cs 片段
Application.Run(new HostForm()); // HostForm 包含 MyControl 實例
MyControl.dllMyControl 到表單 → 屬性視窗調整 HandColor、FaceColor、ActiveSetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true) 消除閃爍[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] 支援巢狀屬性IComponent、DesignerActionList 提供設計器右鍵快速操作Assembly.GetManifestResourceStream 載入System.Windows.Forms NuGet、設計器支援 Visual Studio 16.6+自訂控制項開發核心三步:繼承 Control、重寫 OnPaint、屬性系統整合。本文時鐘控制項展示了 P/Invoke、GDI+ 繪圖、Timer 跨執行緒 UI 更新、資源釋放、設計時屬性支援等完整工程實踐。掌握此流程,即可構建儀表板、圖表、專業輸入控制項等高階 UI 元件,構建屬於團隊的專屬控制項庫。