跳到主要内容

QT 界面设计指南

本教程利用QT5.12.9实现86面板的屏幕界面设计以及控件交互逻辑,旨在搭建一个完整的嵌入式GUI应用程序。

1. 简介

86面板可运用在智能家具的控制层场景中,本设计的智能终端,总共包含三个界面,分别为主界面,wifi配置界面,副界面,其功能清单如下:

界面功能描述
主界面显示WIFI和ETH链接状态显示当前的网络连接状态,包括 WIFI 和以太网(ETH)的 IP 地址信息。如果没有网络连接,显示默认的"--"
主界面显示时间显示当前时间,通过QDateTime获取当前系统时间
副界面继电器开关提供两个继电器开关,用户可以通过滑动开关控制继电器的开合。
副界面音频播放器提供音量调整功能,并支持播放/暂停控制。
WIFI配置界面配置WIFI用户可以输入 WiFi 的 SSID 和密码,支持保存和连接操作。
WIFI配置界面WIFI控制界面用户可以控制wifi链接或者断开,扫描附件wifi

2. 配置环境

对于QT安装及开发板QT环境搭建可参考另一篇教程《QT移植教程》。

本项目基于本项目设计是基于Ubuntu2204 linux下进行开发,QT打开项目文件夹下的.pro文件,并利用对应kits构建工程,构建成功之后,项目架构如下

在Headers目录下存放的是实例化类的头文件,Sources存放的是主函数以及实例化类的具体实现,Forms目录下存放的是通过Qt图形化界面设计的ui文件,Resources目录下存放的是资源文件,主要包括的是图标ICON。

3. 主界面

3.1 主界面UI设计

主界面采用的是图形化界面配合代码设计样式实现UI界面设计,在Widget.ui中可以看到如下组件:

预览ui界面如下
alt text

  • 界面优化方向 可以看出,虽然我在 QT Designer 上简单地做了一些界面效果,但目前设计的界面还十分简陋。我们可以通过设置组件的样式表来改变显示效果,本项目采用的是在构造函数中通过代码来对组件的样式进行设置。
  • 特定组件分析(以 widget_wifi 为例) 其中以 widget_wifi 为例,此 QWidget 包含着主界面显示 Wi-Fi 状态的组件。具体来说:
    - QLabel:用于显示 Wi-Fi 名字及 IP。
    - 按钮:实现自定义功能,在本设计中实现的是对于 Wi-Fi 配置界面的跳转。
  • 其他组件情况 其余组件与 widget_wifi 类似。

在Widget.cpp中的构造函数中有如下代码

Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget),
scrplayer(nullptr)
{
...
ui->btn_wifi->setStyleSheet(R"(
QPushButton {
border-radius: 30px;
background-color: #515151;
color: white;
font-size: 14px;
border: none;
outline: none;
}
QPushButton:hover {
background-color: #515151;
}
QPushButton:pressed {
background-color: #515151;
}
)");
QPixmap pixmap(":/image/wifidis.png");)
QPixmap scaledPixmap = pixmap.scaled(35, 35, Qt::KeepAspectRatio, Qt::SmoothTransformation);
ui->btn_wifi->setIcon(QIcon(scaledPixmap));
ui->btn_wifi->setIconSize(QSize(35, 35));
...
}
  • 代码说明:
    • 设置样式表: 使用setStyleSheet设置了一个圆形按钮且背景颜色为灰色(#515151),无边框(border: none),取消聚焦(outline: none),松开状态(QPushButton:hover),按下状态(QPushButton:hover)等。
    • 设置按钮图标: 通过QPixmap,得到图标文件,该文件保存在工程文件夹下的image文件夹下,并添加到资源文件夹下,后续可通过此种方式增加图标资源。然后通过setIcon设置按钮图标。

其他主界面组件设计与本例类似。

3.2 显示WIFI状态

1.在主界面Widget.ui中有如下控件:widget_wifi,btn_wifi,lb_wifiname,lb_wifiip,用于显示wifi状态,在构造函数对其初始化样式也有设置。作为主界面ETH状态的显示容器。通过类中成员函数updateWifiStatus更新状态组件,其代码如下:

void Widget::updateWifiStatus(QLabel* lb_wifiname, QLabel* lb_wifiip)
{
// 执行wpa_cli status命令
QProcess process;
process.start("wpa_cli", QStringList() << "status");
if (!process.waitForStarted() || !process.waitForFinished(3000)) {
qWarning() << "Failed to run wpa_cli command";
return;
}
// 解析命令输出
QString output = process.readAllStandardOutput();
QStringList lines = output.split('\n');

QString ssid;
QString ip;
bool scanning = false;

for (const QString& line : lines) {
if (line.contains("wpa_state=SCANNING")) {
scanning = true;
break;
}
else if (line.startsWith("ssid=")) {
ssid = line.section('=', 1).trimmed();
}
else if (line.startsWith("ip_address=")) {
ip = line.section('=', 1).trimmed();
}
}
// 更新UI
QFont normalFont = lb_wifiname->font();
if (scanning || ssid.isEmpty()) {
// 未连接状态
lb_wifiname->setText("----");

lb_wifiname->setStyleSheet("color: #ffffff; background: transparent; border: none;");

lb_wifiip->setText("No IP");

lb_wifiip->setStyleSheet("color: #ffffff; background: transparent; border: none;");
ui->btn_wifi->setIcon(QIcon(":/image/wifidis.png"));
ui->widget_wifi->setStyleSheet(
"QWidget#widget_wifi {"
" background-color: #8a8a8a;"
" border: 2px solid #8a8a8a;"
" border-radius: 15px;"
"}");
} else {
// 已连接状态
lb_wifiname->setText(ssid);
lb_wifiname->setStyleSheet("color: #f4ea2a; background: transparent; border: none;");
ui->btn_wifi->setIcon(QIcon(":/image/wifionline.png"));
if (ip.isEmpty()) {
lb_wifiip->setText("No IP");
lb_wifiip->setStyleSheet("color: #f4ea2a; background: transparent; border: none;");
} else {
lb_wifiip->setText(ip);
lb_wifiip->setFont(normalFont);
lb_wifiip->setStyleSheet("color: #f4ea2a; background: transparent; border: none;");
}
ui->widget_wifi->setStyleSheet(
"QWidget#widget_wifi {"
" background-color: #8a8a8a;"
" border: 2px solid #f4ea2a;"
" border-radius: 15px;"
"}");
}
}

updateWifiStatus函数负责获取当前Wi-Fi连接的状态(SSID和IP地址),并更新WIFI状态组件。

  • 代码步骤说明
    • 执行wpa_cli status命令: 构造出"wpa_cli status"字符串,通过QProcess执行命令,并且对进程无法正确进行和超时进行错误处理
    • 解析命令输出:
      • 如果找到包含 wpa_state=SCANNING 的行,表示设备正在扫描可用网络,此时直接跳出循环。
      • 如果找到包含 ssid= 的行,则提取出SSID名称。
      • 如果找到包含 ip_address= 的行,则提取出IP地址。
    • 更新状态组件: 处理 WiFi 连接和未连接时的 UI 显示标识。

2.更新WIFI状态理应实时更新,本设计采用了QTimer定时器,计时刷新,在Wdiget.hpp中有如下成员变量:

    QTimer *networkTimer;

在构造函数中对其初始化和链接定时触发函数

    networkTimer = new QTimer(this);
connect(networkTimer, &QTimer::timeout, this, &Widget::checkNetworkStatus);
networkTimer->start(3000); // 每3秒检查一次
update_eth_status();

在定时触发函数中调用updateWifiStatus,便实现定时刷新主界面WIFI状态。

自此,显示WIFI状态部分完成,运行图像如下:

3.3 显示ETH状态

1.在主界面Widget.ui中有如下控件:widget_net,btn_net,lb_netname,lb_netip,用于显示wifi状态,在构造函数对其初始化样式也有设置。作为主界面WIFI状态的显示容器。通过类中成员函数updateWifiStatus更新状态组件,其代码如下:

/*****eth******/
void Widget::update_eth_status(void)
{
static int last_carrier_state = -1;
int current_carrier_state = 0;

// 1. 检测以太网物理连接状态
FILE *carrier_file = fopen("/sys/class/net/eth0/carrier", "r");
if (carrier_file != NULL) {
fscanf(carrier_file, "%d", &current_carrier_state);
fclose(carrier_file);
}
// 2. 处理连接状态变化
if (current_carrier_state != last_carrier_state) {
if (current_carrier_state == 1) { // 连接状态
// 如果已有 udhcpc 运行则杀死
if (system("pgrep udhcpc") == 0) {
system("killall udhcpc");
}
// 启动新的 DHCP 客户端
system("udhcpc -i eth0 > /tmp/udhcpc.log 2>&1 &");
}else if (last_carrier_state == 1) { // 连接断开
qDebug() << "Ethernet disconnected. Stopping DHCP...";
// 停止 DHCP 客户端
if (system("pgrep udhcpc") == 0) {
system("killall udhcpc");
}
// 立即清除IP显示
ui->label_netname->setText("----");
ui->label_netip->setText("Disconnected");
}

last_carrier_state = current_carrier_state;
}
// 3. 如果连接断开,直接显示断开状态
if (current_carrier_state == 0) {
ui->btn_net->setIcon(QIcon(":/image/internet-error-solid.png"));
ui->label_netname->setText("----");
ui->label_netip->setText("No IP");
ui->label_netname->setStyleSheet("color: #ffffff; background: transparent; border: none;");
ui->label_netip->setStyleSheet("color: #ffffff; background: transparent; border: none;");
ui->widget_net->setStyleSheet(
"QWidget#widget_net {"
" background-color: #8a8a8a;"
" border: 2px solid #8a8a8a;"
" border-radius: 15px;"
"}");
return; // 不需要继续检查IP
}
// 4. 获取网络接口信息
FILE *fp;
char command[MAX_LINE_LEN];
char result[MAX_LINE_LEN];
char eth_name[MAX_LINE_LEN] = {0};
char eth_ip_address[MAX_LINE_LEN] = {0};
strcpy(command, "ifconfig");
// 执行 ifconfig 命令
fp = popen(command, "r");
if (fp == NULL) {
printf("Failed to run command\n");
return;
}
// 5. 解析 ifconfig 输出
int in_eth_block = 0;
static int has_ip = 0;
while (fgets(result, sizeof(result) - 1, fp) != NULL)
{
// 查找 eth0 接口
if (strstr(result, "eth0")) {
in_eth_block = 1;
sscanf(result, "%s", eth_name);
//lv_label_set_text(ui_LabelEth, eth_name);
ui->label_netname->setText(QString(eth_name));
qDebug() << "ethname " << eth_name;
continue;
}
// 检测接口块结束
if (in_eth_block && result[0] == '\n') {
in_eth_block = 0;
break;
}
// 提取 IP 地址
if (in_eth_block && strstr(result, "inet addr:")) {
char *ip_start = strstr(result, "inet addr:");
if (ip_start) {
ip_start += 10; // 跳过 "inet addr:"
char *ip_end = strchr(ip_start, ' ');
if (ip_end) {
*ip_end = '\0';
}
strncpy(eth_ip_address, ip_start, MAX_LINE_LEN - 1);
eth_ip_address[MAX_LINE_LEN - 1] = '\0';
has_ip = 1;
}
break;
}
}
pclose(fp);
if(has_ip)
{
ui->btn_net->setIcon(QIcon(":/image/neton.png"));
ui->label_netip->setText(QString(eth_ip_address));
ui->label_netname->setStyleSheet("color: #f4ea2a; background: transparent; border: none;");
ui->label_netip->setStyleSheet("color: #f4ea2a; background: transparent; border: none;");
ui->widget_net->setStyleSheet(
"QWidget#widget_net {"
" background-color: #8a8a8a;"
" border: 2px solid #f4ea2a;"
" border-radius: 15px;"
"}");
}
else
{
ui->label_netname->setText("-------");
ui->label_netip->setText("No IP");
ui->label_netname->setStyleSheet("color: #ffffff; background: transparent; border: none;");
ui->label_netip->setStyleSheet("color: #ffffff; background: transparent; border: none;");
ui->widget_net->setStyleSheet(
"QWidget#widget_net {"
" background-color: #8a8a8a;"
" border: 2px solid #8a8a8a;"
" border-radius: 15px;"
"}");
}
}
  • 代码步骤说明:
    • 物理连接检测:
      • 读取/sys/class/net/eth0/carrier文件判断物理连接状态(0=断开,1=连接)
      • 使用last_carrier_state静态变量跟踪状态变化
    • DHCP客户端管理:
      • 当物理连接建立时:杀死已有udhcpc进程并启动新进程
      • 当物理连接断开时:杀死udhcpc进程
      • 使用system("pgrep udhcpc")检测进程存在性
    • UI状态更新:
        - 连接断开时:显示灰色断开状态
      - 连接建立时:解析ifconfig输出获取IP地址
      - 如果发现包含 "eth0" 的行,设置 in_eth_block 为1,并尝试从该行中提取eth0接口
      的名称。
          - 如果在eth0信息块内遇到空行,认为eth0接口的信息已经结束,退出循环。
      - 如果在eth0信息块内并且行中包含 "inet addr:" ,则进一步解析出IP地址,并设置has_ip 为1。
      - 根据IP获取结果更新界面样式(黄色连接/灰色断开)

2.与更新显示wifi状态一样,更新显示ETH状态也需要定时器定时刷新状态,本设计ETH与wifi用的是同一个定时器,这样可以同步更新二者状态,定时器触发函数如下:

void Widget::checkNetworkStatus()
{
update_eth_status();
updateWifiStatus(ui->lb_wifiname, ui->lb_wifiip);
}

至此,显示ETH状态部分完成,运行图像如下:

3.4 显示时间

1.在主界面Widget.ui中有如下控件:widget_time,labeltime,labeldate用于显示当前时间,在构造函数对其初始化样式也有设置。作为主界面当前时间的显示容器。以下是更新当前时间状态的代码:

void Widget::updatetimedisplay()
{
// 获取 UTC 时间
QDateTime utcDateTime = QDateTime::currentDateTimeUtc();

// 转换为北京时间(UTC+8)
QDateTime beijingDateTime = utcDateTime.addSecs(8 * 3600);

QTime currenttime = beijingDateTime.time();
QString timeStr = QString("%1:%2")
.arg(currenttime.hour(), 2, 10, QChar('0'))
.arg(currenttime.minute(), 2, 10, QChar('0'));

ui->labeltime->setText(timeStr);

QDate currentdate = beijingDateTime.date();
QString dateStr = currentdate.toString("yyyy-MM-dd dddd");
ui->labeldate->setText(dateStr);
}
  • 代码功能说明
    • 使用QDateTime::currentDateTimeUtc()获取当前的UTC时间。
    • 使用addSecs(8 * 3600)将UTC时间加上8小时(即28800秒)得到北京时间。
    • 使用QDateTime::time()获取时间部分,然后使用QString::arg将小时和分钟格式化为两位数(不足两位前面补0)。
    • 使用QDateTime::date()获取日期部分,然后使用QDate::toString按照指定格式输出字符串。其中"yyyy"代表四位数的年份,"MM"代表两位数的月份,"dd"代表两位数的日期,"dddd"代表星期几的全称(如Monday)。
    • 最后将格式化后的字符串设置到对应的UI标签上。

2.更新当前时间需要时效性,所有并没有将更新时间与wifi和eth更新一同更新,而是定义了另外的定时器,单独为更新时间计时:

//widget.h
QTimer *timer;
//widget.cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget),
scrplayer(nullptr)
{
...
//time
timer = new QTimer(this);
connect(timer,&QTimer::timeout,this,&Widget::updatetimedisplay);
timer->start(1000);
updatetimedisplay();
...
}

定时器每一秒检测当前时间并更新UI状态。 至此,显示时间部分完成,运行图像如下:

3.5 页面切换功能

本项目中总共有三个界面,主界面分别设计了两种切换页面的方式:按钮触发信号与槽切换页面,滑动触发事件处理切换页面,以下是两种方式的具体介绍。

3.5.1 切换wifi界面

本项目,将主界面与wifi配置界面的切换,以btn_wifi的单击事件实现,在widget.ui中右键btn_wifi,选择转到槽,并选择单击clicked(),如图:
alt text

便会在wiget.h中声明该槽函数:

private slots:
void on_btn_wifi_clicked();

实现槽函数:

void Widget::on_btn_wifi_clicked()
{
wifi.show();
this->hide();
}

其中,wifi是wifi配置界面类实例化的对象,通过调用显示函数show(),以及将主界面隐藏,以实现页面切换功能。

3.5.2 切换副界面

本项目,主界面与副界面切换实现为滑动屏幕触发切换动画的效果,原理为重写鼠标点击和移动的事件,以下是实现步骤:

  1. 在wiget.h中声明三个事件函数:

    ```cpp
    protected:
    void mousePressEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);
    void mouseReleaseEvent(QMouseEvent *event);
    ```

    这三个事件分别为鼠标按下,移动,松开事件。

  2. 在widget.cpp中对三个事件进行重写:

    • 鼠标按下事件:
    void Widget::mousePressEvent(QMouseEvent *event)
    {
    m_startcoly = event->y();
    //qDebug() << "old y is " << m_startcoly;
    QWidget::mousePressEvent(event);
    }

    当鼠标按下事件触发时,获取当前鼠标所在位置的坐标y值。

    • 鼠标松开事件:
    void Widget::mouseReleaseEvent(QMouseEvent *event)
    {
    m_curcoly = event->y();
    //qDebug() << "new y is " << m_curcoly;
    QWidget::mousePressEvent(event);
    }

    同样在鼠标松开事件触发时,获取当前鼠标所在位置的坐标y值。

    • 鼠标移动事件:
    void Widget::cancelAnimations()
    {
    // 停止并删除所有进行中的动画
    QList<QPropertyAnimation*> anims = findChildren<QPropertyAnimation*>();
    foreach(QPropertyAnimation* anim, anims) {
    if(anim->state() == QPropertyAnimation::Running) {
    anim->stop();
    anim->deleteLater();
    }
    }
    m_isAnimating = false;
    }
    void Widget::mouseMoveEvent(QMouseEvent *event)
    {
    if (m_isAnimating)
    {
    int reverseDelta = event->y() - m_curcoly;
    if (reverseDelta < -10) { // 反向滑动阈值
    cancelAnimations();
    }
    return;
    }
    m_curcoly = event->y();
    int delta = m_curcoly - m_startcoly;
    if(delta > 30)
    {
    m_isAnimating = true;
    setEnabled(false); // 禁用交互防止打断
    if (!scrplayer) {
    scrplayer = new ScreenPlayer(this,nullptr);
    }
    // 确保位置正确
    scrplayer->move(0, -height());
    if (!scrplayer->isVisible()) {
    scrplayer->setWindowModality(Qt::ApplicationModal);
    scrplayer->show();
    }
    QPropertyAnimation *animation = new QPropertyAnimation(this, "pos");
    animation->setDuration(500);
    animation->setStartValue(pos());
    animation->setEndValue(QPoint(0, height()));
    QPropertyAnimation *animation2 = new QPropertyAnimation(scrplayer, "pos");
    animation2->setDuration(500);
    animation2->setStartValue(scrplayer->pos());
    animation2->setEndValue(QPoint(0, 0));
    // 使用安全连接
    connect(animation, &QPropertyAnimation::finished, this, [=](){
    animation->deleteLater();
    onAnimationFinished();
    });
    connect(animation2, &QPropertyAnimation::finished, this, [=](){
    animation2->deleteLater();
    });
    animation->start();
    animation2->start();
    }
    m_startcoly = m_curcoly;
    QWidget::mouseMoveEvent(event);
    }
    • 代码步骤说明:
    • delta记录最终位置y值-起始位置y值,一旦delta大于30,即为下滑操作,触发切换逻辑。
    • 动画创建:
      • 通过QPropertyAnimation实现切换动画,总共为两个动画,主界面移动到屏幕底部,副界面对象移动到屏幕顶部,实现切换动画。
    • 动画生命周期管理:
      • 利用动画完成时的finish信号实现删除完成的动画对象。
    • 反向滑动检测
      • 当用户在切换界面时突然反向滑动,对此情况进行暂停当前动画,并播放反向滑动的动画逻辑。

至此,切换页面逻辑已实现完毕。

4. WIFI配置界面

4.1 WIFI配置页面UI

本项目中,WIFI的配置界面并没有与主界面一样配合Qt Designer与代码一起实现,而是以纯代码为主,在wifiset.h中可以看到:

其中大多数为WIFI配置界面的显示组件,跟Qt Designer不同的是,我们可以在头文件看到组件的声明,而组件的实例化和样式也需要我们自己实现,在WIFI配置界面的构造函数有调用uiinit成员函数,其为配置界面的初始化函数,代码如下:

void wifiset::uiinit()
{
// 设置主窗口背景颜色
setStyleSheet("background-color: #0D0D0D;");
// 创建主布局
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// 创建列表面板
QWidget *panelList = new QWidget(this);
panelList->setStyleSheet("background-color: #1F1F1F;");
QHBoxLayout *listLayout = new QHBoxLayout(panelList);
// 创建WLAN标签
QLabel *labelWLAN = new QLabel("WLAN", panelList);
labelWLAN->setStyleSheet("color: #545454; font-size: 48px;");
listLayout->addWidget(labelWLAN,1);
// 创建SSID下拉菜单
m_dropdownSSID = new QComboBox(panelList);
m_dropdownSSID->addItems({"---", "---", "---", "---", "---", "---"});
m_dropdownSSID->setStyleSheet("color: #545454; font-size: 32px; background-color: #000000; border: 1px solid #333333; padding: 10px 20px;border-radius: 10px;");
connect(m_dropdownSSID, SIGNAL(currentIndexChanged(int)), this, SLOT(onDropdownSSIDChanged(int)));
listLayout->addWidget(m_dropdownSSID,3);
mainLayout->addWidget(panelList,1);
// 创建信息标签和文本框
QWidget *infoWidget = new QWidget(this);
QVBoxLayout *infoLayout = new QVBoxLayout(infoWidget);
// SSID信息
QHBoxLayout *ssidLayout = new QHBoxLayout();
QLabel *labelSSID = new QLabel("SSID", infoWidget);
labelSSID->setStyleSheet("color: #FFFFFF; font-size: 48px;");
m_textAreaSSID = new QLineEdit("---", infoWidget);
m_textAreaSSID->setFocusPolicy(Qt::NoFocus);
m_textAreaSSID->setStyleSheet("color: #A9A8A8; font-size: 32px; background-color: rgba(255, 255, 255, 15); border: 1px solid rgba(255, 255, 255, 25); padding: 10px 20px;border-radius: 10px;");
ssidLayout->addWidget(labelSSID, 1); // 标签占1份空间
ssidLayout->addWidget(m_textAreaSSID, 3); // 输入框占3份空间
infoLayout->addLayout(ssidLayout);
// Passwd信息
QHBoxLayout *pwLayout = new QHBoxLayout();
QLabel *labelPW = new QLabel("Passwd", infoWidget);
labelPW->setStyleSheet("color: #FFFFFF; font-size: 48px;");
m_textAreaPW = new QLineEdit("waveshare0755", infoWidget);
m_textAreaPW->setStyleSheet("color: #A9A8A8; font-size: 32px; background-color: rgba(255, 255, 255, 15); border: 1px solid rgba(255, 255, 255, 25); padding: 10px 20px;border-radius: 10px;");
pwLayout->addWidget(labelPW,1);
pwLayout->addWidget(m_textAreaPW,3);
infoLayout->addLayout(pwLayout);
connect(m_textAreaPW, &QLineEdit::selectionChanged, this, &wifiset::onLineEditClicked);
// RSSI信息
QHBoxLayout *rssiLayout = new QHBoxLayout();
QLabel *labelRSSI = new QLabel("RSSI", infoWidget);
labelRSSI->setStyleSheet("color: #FFFFFF; font-size: 48px;");
m_textAreaRSSI = new QLineEdit("--dbm", infoWidget);
m_textAreaRSSI->setStyleSheet("color: #A9A8A8; font-size: 32px; background-color: rgba(255, 255, 255, 15); border: 1px solid rgba(255, 255, 255, 25); padding: 10px 20px;border-radius: 10px;");
m_textAreaRSSI->setReadOnly(true);
rssiLayout->addWidget(labelRSSI,1);
rssiLayout->addWidget(m_textAreaRSSI,3);
infoLayout->addLayout(rssiLayout);
// MGMT信息
QHBoxLayout *mgmtLayout = new QHBoxLayout();
QLabel *labelMGMT = new QLabel("PSK", infoWidget);
labelMGMT->setStyleSheet("color: #FFFFFF; font-size: 48px;");
m_textAreaMgnt = new QLineEdit("------", infoWidget);
m_textAreaMgnt->setStyleSheet("color: #A9A8A8; font-size: 32px; background-color: rgba(255, 255, 255, 15); border: 1px solid rgba(255, 255, 255, 25);padding: 10px 20px;border-radius: 10px;");
m_textAreaMgnt->setReadOnly(true);
mgmtLayout->addWidget(labelMGMT,1);
mgmtLayout->addWidget(m_textAreaMgnt,3);
infoLayout->addLayout(mgmtLayout);
mainLayout->addWidget(infoWidget,4);
QString buttonStyle =
"background-color: #404040;"
"border-radius: 80px;" // 增加圆角半径,使按钮更加圆润
"color: rgb(227, 181, 5);" // 烫金色字体
"font-size: 24px;"
"padding: 10px 40px;"; // 增加左右内边距,使按钮宽度增加

// 创建按钮面板
QWidget *panelBtn = new QWidget(this);
panelBtn->setStyleSheet("background-color: #1F1F1F;");
QHBoxLayout *btnLayout = new QHBoxLayout(panelBtn);
// 创建扫描按钮
QPushButton *buttonScan = new QPushButton("Scan", panelBtn);
buttonScan->setStyleSheet(buttonStyle);
connect(buttonScan, SIGNAL(clicked()), this, SLOT(onButtonScanClicked()));
btnLayout->addWidget(buttonScan);
// 创建连接按钮
m_buttonConnect = new QPushButton("Connect", panelBtn);
m_buttonConnect->setStyleSheet(buttonStyle);
connect(m_buttonConnect, SIGNAL(clicked()), this, SLOT(onButtonConnectClicked()));
btnLayout->addWidget(m_buttonConnect);
// 创建断开连接按钮
QPushButton *m_buttonDiscon = new QPushButton("Discon", panelBtn);
m_buttonDiscon->setStyleSheet(buttonStyle);
connect(m_buttonDiscon, SIGNAL(clicked()), this, SLOT(onButtonDisconClicked()));
btnLayout->addWidget(m_buttonDiscon);
// 创建返回按钮
QPushButton *buttonBack = new QPushButton("Back", panelBtn);
buttonBack->setStyleSheet(buttonStyle);
connect(buttonBack, SIGNAL(clicked()), this, SLOT(on_btn_back_clicked()));
btnLayout->addWidget(buttonBack);
mainLayout->addWidget(panelBtn,1);
}
  • 代码结构说明
    • 整体布局:
      • 主布局:垂直布局(QVBoxLayout)
      • 三部分组件按比例分配:顶部面板(1) + 中间信息面板(4) + 底部按钮面板(1)
    • 顶部面板:
      • WLAN标签 + SSID下拉框
      • 下拉框初始填充6个"---"占位符
      • 下拉框选择变化信号连接到onDropdownSSIDChanged
    • 中间信息面板:
      • 垂直布局包含4个信息项(SSID、Passwd、RSSI、PSK)
      • 每个信息项使用水平布局:标签(1) + 文本框(3)
      • 密码框添加点击信号连接onLineEditClicked
    • 底部按钮面板:
      • 四个功能按钮:Scan、Connect、Discon、Back
      • 按钮使用统一的自定义样式
      • 每个按钮连接对应的槽函数

自此,wifi配置页面UI设计代码已完成,效果如下:
alt text

4.2 WIFI搜索功能

  1. 为搜索按钮链接并实现单击槽函数

    void wifiset::onButtonScanClicked()
    {
    m_dropdownSSID->clear();
    m_dropdownSSID->addItem("Scanning...");

    // 记录当前时间,用于超时判断
    m_scanStartTime = QDateTime::currentDateTime();

    m_wifiScanProcess = new QProcess(this);
    connect(m_wifiScanProcess, &QProcess::readyReadStandardOutput, this, &wifiset::onScanOutputReady);
    connect(m_wifiScanProcess, &QProcess::readyReadStandardError, this, &wifiset::onScanErrorReady);
    connect(m_wifiScanProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),this,&wifiset::scanFinished);

    m_wifiScanProcess->start("wpa_cli", QStringList() << "-i" << "wlan0" << "scan");
    }
  • 代码功能说明
    • 通过QProcess建立WIFI搜索线程,并连接readyReadStandardOutput,readyReadStandardError,finished信号与槽函数,前二者读取标准输出和标准错误,以便在获取搜索信息或者错误,finished信号代表进程完成,链接对于逻辑处理函数,调用start实行"wpa_cli -i wlan0 scan"搜索命令。
  1. 实现scanFinished处理函数

    void wifiset::scanFinished(int exitCode, QProcess::ExitStatus exitStatus)
    {
    if (exitStatus == QProcess::NormalExit && exitCode == 0) {
    // 扫描命令成功,等待一段时间后获取结果
    QTimer::singleShot(3000, this, [this]() {
    // 执行scan_results命令获取扫描结果
    QProcess *resultsProcess = new QProcess(this);
    connect(resultsProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
    this, &wifiset::onResultsFinished);
    connect(resultsProcess, &QProcess::readyReadStandardOutput, this, [resultsProcess, this]() {
    m_scanResults += resultsProcess->readAllStandardOutput();
    });

    resultsProcess->start("wpa_cli", QStringList() << "-i" << "wlan0" << "scan_results");
    });
    } else {
    // 扫描命令失败
    m_dropdownSSID->clear();
    m_dropdownSSID->addItem("Scan failed (exit code: " + QString::number(exitCode) + ")");
    }
    }
  • 代码功能说明
    • 当搜索进程正常退出后,等待一段时间获取结果,创建新的进程执行"wpa_cli -i wlan0 scan_results"命令以获取搜索结果,并且同样链接进程完成信号与槽函数,并且将搜索结果追加到m_scanResults成员变量中,方便后续解析搜索结果并显示。
    • 如果搜索失败,将在下拉列表中显示Scan failed等字样。
  1. 实现解析搜索结果函数


    struct WifiNetwork {
    QString ssid;
    int signalLevel;
    QString flags;
    };

    void wifiset::onResultsFinished(int exitCode, QProcess::ExitStatus exitStatus)
    {
    if (exitStatus == QProcess::NormalExit && exitCode == 0) {
    // 解析扫描结果
    parseScanResults(m_scanResults);
    } else {
    m_dropdownSSID->clear();
    m_dropdownSSID->addItem("Failed to get results (exit code: " + QString::number(exitCode) + ")");
    }

    m_scanResults.clear();
    }

    void wifiset::parseScanResults(const QString &results)
    {
    m_wifiNetworks.clear();

    // 按行分割结果
    QStringList lines = results.split('\n');

    // 跳过标题行
    if (lines.size() > 1) {
    for (int i = 1; i < lines.size(); i++) {
    QString line = lines[i].trimmed();
    if (line.isEmpty()) continue;

    // 按制表符分割每行数据
    QStringList fields = line.split('\t', QString::SkipEmptyParts);
    if (fields.size() >= 5) {
    WifiNetwork network;
    network.ssid = fields[4];
    network.signalLevel = fields[2].toInt();
    network.flags = fields[3];

    // 过滤空SSID
    if (!network.ssid.isEmpty()) {
    m_wifiNetworks.append(network);
    }
    }
    }
    }
    }
  • 代码步骤说明
    • 等待搜索结果进程正常退出,执行解析扫描结果函数
    • 按行分割结果
    • 跳过标题行,以制表符分隔数据,将对于数据保存在自定义结构体WifiNetwork,并追加到顶部面板中的下拉列表,方便用户查看搜索结果
  1. 更新信息

    链接下拉列表选中,更新wifi显示组件以显示选中wifi信息

    //构造函数中链接信号与槽
    connect(m_dropdownSSID, QOverload<int>::of(&QComboBox::currentIndexChanged),
    this, &wifiset::onDropdownSSIDChanged);
    //槽函数实现
    void wifiset::onDropdownSSIDChanged(int index)
    {
    if (index >= 0 && index < m_wifiNetworks.size()) {
    // 获取选中的WiFi网络信息
    const WifiNetwork &network = m_wifiNetworks[index];

    // 更新文本框显示SSID
    m_textAreaSSID->setText(network.ssid);
    m_textAreaRSSI->setText(QString::number(network.signalLevel) + " dBm");
    m_textAreaMgnt->setText(network.flags);
    } else {
    // 未选择有效网络时清空文本框
    m_textAreaSSID->clear();
    m_textAreaRSSI->clear();
    m_textAreaMgnt->clear();
    }
    }
  • 代码功能说明说明
    • 获取选中的WIFI信息更新UI,未选择有效网络时清空文本框。

至此WIFI搜索功能实现完毕,运行图像如下:
alt text

4.3 虚拟键盘实现

在 wifi 配置界面时,需要获得用户输入的WIFI密码以及WIIF的SSID来配置信息,本项目中自定义了一个类,用来描绘出一个虚拟键盘以及记录用户输入并追加到文本框。

  1. 实现虚拟键盘类的构造函数

    VirtualKeyboard::VirtualKeyboard(QWidget *parent) : QWidget(parent) {
    m_targetLineEdit = nullptr;

    // 设置键盘样式
    setStyleSheet("background-color: #1F1F1F; border-radius: 10px;");
    setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
    setAttribute(Qt::WA_TranslucentBackground);

    // 创建主布局
    QVBoxLayout *mainLayout = new QVBoxLayout(this);

    // 创建键盘按钮
    QString keys = "1234567890qwertyuiopasdfghjklzxcvbnm";
    QList<QString> keyRows;
    keyRows << "1234567890" << "qwertyuiop" << "asdfghjkl" << "_-zxcvbnm,.";

    for (const QString &row : keyRows) {
    QHBoxLayout *rowLayout = new QHBoxLayout();
    for (QChar c : row) {
    QPushButton *button = new QPushButton(c, this);
    button->setStyleSheet(
    "background-color: #404040;"
    "border-radius: 10px;"
    "color: rgb(227, 181, 5);"
    "font-size: 24px;"
    "padding: 10px;"
    );
    button->setFocusPolicy(Qt::NoFocus); // 取消焦点
    connect(button, &QPushButton::clicked, this, &VirtualKeyboard::onKeyClicked);
    rowLayout->addWidget(button);
    m_keyButtons.append(button);
    }
    mainLayout->addLayout(rowLayout);
    }

    // 创建功能按钮行
    QHBoxLayout *funcLayout = new QHBoxLayout();

    QPushButton *backspaceBtn = new QPushButton("", this);
    backspaceBtn->setIcon(QIcon(":/image/DEL.png")); // 设置图标路径
    backspaceBtn->setIconSize(QSize(30, 30)); // 设置图标大小
    backspaceBtn->setStyleSheet(
    "background-color: #f0f0f0;"
    "border-radius: 10px;"
    "font-size: 24px;"
    "padding: 10px;"
    );
    backspaceBtn->setFocusPolicy(Qt::NoFocus); // 取消焦点
    connect(backspaceBtn, &QPushButton::clicked, this, &VirtualKeyboard::onBackspaceClicked);
    funcLayout->addWidget(backspaceBtn);

    QPushButton *spaceBtn = new QPushButton("Space", this);
    spaceBtn->setStyleSheet(
    "background-color: #f0f0f0;"
    "border-radius: 10px;"
    "color: rgb(0, 0, 0);"
    "font-size: 24px;"
    "padding: 10px;"
    );
    spaceBtn->setFocusPolicy(Qt::NoFocus); // 取消焦点
    connect(spaceBtn, &QPushButton::clicked, this, &VirtualKeyboard::onSpaceClicked);
    funcLayout->addWidget(spaceBtn);

    QPushButton *enterBtn = new QPushButton("Enter", this);
    enterBtn->setStyleSheet(
    "background-color: #f0f0f0;"
    "border-radius: 10px;"
    "color: rgb(0, 0, 0);"
    "font-size: 24px;"
    "padding: 10px;"
    );
    enterBtn->setFocusPolicy(Qt::NoFocus); // 取消焦点
    connect(enterBtn, &QPushButton::clicked, this, &VirtualKeyboard::onEnterClicked);
    funcLayout->addWidget(enterBtn);

    QPushButton *closeBtn = new QPushButton("Close", this);
    closeBtn->setStyleSheet(
    "background-color: #f0f0f0;"
    "border-radius: 10px;"
    "color: rgb(0, 0, 0);"
    "font-size: 24px;"
    "padding: 10px;"
    );
    closeBtn->setFocusPolicy(Qt::NoFocus); // 取消焦点
    connect(closeBtn, &QPushButton::clicked, this, &VirtualKeyboard::onCloseClicked);
    funcLayout->addWidget(closeBtn);

    mainLayout->addLayout(funcLayout);

    // 计算屏幕尺寸并设置键盘大小为屏幕宽度的一半
    QScreen *screen = QGuiApplication::primaryScreen();

    QRect screenGeometry = screen->geometry();
    int keyboardWidth = screenGeometry.width();
    int keyboardHeight = 350; // 设置键盘高度

    // 设置键盘固定大小
    setFixedSize(keyboardWidth, keyboardHeight);
    }

    void VirtualKeyboard::setTargetLineEdit(QLineEdit *target) {
    m_targetLineEdit = target;
    }
  • 代码步骤说明
    • 创建键盘整体样式以及布局。
    • 创建键盘按钮样式,数字按钮以及英文字母按钮,并且逐行布局以及添加到主布局中。
    • 创建功能按键样式,分别为删除,空格,回车,关闭按钮,添加到主布局中。
    • 计算屏幕尺寸并规定键盘显示为屏幕的一半。
  1. 实现虚拟键盘基本逻辑函数

    void VirtualKeyboard::setTargetLineEdit(QLineEdit *target) {
    m_targetLineEdit = target;
    }
    void VirtualKeyboard::onKeyClicked() {
    if (!m_targetLineEdit) return;

    QPushButton *button = qobject_cast<QPushButton*>(sender());
    if (button) {
    QString text = button->text();
    m_targetLineEdit->insert(text);
    }
    }

    void VirtualKeyboard::onBackspaceClicked() {
    if (m_targetLineEdit) {
    m_targetLineEdit->backspace();
    }
    }

    void VirtualKeyboard::onSpaceClicked() {
    if (m_targetLineEdit) {
    m_targetLineEdit->insert(" ");
    }
    }

    void VirtualKeyboard::onEnterClicked() {
    if (m_targetLineEdit) {
    m_targetLineEdit->returnPressed();
    emit closed();
    }
    }

    void VirtualKeyboard::onCloseClicked() {
    emit closed();
    }
  • 代码功能说明
    • setTargetLineEdit为虚拟键盘聚焦输入对象QLineEdit。
    • onKeyClicked键盘按钮输入其对应文本,追加到输入目标m_targetLineEdit。
    • onBackspaceClicked 键盘删除,实现删除一个字符功能。
    • onSpaceClicked 输入空格
    • onEnterClicked 输入回车,确认输入,并发送键盘关闭信号
    • onCloseClicked 键盘关闭
  1. WIFI配置页面实例化虚拟键盘对象

    m_virtualKeyboard = new VirtualKeyboard(this);
    m_virtualKeyboard->hide();
    connect(m_virtualKeyboard, &VirtualKeyboard::closed, m_virtualKeyboard, &QWidget::hide);
  2. 链接WIFI配置界面m_textAreaPW触发显示键盘

    ```cpp
    //构造函数中链接信号与槽
    connect(m_textAreaPW, &QLineEdit::selectionChanged, this, &wifiset::onLineEditClicked);

    //void wifiset::onLineEditClicked()
    {
    QLineEdit *lineEdit = qobject_cast<QLineEdit*>(sender());
    if (lineEdit) {
    // 设置当前目标LineEdit
    m_virtualKeyboard->setTargetLineEdit(lineEdit);

    // 计算屏幕尺寸
    QScreen *screen = QGuiApplication::primaryScreen();
    QRect screenGeometry = screen->geometry();

    // 计算键盘位置(屏幕底部中央)
    int keyboardWidth = m_virtualKeyboard->width();
    int keyboardHeight = m_virtualKeyboard->height();
    int x = (screenGeometry.width() - keyboardWidth) / 2;
    int y = screenGeometry.height() - keyboardHeight;

    // 将坐标转换为相对于当前窗口的坐标
    QPoint pos = mapFromGlobal(QPoint(x, y));
    m_virtualKeyboard->move(pos);

    // 显示键盘
    m_virtualKeyboard->show();
    }
    }
    ```

    至此,虚拟键盘功能已实现,当用户点击密码的输入框,虚拟键盘便会显示并输入用户输入,实现效果如下:
    alt text

4.4 WIFI链接功能

  1. 实现Connect按钮单击槽函数

    void wifiset::onButtonConnectClicked()
    {
    int index = m_dropdownSSID->currentIndex();
    if(index < 0 || index >= m_wifiNetworks.size())
    {
    qDebug() << "connect failed";
    return;
    }

    m_currentSSID = m_wifiNetworks[index].ssid;
    QString passwd = m_textAreaPW->text();

    if (passwd.isEmpty() && !m_wifiNetworks[index].flags.contains("open", Qt::CaseInsensitive)) {
    qDebug() << "input passwd";
    return;
    }

    if(!m_currentSSID.isEmpty() &&
    passwd.length() >= 8 &&
    passwd.length() < 64)
    {
    wifi_connect_info_t *wifi = (wifi_connect_info_t *)malloc(sizeof(wifi_connect_info_t));
    ::strcpy(wifi->ssid, m_currentSSID.toUtf8().constData());
    ::strcpy(wifi->passwd, passwd.toUtf8().constData());

    pthread_t wifi_connect_thread;
    pthread_create(&wifi_connect_thread, NULL, wifi_connect_thread_handler, wifi);
    pthread_detach(wifi_connect_thread);
    qDebug() << "passwd is" << passwd;
    }
}
```
在链接按钮点下后,获取当前选中WIFI并获取其SSID和密码,验证wifi密码有效性,然后创建线程并链接wifi_connect_thread_handler回调函数,处理链接WIFI逻辑。

2.实现WIFI链接

```cpp
static void *wifi_connect_thread_handler(void *arg)
{
wifi_connect_info_t *wifi = (wifi_connect_info_t *)arg;

wificonnect(wifi->ssid, wifi->passwd);
free(wifi);

return NULL;
}

void wificonnect(const char* ssid, const char* password)
{
qDebug() << "ssid" << ssid << " " << "password"<< password;
FILE *wpa_supplicant_pipe;
char buffer[MAX_CONF_LEN];

// open wpa_supplicant pipe
wpa_supplicant_pipe = popen("wpa_cli", "w");
if (wpa_supplicant_pipe == NULL) {
perror("popen");
exit(1);
}
printf("connect test\n");
// set network ssid adn psk
memset(buffer,0,MAX_CONF_LEN);
snprintf(buffer, MAX_CONF_LEN, "set_network 0 ssid \"%s\"\n", ssid);
fputs(buffer, wpa_supplicant_pipe);

memset(buffer,0,MAX_CONF_LEN);
snprintf(buffer, MAX_CONF_LEN, "set_network 0 psk \"%s\"\n", password);
fputs(buffer, wpa_supplicant_pipe);

// save wifi conf
fputs("save_config\n", wpa_supplicant_pipe);
pclose(wpa_supplicant_pipe);

// save wifi conf to /etc/wpa_supplicant.conf
FILE *file = fopen(WPA_FILE_PATH, "r");
if (file == NULL) {
printf("Failed to open file.\n");
return ;
}

FILE *temp_file = fopen("temp_wpa_supplicant.conf", "w");
if (temp_file == NULL) {
printf("Failed to create temporary file.\n");
fclose(file);
return ;
}

char line[MAX_LINE_LEN];
int inside_network_block = 0;

while (fgets(line, MAX_LINE_LEN, file)) {
// Enter network={} block
if (strstr(line, "network={")) {
inside_network_block = 1;
fputs(line, temp_file);
continue;
}
// Exit network={} block
if (strstr(line, "}")) {
inside_network_block = 0;
}
// Inside network={} block
if (inside_network_block) {
if (strstr(line, "ssid=")) {
memset(buffer,0,MAX_CONF_LEN);
sprintf(buffer, " ssid=\"%s\"\n",ssid);
fputs(buffer, temp_file);
}
else if (strstr(line, "psk=")) {
memset(buffer,0,MAX_CONF_LEN);
sprintf(buffer, " psk=\"%s\"\n",password);
fputs(buffer, temp_file);
}
else {
fputs(line, temp_file);
}
}
else {
fputs(line, temp_file);
}
}

fclose(file);
fclose(temp_file);

remove(WPA_FILE_PATH);
rename("temp_wpa_supplicant.conf", WPA_FILE_PATH);
//printf("SSID and PSK replaced successfully.\n");

// reconnect wifi
// system("killall -9 wpa_cli");
system("killall -9 wpa_supplicant");
system("killall -9 udhcpc");

QThread::sleep(1);
system("wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf");
QThread::sleep(1);
system("wpa_cli reconfigure &");
QThread::sleep(5);
system("udhcpc -i wlan0 &");
return ;
}
```
  • 代码功能分析
    • 配置wpa_supplicant:
      • 通过wpa_cli管道设置SSID和PSK
      • 发送save_config命令保存配置
    • 更新配置文件:
      • 手动编辑/etc/wpa_supplicant.conf
      • 定位第一个network={}块,替换其中的SSID和PSK
      • 使用临时文件实现原子更新
    • 重启网络服务:
      • 杀死wpa_supplicant和udhcpc进程
      • 重启wpa_supplicant和udhcpc
      • 添加休眠等待服务启动

4.5 WIFI断开功能

实现Discon按钮单击槽函数

void wifiset::onButtonDisconClicked()
{
char command[128];
snprintf(command, sizeof(command), "wpa_cli -i %s disconnect", "wlan0");
int ret = system(command);
if (ret == -1) {
perror("system");
return;
}

snprintf(command, sizeof(command), "ifconfig %s 0.0.0.0", "wlan0");
ret = system(command);
if (ret == -1) {
perror("system");
return;
}

return;
}
  • 代码功能分析
    • 断开WiFi连接,发送断开命令到wpa_supplicant。
    • 清除IP地址,将网络接口的IP地址设为0,清除网络配置。

4.6 返回主界面功能

  1. 实现close按钮单击槽函数

    //在头文件中定义信号
    signals:
    void returnToMain();

    void wifiset::on_btn_back_clicked()
    {
    emit returnToMain();
    }

    在关闭按钮点击后发送自定义信号

  2. 在主界面链接信号

    connect(&wifi,&wifiset::returnToMain,this,&Widget::onWifiWidgetClosed);

至此WIFL配置界面功能及设计已完成,整体运行效果如下:
alt text

5.副界面

5.1 继电器控制功能

  1. 初始化继电器控制面板

    // 初始化继电器面板(对应ui_PanelRelay0、ui_PanelRelay1)
    void ScreenPlayer::initRelayPanels()
    {
    // 继电器面板通用样式
    auto initRelayPanel = [this](QFrame*& panel, QLabel*& label, int x, int y, const QString& text) {
    panel = new QFrame(this);
    panel->setGeometry(x, y, 320, 160); // 位置(x,y),大小320x160
    panel->setStyleSheet(R"(
    QFrame {
    background-color: #1F1F1F; /* 深灰色背景 */
    border-radius: 20px; /* 圆角20 */
    border: none; /* 无边框 */
    }
    )");

    // 继电器标签(对应ui_LabelRelay0/1)
    label = new QLabel(text, panel);
    label->setGeometry(0, 0, 320, 160); // 填充面板
    label->setAlignment(Qt::AlignCenter);
    // 字体:Montserrat 48pt,灰色#696969
    QFont font("Montserrat", 16);
    label->setFont(font);
    label->setStyleSheet("color: #696969;");

    // 设置面板可点击
    panel->setAttribute(Qt::WA_TransparentForMouseEvents, false);
    panel->setMouseTracking(true);

    // 使用QObject属性存储状态
    panel->setProperty("isActive", false);

    // 使用事件过滤器处理鼠标点击
    panel->installEventFilter(this);
    };

    // 创建Relay0(x=30, y=530)
    initRelayPanel(panelRelay0, labelRelay0, 30, 530, "Relay 0");
    // 创建Relay1(x=370, y=530)
    initRelayPanel(panelRelay1, labelRelay1, 370, 530, "Relay 1");
    }
  • 代码功能说明
    • 通过Qlabel定义一个通用模板,设置样式以及标签,设置面板可点击,以便后续实现点击控制功能,安装事件过滤器。
    • 通过模板初始化两个继电器模板,分别是Relay0和Relay1.
  1. 对于事件过滤逻辑实现

    bool ScreenPlayer::eventFilter(QObject *obj, QEvent *event)
    {
    ...
    // 检查事件类型是否为鼠标按下
    if (event->type() == QEvent::MouseButtonPress) {
    // 检查对象是否为继电器面板
    QFrame* panel = qobject_cast<QFrame*>(obj);
    if (panel && panel->property("isActive").isValid()) {
    // 获取当前状态
    bool isActive = panel->property("isActive").toBool();
    // 切换状态
    isActive = !isActive;
    panel->setProperty("isActive", isActive);
    // 获取面板上的标签
    QLabel* label = panel->findChild<QLabel*>();
    if (label) {
    if (isActive) {
    // 激活状态 - 橙色渐变
    panel->setStyleSheet(R"(
    QFrame {
    background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
    stop:0 #FFA500,
    stop:0.5 #FF8C00,
    stop:1 #FF4500);
    border-radius: 20px;
    border: none;
    }
    )");
    label->setStyleSheet("color: #FFFFFF;");
    } else {
    // 正常状态 - 深灰色
    panel->setStyleSheet(R"(
    QFrame {
    background-color: #1F1F1F;
    border-radius: 20px;
    border: none;
    }
    )");
    label->setStyleSheet("color: #696969;");
    }
    }

    if(panel == panelRelay0)
    {
    if(isActive)
    {
    set_gpio(32, 1);
    }
    else
    {
    set_gpio(32, 0);
    }
    }
    else if(panel == panelRelay1)
    {
    if(isActive)
    {
    set_gpio(33, 1);
    }
    else
    {
    set_gpio(33, 0);
    }
    }
    // 事件已处理
    return true;
    }
    }
    // 其他事件交给默认处理
    return QWidget::eventFilter(obj, event);
    }
  • 代码步骤说明
    • 检测是否为鼠标按下且对象为继电器控制面板。
    • 获取当前状态并为下一次反转状态,对不同状态的继电器面板设置不同的样式。
    • 判断具体时那个继电器面板并调用set_gpio实现对于继电器控制,如果打开设置为1,关闭则设置为0。
    • 其余实现默认处理
  1. 实现GPIO控制

    int ScreenPlayer::set_gpio(int gpio_pin, int val)
    {
    int len;
    char buff[10];
    char filename[64];
    int ret = 0;
    int result = 0;

    memset(filename, 0x0, sizeof(filename));
    sprintf(filename, "/sys/class/gpio/gpio%d/value", gpio_pin);
    FILE *value_file = fopen(filename, "w");
    if (value_file == NULL)
    {
    ret = gpio_export(gpio_pin);
    if (ret < 0)
    {
    return ret;
    }
    else
    {
    result = gpio_out_direction(gpio_pin);
    if(result < 0)
    return result;
    if (value_file == NULL)
    return ret;
    }
    }
    memset(buff, 0x0, sizeof(buff));
    len = sprintf(buff, "%s", val ? "1" : "0");
    fprintf(value_file,"%s",buff);
    if (ret != len)
    {
    fclose(value_file);
    return ret;
    }
    fclose(value_file);
    return 0;
    }
  • 代码功能说明
    • 构建sysfs路径。
    • 尝试打开value文件:
      • 如果文件不存在(GPIO未导出),则调用gpio_export()导出GPIO,调用gpio_out_direction()设置输出方向。
    • 写入电平值。

至此,实现继电器面板控制继电器,实现效果如下:
alt text

5.2 音乐播放器UI

音乐播放需要将.mp3文件放到/music目录下

  1. 初始化音乐播放器UI
    // 初始化音乐播放器面板(对应ui_PanelMusicPlayer)
    void ScreenPlayer::initMusicPlayerPanel()
    {
    // 音乐播放器面板(660x480,x=30, y=30)
    panelMusicPlayer = new QFrame(this);
    panelMusicPlayer->setGeometry(30, 30, 660, 480);
    panelMusicPlayer->setStyleSheet(R"(
    QFrame {
    background-color: rgba(255, 255, 255, 15); /* 白色透明度15 */
    border-radius: 20px; /* 圆角20 */
    border: 1px solid rgba(255, 255, 255, 30); /* 边框白色透明度30 */
    }
    )");
    ...
    }
  • 代码功能说明
    • initMusicPlayerPanel对于整体音乐播放器控件进行了初始化,包括暂停播放按钮,音量滑动模块等控件的设置样式。
  1. 初始化音乐列表UI

    // 初始化音乐列表面板(对应ui_PanelBoard)
    void ScreenPlayer::initMusicListPanel()
    {
    // 列表面板(720x720,居中,初始隐藏)
    panelBoard = new QFrame(this);
    panelBoard->setGeometry(0, 0, 720, 720); // 居中覆盖主窗口
    panelBoard->setStyleSheet("background-color: #0D0D0D;"); // 深灰黑色
    panelBoard->setVisible(false); // 初始隐藏

    // 1. 列表标题(对应ui_LabelMusicList)
    labelMusicList = new QLabel("Music List", panelBoard);
    labelMusicList->setGeometry(0, 50, 720, 180);
    labelMusicList->setAlignment(Qt::AlignCenter);
    QFont font = QFont("Montserrat", 30);
    labelMusicList->setFont(font);
    labelMusicList->setStyleSheet("color: #545454;"); // 灰色

    // 2. 音乐列表(对应ui_RollerMusic)
    // 创建一个容器Widget,占据整个屏幕
    QWidget *musicListContainer = new QWidget(panelBoard);
    musicListContainer->setGeometry(0, 0, 720, 720); // 假设屏幕高度为1080px

    //黑色背景区域
    blackBackground = new QWidget(musicListContainer);
    blackBackground->setGeometry(0, 0, 720, 200); // 高度为360px (1080*1/3)
    blackBackground->setStyleSheet("background-color: #000000;"); // 纯黑色背景

    // 为黑色背景区域安装事件过滤器,处理点击事件
    blackBackground->installEventFilter(this);

    //音乐列表
    listMusic = new QListWidget(musicListContainer);
    listMusic->setGeometry(0, 200, 720, 520); // 高度为720px (1080*2/3)
    listMusic->setStyleSheet(R"(
    QListWidget {
    background-color: #1F1F1F; /* 面板深灰 */
    border-radius: 10px;
    color: white; /* 文字白色 */
    font-family: 'Montserrat';
    font-size: 30px;
    }
    QListWidget::item:selected {
    color: #FFA300; /* 选中项橙色 */
    background-color: rgba(255, 255, 255, 25); /* 白色透明度25 */
    }
    )");

    }
  • 代码功能说明
    • initMusicListPanel初始化了音乐列表的显示,将音乐列表按钮btnList链接槽函数,显示音乐列表。
    • blackBackground为音乐列表的黑色背景区域,设置样式,并安装事件过滤器。
  1. 读取音乐文件,更新音乐列表

    void ScreenPlayer::scanMusicFiles()
    {
    // 1. 清除现有列表
    listMusic->clear();
    musicFiles.clear();

    // 2. 检查目录是否存在
    QDir musicDir(MUSIC_DIR_PATH);
    if (!musicDir.exists()) {
    listMusic->addItem("NOT FOUND");
    labelMusicList->setText("Music List(0)");
    return;
    }

    // 3. 扫描MP3文件
    QStringList filters;
    filters << "*.mp3";
    QFileInfoList fileList = musicDir.entryInfoList(filters, QDir::Files | QDir::NoDotAndDotDot);

    // 4. 处理扫描结果
    if (fileList.isEmpty()) {
    listMusic->addItem("NOT FOUND");
    labelMusicList->setText("Music List(0)");
    return;
    }

    // 5. 按文件名排序
    std::sort(fileList.begin(), fileList.end(), [](const QFileInfo &a, const QFileInfo &b) {
    return a.fileName().compare(b.fileName(), Qt::CaseInsensitive) < 0;
    });

    // 6. 添加到列表
    for (const QFileInfo &fileInfo : fileList) {
    QString fileName = fileInfo.fileName();
    musicFiles.append(fileInfo.absoluteFilePath());
    listMusic->addItem(fileName);
    }

    // 7. 更新标题显示
    labelMusicList->setText(QString("Music List(%1)").arg(fileList.size()));

    //8. 可选:自动选择第一首
    if (listMusic->count() > 0) {
    listMusic->setCurrentRow(0);
    }
    }
  • 代码功能说明
    • 清理状态:清除现有列表和文件路径缓存。
    • 目录检查:验证音乐目录是否存在。
    • 文件扫描:查找目录中所有MP3文件。
    • 结果处理:
      • 空目录:显示"NOT FOUND"。
      • 有文件:按文件名排序后添加到列表。
    • UI更新:更新列表标题显示文件数量,自动选择第一个文件。
  1. 事件过滤,实现音乐列表切换音乐界面功能

    bool ScreenPlayer::eventFilter(QObject *obj, QEvent *event)
    {
    if (obj == blackBackground && event->type() == QEvent::MouseButtonPress) {
    // 处理点击事件,关闭音乐列表
    updateMusicTitle();
    onBtnListClicked(); // 调用关闭音乐列表的函数

    return true;
    }
    ...
    }

    void ScreenPlayer::updateMusicTitle()
    {
    // 获取当前选中的项目
    QListWidgetItem *selectedItem = listMusic->currentItem();
    if (!selectedItem) {
    labelMusic->setText("---------");
    return;
    }
    // 获取选中的行号和文件路径
    int row = listMusic->currentRow();
    if (row < 0 || row >= musicFiles.size()) {
    labelMusic->setText("---------");
    return;
    }
    QString fileName = selectedItem->text();

    newFilePath = musicFiles.at(row);
    // 设置标题文本
    labelMusic->setText(fileName);

    if(currentFilePath != newFilePath)
    {
    currentFilePath = newFilePath;
    if(isfirsttime)
    {
    btnPlay->setEnabled(true);
    btnPrev->setEnabled(true);
    btnNext->setEnabled(true);
    isfirsttime = false;
    return;
    }
    playCurrentMusic();
    }
    }

    // 显示/隐藏音乐列表
    void ScreenPlayer::onBtnListClicked()
    {
    bool wasVisible = panelBoard->isVisible();
    panelBoard->setVisible(!wasVisible);

    // 仅在显示面板时刷新列表
    if (!wasVisible) {
    scanMusicFiles();

    // 添加延迟确保列表已渲染
    QTimer::singleShot(50, this, [=]() {
    if (listMusic->count() > 0) {
    listMusic->setCurrentRow(0);
    //updateMusicTitle(); // 手动更新标题
    }
    });
    }
    }
  • 代码功能说明
    • 判断点击对象是否为黑色背景,触发关闭音乐列表函数,并且更新选中音乐到音乐播放器的歌曲名字显示控件上。
    • updateMusicTitle选中的音乐,并更新到歌曲名字显示控件。
    • onBtnListClicked按钮点击槽函数,刷新音乐列表,并且显示/隐藏音乐列表

至此,音乐列表逻辑已完成,将.mp3文件放在开发板的/music目录下,效果如下:

5.3 音乐播放器逻辑功能

  1. 运行MPV

    bool ScreenPlayer::initMusicPlayer()
    {
    if (isPlayerInitialized) {
    return true; // 已经初始化
    }

    // 创建子进程运行 MPV
    music_pid = vfork();
    if (music_pid == 0) { // 子进程
    prctl(PR_SET_PDEATHSIG, SIGKILL);
    execlp("mpv", "mpv", "--quiet", "--no-terminal", "--no-video",
    "--idle=yes", "--term-status-msg=",
    "--input-ipc-server=/tmp/mpvsocket", NULL);
    _exit(EXIT_FAILURE); // 如果 execlp 失败
    }
    else if (music_pid > 0) { // 父进程
    // 等待 MPV 启动
    QElapsedTimer timer;
    timer.start();

    // 创建套接字
    struct sockaddr_un music_addr;
    music_addr.sun_family = AF_UNIX;
    strcpy(music_addr.sun_path, "/tmp/mpvsocket");

    fd_mpv = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd_mpv == -1) {
    perror("Create socket failed");
    return false;
    }

    // 尝试连接,最多等待 2 秒
    bool connected = false;
    while (timer.elapsed() < 2000) {
    if (::connect(fd_mpv, (struct sockaddr *)&music_addr, sizeof(music_addr)) == 0) {
    connected = true;
    break;
    }
    usleep(100000); // 等待 100ms
    }

    if (!connected) {
    perror("Cannot connect to socket");
    ::close(fd_mpv);
    fd_mpv = -1;
    return false;
    }

    isPlayerInitialized = true;
    return true;
    }
    else {
    perror("fork error");
    return false;
    }
    }
  • 代码步骤说明
    • 播放器初始化,使用vfork()创建MPV子进程,启动音乐播放器。
    • 设置--input-ipc-server启用IPC通信。
    • 父进程通过Unix域套接字连接MPV。
    • 设置超时机制,确保启动可靠性。
  1. 实现对mpv发送命令

    // 发送命令到 MPV
    bool ScreenPlayer::sendMpvCommand(const QString &jsonCmd)
    {
    if (fd_mpv < 0) {
    qWarning() << "MPV not initialized";
    return false;
    }

    QByteArray cmdData = jsonCmd.toUtf8() + '\n';
    ssize_t bytesWritten = write(fd_mpv, cmdData.constData(), cmdData.size());

    if (bytesWritten < 0) {
    perror("Failed to write to MPV");
    return false;
    }
    return true;
    }
  • 代码功能说明
    • 通过socket发送JSON-RPC命令,每条命令以换行符终止。
  1. 播放当前音乐

    // 播放当前选中的音乐
    void ScreenPlayer::playCurrentMusic()
    {
    int currentIndex = listMusic->currentRow();
    if (currentIndex >= 0 && currentIndex < musicFiles.size()) {
    QString filePath = musicFiles.at(currentIndex);

    // 发送播放命令
    QString cmd = QString(
    "{ \"command\": [\"loadfile\", \"%1\", \"replace\"] }"
    ).arg(currentFilePath);

    if (sendMpvCommand(cmd)) {
    //updatePlaybackStatus(true);
    }
    }
    }

    调用发送命令函数sendMpvCommand发送命令,播放当前选中音乐。

  2. 暂停播放

    // 暂停音乐
    void ScreenPlayer::pauseMusic()
    {
    // 发送暂停命令
    QString cmd = "{ \"command\": [\"set_property\", \"pause\", true] }";

    if (sendMpvCommand(cmd)) {
    //updatePlaybackStatus(false);

    isPaused = true;
    }
    }
  3. 恢复播放

    // 恢复播放
    void ScreenPlayer::resumeMusic()
    {
    // 发送恢复命令
    QString cmd = "{ \"command\": [\"set_property\", \"pause\", false] }";

    if (sendMpvCommand(cmd)) {
    //updatePlaybackStatus(true);
    }
    }
  4. 播放按钮单击槽函数实现

    // 播放/暂停按钮点击(切换图标)
    void ScreenPlayer::onBtnPlayClicked()
    {
    // 确保播放器已初始化
    if (!isPlayerInitialized && !initMusicPlayer()) {
    qWarning() << "Failed to initialize music player";
    return;
    }

    // 切换播放状态
    isPlaying = !isPlaying;
    // 根据状态更新图标
if (isPlaying) {
// 播放状态:显示暂停图标
btnPlay->setIcon(QIcon(":/image/pause.png"));

if(isPaused)
{
resumeMusic();
return;
}
// 开始播放当前音乐
playCurrentMusic();
}
else {
// 暂停状态:显示播放图标
btnPlay->setIcon(QIcon(":/image/play.png"));

// 暂停音乐
pauseMusic();
}
}
```
据播放状态,切换按钮图标,如果暂停音乐,调用resumeMusic恢复播放。
  1. 单曲循环按钮实现

    // 播放模式切换(循环所有→循环单曲)
    void ScreenPlayer::onBtnModeClicked()
    {
    if (btnMode->isChecked()) {
    // 选中状态:循环单曲图标
    btnMode->setIcon(QIcon(":/image/cycle1.png"));
    cycleflag = true;
    } else {
    // 未选中:循环所有图标
    btnMode->setIcon(QIcon(":/image/cycle.png"));
    cycleflag = false;
    }
    }

    点击更新图标并且切换成员cycleflag状态。

  2. 下一首/上一首功能实现

    void ScreenPlayer::onBtnNextClicked()
    {
    int row = listMusic->currentRow();
    if(cycleflag)
    {

    }
    else
    {
    row++;
    if(row > musicFiles.size() - 1)
    {
    row = 0;
    }
    }
    currentFilePath = musicFiles.at(row);

    playCurrentMusic();
    listMusic->setCurrentRow(row);
    QListWidgetItem *selectedItem = listMusic->currentItem();

    QString fileName = selectedItem->text();
    labelMusic->setText(fileName);
    }

    void ScreenPlayer::onBtnPrevClicked()
    {
    int row = listMusic->currentRow();
    if(cycleflag)
    {

    }
    else
    {
    row--;
    if(row < 0)
    {
    row = musicFiles.size() - 1;
    }
    }

    currentFilePath = musicFiles.at(row);

    playCurrentMusic();
    listMusic->setCurrentRow(row);
    // 获取当前选中的项目
    QListWidgetItem *selectedItem = listMusic->currentItem();

    QString fileName = selectedItem->text();
    labelMusic->setText(fileName);
    }

    若是单曲循环,则不做处理播放当前歌曲,否则setCurrentRow选中上/下一个歌曲并播放。

  3. 音量控制

    链接音量滑动模块值变动信号并实现槽函数


    connect(sliderVolume, &QSlider::valueChanged, this, &ScreenPlayer::onSliderVolumeValueChanged);

    // 音量滑块值变化时触发
    void ScreenPlayer::onSliderVolumeValueChanged(int value)
    {
    // 直接调用C语言的music_set_volume函数,传入当前滑块值
    music_set_volume(value);
    }

    // voice
    void ScreenPlayer::music_set_volume(int value)
    {
    char cmd[256];
    sprintf(cmd, "{ \"command\": [\"set_property\", \"volume\", %d] }\n",value);
    //printf("%s\n", cmd);
    write(fd_mpv, cmd, strlen(cmd));
    }

至此,音乐播放器的逻辑已完成,效果如下:
alt text

5.4 返回主界面功能

与主界面切换副界面逻辑类似处理,捕获鼠标点击,移动,松开事件。

void ScreenPlayer::mouseMoveEvent(QMouseEvent *event)
{
...
if(delta < -30)
{
m_isAnimating = true;
setEnabled(false); // 禁用交互

// 确保主界面位置正确
m_mainWidget->move(0, height());

QPropertyAnimation *animation = new QPropertyAnimation(this, "pos");
animation->setDuration(500);
animation->setStartValue(pos());
animation->setEndValue(QPoint(0, -height()));

QPropertyAnimation *animation2 = new QPropertyAnimation(m_mainWidget, "pos");
animation2->setDuration(500);
animation2->setStartValue(m_mainWidget->pos());
animation2->setEndValue(QPoint(0, 0));

// 安全连接
connect(animation, &QPropertyAnimation::finished, this, [=](){
animation->deleteLater();
hide(); // 动画完成后隐藏
onAnimationFinished();
});

connect(animation2, &QPropertyAnimation::finished, this, [=](){
animation2->deleteLater();
m_mainWidget->setEnabled(true); // 启用主界面
});

animation->start();
animation2->start();
}
...
}

当delta < -30时,即用户上滑,触发切换动画。

6. 编译运行

选择交叉编译的kits,此kits与开发板环境配置中搭载交叉编译的kits一样。

点击构建

到工程目录下,输入make命令

将生成的可执行文件上传到开发版并运行