绘制可滚动的窗口---介绍如何绘制的内容不适合窗口的大小,需要做哪些工作。
下面扩展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,ellipseSize);
dc.DrawRectangle(bluePen,rectangleArea);
dc.DrawEllipse(redPen,ellipseArea);
}
}
注意这里还把Pen、Size、Point对象变成成员字段---这比每次需要绘图时都创建一个新Pen的效率高。
这里有一个问题,图形在300*300像素的绘图区域中放不下。
一般情况下,如果文档太大,不能完全显示,应用程序就会添加滚动条,以便用户滚动窗口,查看其中选中的
部分。这是另一个区域,在该区域中如果使用标准控件建立Windows窗体,就让.NET运行环境和基类处理程序。如
果窗体上有各种控件,Form实例一般知道这些控件在哪里,如果其窗体可能比较小,Form实例就知道需要添加滚动
条。Form实例还会自动添加滚动条,不仅如此,它还可以正确绘制用户滚动到的部分屏幕。此时,用户不需要在代
码中做什么工作。但在本章中,我们要在屏幕上绘制图形,所以要帮助Form实例确认何时能滚动。
添加滚动条是很简单的。Form仍会处理所有的操作---因为它不知道绘图区域有多大。在上面的BigShapse示例
中没有滚动条的原因是,Windows不知道它们需要滚动条。我们需要确认的是,矩形的大小从文档的左上角(或者
是在进行任何滚动前的客户区域左上角)开始向下延伸,其大小应足以包含整个文档。本章把这个区域称为文档区
域。在下图可以看出,本例的文档区域应是(250,350)像素。
使用相关的属性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);
}
得到正确的滚动屏幕.