Qt基础

在Qt5后,Qt不断开发QML和Qt Quick,同时在抛弃传统的基于C++的Qt Widgets,致力于更快捷、直观地绘制应用

当然,Qt的优势在于极致的跨平台和前后端的顺畅结合,Qt Widgets和Qt Quick的设计思路是相似的;多是QML/C++混合开发,纯粹的Qt Widgets在前端开发上是较为低效的

本文并不想介绍QML,而是介绍Qt Widgets

元对象系统

简介

元对象系统(Meta-Object System, MOS)是Qt的核心机制,元对象即用于描述对象的对象;它提供以下功能:

  • 信号-槽:允许对象间安全的通信,方便响应事件
  • 反射系统:允许程序自己获取(运行时获取)对象的信息,在java、dotNet中也有类似的概念
  • 动态属性:允许程序在运行时添加属性

所有继承QObject的类中,只要私有声明了宏Q_OBJECT,就可使用MOS

信号与槽

  • 关键字:借助MOC(元对象编译器),Qt为了实现信号槽机制,添加了slots,signals关键字(宏)

    • 信号和槽都是成员函数,一般声明如下:

      c++
      class Class : public QObject {
          Q_OBJECT
      signals:
          ...
      private slots:
          ...
      };

      signals宏定义为public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal),一定是公有的

      slots则可以在任意域中,但一般是保护或私有的

    • 一般,每个QObject的子类中的信号都应该有对应的用于发送该信号的成员函数(Qt自带类中一般定义为公有槽函数)

    • 一般控件有继承自父类的Qt实现的信号-槽和发送-响应机制

  • 自定义信号:

    • 无返回值,可以有参数
    • 只需声明,无需实现(MOC自动实现)
  • 自定义槽:

    • 返回值及参数必须和相连接的信号一致
    • 一旦声明,必须实现
  • 发送信号:

    • 通常在成员函数中,可以使用emit关键字发送自定义信号,同时传递参数:

      c++
      emit mySignal(...);
    • 需要主动发送信号时,通常调用封装好的发送函数,而不是直接使用emit

      c++
      // 官方命名习惯(例如按钮类):
      public slots:
       click(); // 发送信号.
      signals:
       clicked(); // 信号.
      
  • 连接方式:

    • 手动连接:调用connect(),四个必须填写的参数依次为发送者指针、信号、接收者指针、槽

      有三种输入信号-槽参数的方式:

      • 旧版本:调用宏函数SIGNAL()和SLOT(),处理输入的函数名后返回特定字符串

        连接失败后无反馈,例:

        c++
        // 参数不用添加限定符.
        connect(ui->lineEdit, SIGNAL(textChanged(QString)),
                this, SLOT(doSomething(QString)));
      • 新版本:传递函数指针,好处是连接失败后会报错,如:

        c++
        connect(ui->lineEdit, &QLineEdit::textChanged,
                this, &MainWindow::doSomething);
      • lambda表达式(匿名函数):好处是不用额外声明槽函数

        c++
        connect(ui->button, SIGNAL(textChanged(QString)), [this](const QString&) {
            ...
        });
    • 连接函数的第五个参数表示连接方式,默认为自动关联,常用的有:

      • AutoConnection:信号槽在同一线程则为DirectionConnection,否则为QueuedConnection
      • DirectionConnection:信号发射后直接调用槽,槽函数执行完毕后再执行emit语句后的代码
      • QueuedConnection:槽在接收者的线程中执行(若在不同线程,则发送线程继续进行)
    • 默认连接:Qt编译器会尝试让所有名为on_objName_signalName()的槽函数进行连接

      类似于调用:

      c++
      connect(&objName, &SenderClass::signalName,
              this, &thisClass::on_objName_signalName);

      注:objName命名不能太长,否则连接失败;连接失败后不会终止程序,但会打印错误信息

  • 常用控件的信号:

    python
    QPushButton:    # 按钮
        clicked()      # 被点击
    QLineEdit:     # 行编辑器
        textChanged(const QString&)  # 文本被修改, 同时发送修改后的文本
        returnPressed()     # 焦点在控件上时, 按下回车
     cursorPositionChanged()   # 光标移动
        editingFinished()    # 失去焦点/焦点在该控件上时, 按下回车
        selectionChanged()    # 选中区域被修改
    QDialog:     # 对话框
        accepted()/rejected()   # 因用户确认/拒绝对话框而被关闭
        finished()      # 被关闭
    QStackedWidget/QToolBox: # 堆栈窗口(多窗口器)/工具箱
        currentChanged(int idx)   # 当前窗口/选项修改, 同时发送新窗口的索引
  • 可能用到的控件的信号:

    python
    QSplitter:     # 窗口分割器
        splitterMoved(int pos, int idx) # 分割条被移动, 同时发送移动后的位置、被移动条的索引
    QAction/QToolButton:  # 动作/工具按钮
        triggered()      # 被触发
    QSpinBox/QDoubleSpinBox: # 整数旋钮/浮点数旋钮
        valueChanged(int/double val) # 值被修改, 同时发送新的值

事件系统

事件和信号槽的区别:

  • 信号本身没有和底层硬件、操作系统交流的能力,部分信号还需要通过事件发出(如clicked())
  • 关注点:
    • 信号槽:发出、接收信号的对象
    • 事件处理:发生的是哪种事件,并不关注对象间的通信
  • 来源:
    • 信号:程序里的函数,由对象发出
    • 事件:系统的底层消息
  • 本质:
    • 信号槽:成员函数
    • 事件:对象

Qt的事件循环:

  • 产生:应用启动(QApplication::exec())后,Qt即开启事件循环,不断检测底层消息并将它们转换为事件
  • 分发:事件通过QApplication::notify()传入到事件队列,按队列的顺序取出事件,调用事件分发函数bool event(QEvent* event)
  • 处理:event()会分情况调用不同的事件处理函数
  • 事件循环里,事件的产生和处理是异步的

事件也可手动发送,它们不经过事件循环

有些类(如按钮类)的处理函数已经封装好了,而有些类(如窗口类)的键鼠事件处理函数没有语句,因此有需要时,需要重写它们,一般有两种处理方式:

  • 发送信号,将事件的处理解耦成多个槽函数,可扩展性强,便于跨对象、跨线程通信
  • 直接在函数内部处理,可用于事件处理较为简单的情况

常见的事件处理函数:

python
# 启用某种属性的函数,某些事件需要特定属性才能产生
setAttribute(Qt::WidgetAttribute, bool)

# 以下函数返回值为void
键盘:
    keyPressEvent(QKeyEvent* event)    # 按键按下
    keyReleaseEvent(QKeyEvent* event)   # 按键松开
鼠标:
    mousePressEvent(QMouseEvent* event)   # 鼠标按下
    mouseReleaseEvent(QMouseEvent* event)  # 鼠标松开
    mouseDoubleClickEvent(QMouseEvent* event) # 鼠标双击
 wheelEvent(QWheelEvent* event)    # 鼠标滚轮滑动
    enterEvent(QEvent* event)     # 鼠标移进
 leaveEvent(QEvent* event)     # 鼠标移出
    hoverMoveEvent(QHoverEvent* event)   # 鼠标悬停(需启用WA_Hover)
焦点:
    focusInEvent(QFocusEvent* event)   # 获得焦点
 focusOutEvent(QFocusEvent* event)   # 失去焦点
拖动:
    setAcceptDrops(bool)      # 设置控件能否拖动
    dragEnterEvent(QDragEnterEvent* event)  # 被拖动的物体进入可放置区域
    dragMoveEvent(QDragMoveEvent* event)  # 被拖动的物体移动
    dragLeaveEvent(QDragLeaveEvent* event)  # 被拖动的物体离开可放置区域
    dropEvent(QDropEvent* event)    # 放下

传入的事件本身是带有信息的,例如:

python
QKeyEvent:
    key()    # 返回Qt::Key枚举值, 表示按下的是哪个键
QMouseEvent:
    pos()    # 返回QPoint, 表示鼠标相对于该控件的位置
    globalPos()   # 返回QPoint, 表示鼠标在屏幕上的位置

关于事件分发函数event()的重写:

c++
// event()是事件分发函数, 是有返回值的(表示事件是否已处理)
// 重写event()的目的是实现 事件过滤 或 想在事件处理前执行一些代码 或 分发自定义事件
bool event(QEvent* event) override {
    switch (event->type()) {
        // 想处理的事件
        case QEvent::KeyPress: 
            ...
            keyPressEvent((QKeyEvent*)event);
            // 标记为已处理(Qt默认调用accepted, 可以在处理函数里手动调用ignore())
            if (((QKeyEvent *)event)->isAccepted())
                return true;  // 则不传递
            break;
        case 想忽略的事件: return true; // 虽然未处理, 但可以直接返回true表示已处理
    }
 return QWidget::event(event); // 未处理, 交给父类处理
}

项目初始化

通过项目初始化来介绍Qt在控件上的一些机制和默认

.ui文件

在创建Qt项目时,应该自选生成.ui文件,能轻易地在Qt Designer中修改它,并定义窗口和控件对象、以及它们的初始化

  • 该文件使用xml语言,在Qt Designer上修改比直接编辑快的多

  • 其能在C++程序中起作用的原因是,Qt编译器会通过uic(ui编译器)将name.ui翻译成头文件ui_name.h,其中定义了类Ui_name:

    • 该类定义了所有在QD设计的、除窗口类外的控件

    • 定义了成员函数setupUi(QMainWindow*),用于初始化各种控件

    • 自动生成的项目主窗口类中,默认定义Ui_name类的指针ui

      所有构造函数的第一行应调用**ui->setupUi(this)**,以确保能安全调用ui中的控件

主窗口类及内存机制

学习自动创建的主窗口头文件格式,便于学会自定义控件类

设创建项目时定义的主窗口类为MainWindow,则应有头文件定义:

c++
class MainWindow : public QMainWindow {
    Q_OBJECT
public:
    MainWindow(QWidget* parent = nullptr);
    ~MainWindow();
};
  • Q_OBJECT:Qt定义的宏,只有声明了它,才能使用元对象系统

    任何继承了QObject类的子类都应私有声明该宏

  • 简单窗口类可直接继承自QWidget

    复杂窗口类(如包含菜单栏、对话框)应继承QMainWindow,才能正常显示

构造函数的实现至少是这样的:

c++
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent),
 ui(new Ui_MainWindow) {
    ui->setupUi(this);
}

作为参数传入的parent和this是有必要的,因为Qt有自己的一套回收内存的机制:

  • 半自动回收:所有QObject类及其子类对象都定义了parent和链表(用于存储子控件的指针)

    习惯上,所有控件类对象都使用堆内存,并在构造时指定父控件

    • 当释放父控件的内存时,会先释放所有子控件的内存,因此只要记得释放根控件的内存,就不会出现内存泄露

    • 而释放子控件的内存后,它会从其父控件的链表中删去,因此不会出现重复释放内存的情况

    • 所有这些对象都有**QObject::setParent()**方法,用于设置父控件

  • 手动释放:对于父控件在不同线程、或非QObject系的占用堆内存的对象,无法半自动回收,应在其所在线程里调用**deleteLater()方法,调用后会在线程结束时释放其内存**

外部资源管理

Qt中通过.qrc(Qt Resource)文件管理外部资源,该文件使用xml语言,在QC中能更直观地使用

xml
<!DOCTYPE RCC>
<RCC>
 <qresource prefix="/Icons">
        <file>images/name.jpg</file>
    </qresource>
</RCC>

直接编码也不难理解,这些标签的含义是:

  • RCC:其内容会被rcc(Qt的资源编译器)识别,并把资源编译到可执行文件里
  • qresource:用于指定其所属文件的前缀,前缀默认为"/"
  • file:其内容为资源的真正的路径,例如上述语句引入了处于./images/name.jpg路径(相对于该资源文件)的图片

.qrc文件通过独特的相对路径管理资源,即冒号+前缀+文件路径,例如通过":/Icons/images/name.jpg"访问上述语句中的文件

使用资源的方式:

c++
setWindowIcon(QIcon(路径)) // 设置窗口图标,仅窗口类可使用
    
setIcon(QIcon(路径))   // 设置控件图标,仅支持图标的控件可使用
    
QFile qssFile(":/path/name.css"); // 引入外部样式表
qssFile.open(QFile::ReadOnly);
qApp->setStyleSheet(qssFile.readAll()); // 加载到整个应用程序(所有QWidget类对象都可加载)

QSound::play(":/path/name.wav"); // 播放wav音频文件(静态播放,只能在主线程中调用)
QSound* musicPlayer {new QSound(路径)}; // 加载进内存后播放
musicPlayer->play();

QMediaPlayer* musicPlayer {new QMediaPlayer}; // 可播放wav、mp3、mp4等
musicPlayer->setMedia(QUrl::fromLocalFile(路径));
musicPlayer->play();

样式

关于样式,推荐通过QD进行初始化,或一律通过外部样式表统一,而不是通过C++编码修改

如果有CSS基础,许多属性会很熟悉

控件

所有QWidget系对象都有(由以下类组合而成):

  • font:存储字体族、字体大小、粗体、斜体等(构造函数的参数就是这四个属性)

    对象可调用setFont(QFont)方法修改

  • sizePolicy:存储大小和伸缩的策略

    • 伸展策略:实质是枚举QSizePolicy::Policy,可通过setSizePolicy()修改

      • 基于sizeHint有不同的策略,以下称其为默认值

      • Fixed:控件大小始终为默认值

      • Minimun/Maximun:控件的默认值为最小/最大尺寸,大小可以大于/小于默认值

        最小/最大尺寸可通过setMinimunSize()/setMaximunSize()设置

      • Preferred:控件大小是默认值,但可以放大/缩小

      • Expanding:控件可以随意放大/缩小

      • 当Preferred和Expanding的控件同时存在于布局中时,前者会把空间让给后者且尽量保持默认值

    • 伸展因子:一般通过布局来管理,伸展因子越大,拉伸控件时改变的空间越多

  • focusPolicy:存储获取焦点的策略,实质是枚举Qt::FocusPolicy

    • StrongFocus:默认值,也最常用,可通过鼠标点击、Tab键获取焦点
    • TabFocus/ClickFocus:Tab键/鼠标点击获取焦点
    • NoFocus:不可通过键鼠获取焦点
  • geometry:不重要,一般通过布局来控制它

布局

Qt提供以下几种布局管理器:

  • QHBoxLayout/QVBoxLayout:水平/垂直布局
  • QGridLayout:网格布局
  • QFormLayout:表单布局

布局有助于控件随窗口拉伸而自动拉伸,以下操作可在QD快速实现:

  • 窗口调用setCentralWidget()设置中心控件
  • 该中心控件调用setLayout()设置布局

窗口

如要限制窗口的放大/缩小键,可通过setWindowFlags()设置,参数为枚举Qt::WindowType

  • WindowMaximunButtonHint/WindowMinimunButtonHint:最大化/最小化
  • WindowCloseButtonHint:关闭键

更多常用方法

python
QWidget:
    resize()   # 重置大小
    setFocus()   # 主动吸引焦点
    setHidden()   # 设置是否隐藏控件
QMainWindow:
    setWindowTitle() # 设置标题
    show()    # 显示窗口
    close()    # 关闭窗口
QDoubleSpinBox:
    setDecimals()  # 设置浮点数精度(实质是字符串)
文本编辑/显示器:
    setText()   # 设置文本, 会解析HTML内容
    setPlainText()  # 设置纯文本
 text()    # (QLineEdit)获取文本
    toPlainText()  # (其它)获取文本
按钮类:
    setEnabled()  # 设置是否启用
布局管理器:
    addWidget()   # 添加控件
    columnCount()/rowCount() # 列/行数
QStackedWidget:
    setCurrentIndex() # 设置当前窗口
QString:
    number()   # 静态成员, 将整型/浮点型转化为QString
QMessageBox:
    information()  # 静态成员, 呼出简单的小窗口(可添加Ok/Cancel按钮并返回用户的选择)

qDebug() << Data  # 打印信息