RK3506 嵌入式 Qt 控制面板开发教程
从零开始,在幸狐 RK3506 Lyra Pi W 上用 Qt 5.12.9 开发一个带触屏界面的嵌入式控制面板。
硬件: 幸狐 RK3506 Lyra Pi W / 1024×600 触摸屏
软件: Qt 5.12.9 / C++11 / Buildroot Linux
一、环境搭建
1.1 两个 qmake
开发嵌入式 Qt 需要两套 qmake:
| 用途 | 路径 | 说明 |
|---|
| x86 本地调试 | Qt5.12.9/5.12.9/gcc_64/bin/qmake | 编译 x86 版本,电脑上直接运行测试 |
| ARM 交叉编译 | Lyra-sdk/buildroot/output/.../host/bin/qmake | 编译 ARM 版本,部署到开发板 |
为什么需要两个? 嵌入式设备不方便频繁部署调试,先在电脑上跑通 UI 逻辑,再交叉编译到板子上。
1.2 x86 本地测试
mkdir build_x86 && cd build_x86
/home/robbieht/Qt5.12.9/5.12.9/gcc_64/bin/qmake ../YourProject.pro
make -j$(nproc)
./YourProject
1.3 ARM 交叉编译
mkdir build && cd build
/home/robbieht/Lyra-sdk/buildroot/output/rockchip_rk3506_luckfox/host/bin/qmake \
../YourProject.pro "CONFIG+=release" "CONFIG-=debug"
make -j$(nproc)
注意: Release 模式用 CONFIG+=release CONFIG-=debug,嵌入式设备资源有限,不要带调试符号。
1.4 部署到设备
# 把编译产物和插件一起打包
mkdir -p release/plugins
cp build/YourProject release/
cp build/plugins/*.so release/plugins/
# 推送到设备
scp -r release/* root@<设备IP>:/opt/
ssh root@<设备IP>
chmod +x /opt/YourProject
/opt/YourProject
二、插件系统架构
当控制面板需要多个功能模块(终端、文件管理器、蓝牙等)时,插件化是最干净的方案。
2.1 定义接口
// appinterface.h
#include <QWidget>
#include <QString>
class AppInterface {
public:
virtual ~AppInterface() = default;
virtual QString appName() const = 0; // 模块名称
virtual QString iconPath() const = 0; // 侧边栏图标
virtual QWidget* contentWidget() = 0; // 内容页面
virtual void initialize() = 0; // 初始化
};
#define AppInterface_iid "com.lyra.shell.AppInterface"
Q_DECLARE_INTERFACE(AppInterface, AppInterface_iid)
2.2 实现插件
// shellplugin.h
#include <QObject>
#include "appinterface.h"
class ShellPlugin : public QObject, public AppInterface {
Q_OBJECT
Q_INTERFACES(AppInterface)
Q_PLUGIN_METADATA(IID "com.lyra.shell.AppInterface")
public:
QString appName() const override { return "终端"; }
QString iconPath() const override { return ":/icons/terminal.png"; }
QWidget* contentWidget() override;
void initialize() override;
private:
QWidget* m_widget = nullptr; // 懒加载,缓存
};
2.3 独立 .pro 文件
每个插件一个 .pro,编译为独立 .so:
QT += core gui widgets
TARGET = shell
TEMPLATE = lib
CONFIG += plugin
DESTDIR = ../plugins
SOURCES += shellplugin.cpp
HEADERS += shellplugin.h
2.4 主程序加载插件
QString pluginsPath = QCoreApplication::applicationDirPath() + "/plugins";
QDir dir(pluginsPath);
for (const QString& fileName : dir.entryList({"*.so"}, QDir::Files)) {
QPluginLoader loader(dir.absoluteFilePath(fileName));
QObject* obj = loader.instance();
if (AppInterface* app = qobject_cast<AppInterface*>(obj)) {
app->initialize();
// 注册到侧边栏...
}
}
踩坑提醒: 插件类中不要定义名为 connect() 的方法,否则会遮蔽 QObject::connect(),导致所有信号槽连接静默失效。取别的名字,比如 connectDevice()。
三、PTY 伪终端
在 Qt 应用里嵌入一个真正的 bash 终端,需要 Linux 的 PTY(伪终端)。
3.1 核心原理
┌─────────────┐ PTY ┌──────────┐
│ Qt 界面 │ ◄────────► │ bash │
│ (父进程) │ fd 通信 │ (子进程) │
└─────────────┘ └──────────┘
forkpty() 一步完成:创建伪终端对 + fork 子进程。
3.2 实现代码
#include <pty.h>
#include <unistd.h>
#include <QSocketNotifier>
struct winsize ws = {rows, cols, 0, 0};
int ptyFd;
pid_t childPid = forkpty(&ptyFd, nullptr, nullptr, &ws);
if (childPid == 0) {
// 子进程:启动 bash
execlp("/bin/bash", "bash", "--login", nullptr);
_exit(127);
}
// 父进程:用 QSocketNotifier 把 PTY 数据接入 Qt 事件循环
auto* notifier = new QSocketNotifier(ptyFd, QSocketNotifier::Read, this);
connect(notifier, &QSocketNotifier::activated, this, [this, ptyFd]() {
char buf[4096];
ssize_t n = read(ptyFd, buf, sizeof(buf));
if (n > 0) {
// 将终端输出追加到 QTextEdit 或 QPlainTextEdit
appendOutput(QByteArray(buf, n));
}
});
3.3 发送按键到终端
void sendToTerminal(const QString& text) {
QByteArray data = text.toLocal8Bit();
write(ptyFd, data.constData(), data.size());
}
3.4 方向键处理
方向键不是普通字符,需要发送 ANSI 转义序列:
| 按键 | 序列 | 十六进制 |
|---|
| ↑ | \033[A | 1B 5B 41 |
| ↓ | \033[B | 1B 5B 42 |
| → | \033[C | 1B 5B 43 |
| ← | \033[D | 1B 5B 44 |
四、软键盘
嵌入式触屏没有物理键盘,需要自己做一个浮动软键盘。
4.1 布局
┌─────────────────────────────────┐
│ 1 2 3 4 5 6 7 8 9 0 │ ← 数字行
│ Q W E R T Y U I O P │
│ A S D F G H J K L │
│ Z X C V B N M │
│ [Ctrl] [Space] [←] [→] [Enter]│ ← 特殊键
└─────────────────────────────────┘
4.2 实现
// 创建一行按键
QHBoxLayout* row = new QHBoxLayout;
for (const QString& key : {"Q","W","E","R","T","Y","U","I","O","P"}) {
auto* btn = new QPushButton(key);
btn->setFixedSize(60, 50); // 触屏友好:按钮要大
connect(btn, &QPushButton::clicked, [this, key]() {
emit keyPressed(key.toLower());
});
row->addWidget(btn);
}
4.3 特殊键映射
// Enter
connect(btnEnter, &QPushButton::clicked, [this]() {
sendToTerminal("\r");
});
// Backspace
connect(btnBack, &QPushButton::clicked, [this]() {
sendToTerminal("\x7f");
});
// 方向键
connect(btnUp, &QPushButton::clicked, [this]() {
sendToTerminal("\033[A");
});
踩坑提醒: 写键盘布局注释时,行尾不要有 \,它是 C++ 的续行符,会把下一行代码吃掉。看起来完全正常,但行为截然不同。
五、QSS 主题系统
5.1 基本架构
themes/
├── dark.qss # 深色主题
└── light.qss # 浅色主题
通过 resources.qrc 编译进二进制,运行时通过 ThemeManager 切换。
5.2 QSS 写法
/* dark.qss */
QWidget#sidebar {
background-color: #1e1e2e;
}
QPushButton#sidebar-item {
color: #cdd6f4;
border: none;
padding: 12px;
border-radius: 8px;
}
QPushButton#sidebar-item:hover {
background-color: #313244;
}
5.3 主题切换
// ThemeManager 单例
class ThemeManager : public QObject {
Q_OBJECT
public:
static ThemeManager* instance();
void setTheme(const QString& name); // "dark" 或 "light"
QString currentTheme() const;
private:
QString m_theme;
// 配置持久化到 ~/.lyrashell/theme.conf
};
5.4 自定义绘制组件
QSS 能覆盖标准控件,但自己用 QPainter 画的组件需要手动判断主题:
void SidebarItem::drawIcon(QPainter& painter) {
if (ThemeManager::instance()->currentTheme().startsWith("light")) {
painter.setPen(QColor("#333333")); // 浅色主题用深色图标
} else {
painter.setPen(QColor("#ffffff")); // 深色主题用浅色图标
}
// QPainterPath 绘制图标...
}
六、触屏交互
6.1 长按菜单
触屏没有右键,用长按模拟:
// 安装事件过滤器
widget->installEventFilter(this);
widget->setContextMenuPolicy(Qt::CustomContextMenu);
bool MyWidget::eventFilter(QObject* obj, QEvent* event) {
if (event->type() == QEvent::MouseButtonPress ||
event->type() == QEvent::TouchBegin) {
// 启动 600ms 定时器
m_longPressTimer->start(600);
}
else if (event->type() == QEvent::MouseButtonRelease ||
event->type() == QEvent::TouchEnd) {
m_longPressTimer->stop(); // 提前释放,取消
}
return QObject::eventFilter(obj, event);
}
// 定时器触发 → 发射右键信号
connect(m_longPressTimer, &QTimer::timeout, [this, pos]() {
emit customContextMenuRequested(pos);
});
6.2 中文字体
嵌入式系统默认没有中文字体。添加文泉驿正黑:
// 编译时把字体加入 resources.qrc
// 运行时加载
int fontId = QFontDatabase::addApplicationFont(":/fonts/wqy-zenhei.ttc");
QString family = QFontDatabase::applicationFontFamilies(fontId).at(0);
QApplication::setFont(QFont(family));
七、读取系统信息
7.1 CPU 使用率
读 /proc/stat,两次采样算差值:
QFile file("/proc/stat");
file.open(QIODevice::ReadOnly | QIODevice::Text);
QString line = file.readLine(); // "cpu user nice system idle iowait irq softirq"
QStringList parts = line.split(QRegExp("\\s+"));
qint64 idle = parts[4].toLongLong();
qint64 total = 0;
for (int i = 1; i <= 7; i++) total += parts[i].toLongLong();
// 两次采样的差值
int cpuPercent = (totalDiff - idleDiff) * 100 / totalDiff;
7.2 内存信息
读 /proc/meminfo,用 QFile::readLine() 而不是 QTextStream:
QFile file("/proc/meminfo");
file.open(QIODevice::ReadOnly | QIODevice::Text);
while (!file.atEnd()) {
QString line = QString::fromLocal8Bit(file.readLine()).trimmed();
if (line.startsWith("MemTotal:")) {
// 提取冒号后的数字
QString val = line.section(':', 1).trimmed();
QString numStr;
for (const QChar& c : val) {
if (c.isDigit()) numStr += c;
else if (!numStr.isEmpty()) break;
}
memTotal = numStr.toLongLong(); // 单位 kB
}
// MemFree, MemAvailable, Buffers, Cached 同理...
}
重要: 读 /proc 和 /sys 虚拟文件时,不要用 QTextStream。它内部的缓冲区和 seek 操作与虚拟文件系统不兼容,在嵌入式设备上可能读到空内容。用 QFile::readLine() 或 QFile::readAll() 直接读取。
八、双栏文件管理器
8.1 架构
┌──────────────────────────────────────┐
│ 工具栏: [复制] [剪切] [粘贴] [删除] │
├────────────────┬─────────────────────┤
│ 左栏 │ 右栏 │
│ /home/user │ /media/usb │
│ ├─ Documents │ ├─ file1.txt │
│ ├─ Pictures │ └─ file2.txt │
│ └─ Music │ │
├────────────────┴─────────────────────┤
│ 状态栏 │
└──────────────────────────────────────┘
8.2 活动面板追踪
int m_activePanel = 0; // 0=左, 1=右
// 点击左栏
connect(leftView, &QListView::clicked, [this]() {
m_activePanel = 0;
});
// 点击右栏
connect(rightView, &QListView::clicked, [this]() {
m_activePanel = 1;
});
// 工具栏操作作用于活动面板
void onCopy() {
if (m_activePanel == 0) {
// 从左栏复制到右栏
m_rightManager->paste(m_leftManager->selectedFiles());
} else {
m_leftManager->paste(m_rightManager->selectedFiles());
}
}
九、蓝牙
9.1 bluetoothctl 常用命令
bluetoothctl
power on
scan on
trust XX:XX:XX:XX:XX:XX
pair XX:XX:XX:XX:XX:XX
connect XX:XX:XX:XX:XX:XX
9.2 程序化调用
# 非交互式执行
echo 'power on' | bluetoothctl
echo 'scan on' | bluetoothctl
注意: RK3506 上 Wi-Fi 和蓝牙共用模块,切换蓝牙可能需要修改 /usr/bin/wifibt-util.sh。
十、常见问题速查表
| 问题 | 原因 | 解决 |
|---|
| SVG 图标不显示 | 嵌入式 Qt 无 SVG 模块 | 用 QPainter 绘制或 PNG 替代 |
| 中文显示方块 | 缺少中文字体 | 添加文泉驿 wqy-zenhei.ttc |
| 图片加载失败 | qrc 路径与代码不一致 | 确保 ":/path" 与 qrc 中完全一致 |
| 信号槽不生效 | 自定义方法名遮蔽 QObject::connect | 重命名,避免 connect() 等保留名 |
QProcess::finished 编译错误 | Qt 5 有两个重载 | 用 QOverload<int, QProcess::ExitStatus>::of(...) |
| 内存信息 N/A | QTextStream 不兼容 /proc | 改用 QFile::readLine() |
| 触屏无右键菜单 | customContextMenuRequested 不响应触摸 | eventFilter + 600ms 长按定时器 |
| 奇怪的编译/运行错误 | 注释末尾的 \ 续行符 | 删除注释末尾的反斜杠 |
项目结构参考
YourProject/
├── main.cpp
├── mainwindow.h/cpp # 主窗口
├── YourProject.pro
├── resources.qrc # 资源文件
├── fonts/ # 字体(编译进资源)
├── resources/ # 图片资源
├── themes/ # QSS 主题文件
├── utils/ # 工具类(ThemeManager 等)
├── sidebar/ # 侧边栏
├── content/ # 内容区域
├── widgets/ # 自定义控件
├── apps/ # AppInterface + AppManager
├── sysinfo/ # 系统信息模块
├── filemanager/ # 文件管理器
├── shell/ # Shell 终端插件(独立 .pro)
├── bluetooth/ # 蓝牙模块
├── build/ # ARM 编译输出(.gitignore)
├── build_x86/ # x86 编译输出(.gitignore)
└── release/ # 部署包(.gitignore)