GDI在Windows中定义为Graphics Device Interface,即图形设备接口,是Windows API(Application Programming Interface)的一个重要组成部分。它是Windows图形显示程序与实际物理设备之间的桥梁,GDI使得用户无需关心具体设备的细节,而只需在一个虚拟的环境(即逻辑设备)中进行操作。它的桥梁作用体现在:
(1)用户通过调用GDI函数将逻辑空间的操作转化为具体针对设备驱动程序的调用。
为实现图形设备无关性,Windows 的绘图操作在一个设备描述表上进行。用户拥有自己的"逻辑坐标"系统,它独立于实际的物理设备,与"设备坐标"相对应。开发Windows应用程序时,程序员关心的是逻辑坐标,我们在逻辑坐标系上绘图,利用GDI将逻辑窗口映射到物理设备上。
(2)GDI能检测具体设备的能力,并依据具体的设备以最优方式驱动这些设备,完成真实的显示。
GDI函数大致可分类为:设备上下文函数(如GetDC、CreateDC、DeleteDC)、 画线函数(如LineTo、Polyline、Arc)、填充画图函数(如Ellipse、FillRect、Pie)、画图属性函数(如SetBkColor、SetBkMode、SetTextColor)、文本、字体函数(如TextOut、GetFontData)、位图函数(如SetPixel、BitBlt、StretchBlt)、坐标函数(如DPtoLP、LPtoDP、ScreenToClient、ClientToScreen)、映射函数(如SetMapMode、SetWindowExtEx、SetViewportExtEx)、元文件函数(如PlayMetaFile、SetWinMetaFileBits)、区域函数(如FillRgn、FrameRgn、InvertRgn)、路径函数(如BeginPath、EndPath、StrokeAndFillPath)、裁剪函数(如SelectClipRgn、SelectClipPath)等。
GDI虽然使程序员得到了一定程度的解脱,但是其编程方式仍很麻烦。譬如,显示一张位图,程序员需要进行"装入位图―读取位图文件头信息―启用设备场景―调色板变换"等一连串操作。而有了GDI+,这些问题便迎刃而解了。
顾名思义,GDI+是GDI的增强版。它是微软在Windows 2000以后操作系统中提供的新接口,其通过一套部署为托管代码的类来展现,这套类被称为GDI+的"托管类接口"。GDI+主要提供了以下三类服务:
(1) 二维矢量图形:GDI+提供了存储图形基元自身信息的类(或结构体)、存储图形基元绘制方式信息的类以及实际进行绘制的类;
(2) 图像处理:大多数图片都难以划定为直线和曲线的集合,无法使用二维矢量图形方式进行处理。因此,GDI+为我们提供了Bitmap、Image等类,它们可用于显示、操作和保存BMP、JPG、GIF等图像格式。
(3) 文字显示:GDI+支持使用各种字体、字号和样式来显示文本。
GDI接口是基于函数的,而GDI+是基于C++类的对象化的应用程序编程接口,因此使用起来比GDI要方便。
2.例程简述
单击此处下载本文例程源代码。
本文后续的讲解都基于这样的一个例子工程(例程的开发环境为Visual C++6.0,操作系统为Windows XP),它是一个基于对话框的MFC应用程序,包括2个父菜单:
(1) GDI
GDI父菜单下包括一个子菜单:
ID:IDM_GDI_DRAW_LINE caption:画线
单击事件:在窗口绘制正旋曲线
(2) GDI+
DIB位图父菜单下包括两个子菜单:
a. ID:IDM_GDIP_DRAW_LINE caption:画线
单击事件:在窗口绘制正旋曲线
b. caption:新增功能,其下又包括下列子菜单:
(ⅰ)ID:IDM_Gradient_Brush caption:渐变画刷
单击事件:在窗口演示GDI+的渐变画刷功能
(ⅱ)ID:IDM_Cardinal_Spline caption:基数样条
单击事件:在窗口演示GDI+的基数样条函数功能
(ⅲ)ID:IDM_Transformation_Matrix caption:变形和矩阵对象
单击事件:在窗口演示GDI+的变形和矩阵对象功能
(ⅳ)ID:IDM_Scalable_Region caption:可伸缩区域
单击事件:在窗口演示GDI+的可伸缩区域功能
(ⅴ)ID:IDM_IMAGE caption:图像
单击事件:在窗口演示GDI+的多种图像格式支持功能
(ⅵ)ID:IDM_Alpha_Blend caption:Alpha混合
单击事件:在窗口演示GDI+的Alpha混合功能
(ⅶ)ID:IDM_TEXT caption:文本
单击事件:在窗口演示GDI+的强大文本输出能力
后续篇章将集中在对上述菜单单击事件消息处理函数的讲解,下面的代码是整个对话框类CGdiexampleDlg的消息映射:
BEGIN_MESSAGE_MAP(CGdiexampleDlg, CDialog) //{{AFX_MSG_MAP(CGdiexampleDlg) ON_WM_SYSCOMMAND() ON_WM_PAINT() ON_WM_QUERYDRAGICON() ON_COMMAND(IDM_GDI_DRAW_LINE, OnGdiDrawLine) ON_COMMAND(IDM_GDIP_DRAW_LINE, OnGdipDrawLine) ON_COMMAND(IDM_Gradient_Brush, OnGradientBrush) ON_COMMAND(IDM_Cardinal_Spline, OnCardinalSpline) ON_COMMAND(IDM_Transformation_Matrix, OnTransformationMatrix) ON_COMMAND(IDM_Scalable_Region, OnScalableRegion) ON_COMMAND(IDM_IMAGE, OnImage) ON_COMMAND(IDM_Alpha_Blend, OnAlphaBlend) ON_COMMAND(IDM_TEXT, OnText) //}}AFX_MSG_MAP END_MESSAGE_MAP() |
3.GDI编程
"GDI"菜单下的"画线"子菜单单击事件消息处理函数的代码如下:
void CGdiexampleDlg::OnGdiDrawLine() { // TODO: Add your command handler code here CClientDC dc(this); //逻辑坐标与设备坐标变换 CRect rect; GetClientRect(&rect); dc.SetMapMode(MM_ANISOTROPIC); dc.SetWindowOrg(0, 0); dc.SetWindowExt(rect.right, rect.bottom); dc.SetViewportOrg(0, rect.bottom / 2); dc.SetViewportExt(rect.right, - rect.bottom); //创建绘制正旋曲线的pen并将其选入设备上下文 CPen pen(PS_SOLID, 1, RGB(255, 0, 0)); HGDIOBJ oldObject = dc.SelectObject(pen.GetSafeHandle()); //绘制正旋曲线 dc.MoveTo(0, 0); for (int i = 0; i < rect.right; i++) { dc.LineTo(i, 100 *sin(2 *(i / (rect.right / 5.0)) *PI)); } //创建绘制x轴的pen并将其选入设备上下文 CPen penx(PS_SOLID, 1, RGB(0, 0, 255)); dc.SelectObject(penx.GetSafeHandle()); //绘制X轴 dc.MoveTo(0, 0); dc.LineTo(rect.right, 0); //恢复原先的pen dc.SelectObject(oldObject); } |
单击这个按钮,会出现如图1所示的效果,我们来对此进行解读。
图1 绘制正旋曲线 |
前文提到,GDI编程需进行设备坐标和逻辑坐标的转化。而屏幕上的设备坐标通常会按客户坐标给出,客户坐标依赖于窗口的客户区域,其起始位置位于客户区域的左上角。为示区别,图2给出了设备坐标和用户逻辑坐标的示例。
图2 设备坐标与逻辑坐标 |
设备坐标与逻辑坐标的转换关系如下:
公式中的<Xvorg, Yvorg>是设备空间中视口的原点,而< Xworg, Yworg >是逻辑空间中窗口的原点。 Xwext/Xvext和Ywext/Yvext分别是窗口与视口水平和垂直范围的比例。
因此,经过程序中的dc.SetWindowOrg (0,0) 和dc.SetViewportOrg (0,rect.bottom/2)语句我们设置了视口和窗口的原点;而经过程序中的dc.SetWindowExt (rect.right,rect.bottom) 和dc.SetViewportExt (rect.right,-rect.bottom) 语句我们设置了视口和窗口的范围。由于视口和窗口的纵坐标方向相反,设置视口的垂直范围为负值。这样我们得到了一个逻辑坐标原点为客户区水平方向最左边和垂直方向居中的坐标系,我们在这个坐标系上直接绘制正旋曲线,不需要再理睬Windows对话框客户区坐标了。
void CGdiexampleDlg::OnGdiDrawLine()函数中未指定逻辑设备和物理设备的映射模式,则为缺省的MM_TEXT。在这种模式下,一个逻辑单位对应于一个像素点。映射模式是GDI中的一个重要概念,其它的映射模式还有MM_LOENGLlSH、MM_HIENGLISH、MM_LOMETRIC和MM_HIMETRIC等。我们可以通过如下语句指定映射模式为MM_TEXT:
dc.SetMapMode(MM_TEXT); |
值得一提的是,从上述代码可以看出:在GDI编程中,几乎所有的操作都围绕设备上下文dc展开。的确,这正是GDI编程的特点!设备上下文是Windows 使用的一种结构,所有GDI操作前都需取得特定设备的上下文,函数中的CClientDC dc (this) 语句完成这一功能。
归纳可得,利用GDI进行图形、图像处理的一般操作步骤为:
1. 取得指定窗口的DC;
2. 确定使用的坐标系及映射方式;
3. 进行图形、图像或文字处理;
4. 释放所使用的DC。
4.GDI+编程
"GDI+"菜单下的"画线"子菜单单击事件消息处理函数的代码如下:
void CGdiexampleDlg::OnGdipDrawLine() { // TODO: Add your command handler code here CClientDC dc(this); //逻辑坐标与设备坐标变换 CRect rect; GetClientRect(&rect); dc.SetMapMode(MM_ANISOTROPIC); dc.SetWindowOrg(0, 0); dc.SetWindowExt(rect.right, rect.bottom); dc.SetViewportOrg(0, rect.bottom / 2); dc.SetViewportExt(rect.right, - rect.bottom); //创建Graphics对象 Graphics graphics(dc); //创建pen Pen myPen(Color::Red); myPen.SetWidth(1); //画正旋曲线 for (int i = 0; i < rect.right; i++) { graphics.DrawLine(&myPen, i, 100 *sin(2 *(i / (rect.right / 5.0)) *PI), i + 1, 100 *sin(2 *((i + 1) / (rect.right / 5.0)) *PI)); } //画X轴 myPen.SetColor(Color::Blue); graphics.DrawLine(&myPen, 0, 0, rect.right, 0); } |
由于我们使用的是Visual C++6.0而非VS.Net,我们需要下载微软的GDIPLUS支持包。在微软官方网站下载时需认证Windows为正版,我们可从这个地址下载:http://www.codeguru.com/code/legacy/gdi/GDIPlus.zip。一个完整的GDI+支持包至少包括如下文件:
(1)头文件:gdiplus.h
(2)动态链接库的.lib文件:gdiplus.lib
(3)动态链接库的.dll文件:gdiplus.dll
少了(1)、(2)程序不能编译,少了(3)程序能以共享DLL的方式编译但是不能运行,运行时找不到.dll文件。
为使得Visual C++6.0支持GDI+,我们需要在使用GDI+对象的文件的开头添加如下代码:
#define UNICODE #ifndef ULONG_PTR #define ULONG_PTR unsigned long* #endif #include "c:\gdiplus\includes\gdiplus.h" using namespace Gdiplus; #pragma comment(lib, "c:\gdiplus\lib\gdiplus.lib") |
在Visual C++中使用GDI+必须先进行GDI+的初始化,我们在CWinApp派生类的InitInstance函数中进行此项工作是最好的:
/// Call this when linking to MFC statically |
单击"GDI+"菜单下的"画线"子菜单,也会出现如图1所示的效果。观察void CGdiexampleDlg::OnGdipDrawLine() 函数,我们发现用GDI+进行图形、图像操作的步骤为:
(1)创建 Graphics 对象:Graphics 对象表示GDI+绘图表面,是用于创建图形图像的对象;
(2)使用 Graphics 对象绘制线条和形状、呈现文本或显示与操作图像。
Graphics 对象是GDI+的核心,GDI中设备上下文dc和Graphics 对象的作用相似,但在GDI中使用的是基于句柄的编程模式,而GDI+中使用的则是基于对象的编程模式。Graphics封装了GDI+ 绘图面,而且此类无法被继承,它的所有成员函数都不是虚函数。
下面,我们来逐个用实际代码实现GDI+的新增功能,这些新增功能包括:渐变的画刷(Gradient Brushes)、基数样条函数(Cardinal Splines)、持久的路径对象(Persistent Path Objects)、变形和矩阵对象(Transformations &Matrix Object)、可伸缩区域(Scalable Regions)、Alpha混合(Alpha Blending)和丰富的图像格式支持等。
渐变的画刷
GDI+提供了用于填充图形、路径和区域的线性渐变画刷和路径渐变画刷。
线性渐变画刷使用渐变颜色来填充图形。
当用路径渐变画刷填充图形时,可指定从图形的一部分移至另一部分时画刷颜色的变化方式。例如,我们可以只指定图形的中心颜色和边缘颜色,当画刷从图形中间向外边缘移动时,画刷会逐渐从中心颜色变化到边缘颜色。
void CGdiexampleDlg::OnGradientBrush() { // TODO: Add your command handler code here CClientDC dc(this); CRect rect; GetClientRect(&rect); //创建Graphics对象 Graphics graphics(dc); //创建渐变画刷 LinearGradientBrush lgb(Point(0, 0), Point(rect.right, rect.bottom), Color::Blue, Color::Green); //填充 graphics.FillRectangle(&lgb, 0, 0, rect.right, rect.bottom); } |
本程序使用线性渐变画刷,当画刷从客户区左上角移向客户区右下角的过程中,颜色逐渐由蓝色转变为绿色。
图3 GDI+渐变画刷 |
基数样条函数
GDI+支持基数样条,基数样条指的是一连串单独的曲线,这些曲线连接起来形成一条较大的曲线。样条由点(Point结构体)的数组指定,并通过该数组中的每一个点。基数样条平滑地穿过数组中的每一个点(不出现尖角),因此比用直线连接创建的路径精确。
void CGdiexampleDlg::OnCardinalSpline() { // TODO: Add your command handler code here CClientDC dc(this); //创建Graphics对象 Graphics graphics(dc); Point points[] = { Point(0, 0), Point(100, 200), Point(200, 0), Point(300, 200), Point(400, 00) }; //直接画线 for (int i = 0; i < 4; i++) { graphics.DrawLine(&Pen(Color::Blue, 3), points[i], points[i + 1]); } //利用基数样条画线 graphics.DrawCurve(&Pen(Color::Red, 3), points, 5); } |
图4演示了直接连线和经过基数样条平滑拟合后的线条的对比,后者的曲线(Curve)没有尖角。这个工作我们在中学的数学课上把离散的点连接成曲线时做过。
图4 GDI+基数样条 |
持久的路径对象
在GDI中,路径隶属于一个设备上下文,一旦设备环境指针超过它的生存期,路径也会被删除。利用GDI+,可以创建并维护与Graphics对象分开的GraphicsPath 对象,它不依赖于Graphics对象的生存期。
变形和矩阵对象
GDI+提供了Matrix对象,它是一种可以使变形(旋转、平移、缩放等) 简易灵活的强大工具,Matrix对象需与要被变形的对象联合使用。对于GraphicsPath类,我们可以使用其成员函数Transform接收Matrix参数用于变形。
void CGdiexampleDlg::OnTransformationMatrix() { // TODO: Add your command handler code here CClientDC dc(this); //创建Graphics对象 Graphics graphics(dc); GraphicsPath path; path.AddRectangle(Rect(250, 20, 70, 70)); graphics.DrawPath(&Pen(Color::Black, 1), &path); // 在应用变形矩阵之前绘制矩形 // 路径变形 Matrix matrix1, matrix2; matrix1.Rotate(45.0f); //旋转顺时针45度 path.Transform(&matrix1); //应用变形 graphics.DrawPath(&Pen(Color::Red, 3), &path); matrix2.Scale(1.0f, 0.5f); //转化成为平行四边形法则 path.Transform(&matrix2); //应用变形 graphics.DrawPath(&Pen(Color::Blue, 3), &path); } |
图5演示了正方形经过旋转和拉伸之后的效果:黑色的为原始图形,红色的为旋转45度之后的图形,蓝色的为经过拉伸为平行四边形后的图形。
图5 GDI+变形和矩阵对象 |
可伸缩区域
GDI+通过对区域(Region)的支持极大地扩展了GDI。在GDI 中,区域存储在设备坐标中,可应用于区域的唯一变形是平移。但是在GDI +中,区域存储在全局坐标(世界坐标)中,可对区域利用变形矩阵进行变形(旋转、平移、缩放等)。
void CGdiexampleDlg::OnScalableRegion() { // TODO: Add your command handler code here CClientDC dc(this); //创建Graphics对象 Graphics graphics(dc); //创建GraphicsPath GraphicsPath path; path.AddLine(100, 100, 150, 150); path.AddLine(50, 150, 150, 150); path.AddLine(50, 150, 100, 100); //创建Region Region region(&path); //填充区域 graphics.FillRegion(&SolidBrush(Color::Blue), ®ion); //区域变形 Matrix matrix; matrix.Rotate(10.0f); //旋转顺时针20度 matrix.Scale(1.0f, 0.3f); //拉伸 region.Transform(&matrix); //应用变形 //填充变形后的区域 graphics.FillRegion(&SolidBrush(Color::Green), ®ion); } |
上述程序中以蓝色填充一个三角形区域,接着将此区域旋转和拉伸,再次显示,其效果如图6。
图6 GDI+区域变形 |
丰富的图像格式支持
GDI +提供了Image、Bitmap 和Metafile 类,方便用户进行图像格式的加载、操作和保存。GDI+支持的图像格式有BMP、GIF、JPEG、EXIF、PNG、TIFF、ICON、WMF、EMF等,几乎涵盖了所有的常用图像格式。
void CGdiexampleDlg::OnImage() { // TODO: Add your command handler code here CClientDC dc(this); //创建Graphics对象 Graphics graphics(dc); Image image(L "d:\1.jpg"); //在矩形区域内显示jpg图像 Point destPoints1[3] = { Point(10, 10), Point(220, 10), Point(10, 290) }; graphics.DrawImage(&image, destPoints1, 3); //在平行四边形区域内显示jpg图像 Point destPoints2[3] = { Point(230, 10), Point(440, 10), Point(270, 290) }; graphics.DrawImage(&image, destPoints2, 3); } |
上述程序将D盘根目录下文件名为"1.jpg"的jpg图像以矩阵和平行四边形两种方式显示,效果如图7。
图7 GDI+多种图像格式支持 |
由此我们可以看出,GDI+在图像显示和操作方面的确比GDI简单许多。回忆我们在《Visual C++中DDB与DIB位图编程全攻略》一文中所介绍的用GDI显示位图的方式,其与GDI+图像处理的难易程度真是有天壤之别。
Alpha混合
Alpha允许将两个物体混合起来显示,在3D气氛和场景渲染等方面有广泛应用。它能"雾化"图像,使得一个图像着色在另一个半透明的图像上,呈现一种朦胧美。我们知道,一个像素可用R,G,B三个维度来表示,我们可以再加上第4个即:Alpha维度(channel),表征透明程度。
void CGdiexampleDlg::OnAlphaBlend() { // TODO: Add your command handler code here CClientDC dc(this); //创建Graphics对象 Graphics graphics(dc); //创建ColorMatrix ColorMatrix ClrMatrix = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; //将ColorMatrix赋给ImageAttributes ImageAttributes ImgAttr; ImgAttr.SetColorMatrix(&ClrMatrix, ColorMatrixFlagsDefault,ColorAdjustTypeBitmap); //在矩形区域内显示jpg图像 Image img1(L "d:\1.jpg"); Point destPoints1[3] = { Point(10, 10), Point(220, 10), Point(10, 290) }; graphics.DrawImage(&img1, destPoints1, 3); //Alpha混合 Image img2(L "d:\2.jpg"); int width, height; width = img2.GetWidth(); height = img2.GetHeight(); graphics.DrawImage(&img2, RectF(10, 10, 210, 280), 0, 0, width, height,UnitPixel, &ImgAttr); //在平行四边形区域内显示jpg图像 Point destPoints2[3] = { Point(230, 10), Point(440, 10), Point(270, 290) }; graphics.DrawImage(&img1, destPoints2, 3); //Alpha混合 graphics.DrawImage(&img2, destPoints2, 3, 0, 0, width, height, UnitPixel,&ImgAttr); } |
上述程序中将D盘根目录下文件名为"1.jpg"的图像以矩阵和平行四边形两种方式显示,然后将文件名为为"2.jpg"的图像与之进行混合,其效果如图8。
图8 GDI+ Alpha混合 |
为了能进行Alpha混合,我们需要使用ImageAttributes类和ColorMatrix矩阵,ImageAttributes可以进行颜色、灰度等调整从而达到控制图像着色方式的目的。ColorMatrix是ImageAttributes类大多数函数的参数,它包含了Alpha、Red、Green、Blue维度的值,以及另一维w,顺序为RGBaw。
CGdiexampleDlg::OnAlphaBlend()函数中ColorMatrix的实例ClrMatrix中元素(4,4)的值为0.5,表示Alpha度的值为0.5(即半透明)。在ColorMatrix中,元素(5,5)的值恒定为1.0。我们把ClrMatrix的元素(0,0)修改为0.0,即使得图像2.jpg的红色维度全不显示,再看效果,为图9。列位读者,我们以前在豪杰超级解霸中调整R,G,B值从而控制图像输出颜色的时候,调的就是这个东东!图9的效果很像破旧彩色电视机,红色电子枪"嗝"了。刚大学毕业时,俺那个叫穷啊,就买了这么个电视机,还看得很爽,真是往事不堪回首!
图9 GDI+中的ColorMatrix |
强大的文字输出
GDI+拥有极其强大的文字输出处理能力,输出文字的颜色、字体、填充方式都可以直接作为Graphics类DrawString成员函数的参数进行设置,其功能远胜过GDI设备上下文的TextOut函数。
void CGdiexampleDlg::OnText() { // TODO: Add your command handler code here CClientDC dc(this); //创建Graphics对象 Graphics graphics(dc); //创建20号"楷体"字体 FontFamily fontFamily1(L "楷体_GB2312"); // 定义"楷体"字样 Font font1(&fontFamily1, 20, FontStyleRegular, UnitPoint); //定义输出UNICODE字符串 WCHAR string[256]; wcscpy(string, L "天极网的读者朋友,您好!"); //以蓝色画刷和20号"楷体"显示字符串 graphics.DrawString(string, (INT)wcslen(string), &font1, PointF(30, 10),&SolidBrush(Color::Blue)); //定义字符串显示画刷 LinearGradientBrush linGrBrush(Point(30, 50), Point(100, 50), Color(255, 255,0, 0), Color(255, 0, 0, 255)); //以线性渐变画刷和创建的20号"楷体"显示字符串 graphics.DrawString(string, (INT)wcslen(string), &font1, PointF(30, 50),&linGrBrush); //创建20号"华文行楷"字体 FontFamily fontFamily2(L "华文行楷"); // 定义"楷体"字样 Font font2(&fontFamily2, 20, FontStyleRegular, UnitPoint); //以线性渐变画刷和20号"华文行楷"显示字符串 graphics.DrawString(string, (INT)wcslen(string), &font2, PointF(30, 90),&linGrBrush); //以图像创建画刷 Image image(L "d:\3.jpg"); TextureBrush tBrush(&image); //以图像画刷和20号"华文行楷"显示字符串 graphics.DrawString(string, (INT)wcslen(string), &font2, PointF(30, 130),&tBrush); //创建25号"华文中宋"字体 FontFamily fontFamily3(L "华文中宋"); // 定义"楷体"字样 Font font3(&fontFamily2, 25, FontStyleRegular, UnitPoint); //以图像画刷和20号"华文行楷"显示字符串 graphics.DrawString(string, (INT)wcslen(string), &font3, PointF(30, 170),&tBrush); } |
上述代码的执行效果如图10所示,字体、颜色和填充都很丰富!