简介:面向 Linux 平台 Qt 开发者的无线网络连接示例工程,围绕 WIFI 扫描、列表展示、连接认证与密码输入等常见需求,提供一套可参考的桌面端实现。项目通过系统调用与 wpa_supplicant 服务交互,支持刷新搜索附近网络、最多显示 20 个 WIFI、连接 WEP/WPA2 等加密方式,并在密码输入环节集成可弹出的软键盘,适合课程设计、嵌入式界面原型及 Qt 网络模块学习。压缩包共 21 个文件,约 2.94MB,以 5 个 cpp 与 4 个 h 文件承载扫描、连接及界面逻辑,3 个 ui 文件描述主界面与 WIFI 设置布局,qrc、png、bmp 等提供图标和背景素材,pro 文件与软键盘模块便于直接构建和复用。已有 1014 人学习。读者可据此梳理 Linux 下 Qt 调用外部服务完成无线管理的完整链路,理解界面刷新、加密类型选择、密码软键盘联动及列表数量控制等实现方式,并在此基础上替换业务界面或扩展连接状态提示、错误处理与配置保存,形成可运行的 WIFI 连接工具。
1. 一块触摸屏加一个 WiFi 列表:这个 Qt 小工具的定位
现场装好的设备只有一块七寸触摸屏,没有鼠标也没有实体键盘,客户需要自己选择 WiFi、输入密码、看到连接结果。在这个场景里,Qt 自带的那套网络配置接口往往帮不上忙,真正负责连接的是系统里的 wpa_supplicant,而 Qt 要做的是把它的文本输出变成可以点击的列表,再配一个能自动弹出的软键盘。这份源码解决的正是这三件事:WiFi 搜索、连接状态显示、输入法弹窗。它适合做嵌入式面板、自助终端、工控屏的开发者,也适合刚接手 Qt Widgets 项目、想把 QProcess 和焦点管理弄明白的人。整个工程不引入第三方库,纯 Qt 加系统命令,换块板子改两行配置就能跑。
2. 工程结构与技术选型:Qt Widgets 加 QProcess 两件套
这个项目的技术组合非常朴素:界面用 Qt Widgets 手写,底层靠 QProcess 调系统命令。看着不够“现代”,但在嵌入式面板这个场景里是最稳的路子:不依赖额外的服务,编译产物小,出问题时能直接在命令行里复现同一条命令,排查起来不会面对无法直接观察的状态。
2.1 目录结构与模块划分
工程按职责切成了四层,UI 层不碰任何系统命令,后端层不碰任何控件,中间用一个数据结构串起来。这种切法的好处是换平台时只动core/一个目录,界面代码一行不改。
WifiPanel/ ├── WifiPanel.pro # qmake 工程,Qt5.9 以上都能直接打开 ├── main.cpp # 启动、全局焦点监听、键盘单例 ├── core/ │ ├── wifibackend.h # 后端抽象接口,UI 只认它 │ ├── wpabackend.h/.cpp # Linux 实现:wpa_cli │ └── netshbackend.h/.cpp # Windows 实现:netsh wlan ├── model/ │ ├── wifilistmodel.h/.cpp # 列表数据,按信号强度降序 │ └── wifiitem.h # 一条热点的结构体 ├── ui/ │ ├── mainwindow.h/.cpp/.ui # 扫描按钮、热点列表、状态栏 │ ├── wificard.h/.cpp # 单条热点卡片,含信号格与锁图标 │ └── passworddialog.* # 密码输入弹窗 └── input/ ├── softkeyboard.h/.cpp # 自绘软键盘 └── keyboard_layout.h # 按键布局表,换键盘只动这个文件wifiitem.h里只有四个字段,是整个工程里被引用最多的一个结构体:
struct WifiItem { QString ssid; // 热点名,显示在卡片第一行 QString bssid; // 用于区分同名热点,列表里不显示 int rssi = -100; // dBm,-30 极强,-90 几乎连不上 bool secure = false; // true 时卡片上挂一把锁,点进去弹密码框 };字段顺序和后续解析代码强绑定,rssi默认给 -100 而不是 0,是为了让“没解析到信号强度”的热点自动排到列表最后,不至于因为默认值 0 反而被当成满格信号顶到第一位。
2.2 为什么不用 Qt 自带的网络配置接口
QNetworkConfigurationManager这条路看起来最“Qt”,实际用起来坑不少。Qt 5.15 起它已经被标记废弃,Qt 6 直接移除,一旦哪天升级 Qt 版本,整个扫描模块就得重写。更麻烦的是它在 Linux 上的实现依赖 NetworkManager 的 bearer 插件,而大量精简过的嵌入式镜像里只装了 wpa_supplicant,根本没有 NetworkManager,allConfigurations()返回的是空列表,界面上就是一个永远不会填满的空框。
所以这里改成了最原始也最可控的做法:QProcess 调wpa_cli,拿回纯文本自己解析。代价是要自己处理异步和编码,收益是任何一台能连 WiFi 的 Linux 设备上都能跑,而且出问题时你在终端里敲同一条命令,看到的就是程序看到的东西,不用去猜 Qt 内部到底做了什么。
软键盘同理。Qt 官方的虚拟键盘模块(QT_IM_MODULE=qtvirtualkeyboard)功能完整,但它要求编译 Qt 时带上该模块,很多预编译的嵌入式 Qt 包并不包含。工程里干脆自己画一个键盘,按键布局放在keyboard_layout.h的数组里,改键位就是改几行数据。
2.3 编译配置与运行时依赖
.pro文件很干净,只依赖 core、gui、widgets 三个模块,没有 network 也没有 dbus:
# WifiPanel.pro —— 只依赖三个 Qt 模块,交叉编译时不用同步裁剪 QT += core gui widgets CONFIG += c++11 TARGET = WifiPanel TEMPLATE = app SOURCES += main.cpp \ core/wpabackend.cpp \ model/wifilistmodel.cpp \ ui/mainwindow.cpp \ ui/wificard.cpp \ ui/passworddialog.cpp \ input/softkeyboard.cpp HEADERS += core/wifibackend.h \ core/wpabackend.h \ model/wifiitem.h \ ui/mainwindow.h \ input/softkeyboard.h # 嵌入式板子上常用 eglfs 或 linuxfb,没有窗口管理器 # 编译时不用写死,运行时用 QT_QPA_PLATFORM 指定即可运行时依赖只有三样,缺一样程序都会“能启动但扫不到任何热点”:一是系统里wpa_supplicant服务正在跑,二是wpa_cli可执行文件在 PATH 里,三是网卡接口名对得上。接口名默认写wlan0,很多板子叫wlp2s0或者wlan1,程序启动时会先用ip link列一遍做校验。
权限是另一个必须提前处理的问题。wpa_cli默认走/var/run/wpa_supplicant/wlan0这个控制 socket,普通用户没有读写权限。常见做法是给运行账号加一条 sudoers 规则放行wpa_cli,或者更干脆,在wpa_supplicant.conf里加ctrl_interface_group=netdev,把运行账号塞进netdev组,省掉每次弹密码的麻烦。
提示:如果用 eglfs 平台插件跑,记得确认 Qt 编译时带了该插件;不少预编译包只带 linuxfb,直接跑会停在“This plugin does not support propagateSizeHints”的报错上。
3. 核心链路拆解:从一次扫描到一次连接成功
整条链路上有四个关键节点:触发扫描、拉取结果、解析成列表、发起连接,最后再加一个状态回传把界面文案接上。这里的每一环都有“看起来能用、真机上就崩”的细节,下面按执行顺序拆。
3.1 扫描:异步命令加轮询等待结果
wpa_cli scan是个异步命令,敲下去立刻返回OK,但结果要等射频扫完一轮才有,通常 1 到 3 秒。很多人第一版写成了“scan 完马上 scan_results”,拿到的永远是上一次的缓存,表现就是“第一次点扫描是空的,点第二次才出东西”,这个现象在真机上极其容易被误判成权限问题。
正确的做法是先发扫描,再用一个短周期定时器轮询结果,等到结果非空才认为扫完了:
void WpaBackend::startScan() { // 1. 先触发一次扫描,这个命令是异步的,返回 OK 不代表扫完了 run({"scan"}); // 2. 300ms 轮询一次结果,最多等 10 轮,超时就当失败处理 auto *timer = new QTimer(this); timer->setInterval(300); auto *tries = new int(0); connect(timer, &QTimer::timeout, this, [this, timer, tries] { if (++(*tries) > 10) { // 3 秒还没结果,放弃 timer->stop(); timer->deleteLater(); delete tries; emit scanFailed(QStringLiteral("扫描超时,检查网卡是否被占用")); return; } const QString raw = run({"scan_results"}, 1500); const QList<WifiItem> list = parseScanResults(raw); if (list.isEmpty()) return; // 结果为空说明还没扫完,继续等 timer->stop(); timer->deleteLater(); delete tries; emit scanFinished(list); // 结果非空才算收敛 }); timer->start(); }run()是个薄封装,负责起进程、等结束、取标准输出:
QString WpaBackend::run(const QStringList &args, int timeoutMs) { QProcess p; // wpa_cli 的输出编码跟着系统 locale 走,固定成 UTF-8 免得中文 SSID 变问号 QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.insert("LANG", "C.UTF-8"); p.setProcessEnvironment(env); p.start("wpa_cli", QStringList{"-i", m_iface} + args); if (!p.waitForFinished(timeoutMs)) { p.kill(); // 超时必须 kill,否则进程越积越多 return QString(); } return QString::fromUtf8(p.readAllStandardOutput()); }-i后面跟的是网卡接口名,这是整个后端里唯一必须和硬件对上号的参数。timeoutMs给 1500 是为了兜住“射频卡死”这种极端情况,进程不 kill 掉的话,下一次点击会在后台堆一串僵尸wpa_cli,几分钟后整个界面就开始卡。超时上限设 10 轮是因为 2.4G 环境下扫一轮全信道最坏也就 2 秒多,超过 3 秒基本可以判定是硬件问题而不是等待时间不够。
3.2 结果解析:把文本变成卡片列表
scan_results返回的是标准的制表符分隔文本,第一行是bssid / frequency / signal level / flags / ssid表头,后面每行一条记录。解析时有两个容易踩的点:同名的多 AP 会重复出现,flags 字段里[ESS]并不代表开放网络。
static QList<WifiItem> parseScanResults(const QString &raw) { QHash<QString, WifiItem> best; // key 用 ssid,同名只留信号最强的一条 const QStringList lines = raw.split('\n', Qt::SkipEmptyParts); for (const QString &line : lines) { const QStringList f = line.split('\t'); if (f.size() < 5) continue; // 表头、"Selected interface" 都落在这 if (f.at(0).contains(':')) continue; // 保险起见再挡一次非记录行 WifiItem it; it.bssid = f.at(0).trimmed(); it.rssi = f.at(2).toInt(); // flags 形如 [WPA2-PSK-CCMP][ESS],开放的只有 [ESS],没有 WPA/WEP 字样 it.secure = f.at(3).contains("WPA") || f.at(3).contains("WEP"); it.ssid = f.at(4).trimmed(); if (it.ssid.isEmpty()) continue; // 隐藏网络先不上列表 auto old = best.find(it.ssid); if (old == best.end() || it.rssi > old->rssi) best.insert(it.ssid, it); // 同名热点保留信号更强的那台 } return best.values(); }f.size() < 5这一行同时过滤掉了表头和Selected interface 'wlan0'这类干扰输出,比逐条判断前缀要省事。去重用的是按 ssid 建哈希表、保留 rssi 最大值的策略,因为一个路由器挂 2.4G 和 5G 两个频段时会返回两条完全同名的记录,列表里显示两个一模一样的卡片会让用户很困惑,他分不清该点哪个。
解析完交给WifiListModel之前要按rssi降序排一次,界面上最上面永远是信号最好的那个,用户扫完直接点第一个基本上就能连上。
3.3 连接:add_network 到 select_network
连接一个从未连过的热点,在 wpa_supplicant 的语义里是三步:先申请一个 network 槽位,再往槽位里写 ssid 和密码,最后选中它。这三步任何一步失败都要把槽位删掉,否则配置里会堆一堆残废的 network,下次开机会依次重试,表现就是“开机后 WiFi 图标转半天才连上”。
bool WpaBackend::connectTo(const QString &ssid, const QString &psk) { const QString id = run({"add_network"}).trimmed(); // 返回自增的 network id if (id.isEmpty()) return false; run({"set_network", id, "ssid", "\"" + ssid + "\""}); if (psk.isEmpty()) { run({"set_network", id, "key_mgmt", "NONE"}); // 开放网络必须显式声明 } else { // 密码要用双引号包住,wpa_cli 靠这个区分“字符串参数”和“命令参数” run({"set_network", id, "psk", "\"" + psk + "\""}); } const QString ret = run({"select_network", id}).trimmed(); if (ret != QLatin1String("OK")) { // 密码长度不对、ssid 不存在都会 FAIL run({"remove_network", id}); run({"save_config"}); emit connectFailed(ssid, ret); return false; } m_currentId = id; run({"save_config"}); // 写盘,断电重启后能自动连回来 return true; }几个参数值得单独说。ssid和psk外面的双引号不是可有可无的装饰,wpa_cli 在解析命令行时会把带空格或特殊字符的参数拆开,SSID 里有个空格就整个错位。key_mgmt NONE对应的是开放网络,如果漏掉这一行,wpa_supplicant 会默认按 WPA 去握手,连开放热点会一直卡在4WAY_HANDSHAKE状态。save_config会触发一次全量写盘,在 SD 卡上频繁调用有磨损风险,所以只在连接成功后调一次,不要放在轮询里。
3.4 状态回传:把 wpa_state 翻译成界面文案
连接发起之后状态是异步变的,用一个 500ms 的定时器轮询status最省事,比起上wpa_cli -a挂动作脚本要少很多进程管理的心智负担:
void WpaBackend::pollStatus() { const QString out = run({"status"}, 800); QString state = QStringLiteral("DISCONNECTED"); for (const QString &line : out.split('\n')) { if (line.startsWith("wpa_state=")) state = line.mid(10).trimmed(); if (line.startsWith("ssid=")) m_ssid = line.mid(5).trimmed(); if (line.startsWith("ip_address=")) m_ip = line.mid(11).trimmed(); } emit statusChanged(state, m_ssid, m_ip); }界面层拿到的state是原始字符串,映射关系放在 UI 里做,后端不掺和文案:
| wpa_state | 界面显示 | 界面动作 |
|---|---|---|
| SCANNING | 正在扫描… | 列表显示骨架占位,禁用扫描按钮 |
| ASSOCIATING | 正在连接… | 密码框按钮置灰,防重复提交 |
| 4WAY_HANDSHAKE | 正在验证密码… | 同上,此时不要提前判失败 |
| COMPLETED | 已连接 | 显示 IP,关闭密码框,刷新顶部状态条 |
| DISCONNECTED | 未连接 | 恢复列表可点,允许重试 |
| INACTIVE | 网卡未就绪 | 提示检查接口名,不建议让用户自己重试 |
4WAY_HANDSHAKE这个状态被单独拎出来,是因为它在信号弱的时候能停留七八秒,如果这时界面还显示“正在连接”并允许用户再点一次,很容易触发并发连接,最终两边都失败。
4. 软键盘模块:焦点驱动的输入方案
密码框和 SSID 输入框在触摸屏上没法用物理键盘,软键盘必须满足两个条件才算合格:输入框一获得焦点就自动弹出,失去焦点自动收起;弹出的位置不能盖住正在输入的那个框。工程里用的是自绘 QWidget 加事件注入的方案,比配置输入法插件更可控。
4.1 自绘键盘的按键布局与事件注入
键盘本身是个普通 QWidget,按键矩形由一个静态数组生成,改键位只改keyboard_layout.h:
// keyboard_layout.h —— 每行按键,{显示文字, Qt 键值, 相对宽度权重} static const QList<QList<KeyDef>> kLayout = { {{"1",Qt::Key_1,1},{"2",Qt::Key_2,1},{"3",Qt::Key_3,1},{"4",Qt::Key_4,1}, {"5",Qt::Key_5,1},{"6",Qt::Key_6,1},{"7",Qt::Key_7,1},{"8",Qt::Key_8,1}, {"9",Qt::Key_9,1},{"0",Qt::Key_0,1}}, {{"q",Qt::Key_Q,1},{"w",Qt::Key_W,1},{"e",Qt::Key_E,1},/* … */}, {{"大写",Qt::Key_CapsLock,1.5},{"空格",Qt::Key_Space,5},{"退格",Qt::Key_Backspace,1.5}} };按键事件的注入方式决定了这套键盘好不好用。有人喜欢直接edit->setText(edit->text() + ch)拼字符串,两三行代码就能跑,但光标位置、选中替换、撤销栈全丢了,用户点中间插一个字会发现光标跳到了末尾。正确做法是把按键包成 QKeyEvent 发给当前输入框,让 QLineEdit 自己维护这些状态:
void SoftKeyboard::sendKey(int key, const QString &text) { if (!m_target) return; // 退格、回车这类控制键不能带 text, // 否则 QLineEdit 会先删一个字符、再把这个“文字”插回去 const QString t = (key == Qt::Key_Backspace || key == Qt::Key_Return) ? QString() : text; QKeyEvent press(QEvent::KeyPress, key, Qt::NoModifier, t); QApplication::sendEvent(m_target, &press); // 只发 press 就够了 if (key == Qt::Key_Return) { // 回车即收键盘 emit committed(); hide(); } }只发KeyPress不发KeyRelease是有意为之:QLineEdit 处理字符输入只看 press,多发一个 release 在某些 Qt 版本上会触发两次自动补全。Qt::Key_Space的text要传一个真正的空格字符串" ",传空字符串的话空格键会变成“什么都没发生”。
4.2 焦点跟踪:什么时候弹出、什么时候收起
键盘做成全局单例,靠QApplication::focusChanged信号决定弹不弹。这个信号在main.cpp里只连一次,所有输入框共享同一个键盘实例:
QObject::connect(qApp, &QApplication::focusChanged, [](QWidget *, QWidget *now) { auto *edit = qobject_cast<QLineEdit *>(now); SoftKeyboard *kb = SoftKeyboard::instance(); if (edit && !edit->isReadOnly()) { kb->bindTo(edit); // 记住当前目标,按键往这里打 kb->showAtBottom(); // 贴屏幕底部弹出 } else { kb->hide(); // 焦点跑到列表或按钮上就收起来 } });bindTo()里存的是裸指针,这里必须处理一个常见崩溃:输入框在键盘还开着的时候被deleteLater()销毁,键盘再按键就是野指针。稳妥的做法是在bindTo里连一次QObject::destroyed信号,把m_target置空。isReadOnly()的判断也必要,否则用户点一个只读的展示框,键盘弹出来却打不进字,体验上很像“程序卡了”。
showAtBottom()自己算坐标,不依赖布局管理器:
void SoftKeyboard::showAtBottom() { QWidget *host = parentWidget(); if (!host) return; const int h = 260; // 键盘高度,按屏幕 DPI 调 setGeometry(0, host->height() - h, host->width(), h); raise(); // 必须置顶,不然被热点列表盖住 show(); }4.3 布局避让:键盘不能盖住输入框
键盘占了屏幕底部 260 像素,密码框如果正好在列表下方,弹出来就被压住了,用户一边打字一边看不到自己输的是什么。工程里把内容放进QScrollArea,键盘弹出后主动把焦点控件滚到可视区:
void MainWindow::ensureVisible(QWidget *w) { QScrollArea *area = ui->scrollArea; // 传控件中心点坐标,Y 方向留 40 像素余量,避免控件刚好贴在键盘边缘 const QRect r = w->geometry(); area->ensureVisible(r.center().x(), r.center().y(), 0, 40); }在 800×480 这种小屏上,h不要死磕 260,按屏幕高度取 45% 更合理,横竖屏切换时也不会突然把整个界面挤没。
5. 排查与避坑:五个真机上翻车的现场
下面这些都在真设备上遇到过,共同点是模拟器上完全正常、装到板子上才暴露。
5.1 扫描与连接类问题
现象:程序启动正常,点“扫描”后列表永远是空的,命令行敲wpa_cli scan_results却有数据。原因:网卡接口名写死成了wlan0,而板子上实际叫wlp1s0,wpa_cli -i wlan0一直在连一个不存在的控制 socket,返回的是空字符串而不是报错。解决:启动时先跑一次ip -o link show,把结果里所有非lo的接口名取出来做匹配,找不到就退回到手工配置。别指望用QNetworkInterface::allInterfaces()反查,无线接口在部分驱动下 flags 里不带IsWireless。
现象:第一次连接成功,断开后第二次连同一个热点,一直卡在4WAY_HANDSHAKE直到超时。原因:连接失败时没有清理刚add_network出来的槽位,配置里残留着旧密码,wpa_supplicant 优先用了那个旧槽位。解决:在select_network返回非 OK 的分支里补上remove_network加save_config,并且程序启动时用list_networks清一遍所有历史槽位,只保留当前要用的那个。
现象:中文 SSID 的热点在列表里显示成方块或者问号,点进去连接必失败。原因:进程环境继承了系统的LANG,很多精简镜像里是LANG=空值,wpa_cli输出的编码于是跟着 C locale 走,读出来再fromUtf8就散了。解决:在run()里显式往QProcessEnvironment塞LANG=C.UTF-8,读输出统一fromUtf8。这一条看似玄学,实际是最容易复现的编码坑。
5.2 界面与输入类问题
现象:触摸屏上点密码框,键盘弹出来了,但按键没反应,点了好几下才偶尔打进去一个字。原因:键盘 QWidget 只重写了mousePressEvent,而触摸屏驱动在部分配置下派发的是QTouchEvent,Qt 没做合成,mousePressEvent压根不触发。解决:给键盘控件设setAttribute(Qt::WA_AcceptTouchEvents),并重写event()拦QEvent::TouchBegin,把它转换成自己的按键命中判断。这一条改完之后,触摸响应从“时灵时不灵”变成了跟鼠标完全一致。
现象:键盘弹出后输入框被盖住,用户看不见自己输的密码,输完点确定还报密码错误。原因:只做了showAtBottom(),没做滚动避让,输入框在QScrollArea里的位置没变。解决:在键盘的showEvent里回调主窗口的ensureVisible(m_target),把当前焦点控件滚进可视区。同时在密码框下面预留 40 像素的底部边距,滚动到底时也不会贴边。
6. 二次开发:把后端抽成接口,换平台只改一个类
第一次给这套代码加 Windows 支持时,我把#ifdef Q_OS_WIN直接塞进了 UI 的槽函数里,三个月后再看那段代码已经没法读了。后来重构成一个纯虚接口,UI 层只认接口,具体实现按平台在工厂里选:
// wifibackend.h —— 所有平台实现这一个接口,界面层不出现任何平台宏 class IWifiBackend : public QObject { Q_OBJECT public: explicit IWifiBackend(QObject *parent = nullptr) : QObject(parent) {} virtual void startScan() = 0; virtual bool connectTo(const QString &ssid, const QString &psk) = 0; virtual void disconnectFrom(const QString &ssid) = 0; virtual QString backendName() const = 0; // 用于状态栏显示当前后端 signals: void scanFinished(const QList<WifiItem> &items); void scanFailed(const QString &reason); void statusChanged(const QString &state, const QString &ssid, const QString &ip); }; // 工厂:换平台只动这几行,UI 代码一行不改 static IWifiBackend *createBackend(QObject *parent) { #ifdef Q_OS_WIN return new NetshBackend(parent); // netsh wlan show networks mode=bssid #else return new WpaBackend(parent); #endif }Windows 后端的实现思路和 Linux 完全不同,值得提前知道:netsh wlan show networks mode=bssid只能扫,连接必须先用netsh wlan add profile filename=xxx.xml导入一个描述网络的 XML 配置,再netsh wlan connect name=...。所以那个后端里多了一步“用 SSID 和密码拼 XML 写进临时目录”,密码字段是明文,文件权限记得设成只读,用完立刻删掉。
改造时有几个验证顺序值得记一下:先只跑后端,在main.cpp里手写几行连接scanFinished把结果qDebug出来,确认能扫到再往上接界面;然后单独测connectTo,用错误的密码跑一遍,确认失败分支真的会清理槽位;最后才接界面,重点看快速连点扫描按钮时会不会并发起多个进程。跳过前两步直接调界面,出了问题你分不清是后端解析错了还是信号槽没连上。
一个具体的技巧:在WpaBackend里加一个环境变量开关WIFI_DEBUG=1,打开后把每一行wpa_cli命令和返回内容都打到日志里。我在现场排查时全靠这个开关,只要用户把日志拷回来,基本上一眼能定位是密码错、信号弱还是接口名不对。记得日志里的密码字段要做一次掩码替换,别把用户密码原样写进文件。
从那以后我每次接新的嵌入式板子,都会先在终端里手敲一遍wpa_cli -i xxx scan、scan_results、status,确认三条命令的输出格式跟程序里假设的一致,再动代码。看起来多花五分钟,能省掉后面半小时盯着空列表猜原因的功夫。希望帮到你。
本文还有配套的精品资源,点击获取