项目描述:一个MP4系统中有许多的按钮、选项。点击这些按钮需要调用许多本地磁盘中的图片、视频、音乐等各种类型的资源文件。
解决办法:
A,做一个文件系统,比如树形文件系统,在这个系统中按照一定规则嵌套文件夹,不同资源文件按一定规则命名并存放在适当位置,系统做一个文件遍历系统,在不同的点击事件中遍历并提取不同位置的资源文件。
B,做一个XML文件,在这里面建立一个网状或链状数据结构,在XML的每个文件节点上存放每个文件的名字、地址、点击事件响应方法、类型、属性等一系列的标签。项目系统调用分析这个XML文件从而实现对所有资源文件的DIY控制。
分析:
这两个办法我都是深入的做过了,最早的时候还没有接触到XML,在项目中我建立了树形文件系统来组织资源文件,这块的开发与逻辑的编写,花了我们项目组不少的脑力和时间。首先是做了一个对本地文件系统的定位、遍历、提取等一系列文件操作的库;之后开发出来了一个文件名命名法则,对文件的定位、类型描述、功能描述都抽象进文件的名字里去,这有点类似单片机里的控制字;最后编写调用逻辑,上一步,下一步,回退等各种逻辑。整个这块实现代码量就达到了近千行,花了半个多月。
后来接触了XML,才发现以前的办法是多么的落后与笨重,整个这个模块开发出来,我前后只花了不到3天,代码量也就百行左右。易维护,轻量易懂。
项目案例:模拟一个MP4系统,一进系统弹出一个菜单,点击播放音乐按钮跳转到音乐播放界面,在点击音乐名播放音乐。
我依然使用openframeworks来实现这一套东西,openframeworks自带XML文件解析函数。
为了理解,我做了几个粗糙的UI:
主菜单:位置为:c:\xxxx\mp3Btn.png,c:\xxxx\mp4Btn.png,c:\xxxx\setBtn.png三个图片叠加而成。
点击音乐后的子菜单:位置为:c:\xxxx\musicMenu.png
实现的功能大致能看出来了,就是一个简单的MP4,点击音乐进入音乐界面,点击音乐名字就播放相应的音乐。为了节约时间我只举音乐事件的例子。
一,来看看我的XML文档中的数据结构mySetting.xml:
<FILEBROWSER>
<BEGIN>
<file>
<name>mp3Btn_mp4Btn_setBtn</name>
</file>
</BEGIN>
<FILE>
<name>mp3Btn</name>
<type>pic</type>
<address>C:\\xxxx\\mp3Btn.png</address>
<poseX>0</poseX>
<poseY>0</poseY>
<poseW>600</poseW>
<poseH>200</poseH>
<focusXA>0</focusXA>
<focusXB>600</focusXB>
<focusYA>0</focusYA>
<focusYB>200</focusYB>
<jump>musicMenu_nullMusic1</jump>
</FILE>
<FILE>
<name>mp4Btn</name>
<type>pic</type>
<address>C:\\xxxx\\mp4Btn.png</address>
<poseX>0</poseX>
<poseY>200</poseY>
<poseW>600</poseW>
<poseH>200</poseH>
<focusXA>0</focusXA>
<focusXB>600</focusXB>
<focusYA>200</focusYA>
<focusYB>400</focusYB>
<jump>videoMenu</jump>
</FILE>
<FILE>
<name>setBtn</name>
<type>pic</type>
<address>C:\\xxxx\\setBtn.png</address>
<poseX>0</poseX>
<poseY>400</poseY>
<poseW>600</poseW>
<poseH>200</poseH>
<focusXA>0</focusXA>
<focusXB>600</focusXB>
<focusYA>400</focusYA>
<focusYB>600</focusYB>
<jump>setMenu</jump>
</FILE>
<FILE>
<name>musicMenu</name>
<type>pic</type>
<address>C:\\xxxx\\musicMenu.png</address>
<poseX>0</poseX>
<poseY>0</poseY>
<poseW>600</poseW>
<poseH>600</poseH>
</FILE>
<FILE>
<name>nullMusic1</name>
<type>pic</type>
<address>C:\\xxxx\\nullMusic1.png</address>
<poseX>0</poseX>
<poseY>0</poseY>
<poseW>0</poseW>
<poseH>0</poseH>
<focusXA>0</focusXA>
<focusXB>600</focusXB>
<focusYA>0</focusYA>
<focusYB>200</focusYB>
<jump>music1</jump>
</FILE>
<FILE>
<name>music1</name>
<type>mp3</type>
<address>C:\\xxxx\\xxx.mp3</address>
</FILE>
</FILEBROWSER>
它的主干是这样的:
<FILEBROWSER>
<BEGIN>
<file>
<name>A_B_C</name>
</file>
</BEGIN>
<FILE>
<name>A</name>
<type>XX</type>
<address>XX</address>
<poseX>XX</poseX>
<poseY>XX</poseY>
<poseW>XX</poseW>
<poseH>XX</poseH>
<focusXA>XX</focusXA>
<focusXB>XX</focusXB>
<focusYA>XX</focusYA>
<focusYB>XX</focusYB>
<jump>XX</jump>
</FILE>
<FILE>
......
</FILE>
.
.
.
.
</FILEBROWSER>
<FILEBROWSER>是主节点,下面有2个子节点BEGIN和FILE,先看<FILE>节点:
这个节点中我存放了11个标签,分别存放名字、类型、地址、坐标、点击区域和点击后跳转到下一节点的名字。
<BEGIN>节点里面存放系统第一次进入的最开始的UI图片,考虑到一进系统可能有多个菜单显示和多个点击区域,所以我特意加了一个这个<BEGIN>节点,A_B_C为同时出现的三个按钮用下划线隔开。(其实去掉<BEGIN>这个节点完全也可以)要点是:
1,每个节点存储一个资源文件,在节点内用统一的标签描述其情况。
2,设置<jump>标签,用来指示点击事件的跳转目标,当有多个跳转目标时用下划线隔开。
3,其中nullMusic1是一个奇葩的文件,但是确实本数据结构的一大精髓,nullMusic1指向的图片文件是一个1X1像素的空白图片,叠加的大小也设置为0,意思就是在系统中这个图片是不显示的,但是它却有点击区域和下一跳。所以这样做的目的是你可以做一个空白的空图片来任意指定界面中某个区域为点击区域并且有点击跳转。(想象一下我可能会在一张图片中做一百个按钮,然后用一百个空图片文件节点去设置每一个点击区域,这样就不用单独做一百个按钮了。)
二,有了XML文件结构,那我们就来调用XML为我们服务吧。
这是实现系统主菜单的第一段程序:
ofXml XML;//openframeworks自带的XML解析器
vector<map<string, string>>fileVec;//用来存放文件各种标签数据的向量vector<ofImage>imgVec;//存放图片数据
ofSoundPlayer _sound;//音频加载器
if (XML.load("mySettings.xml"))
{cout << "mySettings.xml loaded!" << endl;
}else {
cout << "unable to load mySettings.xml check data/ folder" << endl;
}if (XML.exists("BEGIN"))//找BEGIN节点
{
XML.setTo("BEGIN[0]");//设置解析器位置到第一个BEGIN节点处
if (XML.getName() == "BEGIN"&&XML.setTo("file[0]")){
do {
//这里做一个map用来让系统显示主菜单的三个按钮
map<string, string> map;
map["jump"] = XML.getValue<string>("name");//获取到的是mp3Btn_mp4Btn_setBtn
map["focusXA"] = "0";//构造伪点击区域
map["focusXB"] = "10";
map["focusYA"] = "0";
map["focusYB"] ="10";
fileVec.push_back(map);
//文件数据存进fileVec
} while (XML.setToSibling());//遍历
}
mousePressed(5, 5, 0);//扔出点击事件,模拟点击伪点击区域,之后的点击事件响应中处理这个系统一开始运行的点击事件。
}
</pre><p></p><pre>
这段代码写的很肤浅,构造了一个伪文件和一个伪点击事件,这样做后面解释。
三,接下来是点击事件响应代码:
void ofApp::mousePressed(int x, int y, int button){
for each (map<string, string> var in fileVec)//遍历<span style="font-family: Arial, Helvetica, sans-serif;">fileVec</span>
{
if (x < atoi((var["focusXB"]).c_str()) && x>atoi((var["focusXA"]).c_str()))
{
if (y < atoi((var["focusYB"]).c_str()) && y>atoi((var["focusYA"]).c_str())) {//判断点击区域
jump = var["jump"];//jump是一个全局字符串变量用来存放‘下一跳’
fileVec.clear();
imgVec.clear(); _sound.clear();
while (1) {
XML.setToParent();//解析器回到开始处
if (XML.exists("FILE"))//找FILE节点
{
XML.setTo("FILE[0]");//定位到第一个FILE节点
if (XML.getName() == "FILE")
{
if (jump.find('_') > jump.length())//‘下一跳’为单个文件
{
//cout << "无分割下划线" << endl;
do {
string name = XML.getValue<string>("name");
if (name.compare(jump) == 0)//找到‘下一跳’一致的文件进行提取
{
addMap();
}
} while (XML.setToSibling());//遍历
break;
}
else {//‘下一跳’为多个文件的情况
//cout << "有分割下划线" << endl;
do {
string name = XML.getValue<string>("name");
if (name.compare(jump.substr(0, jump.find('_'))) == 0)//提取下划线分隔开的第一个下一跳名字与文件名比较
{
addMap();
}
} while (XML.setToSibling());
jump = jump.substr(jump.find('_') + 1, jump.length() - jump.find('_') - 1);//去掉第一个下划线及其之前的字符串再赋值给jump
}
}
}
}
break;
}
}
}
}
其中addMap()是这样的:
void addMap() {
map<string, string> map;
map["name"] = XML.getValue<string>("name");
map["type"] = XML.getValue<string>("type");
map["address"] = XML.getValue<string>("address");
map["jump"] = XML.getValue<string>("jump");
map["poseX"] = XML.getValue<string>("poseX");
map["poseY"] = XML.getValue<string>("poseY");
map["poseZ"] = XML.getValue<string>("poseZ");
map["poseW"] = XML.getValue<string>("poseW");
map["poseH"] = XML.getValue<string>("poseH");
map["focusXA"] = XML.getValue<string>("focusXA");
map["focusXB"] = XML.getValue<string>("focusXB");
map["focusYA"] = XML.getValue<string>("focusYA");
map["focusYB"] = XML.getValue<string>("focusYB");
fileVec.push_back(map);
if (XML.getValue<string>("type") == "pic") {
ofImage imgS;//图片加载器
imgS.load(XML.getValue<string>("address"));//加载图片
imgVec.push_back(imgS);
}
if (XML.getValue<string>("type") == "mp3") {
_sound.load(XML.getValue<string>("address"));//加载音频
}
}
四,接下来就要在窗口画出菜单按钮和播放音频等操作了:
在draw()方法里面的适当位置写如下代码:
int i = 0;
if (fileVec.size() != 0) {
for each (map<string, string> var in fileVec)//遍历文件容器
{
if (var["type"].compare("pic") == 0)
{
if(imgVec.size() != 0){
imgVec[i].draw(atoi((var["poseX"]).c_str()), atoi((var["poseY"]).c_str()), atoi((var["poseW"]).c_str()), atoi((var["poseH"]).c_str()));//用i来记录图片在imgVec中的位置,保证每一个图片都要画出来
}
if (i<imgVec.size() - 1) {
i++;
}
}
if (var["type"].compare("mp3") == 0)
{
_sound.play();//播放音乐
}
}
}