作者:King Dark (AKA 小虾 ) 2007年10月6日
SGOS,全称Simple Graphical Operating System,是一个PC多任务图形操作系统。顾名思义,该系统的开发目的估计是对国产图形系统研究的一个尝试,并且计划集中一群OS爱好者来共同开发它。在这里我们要介绍一下SGOS里的精髓部分。
近年来,国内的民间操作系统一浪接一浪地登上了中国的互联网上,各具特色。但是可能其中的大部分操作系统开发都是为了个人学习或者个人强大的兴趣,很多人都在自己开发的系统上面实现了进程管理、内存管理以及文件系统,却还是停留在字符界面之中。也有不少人尝试去对立开发一个GUI(Graphical User Interface,图形用户界面),有的也因为对GUI工作原理了解不透而无从下手。国内对于GUI原理的资料还是比较少的,甚至在国外也比较难找。资料的缺乏使到程序感到无能为利,看到像Windows、KDE等强大的GUI就感觉那是一座不可攀越的高峰。哈哈,当然,这都是笔者的猜测而已,相信你可能对GUI的实现早已胸有成足了。OK,下面笔者就带领大家了解一下SGOS GUI的实现原理。
首先我们来分析一下做一个GUI系统到底需要些什么?
看下面是一个简单的Windows应用程序的运行界面。我们从其界面可以看出,这个对话框由两部分组成,分别是标题栏和客户区部分。
图(1)
在标题栏里,我们又可以拆分为一个程序图标Icon,一段标题文字的标签,还有几个常用按钮(最小化,最大化,关闭窗口)。在GUI里,我们把这些图标、标签、按钮等统称为元素。当然GUI元素还有很多,并不是单一指这一些。同样,在客户区里,我们可以看到它由一段文字(可以看作是标签或者文本框)和两个按钮组成(窗体背景不算一个元素)。元素是允许包含另一个元素的,即一个元素可以由另外几个元素组成,例如刚才所提及的标题栏和客户区都是元素,你也可以称他们为“大元素”。好了,现在我们总结一下这些元素和元素之间的关系:
下图:
此时你可能会想到,我们还可以划分得更细啊,就是把按钮、图标、标签、文本框这些元素再划分。OK,我们试试看这些元素能够划分成什么。
我们先简单地想象,对于一个按钮来说,它可以由一个填充矩形和一段文字组成,而一个图标其实就是一张图片,当然它可能有边框。对于我们常见的文本框,它也是由一个填充矩形和一段文字组成。强大的文本框还有滚动条,我们又可以划分它的滚动条。例如一个垂直滚动条由一个上滚按钮、下滚按钮和中间的一个拖动按钮,这些按钮又能够分成一个填充矩形和一段文字。哈哈,这就好了,我们发现这些元素最终都是由图形、文字、图片这三样东西组成的,那么我们可以画另外一个图,这次从小到大看看。
上图告诉我们,从图形、文字、图片这些基本元素可以直接或间接衍生出按钮、滚动条、文本框、图片框这些东西。于是我们就说“一切都是图形、文字、图片而已”,哈哈,或许再细一点,一切都是点了,再细一点,一切都是粒子,再细一点,一切都是虚无……
这也算是面向对象编程的一种,这些元素之间互相继承,继承者具有被继承者的结构和表达。
好了,到现在,我们好像还是不知道如何下手,那么我们来看一下SGOS GUI里是如何实现的。SGOS GUI由内核态部分和用户态部分组成,其中在SGOS里内核态部分叫做GFX,这部分主要是提供一些基本的作图函数、图形数据缓冲区的申请和释放、以及窗体的管理和显示。在用户态部分里主要实现的是一些按钮Button、标签Label、文本框Text Box、还有窗口Form之类的基本控件。用户在编写GUI程序时候,无需去写这些代码了,因为SGOS封装了用户态部分代码在System库中,即System.dll。下面我们看一个GUI的简单程序:
#include <System.h> using namespace System;
class OKButton: public Button{ public: OKButton(BaseWindow* parent):Button( parent, string("Hello"), 0 ){} virtual int OnMouseUp(int button, Point* p ){ MessageBox::Show(string("Hello World.")); Button::OnMouseUp(button,p); } };
class MainForm: public Form{ private: OKButton* buttonHello; public: MainForm():Form( (BaseWindow*)0, string("Hello"), 0 ){ } virtual ~MainForm(){ Application::Exit(); }
virtual int OnLoad(){ Move( 100, 200, 400, 350 ); buttonHello = new OKButton(GetClient()); buttonHello->Move( 20, 20 ); buttonHello->Show(); }
};
extern "C" int Entry() { Application::Run(new MainForm()); }
|
这是一个Hello world的小程序,如果你没试过在SGOS上编写程序,这段代码对你来说可能要吃点力。Entry()是程序入口,然后创建一个MainForm类执行。我们先不管SGOS库是怎么封装这些类的,创建MainForm后,系统会在内核注册一个window,然后得到一个指针就交给MainForm类管理,MainForm类是继承于Form类的,而Form类又继承于BaseWindow类。
在BaseWindow类里,封装了对一个window的基本操作,例如改变位置、大小、标题等,也包含了图形缓冲区bitmap的相关信息。在注册一个window时候,往往会同时注册一个bitmap,并使window与bitmap进行关联。你只需要了解window是记录窗体的位置、大小、类型和风格等属性信息的,bitmap则是记录窗体上要显示的图形数据。通过对bitmap的作图,就可以改变这个window显示的内容了。
BaseWindow类是窗体基本类,即所有其他例如按钮、文本框、标签这些空间都是由它派生出来的。而这些派生出来的东西都具有BaseWindow的属性和操作。
Form就是我们所说的窗口,是继承于BaseWindow类的,当然一个窗口也像按钮这些基本空间一样具有大小位置的属性了。然后就是Form本身还包含有标题栏的控件(例如程序图标的图片框、程序名的标签、还有窗口的常用按钮)和Client客户区窗体,但是对于菜单以及客户去内里的窗体是程序员自己动态添加到里面的。
MainForm继承于Form,这样用户编写MainForm时候就可以直接得到Form::OnLoad()事件的响应。
SGOS里的各个窗体,除了桌面,都是具有从属关系的。例如Button属于Form,Form属于桌面。这些从属关系在创建窗体的时候指定,如上述代码种创建Button,其中GetClient()是获取MainForm的客户区,即Button直接从属于Client,然后Client直接从属于Form。这和我们之前谈到的窗口由按钮等控件组成的原理是一致的。
说说window。window是GFX里存储窗体信息的结构,在用户态表现为指针。其中window包含什么信息相信你已经知道了,因为GFX里这个window的结构成员比较多,这里不一一解释了。
//Window来保存一个窗体的信息 typedef struct tagWindow{ PProcess
owner; //所属进程 int width,
height; //窗体的宽和高 int xPos,
yPos; //窗体位置 char text[TITLE_LENGTH]; //文本内容
PBitmap winBitmap; //窗体相关位图 struct
tagWindow* pre; //并列前一个窗体 struct
tagWindow* next; //并列后一个窗体 struct
tagWindow* child; //子窗体链 struct
tagWindow* parent; //父窗体 int
visible; //是否可以显示? int
flag; //窗体样式 WF_BACKGROUND WF_ALWAYSONTOP WF_ALWAYSONBOTTOM int
wholeDirty; //脏,是否需要刷新“整个”显示缓冲区? int
showMouse; //是否显示鼠标图标,
0:不显示 1:显示普通 -1:自定义 PBitmap
mouseIcon; //鼠标图标 PFont
font; //font to use PRectList
redrawList; //重画的矩形链表 t_32
windowData[WINDOWDATALENGTH]; int
enabled; //可用 PBitmap
backGround; //背景位图 int
alpha; //窗体透明度 struct
tagWindow* activeChild; //指向活动的子窗口 int
jumpingChildren; //说明子窗体是否允许自动跳链,即激活时候自动置前。 }Window, *PWindow; |
GFX管理的就是window和bitmap这两个东西,而一般创建了一个window都会同时创建一个对应的bitmap,但创建一个bitmap不一定意味着一定要有window。例如一个游戏程序里需要读入大量的图片,则可以用bitmap来存储。GFX里用树的结构形式来组织这些window,树根是桌面。
在SGOS调试控制台里按F7可以得到下面一个图:
Draw Window Tree:
-Win:| Addr:80E51168 Text:Screen [PID:0] Pos(0,0)
-Win:| Addr:81028C40 Text:Demo2() [PID:2] Pos(125,125)
-Win:| Addr:810AEF98 Text:Client [PID:2] Pos(5,25)
-Win:| Addr:810AEAC8 Text:Demo2() [PID:2] Pos(5,5)
-Win:| Addr:81027D00 Text:Hello World! [PID:2] Pos(95,95)
-Win:| Addr:810286F8 Text:Client [PID:2] Pos(5,25)
-Win:| Addr:810B0008 Text:TextBox [PID:2] Pos(5,100)
-Win:| Addr:810AFB38 Text:Close [PID:2] Pos(300,36)
-Win:| Addr:810AF4C8 Text:Say Hello [PID:2] Pos(300,10)
-Win:| Addr:81028208 Text:Hello World! [PID:2] Pos(5,5)
-Win:| Addr:810267D0 Text:Console [PID:0] Pos(5,5)
-Win:| Addr:810271A0 Text:Client [PID:0] Pos(5,25)
-Win:| Addr:810276C8 Text:TextBox [PID:0] Pos(3,3)
-Win:| Addr:81026CB8 Text:Console [PID:0] Pos(5,5) |
-Win:| Addr:80E51168 Text:Screen [PID:0] Pos(0,0) 这一行里,Win表示一个window结构,Addr表示它的内存地址,Text表示窗体的标题,PID是创建该窗体的进程,Pos表示窗体当前位置。Text内容为Screen的就是桌面。从该树目录中,我们可以知道,有三个窗口在桌面。Text为 Client的就是窗口的客户区,其中还有另一个窗体我们不难猜出它就是标题栏了(这里标题栏省略显示它包含的其他元素)。在Hello World! 窗口中,客户区有一个TextBox,一个Close按钮和一个Say Hello的按钮。
从这个树结构中,我们可以很清晰地看到各个窗体的从属关系。在SGOS里,销毁一个窗体时,则其该窗体子树下所有其他的窗体也会被销毁。
通过了解了GUI的结构组成之后,下面我们再讨论一下怎么画窗体。用户程序里在创建完毕一个窗体后,可以通过调用该窗体类里的show()函数将该窗体显示出来。那么这一个显示过程是如何进行的呢?
SGOS里的每个窗体都有自己的缓冲区,在窗体树低一层的窗体把自己的图形缓冲区的内容拷贝到高一层的窗体(父窗体),然后高一层的窗体再拷贝自己的图形内容到再高一层的窗体(大父窗体),接着再高一层的窗体再拷贝自己的图形内容到更高一层的窗体(超级父窗体)。一直拷贝到桌面为止,这样显示桌面就可以看到所有内容了。这如此一来我们会发现,这样地拷贝效率可能不高,例如当两个在树同一层的窗体重叠时,一个底层的更新了,而高层的没有更新,则这样做不是覆盖了高层的吗?一个窗体更新之后,父窗体又更新整个窗体,这样效率显然很低,其实我们只要更新修改过的部分就可以了。当同一层重叠时候,我们也进行判断把重叠部分去掉不重画就可以。这样效率不是大大提高了吗?嗯,开发者Huang Guan早就想过这个问题,并且用了一个很巧妙的脏算法实现窗体区域的重画。原理还是没变,都是从树底层往上提交,但是指明了重画区域。算法比较复杂,大家可以参看Kernel/Gfx/WindowRedraw.c实现文件。
现在你应该知道了,当窗体的显示函数被调用时,该窗体会在它的图形缓冲区里绘制要显示的内容,可以是图形、一段文字或者一张图片。绘制完毕后,必须要提交一个通知给父窗体,告诉父窗体哪一个区域更新了。如果这个区域在父窗体中有一部分不可显或者被遮挡的,GFX会适当裁减或者拆分这个区域再提交。
首先要明确一点,在你调用窗口的显示函数之前,保证窗口里的其他需要显示控件(例如按钮、文本框)已经调用了显示函数,使得各个控件的图形都绘制到了窗口的图形缓冲区了。这样当调用窗体显示的函数之后,GFX就把窗体要显示的区域拷贝到桌面,最后显示到屏幕上。
OK,相信到这里你已经对GUI如何显示图形也了解了。下面我们再讨论另外一个重点,关于GUI的交互。
现在来说,用户和窗口进行交互一般都通过鼠标和键盘。而GUI是怎样响应鼠标和键盘事件的呢?
在SGOS里,GUI通过系统提供的一套消息机制来响应这些事件。在GFX被初始化的时候,已经注册了鼠标和键盘的响应函数,当然这是系统级响应,具体还没发送到窗口去。假设用户把鼠标放在一个按钮上移动,则GFX会收到鼠标移动的消息,然后通过鼠标所在的位置来判断到底把该消息发送给哪个窗体,其实系统是把消息发送给进程的,消息指明了接收该消息的窗体。在进程进入运行时候,进程获取该消息,再把消息转交给对应的窗体的响应处理函数。在窗体的消息处理函数里,例如XXXWindow::OnMessage( Message msg ),处理函数会判断这个是什么消息,然后再响应对应的事件,例如OnMouseMove或者OnButtonClick,又或者OnKeyDown等。
哈哈,要做一个GUI可真不容易哦。难怪Huang Guan说这个GUI也算是SGOS的精髓部分了。其实SGOS的GUI虽然也是以Simple为主的,但也和一般的GUI一样具有比较全的功能,例如字体可以使用不同的大小和颜色,窗体背景可以是图片,文本框也支持滚动条。
本文能够介绍的就这么多了,因为本文主要是谈SGOS GUI的原理,而不是深入去研究SGOS GUI的代码。所以如果你打算实现一个GUI,可以参考一下SGOS的代码。如果你觉得GUI的实现是小菜一碟啦,就可以去看一下Qt的实现方式,其实跟SGOS也没很大差别,不过功能多了。
希望本文能够对有点作用啦。由于时间创促,可能部分内容写得不是很明确,也可能有错别字,请见谅!
最后附上SGOS的网站地址和几张截图。
比较早的SGOS GUI截图。
后来改进后:
如今的SGOS GUI: