GDI+ 是一种通用的面向对象的 .NET 应用程序绘图模型。GDI+ 在 .NET 里有很多用途,包括向打印机输出文档、在一个 Windows 应用程序里显示图形、以及在网页里呈现图形。
你可以创建采用了用户指定信息的富图形,也可以基于数据库记录动态呈现图表或图形。
GDI+ 编程的核心是 System.Drawing.Graphics 类。它封装了一个 GDI+ 绘图表面,它可能是一个窗口、一个打印文档或者一个内存里的位图。ASP.NET 人员很少需要绘制窗口或者打印文档,因此只有最后一个选项是最实际的。
要在 ASP.NET 里使用 GDI+,需要遵循如下 4 个步骤:
- 创建一个内存里的位图,在其上你将完成所有绘制工作。
- 为图像创建一个 GDI+ 图形上下文。这令你获得你需要的 System.Drawing.Graphics 实例。
- 使用 Graphics 的实例方法完成绘制。你可以绘制和填充线条和形状,你甚至可以从现有文件中复制位图内容。
- 使用 Response.OutputStream 属性输出图像到浏览器。
之后,会介绍一些例子,确保以下命名空间被引入:
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
简单绘制
下例展示了最简单的 GDI+ 页面,所有的工作都在 Load 事件里执行。
先创建 System.Drawing.Bitmap 类的一个实例来创建一个内存中的位图,需要指定图像的高度和宽度,单位为像素。应尽可能的小,较大的位图会消耗更多服务器内存,会减慢传输时间。
// Create the in-memory bitmap where you will draw the image.
// This bitmap is 300 pixel wide and 50 pixel high.
Bitmap image = new Bitmap(300, 50);
接着为图像创建 GDI+ 绘图的上下文,由 System.Drawing.Graphics 对象表示。该对象提供方法让你在内存里的位图上绘制内容。只需使用静态方法 Graphics.FromImage()即可。
Graphics g = Graphics.FromImage(image);
下面是很有意思的的部分。使用 Graphics 类的方法,可以绘制文本,形状和位图上的图像。本例中用实的白色背景填充一个矩形(新位图中的每个像素最初被设置为黑色)。
// Draw a solid white rectangle.
// Start from point(1,1).
// Make it 298 pixels wide and 48 pixels high.
g.FillRectangle(Brushes.White, 1, 1, 298, 48);
下一部分使用 System.Drawing.Font 对象来呈现一个静态的标签信息。不要将它与 ASP.NET 控件里用来指定网页中所需字体的 FontInfo 对象混淆。因为图像是在服务器上生成的,所有创建图形时可以使用服务器上安装好的任何字体,而客户端并不需要这些字体,客户端收到的是文本呈现后的图像。
Font font = new Font("Impact", 20, FontStyle.Regular);
将文本绘制到位图上,(10,5)指定文本位于位图上左上角的坐标:
g.DrawString("This is a test", font, Brushes.Blue, 10, 5);
一旦图像完成,就可以使用 Image.Save()将其发送到浏览器(浏览器的响应流)。然后,它就被发送到客户端,并且在客户端的浏览器里显示。当类似这样的响应流直接写入数据时,你的图像替代了任何其他网页数据并且跳过了 Web 控件模型!
// Render the image to the output stream.
image.Save(Response.OutputStream, ImageFormat.Gif);
注意,可以保存图像到任意一个有效的流,包括一个 FileStream。这使你能保存动态生成的图像到磁盘。
完成后,应该立即调用图像和绘图上下文的 Dispose() 方法,因为两者都占用了一些不会立即被释放的非托管资源:
g.Dispose();
image.Dispose();
图像格式和质量
当保存图像时,你可以选择想要使用的图像格式:
- JPEG:提供最好的色彩支持和绘图,但可能会失去细节信息的压缩,并会令文本看起来失真。
- GIF:是包含文本的图形的更好选项,但对色彩的支持不好。它只使用 256 色固定的调色板。
- PNG:结合了 JPEG 和 GIF 两种格式的优点,但不能直接用在网页,而需要将其包装在 <img> 标签中。
质量不仅仅由图像格式决定,还取决于你呈现原始位图的方式。 GDI+ 允许你选择外观或是速度优化绘图代码。当选择最佳外观优化时,.NET 使用额外的呈现技术提高绘画质量。例如,反锯齿。
反锯齿平滑了形状和文本的锯齿边沿。其原理是在边缘的边界上添加阴影。为了使用平滑技术,可以设置 Graphics 对象的 SmoothingMode 属性。你可以选择 None(默认值)、HighSpeed、AntiAlias 和 HighQuality(和 AntiAlias 相似,但使用更慢的优化算法改进在液晶屏上的显示效果)。
SmoothingMode 属性是 Graphics 类成员的几个有状态的属性之一。也就是说,你要在开始绘制之前设置其值,效果会应用到后面绘制过程中的所有文本和形状,直到 Graphics 对象被释放。
g.SmoothingMode = SmoothingMode.AntiAlias;
反锯齿技术在显示曲线时效果最明显,将极大地提升椭圆、圆和弧线的显示效果,但不会对直线、正方形、矩形有什么改进。
还可以对字体使用反锯齿技术来柔滑文本的边界。可以设置 Graphics.TextRenderingHint 属性得到优化后的文本:
- SingleBitPerPixelGridFit:性能最快,质量最低。
- AntiAlias:进行平滑处理,质量比较好。
- AntiAliasGridFit:进行平滑处理和微调,质量较好但性能较慢。
- ClearTypeGridFit:液晶屏上显示质量最好。
也可以使用 SystemDefault 值来使用用户配置的字体平滑设置,这是默认设置,对于大多数计算机的默认系统设置,都启用了文本反锯齿。即使没有设置这点,动态呈现的文本也通常以高质量绘制。然而,因为不一定能控制 Web 服务器的系统设置,所以如果需要在图像上绘制文本,较好的做法是显式地指定这些设置。
Graphics 类
Graphics 类提供了大量绘制特别形状、图像、文本的方法。
DrawArc() | 指定一个坐标、高度、宽度绘制椭圆的一段弧线 |
DrawBezier() DrawBeziers() | 绘制著名的贝塞尔曲线,需要由 4 个控制点定义 |
DrawClosedCurve() | 绘制一条曲线,并链接到端点封闭它 |
DrawCurve() | 绘制一条曲线,从技术上讲,是基数样条曲线 |
DrawEllipse() | 绘制一个由一对坐标,高,宽来定义的矩形限制范围内的椭圆 |
DrawIcon() DrawIconUnstreched() | 绘制 Icon 对象表示的图标,并且(可选的)可伸展以满足一个给定矩形 |
DrawImage() | 绘制一个派生自 Image 的对象并延展以适应矩形区域。(如文件加载的 Bitmap 对象) |
DrawImageUnscaled() DrawImageUnscaledAndClipped() |
绘制一个派生自 Image 的对象,不放大并裁剪它以适应指定的矩形区域 |
DrawLine() DrawLines() | 绘制一条或多条线段,每条线段连接由坐标对指定的两个点 |
DrawPath() | 绘制一个 GraphicsPath 对象,该对象可以代表曲线和形状的一个组合 |
DrawPie() | 绘制一个扇形,由一个坐标对、宽度、高度、两条射线所指定的椭圆定义 |
DrawPolygon() | 绘制由点的数组定义的多边形 |
DrawRectangle() DrawRectangles() | 绘制一个或多个由起始点坐标、宽、高指定的矩形 |
DrawString() | 绘制给定字体的文本字符串 |
FillClosedCurve() | 绘制一条首尾连接的封闭曲线,并填充 |
FillEllipse() | 填充椭圆 |
FillPath() | 填充由一个 GraphicsPath 对象表示的形状的内部 |
FillPie() | 填充扇形 |
FillPolygon() | 填充多边形 |
FillRectangle() FillRectangles() | 填充一个或多个矩形 |
DrawXXX()方法绘制轮廓,FillXXX()方法填充区域。唯一例外的是 DrawString()方法(它使用指定的字体绘制填充的文本)以及 DrawIcon()和 DrawImage(),它们复制位图图像到绘制表面。
如果想通过将边界用一种颜色而将填充用另一种颜色来绘制一个图形,就需要组合一个绘制方法和一个填充方法:
g.FillRectangle(Brushes.White, 0, 0, 300, 50);
g.DrawRectangle(Pens.Green, 0, 0, 299, 49);
如果指定了不在绘图区域内的坐标,并不会得到异常。但是,绘制在边界之外的内容不会出现在最后的图像上。
使用已学到的技巧,很容易创建一个用于绘制复杂 GDI+ 图像的简单网页。下面这个例子使用 Graphics 类来绘制椭圆、文本消息、来自文件的图像:
protected void Page_Load(object sender, EventArgs e)
{
Bitmap image = new Bitmap(450, 100);
Graphics g = Graphics.FromImage(image);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillRectangle(Brushes.White, 0, 0, 450, 100);
g.FillEllipse(Brushes.PaleGoldenrod, 120, 13, 300, 50);
g.DrawEllipse(Pens.Green, 120, 13, 299, 49);
Font font = new Font("Harrington", 20, FontStyle.Bold);
g.DrawString("Oranges are tasty!", font, Brushes.DarkOrange, 150, 20);
System.Drawing.Image orangeImage =
System.Drawing.Image.FromFile(Server.MapPath("oranges.gif"));
g.DrawImageUnscaled(orangeImage, 0, 0);
Response.ContentType = "image/jpeg";
image.Save(Response.OutputStream, ImageFormat.Jpeg);
g.Dispose();
image.Dispose();
}
使用 GraphicsPath
有两个有趣的方法,DrawPath()和 FillPath(),它们用于 System.Drawing.Drawing2D 命名空间中的 GraphicsPath 类。这个类包装了一系列连接线、曲线和文本。
GraphicsPath path = new GraphicsPath();
path.AddEllipse(0, 0, 100, 50);
path.AddRectangle(new Rectangle(100, 50, 100, 50));
g.DrawPath(Pens.Black, path);
g.FillPath(Brushes.Yellow, path);
GraphicsPath 方法
AddXXXX…() | 绘制一系列的各种基本形状:椭圆,矩形,曲线等等 |
AddString() | 以给定字体绘制文本字符串 |
StartFigure() CloseFigure() | 前者定义一个封闭图形的起始点,使用后者时,起始点将被一条额外的线条连接到终点 |
Transform() Warp() Widen() | 分别使用:矩阵变形、弯曲变形(矩形和平行四边形定义)、膨胀变形 |
画笔
当使用 Graphics 类的 DrawXxxx()方法时,图形的边界或者曲线是使用你提供的 Pen 对象绘制的。使用 System.Drawing.Pens 类的静态属性获得一个标准画笔,宽度都是 1 像素,只是颜色不同。例如下面:
Pen myPen = Pens.Black;
也可以创建一个自定义的 Pen 对象,配置下表列出的所有属性即可:
DashPattern | 使用短划线和空格数组定义虚线样式 |
DashStyle | 使用枚举定义虚线样式 |
LineJoin | 定义了一个图形中连接的线段如何连接 |
PenType | 用于直线的填充类型。典型值是 SolidColor (纯色),但也可使用渐变、位图纹理或者当你创建画笔时提供一个画刷对象来创建一个阴影模式。 |
StartCap 、EndCap | 决定线段的起始点和终点如何呈现 |
Width | 当前画笔画的线的像素宽度 |
理解 LineCap 和 DashStyle 属性最简单的办法是创建一个枚举所有选项的测试页面:
protected void Page_Load(object sender, EventArgs e)
{
int y = 60;
Bitmap image = new Bitmap(500, 400);
Graphics g = Graphics.FromImage(image);
g.FillRectangle(Brushes.White, 0, 0, 500, 400);
Pen myPen = new Pen(Color.Blue,10);
g.DrawString("LineCap Choices",
new Font("Tahoma", 15, FontStyle.Bold), Brushes.Blue, 0, 10);
foreach (LineCap cap in System.Enum.GetValues(typeof(LineCap)))
{
myPen.StartCap = cap;
myPen.EndCap = cap;
g.DrawLine(myPen, 20, y, 100, y);
g.DrawString(cap.ToString(), new Font("Tahoma", 8), Brushes.Black, 120, y - 10);
y += 30;
}
y = 60;
g.DrawString("DashStyle Choices",
new Font("Tahoma", 15, FontStyle.Bold), Brushes.Blue, 200, 10);
foreach (DashStyle dash in System.Enum.GetValues(typeof(DashStyle)))
{
myPen.DashStyle = dash;
g.DrawLine(myPen, 220, y, 300, y);
g.DrawString(dash.ToString(), new Font("Tahoma", 8), Brushes.Black, 320, y - 10);
y += 30;
}
Response.ContentType = "image/jpeg";
image.Save(Response.OutputStream, ImageFormat.Gif);
g.Dispose();
image.Dispose();
}
画刷
画刷用于填充线之间的空间。当绘制文本或者使用 Graphics 类的任何 FillXxxx()方法来绘制形状的内部时,会用到画刷。使用 Brushes 类的静态属性获取一个预定义的实心画刷:
Brush myBrush = Brushes.White;
也可以创建自定义画刷。实心画刷创建自类 SolidBrush;而其他类允许创建更复杂的画刷:
- HatchBrush:有前景色、背景色和一个决定这些颜色如何组合的阴影样式。通常,色彩通过使用条纹、网格或者点来点缀。但你仍可以选择不常见的模式,如 brick(砖块)、confetti(五彩)、weave(编织)、shingle(鹅卵石)。
- LinearGradientBrush:以一种渐变模式混合两种颜色。你可以选择任意两种色彩(就像 hatch 画刷那样),然后选择水平混合、垂直混合、对角线方向混合,还可以为渐变的两边指定起点。
- TextureBrush:给画刷添加位图。这个图像在画刷的着色区域被分成区块,不管这个区域是文本还是简单的矩形。
这里给出测试 LinearGradientBrush 所有样式的绘制逻辑的示例代码(其余的有兴趣的童鞋可以自己测试):
protected void Page_Load(object sender, EventArgs e)
{
Bitmap image = new Bitmap(300, 300);
Graphics g = Graphics.FromImage(image);
g.FillRectangle(Brushes.White, 0, 0, 300, 300);
LinearGradientBrush myBrush;
int y = 20;
foreach (LinearGradientMode gradientStyle in
System.Enum.GetValues(typeof(LinearGradientMode)))
{
myBrush = new LinearGradientBrush(new Rectangle(20, y, 100, 60),
Color.Violet, Color.White, gradientStyle);
g.FillRectangle(myBrush, 20, y, 100, 60);
g.DrawString(gradientStyle.ToString(), new Font("Tahoma", 8),
Brushes.Black, 130, y + 20);
y += 70;
}
Response.ContentType = "image/jpeg";
image.Save(Response.OutputStream, ImageFormat.Jpeg);
g.Dispose();
image.Dispose();
}
提示:
你还可以利用这个技术创建使用画刷的填充样式进行绘制的画笔。这使你可以绘制用渐变色彩和纹理填充的线。首先创建一个合适的画刷,然后创建一个新的画笔,其中某个被重载的画笔的构造函数接受对一个画刷的引用。