目录
[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。
现在就是万事俱备,只欠运行了。运行后就会看到进出都会触发对应的处理函数。
所以之前的哪那个表白程序还可以让按钮跑的更快。
cppvoid 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来捕获的,通过鼠标点击,我们也可以获取鼠标点击的位置。
cppvoid PushButton::mousePressEvent(QMouseEvent *event) { // 以PushButton左上角为原点 qDebug() << event->x() << " : " << event->y(); // 包含QMouseEvent头文件 // 以整个屏幕左上角为原点 qDebug() << event->globalX() << " : " << event->globalY(); }
这里点击了一个PushButton左上角的位置,获取的位置信息也是不一样的。
还有一点要注意的就是,不管是使用鼠标左键还是右键,甚至按下滚轮也可以触发这个事件。
cppvoid 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来捕获的。
cppvoid 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来实现。
cppvoid 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。
cppPushButton::PushButton(QWidget* parent) :QPushButton(parent) { this->setMouseTracking(true); } void PushButton::mouseMoveEvent(QMouseEvent *event) { qDebug() << event->x() << event->y(); }
滚轮事件
Qt 中滚轮事件是通过QWheelEvent类实现的,而滚轮滑动的距离可以通过delta方法获取。
cppvoid PushButton::wheelEvent(QWheelEvent *event) { qDebug() << event->delta(); }
打印的值为正负120,滚轮向上滚动为+,向下滚动为-。
现在我们可以写一个通过滚轮调节字体大小的。
cppvoid 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。
cppvoid MainWindow::keyPressEvent(QKeyEvent *event) { qDebug() << event->key(); }
按照abcde的顺序按下键盘上的键,输出的窗口就会打印这样的内容,当然也可以试试别的按键,想要判断还可以这样写。
cppvoid 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 | 用于在输入法之间切换 |
cppvoid 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,设置一个每一秒就触发一次的定时器。
cppMainWindow::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中使用过,这里演示一下就可以了。
cppMainWindow::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 是窗口大小改变时触发的事件。
拖动窗口和调整窗口大小就会打印相应的内容。
cppvoid 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这个方法。
现在我们就可以实现一个记事本的功能了。
cppMainWindow::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 获取文件创建时间、修改时间、最近访问时间 我们还是打开保存在桌面的文件,就会获取我们想要的信息。
cppvoid 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方法可以让线程休眠一秒,之后发送我们自定义的信号。
主线程要添加一个新线程对象,连接信号槽并启动线程,只要新线程发出信号,主线程就可以捕捉到,从而达成定时的效果。
cppclass 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(); } }
cppclass 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 的章节我们也详细说过了,这里我们在实现一下。
cppclass 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。
cppclass 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的智能指针。
cppvoid 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服务器就做好了。
cppMainWindow::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就可以了。
这些设置都是可以调整的,可以按照自己喜欢的方式调整。
我们想要实现的功能是现在输入框输入内容,点击发送按钮发送给服务端,所以先写一个按钮的槽函数。
cppvoid 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信号。
cppclass 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的操作。
cppclass 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客户端没有太大的差别。
cppconst 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 对象,之后就可以写槽函数了。
cppMainWindow::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来保存。
cppclass 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 视频输出附加到媒体播放器。 如果媒体播放器已经附加了视频输出,将更换一个新的。 首先我们先定义几个成员变量。
cppclass Widget : public QWidget { Q_OBJECT public: // ... private: Ui::Widget *ui; QMediaPlayer *mediaPlayer; // 播放声音 QVideoWidget *videoWidget; // 显示视频 //创建两个按钮:选择视频按钮和播放按钮 QPushButton *chooseBtn, *playBtn; };
接下来就是设置视频播放窗口的代码。
cppWidget::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(); }); }
之后运行就可以选择播放视频了,但是第一次运行可能出现一些不能播放的问题,这里各位可以自行查找解决的方案,也并不复杂。