QT5编程基础

QT Module中的Qt Core和Qt GUI是唯二有必要仔细学习的部分, 这两部分涉及了QT的所有核心功能, 其余部分都可以在此基础上现学现卖.

本文主要介绍QT中比较重要的机制,其他知识相对来说都很独立,可以直接现学现用.

QT开发基础

  • Qt的很多特性都使用到了thread_local变量, 在多线程环境中,与QObject相关的功能只有两种保险的策略.
      1. 不直接跨线程使用QObject对象
      1. QObject关联的对象树移动到某个线程再使用.(注意,QT要求,单个对象树都中的所有对象必须位于同一个线程中)
  • 所有与GUI直接相关的对象都必须放在主线程中,只有主线程会处理UI相关的事务.
  • Qt 使用 moc预处理机制,在C++预处理器前对头文件进行处理,自动生成一部分代码,为标准 C++ 增加了很多特性
    • 与之类似的还有uic,它负责将.ui文件转化为对应的UI类头文件ui_xx.h.
  • 只有继承了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().

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风格编程,持有一个privateUi::Wxx ui的成员.
    • 由于Ui::Wxx所在的头文件必须在uic后才存在,所以在源代码中引用时常常要进行前向声明
    • UI元素ObjectName的同名变量是该类的public的指针型成员.
  • Note: 虽然实践中不建议使用继承,但是在使用继承的策略进行开发时,可以直接override名为on_objectName_signalName()的虚函数,来响应Wxx::objectNamesingalName,不必手动connect.
    • ui类生成的代码中,把所有信号都已经连接到空的virtual函数上了,我们可以直接override

QT的事件机制

信号槽

  • 信号和槽形式上和一般的成员函数没有区别. 在QT5中,槽的概念已经淡化,任何可调用对象都可以作为槽,而不必限定在slot代码块中.
    • 信号声明与函数类似,返回void,不需要函数体(函数体由moc提供)
    • 信号和槽的声明也受public:private:的权限限制
  • 信号-槽的触发相当于对信号实参进行一次forward,但是要比函数调用宽松的一些
    • 例如,信号的形参可以比槽的多,多出的参数不会被forward,只是直接忽略.
  • 当connect语法比较复杂时,就应该考虑使用lambda,用lambda创建一个简单的可调用对象作为slot通常会更清晰一些.
  • 信号自身也可以作为”槽”,此时相当于emit一个新信号.
  • 信号槽机制保证:如果信号有槽接收,那么sender和reciver都不会在响应完成前被析构.
  • 成员函数可以通过this->sender()获得信号来源对象(仅当该成员函数作为槽被触发时,sender()返回的才是有意义的值.)
  • 信号-槽的部分功能是基于事件系统实现的.例如,不同线程的两个对象之间触发信号-槽时,就需要使用事件系统.
    • 如果connect的时候,senderreciver是绑定到同一个线程的,那么这样绑定的slot就会在emit sigal时直接原地调用.(同步)
    • 如果connect的时候,senderreciver不是绑定到同一个线程的,那么默认就会在emit sigal时向reciver post 一个event来触发slot的调用.(异步)
  • 有很多UI事件的默认handler就是emit某个信号(如鼠标点击就是在响应抬起事件时emit clicked()),所以信号-槽系统和事件系统在功能上是有一定重叠的
    • 一般而言,总是应该优先使用信号槽,只有信号槽系统无法满足需求时,才考虑使用事件系统.例如,对于鼠标点击,写一个槽来连接到clicked()要比处理鼠标事件更好.

事件系统

  • 每个QObject都有一个与自身绑定的thread_localqthread,这个qthread不只代表线程,还会持有一些线程私有的管理数据. 在QObject构造时,会自动绑定到当前所在的线程.
    • 例如,每个qthread都有一个事件队列,负责将收到的各种事件queue起来.(也就是说,每个线程都有自己私有的事件队列)
  • QT的事件循环可以嵌套,例如,开启一个模态窗口后,再开启一个模态窗口.
    • 嵌套的事件循环可以带来一定的灵活性,例如,内层循环的某些事件可以delay到外层循环处理.比如,使用deleteLater就可以在内层事件循环结束后才开始销毁某些对象.
  • Eventhandler一定是在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()虚函数,switchdefault分支将执行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(),一定要考虑是否需要调用一下默认的实现,如果不手动调用默认的实现,那么默认的行为就都会消失.
  • 如果我们希望事件能继续在对象树中向上流动,那么需要手动的event->ignore()一下.

QT 绘制系统简介

  • 可以使用传统实时绘图风格来绘制,即QPainter+QPainterDevice, 也可以使用类似于OpenGL的场景描述式渲染.
  • QPainter: 相当于绘图 Context, 所有的绘图API也是由它提供.
    • 它是在world coordinate下绘图.
    • 可以通过painter.setWindow来设置裁剪范围,超出这个范围的点都会被裁剪掉.
    • 可以通过painter.setViewPort在当前widget中划出一个小窗用于显示,裁剪后剩余的部分将渲染到这个小窗内.
  • QPainterDevice: 对应了帧缓冲的抽象,也就是说,它逻辑上是一个有颜色缓冲的像素图,每个像素都有自己的像素坐标.
    • QPainter构造时就要指定其PainterDevice.
    • 我们可以直接操作QPanterDevice的颜色缓冲,修改结果会直接显示出来.