QT Module中的Qt Core和Qt GUI是唯二有必要仔细学习的部分, 这两部分涉及了QT的所有核心功能, 其余部分都可以在此基础上现学现卖.
本文主要介绍QT中比较重要的机制,其他知识相对来说都很独立,可以直接现学现用.
QT开发基础
- Qt的很多特性都使用到了
thread_local
变量, 在多线程环境中,与QObject
相关的功能只有两种保险的策略.-
- 不直接跨线程使用
QObject
对象
- 不直接跨线程使用
-
- 将
QObject
关联的对象树移动到某个线程再使用.(注意,QT要求,单个对象树都中的所有对象必须位于同一个线程中)
- 将
-
- 所有与GUI直接相关的对象都必须放在主线程中,只有主线程会处理UI相关的事务.
- Qt 使用 moc预处理机制,在C++预处理器前对头文件进行处理,自动生成一部分代码,为标准 C++ 增加了很多特性
- 与之类似的还有uic,它负责将
.ui
文件转化为对应的UI类头文件ui_xx.h
.
- 与之类似的还有uic,它负责将
- 只有继承了
QObject
的类,才可以具有QT的额外特性QObject
的派生类都必须在private:
部分定义Q_OBJECT
宏以通知moc
系统进行处理,没有Q_OBJECT
宏的类将无法被moc自动处理.- 与QT相关的特性必须放在
.h
头文件内,因为moc
默认只自动处理.h
型文件的内容
QApplication
对象是必须的,总应该放在main()
的第一行,在QApplication
创建之前,不应当有任何的QObject
被创建.- 因此,不应当使用全局的
QObjcet
QApplication
构造时会创建主线程的QT环境,它还提供了exec以启动主线程的事件循环.- GUI 程序是
QApplication
,非 GUI 程序是QCoreApplication
,QApplication
实际上是QCoreApplication
的子类.
- 因此,不应当使用全局的
QApplication::setMainWidget(&mainwindow)
可用于设置主部件,当主部件关闭后,将开始执行quit()
;show()
的时候会递归的触发所有子成员的show()
,子成员默认显示在父成员的左上角show()
的调用是非阻塞式的- 如果
show()
时没有窗口,会创建一个新的非模态窗口.
exec()
总是会启动一个新的事件循环.- 对于有
show()
成员的类型,exec()
和open()
会先创建一个新模态窗口,再触发show()
,最后启动新的事件循环exec()
是顶层模态窗口(对Windows而言,会出现在任务栏中),open()
显示的是一个非顶层模态窗口- 模态有两个意义: 一方面它会阻止系统的其他部分触发UI事件(其他UI组件不会成为焦点),另一方面模态窗口在show()之后开启了新的事件循环,这将增加事件循环的嵌套等级.
- 在构造
QObject
时总应当指定parent
,使对象在某个对象树中. - 对于堆中的对象,QT的自动析构机制有:
- 在窗口不可见时析构.
- 在对象树销毁时析构.
- 通过
deleteLater
析构:可以延迟销毁的时间,具体可以参考reference
- QT自带有完整的国际化及跨平台机制,快键键,图像,文字都能很好的支持.
- 用户可见的字符串都应该用
tr
包起来,以便于进行国际化.最好是在代码中用英文,然后通过国际化系统添加中文支持.
- 用户可见的字符串都应该用
- 一个可执行程序可以链入多个
.qrc
,它们将会在运行期挂载到一个虚拟的内存磁盘系统中,这个磁盘系统的根节点是:\
- QT中
QString
/QChar
底层存储的是UTF-16编码字符串.- tr(“xxx”)最终也是生成
Qchar
型数据. QString
输出到外部时,最好通过tolocal8bit().data()
.
- tr(“xxx”)最终也是生成
Qt Designer及其UI系统
Qt Designer
生成的UI只支持Qt
标准组件,其工作原理为: uic将.ui
文件转换为一个类,这个类有setUI(QWidget * target)
函数,使用这个函数,我们可以按照.ui
的描述为target
动态添加若干children
.
ui
文件及其uic
生成UI的特性.
- UI文件名
file.ui
将决定头文件的文件名ui_file.h
. - UI文件中,根对象
Wxx
将被生成为Ui_Wxx
类Ui_Wxx
类名以别名Wxx
导入Ui
命名空间,我们通常用Ui::Wxx
来使用这个类,而非Ui_Wxx
Ui
命名空间内只有UI类名,没有其他元素,这个命名空间便于QT组织及管理Ui类.Ui::Wxx
是纯C++类,没有Q_OBJECT
宏标记,不会进行moc,可以作为继承的基类,也可以作为private成员使用.- 一般建议使用
Composition
风格编程,持有一个private
的Ui::Wxx ui
的成员. - 由于
Ui::Wxx
所在的头文件必须在uic
后才存在,所以在源代码中引用时常常要进行前向声明 - UI元素
ObjectName
的同名变量是该类的public
的指针型成员.
- Note: 虽然实践中不建议使用继承,但是在使用继承的策略进行开发时,可以直接override名为
on_objectName_signalName()
的虚函数,来响应Wxx::objectName
的singalName
,不必手动connect
.ui
类生成的代码中,把所有信号都已经连接到空的virtual
函数上了,我们可以直接override
QT的事件机制
信号槽
- 信号和槽形式上和一般的成员函数没有区别. 在QT5中,槽的概念已经淡化,任何可调用对象都可以作为槽,而不必限定在slot代码块中.
- 信号声明与函数类似,返回
void
,不需要函数体(函数体由moc提供) - 信号和槽的声明也受
public:
和private:
的权限限制
- 信号声明与函数类似,返回
- 信号-槽的触发相当于对信号实参进行一次forward,但是要比函数调用宽松的一些
- 例如,信号的形参可以比槽的多,多出的参数不会被forward,只是直接忽略.
- 当connect语法比较复杂时,就应该考虑使用lambda,用lambda创建一个简单的可调用对象作为slot通常会更清晰一些.
- 信号自身也可以作为”槽”,此时相当于emit一个新信号.
- 信号槽机制保证:如果信号有槽接收,那么sender和reciver都不会在响应完成前被析构.
- 成员函数可以通过
this->sender()
获得信号来源对象(仅当该成员函数作为槽被触发时,sender()
返回的才是有意义的值.) - 信号-槽的部分功能是基于事件系统实现的.例如,不同线程的两个对象之间触发信号-槽时,就需要使用事件系统.
- 如果
connect
的时候,sender
和reciver
是绑定到同一个线程的,那么这样绑定的slot就会在emit sigal时直接原地调用.(同步) - 如果
connect
的时候,sender
和reciver
不是绑定到同一个线程的,那么默认就会在emit sigal时向reciver
post 一个event来触发slot的调用.(异步)
- 如果
- 有很多UI事件的默认handler就是emit某个信号(如鼠标点击就是在响应抬起事件时
emit clicked()
),所以信号-槽系统和事件系统在功能上是有一定重叠的- 一般而言,总是应该优先使用信号槽,只有信号槽系统无法满足需求时,才考虑使用事件系统.例如,对于鼠标点击,写一个槽来连接到
clicked()
要比处理鼠标事件更好.
- 一般而言,总是应该优先使用信号槽,只有信号槽系统无法满足需求时,才考虑使用事件系统.例如,对于鼠标点击,写一个槽来连接到
事件系统
- 每个
QObject
都有一个与自身绑定的thread_local
型qthread
,这个qthread
不只代表线程,还会持有一些线程私有的管理数据. 在QObject
构造时,会自动绑定到当前所在的线程.- 例如,每个qthread都有一个事件队列,负责将收到的各种事件queue起来.(也就是说,每个线程都有自己私有的事件队列)
- QT的事件循环可以嵌套,例如,开启一个模态窗口后,再开启一个模态窗口.
- 嵌套的事件循环可以带来一定的灵活性,例如,内层循环的某些事件可以delay到外层循环处理.比如,使用
deleteLater
就可以在内层事件循环结束后才开始销毁某些对象.
- 嵌套的事件循环可以带来一定的灵活性,例如,内层循环的某些事件可以delay到外层循环处理.比如,使用
Event
的handler
一定是在reciver
绑定的qthread
中执行的- 主线程的
qthread
只能由QApplication
创建.也就是说,在QApplication
构造之前,不应该创建QObject
,否则这些对象的事件系统将不能工作. - 只有
QThread
创建的新线程才会带有qthread
,其他线程库创建的线程都不会创建qthread
,自然也就无法支持和qt事件相关的特性.- 尽管通过
QThread
创建的线程有事件队列,但是我们仍然要确保线程中有事件循环来处理事件.QThread
的虚函数run
默认就会调用exec
开启事件循环. - 注意,
Qthread
只保证run()
一定是在新线程中执行的,Qthread
自身及其成员都是在原来的线程中创建的.
- 尽管通过
- 对于用户自定义的事件,在发送时就可以指定
receiver
. 对于GUI
事件,默认的reciver一般就是界面的焦点widget.- 因为可见元素对应的对象一定是在主线程,所以,UI事件都是在主线程的事件循环处理的.
- QT的事件系统可供定制的部分是从
QApplication::notify(QObject * receiver, QEvent * event)
开始的. 在此之前,事件会暂存到一个全局的事件队列中,notify()
会把event
发送到receiver
所在的线程中,从逻辑上看,各个线程的事件循环将会阻塞式的处理事件,逻辑大致如下
// 注意,仅展示逻辑
void thread_event_process(QObject * receiver, QEvent * event){
if(qApp->all_filter->eventFilter(receiver,event)){
return; //全局过滤
}
if(receiver->all_filter->eventFilter(receiver,event)){
return; // reciver过滤
}
QObject * p = receiver;
bool ret=p->event(event); //注意 event 默认处于isAccpeted()为true的状态
while(!(ret && event.isAccepted())){
p=p->parent(); // 沿着对象树向上流动
ret=p->event(event);
}
}
notify()
和event()
都是virtual
的,可供用户override
event()
函数是对象中最顶层handler
,它默认只简单的调用具体的响应函数,其默认实现是: 根据switch(event.type())
执行某个case
内的xxxEvent()
虚函数,switch
的default
分支将执行customEvent(event)
函数.- 从设计上说,
event()
可以支持响应所有QT
自带的事件.所有未识别的事件肯定都是用户自定义的,用customEvent()
处理即可.
- 从设计上说,
- 综上,当我们需要使用事件系统时,一般有如下套路:
- 通过 override
xxxEvent
虚函数,可以拦截默认的handler,这一般用于修改默认的事件响应行为. 注意, 在override时, 最好调用一下基类的xxxEvent
,以避免丢失掉一些默认的行为. - 通过 override
event()
,我们可以更早就拦截到event
,当我们希望对象只响应某几个Event
时就需要这么做. - 通过安装
eventFilter
来提前过滤某些事件. qApp
的过滤器对于所有事件都会应用,receiver
也可以有自己的过滤器.- 最好如字面意思,仅仅在
eventFilter
里做过滤操作,也就是丢弃不关心的事件.(实践中事件过滤器的用处其实很多) eventfilter
是按安装顺序调用的,并不是代码里这么简单. 对于继承体系,基类在构造时安装的filter一定比派生类早,所以先于派生类进行过滤.- 通过override
notify
来拦截所有系统中发生的事件. - 在上面的这些方法中,越靠近上层,则拦截的时机越早,我们可以按自己的需求进行拦截, 但是一定要考虑好性能和代码风格的问题.
- 性能上: 拦截的越早,我们添加的代码被调用的频率可能就越高.
- 风格上: 尽管拦截到事件时,我们想做什么都可以, 但是最好不要在拦截时做多余的操作, 能下放的都下放处理.
- 注意: 如果确实需要override
event()
或notify()
,一定要考虑是否需要调用一下默认的实现,如果不手动调用默认的实现,那么默认的行为就都会消失.
- 通过 override
- 如果我们希望事件能继续在对象树中向上流动,那么需要手动的
event->ignore()
一下.
QT 绘制系统简介
- 可以使用传统实时绘图风格来绘制,即QPainter+QPainterDevice, 也可以使用类似于OpenGL的场景描述式渲染.
- QPainter: 相当于绘图 Context, 所有的绘图API也是由它提供.
- 它是在world coordinate下绘图.
- 可以通过
painter.setWindow
来设置裁剪范围,超出这个范围的点都会被裁剪掉. - 可以通过
painter.setViewPort
在当前widget
中划出一个小窗用于显示,裁剪后剩余的部分将渲染到这个小窗内.
QPainterDevice
: 对应了帧缓冲的抽象,也就是说,它逻辑上是一个有颜色缓冲的像素图,每个像素都有自己的像素坐标.QPainter
构造时就要指定其PainterDevice
.- 我们可以直接操作
QPanterDevice
的颜色缓冲,修改结果会直接显示出来.