一个显示模型
一种图形及图形用户界面直接对应屏幕显示的思想。其基本概念先天就是图形化的(而且都是二维的,适应计算机屏幕的矩形区域),这些基本概念包括坐标、线、矩形和圆等。从编程的角度看,其目的是建立内存中的对象和屏幕图像的直接对应关系。
其基本模型如下:我们利用图形系统提供的基本对象(如线)组合出更复杂的对象;然后将这些对象添加到一个表示物理屏幕的窗口对象中;最后,用一个程序将我们添加到窗口上的对象显示在屏幕上,我们可以将这个程序看做屏幕显示本身,或者是一个“显示引擎”,或者是“我们的图形库”,或者“GUI库”,甚至将其看做“在屏幕背后进行画图工作的小矮人”,然后在屏幕上画出我们要添加到窗口的对象。
#include "Simple_windows.h" #include "Graph.h" int main() { using namespace Graph_lib; Point tl(100,100); Simple_windows win(tl, 600,400, "Canvas"); Polygon poly; poly.add(Point(300,200)); poly.add(Point(350,100)); poly.add(Point(400,200)); poly.set_color(Color::red); win.attach(poly); win.wait_for_button(); }
坐标系
计算机屏幕是一个由像素组成的矩形区域,像素是一个可以设置为某种颜色的点。在程序中,最常见的方式就是将屏幕建模为由像素组成的矩形区域,每个像素由x(水平)坐标和y(垂直)坐标确定。最左端的像素的x坐标为0,向右逐步递增,直到最右端的像素为止:最顶端的像素y坐标为0,向下逐步递增,直到最底端的像素为止。注意,y坐标是“向下增长”的。这可能有点奇怪,特别是对数学家而言。但是,“屏幕(窗口)大小各异,左上角可能是不同屏幕的唯一共同之处了,因些将其设定为原点。
按照惯例,main()函数包含我们要(直接或间接)执行的代码及例外处理:
int main() { try { //... here is our code... } catch(exception& e) { // some error reporting return 1; } catch(...) { // some more error reporting return 2; } }
为了使此main()编译能过,我需要定义exception。如果像往常一样包含了头文件std lib facilities.h或者直接包含标准库头文件<stdexcept>,就会获得exception的定义。
坐标轴
Axis xa(Axis::x, Point(20,300),280,10, "x axis"); win.attach(xa); win.set_label("Canvas #2"); win.wait_for_button();
具体操作步骤为:创建坐标轴对象,将其添加到窗口,最后进行显示,如右图所示。可以看到,Axis::x是一条水平线,其上有指定数量(10个)的刻度和一个标签"x axis"。通常,标签用于解释坐标轴和刻度的含义。
绘制函数图
现在,我们已经有了一个包含坐标轴的窗口,因此看起来画出一个函数是个好主意。我们创建一个形状来表示正弦函数,并将它添加到窗口:
function sine(sin, 0, 100, Point(20,150),1000,50,50); win.attach(sine); win.set_label("Canvas #4"); win.wait_for_button();
在这段代码中,名为sine的function对象使用标准库函数sin()产生的值绘制一条正弦曲线。
Polygon函数图是表示数据的一种方法。Polygon(多边形)描述为一个点的序列,这些点通过线连接起来就构成Polygon类。第一条线连接第一个点到第二个点,第二条线连接第二个点到第三个点......最后一条线连接最后一个点到第一个点:
sine.set_color(Color::blue); Polygon poly; poly.add(Point(300,200)); poly.add(Point(350,100)); poly.add(Point(400,200)); poly.set_color(Color::red); poly.set_style(Line_style::dash); win.attach(poly); win.set_label("Canvas #5); win.wait_for_button();
屏幕是矩形,窗口是矩形,一张纸也是一个矩形。实际上,现实世界中很多形状都是矩形(或者至少是圆角矩形),原因在于矩形是最容易处理的形状。
Rectangle r(Point(200,200),100,50);
填充
前面绘制形状都是绘制轮廓,我们也可以使用某种颜色“填充”形状:
r.set_fill_color(Color::yellow); poly.set_style(Line_style(Line_style::dash,4));
任何封闭的形状都可以填充。矩形很特殊,它非常容易填充。
文本
最后,任何一个绘图系统都不可能完全没有简单的文本输出方式 --将每个字符看做线的集合来绘制,并保证不会剪切掉字符。
Text t(Point(150,150), "Hello, graphical world!");
利用此例中的基本图形元素,你可以生成任何复杂、微妙的显示效果。请注意本章所有代码的一个共同特点:没有循环和选择语句,而且所有数据都是“硬编码的”。输出内容只是基本图形元素的简单组合。一旦我们开始使用数据和算法来组合这些基本图形,就可以得到更复杂、更有趣的输出效果了。
我们还可以从文件中加载图片:
Image ii(Point(100,50), "image.jpg");
基类和派生类
让我们从一个更为技术性的角度来观察基类和派生类。当设计一个图形接口库时,我们依赖以下3个关键的语言机制:
(一)派生(derivation):从一个类构造另一个类的方法,使新构造的类可以替换原来的类。派生类除了自己的成员外,还包括基类的所有成员。这通常称为继承,因为派生类“继承”了其基类的所有成员。在某些上下文环境中,派生类称为子类,而基类称为父类。
(二)虚函数(virtual function):在基类中定义一个函数,在派生类中有一个类型和名称完全一样的函数,当用户调用基类函数时,实际上调用的是派生类中的函数。
(三)私有和保护成员(Private and protected member):我们保持类的实现细节为私有的,以保护它们不被直接访问,简化维护操作,这通常称为封装。
继承、运行时多态和封装的使用,实际上就是面向对象程序设计最常见的标志。因此,除了其他的程序设计网络之外,C++还直接支持面向对象程序设计。
对象布局
对象在内在中是如何布局的呢。一个类的成员定义了对象的布局:数据成员在内存中一个接一个地存储。当使用继承时,派生类的数据成员被简单地放在基类的成员之后。
类的派生和虚函数的定义
我们通过在类名后给出一个基类来指定一个类为派生类。
struct Circle::Shape { /*....*/};
默认情况下,结构体(struct)的成员都是公有的。基类中的公用成员也会成为结构体的公有成员。另一种等价的定义方式如下:
class Circle:public Shape {public:/*......*/};
一个虚函数必须在类的声明中被声明为virtual,但是如果你把函数定义放在类外,关键字virtual就不必且不能出现在那里了。例如:
struct Shape { //... virtual void draw_lines() const; virtual void movd(); //... }; virtual void Shape::draw_lines() const {/**.../} //error void Shape::move() {/*...*/} //OK
当你希望覆盖一个虚函数时,必须使用与基类中完全相同的名字和类型。例如:
struct Circle: Shape { void draw_lines(int) const; void drawlines() const; void draw_lines(); //... }
访问
C++为类成员访问提供了一个简单的模型。类的成员可以是:
(一)私有的:如果一个成员是私有的,它的名字只能被其所属类的成员使用。
(二)受保护的(protected):如果一个成员是受保护的,它的名字只能被其所属类及其派生类的成员使用。
(三)公有的(public):如果一个成员是公有的,它的名字可以被所有函数使用。
纯虚函数
一个抽象类是一个只能作为基类的类。我们使用抽象类来表示那些抽象的概念,即相关实体共性的一般化所对应的那些概念。在程序中,抽象类通常定义了一组相关类(类层次)的接口。