本文目录
- 1.Qt事件
- 事件的处理
- 标签事件
- 鼠标事件
- 滚轮事件
- 按键事件
- 定时器事件
- 窗口事件
- 事件派发器
- 2.Qt文件操作
- QFile的基本使用
- 3.Qt多线程
- 使用线程
- 线程锁
- connect的第五个参数
- 条件变量和信号量
- 4.Qt网络编程
- UDP Socket
- TCP Socket
- QTcpServer
- QTcpSocket
- HTTP的编写
- 5.QT多媒体播放
- 音频播放
- 视频播放
前言:
大家好啊,😍😍虽然说QT是一套跨平台的c++开发框架,但是Qt很多技术都是封装系统的API实现的,所以我们就需要去了解哪些是封装的系统的API。😳
1.Qt事件
什么是事件 对于事件着机制,操作系统本身就有这个机制,我们可以理解Qt对操作系统的事件机制进行了封装。那什么是事件呢? 之前我们在学习信号与槽是为了应对用户在进行某些操作之后,对该操作进行处理,通过产生信号调用对应的槽函数。那么所谓的事件也是非常的类似,用户进行的各种操作也会产生事件,我们也可以去对事件进行一些事件处理。可以发现,这两种机制实现的效果基本一样。 由于事件处理比较麻烦,因此Qt进行了进一步的封装,形成了信号槽,通过事件与信号槽,我们都可以完成某种信号或者事件下,对用户的操作进行处理。所以可以认为信号槽的“爸爸”就是事件。 绝大多数情况下我们都是使用信号槽进行交互的,但是面对一些特殊情况,我们还是需要去使用事件(比如没有这种信号然我们去处理这个动作),此时我们就要去重写事件处理函数去去实现特定操作。事件的处理
在Qt,用QEvent表示事件,继承QEvent的有许多事件,如下图:
用户进行某些操作如点击,拖拽,键盘,事件变化就会调用产生的事件处理函数。
信号槽是通过connect绑定对应的事件和信号,但是事件这里是通过重写继承父类的虚函数也就是事件函数来实现的。
我们以鼠标进入与鼠标离去这两个事件为例:
标签事件
第一种方法我们使用designer创建了一个label,之后我们创建了一个label类并继承QLabel,此时我们声明并重写了虚函数,在ui界面中右键提升当前的标签为我们自己的label,此时运行观察情况发现可以进行事件的触发了。
在头文件中以声明
//重写虚函数
void Label::enterEvent(QEnterEvent *event)
{
qDebug()<<"调用进入事件";
(void)event;
}
void Label::leaveEvent(QEvent *event)
{
(void)event;
qDebug()<<"调用离开事件";
}
第二种方法就是直接编码,创建我我们这个label并且进行设置运行。
//在构造中进行标签的创建。
Label * label=new Label(this);
QRect rect=label->geometry();
label->setGeometry(rect.x(),rect.y(),100,100);
label->setStyleSheet("QLabel {color:blue;}");
label->setStyleSheet("QLabel { border: 1px solid black; }");//设置边框
label->setText("这是一个标签");
当我们鼠标进入标签区域或者离开就会掉用
鼠标事件
接下来我们在看看点击事件:
创建一个button为了能重写它的虚函数,老样子我们还是创建一个button类,在里面进行重写,并提升ui中的button。
void button::mousePressEvent(QMouseEvent *event)
{
(void) event;
if(event->button()==Qt::LeftButton)
{
qDebug()<<"左键";
}else if()
{
qDebug()<<"右键";
}
qDebug()<<"用户点击"<<event->x()<<event->y();
}
void button::mouseReleaseEvent(QMouseEvent *event)
{
qDebug()<<"用户释放按钮";
}
除此之外,还有双击事件:
void button::mouseDoubleClickEvent(QMouseEvent *event)
{
qDebug()<<"用户双击"<<event->x()<<event->y();
if(event->button()==Qt::LeftButton)
{
qDebug()<<"左键双击";
}else
{
qDebug()<<"右键双击";
}
}
鼠标也存在移动事件,但是直接写你会法案并不会触发,这是因为鼠标移动不同于点击,这个时间很频繁,移动一下,就会产生大量的移动事件,为了保证程序流畅,Qt默认不会对数表进行追踪,除非你真的需要该功能。
//对需要定位鼠标位置的组件进行设置
this->setMouseTracking(true);
void Widget::mouseMoveEvent(QMouseEvent *event)
{
qDebug()<<"鼠标移动"<<event->x()<<","<<event->y();
}
滚轮事件
void Widget::wheelEvent(QWheelEvent *event)
{
qDebug()<<"滚轮滑动"<<event->angleDelta();
}
往上滑就是(0,120),往下滑就是(0,-120)。
按键事件
对于按键事件,在我们之前学的某些组件(如按钮)就提供有快捷键的设置,它的设计就是按键事件家信号槽实现的。
//按下某个字母,显示它的枚举值
void Widget::keyPressEvent(QKeyEvent *event)
{
int content=event->key();//qt将所有按键都枚举为一个整型
QRect rect=label->geometry();
label->setGeometry(rect.x(),rect.y(),100,100);
label->setStyleSheet("QLabel {color:blue;}");
label->setStyleSheet("QLabel { border: 1px solid black; }");//设置边框
label->setText(QString::number(content));
}
//需要注意的一点是,我们的按键触发是要对应的窗口在激活状态,而不是后台
if((event->key()==Qt::Key_A) && (event->modifiers()==Qt::ControlModifier))
{
qDebug()<<"按下了组合键 a+ctrl";
}
定时器事件
我们之前就了解过有一个组件就叫定时器QTimer,它的内部实现就是基于一个事件QTimerEvent.QObject也提供了一个函数timerEvent周期性触发一些操作。当然该接口还要基于两个函数使用:
QStartTimer与QKillTimer—打开定时器与关闭定时器。这里我们在designer中创建一个LCDnumber配合我们定时器使用。⭐️
//构造函数中
//开启定时器并指定周期
Timerid=this->startTimer(1000);//每过一秒就触发定时器事件
ui->lcdNumber->display(10);//设置初始值
//事件处理
void Widget::timerEvent(QTimerEvent *event)
{
//先判断是哪个定时器触发的
if(event->timerId()!=this->Timerid)
{
//不是就忽略
}else
{
//获取lcdNumber的事件
int val=ui->lcdNumber->value();
if(val<=0)
{
//关闭定时器,参数为timerid,一个定时器的表标识
this->killTimer(this->Timerid);
return;
}
val-=1;//每次过一秒-1
ui->lcdNumber->display(val);//在设置给定时器,表示每过一秒
}
}
可以看到我们自己去搞了一个定时器,通过lcdnumber显示出来。不过实际中,我们还是直接hi用QTimer这个类型进行定时器的设置即可,它内部有定时器处理。我们可以对比看看
//设置LCDNumber
ui->lcdNumber->display(10);//设置初始值
time=new QTimer(this);//创建timea对象
connect(time,&QTimer::timeout,this,&Widget::handle);
time->start(1000);//参数出发定时器为周期,单位ms
int num=ui->lcdNumber->intValue();
if(num<=0)
{
this->time->stop();
return ;
}
ui->lcdNumber->display(num-1);
窗口事件
窗口事件主要有两个窗口移动事件moveEvent,以及窗口ReiszeEvent 窗口大小改变触发事件。⭐️
void Widget::moveEvent(QMoveEvent *event)
{
qDebug()<<"窗口位置发生改变"<<event->pos();
}
void Widget::resizeEvent(QResizeEvent *event)
{
qDebug()<<"窗口大小改变"<<event->size();
}
事件派发器
事件分发器
概述:
在 Qt 中,事件分发器(Event Dispatcher) 是⼀个核⼼概念,⽤于处理 GUI 应⽤程序中的事件。事件分发器负责将事件从⼀个对象传递到另⼀个对象,直到事件被处理或被取消。每个继承⾃ QObject类 或QObject类 本⾝都可以在本类中重写 bool event(QEvent *e) 函数,来实现相关事件的捕获和拦截。
事件分发器⼯作原理:
在 Qt 中,我们发送的事件都是传给了 QObject 对象,更具体点是传给了 QObject 对象的 event() 函数。所有的事件都会进⼊到这个函数⾥⾯,那么我们处理事件就要重写这个 event() 函数。event() 函数本⾝不会去处理事件,⽽是根据 事件类型(type值)调⽤不同的事件处理函数。事件分发器就是⼯作在应⽤程序向下分发事件的过程中。
2.Qt文件操作
早在c语言/c++我们就知晓了基本的文件操作,c语言我们可以通过fopen,fread,fwrite,fclose进行文件打开与读写,在c++中我们通过ofsream.ifstream进行文件打开关闭,使用<< >>进行读写,并且在linux中我们也学了系统接口,open,read,write,read。进行文件读写与打开关闭。而对于Qt,Qt自身也封装了文件的接口方便我们读写,这些操作都是QFile类里的成员。💖
QFile的继承也是分了几层,首先是最底层的IO装置类,这个的类里面的操作就是底层对磁盘的操作,之后又有6大类继承子该类,分别有文件类(作用于我们文件的读写),套接字类(QT对网络套接字也进行了封装,方便我们更简单的进行网络通信),串口类(比较古老的一个类。用于嵌入式通信),蓝牙套接字(顾名思义,用于蓝牙通信的网络编程),QProcess(进程类,可以让我们进行进程操作,如进程替换),最后是缓冲区类(辅助文件操作)。
而对于QFileDevice,有两个子类,QFile就是我们要来读写文件的类,QSaveFile是一个特殊的文件类,应用场景-在我们对旧文件修改,删除内容后编写新文件是出错了,此时也看不到旧文件。但是savefile文件读写会在提供一个缓冲文件,QTempararyFile,继承QFile表示一个临时文件类。
QFile的基本使用
文件打开方式 分别是:未打开,只读,只写。追加,打开清除,文本方式结尾用\n,无缓冲区的方式打开,不存在就创建的方式打开void Widget::on_pushButton_open_clicked()
{
//弹出文件框,用户选择打开哪个文件
QString path=QFileDialog::getOpenFileName(this);//返回路径
//构造QFile对象
QFile file(path);
//只读方式打开文件
if(!file.open(QIODevice::ReadOnly ))
{
qDebug()<<path<<":打开失败";
return ;
}
//读取文件
QString content=file.readAll();//读取文件所有内容 ffile.readLine();//读取一行
//如果是二进制文件不能用QString
//关闭文件
file.close();
//将读到的内容设置到输入框
ui->textEdit->setText(content);
}
void Widget::on_pushButton_save_clicked()
{
//弹出保存到的位置的对话框并返回选择的路径
QString path=QFileDialog::getSaveFileName(this);//返回路径
//根据用户选择的路径构造QFile对象
QFile file(path);
//以写的方式打开文件
if(!file.open(QIODevice::WriteOnly))
{
qDebug()<<path<<":打开失败";
return ;
}
//将内容全写到文件中
const QString content=ui->textEdit->toPlainText();
file.write(content.toUtf8());//在写的时候转化格式
//关闭文件
file.close();
}
当然文件操作不仅仅是读文件写文件,还有其它操作,QFileInfo 是 Qt 提供的⼀个⽤于获取⽂件和⽬录信息的类,如获取⽂件名、⽂件⼤⼩、⽂件修改⽇期等。QFileInfo类中提供了很多的⽅法,常⽤的有如下
isDir() 检查该⽂件是否是⽬录;
• isExecutable() 检查该⽂件是否是可执⾏⽂件;
• fileName() 获得⽂件名;
• completeBaseName() 获取完整的⽂件名;
• suffix() 获取⽂件后缀名;
• completeSuffix() 获取完整的⽂件后缀;
• size() 获取⽂件⼤⼩;
• isFile() 判断是否为⽂件;
• fileTime() 获取⽂件创建时间、修改时间、最近访问时间等;
QString path=QFileDialog::getOpenFileName(this);//返回路径
//根据路径构造QFileinfo对象
QFileInfo info(path);
qDebug()<<info.fileName();//打印文件名
qDebug()<<info.suffix();//打印文件后缀
qDebug()<<info.path();//打印文件路径
qDebug()<<info.size();//打印文件大小
qDebug()<<info.isFile();//是否为普通文件
qDebug()<<info.birthTime().toString();//创建时间
qDebug()<<info.lastModified().toString();//罪行修改时间
qDebug()<<info.lastRead();//最新读的时间
3.Qt多线程
Qt的多线程和linux的多线程本质上是一个东西,Qt对系统的线程进行了封装,这使得即使是windows或者linux一套代码都适用,在Qt中使用的是QHread类,重写类中的run函数-程序入口函数。
但是这种写法很多c++大佬认为,由于堕胎有一个去查虚函数表的过程,者在运行过程中带来了一定的时间消耗,他们会更多的使用回调的方法。
但是对于客户端开发来说,其实这点性能可有可无,QT也并不太追求。😎
run() | 线程的⼊⼝函数 |
---|---|
start() | 通过调⽤ run() 开始执⾏线程。操作系统将根据优先级参数调度线程。如果线程已经在运⾏,这个函数什么也不做 |
currentThread() | 返回⼀个指向管理当前执⾏线程的 QThread的指针。 |
isRunning() | 如果线程正在运⾏则返回true;否则返回false。 |
sleep() / msleep() /usleep() | 使线程休眠,单位为秒 / 毫秒 / 微秒 |
wait() | 阻塞线程,直到满⾜以下任何⼀个条件:与此 QThread 对象关联的线程已经完成执⾏(即当它从run()返回时)。如果线程已经完成,这个函数将返回 true。如果线程尚未启动,它也返true,已经过了⼏毫秒。如果时间是 ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回 false。这提供了与 POSIX pthread_join() 函数类似的功能。 |
terminate() | 终⽌线程的执⾏。线程可以⽴即终⽌,也可以不⽴即终⽌,这取决于操作系统的调度策略。在terminate() 之后使⽤ QThread::wait() 来确保。 |
finished() | 当线程结束时会发出该信号,可以通过该信号来实现线程的清理⼯作。 |
使用线程
这里写一个例子,创建一个线程来进行计时器来计时。首先创建一个新类,然后去继承QThread,之后去重写run方法即可。
在继承的线程类中,我们重写run,进行每1s发送一个信号
void Thread::run()
{
//当然创建了线程,也无法在线程里修改主窗口
//这里我们可以创建一个定时器,每次循环减一秒
for(int i=0;i<10;i++)
{
sleep(1);//成员函数 --休眠一秒
emit notified();
}
}
在主界面我们进行信号的槽的连接,以及接收信号,只要接收信号就去执行:
//构造函数中
//链接信号槽
connect(&_thread,&Thread::notified,this,&Widget::handle);
//启动线程
_thread.start();
//该信号槽每过一秒就会接受送信号执行:
void Widget::handle()
{
//次数刷新界面中lcdnumber的值
int val=ui->lcdNumber->value();
val--;
ui->lcdNumber->display(val);
}
注意在创建类继承之后重构一下项目,否则可能会报错或者没语法提示。通过以上这种方式我们简单的认识了QThread.与服务端不同,服务端主要是利用多线程高效的的处理并发请求,而客户端更加关注的是用户体验,将一些消耗的的代码使用线程来执行,以减少等待时间,例如一些密集的IO操作文件读写等。
线程锁
使用多线程就不得不提到线程安全了,常用的方法就是线程锁了,这里写一个简单的同步例子-让两个线程同时去执行一个循环加一加到5000,第一次没加锁前,结果小于10000,第二次枷锁了后结果正确。😁
void Thread::run()
{
for(int i=0;i<5000;i++)
{
//串行化的进行++,而不是并发加加,这可能会导致结果异常
mutex.lock();
num++;
mutex.unlock();
}
}
//创建两个线程对象
Thread thread1;
Thread thread2;
thread1.start();
thread2.start();
//增加线程等待
thread1.wait();
thread2.wait();//估计是使用了条件变量进行等待,使主线程等待这两个线程运行完
qDebug()<<Thread::num;
对于线程锁,我们在开发过程中为了确保他得到了释放,会使用智能指针来管理锁,让锁在出了作用域的时候自动销毁,C++也提供了智能指针锁,lokc_guard guard(mutex).那么Qt也有换了个名字叫做QMutexLocker,使用该方式管理锁,我们不用进行管理解锁。
void Thread::run()
{
for(int i=0;i<5000;i++)
{
//串行化的进行++,而不是并发加加,这可能会导致结果异常
QMutexLocker mutexlocker(&mutex);
// mutex.lock();
num++;
//mutex.unlock();
}
}
既然有生产消费者模型,那肯定哈有读者模型,Qt也对这种模型的特殊锁进行了封装:
QReadWriteLocker、QReadLocker、QWriteLocker
特点:
QReadWriteLock 是读写锁类,⽤于控制读和写的并发访问。
QReadLocker ⽤于读操作上锁,允许多个线程同时读取共享资源。
QWriteLocker ⽤于写操作上锁,只允许⼀个线程写⼊共享资源。
⽤途:在某些情况下,多个线程可以同时读取共享数据,但只有⼀个线程能够进⾏写操作。读写锁提供了更⾼效的并发访问⽅式。用法与上述智能指针锁一样
connect的第五个参数
1、线程函数内部不允许操作 UI 图形界⾯,⼀般⽤数据处理;
2、connect() 函数第五个参数表⽰的为连接的⽅式,且只有在多线程的时候才意义。connect() 函数第五个参数为 Qt::ConnectionType,⽤于指定信号和槽的连接类型。同时影响信号的传递⽅式和槽函数的执⾏顺序。Qt::ConnectionType 提供了以下五种⽅式:
Qt::AutoConnection | 在 Qt 中,会根据信号和槽函数所在的线程⾃动选择连接类型。如果信号和槽函数在同⼀线程中,那么使⽤ Qt:DirectConnection 类型;如果它们位于不同的线程中,那么使⽤Qt::QueuedConnection 类型。 |
Qt::DirectConnection | 当信号发出时,槽函数会⽴即在同⼀线程中执⾏。这种连接类型适⽤于信号和槽函数在同⼀线程中的情况,可以实现直接的函数调⽤,但需要注意线程安全性。 |
Qt::DirectConnection | 当信号发出时,槽函数会⽴即在同⼀线程中执⾏。这种连接类型适⽤于信号和槽函数在同⼀线程中的情况,可以实现直接的函数调⽤,但需要注意线程安全性。 |
Qt::QueuedConnection | 当信号发出时,槽函数会被插⼊到接收对象所属的线程的事件队列中,等待下⼀次事件循环时执⾏。这种连接类型适⽤于信号和槽函数在不同线程中的情况,可以确保线程安全。 |
Qt::BlockingQueuedConnection | 与 Qt:QueuedConnection 类似,但是发送信号的线程会被阻塞,直到槽函数执⾏完毕,这种连接类型适⽤于需要等待槽函数执⾏完毕再继续的场景,但需要注意可能引起线程死锁的⻛险。 |
Qt::UniqueConnection | 这是⼀个标志,可以使⽤位或与上述任何⼀种连接类型组合使⽤。 |
条件变量和信号量
既然有了锁,那么条件变量也不能少,经典的生产者消费者模型就是基于锁与条件变量的,除了条件变量和锁能面对这种出场景下,灵活的使用信号量则能大大减少负担,效果和条件变量一致。当然linux学习的条件变量与信号量这里都与QT这里的一致,本质也是基于系统封装的。
对于QT中条件变量封装为一个类–QWaitCondition.提供了两个方法wait进行线程等待,wake进行唤醒,还有一个wakeall(唤醒所有)这个两个唤醒作用一致。在这里当然也存在之前学的Linux时的多线程下生产消费模型的虚假唤醒情况,不知大家是否还记得清呢----使用循环进行条件的判断。
对与QT中的信号量也是封装成一个类,叫做QSemaphore,创建信号量对象的时候需要设定初始值QSemaphore semaphore(2).😃
QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled()) //while防止虚假唤醒
{
condition.wait(&mutex); //等待条件满⾜并释放锁
}
//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
😍 信号量的使用
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作
这里都不做详细介绍,大家感兴趣只需要对linux的线程掌握熟透,其他的封装的也都会用了。😭
4.Qt网络编程
😇💘看到这里,我们就来到了网络编程,还是在与linux中学习到soketAPI的一样,只不过Qt对其进行封装。其次,虽然早都有了socket编程,但对于c++,至今都没有封装网络编程的API。由于我们的网络编程主要就是应用层代码的编写,而应用层需要传输层的支持,对于传输层主要就两种通信方式TCP与UDP,由于这两个之间的通信方式差别很大,因此QT提供了两套网络编程的API。
首先在进行QT网络编程时,需要在。proc文件中添加ntework模块:
对于这个.proc文件,其实是QT工程的pro文件,在创建工程时由QTCreater自动创建,我们可以往里面添加内容,增加库文件的声明,包含路径、预处理器定义,生成目录,输出中间目录等等设置,通过这样的方式,在项目创建生成时会快速的生成基本的使用环境,二在你需要使用其他模块时,在修改.proc文件,主要也是因为要对一个项目更好的管理各种库与模块。对这些库,QT也提供了静态库与动态库两种方式。❤️
UDP Socket
在Qt中,对于UDP Socket提供了两个类,QUdpSocket与QNetworkDategram,QUdpSocke就是udp的socketAPI文件。如下:
名称 | 类型 | 说明 | 对应的原生API |
---|---|---|---|
bind(const QHostAddress&,quint16) | ⽅法 | 绑定指定的端⼝号. | bind |
receiveDatagram() | ⽅法 | 返回 QNetworkDatagram . 读取⼀个 UDP 数据报 | recvfrom |
writeDatagram(constQNetworkDatagram&) | ⽅法 | 发送⼀个 UDP 数据报 | sendto |
readyRead | 信号 | 在收到数据并准备就绪后触发 | ⽆ (类似于 IO 多路复⽤的通知机制) |
QNetworkDategram代表一个udp数据包。QT的网络接口都不是阻塞式的,而是利用了信号槽机制,如果有客户按连接服务器并发送数据,即服务端收到了请求,此时就去对应的槽函数里去处理请求。对应的,客户端再发完数据,服务端也会发一个响应信号(响应信息),此时去槽函数中处理详细信息。
先编写服务端,如下是服务端:
#include "widget.h"
#include "ui_widget.h"
#include<QUdpSocket>
#include<QMessageBox>
#include<QNetworkDatagram>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//创建套接字对象
udpserver=new QUdpSocket(this); //作为父元素挂到对象树中不需手动释放了
//设置窗口标题
this->setWindowTitle("服务器");
//服务器链接读信号槽,通过信号槽机制来驱动服务器
connect(udpserver,&QUdpSocket::readyRead,this,&Widget::ProcessRequest);
//绑定端口号,--注意顺寻先链接信号槽,再绑定端口号,注意收到信号就可以请求读取,不然有丢失请求
bool result=udpserver->bind(QHostAddress::Any,8080); //绑定的ip地址可以侦听IPv4,ipv6
if(!result)
{
qDebug()<<"绑定失败";//弹出对话框进行提示
QMessageBox::warning(this,"警告",udpserver->errorString());//这里的错误信息本质就是errno
return ;
}
}
Widget::~Widget()
{
delete ui;
}
//请求处理,主要做三件事读取请求并解析,根据请求计算相应,把请求写回客户端
void Widget::ProcessRequest()
{
//1.读取请求并解析
const QNetworkDatagram& requestdatagram= udpserver->receiveDatagram();//读取之后返回请求的数据报
QString request=requestdatagram.data();//返回的是一个字节数组 byteArray,调用data获取字符串
//2.根据请求计算响应(由于我们这里就是单纯显示一下信息,因此无计算)
const QString& response=this->procees(request);
//3.把响应写回给客户端
QNetworkDatagram responseDatagram= QNetworkDatagram(response.toUtf8(),requestdatagram.senderAddress(),requestdatagram.senderPort());
//通过respone构造响应的数据报,从请求数据报中获取ip地址与端口号,即把响应写回给客户端
udpserver->writeDatagram(responseDatagram);
//打印信息到界面
QString log="["+(requestdatagram).senderAddress().toString()+","+QString::number((requestdatagram).senderPort())+" ]"+"request:"+request+",response:"+response+" .";
ui->listWidget->addItem(log);
}
QString Widget::procees(QString &request)
{
//设计一个服务器,在处理请求计算响应的过程是非常复杂的,不过我们这里不设计负责的业务场景
return request;
}
如下是客户端😭
#include "widget.h"
#include "ui_widget.h"
#include<QNetworkDatagram>
//客户端来连接服务端
static const QString serverip="192.168.1.3";//服务器地址,z这里就是自己连自己,本地环回
const quint16 serverport=8080;//服务器端口号quint6 就是 uint6_t
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
udpclient=new QUdpSocket(this);
//发送完数据之后,还需要服务端会发送响应给客户端,通过信号槽来处理返回的数据
connect(udpclient,&QUdpSocket::readyRead,this,&Widget::processResponse);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
//获取输入框数据
QString content=ui->lineEdit->text();
//构建请求
//将QString转化为字节数组
QNetworkDatagram requestdatagram(content.toUtf8(),QHostAddress(serverip),serverport);
// QNetworkDatagram(const QByteArray &data, const QHostAddress &destinationAddress = QHostAddress(), quint16 port = 0); // implicit
//发送请求给服务端
udpclient->writeDatagram(requestdatagram);//直接写回udpclient ---对应的文件描述符
//发送的时候把发送的请求显示到listwidget上
ui->listWidget->addItem("客户端说:"+content);
}
void Widget::processResponse()
{
//槽函数处理响应
const QNetworkDatagram& UdpResponse=udpclient->receiveDatagram();//获取响应的数据报
QString response=UdpResponse.data();//数据报是字节数组,data转化QString
//把响应数据显示到界面中
ui->listWidget->addItem("服务器说(response):"+response);
}
我们知道udp不可靠,面向数据报,无连接。因此大多数情况我们使用tcp进行传输的,tcp是有连接,可靠的,面向字节流的。
TCP Socket
QT中TCP Socket常用的两个类是QTcpServer和QTcpSocket,其中QTcpServer用于监听端口,获取客户端连接。如下是核心方法:
QTcpServer
名称 | 类型 | 说明 | 原生API |
---|---|---|---|
listen(const QHostAddress&,quint16 port) | ⽅法 | 绑定指定的地址和端⼝号, 并开始监听. | bind 和 listen |
nextPendingConnection() | ⽅法 | 从系统中获取到⼀个已经建⽴好的tcp 连接.返回⼀个 QTcpSocket , 表⽰这个客⼾端的连接.通过这个 socket 对象完成和客⼾端之间的通信. | accept |
newConnection | 信号 | 有新的客⼾端建⽴连接好之后触发. | ⽆ (但是类似于 IO 多路复⽤中的通知机制) |
我们知道在套接字层面,tcp比udp多了两个步骤,一个是绑定知道后要监听套接字,将主动套接字转化为监听套接字,此时就可以接受来自客户端的请求了,具体来说,监听套接字,是服务器作为客户端连接请求的一个端点,它被创建一次,并存在于服务器的整个生命周期。已连接套接字是客户端与服务器之间已经建立起来了的连接的一个端点,服务器每次接受连接请求时都会创建一次已连接套接字,它只存在于服务器为一个客户端服务的过程中。
其二也就是accept,告诉服务端三次握手已经成功,建立连接了,可以进行传输。
这里使用应该是将newconnection与nextpendingconnection结合使用,与我们linux学到的一样,在监听之后,如果请求建立连接,此时再去accept–数据传输。当然这里的默认是阻塞式的传输,我们还可以对标非阻塞式的epoll模型进行非阻塞式传输。
QTcpSocket
QTcpSocket这个类用于客户端与服务端之间的数据交互:
名称 | 类型 | 说明 | 原生API |
---|---|---|---|
readAll() | ⽅法 | 读取当前接收缓冲区中的所有数据返回 QByteArray 对象… | read |
write(const QByteArray& ) | ⽅法 | 数据写⼊ socket 中. | write |
deleteLater | ⽅法 | 暂时把 socket 对象标记为⽆效. Qt会在下个事件循环中析构释放该对象. | ⽆ (但是类似于 “半⾃动化的垃圾回收”) |
readyRead | 信号 | 有数据到达并准备就绪时触发. | ⽆ (但是类似于 IO 多路复⽤中的通知机制) |
disconnected | 信号 | 连接断开时触发 | ⽆ (但是类似于 IO 多路复⽤中的通知机制) |
编写服务端-这里的用例我们跟udp的一样,服务端使用一个listwidget显示收到的请求以及做出的响应:
#include "widget.h"
#include<QMessageBox>
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("TcpServer");
tcpserver=new QTcpServer(this);//创建用来进行通信的socket对象
//信号槽的绑定
connect(tcpserver,&QTcpServer::newConnection,this,&Widget::ProcessConnection);
//绑定并监听,先准备好信号到来的准备工作---信号槽的编写,请求的处理
//最后一步才是绑定
bool result=tcpserver->listen(QHostAddress::Any,8080);
if(!result)
{
QMessageBox::warning(this,"警告-绑定失败",tcpserver->errorString());
exit(1);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::ProcessConnection()
{
//获取连接之后的scoket对象,即对应accept之后连接客户端的端口
QTcpSocket* clientSocket=tcpserver->nextPendingConnection();
//向窗口打印日志,某某客户端上线
QString log="[ "+clientSocket->peerAddress().toString()+QString::number(clientSocket->peerPort())+" ],客户端上线";//这里的peer指的是对等端
//这里的对端指的就是所连接的客户端的端口
ui->listWidget->addItem(log);
//连接建立完毕,此时就要去看是否有收到请求的信号,如果有,就去处理请求,对象为accept之后的对等的端口号
connect(clientSocket,&QTcpSocket::readyRead,this,[=](){
//获取请求
QString request=clientSocket->readAll();//此时的数据流就是字节流
//根据请求处理响应,当前无业务需求直接返回
QString response=process(request);//获取到了响应
//获取到之后写会给客户端
clientSocket->write(response.toUtf8());//转为utf8,就可获取到字节数组
//写回的同时,记录在窗口上
QString log1="[ "+clientSocket->peerAddress().toString()+QString::number(clientSocket->peerPort())+" ]"+",response:"+response;
ui->listWidget->addItem(log1);
});
//上述代码不够严谨,对于回显服务器足够了,但是我们并不能保证在传输过来的请求是不是完整的请求,还是分段的
//更严谨的做法,是提供一块缓冲区,将请求都放在一起,在进行一条条完整的请求的解析------协议的定制
//完成以上工作,之后如果客户端断开连接也需要处理,通过信号槽获取断开联机的信号之后处理
connect(clientSocket,&QTcpSocket::disconnected,this,[=](){
//打印断开连接的日志
QString log="[ "+clientSocket->peerAddress().toString()+QString::number(clientSocket->peerPort())+" ]"+"客户端请求断开连接";
ui->listWidget->addItem(log);
//手动释放socket,这里会为每个客户端维护一个accept之后的socket用于通信,所以如果没释放,将会造成严重的内存泄漏与文件描述符泄露
//delete clientSocket;
clientSocket->deleteLater();
//释放需要考虑两个问题:1.释放一定是在最会一步 2.释放一定会被执行不会被异常,retrurn 跳过
//其次除了直接delete,还有一个接口deletelater ,表示在下一层循环(信号槽处于一个循环)进行释放,通过这种方式,上述的两个问题都不需要考虑了。
});
}
QString Widget::process(QString &request)
{
return request;
}
与编写服务端不同,服务端需要两个类,QTcpServer用来建立连接和销毁链接,而QTcpSocket是用来处理请求的,对于客户端来说,只需要发送请求和处理响应,因此不需要Tcpserver.
#include "widget.h"
#include "ui_widget.h"
#include<QMessageBox>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("客户端");
clientSocket=new QTcpSocket(this);
//向服务器发起连接
clientSocket->connectToHost("127.0.0.1",8080);//建立三次握手,该接口是非阻塞式建立连接
//连接建立成功之后便可以发送请求
//请求发送完,客户端就会收到服务端的响应,因此此时还需要处理响应
connect(clientSocket,&QTcpSocket::readyRead,this,[=](){
//处理响应,先获取响应
QString response=clientSocket->readAll();
//相应内容显示到界面上
ui->listWidget->addItem("服务器响应: "+response);
});
//等待连接建立的结果,确认是否建立成功
bool ret=clientSocket->waitForConnected();//通过该接口,也使得socket阻塞式的等待
if(!ret)
{
//连接建立失败
QMessageBox::warning(this,"警告,连接失败",clientSocket->errorString());
exit(1);
}
//连接建立成功之后便可以发送请求
//请求发送完,客户端就会收到服务端的响应,因此此时还需要处理相应,读取响应
}
Widget::~Widget()
{
delete ui;
}
//点击按钮就是发送请求
void Widget::on_pushButton_clicked()
{
//内容就是数据
QString content=ui->lineEdit->text();
//面向字节流,不需要构建请求,直接写给服务端
clientSocket->write(content.toUtf8());
//回显发送数据给屏幕
ui->listWidget->addItem("客户端发送: "+content);
//清空输入框内容
ui->lineEdit->setText("");
}
😭注意事项,之前我们在linux中编写Tcp服务器时,一个客户端连接的时候是正常的,但是当多个客户端来连接就只有一个客户端生效,因此当时使用的是多线程的方式(创建了一个子线程用来处理连接之后的进行数据传输的操作),但是我们当前编写的这个,多个客户端都可以访问。首先tcp服务器这里并不是要靠多线程才能达到多台连接,至于为什么linux哪里会出现问题,这是因为在linux的时候,由于我们是通过循环来处理连接和请求的,写的是双重循环,里层循环还没结束,外层循环就不能快速的第二次调用accept处理其他请求,多线程是的两个循环独立,因此可以多个访问,而qt这里使用的是信号槽机制,很好的解决了这个问题。
HTTP的编写
相对于传输层的TCP和UDP,使用这两个通信,还需要我们自定格式再加上协议才能较好的应用层上进行通信,但是我们也可以使用现成的应用层协议–http,不需要我们自己封装,底层基于TCP实现,相比之下http的使用是更为广泛的。
由于QT主要使用看来编写客户端的,因此QT也只提供了客户端的组件,是没有服务器的库。
这里我们对QT的http客户端编写也大概做一下简单的实现。
首先http是有两种请求的,一种get请求,一种post请求,请求报文的格式我们也在linux中介绍过,请求头+请求行+(空行)+正文,对应的响应报文-响应头加响应行+响应报文。
关键类主要是三个. QNetworkAccessManager , QNetworkRequest , QNetworkReply .😭
QNetworkAccessManager
方法 | 说明 |
---|---|
get(const QNetworkRequest& ) | 发起⼀个 HTTP GET 请求. 返回 QNetworkReply 对象 |
post(const QNetworkRequest& , constQByteArray& ) | 发起⼀个 HTTP POST 请求. 返回 QNetworkReply 对象 |
这里的返回值是一个QNetworkReply对象,但不含 body(正文),该对象就表示了一个类的请求,如果想添加body,在 QNetworkAccessManager 的 post ⽅法中通过单独的参数来传⼊ body。
QNetworkRequest
方法 | 说明 |
---|---|
QNetworkRequest(const QUrl& ) | 通过 URL 构造⼀个 HTTP 请求 |
setHeader(QNetworkRequest::KnownHeadersheader,const QVariant &value) | 设置请求头 |
其中的 QNetworkRequest::KnownHeaders 是⼀个枚举类型, 常⽤取值:
方法 | 说明 |
---|---|
ContentTypeHeader | 描述 body 的类型. |
ContentLengthHeader | 描述 body 的⻓度. |
LocationHeader | ⽤于重定向报⽂中指定重定向地址. (响应中使⽤, 请求⽤不到) |
CookieHeader | 设置 cookie |
UserAgentHeader | 设置 User-Agen |
QNetworkReply 表⽰⼀个 HTTP 响应. 这个类同时也是 QIODevice 的⼦类.
方法 | 说明 |
---|---|
error() | 获取出错状态. |
errorString() | 获取出错原因的⽂本. |
readAll() | 读取响应 body |
header(QNetworkRequest::KnownHeadersheader) | 读取响应指定的header的值 |
接下来简单的编写一个客户端来看看,首先还是弄一个和之前tcp一样的窗口,注意这里的texEdit与plainEdit文本不一样,textEidt是在顶html会对内容进行渲染,而plianEdit则是纯文本输入框。
我们这里就简单发送一个的访问一下百度主页url
5.QT多媒体播放
无论是音频播放,还是视频播放,都需要在proc文件中引入新的库:
QT +=multimedia
QT +=multimediawidgets
音频播放
在 Qt 中,⾳频主要是通过 QSound 类来实现。但是需要注意的是 QSound 类只⽀持播放 wav 格式的⾳频⽂件。也就是说如果想要添加⾳频效果,那么⾸先需要将 ⾮wav格式 的⾳频⽂件转换为 wav 格式。这个类的用法也很简单,主要通过方法play播放声音:
注意:qt6中已经没有该类,而是使用QSoundEffect.
#include <QSoundEffect>
QSoundEffect* startSound = new QSoundEffect(this);
startSound->setSource(QUrl::fromLocalFile(":/music.wav"));
startSound->play();
以上方式是比较简单的一种方式,除此之外还有另一种方式,支持ogg,mp3格式的音频播放:
先包含这两个库
QT +=multimedia
#include <QMediaPlayer>
#include <QUrl>
#include<QVideoWidget>
QMediaPlayer *music;
QAudioOutput *output;//头文件申明
//设置播放
//创建音频对象
music=new QMediaPlayer(this);//播放器用来播放音乐
music->setSource(QUrl("qrc:/kun.ogg"));//设置资源路径
output=new QAudioOutput(this);//用来控制音乐输出
music->setAudioOutput(output);//将输出设置进播放器
void Widget::on_pushButton_clicked()
{
QIcon start(QPixmap(":/start.png"));//音乐播放图标
QIcon stop(QPixmap(":/stop1.png"));//音乐暂停图标
if(music->playbackState()==QMediaPlayer::PausedState ||music->playbackState()==QMediaPlayer::StoppedState) //判断是否为暂停或者是终止状态
{
music->play();//播放
ui->pushButton_godess->setIcon(start);
ui->pushButton_godess->setIconSize(QSize(50,50));
}else if(music->playbackState()==QMediaPlayer::PlayingState)
{
music->pause();
ui->pushButton_godess->setIcon(stop);
}
}
感觉这种方式貌似更好一点,这是只播放一个音乐的写法,如果是多个播放的媒体,可以使用QMediaPlayList将需要播放的媒体添加到list中:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 创建一个 QMediaPlayer 实例
QMediaPlayer *player = new QMediaPlayer;
// 创建一个 QMediaPlaylist 实例
QMediaPlaylist *playlist = new QMediaPlaylist(player);
// 将媒体添加到播放列表中
playlist->addMedia(QUrl::fromLocalFile("/path/to/your/audiofile1.mp3"));
playlist->addMedia(QUrl::fromLocalFile("/path/to/your/audiofile2.wav"));
playlist->addMedia(QUrl::fromLocalFile("/path/to/your/audiofile3.ogg"));
// 设置 QMediaPlayer 使用播放列表
player->setPlaylist(playlist);
// 连接信号和槽以处理播放状态变化
QObject::connect(player, &QMediaPlayer::mediaStatusChanged,
[](QMediaPlayer::MediaStatus status) {
qDebug() << "Media status changed to" << status;
if (status == QMediaPlayer::InvalidMedia) {
qDebug() << "Invalid media source!";
}
});
// 开始播放播放列表中的第一个媒体
player->play();
// 应用程序将一直运行,直到你停止它
return a.exec();
}
视频播放
音乐和视频播放基本一致,不同的是除了音乐要设置媒体播放器,设置输出设备,视屏播放器多加了一个QVideoWidget来显示视频播放的窗口。😄
ui->setupUi(this);
player=new QMediaPlayer(this);
widgetplayer=new QVideoWidget(this);
output=new QAudioOutput();//设置媒体播放
output->setVolume(0.5);
//设置声音
player->setAudioOutput(output);
//设置播放画面的最小窗口
this->widgetplayer->setMinimumSize(400,400);//大小默认为窗口的大小
this->widgetplayer->setGeometry(0,0,400,200);
//设置媒体源,就是选择的文件
this->player->setSource(QUrl("qrc:/test.mp4"));
//输出视频画面
this->player->setVideoOutput(this->widgetplayer);
this->player->play();