第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型