第5章

图形图像编
程


对于传统的程序设计语言来说,图形图像方向的程序设计是相对复杂和困难的。
Microsoft图形设备接口(GraphicsDeviceInterface,GDI)解决方案提供了丰富的图形功
能,方便了图形应用程序的开发。NET 中使用了其升级版本GDI+,为便于充分利用
Windows图形库,提供了一个新的接(.) 口。GDI+ 以继承类的方式来完成图形处理,是一个
完全面向对象的2D 图形系统。GDI+ 中的“+”表示相对于GDI 来说有了很大的改进, 
允许编写与设备无关的图形应用程序,如游戏、计算机辅助设计和绘图程序等。

本章将介绍如何在.

NET 中建立基于C# 的GDI+ 图形应用程序。

1 
GDI+ 
绘图基础

5.
5.1 
GDI+ 
概述
1.
图形设备接口(GDI)是一个可执行程序,它接受Windows应用程序的绘图请求(表
现为GDI 的方法调用), 并且将它们传给相应的设备驱动程序。GDI+ 是对图形设备接
口的一个扩展,它所提供的类可用于创建二维矢量图形、操纵字体以及插入图像。

1.GDI+ 
图形输出类型
使用GDI+ 可以创建3种类型的图形输出:矢量图形输出、光栅图形输出和文本
输出。

①矢量图形输出。矢量图形输出指的是创建线条和填充图形,包括点、线、多边形、
扇形和矩形的绘制。
②光栅图形输出。光栅图形输出是指用光栅图形方法对以位图形式存储的数据进
行操作。在屏幕上表现为对若干行和列的像素的操作,它是直接从内存到显存的复制操
作,在打印机上则是若干行和列的点阵的输出。Windows在绘制界面时使用了大量的光
栅输出。
③文本输出。Windows是按图形方式输出文本。用户可以通过调用各种GDI+ 方
法,创造出各种文本输出效果,包括加粗、斜体、设置颜色等。

第5 章 图形图像编程2 47 
程序员使用GDI+时不需要考虑GDI+内部是如何实现的,直接使用其提供的类进
行编程即可。GDI+在System.Drawing.dll动态链接库中定义,与其相关的名称空间如
表5-1所示,其中最常用的名称空间是System.Drawing,主要有Graphics、Pen、Brush、
Image、Bitmap等类。
表5-1 GDI+相关名称空间
命名空间功 能
System.Drawing 提供对GDI+基本图形功能的访问
System.Drawing.Drawing2D 提供高级的二维和矢量图形功能
System.Drawing.Imaging 提供高级GDI+图像处理功能
System.Drawing.Text 允许用户创建和使用多种字体
2.编程步骤
通过GDI+进行图形图像编程的一般步骤为:构造画布、建立绘图对象、调用绘图方
法以及释放绘图对象。
(1)构造画布
绘图前,先要准备好“画布”,可以使用Graphics类创建画布对象。通常情况下,直接
在窗体上或在图像框中绘图,可以在相应的控件对象上构造画布。例如: 
Graphics g = this.CreateGraphics(); //以窗体为画布
表示在窗体上构造画布。又如: 
Graphics g = pictureBox1.CreateGraphics(); //以图像框为画布
表示在图像框中构造画布。
(2)建立绘图对象
有了“画布”后,还需要准备“画笔”和“画刷”。通常情况下,画笔通过Pen类创建,可
以用来绘制直线、曲线或闭合图形的外轮廓;画刷通过Brush类创建,可以用来填充闭合
图形内部或输出文字。
① 建立画笔时,可以定义画笔的颜色和粗细,详见5.2.1节的介绍。例如: 
Pen p = new Pen(Color.Blue, 2); //构造一支线宽为2 的蓝色画笔
② 建立画刷时,需要根据绘图需求定义不同的画刷类对象(有单色刷、网格刷、纹理
刷和渐变刷等多种),详见5.2.2节的介绍。例如: 
SolidBrush sb = new SolidBrush(Color.Pink); //构造一支粉红色的单色刷
③ 若要利用画刷输出文字,还需要创建字体对象,声明要输出文字的字体、字号和格

2 48 C#程序设计教程
式等,详见1.3.1节的介绍。例如: 
Font f = new Font("宋体", 10, FontStyle.Bold); //创建字体对象
(3)调用绘图方法
绘图工具(画布、画笔、画刷等)准备齐全后,就可以开始画图。
① 要绘制直线、曲线或闭合图形的外轮廓,使用Graphics类中以Draw 开头的方法
并结合画笔对象实现,详见5.2.1节的介绍。例如: 
g.DrawEllipse(p, 10, 10, 100, 60); //用画笔p 绘制椭圆
表示在左上角坐标为(10,10)、宽和高分别为100和60的矩形中,绘制其内切椭圆外
轮廓。②
要填充闭合图形,使用Graphics类中以Fill开头的方法并结合画刷对象实现,详
见5.2.2节的介绍。例如: 
g.FillEllipse(sb, 10, 10, 100, 60); //用画刷sb 填充椭圆
表示在左上角坐标为(10,10)、宽和高分别为100和60的矩形中,填充其内切椭圆。
③ 要输出文本,使用Graphics类的DrawString()方法,并且结合字体和画刷对象实
现,详见5.2.3节的介绍。例如: 
g.DrawString("GDI+绘图", f, sb, 50, 35); //用画刷sb 输出字体为f 的文字
表示在坐标为(50,35)的位置输出文字“GDI+绘图”,文字颜色和字体样式等根据画刷sb 
和字体f指定。
(4)释放绘图对象
图形绘制完成后,需要调用绘图对象的Dispose()方法,释放内存资源,提高程序运行
效率。例如: 
p.Dispose(); //释放画笔对象
sb.Dispose(); //释放画刷对象
f.Dispose(); //释放字体对象
g.Dispose() //释放画布对象
可以用一个较为形象的方法去理解.NET中的图形绘制。“画布”就是手工绘图时用
到的“纸”,“画笔”在绘图时可以用来画线条、勾轮廓,“画刷”在绘图时用来填充颜色、
写字。想
一想,为什么文字的输出用的是画刷而不是画笔? 因为在输出文字时,可以指定
不同的字体,尤其是输出汉字时,可以是宋体、楷体、隶书等不同的形体。为完成这些
形体的书写,在实际生活中,我们会选择使用毛笔,而这里用到的“画刷”是不是更像一
支毛笔呢?

第5 章 图形图像编程2 49 
例5.1 在窗体上绘制椭圆并填充,同时输出一行文字,程序运行界面如图5-1所示。
图5-1 例5.1程序运行界面
分析: 
① 按钮单击时触发绘图程序,并且使用Graphics类在
窗体上创建画布。
② 需要声明一个画笔对象来绘制椭圆外轮廓,声明两
个画刷对象分别用来填充椭圆和输出文字,声明一个字体
对象来指定输出文字的字体信息。
③ 绘制椭圆外轮廓用DrawEllipse()方法,填充椭圆
用FillEllipse()方法,输出文字用DrawString()方法。
④ 图形绘制完成后记得释放各种绘图对象,节省内存空间。
程序源代码如下。 
private void button1_Click(object sender, EventArgs e) 
{ 
//在窗体上构建画布 
Graphics g= this.CreateGraphics(); 
//建立画笔对象,调用绘图方法绘制图形 
Pen p = new Pen(Color.Blue, 5); 
g.DrawEllipse(p, 10, 10, 150, 90); 
//建立画刷对象,调用绘图方法填充图形 
SolidBrush sb1 = new SolidBrush(Color.Yellow); 
g.FillEllipse(sb1, 10, 10, 150, 90); 
//建立画刷和字体工具,调用DrawString()方法绘制文字 
SolidBrush sb2 = new SolidBrush(Color.Red); 
Font f = new Font("楷体", 15, FontStyle.Bold); 
g.DrawString("GDI+绘图", f, sb2, 120, 50); 
//调用各绘图对象的Dispose()方法释放系统资源 
p.Dispose(); 
sb1.Dispose(); 
sb2.Dispose (); 
f.Dispose(); 
g.Dispose(); 
} 
5.1.2 坐标系
2D图形的绘制需要一个可绘图的对象,如窗体、图形框。为了能定位图形,需要一个
二维坐标系。在GDI+中,对象坐标系以像素为单位。像素是指屏幕上的亮点,即显示器

能分辨的最小单元,每个像素都有一个坐标点与之
对应,如图5-2所示。默认的坐标原点为对象的左
上角,横向向右为X轴的正向,纵向向下为Y轴的
正向。例如,如果在窗体上绘画,那么默认坐标原点
就在它的左上角(标题栏的左下方)。

GDI+ 默认的坐标系统与数学中的坐标系统并
不一样,在绘制数学函数y=f(x)的图形时,要使所
画的图产生与数学坐标系相同的效果,需要在默认
坐标的基础上进行坐标变换,如旋转、平移等。
Graphics对象提供了坐标变换方法,常用的坐标变换方法如表5-2所示。

表5-
2 
Graphics对象常用的坐标变换方法

图5-2 屏幕像素与坐标
方法名功能使用范例
TranslateTransform 平移TranslateTransform(40,30) 将(40,30)设为原点
RotateTransform 旋转RotateTransform(-30) 将坐标系逆时针方向旋转30° 
ScaleTransform 缩放ScaleTransform(2,3) 按目前的宽的2倍和高的3倍放大
ResetTransform 还原ResetTransform() 还原为默认坐标

例如,坐标系经过平移→旋转→缩放后的效果如图5-3所示。其变换参数如下。


图5-3 坐标变换后的效果

250
C# 
程序设计教程


第5 章 图形图像编程2 51 
① 坐标原点平移至(40,30)。
② 坐标系顺时针方向旋转15°。
③ 坐标系水平放大20%,垂直缩小20%。
注意,每次坐标变换后,再次变换坐标是在前一次变换的基础上进行的。图5-3中的
矩形左上角坐标始终位于(25,30),宽和高分别为100和60。
例5.2 将例5.1中绘制的椭圆和文字经过如图5-3所示的坐标变换后,绘制在窗体
图5-4 例5.2程序运行界面
上,程序运行界面如图5-4所示。
分析: 
① 按钮单击时触发绘图程序,并且使用Graphics 
类在窗体上创建画布。
② 使用如表5-2所示的Graphics对象常用的坐
标变换方法,实现坐标系的平移、旋转和缩放。
③ 在窗体上建立画笔、画刷和字体对象,用相应
的绘图方法绘制椭圆和文字。
程序源代码如下。 
private void button1_Click(object sender, EventArgs e) 
{ 
Graphics g = this.CreateGraphics(); // 构建画布 
g.TranslateTransform(40, 30); //坐标平移,原点平移至(40,30) 
g.RotateTransform(15); //坐标旋转,顺时针方向旋转15° 
g.ScaleTransform(1.2f,0.8f); //坐标缩放,水平1.2 倍、垂直0.8 倍 
... //省略图形绘制源代码,详见例5.1 
}
注意,用于坐标平移、旋转和缩放的TranslateTransform()、RotateTransform()和
ScaleTransform()函数,其参数应为float数据类型。在调用这些方法时,若输入的实参
为整数,则能自动转换成float类型;若输入的实参为小数,程序会默认视其为double数
据类型,此时需要人工地将数据转换成float类型。例如,本例中有一个语句。 
g.ScaleTransform(1.2f,0.8f); 
通过在小数常量后加上类型符f,实现了从double到float的数据类型转换。
5.1.3 Graphics类
在图形图像应用程序开发过程中,最常用到的类有Graphics、Pen、Brush和Font,如
表5-3所示。这些类被包含在System.Drawing名称空间中。当建立Windows窗体应用
程序后,系统会默认地将该名称空间通过using关键字引用进来。 
using System.Drawing;

2 52 C#程序设计教程
表5-3 GDI+常用类
类 名功 能
Graphics类包含完成绘图的基本方法,如直线、曲线、矩形等
Pen类处理图形的轮廓部分
Brush类对图形进行填充处理
Font类字体功能,如字体样式、旋转等 
用GDI+绘图时,必须先创建Graphics类的画布对象实例。只有创建了Graphics的
实例后,才可以调用Graphics类的绘图方法。窗体和所有具有Text属性的控件都可以
构成画布。创建Graphics画布对象有以下几种方法。
(1)使用CreateGraphics()方法
通过窗体或控件的CreateGraphics()方法来获取对Graphics对象的引用,需要先定
义一个Graphics类的对象,再调用CreateGraphics()方法。这种方法一般应用于对象已
经存在的情况,语法格式如下。 
Graphics 画布对象; 
画布对象= 窗体或控件名.CreateGraphics(); 
上述语句也可以合成一句。 
Graphics 画布对象= 窗体或控件名.CreateGraphics(); 
(2)利用PaintEventArgs参数传递Graphics对象
窗体或控件的Paint事件可以直接完成图形绘制。在编写Paint事件处理程序时,参
数PaintEventArgs提供了图形对象,格式如下。 
private void Form1_Paint(object sender, PaintEventArgs e) 
{ 
Graphics g = e.Graphics; //声明对象g 并获取对Graphics 对象的引用 
... //在画布g 上绘制图形
}
将绘图程序放在Paint事件处理程序中,该事件在控件需要重新绘制时触发。在
Paint事件结束时,会自动释放Graphics对象所占用的系统资源。因此,不必特地使用
Dispose()方法来释放绘图资源。
(3)使用Graphics.FromImage()方法从Image对象创建
该方法适用于需要处理已经存在的图像的场合。例如,利用图像文件mypic.bmp创
建Graphics对象。

第5 章 图形图像编程2 53 
Bitmap b = new Bitmap(@"c:\mypic.bmp"); // 根 据图像文件声明Bitmap 对象b 
Graphics g = Graphics.FromImage(b); // 将 图像b 作为画布对象g 
... //在画布g 上进行绘制,相当于修改图像b 
注意:在C#中,为了写文件路径时不加转义符“\”,可以在路径前面加上@标识符。
例如,在以上代码中,通过用@"c:\mypic.bmp"代替路径字符串"c:\\mypic.bmp",就能
忽略转义字符。
此外,由于Graphics类的构造函数是私有的,因此不能直接实例化,即不能使用类似
下面的语句来创建Graphics类的一个实例。 
Graphics g = new Graphics(); 
绘图时,还会经常用到一些方法,如清理画布、刷新绘图控件、释放绘图对象等,这些
方法如表5-4所示。
表5-4 绘图时的常用方法
方法说 明
Clear() 
功能:清理画布对象
格式:画布对象.Clear(颜色) 
范例1:g.Clear(Color.Pink); //将画布对象g清理为粉色
范例2:g.Clear(this.BackColor); //将画布对象g清理为绘图控件(窗体对象)的原底色
Refresh() 
功能:刷新绘图控件
格式:对象.Refresh() 
范例:pictureBox1.Refresh(); //刷新绘图控件pictureBox1 
Dispose() 
功能:释放绘图对象
格式:绘图对象.Dispose 
范例1:g.Dispose(); //释放画布g 范例2:sb.Dispose(); //释放画刷sb 
例5.3 编写一个简单的图像编辑程序,要求将图像文件fruit.jpg载入窗体后,以该
图像作为画布,对其进行适当修改(即圈出水果篮中的葡萄),并且能对编辑后的图像进行
保存。程序运行界面如图5-5所示。
分析: 
① 窗体载入时,通过Bitmap类创建图像对象,然后将其装入窗体背景图,调整窗体
大小,使之与图像大小一致。
② 为实现图像的简单编辑,需要首先将图像文件作为画布,然后在画布上对该图像
进行修改。可以使用上述第3种创建画布对象的方法,即使用Graphics.FromImage()方
法从Image对象创建;然后再声明绘图工具,进行图形绘制等后续工作。
③ 为实现图像文件的保存,需要建立一个“保存文件”对话框,获取保存路径;并且使
用Bitmap类的Save()方法来实现保存。格式如下。 
图像对象.Save("保存路径");

2 54 C#程序设计教程
图5-5 例5.3程序运行界面
程序源代码如下。 
Bitmap b; 
private void Form1_Load(object sender, EventArgs e) 
{ 
b = new Bitmap("fruit.jpg"); //建立图像对象b 
this.BackgroundImage = b; //图像装入窗体背景 
this.ClientSize = b.Size; //调整窗体大小
}
private void button1_Click(object sender, EventArgs e) 
{ 
Graphics g = Graphics.FromImage(b); //在图像对象b 上创建画布g 
Pen p = new Pen(Color.Red, 5); //创建画笔p 
g.DrawEllipse(p, 250, 150, 140, 80); //绘图,圈出葡萄 
this.Refresh(); //刷新窗体,更新修改 
p.Dispose(); 
g.Dispose(); 
}
private void button2_Click(object sender, EventArgs e) 
{ 
saveFileDialog1.FileName = "fruit"; 
saveFileDialog1.Filter = "JPEG 图像|*.jpg|PNG 图像|*.png|BMP 图像|*.bmp"; 
if (saveFileDialog1.ShowDialog() == DialogResult.OK) b.Save 
(saveFileDialog1.FileName); //保存图像
}

第5 章 图形图像编程2 55 
private void button3_Click(object sender, EventArgs e) 
{ 
Form1_Load (sender,e); 
}
思考:在以上程序源代码中,语句this.Refresh();起到了什么作用? 是否能将其
去除?
由于原始图像在编辑前已经装入窗体显示,然后通过画布g对图像的编辑实际上是
对内存中创建的Bitmap对象b的修改。当修改完成后,为了使窗体上的显示也得到同步
的更新,需要使用语句this.Refresh();刷新窗体,同步显示图像的更新效果,因此该语句
不能去除。
注意:本程序在调试时,若选择的另存位置和图像载入的路径相同,则无法覆盖保
存,原因和解决方法可以参考5.3.4节的介绍及例5.11中的程序源代码。
5.1.4 GDI+中常用的数据类型
进行图形图像编程时,需要使用相关基础类型与结构类型来表示位置、大小、点、矩形
等,表5-5列出了常用的数据类型。
表5-5 常用数据类型
类型名说 明
Point 
功能:表示一个二维坐标点(X,Y) 
声明方法:Pointpt= newPoint(X,Y) //X、Y为int型
范例:Pointpt= newPoint(10,20); //定义坐标pt为(10,20)的点
PointF 与Point相似,坐标点X、Y为float型
Size 
功能:用W(宽度)和H(高度)两个属性来表示尺寸大小
声明方法:Sizes= newSize(W,H); //W、H 为int型
范例:Sizes= newSize(30,50); //定义宽为30和高为50的尺寸大小
SizeF 与Size相似,W 和H 均为float型
Rectangle 
功能:定义一个矩形区域,以(X,Y)或PT为左上角坐标,以W 为宽和H 为高或者以SZ 
为大小
声明方法1:Rectanglerect=newRectangle(X,Y,W,H) //X、Y、W、H 均为int型
声明方法2:Rectanglerect= newRectangle(PT,SZ) //PT为Point型,SZ为Size型
范例1:Rectanglerect= newRectangle(20,30,10,15); 
//创建一个左上角坐标为(20,30)、宽度为10、高度为15的矩形区域rect 
范例2:Rectanglerect= newRectangle(PT,SZ);//PT和SZ,前已声明,表示位置和大小
//创建一个以PT为左上角坐标、SZ为大小的矩形区域rect 
RectangleF 与Rectangle相似,X、Y、W、H 均为float型,或者PT为PointF型、SZ为SizeF型