第6章 面向对象思想+事件驱动结构
先来看一下这个东西吧:http://v.youku.com/v_show/id_XNTk2NzExMjg4.html。
看完之后应该会觉得这个东西的结构非常复杂,这是笔者做过的最复杂的项目之一。由于是给公司开发的,所以和上一章一样不能公开源代码,但是会举一些简单的例子说明。
刚接到这个项目了解了大体功能后,第一反应是用“界面函数”的结构。确实,这个东西是非常适合用界面函数完成的,但是由于当时笔者正在自学C++,于是用C++的思路分析了一下这个项目:
1、总共有4个界面,而且有两个和列表框好像啊。
2、几乎所有的动作都是由旋转编码器触发的。
经过一番思考之后,笔者决定做一次尝试,用面向对象的思想加上事件驱动的机制完成它。
下面就介绍一下这种结构,也希望能借此说明“对象”和“事件”的概念。
1. 对象和事件
基本上每个面向对象语言的书都会把对象的概念说一下。在这个项目里有4个界面,把这四个界面看作四个对象,这四个对象的所有动作都由事件驱动。
什么又是“事件”呢?简单来说“一个对象发生了某个事情”就是这个对象的某种事件。事件一定是基于某个特定的对象而言的,不能简单地说“发生了某个事情”,应该说“某个对象发生了某个事情”。而我们要做的就是确定“每个对象有哪些事情会发生”,并完成“某个对象在发生某个事情时要做的事”。
所以对于一个对象而言,它应该有:
- 1、与它对应的事件函数,用于执行“某个事件发生时要做的事”,一个对象所拥有的这些函数的个数和它可能发生的事件数是相等的。
- 2、完成上述函数所需要的辅助函数。
这些函数有些可能是公共的,是由系统提供的API或者其他工具函数;
也可能是这个对象特有的,是对这个对象做的某种更改。 - 3、每个对象都有自己的属性,这些属性在程序中的体现就是变量。
这里面,1和2的函数本质上都是一样的,都是这个对象所包含的函数。不同的是:
与事件相对应的函数称它为这个对象的“事件”,这些函数数量与这个对象的事件数是相等的;
完成某种特定操作的函数称它为这个对象的“方法”,也就是“这个对象可以做的事”。并且这些方法中,有些是允许被外界调用的,有些只允许在本对象内使用,所以又分为“公共方法”和“私有方法”。
下面就以一张表来说明一个对象内部的组成,以及各个成员的含义:
有了这些了解之后我们就可以开始构建整个系统了。
2. C语言对一个对象的封装
在C++ 中有专门的对象结构,它可以把对象里的函数和变量分为公共、私有等类型。
而在C语言中没有这样的结构,我们通过使用上的约定也可以达到同样的效果。比如一个内部的函数,我们约定有些函数外部可以调用,它就是公共函数;约定有些函数外部不要调用,它就是私有函数。
下面是对象的各个部分在C语言中的形式及意义:
公共方法和事件本质上都是对外公开的函数。公共方法完成对本对象的某一主动的操作,而事件是完成对外部被动的响应。
甚至可以把事件从对象的组成中取出来,把事件归到系统范畴。
下面就以一个简单的列表框的例子说明如何封装一个对象,假设这个列表框(对象)的名字叫做List1,一共只有3个列表项,每个列表项都有一个自己的名称和内容(0~255),有一个光标指示当前选中列表项,分配的事件有“列表项+1”“列表项-1”“列表项内容+1”“列表项内容-1”,并且这些改变是可以循环的(255+1=0,0-1=255)。
假设屏幕显示的网格是4行16列,外部提供的绘图函数有:
Disp(r,c,unsigned char *) //往r行c列写入一个字符串
Clear() //清屏
有了这些条件后,对这个对象的封装如下:
- List1.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
|
在其他地方只要包含这个List1.c文件就可以调用该对象里的各种方法和事件函数了,里面的私有方法和私有变量虽然约定外部是不要使用的,但是外部确实是可以调用的。所以也可以为这个对象写一个List1.h文件,只把公开的部分进行声明:
- List1.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
如果采用了List1.h的话,要把List1.c文件里重复定义的部分给去掉。
每个对象有每个对象的特点,它们差别很大,构建方法也是大不相同的。但是每个对象构建好之后就是一个模板,它是非常独立的,在其他地方只要把代码直接复制过去做少量更改就可以使用了。
3. 事件分配机制
一个工程中会有多个对象,每个对象都有一些可能会发生的事件,这些事件函数是由系统调用的,由系统来判断什么对象发生了什么事件。
比如在这个项目中,基本的事件有:左编码器按下、右编码器按下、左编码器左旋、左编码器右旋、右编码器左旋、右编码器右旋、左编码器按下左编码器左旋、左编码器按下左编码器右旋、右编码器按下右编码器左旋、右编码器按下右编码器右旋、串口接收数据事件、IIC接收数据事件。
由于旋转编码器的驱动本身用的就是外部中断,外部中断对单片机来说就是一种意外事件。所以只要在中断里判断当前进行的是什么操作,并记录当前哪个界面正在被使用,就可以调用相应对象的相应事件了。
然而,并不是所有的对象都一定具有所有的这些事件,根据该对象功能的需要,选用一部分有用的事件进行响应。比如在IIC界面里不考虑串口,这个界面就只响应IIC接收数据事件,而忽略串口接收数据事件。
事件并不一定非要由外部中断产生,也可能是系统虚拟的。总之事件分配有系统完成,系统利用各个资源抽象出事件概念,然后分配到相应对象上。
下面是一个完成事件分配的例子:
有两个按键,任意一个按键按下都会触发int0中断。假设共有2个对象,下面是事件分配的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
采用FocusNum来记录当前激活的对象。
在此想说明的是,k0和k1两个操作都是放在一个中断里的,具体产生什么事件则是再次通过代码判断的,这些代码属于系统层,是根据实际需要对事件的抽象。
比如如果需要产生“两个按键同时按下”事件,则可按如下方式分配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
甚至可以继续改,实现“k0按下时k1按下”“k1按下时k0按下”“k0长按”“k0连按”等事件,这里就不一个个实现了。
总之只要系统层能够识别的动作都可以抽象成事件。
4. 系统层构建
在以前的编程中从来没把程序这么明确地分层,可能是因为这次项目比较复杂,并且用来面向对象的方法,所以这种结构自然就产生了。在各个对象中有一些公共方法,这些函数完成特定的功能,而他们都依赖于底层的支持。
当然可以让对象直接操作驱动函数,从最底层开始。但是这样的话,一方面用起来会很麻烦,另一方面可能不是所有的底层功能都会用到。
所以让系统层根据上层对象的需要把这些功能封装,并向上层提供使用接口。这是系统层做的事情之一。
另外系统层也会构建一些控制逻辑,这些功能并不在底层有实体的驱动函数,它是系统在软件层面抽象出来的。比如当前系统的数据显示进制、背光灯时间等,它们不对应底层的操作,仅在系统层抽象。对于上层的对象来说,根本不需要考虑这些,只要调用系统层提供的API就行了。
此外,系统层要做的当然还有事件分配,上一节介绍的事件分配系统是系统层核心的一部分,而系统层还有其他很多功能。事件分配系统是利用各个资源抽象出事件概念,并分配给各个对象。而本节讨论的系统层是利用各个资源,在底层驱动的支持下,根据需要构建出一些控制逻辑,并封装成系统API,供上层软件使用。
综上,系统层做的事情有:
1、构建事件分配系统。
2、对底层驱动封装,并向上层提供操作接口。
3、根据需要再构建一些其他控制结构,并向上提供接口。
5. 库函数
库函数也属于对系统对底层的封装。某些功能可能比较复杂,可以构成一整套体系,那么就可以把这些函数归位一类,作为完成某个功能的库函数。
这点和类似于操作系统的GTK库或者QT库,它们提供了大量的绘图函数。
在本项目中,使用了绘图库,它就是在底层对液晶屏操作的基础上建立的文字和图片显示函数,提供了在指定位置写入字符、汉字,以及反色写入、垂直镜像写入等功能。上层的对象调用这些函数会非常方便。
总结一下库的作用:利用系统层的API,或者跳过系统层直接调用驱动函数,构建出自己的一套控制逻辑,并对外提供基于这个控制逻辑的函数库。