Qt相关知识

信号与槽

Qt信号与槽机制可以看作是一种观察者模式(Observer Pattern)的设计模型。观察者模式中,有一个被观察的对象(Subject),它可以发出事件或状态改变,并通知所有依赖它的观察者(Observer)。Qt中的信号可以看作是 Subject 发出的事件,而槽可以看作是 Observer 对事件作出的响应。通过信号和槽机制,Qt 实现了一种松耦合、灵活性高的对象间通信方法,能够在许多场景下简化代码逻辑,提高程序的可维护性和可扩展性。

信号与槽的实现需要借助元对象编译器MOC(Meta Object Compiler),这个工具被集成在了 Qt 的编译工具链 qmake中,在开始编译 Qt工程时,会先去执行 MOC。要使用信号 - 槽功能,先决条件是继承 QObject类,并在类声明中增加 Q_OBJECT宏。之后在signals字段之后声明一些函数,这些函数就是信号。在public slots之后声明的函数,就是槽函数。

static QMetaObject::Connection connect(
const QObject *sender, //信号发送对象指针
const char *signal, //信号函数字符串,使用SIGNAL()
const QObject *receiver, //槽函数对象指针
const char *member, //槽函数字符串,使用SLOT()
Qt::ConnectionType = Qt::AutoConnection//连接类型,一般默认即可
);

//例如
connect(pushButton, SIGNAL(clicked()), dialog, SLOT(close()));
static QMetaObject::Connection connect(
const QObject *sender, //信号发送对象指针
const QMetaMethod &signal,//信号函数地址
const QObject *receiver, //槽函数对象指针
const QMetaMethod &method,//槽函数地址
Qt::ConnectionType type = Qt::AutoConnection//连接类型,一般默认即可
);

//例如
connect(pushButton, QPushButton::clicked, dialog, QDialog::close);

信号和槽的关联方式,它决定了信号是立即传送到一个槽还是在稍后时间排队等 待传送。关联方式使用枚举 Qt::ConnectionType进行描述,下表为其取值及意义:

枚举 说明
Qt::AutoConnection 0 自动关联,默认值。若信号和槽在同一线程中,则使用Qt::DirectConnection,否则,使用 Qt::QueuedConnection。当信号发射时确定使用哪种关联类型。
Qt::DirectConnection 1 直接关联。当信号发射后,立即调用槽。在槽执行完之后,才会执行发射信号之后的代码 (即 emit关键字之后的代码)。该槽在信号线程中执行。
Qt::QueuedConnection 2 队列关联。当控制权返回到接收者线程的事件循环后,槽才会被调用,也就是说 emit关键字后面的代码将立即执行槽将在稍后执行,该槽在接收者的线程中执行。(异步非阻塞)
Qt::BlockingQueuedConnection 3 阻塞队列关联。和 Qt::QueuedConnection一样,只是信号线程会一直阻塞,直到槽返回。如果接收者驻留在信号线程中,则不能使用此连接,否则应用程序将会死锁。(同步阻塞)
Qt::UniqueConnection 0x80 唯一关联。这是一个标志,可使用按位或与上述任何连接类型组合。当设置 Qt::UniqueConnection 时,则只有在不重复的情况下才会进行连接,如果已经存在重复连接 (即,相同的信号指向同一对象上的完全相同的槽),则连接将失败,此时将返回无效的 QMetaObject::Connection

Qt 信号与槽机制是 Qt 框架的核心部分之一,它能够实现对象之间的解耦,使得一个对象的变化可以自动地通知到其他对象,并且能够方便地在不同的线程之间进行通信。它的底层实现原理主要涉及到三个方面:元对象系统、信号和槽的连接、信号和槽的触发

  1. 元对象系统

Qt的元对象系统是由 QObject类提供的,允许在运行时查询对象的属性和方法,并提供了信号和槽机制。在运行时,QObject类会为每个类创建一个元对象(meta-object),它是Q_OBJECT宏展开后创建的,它包含了该类的所有信息,例如类名、父类和成员函数等。元对象还包含了信号和槽的信息,即它们的名称和参数类型等。

  1. 信号和槽的连接

当一个信号和一个槽连接时,会在元对象中创建一个连接表(connection table),用于存储这个连接的信息。连接表是一个二维数组,其中每行表示一个信号和一个槽的连接,第一列和第二列分别表示信号和槽的指针,第三列是一个整数,表示连接类型(例如,连接方式是直接连接还是队列连接)。连接表还包含了一个函数指针数组,用于执行连接的操作。当信号被触发时,连接表中与这个信号相关的函数指针就会被调用,从而触发槽的执行。

  1. 信号和槽的触发

当一个信号被触发时,Qt 会根据连接表中的信息,调用相应的函数指针来触发槽的执行。如果连接类型是直接连接,那么槽函数就会直接在当前线程中执行;如果连接方式是队列连接,那么槽函数就会被放入事件队列中,等待事件循环处理。这种方式可以保证槽函数在正确的线程中执行,从而避免了线程安全问题。

总之,Qt信号与槽机制是一种高效、灵活和易用的编程模式,它能够帮助我们实现对象之间的松耦合和消息传递,减少代码的耦合度和复杂度。其底层实现主要依赖于元对象系统、信号和槽的连接以及信号和槽的触发等。

Http

在 Qt 中,可以使用 QNetworkAccessManager类来实现 Http 下载文件。具体步骤如下:

// 1. 创建QNetworkAccessManager对象:
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
// 2. 创建QNetworkRequest对象,并设置下载地址:
QUrl url("http://www.example.com/file.zip");
QNetworkRequest request(url);
// 3. 发送 Http 请求并获取响应:
QNetworkReply *reply = manager->get(request);
// 4. 接收响应并保存文件:
QFile file("file.zip");
if (file.open(QIODevice::WriteOnly)) {
file.write(reply->readAll());
file.close();
}

TCP

服务器端QTcpServer

auto tcpServer = new QTcpServer(this);
connect(tcpServer, &QTcpServer::newConnection, this, &Dialog::sendData);
connect(tcpServer, &QTcpServer::acceptError, this, &Dialog::displayError);
tcpServer->listen(QHostAddress::Any, 8888);

TcpServer是 Qt 中用来创建 TCP 服务的类,其主要功能是监听客户端连接请求并创建对应的 QTcpSocket对象。QTcpServer类的重要函数如下:

重要函数:

  1. listen():开始监听指定的端口。
  2. close():停止监听端口。
  3. nextPendingConnection():获取下一个等待处理的连接,如果没有等待的连接,则返回 nullptr
  4. error():获取最后一次出现的错误。
  5. hasPendingConnections():判断是否有等待处理的连接。
  6. maxPendingConnections():获取最大等待连接数。
  7. setMaxPendingConnections():设置最大等待连接数。

重要信号:

  1. newConnection():当有新的连接请求时,会发出该信号。
  2. acceptError():当接受连接时出现错误时,会发出该信号。
  3. closed():当 server关闭时,会发出该信号。

需要注意的是,QTcpServer是异步的,当有新的连接请求时,会发出 newConnection() 信号,需要在该信号的槽函数中调用 nextPendingConnection()函数获取新的 QTcpSocket 对象,并使用信号和槽机制来处理连接事件。

客户端QTcpSocket

// 1. 创建 QTcpSocket 对象:
QTcpSocket *socket = new QTcpSocket(this);
// 2. 连接服务器:
socket->connectToHost("localhost", 12345);
if (!socket->waitForConnected()) {
qDebug() << "Failed to connect to server";
return;
}
// 3. 发送数据:
socket->write("Hello, world!");
// 4. 接收数据:
if (socket->waitForReadyRead()) {
QByteArray data = socket->readAll();
qDebug() << "Received data: " << data;
}

需要注意的是,Qt的网络模块是异步的,因此需要使用信号和槽机制来处理网络事件。同时需要注意,在发送完数据后,需要等待服务器返回数据才能读取数据。可以使用waitForReadyRead()函数等待数据到达,也可以使用readyRead() 信号来处理数据到达事件。

QTcpSocketQt中用来实现 TCP客户端的类,其重要函数和信号如下:

重要函数:

  1. connectToHost():连接到指定的主机和端口。
  2. disconnectFromHost():断开与主机的连接。
  3. write():向 socket写入数据。
  4. readAll():读取所有可用的数据。
  5. state():获取当前 socket的状态。
  6. setReadBufferSize():设置读取缓冲区的大小。

重要信号:

  1. connected():当 socket成功连接到远程主机时发出。
  2. disconnected():当 socket从远程主机断开连接时发出。
  3. readyRead():当有数据可读时发出。
  4. stateChanged():当 socket状态发生改变时发出。
  5. error():当 socket 发生错误时发出。

UDP

// 1. 创建 QUdpSocket 对象:
auto socket = new QUdpSocket(this);
// 2. 绑定端口:
if (!socket->bind(QHostAddress::Any, 12345)) {
qDebug() << "Failed to bind port";
return;
}
// 3. 实现接收数据的处理:
connect(socket, &QUdpSocket::readyRead, this, &MyServer::readDataHandler);
void MyServer::readDataHandler() {
while (socket->hasPendingDatagrams()) {
QByteArray data;
data.resize(socket->pendingDatagramSize());
QHostAddress sender;
quint16 senderPort;
socket->readDatagram(data.data(), data.size(), &sender, &senderPort);
qDebug() << "Received data: " << data;
}
}

QUdpSocketQt中用来实现 UDP通信的类,其重要函数和信号如下:

重要函数:

  1. bind():绑定指定的端口和地址。
  2. writeDatagram():向指定的地址和端口发送数据。
  3. close():关闭 socket。
  4. hasPendingDatagrams():判断是否有等待处理的数据报。
  5. readDatagram():读取一个数据报。
  6. joinMulticastGroup():加入指定的多播组。
  7. leaveMulticastGroup():离开指定的多播组。
  8. setSocketOption():设置 socket 选项。

重要信号:

  1. readyRead():当有数据可读时发出。
  2. stateChanged():当 socket 状态发生改变时发出。
  3. error():当 socket 发生错误时发出。
  4. connected():当 socket 成功连接到远程主机时发出。
  5. disconnected():当 socket 从远程主机断开连接时发出。

需要注意的是,由于 Qt 的网络模块是异步的,因此需要使用信号和槽机制来处理网络事件。在处理完一个数据包后,需要循环读取所有的数据包以处理后续数据。

Qt单例

在 Qt 中,可以通过QSharedMemory判断当前进程是否已经存在来实现 exe 进程单例。具体步骤如下:

QSharedMemory sharedMemory("MyApplication");
if (!sharedMemory.create(1)) {
qDebug() << "Application already running";
return 0;
}

QWidgetQML

QWidgetQML都是 Qt中用来实现 GUI 工具,但其本质和使用方式有所不同。

  1. QWidget是基于C++GUI 工具,而 QML是基于 JavaScript的 GUI 工具QWidget需要通过编写 C++ 代码来创建和管理 GUI,而 QML则使用一种类似于 HTML的声明式语言来创建 GUI
  2. QWidget使用的是 Qt Widgets 模块,而 QML使用的是 Qt Quick 模块。Qt Widgets 提供了一系列的控件和布局管理器,可以方便地创建传统的桌面应用程序。而Qt Quick则提供了一套用于创建流畅、高效的界面的 API,适用于创建现代化的移动应用程序。
  3. QWidget支持的平台包括 Windows、MacLinux 等桌面操作系统,而 QML则可以运行在多种平台上,包括桌面、移动、嵌入式和 Web等。
  4. 在使用上,QWidget需要通过信号和槽机制来实现控件之间的交互,而 QML 则可以直接在代码中绑定属性和函数来实现交互。
  5. QWidget使用的绘制引擎是基于底层操作系统的原生控件实现的,例如在 Windows下使用的是 GDI+,在 macOS下使用的是 Quartz,这些底层控件都是由操作系统提供的,因此 QWidget的绘制效果和性能与操作系统有直接关系。
  6. QML使用的是 OpenGL ES 2.0 来实现绘制,这是一种跨平台的图形 API,可以在多种平台上运行,并且具有高效的绘制性能。QML使用 OpenGL ES 2.0来绘制所有的 GUI元素,包括文本、图像和动画等。
  7. 由于 QWidget使用的是底层操作系统的原生控件,因此其绘制效果和性能稳定可靠,但缺乏灵活性,不易于实现自定义控件。而 QML则可以实现高度自定义的控件,并且具有流畅、高效的动画效果,但其绘制效果和性能可能会因为底层 OpenGL实现的不同而有所不同。

Qt多线程

在 Qt 中,可以使用多种方式实现多线程执行任务,例如:

  1. 使用 QThread类:继承 QThread类并重载其run() 函数来执行任务,然后创建新线程并启动。
class MyThread : public QThread
{
public:
void run() override
{
// 执行任务
}
};
MyThread *thread = new MyThread;
thread->start();
  1. 使用 QtConcurrent类:使用QtConcurrent::run() 函数来启动一个新线程并执行任务。
QtConcurrent::run([](){
// 执行任务
});
  1. 使用 QThreadPool类:将任务封装为一个 QRunnable对象,并将其添加到线程池中,线程池会自动分配线程来执行任务.
class MyTask : public QRunnable
{
public:
void run() override
{
// 执行任务
}
};

MyTask *task = new MyTask;
QThreadPool::globalInstance()->start(task);
  1. 使用信号和槽机制:创建一个 QObject对象,并将其移动到新线程中,然后使用信号和槽机制来执行任务。
class MyObject : public QObject
{
Q_OBJECT
public slots:
void doTask()
{
// 执行任务
}
};
MyObject *object = new MyObject;
QThread *thread = new QThread;
object->moveToThread(thread);
connect(thread, &QThread::started, object, &MyObject::doTask);
thread->start();