注:文章里面有不少个人见解,欢迎大家一起互相讨论。希望高人能给予相应理解与意见建议。
在实际3D游戏开发中,编辑器是极其重要的一个部分,一个优秀健壮的编辑器,可以使项目事半功倍,而相反,一款BUG超多(随时会挂)又不注重操作习惯(完全基于快捷键,又没有详细的使用说明)的编辑器,不仅会使项目事倍功半,而且会削弱开发人员的积极性,甚至让开发人员对项目产生排斥情绪。
编辑器在游戏里面应用很广泛,一般都有地图编辑器(关卡、世界)、粒子编辑器、动画编辑器、字体编辑器(单机里面较多)、UI编辑器、材质编辑器、脚本编辑器等等,编辑器设计制作方法也大致可分为两个趋势,一种是倾向于做大而全的世界编辑器,一种是做小而精的功能编辑器,在这里我不想讨论这两者的利与弊,我只能说,只要这个解决方案可以解决我们当前的问题,那么它就是一个适合现阶段的解决方案,但并不一定是最好的解决方案。
一、工具
现在制作编辑器,流行以下几种方式:
1、 使用C#制作基于WinForm的编辑器。
2、 制作基于MFC的编辑器。
3、 制作基于WxWidgets的编辑器。
4、 制作基于QT的编辑器。
基于C#来制作编辑器,在制作一些小工具上面很有利,比如说打包工具,加密器等等和图形关系不大的工具,它的优势在于它的简单易用,但是当你涉及到图形这一块的时候,如果引擎支持不C#,那么使用XNA、Manage DX 都不是很好的一种解决方案(除非你的游戏就是基于两者),导入动态链接库的方法又会比较麻烦,C#和C++之间还是有不小的区别。
基于MFC做编辑器,在以前基本是首选,它的优势在于文档应用特别多,你遇到问题的时候,基本上网络上都能找到解决方案,但是它相对门槛高,一个初学者经常会被它折磨得兴趣殆尽,应用也很麻烦,特别是在多窗口应用上面,所以以前我用MFC做编辑器都是基于Dialog来做。
WxWidgets和QT都是跨平台的GUI 库,目前来说还算主流,我个人倾向于QT,WxWidgets了解不多,QT目前由诺基亚负责,有自己的IDE、设计工具、详细的例子、比较充实的文档、与VS的结合还算完美,还有一些第三方的库支持,网络上的资料也还多,是个发展潜力不错的GUI库。
因为我将要做一个3D地图编辑器,在图形这一块也有不少选择,OGRE与Irrlicht等,我选择使用OGRE,当然你也可以选择自己的引擎。
OGRE是一个开源的图形渲染引擎,它的材质脚本还是很强大的,简单易用、目的性明确,让你的Shader容易应用与修改。早期的版本在地形这一块做得不够,所以早期做OGRE的地形编辑时一般会选择ETM,PLSM等库,新的1.7版本对地形这一块增强不少,而我也会在编辑器里面应用它地形编辑的功能。
二、工具安装指南
1、OGRE下载与编译
OGRE官方网站:http://www.ogre3d.org
下载最高版本的OGRE(1.7.1),有两种方式:
第一种方式是直接下载SDK,下载的SDK可以直接使用,但是由于编译环境不同,可能会缺少一些DX的DLL,你得在网络下另外下载缺少的DLL,下载方法是从网站左侧的DownLoad里面选择SDK,然后选择相应VS的版本,我们推荐使用VS2008,因为QT针对2008做了一个AddIn。
第二种方式是下载源代码进行编译。个人觉得使用OGRE应该使用自己编译的库,毕竟有什么需要的时候还可以自己修改,自己编译需要注意几点:
1、除了OGRE源码外,你需要额外下载Microsoft Visual C++ Dependencies Package,并把它解压到OGRE目录(你自己的OGRE目录)后编译。
2、你需要下载CMAKE,官方网站是www.cmake.org。下载一个最新版本就行。
3、你机器需要安装DX的SDK,不然OIS和DX的渲染系统插件无法编译。
4、使用Cmake生成Ogre VS解决方案的时候要记得指定Dependencies目录(在Cmake提醒你的时候指定)。此过程可以参考
用VS打开生成的解决方案,
然后直接编译就可以获得dll和lib.
2、QT下载
QT官方网站:http://qt.nokia.com/products
下载QT也有两种方式,一种是纯SDK(Qt SDK for Windows* (287 MB)),另外一种是针对VS2008的库(Qt libraries 4.6.2 for Windows (VS 2008, 194 MB)),这两者有一定的区别,前者带有更多的工具(IDE等)。我推荐下载针对VS2008的库,下载安装完之后,还需要下载一个Addin,这个Addin比较难找,在Other downloads里面下载Visual Studio Add-in (44 MB)。
安装完Add-In之后,打开VS2008应该就可以找到QT的模板了。QT4 projects下面有一些选项,选择新建一个QT Application。新建完编译通过,运行发现这是一个基本窗口。
如果编译OIS没有成功,请在项目属性里面填入DX的include和lib路径。
三、开始之前的配置
我看到过很多同志在做项目时,直接新建项目后立马就直接开始编程,使用的是VS默认目录,结果在Debug的时候老是找不到dll,找不到资源,然后又花了一堆的时间去查找问题,白白地浪费了不少时间,更有甚者就在此时便失去了继续向下的动力,觉得这个东西太难理解了(一遇挫折就跑)。所以我觉得在每次开始项目前都应该好好地把解决方案配置一下。
我做项目的时候喜欢这种方式,项目目录下面存在以下几个目录。
Bin目录不难理解,里面放的是生成的可执行文件,下面又分了Debug、Release、Data(Media)等目录,Debug、Release里面放的是执行文件和dll,命名的时候Debug要命名为_d.exe.因为资源文件是共用的,所以资源不应该放在Debug或Release下面,直接放Bin下面就行了。
Docs目录里面放的是相关文档。
Objs目录里面存放编译过程中的中间文件,临时文件。
Scripts目录里面存放解决方案,Sln或其他格式。
SDKS 目录存放第三方库,比如OGRE,Boost,Lua等。
Tools目录存放着制作时的一些工具。
剩下那个目录一般改名为Src或source.
为什么目录要这样分?Bin文件夹分出来有利于你程序的发布,调试。把Objs从source分出来,有利于你的源代码版本控制,备份。把解决方案单独拿出来,有利于你的跨平台或换IDE,SDKS拿出来很重要,因为有可能两年后你的引擎或者底层更新或者大改动过,但是你又需要把两年前的游戏重新编译,如果没有备份好,结果自然不难想像。同样,工具也是这样,比如说加密器算法经常改动,你不备份好你的东西以后都没有办法修改了。
接下来要调整VS来适应这一套目录结构。
第一件事,用文本工具打开修改sln,把它指向source目录里面的工程文件。
# Visual Studio 2008
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test", "../Source/Test.vcproj", "{83E01383-8BC1-404F-9C25-A9AFFCDBB210}"
EndProject
像上面那样修改为你的工程名.
之后用新的解决方案打开项目,在VS里项目名上右键打开属性。
之后的第一件事情就是修改工作目录,很多同志就是因为没有设定这个目录导致找不到DLL,它在配置属性中调试一栏里面,修改成你当前的Bin所在目录,最好设置为相对目录,Debug模式下是../Bin/Debug,对应的Release下面是../Bin/Release。
接着在常规里面修改中间目录和输出目录,我们都修改成../Objs/Debug和../Objs/Release。
之后在链接器>常规里面修改输出文件,修改成../bin/Debug/$(ProjectName)_d.exe和../Bin/Release/$(ProjectName).exe。
然后在C/C++>常规中把你要的include添加进去,在链接器>附加库中把你要的lib目录添加进去。
完成这些我们就配置完了。
附:Ogre1.7.1的配置要注意:由于Ogre使用了boost,所以一定要把Ogre自带的Boost目录放进SDKs中,如果要使用OIS,还得包含OIS的头文件路径,库文件和OGRE放在一起,所以不用再设置。
另:如果是在IDE中新建QT Application,QT头文件与库的相关配置会自动帮你设置好。你只需要在它的基础上把其他库添加好就行了。
四、QT基本知识
回到QT,先在VS中新建一个QT Application,项目里面有几个目录:
1、 Form Files目录,它里面放的是使用QT designer制作的基于XML的布局文件,双击它就会自动进入QT designer。
2、 Generated Files目录,它里面放的是一些临时生成的文件,这些文件用来处理QT的信号和槽等机制。
3、 Resource Files目录,它里面放的是基于XML的资源文件,你可以在窗体里面使用它们。
4、 Header Files和Source Files这两个和VS默认是一样的。
理解了目录结构之后,先来试着写一个Hello World,先把除了main.cpp之外的所有文件移除(使用QT designer会提高制作效率,但是会让QT入门门槛变高)!打开main.cpp,仅保留以下代码:
view plaincopy to clipboardprint?
#include <QtGui/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
return a.exec();
}
#include <QtGui/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
return a.exec();
}
编译通过。运行没有任何反应,因为还没有往里面增加任何东西。
在代码中,Main函数是C语言的入口,之后申请的QApplication用来管理控制流和主要设置,这是核心,一定要保留。
按钮是GUI中最基本的一个控件,先看看怎么增加一个按钮。使用按钮控件必须先包含头文件:
view plaincopy to clipboardprint?
#include <QtGui/QPushButton>
#include <QtGui/QPushButton>
然后在QApplication a(argc, argv);与return a.exec();中间插入下面代码:
view plaincopy to clipboardprint?
QPushButton button("HELLO");
button.setGeometry(100,100,300,300);
button.show();
QPushButton button("HELLO");
button.setGeometry(100,100,300,300);
button.show();
代码第一行是申请一个按钮,并把按钮的Caption标题设为HELLO,第二行表示这个按钮出现在屏幕坐标(100,100)的位置,宽高为(300,300),最后一行是显示这个按钮,你可以尝试把它去掉看看效果(官方助手里有QPushButton的更多资料,请自行查看)。
编译出来,发现屏幕上出现一个框,框里面有一个按钮,按钮可以点击,但是没有任何反应,因为还没有为这个按钮增加任何的槽(Slot)。
在MFC对控件的处理一般是通过事件机制,而在QT中是使用信号(Signal)和槽(Slot)机制,其实你也可以把它理解为事件机制。
简单理解信号其实就是输入,而槽就是输出,拿按钮打比方,在一次点击中,这个点击,就是一个信号,而点击后的反馈,就是槽。
每一个控件都拥有一些默认的Signal和Slot,这些都可以在官方提供的助手中查看。
绑定Signal和slot是使用静态函数connect。函数原型是:
view plaincopy to clipboardprint?
Bool connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection )
Bool connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection )
其中sender是发送者,而receiver是接收者,signal是信号,而method就是slot,type里面提供了几种绑定方式,可以详细查看助手。
先看一个例子,在上面代码中加入点击按钮后关闭应用程序的效果。很简单,只需要在
view plaincopy to clipboardprint?
button. setGeometry(100,100,300,300);
button. setGeometry(100,100,300,300);
后面加入
view plaincopy to clipboardprint?
QObject::connect(&button, SIGNAL(clicked()), &a, SLOT(quit()));
QObject::connect(&button, SIGNAL(clicked()), &a, SLOT(quit()));
编译运行,点击后窗体关闭。
这是使用默认槽的例子,有时候需要点击按钮之后执行自定义的效果,那么就需要使用自定义槽了。
下面是一个使用自定义Slot的例子,鼠标点击按钮之后,文本框文字会改变。
先加入一个QLabel控件,你先加入头文件:
view plaincopy to clipboardprint?
#include <QtGui/QLabel>
#include <QtGui/QLabel>
然后在connect前加入
view plaincopy to clipboardprint?
QLabel label("World");
label.setGeometry(50,50,300,300);
QLabel label("World");
label.setGeometry(50,50,300,300);
先尝试编译一下,结果label没有出现在窗体里面!它当然不会出现在窗体里面,因为我们只是对Button使用了Show()函数,尝试加入label.show(),结果出现了两个窗体,一个里面有按钮,另一个里面有一个label。那么怎么把它们放在一起呢?
通过上面的测试发现,调用一次show就会产生一个窗口,那么是不是只调用一次show就行了?把函数里面代码改为:
view plaincopy to clipboardprint?
QApplication a(argc, argv);
QWidget window;
QPushButton button("HELLO");
button.setGeometry(100,100,300,300);
QLabel label("World");
label.setGeometry(50,50,300,300);
QHBoxLayout layout;
Layout.addWidget(&button);
Layout.addWidget(&label);
QObject::connect(&button, SIGNAL(clicked()), &window, SLOT(close()));
window.setLayout(&layout);
window.show();
return a.exec();
QApplication a(argc, argv);
QWidget window;
QPushButton button("HELLO");
button.setGeometry(100,100,300,300);
QLabel label("World");
label.setGeometry(50,50,300,300);
QHBoxLayout layout;
Layout.addWidget(&button);
Layout.addWidget(&label);
QObject::connect(&button, SIGNAL(clicked()), &window, SLOT(close()));
window.setLayout(&layout);
window.show();
return a.exec();
附上此时的头文件列表:
view plaincopy to clipboardprint?
#include <QtGui/QPushButton>
#include <QtGui/QApplication>
#include <QtGui/QLabel>
#include <QtGui/QHBoxLayout>
#include <QtGui/QWidget>
#include <QtGui/QPushButton>
#include <QtGui/QApplication>
#include <QtGui/QLabel>
#include <QtGui/QHBoxLayout>
#include <QtGui/QWidget>
一开始,我就申请了一个QWidget,QWidget类是QT中所有用户界面对象的基类,它本身并没有什么实际意义,在这里你可以把它看成一个窗体容器,然后又添加了一个
QHBoxLayout layout; QHBoxLayout这是个可以对子widget进行特定布局的控件,通过它可以把按钮和label并排,之后把窗体的layout设为指定的layout,然后调用show()。
调试运行,终于两个控件都出现了。
回到之前的话题,自定义槽。在QT中所有自定义槽都需要先编译成moc,才可以被使用。不过你放心,这个过程由QT自动完成,当然你也可以手动进行编译,QT的Bin目录里面有moc.exe,参照说明进行使用。
你应该可以看到我已经偷偷把按钮的点击信号转向了窗体的close槽。为什么要这样做呢,因为我们需要把自定义槽函数定义放在头文件里。
第一步,先把window封装起来,我新建一个MainWidget类,继承自QWidget类,类的头文件如下:
view plaincopy to clipboardprint?
#ifndef _MAIN_WIDGET_H_
#define _MAIN_WIDGET_H_
#include <QtGui/QLabel>
#include <QtGui/QHBoxLayout>
#include <QtGui/QWidget>
#include <QtGui/QPushButton>
class MainWidget: public QWidget
{
public:
MainWidget();
~MainWidget();
protected:
private:
QLabel* m_pLabel;
QPushButton* m_pButton;
QHBoxLayout* m_pLayout;
};
#endif
CPP如下:
#include "MainWidget.h"
MainWidget::MainWidget()
{
m_pLabel = new QLabel("World");
m_pLabel ->setGeometry(50,50,300,300);
m_pButton = new QPushButton ("HELLO");
m_pButton ->setGeometry(100,100,300,300);
m_pLayout = new QHBoxLayout();
m_pLayout -> addWidget(m_pButton);
m_pLayout -> addWidget(m_pLabel);
connect(m_pButton, SIGNAL(clicked()), this, SLOT(close()));
setLayout(m_pLayout);
}
MainWidget::~MainWidget()
{
}
#ifndef _MAIN_WIDGET_H_
#define _MAIN_WIDGET_H_
#include <QtGui/QLabel>
#include <QtGui/QHBoxLayout>
#include <QtGui/QWidget>
#include <QtGui/QPushButton>
class MainWidget: public QWidget
{
public:
MainWidget();
~MainWidget();
protected:
private:
QLabel* m_pLabel;
QPushButton* m_pButton;
QHBoxLayout* m_pLayout;
};
#endif
CPP如下:
#include "MainWidget.h"
MainWidget::MainWidget()
{
m_pLabel = new QLabel("World");
m_pLabel ->setGeometry(50,50,300,300);
m_pButton = new QPushButton ("HELLO");
m_pButton ->setGeometry(100,100,300,300);
m_pLayout = new QHBoxLayout();
m_pLayout -> addWidget(m_pButton);
m_pLayout -> addWidget(m_pLabel);
connect(m_pButton, SIGNAL(clicked()), this, SLOT(close()));
setLayout(m_pLayout);
}
MainWidget::~MainWidget()
{
}
Main.cpp改为:
view plaincopy to clipboardprint?
#include <QtGui/QApplication>
#include "MainWidget.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWidget window;
window.show();
return a.exec();
}
#include <QtGui/QApplication>
#include "MainWidget.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWidget window;
window.show();
return a.exec();
}
编译运行,结果和上次一样。
接下来申请一个自定义Slot,首先在头文件public:前加入宏
Q_OBJECT;
只有加入了Q_OBJECT,你才能使用QT中的signal和slot机制。这点很重要,不然你编译的时候会报“找不到slot”的错误。
然后在protected: 前加入:
view plaincopy to clipboardprint?
private slots:
void SetText ();
private slots:
void SetText ();
slot同样也分private、public、protected,意义和c++一样。
CPP中加入相应执行:
view plaincopy to clipboardprint?
void MainWidget:: SetText ()
{
m_pLabel -> setText("Test");
}
void MainWidget:: SetText ()
{
m_pLabel -> setText("Test");
}
把connect改成:
view plaincopy to clipboardprint?
connect(m_pButton, SIGNAL(clicked()), this, SLOT(SetText ()));
connect(m_pButton, SIGNAL(clicked()), this, SLOT(SetText ()));
编译运行,这时点击按钮就会改变文字了。就这么简单。
信号也可以自定义,不过信号自定义相对来说用武之地稍微小一点,定义的方式和slot定义差不多,都得在头文件中定义,举个例子:点击按钮后文本改变,触发一个新信号,这个信号会把文字又变回来。
在头文件中加入:
view plaincopy to clipboardprint?
signals:
void TextChanged ();
signals:
void TextChanged ();
再增加一个Slot用来对这个信号进行反馈。在private slots:后加入
view plaincopy to clipboardprint?
void RecoverText ();
void RecoverText ();
CPP中加入执行:
view plaincopy to clipboardprint?
void MainWidget::RecoverText()
{
m_pLabel -> setText("Hello");
}
void MainWidget::RecoverText()
{
m_pLabel -> setText("Hello");
}
注意信号是不需要加执行代码。
然后修改SetText()函数加入触发新信号的代码:
view plaincopy to clipboardprint?
emit TextChanged();
emit TextChanged();
最后加入新的connect:
view plaincopy to clipboardprint?
connect(this, SIGNAL(TextChanged()), this, SLOT(RecoverText()));
connect(this, SIGNAL(TextChanged()), this, SLOT(RecoverText()));
编译运行,结果和我们想要的一样。
注:信号和槽都是可以有参数的。
有关QT的基础知识就介绍到这里,具体控件的使用方法,请自行参考助手。
五、OGRE基础知识
友善提醒:如果你对OGRE比较了解,请自觉跳过此节。
本节并不打算提供详细的入门教程,只是对OGRE的简单介绍,如果需要OGRE的详细资料,请自行使用网络功能。
1、OGRE是什么
Ogre是一款开源的图形渲染引擎,它的全名叫(Object-oriented Graphics Rendering Engine),目前在开源图形渲染引擎这一块排名第一,由于它功能齐全(跨平台,支持DX和OPENGL)、知名度高,而且不断更新,所以国内学习资料也比较多,在网络游戏在一块已有不少游戏公司已经使用过或者正在使用OGRE(《天龙八部》、《成吉思汗》等),部分公司招聘要求里面也明确表示熟悉OGRE者优先,所以说学习OGRE是前景可观的。
2、OGRE可以做什么
首先OGRE只是一个图形渲染引擎,连输入输出都使用第三方的OIS,目前大部分应用都在游戏、VR。但是如果你需要用它来做网络游戏,你还需要网络库、UI库、音频音效库等。
3、如何学习OGRE
OGRE自1.7以来,抛弃了它的ExampleApplication的框架,开始使用SampleBrower加dll的方式来表示例子,我个人认为虽然看起来更专业了,但是对于新人入门来说,难度比 ExampleApplication还高,尽管ExampleApplication就已经让新人晕头转向了!
那么怎么去学习OGRE呢,有一本书是必备的,名字叫《PRO OGRE 3D PROGRAMMING》(现在已经有爱好者翻译的中文版了),这本书是OGRE入门的圣典,推荐方法是先仔细地看一遍,然后再重头开始码例子,为什么推荐这样做,因为我发现有些人在学习Opengl的时候,看完glbegin,glend就不看了,甚至还动手写引擎,人家红宝书后面明确地表明尽量不要使用glbegin,glend!
官方手册也是必看的,里面对一些模块进行了详细的讲解,材质脚本说得挺细。
个人推荐OGRE入门掌握顺序:
A、 渲染窗体管理(初级:初始化,销毁)
B、 OIS输入输出(初级:两种模式(回调、缓冲)、按键处理)
C、场景管理(初级:管理器选择,节点管理、实体管理)
D、材质(初级:材质使用、材质脚本)
E、资源管理(初级:资源组、资源)
D、动画(初级:骨骼动画)
E、面片相关(初级:表层、公告板、粒子)
基本掌握这些就可以做些简单的游戏了,然后在这基础之上再慢慢探索OGRE的庞大的世界。
几个学习的地方:
1、OGRE官方网站:www.ogre3d.org、官方论坛、Addon论坛、wiki是学习OGRE的好地方。
2、中文社区:www.ogre3d.cn也聚集了不少OGRE的爱好者。
3、游戏资源网也是一个学习游戏开发的不错的网站。
请充分利用你手头上面的搜索工具,百度适合搜索国内中文资料,google适合搜索英文资料。
第二章 编辑器的基本框架
一、几个问题
前面说了很多编辑器之外的东西,真正要动手做编辑器了,也不能一股脑地就开始了,这之前必须要问自己几个问题:
1、这个地图编辑器有什么基本功能?
2、导入导出文件格式?
A、3D地图编辑器的基本功能
正如开篇所说,编辑器制作有两种趋势,其中一种是大而全的世界编辑器,这种方式可以带给极大的成就感,正合很多新人的意,但是我觉得一开始给自己(特别是新人)设定一个庞大的计划是件空洞而不现实的事情,一个编辑器越是大而全它的应用方向就越窄,越不利用拓展,使用就越费劲,问题BUG也就越集中,维护成本也就越高。
其实可以从小做起,先来分析基本需求:
所谓地图编辑器,地图编辑是其基础功能,一般地图都是在地形(平面)上面放置演员(把它叫作演员是不希望和OGRE的实体概念冲突),那么我们就确定了我们两个需求:地形编辑、演员管理。
那么这两个需求又引申出新的需求,地形不能是光模吧,演员不能永远是编辑器预设的几个模型吧,所以我们又需要实体、纹理加载与删除的功能。加载之后的纹理和实体总应该有个地方可以浏览吧,不然怎么选择使用?
好了,因为我们的目标暂时是做一个基本框架。所以我们暂时确定以下基本需求:
1、 添加删除浏览实体、纹理
2、 地形编辑
3、 演员管理
除了基本需求外,我们还有另外一些编辑器本身的一些需求:
1、 菜单、工具栏、状态栏。
2、 日志管理。
日志管理是一个很重要的东西,它得支持两种方式,一种是导成文本,另一种是在编辑器里面实时看到,为什么要提供这两种东西呢,如果没有文本,有时候挂的时候你看不到为什么挂,如果没有实时地看到每次去看文本又很麻烦。
B、文件格式
导入导出文件格式是一个很纠结的问题,现在一般流行几种方式:
1、 纯二进制数据,优点是读取速度非常快,几乎无浪费数据,缺点是不易被修改,如果没有工具基本上几乎不可能被改动(当然你要约定某些字符串也是可以的),这种方法还有不少应用。
2、 自定义格式,类似于INI,优点是终于可能手动修改了,缺点是得花不少时间去写解析模块,应该是一种过渡解决方案,这种方案和上面那种有模糊的界定,区别在于这个拥有一个解析器。
3、 XML,现在应该是主流,优点是编辑修改很方式,手改也行,工具也很多,还不用写解析器,TinyXML,RapidXML等都是不错的解析器,缺点是效率低,在特定环境下会出现偶尔读不出文件的情况(可能是解析器的问题)。
现在不少游戏使用两种1和3两种方式结合的方法,在编辑时使用XML,结果用工具导成纯二进制加密文件,我也打算使用这种方法:
编辑器配置文件(需要对窗体的开关状态进行存储)和生成的地图使用XML。导入纹理、实体使用OGRE默认支持的格式。
二、基本框架布局
根据上一节的一些基本需求,做出一个简单的布局如下图:
窗体大小:1024*768,太大了有点显示器放不下!支持最大化,最小化,关闭按钮,支持手动拖大拖小。
拥有菜单项(支持快捷键,图标):
File : New, Open, Save, Save As, Exit
Edit: Undo, Redo,Copy, Paste(这个功能暂时保留)
Window:Entity List, Texture List ,Node List, property,Log,(支持图标check)
Help:About
工具栏拥有按钮:
New, Open, Save, Save As, Undo, Redo,Copy, Paste, Entity List, Texture List , Scene, property,Log,(部分支持check)
Dock窗口(支持自由拖动,重新排列,叠加,关闭,打开):
EntityList, Texture List, Node List, property,(这四个都可以是左右两侧),log窗口(只能在下侧)
视口:OGRE的显示窗口,大小可以改变。我们这里暂时只提供一个窗口,通过状态机加多个摄影机来管理浏览。
各个Dock窗口都拥有自己的小工具栏,Entity List/Texture List/Node list都是树型结构,栏目里面拥有相应添加删除的子功能。
属性栏使用了QT的第三方库,这个库得另行下载,后面会有详细介绍。
日志栏使用QListWidget,提供SystemLog方法,添加日志后自动跳转到最新。
状态栏显示鼠标移上控件时的一些详细说明。
三、窗体、菜单栏、工具栏、状态栏
A、创建窗体
我还是按照QT基础那节的内容创建一个窗体类,不同的是这个窗体类现在继承于QMainWindow,它会使得封装中央部件、菜单条和工具条以及窗口状态变得更容易。
接下去,设置标题和窗口大小:
view plaincopy to clipboardprint?
setWindowTitle("Editor"); //设置窗口标题
resize( WINDOW_WIDTH, WINDOW_HEIGHT);// 设置窗口大小
setWindowTitle("Editor"); //设置窗口标题
resize( WINDOW_WIDTH, WINDOW_HEIGHT);// 设置窗口大小
其中WINDOW_WIDTH和WINDOW_HEIGHT被定义在头文件中:
view plaincopy to clipboardprint?
static const int WINDOW_WIDTH = 1024;
static const int WINDOW_HEIGHT = 768;
static const int WINDOW_WIDTH = 1024;
static const int WINDOW_HEIGHT = 768;
在C++里尽量减少使用宏。
设置中央部件:
view plaincopy to clipboardprint?
setCentralWidget(m_pButton);//暂时把按钮设置为中央部件
setCentralWidget(m_pButton);//暂时把按钮设置为中央部件
为什么要设置中央部件呢?因为接下去我要使用Dock Widget,如果没有中央部件,左侧,右侧,下部就没有参照,也没意义了。
针对我们的基本框架,目前也仅仅需要这些简单的功能。
B、创建菜单栏和工具栏
在QT创建菜单、工具栏前,必须先创建QAction,然后把这个QAction添加给菜单或者工具栏。
QAction是什么,它是用户的UI动作,在一列菜单中,比如说File下面的new 就是一个QAction,这个QAction包括包括图标,名字,快捷方式,状态栏信息等。
我通过以下方法来设置QAction:
头文件中加入:
view plaincopy to clipboardprint?
QAction* m_pFileNew;
QAction* m_pFileNew;
因为工具栏和菜单都共用一个QAction,所以我把它用为类成员放在头文件中。
Cpp中加入:
view plaincopy to clipboardprint?
m_pFileNew = new QAction(QIcon(":/images/new.png"), tr("&New"), this);
m_pFileNew -> setShortcuts(QKeySequence::New);
m_pFileNew -> setStatusTip(tr("Create a new map"));
connect(m_pFileNew, SIGNAL(triggered()), this, SLOT(newFile()));
m_pFileNew = new QAction(QIcon(":/images/new.png"), tr("&New"), this);
m_pFileNew -> setShortcuts(QKeySequence::New);
m_pFileNew -> setStatusTip(tr("Create a new map"));
connect(m_pFileNew, SIGNAL(triggered()), this, SLOT(newFile()));
第一行是申请一个QAction,第一个参数是指定一个图标QIcon,待会我才来讨论图片的路径。第二个参数就是QAction显示的文字内容,tr()是将来用作本地化的,你只需要记得在你文本前加上tr()就行,this是父部件指针。
第二行是设置一个快捷键,在这里使用的是QT定义的NEW快捷键,你也可以使用QKeySequence(Qt::ALT + Qt::Key_E)的方式来取得QKeySequence。
第三行就是设置状态栏要显示的文本。
第四行是设置信号与槽,这里使用的是自定义槽,不熟悉的话回过头去看QT基础知识那节。
依照以上方法分别设定好(New,Open,Open,Open As,Exit),我想你这时候应该用一个函数管理了这些QAction的生成,这是一种好的习惯,不要把一大堆的函数都挤在构造函数里面,原则上超过50行的函数就得考虑增加一个新函数。
接下去把QAction添加进菜单和工具栏里面去。在QT4.6里面菜单使用的是QMenu类(以前是使用QPopupMenu,如果你看到一些教程上面写的是这个,那么你最好换一个教程),工具栏使用的是QToolBar类。
因为当前窗体继承于QMainWidow,所以可以通过menuBar()函数来获得窗体菜单条指针(菜单条和菜单不是同一个东西,菜单条指的是那一行可以放菜单的长条,而菜单只是File那一列),把菜单添加到菜单条里面去,就可以在菜单条上看到了。
注:菜单有两种方式,一种是添加进菜单条后变成固定菜单,另一种是弹出式菜单,两者区别不大,这个在后面会详细说明。
头文件中加入:
view plaincopy to clipboardprint?
QMenu* m_pFile;
QMenu* m_pFile;
Cpp中加入:
view plaincopy to clipboardprint?
m_pFile = menuBar()->addMenu(tr("&File"));
m_pFile -> addAction(m_pFileNew);
m_pFile -> addSeparator();
m_pFile = menuBar()->addMenu(tr("&File"));
m_pFile -> addAction(m_pFileNew);
m_pFile -> addSeparator();
第一行首先是获得菜单条的指针,然后添加一个File的新菜单,并把返回指针。
第二行是把QAction增加进菜单。
第三行是在QAction中间增加一个分隔条(横条)。
添加工具栏相对来说比添加菜单还更简单,你甚至还不用menuBar()取得菜单条,看代码:
头文件中加入:
view plaincopy to clipboardprint?
QToolBar* m_FileToolBar;
QToolBar* m_FileToolBar;
Cpp中加入:
view plaincopy to clipboardprint?
m_FileToolBar = addToolBar(tr("File"));
m_FileToolBar->addAction(m_pFileNew);
m_FileToolBar -> addSeparator();
m_FileToolBar = addToolBar(tr("File"));
m_FileToolBar->addAction(m_pFileNew);
m_FileToolBar -> addSeparator();
第一行增加一个名字叫File的工具栏,File这个文字不显示,它会生成一个特别的分隔条:两条竖杠,如果这个工具栏不是第一个工具栏的话,它可以被左右拖动。每增加一个工具栏都会产生这个分隔条。这是使用起来很简单也很有效果的东西。
第二行是把QAction增加进工具栏。
第三行是在QAction中间增加一个分隔条(单行竖条)。
上面都是最基本的函数,还有两个函数经常使用。
view plaincopy to clipboardprint?
m_ FileToolBar ->setIconSize(QSize(20,20));
m_ FileToolBar ->setToolButtonStyle(Qt::ToolButtonIconOnly);
m_ FileToolBar ->setIconSize(QSize(20,20));
m_ FileToolBar ->setToolButtonStyle(Qt::ToolButtonIconOnly);
第一个是设置显示图标大小。
第二个是设置按钮风格,现在是只显示图标,它有好几种文字和图标显示风格,可以在助手查看详细说明。
A、 状态栏
状态栏就非常简单了,工具栏与菜单栏都自动更改状态栏信息,如果你要手动更改的话,就直接加上这句话:
view plaincopy to clipboardprint?
statusBar()->showMessage(tr("Ready"));
statusBar()->showMessage(tr("Ready"));
这是在窗体创建时状态栏显示的内容。
四、QT资源系统
上一节中QAction设置图标时使用了QIcon,当时用的是QIcon (":/images/new.png")来申请一个QIcone,里面有一个路径,实际上这个路径并不是系统路径,而是通过Qrc来管理的路径。
QT的资源管理系统是一个跨平台的用来存储应用程序执行时所需的二进制资源,它用QRC(QT Resource Collection)文件来管理,QRC基于XML,使用起来非常方便。
下面是最基本的QRC文件形式:
- <RCC>
- <qresource>
- <file>images/new.png</file>
- </qresource>
- </RCC>
需要关注的是<file>images/new.png</file>,这是资源的相对路径,而且也是资源名,所以一定要注意大小写。
你可以通过:/images/new.png的方法把资源取出来。
你还可以给资源取个别名:
- <file alias=" new.png "> images/new.png </file>
这时候你就可以使用:/new.png了,注意:/images/new.png的方法已经取不到了。
当然你还可以把你的资源分组,其实就是加了一个前缀:
- <qresource prefix="/File">
- <file alias=" new.png "> images/new.png </file>
- </qresource>
如果你的应用程序是多语言的,那么你可以通过lang来设置不同语言环境下的资源:
- <qresource>
- <file>new.png</file>
- </qresource>
- <qresource lang="fr">
- <file alias=" new.png "> images/new_fr.png </file>
- </qresource>
这样,当你在法语环境下,就自动指向了新图了。
添加资源就这么简单,但是你的项目目前里面是没有Qrc的,所以你得新建一个QRC文件,怎么去创建一个QRC文件呢?在项目上面右键,添加,新建项,这时候不是要添加头文件或cpp文件,在新建项对话框的左侧,有一个QT Resource,点击右边模板里面的QT Resource输入名称就行了。
现在QRC文件已经建立好了,但是还是不能直接使用,因为还没有初始化它,使用
Q_INIT_RESOURCE(文件名);
初始化。
注意:填的仅仅是文件名,不带扩展名,也不需要双引号,一定要在使用资源前,初始化资源,资源会在应用程序关闭时释放。
现在工具栏有图标了。
五、日志栏与DockWidget
QDockWidget是QT中一个使用方便但又实用的控件,稍微大一点的项目都会使用它,只需要简简单单几句话就可以做出很酷的效果,为什么不用?而且我相信只要你使用过一次就会对它爱不释手。
在基本需求里,我们提出日志栏必须能够自由拖动,而且可以嵌入窗体,所以日志栏就得使用了一个QDockWidget,功能控件这一块很简单,只是不断地添加相应的日志信息,所以我使用QListWidget来显示日志信息,如果你有什么更好的建议,不妨告诉我,谢谢。
QDockWidget的申请很简单。
在头文件中加入:
- QDockWidget * m_pLogDock;
CPP中加入:
- m_pLogDock = new QDockWidget(tr("Log"), this);
- m_pLogDock->setAllowedAreas(Qt::BottomDockWidgetArea);
- addDockWidget(Qt::BottomDockWidgetArea, m_pLogDock);
行一是新建一个标题栏为Log的DockWidget,指定this为父Widget。
行二是指定这个Dock的允许挂接的区域,现在指定的是下部。你可以通过|的形式来指定多区域,比如
Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea|Qt::BottomDockWidgetArea。
行三是把DockWidget添加进窗体,这里提供了一个标志,这个标志指的是初始默认位置。
就这样,一个DockWidget就出来了,够简单吧,我们还可以把DockWidget的QAction取出来,放在工具栏和菜单里面去,只要通过以下代码:
- QAction* logAction = m_pLogDock -> toggleViewAction();
- logAction -> setShortcut(QKeySequence(Qt::ALT + Qt::Key_L));
- logAction ->setIcon(QIcon(":/images/Log.png"));
行一是取得QAction。
行二是设置QAction快捷键。
行三是设置QAction图标。
然后把QAction添加进你的菜单或工具栏。
m_pDockMenu-> addAction(logAction);
接下来,要在DockWidget里面加入QListWidget来实现日志功能。方法也很简单。
在头文件中加入:
- QListWidget* m_pLogList;
在Cpp中加入:
- m_pLogList = new QListWidget(m_pLogDock);
- m_pLogDock -> setWidget(m_pLogList);
行一是新建一个List,父类是LogDock。
行二是设置LogDock的主部件是这个List。
然后我们要写一个函数让系统可以把新日志放进去。函数代码如下:
- void MainWidget::SystemLog(const char* log)
- {
- QDateTime time;
- QString str = time.currentDateTime().toString(" yyyy.MM.dd. HH:mm:ss ");
- //取得系统当前时间,并按指定格式排列
- str.append(log);
- //把字符串追加进去。
- m_pLogList->addItem(str);
- //添加进List
- m_pLogList->scrollToItem (m_pLogList->item(m_pLogList -> count() - 1));
- //自动滚到最新那行,MFC有专门的方法,但是QT我没有找到,如果您知道,请您告
- //诉我
- }
好了,我们只要使用这个函数就可以在List中写入日志了,你也可以适当地扩展一下,比如说图标加文本的方式。
如何在文本中加入日志呢?我这里也提供两个函数。
- void Log_Clear(const char* filename)
- {
- FILE *hf = NULL;
- hf = fopen(filename, "w");
- if(!hf)
- {
- return;
- }
- else
- fclose(hf);
- }
- void Log_Write(const char* filename, const char *szFormat, ...)
- {
- FILE *hf = NULL;
- va_list ap;
- if(filename == NULL) return;
- hf = fopen(filename, "a");
- if(!hf) return;
- va_start(ap, szFormat);
- vfprintf(hf, szFormat, ap);
- va_end(ap);
- fprintf(hf, "/n");
- fclose(hf);
- }
第一个函数是清空日志文件。
第二个函数是写入日志。
这节最后来说一下QT的内存管理,可能一路看下来,之前都没提过delete吧,其实这一切QT都帮我们做好了,QT的内存管理你只要记住一句话,有父的Widget就不用显式删除,如果没有添加进父的Widget,new 了之后一定要显式删除哦,不然会内存泄漏。
六、实体管理(QTreeWidget、QFileDialog)
这一节的内容相对来说比较多,但是依然是很简单,只不过因为我们要求的操作比较多,制作起来比较麻烦一点而已。最后成品如下图:
在开始之前先把实体管理的XML表确定下来。暂时定了以下结构:
- <GroupList>
- <Group name=”Tree”/>
- <Group name=”House”/>
- </GroupList>
- <EntityList>
- <Entity name=”Test” filename=”test.mesh” Group=”Tree” />
- </EntityList>
用名字字符串还是ID,这是个很纠结的问题,不过只是使用编辑器的话那倒无所谓,编辑器在乎的是其稳定性与实用性,效率只要不是太差,一般都能接受(有些商业引擎编译时间那是相当地长啊)。
根据XML表就可以确定树型结构的层次,这种简单的实体管理把层级设定超过两层,简直就是找抽行为。
完成XML表后,现在来分析更细的需求:
1、工具栏:实体管理还有一些常用操作,我把它们分为添加组,删除组,添加实体,删除实体,清空组,清空所有。
2、编辑修改:组这一层不支持从文件中提取,只能由工具栏添加,所以我们要支持双击修改保存文字,实体则是由文件对话框添加的,以后配合好属性栏,就能很方便的修改,所以不需要双击编辑修改。
3、右键菜单:组这一层对应的操作应该是添加实体,删除组,清空组,而实体这一层则只有删除实体这个选项。
4、图标:工具栏肯定需要图标,组和实体前面也需要一个图标,不然看一堆文字很不舒服。
5、拖放:实体可以拖到另一个组。
需求确定后,首先按照上一节的方法新建一个Dock Widget,命名为m_pEntityDock。并把它的可挂接的位置,设为左,右,下三个区域,默认区域设为左侧。
然后我们添加一个新类,类名叫EntityViewWidget,继承于QWidget,构造函数接受父类指针,并把它赋给Qwidget。
类申明如下:
- class EntityViewWidget : public QWidget
- {
- Q_OBJECT;
- public:
- explicit EntityViewWidget(QWidget *parent = 0);
- virtual ~EntityViewWidget();
- public Q_SLOTS:
- void SSelectionChanged();
- void SRemoveGroup();
- void SAddGroup();
- void SClearGroup();
- void SClearAll();
- void SRemoveEntity();
- void SAddEntity();
- protected:
- static const int MAX_GROUP_COUNT = 64;
- EntityTreeWidget *m_pTreeWidget;
- QToolBar *m_pToolBar;
- QAction *m_pAddGroup;
- QAction *m_pRemoveGroup;
- QAction *m_pClearGroup;
- QAction *m_pClearAll;
- QAction *m_pAddEntity;
- QAction *m_pRemoveEntity;
- QTreeWidgetItem *m_pRootItems[MAX_GROUP_COUNT];
- QTreeWidgetItem *m_pCurrGroupItem;
- QTreeWidgetItem *m_pCurrEntityItem;
- bool RemoveGroup(int index);
- int m_nGroupCount;
- };
先按照第四节的方法在里面创建一个QToolBar,并添加6个基于图标的QAction,并按照上章QT基础里面介绍设定自定义Slot的方法,分别设定好公有Slot函数(SAddEntity、SRemoveEntity、SAddGroup、SRemoveGroup、SClearGroup、SClearAll),因为QT的函数使用的是小写字母开头的驼峰式,所以我把自定义的SLOT用S前缀命名,以免混淆,而普通函数则是大写字母开头,注:这种命名方式并不一定是最好的命名方式。
Slots中有一个SSelectionChanged(),这是针对QTreeWidget的信号itemSelectionChanged的槽,因为添加删除操作和当前选中的item相关联,在这个Slot里面就添加了选中处理代码。
MAX_GROUP_COUNT指的是允许的最大组数。因为QTreeWidget只是指向指针,所以要申请空间给QTreeWidgetItem,这里使用了预分配机制,配合一个int型的m_nGroupCount来避免遍历的工作,这个具体在后面再说。
留了两个指针分别表示当前选中的组和实体,之所以把它们分开是因为有可能会同时用到两个。
EntityTreeWidget是自定义的QTreeWidget,类申明如下:
- class EntityTreeWidget : public QTreeWidget
- {
- Q_OBJECT;
- public:
- EntityTreeWidget(QWidget *parent = 0);
- virtual ~EntityTreeWidget() {};
- public Q_SLOTS:
- void SItemEditDone();
- protected:
- QTreeWidgetItem *m_pCurrentItem;
- void mousePressEvent(QMouseEvent *evt);
- void dragEnterEvent(QDragEnterEvent *evt);
- void dragMoveEvent(QDragMoveEvent *evt);
- void dropEvent(QDropEvent *evt);
- void contextMenuEvent(QContextMenuEvent *evt);
- void keyPressEvent(QKeyEvent *evt);
- void mouseDoubleClickEvent( QMouseEvent *evt );
- };
把这个类放在EntityViewWidget类的上面,因为需要在这个类中调用一些EntityViewWidget的方法。
因为需求,所以重载了一些QTreeWidget的事件,这也是使用自定义TreeWidget的主要原因,它们用来处理按键、拖动、上下文菜单,另外还自定义了一个Slot,用来解决编辑后的反馈。
再来看EntityTreeWidget的实现,代码如下:
- EntityTreeWidget::EntityTreeWidget(QWidget *parent) :
- QTreeWidget(parent),m_pCurrentItem(0)
- {
- setColumnCount(1); //设置栏目数
- setHeaderHidden(true); //隐藏栏目名
- setSelectionMode(QAbstractItemView:: SingleSelection);//我们只允许选中单行
- setSelectionBehavior(QAbstractItemView::SelectItems);//设定几种蓝框选中方式
- setDragDropOverwriteMode(false);//关闭拖动放下覆盖指定项模式
- setDragDropMode(QAbstractItemView::DragDrop);//设定拖放方式,我们这里支持拖和放
- connect(itemDelegate(),SIGNAL(closeEditor(QWidget*, QAbstractItemDelegate::EndEditHint)), this, SLOT(SItemEditDone()));//在编辑完成时去触发指定槽
- }
- //-------------------------编辑完成时的槽---------------------------------------------------------------
- //-------用来修改item的文字,以及更改属性栏值
- void EntityTreeWidget::SItemEditDone()
- {
- if(m_pCurrentItem)
- {
- int index = indexOfTopLevelItem(m_pCurrentItem);//取得当前的ID
- m_pCurrentItem->text(0).toStdString();//修改名字
- m_pCurrentItem = 0;
- }
- }
- //-------------------刚开始拖放---------------------------------------------------------------------
- void EntityTreeWidget::dragEnterEvent(QDragEnterEvent *evt)
- {
- if(evt->source() == this )
- {
- evt->acceptProposedAction();//开启拖放行为,你如果不接受的话,
- //dragMoveEvent和dropEvent就不起作用
- }
- }
- //--------------拖动移动要告诉用户是否可以被放下--------------------
- void EntityTreeWidget::dragMoveEvent(QDragMoveEvent *evt)
- {
- bool allow = false;
- QTreeWidgetItem *item = itemAt(evt->pos());//取得当时所在的Item
- if(item) //如果这时鼠标位置有item
- {
- if(item->parent() == 0) //判断目标位置是否为组,实体不允许放在实体下面
- {
- QList<QTreeWidgetItem*> list = selectedItems(); //取得当前选中列表
- if (list.count() == 1) //因为只支持单个选中
- {
- if(list[0]->parent() != 0)//组也不能放在组下面
- {
- allow = true;
- }
- }
- allow = true;
- }
- }
- if(allow)
- evt->accept(); //接受放置
- else
- evt->ignore(); //不接受放置
- }
- //------------------------放下处理----------------------------------------------------------------
- void EntityTreeWidget::dropEvent(QDropEvent *evt)
- {
- bool allow = false;
- QTreeWidgetItem *item = itemAt(evt->pos());
- if(!item || !(item->parent() == 0) || !(evt->source() == this))//如果为空,或者是实体,
- //或者是从别的地方拖过来的,就忽略
- {
- evt->ignore();
- return;
- }
- int index = indexOfTopLevelItem(item);
- QList<QTreeWidgetItem*> list = selectedItems();
- if (list.count() == 1)
- {
- if(list[0]->parent() != 0)
- {
- list[0]->parent() -> removeChild(list[0]); //从原组中移除
- item->addChild(list[0]); //添加进新组
- }
- }
- }
- //-------------------------处理上下文菜单---------------------------------------------------------
- void EntityTreeWidget::contextMenuEvent(QContextMenuEvent *evt)
- {
- QTreeWidgetItem *item = itemAt(evt->x(), evt->y());//取得item和,evt->pos()一样效果
- if(item)
- {
- if (item->parent() == 0)
- {
- QMenu* contextMenu = new QMenu(this); //创建上下文菜单
- int index = indexOfTopLevelItem(item);
- contextMenu->addAction(tr("Add Entity"), parent(), SLOT(SAddEntity()));
- contextMenu->addSeparator();
- contextMenu->addAction(tr("Remove Group"), parent(), SLOT(SRemoveGroup())); contextMenu->addSeparator();
- contextMenu->addAction(tr("Clear Group"), parent(), SLOT(SClearGroup()));
- contextMenu->addSeparator();
- contextMenu->exec(QCursor::pos()); //执行上下文菜单
- delete contextMenu; //一定要记得Delete,不然会内存泄漏
- return;
- }
- else
- {
- QMenu* contextMenu = new QMenu(this);
- int index = indexOfTopLevelItem(item);
- contextMenu->addAction(tr("Remove"), parent(), SLOT(SRemoveEntity()));
- contextMenu->addSeparator();
- contextMenu->exec(QCursor::pos());
- delete contextMenu;
- return;
- }
- }
- evt->accept();
- }
- //------------------------------------按键处理-----------------------------------------
- void EntityTreeWidget::keyPressEvent(QKeyEvent *evt)
- {
- QTreeWidget::keyPressEvent(evt);
- }
- //-------------------鼠标按下处理,修改属性栏值(现在无)-----------------------
- void EntityTreeWidget::mousePressEvent(QMouseEvent *evt)
- {
- QTreeWidget::mousePressEvent(evt);
- }
- //---------------------------------双击事件-------------------------------------------------------
- void EntityTreeWidget::mouseDoubleClickEvent(QMouseEvent *evt)
- {
- m_pCurrentItem = 0;
- QTreeWidgetItem *item = itemAt(evt->pos());
- if(item && item->parent() == 0)
- {
- m_pCurrentItem = item;
- editItem(item, 0);//打开编辑功能
- return;
- }
- }
下面是EntityViewWidget的实现:
- EntityViewWidget::EntityViewWidget(QWidget *parent) :
- QWidget(parent),m_pCurrGroupItem(0),
- m_nGroupCount(0),
- m_pCurrEntityItem(0)
- {
- m_pAddGroup = new QAction(tr("Add New Group"), this);
- m_pAddGroup->setStatusTip(tr("Add a New Group"));
- m_pAddGroup->setIcon( QIcon( ":/images/addGroup.png" ));
- m_pAddGroup->setEnabled(true);
- m_pRemoveGroup = new QAction(tr("Remove Group"), this);
- m_pRemoveGroup->setStatusTip(tr("Remove Selected Group"));
- m_pRemoveGroup->setIcon( QIcon( ":/images/RemoveGroup.png" ));
- m_pRemoveGroup->setEnabled(false);
- m_pAddEntity = new QAction(tr("Add Entity"), this);
- m_pAddEntity->setStatusTip(tr("Add Entity"));
- m_pAddEntity->setIcon( QIcon( ":/images/addEntity.png"));
- m_pAddEntity->setEnabled(false);
- m_pRemoveEntity = new QAction(tr("Remove Entity"), this);
- m_pRemoveEntity->setStatusTip(tr("Remove Entity"));
- m_pRemoveEntity->setIcon( QIcon( ":/images/RemoveEntity.png"));
- m_pRemoveEntity->setEnabled(false);
- m_pClearGroup = new QAction(tr("Clear Group"), this);
- m_pClearGroup->setStatusTip(tr("Clear Selected Group"));
- m_pClearGroup->setIcon( QIcon( ":/images/clearGroup.png" ));
- m_pClearGroup->setEnabled(false);
- m_pClearAll = new QAction(tr("Clear All"), this);
- m_pClearAll->setStatusTip(tr("Clear All Groups"));
- m_pClearAll->setIcon( QIcon( ":/images/clearAll.png" ));
- m_pClearAll->setEnabled(true);
- connect(m_pAddGroup, SIGNAL(triggered()), this, SLOT(SAddGroup()));
- connect(m_pRemoveGroup, SIGNAL(triggered()), this, SLOT(SRemoveGroup()));
- connect(m_pClearGroup, SIGNAL(triggered()), this, SLOT(SClearGroup()));
- connect(m_pClearAll, SIGNAL(triggered()), this, SLOT(SClearAll()));
- connect(m_pAddEntity, SIGNAL(triggered()), this, SLOT(SAddEntity()));
- connect(m_pRemoveEntity, SIGNAL(triggered()), this, SLOT(SRemoveEntity()));
- m_pTreeWidget = new EntityTreeWidget(this);
- m_pToolBar = new QToolBar("", this);
- m_pToolBar->setIconSize(QSize(20,20));
- m_pToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);
- m_pToolBar->addAction(m_pAddGroup);
- m_pToolBar->addAction(m_pRemoveGroup);
- m_pToolBar->addAction(m_pAddEntity);
- m_pToolBar->addAction(m_pRemoveEntity);
- m_pToolBar->addAction(m_pClearGroup);
- m_pToolBar->addAction(m_pClearAll);
- QVBoxLayout *boxlayout = new QVBoxLayout(this);
- boxlayout -> setMargin(0);//设置页边空白大小
- boxlayout -> addWidget(m_pToolBar);
- boxlayout -> addWidget(m_pTreeWidget);
- for(int i = 0;i < MAX_GROUP_COUNT; ++i)
- m_pRootItems[i] = 0; //初始化
- //绑定选择项改动信号
- connect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));
- }
- void EntityViewWidget::SRemoveEntity()
- {
- if (m_pCurrEntityItem)
- {
- disconnect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));//先关闭信号,以免引起不必要的麻烦
- //m_pCurrEntityItem ->parent() -> removeChild(m_pCurrEntityItem);
- delete m_pCurrEntityItem;//两种方式都可以除移子项
- m_pCurrEntityItem = 0;
- //把选中项的选中状态取消
- QList<QTreeWidgetItem*> list = m_pTreeWidget->selectedItems();
- if(list.size() == 1 && list[0]->parent() != 0)
- {
- list[0]->setSelected(false);
- }
- connect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));
- }
- }
- void EntityViewWidget::SAddEntity()
- {
- if (m_pCurrGroupItem)
- {
- disconnect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));//先关闭信号,以免引起不必要的麻烦
- QFileDialog::Options options;
- QString openFilesPath;
- QString selectedFilter;
- QStringList files = QFileDialog::getOpenFileNames(
- this, tr("Add Entity"),
- openFilesPath,
- tr("Mesh Files (*.Mesh);;All Files (*.*)"),//这里你可以设为更多格式,比如说支持粒子,目前暂时只支持Mesh
- &selectedFilter,
- options);
- QStringList strlist;
- if (files.count())
- {
- for (int i =0; i < files.count() ;++i)
- {
- //对文件路径解析,我们只需要文件名,我找了半天没找到怎么取得文件名,结果用以下这种方法。
- openFilesPath = files[i];
- strlist = openFilesPath.split ( "/" );
- strlist = strlist[strlist.count() - 1].split ( "." );
- openFilesPath = strlist[0];
- QTreeWidgetItem* item = new QTreeWidgetItem((QTreeWidget*)0, QStringList(openFilesPath));
- item->setIcon(0, QIcon(":/images/Entity.png"));
- //添加进组
- m_pCurrGroupItem->addChild(item);
- }
- }
- connect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));
- }
- }
- EntityViewWidget::~EntityViewWidget()
- {
- }
- void EntityViewWidget::SClearGroup()
- {
- if (m_pCurrGroupItem)
- {
- disconnect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));//先关闭信号,以免引起不必要的麻烦
- int count = m_pCurrGroupItem -> childCount();
- QTreeWidgetItem* item;
- while (m_pCurrGroupItem -> childCount())
- {
- //把每个子项都删除
- item = m_pCurrGroupItem -> child(0);
- //m_pCurrGroupItem -> removeChild(item);
- delete item;
- item = 0;
- }
- connect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));
- }
- }
- void EntityViewWidget::SClearAll()
- {
- disconnect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));//先关闭信号,以免引起不必要的麻烦
- //把选中状态取消
- QList<QTreeWidgetItem*> list = m_pTreeWidget->selectedItems();
- if(list.size() == 1 )
- {
- list[0]->setSelected(false);
- }
- //持续删除到没有组
- while(m_nGroupCount)
- {
- m_pCurrGroupItem = m_pRootItems[m_nGroupCount - 1];
- SClearGroup();
- RemoveGroup(m_pTreeWidget->indexOfTopLevelItem(m_pCurrGroupItem));
- }
- m_pCurrGroupItem = 0;
- m_pCurrEntityItem = 0;
- connect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));
- }
- //--------------------当选择项改变时----------------------------------------------------------------
- void EntityViewWidget::SSelectionChanged()
- {
- m_pCurrGroupItem = 0;
- m_pCurrEntityItem = 0;
- QList<QTreeWidgetItem*> list = m_pTreeWidget->selectedItems();
- if(list.size() == 1 )//如果有选中某项
- {
- list[0]->setSelected(true);//设为选中状态
- if ( list[0]->parent() == 0)//如果是组
- {
- m_pCurrGroupItem = list[0];
- //修改工具栏QAction的状态
- m_pClearGroup ->setEnabled(true);
- m_pRemoveGroup->setEnabled(true);
- m_pAddEntity->setEnabled(true);
- m_pRemoveEntity->setEnabled(false);
- }
- else
- {
- m_pCurrEntityItem = list[0];
- m_pRemoveEntity->setEnabled(true);
- m_pClearGroup ->setEnabled(false);
- m_pRemoveGroup->setEnabled(false);
- m_pAddEntity->setEnabled(false);
- }
- }
- else
- {
- m_pRemoveEntity->setEnabled(false);
- m_pClearGroup ->setEnabled(false);
- m_pRemoveGroup->setEnabled(false);
- m_pAddEntity->setEnabled(false);
- }
- }
- void EntityViewWidget::SAddGroup()
- {
- if (m_nGroupCount < MAX_GROUP_COUNT)
- {
- QString name = QString("Group");
- disconnect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));
- m_pRootItems[m_nGroupCount] = new QTreeWidgetItem((QTreeWidget*)0, QStringList(name));
- m_pRootItems[m_nGroupCount]->setIcon(0, QIcon(":/images/Group.png"));
- QFont fnt = m_pRootItems[m_nGroupCount]->font(0);//取得字体
- fnt.setBold(true);//设置粗体
- m_pRootItems[m_nGroupCount]->setFont(0, fnt);//把改变设置回去
- m_pRootItems[m_nGroupCount]->setFlags(m_pRootItems[m_nGroupCount]->flags() | Qt::ItemIsEditable);//指定它可以被修改
- m_pTreeWidget->insertTopLevelItem(0, m_pRootItems[m_nGroupCount]);//把item放进QTreeWidget
- connect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));
- ++m_nGroupCount;//计数器自加
- //如果超过总额就关闭添加组的按钮
- if (m_nGroupCount >= MAX_GROUP_COUNT)
- {
- m_pAddGroup -> setEnabled(false);
- }
- }
- }
- void EntityViewWidget::SRemoveGroup()
- {
- if(m_pCurrGroupItem)
- {
- disconnect(m_pTreeWidget,SIGNAL(itemSelectionChanged()),this, SLOT(SSelectionChanged()));//先关闭信号,以免引起不必要的麻烦
- if(RemoveGroup(m_pTreeWidget->indexOfTopLevelItem(m_pCurrGroupItem)))
- {
- m_pCurrGroupItem = 0;
- QList<QTreeWidgetItem*> list = m_pTreeWidget->selectedItems();
- if(list.size() == 1 && list[0]->parent() == 0)
- {
- list[0]->setSelected(false);
- }
- }
- }
- }
- bool EntityViewWidget::RemoveGroup(int index)
- {
- index = m_nGroupCount - index - 1;
- if(m_pRootItems[index]->childCount())//如果还有子项
- {
- QMessageBox::information(QApplication::activeWindow(),"Editor", tr("Can not delete group : Group is not empty!!"));
- return false;
- }
- disconnect(m_pTreeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(SSelectionChanged()));
- --m_nGroupCount;//计数器自减
- //m_pTreeWidget ->takeTopLevelItem(index);
- QTreeWidgetItem* bufItem;
- //把要删除的项与最后一项交换
- bufItem = m_pRootItems[index];
- m_pRootItems[index] = m_pRootItems[m_nGroupCount];
- m_pRootItems[m_nGroupCount] = bufItem;
- //把最后一项删除
- delete m_pRootItems[m_nGroupCount];
- m_pRootItems[m_nGroupCount] = 0;
- connect(m_pTreeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(SSelectionChanged()));
- return true;
- }
注:把要删除的项与最后一项交换,这是避免遍历的一种简便方法,在游戏中应用比较广,比如说粒子系统,一开始分配好N个粒子空间,当一个粒子生命时间结束,就把它和最后一个粒子对调,计数器自减一,这样的话,你的遍历代码就不需要更改,效率就高了不少!其实这就是某些粒子系统高效的秘决哦!
OK,编译运行就可以达到例图的效果了。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/vickylh/article/details/5614271