首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Qt | 安全的udp客户端搭建(代码框架值得学习)

Qt | 安全的udp客户端搭建(代码框架值得学习)

原创
作者头像
Qt历险记
发布2024-12-15 11:00:48
发布2024-12-15 11:00:48
7220
举报
文章被收录于专栏:Qt6 研发工程师Qt6 研发工程师

点击上方"蓝字"关注我们

01、QUdpSocket

>>>QUdpSocket 是 Qt 框架中用于支持用户数据报协议(UDP)网络通信的类。它允许你在网络上发送和接收数据报,适用于需要高效传输数据而不要求可靠性的应用场景,例如实时视频、语音通话等。

02、QDtls

>>>QDtls 是 Qt 框架中用于实现 Datagram Transport Layer Security (DTLS) 的类,DTLS 是基于 UDP 的安全协议,常用于保护实时数据传输,如音频或视频流。

03、QSharedPointer

>>>QSharedPointer 是 Qt 框架中提供的一个智能指针类,用于管理动态分配的对象。在 C++ 中,使用智能指针可以避免内存泄漏和资源管理问题。QSharedPointer 实现了引用计数,当最后一个引用被销毁时,所管理的对象会被自动删除。

04、QHostInfo

>>>QHostInfo 是 Qt 框架中用于获取主机信息的类,主要用于处理主机名和 IP 地址的解析。它可以方便地获取与主机相关的信息,包括主机名、别名及其对应的 IP 地址列表等。

05、QIntValidator

>>>QIntValidator 是 Qt 框架中的一个输入验证器,用于限制输入框只能输入整数值。它可以用于 QLineEdit 或其他输入组件,以确保用户输入符合设定的整数范围。

06、实战

>>>addressdialog.h

代码语言:javascript
复制
#ifndef ADDRESSDIALOG_H // 如果没有定义 ADDRESSDIALOG_H#define ADDRESSDIALOG_H // 定义 ADDRESSDIALOG_H,避免重复包含​#include <QDialog> // 包含 QDialog 头文件,用于创建对话框​QT_BEGIN_NAMESPACE // 开始 Qt 命名空间namespace Ui { // 创建 Ui 命名空间class AddressDialog; // 前向声明 AddressDialog 类}QT_END_NAMESPACE // 结束 Qt 命名空间​class AddressDialog : public QDialog // 定义 AddressDialog 类,继承自 QDialog{    Q_OBJECT // 宏,启用 Qt 的信号和槽机制​public:    explicit AddressDialog(QWidget *parent = nullptr); // 构造函数,接受一个 QWidget 指针作为父对象    ~AddressDialog(); // 析构函数​    QString remoteName() const; // 获取远程名称的函数声明    quint16 remotePort() const; // 获取远程端口的函数声明​private:    void setupHostSelector(); // 初始化主机选择器的私有函数声明    void setupPortSelector(); // 初始化端口选择器的私有函数声明​    Ui::AddressDialog *ui = nullptr; // 指向 Ui::AddressDialog 的指针,初始化为 nullptr};​#endif // ADDRESSDIALOG_H // 结束条件编译指令,确保避免重复定义​

07、addressdialog.cpp

>>>

代码语言:javascript
复制
#include "addressdialog.h" // 包含自定义的 AddressDialog 头文件#include "ui_addressdialog.h" // 包含自动生成的 UI 界面头文件​#include <QtCore> // 包含 Qt 核心模块#include <QtNetwork> // 包含 Qt 网络模块#include <QtWidgets> // 包含 Qt 小部件模块​#include <limits> // 包含上限和下限的头文件​AddressDialog::AddressDialog(QWidget *parent) // 构造函数,接受一个 QWidget 指针作为父对象    : QDialog(parent), // 初始化父类 QDialog    ui(new Ui::AddressDialog) // 初始化 ui 指针{    ui->setupUi(this); // 设置用户界面    setupHostSelector(); // 调用函数设置主机选择器    setupPortSelector(); // 调用函数设置端口选择器}​AddressDialog::~AddressDialog() // 析构函数{    delete ui; // 删除 ui 指针以释放内存}​QString AddressDialog::remoteName() const // 获取远程名称的函数{    if (ui->addressSelector->count()) // 如果地址选择器有项        return ui->addressSelector->currentText(); // 返回当前选中的文本    return {}; // 否则返回一个空字符串}​quint16 AddressDialog::remotePort() const // 获取远程端口的函数{    return quint16(ui->portSelector->text().toUInt()); // 将端口选择器的文本转换为无符号整型}​void AddressDialog::setupHostSelector() // 设置主机选择器的函数{    QString name(QHostInfo::localHostName()); // 获取本地主机名    if (!name.isEmpty()) { // 如果主机名不为空        ui->addressSelector->addItem(name); // 将主机名添加到地址选择器        const QString domain = QHostInfo::localDomainName(); // 获取本地域名        if (!domain.isEmpty()) // 如果域名不为空            ui->addressSelector->addItem(name + QChar('.') + domain); // 添加主机名和域名的组合    }​    if (name != QStringLiteral("localhost")) // 如果主机名不是 "localhost"        ui->addressSelector->addItem(QStringLiteral("localhost")); // 添加 "localhost"​    const QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses(); // 获取所有 IP 地址    for (const QHostAddress &ipAddress : ipAddressesList) { // 遍历 IP 地址列表        if (!ipAddress.isLoopback()) // 如果 IP 地址不是回环地址            ui->addressSelector->addItem(ipAddress.toString()); // 添加 IP 地址到选择器    }​    ui->addressSelector->insertSeparator(ui->addressSelector->count()); // 在选择器中插入分隔符​    for (const QHostAddress &ipAddress : ipAddressesList) { // 再次遍历 IP 地址列表        if (ipAddress.isLoopback()) // 如果是回环地址            ui->addressSelector->addItem(ipAddress.toString()); // 添加回环地址到选择器    }}​void AddressDialog::setupPortSelector() // 设置端口选择器的函数{    // 设置端口选择器的验证器,范围为 0 到 quint16 的最大值    ui->portSelector->setValidator(new QIntValidator(0, std::numeric_limits<quint16>::max(),                                                     ui->portSelector));    ui->portSelector->setText(QStringLiteral("22334")); // 将端口选择器的文本设置为默认值 "22334"}​

08、association.h

>>>

代码语言:javascript
复制
#ifndef ASSOCIATION_H // 如果没有定义 ASSOCIATION_H#define ASSOCIATION_H // 定义 ASSOCIATION_H,避免重复包含​#include <QtNetwork> // 包含 Qt 网络模块的头文件#include <QtCore> // 包含 Qt 核心模块的头文件​//! [0]class DtlsAssociation : public QObject // 定义 DtlsAssociation 类,继承自 QObject{    Q_OBJECT // 宏,启用 Qt 的信号和槽机制​public:    // 构造函数,接受一个 QHostAddress、一个端口号和一个连接名称    DtlsAssociation(const QHostAddress &address, quint16 port,                    const QString &connectionName);    ~DtlsAssociation(); // 析构函数    void startHandshake(); // 启动 TLS 握手的公共函数声明​signals: // 信号部分,用于向外部发送消息    void errorMessage(const QString &message); // 错误消息信号    void warningMessage(const QString &message); // 警告消息信号    void infoMessage(const QString &message); // 信息消息信号    void serverResponse(const QString &clientInfo, const QByteArray &datagraam,                        const QByteArray &plainText); // 服务器响应信号,包含客户端信息、数据报和明文​private slots: // 槽函数部分,处理信号的响应    void udpSocketConnected(); // UDP 套接字连接的槽函数    void readyRead(); // 数据可读的槽函数    void handshakeTimeout(); // 握手超时的槽函数    void pskRequired(QSslPreSharedKeyAuthenticator *auth); // 需要预共享密钥的槽函数    void pingTimeout(); // Ping 超时的槽函数​private:    QString name; // 连接名称    QUdpSocket socket; // UDP 套接字    QDtls crypto; // DTLs 加密对象​    QTimer pingTimer; // Ping 定时器    unsigned ping = 0; // Ping 计数器​    Q_DISABLE_COPY(DtlsAssociation) // 禁用拷贝构造函数和拷贝赋值运算符,以避免意外复制};//! [0]​#endif // ASSOCIATION_H // 结束条件编译指令,确保避免重复定义​

09、association.cpp

>>>

代码语言:javascript
复制
/*
* 提示:该行代码过长,系统自动注释不进行高亮。一键复制会移除系统注释 
* #include "association.h" // 包含自定义的 association 头文件​DtlsAssociation::DtlsAssociation(const QHostAddress &address, quint16 port,                                 const QString &connectionName) // 构造函数,接受地址、端口和连接名称    : name(connectionName), // 初始化名称    crypto(QSslSocket::SslClientMode) // 初始化加密对象为客户端模式{    //! [1]    auto configuration = QSslConfiguration::defaultDtlsConfiguration(); // 获取默认的 DTLS 配置    configuration.setPeerVerifyMode(QSslSocket::VerifyNone); // 设置对等体验证模式为不验证    crypto.setPeer(address, port); // 设置对等体地址和端口    crypto.setDtlsConfiguration(configuration); // 设置 DTLS 配置    //! [1]​    //! [2]    connect(&crypto, &QDtls::handshakeTimeout, this, &DtlsAssociation::handshakeTimeout); // 连接握手超时信号    //! [2]    connect(&crypto, &QDtls::pskRequired, this, &DtlsAssociation::pskRequired); // 连接需要预共享密钥的信号    //! [3]    socket.connectToHost(address.toString(), port); // 连接到指定地址和端口    //! [3]    //! [13]    connect(&socket, &QUdpSocket::readyRead, this, &DtlsAssociation::readyRead); // 连接 UDP 套接字准备读信号    //! [13]    //! [4]    pingTimer.setInterval(5000); // 设置 Ping 定时器间隔为5000毫秒    connect(&pingTimer, &QTimer::timeout, this, &DtlsAssociation::pingTimeout); // 连接 Ping 超时信号    //! [4]}​//! [12]DtlsAssociation::~DtlsAssociation() // 析构函数{    if (crypto.isConnectionEncrypted()) // 如果连接是加密的        crypto.shutdown(&socket); // 关闭加密连接}//! [12]​//! [5]void DtlsAssociation::startHandshake() // 启动握手的函数{    if (socket.state() != QAbstractSocket::ConnectedState) { // 如果套接字状态不是已连接状态        emit infoMessage(tr("%1: connecting UDP socket first ...").arg(name)); // 发送信息消息,提示先连接 UDP 套接字        connect(&socket, &QAbstractSocket::connected, this, &DtlsAssociation::udpSocketConnected); // 连接套接字连接信号        return; // 返回    }​    if (!crypto.doHandshake(&socket)) // 如果握手失败        emit errorMessage(tr("%1: failed to start a handshake - %2").arg(name, crypto.dtlsErrorString())); // 发送错误消息    else        emit infoMessage(tr("%1: starting a handshake").arg(name)); // 发送信息消息,提示开始握手}//! [5]​void DtlsAssociation::udpSocketConnected() // UDP 套接字连接后的处理函数{    emit infoMessage(tr("%1: UDP socket is now in ConnectedState, continue with handshake ...").arg(name)); // 发送信息消息,提示 UDP 套接字已连接,继续握手    startHandshake(); // 启动握手}​void DtlsAssociation::readyRead() // 准备读取数据的函数{    if (socket.pendingDatagramSize() <= 0) { // 如果待处理的数据报大小小于等于0        emit warningMessage(tr("%1: spurious read notification?").arg(name)); // 发送警告消息,提示虚假的读取通知        return; // 返回    }​    //! [6]    QByteArray dgram(socket.pendingDatagramSize(), Qt::Uninitialized); // 创建一个数据报数组    const qint64 bytesRead = socket.readDatagram(dgram.data(), dgram.size()); // 读取数据报    if (bytesRead <= 0) { // 如果读取的字节小于等于0        emit warningMessage(tr("%1: spurious read notification?").arg(name)); // 发送警告消息,提示虚假的读取通知        return; // 返回    }​    dgram.resize(bytesRead); // 调整数据报大小    //! [6]    //! [7]    if (crypto.isConnectionEncrypted()) { // 如果连接是加密的        const QByteArray plainText = crypto.decryptDatagram(&socket, dgram); // 解密数据报        if (plainText.size()) { // 如果解密后的明文大小不为空            emit serverResponse(name, dgram, plainText); // 发送服务器响应            return; // 返回        }​        if (crypto.dtlsError() == QDtlsError::RemoteClosedConnectionError) { // 如果遇到远程关闭连接的错误            emit errorMessage(tr("%1: shutdown alert received").arg(name)); // 发送错误消息,提示收到关闭警告            socket.close(); // 关闭套接字            pingTimer.stop(); // 停止 Ping 定时器            return; // 返回        }​        emit warningMessage(tr("%1: zero-length datagram received?").arg(name)); // 发送警告消息,提示接收到零长度数据报    } else {        //! [7]        //! [8]        if (!crypto.doHandshake(&socket, dgram)) { // 如果握手失败            emit errorMessage(tr("%1: handshake error - %2").arg(name, crypto.dtlsErrorString())); // 发送错误消息,提示握手错误            return; // 返回        }        //! [8]​        //! [9]        if (crypto.isConnectionEncrypted()) { // 如果连接是加密的            emit infoMessage(tr("%1: encrypted connection established!").arg(name)); // 发送信息消息,提示加密连接已建立            pingTimer.start(); // 启动 Ping 定时器            pingTimeout(); // 处理 Ping 超时        } else {            //! [9]            emit infoMessage(tr("%1: continuing with handshake ...").arg(name)); // 发送信息消息,提示继续握手        }    }}​//! [11]void DtlsAssociation::handshakeTimeout() // 握手超时的函数{    emit warningMessage(tr("%1: handshake timeout, trying to re-transmit").arg(name)); // 发送警告消息,提示握手超时,尝试重新传输    if (!crypto.handleTimeout(&socket)) // 如果处理超时失败        emit errorMessage(tr("%1: failed to re-transmit - %2").arg(name, crypto.dtlsErrorString())); // 发送错误消息,提示重新传输失败}//! [11]​//! [14]void DtlsAssociation::pskRequired(QSslPreSharedKeyAuthenticator *auth) // 需要预共享密钥的函数{    Q_ASSERT(auth); // 确保 auth 不是 nullptr​    emit infoMessage(tr("%1: providing pre-shared key ...").arg(name)); // 发送信息消息,提示提供预共享密钥    auth->setIdentity(name.toLatin1()); // 设置身份为连接名称    auth->setPreSharedKey(QByteArrayLiteral("\x1a\x2b\x3c\x4d\x5e\x6f")); // 设置预共享密钥}//! [14]​//! [10]void DtlsAssociation::pingTimeout() // Ping 超时的函数{    static const QString message = QStringLiteral("I am %1, please, accept our ping %2"); // 定义 Ping 消息模板    const qint64 written = crypto.writeDatagramEncrypted(&socket, message.arg(name).arg(ping).toLatin1()); // 发送加密的 Ping 消息    if (written <= 0) { // 如果发送失败        emit errorMessage(tr("%1: failed to send a ping - %2").arg(name, crypto.dtlsErrorString())); // 发送错误消息,提示发送 Ping 失败        pingTimer.stop(); // 停止 Ping 定时器        return; // 返回    }​    ++ping; // 增加 Ping 计数}//! [10]​
*/

10、mainwindow.h

>>>

代码语言:javascript
复制
#ifndef MAINWINDOW_H // 如果没有定义 MAINWINDOW_H#define MAINWINDOW_H // 定义 MAINWINDOW_H,避免重复包含​#include <QMainWindow> // 包含 QMainWindow 头文件,用于创建主窗口#include <QSharedPointer> // 包含 QSharedPointer 头文件,用于使用共享指针#include <QList> // 包含 QList 头文件,用于使用 QList 容器​QT_BEGIN_NAMESPACE // 开始 Qt 命名空间namespace Ui { // 创建 Ui 命名空间class MainWindow; // 前向声明 MainWindow 类}QT_END_NAMESPACE // 结束 Qt 命名空间​class QHostAddress; // 前向声明 QHostAddress 类class QHostInfo; // 前向声明 QHostInfo 类​class DtlsAssociation; // 前向声明 DtlsAssociation 类​class MainWindow : public QMainWindow // 定义 MainWindow 类,继承自 QMainWindow{    Q_OBJECT // 宏,启用 Qt 的信号和槽机制​public:    explicit MainWindow(QWidget *parent = nullptr); // 构造函数,接受一个 QWidget 指针作为父对象    ~MainWindow(); // 析构函数​private slots: // 槽函数部分    void addErrorMessage(const QString &message); // 添加错误信息的槽函数    void addWarningMessage(const QString &message); // 添加警告信息的槽函数    void addInfoMessage(const QString &message); // 添加信息的槽函数    void addServerResponse(const QString &clientInfo, const QByteArray &datagram,                           const QByteArray &plainText); // 添加服务器响应的槽函数​    void on_connectButton_clicked(); // 连接按钮点击的槽函数    void on_shutdownButton_clicked(); // 关闭按钮点击的槽函数​    void lookupFinished(const QHostInfo &hostInfo); // 查询完成的槽函数​private:    void updateUi(); // 更新用户界面的私有函数声明    void startNewConnection(const QHostAddress &address); // 启动新连接的私有函数声明​    Ui::MainWindow *ui = nullptr; // 指向 Ui::MainWindow 的指针,初始化为 nullptr​    using AssocPtr = QSharedPointer<DtlsAssociation>; // 定义共享指针类型别名    QList<AssocPtr> connections; // 连接的列表,存储 DtlsAssociation 的共享指针​    QString nameTemplate; // 名称模板    unsigned nextId = 0; // 下一个 ID​    quint16 port = 0; // 端口号    int lookupId = -1; // 查询 ID,初始化为 -1};​#endif // MAINWINDOW_H // 结束条件编译指令,确保避免重复定义​

11、mainwindow.cpp

>>>

代码语言:javascript
复制
#include <QtCore> // 包含 Qt 核心模块#include <QtNetwork> // 包含 Qt 网络模块​#include "addressdialog.h" // 包含地址对话框头文件#include "association.h" // 包含关联头文件#include "mainwindow.h" // 包含主窗口头文件#include "ui_mainwindow.h" // 包含用户界面头文件​#include <utility> // 包含工具模块,用于 std::move​MainWindow::MainWindow(QWidget *parent) // 构造函数,接受一个 QWidget 指针作为父对象    : QMainWindow(parent), // 初始化父类 QMainWindow    ui(new Ui::MainWindow), // 初始化 ui 指针    nameTemplate(QStringLiteral("Alice (clone number %1)")) // 初始化名称模板{    ui->setupUi(this); // 设置用户界面    updateUi(); // 更新用户界面}​MainWindow::~MainWindow() // 析构函数{    delete ui; // 删除 ui 指针以释放内存}​//! [0]​const QString colorizer(QStringLiteral("<font color=\"%1\">%2</font><br>")); // 定义一个消息字体颜色化的常量​void MainWindow::addErrorMessage(const QString &message) // 添加错误消息的函数{    ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("Crimson"), message)); // 插入错误消息}​void MainWindow::addWarningMessage(const QString &message) // 添加警告消息的函数{    ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("DarkOrange"), message)); // 插入警告消息}​void MainWindow::addInfoMessage(const QString &message) // 添加信息消息的函数{    ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("DarkBlue"), message)); // 插入信息消息}​void MainWindow::addServerResponse(const QString &clientInfo, const QByteArray &datagram,                                   const QByteArray &plainText) // 添加服务器响应的函数{    static const QString messageColor = QStringLiteral("DarkMagenta"); // 定义消息颜色    static const QString formatter = QStringLiteral("<br>---------------"                                                    "<br>%1 received a DTLS datagram:<br> %2"                                                    "<br>As plain text:<br> %3"); // 定义格式化字符串​    const QString html = formatter.arg(clientInfo, QString::fromUtf8(datagram.toHex(' ')),                                       QString::fromUtf8(plainText)); // 创建响应消息的 HTML    ui->serverMessages->insertHtml(colorizer.arg(messageColor, html)); // 插入服务器响应消息}​//! [0]​void MainWindow::on_connectButton_clicked() // 连接按钮点击的槽函数{    if (lookupId != -1) { // 如果正在进行主机查询        QHostInfo::abortHostLookup(lookupId); // 取消主机查询        lookupId = -1; // 重置查询 ID        port = 0; // 重置端口        updateUi(); // 更新用户界面        return; // 返回    }​    AddressDialog dialog; // 创建地址对话框实例    if (dialog.exec() != QDialog::Accepted) // 显示对话框,如果未被接受则返回        return;​    const QString hostName = dialog.remoteName(); // 获取远程主机名    if (hostName.isEmpty()) // 如果主机名为空        return addWarningMessage(tr("Host name or address required to connect")); // 添加警告消息,提示需要主机名或地址​    port = dialog.remotePort(); // 获取远程端口    QHostAddress remoteAddress; // 创建远程地址对象    if (remoteAddress.setAddress(hostName)) // 如果能将主机名转换为地址        return startNewConnection(remoteAddress); // 启动新连接​    addInfoMessage(tr("Looking up the host ...")); // 添加信息消息,提示正在查找主机    lookupId = QHostInfo::lookupHost(hostName, this, SLOT(lookupFinished(QHostInfo))); // 开始异步主机查找    updateUi(); // 更新用户界面}​void MainWindow::updateUi() // 更新用户界面的函数{    ui->connectButton->setText(lookupId == -1 ? tr("Connect ...") : tr("Cancel lookup")); // 根据查询 ID 设置按钮文本    ui->shutdownButton->setEnabled(connections.size() != 0); // 启用/禁用关闭按钮}​void MainWindow::lookupFinished(const QHostInfo &hostInfo) // 主机查找完成的槽函数{    if (hostInfo.lookupId() != lookupId) // 如果返回的查找 ID 与当前的查找 ID 不匹配        return; // 返回​    lookupId = -1; // 重置查找 ID    updateUi(); // 更新用户界面​    if (hostInfo.error() != QHostInfo::NoError) { // 如果查找发生错误        addErrorMessage(hostInfo.errorString()); // 添加错误消息        return; // 返回    }​    const QList<QHostAddress> foundAddresses = hostInfo.addresses(); // 获取查找到的地址列表    if (foundAddresses.empty()) { // 如果地址列表为空        addWarningMessage(tr("Host not found")); // 添加警告消息,提示未找到主机        return; // 返回    }​    const auto remoteAddress = foundAddresses.at(0); // 获取第一个找到的地址    addInfoMessage(tr("Connecting to: %1").arg(remoteAddress.toString())); // 添加信息消息,提示正在连接    startNewConnection(remoteAddress); // 启动新连接}​void MainWindow::startNewConnection(const QHostAddress &address) // 启动新连接的函数{    AssocPtr newConnection(new DtlsAssociation(address, port, nameTemplate.arg(nextId))); // 创建新的 DtlsAssociation 实例    connect(newConnection.data(), &DtlsAssociation::errorMessage, this, &MainWindow::addErrorMessage); // 连接错误消息信号    connect(newConnection.data(), &DtlsAssociation::warningMessage, this, &MainWindow::addWarningMessage); // 连接警告消息信号    connect(newConnection.data(), &DtlsAssociation::infoMessage, this, &MainWindow::addInfoMessage); // 连接信息消息信号    connect(newConnection.data(), &DtlsAssociation::serverResponse, this, &MainWindow::addServerResponse); // 连接服务器响应信号    connections.push_back(std::move(newConnection)); // 将新连接添加到连接列表    connections.back()->startHandshake(); // 启动握手    updateUi(); // 更新用户界面​    ++nextId; // 增加下一个 ID}​void MainWindow::on_shutdownButton_clicked() // 关闭按钮点击的槽函数{    connections.clear(); // 清空连接列表    updateUi(); // 更新用户界面}​

总结

>>>

通过网盘分享的文件:secureudpclient 链接: https://pan.baidu.com/s/1txCWIo7-WhM-CjVkp_aDdg?pwd=13j9 提取码: 13j9 【一定要转存】

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 点击上方"蓝字"关注我们
  • 01、QUdpSocket
  • 02、QDtls
  • 03、QSharedPointer
  • 04、QHostInfo
  • 05、QIntValidator
  • 06、实战
  • 07、addressdialog.cpp
  • 08、association.h
  • 09、association.cpp
  • 10、mainwindow.h
  • 11、mainwindow.cpp
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档