Qt——系统

目录

概述

事件

鼠标事件

进入、离开事件

按下事件

释放事件

双击事件

移动事件

滚轮事件

按键事件

单个按键

组合按键

定时器

QTimerEvent

QTimer

窗口事件

文件

输入输出设备

文件读写类

文件和目录信息类

多线程

常用API

线程安全

互斥锁

条件变量

信号量

网络

[UDP Socket](#UDP Socket)

[TCP Socket](#TCP Socket)

[HTTP Client](#HTTP Client)

[Qt 音视频](#Qt 音视频)

[Qt 音频](#Qt 音频)

[Qt 视频](#Qt 视频)


概述

Qt 是一个跨平台的C++开发框架,我们要知道的是 Qt 中许多的功能都是操作系统提供的,Qt 封装了系统调用,那么这一篇就来看一看系统为我们支持了什么吧。


事件

在上上篇中介绍的信号和槽中,用户进行的操作可能会产生某种信号,给一个信号连接上槽函数,当信号触发的时候,就可以执行对应的槽函数,进而完成各种功能。

除了信号,用户进行的操作也会产生事件,我们也可以给事件关联上处理函数,当事件触发的时候,就可以执行对应的代码。

这么一看,事件和信号还是差不多的,事件本身是操作系统提供的机制,Qt 把操作系统的事件机制进行封装,但是事件的代码编写起来不是很方便,所以 Qt 对于事件进一步封装,这就是信号和槽,事件就是它的底层机制。

实际 Qt 开发过程中,多数的交互功能都是通过信号和槽来完成的,但是也有特殊的情况,信号和槽无法实现,就比如 Qt 中没有这个信号,这就需要重写事件处理函数,来手动处理事件的逻辑。

所有的 Qt 事件均继承了抽象类 QEvent。当用户进行一些操作就会触发事件,常见的 Qt 事件如下:

事件名称 描述
鼠标事件 鼠标左键,右键,滚轮,移动,按下和松开
键盘事件 按键类型,按键按下,按键松开
定时器事件 定时到达
进入、离开事件 鼠标的进入和离开
滚轮事件 鼠标滚轮滚动
绘屏事件 重绘屏幕的某些部件
显示隐藏事件 窗口的显示和隐藏
移动事件 窗口位置的变化
窗口事件 是否为当前窗口
大小改变事件 窗口大小改变
焦点事件 键盘焦点移动
拖拽事件 用鼠标进行拖拽

鼠标事件

进入、离开事件

之前信号和槽是通过connect来关联的,对于事件的处理方式为让当前的类重写某个事件的处理函数,使用的是多态的机制,子类重写父类的事件处理函数,事件触发过程中就可以子类调用子类,父类调用父类。

为了理解一下这个事件就写一个简单的程序,创建一个按钮,当鼠标进入和离开这个控件就会触发事件。处理事件要再创建一个类,这个类中重写对应的事件处理函数。类创建好之后,给构造函数中添加一个父元素参数,这个类已经继承了QPushButton,重写两个事件函数就可以了。

之后就可以在图形化界面中拖拽一个QPushButton,但是还有一个问题就是这个控件不是我们自己创建的PushButton,只有PushButton才可以触发我们自己写的这个事件处理函数。

这里就需要使用这个功能,输入要提升的类名称,头文件会自己填充,一定要注意不要拼错。

之后这里的类名就变成了我们定义的PushButton。

现在就是万事俱备,只欠运行了。运行后就会看到进出都会触发对应的处理函数。

所以之前的哪那个表白程序还可以让按钮跑的更快。

cpp 复制代码
void PushButton::enterEvent(QEvent *event)
{
    (void) event; // 这个参数暂时用不到
    qDebug() << "enterEvent";

    QRect rect = this->geometry();
    qDebug() << rect;

    this->setGeometry(rand() % (800 - rect.width()), rand() % (600 - rect.height()), rect.width(), rect.height());
}

按下事件

鼠标按下事件是通过虚函数mousePressEvent来捕获的,通过鼠标点击,我们也可以获取鼠标点击的位置。

cpp 复制代码
void PushButton::mousePressEvent(QMouseEvent *event)
{
    // 以PushButton左上角为原点
    qDebug() << event->x() << " : " << event->y(); // 包含QMouseEvent头文件
    // 以整个屏幕左上角为原点
    qDebug() << event->globalX() << " : " << event->globalY();
}

这里点击了一个PushButton左上角的位置,获取的位置信息也是不一样的。

还有一点要注意的就是,不管是使用鼠标左键还是右键,甚至按下滚轮也可以触发这个事件。

cpp 复制代码
void PushButton::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
        qDebug() << "左键";
    else if (event->button() == Qt::RightButton)
        qDebug() << "右键";
    else
        qDebug() << "其他键";


    // 以PushButton左上角为原点
    qDebug() << event->x() << " : " << event->y(); // 包含QMouseEvent头文件
    // 以整个屏幕为原点
    qDebug() << event->globalX() << " : " << event->globalY();
}

释放事件

鼠标释放事件就是通过mouseReleaseEvent来捕获的。

cpp 复制代码
void PushButton::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
        qDebug() << "左键按下";
    else if (event->button() == Qt::RightButton)
        qDebug() << "右键按下";
    else
        qDebug() << "其他键按下";
}

void PushButton::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
        qDebug() << "左键释放";
    else if (event->button() == Qt::RightButton)
        qDebug() << "右键释放";
    else
        qDebug() << "其他键释放";
}

双击事件

双击事件通过虚函数mouseDoubleClickEvent来实现。

cpp 复制代码
void PushButton::mouseDoubleClickEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
        qDebug() << "左键双击";
    else if (event->button() == Qt::RightButton)
        qDebug() << "右键双击";
    else
        qDebug() << "其他键双击";
}

移动事件

鼠标移动事件通过mouseMoveEvent来实现,为了实时捕捉鼠标位置的信息,需要通过setMouseTracking方法追踪鼠标的位置。

鼠标移动不同于以上操作。随便移动鼠标就会产生大量事件,当捕获这个事件时,再进行一些复杂的逻辑,那么程序负担就很重,很容易产生卡顿等问题。

Qt 为了保证程序的流畅性,默认情况下不会对鼠标移动进行追踪,也就不会调用mouseMoveEvent,只有在构造函数中指明当前窗口需要捕捉鼠标移动事件,使用setMouseTracking方法,参数设置为true。

cpp 复制代码
PushButton::PushButton(QWidget* parent)
    :QPushButton(parent)
{
    this->setMouseTracking(true);
}

void PushButton::mouseMoveEvent(QMouseEvent *event)
{
    qDebug() << event->x() << event->y();
}

滚轮事件

Qt 中滚轮事件是通过QWheelEvent类实现的,而滚轮滑动的距离可以通过delta方法获取。

cpp 复制代码
void PushButton::wheelEvent(QWheelEvent *event)
{
    qDebug() << event->delta();
}

打印的值为正负120,滚轮向上滚动为+,向下滚动为-。

现在我们可以写一个通过滚轮调节字体大小的。

cpp 复制代码
void PushButton::wheelEvent(QWheelEvent *event)
{
    QFont font = this->font();
    qDebug() << font;
    if (event->delta() > 0)
        font.setPointSize(font.pointSize() + 1);
    else if (event->delta() < 0)
        font.setPointSize(font.pointSize() - 1);
    this->setFont(font);
}

按键事件

Qt 中的按键事件是通过 QKeyEvent 类来实现的。按键上的按键按下或者被释放时都会触发按键事件。

单个按键

之前我们也使用过QShortCut,这个是信号和槽封装的获取键盘的方式,站在更底层的角度课可以通过事件获取当前用户键盘按下的情况,使用的是keyPressEvent。

cpp 复制代码
void MainWindow::keyPressEvent(QKeyEvent *event)
{
    qDebug() << event->key();
}

按照abcde的顺序按下键盘上的键,输出的窗口就会打印这样的内容,当然也可以试试别的按键,想要判断还可以这样写。

cpp 复制代码
void MainWindow::keyPressEvent(QKeyEvent *event)
{
    qDebug() << event->key();
    if (event->key() == Qt::Key_A)
        qDebug() << "按下了a键";
}

还有一点要注意的是,我们这里直接在QMainWindow中重写了这个事件函数,也可以在QWidget中重写这个事件函数,这里就要注意了,想要触发这个事件,一定要让该控件获取焦点,也就是说焦点不在,是触发不了事件的,什么是焦点,那就是要选中这个控件。

组合按键

想要使用组合键就要通过modifiers来获取。Qt::KeyboardModifier 中定义了在处理键盘事件是对应的修改键。

|-------------------------|----------------------------------------|
| Qt::NoModifier | 无修改键 |
| Qt::ShiftModifier | Shift 键 |
| Qt::ControlModifier | Crtl 键 |
| Qt::AltModifier | Alt 键 |
| Qt::MetaModifier | Meta 键(Windows上指Win键, macOS上指Command键) |
| Qt::KeypadModifier | 使用数字键盘进行输入时,Num Lock键处于打开状态 |
| Qt::GroupSwitchModifier | 用于在输入法之间切换 |

cpp 复制代码
void MainWindow::keyPressEvent(QKeyEvent *event)
{
    qDebug() << event->key();
    if (event->key() == Qt::Key_A && event->modifiers() == Qt::ControlModifier)
        qDebug() << "按下了Crtl + a键";
}

定时器

Qt 在进行窗口程序处理的过程中,经常要周期性的执行某些操作,或者制作一些动画效果,使用定时器就可以实现,定时器就是间隔一段时间后执行某些任务。

Qt 中的定时器分为QTimerEvent和QTimer两个类:

  • QTimerEvent类,用来描述一个定时器事件,使用startTimer函数来开启定时器,需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,它返回的整型值代表一个定时器。当定时器溢出时(定时时间到达)就可以在timeEvent函数中获取该定时器的编号来进行相关操作。
  • QTimer类,用来实现定时器,它提供了更高一层的编程接口,比如:可以连接信号和槽,还可以设置只运行一次的定时器。

QTimer 的背后是QTimerEvent 定时器事件进行支撑的。

QTimerEvent

我们还是使用LCD Number这个控件,这个控件显示的是一个数字,把初始值设置为10,设置一个每一秒就触发一次的定时器。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 开启定时器事件
    // 返回一个定时器id
    timeId = this->startTimer(1000); // 因为后续还要使用,定义为成员变量

}

void MainWindow::timerEvent(QTimerEvent *event)
{
    // 如果一个程序中存在多个定时器(startTimer创建的定时器),此时每个定时器都会触发这个函数
    // 先判断
    if (event->timerId() != this->timeId) // 如果不是就忽略
        return;

    int value = ui->lcdNumber->intValue();
    if (value <= 0)
    {
        // 停止定时器
        this->killTimer(this->timeId);
        return;
    }
    value -= 1;
    ui->lcdNumber->display(value);
}

使用timerEvnet比QTimer还要复杂,需要手动管理timerId,区分这次的timerId是否正确,所以后续还是使用QTimer。

QTimer

QTimer 在LCD Number中使用过,这里演示一下就可以了。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    timer = new QTimer(this);
    timer->start(1000);

    connect(timer, &QTimer::timeout, this, &MainWindow::handle);
}

void MainWindow::handle()
{
    int value = ui->lcdNumber->intValue();
    if (value <= 0)
    {
        timer->stop();
        return;
    }
    value -= 1;
    ui->lcdNumber->display(value);
}

窗口事件

moveEvent 是窗口移动时触发的事件,resizeEvent 是窗口大小改变时触发的事件。

拖动窗口和调整窗口大小就会打印相应的内容。

cpp 复制代码
void Widget::moveEvent(QMoveEvent *event)
{
    qDebug() << event->pos();
}

void Widget::resizeEvent(QResizeEvent *event)
{
    qDebug() << event->size();
}

文件

对于文件操作以及不再陌生了,不管是C语言的文件操作还是C++的,还有Linux中提供的系统接口,前面都已经介绍过了,这些方法都是封装了系统的API,Qt 中也提供了一套文件操作的API,而且也更推荐使用这一套。

输入输出设备

在 Qt 中,文件读写的类为QFile,它的父类是QFileDevice,这个类提供了文件交互的底层功能,这个类的父类又是QIODevice,在往上就是QObject。

QIODevice 是 Qt 中所有输入输出设备的基础类。

  • QFile,用于文件操作和文件读写的类。
  • QSaveFile,用于安全保存文件的类,他会把数据写入一个临时文件,成功提交后再将数据写入最终文件。如果出现错误,可以保证不会丢失原本的数据或者只有部分写入。
  • QTemporaryFile,用于创建临时文件的类,使用QTemporaryFile::open可以创建一个文件名唯一的临时文件,对象被删除时,临时文件自动被删除。
  • QTcpSocket 和 QUdpSocket,实现网络通信。
  • QSerialPort,为串口通信方式,一般用在嵌入式系统上。
  • QBluetoothSocket,用于蓝牙通信的类。
  • QProcess,对系统操作做的封装。
  • QBuffer,内置的缓冲区类。

文件读写类

再 Qt 中,文件的读写主要是通过 QFile 类实现的。

要对数据做读取,就要先打开文件。

这两个方法还需要使用C语言的方式,就比较麻烦,下面还有一个。

再构造函数中指定路径后,直接使用这个方法就可以打开,参数为OpenMode,意思是打开方式,有读有写,还有追加写这几种方式,文档中也有对应的枚举类型。

对文件的操作有读数据,在QIODevice这个类中可以找到。

对文件的操作还有写数据。

最后也不要忘了关闭文件,使用的是close这个方法。
现在我们就可以实现一个记事本的功能了。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 获取菜单栏
    QMenuBar* menuBar = this->menuBar();
    this->setMenuBar(menuBar);

    // 添加菜单
    QMenu* menu = new QMenu("文件(&F)");
    menuBar->addMenu(menu);

    // 添加菜单项
    QAction* action1 = new QAction("打开");
    QAction* action2 = new QAction("保存");
    menu->addAction(action1);
    menu->addAction(action2);

    // 指定一个输入框
    edit = new QPlainTextEdit();
    QFont font = edit->font();
    font.setPointSize(20);
    edit->setFont(font);
    this->setCentralWidget(edit);

    // 连接信号和槽
    connect(action1, &QAction::triggered, this, &MainWindow::handleAction1);
    connect(action2, &QAction::triggered, this, &MainWindow::handleAction2);
}

void MainWindow::handleAction1()
{
    // 打开文件先弹出对话框
    QString path = QFileDialog::getOpenFileName(this);

    // 把文件名显示到状态栏
    QStatusBar* statusBar = this->statusBar();
    statusBar->showMessage(path);

    // 根据用户选择的路径,构造一个QFile对象
    QFile file(path);
    bool ret = file.open(QIODevice::ReadOnly);
    if (!ret)
    {
        // 打开文件失败
        statusBar->showMessage(path + "打开失败!");
        return;
    }

    // 读取文件
    QString text = file.readAll(); // 虽然返回的是一个QByteArray,但是转换成QString,一定要确保打开的文件是文本文件

    // 关闭文件
    file.close();

    // 把读取到的内容显示到输入框
    edit->setPlainText(text);
}

void MainWindow::handleAction2()
{
    // 弹出保存文件的对话框
    QString path = QFileDialog::getSaveFileName(this);

    // 在状态里显示文件名
    QStatusBar* statusBar = this->statusBar();
    statusBar->showMessage(path);

    // 根据用户选择的路径,构造一个QFile对象
    QFile file(path);
    bool ret = file.open(QIODevice::WriteOnly);
    if (!ret)
    {
        statusBar->showMessage(path + "打开失败!");
        return;
    }

    // 写入文件
    const QString& text = edit->toPlainText();
    file.write(text.toUtf8());

    // 关闭文件
    file.close();
}

先在QPlainTextEdit中输入一些内容,点击文件再点击保存,选择保存到桌面,输入文件名,这样就保存成功了。

关闭程序,再次运行,这次输入框中什么也没有,点击文件再点击打开,找到刚才保存的文件,这样就打开成功了。

文件和目录信息类

QFileInfo 是 Qt 中提供的一个用于获取文件和目录信息的类,例如获取文件名,文件大小和文件修改日期等。常用的方法有:

方法 说明
isDir 文件是否为目录
isExecutable 文件是否为可执行程序
fileName 文件名
completeBaseName 完整的文件
suffix 文件名后缀
completeSuffix 完整的文件名后缀
size 文件大小
isFile 是否为文件
fileTime 获取文件创建时间、修改时间、最近访问时间

我们还是打开保存在桌面的文件,就会获取我们想要的信息。

cpp 复制代码
void Widget::on_pushButton_clicked()
{
    // 打开文件获取路径
    QString path = QFileDialog::getOpenFileName(this);
    // 构造一个QFileInfo对象
    QFileInfo fileInfo(path);
    // 打印
    qDebug() << fileInfo.fileName();         // 文件名
    qDebug() << fileInfo.suffix();           // 文件后缀
    qDebug() << fileInfo.path();             // 文件路径
    qDebug() << fileInfo.completeSuffix();   // 完整后缀
    qDebug() << fileInfo.completeBaseName(); // 完整文件名
}

多线程

Qt 多线程和 Linux 中的多线程本质是一样的。在 Linux 中使用的API是Linux系统提供的pthread库,Qt 也重新封装了。

Qt 中的多线程一般是通过 QThread类 实现的。QThread 代表一个程序中可以独立控制的进程,也可以和进程中其他线程共享数据。

常用API

方法 说明
run 线程入口函数。
start 通过调用run开始执行线程,操作系统将根据优先级参数调度线程,如果线程已经在运行,那就忽略。
currentThread 返回一个指向当前线程的QThread指针。
isRunning 如果线程正在运行返回true,反之返回false。
sleep/msleep/usleep 使线程休眠,单位为秒/毫秒/微秒。
wait 阻塞线程,满足以下任何一个条件: * 与此 QThread 对象关联的线程已经完成执行,即从run方法返回,如果线程已经完成或者还没有启动,都返回true。 * 已经过了几毫秒,如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run返回),如果等待超时,函数返回false。 类似于 pthread_join类似的功能。
terminate 终止线程执行,何时终止取决于调度策略,之后可以使用QThread::wait来确保
finished 线程结束发出该信号,可以通过该信号实现线程清理工作

之前使用定时器来完成倒计时程序,也可以通过线程来完成类似的功能,现在创建一个新线程。因为存在线程安全的问题,多个线程同时修改界面,就会导致界面出错,所以 Qt 要对界面的控件进行修改,一定要在主线程内执行。

虽然不能修改界面,但是可以计时,也可以写一个类似定时器的功能。

创建一个线程类,继承QThread,重要的就是重写run方法,使用QThread中的sleep方法可以让线程休眠一秒,之后发送我们自定义的信号。

主线程要添加一个新线程对象,连接信号槽并启动线程,只要新线程发出信号,主线程就可以捕捉到,从而达成定时的效果。

cpp 复制代码
class Thread : public QThread
{
    Q_OBJECT
public:
    Thread();

    void run();

signals:
    void notify();
};

// 重要是重写父类run方法
void Thread::run()
{
    // 在新线程中不能直接修改界面内容

    // 每到一秒,通过信号槽。通知主线程负责更新界面
    for (int i = 0; i < 10; i++)
    {
        sleep(1);
        // 发送信号通知主线程
        emit notify();
    }
}
cpp 复制代码
class MainWindow : public QMainWindow
{
    Q_OBJECT
    // ... 
public:
    void handle();

private:
    // ...
    Thread thread;
};

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    
    // 连接信号和槽通过槽函数更新界面
    connect(&thread, &Thread::notify, this, &MainWindow::handle);
    
    // 启动线程
    thread.start();
}

void MainWindow::handle()
{
    int value = ui->lcdNumber->intValue();
    if (value <= 0)
        return;
    
    value--;
    ui->lcdNumber->display(value);
}

我们之前提到多线程,主要还是站在服务器开发的角度考虑的,就是利用了CPU多核的资源,或者双路CPU,从而达到高效的处理。

那么客户端多线程的意义不在这里,对于客户端用户来说,体验感是主要的,还是要通过多线程的方式执行一些耗时的等待IO的操作,避免主线程卡死,比如客户端和服务端进行上传和下载一个很大的文件,传输需要消耗很长时间,这种密集IO操作就会使程序被系统阻塞,所以只要被阻塞,用户的操作也就无法响应。

所以更好的做法是使用单独的线程来处理密集IO操作,主线程主要负责事件循环,负责处理用户的操作。

线程安全

常用的实现线程互斥和同步的类有:

  • 互斥锁:QMutex、QMutexLocker
  • 条件变量:QWaitCondition
  • 信号量:QSemaphore
  • 读写锁:QReadLocker、QWriteLocker、QReadWriteLock

互斥锁

互斥锁是为了防止多个线程同时访问同一对象实例的方法,在 Qt 中主要是通过QMutex类来处理的。

在 Linux 的章节我们也详细说过了,这里我们在实现一下。

cpp 复制代码
class Thread : public QThread
{
    Q_OBJECT
public:
    // ...
    static int num; // 静态成员变量
};

int Thread::num = 0;
// 重写run方法
void Thread::run()
{
    for (int i = 0; i < 50000; i++)
    {
        Thread::num++;
    }
}

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建两个线程对象
    Thread t1;
    Thread t2;

    t1.start();
    t2.start();

    // 线程等待
    t1.wait();
    t2.wait();

    qDebug() << Thread::num;
}

如果不加锁就是上面的结果,两个线程同时++,最后的结果应该是100000。

cpp 复制代码
class Thread : public QThread
{
    Q_OBJECT
public:
    // ...
    static int num;

    static QMutex mutex;
};

void Thread::run()
{
    for (int i = 0; i < 50000; i++)
    {
        mutex.lock();
        num++;
        mutex.unlock();
    }
}

这种就是加锁和解锁,但是,话又说回来,作为一个C++程序员,内存泄漏是一个很大的问题,所以一定要注意unlock的问题,如果代码执行过程中return了,或者因为抛异常,都可能导致无法调用unlock的问题,同理,释放内存也是这样的,所以为了解决这样的问题,C++中就出现了智能指针,同样是使用RAII的机制。Qt 中的QMutexLocker就是QMutex的智能指针。

cpp 复制代码
void Thread::run()
{
    for (int i = 0; i < 50000; i++)
    {
        QMutexLocker locker(&mutex);
        num++;
    }
}

不管是C++11中的mutex还是Qt的QMutex,都是封装了系统的锁。

条件变量

多线程编程中访问临界资源,一定要先检测临界资源是否存在,因为检测也是访问临界资源,所以在这之前就要加锁,但是如果一直加锁、检测、检测失败、释放锁,频繁执行这种操作也是无意义的。

所以某个线程还需要等待某些条件满足才能执行,这也是Linux篇章中说过的,Qt 中提供了一个QWaitCondition类解决上述问题,其中wait方法是释放锁+等待,wake方法是加锁+唤醒,wakeAll是唤醒所有,此外,检测也是采用while循环检测的方式。

信号量

原来我们就说过信号量本质上是一个计数器,在多线程场景中,多个线程访问一个数量有限的资源,信号量本是也是一个预定机制,有两个方法,分别是P操作获取信号量和V操作释放信号量,在 Qt 中 QSemaphore 封装了信号量,P操作和V操作变成了acquire方法和release方法。


网络

Qt 为了支持跨平台,对网络编程的API也重新封装了。 网络编程其实编写的是应用层代码,但是需要传输层的支持,传输层的核心协议有UDP和TCP,Qt 也提供了两套API,分别是QUdpSocket和QTcpSocket。

还有一点要注意的是,要想实现网络编程,还要在.pro文件中添加network模块。我们之前提到过的各种控件都包含在QtCore模块中,为了不让可执行程序变得过于庞大,导致一些性能不够好的机器承受太大的压力,所以就进行了模块化的处理,默认情况下额外的模块不会参与编译,有需要就在.pro文件中添加。

UDP Socket

主要有两个类,QUdpSocket 和 QNetworkDatagram。

名称 类型 说明
bind(const QHostAddress&, quint16) 方法 绑定指定的端口号,类似bind。
receiveDatagram() 方法 返回 QNetworkDatagram,读取一个UDP数据报,对标recvfrom。
writeDatagram(constQNetworkDatagram&) 方法 发送一个UDP数据报,对标sendto。
readyRead 信号 在收到数据并准备就绪后触发。此时就可以在槽函数中完成读取请求的操作。

QNetworkDatagram 表示一个UDP数据报。

名称 类型 说明
QNetworkDatagram(const QByteArray&, const QHostAddress&, quint16) 构造函数 通过 QByteArray,目标IP地址,目标端口号 来构造一个UDP数据报。
data() 方法 获取数据报内部持有的数据,返回 QByteArray。
senderAddress() 方法 获取数据报中包含的对端IP地址。
senderPort() 方法 获取数据报中包含的对端的端口号。

现在我们就可以写一个回显服务器,在界面中拖拽一个 QListWidget 来显示消息。

在写代码之前一定要在.pro文件中添加network模块。写一个服务器首先就要有一个Socket对象,之后就要连接信号和槽,捕捉readyRead信号,对应的槽函数就要完成服务器的核心逻辑,之后就是bind端口号,一个Udp服务器就做好了。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建出socket这个对象
    socket = new QUdpSocket(this);

    // 设置窗口标题
    this->setWindowTitle("服务器");

    // 连接信号槽
    connect(socket, &QUdpSocket::readyRead, this, &MainWindow::processRequest);

    // 一定是先连接信号槽,再bind
    // 绑定的Any就类似Linux中bind的INADDR_ANY,可以绑定多个网卡,就可以接收不同网卡的数据
    bool ret = socket->bind(QHostAddress::Any, 8080);
    if (!ret)
    {
        // 绑定失败
        // errorString就类似于perror
        QMessageBox::critical(this, "服务器启动出错", socket->errorString());
        return;
    }
}

之后就是服务器的核心逻辑。

cpp 复制代码
// 完成服务器的核心逻辑
void MainWindow::processRequest()
{
    // 1. 读取请求并分析
    const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
    const QString& request = requestDatagram.data(); // data返回的是QByteArray,可以转换成QString

    // 2. 根据请求计算响应
    const QString& response = process(request);

    // 3. 把响应写回客户端
    QNetworkDatagram responseDatagram(response.toUtf8(), requestDatagram.senderAddress(), requestDatagram.senderPort());
    socket->writeDatagram(responseDatagram);

    // 显示到服务器
    QString log = "[" + requestDatagram.senderAddress().toString() + ":" + QString::number(requestDatagram.senderPort()) + "] req: "\
            + request + ", resp: " + response;
    ui->listWidget->addItem(log);
}

QString MainWindow::process(const QString &request)
{
    // 由于是回显服务器,请求就是响应
    return request;
}

Udp使用的是数据报的形式,所以接收要接收一个数据报对象,这个数据报中有对端发来的数据和其他属性字段。给客户端进行响应的时候,也要响应一个数据报,构建一个数据报对象,再填充数据,使用toUtf8就可以把QString转换成QByteArray。最后再显示到服务器的QListWidget中就可以了。
下面就是客户端的界面了,给客户端设计一个界面。有一个回显框、输入框和发送按钮。

再使用布局管理器修饰一下。

调整一下垂直布局管理器,让下面的发送栏宽一点。

没有变宽就是因为没有调整下面两个控件的sizePolicy,都设置成Expanding就可以了。

这些设置都是可以调整的,可以按照自己喜欢的方式调整。

我们想要实现的功能是现在输入框输入内容,点击发送按钮发送给服务端,所以先写一个按钮的槽函数。

cpp 复制代码
void MainWindow::on_pushButton_clicked()
{
    // 获取输入框的内容
    const QString& text = ui->lineEdit->text();
    ui->lineEdit->setText("");
    
    // 构造 UDP 的请求数据
    QNetworkDatagram requestDatagram(text.toUtf8(), QHostAddress(SERVER_IP), SERVER_PORT);
    
    // 发送请求数据
    socket->writeDatagram(requestDatagram);
    
    // 把发送的请求添加到列表框中
    ui->listWidget->addItem("客户端: " + text);
    
}

现在客户端就有了发送的能力,接下来就要写接收服务端数据的代码了。

cpp 复制代码
// 两个常量描述服务器的地址和端口
const QString& SERVER_IP = "127.0.0.1";
const quint16& SERVER_PORT = 8080;

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建socket对象
    socket = new QUdpSocket(this);

    // 修改标题
    this->setWindowTitle("客户端");

    // 通过信号槽处理服务端返回的数据
    connect(socket, &QUdpSocket::readyRead, this, &MainWindow::processResponse);

}

void MainWindow::processResponse()
{
    // 通过这个槽函数处理收到的响应

    // 读取响应数据
    const QNetworkDatagram& responseDatagram = socket->receiveDatagram();
    const QString& response = responseDatagram.data();

    // 把响应数据显示到界面中
    ui->listWidget->addItem(response);
}

接下来就来看一看效果,让 Qt Creater 编译两个程序。

TCP Socket

核心的类有两个:QTcpServer 和 QTcpSocket。QTcpServer 用于监听端口,获取客户端连接。

名称 类型 说明
listen(const QHostAddress&, quint16 port) 方法 绑定指定的地址和端口号,并开始监听,对标 bind 和 listen。
nextPendingConnection() 方法 从系统中获取到一个已经建立好的TCP连接。 返回一个 QTcpSocket,表示这个客户端的连接。 通过这个socket对象完成和客户端之间的通信。 对标accept。
newConnection 信号 有新的客户端连接建立好后触发。

QTcpSocket 用户客户端额服务端之间的数据交互。

名称 类型 说明
readAll() 方法 读取当前接收缓冲区中的所有数据,返回 QByteArray 对象,对标read。
write(const QByteArray& ) 方法 把数据写入socket,对标write。
deleteLater() 方法 暂时把 socket 对象标记位无效,Qt 会在下个事件循环中析构释放该对象。
readyRead 信号 有数据到达并准备就绪时触发。
disconnected 信号 连接断开时触发。

方法和信号知道了,下面就可以继续编写代码了,客户端和服务端的界面都是不变的,变得是这是一个TCP服务器,除了bind还需要设置成监听状态,使用listen方法就可以完成,只要有新的连接就会触发newConnection信号。

cpp 复制代码
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    void processConnection();

private:
    QTcpServer* tcpServer;
};

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    
    // 修改窗口标题
    this->setWindowTitle("服务器");
    
    // 创建tcpServer
    tcpServer = new QTcpServer(this);
    
    // 一定要先连接信号和槽
    connect(tcpServer, &QTcpServer::newConnection, this, &MainWindow::processConnection);
    
    // bind 并且 listen,这是初始化的最后一步,在这之前一定要做好准备
    bool ret = tcpServer->listen(QHostAddress::Any, 8080);
    if (!ret)
    {
        QMessageBox::critical(this, "服务器启动失败!", tcpServer->errorString());
        exit(1);
    }
}

接下来就是设置好listen状态后,触发了newConnection信号之后执行processConnection的操作。

cpp 复制代码
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    void processConnection();

    QString process(const QString request);
private:
    QTcpServer* tcpServer;
};



void MainWindow::processConnection()
{
    // 通过 tcpServer 拿到一个socket对象,通过这个对象和客户端进行通信
    QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
    QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线"; // peerAddress 表示对端的IP地址
    ui->listWidget->addItem(log);

    // 通过信号和槽处理客户端发来的请求
    connect(clientSocket, &QTcpSocket::readyRead, this, [=](){
        // 读取请求数据
        QString request = clientSocket->readAll();
        // 根据请求处理响应
        const QString& response = process(request);
        // 写回响应数据
        clientSocket->write(response.toUtf8());
        // 显示到界面中
        QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] "\
                + "req: " + request + ", resp: " + response;
        ui->listWidget->addItem(log);
    });

    // 如果客户端断开连接也是要处理的,还是通过信号槽的方式
    connect(clientSocket, &QTcpSocket::disconnected, this, [=](){
       // 显示断开连接的日志
        QString log = "[" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "] 客户端断开连接";
        ui->listWidget->addItem(log);

        // 释放socket
        // delete clientSocket; // 一旦delete就代表clientSocket不能使用了,它一定得是最后一步,但是有可能被return和抛异常跳过
        clientSocket->deleteLater(); // 这个操作就不会立即释放socket,而是在下一轮事件循环中再释放(槽函数都是在事件循环中执行的)。
    });
}

QString MainWindow::process(const QString request)
{
    return request; // 因为是回显服务器,所以直接返回
}

服务端的逻辑就写完了,但是这是TCP服务器,与UDP不同的是,TCP是面向字节流的,并不能确定发过来的就是一个完整的报文,所以还需要一些其他操作,因为是回显服务器,这里也就不写了,如果想要知道如何做可以看网络专栏中的应用层协议篇章,那里讲解了如何序列化和反序列化等操作。
下面就是客户端的代码了,除了要维护连接,编写上和Udp客户端没有太大的差别。

cpp 复制代码
const QString& SERVER_IP = "127.0.0.1";
const quint16& SERVER_PORT = 8080;

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 设置窗口标题
    this->setWindowTitle("客户端");
    
    // 创建socket对象,因为是客户端,只有一个socket对象
    socket = new QTcpSocket(this);

    // 建立与服务端的连接
    socket->connectToHost(SERVER_IP, SERVER_PORT);
    
    // 连接信号槽
    connect(socket, &QTcpSocket::readyRead, this, [=](){
        // 读取服务端的响应
       QString response = socket->readAll();

       // 显示到listWidget中
       ui->listWidget->addItem("服务端: " + response);
    });

    // 阻塞式等待连接建立结果
    if (!socket->waitForConnected())
    {
        QMessageBox::critical(this, "连接服务器失败!", socket->errorString());
        exit(1);
    }
}

void MainWindow::on_pushButton_clicked()
{
    // 获取输入框中的内容
    const QString& text = ui->lineEdit->text();
    ui->lineEdit->setText("");

    // 发送数据
    socket->write(text.toUtf8());

    // 把发送的数据添加到ListWidget
    ui->listWidget->addItem("客户端: " + text);
}

代码写完了,下面就来看一下效果。

HTTP Client

进行 Qt 开发时,和服务器之间的通信很多时候也会用到HTTP协议,通过HTTP向服务器提交数据,或者通过HTTP从服务器获取数据。HTTP相比TCP/UDP还要使用的更多一点,而HTTP协议本质上是基于TCP协议实现的,也就是封装了TcpSocket。

Qt 只是提供了 HTTP客户端,并没有提供服务端

下面是核心API,三个类,分别是QNetworkAccessManager,QNetworkRequest,QNetworkReply。
QNetworkAccessManager 提供了HTTP的核心操作。

方法 说明
get(const QNetworkRequest& ) 发起一个 HTTP GET 请求,返回 QNetworkReply 对象。
post(const QNetworkRequest& , const QByteArray& ) 发起一个 HTTP POST 请求,返回 QNetworkReply 对象。

QNetworkRequest 表示一个 HTTP 请求(不包含请求正文 body),想要发送一个带有body的请求需要再QNetworkAccessManager的post方法中的参数传入body。

方法 说明
QNetworkRequest(const QUrl& ) 通过 URL 构造一个 HTTP 请求。
setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value) 设置请求头。

其中 QNetworkRequest::KnownHeaders 是一个枚举类型,常用取值为:

取值 说明
ContentTypeHeader 描述 body 的类型。
ContentLengthHeader 表述 body 的长度。
LocationHeader 用于重定向报文中指定的重定向地址。
CookieHeader 设置 Cookie
UserAgentHeader 设置 User-Agent

QNetworkReply 表示一个 HTTP响应,这个类同时也是 QIODevice 的子类。QNetworkReply 还有一个重要的信号 finishied,在客户端收到完整的响应数据后触发。

方法 说明
error() 获取出错状态。
errorString() 获取出错原因的文本。
readAll() 读取响应的文本。
header(QNetworkRequest::KnownHeaders header) 读取响应指定 header 的值。

需要使用的API介绍完了,下面就来写一个HTTP客户端,使用的界面与上面的差不多,通过指定一个Url发送请求,响应的结构大概率是一个 HTML,这里使用的是 QPlainTextEdit 来表示。

现在构造函数中设置一下标题,并new一个 QNetworkAccessManager 对象,之后就可以写槽函数了。

cpp 复制代码
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 设置窗口标题
    this->setWindowTitle("客户端");

    manager = new QNetworkAccessManager(this);
}

void MainWindow::on_pushButton_clicked()
{
    // 获取输入框的Url
    QUrl url(ui->lineEdit->text());

    // 构造 HTTP 请求对象
    QNetworkRequest request(url);

    // 发送请求
    QNetworkReply* response = manager->get(request);

    // 因为get本身并不是阻塞函数,它只负责发请求,不负责等待响应,需要编写finishied信号的槽函数
    connect(response, &QNetworkReply::finished, this, [=](){
       if (response->error() == QNetworkReply::NoError)
       {
           // 响应已经获取到
           QString html = response->readAll();
           ui->plainTextEdit->setPlainText(html);
       }
       else
       {
           // 响应出错
           ui->plainTextEdit->setPlainText(response->errorString());
       }
       // 释放Response
       response->deleteLater();
    });
}

代码写完了就可以看一下效果,输入一个Url就会返回一个html格式的文本。


Qt 音视频

Qt 音频

在 Qt 中,音频主要通过 QSound 类来实现。但是需要注意的是 QSound 类只支持播放 wav 格式的音频文件。在这之前也需要先引入 multimedia 模块,最核心的API就是play方法,用来播放音频。

在界面中添加一个按钮,命名为播放,当我们点击按钮,就会播放音乐。首先要有一个wav后缀的文件,像这种文件还是使用qrc来保存。

cpp 复制代码
class MainWindow : public QMainWindow
{
    Q_OBJECT

private slots:
    void on_pushButton_clicked();

private:
    QSound* sound;
};

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    sound = new QSound(":/music/zjl_qingtian.wav", this);
}

void MainWindow::on_pushButton_clicked()
{
    // 在这里进行音频播放
    sound->play();
}

Qt 视频

在 Qt 中,视频播放的功能主要是通过 QMediaPlayer类 和 QVideoWidget类 来实现。在使用这两个类时要添加对应的模块:multimedia 和 multimediawidgets。它也有核心的API:

方法 说明
setMedia() 设置当前媒体源。
setVideoOutput() 将 QVideoWidget 视频输出附加到媒体播放器。 如果媒体播放器已经附加了视频输出,将更换一个新的。

首先我们先定义几个成员变量。

cpp 复制代码
class Widget : public QWidget
{
    Q_OBJECT
public:
    // ...
private:
    Ui::Widget *ui;

    QMediaPlayer *mediaPlayer; // 播放声音
    QVideoWidget *videoWidget; // 显示视频

    //创建两个按钮:选择视频按钮和播放按钮
    QPushButton *chooseBtn, *playBtn;
};

接下来就是设置视频播放窗口的代码。

cpp 复制代码
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    // 对象实例化
    mediaPlayer = new QMediaPlayer(this);
    videoWidget = new QVideoWidget(this);

    // 设置播放窗口
    videoWidget->setMinimumSize(600, 600);

    // 垂直布局
    QVBoxLayout *vbox = new QVBoxLayout();
    this->setLayout(vbox);

    // 实例化按钮
    chooseBtn = new QPushButton("选择视频", this);
    playBtn = new QPushButton(this);

    // 设置图标
    playBtn->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));

    // 创建水平布局
    QHBoxLayout* hbox = new QHBoxLayout();
    hbox->addWidget(chooseBtn);
    hbox->addWidget(playBtn);

    // 添加到垂直布局管理器中
    vbox->addWidget(videoWidget);
    vbox->addLayout(hbox);

    connect(chooseBtn, &QPushButton::clicked, this, [=](){
       // 选择视频,返回视频的路径
       QString url = QFileDialog::getOpenFileName(this, "选择视频");

       // 设置声音
       mediaPlayer->setMedia(QUrl(url));

       // 输出画面
       mediaPlayer->setVideoOutput(videoWidget);

       // 播放
       mediaPlayer->play();

    });
}

之后运行就可以选择播放视频了,但是第一次运行可能出现一些不能播放的问题,这里各位可以自行查找解决的方案,也并不复杂。

相关推荐
小悟空GK13 分钟前
Http介绍
开发语言
DS小龙哥20 分钟前
QT+OpenCV在Android上实现人脸实时检测与目标检测
android·人工智能·qt·opencv·目标检测
502胶水20523 分钟前
腾讯地图异步调用
开发语言·ios·swift
SwBack33 分钟前
【pearcmd】通过pearcmd.php 进行GetShell
android·开发语言·php
Lingoesforstudy33 分钟前
c#中的超时终止
开发语言·笔记·c#
**K43 分钟前
C++ 智能指针使用不当导致内存泄漏问题
开发语言·c++·算法
u0104058361 小时前
如何利用Java Stream API简化集合操作?
java·开发语言
湫兮之风1 小时前
C++:.front()函数作用
开发语言·c++
小老鼠不吃猫1 小时前
力学笃行(四)Qt 线程与信号槽
c++·qt·信息可视化
流星白龙1 小时前
【C语言题目】34.猜凶手
c语言·开发语言