1、.pro 就是工程文件(project),它是 qmake 自动生成的用于生产 makefile 的配置文件。
- 模板变量 TEMPLATE 告诉 qmake 为这个应用程序生成哪种 makefile。
- CONFIG 用来告诉 qmake 关于应用程序的配置信息。例如,CONFIG += c++ 11 //使用c++11的特性
- 工程中包含的 .ui 设计文件用 FORMS。例如,FORMS += forms/painter.ui
- 工程中包含的资源文件用 RESOURCES。例如,RESOURCES += qrc/painter.qrc
2、Qt 头文件没有 .h 后缀。
- Qt一个类对应一个头文件,类名就是头文件名。
- QApplication 应用程序类管理图形用户界面应用程序的控制流和主要设置,是Qt的整个后台管理的命脉它包含主事件循环,在其中来自窗口系统和其它资源的所有事件处理和调度。它也处理应用程序的初始化和结束,并且提供对话管理。
- a.exec():程序进入消息循环,等待对用户输入进行响应。这里 main() 把控制权转交给 Qt,Qt 完成事件处理工作,当应用程序退出的时候 exec() 的值就会返回。
- 在 exec() 中,Qt 接受并处理用户和系统的事件并且把它们传递给适当的窗口部件。
3、所谓信号槽,就类似于观察者模式。
- 当信号发出时,被连接的槽函数会自动被回调。
- 信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。
- 在 Qt4 中,connect()函数的 signal 和 slot 都是接受字符串,一旦出现连接不成功的情况,Qt4 是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。在 Qt5 中使用成员函数指针作为 connect() 函数的第四个参数,没有该槽函数会编译错误。
4、只有继承了 QObject 类的类,才具有信号槽的能力。
- 凡是QObject类(不管是直接子类还是间接子类),都应该在第一行代码写上Q_OBJECT。不管是不是使用信号槽,都应该添加这个宏。这个宏的展开将为我们的类提供信号槽机制、国际化机制以及 Qt 提供的不基于 C++ RTTI 的反射能力。
- 信号就是一个个的函数名,返回值是 void(因为无法获得信号的返回值,所以也就无需返回任何值),参数是该类需要让外界知道的数据。信号作为函数名,不需要在 cpp 函数中添加任何实现。
- Qt5 中,任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。
- 发送者和接收者都需要是 QObject 的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外)。
5、一个信号与多个槽相连时,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。另外,槽可以被取消链接。当一个对象 delete 之后,Qt 自动取消所有连接到这个对象上面的槽。
6、C++11中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。
- [],标识一个 Lambda的开始,这部分必须存在,不能省略。
- 函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量(包括 Lambda 所在类的 this)。
- 函数对象参数有以下形式:
- ①空。没有使用任何函数对象参数。
- ②=。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
- ③&。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
- ④this。函数体内可以使用 Lambda 所在类中的成员变量。
- ⑤a。将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下函数是 const 的。要修改传递进来的 a 的拷贝,可以添加 mutable 修饰符。
- ⑥&a。将 a 按引用进行传递。
- ⑦a,&b。将 a 按值进行传递,b 按引用进行传递。
- ⑧=,&a,&b。除 a 和 b 按引用进行传递外,其他参数都按值进行传递。
- ⑨&,a,b。除 a 和 b 按值进行传递外,其他参数都按引用进行传递。
- 按值传递函数对象参数时,加上 mutable 修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。
7、对于嵌套窗口,其坐标是相对于父窗口来说的。
- 所有窗口及窗口控件都是从 QWidget 直接或间接派生出来的。
- QObject 是以对象树的形式组织起来的。在创建 QObject 对象时,可以提供一个其父对象,我们创建的这个 QObject 对象会自动添加到其父对象的 children() 列表。当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)
- QWidget 是能够在屏幕上显示的一切组件的父类。QWidget 继承自 QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。当然,我们也可以自己删除子对象,它们会自动从其父对象列表中删除。
8、Qt 引入对象树的概念,在一定程度上解决了内存问题。
- 当一个 QObject 对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。
- 任何对象树中的 QObject 对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的 children() 列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有 QObject 会被 delete 两次,这是由析构顺序决定的。
- 如果 QObject 在栈上创建,Qt 保持同样的行为。
- 标准 C++ 要求,局部对象的析构顺序应该按照其创建顺序的相反过程。C++ 不允许调用两次析构函数。
- 在 Qt 中,尽量在构造的时候就指定 parent 对象并且大胆在堆上创建。
9、Qt 资源系统是一个跨平台的资源机制,用于将程序运行时所需要的资源以二进制的形式存储于可执行文件内部。如果你的程序需要加载特定的资源(图标、文本翻译等),那么,将其放置在资源文件中,就再也不需要担心这些文件的丢失。也就是说,如果你将资源以资源文件形式存储,它是会编译到可执行文件内部。使用 Qt Creator 可以很方便地创建资源文件。我们可以在工程上点右键,选择“添加新文件…”,可以在 Qt 分类下找到“Qt 资源文件”。
如果以后我们要更改文件名,比如将 docuemnt-open.png 改成 docopen.png,那么,所有使用了这个名字的路径都需要修改。所以,更好的办法是,我们给这个文件去一个“别名”,以后就以这个别名来引用这个文件。
如果我们使用文本编辑器打开 res.qrc 文件,可以看到 Qt Creator 帮我们生成的是怎样的 qrc 文件。当我们编译工程之后,我们可以在构建目录中找到 qrc_res.cpp 文件,这就是 Qt 将我们的资源编译成了 C++ 代码。
10、Qt 中使用QDialog类实现对话框。
QDialog(及其子类,以及所有 Qt::Dialog 类型的类)的对于其 parent 指针都有额外的解释:如果 parent 为 NULL,则该对话框会作为一个顶层窗口,否则则作为其父组件的子对话框(此时,其默认出现的位置是 parent 的中心)。顶层窗口与非顶层窗口的区别在于,顶层窗口在任务栏会有自己的位置,而非顶层窗口则会共享其父组件的位置。
11、对话框分为模态对话框和非模态对话框。
- 模态对话框,就是会阻塞同一应用程序中其它窗口的输入。模态对话框很常见,比如“打开文件”功能。你可以尝试一下记事本的打开文件,当打开文件对话框出现时,我们是不能对除此对话框之外的窗口部分进行操作的。与此相反的是非模态对话框,例如查找对话框,我们可以在显示着查找对话框的同时,继续对记事本的内容进行编辑。
Qt 支持模态对话框和非模态对话框。模态与非模态的实现:
- ①使用 QDialog::exec() 实现应用程序级别的模态对话框;
- ②使用 QDialog::open() 实现窗口级别的模态对话框;
- ③使用 QDialog::show() 实现非模态对话框。
Qt 有两种级别的模态对话框:
- ①应用程序级别的模态:当该种模态的对话框出现时,用户必须首先对对话框进行交互,直到关闭对话框,然后才能访问程序中其他的窗口。
- ②窗口级别的模态:该模态仅仅阻塞与对话框关联的窗口,但是依然允许用户与程序中其它窗口交互。窗口级别的模态尤其适用于多窗口模式。
- 一般默认是应用程序级别的模态。
12、创建一个 QDialog 指针变量 dialog 使用 new 在堆上分配空间,却一直没有 delete。
解决方案:将 MainWindow 的指针赋给 dialog 即可。但如果如果我们的对话框不是在一个界面类中出现呢?由于QWidget的 parent 必须是 QWidget 指针,那就限制了我们不能将一个普通的 C++ 类指针传给 Qt 对话框。另外,如果对内存占用有严格限制的话,当我们将主窗口作为 parent 时,主窗口不关闭,对话框就不会被销毁,所以会一直占用内存。在这种情景下,我们可以设置 dialog 的WindowAttribute:
dialog->setAttribute(Qt::WA_DeleteOnClose);
setAttribute()函数设置对话框关闭时,自动销毁对话框。
13、Qt提供了两种组件定位机制:绝对定位和布局定位。
- 绝对定位就是一种最原始的定位方法:给出这个组件的坐标和长宽值。但是这样做带来的一个问题是,如果用户改变了窗口大小,比如点击最大化按钮或者使用鼠标拖动窗口边缘,采用绝对定位的组件是不会有任何响应的。
- 布局定位:你只要把组件放入某一种布局,布局由专门的布局管理器进行管理。当需要调整大小或者位置的时候,Qt使用对应的布局管理器进行调整。
- 布局定位完美的解决了使用绝对定位的缺陷。
- Qt 提供的布局中以下三种是我们最常用的:
- ①QHBoxLayout:按照水平方向从左到右布局;
- ②QVBoxLayout:按照竖直方向从上到下布局;
- ③QGridLayout:在一个网格中进行布局,类似于 HTML 的 table.
14、像 QSpinBox 有两个信号的情况:
void valueChanged(int)
void valueChanged(const QString &)
当我们使用 &QSpinBox::valueChanged 取函数指针时,编译器不知道应该取哪一个函数(记住前面我们介绍过的,signal 也是一个普通的函数)的地址,因此报错。解决的方法很简单,编译器不是不能确定哪一个函数吗?那么我们就显式指定一个函数。方法就是,我们创建一个函数指针,这个函数指针参数指定为 int:
void (QSpinBox:: *spinBoxSignal)(int) = &QSpinBox::valueChanged;
然后我们将这个函数指针作为 signal,与 QSlider 的函数连接:
QObject::connect(spinBox, spinBoxSignal, slider, &QSlider::setValue);
这样便避免了编译错误。
15、自定义控件通过先代码实现一个类继承于 QWidget,然后在界面加入这个一个空白的 QWidget,然后右键提升为自己所实现的类。
16、Qt 程序需要在 main() 函数创建一个 QApplication 对象,然后调用它的 exec() 函数。这个函数就是开始 Qt 的事件循环。在执行 exec() 函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于 QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给 QObject 的 event() 函数。event() 函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)。event() 函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个 event() 函数了。
我们重写了它的 event() 函数,这个函数有一个 QEvent 对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。
如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。
在 event() 函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。
17、event() 函数中实际是通过事件处理器来响应一个具体的事件。这相当于 event() 函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 Qt 调用我们自己实现的版本。
由此可以见,event() 是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个 event() 函数,通过 QEvent::type() 判断不同的事件。鉴于重写 event() 函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了 event() 函数的另外一个作用:屏蔽掉某些不需要的事件处理器。
18、Qt 创建了 QEvent 事件对象之后,会调用 QObject 的 event() 函数处理事件的分发。显然,我们可以在 event() 函数中实现拦截的操作。由于 event() 函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个 event() 函数。这当然相当麻烦,更不用说重写 event() 函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。
QObject 有一个 eventFilter() 函数,用于建立事件过滤器。 函数原型如下:
virtual bool QObject::eventFilter(QObject* watched, QEvent* event);
这个函数正如其名字显示的那样,是一个“事件过滤器”。所谓事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched 对象以及以后所有的事件过滤器根本不会知道这么一个事件。
19、eventFilter() 函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用 QObject::installEventFilter() 函数。函数的原型如下:
void QObject::installEventFilter(QObject* filterObj);
这个函数接受一个 QObject* 类型的参数。记得刚刚我们说的,eventFilter() 函数是 QObject 的一个成员函数,因此,任意 QObject 都可以作为事件过滤器(问题在于,如果你没有重写 eventFilter() 函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过 QObject::removeEventFilter() 函数移除。
我们可以向一个对象上面安装多个事件处理器,只要调用多次 installEventFilter() 函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序。
20、事件过滤器的强大之处在于,我们可以为整个应用程序添加一个事件过滤器。记得,installEventFilter() 函数是 QObject 的函数,QApplication 或者 QCoreApplication 对象都是 QObject 的子类,因此,我们可以向 QApplication 或者 QCoreApplication 添加事件过滤器。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话我们不应该这么做。
注意,事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
21、所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择,其代表作就是win32 API的 WndProc() 函数。
每一种事件对应一个事件处理函数。
Qt 具有这么多种事件处理函数,肯定有一个地方对其进行分发,否则,Qt 怎么知道哪一种事件调用哪一个事件处理函数呢?这个分发的函数,就是 event()。显然,当 QMouseEvent 产生之后,event() 函数将其分发给 mouseEvent() 事件处理器进行处理。
22、如果要实现全局的事件过滤器,则可以安装到 QApplication 或者 QCoreApplication 上面。这里需要注意的是,如果使用 installEventFilter() 函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的 eventFilter() 函数进行过滤,其它对象不受影响。如果给 QApplication 对象安装事件过滤器,那么该过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给 eventFilter() 函数。
23、Qt事件的调用最终都会追溯到 QCoreApplication::notify() 函数,因此,最大的控制权实际上是重写 QCoreApplication::notify()。这个函数的声明是:
virtual bool QCoreApplication::notify(QObject* receiver, QEvent* event);
该函数会将 event 发送给 receiver,也就是调用 receiver->event(event),其返回值就是来自 receiver 的事件处理器。注意,这个函数为任意线程的任意对象的任意事件调用,因此,它不存在事件过滤器的线程的问题。不过我们并不推荐这么做,因为 notify() 函数只有一个,而事件过滤器要灵活得多。
24、总结一下Qt的事件处理,实际上是有五个层次:
- ①重写 paintEvent()、mousePressEvent() 等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
- ②重写 event() 函数。event() 函数是所有对象的事件入口,QObject 和 QWidget 中的实现,默认是把事件传递给特定的事件处理函数。
- ③在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。
- ④在 QCoreApplication::instance() 上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和 notify() 函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
- ⑤重写 QCoreApplication::notify() 函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为 QCoreApplication 是单例的)。
25、Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系统基于 QPainter,QPainterDevice 和 QPaintEngine三个类。
QPainter 用来执行绘制的操作;QPaintDevice 是一个二维空间的抽象,这个二维空间允许 QPainter 在其上面进行绘制,也就是 QPainter 工作的空间;QPaintEngine 提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口。
Qt的绘图系统实际上是,使用 QPainter 在 QPainterDevice 上进行绘制,它们之间使用 QPaintEngine 进行通讯(也就是翻译 QPainter 的指令)。
26、绘图设备是指继承QPainterDevice的子类。Qt一共提供了四个这样的类,分别是 QPixmap、QBitmap、QImage 和 QPicture。其中,
- ①QPixmap 专门为图像在屏幕上的显示做了优化
- ②QBitmap 是 QPixmap 的一个子类,它的色深限定为 1,可以使用 QPixmap 的 isQBitmap() 函数来确定这个 QPixmap 是不是一个 QBitmap。
- ③QImage 专门为图像的像素级访问做了优化。
- ④QPicture 则可以记录和重现 QPainter 的各条命令。
27、QPixmap 继承了 QPaintDevice,因此,你可以使用 QPainter 直接在上面绘制图形。QPixmap 也可以接受一个字符串作为一个文件的路径来显示这个文件,比如你想在程序之中打开 png、jpeg 之类的文件,就可以使用 QPixmap。使用 QPainter 的 drawPixmap() 函数可以把这个文件绘制到一个 QLabel、QPushButton 或者其他的设备上面。QPixmap 是针对屏幕进行特殊优化的,因此,它与实际的底层显示设备息息相关。注意,这里说的显示设备并不是硬件,而是操作系统提供的原生的绘图引擎。所以,在不同的操作系统平台下,QPixmap 的显示可能会有所差别。
28、QBitmap 继承自 QPixmap,因此具有 QPixmap 的所有特性,提供单色图像。QBitmap 的色深始终为 1.
- 色深这个概念来自计算机图形学,是指用于表现颜色的二进制的位数。我们知道,计算机里面的数据都是使用二进制表示的。为了表示一种颜色,我们也会使用二进制。比如我们要表示8种颜色,需要用 3 个二进制位,这时我们就说色深是3. 因此,所谓色深为 1,也就是使用1个二进制位表示颜色。1个位只有两种状态:0和1,因此它所表示的颜色就有两种,黑和白。所以说,QBitmap 实际上是只有黑白两色的图像数据。
- 由于 QBitmap 色深小,因此只占用很少的存储空间,所以适合做光标文件和笔刷。
29、QPixmap 使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而 QImage 则是使用独立于硬件的绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示形式。
30、QImage 与 QPixmap 的区别
- ①QPixmap 主要是用于绘图,针对屏幕显示而最佳化设计,QImage 主要是为图像I/O、图片访问和像素修改而设计的;
- ②QPixmap 依赖于所在的平台的绘图引擎,故例如反锯齿等一些效果在不同的平台上可能会有不同的显示效果,QImage 使用 Qt 自身的绘图引擎,可在不同平台上具有相同的显示效果;
- ③由于 QImage 是独立于硬件的,也是一种 QPaintDevice,因此我们可以在另一个线程中对其进行绘制,而不需要在 GUI 线程中处理,使用这一方式可以很大幅度提高UI响应速度;
- ④QImage 可通过 setPixpel() 和 pixel() 等方法直接存取指定的像素。
31、QImage 与 QPixmap 之间的转换:
- ①QImage 转 QPixmap:
- 使用 QPixmap 的静态成员函数: fromImage()
- QPixmap fromImage(const QImage & image, Qt::ImageConversionFlags flags = Qt::AutoColor)
- ②QPixmap 转 QImage:
- 使用 QPixmap 类的成员函数: toImage()
- QImage toImage() const
32、QPicture 是一个可以记录和重现QPainter命令的绘图设备。
QPicture 将 QPainter 的命令序列化到一个 IO 设备,保存为一个平台独立的文件格式。这种格式有时候会是“元文件(meta- files)”。Qt 的这种格式是二进制的,不同于某些本地的元文件,Qt 的 pictures 文件没有内容上的限制,只要是能够被 QPainter 绘制的元素,不论是字体还是 pixmap,或者是变换,都可以保存进一个 picture 中。
QPicture 是平台无关的,因此它可以使用在多种设备之上,比如 svg、pdf、ps、打印机或者屏幕。
QPicture 使用系统的分辨率,并且可以调整 QPainter 来消除不同设备之间的显示差异。
33、QFile 需要使用 / 作为文件分隔符,不过,它会自动将其转换成操作系统所需要的形式。例如 C:/windows 这样的路径在 Windows 平台下同样是可以的。
34、QFile 主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。我们可以使用 QDataStream 或 QTextStream 类来读写文件,也可以使用 QIODevice 类提供的 read()、readLine()、readAll() 以及 write() 这样的函数。
值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取,而不是自己分析文件路径字符串。
35、QDataStream 提供了基于 QIODevice 的二进制数据的序列化。数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。
QDataStream 提供流的形式,性能上一般比直接调用原始 API 更好一些。
36、最好使用 Qt 整型来进行读写,比如程序中的 qint32。这保证了在任意平台和任意编译器都能够有相同的行为。
37、为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:
file.close(); // 如果不想关闭文件,可以使用 file.flush();
38、QTextStream 会自动将 Unicode 编码同操作系统的编码进行转换,这一操作对开发人员是透明的。它也会将换行符进行转换,同样不需要自己处理。
QTextStream使用 16 位的 QChar 作为基础的数据存储单位,同样,它也支持 C++ 标准类型,如 int 等。实际上,这是将这种标准类型与字符串进行了相互转换。
默认情况下,QTextStream 的编码格式是 Unicode,如果我们需要使用另外的编码,可以使用:
stream.setCodec("UTF-8");
这样的函数进行设置。
当使用 QDataStream 写入的时候,实际上会在要写入的内容前面,额外添加一个这段内容的长度值。而以文本形式写入数据,是没有数据之间的分隔的。
39、Qt 中提供的所有的 Socket 类都是非阻塞的。
Qt中常用的用于socket通信的套接字类:
- QTcpServer:用于TCP/IP通信, 作为服务器端套接字使用
- QTcpSocket:用于TCP/IP通信,作为客户端套接字使用。
- QUdpSocket:用于UDP通信,服务器、客户端均使用此套接字。
40、在 Qt 中实现 TCP/IP 服务器端通信的流程:
- ①创建套接字;
- ②将套接字设置为监听模式;
- ③等待并接受客户端请求;
- 可以通过 QTcpServer 提供的void newConnection()信号来检测是否有连接请求,如果有可以在对应的槽函数中调用 nextPendingConnection 函数获取到客户端的 Socket 信息(返回值为 QTcpSocket* 类型指针),通过此套接字与客户端之间进行通信。
- ④接收或者向客户端发送数据;
- 接收数据:使用 read() 或者 readAll() 函数;
- 发送数据:使用 write() 函数。
客户端通信流程:
- ①创建套接字;
- ②连接服务器;
- 可以使用QTcpSocket类的 connectToHost() 函数来连接服务器。
- ③向服务器发送或者接受数据。
41、使用 Qt 提供的 QUdpSocket 进行 UDP 通信。在 UDP 方式下,客户端并不与服务器建立连接,它只负责调用发送函数向服务器发送数据。类似的服务器也不从客户端接收连接,只负责调用接收函数,等待来自客户端的数据的到达。
在UDP通信中,服务器端和客户端的概念已经显得有些淡化,两部分做的工作都大致相同:
- ①创建套接字;
- ②绑定套接字;
- 在UDP中如果需要接收数据则需要对套接字进行绑定,只发送数据则不需要对套接字进行绑定。通过调用bind()函数将套接字绑定到指定端口上。
③接收或者发送数据: - 1)接收数据:使用readDatagram()接收数据,函数声明如下:
qint64 readDatagram(char * data, qint64 maxSize, QHostAddress * address = 0, quint16 * port = 0) 参数: data: 接收数据的缓存地址; maxSize: 缓存接收的最大字节数; address: 数据发送方的地址(一般使用提供的默认值); port: 数据发送方的端口号(一般使用提供的默认值). 使用pendingDatagramSize()可以获取到将要接收的数据的大小,根据该函数返回值来准备对应大小的内存空间存放将要接收的数据。
- 2)发送数据: 使用writeDatagram()函数发送数据,函数声明如下:
qint64 writeDatagram(const QByteArray & datagram, const QHostAddress & host, quint16 port) 参数: datagram:要发送的字符串; host:数据接收方的地址; port:数据接收方的端口号.
- 在UDP中如果需要接收数据则需要对套接字进行绑定,只发送数据则不需要对套接字进行绑定。通过调用bind()函数将套接字绑定到指定端口上。
42、在使用 QUdpSocket 类的 writeDatagram() 函数发送数据的时候,其中第二个参数 host 应该指定为广播地址:QHostAddress::Broadcast 此设置相当于 QHostAddress(“255.255.255.255”)
使用UDP广播的的特点:
- ①使用UDP进行广播,局域网内的其他的UDP用户全部可以收到广播的消息;
- ②UDP广播只能在局域网范围内使用。
43、我们再使用广播发送消息的时候会发送给所有用户,但是有些用户是不想接受消息的,这时候我们就应该使用组播,接收方只有先注册到组播地址中才能收到组播消息,否则则接受不到消息。另外组播是可以在Internet中使用的。
在使用 QUdpSocket 类的 writeDatagram() 函数发送数据的时候,其中第二个参数 host 应该指定为组播地址,关于组播地址的分类:
- ①224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
- ②224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet;
- ③224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;
- ④239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
注册加入到组播地址需要使用QUdpSocket类的成员函数:
bool joinMulticastGroup(const QHostAddress & groupAddress)
44、TCP/IP 和 UDP 的比较
- | TCP/IP | UDP |
---|---|---|
是否连接 | 面向连接 | 无连接 |
传输方式 | 基于流 | 基于数据报 |
传输可靠性 | 可靠 | 不可靠 |
传输效率 | 效率低 | 效率高 |
能否广播 | 不能 | 能 |
45、当一个操作耗时很长时,整个系统都会等待这个操作,程序就不能响应键盘、鼠标、菜单等操作,而使用多线程技术可将耗时长的操作置于一个新的线程,避免以上问题。
46、Qt 中所有界面都是在 UI 线程中(也被称为主线程,就是执行了 QApplication::exec() 的线程),在这个线程中执行耗时的操作(比如那个循环),就会阻塞 UI 线程,从而让界面停止响应。界面停止响应,用户体验自然不好,不过更严重的是,有些窗口管理程序会检测到你的程序已经失去响应,可能会建议用户强制停止程序,这样一来你的程序可能就此终止,任务再也无法完成。
47、run() 函数就是新的线程需要执行的代码。
run() 是线程的入口,就像 main() 对于应用程序的作用,使用 QThread::start() 函数启动一个线程(注意,这里不是run()函数)。
另外,我们将 WorkerThread::deleteLater() 函数与 WorkerThread::finished() 信号连接起来,当线程完成时,系统可以帮我们清除线程实例。这里的 finished() 信号是系统发出的。
48、在 Qt4.7 及以后版本推荐使用以下的工作方式。其主要特点就是利用 Qt 的事件驱动特性,将需要在次线程中处理的业务放在独立的模块(类)中,由主线程创建完该对象后,将其移交给指定的线程,且可以将多个类似的对象移交给同一个线程。
由于队列连接的作用,在不同线程间连接信号和槽是很安全的。
49、关于 QObject 类的 connect 函数最后一个参数,连接类型:
- ①自动连接(AutoConnection),默认的连接方式。
- 1)如果信号与槽,也就是发送者与接受者在同一线程,等同于直接连接;
- 2)如果发送者与接受者处在不同线程,等同于队列连接。
- ②直接连接(DirectConnection):
- 当信号发射时,槽函数立即直接调用。无论槽函数所属对象在哪个线程,槽函数总在发送者所在线程执行。
- ③队列连接(QueuedConnection):
- 当控制权回到接受者所在线程的事件循环时,槽函数被调用。槽函数在接受者所在线程执行。
50、多线程使用过程中注意事项:
- ①线程不能操作UI对象(从Qwidget直接或间接派生的窗口对象);
- ②需要移动到子线程中处理的模块类,创建的对象的时候不能指定父对象。
51、Qt 使用 QSqlDatabase 表示一个数据库连接。更底层上,Qt 使用驱动(drivers)来与不同的数据库 API 进行交互。
如果习惯于使用 SQL 语句,我们可以选择 QSqlQuery 类;如果只需要使用高层次的数据库接口(不关心 SQL 语法),我们可以选择使用 QsqlTableModel 类。
在使用时,我们可以通过
QSqlDatabase::drivers();
找到系统中所有可用的数据库驱动的名字列表。我们只能使用出现在列表中的驱动。由于默认情况下,QtSql 是作为 Qt 的一个模块提供的。为了使用有关数据库的类,我们必须在.pro文件中添加这么一句:
QT += sql
这表示,我们的程序需要使用 Qt 的 core、gui 以及 sql 三个模块。
52、如果需要同时使用 Qt4 和 Qt5 编译程序,通常我们的 .pro 文件是这样的:
QT += core gui sql
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
这两句也很明确:Qt 需要加载 core、gui 和 sql 三个模块,如果主板本大于 4,则再添加 widgets 模块。
53、我们这里使用的是 sqlite 数据库,只需要指定数据库名字即可。如果是数据库服务器,比如 MySQL,我们还需要指定主机名、端口号、用户名和密码。
54、我们需要插入多条数据,此时可以使用 QSqlQuery::exec() 函数一条一条插入数据,但是这里我们选择了另外一种方法:批量执行。首先,我们使用 QSqlQuery::prepare() 函数对这条 SQL 语句进行预处理,问号 ? 相当于占位符,预示着以后我们可以使用实际数据替换这些位置。简单说明一下,预处理是数据库提供的一种特性,它会将 SQL 语句进行编译,性能和安全性都要优于普通的 SQL 处理。在上面的代码中,我们使用一个字符串列表 names 替换掉第一个问号的位置,一个整型列表 ages 替换掉第二个问号的位置,利用 QSqlQuery::addBindValue() 我们将实际数据绑定到这个预处理的 SQL 语句上。需要注意的是,names 和 ages 这两个列表里面的数据需要一一对应。然后我们调用 QSqlQuery::execBatch() 批量执行 SQL,之后结束该对象。这样,插入操作便完成了。
55、对于数据库事务的操作,我们可以使用 QSqlDatabase::transaction() 开启事务,QSqlDatabase::commit() 或者 QSqlDatabase::rollback() 结束事务。使用 QSqlDatabase::database() 函数则可以根据名字获取所需要的数据库连接。
56、Qt 不仅提供了这种使用 SQL 语句的方式,还提供了一种基于模型的更高级的处理方式。这种基于 QSqlTableModel 的模型处理更为高级,如果对 SQL 语句不熟悉,并且不需要很多复杂的查询,这种 QSqlTableModel 模型基本可以满足一般的需求。
值得注意的是,QSqlTableModel并不一定非得结合 QListView 或 QTableView 使用,我们完全可以用其作一般性处理。
注意,由于 QSqlTableModel 没有提供 const_iterator 遍历器,因此不能使用 foreach 宏进行遍历。
另外需要注意,由于 QSqlTableModel 只是一种高级操作,肯定没有实际 SQL 语句方便。具体来说,我们使用 QSqlTableModel 只能进行 SELECT * 的查询,不能只查询其中某些列的数据。
57、Qt给我们提供了一个寻找依赖项的工具 windeployqt。
Windeployqt的使用方法:
如果我们一件配置好了环境变量,在 dos 下输入 windeployqt 会有相应的信息输出,否则需要指定该工具的完全路径才能够正常使用,例如:
D:\Software\Qt\Qt5.7.0\5.7\mingw53_32\bin\windeployqt