本专栏文章列表
一、何为面向对象
二、C语言也能实现面向对象
三、C++中的不优雅特性
四、解决封装,避免接口
五、合理使用模板,避免代码冗余
六、C++也能反射
七、单例模式解决静态成员对象和全局对象的构造顺序难题
八、更为高级的预处理器PHP
九、Gtkmm的最佳实践
本系列文章由 西风逍遥游 原创,转载请注明出处:西风广场 http://sunxfancy.github.io/
九、Gtkmm的最佳实践
在跨平台的gui开发中,Qt一直是非常受欢迎的GUI开发框架,但Qt一个是依赖反射,需要特殊的预处理步骤,一个是库太过庞大,这就造成了一些不便的地方。今天介绍给大家的是Gtk库的C++绑定,Gtkmm,一个方便的跨平台GUI开发框架。
由于是C++的封装,GTK不再那么的难以使用,变得简洁优雅,而且效率非常高,编译也较QT快许多。
虽然C也能编写,而且我们之前也介绍过了GObject的使用。但比较其实现起来较为繁琐,代码行数较C++多一些,而且每个成员函数都要手动传入this指针,较为不便。
现在C++如果合理的封装和按照之前的设计思想进行设计,结构十分紧凑,而且书写非常方便,非常易用。
Gtkmm版的2048程序设计
为了更好的实践,我们举一个简单的2048小游戏的程序作为实例。大家会发现,合理的设计,能够使代码既清晰明了,又方便维护,可靠性很高。
我们简要的进行一下程序设计,这里我们不是要学会2048如何制作,而是要体会程序设计中的思想,以及设计中的美感和艺术感。
首先,2048作为一个简单的小游戏,广受大家喜欢,原理很简单,在一个4×4的数组中,让数字不断向各个方向合并,每次进行后,随机位置创建新数字。
程序界面,一个窗口,上面一行标签书写当前得分,下面一个绘图界面,自由绘图,画上4×4个的矩阵,上面书写内容。
程序结构设计,按照一般程序架构设计,可以用MVC的结构,一个界面类,负责显示,一个控制类,负责游戏逻辑,一个模型类,负责数据的存储与管理。
但由于数据的管理太过简单,就放弃了模型类,直接使用一个4×4的矩阵就完成任务。
程序实践
由于Gtkmm的良好封装,我们并不需要太多复杂的处理,首先是Main文件引导程序的启动,所有gtk程序集合都是这样引导。
1 | /* |
Game类作为最核心的窗口类,也是游戏的主要控制类,并不需要暴露什么方法给外部成员使用,所以它的定义很简单: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/*
* @Author: sxf
* @Date: 2015-05-19 11:20:42
* @Last Modified by: sxf
* @Last Modified time: 2015-05-20 21:58:35
*/
class Game_private;
class Game : public Gtk::Window
{
public:
Game();
virtual ~Game();
private:
Game_private* priv;
};
这种写法,就是在前几章提到的增强代码封装性的方法,通过一个priv指针,解决了C++封装不完善的问题。
这样还有一个很大的好处就是,由于priv指针的书写较为繁琐,如果在public方法中,反复的通过priv指针调用函数,就会显得无比麻烦,但这正提醒你,你的写法有问题,因为一般的方法,要尽可能写成内部的private的,这样你在不自觉的时刻,就形成了最大化private方法,最小化接口的设计习惯,这对于提升程序的内聚性,很有意义。
于是我们的Game类的内部定义就变得十分复杂,但这就使得代码内聚性更高,暴露给外层的接口就更简单。
1 | class Game_private |
我不喜欢比脸还长的函数,但这里的函数设计的还是不是那么尽如人意,虽然如此,这里也是本着简单易懂的方式设计的。
这里的combine方法设计的很特殊,因为合并时,还有可能出现游戏胜利的情况,所以里面包含了判断胜利的条件。
1 | bool combine(int i, int j, int k) { |
这个函数设计的很健壮,考虑了许多边界条件,这么做是在模拟物体碰撞时,碰撞面不断挤压的情况。例如下面的情况:
1 1 2 4
0 0 0 0
0 0 0 0
0 0 0 0
向左合并,能够一次就被合成为8,但这也是和外层的合并顺序控制是分不开的
在游戏主循环控制时,是这样处理的,对于不同的方向,循环顺序是不一样的: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
46void gameRun(int k) {
bool winflag = false;
switch (k) {
case 0 :
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
if (combine(i,j,k)) winflag = true;
break;
case 1 :
for (int j = 3; j >= 0; --j)
for (int i = 0; i < 4; ++i)
if (combine(i,j,k)) winflag = true;
break;
case 2 :
for (int i = 3; i >= 0; --i)
for (int j = 0; j < 4; ++j)
if (combine(i,j,k)) winflag = true;
break;
case 3 :
for (int j = 0; j < 4; ++j)
for (int i = 0; i < 4; ++i)
if (combine(i,j,k)) winflag = true;
break;
}
// 判断胜负条件
if (winflag) {
gameWin();
return;
}
if (!randomNew()) {
gameOver();
}
Glib::RefPtr<Gdk::Window> win = game->get_window();
if (win)
{
m_area.setData(data);
Gdk::Rectangle r(0, 0, 600, 600);
win->invalidate_rect(r, false);
m_area.show();
char score_text[20]; memset(score_text, 0, 20);
sprintf(score_text, "Score : %d", score);
m_score.set_text(score_text);
}
}
而创建新数字的方式也很清晰,但这里使用模拟栈的方式进行了处理。
设计很独特,由于目前的位置数目有限,直接rand的方式,效率较低,我们先扫描所有可能的位置,然后将其入栈,random时,直接找一个位置,然后直接随机从其中找一个就可以了。数组模拟栈的方式,主要是希望避免vector的低效率,实现较简易。而且扩展较方便,如果你想random跟多,修改起来也十分方便。
1 | bool randomNew() { |
渲染类十分简单,主要就是根据数组中的数值,渲染出对应的图像,设计思想就是不断将问题抽象,不断简化,将复杂的问题从上层一层层拨开,这样就使得结构更加简洁优雅。
整个项目完整代码已经放到Github上了,需要的可以参考:
【github仓库】