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[A1B 5B 41
\033[B1B 5B 42
\033[C1B 5B 43
\033[D1B 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/meminfoQFile::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/AQTextStream 不兼容 /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)

标签: none

添加新评论