基于 RK3568 / IMX6ULL / STM32MP157 的智能车载系统
文章目录
- 前言
- 一、项目介绍
- 二、车载系统界面
- 2.1 音乐播放器
- 2.2 视频播放器
- 2.3 天气预报
- 2.4 GPS 定位
- 2.5 环境监测界面
- 三、后台硬件驱动
- 四、总结
前言
项目完整代码:基于 RK3568 / IMX6ULL / STM32MP157 的智能车载系统完整代码
该项目的源代码适用于韦东山老师的开发板,使用韦东山老师的 RK3568 ,IMX6ULL,STM32MP157 开发板可以直接运行代码复刻,其它开发板需要自行移植,原理都一样。
一、项目介绍
该项目通过上位机 QT 界面实现简易车载系统,能够进行 GPS 定位,天气预报,音乐播放,视频播放,环境监测等功能。
项目实物图示例:
RK3568:
IMX6ULL:
STM32MP157:
项目演示视频:
基于RK3568 ,IMX6ULL,MP157的智能车载系统
二、车载系统界面
2.1 音乐播放器
QMediaPlayer 类是一个高级媒体播放类,它可以用来播放歌曲、电影和网络广播等内容。一般用于播放 mp3 和 mp4 等等媒体文件。QMediaPlayer 类常常与 QMediaPlaylist 类一起使用。可以很轻松的设计一个自己喜欢的音乐播放器与视频播放器。
QMediaPlayer 提供了很多信号,我们可以使用这些信号来完成音乐播放器的一系列操作,比如媒体状态改变的信号 stateChanged(QMediaPlayer::State state),判断这个 state 的状态就可以知道什么时候媒体暂停、播放、停止了。
篇幅有限,音乐播放器详解可看我之前的文章:
- https://blog.csdn.net/m0_74712453/article/details/144945961?spm=1001.2014.3001.5501
制作音乐播放器界面我们需要实现以下步骤:
- 媒体播放器初始化
- 点击列表歌曲播放
- 通过播放状态修改图标
- 上下首歌曲切换
- 显示播放进度
- 显示歌词
- 调整声音大小和播放模式
代码示例:
#include "Audio.h"
#include "ui_Audio.h"
Audio::Audio(QWidget *parent) :
QWidget(parent),
ui(new Ui::Audio)
{
ui->setupUi(this);
/* 音臂 */
// 加载旋转图片
QPixmap rotatingPixmap(":/images/styli.png");
// 指定控件并初始化坐标用作旋转中心点
baseWidget = ui->widget_2;
QPointF rotationCenter(34, 54);
// 创建RotatableWidget对象,将图片和旋转中心点传递给构造函数
RotatableWidget *rotatableWidget = new RotatableWidget(baseWidget, rotatingPixmap, rotationCenter);
rotatableWidget->resize(baseWidget->size()); // 确保RotatableWidget的大小与baseWidget相同
// 创建动画来旋转 rotatableWidget
animation = new QPropertyAnimation(rotatableWidget, "rotation");
animation->setDuration(200); // 动画持续200毫秒
/* 唱片*/
// 设置专辑封面
setAlbumArt(QPixmap(":/images/fengshen.jpg"));
QPixmap tmpPixmap(":/images/disc.png");
QPixmap rotatingPixmap2 = tmpPixmap.scaled(240, 240, Qt::KeepAspectRatio, Qt::SmoothTransformation);
baseWidget2 = ui->widget_3;
QPointF rotationCenter2(baseWidget2->width()/2, baseWidget2->height()/2);
RotatableWidget *rotatableWidget2 = new RotatableWidget(baseWidget2, rotatingPixmap2, rotationCenter2);
rotatableWidget2->resize(baseWidget2->size()); // 确保RotatableWidget的大小与baseWidget相同
// 创建动画来旋转 rotatableWidget
animation2 = new QPropertyAnimation(rotatableWidget2, "rotation");
animation2->setStartValue(0); // 开始角度为0
animation2->setEndValue(360); // 结束角度为360
animation2->setDuration(4000); // 动画持续4000毫秒
animation2->setLoopCount(-1); // 设置动画无限循环
/* 媒体播放器初始化 */
mediaPlayerInit();
/* 扫描歌曲 */
scanSongs();
// ui->listWidget->setStyleSheet(R"(
// QListWidget::verticalScrollBar {
// border:none;
// background:transparent;
// }
// QListWidget::item {
// color:white;
// height:60px;
// }
// QListWidget::verticalScrollBar:handle {
// border: none;
// background-color:#aaaaaa;
// width: 20px; /* 设置滚动条宽度 */
// }
// QListWidget::verticalScrollBar:add-page,
// QListWidget::verticalScrollBar:sub-page {
// height: 0px; /* 隐藏按钮 */
// border: none;
// background: none;
// }
// )");
}
void Audio::audio_working()
{
/* 媒体信号槽连接 */
connect(musicPlayer,
SIGNAL(stateChanged(QMediaPlayer::State)),
this,
SLOT(mediaPlayerStateChanged(QMediaPlayer::State)));
connect(mediaPlaylist,
SIGNAL(currentIndexChanged(int)),
this,
SLOT(mediaPlaylistCurrentIndexChanged(int)));
connect(musicPlayer, SIGNAL(durationChanged(qint64)),
this,
SLOT(musicPlayerDurationChanged(qint64)));
connect(musicPlayer,
SIGNAL(positionChanged(qint64)),
this,
SLOT(mediaPlayerPositionChanged(qint64)));
connect(musicPlayer, &QMediaPlayer::metaDataAvailableChanged,
this, &Audio::onMetaDataAvailableChanged);
// 在播放器的播放进度更新的槽函数中调用updateLyrics()
connect(musicPlayer, &QMediaPlayer::positionChanged,
this, &Audio::updateLyrics);
//
connect(mediaPlaylist, &QMediaPlaylist::currentIndexChanged,
this, &Audio::mediaPlaylistCurrentIndexChanged);
/* 列表信号槽连接 */
connect(ui->listWidget, SIGNAL(itemClicked(QListWidgetItem*)),
this, SLOT(listWidgetCliked(QListWidgetItem*)));
/* slider信号槽连接 */
connect(ui->horizontalSlider, SIGNAL(sliderReleased()),
this, SLOT(durationSliderReleased()));
}
void Audio::clearConnections()
{
// 断开音乐播放器状态改变信号的连接
disconnect(musicPlayer,
SIGNAL(stateChanged(QMediaPlayer::State)),
this,
SLOT(mediaPlayerStateChanged(QMediaPlayer::State)));
// 断开媒体播放列表当前索引改变信号的连接
disconnect(mediaPlaylist,
SIGNAL(currentIndexChanged(int)),
this,
SLOT(mediaPlaylistCurrentIndexChanged(int)));
// 断开音乐播放器时长改变信号的连接
disconnect(musicPlayer, SIGNAL(durationChanged(qint64)),
this,
SLOT(musicPlayerDurationChanged(qint64)));
// 断开音乐播放器播放位置改变信号的连接
disconnect(musicPlayer,
SIGNAL(positionChanged(qint64)),
this,
SLOT(mediaPlayerPositionChanged(qint64)));
// 断开音乐播放器元数据可用改变信号的连接
disconnect(musicPlayer, &QMediaPlayer::metaDataAvailableChanged,
this, &Audio::onMetaDataAvailableChanged);
// 断开音乐播放器播放位置改变信号与更新歌词槽函数的连接
disconnect(musicPlayer, &QMediaPlayer::positionChanged,
this, &Audio::updateLyrics);
// 断开媒体播放列表当前索引改变信号与另一个同名槽函数的连接(如果有重复连接情况需要注意)
disconnect(mediaPlaylist, &QMediaPlaylist::currentIndexChanged,
this, &Audio::mediaPlaylistCurrentIndexChanged);
// 断开列表部件的项目点击信号的连接
disconnect(ui->listWidget, SIGNAL(itemClicked(QListWidgetItem*)),
this, SLOT(listWidgetCliked(QListWidgetItem*)));
// 断开水平滑块的滑块释放信号的连接
disconnect(ui->horizontalSlider, SIGNAL(sliderReleased()),
this, SLOT(durationSliderReleased()));
}
Audio::~Audio()
{
delete ui;
}
// 扫描歌曲目录,将歌曲添加到媒体列表中,并为有歌词的歌曲保存歌词路径
void Audio::scanSongs()
{
// 获取当前程序路径下的myMusic目录
QDir dir(QCoreApplication::applicationDirPath()
+ "/myMusic");
// 获取歌曲目录绝对路径
QDir dirbsolutePath(dir.absolutePath());
qDebug() << dirbsolutePath.path() <<endl;
/* 如果目录存在 */
if (dirbsolutePath.exists()) {
/* 定义过滤器 */
QStringList filter;
/* 获取所有以.mp3结尾的文件 */
filter << "*.mp3";
/* 获取符合条件文件的信息 */
QFileInfoList files =
dirbsolutePath.entryInfoList(filter, QDir::Files);
// 歌词映射:歌曲文件名 -> 歌词文件完整路径
QHash<QString, QString> songToLyricsMap;
// 添加歌词文件的搜索
filter.clear(); // 清空过滤器
filter << "*.lrc";
QFileInfoList lrcFiles = dirbsolutePath.entryInfoList(filter, QDir::Files);
// 将去除扩展名的文件以及文件绝对路径插入songToLyricsMap
for (const QFileInfo &lrcFileInfo : lrcFiles) {
QString baseName = lrcFileInfo.completeBaseName(); // 完整的基础文件名,不含扩展名
songToLyricsMap.insert(baseName, lrcFileInfo.absoluteFilePath());
}
/* 遍历 */
for (int i = 0; i < files.count(); i++) {
MediaObjectInfo info;
QString baseName = files.at(i).completeBaseName(); // 去除拓展名
QString fileName = baseName;
// 获取文件名的第二个字符串并拼接 info.fileName: "阿拉斯加海湾 - 蓝心羽\n 蓝心羽"
info.fileName = fileName + "\n"
+ fileName.split("-").at(1);
// 获取对应索引下的文件的绝对路径
info.filePath = QString::fromUtf8(files.at(i)
.filePath()
.toUtf8()
.data());
// 获取歌词路径
QString lyricsPath = songToLyricsMap.value(baseName);
/* 媒体列表添加歌曲 */
if (mediaPlaylist->addMedia(
QUrl::fromLocalFile(info.filePath))) {
// 如果歌词文件存在,则保存其路径
if (!lyricsPath.isEmpty()) {
info.lyricsPath = lyricsPath;
}
/* 添加到容器数组里储存 */
mediaObjectInfo.append(info);
/* 添加歌曲名字至列表 */
ui->listWidget->addItem(info.fileName);
} else {
qDebug()<<
mediaPlaylist->errorString()
.toUtf8().data()
<< endl;
qDebug()<< " Error number:"
<< mediaPlaylist->error()
<< endl;
}
}
}
else
qDebug() << "dir is no exists" <<endl;
}
// 根据播放进度更新歌词显示
void Audio::updateLyrics(qint64 currentTime) {
if (!currentLyricsPath.isEmpty()) {
QFile file(currentLyricsPath);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { // 以只读方式打开文本
QTextStream stream(&file);
// PC端使用不会乱码,开发板则去掉
stream.setCodec("UTF-8");
while (!stream.atEnd()) {
QString line = stream.readLine();
// 解析每一句歌词的时间
// 歌词格式通常是如下形式:[00:12.00]歌词内容
QRegExp timeRegex("\\[(\\d+):(\\d+\\.\\d+)\\]");
if (timeRegex.indexIn(line) != -1) {
// 将分钟数和秒数相加得到歌词播放时间,精确到小数点后两位
qint64 startTime = timeRegex.cap(1).toInt() * 60000 + // 提取分钟数
timeRegex.cap(2).toFloat() * 1000; // 提取秒数
QString currentLyric = line.remove(timeRegex);
// 开始时间小于等于现在播放时间
if (startTime <= currentTime) {
ui->label_4->setText(currentLyric); // 显示
} else {
break;
}
}
}
}
}
else
qDebug() << "currentLyricsPath isEmpty " << endl;
}
// 初始化媒体播放器
void Audio::mediaPlayerInit()
{
musicPlayer = new QMediaPlayer(this);
mediaPlaylist = new QMediaPlaylist(this);
/* 确保列表是空的 */
mediaPlaylist->clear();
/* 设置音乐播放器的列表为mediaPlaylist */
musicPlayer->setPlaylist(mediaPlaylist);
/* 设置播放模式,Loop是列循环 */
mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
}
// 设置专辑封面
void Audio::setAlbumArt(QPixmap pixmap)
{
int cornerRadius = 86; // 圆角大小
// 图像缩放 将输入的pixmap缩放到 172 * 172 大小,同时保持宽高比,并使用平滑变换
QPixmap Pixmap2 = pixmap.scaled(172, 172, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmap roundedPixmap(Pixmap2.size()); // 创建一个跟Pixmap2等大的对象
roundedPixmap.fill(Qt::transparent); // 颜色填充为透明
// 将roundedPixmap传入painter对象 以后的操作都是对roundedPixmap操作
QPainter painter(&roundedPixmap);
// 开启抗锯齿和平滑图像变换
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.setBrush(QBrush(Pixmap2));
painter.setPen(Qt::NoPen); // 不绘制边框
QPainterPath path; // 定义绘制路径
// 将Pixmap2.rect()绘制成带有圆角的矩形区域添加到path中
path.addRoundedRect(Pixmap2.rect(), cornerRadius, cornerRadius);
painter.setClipPath(path); // 设置裁剪路径
painter.drawPixmap(Pixmap2.rect(), Pixmap2); // 将 Pixmap2 按照 Pixmap2.rect() 所确定的位置和大小进行绘制到 roundedPixmap
painter.end();
// 获取当前程序的路径
QDir dir(QCoreApplication::applicationDirPath());
QString filePath = dir.filePath("pixmapTmp.jpg"); // 正确拼接文件的完整路径
bool saved = roundedPixmap.save(filePath);
// 设置widget属性
if(saved) {
ui->widget->setStyleSheet("QWidget#widget{\n"
"border:none;\n"
"border-radius:120px;\n"
"image: url(" + filePath + ");}");
}
else {
ui->widget->setStyleSheet("QWidget#widget{\n"
"border:none;\n"
"border-radius:120px;\n"
"image: url(:/images/fengshen.jpg);}");
}
}
// 播放/暂停按钮点击事件处理
void Audio::on_btn_play_clicked(bool checked)
{
if(checked) {
animation->setStartValue(0); // 开始角度
animation->setEndValue(30); // 结束角度
animation->start(); // 开始动画
animation2->start(); // 开始动画
if(animation3 != nullptr)
animation3->start();
}
else {
animation->setStartValue(30); // 开始角度为30
animation->setEndValue(0); // 结束角度为0
// animation->setLoopCount(-1); // 设置动画无限循环
animation->start(); // 开始动画
animation2->stop(); // 停止动画(disc)
if(animation3 != nullptr)
animation3->stop();
}
int state = musicPlayer->state();
switch (state) {
case QMediaPlayer::StoppedState:
/* 媒体播放 */
musicPlayer->play();
break;
case QMediaPlayer::PlayingState:
/* 媒体暂停 */
musicPlayer->pause();
break;
case QMediaPlayer::PausedState:
musicPlayer->play();
break;
}
}
// 上一曲按钮点击事件处理
void Audio::on_btn_pre_clicked()
{
musicPlayer->stop();
int count = mediaPlaylist->mediaCount();
if (0 == count)
return;
/* 列表上一个 */
mediaPlaylist->previous();
// musicPlayer->play();
emit ui->btn_play->clicked(true);
}
// 下一曲按钮点击事件处理
void Audio::on_btn_next_clicked()
{
musicPlayer->stop();
int count = mediaPlaylist->mediaCount();
if (0 == count) {
qDebug() << "mediaPlaylist->mediaCount = 0" << endl;
return;
}
/* 列表下一个 */
mediaPlaylist->next();
// musicPlayer->play();
emit ui->btn_play->clicked(true);
}
// 循环模式按钮点击事件处理
void Audio::on_btn_loop_clicked(bool checked)
{
if(checked) {
// 当前项循环
mediaPlaylist->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);
}
else {
// 列表循环
mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
}
}
// 媒体播放器状态改变时的槽函数
void Audio::mediaPlayerStateChanged(QMediaPlayer::State state)
{
//click和check相互独立
//click用于实现槽函数;check用于改变图片
switch (state) {
case QMediaPlayer::StoppedState: // 停止
ui->btn_play->setChecked(false);
break;
case QMediaPlayer::PlayingState: // 播放
ui->btn_play->setChecked(true);
break;
case QMediaPlayer::PausedState: // 暂停
ui->btn_play->setChecked(false);
break;
}
}
// 媒体播放列表当前索引改变时的槽函数
void Audio::mediaPlaylistCurrentIndexChanged(int index)
{
if (-1 == index)
return;
/* 设置列表正在播放的项 */
ui->listWidget->setCurrentRow(index);
// 检查索引有效性
if (index >= 0 && index < mediaObjectInfo.size()) {
// 更新当前歌词路径
currentLyricsPath = mediaObjectInfo.at(index).lyricsPath;
qDebug() << currentLyricsPath << endl;
// 如果需要,你可以在这里调用的函数
// updateLyrics();
} else {
// 如果索引无效,清除当前歌词路径
currentLyricsPath.clear();
qDebug() << "currentLyricsPath.clear" << endl;
}
}
// 媒体播放时长改变时的槽函数
void Audio::musicPlayerDurationChanged(qint64 duration)
{
// 设置歌曲播放时间范围 duration / 1000 :毫秒转换为秒
ui->horizontalSlider->setRange(0, duration / 1000);
int second = duration / 1000; // 获取秒
int minute = second / 60; // 获取分
second %= 60;
QString mediaDuration;
mediaDuration.clear();
// 分、秒小于10则前面补0,否则正常显示
if (minute >= 10)
mediaDuration = QString::number(minute, 10);
else
mediaDuration = "0" + QString::number(minute, 10);
if (second >= 10)
mediaDuration = mediaDuration
+ ":" + QString::number(second, 10);
else
mediaDuration = mediaDuration
+ ":0" + QString::number(second, 10);
/* 显示媒体总长度时间 */
ui->label_3->setText(mediaDuration);
}
// 媒体播放位置改变时的槽函数 拖动进度条
void Audio::mediaPlayerPositionChanged(qint64 position)
{
// 判断滑动条是否被按下
if (!ui->horizontalSlider->isSliderDown())
ui->horizontalSlider->setValue(position/1000);
int second = position / 1000; // 获取总的秒数
int minute = second / 60; // 算出分
second %= 60; // 得到剩余的秒
QString mediaPosition;
mediaPosition.clear();
if (minute >= 10)
mediaPosition = QString::number(minute, 10);
else
mediaPosition = "0" + QString::number(minute, 10);
if (second >= 10)
mediaPosition = mediaPosition
+ ":" + QString::number(second, 10);
else
mediaPosition = mediaPosition
+ ":0" + QString::number(second, 10);
/* 显示现在播放的时间 */
ui->label_2->setText(mediaPosition);
}
// 媒体元数据可用状态改变时的槽函数
void Audio::onMetaDataAvailableChanged(bool available) {
if (available) {
// 获取元数据的图片
QVariant coverArtVariant = musicPlayer->metaData(QMediaMetaData::CoverArtImage);
if (coverArtVariant.isValid()) {
QImage coverArtImage = qvariant_cast<QImage>(coverArtVariant); // 转换成QImage后保存
if (!coverArtImage.isNull()) {
// coverArtImage.save("/home/book/Desktop/Program/qt/musicMedia/DescoverArtImage.jpg");
setAlbumArt(QPixmap::fromImage(coverArtImage));
} else {
// 如果没有封面图像,则可以设置一个默认图片
setAlbumArt(QPixmap(":/images/fengshen.jpg"));
}
}
else
setAlbumArt(QPixmap(":/images/fengshen.jpg"));
}
}
// 列表项点击事件处理
void Audio::on_listWidget_itemClicked(QListWidgetItem *item)
{
musicPlayer->stop();
mediaPlaylist->setCurrentIndex(ui->listWidget->row(item));
// musicPlayer->play();
emit ui->btn_play->clicked(true);
}
// 水平滑块释放事件处理
void Audio::on_horizontalSlider_sliderReleased()
{
/* 当进度条被拖动释放后设置媒体播放的位置 */
musicPlayer->setPosition(ui->horizontalSlider->value() * 1000);
}
// 关闭按钮点击事件处理
void Audio::on_pushButton_clicked()
{
clearConnections();
// 设置音乐播放器状态为停止状态
musicPlayer->stop();
// 播放/暂停按钮设置为未选中状态
ui->btn_play->setChecked(false);
// 重置播放列表索引为 -1
mediaPlaylist->setCurrentIndex(-1);
// 清除列表中的当前选中项
ui->listWidget->setCurrentRow(-1);
// 将水平滑块设置到初始位置(值为0)
ui->horizontalSlider->setValue(0);
// 清空显示的播放时间等信息
ui->label_2->setText("");
ui->label_3->setText("");
// 设置专辑封面为初始默认图片
setAlbumArt(QPixmap(":/images/fengshen.jpg"));
// 重置当前歌词路径为空
currentLyricsPath.clear();
// 清除歌词显示标签的文本内容
ui->label_4->setText("");
emit this->back();
}
// 垂直滑块释放事件处理
void Audio::on_verticalSlider_sliderReleased()
{
musicPlayer->setVolume(ui->verticalSlider->value());
}
运行结果:
2.2 视频播放器
视频播放器与音乐播放器一样使用 QMediaPlayer 类,然后还需设置一个视频输出窗口,好让视频在此窗口显示。
代码示例:
#include "playercontrols.h"
#include <QBoxLayout>
#include <QSlider>
#include <QStyle>
#include <QToolButton>
#include <QComboBox>
#include <QAudio>
PlayerControls::PlayerControls(QWidget *parent)
: QWidget(parent)
{
// 播放按钮,默认播放状态
m_playButton = new QToolButton(this);
QIcon customIcon(":/images/play.png");
m_playButton->setIcon(customIcon);
m_playButton->setStyleSheet(
"QToolButton { "
"background-color: transparent; "
"}"
);
connect(m_playButton, &QAbstractButton::clicked, this, &PlayerControls::playClicked);
// 停止按钮,默认不可用
m_stopButton = new QToolButton(this);
QIcon stopIcon(":/images/stop.png");
m_stopButton->setIcon(stopIcon);
m_stopButton->setEnabled(false);
m_stopButton->setStyleSheet("QToolButton { background-color: transparent; }");
connect(m_stopButton, &QAbstractButton::clicked, this, &PlayerControls::stop);
// 下一曲按钮
m_nextButton = new QToolButton(this);
QIcon nextIcon(":/images/next.png");
m_nextButton->setIcon(nextIcon);
m_nextButton->setStyleSheet("QToolButton { background-color: transparent; }");
connect(m_nextButton, &QAbstractButton::clicked, this, &PlayerControls::next);
// 上一曲按钮
m_previousButton = new QToolButton(this);
QIcon lastIcon(":/images/last.png");
m_previousButton->setIcon(lastIcon);
m_previousButton->setStyleSheet("QToolButton { background-color: transparent; }");
connect(m_previousButton, &QAbstractButton::clicked, this, &PlayerControls::previous);
// 静音按钮
m_muteButton = new QToolButton(this);
QIcon soundopenIcon(":/images/sound_open.png");
m_muteButton->setIcon(soundopenIcon);
m_muteButton->setStyleSheet("QToolButton { background-color: transparent; }");
connect(m_muteButton, &QAbstractButton::clicked, this, &PlayerControls::muteClicked);
// 水平方向的音量滑块,设置其范围为0到100
m_volumeSlider = new QSlider(Qt::Horizontal, this);
m_volumeSlider->setRange(0, 100);
m_volumeSlider->setStyleSheet("QToolButton { background-color: rgba(0, 0, 0, 0); }");
connect(m_volumeSlider, &QSlider::valueChanged, this, &PlayerControls::onVolumeSliderValueChanged);
// 播放速率组合框
m_rateBox = new QComboBox(this);
m_rateBox->addItem("0.5x", QVariant(0.5));
m_rateBox->addItem("1.0x", QVariant(1.0));
m_rateBox->addItem("2.0x", QVariant(2.0));
m_rateBox->setCurrentIndex(1);
m_rateBox->setStyleSheet(
"QComboBox {"
"background-color: transparent; " // 设置背景为透明
"border: 1px solid white; " // 设置边框为白色,这里边框宽度为1px,可根据需求调整宽度
"color: white; " // 设置字体颜色为白色
"}"
"QComboBox::drop-down {"
"subcontrol-origin: padding; "
"subcontrol-position: top right; "
"width: 15px; " // 下拉箭头区域宽度,可按需调整
"border-left-width: 1px; "
"border-left-color: white; " // 下拉箭头区域左边框颜色设置为白色,保持整体边框风格一致
"border-left-style: solid; "
"}"
"QComboBox::down-arrow {"
"image: url(:/images/bottom.png); " // 可自定义下拉箭头图标,这里需替换为实际路径
"width: 20px;"
"height: 20px;"
"}"
);
connect(m_rateBox, QOverload<int>::of(&QComboBox::activated), this, &PlayerControls::updateRate);
// 创建水平布局对象,用于放置所有的控制按钮和部件,设置其边距为0
QBoxLayout *layout = new QHBoxLayout;
layout->setMargin(0);
layout->addWidget(m_stopButton);
layout->addWidget(m_previousButton);
layout->addWidget(m_playButton);
layout->addWidget(m_nextButton);
layout->addWidget(m_muteButton);
layout->addWidget(m_volumeSlider);
layout->addWidget(m_rateBox);
// 设置PlayerControls窗口的布局为上述创建的水平布局
setLayout(layout);
}
// 获取媒体播放器当前状态
QMediaPlayer::State PlayerControls::state() const
{
return m_playerState;
}
// 设置媒体播放器状态
void PlayerControls::setState(QMediaPlayer::State state)
{
QIcon customIcon(":/images/play.png");
QIcon pauseIcon(":/images/pause.png");
if (state!= m_playerState) {
m_playerState = state;
switch (state) {
case QMediaPlayer::StoppedState:
// 当媒体播放器处于停止状态时,设置停止按钮不可用,播放按钮图标为播放图标
m_stopButton->setEnabled(false);
m_playButton->setIcon(customIcon);
break;
case QMediaPlayer::PlayingState:
// 当媒体播放器处于播放状态时,设置停止按钮可用,播放按钮图标为暂停图标
m_stopButton->setEnabled(true);
m_playButton->setIcon(pauseIcon);
break;
case QMediaPlayer::PausedState:
// 当媒体播放器处于暂停状态时,设置停止按钮可用,播放按钮图标为播放图标
m_stopButton->setEnabled(true);
m_playButton->setIcon(customIcon);
break;
}
}
}
// 获取当前音量值
int PlayerControls::volume() const
{
qreal linearVolume = QAudio::convertVolume(m_volumeSlider->value() / qreal(100),
QAudio::LogarithmicVolumeScale,
QAudio::LinearVolumeScale);
return qRound(linearVolume * 100);
}
// 设置音量值
void PlayerControls::setVolume(int volume)
{
qreal logarithmicVolume = QAudio::convertVolume(volume / qreal(100),
QAudio::LinearVolumeScale,
QAudio::LogarithmicVolumeScale);
m_volumeSlider->setValue(qRound(logarithmicVolume * 100));
}
// 判断是否静音
bool PlayerControls::isMuted() const
{
return m_playerMuted;
}
// 设置静音状态
void PlayerControls::setMuted(bool muted)
{
QIcon soundopenIcon(":/images/sound_open.png");
QIcon soundcloseIcon(":/images/sound_close.png");
if (muted!= m_playerMuted) {
m_playerMuted = muted;
m_muteButton->setIcon(muted
? soundopenIcon
: soundcloseIcon);
}
}
void PlayerControls::playClicked()
{
switch (m_playerState) {
case QMediaPlayer::StoppedState:
case QMediaPlayer::PausedState:
// 如果媒体播放器处于停止状态或暂停状态,则发送播放信号
emit play();
break;
case QMediaPlayer::PlayingState:
// 如果媒体播放器处于播放状态,则发送暂停信号
emit pause();
break;
}
}
void PlayerControls::muteClicked()
{
emit changeMuting(!m_playerMuted);
}
qreal PlayerControls::playbackRate() const
{
return m_rateBox->itemData(m_rateBox->currentIndex()).toDouble();
}
void PlayerControls::setPlaybackRate(float rate)
{
for (int i = 0; i < m_rateBox->count(); ++i) {
if (qFuzzyCompare(rate, float(m_rateBox->itemData(i).toDouble()))) {
m_rateBox->setCurrentIndex(i);
return;
}
}
m_rateBox->addItem(QString("%1x").arg(rate), QVariant(rate));
m_rateBox->setCurrentIndex(m_rateBox->count() - 1);
}
void PlayerControls::updateRate()
{
emit changeRate(playbackRate());
}
void PlayerControls::onVolumeSliderValueChanged()
{
emit changeVolume(volume());
}
运行结果:
2.3 天气预报
天气预报界面主要涉及 HTTP 通信、JSON 数据解析、自定义控件绘制温湿度曲线、Qt 信号与槽等的应用,通过调用天气预报API解析 JSON 数据获取未来七天的温湿度、风力强度、天气状态等信息,最后显示在Qt UI界面。
篇幅有限,天气预报详解可看我之前的文章:
- https://blog.csdn.net/m0_74712453/article/details/144778215?spm=1001.2014.3001.5501
示例代码:
#include "weather.h"
#include "ui_weather.h"
#include <QMouseEvent>
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QMessageBox>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QPainter>
Weather::Weather(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Weather)
{
ui->setupUi(this);
// 设置字体格式
this->setFont(QFont("Airal",15));
// 固定窗口 防止拖拉
setFixedSize(1024,600); // 设置窗口固定尺寸
setWindowFlag(Qt::FramelessWindowHint);
// 新建菜单组件
menuQuit = new QMenu(this);
// 设置菜单项文字颜色
menuQuit->setStyleSheet("QMenu::item {color:white}");
// 为菜单组件添加一个动作
QAction *closeAct = new QAction(QIcon(":/images/close.png"),tr("退出"),this);
menuQuit->addAction(closeAct);
// 用户选择菜单项并点击后回调函数,退出程序
connect(menuQuit,&QMenu::triggered,this,[=]{
this->close();
});
mWeekList << ui->labelday0 << ui->labelday1
<< ui->labelday2 << ui->labelday3
<< ui->labelday4 << ui->labelday5;
mDateList << ui->labelDate0 << ui->labelDate1
<< ui->labelDate2 << ui->labelDate3
<< ui->labelDate4 << ui->labelDate5;
mIconList << ui->labelWeaterIcon0 << ui->labelWeaterIcon1
<< ui->labelWeaterIcon2 << ui->labelWeaterIcon3
<< ui->labelWeaterIcon4 << ui->labelWeaterIcon5;
mWeaTypeList << ui->lbweatherTypeDate0 << ui->lbweatherTypeDate1
<< ui->lbweatherTypeDate2 << ui->lbweatherTypeDate3
<< ui->lbweatherTypeDate4 << ui->lbweatherTypeDate5;
mAirqList << ui->labelairq0 << ui->labelairq1 << ui->labelairq2
<< ui->labelairq3 << ui->labelairq4 << ui->labelairq5;
mFxList << ui->labelFX0 << ui->labelFX1 << ui->labelFX2
<< ui->labelFX3 << ui->labelFX4 << ui->labelFX5;
mFlList << ui->labelFL0 << ui->labelFL1 << ui->labelFL2
<< ui->labelFL3 << ui->labelFL4 << ui->labelFL5;
//根据keys,设置icon的路径
mTypeMap.insert("暴雪",":/images/type/BaoXue.png");
mTypeMap.insert("暴雨",":/images/type/BaoYu. png");
mTypeMap.insert("暴雨到大暴雨",":/images/type/BaoYuDaoDaBaoYu.png");
mTypeMap.insert("大暴雨",":/images/type/DaBaoYu.png");
mTypeMap.insert("大暴雨到特大暴雨",":/images/type/DaBaoYuDaoTeDaBaoYu.png");
mTypeMap.insert("大到暴雪",":/images/type/DaDaoBaoXue.png");
mTypeMap.insert("大雪",":/images/type/DaXue.png");
mTypeMap.insert("大雨",":/images/type/DaYu.png");
mTypeMap.insert("冻雨",":/images/type/DongYu.png");
mTypeMap.insert("多云",":/images/type/DuoYun.png");
mTypeMap.insert("浮沉",":/images/type/FuChen.png");
mTypeMap.insert("雷阵雨",":/images/type/LeiZhenYu.png");
mTypeMap.insert("雷阵雨伴有冰雹",":/images/type/LeiZhenYuBanYouBingBao.png");
mTypeMap.insert("霾",":/images/type/Mai.png");
mTypeMap.insert("强沙尘暴",":/images/type/QiangShaChenBao.png");
mTypeMap.insert("晴",":/images/type/Qing.png");
mTypeMap.insert("沙尘暴",":/images/type/ShaChenBao.png");
mTypeMap.insert("特大暴雨",":/images/type/TeDaBaoYu.png");
mTypeMap.insert("undefined",":/images/type/undefined.png");
mTypeMap.insert("雾",":/images/type/Wu.png");
mTypeMap.insert("小到中雪",":/images/type/XiaoDaoZhongXue.png");
mTypeMap.insert("小到中雨",":/images/type/XiaoDaoZhongYu.png");
mTypeMap.insert("小雪",":/images/type/XiaoXue.png");
mTypeMap.insert("小雨",":/images/type/XiaoYu.png");
mTypeMap.insert("雪",":/images/type/Xue.png");
mTypeMap.insert("扬沙",":/images/type/YangSha.png");
mTypeMap.insert("阴",":/images/type/Yin.png");
mTypeMap.insert("雨",":/images/type/Yu.png");
mTypeMap.insert("雨夹雪",":/images/type/YuJiaXue.png");
mTypeMap.insert("阵雪",":/images/type/ZhenXue.png");
mTypeMap.insert("阵雨",":/images/type/ZhenYu.png");
mTypeMap.insert("中到大雪",":/images/type/ZhongDaoDaXue.png");
mTypeMap.insert("中到大雨",":/images/type/ZhongDaoDaYu.png");
mTypeMap.insert("中雪",":/images/type/ZhongXue.png");
mTypeMap.insert("中雨",":/images/type/ZhongYu.png");
// 安装事件过滤器
ui->widget0404->installEventFilter(this);
ui->widget0405->installEventFilter(this);
}
void Weather::weather_working()
{
// 由QNetworkAccessManager发起get请求
manager = new QNetworkAccessManager(this);
//strUrl = "http://v1.yiketianqi.com/api?unescape=1&version=v63&appid=71755637&appsecret=cxCm8AxT";
strUrl = "http://v1.yiketianqi.com/api?unescape=1&version=v9&appid=71755637&appsecret=cxCm8AxT";
//strUrl = "http://v1.yiketianqi.com/api?unescape=1&version=v9&appid=26371314&appsecret=qOhoD413";
QUrl urlTianQi(strUrl);
// 指定请求的url地址
QNetworkRequest res(urlTianQi);
reply = manager->get(res);
// 网络请求后进行数据读取
connect(manager,&QNetworkAccessManager::finished,this,&Weather::readHttpRply);
}
Weather::~Weather()
{
delete ui;
}
// 重写鼠标点击
void Weather::mousePressEvent(QMouseEvent *event)
{
// 捕获右键被按下
if(event->button() == Qt::RightButton){
//qDebug() << "right Mouse clicked!";
menuQuit->exec(QCursor::pos());
}
// 捕获左键被按下
// if(event->button() == Qt::LeftButton){
// // qDebug() << "left Mouse clicked!";
// // 鼠标当前位置
// // qDebug() << event->globalPos();
// // 窗口当前位置
// // qDebug() << this->pos();
// // 窗口新位置 event->globalPos() - this->pos()
// mOffset = event->globalPos() - this->pos();
// }
}
// 鼠标左键按下后的移动,导致事件被调用,设置窗口的新位置
//void Widget::mouseMoveEvent(QMouseEvent *event)
//{
// this->move(event->globalPos() - mOffset);
//}
// 绘制温度曲线变化
bool Weather::eventFilter(QObject *watched, QEvent *event)
{
if(watched == ui->widget0404 && event->type() == QEvent::Paint){
drawTempLineHigh();
return true;
}
if(watched == ui->widget0405 && event->type() == QEvent::Paint){
drawTempLineLow();
return true;
}
return QWidget::eventFilter(watched,event);
}
// 解析json数据 单天数据
void Weather::parseWeatherJsonData(QByteArray rawData)
{
QJsonDocument jsonObj = QJsonDocument::fromJson(rawData);
if(!jsonObj.isNull() && jsonObj.isObject()){
QJsonObject objRoot = jsonObj.object();
// 解析当前日期
QString date = objRoot["date"].toString();
QString week = objRoot["week"].toString();
ui->labelCurrentDate->setText(date + " " + week);
// 解析城市名词
QString cityName = objRoot["city"].toString();
ui->labelCity->setText(cityName + "市");
// 解析当前温度
QString currentTemp = objRoot["tem"].toString();
ui->labelTmp->setText(currentTemp + "℃");
ui->labelTempRange->setText(objRoot["tem2"].toString() + "~" + objRoot["tem1"].toString());
// 解析天气类型
ui->labelweatherType->setText(objRoot["wea"].toString());
ui->labelWeatherIcon->setPixmap(mTypeMap[objRoot["wea"].toString()]);
// 解析感冒指数
ui->labelGanmao->setText(objRoot["air_tips"].toString());
// 解析风向
ui->labelFXType->setText(objRoot["win"].toString());
// 解析风力
ui->labelFXType_2->setText(objRoot["win_speed"].toString());
// 解析PM2.5
ui->labelPM25Data->setText(objRoot["air_pm25"].toString());
// 解析湿度
ui->labelShiduData->setText(objRoot["humidity"].toString());
// 解析空气质量
ui->labelairData->setText(objRoot["air_level"].toString());
}
}
// 解析json数据 七天数据
void Weather::parseWeatherJsonDataNew(QByteArray rawData)
{
// 将JSON作为UTF-8编码的JSON文档进行解析,并基于它创建一个 QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData);
// 解析对象
if(!jsonDoc.isNull() && jsonDoc.isObject()){
QJsonObject jsonRoot = jsonDoc.object();
days[0].mCity = jsonRoot["city"].toString();
days[0].mPm25 = jsonRoot["aqi"].toObject()["pm25"].toString();
// 解析数组
if(jsonRoot.contains("data") && jsonRoot["data"].isArray()){
// 转换类型为QT表示JSON数组的数据类型
QJsonArray weaArray = jsonRoot["data"].toArray();
for(int i = 0; i < weaArray.size(); i++){
// 转换类型为QT表示JSON对象的数据类型
QJsonObject obj = weaArray[i].toObject();
days[i].mDate = obj["date"].toString();
days[i].mWeek = obj["week"].toString();
days[i].mWeathType = obj["wea"].toString();
days[i].mTemp = obj["tem"].toString();
days[i].mTempLow = obj["tem2"].toString();
days[i].mTempHigh = obj["tem1"].toString();
days[i].mFx = obj["win"].toArray()[0].toString();
days[i].mFl = obj["win_speed"].toString();
days[i].mAirq = obj["air_level"].toString();
days[i].mTips = obj["air_tips"].toString();
days[i].mHu = obj["humidity"].toString();
}
}
}
// 更新界面
updateUI();
}
void Weather::updateUI()
{
QPixmap pixmap;
// 解析当前日期
ui->labelCurrentDate->setText(days[0].mDate + " " + days[0].mWeek);
// 解析城市名词
ui->labelCity->setText(days[0].mCity + "市");
// 解析当前温度
ui->labelTmp->setText(days[0].mTemp + "℃");
ui->labelTempRange->setText(
days[0].mTempLow + "℃" + "~" + days[0].mTempHigh + "℃");
// 解析天气类型
ui->labelweatherType->setText(days[0].mWeathType);
ui->labelWeatherIcon->setPixmap(mTypeMap[days[0].mWeathType]);
// 解析感冒指数
ui->labelGanmao->setText(days[0].mTips);
// 解析风向
ui->labelFXType->setText(days[0].mFx);
// 解析风力
ui->labelFXType_2->setText(days[0].mFl);
// 解析PM2.5
ui->labelPM25Data->setText(days[0].mPm25);
// 解析湿度
ui->labelShiduData->setText(days[0].mHu);
// 解析空气质量
ui->labelairData->setText(days[0].mAirq);
ui->lbweatherTypeDate3->setText(days[3].mWeathType);
ui->lbweatherTypeDate0->setText(days[0].mWeathType);
for(int i = 0; i < 6; i++){
mWeekList[i]->setText(days[i].mWeek);
mWeekList[0]->setText("今天");
mWeekList[1]->setText("明天");
mWeekList[2]->setText("后天");
QStringList dayList = days[i].mDate.split("-");
mDateList[i]->setText(dayList.at(1) + "-" + dayList.at(2));
// 缩放图片大小和label大小能匹配
int index = days[i].mWeathType.indexOf("转");
if(index != -1){
pixmap = mTypeMap[days[i].mWeathType.left(index)];
}else{
pixmap = mTypeMap[days[i].mWeathType];
}
pixmap = pixmap.scaled(mIconList[i]->size(),Qt::KeepAspectRatio,Qt::SmoothTransformation);
mIconList[i]->setMaximumHeight(50);
mIconList[i]->setMaximumWidth(ui->widget02->width()/6.5);
mIconList[i]->setPixmap(pixmap);
mWeaTypeList[i]->setText(days[i].mWeathType);
QString airQ = days[i].mAirq;
mAirqList[i]->setText(airQ);
if(airQ == "优"){
mAirqList[i]->setStyleSheet("background-color:rgb(150, 213, 32);border-radius: 7px;color: rgb(230, 230, 230)");
}
if(airQ == "良"){
mAirqList[i]->setStyleSheet("background-color:rgb(255, 170, 127);border-radius: 7px;color: rgb(230, 230, 230)");
}
if(airQ == "轻度污染"){
mAirqList[i]->setStyleSheet("background-color:rgb(170, 200, 129);border-radius: 7px;color: rgb(230, 230, 230)");
}
if(airQ == "中度污染"){
mAirqList[i]->setStyleSheet("background-color:rgb(255, 17, 17);border-radius: 7px;color: rgb(230, 230, 230)");
}
if(airQ == "重度污染"){
mAirqList[i]->setStyleSheet("background-color:rgb(153, 0, 0);border-radius: 7px;color: rgb(230, 230, 230)");
}
mFxList[i]->setText(days[i].mFx);
index = days[i].mFl.indexOf("转");
if(index != -1){
mFlList[i]->setText(days[i].mFl.left(index));
}else{
mFlList[i]->setText(days[i].mFl);
}
}
update();
}
// 绘制最高温度曲线
void Weather::drawTempLineHigh()
{
QPainter painter(ui->widget0404);
painter.setRenderHint(QPainter::Antialiasing,true);
painter.setBrush(Qt::yellow);
painter.setPen(Qt::yellow);
int ave;
int sum = 0;
int offSet = 0;
int middle = ui->widget0404->height()*0.7;
for (int i = 0; i < 6; i++) {
sum += days[i].mTempHigh.toInt();
}
ave = sum / 6;
// 定义6个点
QPoint points[6];
for (int i = 0; i < 6; i++) {
// 以mAirqList为参照算出 x 位置
points[i].setX(mAirqList[i]->x() + mAirqList[i]->width()/2);
// 算出偏移量 (最高温度 - 平均温度) * 2
offSet = (days[i].mTempHigh.toInt() - ave)*2;
// 算出 Y 位置 中值 - 偏移量
points[i].setY(middle - offSet);
// 画出6个点
painter.drawEllipse(QPoint(points[i]),3,3);
// 画出当天温度
painter.drawText(points[i].x() - 15,points[i].y()-15,days[i].mTempHigh + "°");
}
for (int i = 0; i < 5; i++) {
// 点与点之间连线
painter.drawLine(points[i],points[i+1]);
}
}
// 绘制最低温度曲线
void Weather::drawTempLineLow()
{
QPainter painter(ui->widget0405);
// 抗锯齿
painter.setRenderHint(QPainter::Antialiasing,true);
// 设置画刷
painter.setBrush(QColor(70,192,203));
// 设置画笔
painter.setPen(QColor(70,192,203));
int ave;
int sum = 0;
int offSet = 0;
int middle = ui->widget0405->height()*0.7;
for (int i = 0; i < 6; i++) {
sum += days[i].mTempLow.toInt();
}
ave = sum / 6;
// 定义6个点
QPoint points[6];
for (int i = 0; i < 6; i++) {
points[i].setX(mAirqList[i]->x() + mAirqList[i]->width()/2);
offSet = (days[i].mTempLow.toInt() - ave)*2;
points[i].setY(middle - offSet);
// 画出6个点
painter.drawEllipse(QPoint(points[i]),3,3);
// 画出当天温度
painter.drawText(points[i].x() - 15,points[i].y()-15,days[i].mTempLow + "°");
}
for (int i = 0; i < 5; i++) {
// 画出两点之间的线
painter.drawLine(points[i],points[i+1]);
}
}
// 读取数据
void Weather::readHttpRply(QNetworkReply *reply)
{
// 获取返回码 获取从HTTP服务器接收到的HTTP状态码 2xx:成功状态码 4xx:客户端错误状态码 5xx:服务端错误状态码
int resCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if(reply->error() == QNetworkReply::NoError && resCode == 200){
// 从设备中读取所有剩余数据,并将其作为字节数组返回。
QByteArray data = reply->readAll();
//parseWeatherJsonData(data);
// 解析数据
parseWeatherJsonDataNew(data);
qDebug() << QString::fromUtf8(data);
}else{
//qDebug() << "请求失败:" << reply->errorString();
QMessageBox mes;
mes.setWindowTitle("错误");
mes.setText("网络请求失败");
mes.setStyleSheet("QPushButton {color:red}");
mes.setStandardButtons(QMessageBox::Ok);
mes.exec();
}
}
运行结果:
2.4 GPS 定位
我们主要使用百度地图API的调用、GPS模块驱动,通过获取GPS所获取到的经纬度信息去地图进行定位。
GPS模块与外部控制器的通讯接口有多种方式,这里我们使用串口进行通讯,波特率为9600bps,1bit停止位,无校验位,无流控,默认每秒输出一次标准格式数据。
篇幅有限,串口详解可以看我之前的文章:https://blog.csdn.net/m0_74712453/article/details/142493394?spm=1001.2014.3001.5501
代码示例:
#include "map.h"
#include "simplemessagebox.h"
#include "commonhelper.h"
#include <QSslConfiguration>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QVBoxLayout>
#include <QPixmap>
#include <QSerialPortInfo>
Map::Map(QWidget *parent) : QDialog(parent)
{
initUi();
}
Map::~Map()
{
if (m_port.isOpen())
m_port.close();
}
void Map::initUi()
{
auto pMovie = new QMovie(this);
pMovie->setFileName(":/images/loading.gif");
pMovie->start();
this->setFixedSize(1024,600);
m_logoLabel.setObjectName("logoLabel");
m_logoLabel.setText(QString("当前展示定位坐标:[") +m_location + "]");
m_back = new QPushButton(tr("返回"),this);
m_iconLabel.setMovie(pMovie);
m_iconLabel.setAlignment(Qt::AlignCenter);
m_logoLabel.setAlignment(Qt::AlignRight);
auto *pVLayout = new QVBoxLayout;
pVLayout->addStretch();
pVLayout->addWidget(&m_logoLabel);
pVLayout->addWidget(m_back);
m_iconLabel.setLayout(pVLayout);
connect(m_back,&QPushButton::clicked,this,&Map::on_back_clicked);
auto *pMainLayout = new QVBoxLayout;
pMainLayout->addWidget(&m_iconLabel);
pMainLayout->setMargin(0);
setLayout(pMainLayout);
setObjectName("map_widget");
CommonHelper::setStyleSheet(":/style/default.qss", this);
}
void Map::initCtrl()
{
initSerialPort();
requestMapImage(m_location);
}
// 串口初始化
void Map::initSerialPort()
{
// 设置串口名称
m_port.setPortName("ttymxc5");
// 以读写方式打开串口设备
if(!m_port.open(QIODevice::ReadWrite)) {
SimpleMessageBox::infomationMessageBox("未检测到设备,请重试");
m_logoLabel.setText(QString("未检测到GPS设备,当前展示定位坐标:[") +m_location + "]");
return;
}
// 设置波特率为9600
m_port.setBaudRate(QSerialPort::Baud9600, QSerialPort::AllDirections);
// 设置数据位为8位
m_port.setDataBits(QSerialPort::Data8);
// 设置流控制为无流控制模式
m_port.setFlowControl(QSerialPort::NoFlowControl);
// 设置为无校验位
m_port.setParity(QSerialPort::NoParity);
// 设置停止位为1位
m_port.setStopBits(QSerialPort::OneStop);
// readyRead信号是当串口接收到新的数据并且这些数据可以从串口读取时被触发。
connect(&m_port, &QSerialPort::readyRead, this, &Map::portReadReady);
}
// 请求地图照片
void Map::requestMapImage(const QString ¢er)
{
// 构建请求地图图片的URL地址
QString url = baidudUrl.arg(baidudAk).arg(center);
m_array.clear();
// 发出网络请求
requestNetwork(url);
}
void Map::requestNetwork(const QString &url)
{
QSslConfiguration config;
// 设置对等方验证模式为不进行验证
config.setPeerVerifyMode(QSslSocket::VerifyNone);
// 设置SSL协议版本为TlsV1_0
config.setProtocol(QSsl::TlsV1_0);
QNetworkRequest networkRequest;
// 将之前配置好的SSL配置信息设置到网络请求对象中
networkRequest.setSslConfiguration(config);
// 设置请求地址
networkRequest.setUrl(url);
// 获取请求返回数据
QNetworkReply *newReply = m_networkAccessManager.get(networkRequest);
// 当一旦请求到数据时执行netReadyRead
connect(newReply, &QNetworkReply::readyRead, this, &Map::netReadyRead);
// 网络请求完成后执行deleteLater
connect(newReply, &QNetworkReply::finished, newReply, &QNetworkReply::deleteLater);
}
void Map::netReadyRead()
{
// 使用qobject_cast将发送信号的对象转换为QNetworkReply *类型
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
// 追加请求到的数据
m_array.append(reply->readAll());
QPixmap pixmap(m_array);
// 从获取的数据构建出有效的QPixmap对象并设置
if(pixmap.loadFromData(m_array)) {
m_iconLabel.setPixmap(pixmap);
}
}
// 读取串口数据
void Map::portReadReady()
{
// 当串口仍有数据
while (m_port.bytesAvailable()) {
QString line = m_port.readLine(); // 读取一行
auto location = parseGpsData(line); // 解析GPS数据
if (!location.isEmpty()) {
m_location = location;
line = "定位成功,[" + location + "]";
requestMapImage(location); // 根据经纬度定位
}
else {
line = ("定位中... 请尝试移到空旷位置,展示坐标 [" + m_location + "]");
}
m_logoLabel.setText(line);
}
}
void Map::map_working()
{
initCtrl();
setModal(true);
this->show();
}
void Map::on_back_clicked()
{
emit backout();
}
// 解析GPS数据
QString Map::parseGpsData(const QString &data)
{
QString ret;
// 过滤无效信息
if (data.contains(",,,,,"))
return ret;
// 去除输入数据字符串两端的空白字符后,按照逗号进行分割
auto list = data.trimmed().split(',');
if (list.count() != 15)
return ret;
// 判断第一个字符串是否是GPS定位数据
if (!list.at(0).contains("$GPGGA"))
return ret;
// 保存原始经度数据
bool ok;
auto e = list.at(4).toDouble(&ok);
if (!ok)
return ret;
// 保存原始纬度数据
auto n = list.at(2).toDouble(&ok);
if (!ok)
return ret;
// 计算经度的整数部分
int i = e / 100;
// 计算经度的小数部分
double j = (e / 100 - i) * 100 / 60.0;
// 拼接并保留6位小数
ret = QString::number(i+j, 'f', 6);
// 计算纬度的整数部分
i = n / 100;
// 计算纬度的小数部分
j = (n / 100 - i) * 100 / 60.0;
// 将经纬度拼接
ret = ret + ',' + QString::number(i+j, 'f', 6);
return ret;
}
在电脑上运行显示不出定位:
开发板接上 GPS 模块后,运行代码即可显示:
2.5 环境监测界面
我们得先写好后台硬件驱动程序,才可以通过前台 QT GUI 界面显示或者控制硬件。
QT 界面展示:
三、后台硬件驱动
该项目主要用到 LED,DHT11,SR501,ADC,蜂鸣器模块,我们需要分别实现它们的驱动程序,规定好底层编程和硬件接口,以便应用层可以直接调用。
我们通常都是在Linux的终端上打开一个可执行文件,然后可执行文件就会执行程序。那么这个可执行文件做了什么呢?
可执行文件先是在应用层读取程序,其中会有很多库函数,库函数是属于内核之中。而内核又会往下调用驱动层程序。最终驱动层控制具体硬件。
所以我们需要编写两个程序,一个是驱动层的,一个是应用层的,最后驱动层需要注册进入内核,应用层才能够使用。
框架流程图:
该项目中QT代码编写相当于应用层,所以我们还需要编写后台硬件驱动。
编写驱动主要为以下七个步骤:
- 确定主设备号,也可以让内核分配
- 定义自己的 file_operations 结构体
- 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file operations 结构体
- 把 file_operations 结构体告诉内核: register_chrdev
- 谁来注册驱动程序啊? 得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
- 有入口函数就应该有出口函数: 卸载驱动程序时,出口函数调用unregister_chrdev
- 其他完善:提供设备信息,自动创建设备节点: class_create,device_create
篇幅有限,具体模块的驱动实现可以看我之前的文章:
-
- hello驱动
-
- LED驱动
-
- SR501(人体红外模块)驱动
-
- SR04(超声波)驱动
-
- DHT11(温湿度传感器)驱动
将每个模块的驱动都装载到开发板后,编译运行QT程序,就可以通过上位机界面控制下位机传感器了。
四、总结
该项目是作者去年在公司实习的时候结合自身所学知识做的,教学博客篇幅有限,作者基本将所有用到的知识点都列了出来,在以往写的文章里面。项目代码整体框架兼容性很高,方便大家扩展,你们可以根据自己的逻辑思维在作者的代码上自行添加功能。
该项目可以使用在IMX6ULL,STM32MP157,RK3568开发板,具体开发板配置 QT 环境可看作者之前的文章:
- 将QT移植到RK3568开发板
- 将QT移植到IMX6ULL开发板
- 将QT移植到STM32MP157开发板
项目改进之处:
对于比较复杂的程序,前台界面显示、后台程序由不同的团队进行开发,双方定义好交互的接口即可。这样,前台、后台程序可以分别独立开发,降低相互之间的依赖。
- 我们可以使用JsonRPC远程调用来实现前后台程序的交互。
刚开始接触界面,布局确实不是那么重要,我们的界面一共也没几个组件,就几个 button,几个 label ,在 UI 界面拉拉组件即可,你是否考虑过软件大小随意变化的问题,你是否考虑过后期添加组件,随着我们的软件越来越庞大,让组件自动分配空间显的尤为重要。
- 考虑优化布局,让组件自动分配空间。