简介:在Linux下借助QT框架编写连接Wi-Fi的程序,是桌面与嵌入式开发中常见的实用场景。这份资源正是为此准备的一份可运行源码工程,适合具备基础C++与QT知识、希望结合wpa_supplicant或NetworkManager构建无线网络交互界面的开发者。工程包共12个文件,以4个cpp实现逻辑、3个头文件声明类与接口、3个ui定义界面布局,另含pro工程文件和用户配置,整体仅9KB,结构精简便于逐文件阅读。代码通过QNetworkConfigurationManager完成热点扫描,并封装了密码输入、连接激活与状态反馈等典型流程,UI层与网络管理逻辑分离,便于扩展到其他Linux设备。目前已有4522人学习,是快速上手QT网络编程与Linux无线管理的轻量参考。
1. linux下QT编写的连接wifi程序:能扫描能连网,但坑比你想的多
做嵌入式或桌面Linux开发的人,早晚会遇到一个需求:给设备做一个WiFi管理界面,让用户不用敲命令行就能选网、输密码、连上去。用QT来做这件事是最常见的路线,但网上能找到的所谓“QT连接WiFi程序”大多只是把nmcli命令包了一层,能跑,离能用还差得远。这份资源的核心不是那些花哨的UI,而是把扫描、连接、状态回调、断线重连这几个环节在QT里串起来,并且处理了密码特殊字符、中文SSID、授权弹窗这些真实环境才会撞上的问题。适合刚入门QT网络编程的开发者,也适合正在做路由器管理页、工业触屏、车载中控这类需要WiFi配置界面的项目。它的性质更像一套工程骨架——拿到手先别急着改界面,把底层那套回调机制和状态机理顺了,你就能省掉好几天的调试时间。
2. Linux连接WiFi的三条技术路线:选型决定了你后面踩坑的数量
2.1 为什么说QProcess直接调nmcli只能算及格分
很多教程里的做法是直接用QProcess执行nmcli device wifi connect或nmcli dev wifi list,然后把标准输出解析出来塞进ListView。这个方案最快,两三行代码就能出结果,它在资源里也保留了完整实现。对于临时演示、内部工具、不需要长期维护的Demo来说,这个方案没有任何问题。
# 扫描可见WiFi,过滤掉重复行并去掉BSSID列 nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list | awk -F: '!seen[$1]++'-t表示以:作为字段分隔符的紧凑模式,-f指定只取SSID、信号强度和安全类型三列。awk里!seen[$1]++的作用是去重,因为路由器开启了双频段时会返回两个同名但不同BSSID的条目,界面上不需要重复展示。信号强度拿到的是一个0-100的整数,可以直接喂给进度条;SECURITY列如果显示为空,说明是开放网络。
但真实项目里这个方案有软肋。密码是作为命令行参数传进去的,在ps输出里直接明文暴露;SSID或密码里带$、空格、单引号时,拼接命令非常容易翻车;最麻烦的是连接是异步的,nmcli命令返回0不代表真的连上了,你还得轮询去查连接状态,体验很愚蠢。所以资源里的代码虽然保留了nmcli方案,但主推的是后面要说的D-Bus方案。
2.2 深入NetworkManager D-Bus接口:代码和系统服务对话的规范路子
NetworkManager自己提供了一个基于D-Bus的总线接口,QT有现成的QtDBus模块可以直接对接,不需要额外启动任何进程。连接流程就变成了:通过D-Bus向NetworkManager发送AddAndActivateConnection请求,系统服务自己处理无线网卡加网流程,完成后通过StateChanged信号回调通知你的界面。整个过程中密码只在D-Bus消息里传输,不经过shell,不会出现留言掉密码的问题。
从稳定性角度讲,D-Bus接口受nmcli命令行输出格式变化的影响非常小。nmcli的输出结构在NetworkManager的不同大版本之间经常调整,比如某些版本会额外打印设备的MAC或激活状态列,你写死的列序号就失效了。而D-Bus的信号名和属性路径是公开稳定API,不会因为小版本升级就改掉。
2.3 在QT里通过QProcess调用iwlist作为辅助兜底
有一个场景nmcli会显得很别扭:有些精简的嵌入式系统剪裁掉了NetworkManager,只保留了wireless-tools里的iwlist和wpa_supplicant。这时候你只能退回到iwlist扫描。这个方案在建立WiFi连接时需要手动写wpa_supplicant配置并管理进程,本质上就是自己实现了一遍NetworkManager,工作量很大。除非你的目标系统确实没有NetworkManager,否则不建议选这条路线。资源的代码里没有重点讲这一块,它们是作为接口预留的,方便你把同样的逻辑移植到没有NetworkManager的系统上。
3. 核心模块拆解:扫描、连接、状态机,一份可上手的QT代码路径
3.1 通过QtDBus实现WiFi扫描与结果回传
类设计上,资源把WiFi功能封装成一个单例服务WifiController,内部持有QDBusInterface负责与NetworkManager通信。扫描调用走的是WiFi设备对象的RequestScan方法。这里有一个很重要的点:RequestScan是异步的,调用后不能立刻读取结果,必须等系统服务广播AccessPointAdded信号后再拿数据。
接口以org.freedesktop.NetworkManager.Device.Wireless为准。声明接口的基本写法:
// wifi_controller.h class WifiController : public QObject { Q_OBJECT public: static WifiController* instance(); // 供界面调用的扫描入口,内部其实是非阻塞的 void startScan(); private: explicit WifiController(QObject *parent = nullptr); QDBusInterface *m_nmWifiDevIface; // 指向无线网卡设备对象 QDBusInterface *m_nmDevIface; // 指向通用设备对象 QDBusInterface *m_nmSettingsIface; // 用于创建连接配置 QString m_activeConnPath; // 当前激活连接的对象路径 signals: void scanFinished(QVariantList apList); // 扫描完成后把AP列表发给界面 void connectionChanged(int state); // 连接状态变化 void connectionFailed(const QString &ssid, const QString &reason); };参数说明:三个QDBusInterface分别对应设备上的三个不同接口路径。Wireless接口管扫描和AP属性读取;通用设备接口管连接激活;Settings接口用来创建持久化连接配置。如果没有分开持有这三个接口,会频繁出现“接口不支持该方法”的诡异报错,这是新手最容易犯的错误。
startScan的实现逻辑很简单,就是往RequestScan传一对空的键值选项:
// wifi_controller.cpp void WifiController::startScan() { if (!m_nmWifiDevIface || !m_nmWifiDevIface->isValid()) { emit scanFailed(); return; } // 参数是一组a{sv}字典,这里是空选项 QDBusMessage reply = m_nmWifiDevIface->call("RequestScan", QVariantMap()); if (reply.type() == QDBusMessage::ErrorMessage) { // 扫描已经在进行中时,部分版本会返回错误,这里忽略即可 qDebug() << "RequestScan error:" << reply.errorMessage(); return; } // 不要在这里立刻去读AP列表,需要等信号 }这里要解释一个关键细节:为什么RequestScan传QDBusMessage而不是直接用QDBusInterface的异步调用。因为手动构造QDBusMessage可以拿到ErrorMessage判断错误码。多频段网卡在切换信道的场景下,扫描请求可能返回“scan already in progress”的错误,这不算失败,只是告诉你上一次扫描还没结束。正确的处理方式是把这个错误忽略掉,等AccessPointAdded信号轮到最后一个AP时再通知界面刷新。
然后是第一层回调,连接信号:
void WifiController::onApAdded(const QDBusObjectPath &path, const QVariantMap &properties) { // 从AP对象上读取SSID、信号、安全模式 QString ssid = QString::fromUtf8(properties.value("Ssid").toByteArray()); int signal = properties.value("Strength").toInt(); uint securityFlag = properties.value("Flags").toUInt(); // 部分系统里隐藏WiFi的SSID是空的,这时用BSSID占位 if (ssid.isEmpty()) { ssid = properties.value("HwAddress").toString() + "_hidden"; } m_apList << ApInfo{ssid, signal, securityFlag, path.path()}; // 这里不能每次加一个AP就发一次界面刷新,要等"LastScan"属性更新后再发 if (properties.contains("LastScan")) { // 简单节流:延迟50毫秒再触发一次界面刷新 QTimer::singleShot(50, this, [this]() { emit scanFinished(m_apList); }); } }逻辑说明:这个回调函数接收的是NetworkManager广播的AP对象路径和属性集合,重点在于属性名必须取Ssid、Strength、Flags这三个字段,频率和信道等信息在这个阶段不必展示。QString::fromUtf8这次的强制转换是必须的——Ssid属性在D-Bus里是字节数组,不转的话中文SSID在界面上会变成乱码。最后那个LastScan判断是整个扫描流程的终止条件,等系统把当前信道扫完并更新了LastScan时间戳后,才允许刷新UI。
参数说明:QTimer::singleShot(50, ...)做一个简单合并,避免频繁触发界面重绘。你会注意到信号连接要在构造时就connect好。连接信号用的是老式语法:
connect(m_nmWifiDevIface, SIGNAL(AccessPointAdded(QDBusObjectPath, QVariantMap)), this, SLOT(onApAdded(QDBusObjectPath, QVariantMap)));这里特别说明一下:这里不用QOverload的模板形式,原因是AccessPointAdded是一组带DBus类型签名的特殊信号,模板形式在有些QT版本上匹配不上。
3.2 发起连接请求:AddAndActivateConnection的参数细节
核心的调用是AddAndActivateConnection,它需要你构造一个嵌套的QVariantMap来描述连接配置。许多到这里就断了的同学,十有八九是因为嵌套结构里Key大小写写错,或类型对不上。下面把参数完整展示:
bool WifiController::connectToAp(const ApInfo &ap, const QString &password) { // 第一层:connection,描述连接类型和权限 QVariantMap connectionMap; connectionMap["type"] = "802-11-wireless"; connectionMap["uuid"] = QUuid::createUuid().toString(); connectionMap["id"] = ap.ssid; // 显示名称,可以随意 // 第二层:802-11-wireless,定义SSID QVariantMap wirelessMap; wirelessMap["ssid"] = ap.ssid.toUtf8(); wirelessMap["mode"] = "infrastructure"; // 自动选择WPA/WPA2/WPA3,不写死具体加密协议 wirelessMap["security"] = "802-11-wireless-security"; // 第三层:无线安全,设置密码或密钥 QVariantMap securityMap; if (ap.securityFlag == NM_802_11_AP_SEC_NONE) { securityMap["key-mgmt"] = "none"; // 开放网络不需要密码 } else { securityMap["key-mgmt"] = "wpa-psk"; securityMap["psk"] = password; } QVariantMap settings; settings["connection"] = connectionMap; settings["802-11-wireless"] = wirelessMap; settings["802-11-wireless-security"] = securityMap; // 调用AddAndActivateConnection,参数是三个: // 1) 连接配置字典 2) 设备对象路径 3) 具体AP对象路径 QDBusMessage reply = m_nmSettingsIface->call("AddAndActivateConnection", settings, QVariant::fromValue(QDBusObjectPath(NM_DEVICE_OBJECT_PATH)), QVariant::fromValue(QDBusObjectPath(ap.apPath))); ... }键名必须精确到大小写。AddAndActivateConnection的三个参数一个都不能少——第三个参数就是上面扫描结果里记住的AP对象路径,如果填了空路径,系统会强制让你先手动选网络而不是直接连接。
再说回调。连接请求发出后,NetworkManager会先在内部创建配置、配置网卡、四次握手,整个流程需要几秒到十几秒不等。这期间界面必须监听StateChanged信号。激活状态码里最有用的是32(正在连接)、50(已连接)、20(连接失败):
void WifiController::onStateChanged(uint newState, uint /*oldState*/, uint /*reason*/) { switch (newState) { case 50: // NM_DEVICE_STATE_ACTIVATED emit connectionChanged(CONNECTED); break; case 20: // 失败,此时需要读取reason字段 emit connectionFailed(m_currentSsid, tr("认证失败或网络不可达")); break; case 32: // 正在连接,可更新UI状态 emit connectionChanged(CONNECTING); break; default: break; } }状态机处理要覆盖连接中和连接失败两种情况。用户连错密码时,其实会先进入32然后再退回20,如果你的界面在32时把输入框禁用掉了,用户会卡在“连接中”的状态里得不到反馈。更好的做法是:连接中禁用按钮而非禁用输入框,收到20信号后立即把输入框恢复,并把焦点定位在密码那一行。
3.3 界面和密码输入:连接按钮的完整交互逻辑
界面上选用QDialog做连接窗口。点击列表里某一项之后弹出输入框,密码输入框勾选echoMode = QLineEdit::Password。这里有一个细节值得写进笔记:用户在连接失败后重新输入密码时,界面要默认把上次输入的密码清掉。因为用户在多个WiFi之间切换时,错误密码会存留在输入框里,容易造成下一次连别的WiFi时误用了旧密码:
void WifiDialog::onConnectClicked() { QString ssid = m_ui->ssidLabel->text(); QString psk = m_ui->passwordEdit->text(); if (psk.isEmpty()) { m_ui->statusLabel->setText("请输入密码"); return; } // 把输入框立即禁用掉,防止用户在连接过程中再次点击 m_ui->connectBtn->setEnabled(false); emit requestConnect(ssid, psk); }参数说明:setEnabled(false)这一行是必须的。NetworkManager允许多个连接请求同时提交,如果你不锁住按钮,用户在慢网环境下连续点了两下,会触发两个连接请求,后一个会顶掉前一个,表现为“一直显示连接中但永远连不上”。锁完之后,在收到CONNECTED或FAILED状态时再重新setEnabled(true)恢复按钮。
4. 连接WiFi的五个常见坑:从密码翻车到中文SSID乱码
4.1 密码里的特殊字符导致nmcli连接失败
现象:用QProcess执行nmcli dev wifi connect SSID password 123456$abc时,终端里能连上,但QT里就是连接失败。
原因:$在QProcess里确实不会再做变量展开,但问题出在SSID含有空格时会分割参数;另外部分版本的nmcli会把密码里连续的反斜杠吃掉。
解决:如果坚持走nmcli路线,把命令拼成列表形式传入而不是拼字符串:
nmcli dev wifi connect "my wifi" password "123456$abc"4.2 扫描结果里中文SSID显示成乱码
现象:扫描列表里中文SSID显示为类似\xFF\xFE...的乱码,数据库里存的却是正常中文。
原因:NetworkManager通过D-Bus传递SSID时用的是UTF-8字节数组,但是你的QT应用没按UTF-8来处理这个字节数组,或者直接调用了QString::fromLocal8Bit。
解决:统一走QString::fromUtf8(byteArray)。很多代码里写的是toLatin1(),那种写法在默认编码是UTF-8的现代系统上没问题,但在旧的GBK环境的嵌入式Linux上就会翻车。这门资源里的代码强制全部用fromUtf8,就是为了避免依赖locale。
4.3 连接过程中界面卡死
现象:点击连接后,界面无响应约5秒,然后一次性弹出连接成功或失败的结果。
原因:代码里用了QDBusInterface::call()同步版本,连动的握手过程把QTimer事件循环给阻塞了。NetworkManager处理连接请求需要扫描信道、与AP四次握手,这个过程少则1秒多则10秒,同步等待时间难以接受。
解决:改用callWithCallback或把连接动作放到QtConcurrent::run里,通过信号回传结果。资源代码中音频模块那只用回调的就是这个原因。
QDBusPendingCall pcall = iface->asyncCall("AddAndActivateConnection", ...); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pcall, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w){ w->deleteLater(); // 连接结果在这里检查 });4.4 连接成功回调失败:始终停在“正在连接”
现象:使用密码正确的网络时,界面显示“正在连接”永远不跳转到“已连接”。
原因:只监听了Wireless对象的AccessPointAdded,没有监听设备级别的StateChanged。NetworkManager的连接过程是设备级别的,AP信号只是告诉你网卡发现了这个热点,后面真正的连接状态都是在设备对象上广播的。
解决:在代码里同时连接Wireless接口的AccessPointAdded信号和通用设备接口的StateChanged信号。两个信号都必须连,否则会出现扫得到但连不上的假象。
5. 进阶:封装成QML可调用的接口,加上自动重连和密码保存
如果只满足于对话框连WiFi,那这套资源的利用率还没拉满。更常见的需求是:主界面用QML写的,WiFi管理作为一个独立页面嵌入在里面。这时候QML不能直接调C++的普通方法,需要通过Q_PROPERTY或Q_INVOKABLE把接口暴露出去。资源里已经给出了这个封装层,我把核心思路复述一遍。
class WifiController : public QObject { Q_OBJECT Q_PROPERTY(QVariantList apList READ apList NOTIFY scanFinished) Q_PROPERTY(int connectionState READ connectionState NOTIFY connectionChanged) public: Q_INVOKABLE void startScan(); Q_INVOKABLE void connectToAp(QString ssid, QString password); Q_INVOKABLE void disconnectWifi(); };暴露成Q_INVOKABLE之后,QML里可以直接这样调用:
ListView { model: wifiController.apList delegate: Rectangle { Text { text: modelData.ssid } MouseArea { onClicked: { wifiController.connectToAp(modelData.ssid, passwordInput.text) } } } }这个封装的另一个价值在于:它让连接状态从“一次性操作”变成了“可观察属性”。主界面可以根据wifiController.connectionState === 50来决定是否显示网络图标,不需要在C++里写额外的通知逻辑。
自动重连是我自己补的一段逻辑,资源原始代码只是预留了接口。具体做法是监听StateChanged信号,一旦设备进入断开状态且满足“之前连接过”“网络名在历史列表里”“当前没有用户手动断开”三个条件,就自动调connectToAp重连。需要注意加一个退避策略:连续失败三次就放弃,隔30秒再重试,防止网卡在错误密码下反复折腾。
密码保存也有一个现成方案:用QSettings存到~/.config目录之外,因为WiFi密码是敏感信息,建议用libsecret或qtkeychain集成系统钥匙串。项目里默认的Demo用明文QSettings存储,那只是个演示。真实产品必须接钥匙串接口,我的习惯是留一个密码存储接口的抽象类,重写它的storePassword和loadPassword两个虚函数,方便在不同目标系统上切换实现。
第一次跑这套资源时,我拿着扫描列表直接点了自家的WiFi,密码输进去等了约十秒才看到连接成功,期间一度以为程序卡死了。那次的教训是:所有WiFi连接操作都必须有进度显示,哪怕是简单的“连接中…”三个字加转圈,也比静默等待要好得多。从那以后我每次写这类界面都强制把状态机拆成明确的四态——空闲、扫描中、连接中、已连接——并确保每一个耗时操作都绑定了对应的界面变化。希望帮到你。
本文还有配套的精品资源,点击获取