目录
概述
事件
鼠标事件
进入、离开事件
按下事件
释放事件
双击事件
移动事件
滚轮事件
按键事件
单个按键
组合按键
定时器
QTimerEvent
QTimer
窗口事件
文件
输入输出设备
文件读写类
文件和目录信息类
多线程
常用API
线程安全
互斥锁
条件变量
信号量
网络
UDP Socket
TCP Socket
HTTP Client
Qt 音视频
Qt 音频
Qt 视频
概述
Qt 是一个跨平台的C++开发框架,我们要知道的是 Qt 中许多的功能都是操作系统提供的,Qt 封装了系统调用,那么这一篇就来看一看系统为我们支持了什么吧。
事件
在上上篇中介绍的信号和槽中,用户进行的操作可能会产生某种信号,给一个信号连接上槽函数,当信号触发的时候,就可以执行对应的槽函数,进而完成各种功能。
除了信号,用户进行的操作也会产生事件,我们也可以给事件关联上处理函数,当事件触发的时候,就可以执行对应的代码。
这么一看,事件和信号还是差不多的,事件本身是操作系统提供的机制,Qt 把操作系统的事件机制进行封装,但是事件的代码编写起来不是很方便,所以 Qt 对于事件进一步封装,这就是信号和槽,事件就是它的底层机制。
实际 Qt 开发过程中,多数的交互功能都是通过信号和槽来完成的,但是也有特殊的情况,信号和槽无法实现,就比如 Qt 中没有这个信号,这就需要重写事件处理函数,来手动处理事件的逻辑。
所有的 Qt 事件均继承了抽象类 QEvent。当用户进行一些操作就会触发事件,常见的 Qt 事件如下:
事件名称 描述 鼠标事件 鼠标左键,右键,滚轮,移动,按下和松开 键盘事件 按键类型,按键按下,按键松开 定时器事件 定时到达 进入、离开事件 鼠标的进入和离开 滚轮事件 鼠标滚轮滚动 绘屏事件 重绘屏幕的某些部件 显示隐藏事件 窗口的显示和隐藏 移动事件 窗口位置的变化 窗口事件 是否为当前窗口 大小改变事件 窗口大小改变 焦点事件 键盘焦点移动 拖拽事件 用鼠标进行拖拽
鼠标事件
进入、离开事件
之前信号和槽是通过connect来关联的,对于事件的处理方式为让当前的类重写某个事件的处理函数,使用的是多态的机制,子类重写父类的事件处理函数,事件触发过程中就可以子类调用子类,父类调用父类。
为了理解一下这个事件就写一个简单的程序,创建一个按钮,当鼠标进入和离开这个控件就会触发事件。处理事件要再创建一个类,这个类中重写对应的事件处理函数。类创建好之后,给构造函数中添加一个父元素参数,这个类已经继承了QPushButton,重写两个事件函数就可以了。
之后就可以在图形化界面中拖拽一个QPushButton,但是还有一个问题就是这个控件不是我们自己创建的PushButton,只有PushButton才可以触发我们自己写的这个事件处理函数。
这里就需要使用这个功能,输入要提升的类名称,头文件会自己填充,一定要注意不要拼错。
之后这里的类名就变成了我们定义的PushButton。
现在就是万事俱备,只欠运行了。运行后就会看到进出都会触发对应的处理函数。
所以之前的哪那个表白程序还可以让按钮跑的更快。
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来捕获的,通过鼠标点击,我们也可以获取鼠标点击的位置。
void PushButton::mousePressEvent(QMouseEvent *event) { // 以PushButton左上角为原点 qDebug() << event->x() << " : " << event->y(); // 包含QMouseEvent头文件 // 以整个屏幕左上角为原点 qDebug() << event->globalX() << " : " << event->globalY(); }
这里点击了一个PushButton左上角的位置,获取的位置信息也是不一样的。
还有一点要注意的就是,不管是使用鼠标左键还是右键,甚至按下滚轮也可以触发这个事件。
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来捕获的。
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来实现。
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。
PushButton::PushButton(QWidget* parent) :QPushButton(parent) { this->setMouseTracking(true); } void PushButton::mouseMoveEvent(QMouseEvent *event) { qDebug() << event->x() << event->y(); }
滚轮事件
Qt 中滚轮事件是通过QWheelEvent类实现的,而滚轮滑动的距离可以通过delta方法获取。
void PushButton::wheelEvent(QWheelEvent *event) { qDebug() << event->delta(); }
打印的值为正负120,滚轮向上滚动为+,向下滚动为-。
现在我们可以写一个通过滚轮调节字体大小的。
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。
void MainWindow::keyPressEvent(QKeyEvent *event) { qDebug() << event->key(); }
按照abcde的顺序按下键盘上的键,输出的窗口就会打印这样的内容,当然也可以试试别的按键,想要判断还可以这样写。
void MainWindow::keyPressEvent(QKeyEvent *event) { qDebug() << event->key(); if (event->key() == Qt::Key_A) qDebug() << "按下了a键"; }
还有一点要注意的是,我们这里直接在QMainWindow中重写了这个事件函数,也可以在QWidget中重写这个事件函数,这里就要注意了,想要触发这个事件,一定要让该控件获取焦点,也就是说焦点不在,是触发不了事件的,什么是焦点,那就是要选中这个控件。
组合按键
想要使用组合键就要通过modifiers来获取。Qt::KeyboardModifier 中定义了在处理键盘事件是对应的修改键。
Qt::NoModifier无修改键 Qt::ShiftModifierShift 键 Qt::ControlModifierCrtl 键 Qt::AltModifierAlt 键 Qt::MetaModifierMeta 键(Windows上指Win键, macOS上指Command键) Qt::KeypadModifier使用数字键盘进行输入时,Num Lock键处于打开状态 Qt::GroupSwitchModifier用于在输入法之间切换
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,设置一个每一秒就触发一次的定时器。
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中使用过,这里演示一下就可以了。
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 是窗口大小改变时触发的事件。
拖动窗口和调整窗口大小就会打印相应的内容。
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这个方法。
现在我们就可以实现一个记事本的功能了。
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 获取文件创建时间、修改时间、最近访问时间
我们还是打开保存在桌面的文件,就会获取我们想要的信息。
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方法可以让线程休眠一秒,之后发送我们自定义的信号。
主线程要添加一个新线程对象,连接信号槽并启动线程,只要新线程发出信号,主线程就可以捕捉到,从而达成定时的效果。
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(); } }
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 的章节我们也详细说过了,这里我们在实现一下。
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。
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的智能指针。
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服务器就做好了。
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; } }
之后就是服务器的核心逻辑。
// 完成服务器的核心逻辑 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就可以了。
这些设置都是可以调整的,可以按照自己喜欢的方式调整。
我们想要实现的功能是现在输入框输入内容,点击发送按钮发送给服务端,所以先写一个按钮的槽函数。
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); }
现在客户端就有了发送的能力,接下来就要写接收服务端数据的代码了。
// 两个常量描述服务器的地址和端口 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信号。
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的操作。
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客户端没有太大的差别。
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 对象,之后就可以写槽函数了。
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来保存。
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 视频输出附加到媒体播放器。
如果媒体播放器已经附加了视频输出,将更换一个新的。
首先我们先定义几个成员变量。
class Widget : public QWidget { Q_OBJECT public: // ... private: Ui::Widget *ui; QMediaPlayer *mediaPlayer; // 播放声音 QVideoWidget *videoWidget; // 显示视频 //创建两个按钮:选择视频按钮和播放按钮 QPushButton *chooseBtn, *playBtn; };
接下来就是设置视频播放窗口的代码。
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(); }); }
之后运行就可以选择播放视频了,但是第一次运行可能出现一些不能播放的问题,这里各位可以自行查找解决的方案,也并不复杂。