1.事件的基本概念
前面我们学过信号槽,用户进行的各种操作,就可能会产生信号。进而可以给某个信号指定槽函数,当信号触发时,就能够自动执行到对应的槽函数。事件非常类似,用户进行的各种操作,也会产生事件,程序员同样可以给事件关联上处理函数(处理的逻辑),当事件触发的时候,就能够执行到对应的代码。事件本身是操作系统提供的机制,Qt也同样把操作系统的事件机制进行了封装,拿到了Qt中。但是由于事件对应的代码编写起来不是很方便,Qt对于事件机制又进行了进一步的封装,就得到了信号槽。因此信号槽就是对于事件的进一步封装,事件是信号槽的底层机制。实际Qt开发程序过程中,绝大部分和用户之间进行的交互都是通过信号槽来完成的。有些特殊情况下,信号槽不一定能搞定(某个用户的动作行为,Qt没有提供对应的信号),此时就需要提供重写事件处理函数的形式,来手动处理事件的响应逻辑。开发事件机制给咱们程序员,咱们就可以根据实际的需要进行更深度的定制化diy操作了。下面是一些开发中典型的事件:
Qt中使用QEvent类去表示事件这样的概念,QEvent又派生出很多子类,代表各种具体的事件。
1.1.处理事件
处理是让一段代码和某个事件关联起来,当事件触发的时候,就能指定到这段代码。之前信号槽这里通过connect来完成上述关联的,对于事件来说还不一样。事件是让当前的某个类,重写某个事件处理函数。这里用到是多态机制,创建子类,继承自Qt已有的类,在子类中重写父类的事件处理函数,后续事件触发过程中,就会通过多态这样的机制,执行到咱们自己写的子类的函数中。
下面演示一下,重写处理一下鼠标进入和鼠标离开(鼠标移进出/鼠标移出来)。我们在界面放置一个label,当鼠标进入和离开的时候分别有提示。先编辑一下ui:
下面创建一个QLabel的子类,在子类里重写enterEvent和leaveEvent:
继续:
类名随便取,选择好继承自哪个类以及要包的头文件。我们创建的类也要有个父控件,这里调整一下:
下面给创建类添加要重写的函数,先完成声明:
要想重写父类的函数,就需要确保你这边写的函数的名字和函数的参数列表都完全一致(形参名无所谓)。继续完善:
此时鼠标进入和出去并没有反应。上述代码虽然重写了这俩函数,但是还有点问题:
当前在界面上创建的这个label,其实是一个QLabel,不是咱们自己写的Label,必须要确保界面上的这个label是一个咱们自己定义的Label类的实例,才会执行到。但是拖的控件怎么变成定义的呢?这有这样的操作:
点击看看:
一定要确保这的类名以及头文件的名字和上述自定义的类名头文件啥的都匹配。输入正确后点击添加,然后点击提示就行。此时再来观察:
此时的类型变成了Label类型。通过“提升为”这样的方式,就可以把QtDesigner中托上去的控件的类型转换成自定义的控件类型。下面运行看看:
此时说明当前的enterEvent和leaveEvent这两个事件就被咱们给捕获到了。
1.2处理鼠标事件
再写个例子,通过事件获取到鼠标点击的位置。先编辑ui:
下面创建类继承自QLabel,并且提升为我们自定义的label。下面看看:
下面进行重写:
当前点击区域中坐标原点就是在QLabel的左上角。还可以这样获取:
此时点左上角位置变化了,因为这里是以整个屏幕左上角为原点来获取的坐标。这里我们按鼠标左键、右键、滚轮都是可以触发mousePressEvent事件的。想区分按什么键可以这样:
介绍完鼠标按下事件,再看看鼠标释放事件,释放是通过mouseRealseEvent事件来去处理的。下面来写一下:
像clicked这样的信号,就相当于是一次鼠标按下事件和一次鼠标释放事件。
Qt还提供了鼠标双击事件,可以通过重写mouseDoubleClickEvent函数来获取到双击的动作。下面演示一下:
发现是第二次按下的时候,才能够识别到是双击。这里有时候要注意,比如有的程序,可能是单机有一些逻辑,双击有另一些逻辑。如果我们没注意,可能双击操作就能触发单机的逻辑,可能就有bug。
鼠标移动也是可以触发事件的,下面看看。刚才重写鼠标事件的操作,都是在自定义的Label中完成的,此时鼠标只有在Label范围内进行动作的时候才能捕获到。我们也可以把这些操作直接放到Widget(QWIdget子类)来完成,这样的话鼠标在整个窗口中进行的各种动作都能获取到了。下面来看看:
程序运行起来后鼠标放上面移动发现并没有效果。下面说说,鼠标移动不同于鼠标按下,随便移动一下鼠标,就会产生大量的鼠标移动事件。当你进行捕获事件的时候,尤其是在这里再进行一些复杂逻辑的时候,程序负担就会很重,就容易产生卡顿之类的情况。Qt为了保证程序的流畅性,默认情况下不会对鼠标移动进行追踪。鼠标移动的时候不会调用mouseMoveEvent,除非显示告诉Qt就要追踪鼠标位置。下面看看:
鼠标滚轮的滚动动作,我们也是可以把它捕获到的,通过wheelEvent函数,它的参数QWheelEvent中的delta可以获取到这次事件鼠标滚轮滚动了多远。下面看看:
这个单位是像素。要想知道滚动的总距离,可以delta进行累加:
1.3.处理键盘事件
要想获取到用户的键盘按键,以前用过QShortCut,这是信号槽机制封装过的获取键盘的方式;站在更底层的角度,也可以通过事件获取到当前用户键盘按下的情况。我们通过重写keyPressEvent函数完成,下面演示一下:
我们按下abc出现了这样的数字,这是Qt把键盘的每个按键定义了一个数值。我们可以这样判断:
有时候可能按下的是组合键,Qt中把这些用来搭配组合键的功能单独拎出来了,modifiers(修饰符)。下面看看比如如何获取到ctl+a这样的操作:
1.4定时器事件
下面谈谈Qt中的定时器事件:前面用QTimer类实现了定时器的功能。在QTimer背后,是QTimerEvent定时器事件进行支撑的。在QObject里提供了一个timeEvent这个函数,可以通过定时器周期性的触发一些相关的操作。还要搭配两个函数使用,startTimer是启动定时器,killTimer是关闭定时器。
下面通过代码感受一下,实现一个倒计时,先编辑一下ui文件:
下面编写代码:
可以发现,使用timerEvent比QTimer还是要更复杂一些,还要手动管理timerid,还要区分这次函数调用是哪个timer引起的。后续实际开发中,使用QTimer即可。
1.5.窗口移动和大小改变事件
接下来介绍和窗口有关的事件,一个是moveEvent,这是窗口移动触发的事件;一个是resizeEvent,这是窗口大小改变时触发的事件。moveEvent的参数是QMoveEvent,它里面提供了oldPos和pos方法,可以知道移动前后的位置。resizeEvent的参数是QResizeEvent,里面提供了oldSize和size,可以知道大小变化前后的数值。下面通过代码感受一下:
2.Qt文件--QFile类的介绍
c语言中,我们学过fopen打开文件,fread fwrite读写文件,fclose关闭文件。c++中,我们用fstream打开文件,<< >> 读写文件,close关闭文件。Linux中,open打开文件,read write读写文件,close关闭文件。Qt中也提供了一套文件操作,但Qt中使用上述的几种方案来读写文件也是完全可以的(Linux这一套局限于Linux系统,Windows上的Qt就需要使用Windows的api)。我们在编写Qt程序的时候,更推荐使用Qt自己提供的一套文件操作,因为可以和QString等Qt内置的类可以很好的配合。Qt中的文件操作,核心操作也是打开,读写,关闭这几个部分,它提供了QFile类来完成上述的操作。这是它的一个相关的继承关系:
接下来看看QFile类的基本使用,对于文件操作有:1.打开 2.读 3.写 4.关闭,我们可以用QFile类提供的成员函数来完成相应的功能。看一下文档:
构建对象时参数这里可以使用绝对路径,也可以使用相对路径。打开有open操作:
这两个需要搭配c语言或系统原生的api来进行打开,这样有点麻烦。我们更常用继承来的:
构造函数中指定了路径之后,此时直接通过这个版本的open打开即可。这里的mode是打开方式,有读方式、写方式、追加写方式,可以看到它提供的枚举类型:
读有read操作:
可以通过参数或返回值的方式返回读到的数据。还有readAll,读所有数据,返回的也是字节数组。还有readLine,read是尽可能的把缓冲区读满,randLine是读到一行就结束了。还有QByteArray可以很方便的转成QString,可以把它当作是字符数组。写有write操作:
关闭有close操作:
关闭本质上是释放文件描述符表项,文件描述符表会存在上限,如果一直打开,不关闭的话,就可能会使文件描述符表被占满,后续再尝试打开,就打开不了了。
2.1QFile类的使用
接下来写例子演示一下QFile类的基本用法,我们创建一个窗口,窗口里添加一个多行编辑框,再给菜单里添加菜单。菜单中指定两个菜单项,打开和保存。接下来完成:
接下来完成菜单项目中涉及到的功能:
再来写保存:
2.2QFileInfo的使用
接下来介绍一下Qt中其它的和文件相关的操作,文件操作不仅仅是读写文件,还有获取文件属性这样的操作,Qt中提供了QFileInfo这样的类来去完成的,QFielInfo可以获取到Qt的文件的相关属性。下面通过代码来了解一下基本的用法:
3.Qt多线程
Qt多线程和Linux中的线程本质是一个东西,Linux谈到的各种和线程相关的原理和注意事项都是在Qt中适用的。Linux中学过多线程api,这是依赖Linux系统提供的pthread库。Qt中针对系统提供的线程api重新封装了,c++11中也引入了线程std::thread。Qt的api设计的时候,博采众家之长。Linux原生的多线程api,设计的非常差,使用起来非常不方便的(也是c语言本身的局限性引起的),实际开发中很少会用原生api。std::thread要比Linux的api要好一些。Qt中的多线程api,还要更好一点,其实参考了Java中的线程库api的设计方式。Qt中提供QThread类去表示线程,要想创建线程,就需要创建出这样类的实例。创建线程的时候,需要重点指定线程的入口函数。它是创建一个QThread的子类,重写其中的run函数,起到指定入口函数的方式(多态)。这是相关api:
其中start这个操作就是真正调用系统api创建线程,新的线程创建出来之后自然就会自动的执行run函数。可以使用wait,让一个线程等待另一个线程结束。
3.1QThread的使用
我们之前基于定时器,写过倒计时这样的程序。我们可以通过线程,来完成类似的功能,定时器内部本质上也是可以基于多线程实现的。我们创建另一个线程,新线程中,进行计时(搞一个循环,每循环一次,sleep 1s,sleep完成就可以更新界面了)。先编辑一下ui文件:
创建好新的类:
由于存在线程安全问题,多个线程同时对于界面的状态进行修改,此时就会导致界面出错了。Qt选择了一刀切,针对界面的控制状态进行任何修改,务必在主线程中执行。继续看线程:
下面回到主线程:
我们先在构造函数中启动线程,然后进入到线程的入口函数执行for循环,每sleep1s就触发一个notify信号,然后信号触发执行到主函数的槽函数。
3.2Qt中的锁
使用多线程的时候我们要关心线程安全问题,主要因为多线程程序太复杂了。加锁是把多个线程要访问的公共资源,通过锁保护起来,把并发执行变成串行执行。Linux提供mutex互斥量作为锁,c++11中引入std::mutex,Qt同样也提供了对应的锁,来针对系统提供的锁进行封装。Qt中提供了QMutex类,其中一个是lock进行加锁,另一个是unock进行解锁。
下面通过代码来演示一下,我们弄两个线程,针对同一个变量进行一个循环增加:
理论上num会变成10w,但由于三个线程之间是并发执行的关系,当t1和t2运行起来之后,主线程仍然会继续往后1执行,执行到打印的时候,大概率t1 t2还没有执行完。所以需要有等待:
现在看一下结果:
很明显这个值不是10w,说明是存在线程安全问题的。现在这样:
当前num是两个线程访问的公共访问的变量,之前如果是并发执行,就可能第一个线程修改了一半,第二个线程也进行修改,就容易出现问题。加了锁之后第一个线程顺利拿到锁,继续执行++,在第一个线程没执行完的时候,第二个线程也尝试加锁,就会阻塞等待。一直等到第一个线程释放锁,第二个线程才能从阻塞中被唤醒。此时再次执行程序:
现在有个问题:
这里的锁很容易忘记释放,忘记unlock。实际开发中,加锁之后,涉及到的逻辑可能很复杂,下面就很容易释放锁。c++11引入了std::lock_guard,相当于std::mutex的指针指针,也是借助RALL机制。类似于这样:
上述方法Qt也参考来了,它提供的是QMutexLocker。下面看看:
3.3条件变量和信号量
除了上述说的QMutex锁以外,Qt还提供了另外两个很重要的机制,也是和多线程开发密切相关的。一个叫做条件变量,另一个叫做信号量,和Linux中谈到的完全一致。我们使用多线程开发程序的时候,由于多个线程之间的调度是无序的,为了能够一定程度的干预线程之间的执行顺序,所以引入了条件变量。Qt中封装成为了QWaitCondition类,一个是wait接口,另一个是wake或wakeAll接口。这里谈到的信号量,其实还可以进行进程之间的控制;当然也同样可以作为同一个进程内部的线程之间的通信方式。Qt中提供了QSemaphore来表示信号量,参数设置资源数量。提供的acquire是获取信号量,release是释放信号量(主要结合linux中的内容来了解)。
4.网络编程概述
网络编程,靠的是操作系统提供的一组APi(socket api)。Qt中进行了封装,我们就可以采用Qt的那一套了。c++标准库中,并没有提供网络编程的api的封装。进行网络编程的时候,本质上是在编写应用层代码,需要传输层提供支持。传输层最核心的协议,有UDP和TCP,并且这两协议差别还很大。所以Qt提供了两套api,分别去完成udp和tcp的开发。使用Qt网络编程的api,需要先在.pro文件中添加netwwork模块。之前学过的Qt中的各种控件,各种内容,都是包含在QtCore模块中的,它是默认就添加的。为啥Qt要划分出这些模块呢》Qt本身是一个非常庞大,包罗万象的框架。如果把所有的Qt的功能都放到一起,即使咱们就只是写一个简单的hello world,此时生成的可执行程序也会非常庞大(这里就包含了大量其实没有使用的功能)。所以模块化处理,除了QtCore,其它的功能分别封装成不同的模块。默认情况下这些额外的模块不会参与编译,需要在.pro文件中引入对应的模块,才能把对应的功能给编译加载进来。
4.1UDP回显服务器
Qt中针对udp socket提供了两个类,一个是QUdpSocket,还有一个是QNetworkDatagram。QUdpSocket表示一个udp的socket文件:
当我们的socket收到请求的时候,QUdpSocket就会触发这个信号,此时就可以在槽函数里完成读取请求的操作了。基于信号槽,就天然达成了“事件驱动”这样的一种网络编程的方式。QNetworkDatagram表示一个udp的数据包:
下面基于udp socket写一个带有界面的udp回显服务器(ui上放一个ListWidget来显示多个消息),做好准备工作:
继续看:
这里先连接信号槽,后绑定端口号。一旦绑定端口号了,意味着请求就可以被收到了。如果在完成绑定之后,在连接信号槽之前,有客户端把请求发过来了,此时就可能读不到这样的请求。继续完善:
现在大逻辑完成,最后还是要显示的:
4.2UDP回显客户端
下面编写UDP客户端,我们先创建好项目:
Qt Creator中是可以同时打开多个项目的,但如果两个项目中存在同名的文件就非常容易混淆(此处为了后续方便演示才这样做)。下面继续来写,还是先编辑.pro文件:
此时写的客户端,要能够主动给服务器发起请求,然后把服务器返回的内容显示出来(也可以显示已经发送的内容),下面编辑ui(下面两个水平布局,再和上面的垂直布局):
下面编写代码逻辑:
这样就把点击按钮发送的槽函数编写完毕了。作为客户端还要接收服务器返回的响应,响应数据也是通过信号槽的方式来去处理的:
客户端服务器程序测试时候的基本原则,一定是先启动服务器,后启动客户端。下面运行一下(也可以通过找到.exe程序启动多个客户端):
现在有个问题,之前学linux网络编程的时候,当时是使用云服务器部属服务器程序,其它的人也能连上。那能否把现在的Udp服务器放到云服务器上呢?大概率不行,这个取决于服务器是否安装了图形化界面。qt程序,需要依赖图形化界面来运行的。咱们之前弄的Linux的云服务器,一般都是没有图形化界面的,如果需要你得额外手动安装。作为一个服务器,本身就是没有图形化界面的(此处只是为了演示1Qt网络编程的情况,也不会使用Qt来写服务器程序)。那能否使用现在的Udp客户端连之前linux阶段写的udp服务器呢?完全Ok的,这也是网络编程/协议的意义。
4.3TCP的API介绍
Qt给我们提供了QTcpServer和QTcpSocket类。QTcpServer用于监听端口和获得客户端的连接:
QTcpSocket用于客户端和服务器之间的数据交互:
事件循环简单理解一下,可以认为是Qt程序内部,带有一个“生物钟”这样的东西,周期性的执行一些逻辑。这里还有注意点:
4.4TCP回显服务器
接下来进行代码编写,写一个TCP回显服务器,服务器界面和前面一样(在pro文件里添加network)。先把初始化逻辑完成:
接下来重点实现函数:
之前学Linux网络编程,需要弄一个循环,循环的读取请求,循环的处理...。在Qt中基于信号槽就不必循环了,每次客户端发来请求,都能触发readyRead信号。继续完善:
上述代码其实不够严谨,作为回显服务器已经够了的。实际使用Tcp的过程中,Tcp是面向字节流的,可能会分成多段字节数组进行传输。虽然Tcp已经帮我们处理了很多棘手的问题,但是Tcp本身不负责区分,从哪里到哪里是一个完整的应用层数据包(粘包问题)。更严谨的做法,应该是每次收到的数据都给放到一个大的字节数组缓冲区中,并且提前约定好应用层协议的格式(分割符、长度等),再按照协议格式对缓冲区数据进行更细致的解析处理。
继续来完善:
每个客户端都有一个QTcpSocket对象,存在N个,随着服务器的运行,客户端越来越多,如果不释放,此时累计的clientSocket也会越来越多,会造成文件描述符泄漏。像QTcpServer和QUdpSocket都是只有一份,就算不是放,影响不大。可以这样吗:
当前这个槽函数,主要就是围绕着clientSocket进行操作的。一旦delete就意味着其它逻辑无法使用clientSocket,务必要保证delete是这个槽函数中的最后一步,而且也要保证delete肯定能执行到,不会被return/抛出异常给跳过。直接使用delete是下策,我们使用deleteLater更合适:
这个操作,不是立即销毁clientSocket,而是告诉Qt,下一轮事件循环中,再进行上述的销毁操作。因为槽函数都是在事件循环中执行的,进入到下一轮事件循环,意味着上一轮事件循环肯定结束了,也意味着当前的槽函数肯定是执行结束了。
4.5TCP回显客户端
下面来写客户端,还是改pro文件,界面设计和前面一样。下面完善:
调用这个函数,此时系统内核就会和对方的服务器之间三从握手了,三次握手也是需要消耗一定的时间的。此处的这个函数不会阻塞等待三次握手完毕(非阻塞函数),原生的Linux api,也有一个connect函数。一般来说一个socket默认都是阻塞IO通信的,此时针对这样的socket进行connect也就是阻塞的了。因此这样:
以上是初始化的代码。
初始化完成后,要考虑消息如何发出去,服务器返回的响应应该如何去处理。下面继续:
下面运行看一下:
之前在学习linux时写Tcp的回显服务器的时候,有这样一个问题,多个客户端同时访问的时候,就只有一个生效。后来引入了多线程,每个客户端安排一个单独的线程,问题才得到改善。Qt这里打开多个.exe测试没有问题,所以之前存在这个问题的本质原因,是写双重循环,里层循环没有及时结束,导致外层循环不能快速的第二次调用到accept,导致第二个客户端无法进行处理了。当时引入多线程,本质上就是把双重循环,化简成两个独立的循环。上述的Qt服务器程序中,其实一个循环都没写,是通过Qt内置的信号槽来驱动的,所以信号槽机制很好的化简了程序。
4.6HTTP客户端
Qt中提供了HTTP的客户端,Http协议本质上也就是基于Tcp协议实现的。实现一个Http客户端/服务器,本质上也就是基于Tcp socket进行封装。Qt只是提供了Http的客户端,而没有提供Http的服务器的库。核心api相关的类是这样三个:
第一个是核心的一个类,提供了构造或发送一个http请求:
第二个类是这样的:
其中KnownHeaders是一个枚举类型,QVariant表示一个类型可变的值,类似于c语言中的void*。第三个类是这样的:
下面通过代码演示一下(记得添加network模块),先编辑一下ui文件:
此处显示的响应结果,大概率是一个HTML。QPlainTextEdit来进行表示,能够看到响应的原始模样。QTextEdit(天然支持对HTML的解析)会对HTML进行解析渲染,最终显示的效果就不是原始的HTML。QTextEdit背后还做了很多工作,当得到的HTML比较大的时候,也会造成卡顿。来编写:
get本身不是阻塞函数,get只是负责发出去请求,不负责等待响应回来。当响应来的时候会触发finished信号:
下面运行一下:
实际开发中,Http Client获取到的数据,也不一定非得是HTML,更大的可能是客户端开发和服务器开发约定好交互的数据格式,按照约定的格式,客户端拿到之后,并显示到界面上。
5.播放声音文件
接下来说说Qt中的多媒体操作,所谓多媒体,一个是播放声音,一个是播放视频,这里主要说播放声音。使用这些我们需要引入multimedia模块,我们才能进行相关的操作。我们这里需要用到一个类叫做QSound,里面最核心的方法就是play,构造的时候指定一下播放声音文件的路径就可以了。要播放我们需要一个声音文件,QSound只能播放.wav格式的音频文件。一样使用qrc管理外部资源。下面来演示(ui中设置一个按钮):
运行的时候没有声音,因为play完后就去释放了,导致还没播放对象就销毁了。所以这样写:
此时就可以播放了。