简介:面向Linux平台Qt开发者与网络编程学习者的实战型示例工程,聚焦在Ubuntu 16.04环境下用Qt框架实现Wi-Fi扫描与连接这一常见需求。项目围绕QNetworkConfigurationManager获取可用热点、通过QNetworkConfiguration激活网络完成认证连接,并借助Qt信号槽机制驱动界面状态更新,涵盖Wi-Fi列表展示、密码输入与连接断开等交互环节,适合希望打通GUI与Linux网络管理底层机制的中级开发者参考。压缩包共12个文件,以4个cpp源文件、3个h头文件、3个ui界面文件为主,另含1个pro工程配置与1个user用户配置,整体约9KB,源码与界面分离、结构紧凑便于阅读。已有4524人学习下载,可据此理解扫描、连接与UI解耦的类设计思路,并作为跨平台网络应用开发的练手起点。
1. 一个把“连 WiFi”从命令行搬进界面的 Qt 小工程
在 Linux 上连 WiFi,多数人第一反应是敲nmcli或者改wpa_supplicant.conf,能用,但一旦要交付给非技术用户,命令行就成了黑匣子。这个资源是一份用 Qt 编写的 Linux WiFi 连接程序源码,核心思路是把 NetworkManager 的 D-Bus 接口包一层图形界面:扫描热点、输入密码、发起连接、回显状态,全在窗口里完成。它适合两类人——正在做嵌入式 Linux 或桌面 Linux 工具、需要把网络配置做成 GUI 的开发者,以及想学 Qt 与系统服务交互的从业者。下面按“它靠什么工作、怎么编译跑起来、参数怎么调、哪里会翻车”的顺序拆,能直接照着复现。
2. 原理选型:为什么走 NetworkManager 的 D-Bus 而不是直接调 wpa_supplicant
2.1 三条可选路线与取舍
Linux 下让程序控制 WiFi,常见做法有三条:直接读写wpa_supplicant的控制接口、调用nmcli命令行再解析输出、通过 D-Bus 调 NetworkManager 的 API。这个工程选的是第三条,理由很实际。
直接对接wpa_supplicant最底层,但你要自己管配置文件、处理wpa_cli的交互协议、还要在系统里保证 supplicant 以正确参数启动,移植到不同发行版时启动方式差异很大,血泪经验是换个系统就得重调一遍。调nmcli最省事,可它是文本输出,解析靠字符串匹配,一旦系统语言或版本变了,字段顺序和措辞都可能变,属于典型的玄学解析。D-Bus 路线则是 NetworkManager 官方暴露的结构化接口,方法、属性、信号都有明确签名,返回的是强类型数据,不依赖界面语言,也不怕输出格式微调。
代价是 D-Bus 本身有一层学习成本:对象路径、接口名、方法参数都得对上。但对一个要长期维护的 GUI 工具来说,这层成本一次性付清,后面加功能(比如显示信号强度、切换频段、记住历史网络)都是加方法调用,比反复修解析逻辑划算。所以选型结论是:只要目标系统跑的是 NetworkManager(主流桌面发行版和多数嵌入式方案默认都是),就走 D-Bus。
2.2 关键接口与对象路径
NetworkManager 在系统总线上的服务名是org.freedesktop.NetworkManager,根对象路径/org/freedesktop/NetworkManager。真正干活的几个接口:
| 接口 | 作用 | 典型用途 |
|---|---|---|
org.freedesktop.NetworkManager | 总管 | 查 WiFi 是否启用、取所有网络设备 |
org.freedesktop.NetworkManager.Device.Wireless | 无线设备 | 触发扫描、取扫描结果、取接入点列表 |
org.freedesktop.NetworkManager.AccessPoint | 单个热点 | 读 SSID、信号强度、加密标志 |
org.freedesktop.NetworkManager.Settings | 连接配置 | 新建/修改连接配置 |
org.freedesktop.NetworkManager.Device | 设备通用 | 发起 ActivateConnection |
流程是:先拿无线设备对象,调RequestScan触发扫描,等AccessPointAdded或扫描完成信号,遍历GetAccessPoints拿到每个 AP 的对象路径,再逐个读Ssid(注意是字节数组,要转成字符串)、Strength、Flags。用户选中某个 SSID 并输入密码后,用Settings接口的AddConnection建一份配置,最后调设备的ActivateConnection发起连接,靠StateChanged信号回显进度。
2.3 Qt 侧怎么接 D-Bus
Qt 自带QtDBus模块,两种用法:QDBusInterface动态调用,或者QDBusAbstractInterface生成静态代理类。这个工程用前者居多,因为接口调用点分散、不需要为每个接口生成类。核心是构造QDBusInterface(service, path, interface, QDBusConnection::systemBus()),然后call("方法名", 参数...)。注意必须用系统总线,不是会话总线——NetworkManager 跑在系统总线上,用错总线会一直返回“服务不存在”,这是新手最常见的翻车点之一。
// 取无线设备对象路径,再触发一次扫描 QDBusInterface nm("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", QDBusConnection::systemBus()); // GetDevices 返回所有设备路径,需再判断哪些是无线设备 QDBusReply<QList<QDBusObjectPath>> devReply = nm.call("GetDevices"); if (!devReply.isValid()) { qWarning() << "取设备失败:" << devReply.error().message(); return; } for (const QDBusObjectPath &p : devReply.value()) { QDBusInterface dev("org.freedesktop.NetworkManager", p.path(), "org.freedesktop.NetworkManager.Device", QDBusConnection::systemBus()); // DeviceType == 2 表示 WiFi if (dev.property("DeviceType").toUInt() == 2) { QDBusInterface wifi("org.freedesktop.NetworkManager", p.path(), "org.freedesktop.NetworkManager.Device.Wireless", QDBusConnection::systemBus()); wifi.call("RequestScan", QVariantMap()); // 空 map 表示默认扫描参数 } }这段代码的逻辑是:先通过总管接口拿设备列表,再按DeviceType过滤出无线设备,最后对无线设备调RequestScan。参数QVariantMap()是扫描选项,留空即默认行为;如果只想扫特定频段,可以传{"ssids": ...}之类的键,但多数场景留空就够。QDBusReply的isValid()判断不能省,D-Bus 调用失败不会抛异常,只会静默返回无效值,不检查就会拿到空列表还以为“附近没热点”。
3. 从源码到可运行:编译、权限与界面联调
3.1 依赖与构建配置
工程依赖 Qt5 的 Core、Widgets、DBus 三个模块。.pro文件里至少要写:
QT += core gui widgets dbus CONFIG += c++11 TARGET = wifitool SOURCES += main.cpp mainwindow.cpp wifimanager.cpp HEADERS += mainwindow.h wifimanager.h如果用的是 CMake,对应find_package(Qt5 COMPONENTS Core Widgets DBus REQUIRED)再target_link_libraries挂上Qt5::DBus。构建命令就是常规的qmake && make或cmake -B build && cmake --build build。这里没有隐藏依赖,不需要额外装 NetworkManager 的开发包——D-Bus 调用是运行时通过总线完成的,编译期只依赖 QtDBus。
3.2 权限:为什么普通用户调不通
这是整个工程最容易卡住的地方。NetworkManager 的 D-Bus 接口受 PolicyKit 管控,普通用户直接调AddConnection、ActivateConnection这类写操作,会返回org.freedesktop.NetworkManager.NotAuthorized。读操作(扫描、读 AP 列表)通常放行,写操作不放行。
常见做法有两条:一是让程序以 root 运行,简单粗暴但 GUI 程序跑 root 不推荐;二是配置 PolicyKit 规则,允许特定用户组执行网络配置。后者更规范,在/etc/polkit-1/rules.d/下放一条规则,允许netdev组用户操作 NetworkManager 的连接设置。规则文件内容大致是判断action.id属于org.freedesktop.NetworkManager.settings.modify.system时返回YES。改完记得重启 polkit 服务或重新登录,规则不会热加载。
提示:调试阶段可以先临时用
sudo跑一次确认逻辑通不通,确认是权限问题而不是代码问题,再去配 PolicyKit,别一上来就怀疑 D-Bus 签名写错了。
3.3 扫描结果到界面列表的映射
扫描是异步的,RequestScan只是发起请求,结果要等信号。工程里的做法是监听AccessPointAdded信号,每来一个就往列表里加一项;同时监听ScanDone(部分版本是设备属性LastScan变化)来收尾。界面侧用一个QListWidget或自定义 model,每项存 SSID、信号强度、是否加密、AP 对象路径。
// 连接信号:新 AP 出现时刷新列表 QDBusConnection::systemBus().connect( "org.freedesktop.NetworkManager", wifiPath, "org.freedesktop.NetworkManager.Device.Wireless", "AccessPointAdded", this, SLOT(onApAdded(QDBusObjectPath))); void MainWindow::onApAdded(const QDBusObjectPath &ap) { QDBusInterface iface("org.freedesktop.NetworkManager", ap.path(), "org.freedesktop.NetworkManager.AccessPoint", QDBusConnection::systemBus()); // Ssid 是 QByteArray,直接 toString 可能乱码,按 UTF-8 转 QString ssid = QString::fromUtf8(iface.property("Ssid").toByteArray()); uint strength = iface.property("Strength").toUInt(); // 0-100 if (ssid.isEmpty()) return; // 隐藏 SSID 跳过 addToList(ssid, strength, ap.path()); }Ssid属性类型是字节数组不是字符串,这是 D-Bus 接口定义决定的,直接toString()在含中文或特殊字符的 SSID 上会出乱码,必须fromUtf8。Strength是 0 到 100 的整数,可以直接拿来画信号格。隐藏 SSID 的 AP 其Ssid为空,界面上要么过滤要么单独提供手动输入入口,否则用户会看到一个空白条目。
3.4 发起连接:配置对象与激活
连接分两步。第一步用Settings.AddConnection提交一份连接配置字典,里面至少要有connection(id、type=802-11-wireless)、802-11-wireless(ssid、mode=infrastructure)、802-11-wireless-security(key-mgmt=wpa-psk、psk=密码)。第二步拿返回的连接路径调ActivateConnection。
QVariantMap conn, wifi, sec; conn["id"] = ssid; conn["type"] = "802-11-wireless"; wifi["ssid"] = ssid.toUtf8(); wifi["mode"] = "infrastructure"; sec["key-mgmt"] = "wpa-psk"; sec["psk"] = password; QVariantMap settings; settings["connection"] = conn; settings["802-11-wireless"] = wifi; settings["802-11-wireless-security"] = sec; QDBusInterface settingsIface("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings", "org.freedesktop.NetworkManager.Settings", QDBusConnection::systemBus()); QDBusReply<QDBusObjectPath> addReply = settingsIface.call("AddConnection", settings); if (!addReply.isValid()) { showError(addReply.error().message()); // 常见:NotAuthorized / InvalidConnection return; } // 再用设备接口 ActivateConnection(连接路径, 设备路径, 特定对象)参数说明:ssid必须转成QByteArray再塞进 map,直接传QString在部分 Qt 版本上会被当成字符串类型,NetworkManager 侧校验不过。key-mgmt除了wpa-psk还有wpa-eap(企业级)、none(开放网络),开放网络要省略整个 security 段而不是传空密码。AddConnection失败时错误信息里NotAuthorized是权限问题,InvalidConnection多半是字段名拼错或类型不对,对着nm-settings的字段表逐项核。
4. 避坑与排查:五个真实会卡住人的点
4.1 现象:程序启动就报“服务不存在”,扫描永远空
原因:用了QDBusConnection::sessionBus()而不是systemBus()。NetworkManager 只注册在系统总线上,会话总线上查不到这个服务名。
解决:全局搜一遍sessionBus,凡是访问 NetworkManager 的接口全部改成systemBus。可以用qdbus命令先验证:qdbus --system org.freedesktop.NetworkManager能列出对象就说明总线对了。
4.2 现象:扫描有结果,点连接没反应,日志里是 NotAuthorized
原因:PolicyKit 没放行写操作,普通用户只能读不能改。
解决:按 3.2 配规则,或者调试期临时sudo验证。注意规则文件里的 action id 要写全,少一段前缀匹配不上,改完必须重新登录会话才生效。
4.3 现象:中文 SSID 显示成乱码或问号
原因:Ssid属性是QByteArray,用toString()走的是 Latin-1 转换。
解决:统一QString::fromUtf8(...toByteArray())。另外提交连接时wifi["ssid"]也要传toUtf8()的字节数组,两头都按 UTF-8 对齐。
4.4 现象:连接成功但界面状态一直停在“连接中”
原因:只监听了AccessPointAdded,没监听设备或连接的StateChanged信号,状态机没闭环。
解决:连上org.freedesktop.NetworkManager.Device的StateChanged(uint, uint, uint),按新状态值映射文案(如 100 已连接、70 正在获取 IP、120 失败)。失败时第三个参数是原因码,可以查表给出可读提示,比干等强。
4.5 现象:换一台机器编译通过,运行时段错误或接口调用全失败
原因:不同发行版的 NetworkManager 版本不同,部分属性或方法在老版本上不存在,QDBusInterface构造时不会报错,调用时才失败。
解决:调用前用interface.isValid()判断接口是否可用,属性读取用property()后检查isValid()。对可选功能做降级,比如老版本没有某个扫描参数就退回默认调用。别假设所有机器接口一致,这是跨发行版交付的基本功。
5. 进阶:把连接状态做成可观测的,而不是靠猜
基础版能连上就完事,但交付给别人用,最怕的是“点了没反应也不知道为什么”。我的做法是给这个工程加一层状态回显:把StateChanged的原因码翻译成人话,同时在界面上留一个可折叠的日志区,把每次 D-Bus 调用的方法名、返回是否有效、错误消息打进去。这样用户截图反馈问题时,你一眼能看出是权限、密码错还是设备没就绪。
原因码映射可以做成一张小表,常用的几个:
| 状态值 | 含义 | 界面提示 |
|---|---|---|
| 70 | 正在获取 IP | 正在获取地址,请稍候 |
| 100 | 已连接 | 连接成功 |
| 110 | 连接中断 | 连接已断开 |
| 120 | 连接失败 | 连接失败,请检查密码或信号 |
验证方法也简单:连一个错误密码的热点,看是否走到 120 并给出提示;连上后手动nmcli device disconnect断开,看界面是否收到 110 并刷新列表。这两步走通,状态机基本就闭环了。
还有一个容易忽略的点是清理旧配置。每次AddConnection都会在系统里留一份连接配置,反复试错会攒一堆同名连接,下次连接时 NetworkManager 可能复用旧的错误配置。我一般会在连接失败后调Settings的DeleteConnection把刚建的那份删掉,或者在新建前先按 SSID 查一遍已有连接、有就复用或先删。这个习惯是从一次“密码明明改对了却一直连不上”的排查里换来的——最后发现是旧配置在作祟。
从那以后我每次改这类网络工具,都会先把“失败路径”跑一遍:错密码、断信号、无权限、旧配置残留,四个场景全过一遍才敢交付。希望这些能帮到你,少走几段我踩过的弯路。
本文还有配套的精品资源,点击获取