理解绘图规则 GDI+提供了一个抽象层,隐藏了不同视频卡之间的区别,这样就可以调用Windows API函数完成指定的任务了,GDI还在内部指出在运行特定的代码时,如果让客户机的视频卡完成要绘制的图形。GDI还可以完成其他任务。大多数计算机都有多个显示设备---监视器、打印机。GDI成功的使应用程序所使用的打印机看起来与屏幕一样。如果要打印某些东西,而不是显示他们,只需告诉系统输出设备是打印机,再用相同的方式调用相同的Windows API函数可以。 可以看出DC(设备环境)是一个功能非常强大的对象,在GDI下,所有的绘图工作都必须通过设备环境完成。DC甚至可用于不涉及在屏幕或其他硬件设备上绘图的其他操作,例如在内存中修改图像。 GDI开发人员提供了一个相当高级的API,但它仍是一个基于旧Windows API并且有C语言风格函数的API,所以使用起来不是很方便。GDI+在很大程度上是GDI和应用程序之间的一层,提供了更直观、基于继承性的对象模型。尽管GDI+基本上是GDI的一个包装器,但Microsoft已经能通过GDI+提供新的功能了并宣称他又一些性能方面的改进。 绘制图形 private void InitializeComponent() 接着给Form1构造函数添加代码。使用窗体的CreateGraphics()方法创建一个Graphics对象,其中包括绘图时需要的使用的Windows DC。创建的DC即与显示设备相关也与窗口相关。 public Form1() Graphics dc = this.CreateGraphics();
然后调用Show()方法显示窗口。必须让窗口立即显示,因为在其显示之前不能作任何工作。(没有绘图的地方) 上面程序窗体如果最小化再恢复,绘制好的图形就不见了。如果在该窗体上拖动另一个窗口,使之只遮挡一部分图形,再把该窗口拖离这个窗体,临时被遮挡的部分就消失了,只剩下一半椭圆或矩形了!原因是:如果窗体的一部分被隐藏了,Windows通常会立即删除与其中显示的内容相关的所有信息。在窗口的某一部分消失时,那些像素也就丢失了(即Windows释放了保存这些像素的内存)。 protected override void OnPaint(PaintEventarges e) Pen bluePen = new Pen(Color.Blue,3); Pen redpen = new Pen(Color.Red,2); PaintEventArgs是一个派生自EventArgs的类,一般用于传送有关事件的信息。PaintEventArgs有另外两个属性,其中一个比较重要的是Graphics实例,它们主要用于优化绘制窗口中需要绘制的部分。这样就不必调用CreateGraphics(),在OnPaint()方法中获取DC。 使用剪切区域
在本例中,没有重新绘制图形。原因是我们使用了设备环境。Windows将利用重新绘制某些区域所需要的信息预先初始化设备环境。在GDI中,被标记出来的重绘区域称为无效区域,但在GDI+中,该术语改为剪切区域,设备环境知道这个区域的内容,它截取在这个区域外部的绘图操作,且不把相关的绘图命令传送给显卡。这听起来不错,但仍有一个潜在的性能损失。在确定是在无效区域外部绘图前,我们不知道必须进行多少设备环境处理。在某些情况下,要处理的任务比较多,因为计算哪些像素需要改变什么颜色,将会占用许多处理器时间。 其底线是让Graphics实例完成在无效区域外部的绘图工作,肯定会浪费处理器时间,减慢应用程序的运行。在设计优良的应用程序中,代码将执行一些检查,以查看需要进行哪些绘图工作,然后调用相关的Graphics实例方法。下面将编写一个示例DrawShapesClipping,修改DisplayShapes示例,只完成需要的重新绘制工作。在OnPaint()代码中,进行一个简单的测试,看看无效区域是否需要绘制的区域重叠,如果是就调用绘图方法。 首先,需要获得剪切区域的信息。这需要使用PaintEventArgs的另一个属性。这个属性叫做ClipRectangle,包含要重绘区域的坐标,并包装在一个结构实例System.Drawing.Rectangle中。Rectangle是一个相当简单的结构,包含4个属性:Top、Bottom、Left、Right。它们分别含矩形的上下的垂直坐标,左右的水平坐标。 接着,需要确定进行什么测试,以决定是否进行绘制。这里进行一个简单的测试。注意,在我们的绘图过程中,矩形和椭圆完全包含在(0,0)到(80,130)的矩形客户区域中,实际上,点(82,132)就已经在安全区域中了,因为线条大约偏离这个区域一个像素。所以我们要看看剪切区域的左上角是否在这个矩形区域内。如果是,就重新绘制如果不是就不必麻烦了。 protected override void OnPaint(PaintEventArgs e) if (e.ClipRectangle.Top < 32 && e.ClipRectangle.Left < 82) 注意:这个结果和前一个结果完全相同,只是进行了早期测试,确定不需要重绘制的区域,提高了性能。还要注意这个是否进行绘图测试是非常粗略的。还可以进行更精细的测试,确定矩形和椭圆是否要重新绘制。这里有一个平衡。可以在OnPaint()中进行更复杂的测试,以提高性能,也可以使OnPaint()代码复杂一些。进行一些测试总是值得的,因为编写一些代码,可以更多的解除Graphics实例之外的会址内容,Graphics实例只是盲目地执行绘图命令。
测量坐标和区域 结构 | 主 要 公 共 属 性| (1) Point、PointrF结构 为了从点A到点B,需要水平移动20个单位,并向下垂直移动10个单位,在图中标为x和y,这就是他们的一般含义。创建一个Point结构,表示他们: Point ab = new Point(20,10); Point ab = new Point(); PointF abFloat = new PointF(25.5F,10.9F); // converting to Point // but conversion back to PointF is implicit 在默认情况下,GDI+把单位看作是屏幕(或打印机,无论图形设备是什么,都可以这样认为)的像素,这就是Graphics对象方法把它们接受到的坐标看作其参数的方式。例如:点 new Point(20,10)表示在屏幕上水平移动20个像素,向下垂直移动10个像素。通常这些像素从窗体客户区域的左上角开始测量,如上图。但是,情况并不是如此。在某些情况下,需要以窗口的左上角(包括其边框)为原点来绘图,甚至以屏幕的左上角为原点。除特殊说明,大多数可以假定像素是相对于客户区域的左上角。 (2) Size、SizeF结构 Size ab = new Size(20,10); 例如:前面绘制的矩形,其左上角的坐标是(0,0),大小是(50,50)。这个矩形的大小是(50,50),可以用一个Size实例来表示。其右下角的坐标也是(50,50),但它由一个Point来表示。 Point和Size结构的相加运算符都已经重载了,所以可以把一个Size加到Point结构上,得到另一个Point结构: 这个结果说明Point和Size的ToString()方法已被重写并以{X,Y}的格式显示。 Graphics dc = e.Graphics; (4)Region 可以想象,初始化Region实例的过程相当复杂。从广义上看,可以指定哪些简单的图形组成这个区域,或者绘制这个区域的边界的路径。这种处理就需要Region类。
这是典型的一种情况。要明白程序为什么应用程序没有正确显示,可以在OnPaint上设置断点。应用程序会像期望的那样,遇到断点后进入调试程序。此时在 前景上会显示开发环境MDI窗体。如果把开发环境设置为满屏显示,以便更易于观察所有的调试信息,就会完全隐藏目前正在调试的应用程序。 接着检查某些变量的值,希望找出某些有用的信息。然后按F5,告诉程序继续执行,告诉应用程序继续执行,完成某些处理后,看看应用程序在显示其他内容时会 发生什么。但首先发生的是应用程序显示在前景中,Windows检测到窗体再次可见,并提示给他发送了一个Paint事件。当然这表示程序遇到了断点。如 果这就是我们希望的结果,那就很好。但更常见的是,我们希望以后在应用程序绘制了某些有趣的内容之后再遇到断点。我们根本没有在OnPaint中设置断 点,应用程序也不会显示它在最初的启动窗口中显示的内容之外的其他内容。 有一种方式可以解决这个问题。如果有足够大的屏幕,最简单的方式就是恢复开发环境窗口,而不是把它设置为最大化,使之远离应用程序窗口,这样应用程序就不 会被挡住了。但在大多数情况下,这并不是一个有效的解决方案,因为这样会使开发环境窗口过小。另一个解决方案使用相同的规则,即使应用程序声明为在调试时 放在最上层。方法是在Form类中设置属性TopMost,这很容易在InitialzeComponet方法中完成: priavte void InitialzeComponent() { this.TopMost = true; } 也可以在 Visual Studio 2005的属性窗口中设置这个属性。 窗口这是为TopMost 表示应用程序不会被其他窗口挡住(除了其他放在最上层的窗口)。它总是放在其他窗口的上面,甚至在另一个应用程序得到焦点时,也是这样。这是任务管理器的执行方式。 利用这个技巧是必须小心,因为我们不能确定Windows何时会决定应为某种原因引发Paint事件。如果在某些特殊的情况下,OnPaint出了问题 (例如:应用程序在选择某个菜单项后绘图,但此时出了问题)。最好的方式是在OnPaint中编写一些虚拟代码,测试某些条件,这些条件只在特殊情况下才 为True。然后在if 块中设置断点,如下所示: protected override void OnPaint(PaintEventArgs e) { // Condition() evaluates to true when we want to break if (Condition() == true) { int ii = 0; // <-- SET BREAKPOINT!!! } // member fields private override void OnPaint(PaintEventArgs e) 注意这里还把Pen、Size、Point对象变成成员字段---这比每次需要绘图时都创建一个新Pen的效率高。 这里有一个问题,图形在300*300像素的绘图区域中放不下。 添加滚动条是很简单的。Form仍会处理所有的操作---因为它不知道绘图区域有多大。在上面的BigShapse示例中没有滚动条的原因是,Windows不知道它们需要滚动条。我们需要确认的是,矩形的大小从文档的左上角(或者是在进行任何滚动前的客户区域左上角)开始向下延伸,其大小应足以包含整个文档。本章把这个区域称为文档区域。在下图可以看出,本例的文档区域应是(250,350)像素。 使用相关的属性Form.AutoScrollMinSize即可确定文档的大小。因此给InitializeComponent()方法或Form1构造函数添加下述代码: private void InitializeComponent() 另外,AutoScrollSize属性还可以用VS2005属性窗口设置。 设置MinScrollSize只是一个开始,仅有它是不够的。下图为示例应用程序目前的外观。 注意,不仅窗体正确设置了滚动条,而且他们的大小也正确设置了,以指定文档正确显示的比例。可以试着在运行示例重新设置窗口的大小,这样就会发现滚动条会正确响应,甚至如果窗口变得足够大,不再需要滚动条时,他会消失。 出错的原因是我们没有在OnPaint()重写方法的代码中考虑滚动条的位置。如果最小化窗口,再恢复它,重新绘制一遍窗口,就可以很清楚地看出这一点。结果如图所示。 图形像以前一样进行了绘制,矩形的左上角嵌套在客户区域的左上角,就好像根本没有移动过滚动条一样。 在更正这个问题前,先介绍一下在这些屏幕图上发生了什么。 首先从BigShapes示例开始,如图---所示。在这个例子中,整个窗口刚刚重新进行了绘制。看看前面的代码,该代码的作用是使graphics实例用左上角坐标(0,0)(相对于窗口客户区域的左上角)绘制一个矩形---它是已经绘制过的。问题是,graphics实例在默认情况下把坐标解释为是相对于客户窗口的,它不知道滚动条的存在。代码还没有尝试为滚动条的位置调整坐标。椭圆也是这样。 下面处理图---的问题。在滚动后,注意窗口上半部分显示正确,这是因为它们是在应用程序第一次启动时绘制的。在滚动窗口时,Windows没有要求应用程序重新绘制已经显示在屏幕中的内容。Windows只指出屏幕上目前显示的内容可以平滑移动,以匹配滚动条的位置。这是一个非常高效的过程,因为它也能使用某些硬件加速来完成。在这个屏幕图中,有错的是窗口下部的1/3部分。在应用程序第一次显示时,没有绘制这部分窗口,因为在滚动窗口前,在部分在客户区域的外部。这表示Windows要求BigShapes应用程序绘制这个区域。它引发Paint事件,把这个区域作为剪切的矩形。这也是OnPaint() 重载方法完成的任务。 问题的另一种表达方式是我们把坐标表示为相对于文档开头的左上角---需要转换它们,使之相对于客户区域的左上角。图---说明了这一点。 为了使该图更清晰,我们向下向右扩展了该文档,超出了屏幕的边界,但这不会改变我们的推论,我们还假定其上有一个水平滚动条和一个垂直滚动条。 在该图中,细矩形标记了屏幕区域的边框和整个文档的边框。粗线条标记试图要绘制的矩形和椭圆。P标记要绘制的某个随意点,这个点在后面会作为一个示例。在调用绘图方法时,提供graphics实例和从B点到P点的矢量,这个矢量表示为一个Point实例。我们实际上需要给出从点A到点B的矢量。 不知道A点到P点的矢量,而知道B点到P点的矢量,这是P相对于文档左上角的坐标---要在文档的P点绘图.还知道从B点到A点的矢量,这是滚动的距离,它存储在Form类的一个属性AutoScrollPosition中.但是不知道从A点到P点的矢量. 现在只需进行矢量相减即可.为了使之更简便,Graphics类执行了一个方法来进行这些计算---TranlateTransform.提供水平和垂直坐标,表示客户区域的左上角相对于文档的左上角,然后Graphics设备考虑客户区域相对于文档区域的位置,计算这些坐标. dc.TranslateTranform(this.AutoScrollPosition.X,this.AutoScrollPosition.Y); 在本例还要测试剪切区域,看看是否需要进行绘制工作.这个测试需要调整,把滚动的位置也考虑在内.完成后,该实例的整个绘图代码如下所示: protected override void OnPaint(PaintEventArgs e) if (e.ClipRectangle.Top + scrollOffset.Width < 350 || e.ClipRectangle.Left + scrollOffset.Height < 250) dc.DrawRectangle(bluePen ,rectangleArea); 得到正确的滚动屏幕.
世界\页面\设备坐标 测量相对于文档区域左上角的位置和测量相对于屏幕(桌面)左上角的位置之间的区别非常重要,GDI+为它们指定了不同的名称. 注意: 熟悉GDI开发的人员要注意,世界坐标对应于GDI中的逻辑坐标.页面坐标对应于设备坐标.编写逻辑坐标和设备坐标之间的转换代码在GDI+中有了变化.在GDI中,转化是使用Widows API函数LPtoDP()和DPtoLP()通过设备环境进行的,而在GDI+中,由Control类来维护转化过程中的所需要的信息,Form和各种Windows窗体控件设备派生于Control类. GDI+还有第3种坐标,即设备坐标(Device Coordinate).设备坐标类似于页面坐标,但其测量单位不是像素,而是用户通过调用Graphics.PageUnit属性指定的单位.它可以使用的单位除了默认的像素外,还包括英寸和毫米.它可以用作获取设备的不同像素密度方式.如:在监视器上,100像素约是1英寸.但激光打印机可以达到1200dpi(点/英寸)---这表示一个100像素宽的图形在该激光打印机上打印时会比较小.把单位设置为英寸,指定图形为1英寸宽,就可以确保图形在不同的设备上有相同的大小. 颜色 在GDI+中,颜色用System.Drawing.Color结构的实例来表示。一般情况下,初始化这个结构后,就不能使用对应的Color实例对该结构进行一些操作了----只能把它传送给其他的需要Color的调用方法。前面遇到这种结构,在前面的每个示例中都设置了窗口客户区域的背景色,还设置了要显示的各种图形的颜色。Form.BackColor属性返回一个Color实例。本节将详细介绍这个结构,特别是要介绍构建Color的几种不同方式。 (1) 红绿蓝(RGB)值 监视器可以显示的颜色总数非常大---超过160亿。其确切的数字是2的24方式,即16,777,216。显示,需要对这些颜色进行索引,才能指定在给定的某个像素上要显示什么颜色。 这些出了向GDI+说明颜色的第一种方式。可以调用静态函数Color.FromArgb()指定该颜色的红绿蓝值。微软没有为此提供构造函数,原因是除了一般的RGB成份外,还有其它方式表示颜色。因此,微软认为i给定以的构造函数传递会引起误解: Color redColor = Color.FromArgb(255,0,0); (2)命名颜色 使用FromArgb()构造颜色是一种非常灵活的技巧,因为它表示可以指定人眼睛辨识出的任何颜色。但是,如果要得到一些标准、众所周知的纯色,例如红色或蓝色,命名想要的颜色是比较简单的。因此微软还在Color中提供了许多静态属性,每个属性返回一种命名颜色。在下面的实例中,把窗口的背景设置为白色时,就使用了其中一种属性: this.BackColor = Color.White; // has the same effect as; 有几百种这样的颜色。完整的列表参见SDK文档。包括所有的纯色:红、白、蓝、绿和黑,还包括MediumAquamarine、LightCoral、DarkOrchid等颜色。还有一个KnownColor枚举,列出了命名的颜色。 (3)图形显示模式和安全的调色板 原则上监视器可以显示超出160亿种RGB颜色,实际上这种取决于如何在计算机上这置显示属性。在Windows中,传统上有3个主要的颜色选项:真彩色(24位)、增强色(16位)、256色。(在目前的一些图形卡上,真彩色是32位的,因为硬件进行了优化,但此时32位中只有24位用于该颜色)。 (4)安全调色板 这是一种非常常见的默认调色板。它工作的方式是为每种颜色成分这置6个间隔相等的值,这些值分别是0,51,102,153,204,255。换言之,红色成分可以是这些值中的任一个。绿色成分和蓝色成分也一样。所以安全调色板中的颜色就包括(0,0,0)(黑色)、(153,0,0)(暗红色)、(0,255,102)(蓝绿色)等,这样就得到了6的立方=216种颜色。这是一种让调色板包含色谱中颜色和所有亮度的简单方式,但实际上这是不可行的,因为数学上登间隔的颜色成分并不表示这些颜色的区别在人眼看来也是相等的。但安全调色板使用非常广泛,相当多的应用程序和图像仍然使用安全调色板上的颜色。 如果把Windows设置为256色模式,默认的调色板就是安全调色板,其中添加了20种标准的Windows颜色和20种备用颜色。
画笔和钢笔 本节介绍两个辅助类,在绘制图形时需要使用它们。前面已经见过了Pen类,它用于告诉工人graphics实例如何绘制线条。相关的类是System.Drawing.Brush,告诉graphics实例如何填充区域。例如,Pen用于绘制前面示例中的矩形和椭圆的边框。如果需要把这些图形绘制为实心的,就要使用画笔指定如何填充它们。这两个类有一个共同点:很难对他们调用任何方法。用需要的颜色和其他属性构造一个Pen或Brush实例,再把它传送给需要Pen或Brush的绘图方法即可。 <注>: <1>画笔 GDI+有几种不同类型的画笔,这里只解释几个比较简单的画笔,每种画笔都由一个派生自抽象类System.Drawing.Brush的类实例来表示.最简单的画笔System.Drawing.SolidBrush仅指定了区域用纯色来填充: Brush solidBeigeBrush = new SolidBrush(Color.Beige); 另外,如果画笔是一种Web安全颜色,就可以用另一个类System.Drawing.Brushes构造出画笔.Brushes是永远不能实例化的一个类(它有一个私有构造函数,禁止实例化).它有许多静态属性,每个属性都返回指定颜色的画笔.如下: Brush solidAzureBrush = Burshes.Azure; 比较复杂的一种画笔是影线画笔(hatch brush),它通过绘制一种模式填充区域,这种类型的画笔比较高级,所以Drawing2D命名空间中,用System.Drawing.Drawing2D.HatchBrush类表示.Brushes类不能帮助我们使用影线画笔,而需通过提供一个影线型式和两种颜色(前景色和背景色,背景色可以忽略,此时将使用默认的黑色),来显示构造一个影线画笔.影线型式可以取自于枚举Sysytem.Drawing.Drawing2D.HatchStyle,其中有许多HatchStyle值,其完整列表参阅SDK. 一般型式包括: ForwardDiagonal,Cross,DiagonalCross,SmallConfetti,ZigZag.示例如下: Brush crossBrush = new HatchBrush(HatchStyle.Cross,Color.Azure); GDI只能使用实践和影线画笔,GDI+添加了两种新画笔: <2>钢笔 钢笔只使用一个类System.Drawing.Pen来表示.但钢笔比画笔复杂一些,因为它需要指定线条应有多宽(像素),对于一条比较宽的线段,还要确定如何填充该线条中的区域.钢笔还可以指定其他许多属性,本章不讨论它们,其中包括前面提到的Alignment属性,该属性表示相对于图形的边框,线条该如何绘制,以及在线条的末尾绘制什么图形(是否使图形光滑过度). 粗线条中的区域可以用纯色填充,或者使用画笔来填充.因此Pen实例可以包括Brush实例的引用.这是非常强大的,因为这表示可以绘制有影线填充或线性阴影的线条.构造Pen实例有四中不同的方式.可以通过传送一种颜色,或者传送一种画笔.这两个构造函数都会生成一个像素宽的钢笔.另外,还可以传送一种颜色或画笔,以及一个表示钢笔宽度的float类型的值.(该宽度必须是一个float类型的值,以防执行绘图操作的Graphics对象使用非默认的单位,例如毫米或英寸,例如可以指定宽度是英寸的某个分数).例如可以构造如下的钢笔: Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick,Color.DarkGoldenrod,Color.Cyan); 另外,为了快速构造钢笔,还可以使用类System.Drawing.Pens,它与Brushes类一样.包括许多存储好的钢笔.这些钢笔的宽度都是一个像素,使用通常的Web安全颜色,这样就可以用下述方式构建一个钢笔: Pen solidYellowPen = Pens.Yellow;
绘制图形和线条System.Drawing.Graphics有很多方法,利用这些方法可以绘制各种线条、空心图形和实心图型。下图给出了只要方法。
using System; 接着是Form1类中的一些额外字段,其中包含了要绘制图形的位置信息,以为要使用的各种钢笔和画笔: 把BrickBrush字段声明为静态,就可以使用该字段的值初始化BrickWidePen字段了。C#不允许使用一个实例字段初始化另一个实例字段,因为还没有定义要先初始化哪个实例字段,如果把字段声明为静态字段就可以解决这个问题,因为只实例化了Form1类的实例,字段是静态字段还是实例字段就不重要了。 |
|