Qt学习之路


QtQtQtQt学习专题 Qt 学习之路(1)-前言 Qt是一个著名的 C++库——或许并不能说这只是一个 GUI库,因为 Qt 十分庞大,并不仅仅是 GUI。使用 Qt,在 一定程序上你获得的是一个“一站式”的服务:不再需要研究 STL,不再需要 C++的,因为 Qt 有它自己的 QString 等等。或许这样说很偏激,但 Qt确实是一个“伟大的 C++库”。 我们所使用的 Qt,确切地说也就是它的 GUI编程部分。C++的GUI编程同 Java 不同:GUI并不是 C++标准的 一部分。所以,如果使用 Java,那么你最好的选择就是 AWT/Swing,或者也可以使 SWT/JFace,但是,C++的GUI 编程给了你更多的选择:wxWidget, gtk++以及 Qt。这几个库我都有接触,但是接触都不是很多,只能靠一些资 料和自己的一点粗浅的认识说一下它们之间的区别(PS: 更详尽的比较在前面的文章中有)。 首先说 wxWidget,这是一个标准的 C++库,和 Qt一样庞大。它的语法看上去和 MFC类似,有大量的宏。据 说,一个 MFC程序员可以很容易的转换到 wxWidget上面来。wxWidget 有一个很大的优点,就是它的界面都是原 生风格的。这是其他的库所不能做到的。wxWidget的运行效率很高,据说在 Windows 平台上比起微软自家的 MFC 也不相上下。 gtk++其实是一个 C 库,不过由于 C++和C之间的关系,这点并没有很大的关系。但是,gtk++是一个使用 C 语言很优雅的实现了面向对象程序设计的范例。不过,这也同样带来了一个问题——它的里面带有大量的类型转 换的宏来模拟多态,并且它的函数名“又臭又长(不过这点我倒是觉得无所谓,因为它的函数名虽然很长,但是 同样很清晰)”,使用下划线分割单词,看上去和 Linux 如出一辙。由于它是 C 语言实现,因此它的运行效率当 然不在话下。gtk++并不是模拟的原生界面,而有它自己的风格,所以有时候就会和操作系统的界面显得格格不 入。 再来看 Qt,和wxWidget一样,它也是一个标准的 C++库。但是它的语法很类似于 Java 的Swing,十分清晰, 而且 SIGNAL/SLOT 机制使得程序看起来很明白——这也是我首先选择 Qt的一个很重要的方面,因为我是学 Java 出身的 :)。不过,所谓“成也萧何,败也萧何”,这种机制虽然很清楚,但是它所带来的后果是你需要使用 Qt的qmake对程序进行预处理,才能够再使用 make 或者 nmake进行编译。并且它的界面也不是原生风格的,尽 管Qt使用 style机制十分巧妙的模拟了本地界面。另外值得一提的是,Qt不仅仅运行在桌面环境中,Qt已经被 Nokia收购,它现在已经会成为 Symbian系列的主要界面技术——Qt是能够运行于嵌入式平台的。 以往人们对 Qt的授权多有诟病。因为 Qt 的商业版本价格不菲,开源版本使用的是 GPL协议。但是现在 Qt 的开源协议已经变成 LGPL。这意味着,你可以将 Qt作为一个库连接到一个闭源软件里面。可以说,现在的 Qt 协议的争议已经不存在了——因为 wxWidgets或者 gtk+同样使用的是类似的协议发布的。 在本系列文章中,我们将使用 Qt4进行 C++ GUI的开发。我是参照着《C++ GUI Programming with Qt4》一 书进行学习的。其实,我也只是初学 Qt4,在这里将这个学习笔记记下来,希望能够方便更多的朋友学习 Qt4。 我是一个 Java 程序员,感觉 Qt4的一些命名规范以及约束同 Java 有异曲同工之妙,因而从 Java 迁移到 Qt4似 乎困难不大。不过,这也主要是因为 Qt4良好的设计等等。 闲话少说,还是尽快开始下面的学习吧! Qt 学习之路(2)-Hello, world! 任何编程技术的学习第一课基本上都会是 Hello, world!,我也不想故意打破这个惯例——照理说,应该首先回 顾一下 Qt的历史,不过即使不说这些也并无大碍。 或许有人总想知道,Qt这个单词是什么意思。其实,这并不是一个缩写词,仅仅是因为它的发明者,TrollTech 公司的 CEO,Haarard Nord和Trolltech公司的总裁 Eirik Chambe-Eng在联合发明 Qt的时候并没有一个很好的 名字。在这里,字母 Q是Qt 库中所有类的前缀——这仅仅是因为在 Haarard 的emacs的字体中,这个字母看起 来特别的漂亮;而字母 t 则代表“toolkit”,这是在 Xt( X toolkit )中得到的灵感。 顺便说句,Qt原始的公司就是上面提到的 Trolltech,貌似有一个中文名字是奇趣科技——不过现在已经被 Nokia收购了。因此,一些比较旧的文章里面会提到 Trolltech 这个名字。 好了,闲话少说,先看看 Qt的开发吧!事先说明一下,我是一个比较懒的人,不喜欢配置很多的东西,而 Qt已经提供了一个轻量级的 IDE,并且它的网站上也有 for Eclipse 和 VS 的开发插件,不过在这里我并不想 用这些大块头 :) Qt有两套协议——商业版本和开源的 LGPL版本。不同的是前者要收费,而后者免费,当然,后者还要遵循 LGPL协议的规定,这是题外话。 Qt的网址是 https://qt.nokia.com/downloads,不过我打开这个站点总是很慢,不知道为什么。你可以找 到大大的 LGPL/Free 和 Commercial,好了,我选的是 LGPL版本的,下载包蛮大,但是下载并不会很慢。下载 完成后安装就可以了,其它不用管了。这样,整个 Qt的开发环境就装好了——如果你需要的话,也可以把 qmake 所在的目录添加进环境变量,不过我就不做了。 安装完成后会有个 Qt Creator 的东西,这就是官方提供的一个轻量级 IDE,不过它的功能还是蛮强大的。 运行这个就会发现,其实 Qt 不仅仅是 Linux KDE桌面的底层实现库。而且是这个 IDE的实现 :) 这个 IDE就是 用Qt完成的。 Qt Creator左面从上到下依次是 Welcome(欢迎页面,就是一开始出现的那个);Edit(我们的代码编辑窗 口);Debug(调试窗口);Projects(工程窗口);Help(帮助,这个帮助完全整合的 Qt的官方文档,相当有 用);Output(输出窗口)。 下面我们来试试我们的 Hello, world! 吧! 在Edit窗口空白处点右键,有 New project... 这里我们选第三项,Qt Gui Application。 Qt 学习之路(3)-Hello, world!(续) 下面来逐行解释一下前面的那个 Hello, world!程序,尽管很简单,但却可以对 Qt程序的结构有一个清楚的认 识。现在再把代码贴过来: #include #include int main(int argc, char *argv[]) { QApplication app(argc, argv); QLabel *label = new QLabel("Hello, world!"); label->show(); return app.exec(); } 第1行和第 2 行就是需要引入的头文件。和普通的 C++程序没有什么两样,如果要使用某个组件,就必须要 引入相应的头文件,这类似于 Java 的import机制。值得说明的是,Qt中头文件和类名是一致的。也就是说, 如果你要使用某个类的话,它的类名就是它的头文件名。 第3行是空行 :) 第4行是 main函数函数头。这与普通的 C++程序没有什么两样,学过 C++的都明白。因此你可以看到,实际 上,Qt完全通过普通的 main 函数进入,这不同于 wxWidgets,因为 wxWidgets的Hello, world需要你继承它的 一个 wxApp 类,并覆盖它的 wxApp::OnInit 方法,系统会自动将 OnInit编译成入口函数。不过在 Qt中,就不需 要这些了。 第5行,噢噢,大括号… 第6行,创建一个 QApplication对象。这个对象用于管理应用程序级别的资源。QApplication的构造函数 要求两个参数,分别来自 main 的那两个参数,因此,Qt在一定程度上是支持命令行参数的。 第7行,创建一个 QLabel对象,并且能够显示 Hello, world!字符串。和其他库的 Label控件一样,这是 用来显示文本的。在Qt中,这被称为一个 widget(翻译出来是小东西,不过这个翻译并不好…),它等同于 Windows 技术里面的控件(controls)和容器(containers)。也就是说,widget可以放置其他的 widget,就像 Swing的组 件。大多数 Qt 程序使用 QMainWindow或者 QDialog 作为顶级组件,但 Qt 并不强制要求这点。在这个例子中,顶 级组件就是一个 QLabel。 第8行,使这个 label可见。组件创建出来之后通常是不可见的,要求我们手动的使它们可见。这样,在创 建出组建之后我们就可以对它们进行各种定制,以避免出现之后在屏幕上面会有闪烁。 第9行,将应用程序的控制权移交给 Qt。这时,程序的事件循环就开始了,也就是说,这时可以相应你发 出的各种事件了。这类似于 gtk+最后的一行 gtk_main()。 第10行,大括号……程序结束了。 注意,我们并没有使用 delete去删除创建的 QLabel,因为在程序结束后操作系统会回收这个空间——这只 是因为这个 QLabel占用的内存比较小,但有时候这么做会引起麻烦的,特别是在大程序中,因此必须小心。 好了,程序解释完了。按照正常的流程,下面应该编译。前面也提过,Qt的编译不能使用普通的 make,而 必须先使用 qmake 进行预编译。所以,第一步应该是在工程目录下使用 qmake -project 命令创建.pro文件(比如说是叫 helloworld.pro)。然后再在.pro文件目录下使用 qmake helloworld.pro (make) 或者 qmake -tp vc helloworld.pro (nmake) 生成 makefile,然后才能调用 make或者是 nmake进行编译。不过因为我们使用的是 IDE,所以这些步骤就 不需要我们手动完成了。 值得说明一点的是,这个 qmake能够生成标准的 makefile 文件,因此完全可以利用 qmake自动生成 makefile——这是题外话。 好了,下面修改一下源代码,把 QLabel的创建一句改成 QLabel *label = new QLabel(" Hello, world! "); 同Swing的JLabel一样,Qt也是支持 HTML 解析的。 好了,这个 Hello, world就说到这里!明确一下 Qt的程序结构,在一个 Qt源代码中,一下两条语句是必不 可少的。 QApplication app(argc, argv); //... return app.exec(); Qt 学习之路(4)-初探信号槽 过了简单的 Hello, world! 之后,下面来看看 Qt最引以为豪的信号槽机制! 所谓信号槽,简单来说,就像是插销一样:一个插头和一个插座。怎么说呢?当某种事件发生之后,比如, 点击了一下鼠标,或者按了某个按键,这时,这个组件就会发出一个信号。就像是广播一样,如果有了事件,它 就漫天发声。这时,如果有一个槽,正好对应上这个信号,那么,这个槽的函数就会执行,也就是回调。就像广 播发出了,如果你感兴趣,那么你就会对这个广播有反应。干巴巴的解释很无力,还是看代码 #include #include int main(int argc, char *argv[]) { QApplication a(argc, argv); QPushButton *button = new QPushButton("Quit"); QObject::connect(button, SIGNAL(clicked()), &a, SLOT(quit())); button->show(); return a.exec(); } 这是在 Qt Creator 上面新建的文件,因为前面已经详细的说明怎么新建工程,所以这里就不再赘述了。这 个程序很简单,只有一个按钮,点击之后程序退出。(顺便说一句,Qt里面的 button被叫做 QPushButton,真搞 不明白为什么一个简单的 button非得加上 push 呢?呵呵) 主要是看这一句: QObject::connect(button, SIGNAL(clicked()), &a, SLOT(quit())); QObject是所有类的根。Qt使用这个 QObject实现了一个单根继承的 C++。它里面有一个 connect 静态函数, 用于连接信号槽。 当一个按钮被点击时,它会发出一个 clicked信号,意思是,向周围的组件们声明:我被点击啦!当然,其 它很多组件都懒得理他。如果对它感兴趣,就告诉 QObject 说,你帮我盯着点,只要 button 发出 clicked信号, 你就告诉我——想了想之后,说,算了,你也别告诉我了,直接去执行我的某某某函数吧!就这样,一个信号槽 就形成了。具体来说呢,这个例子就是 QApplication 的实例 a 说,如果 button 发出了 clicked 信号,你就去执 行我的 quit函数。所以,当我们点击 button的时候,a 的quit函数被调用,程序退出了。所以,在这里,clicked() 就是一个信号,而 quit()就是槽,形象地说就是把这个信号插进这个槽里面去。 Qt使用信号槽机制完成了事件监听操作。这类似与 Swing里面的 listener机制,只是要比这个 listener 简单得多。以后我们会看到,这种信号槽的定义也异常的简单。值得注意的是,这个信号槽机制仅仅是使用的 QObject的connect函数,其他并没有什么耦合——也就是说,完全可以利用这种机制实现你自己的信号监听! 不过,这就需要使用 qmake预处理一下了! 细心的你或许发现,在Qt Creator 里面,SIGNAL和SLOT竟然变颜色了!没错,Qt确实把它们当成了关键字! 实际上,Qt 正是利用它们扩展了 C++语言,因此才需要使用 qmake进行预处理,比便使普通的 C++编译器能够顺 利编译。另外,这里的 signal和Unix 系统里面的 signal没有任何的关系!哦哦,有一点关系,那就是名字是一 样的! 信号槽机制是 Qt 关键部分之一,以后我们还会再仔细的探讨这个问题的。 Qt 学习之路(5)-组件布局 同Swing类似,Qt也提供了几种组件定位的技术。其中就包括绝对定位和布局定位。 顾名思义,绝对定位就是使用最原始的定位方法,给出这个组件的坐标和长宽值。这样,Qt就知道该把组 件放在哪里,以及怎么设置组件的大小了。但是这样做的一个问题是,如果用户改变了窗口大小,比如点击了最 大化或者拖动窗口边缘,这时,你就要自己编写相应的函数来响应这些变化,以避免那些组件还只是静静地呆在 一个角落。或者,更简单的方法是直接禁止用户改变大小。 不过,Qt提供了另外的一种机制,就是布局,来解决这个问题。你只要把组件放入某一种布局之中,当需 要调整大小或者位置的时候,Qt 就知道该怎样进行调整。这类似于 Swing 的布局管理器,不过 Qt的布局没有那 么多,只有有限的几个。 来看一下下面的例子: #include #include #include #include #include int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget *window = new QWidget; window->setWindowTitle("Enter your age"); QSpinBox *spinBox = new QSpinBox; QSlider *slider = new QSlider(Qt::Horizontal); spinBox->setRange(0, 130); slider->setRange(0, 130); QObject::connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int))); QObject::connect(spinBox, SIGNAL(valueChanged(int)), slider, SLOT(setValue(int))); spinBox->setValue(35); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(spinBox); layout->addWidget(slider); window->setLayout(layout); window->show(); return app.exec(); } 这里使用了两个新的组件:QSpinBox和QSlider,以及一个新的顶级窗口 QWidget。QSpinBox是一个有上下 箭头的微调器,QSlider 是一个滑动杆,只要运行一下就会明白到底是什么东西了。 代码并不是那么难懂,还是来简单的看一下。首先创建了一个 QWidget 的实例,调用 setWindowTitle函数 来设置窗口标题。然后创建了一个 QSpinBox和QSlider,分别设置了它们值的范围,使用的是 setRange函数。 然后进行信号槽的链接。这点后面再详细说明。然后是一个 QHBoxLayout,就是一个水平布局,按照从左到右的 顺序进行添加,使用 addWidget添加好组件后,调用 QWidget 的setLayout把QWidget 的layout设置为我们定 义的这个 Layout,这样,程序就完成了! Qt 学习之路(6)-API 文档的使用 今天来说一下有关 Qt API 文档的使用。因为 Qt有一个商业版本,因此它的文档十分健全,而且编写良好。对于 开发者来说,查看文档时开发必修课之一——没有人能够记住那么多 API的使用! 在Qt中查看文档是一件很简单的事情。如果你使用 QtCreator,那么左侧的 Help 按钮就是文档查看入口。 否则的话,你可以在 Qt的安装目录下的 bin里面的 assistant.exe中看到 Qt的文档。在早期版本中,Qt的文 档曾以 HTML格式发布,不过在 2009.03版中我没有找到 HTML 格式的文档,可能 Qt 已经把它全部换成二进制格 式的了吧?——当然,如果你全部安装了 Qt的组件,是可以在开始菜单中找到 assistant的! 其中,第一个是帮助的帮助:-);第二个是 Qt Designer 的帮助;第三个是 Qt Linguist 的帮助;第四个是 QMake 的帮助;最后一个是 Qt的API文档,在 QtCreator中默认打开的就是这部分。 不过,关于文档的内容这里实在不好阐述,因为整个文档太大了,我也并没有看过多少,很多时候都是随用 随查,就好像是字典一样——谁也不会天天没事抱着本字典去看不是?还有就是这里的文档都是英文的,不过如 果是做开发的话,了解一些英文还是很有帮助的,不是吗? Qt 学习之路(7)-创建一个对话框(上) 首先说明一点,在 C++ GUI Programming with Qt4, 2nd中,这一章连同以后的若干章一起,完成了一个比较完 整的程序——一个模仿 Excel的电子表格。不过这个程序挺大的,而且书中也没有给出完整的源代码,只是分段 分段的——我不喜欢这个样子,我想要看到我写出来的是什么东西,这是最主要的,而不是慢慢的过上几章的内 容才能看到自己的作品。所以,我打算换一种方式,每章只给出简单的知识,但是每章都能够运行出东西来。好 了,扯完了,下面开始! 以前说的主要是一些基础知识,现在我们来真正做一个东西——一个查找对话框。什么?什么叫查找对话框? 唉唉,先看看我们的最终作品吧! 好了,首先新建一个工程,就叫 FindDialog 吧!嗯,当然还是 Qt Gui Application,然后最后一步注意, Base Dialog 选择 QDialog,而不是默认的 QMainWindow,因为我们要学习建立对话框嘛!名字随便起,不过我就 叫finddialog 啦!Ganarate form还是不要的。然后 Finish 就好了。 打开 finddialog.h,开始编写头文件。 #ifndef FINDDIALOG_H #define FINDDIALOG_H #include class QCheckBox; class QLabel; class QLineEdit; class QPushButton; class FindDialog : public QDialog { Q_OBJECT public: FindDialog(QWidget *parent = 0); ~FindDialog(); signals: void findNext(const QString &str, Qt::CaseSensitivity cs); void findPrevious(const QString &str, Qt::CaseSensitivity cs); private slots: void findClicked(); void enableFindButton(const QString &text); private: QLabel *label; QLineEdit *lineEdit; QCheckBox *caseCheckBox; QCheckBox *backwardCheckBox; QPushButton *findButton; QPushButton *closeButton; }; #endif //FINDDIALOG_H 大家都是懂得 C++的啊,所以什么#ifndef,#define和#endif 的含义和用途就不再赘述了。 首先,声明四个用到的类。这里做的是前向声明,否则的话是编译不过的,因为编译器不知道这些类是否存 在。简单来说,所谓前向声明就是告诉编译器,我要用这几个类,而且这几个类存在,你就不要担心它们存不存 在的问题啦! 然后是我们的 FindDialog,继承自 QDialog。 下面是一个重要的东西:Q_OBJECT。这是一个宏。凡是定义信号槽的类都必须声明这个宏。至于为什么,我 们以后再说。 然后是 public的构造函数和析构函数声明。 然后是一个 signal:,这是 Qt的关键字——还记得前面说过的嘛?Qt扩展了 C++语言,因此它有自己的关键 字——这是对信号的定义,也就是说,FindDialog 有两个 public的信号,它可以在特定的时刻发出这两个信号, 就这里来说,如果用户点击了 Find 按钮,并且选中了 Search backward,就会发出 findPrevious(),否则发出 findNext()。 紧接着是 private slots:的定义,和前面的 signal 一样,这是私有的槽的定义。也就是说,FindDialog 具有两个槽,可以接收某些信号,不过这两个槽都是私有的。 为了 slots的定义,我们需要访问 FindDialog的组件,因此,我们把其中的组件定义为成员变量以便访问。 正是因为需要定义这些组件,才需要对它们的类型进行前向声明。因为我们仅仅使用的是指针,并不涉及到这些 类的函数,因此并不需要 include它们的头文件——当然,你想直接引入头文件也可以,不过那样的话编译速度 就会慢一些。 好了,头文件先说这些,下一篇再说源代码啦!休息,休息一下! C++教程:Qt学习之路(8)-创建一个对话框(下) 接着前一篇,下面是源代码部分: #include #include "finddialog.h" FindDialog::FindDialog(QWidget *parent) : QDialog(parent) { label = new QLabel(tr("Find &what:")); lineEdit = new QLineEdit; label->setBuddy(lineEdit); caseCheckBox = new QCheckBox(tr("Match &case")); backwardCheckBox = new QCheckBox(tr("Search &backford")); findButton = new QPushButton(tr("&Find")); findButton->setDefault(true); findButton->setEnabled(false); closeButton = new QPushButton(tr("Close")); connect(lineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(enableFindButton(const QString&))); connect(findButton, SIGNAL(clicked()), this, SLOT(findClicked())); connect(closeButton, SIGNAL(clicked()), this, SLOT(close())); QHBoxLayout *topLeftLayout = new QHBoxLayout; topLeftLayout->addWidget(label); topLeftLayout->addWidget(lineEdit); QVBoxLayout *leftLayout = new QVBoxLayout; leftLayout->addLayout(topLeftLayout); leftLayout->addWidget(caseCheckBox); leftLayout->addWidget(backwardCheckBox); QVBoxLayout *rightLayout = new QVBoxLayout; rightLayout->addWidget(findButton); rightLayout->addWidget(closeButton); rightLayout->addStretch(); QHBoxLayout *mainLayout = new QHBoxLayout; mainLayout->addLayout(leftLayout); mainLayout->addLayout(rightLayout); setLayout(mainLayout); setWindowTitle(tr("Find")); setFixedHeight(sizeHint().height()); } FindDialog::~FindDialog() { } void FindDialog::findClicked() { QString text = lineEdit->text(); Qt::CaseSensitivity cs = caseCheckBox->isChecked() ? Qt::CaseInsensitive : Qt::CaseSensitive; if(backwardCheckBox->isChecked()) { emit findPrevious(text, cs); } else { emit findNext(text, cs); } } void FindDialog::enableFindButton(const QString &text) { findButton->setEnabled(!text.isEmpty()); } CPP 文件要长一些哦——不过,它们的价钱也会更高,嘿嘿——嗯,来看代码,第一行 include的是 QtGui。 Qt是分模块的,记得我们建工程的时候就会问你,使用哪些模块?QtCore?QtGui?QtXml?等等。这里,我们引入 QtGui,它包括了 QtCore 和QtGui模块。不过,这并不是最好的做法,因为 QtGui文件很大,包括了 GUI的所有 组件,但是很多组件我们根本是用不到的——就像 Swing的import,你可以 import 到类,也可以使用*,不过 都不会建议使用*,这里也是一样的。我们最好只引入需要的组件。不过,那样会把文件变长,现在就先用 QtGui 啦,只要记得正式开发时不能这么用就好啦! 构造函数有参数初始化列表,用来调用父类的构造函数,相当于 Java 里面的 super()函数。这是 C++的相关 知识,不是 Qt 发明的,这里不再赘述。 然后新建一个 QLabel。还记得前面的 Hello, world!里面也使用过 QLabel 吗?那时候只是简单的传入一个字 符串啊!这里怎么是一个函数 tr()?函数 tr()全名是 QObject::tr(),被它处理的字符串可以使用工具提取出来 翻译成其他语言,也就是做国际化使用。这以后还会仔细讲解,只要记住,Qt的最佳实践:如果你想让你的程 序国际化的话,那么,所有用户可见的字符串都要使用 QObject::tr()!但是,为什么我们没有写 QObject::tr(), 而仅仅是 tr()呢?原来,tr()函数是定义在 Object里面的,所有使用了 Q_OBJECT宏的类都自动具有 tr()函数。 字符串中的&代表快捷键。注意看下面的 findButton的&Find,它会生成 Find字符串,当你按下 Alt+F的时 候,这个按钮就相当于被点击——这么说很难受,相信大家都明白什么意思。同样,前面 label里面也有一个&, 因此它的快捷键就是 Alt+W。不过,这个 label使用了 setBuddy函数,它的意思是,当 label获得焦点时,比 如按下 Alt+W,它的焦点会自动传给它的 buddy,也就是 lineEdit。看,这就是伙伴的含义(buddy英文就是伙伴 的意思)。 后面几行就比较简单了:创建了两个 QCheckBox,把默认的按钮设为 findButton,把 findButton设为不可 用——也就是变成灰色的了。 再下面是三个 connect 语句,用来连接信号槽。可以看到,当lineEdit发出 textChanged(const QString&) 信号时,FindDialog的enableFindButton(const QString&)函数会被调用——这就是回调,是有系统自动调用, 而不是你去调用——当findButton 发出 clicked()信号时,FindDialog 的findClicked()函数会被调用;当 closeButton发出 clicked()信号时,FindDialog 的close()函数会被调用。注意,connect()函数也是 QObject 的,因为我们继承了 QObject,所以能够直接使用。 后面的很多行语句都是 layout的使用,虽然很复杂,但是很清晰——编写 layout布局最重要一点就是思路 清楚,想清楚哪个套哪个,就会很好编写。 注意那个 spacer是由 rightLayout的addStretch()添加的,就像弹簧一样,把上面的组件“顶起来”。 最后的 setWindowTitle()就是设置对话框的标题,而 setFixedHeight()是设置成固定的高度,其参数值 sizeHint()返回“最理想”的大小,这里我们使用的是 height()函数去到“最理想”的高度。 好了,下面该编写槽了——虽然说是 slot,但实际上它就是普通的函数,既可以和其他函数一样使用,又 可以被系统回调。 先看 findClicked()函数。首先取出 lineEdit的输入值;然后判断 caseCheckBox是不是选中,如果选中就 返回 Qt::CaseInsensitive,否则返回 Qt::CaseSensitive,用于判断是不是大小写敏感的查找;最后,如果 backwardCheckBox被选中,就 emit(发出)信号 findPrevious(),否则 emit 信号 findNext。 enableFindButton()则根据 lineEdit 的内容是不是变化——这是我们的 connect 连接的——来设置 findButton是不是可以使用,这个很简单,不再说了。 这样,FindDialog.cpp也就完成了。下面编写 main.cpp——其实 QtCreator已经替我们完成了—— #include #include "finddialog.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); FindDialog *dialog = new FindDialog; dialog->show(); return app.exec(); } 运行一下看看我们的成果吧! 虽然很简单,也没有什么实质性的功能,但是我们已经能够制作对话框了——Qt的组件成百上千,不可能 全部介绍完,只能用到什么学什么,更重要的是,我们已经了解了其编写思路,否则的话,即便是你拿着全世界 所有的砖瓦,没有设计图纸,你也不知道怎么把它们组合成高楼大厦啊! 嘿嘿,下回见! Qt 学习之路(9):深入了解信号槽 信号槽机制是 Qt编程的基础。通过信号槽,能够使 Qt各组件在不知道对方的情形下能够相互通讯。这就将类之 间的关系做了最大程度的解耦。 槽函数和普通的 C++成员函数没有很大的区别。它们也可以使 virtual的;可以被重写;可以使 public、 protected或者 private 的;可以由其它的 C++函数调用;参数可以是任何类型的。如果要说区别,那就是,槽函 数可以和一个信号相连接,当这个信号发生时,它可以被自动调用。 connect()语句的原型类似于: connect(sender, SIGNAL(signal), receiver, SLOT(slot)); 这里,sender 和receiver都是 QObject 类型的,singal和slot 都是没有参数名称的函数签名。SINGAL() 和SLOT()宏用于把参数转换成字符串。 深入的说,信号槽还有更多可能的用法,如下所示。 一个信号可以和多个槽相连: connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int))); connect(slider, SIGNAL(valueChanged(int)), this, SLOT(updateStatusBarIndicator(int))); 注意,如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。 多个信号可以连接到一个槽: connect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError())); connect(calculator, SIGNAL(divisionByZero()), this, SLOT(handleMathError())); 这是说,只要任意一个信号发出,这个槽就会被调用。 一个信号可以连接到另外的一个信号: connect(lineEdit, SIGNAL(textChanged(const QString &)), this, SIGNAL(updateRecord(const QString &))); 这是说,当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有 什么区别。 槽可以被取消链接: disconnect(lcd, SIGNAL(overflow()), this, SLOT(handleMathError())); 这种情况并不经常出现,因为当一个对象 delete之后,Qt自动取消所有连接到这个对象上面的槽。 为了正确的连接信号槽,信号和槽的参数个数、类型以及出现的顺序都必须相同,例如: connect(ftp, SIGNAL(rawCommandReply(int, const QString &)), this, SLOT(processReply(int, const QString &))); 这里有一种例外情况,如果信号的参数多于槽的参数,那么这个参数之后的那些参数都会被忽略掉,例如: connect(ftp, SIGNAL(rawCommandReply(int, const QString &)), this, SLOT(checkErrorCode(int))); 这里,const QString &这个参数就会被槽忽略掉。 如果信号槽的参数不相容,或者是信号或槽有一个不存在,或者在信号槽的连接中出现了参数名字,在Debug 模式下编译的时候,Qt都会很智能的给出警告。 在这之前,我们仅仅在 widgets中使用到了信号槽,但是,注意到 connect()函数其实是在 QObject 中实现 的,并不局限于 GUI,因此,只要我们继承 QObject 类,就可以使用信号槽机制了: class Employee : public QObject { Q_OBJECT public: Employee() { mySalary = 0; } int salary() const { return mySalary; } public slots: void setSalary(int newSalary); signals: void salaryChanged(int newSalary); private: int mySalary; }; 在使用时,我们给出下面的代码: void Employee::setSalary(int newSalary) { if (newSalary != mySalary) { mySalary = newSalary; emit salaryChanged(mySalary); } } 这样,当setSalary()调用的时候,就会发出 salaryChanged()信号。注意这里的 if判断,这是避免递归的 方式!还记得前面提到的循环连接吗?如果没有 if,当出现了循环连接的时候就会产生无限递归。 Qt 学习之路(10): Meta-Object 系统 前面说过,Qt使用的是自己的预编译器,它提供了对 C++的一种扩展。利用 Qt的信号槽机制,就可以把彼此独 立的模块相互连接起来,不需要实现知道模块的任何细节。 为了达到这个目的,Qt提出了一个 Meta-Object系统。它提供了两个关键的作用:信号槽和内省。 面向对象程序设计里面会讲到 Smalltalk语言有一个元类系统。所谓元类,就是这里所说的 Meta-Class。 如果写过 HTML,会知道 HTML标签里面也有一个 ,这是用于说明页面的某些属性的。同样,Qt的Meta-Object 系统也是类似的作用。内省又称为反射,允许程序在运行时获得类的相关信息,也就是 meta-information。什 么是 meta-information 呢?举例来说,像这个类叫什么名字?它有什么属性?有什么方法?它的信号列表?它的槽列 表?等等这些信息,就是这个类的 meta-information,也就是“元信息”。这个机制还提供了对国际化的支持, 是QSA(Qt Script for Application)的基础。 标准 C++并没有 Qt的meta-information 所需要的动态 meta-information。所以,Qt提供了一个独立的工 具,moc,通过定义 Q_OBJECT宏实现到标准 C++函数的转变。moc使用纯 C++实现的,因此可以再任何编译器中 使用。 这种机制工作过程是: 首先,Q_OBJECT宏声明了一些 QObject子类必须实现的内省的函数,如metaObject(),tr(),qt_metacall() 等; 第二,Qt的moc工具实现 Q_OBJECT宏声明的函数和所有信号; 第三,QObject成员函数 connect()和disconnect()使用这些内省函数实现信号槽的连接。 以上这些过程是 qmake,moc和QObject 自动处理的,你不需要去考虑它们。如果实现好奇的话,可以通过 查看 QMetaObject 的文档和 moc的源代码来一睹芳容。 Qt 学习之路(11): MainWindow 尽管 Qt提供了很方便的快速开发工具 QtDesigner 用来拖放界面元素,但是现在我并不打算去介绍这个工具,原 因之一在于我们的学习大体上是依靠手工编写代码,过早的接触设计工具并不能让我们对 Qt的概念突飞猛进…… 前面说过,本教程很大程度上依照的是《C++ GUI Programming with Qt4, 2nd Edition》这本书。但是,这本 书中接下来的部分用了很大的篇幅完成了一个简单的类似 Excel的程序。虽然最终效果看起来很不错,但我并不 打算完全依照这个程序来,因为这个程序太大,以至于我们在开始之后会有很大的篇幅接触不到能够运行的东西, 这无疑会严重打击学习的积极性——至少我是如此,看不到做的东西很难受——所以,我打算重新组织一下这个 程序,请大家按照我的思路试试看吧! 闲话少说,下面开始新的篇章! 就像 Swing 的顶层窗口一般都是 JFrame 一样,Qt的GUI 程序也有一个常用的顶层窗口,叫做 MainWindow 。 好了,现在我们新建一个 Gui Application 项目 MyApp,注意在后面选择的时候选择 Base Class 是QMainWindow。 然后确定即可。此时,QtCreator 已经为我们生成了必要的代码,我们只需点击一下 Run,看看运行出来的结 果。 一个很简单的窗口,什么都没有,这就是我们的主窗口了。 MainWindow 继承自 QMainWindow。QMainWindow 窗口分成几个主要的区域: 最上面是 Window Title,用于显示标题和控制按钮,比如最大化、最小化和关闭等;下面一些是 Menu Bar,用 于显示菜单;再下面一点事 Toolbar areas,用于显示工具条,注意,Qt 的主窗口支持多个工具条显示,因此这里是 ares,你可以把几个工具条并排显示在这里,就像 Word2003 一样;工具条下面是 Dock window areas,这是停靠窗 口的显示区域,所谓停靠窗口就是像 Photoshop 的工具箱一样,可以在主窗口的四周显示;再向下是 Status Bar, 就是状态栏;中间最大的 Central widget 就是主要的工作区了。 好了,今天的内容不多,我们以后的工作就是要对这个 MainWindow 进行修改,以满足我们的各种需要。 Qt 学习之路(12): 菜单和工具条 在前面的 QMainWindow的基础之上,我们开始着手建造我们的应用程序。虽然现在已经有一个框架,但是,确切 地说我们还一行代码没有写呢!下面的工作就不那么简单了!在这一节里面,我们要为我们的框架添加菜单和工具 条。 就像 Swing里面的 Action一样,Qt里面也有一个类似的类,叫做 QAction。顾名思义,QAction类保存有 关于这个动作,也就是 action的信息,比如它的文本描述、图标、快捷键、回调函数(也就是信号槽),等等。 神奇的是,QAction能够根据添加的位置来改变自己的样子——如果添加到菜单中,就会显示成一个菜单项;如 果添加到工具条,就会显示成一个按钮。这也是为什么要把菜单和按钮放在一节里面。下面开始学习! 首先,我想添加一个打开命令。那么,就在头文件里面添加一个私有的 QAction变量: class QAcion; //... private: QAction *openAction; //... 注意,不要忘记 QAction类的前向声明哦!要不就会报错的! 然后我们要在 cpp文件中添加 QAction的定义。为了简单起见,我们直接把它定义在构造函数里面: openAction = new QAction(tr("&Open"), this); openAction->setShortcut(QKeySequence::Open); openAction->setStatusTip(tr("Open a file.")); 第一行代码创建一个 QAction 对象。QAction有几个重载的构造函数,我们使用的是 QAction(const QString &text, QObject* parent); 这一个。它有两个参数,第一个 text 是这个动作的文本描述,用来显示文本信息,比如在菜单中的文本; 第二个是 parent,一般而言,我们通常传入 this指针就可以了。我们不需要去关心这个 parent参数具体是什 么,它的作用是指明这个 QAction 的父组件,当这个父组件被销毁时,比如 delete或者由系统自动销毁,与其 相关联的这个 QAction也会自动被销毁。 如果你还是不明白构造函数的参数是什么意思,或者说想要更加详细的了解 QAction这个类,那么就需要自 己翻阅一下它的 API文档。前面说过有关 API的使用方法,这里不再赘述。这也是学习 Qt 的一种方法,因为 Qt 是一个很大的库,我们不可能面面俱到,因此只为说道用到的东西,至于你自己想要实现的功能,就需要自己去 查文档了。 第二句,我们使用了 setShortcut函数。shortcut是这个动作的快捷键。Qt的QKeySequence已经为我们定 义了很多内置的快捷键,比如我们使用的 Open。你可以通过查阅 API文档获得所有的快捷键列表,或者是在 QtCreator中输入::后会有系统的自动补全功能显示出来。这个与我们自己定义的有什么区别呢?简单来说,我 们完全可以自己定义一个 tr("Ctrl+O")来实现快捷键。原因在于,这是 Qt跨平台性的体现。比如 PC键盘和 Mac 键盘是不一样的,一些键在 PC键盘上有,而Max键盘上可能并不存在,或者反之,所以,推荐使用 QKeySequence 类来添加快捷键,这样,它会根据平台的不同来定义不同的快捷键。 第三句是 setStatusTip函数。这是添加状态栏的提示语句。状态栏就是主窗口最下面的一条。现在我们的 程序还没有添加状态栏,因此你是看不到有什么作用的。 下面要做的是把这个 QAction 添加到菜单和工具条: QMenu *file = menuBar()->addMenu(tr("&File")); file->addAction(openAction); QToolBar *toolBar = addToolBar(tr("&File")); toolBar->addAction(openAction); QMainWindow有一个 menuBar()函数,会返回菜单栏,也就是最上面的那一条。如果不存在会自动创建,如 果已经存在就返回那个菜单栏的指针。直接使用返回值添加一个菜单,也就是 addMenu,参数是一个 QString, 也就是显示的菜单名字。然后使用这个 QMenu 指针添加这个 QAction。类似的,使用 addToolBar 函数的返回值 添加了一个工具条,并且把这个 QAction 添加到了上面。 好了,主要的代码已经写完了。不过,如果你只修改这些的话,是编译不过的哦!因为像 menuBar()函数返 回一个 QMenuBar指针,但是你并没有 include它的头文件哦!虽然没有明着写出 QMenuBar 这个类,但是实际上 你已经用到了它的 addMenu 函数了,所以还是要注意的! 下面给出来全部的代码: 1. mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include class QAction; class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); private: QAction *openAction; }; #endif //MAINWINDOW_H 2. mainwindow.cpp #include #include #include #include #include #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { openAction = new QAction(tr("&Open"), this); openAction->setShortcut(QKeySequence::Open); openAction->setStatusTip(tr("Open a file.")); QMenu *file = menuBar()->addMenu(tr("&File")); file->addAction(openAction); QToolBar *toolBar = addToolBar(tr("&File")); toolBar->addAction(openAction); } MainWindow::~MainWindow() { } main.cpp没有修改,这里就不给出了。 很丑,是吧?不过我们已经添加上了菜单和工具条了哦!按一下键盘上的 Alt+F,因为这是我们给它定义的快 捷键。虽然目前挺难看,不过以后就会变得漂亮的!想想看,Linux的KDE 桌面可是 Qt实现的呢! Qt 学习之路(13): 菜单和工具条(续) 前面一节我们已经把 QAction添加到菜单和工具条上面。现在我们要添加一些图片美化一下,然后把信号槽加上, 这样,我们的 action 就可以相应啦! 首先来添加图标。QAction的图标会显示在菜单项的前面以及工具条按钮上面显示。 为了添加图标,我们首先要使用 Qt的资源文件。在 QtCreator的项目上右击,选择 New File...,然后选择 resource file。 然后点击 next,选择好位置,Finish 即可。为了使用方便,我就把这个文件建在根目录下,建议应该在仔细 规划好文件之后,建在专门的 rsources 文件夹下。完成之后,生成的是一个.qrc 文件,qrc 其实是 Qt Recource Collection 的缩写。它只是一个普通的 XML 文件,可以用记事本等打开。不过,这里我们不去深究 它的结构,完全利用 QtCreator操作这个文件, 点击 Add 按钮,首先选择 Add prefix,然后把生成的/new/prefix改成/。这是 prefix 就是以后使用图标时需要 提供的前缀,以/开头。添加过 prefix之后,然后在工程文件中添加一个图标,再选择 Add file,选择那个图标。 这样完成之后保存 qrc 文件即可。 说明一下,QToolBar 的图标大小默认是 32*32,菜单默认是 16*16。如果提供的图标小于要求的尺寸,则不 做操作,Qt不会为你放大图片;反之,如果提供的图标文件大于相应的尺寸要求,比如是 64*64,Qt会自动缩小 尺寸。 图片的路径怎么看呢?可以看出,Qt的资源文件视图使用树状结构,根是/,叶子节点就是图片位置,连接在 一起就是路径。比如这张图片的路径就是/Open.png。 注意,为了简单起见,我们没有把图标放在专门的文件夹中。正式的项目中应该单独有一个 resources 文件夹 放资源文件的。 然后回到前面的 mainwindow.cpp,在构造函数中修改代码: openAction = new QAction(tr("&Open"), this); openAction->setShortcut(QKeySequence::Open); openAction->setStatusTip(tr("Open a file.")); openAction->setIcon(QIcon(":/Open.png")); // Add code. 我们使用 setIcon添加图标。添加的类是 QIcon,构造函数需要一个参数,是一个字符串。由于我们要使 用 qrc 中定义的图片,所以字符串以 : 开始,后面跟着 prefix,因为我们先前定义的 prefix是/,所以就需要一个/, 然后后面是 file 的路径。这是在前面的 qrc 中定义的,打开 qrc 看看那张图片的路径即可。 好了,图片添加完成,然后点击运行,看看效果吧! 瞧!我们只需要修改 QAction,菜单和工具条就已经为我们做好了相应的处理,还是很方便的! 下一步,为 QAction 添加事件响应。还记得 Qt 的事件响应机制是基于信号槽吗?点击 QAction 会发出 triggered() 信号,所以,我们要做的是声名一个 slot,然后 connect这个信号。 mainwindow.h class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void open(); private: QAction *openAction; }; 因为我们的 open()目前只要在类的内部使用,因此定义成 private slots即可。然后修改 cpp文件: MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { openAction = new QAction(tr("&Open"), this); openAction->setShortcut(QKeySequence::Open); openAction->setStatusTip(tr("Open a file.")); openAction->setIcon(QIcon(":/Open.png")); connect(openAction, SIGNAL(triggered()), this, SLOT(open())); QMenu *file = menuBar()->addMenu(tr("&File")); file->addAction(openAction); QToolBar *toolBar = addToolBar(tr("&File")); toolBar->addAction(openAction); } void MainWindow::open() { QMessageBox::information(NULL, tr("Open"), tr("Open a file")); } 注意,我们在 open()函数中简单的弹出一个标准对话框,并没有其他的操作。编译后运行, 好了,关 于QAction 的动作也已经添加完毕了! 至此,QAction有关的问题先告一段落。最后说一下,如果你还不知道怎么添加子菜单的话,看一下 QMenu 的API,里面会有一个 addMenu 函数。也就是说,创建一个 QMenu然后添加就可以的啦! Qt 学习之路(14): 状态栏 有段时间没有写过博客了。假期去上海旅游,所以一直没有能够上网。现在又来到这里,开始新的篇章吧! 今天的内容主要还是继续完善前面的那个程序。我们要为我们的程序加上一个状态栏。 状态栏位于主窗口的最下方,提供一个显示工具提示等信息的地方。一般地,当窗口不是最大化的时候,状 态栏的右下角会有一个可以调节大小的控制点;当窗口最大化的时候,这个控制点会自动消失。Qt 提供了一个 QStatusBar 类来实现状态栏。 Qt具有一个相当成熟的 GUI 框架的实现——这一点感觉比 Swing 要强一些——Qt 似乎对 GUI 的开发做了很 多设计,比如 QMainWindow类里面就有一个 statusBar()函数,用于实现状态栏的调用。类似 menuBar()函数,如 果不存在状态栏,该函数会自动创建一个,如果已经创建则会返回这个状态栏的指针。如果你要替换掉已经存在 的状态栏,需要使用 QMainWindow 的setStatusBar()函数。 在Qt里面,状态栏显示的信息有三种类型:临时信息、一般信息和永久信息。其中,临时信息指临时显示 的信息,比如 QAction的提示等,也可以设置自己的临时信息,比如程序启动之后显示 Ready,一段时间后自动 消失——这个功能可以使用 QStatusBar 的showMessage()函数来实现;一般信息可以用来显示页码之类的;永久信息 是不会消失的信息,比如可以在状态栏提示用户 Caps Lock键被按下之类。 QStatusBar 继承自 QWidget,因此它可以添加其他的 QWidget。下面我们在 QStatusBar 上添加一个 QLabel。 首先在 class 的声明中添加一个私有的 QLabel 属性: private: QAction *openAction; QLabel *msgLabel; 然后在其构造函数中添加: msgLabel = new QLabel; msgLabel->setMinimumSize(msgLabel->sizeHint()); msgLabel->setAlignment(Qt::AlignHCenter); statusBar()->addWidget(msgLabel); 这里,第一行创建一个 QLabel 的对象,然后设置最小大小为其本身的建议大小——注意,这样设置之后, 这个最小大小可能是变化的——最后设置显示规则是水平居中(HCenter)。最后一行使用 statusBar()函数将这个 label 添加到状态栏。编译运行,将鼠标移动到工具条或者菜单的 QAction上,状态栏就会有相应的提示: 看起来是不是很方便?只是,我们很快发现一个问题:当没有任何提示时,状态栏会有一个短短的竖线: 这是什么呢?其实,这是 QLabel 的边框。当没有内容显示时,QLabel 只显示出自己的一个边框。但是,很多 情况下我们并不希望有这条竖线,于是,我们对 statusBar()进行如下设置: statusBar()->setStyleSheet(QString("QStatusBar::item{border: 0px}")); 这里先不去深究这句代码是什么意思,简单来说,就是把 QStatusBar 的子组件的 border 设置为 0,也就是没 有边框。现在再编译试试吧!那个短线消失了! QStatusBar 右下角的大小控制点可以通过 setSizeGripEnabled()函数来设置是否存在,详情参见 API文档。 好了,现在,我们的状态栏已经初步完成了。由于 QStatusBar 可以添加多个 QWidget,因此,我们可以构建 出很复杂的状态栏。 Qt学习之路(15): Qt标准对话框之 QFileDialog Qt学习之路》已经写到了第 15篇,然而现在再写下去却有点困难,原因是当初并没有想到会连续的写下去,因 此并没有很好的计划这些内容究竟该怎样去写。虽然前面说过,本教程主要线路参考《C++ Gui Programming with Qt 4, 2nd Edition》,然而最近的章节由于原文是一个比较完整的项目而有所改变,因此现在不知道该从何写 起。 我并不打算介绍很多组件的使用,因为 Qt有很多组件,各种组件用法众多,根本不可能介绍完,只能把 API 放在手边,边用边查。所以,对于很多组件我只是简单的介绍一下,具体用法还请自行查找(确切地说,我知道 的也并不多,很多时候还是要到 API里面去找)。 下面还是按照我们的进度,从Qt的标准对话框开始说起。所谓标准对话框,其实就是 Qt内置的一些对话框, 比如文件选择、颜色选择等等。今天首先介绍一下 QFileDialog。 QFileDialog是Qt中用于文件打开和保存的对话框,相当于 Swing里面的 JFileChooser。下面打开我们前 面使用的工程。我们已经很有先见之明的写好了一个打开的 action,还记得前面的代码吗?当时,我们只是弹出 了一个消息对话框(这也是一种标准对话框哦~)用于告知这个信号槽已经联通,现在我们要写真正的打开代码了! 修改 MainWindow 的open函数: void MainWindow::open() { QString path = QFileDialog::getOpenFileName(this, tr("Open Image"), ".", tr("Image Files(*.jpg *.png)")); if(path.length() == 0) { QMessageBox::information(NULL, tr("Path"), tr("You didn't select any files.")); } else { QMessageBox::information(NULL, tr("Path"), tr("You selected ") + path); } } 编译之前别忘记 include QFileDialog哦!然后运行一下吧!点击打开按钮,就会弹出打开对话框,然后选择 文件或者直接点击取消,会有相应的消息提示。 QFileDialog提供了很多静态函数,用于获取用户选择的文件。这里我们使用的是 getOpenFileName(), 也 就是“获取打开文件名”,你也可以查看 API找到更多的函数使用。不过,这个函数的参数蛮长的,而且类型都 是QString,并不好记。考虑到这种情况,Qt 提供了另外的写法: QFileDialog *fileDialog = new QFileDialog(this); fileDialog->setWindowTitle(tr("Open Image")); fileDialog->setDirectory("."); fileDialog->setFilter(tr("Image Files(*.jpg *.png)")); if(fileDialog->exec() == QDialog::Accepted) { QString path = fileDialog->selectedFiles()[0]; QMessageBox::information(NULL, tr("Path"), tr("You selected ") + path); } else { QMessageBox::information(NULL, tr("Path"), tr("You didn't select any files.")); } 不过,这两种写法虽然功能差别不大,但是弹出的对话框却并不一样。getOpenFileName()函数在 Windows 和 MacOS X 平台上提供的是本地的对话框,而 QFileDialog 提供的始终是 Qt 自己绘制的对话框(还记得前面说过, Qt的组件和 Swing类似,也是自己绘制的,而不都是调用系统资源 API)。 为了说明 QFileDialog::getOpenFileName()函数的用法,还是先把函数签名放在这里: QString QFileDialog::getOpenFileName ( QWidget * parent = 0, const QString & caption = QString(), const QString & dir = QString(), const QString & filter = QString(), QString * selectedFilter = 0, Options options = 0 ) 第一个参数 parent,用于指定父组件。注意,很多 Qt组件的构造函数都会有这么一个 parent参数,并提 供一个默认值 0; 第二个参数 caption,是对话框的标题; 第三个参数 dir,是对话框显示时默认打开的目录,"." 代表程序运行目录,"/" 代表当前盘符的根目录 (Windows,Linux下/就是根目录了),也可以是平台相关的,比如"C:\\"等; 第四个参数 filter,是对话框的后缀名过滤器,比如我们使用"Image Files(*.jpg *.png)"就让它只能显 示后缀名是 jpg或者 png的文件。如果需要使用多个过滤器,使用";;"分割,比如"JPEG Files(*.jpg);;PNG Files(*.png)"; 第五个参数 selectedFilter,是默认选择的过滤器; 第六个参数 options,是对话框的一些参数设定,比如只显示文件夹等等,它的取值是 enum QFileDialog::Option,每个选项可以使用 | 运算组合起来。 如果我要想选择多个文件怎么办呢?Qt 提供了 getOpenFileNames()函数,其返回值是一个 QStringList。你 可以把它理解成一个只能存放 QString 的List,也就是 STL 中的 list。 好了,我们已经能够选择打开文件了。保存也是类似的,QFileDialog类也提供了保存对话框的函数 getSaveFileName,具体使用还是请查阅 API。 Qt学习之路(16): Qt标准对话框之 QColorDialog 继续来说 Qt的标准对话框,这次说说 QColorDialog。这是 Qt提供的颜色选择对话框。 使用 QColorDialog也很简单,Qt提供了 getColor()函数,类似于 QFileDialog的getOpenFileName(),可 以直接获得选择的颜色。我们还是使用前面的 QAction 来测试下这个函数: QColor color = QColorDialog::getColor(Qt::white, this); QString msg = QString("r: %1, g: %2, b: %3").arg(QString::number(color.red()), QString::number(color.green()), Q String::number(color.blue())); QMessageBox::information(NULL, "Selected color", msg); 不要忘记 include QColorDialog哦!这段代码虽然很少,但是内容并不少。 第一行 QColorDialog::getColor()调用了 QColorDialog的static函数 getColor()。这个函数有两个参数, 第一个是 QColor类型,是对话框打开时默认选择的颜色,第二个是它的 parent。 第二行比较长,涉及到 QString 的用法。如果我没记错的话,这些用法还没有提到过,本着“有用就说”的 原则,尽管这些和 QColorDialog毫不相干,这里还是解释一下。QString("r: %1, g: %2, b: %3")创建了一个 QString对象。我们使用了参数化字符串,也就是那些%1之类。在Java的properties 文件中,字符参数是用{0}, {1}之类实现的。其实这都是一些占位符,也就是,后面会用别的字符串替换掉这些值。占位符的替换需要使用 QString的arg()函数。这个函数会返回它的调用者,因此可以使用链式调用写法。它会按照顺序替换掉占位符。 然后是 QString::number()函数,这也是 QString 的一个 static 函数,作用就是把 int、double等值换成 QString 类型。这里是把 QColor的R、G、B 三个值输出了出来。关于 QString 类,我们会在以后详细说明。 第三行就比较简单了,使用一个消息对话框把刚刚拼接的字符串输出。 现在就可以运行这个测试程序了。看上去很简单,不是吗? QColorDialog 还有一些其他的函数可以使用。 QColorDialog::setCustomColor()可以设置用户自定义颜色。这个函数有两个值,第一个是自定义颜色的索 引,第二个是自定义颜色的 RGB值,类型是 QRgb,大家可以查阅 API文档来看看这个类的使用,下面只给出一 个简单的用发: QColorDialog::setCustomColor(0, QRgb(0x0000FF)); getColor()还有一个重载的函数,签名如下: QColorDialog::( const QColor & initial, QWidget * parent, const QString & title, ColorDialogOptions options = 0 ) 第一个参数 initial 和前面一样,是对话框打开时的默认选中的颜色; 第二个参数 parent,设置对话框的父组件; 第三个参数 title,设置对话框的 title; 第四个参数 options,是 QColorDialog::ColorDialogOptions类型的,可以设置对话框的一些属性,如是 否显示 Alpha值等,具体属性请查阅 API文档。特别的,这些值是可以使用 OR操作的。 QColorDialog 相对简单一些,API文档也很详细,大家遇到问题可以查阅文档的哦! Qt学习之路(17): Qt标准对话框之 QMessageBox 好久没有更新博客,主要是公司里面还在验收一些东西,所以没有及时更新。而且也在写一个基于 Qt的画图程 序,基本上类似于 PS 的东西,主要用到的是 Qt Graphics View Framework。好了,现在还是继续来说说 Qt的标 准对话框吧! 这次来说一下 QMessageBox以及类似的几种对话框。其实,我们已经用过 QMessageBox了,就在之前的几 个程序中。不过,当时是大略的说了一下,现在专门来说说这几种对话框。 先来看一下最熟悉的 QMessageBox::information。我们在以前的代码中这样使用过: QMessageBox::information(NULL, "Title", "Content", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); 下面是一个简单的例子: 现在我们从 API中看看它的函数签名: static StandardButton QMessageBox::information ( QWidget * parent, const QString & title, const QString & text, Stand ardButtons buttons = Ok, StandardButton defaultButton = NoButton ); 首先,它是 static 的,所以我们能够使用类名直接访问到(怎么看都像废话…);然后看它那一堆参数,第一个 参数 parent,说明它的父组件;第二个参数 title,也就是对话框的标题;第三个参数 text,是对话框显示的内容;第四 个参数 buttons,声明对话框放置的按钮,默认是只放置一个 OK按钮,这个参数可以使用或运算,例如我们希望 有一个Yes 和一个No 的按钮,可以使用 QMessageBox::Yes | QMessageBox::No,所有的按钮类型可以在 QMessageBox 声明的StandarButton 枚举中找到;第五个参数 defaultButton 就是默认选中的按钮,默认值是 NoButton,也就是哪个按钮都不选中。这么多参数,豆子也是记不住的啊!所以,我们在用 QtCreator写的时候, 可以在输入 QMessageBox::information 之后输入(,稍等一下,QtCreator就会帮我们把函数签名显示在右上方了, 还是挺方便的一个功能! Qt提供了五个类似的接口,用于显示类似的窗口。具体代码这里就不做介绍,只是来看一下样子吧! QMessageBox::critical(NULL, "critical", "Content", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); QMessageBox::warning(NULL, "warning", "Content", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); QMessageBox::question(NULL, "question", "Content", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); QMessageBox::about(NULL, "About", "About this application"); 请注意,最后一个 about()函数是没有后两个关于 button 设置的按钮的! QMessageBox对话框的文本信息时可以支持 HTML标签的。例如: QMessageBox::about(NULL, "About", "About this application"); 运行效果如下: 如果我们想自定义图片的话,也是很简单的。这时候就不能使用这几个 static 的函数了,而是要我们自己定 义一个 QMessagebox来使用: QMessageBox message(QMessageBox::NoIcon, "Title", "Content with icon."); message.setIconPixmap(QPixmap("icon.png")); message.show(); 需要注意的是,同其他的程序类似,我们在程序中定义的相对路径都是要相对于运行时的.exe文件的地址的。 比如我们写"icon.png",意思是是在.exe 的当前目录下寻找一个"icon.png"的文件。这个程序的运行效果如下: 还有一点要注意,我们使用的是 png 格式的图片。因为 Qt内置的处理图片格式是 png,所以这不会引起很大 的麻烦,如果你要使用 jpeg 格式的图片的话,Qt是以插件的形式支持的。在开发时没有什么问题,不过如果要 部署的话,需要注意这一点。 最后再来说一下怎么处理对话框的交互。我们使用 QMessageBox 类的时候有两种方式,一是使用 static 函数, 另外是使用构造函数。 首先来说一下 static 函数的方式。注意,static 函数都是要返回一个 StandardButton,我们就可以通过判断这 个返回值来对用户的操作做出相应。 QMessageBox::StandardButton rb = QMessageBox::question(NULL, "Show Qt", "Do you want to show Qt dialog?", Q MessageBox::Yes | QMessageBox::No, QMessageBox::Yes); if(rb == QMessageBox::Yes) { QMessageBox::aboutQt(NULL, "About Qt"); } 如果要使用构造函数的方式,那么我们就要自己运行判断一下啦: QMessageBox message(QMessageBox::NoIcon, "Show Qt", "Do you want to show Qt dialog?", QMessageBox::Yes | Q MessageBox::No, NULL); if(message.exec() == QMessageBox::Yes) { QMessageBox::aboutQt(NULL, "About Qt"); } 其实道理上也是差不多的。 Qt学习之路(18): Qt标准对话框之 QInputDialog 这是 Qt标准对话框的最后一部分。正如同其名字显示的一样,QInputDialog 用于接收用户的输入。QInputDialog 提供了一些简单的 static 函数,用于快速的建立一个对话框,正像 QColorDialog 提供了 getColor函数一样。 首先来看看 getText 函数: bool isOK; QString text = QInputDialog::getText(NULL, "Input Dialog", "Please input your comment", QLineEdit::Normal, "your comment", &isOK); if(isOK) { QMessageBox::information(NULL, "Information", "Your comment is: " + text + "", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); } 代码比较简单,使用 getText 函数就可以弹出一个可供用户输入的对话框: 下面来看一下这个函数的签名: static QString QInputDialog::getText ( QWidget * parent, const QString & title, const QString & label, QLineEdit::EchoMode mode = QLineEdit::Normal, const QString & text = QString(), bool * ok = 0, Qt::WindowFlags flags = 0 ) 第一个参数 parent,也就是那个熟悉的父组件的指针;第二个参数 title就是对话框的标题;第三个参数 label 是 在输入框上面的提示语句; 第四个参数 mode 用于指明这个QLineEdit 的输入模式,取值范围是 QLineEdit::EchoMode,默认是 Normal,也就是正常显示,你也可以声明为 password,这样就是密码的输入显示 了,具体请查阅 API;第五个参数 text是QLineEdit 的默认字符串;第六个参数 ok是可选的,如果非 NLL,则当用 户按下对话框的 OK按钮时,这个 bool 变量会被置为 true,可以由这个去判断用户是按下的 OK还是 Cancel,从 而获知这个 text是不是有意义;第七个参数 flags 用于指定对话框的样式。 虽然参数很多,但是每个参数的含义都比较明显,大家只要参照 API就可以知道了。 函数的返回值是 QString,也就是用户在 QLineEdit 里面输入的内容。至于这个内容有没有意义,那就要看那 个ok参数是不是 true 了。 QInputDialog 不仅提供了获取字符串的函数,还有 getInteger,getDouble,getItem 三个类似的函数,这里就 不一一介绍。 Qt 学习之路(19): 事件(event) 前面说了几个标准对话框,下面不打算继续说明一些组件的使用,因为这些使用很难讲完,很多东西都是与实际 应用相关的。实际应用的复杂性决定了我们根本不可能把所有组件的所有使用方法都说明白。这次来说说 Qt相 对高级一点的特性:事件。 事件(event)是有系统或者 Qt本身在不同的时刻发出的。当用户按下鼠标,敲下键盘,或者是窗口需要重新 绘制的时候,都会发出一个相应的事件。一些事件是在对用户操作做出响应的时候发出,如键盘事件等;另一些 事件则是由系统自动发出,如计时器事件。 一般来说,使用 Qt 编程时,我们并不会把主要精力放在事件上,因为在 Qt中,需要我们关心的事件总会发 出一个信号。比如,我们关心的是 QPushButton的鼠标点击,但我们不需要关心这个鼠标点击事件,而是关心它 的clicked()信号。这与其他的一些框架不同:在 Swing中,你所要关心的是 JButton 的ActionListener这个 点击事件。 Qt的事件很容易和信号槽混淆。这里简单的说明一下,signal由具体对象发出,然后会马上交给由 connect 函数连接的 slot 进行处理;而对于事件,Qt使用一个事件队列对所有发出的事件进行维护,当新的事件产生时, 会被追加到事件队列的尾部,前一个事件完成后,取出后面的事件进行处理。但是,必要的时候,Qt的事件也 是可以不进入事件队列,而是直接处理的。并且,事件还可以使用“事件过滤器”进行过滤。总的来说,如果我 们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件。因为我们可以通过事件来改变组 件的默认操作。比如,如果我们要自定义一个 QPushButton,那么我们就需要重写它的鼠标点击事件和键盘处理 事件,并且在恰当的时候发出 clicked()信号。 还记得我们在 main 函数里面创建了一个 QApplication对象,然后调用了它的 exec()函数吗?其实,这个函 数就是开始 Qt 的事件循环。在执行 exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生 时,Qt将创建一个事件对象。Qt的所有事件都继承于 QEvent类。在事件对象创建完毕后,Qt将这个事件对象 传递给 QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件 处理函数(event handler)。关于这一点,我们会在以后的章节中详细说明。 在所有组件的父类 QWidget 中,定义了很多事件处理函数,如 keyPressEvent()、keyReleaseEvent()、 mouseDoubleClickEvent()、mouseMoveEvent ()、mousePressEvent()、mouseReleaseEvent()等。这些函数都是 protected virtual的,也就是说,我们应该在子类中重定义这些函数。下面来看一个例子。 #include #include #include #include class EventLabel : public QLabel { protected: void mouseMoveEvent(QMouseEvent *event); void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); }; void EventLabel::mouseMoveEvent(QMouseEvent *event) { this->setText(QString("

Move: (%1, %2)

") .arg(QString::number(event->x()), QString::number(event->y()) )); } void EventLabel::mousePressEvent(QMouseEvent *event) { this->setText(QString("

Press: (%1, %2)

") .arg(QString::number(event->x()), QString::number(event->y()) )); } void EventLabel::mouseReleaseEvent(QMouseEvent *event) { QString msg; msg.sprintf("

Release: (%d, %d)

", event->x(), event->y()); this->setText(msg); } int main(int argc, char *argv[]) { QApplication app(argc, argv); EventLabel *label = new EventLabel; label->setWindowTitle("MouseEvent Demo"); label->resize(300, 200); label->show(); return app.exec(); } 这里我们继承了 QLabel类,重写了 mousePressEvent、mouseMoveEvent 和MouseReleaseEvent三个函数。 我们并没有添加什么功能,只是在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)时把坐标显示在这个 Label上面。注意我们在 mouseReleaseEvent函数里面有关 QString的构造。我们没有使用 arg参数的方式,而 是使用 C语言风格的 sprintf来构造 QString 对象,如果你对 C 语法很熟悉(估计很多 C+++程序员都会比较熟悉 的吧),那么就可以在 Qt 中试试熟悉的 C 格式化写法啦! Qt 学习之路(20)—事件接收与忽略 本章内容也是关于 Qt 事件。或许这一章不能有一个完整的例子,因为对于事件总是感觉很抽象,还是从底层上 理解一下比较好的吧! 前面说到了事件的作用,下面来看看我们如何来接收事件。回忆一下前面的代码,我们在子类中重写了事件 函数,以便让这些子类按照我们的需要完成某些功能,就像下面的代码: void MyLabel::mousePressEvent(QMouseEvent * event) { if(event->button() == Qt::LeftButton) { // do something } else { QLabel::mousePressEvent(event); } } 上面的代码和前面类似,在鼠标按下的事件中检测,如果按下的是左键,做我们的处理工作,如果不是左键, 则调用父类的函数。这在某种程度上说,是把事件向上传递给父类去响应,也就是说,我们在子类中“忽略”了 这个事件。 我们可以把 Qt的事件传递看成链状:如果子类没有处理这个事件,就会继续向其他类传递。其实,Qt的事 件对象都有一个 accept()函数和 ignore()函数。正如它们的名字,前者用来告诉 Qt,事件处理函数“接收”了 这个事件,不要再传递;后者则告诉 Qt,事件处理函数“忽略”了这个事件,需要继续传递,寻找另外的接受者。 在事件处理函数中,可以使用 isAccepted()来查询这个事件是不是已经被接收了。 事实上,我们很少使用 accept()和ignore()函数,而是想上面的示例一样,如果希望忽略事件,只要调用 父类的响应函数即可。记得我们曾经说过,Qt中的事件大部分是 protected的,因此,重写的函数必定存在着 其父类中的响应函数,这个方法是可行的。为什么要这么做呢?因为我们无法确认父类中的这个处理函数没有操 作,如果我们在子类中直接忽略事件,Qt不会再去寻找其他的接受者,那么父类的操作也就不能进行,这可能 会有潜在的危险。另外我们查看一下 QWidget 的mousePressEvent()函数的实现: void QWidget::mousePressEvent(QMouseEvent *event) { event->ignore(); if ((windowType() == Qt::Popup)) { event->accept(); QWidget* w; while ((w = qApp->activePopupWidget()) && w != this){ w->close(); if (qApp->activePopupWidget() == w) // widget does not want to dissappear w->hide(); // hide at least } if (!rect().contains(event->pos())){ close(); } } } 请注意第一条语句,如果所有子类都没有覆盖 mousePressEvent函数,这个事件会在这里被忽略掉,也就是 停止传播。另外也可以看到,如果你在子类直接 ignore了这个事件,QWidget事件处理函数就不会被调用,那 么,后面的 Popup 操作或许就这么“莫名其妙”地消失了。 不过,事情也不是绝对的。在一个情形下,我们必须使用 accept()和ignore()函数,那就是在窗口关闭的 时候。如果你在窗口关闭时需要有个询问对话框,那么就需要这么去写: void MainWindow::closeEvent(QCloseEvent * event) { if(continueToClose()) { event->accept(); } else { event->ignore(); } } bool MainWindow::continueToClose() { if(QMessageBox::question(this, tr("Quit"), tr("Are you sure to quit this application?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes) { return true; } else { return false; } } 这样,我们经过询问之后才能正常退出程序。 Qt 学习之路(21): event() 今天要说的是 event()函数。记得之前曾经提到过这个函数,说在事件对象创建完毕后,Qt将这个事件对象 传递给 QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型, 分发给不同的事件处理器(event handler)。 event()函数主要用于事件的分发,所以,如果你希望在事件分发之前做一些操作,那么,就需要注意这个 event()函数了。为了达到这种目的,我们可以重写 event()函数。例如,如果你希望在窗口中的 tab 键按下时 将焦点移动到下一组件,而不是让具有焦点的组件处理,那么你就可以继承 QWidget,并重写它的 event()函数, 已达到这个目的: bool MyWidget::event(QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Tab) { // 处理 Tab鍵 return true; } } return QWidget::event(event); } event()函数接受一个 QEvent对象,也就是需要这个函数进行转发的对象。为了进行转发,必定需要有一系 列的类型判断,这就可以调用 QEvent的type()函数,其返回值是 QEvent::Type 类型的枚举。我们处理过自己 需要的事件后,可以直接 return回去,对于其他我们不关心的事件,需要调用父类的 event()函数继续转发, 否则这个组件就只能处理我们定义的事件了。 event()函数返回值是 bool 类型,如果传入的事件已被识别并且处理,返回 true,否则返回 false。如果返 回值是 true,QApplication会认为这个事件已经处理完毕,会继续处理事件队列中的下一事件;如果返回值是 false,QApplication 会尝试寻找这个事件的下一个处理函数。 event()函数的返回值和事件的 accept()和ignore()函数不同。accept()和ignore()函数用于不同的事件 处理器之间的沟通,例如判断这一事件是否处理;event()函数的返回值主要是通知 QApplication的notify()函 数是否处理下一事件。为了更加明晰这一点,我们来看看 QWidget 的event()函数是如何定义的: bool QWidget::event(QEvent *event) { switch (e->type()) { case QEvent::KeyPress: keyPressEvent((QKeyEvent *)event); if (!((QKeyEvent *)event)->isAccepted()) return false; break; case QEvent::KeyRelease: keyReleaseEvent((QKeyEvent *)event); if (!((QKeyEvent *)event)->isAccepted()) return false; break; // more... } return true; } QWidget的event()函数使用一个巨大的 switch来判断 QEvent的type,并且分发给不同的事件处理函数。 在事件处理函数之后,使用这个事件的 isAccepted()方法,获知这个事件是不是被接受,如果没有被接受则 event()函数立即返回 false,否则返回 true。 另外一个必须重写 event()函数的情形是有自定义事件的时候。如果你的程序中有自定义事件,则必须重写 event()函数以便将自定义事件进行分发,否则你的自定义事件永远也不会被调用。关于自定义事件,我们会在 以后的章节中介绍。 Qt 学习之路(22): 事件过滤器 Qt创建了 QEvent事件对象之后,会调用QObject 的 event()函数做事件的分发。有时候,你可能需要在调用 event() 函数之前做一些另外的操作,比如,对话框上某些组件可能并不需要响应回车按下的事件,此时,你就需要重新 定义组件的 event()函数。如果组件很多,就需要重写很多次 event()函数,这显然没有效率。为此,你可以使 用一个事件过滤器,来判断是否需要调用 event()函数。 QOjbect有一个 eventFilter()函数,用于建立事件过滤器。这个函数的签名如下: virtual bool QObject::eventFilter ( QObject * watched, QEvent * event ) 如果 watched 对象安装了事件过滤器,这个函数会被调用并进行事件过滤,然后才轮到组件进行事件处理。 在重写这个函数时,如果你需要过滤掉某个事件,例如停止对这个事件的响应,需要返回 true。 bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (obj == textEdit) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); qDebug() << "Ate key press" << keyEvent->key(); return true; } else { return false; } } else { // pass the event on to the parent class return QMainWindow::eventFilter(obj, event); } } 上面的例子中为 MainWindow建立了一个事件过滤器。为了过滤某个组件上的事件,首先需要判断这个对象 是哪个组件,然后判断这个事件的类型。例如,我不想让 textEdit组件处理键盘事件,于是就首先找到这个组 件,如果这个事件是键盘事件,则直接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以 返回 false。对于其他组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。 在创建了过滤器之后,下面要做的是安装这个过滤器。安装过滤器需要调用 installEventFilter()函数。 这个函数的签名如下: void QObject::installEventFilter ( QObject * filterObj ) 这个函数是 QObject 的一个函数,因此可以安装到任何 QObject 的子类,并不仅仅是 UI组件。这个函数接 收一个 QObject对象,调用了这个函数安装事件过滤器的组件会调用 filterObj定义的 eventFilter()函数。例 如,textField.installEventFilter(obj),则如果有事件发送到 textField组件是,会先调用 obj->eventFilter() 函数,然后才会调用 textField.event()。 当然,你也可以把事件过滤器安装到 QApplication上面,这样就可以过滤所有的事件,已获得更大的控制 权。不过,这样做的后果就是会降低事件分发的效率。 如果一个组件安装了多个过滤器,则最后一个安装的会最先调用,类似于堆栈的行为。 注意,如果你在事件过滤器中 delete了某个接收组件,务必将返回值设为 true。否则,Qt还是会将事件分 发给这个接收组件,从而导致程序崩溃。 事件过滤器和被安装的组件必须在同一线程,否则,过滤器不起作用。另外,如果在 install 之后,这两个 组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。 事件的调用最终都会调用 QCoreApplication 的notify()函数,因此,最大的控制权实际上是重写 QCoreApplication的notify()函数。由此可以看出,Qt 的事件处理实际上是分层五个层次:重定义事件处理函 数,重定义 event()函数,为单个组件安装事件过滤器,为 QApplication 安装事件过滤器,重定义 QCoreApplication的notify()函数。这几个层次的控制权是逐层增大的。 Qt 学习之路(23): 自定义事件 这部分将作为 Qt事件部分的结束。我们在前面已经从大概上了解了 Qt 的事件机制。下面要说的是如何自定义事 件。 Qt允许你创建自己的事件类型,这在多线程的程序中尤其有用,当然,也可以用在单线程的程序中,作为 一种对象间通讯的机制。那么,为什么我需要使用事件,而不是使用信号槽呢?主要原因是,事件的分发既可以 是同步的,又可以是异步的,而函数的调用或者说是槽的回调总是同步的。事件的另外一个好处是,它可以使用 过滤器。 Qt中的自定义事件很简单,同其他类似的库的使用很相似,都是要继承一个类进行扩展。在Qt中,你需要 继承的类是 QEvent。注意,在Qt3中,你需要继承的类是 QCustomEvent,不过这个类在 Qt4 中已经被废除(这里 的废除是不建议使用,并不是从类库中删除)。 继承 QEvent 类,你需要提供一个 QEvent::Type 类型的参数,作为自定义事件的类型值。这里的 QEvent::Type 类型是 QEvent里面定义的一个 enum,因此,你是可以传递一个 int的。重要的是,你的事件类型不能和已经存 在的 type 值重复,否则会有不可预料的错误发生!因为系统会将你的事件当做系统事件进行派发和调用。在 Qt 中,系统将保留 0 - 999的值,也就是说,你的事件 type要大于 999. 具体来说,你的自定义事件的 type要在 QEvent::User和QEvent::MaxUser的范围之间。其中,QEvent::User值是 1000,QEvent::MaxUser的值是 65535。 从这里知道,你最多可以定义 64536个事件,相信这个数字已经足够大了!但是,即便如此,也只能保证用户自 定义事件不能覆盖系统事件,并不能保证自定义事件之间不会被覆盖。为了解决这个问题,Qt提供了一个函数: registerEventType(),用于自定义事件的注册。该函数签名如下: static int QEvent::registerEventType ( int hint = -1 ); 函数是 static的,因此可以使用 QEvent类直接调用。函数接受一个 int 值,其默认值为-1,返回值是创建 的这个 Type类型的值。如果 hint 是合法的,不会发生任何覆盖,则会返回这个值;如果 hint不合法,系统会自 动分配一个合法值并返回。因此,使用这个函数即可完成 type 值的指定。这个函数是线程安全的,因此不必另 外添加同步。 你可以在 QEvent子类中添加自己的事件所需要的数据,然后进行事件的发送。Qt中提供了两种发送方式: static bool QCoreApplication::sendEvent(QObjecy * receiver, QEvent * event):事件被 QCoreApplication的notify()函数直接发送给 receiver对象,返回值是事件处理函数的返回值。使用这个函数 必须要在栈上创建对象,例如: QMouseEvent event(QEvent::MouseButtonPress, pos, 0, 0, 0); QApplication::sendEvent(mainWindow, &event); static bool QCoreApplication::postEvent(QObject * receiver, QEvent * event):事件被 QCoreApplication追加到事件列表的最后,并等待处理,该函数将事件追加后会立即返回,并且注意,该函数 是线程安全的。另外一点是,使用这个函数必须要在堆上创建对象,例如: QApplication::postEvent(object, new MyEvent(QEvent::registerEventType(2048))); 这个对象不需要手动 delete,Qt会自动 delete掉!因此,如果在 post 事件之后调用 delete,程序可能会 崩溃。另外,postEvent()函数还有一个重载的版本,增加一个优先级参数,具体请参见 API。通过调用 sendPostedEvent()函数可以让已提交的事件立即得到处理。 如果要处理自定义事件,可以重写 QObject 的customEvent()函数,该函数接收一个 QEvent对象作为参数。 注意,在 Qt3中这个参数是 QCustomEvent类型的。你可以像前面介绍的重写 event()函数的方法去重写这个函 数: void CustomWidget::customEvent(QEvent *event) { CustomEvent *customEvent = static_cast(event); //.... } 另外,你也可以通过重写 event()函数来处理自定义事件: bool CustomWidget::event(QEvent *event) { if (event->type() == MyCustomEventType) { CustomEvent *myEvent = static_cast(event); // processing... return true; } return QWidget::event(event); } 这两种办法都是可行的。 好了,至此,我们已经概略的介绍了 Qt的事件机制,包括事件的派发、自定义等一系列的问题。下面的章 节将继续我们的学习之路! Qt学习之路(25): QPainter(续)(图) 过去一天没有接上上章的东西,今天继续啊! 首先还是要先把上次的代码拿上来。 void PaintedWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.drawLine(80, 100, 650, 500); painter.setPen(Qt::red); painter.drawRect(10, 10, 100, 400); painter.setPen(QPen(Qt::green, 5)); painter.setBrush(Qt::blue); painter.drawEllipse(50, 150, 400, 200); } 上次我们说的是 Qt绘图相关的架构,以及 QPainter 的建立和 drawXXXX函数。可以看到,基本上代码中已经设 计到得函数还剩下两个:setPen()和setBrush()。现在,我们就要把这两个函数讲解一下。 Qt绘图系统提供了三个主要的参数设置,画笔(pen)、画刷(brush)和字体(font)。这里我们要说明的是画笔和 画刷。 所谓画笔,是用于绘制线的,比如线段、轮廓线等,都需要使用画笔绘制。画笔类即 QPen,可以设置画笔的样 式,例如虚线、实现之类,画笔的颜色,画笔的转折点样式等。画笔的样式可以在创建时指定,也可以由 setStyle() 函数指定。画笔支持三种主要的样式:笔帽(cap),结合点(join)和线形 (line)。这些样式具体显示如下(图片 来自 C++ GUI Programming with Qt4, 2nd Edition): 上图共分成三行:第一行是 Cap样式,第二行是 Join样式,第三行是 Line样式。QPen 允许你使用 setCapStyle()、 setJoinStyle()和setStyle()分别进行设置。具体请参加 API文档。 所谓画刷,主要用来填充封闭的几何图形。画刷主要有两个参数可供设置:颜色和样式。当然,你也可以使用纹 理或者渐变色来填充图形。请看下面的图片(图片出自 Qt API 文档): 这里给出了不同 style的画刷的表现。同画笔类似,这些样式也可用通过一个 enum 进行设置。 明白了这些之后我们再来看看我们的代码。首先,我们直接使用 drawLine()函数,由于没有设置任何样式,所 以使用的是默认的 1px,,黑色,solid 样式画了一条直线;然后使用 setPen()函数,将画笔设置成 Qt::red,即 红色,画了一个矩形;最后将画笔设置成绿色,5px,画刷设置成蓝色,画了一个椭圆。这样便显示出了我们最 终的样式: 另外要说明一点,请注意我们的绘制顺序,首先是直线,然后是矩形,最后是椭圆。这样,因为椭圆是最后画的, 因此在最上方。 在我们学习 OpenGL的时候,肯定听过这么一句话:OpenGL是一个状态机。所谓状态机,就是说,OpenGL保存的 只是各种状态。怎么理解呢?比如,你把颜色设置成红色,那么,直到你重新设置另外的颜色,它的颜色会一直 是红色。QPainter也是这样,它的状态不会自己恢复,除非你使用了各种 set函数。因此,如果在上面的代码 中,我们在椭圆绘制之后再画一个椭圆,它的样式还会是绿色 5px的轮廓和蓝色的填充,除非你显式地调用了 set进行更新。这可能是绘图系统较多的实现方式,因为无论是 OpenGL、QPainter还是 Java2D,都是这样实现 的(DirectX不大清楚)。 Qt 学习之路(27): 渐变填充 前面说了有关反走样的相关知识,下面来说一下渐变。渐变是绘图中很常见的一种功能,简单来说就是可以把几 种颜色混合在一起,让它们能够自然地过渡,而不是一下子变成另一种颜色。渐变的算法比较复杂,写得不好的 话效率会很低,好在很多绘图系统都内置了渐变的功能,Qt也不例外。渐变一般是用在填充里面的,所以,渐变 的设置就是在 QBrush 里面。 Qt 提供了三种渐变画刷,分别是线性渐变(QLinearGradient)、辐射渐变(QRadialGradient)、角度渐变 (QConicalGradient)。如下图所示(图片出自 C++ GUI Programming with Qt4, 2nd Edition): 下面我们来看一下线性渐变 QLinearGradient 的用法。 void PaintedWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); QLinearGradient linearGradient(60, 50, 200, 200); linearGradient.setColorAt(0.2, Qt::white); linearGradient.setColorAt(0.6, Qt::green); linearGradient.setColorAt(1.0, Qt::black); painter.setBrush(QBrush(linearGradient)); painter.drawEllipse(50, 50, 200, 150); } 同前面一样,这里也仅仅给出了 paintEvent()函数里面的代码。 首先我们打开了反走样,然后创建一个 QLinearGradient 对象实例。QLinearGradient 构造函数有四个参数, 分别是 x1, y1, x2, y2,即渐变的起始点和终止点。在这里,我们从(60, 50)开始渐变,到(200, 200)止。 渐变的颜色是在 setColorAt()函数中指定的。下面是这个函数的签名: void QGradient::setColorAt ( qreal position, const QColor & color ) 它的意思是把 position 位置的颜色设置成 color。其中,position 是一个 0 - 1区间的数字。也就是说,position 是相对于我们建立渐变对象时做的那个起始点和终止点区间的。比如这个线性渐变,就是说,在从(60, 50)到 (200, 200)的线段上,在0.2,也就五分之一处设置成白色,在0.6也就是五分之三处设置成绿色,在1.0也就是终 点处设置成黑色。 在创建 QBrush 时,把这个渐变对象传递进去,就是我们的结果啦: 那么,我们怎么让线段也是渐变的呢?要知道,直线是用画笔绘制的啊!这里,如果你仔细查阅了 API文档就 会发现,QPen是接受 QBrush 作为参数的。也就是说,你可以利用一个 QBrush 创建一个 QPen,这样,QBrush 所有的填充效果都可以用在画笔上了! void PaintedWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); QLinearGradient linearGradient(60, 50, 200, 200); linearGradient.setColorAt(0.2, Qt::white); linearGradient.setColorAt(0.6, Qt::green); linearGradient.setColorAt(1.0, Qt::black); painter.setPen(QPen(QBrush(linearGradient), 5)); painter.drawLine(50, 50, 200, 200); } 看看我们的渐变线吧! Qt 学习之路(28): 坐标变换 经过前面的章节,我们已经能够画出一些东西来,主要就是使用 QPainter的相关函数。今天,我们要看的是 QPainter的坐标系统。 同很多坐标系统一样,QPainter的默认坐标的原点(0, 0)位于屏幕的左上角,X轴正方向是水平向右,Y轴 正方向是竖直向下。在这个坐标系统中,每个像素占据 1 x 1的空间。你可以把它想象成是一张坐标值,其中的 每个小格都是 1个像素。这么说来,一个像素的中心实际上是一个“半像素坐标系”,也就是说,像素(x, y) 的中心位置其实是在(x + 0.5, y + 0.5)的位置上。因此,如果我们使用 QPainter 在(100, 100)处绘制一个像 素,那么,这个像素的中心坐标是(100.5, 100.5)。 这种细微的差别在实际应用中,特别是对坐标要求精确的系统中是很重要的。首先,只有在禁止反走样,也 就是默认状态下,才会有这 0.5像素的偏移;如果使用了反走样,那么,我们画(100, 100)位置的像素时,QPainter 会在(99.5, 99.5),(99.5, 100.5),(100.5, 99.5)和(100.5, 100.5)四个位置绘制一个亮色的像素,这么产生 的效果就是在这四个像素的焦点处(100, 100)产生了一个像素。如果不需要这个特性,就需要将 QPainter的坐 标系平移(0.5, 0.5)。 这一特性在绘制直线、矩形等图形的时候都会用到。下图给出了在没有反走样技术时,使用 drawRect(2, 2, 6, 5)绘制一个矩形的示例。在 No Pen的情况下,请注意矩形左上角的像素是在(2, 2),其中 心位置是在(2.5, 2.5)的位置。然后注意下有不同的 Pen 的值的绘制样式,在Pen宽为 1时,实际画出的矩形的 面积是 7 x 6 的(图出自 C++ GUI Programming with Qt4, 2nd Edition): 在具有反走样时,使用 drawRect(2, 2, 6, 5)的效果如下(图出自 C++ GUI Programming with Qt4, 2nd Edition): 注意我们前面说过,通过平移 QPainter的坐标系来消除着 0.5像素的差异。下面给出了使用 drawRect(2.5, 2.5, 6, 5)在反走样情况下绘制的矩形(图出自 C++ GUI Programming with Qt4, 2nd Edition): 请对比与上图的区别。 在上述的 QPainter的默认坐标系下,QPainter提供了视口(viewport)窗口(window)机制,用于绘制与绘制 设备的大小和分辨率无关的图形。视口和窗口是紧密的联系在一起的,它们一般都是矩形。视口是由物理坐标确 定其大小,而窗口则是由逻辑坐标决定。我们在使用 QPainter进行绘制时,传给 QPainter的是逻辑坐标,然后, Qt的绘图机制会使用坐标变换将逻辑坐标转换成物理坐标后进行绘制。 通常,视口和窗口的坐标是一致的。比如一个 600 x 800的widget(这是一个 widget,或许是一个对话框, 或许是一个面板等等),默认情况下,视口和窗口都是一个 320 x 200的矩形,原点都在(0, 0),此时,视口和 窗口的坐标是相同的。 注意到 QPainter 提供了 setWindow()和setViewport()函数,用来设置视口和窗口的矩形大小。比如,在上 面所述的 320 x 200的widget中,我们要设置一个从(-50, -50)到(+50, +50),原点在中心的矩形窗口,就可 以使用 painter.setWindow(-50, -50, 100, 100); 其中,(-50, -50)指明了原点,100, 100 指明了窗口的长和宽。这里的“指明原点”意思是,逻辑坐标的 (-50, -50)对应着物理坐标的(0, 0);“长和宽”说明,逻辑坐标系下的长 100,宽 100 实际上对应物理坐标系 的长 320,宽 200。 或许你已经发现这么一个好处,我们可以随时改变 window的范围,而不改变底层物理坐标系。这就是前面 所说的,视口与窗口的作用:“绘制与绘制设备的大小和分辨率无关的图形”,如下图所示(图出自 C++ GUI Programming with Qt4, 2nd Edition): 除了视口与窗口的变化,QPainter还提供了一个“世界坐标系”,同样也可以变换图形。所不同的是,视 口与窗口实际上是统一图形在两个坐标系下的表达,而世界坐标系的变换是通过改变坐标系来平移、缩放、旋转、 剪切图形。为了清楚起见,我们来看下面一个例子: void PaintedWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); QFont font("Courier", 24); painter.setFont(font); painter.drawText(50, 50, "Hello, world!"); QTransform transform; transform.rotate(+45.0); painter.setWorldTransform(transform); painter.drawText(60, 60, "Hello, world!"); } 为了显示方便,我在这里使用了 QFont改变了字体。QPainter的drawText()函数提供了绘制文本的功能。 它有几种重载形式,我们使用了其中的一种,即制定文本的坐标然后绘制。需要注意的是,这里的坐标是文字左 下角的坐标(特别提醒这一点,因为很多绘图系统,比如 Java2D都是把左上角作为坐标点的)!下面是运行结果: 我们使用 QTransform做了一个 rotate变换。这个变换就是旋转,而且是顺时针旋转 45度。然后我们使用 这个变换设置了 QPainter的世界坐标系,注意到 QPainter是一个状态机,所以这种变换并不会改变之前的状态, 因此只有第二个 Hello, world!被旋转了。确切的说,被旋转的是坐标系而不是这个文字!请注意体会这两种说 法的不同。 Qt 学习之路(29): 绘图设备 绘图设备是指继承 QPainterDevice 的子类。Qt一共提供了四个这样的类,分别是 QPixmap、QBitmap、QImage 和QPicture。其中,QPixmap 专门为图像在屏幕上的显示做了优化,而QBitmap是QPixmap的一个子类,它的色 深限定为 1,你可以使用 QPixmap的isQBitmap()函数来确定这个 QPixmap是不是一个 QBitmap。QImage专门为 图像的像素级访问做了优化。QPicture则可以记录和重现 QPainter的各条命令。下面我们将分两部分介绍这四 种绘图设备。 QPixmap继承了 QPaintDevice,因此,你可以使用 QPainter直接在上面绘制图形。QPixmap 也可以接受一 个字符串作为一个文件的路径来显示这个文件,比如你想在程序之中打开 png、jpeg之类的文件,就可以使用 QPixmap。使用 QPainter 的drawPixmap()函数可以把这个文件绘制到一个 QLabel、QPushButton或者其他的设 备上面。QPixmap是针对屏幕进行特殊优化的,因此,它与实际的底层显示设备息息相关。注意,这里说的显示 设备并不是硬件,而是操作系统提供的原生的绘图引擎。所以,在不同的操作系统平台下,QPixmap的显示可能 会有所差别。 QPixmap提供了静态的 grabWidget()和grabWindow()函数,用于将自身图像绘制到目标上。同时,在使用 QPixmap时,你可以直接使用传值也不需要传指针,因为 QPixmap提供了“隐式数据共享”。关于这一点,我们 会在以后的章节中详细描述,这里只要知道传递 QPixmap不必须使用指针就好了。 QBitmap继承自 QPixmap,因此具有 QPixmap 的所有特性。QBitmap 的色深始终为 1. 色深这个概念来自计算 机图形学,是指用于表现颜色的二进制的位数。我们知道,计算机里面的数据都是使用二进制表示的。为了表示 一种颜色,我们也会使用二进制。比如我们要表示 8种颜色,需要用 3个二进制位,这时我们就说色深是 3. 因 此,所谓色深为 1,也就是使用 1 个二进制位表示颜色。1 个位只有两种状态:0 和1,因此它所表示的颜色就有 两种,黑和白。所以说,QBitmap实际上是只有黑白两色的图像数据。 由于 QBitmap 色深小,因此只占用很少的存储空间,所以适合做光标文件和笔刷。 下面我们来看同一个图像文件在 QPixmap 和QBitmap下的不同表现: void PaintedWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); QPixmap pixmap("Cat.png"); QBitmap bitmap("Cat.png"); painter.drawPixmap(10, 10, 128, 128, pixmap); painter.drawPixmap(140, 10, 128, 128, bitmap); QPixmap pixmap2("Cat2.png"); QBitmap bitmap2("Cat2.png"); painter.drawPixmap(10, 140, 128, 128, pixmap2); painter.drawPixmap(140, 140, 128, 128, bitmap2); } 这里我们给出了两张 png图片。Cat.png 是没有透明色的纯白背景,而 Cat2.png是具有透明色的背景。我 们分别使用 QPixmap 和QBitmap 来加载它们。注意看它们的区别:白色的背景在 Qbitmap中消失了,而透明色在 QBitmap中转换成了黑色;其他颜色则是使用点的疏密程度来体现的。 QPixmap使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而 QImage 则是使用独立于硬件的 绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示 形式。 如上图所示(出自 Qt API文档),我们声明了一个 QImage对象,大小是 3 x 3,颜色模式是 RGB32,即使用 32 位数值表示一个颜色的 RGB值,也就是说每种颜色使用 8位。然后我们对每个像素进行颜色赋值,从而构成了这 个图像。你可以把 QImage想象成一个 RGB颜色的二维数组,记录了每一像素的颜色。 最后一个需要说明的是 QPicture。这是一个可以记录和重现 QPainter命令的绘图设备。QPicture将 QPainter的命令序列化到一个 IO设备,保存为一个平台独立的文件格式。这种格式有时候会是“元文件 (meta-files)”。Qt的这种格式是二进制的,不同于某些本地的元文件,Qt的pictures文件没有内容上的限制, 只要是能够被 QPainter绘制的元素,不论是字体还是 pixmap,或者是变换,都可以保存进一个 picture中。 QPicture是平台无关的,因此它可以使用在多种设备之上,比如 svg、pdf、ps、打印机或者屏幕。回忆下 我们这里所说的 QPaintDevice,实际上是说可以有 QPainter绘制的对象。QPicture 使用系统的分辨率,并且可 以调整 QPainter来消除不同设备之间的显示差异。 如果我们要记录下 QPainter的命令,首先要使用 QPainter::begin()函数,将QPicture实例作为参数传递 进去,以便告诉系统开始记录,记录完毕后使用 QPainter::end()命令终止。代码示例如下: QPicture picture; QPainter painter; painter.begin(&picture); // paint in picture painter.drawEllipse(10,20, 80,70); // draw an ellipse painter.end(); // painting done picture.save("drawing.pic"); // save picture 如果我们要重现命令,首先要使用 QPicture::load()函数进行装载: QPicture picture; picture.load("drawing.pic"); // load picture QPainter painter; painter.begin(&myImage); // paint in myImage painter.drawPicture(0, 0, picture); // draw the picture at (0,0) painter.end(); C++ Qt学习之路(30): Graphics View Framework 现在基本上也已经到了 2D绘图部分的尾声,所谓重头戏都是在最后压轴的,现在我们就要来看看在绘图部分功 能最强大的 Graphics View。我们经常说 KDE桌面,新版本的 KDE桌面就是建立在 Graphics View的基础之上, 可见其强大之处。 Qt的白皮书里面这样写道:“Qt Graphics View 提供了用于管理和交互大量定制的 2D 图形对象的平面以 及可视化显示对象的视图 widget,并支持缩放和旋转功能。Graphics View 使用 BSP(二进制空间划分)树形可 非常快速地找到对象,因此即使是包含百万个对象的大型场景,也能实时图形化显示。” Graphics View是一个基于 item的M-V架构的框架。 基于 item 意思是,它的每一个组件都是一个 item。这是与 QPainter的状态机不同。回忆一下,使用 QPainter 绘图多是采用一种面向过程的描述方式,首先使用 drawLine()画一条直线,然后使用 drawPolygon()画一个多边 形;而对于 Graphics View 来说,相同的过程可以是,首先创建一个场景 scene,然后创建一个 line对象和一个 polygon对象,再使用 scene的add()函数将 line 和polygon 添加到 scene,最后通过视口 view就可以看到了。 乍看起来,后者似乎更加复杂,但是,如果你的图像中包含了成千上万的直线、多边形之类,管理这些对象要比 管理 QPainter 的draw语句容易得多。并且,这些图形对象也更加符合面向对象的设计要求:一个很复杂的图形 可以很方便的复用。 M-V 架构的意思是,Graphics View提供一个 model 和一个 view。所谓 model就是我们添加的种种对象,所 谓view 就是我们观察这些对象的视口。同一个 model 可以由很多 view 从不同的角度进行观察,这是很常见的需 求。使用 QPainter就很难实现这一点,这需要很复杂的计算,而 Qt的Graphics View 就可以很容易的实现。 Graphics View提供了一个 QGraphicsScene作为场景,即是我们添加图形的空间,相当于整个世界;一个 QGraphicsView作为视口,也就是我们观察的窗口,相当于照相机的取景框,这个取景框可以覆盖整个场景,也 可以是场景的一部分;一些 QGraphicsItem 作为图形元件,以便 scene 添加,Qt内置了很多图形,比如 line、 polygon等,都是继承自 QGraphicsItem。 下面我们来看一下代码: #include class DrawApp : public QWidget { public: DrawApp(); protected: void paintEvent(QPaintEvent *event); }; DrawApp::DrawApp() { } void DrawApp::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.drawLine(10, 10, 150, 300); } int main(int argc, char *argv[]) { QApplication a(argc, argv); QGraphicsScene *scene = new QGraphicsScene; scene->addLine(10, 10, 150, 300); QGraphicsView *view = new QGraphicsView(scene); view->resize(500, 500); view->setWindowTitle("Graphics View"); view->show(); DrawApp *da = new DrawApp; da->resize(500, 500); da->setWindowTitle("QWidget"); da->show(); return a.exec(); } 为了突出重点,我们就直接 include了QtGui,不过在实际应用中不建议这么做。这里提供了直线的两种实 现:一个是 DrawApp 使用我们前面介绍的技术,重写 paintEvent()函数,这里就不在赘述,重点来看 main()函 数里面的实现。 Qt学习之路(31): 一个简易画板的实现(QWidget) 实话,本来我是没有打算放一个很大的例子的,一则比较复杂,二来或许需要很多次才能说得完。不过,现在已 经说完了绘图部分,所以计划还是上一个这样的例子。这里我会只做出一个简单的画板程序,大体上就是能够画 直线和矩形吧。这样,我计划分成两种实现,一是使用普通的 QWidget作为画板,第二则是使用 Graphcis View Framework来实现。因为前面有朋友说不大明白 Graphics View的相关内容,所以计划如此。 好了,现在先来看看我们的主体框架。我们的框架还是使用 Qt Creator 创建一个 Gui Application 工程。 简单的 main()函数就不再赘述了,这里首先来看 MainWindow。顺便说一下,我一般不会使用 ui文件,所以 这些内容都是手写的。首先先来看看最终的运行结果 或许很简单,但是至少我们能够把前面所说的各种知识串连起来,这也就达到目的了。 现在先来看看 MainWindow 的代码: mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include #include "shape.h" #include "paintwidget.h" class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); signals: void changeCurrentShape(Shape::Code newShape); private slots: void drawLineActionTriggered(); void drawRectActionTriggered(); }; #endif //MAINWINDOW_H mainwindow.cpp #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QToolBar *bar = this->addToolBar("Tools"); QActionGroup *group = new QActionGroup(bar); QAction *drawLineAction = new QAction("Line", bar); drawLineAction->setIcon(QIcon(":/line.png")); drawLineAction->setToolTip(tr("Draw a line.")); drawLineAction->setStatusTip(tr("Draw a line.")); drawLineAction->setCheckable(true); drawLineAction->setChecked(true); group->addAction(drawLineAction); bar->addAction(drawLineAction); QAction *drawRectAction = new QAction("Rectangle", bar); drawRectAction->setIcon(QIcon(":/rect.png")); drawRectAction->setToolTip(tr("Draw a rectangle.")); drawRectAction->setStatusTip(tr("Draw a rectangle.")); drawRectAction->setCheckable(true); group->addAction(drawRectAction); bar->addAction(drawRectAction); QLabel *statusMsg = new QLabel; statusBar()->addWidget(statusMsg); PaintWidget *paintWidget = new PaintWidget(this); setCentralWidget(paintWidget); connect(drawLineAction, SIGNAL(triggered()), this, SLOT(drawLineActionTriggered())); connect(drawRectAction, SIGNAL(triggered()), this, SLOT(drawRectActionTriggered())); connect(this, SIGNAL(changeCurrentShape(Shape::Code)), paintWidget, SLOT(setCurrentShape(Shape::Code))); } void MainWindow::drawLineActionTriggered() { emit changeCurrentShape(Shape::Line); } void MainWindow::drawRectActionTriggered() { emit changeCurrentShape(Shape::Rect); } 应该说,从以往的学习中可以看出,这里的代码没有什么奇怪的了。我们在 MainWindow 类里面声明了一个信号, changeCurrentShape(Shape::Code),用于按钮按下后通知画图板。注意,QActio的triggered()信号是没有参 数的,因此,我们需要在 QAction 的槽函数中重新 emit我们自己定义的信号。构造函数里面创建了两个 QAction, 一个是 drawLineAction,一个是 drawRectAction,分别用于绘制直线和矩形。MainWindow的中心组件是 PainWidget,也就是我们的画图板。下面来看看 PaintWidget类: paintwidget.h #ifndef PAINTWIDGET_H #define PAINTWIDGET_H #include #include #include "shape.h" #include "line.h" #include "rect.h" class PaintWidget : public QWidget { Q_OBJECT public: PaintWidget(QWidget *parent = 0); public slots: void setCurrentShape(Shape::Code s) { if(s != currShapeCode) { currShapeCode = s; } } protected: void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); private: Shape::Code currShapeCode; Shape *shape; bool perm; QList shapeList; }; #endif //PAINTWIDGET_H paintwidget.cpp #include "paintwidget.h" PaintWidget::PaintWidget(QWidget *parent) : QWidget(parent), currShapeCode(Shape::Line), shape(NULL), perm(false) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } void PaintWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setBrush(Qt::white); painter.drawRect(0, 0, size().width(), size().height()); foreach(Shape * shape, shapeList) { shape->paint(painter); } if(shape) { shape->paint(painter); } } void PaintWidget::mousePressEvent(QMouseEvent *event) { switch(currShapeCode) { case Shape::Line: { shape = new Line; break; } case Shape::Rect: { shape = new Rect; break; } } if(shape != NULL){ perm = false; shapeList< shape->setStart(event->pos()); shape->setEnd(event->pos()); } } void PaintWidget::mouseMoveEvent(QMouseEvent *event) { if(shape &&!perm) { shape->setEnd(event->pos()); update(); } } void PaintWidget::mouseReleaseEvent(QMouseEvent *event) { perm = true; } PaintWidget类定义了一个 slot,用于接收改变后的新的 ShapeCode。最主要的是,PaintWidget重定义了 三个关于鼠标的事件:mousePressEvent,mouseMoveEvent和mouseReleaseEvent。 我们来想象一下如何绘制一个图形:图形的绘制与鼠标操作息息相关。以画直线为例,首先我们需要按下鼠 标,确定直线的第一个点,所以在 mousePressEvent里面,我们让 shape保存下 start点。然后在鼠标按下的状 态下移动鼠标,此时,直线就会发生变化,实际上是直线的终止点在随着鼠标移动,所以在 mouseMoveEvent中 我们让 shape保存下 end点,然后调用 update()函数,这个函数会自动调用 paintEvent()函数,显示出我们绘 制的内容。最后,当鼠标松开时,图形绘制完毕,我们将一个标志位置为 true,此时说明这个图形绘制完毕。 为了保存我们曾经画下的图形,我们使用了一个 List。每次按下鼠标时,都会把图形存入这个 List。可以 看到,我们在 paintEvent()函数中使用了 foreach遍历了这个 List,绘制出历史图形。foreach是Qt提供的一 个宏,用于遍历集合中的元素。 最后我们来看看 Shape类。 shape.h #ifndef SHAPE_H #define SHAPE_H #include class Shape { public: enum Code { Line, Rect }; Shape(); void setStart(QPoint s) { start = s; } void setEnd(QPoint e) { end = e; } QPoint startPoint() { return start; } QPoint endPoint() { return end; } void virtual paint(QPainter & painter) = 0; protected: QPoint start; QPoint end; }; #endif //SHAPE_H shape.cpp #include "shape.h" Shape::Shape() { } Shape类最重要的就是保存了 start和end两个点。为什么只要这两个点呢?因为我们要绘制的是直线和矩 形。对于直线来说,有了两个点就可以确定这条直线,对于矩形来说,有了两个点作为左上角的点和右下角的点 也可以确定这个矩形,因此我们只要保存两个点,就足够保存这两种图形的位置和大小的信息。paint()函数是 Shape类的一个纯虚函数,子类都必须实现这个函数。我们现在有两个子类:Line 和Rect,分别定义如下: line.h #ifndef LINE_H #define LINE_H #include "shape.h" class Line : public Shape { public: Line(); void paint(QPainter &painter); }; #endif //LINE_H line.cpp #include "line.h" Line::Line() { } void Line::paint(QPainter &painter) { painter.drawLine(start, end); } rect.h #ifndef RECT_H #define RECT_H #include "shape.h" class Rect : public Shape { public: Rect(); void paint(QPainter &painter); }; #endif //RECT_H rect.cpp #include "rect.h" Rect::Rect() { } void Rect::paint(QPainter &painter) { painter.drawRect(start.x(), start.y(), end.x() - start.x(), end.y() - start.y()); } 使用 paint()函数,根据两个点的数据,Line和Rect 都可以绘制出它们自身来。此时就可以看出,我们之 所以要建立一个 Shape作为父类,因为这两个类有几乎完全相似的数据对象,并且从语义上来说,Line、Rect 与Shape也完全是一个 is-a 的关系。如果你想要添加颜色等的信息,完全可以在 Shape类进行记录。这也就是 类层次结构的好处。 代码很多也会比较乱,附件里面是整个工程的文件,有兴趣的朋友不妨看看哦! Qt学习之路(32): 一个简易画板的实现 这一次将介绍如何使用 Graphics View 来实现前面所说的画板。前面说了很多有关 Graphics View 的好话,但是 没有具体的实例很难说究竟好在哪里。现在我们就把前面的内容使用 Graphics View重新实现一下,大家可以对 比一下看有什么区别。 同前面相似的内容就不再叙述了,我们从上次代码的基础上进行修改,以便符合我们的需要。首先来看 MainWindow的代码: mainwindow.cpp #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QToolBar *bar = this->addToolBar("Tools"); QActionGroup *group = new QActionGroup(bar); QAction *drawLineAction = new QAction("Line", bar); drawLineAction->setIcon(QIcon(":/line.png")); drawLineAction->setToolTip(tr("Draw a line.")); drawLineAction->setStatusTip(tr("Draw a line.")); drawLineAction->setCheckable(true); drawLineAction->setChecked(true); group->addAction(drawLineAction); bar->addAction(drawLineAction); QAction *drawRectAction = new QAction("Rectangle", bar); drawRectAction->setIcon(QIcon(":/rect.png")); drawRectAction->setToolTip(tr("Draw a rectangle.")); drawRectAction->setStatusTip(tr("Draw a rectangle.")); drawRectAction->setCheckable(true); group->addAction(drawRectAction); bar->addAction(drawRectAction); QLabel *statusMsg = new QLabel; statusBar()->addWidget(statusMsg); PaintWidget *paintWidget = new PaintWidget(this); QGraphicsView *view = new QGraphicsView(paintWidget, this); setCentralWidget(view); connect(drawLineAction, SIGNAL(triggered()), this, SLOT(drawLineActionTriggered())); connect(drawRectAction, SIGNAL(triggered()), this, SLOT(drawRectActionTriggered())); connect(this, SIGNAL(changeCurrentShape(Shape::Code)), paintWidget, SLOT(setCurrentShape(Shape::Code))); } void MainWindow::drawLineActionTriggered() { emit changeCurrentShape(Shape::Line); } void MainWindow::drawRectActionTriggered() { emit changeCurrentShape(Shape::Rect); } 由于 mainwindow.h的代码与前文相同,这里就不再贴出。而 cpp文件里面只有少数几行与前文不同。由于 我们使用 Graphics View,所以,我们必须把 item添加到 QGprahicsScene里面。这里,我们创建了 scene的对 象,而scene对象需要通过 view 进行观察,因此,我们需要再使用一个 QGraphcisView对象,并且把这个 view 添加到 MainWindow里面。 我们把 PaintWidget当做一个 scene,因此PaintWidget现在是继承 QGraphicsScene,而不是前面的 QWidget。 paintwidget.h #ifndef PAINTWIDGET_H #define PAINTWIDGET_H #include #include #include "shape.h" #include "line.h" #include "rect.h" class PaintWidget : public QGraphicsScene { Q_OBJECT public: PaintWidget(QWidget *parent = 0); public slots: void setCurrentShape(Shape::Code s) { if(s != currShapeCode) { currShapeCode = s; } } protected: void mousePressEvent(QGraphicsSceneMouseEvent *event); void mouseMoveEvent(QGraphicsSceneMouseEvent *event); void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); private: Shape::Code currShapeCode; Shape *currItem; bool perm; }; #endif //PAINTWIDGET_H paintwidget.cpp #include "paintwidget.h" PaintWidget::PaintWidget(QWidget *parent) : QGraphicsScene(parent), currShapeCode(Shape::Line), currItem(NULL), perm(false) { } void PaintWidget::mousePressEvent(QGraphicsSceneMouseEvent *event) { switch(currShapeCode) { case Shape::Line: { Line *line = new Line; currItem = line; addItem(line); break; } case Shape::Rect: { Rect *rect = new Rect; currItem = rect; addItem(rect); break; } } if(currItem) { currItem->startDraw(event); perm = false; } QGraphicsScene::mousePressEvent(event); } void PaintWidget::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if(currItem &&!perm) { currItem->drawing(event); } QGraphicsScene::mouseMoveEvent(event); } void PaintWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { perm = true; QGraphicsScene::mouseReleaseEvent(event); } 我们把继承自 QWidget改成继承自 QGraphicsScene,同样也会有鼠标事件,只不过在这里我们把鼠标事件全部 转发给具体的 item进行处理。这个我们会在下面的代码中看到。另外一点是,每一个鼠标处理函数都包含了调 用其父类函数的语句。 shape.h #ifndef SHAPE_H #define SHAPE_H #include class Shape { public: enum Code { Line, Rect }; Shape(); virtual void startDraw(QGraphicsSceneMouseEvent * event) = 0; virtual void drawing(QGraphicsSceneMouseEvent * event) = 0; }; #endif //SHAPE_H shape.cpp #include "shape.h" Shape::Shape() { } Shape类也有了变化:还记得我们曾经说过,Qt内置了很多 item,因此我们不必全部重写这个 item。所以, 我们要使用 Qt 提供的类,就不需要在我们的类里面添加新的数据成员了。这样,我们就有了不带有额外的数据 成员的 Shape。那么,为什么还要提供 Shape呢?因为我们在 scene的鼠标事件中需要修改这些数据成员,如果 没有这个父类,我们就需要按照 Code写一个长长的 switch来判断是那一个图形,这样是很麻烦的。所以我们依 然创建了一个公共的父类,只要调用这个父类的 draw函数即可。 line.h #ifndef LINE_H #define LINE_H #include #include "shape.h" class Line : public Shape, public QGraphicsLineItem { public: Line(); void startDraw(QGraphicsSceneMouseEvent * event); void drawing(QGraphicsSceneMouseEvent * event); }; #endif //LINE_H line.cpp #include "line.h" Line::Line() { } void Line::startDraw(QGraphicsSceneMouseEvent * event) { setLine(QLineF(event->scenePos(), event->scenePos())); } void Line::drawing(QGraphicsSceneMouseEvent * event) { QLineF newLine(line().p1(), event->scenePos()); setLine(newLine); } Line类已经和前面有了变化,我们不仅仅继承了 Shape,而且继承了 QGraphicsLineItem类。这里我们使用 了C++的多继承机制。这个机制是很危险的,很容易发生错误,但是这里我们的 Shape并没有继承其他的类,只 要函数没有重名,一般而言是没有问题的。如果不希望出现不推荐的多继承(不管怎么说,多继承虽然危险,但 它是符合面向对象理论的),那就就想办法使用组合机制。我们之所以使用多继承,目的是让 Line类同时具有 Shape和QGraphicsLineItem的性质,从而既可以直接添加到 QGraphicsScene中,又可以调用 startDraw()等函 数。 同样的还有 Rect 这个类: rect.h #ifndef RECT_H #define RECT_H #include #include "shape.h" class Rect : public Shape, public QGraphicsRectItem { public: Rect(); void startDraw(QGraphicsSceneMouseEvent * event); void drawing(QGraphicsSceneMouseEvent * event); }; #endif //RECT_H rect.cpp #include "rect.h" Rect::Rect() { } void Rect::startDraw(QGraphicsSceneMouseEvent * event) { setRect(QRectF(event->scenePos(), QSizeF(0, 0))); } void Rect::drawing(QGraphicsSceneMouseEvent * event) { QRectF r(rect().topLeft(), QSizeF(event->scenePos().x() - rect().topLeft().x(), event->scenePos().y() - rect().topLeft().y())); setRect(r); } Line和Rect 类的逻辑都比较清楚,和前面的基本类似。所不同的是,Qt并没有使用我们前面定义的两个 Qpoint对象记录数据,而是在 QGraphicsLineItem中使用 QLineF,在QGraphicsRectItem中使用 QRectF记录数 据。这显然比我们的两个点的数据记录高级得多。其实,我们也完全可以使用这样的数据结构去重定义前面那些 Line之类。 这样,我们的程序就修改完毕了。运行一下你会发现,几乎和前面的实现没有区别。这里说“几乎”,是在 第一个点画下的时候,scene会移动一段距离。这是因为 scene是自动居中的,由于我们把 Line 的第一个点设 置为(0, 0),因此当我们把鼠标移动后会有一个偏移。 看到这里或许并没有显示出 Graphics View的优势。不过,建议在 Line或者 Rect的构造函数里面加上下面 的语句, setFlag(QGraphicsItem::ItemIsMovable, true); setFlag(QGraphicsItem::ItemIsSelectable, true); 此时,你的 Line 和Rect就已经支持选中和拖放了!值得试一试哦!不过,需要注意的是,我们重写了 scene 的鼠标控制函数,所以这里的拖动会很粗糙,甚至说是不正确,你需要动动脑筋重新设计我们的类啦! Qt 学习之路(33): 国际化(上) 2D绘图部分基本告一段落,还在想下面的部分要写什么,本来计划先说下 view-model的相关问题,但是前面 看到有朋友问关于国际化的问题,所以现在先来说说 Qt 的国际化吧! Qt中的国际化的方法有很多,常用的有使用 QTextCodec 类和使用 tr()函数。前者将编码名称写到代码里面, 除非你使用 Unicode 编码,否则国际化依然是一个问题;后者就不会有这个问题,并且这也是 Qt推荐的做法。因 此,我们主要来说使用 tr()函数的方法进行应用程序的国际化。 我们先来看一个很简单的 MainWindow。为了清楚起见,这里只给出了 cpp文件的内容: #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QMenuBar *menuBar = new QMenuBar(this); QMenu *fileMenu = new QMenu(tr("&File"), menuBar); QAction *newFile = new QAction(tr("&New..."), fileMenu); fileMenu->addAction(newFile); QAction *openFile = new QAction(tr("&Open..."), fileMenu); fileMenu->addAction(openFile); menuBar->addMenu(fileMenu); setMenuBar(menuBar); connect(openFile, SIGNAL(triggered()), this, SLOT(fileOpen())); } MainWindow::~MainWindow() { } void MainWindow::fileOpen() { QFileDialog *fileDialog = new QFileDialog(this); fileDialog->setWindowTitle(tr("Open File")); fileDialog->setDirectory("."); if(fileDialog->exec() == QDialog::Accepted) { QString path = fileDialog->selectedFiles()[0]; QMessageBox::information(NULL, tr("Path"), tr("You selected\n%1").arg(path)); } else { QMessageBox::information(NULL, tr("Path"), tr("You didn't select any files.")); } } 这是一个很简单的类,运行结果想必大家也都非常清楚:就是一个主窗口,上面有一个菜单栏,一个 File 菜单,里面有两个菜单项: 之所以把运行图贴出来,是为了大家能够看清,在代码中的&符号实际在界面中显示成为一条下划线,标记出这 个菜单或者菜单项的快捷键。按照代码,当我们点击了 Open时,会弹出一个打开文件的对话框: 这里的 slot 里面的代码在前文中已经详细介绍过。也许你会问,为什么要用这种麻烦的写法呢 ?因为我们曾 经说过,使用 static 函数实际上是直接调用系统的对话框,而这种构造函数法是 Qt 自己绘制的。这对我们后面的 国际化是有一定的影响的。 好了,都已经准备好了,下面开始进行国际化。所谓国际化,实际上不仅仅是把界面中的各种文字翻译成另 外的语言,还有一些工作是要进行书写方式、货币等的转换。比如,阿拉伯书写时从右向左的,这些在国际化工 作中必须完成。但是在这里,我们只进行最简单的工作,就是把界面的文字翻译成中文。 首先,我们需要在 pro文件中增加一行: TRANSLATIONS += myapp.ts myapp.ts 是我们需要创建的翻译文件。这个文件的名字是任意的,不过后缀名需要是 ts。然后我们打开命令 提示符,进入到工程所在目录,比如我的是 E:\My Documents\Workspace\Qt\MyApp,也就是 pro文件所在的文件 夹,然后输入命令 lupdate MyApp.pro ,如果你出现的是命令不存在,请注意将 Qt的bin 目录添加到环境变量中。此时,如果更新的数目,说明 ts 文件创建成功: 最后一行是说,找到 7个需要翻译的原文字,0个已经存在。也就是说,这个文件是新建的。这时你会在工 程目录下找到这个 myapp.ts 文件。也许你会奇怪,为什么这里还会说已存在的数目呢?因为 Qt这个工具很智能的 能够识别出已经存在的文字和修改或新增的文字,这样在以后的工作中就不需要一遍遍重复翻译以前的文字了。 这也就是为什么这个工具的名字是“lupdate”的原因,因为它是“update”,而不仅仅是生成。 如果你有兴趣的话,可以用记事本打开这个 ts 文件,这个文件实际上是一个 XML 文件,结构很清晰。不过, 我们要使用专业的翻译工具进行翻译。Qt提供了一个工具,Qt Linguist,你可以在开始菜单的 Qt项下面的 Tools 中找到。用它可以打开我们的 ts 文件,然后进行我们的翻译工作: 完全翻译完成后保存文件,然后在文件菜单下有个“发布”。点击这个按钮,工程目录下会有一个 myapp.qm 文件,这就是我们翻译得到的文件。Qt的qm 文件实际上是二进制格式的,因此它经过了高度的优化,体积很小。 下面我们要修改 main()函数,使之加载这个 qm 文件: int main(int argc, char *argv[]) { QApplication a(argc, argv); QTranslator qtTranslator; qtTranslator.load("myapp.qm"); a.installTranslator(&qtTranslator); MainWindow w; w.resize(800, 600); w.show(); return a.exec(); } 注意,QTranslator类实际是在 QtCore 下面的。代码还是很清晰:创建一个 QTranslator 对象,然后加载 qm 文件,然后将这个对象安装到 QApplication类。好了,现在大功告成,重新编译后运行下程序吧! 咦?怎么还是英文的?哪里有错误了呢?这里往往令人疑惑,其实,这是由于我们使用 load()函数加载 qm 文件 时使用的是相对路径,这样直接 load(“myapp.qm”),其实会在当前编译后的 exe所在目录下寻找这个 qm 文件, 所以,只要我们把 qm 文件同 exe放在同一目录下,再次运行: 现在,这个界面已经是中文了吧!其实,这一小细节已经说明,qm 文件其实是动态加载到 exe 文件中的,而 不是直接编译进去的。这一点为我们进行动态切换语言提供了基础。 Qt 学习之路(33): 国际化(上) 2D绘图部分基本告一段落,还在想下面的部分要写什么,本来计划先说下 view-model 的相关问题,但是前面看 到有朋友问关于国际化的问题,所以现在先来说说 Qt的国际化吧! Qt中的国际化的方法有很多,常用的有使用 QTextCodec 类和使用 tr()函数。前者将编码名称写到代码里面, 除非你使用 Unicode 编码,否则国际化依然是一个问题;后者就不会有这个问题,并且这也是 Qt推荐的做法。因 此,我们主要来说使用 tr()函数的方法进行应用程序的国际化。 我们先来看一个很简单的 MainWindow。为了清楚起见,这里只给出了 cpp文件的内容: #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QMenuBar *menuBar = new QMenuBar(this); QMenu *fileMenu = new QMenu(tr("&File"), menuBar); QAction *newFile = new QAction(tr("&New..."), fileMenu); fileMenu->addAction(newFile); QAction *openFile = new QAction(tr("&Open..."), fileMenu); fileMenu->addAction(openFile); menuBar->addMenu(fileMenu); setMenuBar(menuBar); connect(openFile, SIGNAL(triggered()), this, SLOT(fileOpen())); } MainWindow::~MainWindow() { } void MainWindow::fileOpen() { QFileDialog *fileDialog = new QFileDialog(this); fileDialog->setWindowTitle(tr("Open File")); fileDialog->setDirectory("."); if(fileDialog->exec() == QDialog::Accepted) { QString path = fileDialog->selectedFiles()[0]; QMessageBox::information(NULL, tr("Path"), tr("You selected\n%1").arg(path)); } else { QMessageBox::information(NULL, tr("Path"), tr("You didn't select any files.")); } } 这是一个很简单的类,运行结果想必大家也都非常清楚:就是一个主窗口,上面有一个菜单栏,一个 File 菜单,里面有两个菜单项: 这里的 slot 里面的代码在前文中已经详细介绍过。也许你会问,为什么要用这种 麻烦的写法呢?因为我们曾经说过,使用 static函数实际上是直接调用系统的对话框,而这种构造函数法是 Qt 自己绘制的。这对我们后面的国际化是有一定的影响的。 好了,都已经准备好了,下面开始进行国际化。所谓国际化,实际上不仅仅是把界面中的各种文字翻译成另 外的语言,还有一些工作是要进行书写方式、货币等的转换。比如,阿拉伯书写时从右向左的,这些在国际化工 作中必须完成。但是在这里,我们只进行最简单的工作,就是把界面的文字翻译成中文。 首先,我们需要在 pro文件中增加一行: TRANSLATIONS += myapp.ts myapp.ts是我们需要创建的翻译文件。这个文件的名字是任意的,不过后缀名需要是 ts。然后我们打开命 令提示符,进入到工程所在目录,比如我的是 E:\My Documents\Workspace\Qt\MyApp,也就是 pro文件所在的 文件夹,然后输入命令 lupdate MyApp.pro ,如果你出现的是命令不存在,请注意将 Qt 的bin目录添加到环境变量中。 C++教程:Qt学习之路(34): 国际化(下) 上次说了国际化的过程,现在来看一下具体的国际化的相关代码。 在代码中,我们使用 tr()将需要翻译的字符串标记出来。lupdate工具就是提取出 tr()函数中的相关字符 串。tr()函数是 QObject 类的一个 static函数,其签名如下: static QString tr(const char *sourceText, const char *comment = 0, int n = -1); 虽然我们只传了一个参数,但是实际上 tr()函数是接受 3 个参数的。第一个参数是我们需要翻译的文字, 如果使用 qm文件有对应的字符串,则使用对应的字符串进行替换,否则将显示 sourceText参数指定的字符串。 第二个参数是一个注释,用于解释前面的 sourceText的含义,比如 table一词既可以当做桌子翻译,又可以当 成表格翻译,这时你就需要提供这个注释。或许你会问,使用翻译工具的时候不是有源代码吗?问题是,有可能 人家不使用这个翻译工具,而使用别的工具,这样就不能保证会有这个源代码的预览;并且,你的程序不一定必 须要发布源代码的;翻译人员往往只得到我们导出的 ts文件,如果你加上注释,就可以方便翻译人员进行翻译。 最后一个参数 n用于指定字符串是否为复数。我们知道,很多语言,比如英语,很多名词的单复数形式是不相同 的,为了解决这个问题,Qt在tr()函数中提供了一个参数 n。请看如下代码: int n = messages.count(); showMessage(tr("%n message(s) saved", "", n)); 对于 n 的值的不同,Qt会翻译成不同的文字,例如: n 翻译结果 0 0 message saved 1 1 message saved 2 2 messages saved 5 5 messages saved tr()函数是 QObject 的函数,如果你的类不是继承自 QObject,就不能直接使用 tr()函数。比如我们在 main() 函数中希望增加一句设置 MainWindow 的title的代码: w.setWindowTitle(tr("MyApp")); 直接这样写是无法通过编译的,因为 main()函数是全局函数,所以这个 tr()是找不到的。解决办法一是显 式地调用 QObject 的函数: w.setWindowTitle(QObject::tr("MyApp")); 或者,你可以使用 QCoreApplication 的translate()函数。你一定还记得,我们的 main()函数的第一句总 是QApplication app;,其实,QApplication就是 QCoreApplication 的子类。所以,我们也能这样去写: w.setWindowTitle(app.translate("MyApp")); 由于在 Qt 程序中,QCoreApplication是一个单例类,因此,Qt提供了一个宏 qApp,用于很方便的访问 QCoreApplication的这个单例。所以,在其他文件中,我们也可以直接调用 qApp.translate()来替换 tr(),不 过这并没有必要。 如果你的翻译文本中包含了需要动态显示的数据,比如我们上次代码中的 QMessageBox::information(NULL, tr("Path"), tr("You selected\n%1").arg(path)); 这句你当然可以写成 QMessageBox::information(NULL, tr("Path"), "You selected\n" + path); 但这种连接字符串的方式就不能够使用 tr()函数了!因此,如果你需要像 C语言的 printf()函数这种能够格 式化输出并且需要翻译时,你必须使用我们例子中的%1加arg()函数! 如果你想要翻译函数外部的字符串,你需要使用两个宏 QT_TR_NOOP()和QT_TRANSLATE_NOOP()。前者是用来 翻译一个字符串,后者可以翻译多个字符串。它们的使用方法如下: QString FriendlyConversation::greeting(int type) { static const char *greeting_strings[] = { QT_TR_NOOP("Hello"), QT_TR_NOOP("Goodbye") }; return tr(greeting_strings[type]); } static const char *greeting_strings[] = { QT_TRANSLATE_NOOP("FriendlyConversation", "Hello"), QT_TRANSLATE_NOOP("FriendlyConversation", "Goodbye") }; QString FriendlyConversation::greeting(int type) { return tr(greeting_strings[type]); } QString global_greeting(int type) { return qApp->translate("FriendlyConversation", greeting_strings[type]); } 好了,以上就是我们用到的大部分函数和宏。除此之外,如果我们运行前面的例子就会发现,实际上我们只 是翻译了菜单等内容,打开文件对话框并没有被翻译。原因是我们没有给出国际化的信息。那么,怎么才能让 Qt翻译这些内建的文字呢?我们要在 main()函数中添加几句: i nt main(int argc, char *argv[]) { QApplication a(argc, argv); QTranslator qtTranslator; qtTranslator.load("myapp.qm"); a.installTranslator(&qtTranslator); QTranslator qtTranslator2; qtTranslator2.load("qt_zh_CN.qm"); a.installTranslator(&qtTranslator2); MainWindow w; w.resize(800, 600); w.show(); return a.exec(); } 我们又增加了一个 QTranslator对象。Qt实际上是提供了内置字符串的翻译 qm文件的。我们需要在 Qt安 装目录下的 translations文件夹下找到 qt_zh_CN.qm,然后同前面一样,将它复制到 exe所在目录。现在再运 行一下程序:哈哈已经完全变成中文了吧! 至此,我们的 Qt 程序的国际化翻译部分就结束啦! Qt学习之路(35): Qt容器类之顺序存储容器 本来计划先来说下 model/view 的,结果发现 model/view 涉及到一些关于容器的内容,于是就把容器部分提前了。 容器 Containers,有时候也被称为集合 collections,指的是能够在内存中存储其他特定类型的对象的对象, 这种对象一般是通用的模板类。C++提供了一套完整的解决方案,成为标准模板库 Standard Template Library, 也就是我们常说的 STL。 Qt提供了它自己的一套容器类,这就是说,在 Qt的应用程序中,我们可以使用标准 C++的STL,也可以使 用Qt的容器类。Qt容器类的好处在于,它提供了平台无关的行为,以及隐式数据共享技术。所谓平台无关,即 Qt容器类不因编译器的不同而具有不同的实现;所谓“隐式数据共享”,也可以称作“写时复制 copy on write”,这种技术允许在容器类中使用传值参数,而不会发生额外的性能损失。Qt容器类提供了类似 Java的 遍历器语法,同样也提供了类似 STL的遍历器语法,以方便用户选择自己习惯的编码方式。最后一点,在一些嵌 入式平台,STL 往往是不可用的,这时你就只能使用 Qt提供的容器类,除非你想自己创建。 今天我们要说的是“顺序储存容器”。所谓顺序存储,就是它存储数据的方式是一个接一个的,线性的。 第一个顺序存储容器是 QVector,即向量。QVector 是一个类似数组的容器,它将数据存储在连续内存区域。 同C++数组不同之处在于,QVector 知道它自己的长度,并且可以改变大小。对于获取随机位置的数据,或者是 在末尾处添加数据,QVector 的效率都是很高的,但是,在中间位置插入数据或者删除数据,它的效率并不是很 高。在内存中 QVector的存储类似下图(出自 C++ GUI Programming with Qt4, 2nd Edition): 同STL的vector类类似,QVector也提供了[]的重载,我们可以使用[]赋值: QVector v(2); v[0] = 1.1; v[1] = 1.2; 如果实现不知道 vector的长度,可以创建一个空参数的 vector,然后使用 append()函数添加数据: QVector v; v.append(1.1); v.append(1.2); 在QVector类中,<<也被重载,因此,我们也可以直接使用<<操作符: QVector v; v << 1.1 << 1.2; 注意,如果 QVector 中的数据没有被显式地赋值,那么,数据项将使用加入类的默认构造函数进行初始化, 如果是基本数据类型和指针,则初始化为 0. QLinekdList是另外一种顺序存储容器。在数据结构中,这是一个链表,使用指针连接起所有数据。它的内 存分布如下(出自 C++ GUI Programming with Qt4, 2nd Edition): 正如数据结构中所描述的那样,QLinkedList的优点是数据的插入和删除很快,但是随机位置值的访问会很 慢。与QVector 不同,QLinkedList并没有提供重载的[]操作符,你只能使用 append()函数,或者<<操作符进行 数据的添加,或者你也可以使用遍历器,这个我们将在后面内容中详细描述。 QList是一个同时拥有 QVector和QLinkedList的大多数有点的顺序存储容器类。它像 QVector一样支持快 速的随机访问,重载了[]操作符,提供了索引访问的方式;它像 QLinkedList一样,支持快速的添加、删除操作。 除非我们需要进行在很大的集合的中间位置的添加、删除操作,或者是需要所有元素在内存中必须连续存储,否 则我们应该一直使用 Qlist。 QList有几个特殊的情况。一个是 QStringList,这是 QList的子类,提供针对 QString 的很多特殊操作。 QStack和QQueue分别实现了数据结构中的堆栈和队列,前者具有 push(), pop(), top()函数,后者具有 enqueue(), dequeue(), head()函数。具体情况请查阅 API文档。 另外需要指出的一点是,我们所说的模板类中的占位符 T,可以使基本数据类型,比如 int,double等,也可以 指针类型,可以是类类型。如果是类类型的话,必须提供默认构造函数,拷贝构造函数和赋值操作符。Qt的内 置类中的 QByteArray,QDateTime,QRegExp,QString 和QVariant是满足这些条件的。但是,QObject 的子类 并不符合这些条件,因为它们通常缺少拷贝构造函数和赋值操作符。不过这并不是一个问题,因为我们可以存储 QObject的指针,而不是直接存储值。T也可以是一个容器,例如: QList > list; 注意,在最后两个>之间有一个空格,这是为了防止编译器把它解析成>>操作符。这个空格是必不可少的, 切记切记! 下面我们来看一个类(出自 C++ GUI Programming with Qt4, 2nd Edition): class Movie { public: Movie(const QString &title = "", int duration = 0); void setTitle(const QString &title) { myTitle = title; } QString title() const { return myTitle; } void setDuration(int duration) { myDuration = duration; } QString duration() const { return myDuration; } private: QString myTitle; int myDuration; }; 我们能不能把这个类放进 Qt容器类呢?答案是肯定的。下面我们来对照着前面所说的要求:第一,虽然这个 类的构造函数有两个参数,但是这两个参数都有默认值,因此,像Movie()这种写法是允许的,所以,它有默认 构造函数;第二,这个类表面上看上去没有拷贝构造函数和赋值操作符,但是 C++编译器会为我们提供一个默认 的实现,因此这个条件也是满足的。对于这个类而言,默认拷贝构造函数已经足够,无需我们自己定义。所以, 我们可以放心的把这个类放进 Qt的容器类。 Qt学习之路 36: Qt容器类之遍历器和隐式数据共享 前面说过,Qt容器类提供了两种遍历器:Java风格的和 STL风格的。前者比较容易使用,后者则可以用在一些 通过算法中,功能比较强大。 对于每一个容器类,都有与之相对应的遍历器:只读遍历器和读写遍历器。只读遍历器有 QVectorIterator, QLinkedListIterator和QListIterator三种;读写遍历器同样也有三种,只不过名字中具有一个 Mutable,即 QMutableVectorIterator,QMutableLinkedListIterator和QMutableListIterator。这里我们只讨论 QList的 遍历器,其余遍历器具有几乎相同的 API。 Java风格的遍历器的位置如下图所示(出自 C++ GUI Programming with Qt4, 2nd Edition): 可以看出,Java 风格的遍历器,遍历器不指向任何元素,而是指向第一个元素之前、两个元素之间或者是 最后一个元素之后的位置。使用 Java 风格的遍历器进行遍历的典型代码是: QList list; //... QListIterator i(list); while (i.hasNext()) { doSomethingWith(i.next()); } 这个遍历器默认指向第一个元素,使用 hasNext()和next()函数从前向后遍历。你也可以使用 toBack()函 数让遍历器指向最后一个元素的后面的位置,然后使用 hasPrevious()和previous()函数进行遍历。 这是只读遍历器,而读写遍历器则可以在遍历的时候进行增删改的操作,例如: QMutableListIterator i(list); while (i.hasNext()) { if (i.next() < 0.0) i.remove(); } 当然,读写遍历器也是可以从后向前遍历的,具体 API和前面的几乎相同,这里就不再赘述。 对应于 Java 风格的遍历器,每一个顺序容器类 C都有两个 STL风格的遍历器:C::iterator和 C::const_iterator。正如名字所暗示的那样,const_iterator不允许我们对遍历的数据进行修改。begin()函 数返回指向第一个元素的 STL风格的遍历器,例如 list[0],而 end()函数则会返回指向最后一个之后的元素的 STL风格的遍历器,例如如果一个 list 长度为 5,则这个遍历器指向 list[5]。下图所示 STL风格遍历器的合法 位置: 如果容器是空的,begin()和end()是相同的。这也是用于检测容器是否为空的方法之一,不过调用 isEmpty() 函数会更加方便。 STL 风格遍历器的语法类似于使用指针对数组的操作。我们可以使用++和--运算符使遍历器移动到下一位 置,遍历器的返回值是指向这个元素的指针。例如 QVector 的iterator返回值是 T* 类型,而const_iterator 返回值是 const T* 类型。 一个典型的使用 STL风格遍历器的代码是: QList::iterator i = list.begin(); while (i != list.end()) { *i = qAbs(*i); ++i; } 对于某些返回容器的函数而言,如果需要使用 STL风格的遍历器,我们需要建立一个返回值的拷贝,然后再 使用遍历器进行遍历。如下面的代码所示: QList list = splitter->sizes(); QList::const_iterator i = list.begin(); while (i != list.end()) { doSomething(*i); ++i; } 而如果你直接使用返回值,就像下面的代码: //WRONG QList::const_iterator i = splitter->sizes().begin(); while (i != splitter->sizes().end()) { doSomething(*i); ++i; } 这种写法一般不是你所期望的。因为 sizes()函数会返回一个临时对象,当函数返回时,这个临时对象就要 被销毁,因此调用临时对象的 begin()函数是相当不明智的做法。并且这种写法也会有性能问题,因为 Qt每次 循环都要重建临时对象。因此请注意,如果要使用 STL风格的遍历器,并且要遍历作为返回值的容器,就要先创 建返回值的拷贝,然后进行遍历。 在使用 Java风格的只读遍历器时,我们不需要这么做,因此系统会自动为我们创建这个拷贝,所以,我们只需 很简单的按下面的代码书写: QListIterator i(splitter->sizes()); while (i.hasNext()) { doSomething(i.next()); } 这里我们提出要建立容器的拷贝,似乎是一项很昂贵的操作。其实并不然。还记得我们上节说过一个隐式数 据共享吗?Qt就是使用这个技术,让拷贝一个 Qt容器类和拷贝一个指针那么快速。如果我们只进行读操作,数 据是不会被复制的,只有当这些需要复制的数据需要进行写操作,这些数据才会被真正的复制,而这一切都是自 动进行的,也正因为这个原因,隐式数据共享有时也被称为“写时复制”。隐式数据共享不需要我们做任何额外 的操作,它是自动进行的。隐式数据共享让我们有一种可以很方便的进行值返回的编程风格: QVector sineTable() { QVector vect(360); for (int i = 0; i < 360; ++i) vect[i] = std::sin(i /(2 *M_PI)); return vect; } // call QVector v = sineTable(); Java中我们经常这么写,这样子也很自然:在函数中创建一个对象,操作完毕后将其返回。但是在 C++中, 很多人都会说,要避免这么写,因为最后一个 return语句会进行临时对象的拷贝工作。如果这个对象很大,这 个操作会很昂贵。所以,资深的 C++高手们都会有一个 STL风格的写法: void sineTable(std::vector &vect) { vect.resize(360); for (int i = 0; i < 360; ++i) vect[i] = std::sin(i /(2 *M_PI)); } // call QVector v; sineTable(v); 这种写法通过传入一个引用避免了拷贝工作。但是这种写法就不那么自然了。而隐式数据共享的使用让我们 能够放心的按照第一种写法书写,而不必担心性能问题。 Qt所有容器类以及其他一些类都使用了隐式数据共享技术,这些类包括QByteArray, QBrush, QFont, QImage, QPixmap和QString。这使得这些类在参数和返回值中使用传值方式相当高效。 不过,为了正确使用隐式数据共享,我们需要建立一个良好的编程习惯。这其中之一就是,对 list或者 vector 使用 at()函数而不是[]操作符进行只读访问。原因是[]操作符既可以是左值又可以是右值,这让 Qt容器很难判 断到底是左值还是右值,而at()函数是不能作为左值的,因此可以进行隐式数据共享。另外一点是,对于 begin(), end()以及其他一些非 const 容器,在数据改变时 Qt 会进行深复制。为了避免这一点,要尽可能使用 const_iterator, constBegin()和constEnd(). 最后,Qt提供了一种不使用遍历器进行遍历的方法:foreach循环。这实际上是一个宏,使用代码如下所示: QLinkedList list; Movie movie; ... foreach (movie, list) { if (movie.title() == "Citizen Kane") { std::cout << "Found Citizen Kane" << std::endl; break; } } 很多语言,特别是动态语言,以及 Java 1.5之后,都有 foreach 的支持。Qt中使用宏实现了 foreach循环, 有两个参数,第一个是单个的对象,成为遍历对象,相当于指向容器元素类型的一个指针,第二个是一个容器类。 它的意思很明确:每次取出容器中的一个元素,赋值给前面的遍历元素进行操作。需要注意的是,在循环外面定 义遍历元素,对于定义中具有逗号的类而言,如 QPair,是唯一的选择。 Qt学习之路(37): Qt容器类之关联存储容器 今天我们来说说 Qt容器类中的关联存储容器。所谓关联存储容器,就是容器中存储的一般是二元组,而不是单 个的对象。二元组一般表述为,也就是“键-值对”。 首先,我们看看数组的概念。数组可以看成是一种形式的键-值对,它的 Key 只能是 int,而值的类型是 Object, 也就是任意类型(注意,这里我们只是说数组可以是任意类型,这个 Object并不必须是一个对象)。现在我们扩 展数组的概念,把 Key也做成任意类型的,而不仅仅是 int,这样就是一个关联容器了。如果学过数据结构,典 型的关联容器就是散列(Hash Map,哈希表)。Qt提供两种关联容器类型:QMap 和QHash。 QMap是一种键-值对的数据结构,它实际上使用跳表 skip-list实现,按照 K进行升序的方式进行存储。使 用QMap 的insert()函数可以向 QMap中插入数据,典型的代码如下: QMap map; map.insert("eins", 1); map.insert("sieben", 7); map.insert("dreiundzwanzig", 23); 同样,QMap也重载了[]运算符,你可以按照数组的复制方式进行使用: map["eins"] = 1; map["sieben"] = 7; map["dreiundzwanzig"] = 23; []操作符同样也可以像数组一样取值。但是请注意,如果在一个非 const 的map中,使用[]操作符取一个不 存在的 Key的值,则这个 Key会被自动创建,并将其关联的 value赋予一个空值。如果要避免这种情况,请使用 QMap的value()函数: int val = map.value("dreiundzwanzig"); 如果 key不存在,基本类型和指针会返回 0,对象类型则会调用默认构造函数,返回一个对象,与[]操作符 不同的是,value()函数不会创建一个新的键-值对。如果你希望让不存在的键返回一个默认值,可以传给 value() 函数第二个参数: int seconds = map.value("delay", 30); 这行代码等价于: int seconds = 30; if (map.contains("delay")) seconds = map.value("delay"); QMap中的 K和T 可以是基本数据类型,如int,double,可以是指针,或者是拥有默认构造函数、拷贝构造 函数和赋值运算符的类。并且 K 必须要重载<运算符,因为 QMap需要按 K 升序进行排序。 QMap提供了 keys()和values()函数,可以获得键的集合和值的集合。这两个集合都是使用 QList作为返回 值的。 Map 是单值类型的,也就是说,如果一个新的值分配给一个已存在的键,则旧值会被覆盖。如果你需要让一 个key可以索引多个值,可以使用 QMultiMap。这个类允许一个 key索引多个 value,如: QMultiMap multiMap; multiMap.insert(1, "one"); multiMap.insert(1, "eins"); multiMap.insert(1, "uno"); QList vals = multiMap.values(1); QHash是使用散列存储的键-值对。它的接口同 QMap几乎一样,但是它们两个的实现需求不同。QHash的查 找速度比 QMap 快很多,并且它的存储是不排序的。对于 QHash 而言,K的类型必须重载了==操作符,并且必须 被全局函数 qHash()所支持,这个函数用于返回 key 的散列值。Qt 已经为 int、指针、QChar、QString 和 QByteArray 实现了 qHash()函数。 QHash会自动地为散列分配一个初始大小,并且在插入数据或者删除数据的时候改变散列的大小。我们可以 使用 reserve()函数扩大散列,使用 squeeze()函数将散列缩小到最小大小(这个最小大小实际上是能够存储这些 数据的最小空间)。在使用时,我们可以使用 reserve()函数将数据项扩大到我们所期望的最大值,然后插入数 据,完成之后使用 squeeze()函数收缩空间。 QHash同样也是单值类型的,但是你可以使用 insertMulti()函数,或者是使用 QMultiHash类来为一个键插 入多个值。另外,除了 QHash,Qt也提供了 QCache来提供缓存,QSet用于仅存储 key的情况。这两个类同 QHash 一样具有 K 的类型限制。 遍历关联存储容器的最简单的办法是使用 Java 风格的遍历器。因为 Java风格的遍历器的 next()和 previous()函数可以返回一个键-值对,而不仅仅是值,例如: QMap map; ... int sum = 0; QMapIterator i(map); while (i.hasNext()) sum += i.next().value(); 如果我们并不需要访问键-值对,可以直接忽略 next()和 previous()函数的返回值,而是调用 key()和 value() 函数即可,如: QMapIterator i(map); while (i.hasNext()) { i.next(); if (i.value() > largestValue) { largestKey = i.key(); largestValue = i.value(); } } Mutable遍历器则可以修改 key 对应的值: QMutableMapIterator i(map); while (i.hasNext()) { i.next(); if (i.value() < 0.0) i.setValue(-i.value()); } 如果是 STL风格的遍历器,则可以使用它的 key()和value()函数。而对于 foreach循环,我们就需要分别 对key和value进行循环了: QMultiMap map; ... foreach (QString key, map.keys()) { foreach (int value, map.values(key)) { doSomething(key, value); } }
还剩92页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 20 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

clancytom

贡献于2011-08-31

下载需要 20 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf