C# GDI+图形编程入门:从绘制正方形到掌握核心原理与实战技巧

📅 2026/7/31 7:41:33
C# GDI+图形编程入门:从绘制正方形到掌握核心原理与实战技巧
1. 项目概述从零到一用C#绘制一个正方形在图形编程的入门世界里绘制一个正方形往往是第一个里程碑。这听起来简单但背后却串联起了图形设备接口GDI/GDI、坐标系理解、绘图逻辑封装等一系列核心概念。对于C#开发者无论是想为WinForms桌面应用添加自定义图表还是为WPF应用创建独特的视觉元素亦或是在控制台里玩点“花样”掌握基础的图形绘制都是绕不开的一步。今天我们就来彻底拆解这个任务不仅提供可以直接运行的完整源码更会深入每一步背后的“为什么”让你知其然更知其所以然。无论你是刚接触C#的新手还是想巩固图形基础的老鸟这篇内容都能让你收获一份清晰、可复现的实操指南。2. 核心思路与方案选型为什么是GDI在C#中绘制图形我们有几个主流选择古老的GDI、现代的GDI、以及WPF中的DrawingContext或Shape类。对于“绘制正方形”这个基础任务我选择使用GDI它封装在System.Drawing命名空间中。原因有三首先它的API直观易懂Graphics对象、Pen、Brush等概念层次清晰非常适合教学和入门理解。其次它兼容性极佳从.NET Framework到.NET Core/.NET 5的Windows平台都能良好支持注意.NET Core中需通过NuGet安装System.Drawing.Common包。最后其原理是许多更高级图形库包括部分游戏引擎和自定义控件绘制的基础打好这个基础对未来大有裨益。我们的核心思路是创建一个绘图表面如窗体或位图获取其Graphics绘图对象然后使用该对象的DrawRectangle方法并指定一个Pen画笔来勾勒正方形的轮廓。这里的关键在于理解坐标系和参数Windows窗体默认以客户区左上角为原点(0,0)X轴向右Y轴向下。DrawRectangle方法需要左上角坐标和宽高。因此绘制一个正方形本质上就是指定一个左上角坐标并让宽度和高度相等。注意在跨平台的.NET 6/8项目中System.Drawing.Common主要用于Windows对于Linux/macOS官方推荐使用ImageSharp或SkiaSharp等库。本文以最经典的Windows窗体应用为例确保概念清晰。2.1 项目环境准备我们将创建一个Windows窗体应用.NET Framework 或 .NET Windows桌面应用。以Visual Studio 2022为例新建项目选择“Windows窗体应用”命名为DrawSquareDemo。目标框架选择.NET 6.0 (Long-term support)或.NET 8.0如果是.NET Core系列。对于.NET Core项目需要手动添加System.Drawing.Common的NuGet包引用。在解决方案资源管理器右键点击项目 - “管理NuGet程序包” - 浏览并安装System.Drawing.Common。3. 核心代码实现与逐行解析我们将实现两个版本一个在窗体Paint事件中直接绘制另一个封装成可复用的方法。先从最直接的方式开始。3.1 基础版本在窗体加载或重绘时绘制打开默认生成的Form1.cs文件切换到代码视图。我们将在窗体的Paint事件处理程序中编写绘图代码。Paint事件在窗体首次显示、被其他窗口遮挡后重新露出、最小化后恢复、以及手动调用Invalidate()方法时触发这是放置绘图逻辑的正确位置。using System; using System.Drawing; using System.Windows.Forms; namespace DrawSquareDemo { public partial class Form1 : Form { public Form1() { InitializeComponent(); // 订阅Paint事件也可以在设计器里双击窗体属性的事件页签中的Paint事件 this.Paint new PaintEventHandler(Form1_Paint); } private void Form1_Paint(object sender, PaintEventArgs e) { // 关键对象从事件参数中获取Graphics对象它代表当前窗体的绘图表面 Graphics g e.Graphics; // 关键工具创建一个红色的画笔宽度为2像素 Pen redPen new Pen(Color.Red, 2); // 定义正方形的参数 int startX 50; // 正方形左上角X坐标 int startY 50; // 正方形左上角Y坐标 int sideLength 200; // 正方形的边长 // 核心绘图方法绘制矩形当宽高相等时即为正方形 // 参数画笔 起始X坐标 起始Y坐标 宽度 高度 g.DrawRectangle(redPen, startX, startY, sideLength, sideLength); // 重要释放画笔资源。虽然.NET有垃圾回收但及时释放GDI对象是良好习惯 redPen.Dispose(); } } }代码解析与注意事项Graphics g e.Graphics;这是绘图的入口。PaintEventArgs的Graphics属性提供了与当前控件这里是窗体关联的绘图上下文。所有绘图操作都通过它进行。不要尝试自己创建new Graphics()这是不可能的必须从有效的上下文如PaintEventArgs、Control.CreateGraphics()或Image对象获取。Pen redPen new Pen(Color.Red, 2);Pen类用于绘制线条和形状的轮廓。Color.Red是预定义颜色2是线宽单位是像素。你可以使用Color.FromArgb(255, 0, 0)来自定义颜色。g.DrawRectangle(...)这是核心方法。它接收一个Pen和定义矩形位置大小的四个参数。注意这里的startX和startY是矩形左上角的坐标。资源释放Pen、Brush、Font、Graphics等实现了IDisposable接口的对象在使用完毕后应调用Dispose()方法释放非托管资源。虽然在窗体关闭时这些资源最终会被回收但显式释放可以避免潜在的内存泄漏和GDI句柄耗尽问题尤其是在频繁绘图的场景下。更现代的做法是使用using语句块。3.2 进阶版本封装绘制逻辑与动态交互基础版本是静态的。让我们把它变得更灵活封装一个绘制方法并允许通过按钮点击来改变正方形的位置或大小以演示动态绘图。首先在设计视图中从工具箱拖拽一个Button控件到窗体上将其Text属性改为“移动正方形”。然后修改Form1.cs的代码如下using System; using System.Drawing; using System.Windows.Forms; namespace DrawSquareDemo { public partial class Form1 : Form { // 将正方形的参数定义为字段以便在多个方法中访问和修改 private int _squareX 50; private int _squareY 50; private const int SIDE_LENGTH 200; private Pen _drawingPen; public Form1() { InitializeComponent(); this.Paint new PaintEventHandler(Form1_Paint); // 初始化画笔 _drawingPen new Pen(Color.Blue, 3); // 为按钮添加点击事件 button1.Click Button1_Click; } private void Form1_Paint(object sender, PaintEventArgs e) { // 调用封装的绘制方法 DrawSquare(e.Graphics, _drawingPen, _squareX, _squareY, SIDE_LENGTH); } /// summary /// 封装的绘制正方形方法 /// /summary /// param nameg绘图表面/param /// param namepen使用的画笔/param /// param namex左上角X坐标/param /// param namey左上角Y坐标/param /// param nameside边长/param private void DrawSquare(Graphics g, Pen pen, int x, int y, int side) { // 使用using语句自动管理画笔资源注意这里传入的pen由外部管理生命周期 // 实际绘制 g.DrawRectangle(pen, x, y, side, side); // 可选绘制一个填充的内部正方形演示Brush的用法 using (Brush fillBrush new SolidBrush(Color.FromArgb(100, Color.LightGreen))) // 半透明的浅绿色 { g.FillRectangle(fillBrush, x 5, y 5, side - 10, side - 10); // 画一个稍小的填充矩形 } } private void Button1_Click(object sender, EventArgs e) { // 点击按钮时改变正方形的位置 _squareX 20; _squareY 10; // 强制使整个客户区无效从而触发Paint事件重绘 this.Invalidate(); } // 窗体关闭时释放画笔资源 protected override void OnFormClosed(FormClosedEventArgs e) { base.OnFormClosed(e); _drawingPen?.Dispose(); } } }进阶解析与技巧封装方法DrawSquare将绘图逻辑独立出来提高了代码的可复用性和可测试性。现在你可以在任何需要Graphics对象的地方调用这个方法。使用using管理资源在DrawSquare方法内部我们为填充画刷使用了using语句。using确保在代码块执行完毕后画刷的Dispose方法会被自动调用这是处理IDisposable对象的推荐方式能有效避免忘记释放。动态更新与重绘在按钮点击事件中我们修改了正方形的坐标_squareX和_squareY然后调用了this.Invalidate()。Invalidate()方法标记窗体的整个客户区或指定区域为“无效”告诉Windows这个区域需要重画。Windows会在适当的时机通常是当前消息处理完毕后触发Paint事件。这是实现图形动画和交互更新的标准模式修改数据 - 调用Invalidate()- 在Paint事件中根据新数据重绘。不要直接在按钮事件里调用CreateGraphics()来画那会导致绘制内容在下次重绘时被擦除因为不是持久化的。填充与轮廓结合我们使用FillRectangle方法配合SolidBrush画刷绘制了一个半透明的填充矩形。Color.FromArgb(alpha, color)可以创建带透明度Alpha通道的颜色alpha值范围0完全透明到255完全不透明。这展示了如何绘制实心图形。4. 完整源码与项目结构为了让项目更完整我们引入一个简单的“设置”概念并通过配置文件或UI来改变正方形的属性。这里我们用一个简单的对话框来演示。添加新窗体右键项目 - 添加 - 窗体(WinForms)命名为SettingsForm。设计SettingsForm添加几个Label、TextBox用于输入X,Y,边长和Button确定。主窗体Form1代码最终版using System; using System.Drawing; using System.Windows.Forms; namespace DrawSquareDemo { public partial class Form1 : Form { private SquareProperties _squareProps; private Pen _drawingPen; // 用一个类来管理正方形属性 private class SquareProperties { public int X { get; set; } 50; public int Y { get; set; } 50; public int Side { get; set; } 200; public Color BorderColor { get; set; } Color.Blue; public int BorderWidth { get; set; } 3; } public Form1() { InitializeComponent(); _squareProps new SquareProperties(); // 初始化画笔但颜色和宽度后续会变 _drawingPen new Pen(_squareProps.BorderColor, _squareProps.BorderWidth); this.Paint Form1_Paint; btnMove.Click BtnMove_Click; // btnMove是“移动”按钮 btnSettings.Click BtnSettings_Click; // btnSettings是“设置”按钮 } private void Form1_Paint(object sender, PaintEventArgs e) { DrawSquare(e.Graphics, _drawingPen, _squareProps); } private void DrawSquare(Graphics g, Pen pen, SquareProperties props) { // 更新画笔属性如果设置改变了 if (pen.Color ! props.BorderColor || pen.Width ! props.BorderWidth) { pen.Color props.BorderColor; pen.Width props.BorderWidth; } // 绘制边框 g.DrawRectangle(pen, props.X, props.Y, props.Side, props.Side); // 绘制填充 using (var fillBrush new SolidBrush(Color.FromArgb(60, Color.YellowGreen))) { g.FillRectangle(fillBrush, props.X 5, props.Y 5, props.Side - 10, props.Side - 10); } } private void BtnMove_Click(object sender, EventArgs e) { _squareProps.X 15; _squareProps.Y 8; this.Invalidate(); // 触发重绘 } private void BtnSettings_Click(object sender, EventArgs e) { using (var settingsForm new SettingsForm(_squareProps)) { if (settingsForm.ShowDialog(this) DialogResult.OK) { // 设置窗体可能修改了_squareProps因为是引用类型 // 直接使整个区域无效以重绘 this.Invalidate(); } } } protected override void OnFormClosed(FormClosedEventArgs e) { _drawingPen?.Dispose(); base.OnFormClosed(e); } } }设置窗体SettingsForm代码using System; using System.Drawing; using System.Windows.Forms; namespace DrawSquareDemo { public partial class SettingsForm : Form { private Form1.SquareProperties _props; // 通过构造函数传入主窗体的正方形属性对象引用 public SettingsForm(Form1.SquareProperties props) { InitializeComponent(); _props props; // 将当前属性加载到文本框 txtX.Text _props.X.ToString(); txtY.Text _props.Y.ToString(); txtSide.Text _props.Side.ToString(); // 颜色选择可以使用ColorDialog这里简化为文本框输入颜色名 txtColor.Text _props.BorderColor.Name; txtWidth.Text _props.BorderWidth.ToString(); } private void btnOK_Click(object sender, EventArgs e) { // 简单的验证和赋值 if (int.TryParse(txtX.Text, out int x)) _props.X x; if (int.TryParse(txtY.Text, out int y)) _props.Y y; if (int.TryParse(txtSide.Text, out int side) side 0) _props.Side side; if (int.TryParse(txtWidth.Text, out int width) width 0) _props.BorderWidth width; try { // 尝试通过颜色名解析颜色 _props.BorderColor Color.FromName(txtColor.Text); } catch { // 解析失败则保持原色 MessageBox.Show(颜色名称无效将保持原颜色。); } this.DialogResult DialogResult.OK; this.Close(); } private void btnCancel_Click(object sender, EventArgs e) { this.DialogResult DialogResult.Cancel; this.Close(); } } }项目结构说明Form1主窗体负责显示和主导绘逻辑。SquareProperties内嵌类数据模型封装正方形的所有可配置属性。使用类而不是一堆分散的字段让数据管理更清晰也便于传递。SettingsForm设置窗体职责单一只负责修改SquareProperties对象的数据。通过构造函数传入引用使得修改能直接反映到主窗体的数据对象上。事件驱动移动按钮修改数据后调用Invalidate()设置窗体修改数据后也由主窗体调用Invalidate()所有绘制统一在Form1_Paint事件中完成。这是WinForms图形编程的黄金法则。5. 常见问题、调试技巧与性能优化即使是一个简单的绘图程序也会遇到各种问题。下面是我在实际开发中积累的一些常见坑点和解决思路。5.1 图形闪烁问题如果你发现正方形在移动或窗体调整大小时频繁闪烁这是因为Paint事件被频繁触发且每次绘制前Windows会先用背景色清除客户区。解决方法叫做双缓冲。解决方案在主窗体构造函数中设置以下样式或者在窗体属性中设置DoubleBuffered true。public Form1() { InitializeComponent(); this.DoubleBuffered true; // 启用双缓冲 // ... 其他初始化 }原理双缓冲技术会在内存中创建一个“离屏”的绘图表面所有的绘图操作先在这个缓冲区内完成然后一次性将完整的图像“贴”到屏幕控件上。这消除了逐帧绘制带来的闪烁感对于动画或复杂图形至关重要。5.2 绘制的内容不显示或瞬间消失这个问题几乎总是因为绘图代码没有写在正确的事件里。如果你在按钮的Click事件中这样写private void button1_Click(object sender, EventArgs e) { using (Graphics g this.CreateGraphics()) using (Pen p new Pen(Color.Red)) { g.DrawRectangle(p, 10, 10, 50, 50); } }你会发现矩形闪了一下就没了。因为CreateGraphics()获取的Graphics对象是临时的绘制的内容不会被窗体记住。当窗体因任何原因如被其他窗口覆盖后露出触发Paint事件时Windows会用背景色重绘客户区你之前画的东西就没了。黄金法则所有希望持久显示的图形都必须将绘制逻辑放在Paint事件处理程序或由Paint事件调用的方法中。按钮事件只应修改影响绘图的数据然后调用Invalidate()。5.3 坐标计算与边界处理当你动态移动图形时需要处理图形移出窗体边界的情况。可以在修改坐标前加入判断private void BtnMove_Click(object sender, EventArgs e) { int newX _squareProps.X 15; int newY _squareProps.Y 8; // 获取窗体客户区的可用大小 int clientWidth this.ClientSize.Width; int clientHeight this.ClientSize.Height; // 简单判断确保正方形至少有一部分在可视区域内 // 如果完全移出右边界则回到左边 if (newX clientWidth) { newX -_squareProps.Side; // 让正方形从左侧重新进入 } // 如果完全移出下边界则回到上边 if (newY clientHeight) { newY -_squareProps.Side; } _squareProps.X newX; _squareProps.Y newY; this.Invalidate(); }5.4 性能考量与资源管理避免在Paint事件中创建Pen/Brush/FontPaint事件可能被高频触发。反复创建和销毁这些GDI对象会给垃圾回收器带来压力并可能耗尽GDI句柄。最佳实践是在类级别声明这些对象作为字段在构造函数或加载事件中初始化并在窗体关闭时销毁。如果颜色等属性需要变化直接修改现有对象的属性如pen.Color newColor。使用using语句或确保Dispose对于在方法内部临时使用的IDisposable对象如上面例子中DrawSquare方法内的SolidBrush务必使用using语句。对于窗体生命周期内一直使用的对象如主画笔_drawingPen在窗体的Dispose方法或FormClosed/OnFormClosed事件中手动调用Dispose()。局部刷新如果只有一小部分区域需要重绘可以使用带参数的Invalidate(Rectangle)方法只标记特定矩形区域为无效然后在Paint事件中通过e.ClipRectangle获取需要重绘的区域进行优化绘制。这对于复杂图形界面能显著提升性能。5.5 调试绘图代码绘图问题有时很视觉化调试器帮不上忙。可以尝试以下方法在绘制代码中添加调试输出使用Debug.WriteLine输出当前的坐标、颜色等参数值确保逻辑正确。绘制参考点或辅助线在Paint事件中临时添加绘制代码比如在原点(0,0)画一个红点或在你认为的边界处画线帮助自己理解坐标系。使用Control.Invalidate(Rectangle)配合Paint中的e.ClipRectangle观察重绘区域是否如你预期。6. 扩展思路从正方形到图形世界掌握了绘制一个正方形你就打开了C# 2D图形编程的大门。以此为基点你可以轻松地扩展出更多功能绘制其他基本形状Graphics对象还提供了DrawEllipse椭圆/圆、DrawLine线、DrawPolygon多边形、DrawPie扇形等方法。原理相通都是指定位置和大小参数。实现拖拽交互通过处理鼠标的MouseDown、MouseMove、MouseUp事件记录鼠标按下时的位置和图形位置在移动时计算偏移量并更新图形坐标然后Invalidate()就能实现用鼠标拖拽正方形。集成到自定义控件你可以创建一个继承自Control或UserControl的类重写其OnPaint方法将绘图逻辑封装进去。这样你就得到了一个可以随处拖放使用的“正方形显示控件”。保存图形到文件创建一个Bitmap对象获取其Graphics对象进行绘制然后调用bitmap.Save(“filename.png”, ImageFormat.Png)即可将绘制的图形保存为图片文件。动画效果使用Timer组件在Tick事件中连续修改图形属性如位置、颜色、大小并调用Invalidate()就能创造出简单的动画效果。绘制一个正方形远不止是调用一个API。它涉及事件驱动模型、资源管理、坐标系、双缓冲、绘制与更新分离等一系列核心概念。把这些基础打牢未来无论是开发数据可视化图表、简单的游戏、还是自定义风格的UI组件你都会发现游刃有余。代码本身并不复杂但理解其背后的运行机制和最佳实践才是从“能运行”到“写得好”的关键跨越。