C# GDI+绘图高级编程

理解绘图规则
一般来说,Windows的一个优点(实际上是现代操作系统的优点)是它可以让开发人员不考虑特定设备的细节。例如:不需要理解硬盘设备驱动程序,只需在相关的.NET类中调用合适的方法,就可以编程读写磁盘上的文件。这个规则也适用于绘图。计算机在屏幕上绘图时,把指令发送给视频卡。问题是市面上有几百种不同的视频卡,大多数有不同的指令集合功能。如果把这个i考虑在内,在应用程序中为每个视频卡驱动程序编写在屏幕上绘图的特定代码,这样的应用程序就根本不可能编写出来。这就是为什么在Windows最早的版本中有Windows Graphical Device Interface(GDI)的原因。

GDI+提供了一个抽象层,隐藏了不同视频卡之间的区别,这样就可以调用Windows API函数完成指定的任务了,GDI还在内部指出在运行特定的代码时,如果让客户机的视频卡完成要绘制的图形。GDI还可以完成其他任务。大多数计算机都有多个显示设备---监视器、打印机。GDI成功的使应用程序所使用的打印机看起来与屏幕一样。如果要打印某些东西,而不是显示他们,只需告诉系统输出设备是打印机,再用相同的方式调用相同的Windows API函数可以。

可以看出DC(设备环境)是一个功能非常强大的对象,在GDI下,所有的绘图工作都必须通过设备环境完成。DC甚至可用于不涉及在屏幕或其他硬件设备上绘图的其他操作,例如在内存中修改图像。

GDI开发人员提供了一个相当高级的API,但它仍是一个基于旧Windows API并且有C语言风格函数的API,所以使用起来不是很方便。GDI+在很大程度上是GDI和应用程序之间的一层,提供了更直观、基于继承性的对象模型。尽管GDI+基本上是GDI的一个包装器,但Microsoft已经能通过GDI+提供新的功能了并宣称他又一些性能方面的改进。

1.GDI+命名空间
(不说了,自己看去吧!!!)
2.设备环境和Graphics对象
GDI使用设备环境(DC)对像识别输出设备。DC对象存储特定设备的信息并把GDI API函数调用转换为要发送给设备的命令。还可以通过DC对象确定对应的设备有什么功能(如打印机是彩色还是黑白的)。如果要求设备完成它不能完成的任务,设备对象就会检测到并采取措施。
DC对象不仅可以硬件还可以用作到Windows的一个桥梁。例如如果Windows知道只有一小部分应用程序窗口需要重新绘制,DC就可以捕获和撤销在该地区外的绘图工作。因为DC与Windows的关系非常密切,通过Dc来工作就可以用其他方式简化代码。

绘制图形
下面举例来说明如何在应用程序的主窗口中绘图。DisplayAtStartup
创建一个C# 应用程序并在启动窗体时在构造函数中绘制它。这并不是在屏幕上绘图的最佳方式,这个示例并不能在启动后按照需要重新绘制窗体。这样只是不必作太多的工作就可以说明一些问题。
首先把窗体的背景色设置为白色。如果使用设计视图设置背景色,系统会自动添加代码:

private void InitializeComponent()
{
this.AutoScaleBaseSize = new System.Drawing.Size(5,13);
this.BackColor = System.Drawing.Color.White;
this.ClientSize = new System.Drawing.Size(292,266);
this.Name = "Form1";
this.Text = "Form1";
}

接着给Form1构造函数添加代码。使用窗体的CreateGraphics()方法创建一个Graphics对象,其中包括绘图时需要的使用的Windows DC。创建的DC即与显示设备相关也与窗口相关。

public Form1()
{
InitializeComponent();

Graphics dc = this.CreateGraphics();
this.Show();
Pen bluePen = new Pen(Color.Blue,3);
dc.DrawRectangle(bluePen,0,0,50,50); //矩形
Pen redPen = new Pen(Color.Red,2);
dc.DrawEllipse(redPen,0,50,80,60); // 椭圆
}



然后调用Show()方法显示窗口。必须让窗口立即显示,因为在其显示之前不能作任何工作。(没有绘图的地方)
最后显示一个矩形和椭圆。注意其中坐标(x,y)表示从窗口的客户区域左上角向右的X个像素,向下的Y个像素。
(其中DrawRectangle()和DrawEillipse()这两个函数前面已经讲过不再重复了。)

上面程序窗体如果最小化再恢复,绘制好的图形就不见了。如果在该窗体上拖动另一个窗口,使之只遮挡一部分图形,再把该窗口拖离这个窗体,临时被遮挡的部分就消失了,只剩下一半椭圆或矩形了!原因是:如果窗体的一部分被隐藏了,Windows通常会立即删除与其中显示的内容相关的所有信息。在窗口的某一部分消失时,那些像素也就丢失了(即Windows释放了保存这些像素的内存)。
但要注意窗口的一部分被隐藏了,当它检测到窗口不再被隐藏时,就请求拥有该窗口的应用程序重新绘制其内容。这个规则有一些例外----窗口的一小部分被挡住的时间比较短(显示菜单时)。一般情况下应用程序就需要在以后重新绘制它。
由于本示例把绘图代码放在Form1的构造函数中,故不能在启动后再次调用该构造函数进行重新绘制。
使用OnPaint()绘制图形
Windows会利用Paint事件通知应用程序完成重新绘制的要求。Paint事件的Form1处理程序处理虚方法OnPaint()的调用,同时传给他一个参数PaintEventArgs。也就是说只要重写OnPaint()执行画图操作。
下面创建一个Windows应用程序DrawShapes来完成这个操作。

protected override void OnPaint(PaintEventarges e)
{
base.OnPaint(e);
Graphics dc = e.Graphics;

Pen bluePen = new Pen(Color.Blue,3);
dc.DrawRectangle(bluePen,0,0,50,50);

Pen redpen = new Pen(Color.Red,2);
dc.DrawEllipse(redPen,0,50,80.60);
}

PaintEventArgs是一个派生自EventArgs的类,一般用于传送有关事件的信息。PaintEventArgs有另外两个属性,其中一个比较重要的是Graphics实例,它们主要用于优化绘制窗口中需要绘制的部分。这样就不必调用CreateGraphics(),在OnPaint()方法中获取DC。
在完成我们的绘图后,还要调用基类OnPaint()方法,因为Windows在绘图过程中可能会执行一些他自己的工作。
这段代码的结果与前面的示例结果相同,但当最小化或隐藏它时,应用程序会正确执行。

使用剪切区域
DrawShapes示例说明了在窗口中绘制的主要规则,但它并不是很高效。原因是它试图绘制窗口中的所有内容,而没有考虑需要绘制多少内容。如下图所示,运行DrawShapes示例,当该示例在屏幕上绘制时,打开一个窗口,把它移动到DrawShapes窗体上,使之隐藏一部分窗体。


到现在为止一切正常。但移动上面的窗口时,DrawShapes窗口会再次全部显示出来,WIndows通常会给窗体发送一个Paint事件,要求它重新绘制本身。矩形和椭圆都位于客户区域的左上角,所以在任何时候都是可见的。在本例中不需要重新绘制这部分,而只要重新绘制白色背景区域。但是,Windows并不知道这一点,他认为应引发Paint事件,调用OnPaint()方法的执行代码。OnPiant()不必重新绘制矩形和椭圆。

在本例中,没有重新绘制图形。原因是我们使用了设备环境。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)
{
base.OnPaint(e);
Graphics dc = e.Graphics;

if (e.ClipRectangle.Top < 32 && e.ClipRectangle.Left < 82)
{
Pen bluePen = new Pen(Color.Blue,3);
dc.DrawRectangle(bluePen,0,0,50,50);
Pen redPen = new Pen(Color.Red,2);
dc.DrawEllipse(redPen,0,50,80,60);
}
}

注意:这个结果和前一个结果完全相同,只是进行了早期测试,确定不需要重绘制的区域,提高了性能。还要注意这个是否进行绘图测试是非常粗略的。还可以进行更精细的测试,确定矩形和椭圆是否要重新绘制。这里有一个平衡。可以在OnPaint()中进行更复杂的测试,以提高性能,也可以使OnPaint()代码复杂一些。进行一些测试总是值得的,因为编写一些代码,可以更多的解除Graphics实例之外的会址内容,Graphics实例只是盲目地执行绘图命令。



测量坐标和区域
GDI+使用几个类似的结构来表示坐标或区域。下面介绍几个结构,他们都是在System.Drawing命名空间中定义的。

结构 | 主 要 公 共 属 性|
| Point | X,Y |
| PointF | |
| Size | Width,Height |
| SizeF | |
| Rectangle | Left,Right,Top,Bottom,Width,Height|
| RectangleF | ,X,Y,Location,Size |

(1) Point、PointrF结构
从概念上讲,Point在这个结构中是最简单的,在数学上,它完全等价于一个二维矢量,包含两个公共整形属性,表示它与某个特定位置的水平和垂直距离(在屏幕上),如下图。

为了从点A到点B,需要水平移动20个单位,并向下垂直移动10个单位,在图中标为x和y,这就是他们的一般含义。创建一个Point结构,表示他们:

Point ab = new Point(20,10);
Console.WriteLine("Moved{0} across,{1} down",ab.x,ab.y);
X 和 Y都是读写属性,也可以在Point中设置这些值:

Point ab = new Point();
ab.X = 20;
ab.Y = 10;
Console.WirteLine("Moved{0} across,{1} down",ab.X,ab.Y);
注意:按照惯例,水平和垂直坐标表示为x和y(小写),但对应的Point属性是X和Y(大写),因为在C#中,公共属性的一般约定是名称以大写字母开头。
PointF与Point完全相同,但X和Y属性的类型是float,而不是int。PointF属性用于坐标不是整数值得情况。已经为这些结构定义了数据类型转换,这样就可以把Point隐式转换为PointF(这个转换是结构之间的)。但没有相应的逆过程,要把Point转换为Point,必须显示的复制值或使用下面的3个转换方法:Round(),Truncate(),Ceiling()。

PointF abFloat = new PointF(25.5F,10.9F);

// converting to Point
Point ab = new Point();
ab.X = (int)abFloat.X;
ab.Y = (int)abFloat.Y;
Point ab1 = Point.Round(abFloat);
Point ab2 = Point.Truncate(abFloat);
point ab3 = Point.Ceiling(abFloat);

// but conversion back to PointF is implicit
PointF abFloat2 = ab;

在默认情况下,GDI+把单位看作是屏幕(或打印机,无论图形设备是什么,都可以这样认为)的像素,这就是Graphics对象方法把它们接受到的坐标看作其参数的方式。例如:点 new Point(2010)表示在屏幕上水平移动20个像素,向下垂直移动10个像素。通常这些像素从窗体客户区域的左上角开始测量,如上图。但是,情况并不是如此。在某些情况下,需要以窗口的左上角(包括其边框)为原点来绘图,甚至以屏幕的左上角为原点。除特殊说明,大多数可以假定像素是相对于客户区域的左上角。

(2) Size、SizeF结构
Size结构用于int类型,SizeF用于float类型。
在许多情况下Size结构与Point结构是相同的。有两个整形属性,表示水平和垂直距离----区别是两个属性的名称是:Width和Heihgt。

Size ab = new Size(20,10);
Console.WriteLine("Moved {0} across,{1} down",ab.Width,ab.Height);
严格的讲,Size在数学上与Point表示的含义相同;但在概念上它使用的方式略有不同。Point用于说明实体在什么地方,而Size用于说明实体有多大。但是Size和 Piont是紧密相关的,目前甚至支持他们之间的显示转换:
Point point = new Point();
Size size = (Size)point;
Point anotherPoint = (Point)size;

例如:前面绘制的矩形,其左上角的坐标是(00),大小是(5050)。这个矩形的大小是(5050),可以用一个Size实例来表示。其右下角的坐标也是(5050),但它由一个Point来表示。

Point和Size结构的相加运算符都已经重载了,所以可以把一个Size加到Point结构上,得到另一个Point结构:
static void Main(string[] args)
{
Point topLeft = new Point(10,10);
Size rectangleSize = new Size(50,50);
Point bottomRight = topLeft + rectangleSize;
Console.WriteLine("topLeft = " + topLeft);
Console.WriteLine("bottomRight = " + bottomRight);
Console.WirteLine("Size = " + rectangleSize);
}
运行结果:
topLeft = {X=10,Y=10}
bottomRight = {X=60,Y=60}
Size = {Width=50,Height=50}

这个结果说明Point和Size的ToString()方法已被重写并以{X,Y}的格式显示。
还可以进行Point和Size之间的显示数据类型转换:
Point topLeft = new Point(10,10);
Size s1 = (Size)topLeft;
Point p1 = (Point)s1;
说明:s1.Width被赋予topLeft.X,s1.Height被赋予topLeft.Y的值。最后p1与topLeft的值相同。
接上一章内容
(3)Rectangle 和 RectangleF
这两个结构表示一个矩形区域。与Point和Size一样,这里只介绍Rectangle结构,Rectangle与RectangleF基本相同,但它的属性类型是float类型,而Rectangle的属性类型是int类型。
Rectangle可以看作由一个Point和一个Size组成,其中Point表示矩形的左上角,Size表示其大小。它的一个结构函数把Point和Size作为其参数。
下面重新编写前面DrawShapes示例代码,绘制一个矩形:

Graphics dc = e.Graphics;
Pen bluePen = new Pen(Color.Blue,3);
Point topLeft = new Point(0,0);
Size howBig = new Size(50,50);
Rectangle rectangleArea = new Rectangle(topLeft,howBig);
dc.DrawRectangle(bluePen,rectangleArea);

(4)Region
Region表示屏幕上一个有复杂图形的区域。如下图:

可以想象,初始化Region实例的过程相当复杂。从广义上看,可以指定哪些简单的图形组成这个区域,或者绘制这个区域的边界的路径。这种处理就需要Region类。


在进行更高级的绘图工作前介绍几个调试问题。(有一些帮助大家还是看看吧!)
如果在本章的示例中设置了断点,就会注意到调试图形程序不是那样简单。因为进入和退出调试程序常常会把Paint信息传送给应用程序。结果是OnPaint重载方法上设置的断点会让应用程序反复地绘制本身这样程序就不能完成任何工作。

这是典型的一种情况。要明白程序为什么应用程序没有正确显示,可以在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!!! }
绘制可滚动的窗口---介绍如何绘制的内容不适合窗口的大小,需要做哪些工作。
下面扩展DrawShapes示例,来解释滚动的概念。为了使该示例更符合实际,首先创建一个BIgShapes示例,该示例将矩形和椭圆画大一些。此时将使用Point,Size,Rectange结构定义绘图域,说明如何使用他们。Form1类的相关部分如下所示:

// member fields
private Point rectangleTopLeft = new Point(0,0);
private Size rectangleSize = new Size(200,200);
private Point ellipseTopLeft = new Point(50,200);
private Size ellipseSize = new Size(200,150);
private Pen bluePen = new Pen(Color.Blue,3);
private Pen redPen = new Pen(Color.Red,2);

private override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics.dc = e.Graphics; // member fields
if (e.ClipRectangle.Top < 350 || e.ClipRectangle.Left < 250)
{
Rectangle rectangleArea = new Rectangle(rectangleTopLeft,rectangleSize);
Rectangle ellipseArea = new Rectangle(ellipseTopLeft,ellipse.Size);
dc.DrawRectangle(bluePen,rectangleArea);
dc.DrawEllipse(redPen,ellipseArea);
}
}

注意这里还把Pen、Size、Point对象变成成员字段---这比每次需要绘图时都创建一个新Pen的效率高。

这里有一个问题,图形在300*300像素的绘图区域中放不下。
一般情况下,如果文档太大,不能完全显示,应用程序就会添加滚动条,以便用户滚动窗口,查看其中选中的部分。这是另一个区域,在该区域中如果使用标准控件建立Windows窗体,就让.NET运行环境和基类处理程序。如果窗体上有各种控件,Form实例一般知道这些控件在哪里,如果其窗体可能比较小,Form实例就知道需要添加滚动条。Form实例还会自动添加滚动条,不仅如此,它还可以正确绘制用户滚动到的部分屏幕。此时,用户不需要在代码中做什么工作。但在本章中,我们要在屏幕上绘制图形,所以要帮助Form实例确认何时能滚动。

添加滚动条是很简单的。Form仍会处理所有的操作---因为它不知道绘图区域有多大。在上面的BigShapse示例中没有滚动条的原因是,Windows不知道它们需要滚动条。我们需要确认的是,矩形的大小从文档的左上角(或者是在进行任何滚动前的客户区域左上角)开始向下延伸,其大小应足以包含整个文档。本章把这个区域称为文档区域。在下图可以看出,本例的文档区域应是(250350)像素。

使用相关的属性Form.AutoScrollMinSize即可确定文档的大小。因此给InitializeComponent()方法或Form1构造函数添加下述代码:

private void InitializeComponent()
{
this.AutoScaleBaseSize = new System.Drawing.Size(5,13);
this.ClientSize = new System.Drawing.Size(292,266);
this.Name = "From1";
this.Text = "BigShapes";
this.BackColor = Color.White;
this.AutoScrollMinSize = new Size(250,350);
}

另外,AutoScrollSize属性还可以用VS2005属性窗口设置。
在应用程序启动时设置最小尺寸,并保持不变,在这个应用程序中是必要的,因为我们知道屏幕区域一般是有多大。在运行该应用程序时,这个“文档”是不会改变大小的。但要记住,如果应用程序执行显示文件内容的操作,或者执行某些改变屏幕区域的操作,就需要在其他时间设置这个属性(此时,必须手工调整代码,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)
{
base.OnPaint(e);
Graphics dc = e.Graphics;
Size scrollOffset = new Size(this.AutoScrollPosition);

if (e.ClipRectangle.Top + scrollOffset.Width < 350 || e.ClipRectangle.Left + scrollOffset.Height < 250)
{
Rectangle rectangleArea = new Rectangle(rectangleTopLeft + scrollOffset, rectanlgeSize);
Rectangle ellipseArea = new Rectangle(ellipseTopLeft + scrollOffset,ellipseSize);

dc.DrawRectangle(bluePen ,rectangleArea);
dc.DrawEllipse(redPen,ellipseArea);
}

得到正确的滚动屏幕.



世界\页面\设备坐标

测量相对于文档区域左上角的位置和测量相对于屏幕(桌面)左上角的位置之间的区别非常重要,GDI+为它们指定了不同的名称.
* 世界坐标(Word Coordinate):要测量的点距离文档区域左上角的位置(以像素为单位).
* 页面坐标(Page Coordiante):要测量的点距离客户区域左上角的位置(以像素为单位).

注意: 熟悉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。显示,需要对这些颜色进行索引,才能指定在给定的某个像素上要显示什么颜色。
给颜色进行索引的最常见方式是把它们分为红绿蓝成份,这种方式基于以下原则:人眼可以分辨的任何颜色都是由一定量的红色光、绿色光和蓝色光组成的。这些光称为成份(component)。实际上,如果每种成份的光分为256种不同的强度,它们提供了足够平滑的过渡,可以把人眼能分辨出来的图像显示为具有照片质量。因此,指定颜色时,可以给出这些成份的量,其值在0~255之间,其中0表示没有这种成份,255表示这种成份的光达到最大的强度。

这些出了向GDI+说明颜色的第一种方式。可以调用静态函数Color.FromArgb()指定该颜色的红绿蓝值。微软没有为此提供构造函数,原因是除了一般的RGB成份外,还有其它方式表示颜色。因此,微软认为i给定以的构造函数传递会引起误解:

Color redColor = Color.FromArgb(255,0,0);
Color funnyOrangyBrownColor = Color.FromArgb(255,155,100);
Color blackColor = Color.FromArgb(0,0,0);
Color whiteColor = Color.FromArgb(255,255,255);

3个参数分别是红绿蓝指。这个函数有许多重载方法,其中一些也允许指定Alpha混合指(这是A在方法FromArgb()中的名称)。Aplha混合超出了本章的范围,但把它与屏幕上已有的颜色混合起来,可以描绘出半透明的颜色。这可以得到一些漂亮的效果,常用于游戏。

2)命名颜色

使用FromArgb()构造颜色是一种非常灵活的技巧,因为它表示可以指定人眼睛辨识出的任何颜色。但是,如果要得到一些标准、众所周知的纯色,例如红色或蓝色,命名想要的颜色是比较简单的。因此微软还在Color中提供了许多静态属性,每个属性返回一种命名颜色。在下面的实例中,把窗口的背景设置为白色时,就使用了其中一种属性:

this.BackColor = Color.White;

// has the same effect as;
// this.BackColor = Color.FromArgb(255,255,255);

有几百种这样的颜色。完整的列表参见SDK文档。包括所有的纯色:红、白、蓝、绿和黑,还包括MediumAquamarine、LightCoral、DarkOrchid等颜色。还有一个KnownColor枚举,列出了命名的颜色。

3)图形显示模式和安全的调色板

原则上监视器可以显示超出160亿种RGB颜色,实际上这种取决于如何在计算机上这置显示属性。在Windows中,传统上有3个主要的颜色选项:真彩色(24位)、增强色(16位)、256色。(在目前的一些图形卡上,真彩色是32位的,因为硬件进行了优化,但此时32位中只有24位用于该颜色)。
只有真彩色模式允许同时显示所有的RGB颜色。这听起来是最佳选择,但它是有代价的:完整的RGB值需要用3个字节来保存,这表示要显示的每个像素都需要用图形卡内存中的3个字节来保存。如果图形卡内存需要额外的费用,就可以选择其他模式。增强颜色模式用两个字节表示以像素。每个RGB成份用5位就足够了。所以红色只有32种不同的强度,而不是256种。蓝色和绿色也是这样,总共有65535种颜色。这对于需要偶尔察看照片质量级的图像来说是足够了,但比较微妙的阴影区域会被破坏。
256色模式给出的颜色更少。但是在这种模式下,可以选择任何颜色,系统会建立一个调色板,这是一个从160亿RGB颜色中选择出来的256种颜色列表。在调色板中指定了颜色后,图形设备就只显示所指定的这些颜色。当获得高性能和视频内存需要额外的费用时,才使用256色模式。大多数计算机游戏都使用 这种模式----它们仍能得到相当好的图形,因为调色板经过了非常仔细的选择。
一般情况下,如果显示设备使用增强色或256色模式,并要显示某种RGB颜色,它就会从能显示的颜色池中选择一种在数学上最接近的匹配颜色。因此知道颜色模式是非常重要的。如果要绘制某些涉及微妙阴影区域或照片质量级的图像,而用户没有选择24位颜色模式,就看不到期望的效果。如果要使用GDI+进行绘制,就应该用不同的颜色模式测试应用程序。

4)安全调色板

这是一种非常常见的默认调色板。它工作的方式是为每种颜色成分这置6个间隔相等的值,这些值分别是0,51102153204255。换言之,红色成分可以是这些值中的任一个。绿色成分和蓝色成分也一样。所以安全调色板中的颜色就包括(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的绘图方法即可。

<注>:
如果使用以前的GDI编程,可能会注意到在前两个示例中,在GDI+中使用Pen的方式是不同的.在GDI中,一般是调用一个WindowsAPI函数SelectObject(),它把钢笔关联到设备环境上.这个钢笔用于所有需要钢笔的绘图操作中,直到再次调用SelectObject()通知设备环境停止使用它时为止.这个规则也适用于画笔或其它对象,例如字体和位图,而使用GDI+,微软使用一种无状态的模式,其中没有默认的钢笔或其它帮助对象.只需给每个方法调用指定合适的帮助对象即可.

<1>画笔

GDI+有几种不同类型的画笔,这里只解释几个比较简单的画笔,每种画笔都由一个派生自抽象类System.Drawing.Brush的类实例来表示.最简单的画笔System.Drawing.SolidBrush仅指定了区域用纯色来填充:

Brush solidBeigeBrush = new SolidBrush(Color.Beige);
Brush solidFunnyOrangeyBrownBrush = new SolidBrush(Color.FromArgb(25,155,100));

另外,如果画笔是一种Web安全颜色,就可以用另一个类System.Drawing.Brushes构造出画笔.Brushes是永远不能实例化的一个类(它有一个私有构造函数,禁止实例化).它有许多静态属性,每个属性都返回指定颜色的画笔.如下:

Brush solidAzureBrush = Burshes.Azure;
Brush solidChoolateBrush = BrushesChoolate;

比较复杂的一种画笔是影线画笔(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);
// background color of CrossBrush is black
Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick,Color.DarkGoldenrod,Color.Cyan);

GDI只能使用实践和影线画笔,GDI+添加了两种新画笔:
* System.Drawing.Drawing2D.LinearGradientBrush用一种在屏幕上可变的颜色填充区域.
* System.Drawing.Drawing2D.PathGradientBrush与此类似,但其颜色沿着要填充的区域的路径而变化.

<2>钢笔

钢笔只使用一个类System.Drawing.Pen来表示.但钢笔比画笔复杂一些,因为它需要指定线条应有多宽(像素),对于一条比较宽的线段,还要确定如何填充该线条中的区域.钢笔还可以指定其他许多属性,本章不讨论它们,其中包括前面提到的Alignment属性,该属性表示相对于图形的边框,线条该如何绘制,以及在线条的末尾绘制什么图形(是否使图形光滑过度).

粗线条中的区域可以用纯色填充,或者使用画笔来填充.因此Pen实例可以包括Brush实例的引用.这是非常强大的,因为这表示可以绘制有影线填充或线性阴影的线条.构造Pen实例有四中不同的方式.可以通过传送一种颜色,或者传送一种画笔.这两个构造函数都会生成一个像素宽的钢笔.另外,还可以传送一种颜色或画笔,以及一个表示钢笔宽度的float类型的值.(该宽度必须是一个float类型的值,以防执行绘图操作的Graphics对象使用非默认的单位,例如毫米或英寸,例如可以指定宽度是英寸的某个分数).例如可以构造如下的钢笔:

Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick,Color.DarkGoldenrod,Color.Cyan);
Pen solidBluePen = new Pen(Color.FromArgb(0,0,255));
Pen solidWideBluePen = new Pen(Color.Blue,4);
Pen brickPen = new Pen(brickBrush);
Pen brickWidePen = new Pen(brickBrush,10);

另外,为了快速构造钢笔,还可以使用类System.Drawing.Pens,它与Brushes类一样.包括许多存储好的钢笔.这些钢笔的宽度都是一个像素,使用通常的Web安全颜色,这样就可以用下述方式构建一个钢笔:

Pen solidYellowPen = Pens.Yellow;



绘制图形和线条System.Drawing.Graphics有很多方法,利用这些方法可以绘制各种线条、空心图形和实心图型。下图给出了只要方法。


在结束绘制简单对象的主题前,用一个简单示例来说明使用画笔可以得到的各种可视效果。该实例是ScrollMoreShapes,它是ScrollShapes的修正版本。除了矩形和椭圆外,我们还添加了一条粗线,用各种定制的画笔填充图形。前面解释了绘图的规则,所以这里只给出代码,而不作多的注释。首先,因为添加了新画笔,所以需要指定使用命名空间System.Drawing.Drawing2D:

using System;
using System.Collection.Generic;
using System.ComponentModel;
usgin System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Text;
using System.Windows.Forms;

接着是Form1类中的一些额外字段,其中包含了要绘制图形的位置信息,以为要使用的各种钢笔和画笔:
private Rectangle rectangleBounds = new Rectangle(new Point(0,0),new Size(200,200));
private Rectangle ellipseBounds = new Rectangle(new Point(50,200),new Size(200,150));
private Pen bluePen = new Pen(Color.Blue,3);
private Pen redPen = new Pen(Color.Red,2);
private Brush solidAzureBrush = Brushes.Azure;
private Brush solidYellowBrush = new SolidBrush(Color.Yellow);
static private Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick,Color.DarkGoldenrod,Color.Cyan);
private Pen brickWidePen = new Pen(brickBrush,10);

把BrickBrush字段声明为静态,就可以使用该字段的值初始化BrickWidePen字段了。C#不允许使用一个实例字段初始化另一个实例字段,因为还没有定义要先初始化哪个实例字段,如果把字段声明为静态字段就可以解决这个问题,因为只实例化了Form1类的实例,字段是静态字段还是实例字段就不重要了。

你可能感兴趣的:(C#)