☰
Qt嵌入式WiFi管理:搜索、连接与软键盘实现
2026/10/15 18:08:40 网站建设 项目流程

简介:面向Linux平台Qt开发者的无线网络管理示例工程,通过调用系统wpa_supplicant服务,在界面上完成附近WiFi的搜索、显示与连接操作,并支持密码输入时弹出软键盘,适合触屏设备、嵌入式或工业终端的无线管理场景。工程压缩包约2.94MB,共21个文件,主体是5个cpp、4个h源码与3个ui界面,配合qrc资源文件、工程引导图片和syszuxpinyin软键盘库,pro工程文件可直接编译运行。功能覆盖WiFi刷新与列表展示,最多支持20个热点,连接认证支持WEP、WPA2等常见加密,软键盘部分可独立复用到其他输入场景。已有1011人下载学习,对需要为Qt应用集成WiFi能力的开发者来说,参考其wpa_supplicant调用方式和界面交互,能明显缩短无线模块的调试周期。

1. 在触摸屏上做WiFi管理:为什么Qt方案绕不开这三个能力

嵌入式触摸屏设备最常见的交付场景,是用户拿到机器后没有网口、没有键盘、没有鼠标,屏幕上只有几个图标。第一次开机联网时,系统里的网络配置工具帮不了忙,Qt程序要自己把WiFi搜索、显示、连接、输密码这条链路跑通,而且输密码的时候必须弹出一个软键盘。标题里“Qt无线连接 WIFI搜索显示连接 可弹出软键盘”其实是同一个功能的三个阶段:先把周边热点扫描出来,再把连接动作做掉,最后满足无物理键盘的输入需求。我做过好几台这种设备,最稳妥的组合是Qt Widgets + NetworkManager 的命令行工具 nmcli,软键盘自己画一个,不引入额外的QML依赖。这个方案能在标准Linux设备上编译直接跑,后续加断线重连、保存密码也都很顺手。下面我把搜索、连接、软键盘三条线分开讲,中间会夹一些参数和血泪经验。

2. 搜索并显示WiFi列表:选型、轮询信号与解析协议

2.1 为什么选nmcli而不是wpa_supplicant或Qt原生接口

在Linux上搜索WiFi,底层其实都是走wpa_supplicant或驱动私有接口,但在应用层有两条常见路:一条直接操作wpa_cli,一条走NetworkManager的nmcli。很多嵌入式设备的Yocto镜像里默认带NetworkManager,nmcli是它的命令行客户端,输出已经做了结构化,还不要求权限(前提是用户有权访问NetworkManager)。wpa_cli虽然更底层、也更轻,但它需要自己处理网络ID的分配、配置序列,连接一个热点要发五六条命令,调试成本明显高。

对比一下这两个方案的适用边界:

对比项nmcliwpa_cli
依赖服务NetworkManagerwpa_supplicant
已保存网络管理自动按ID管理需要自己记network id
连接一条热点一条命令5~7条命令
输出格式支持-t机器可读原始文本
隐藏网络支持支持rescan指定SSID需要配置文件或逐项手工设置
适合阶段快速产品落地追求最小系统、无DBus环境

我个人的选择很简单:如果系统里已经起了NetworkManager,就用nmcli;如果没有,再考虑wpa_supplicant。后文所有流程均以nmcli为例,但解析思路同样适用于wpa_cli。

2.2 扫描的最小命令与QProcess封装

先看命令本身:

nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list --rescan yes

-t让输出变成冒号分隔的机器可读格式,-f指定只输出三个字段,--rescan yes强制触发一次空口扫描,而不是直接读缓存。不加--rescan yes时,列表往往还是上一次扫描的旧结果,用户在界面上点了“搜索”半天没变化,多半就是缓存没刷新。

在Qt里不要用QProcess::execute()同步调用,扫描一次要2到5秒,UI线程会被卡死。我一般这样封装:

// WifiScanner.cpp void WifiScanner::startScan() { auto *proc = new QProcess(this); proc->setProgram("nmcli"); proc->setArguments({ "-t", "-f", "SSID,SIGNAL,SECURITY", "dev", "wifi", "list", "--rescan", "yes" }); connect(proc, &QProcess::finished, this, [this, proc](int exitCode) { if (exitCode == 0) { QByteArray raw = proc->readAllStandardOutput(); parseScannerOutput(QString::fromUtf8(raw)); } else { qWarning() << "scan failed:" << proc->readAllStandardError(); } proc->deleteLater(); }); proc->start(); }

这段代码的关键点是setProgram和setArguments配合使用,而不是把一整条命令拼成字符串再丢给bash -c。nmcli本身不经过shell,后面连接密码时能避免大量特殊字符的转义问题。finished信号只在进程结束时触发一次,所以把deleteLater放这里,避免每次扫描泄漏一个QProcess。

2.3 解析nmcli输出,处理中文SSID和冒号

nmcli-t输出的每一行格式是SSID:SIGNAL:SECURITY,但如果某个WiFi的SSID里包含冒号,这一行就会变成XX\:YY:65:WPA2这样的结构。直接按冒号split会把SSID切碎。我的处理方式是取最后两个冒号作为分隔线,前面所有内容合并为SSID:

// WifiScanner.cpp void WifiScanner::parseScannerOutput(const QString &text) { QHash<QString, WifiInfo> infoMap; const QStringList lines = text.split('\n', Qt::SkipEmptyParts); for (const QString &line : lines) { // 取倒数第二个和倒数第三个冒号,手动找分隔 int lastColon = line.lastIndexOf(':'); if (lastColon <= 0) continue; int secondLast = line.lastIndexOf(':', lastColon - 1); if (secondLast <= 0) continue; QString ssid = line.left(secondLast); ssid.replace("\\:", ":"); // 还原被转义的冒号 int signal = line.mid(secondLast + 1, lastColon - secondLast - 1).toInt(); QString security = line.mid(lastColon + 1); if (ssid.isEmpty()) continue; // 隐藏/无SSID热点跳过后端处理 if (!infoMap.contains(ssid) || infoMap[ssid].signal < signal) { infoMap[ssid] = {ssid, signal, security}; } } emit scanFinished(infoMap.values()); }

这里有几个坑要说明。第一,QString::fromUtf8必须用在读取结果之后,中文SSID在nmcli输出里是UTF-8字节流,如果不做转码,界面上全是乱码。第二,同一SSID可能带来多个BSSID(多个AP),我按SSID去重,保留信号最强的那个。第三,隐藏网络在列表里SSID为空,这类热点靠正常扫描是看不到的,第5章的避坑会专门讲。

信号值本身是0到100的百分比,界面上要显示成格数的话,可以做一个简单的映射:

int WifiInfo::barLevel() const { if (signal >= 75) return 4; if (signal >= 50) return 3; if (signal >= 25) return 2; return 1; }

2.4 定时刷新与扫描状态管理

扫描是一个异步过程,界面上应该有一个区域显示“正在扫描…”,同时把“搜索”按钮禁用,避免用户重复点击触发多个QProcess叠加。常见做法是维护一个bool m_scanning成员变量:

void WifiPage::onSearchClicked() { if (m_scanning) return; m_scanning = true; ui->searchButton->setEnabled(false); ui->statusLabel->setText(QStringLiteral("正在扫描…")); m_scanner.startScan(); } void WifiPage::onScanFinished(const QList<WifiInfo>& list) { m_scanning = false; ui->searchButton->setEnabled(true); ui->statusLabel->setText(QStringLiteral("共发现 %1 个热点").arg(list.size())); // 刷新列表Model }

如果要让列表自动刷新,我一般用QTimer::singleShot(5000, this, &WifiPage::onSearchClicked)做一个5秒轮询,但绝对不会每1秒扫一次,WiFi芯片受不了,列表也会一直跳动。自动刷新时只更新信号格数,不重新置顶列表,这样用户点了某个网络,刷新后不会跳到别处。

3. 连接WiFi:从密码框到网络状态监测

3.1 点击连接后的流程设计

搜索列表的每一行都对应一个热点,点击行或“连接”按钮后的完整流程,我按下面这几步走:

  1. 判断热点是否需要密码,SECURITY字段是空或--时,直接走免密连接。
  2. 需要密码时,弹出一个密码输入框,并调出软键盘(第4章会实现)。
  3. 点击“连接”按钮后,立即把整个页面置为“连接中”状态,防止重复发命令。
  4. 启动一个30秒超时定时器,检查连接是否成功或报错。
  5. 连接成功后保存SSID,关闭窗口,通知主程序。

这个流程看起来简单,但最关键的细节是第3步:一定要有一个bool m_connecting标志。如果没有,用户连点两次连接,两个QProcess会同时执行,NetworkManager会收到两条竞争命令,最后一条覆盖前一条,界面显示和实际状态不一致。

3.2 执行连接命令,避免shell转义

nmcli连接一个加密网络的最小命令是:

nmcli dev wifi connect "SSID" password "PASS"

如果密码为空,可以把password和后面的值都去掉。正确做法是把参数传给QProcess,让它自己处理引号:

// WifiService.cpp bool WifiService::connectToWifi(const QString &ssid, const QString &password) { if (m_connecting) return false; m_connecting = true; auto *proc = new QProcess(this); QStringList args; if (password.isEmpty()) { args = {"dev", "wifi", "connect", ssid}; } else { args = {"dev", "wifi", "connect", ssid, "password", password}; } proc->setProgram("nmcli"); proc->setArguments(args); connect(proc, &QProcess::finished, this, [this, proc](int exitCode) { QByteArray err = proc->readAllStandardError(); if (exitCode == 0) { m_connecting = false; emit connectionSucceeded(proc->arguments().at(3)); } else { m_connecting = false; emit connectionFailed(QString::fromUtf8(err).trimmed()); } proc->deleteLater(); }); proc->start(); return true; }

重点说下参数传值。如果SSID或密码里包含$、空格、单引号、双引号,setArguments会自动把每个参数独立传给子进程,不会经过shell解析。可千万别写成:

QProcess::execute("nmcli dev wifi connect \"" + ssid + "\" password \"" + password + "\"");

这种写法在密码里带上$或反引号时,有时候会被系统shell当成变量展开,导致连接失败。我第一次做这个功能时就被一个带$的密码折磨了半天,后来全部改成参数列表形式,再没出过这类问题。

连接命令发出后,nmcli会等NetworkManager完成认证和DHCP,这个过程在弱信号环境下可能持续十几秒甚至更久。QProcess的finished信号是被动触发的,界面不会卡,但这要求所有结果处理都在回调函数里做,不能有人写的waitForFinished()。我见过有人把waitForFinished(5000)放在按钮回调里,结果弱信号下5秒超时,连接其实还在后台继续,界面上却直接提示“失败”,这是最典型的翻车姿势。

3.3 监听连接状态与失败信息

连接命令返回0只能说明NetworkManager接受了激活请求,不一定代表拿到了IP。我一般连完之后马上启动一个10Hz的轮询,用下面命令读取wlan0状态:

nmcli -g GENERAL.STATE,IP4.ADDRESS device show wlan0

-g表示只输出括号内字段,字段之间用逗号分隔。输出类似:

connected:connected:100.64.0.2/24

如果不带IP4.ADDRESS,可能是--,这时说明还在获取地址。所以我用这样的逻辑判定真正连上:

// WifiService.cpp void WifiService::startStatusPolling() { m_timer = new QTimer(this); m_timer->setInterval(1000); connect(m_timer, &QTimer::timeout, this, &WifiService::checkStatusOnce); m_timer->start(); QTimer::singleShot(30000, this, [this]() { if (!m_connected) { m_timer->stop(); emit connectionFailed(QStringLiteral("连接超时:请确认密码或信号强度")); } }); } void WifiService::checkStatusOnce() { QProcess *proc = new QProcess(this); proc->setProgram("nmcli"); proc->setArguments({"-g", "GENERAL.STATE,IP4.ADDRESS", "device", "show", m_iface}); connect(proc, &QProcess::finished, this, [this, proc](int code) { if (code != 0) { proc->deleteLater(); return; } QString out = QString::fromUtf8(proc->readAllStandardOutput()).trimmed(); QStringList parts = out.split(','); if (parts.size() >= 2 && parts[0] == "connected" && parts[1] != "--") { m_connected = true; m_timer->stop(); emit ipAssigned(parts[1]); } proc->deleteLater(); }); proc->start(); }

在查询结果里,connected:connected前者是设备状态、后者是连接状态。用逗号split是因为-g输出里IP段不再有冒号干扰,比-t格式更适合解析。这个轮询结束后记得把m_connecting复位,否则点了“重试”还会被拦。

4. 可弹出的软键盘:自绘QWidget键盘,还是接入Qt Virtual Keyboard?

4.1 方案对比

标题里“可弹出软键盘”有两个实现方向:一个是直接使用Qt自带的Virtual Keyboard模块,另一个是自己写一个QWidget键盘。官方方案在qmake构建时要用CONFIG += virtualkeyboard,运行时还要设置QT_IM_MODULE=qtvirtualkeyboard,它在QML应用里开箱即用,但在纯Widgets工程里要迁就很多,而且默认键盘是英文全键盘,想改成只包含字母和数字的小尺寸密码盘,几乎要把整个输入法插件重写一遍。自绘软键盘的好处是尺寸可控、样式随界面走、只给当前输入框发字符,不干扰其他输入法。对于WiFi密码输入这种简单场景,我更推荐自绘。

两种方案的取舍用一张表说明:

项目Qt Virtual Keyboard自绘QWidget软键盘
依赖模块QtVirtualKeyboard无,仅Qt Widgets
构建复杂度需要额外模块和QML环境直接编译
定制尺寸/按键麻烦,需扩展插件随便改
中文输入自带拼音插件(需要额外库)基本不支持,但WiFi密码用不上
稳定性风险输入法状态和焦点偶尔乱跳完全可控

我实际做WiFi设置界面时,输入框只有密码一个,就算需要中文SSID手动输入,项目里也没有强制要求,所以自绘键盘是性价比最高的。

4.2 自绘键盘的类和按键布局

先设计一个VirtualKeyboard类,它继承QWidget,内部维护一个目标控件指针:

// VirtualKeyboard.h #ifndef VIRTUALKEYBOARD_H #define VIRTUALKEYBOARD_H #include <QWidget> #include <QTimer> class QPushButton; class QLineEdit; class VirtualKeyboard : public QWidget { Q_OBJECT public: explicit VirtualKeyboard(QWidget *parent = nullptr); void bindTarget(QWidget *target); protected: void initUI(); void createKey(const QString &text, int row, int col, int rowSpan = 1, int colSpan = 1); void handleKey(const QString &text); private: QWidget *m_target = nullptr; bool m_shift = false; }; #endif

实现片段里,createKey负责生成按键并把焦点策略设为Qt::NoFocus,这是软键盘能不能正常工作的核心细节:

void VirtualKeyboard::createKey(const QString &text, int row, int col, int colSpan) { auto *btn = new QPushButton(text, this); btn->setFocusPolicy(Qt::NoFocus); // 点击时不抢占输入框焦点 btn->setMinimumHeight(40); layout()->addWidget(btn, row, col, 1, colSpan); connect(btn, &QPushButton::clicked, this, [this, text]() { handleKey(text); }); }

每个按键都设置Qt::NoFocus,这样点击软键盘时,当前输入框不会收到FocusOut事件,也不会触发输入法隐藏。我之前没设这个策略,结果点一下A,键盘失去目标,后面敲的每个字符都丢到界面上,听客户反馈“软键盘偶尔不好使”就是这个原因。

handleKey里处理三种输入:普通字符、退格、回车:

void VirtualKeyboard::handleKey(const QString &text) { if (!m_target) return; if (text == QStringLiteral("⇧")) { m_shift = !m_shift; return; } if (text == QStringLiteral("◀")) { if (auto *edit = qobject_cast<QLineEdit *>(m_target)) { edit->backspace(); } return; } if (text == QStringLiteral("↵")) { hide(); return; } QString key = m_shift ? text.toUpper() : text; if (m_shift) { // 大小写切换后把按键文字也刷新;示例省略刷新逻辑 } if (auto *edit = qobject_cast<QLineEdit *>(m_target)) { edit->insert(key); } }

这里选择直接调用QLineEdit::backspace()和QLineEdit::insert(),没有往里发QKeyEvent。发键盘事件很容易被输入法、焦点控件拦截,还得考虑KeyPress/KeyRelease的配对问题;直接调用控件自身的槽函数最稳定。缺点是如果目标控件不是QLineEdit(比如QTextEdit),需要再写分支——但在WiFi密码页,输入框一定是QLineEdit,所以这个实现够用。

4.3 让软键盘跟随输入框弹出:事件过滤器

软键盘属于“需要时再现身”的窗口,最省事的触发方式是在密码输入框上安装事件过滤器:

// WifiPasswordDialog.cpp class WifiPasswordDialog private : public QWidget { public: explicit WifiPasswordDialog(QWidget *parent) { m_keyboard = new VirtualKeyboard(this); m_keyboard->hide(); passwordEdit->installEventFilter(this); connect(connectButton, &QPushButton::clicked, this, &WifiPasswordDialog::onConnectClicked); } protected: bool eventFilter(QObject *obj, QEvent *event) override { if (obj == passwordEdit) { if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::FocusIn) { // 延迟0毫秒弹出,让点击事件先落到输入框 QTimer::singleShot(0, this, [this]() { m_keyboard->bindTarget(passwordEdit); m_keyboard->show(); updateKeyboardLayoutPos(); }); } } return QWidget::eventFilter(obj, event); } };

事件过滤器里无论是MouseButtonPress还是FocusIn都触发弹出,覆盖了鼠标点击和Tab键切换两种进入方式。为什么包一层QTimer::singleShot(0)?因为MouseButtonPress发生的同时,输入框还没来得及把光标位置设置好,如果软键盘立刻拿到焦点去绑定,可能读到的是旧的输入焦点。延迟一个事件循环再弹,光标位置已经就位。

绑定目标之后,软键盘的show()不需要设置为模态,也不需要activateWindow()。它只是一个底部停靠的小窗口,点击按钮时因为NoFocus,输入框光标还在原地。

4.4 软键盘遮挡输入框的处理

触摸屏屏幕通常不大,软键盘弹出后会盖住一部分页面。常见的布局是把密码输入框放在页面中部,软键盘停在底部,两者天然错开。但规格书里有时候要求输入框居中,这时就需要动态调整页面高度或滚动区域。

最省事的是让整个窗口使用一个垂直布局,软键盘作为最后一个子控件,外部用一个QScrollArea包住上半部分,键盘显示时把滚动区域的高度缩小:

void WifiPasswordDialog::updateKeyboardLayoutPos() { int kbHeight = m_keyboard->height(); if (m_keyboard->isVisible()) { contentArea->setFixedHeight(this->height() - kbHeight); // 把当前输入框滚动到可见区域 ui->scrollArea->ensureWidgetVisible(passwordEdit); } else { contentArea->setFixedHeight(this->height()); } }

ensureWidgetVisible是QScrollArea的方法,它会在内部计算滚动量,一次函数调用就能把目标控件移到可视范围内,比手动改verticalScrollBar()->setValue()稳得多。注意必须在布局调整之后调用,否则计算出来的坐标还是旧的。这是我个人习惯的做法,在很多触摸屏项目里都适用。

5. WiFi连接与软键盘落地最常踩的5个坑

5.1 中文SSID搜索出来是乱码,连接也报找不到网络

现象:界面上显示的SSID是类似\u4e2d\u56fd的转义序列,或者直接是测试这样的乱码,选中之后连接报“No network with such name”。

原因:系统终端环境变量LANG可能是C或POSIX,nmcli按该系统codepage输出字节流;而Qt内部统一使用UTF-16,两端对不上。另外QProcess读取readAllStandardOutput()返回的字节流如果直接QString(raw),会按本地系统编码来解释,中文自然就错位。

解决:先从字节流构造UTF-8的QString,再把locale固定住。

// main.cpp qputenv("LANG", "en_US.UTF-8"); qputenv("LC_ALL", "en_US.UTF-8"); // 在有nmcli的Linux环境下,这句强制所有系统命令输出UTF-8

解析时永远用QString::fromUtf8(proc->readAllStandardOutput()),不要用QString(raw)。这个规则对扫描结果和错误信息同样适用。

5.2 点击软键盘按钮,密码输入框焦点丢了,键盘新字符全无效

现象:密码输入到一半,再点软键盘上的按键,屏幕没反应,或者字符跑到其他控件上。

原因:QPushButton默认FocusPolicy是StrongFocus,点击按钮会触发焦点转移,输入框失去FocusIn,之前绑定好的target虽然还在,但后续按键事件可能会被系统当成快捷键处理;更糟的是当输入框失焦后,某些QLineEdit会自行取消光标,insert()虽然仍能插入但光标不再更新,给用户一种“没输进去”的感觉。

解决:每个软键盘按钮初始化时都调用setFocusPolicy(Qt::NoFocus),并且把bindTarget的调用时机放在输入框真正可见后。另外,不要在软键盘按钮的pressed信号里发字符,released或clicked更稳,避免触摸屏长按抖动产生两次触发。

5.3 连接命令发出后界面卡住,像死机一样

现象:点击连接后,整个窗口不可拖动,按钮样式保持按下状态,十几秒后才恢复;有时直接报“程序无响应”。

原因:有人在回调里用了QProcess::waitForFinished(),或者用QProcess::execute()同步执行。nmcli连接加密网络时,如果WiFi芯片当前处于繁忙扫描状态,命令可能60秒后才返回,这期间UI事件循环完全停摆。

解决:统一改成QProcess异步模式,所有结果都走信号槽。想要进度反馈,就额外启动一个1秒轮询查状态,把“连接中”作为界面状态而不是依赖命令返回。经验做法是点击连接后立刻禁用按钮并显示“正在连接(30秒超时)”,把屏幕上的操作权交给一个超时定时器。

5.4 列表里同一个WiFi出现好几条,随便点哪条都连不上

现象:扫描结果里某个SSID重复了3次,信号强度不同,安全类型可能一条是WPA2、一条是WPA3,点击信号最强的那条却一直报“connection activation failed”。

原因:一个SSID下有多个BSSID,不同AP的通道和信号不同,nmcli会逐条列出。如果WiFi名为空或重复,再按顺序连接,NetworkManager可能把网络ID对应到另一个BSSID上。

解决:在解析阶段做去重,SSID相同时只保留信号最强的记录,同时合并安全类型。连接时不要拿整个列表项去连,而是用干净的SSID字符串。代码在第2.3节已经实现了保留强信号的逻辑,这里补充一句:去重时还要过滤掉security里的“--”,避免把隐藏热点误并进可见热点。

5.5 隐藏WiFi怎么搜都搜不到

现象:路由器开了“隐藏SSID”,扫描列表里永远没有这个热点,用户手动输入SSID也连不上。

原因:普通扫描请求只会返回广播了Beacon帧的网络,隐藏SSID的AP把SSID字段置空,系统不会把它当作可发现网络列出来。

解决:在连接前先强制扫描指定SSID,再执行连接。命令如下:

nmcli device wifi rescan ssid "你的SSID" nmcli dev wifi connect "你的SSID" password "密码"

rescan返回后需要等待1到2秒让它落到NetworkManager的缓存里,不要立刻connect,否则还是找不到。如果路由器同时关闭了“兼容模式”,只把rescan这一条命令拿出来放到一个QTimer::singleShot(1500)的延时回调里即可。这个场景不多,但项目在代理机房遇到一次后,我把这个逻辑内置进了连接函数。

6. 再进一步:保存连接记录,断线自动重连

6.1 用QSettings保存历史连接

把WiFi连接信息写到本地配置文件,能给设备厂安装和用户都省事。我一般用一个专门的配置位置:

QSettings settings("/etc/myapp/wifi.ini", QSettings::IniFormat); settings.setValue("wifi/ssid", ssid); settings.setValue("wifi/password", password); settings.sync();

直接明文存密码在嵌入式设备上有安全争议,但这类设备本身没有多用户,密码写在/etc目录下权限设为600,可控。真在意安全就只存SSID和“是否已配置过”,密码交给NetworkManager自己的keyring系统去管。但要注意,NetworkManager对非交互会话未必允许存储可控密码,自己存一份能保证断线重连时不需要用户重新输入。

我还会存一个连接状态标识,方便下次启动时判断是否自动连接。QSettings::sync()不要漏,设备意外断电时配置会丢。

6.2 断线重连的状态轮询

写监听QProcess的轮询时,最常见的误区是只查一次网络状态就下结论。断开瞬间无线网卡可能还在漫游,切换AP也需要几秒。我采用的方案是每两秒查一次,连续两次都是“disconnected”才触发重连:

void WifiReconnecter::checkStatus() { QProcess *proc = new QProcess(this); proc->setProgram("nmcli"); proc->setArguments({"-g", "GENERAL.STATE", "device", "show", m_iface}); connect(proc, &QProcess::finished, this, [this, proc]() { QString state = QString::fromUtf8(proc->readAllStandardOutput()).trimmed(); if (state == "disconnected") { m_fails++; if (m_fails >= 2) { m_fails = 0; triggerReconnect(); } } else { m_fails = 0; } proc->deleteLater(); }); proc->start(); }

重连动作就是调用第3章里的WifiService::connectToWifi(),如果之前保存过密码,直接带密码重连;没有再弹软键盘。这个循环在设备启动后启动,后台运行,不阻塞UI。轮询里有m_fails计数是因为弱信号区域偶尔一次“disconnected”是假象,连续两次确认才会让人信服。

6.3 封装成可复用的WifiDialog组件

最后做一个统一入口。把扫描列表、连接状态、软键盘、配置文件这四块都塞进一个WifiDialog类,对外只暴露一个信号:

class WifiDialog : public QDialog { Q_OBJECT public: explicit WifiDialog(QWidget *parent = nullptr); void open(); signals: void connected(const QString &ssid, const QString &ip); };

内部实现时,扫描用WifiScanner,连接用WifiService,输入框和VirtualKeyboard放在同一个页面上。这样主程序里只需一行:

dialog->open();

不用管WiFi扫描是怎么轮询的、软键盘什么时候弹出、密码存到哪个文件。我习惯把这类功能做成独立组件,因为很多设备除了WiFi设置,后面还会做蓝牙配对、蓝牙键盘,这些都需要类似的“扫描-显示-连接-输入”交互,把第一版的WiFi模块复用到其他业务上,比每次重新写一遍省得多。

做完整套流程后,再回头看这个标题,其实最值钱的反而不是那几行Qt代码,而是“把连接动作拆成可轮询的消息驱动”,以及“让软键盘不跟输入框抢焦点”这两个设计决策。真让我再做一遍,我会在软键盘里加一个中英文切换按钮,否则客户拿着设备输中文SSID时只能干瞪眼。希望这些经验能帮你少填几个坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询