17.7 使用Shape类
本节介绍图形库的一些基本工具:Simple_window、Window、Shape、Text、Polygon、Line、Lines、Rectangle、Function、Color、Line_style、Point、Axis。目的是让你知道这些工具能够实现什么功能,而并非详细理解某个类。下一章将会介绍每个类的设计与实现。
下面来学习一个简单的程序,我们将逐行解释代码,并给出每一行代码在屏幕上的显示效果。在程序运行时,你会看到当我们向窗口添加形状以及改变已有形状时,屏幕上图像的变化情况。大体上,我们是通过分析程序的执行情况来“动画演示”代码的流程。
17.7.1 图形头文件和主函数
首先,我们包含定义了图形和GUI工具接口的头文件:
或者
你可能已经猜到,Window.h包含与窗口有关的工具,Graph.h包含在窗口上绘制形状(包括文本)的有关工具,这些工具都定义在Graph_lib名字空间中。为简化起见,我们使用名字空间指令,使得Graph_lib中的名字可以直接在程序中使用。
照例,main()函数包含我们要(直接或间接)执行的代码及例外处理:
main()函数进行编译时,必须已定义了exception。如果我们照例包含了std_lib_facilities.h,就会得到exception,否则我们会从标准头文件处开始直接处理,此时需要包含<stdexcept>。
17.7.2 一个几乎空白的窗口
在这里,我们不讨论错误处理(参见第5章,特别是5.6.3节),直接进入main()函数中的图形代码:
这段代码首先创建一个Simple_window,即一个有“Next”按钮的窗口,并将它显示在屏幕上。显然,为了得到Simple_window对象,我们应该包含头文件Simple_window.h而不是Window.h。在这里,我们明确给出了窗口在屏幕上的显示位置:其左上角位于Point{100, 100}。这个位置很接近屏幕的左上角,但没有过于靠近。很显然,Point是一个类,其构造函数有两个整型参数,表示点在屏幕上的(x, y)坐标。我们可以将代码写为:
然而,为了便于多次使用点(100, 100),我们还是选择给它一个符号名称。600和400分别是窗口的宽度和高度,Canvas是在窗口框上显示的标签。
为了真正将窗口绘制在屏幕上,我们必须将控制权交给GUI系统。我们通过调用win.wait_for_button()来达到这一目的,结果如下:
在窗口的背景中,我们看到了一个笔记本电脑的桌面(已经临时清理过了)。如果你对桌面背景这种不相关的事情感到好奇,我可以告诉你,我拍摄照片时正站在安提布的毕加索资料馆附近俯瞰尼斯湾。隐藏在程序窗口之后的黑色控制台窗口是用来运行我们的程序的。控制台窗口不太美观,而且也不是必需的,但当一个尚未调试通过的程序陷入无限循环或无法继续执行时,我们可以通过它来终止程序。如果你仔细观察,会发现我们使用的是微软C++编译器,当然你可以使用其他的编译器(如Borland或者GNU)。
在之后的介绍中,我们将去掉程序窗口周围分散注意力的内容,仅仅给出窗口本身:
窗口的实际尺寸(以像素计算)依赖于屏幕的分辨率。某些屏幕的像素要比其他屏幕更大。
17.7.3 坐标轴
一个几乎空白的窗口没有什么意思,最好给它添加一些信息。希望添加些什么内容呢?注意:并不是所有的图形都是有趣的或者是关于游戏的,我们将从坐标轴——一种严肃的、有点复杂的图形开始。一个没有坐标轴的图形通常是很难看的。没有坐标轴的帮助,我们通常难以弄清数据的含义。或许你可以借助伴随的文字来解释,但使用坐标轴要保险得多;人们通常不会阅读文字描述,而且好的图形表示通常与其语境是分离的。因此,图形需要坐标轴:
操作步骤为:创建坐标轴对象,将其添加到窗口,最后进行显示:
可以看到,Axis::x是一条水平线,其上有指定数量的“刻度”(10个)和一个标签“x axis”。通常,标签用于解释坐标轴和刻度的含义。我们通常把x轴放在窗口底端附近。在实际应用中,我们更喜欢用符号常量来表示高度和宽度,这样“在底端上方附近”就可以用y_max-bottom_margin这样的符号表示,而不是用300这样的“魔数”(参见4.3.1节和20.6.2节)。
为了帮助识别程序的输出,我们用Window的成员函数set_label()将该窗口的标签重新设置为“Canvas #2”。
现在,添加一个y坐标轴:
我们将y轴和标签的颜色分别设置为cyan和dark_red(只是为了展示一些工具的使用)。
我们并不认为x轴和y轴使用不同颜色是一个好主意。这里只是为了说明如何设置形状或者其中某个元素的颜色。使用很多种颜色未必是个好主意,特别是初学者更容易热衷使用很多颜色。
17.7.4 绘制函数图
接下来做什么呢?现在,我们已经有了一个包含坐标轴的窗口,因此看起来画出一个函数是个好主意。我们创建一个形状来表示正弦函数,并将它添加到窗口:
在这段代码中,名为sine的Function对象使用标准库函数sin()产生的值绘制一条正弦曲线。我们将在20.3节详细讨论如何绘制函数图。现在,你只需知道,绘制函数图时必须给出起始点的位置和输入值集合(值域),并且还需给出一些信息来说明如何将这些内容塞入窗口(缩放)。
请注意在到达窗口右边界时曲线是如何停止的。当我们绘制的点超出窗口矩形区域时,将被GUI系统简单忽略掉,永远不会真正显示出来。
17.7.5 Polygon
函数图是表示数据的一种方法,在第20章将会看到更多实例。我们还可以在窗口中绘制不同类型的对象:几何形状。我们使用几何形状来进行图形演示,可以表示用户交互组件(如按钮),通常还能使演示更加生动。多边形(Polygon)被描述为一个点的序列,这些点通过线连接起来就构成Polygon类。第一条线连接第一个点到第二个点,第二条线连接第二个点到第三个点,以此类推,最后一条线连接最后一个点到第一个点。
这段代码首先展示了如何改变正弦曲线的颜色。然后,与17.3节的例子一样,我们添加了一个三角形,作为一个多边形的例子。然后我们再次设置了颜色,最后设置了线型。Polygon的线都有“线型”,默认线型为实线,但我们也可根据需要改为虚线、点状线等(参见18.5节)。这段程序显示如下图形:
17.7.6 Rectangle
屏幕是矩形,窗口是矩形,一张纸也是一个矩形。实际上,现实世界中有很多形状都是矩形(至少是圆角矩形)。原因在于矩形是最容易处理的形状。例如:矩形易于描述(左上角和宽度、高度,或者左上角和右下角,诸如此类),易于判断一个点在矩形之内还是之外,易于用硬件快速绘制像素构成的矩形。
与其他封闭的形状相比,大多数高级图形库能够更好地处理矩形。因此,我们将矩形类Rectangle从多边形类Polygon中独立出来。一个Rectangle可以用左上角坐标、宽度和高度来描述:
由此可得:
请注意,将位置正确的四个点连接起来并不一定得到一个Rectangle。当然,在屏幕上创建一个看起来像Rectangle的Closed_polyline是很简单的(你甚至可以创建一个看起来像是Rectangle的Open_polyline),例如:
实际上,poly_rect对应的屏幕图像(image)是一个矩形。但内存中的poly_rect对象并不是一个Rectangle对象,而且它也不“知道”有关矩形的任何内容。验证这一点的最简单方法是再添加一个点:
矩形是不会有5个点的:
对于我们分析程序非常重要的一点是:Rectangle不仅仅是在屏幕上看起来像是一个矩形而已,它还应该从根本上保证此形状(几何意义上)始终是一个矩形。这样,我们编写代码时就可以信赖Rectangle——它确实表示屏幕上的一个矩形,而且保证不会改变为其他形状。
17.7.7 填充
前面绘制形状都是绘制轮廓,我们也可以使用某种颜色“填充”一个矩形:
我们还觉得对三角形(poly)当前的线型不满意,所以将其线型设置为“粗(正常线型的4倍粗细)虚线”,我们也改变了poly_rect(现在已经看起来不像是矩形了)的线型。
如果你仔细观察poly_rect,你会发现轮廓是在填充色上层显示的。
任何封闭的形状都可以被填充(参见18.9节)。矩形很特殊,它非常容易填充(填充速度也非常快)。
17.7.8 Text
最后,任何一个绘图系统都不可能完全没有简单的文本输出方式——将每个字符看作线的集合来绘制,并保证不会剪切掉字符。我们已经展示了如何为窗口和坐标轴设置标签,但我们也能使用Text对象将文本放置在任何位置。
利用此例中的基本图形元素,你可以生成任何复杂、微妙的显示效果。请注意本章所有代码的一个共同特点:没有循环和选择语句,而且所有数据都是“硬编码的”。输出内容只是基本图形元素的简单组合。一旦我们开始使用数据和算法来组合这些基本图形,就可以得到更复杂、更有趣的输出效果了。
我们已经看到过如何改变文本的颜色了:坐标轴的标签(参见17.7.3节)本身就是一个Text对象。此外,我们还可以为文本选择字体和字号:
这段代码将Text的字符串“Hello, graphical world!”中的字符放大到20号字,字体设置为粗体Times。
17.7.9 Image
我们还可以从文件中加载图片:
执行上述代码,将在窗口中显示文件名为image.jpg的照片,照片中两架飞机正在突破音障:
这幅照片比较大,我们刚好把它放在了文本和图形上层。因此,为了清理窗口,我们将它稍微移开一点:
请注意不在窗口区域之内的部分图片是如何被简单忽略掉的。超出窗口区域的内容都会这样被图形系统“剪裁”掉。
17.7.10 更多未讨论的内容
下面代码展示了图形库更多的特性,在这里不再进行详细解释:
你能猜出这段代码显示什么内容吗?是不是很容易猜?
代码与屏幕显示内容的关联是很直接的。如果你还未看出这段代码是如何产生这样的输出的,请继续学习后续章节,很快就会搞清楚的。请注意我们是如何使用ostringstream(参见11.4节)来格式化输出尺寸的文本对象的。