第二章教程19:怎样实现菜单


本次教程内容:

  • 地图对象的外部定义
  • 异常处理机制
  • 菜单控制的实现


我们在事件定义的结构上略做修改,把地图的基本参数定义也加进来,这样就可以完全取消主函数中逐个读取地图的硬编码。一张地图,既有属性,又有函数,封装起来,很有对象的感觉了。

设计结构做如下调整:

#snake
@mode=2
@x=40
@y=19
hitWall(){
    jump(start);
}
load(){
    init();
}
length(10){
    jump(snake2,20);
}

为了简单起见,参数定义加一个特殊标记@开头。写起来明确,计算机读起来也方便。
我们设计了一个按名字存储这些参数的对象:

class ParmList{
    map<string, string> data;
  public:
    void addParm(string aLine){
        int i=0;
        string str1, str2;
        str1= readBefore(aLine, i, '=');
        str2= aLine.substr(i+ 1);
        setValue(str1, str2);
    }
    void setValue(string aKey, string aValue){
        data.insert(make_pair(aKey, aValue));
    }
    string getValue(string aKey, string aDefault=""){
       map<string, string>::iterator it = data.find(aKey);
       if (it != data.end()){
           return it->second;
       }  else {
           return aDefault;
       }
    }
    void clear(){
        data.clear();
    }
};

在内部使用了map来存储信息,实现了addParm函数,专门用于拆分这种“=”连接的参数定义形式。

一旦我们让客户来输入什么,有一件事就不得不做了,那就是异常处理。因为你不能假设客户能够输入格式正确的数据。我们的代码应当在客户输入了错误的数据时给出尽量明确的提示,而不是无声无息地崩溃掉。

c++的异常处理包括两个机制:
机制1:抛出异常
抛出异常就是非常简单的一个电话,我这里出错了。之所以用抛出,就是表现了本地不再做任何处理的意思。
机制2:捕获异常
捕获异常,通过两个语句来实现
try 语句(做好准备):我们预见到这里可能会抛出个异常,让我们提高警惕。
catch语句(接住异常):果然异常来了,让我们看看是什么,准备处理它。

我们看具体的代码:
异常抛出者(BasicMap类的loadMapFromFile函数):

    void loadMapFromFile(string aFileName){
        ifstream fin(aFileName.c_str());
        if (fin) {
            while (!fin.eof()){
                string str1;
                getline(fin, str1);
                mapInfo.push_back(str1);
            }
        } else {
            // 这里抛出异常
            throw "Map <"+ aFileName+ "> not exists.";
        }
        fin.close();
    }


    
让我们看看精灵在面对遇到异常时具体都在干什么?
精灵2(异常抛出者):(打电话)文件工作组么?请打开这个文件,把读取接口与fin房间中的对象对接,谢谢。
文件工作组:已办妥,请接收。
精灵2:什么?这就是你们干的活么?接口是空的?
文件工作组(电话已挂断)
精灵2:(打电话)喂,最高监听组么?我投诉!
最高督查组:这里是无人应答电话,听到滴声后请留言。滴。
精灵2:我这里遇到一个情况,我只能告诉你一句话:“文件不存在”。(throw)
(自言自语)这就是我知道的全部内容了,我已经仁至义尽了,其他事情我就都不管了,再见。
(挂掉电话,隐身)

异常捕获者(MapManager类的addMap函数):

    BasicMap* addMap(string aName, int ax, int ay, int aMode){
        aName= lowCase(aName);
        if (mapList.count(aName)== 0){
            BasicMap *pmap;
            if (aMode== 1){
                pmap= new MapRpg();
            } else {
                pmap= new MapSnake();
            } 
            pmap->mm= this;
            pmap->x= ax;
            pmap->y= ay;
            pmap->mode= aMode;
            try {
                // 准备在这里捕获异常
                pmap->loadMap(aName, dataPath+ aName+ ".txt");
            } catch (const string msg){
                // 捕获到了字符串异常,直接显示并退出程序
                cerr << msg << endl;
                exit(1);
            }
            mapList.insert(make_pair(aName, pmap));
            return pmap;
        } else {
            return mapList[aName];
        }
    }

让我们看看精灵在处理异常时是怎么做的。
精灵1(异常处理者):(自言自语)现在我们打算做一件有风险的事情,弄不好会出错,出错了会有人投诉,把最高督查的电话先接过来。(try)
精灵1:(施展魔法)地图对象的loadMap,创建分身!(精灵2出现,并得到执行代码)
精灵1:(打电话)精灵2,请按这个文件名,载入地图。
精灵2:好的,请稍....
(电话已经挂断了)
精灵1:(迷惑中)干好了么?这是啥情况?
这时,最高督查组电话响了。
精灵1:不忙接,我们看看电话留言是什么。
电话显示:收到一条文字信息。
精灵1:如果是文字信息的话,那就归我管。(catch)
(自言自语:不过我不关心它的内容)
精灵1:(打电话)控制台,我们收到一条文字信息,可能是个错误,请把它显示出来给用户看。
控制台:我们已经通过标准错误输出显示了。
精灵1:等等,那个“标准错误输出”是啥?
控制台:其实也就是显示在屏幕上了。
精灵1:OK,我希望用户已经明白出了什么问题。想让我们继续干活,就把该提供的东西弄正确再来!退出程序。

我希望通过上面的模拟对话过程,用户能明白异常处理的机制。

下面让我们来设计菜单的实现。菜单的实现,包括五部分内容:菜单的信息载入,菜单的触发,菜单显示,菜单的控制,菜单的隐藏。设计的重点是,每部分功能的职责放在哪里。

一、菜单的信息的载入:
1、仿照事件定义的脚本结构,设计出菜单定义的脚本结构。

#main
重新开始(){
    jump(start);
}
退出(){
    quit();
}


“#”后面是菜单的名字,然后每个类似函数的结构,函数名是菜单选项,而函数的内容是点击选项后执行的动作。
这套设计与事件定义几乎完全相同,读取逻辑也基本一致。同样,如果菜单有属性,也可以在函数定义的前面添加。

2、载入的行为,由MapManager执行。
3、菜单的存储,由MapManager负责

二、菜单的触发:
1、键盘增加对ESC的监听,通过escMap事件来触发菜单
2、事件的执行,只能由MapManager来执行

三、菜单的显示
1 一个方框,通过调用函数drawWindow来显示
2 方框内多个菜单条目,通过调用函数textOut来显示
3 当选条目的标记,通过设置背景色高亮的方式来表现

四、菜单的职责
1 打开菜单后,当前地图类不能继续接收键盘信息,也不再处理自动任务
2 键盘信息由MapManager直接转交Menu类处理
3 菜单有自己独立的键盘处理函数
4 菜单负责记录自己的事件,但不负责执行
5 菜单能够看见调用它的地图,菜单所有事件均通过地图来执行

五、菜单的控制
1 上下键切换当选条目,在菜单对象内操作,切换后显示刷新
2 ESC键无选项关闭菜单,触发escMenu事件,实际由MapManager来执行,通过所在地图局部刷新来实现
3 回车/空格键选择当前条目对应的事件,并执行脚本,具体谁来执行,根据动作来决定


drawWindow和textOut两个函数,作用分别是在屏幕上显示一个有边框的矩形区,和从某个位置显示文本。
这两个函数具有一定的工具性质,比如除了在现实菜单上用到之外,在显示对话,以及战斗画面上也能用到。
但它们与tools.cpp中的普适工具函数的性质还是有所不同的,又更加具体了一些。
我们把它们单独放在一个cpp文件中,它只能看到tools.cpp。
菜单类的实现,放在menu.cpp中,它可以看到basicmap.cpp,tools.cpp,share.cpp
原来的map.cpp可以上面这个两个源代码。

新的代码逻辑,用三个图进行描述:源代码关系图,核心类图,菜单操作时序图。
从菜单操作时序图看,信息流在很大程度上由MapManger和Map对象负责传递。这部分工作,实际上独立与地图的具体功能实现,最好能由一个专门的机制来集中处理。
另外,MapManger还负担了创建读取地图和菜单的工作,此外地图类也负责了地图信息的读取。这导致这两个类与外部文件的具体格式绑定得太紧密。如果能由一个专门的机制负责创建对象,而地图和管理器只负责运行业务。在结构上会显得更加清晰。


课程小结:

随着功能的不断增加,系统架构的早期设计方案已经面临巨大压力。如果这时候我们来看设计模式,就回发现其中很多模式正是为了解决这些问题而提出的。

欢迎加入编程教学讨论群:102494165

本教程每节课的源代码,统一下载地址
链接:https://pan.baidu.com/s/1q4aoYesre1PHaCoV8gkhDQ 
提取码:8den 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

圣手书生肖让

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值