Qt触摸屏Wi-Fi配置:扫描连接与软键盘源码实践
简介面向 Linux 平台 Qt 桌面开发者的 Wi-Fi 管理示例工程核心通过系统调用 wpa_supplicant 服务完成附近无线网络的搜索、列表展示与连接适合需要为嵌入式或桌面应用补全网络配置模块的初中级开发者参考。工程支持手动刷新扫描结果界面最多呈现 20 个热点连接环节兼容 WEP、WPA2 等加密方式密码输入框可调出软键盘对触摸屏一体机、嵌入式控制面板等无实体键盘场景较友好。压缩包共 21 个文件约 2.94MB以 cpp 与 h 源码为主体配合 ui 界面文件、qrc 资源及 pro 工程配置构成可直接梳理的 Qt 工程骨架。工程将主界面、Wi-Fi 设置页、输入框定制与软键盘模块分开组织便于按界面层、业务层和系统调用层逐段阅读。已有 1014 人学习可据此了解 wpa_supplicant 调用方式、界面与逻辑分层、软键盘接入思路并作为二次开发起点。1. 触摸屏上的 Wi-Fi 配置页为什么它比想象中难缠一块没有物理键盘的触摸屏最容易被低估的页面就是 Wi-Fi 搜索与连接。Qt 工程里要把无线网卡扫描、热点列表展示、密码输入、连接回显和软键盘弹出串成一条链路任何一个环节掉链子现场人员就只能对着“正在连接”干瞪眼。这份源码包做的事很具体用 Qt 做界面调系统无线工具完成 Wi-Fi 搜索与连接点密码框自动弹出自绘软键盘适合做工业触屏、自助终端、车载中控和实验室设备的开发者。它不是纯 UI Demo难点在进程调用、状态同步和触摸输入。2. 技术选型拆解Qt Widgets、QProcess 与无线网络工具链拿到这份工程先别急着编译。先看它把哪些事交给 Qt哪些事交给系统。Qt 负责界面、事件分发、进程封装和信号槽无线部分不是 Qt 自带能力必须调系统里的无线网络工具。这个边界划清楚后面换平台、换后端、加功能才不会乱。2.1 为什么用 Qt Widgets 而不是 QML这份工程走的是 Qt Widgets 路线界面元素以QWidget、QListWidget、QLineEdit、QPushButton为主。Wi-Fi 列表需要频繁刷新密码输入需要控制回显模式软键盘需要跟踪焦点这些用 Widgets 写起来直接C 和界面之间的调用也少一层胶水。QML 也能做同样的页面而且动画和分辨率适配更舒服Qt Virtual Keyboard 也现成。但如果设备端只需要一个固定分辨率的配置页Widgets 的依赖更少交叉编译时少带一套 QML 运行时排查问题也更容易落到具体控件上。我的习惯是页面简单、交互固定、要和系统命令强耦合就先上 Widgets页面复杂、需要多尺寸适配再考虑 QML。Widgets 路线里几个关键点要提前定好。热点列表用QListWidget还是QTableView取决于要不要排序和过滤密码框用QLineEdit并设置EchoMode::Password页面切换用QStackedWidget软键盘不要做成主窗口的子控件而是独立顶层窗口否则很容易被布局裁掉。把这些定下来后面的代码才不会边写边改。2.2 无线接口的三条路线nmcli、wpa_cli 与系统 APIQt 本身不提供 Wi-Fi 扫描和连接接口工程里常见的做法是调系统命令行工具。三条路线各有适用场景路线调用方式优点需要注意nmcliQProcess调命令行输出可读、开发快、适合原型和固定设备依赖 NetworkManager字段格式随系统版本变化wpa_cli交互式命令控制细、适合 wpa_supplicant 环境需要自己解析状态交互式读写更麻烦系统 APID-Bus、ConnMan 或厂商 SDK无需 shell权限模型更清晰平台绑定强移植成本高这份工程从标题和功能上看走的是命令行路线扫描热点、拿列表、发起连接、读返回结果。nmcli的优点是输出适合人看也适合程序切分缺点是它把很多状态藏在退出码和标准错误里不能只看标准输出。wpa_cli更底层适合没有 NetworkManager 的精简系统但交互式命令要求程序持续读写标准输入代码量会上去。我一般会先写一个WifiBackend抽象把“扫描”“连接”“断开”“查询状态”四个动作定成接口第一版用nmcli实现。这样设备换系统时上层 UI 不用动只换后端。这个思路在二次开发里很值钱后面第 6 章会展开。2.3 软键盘的两种落地方式自定义 QWidget 与 Qt Virtual Keyboard软键盘有两种常见做法。第一种是自绘QWidget每个按键就是一个QPushButton按下后往当前焦点输入框里插字符。第二种是用 Qt Virtual Keyboard现成的输入法框架支持多语言和候选词但需要额外编译模块还要处理输入法插件和焦点策略。Wi-Fi 密码通常只包含大小写字母、数字和少量符号不需要中文候选词。这个场景下自绘键盘更轻按键布局可以按设备屏幕定制回车键还能直接触发“连接”。代价是符号页、Shift 状态、长按删除这些细节要自己写。Qt Virtual Keyboard 适合需要中文输入或多语言输入的项目但第一次在嵌入式板子上把它跑起来编译和部署的坑不少。提示软键盘不要用QDialog的默认模态方式弹模态窗口会把焦点从输入框抢走导致“键盘弹出来了但字打不进去”。用独立顶层窗口加Qt::Tool和WA_ShowWithoutActivating更稳。3. 核心模块拆解从扫描到连接的一条完整链路这份工程的代码可以按职责拆成三层core管无线网络状态和命令行进程ui管列表、密码框和软键盘app只做组装和信号连接。三层分开以后扫描失败不会把界面逻辑搅乱软键盘也不会反过来依赖 Wi-Fi 模块。3.1 目录结构把 Wi-Fi 状态、列表模型和键盘分开一个可维护的目录结构大概是这样wifi-keyboard/ ├── app/ │ ├── main.cpp │ ├── mainwindow.cpp │ └── mainwindow.h ├── core/ │ ├── wifimanager.cpp │ ├── wifimanager.h │ └── wifiitem.h ├── ui/ │ ├── softkeyboard.cpp │ ├── softkeyboard.h │ └── keyboardfilter.cpp ├── resources/ │ └── qss/ └── CMakeLists.txtcore/wifiitem.h只放数据结构比如ssid、signal、security、connected。core/wifimanager继承QObject对外发scanFinished、scanFailed、connectStateChanged信号不直接碰任何按钮和列表。ui/softkeyboard只认QLineEdit不关心这个输入框是密码框还是搜索框。app/mainwindow负责把两者接起来。这种拆法有一个实际好处桌面调试时可以把WifiManager换成模拟实现界面照常跑设备上再换回真实命令。UI 不需要知道背后是nmcli还是wpa_cli。3.2 扫描无线网络QProcess 调 nmcli 并解析输出扫描用nmcli的 terse 模式字段之间用冒号分隔。SSID 里可能本身带冒号nmcli会用反斜杠转义所以不能简单split(:)。// core/wifimanager.cpp void WifiManager::scanAsync() { if (m_proc.state() ! QProcess::NotRunning) { m_proc.kill(); m_proc.waitForFinished(500); } m_scanBuffer.clear(); m_proc.setProgram(nmcli); m_proc.setArguments({ -t, -f, SSID,SIGNAL,SECURITY, dev, wifi, list, --rescan, yes }); m_proc.start(); }这段代码只负责启动进程不阻塞界面。-t表示 terse 输出-f指定只取 SSID、信号强度、安全类型三个字段--rescan yes让网卡重新扫一遍。真正的结果在readyReadStandardOutput和finished信号里处理。生产环境不要在主线程里waitForFinished触摸屏上转圈超过两秒用户就会以为程序死了。解析时先处理转义冒号// core/wifimanager.cpp static QStringList splitNmcliLine(const QString line) { QStringList out; QString current; bool escaped false; for (const QChar ch : line) { if (escaped) { current.append(ch); escaped false; } else if (ch \\) { escaped true; } else if (ch :) { out.append(current); current.clear(); } else { current.append(ch); } } out.append(current); return out; }这个函数把\:还原成普通冒号把未转义的冒号当分隔符。解析完一行后cols.at(0)是 SSIDcols.at(1)是信号强度cols.at(2)是安全类型。空 SSID 一般是隐藏网络列表里可以先跳过或者单独做“手动输入网络名”的入口。void WifiManager::onScanFinished(int code, QProcess::ExitStatus status) { if (status ! QProcess::NormalExit || code ! 0) { emit scanFailed(QString::fromUtf8(m_proc.readAllStandardError())); return; } QListWifiItem items; const QListQByteArray lines m_scanBuffer.split(\n); for (const QByteArray raw : lines) { const QString line QString::fromUtf8(raw).trimmed(); if (line.isEmpty()) continue; const QStringList cols splitNmcliLine(line); if (cols.size() 3) continue; WifiItem item; item.ssid cols.at(0); item.signal cols.at(1).toInt(); item.security cols.at(2); if (!item.ssid.isEmpty()) items.append(item); } emit scanFinished(items); }这里统一用QString::fromUtf8不要用fromLocal8Bit否则中文 SSID 在部分精简系统上会变乱码。SIGNAL是 0 到 100 的整数可以直接映射成信号格数。SECURITY为空表示开放网络界面上要给出提示不要默认当成加密网络处理。3.3 连接指定热点拼接命令、处理转义与超时连接动作同样走QProcess参数用QStringList传不要自己拼一整条 shell 字符串。SSID 里可能有空格、引号、$交给QStringList更安全。// core/wifimanager.cpp void WifiManager::connectTo(const QString ssid, const QString password) { if (m_connecting) { emit connectStateChanged(ssid, busy, 上一条连接还没结束); return; } m_connecting true; m_currentSsid ssid; QStringList args; args dev wifi connect ssid; if (!password.isEmpty()) { args password password; } m_proc.setProgram(nmcli); m_proc.setArguments(args); m_proc.start(); }m_connecting是一个简单的状态锁防止用户连续点“连接”启动多个进程。连接结果交给finished信号处理成功就发connected失败就把标准错误带回去。密码为空时省略password参数走开放网络。注意nmcli dev wifi connect SSID password PASS会把密码放进进程参数同机其他用户用ps能看到。设备如果是单用户封闭环境问题不大如果是多用户环境常见做法是用交互式输入或受控配置文件至少不要把这行命令原样打进日志。连接超时也要处理。可以加一个QTimer::singleShot(15000, this, ...)时间到了还没收到finished就kill()进程并提示“连接超时请检查密码或信号”。没有超时保护界面会一直停在“正在连接”现场人员只能重启应用。3.4 软键盘弹出焦点跟踪与输入框回填软键盘的核心不是画按钮而是“知道当前该往哪个输入框里写字”。常见做法是给所有QLineEdit安装事件过滤器焦点进入时记录目标并弹键盘。// ui/keyboardfilter.cpp bool KeyboardFilter::eventFilter(QObject *watched, QEvent *event) { if (event-type() QEvent::FocusIn) { if (auto *edit qobject_castQLineEdit *(watched)) { m_keyboard-setTarget(edit); m_keyboard-showAt(edit); } } return QObject::eventFilter(watched, event); }setTarget里用QPointerQLineEdit保存目标输入框被销毁时指针会自动置空避免野指针。showAt根据输入框在屏幕上的位置决定键盘弹在上面还是下面触摸屏底部空间不够时自动翻到上方。按键回填逻辑很直接// ui/softkeyboard.cpp void SoftKeyboard::onKeyPressed(const QString key) { if (!m_target) return; if (key Backspace) { m_target-backspace(); } else if (key Clear) { m_target-clear(); } else if (key Enter) { emit enterPressed(); hide(); } else { m_target-insert(key); } }backspace()、clear()、insert()都是QLineEdit的现成接口不用自己维护字符串。键盘上的按钮统一设置setFocusPolicy(Qt::NoFocus)否则点按钮时焦点会从输入框跑到按钮上键盘就找不到目标了。键盘窗口用Qt::Tool | Qt::FramelessWindowHint再加WA_ShowWithoutActivating避免抢走输入框焦点。3.5 状态回显把“已连接”落到 UI 上扫描和连接都是异步的界面不能靠“点完按钮就假设成功”。MainWindow里把信号接到槽上connect(m_wifi, WifiManager::scanFinished, this, MainWindow::onScanFinished); connect(m_wifi, WifiManager::connectStateChanged, this, MainWindow::onConnectStateChanged);onScanFinished清空列表按信号强度排序给加密网络加锁图标。onConnectStateChanged根据状态改按钮文字和颜色连接中禁用按钮成功显示“已连接”失败恢复按钮并弹提示。当前连接的 SSID 要单独高亮不要只依赖nmcli的输出顺序否则刷新一次列表高亮就丢了。4. 编译与运行在桌面先跑通再交叉编译到设备这份工程不适合一上来就往板子里塞。先在桌面把界面、软键盘、模拟数据跑通再去设备上接真实无线工具。桌面没有nmcli也能跑加一个--mock参数就行。4.1 工程文件与依赖检查用 CMake 管理Qt 5 和 Qt 6 都能编。核心依赖只有Qt::Widgets不需要网络模块因为 Wi-Fi 操作走的是QProcess。cmake_minimum_required(VERSION 3.16) project(wifi_keyboard_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) add_executable(wifi_keyboard_demo app/main.cpp app/mainwindow.cpp core/wifimanager.cpp ui/softkeyboard.cpp ui/keyboardfilter.cpp ) target_link_libraries(wifi_keyboard_demo PRIVATE Qt${QT_VERSION_MAJOR}::Widgets )CMAKE_AUTOMOC必须打开因为WifiManager和SoftKeyboard都带Q_OBJECT。如果编译时报undefined reference to vtable先检查类头文件有没有加Q_OBJECT再检查 CMake 有没有把对应的.cpp加进目标。4.2 桌面模拟运行用模拟数据先把 UI 跑起来main.cpp里根据命令行参数决定用真实后端还是模拟后端int main(int argc, char *argv[]) { QApplication app(argc, argv); const bool mock app.arguments().contains(--mock); MainWindow window(mock); window.show(); return app.exec(); }模拟模式下WifiManager::scanAsync()不启动nmcli直接用QTimer::singleShot发一组假数据比如 “Office-2.4G”“Lab-5G”“Guest”。这样在没装 NetworkManager 的桌面上也能调列表布局、键盘弹出位置和密码框回显。cmake -S . -B build cmake --build build -j ./build/wifi_keyboard_demo --mock先点密码框确认软键盘能弹、能输入、能退格、回车能收起。再点扫描确认列表能刷新、加密网络有标识、空 SSID 被过滤。最后把--mock去掉在 Linux 桌面用真实nmcli跑一遍。桌面这一步能挡掉大部分 UI 问题别等到板子上再调。4.3 设备端部署权限、服务与开机自启设备端先确认三件事nmcli存在、NetworkManager 在跑、当前用户有修改连接的权限。缺一个界面就会表现为“列表为空”或“连接失败”。which nmcli systemctl status NetworkManager nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list --rescan yes第一条找不到路径说明系统没装 NetworkManager 或者工具不在PATH。第二条没跑先启动服务。第三条能出列表才说明权限和无线网卡都正常。应用里可以把nmcli写成绝对路径避免开机自启时环境变量不同导致找不到命令。部署时把可执行文件和必要的 QSS 资源一起拷过去确认有执行权限。需要开机自启就写系统服务或桌面自启动项工作目录设成资源所在目录。设备上没有终端时日志要落到文件否则出问题只能靠猜。4.4 日志与退出码排查连接失败的第一现场QProcess的exitCode和exitStatus要分开看。exitStatus是进程有没有正常结束exitCode是命令本身的返回码。nmcli连接失败时通常返回非零具体原因在标准错误里。我一般在onConnectFinished里记三样东西SSID、退出码、标准错误前 200 个字符。不要把密码记进去。日志里看到Secrets were required, but not provided多半是密码错了看到No network with SSID多半是扫描结果过期连之前先重新扫一次看到Device not ready先检查无线网卡是不是被别的进程占着。5. 常见问题排查Wi-Fi 列表空、连接超时和软键盘不弹这类项目在现场翻车往往不是大功能没写而是几个小细节没兜住。下面几条都是踩坑记录按“现象、原因、解决”排。5.1 扫描列表为空先看 nmcli 是否可用现象界面点“扫描”没有任何热点或者一直转圈后列表空白。原因nmcli不在PATH、NetworkManager 没启动、当前用户没有权限或者nmcli输出格式和解析代码对不上。解决先在设备终端手动跑扫描命令确认能出数据。应用里用绝对路径启动进程检查QProcess::errorString()和标准错误。解析失败时把原始输出打到日志里不要只发一个空列表给界面。nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list --rescan yes echo $?返回非零就先修系统侧不要急着改 Qt 代码。5.2 连接一直转圈密码、转义与超时现象点连接后按钮一直显示“连接中”最后只能杀进程。原因密码里有空格或特殊字符命令参数被拆错SSID 是中文或带冒号解析和传参不一致nmcli卡在认证阶段没有超时保护。解决参数统一用QStringList不要拼 shell 字符串。连接前重新扫描一次避免拿旧列表里的 SSID 去连。加 15 秒超时超时后kill()进程并提示。同名 SSID 有多个 AP 时nmcli可能连到信号弱的那个二次开发时可以加 BSSID 选择。5.3 软键盘弹出后输入框失焦现象键盘弹出来了点按键没反应或者只有第一下能输入。原因键盘窗口抢了焦点QLineEdit触发FocusOut键盘保存的目标被清空按键按钮本身可聚焦点按钮时焦点又跑走。解决键盘用Qt::Tool加WA_ShowWithoutActivating所有按键设置Qt::NoFocus目标输入框用QPointer保存。如果业务里确实需要在失焦时收起键盘延迟几十毫秒再判断避免“点键盘按钮”被误判成“输入框失焦”。5.4 中文 SSID 显示乱码现象中文热点名在列表里变成问号或方块。原因nmcli输出按本地编码处理Qt 这边用fromLocal8Bit解析两边编码不一致。解决统一按 UTF-8 解析QString::fromUtf8处理标准输出。设备locale不是 UTF-8 时给QProcess设置环境变量LC_ALLC.UTF-8再启动命令。界面字体也要确认包含中文字形否则编码对了也显示不出来。5.5 重复点击导致连接状态错乱现象快速点两次“连接”界面一会儿显示成功一会儿显示失败当前 SSID 还对不上。原因每次点击都启动一个QProcess多个进程的finished信号混在一起后发的请求先返回状态被覆盖。解决加m_connecting状态锁连接中直接忽略新请求或者维护一个请求队列前一个结束后再发下一个。界面上的连接按钮在连接中要禁用不要让用户有机会连点。状态更新时带上 SSID槽函数里比对当前 SSID不匹配的旧信号直接丢弃。6. 二次开发把“能用”推到“好维护”的几个改造点第一版能扫描、能连接、能弹键盘就够了但真要交付建议先把后端抽象出来。定义一个WifiBackend接口把scanAsync、connectAsync、disconnect做成纯虚函数NmcliBackend和MockBackend各自实现。这样桌面调试不依赖系统工具设备换wpa_cli也不用改 UI。class WifiBackend { public: virtual ~WifiBackend() default; virtual void scanAsync() 0; virtual void connectAsync(const QString ssid, const QString password) 0; };第二个改造点是列表信息。现在只显示 SSID 和安全类型可以加信号强度图标、频段、BSSID按信号强度排序。密码框旁边加“显示密码”按钮按钮同样设置Qt::NoFocus否则一点又抢焦点。第三个改造点是记住密码。不要把密码明文写进 QSettings至少做一层设备绑定的简单混淆并且只记住用户勾选过的网络。自动重连要加边界只在应用启动时尝试一次失败就回到列表不要无限重试把界面卡死。从那以后我每次接触摸屏项目都会先把软键盘的焦点策略和 Wi-Fi 状态机画在纸上再动手写界面。源码包拿到后先跑--mock确认键盘和列表没问题再上设备接真实命令。希望帮到你。本文还有配套的精品资源点击获取