本项目客户端使用Qt6.7.3+cmake进行搭建,需要在项目开始时选择git同步项目,具体环境如下图:




没显示的就是不需要勾选的。项目创建时选择编译器为minGw11.2.0 64-bit,debug等一共五种模式全部勾选上。
需要跟随文章进行项目推进的话推荐与文章环境一致。相比于我们之前所实现的仿QQ音乐播放器,这个项目难度要比前者大的多。而且前项目qt5中与qt6中的使用过的函数如果功能没有发生太大变化,我们会省略的去介绍这些功能。希望读者最好是将音乐播放器的项目做完之后再来学习本项目。
同样的与上一个项目相同,因为文章描述不便,所以我们实现时如果是界面ui摆放,qss美化等工作,只给出当前步骤做完之后的示例图以及控件的嵌套关系。如若需要布局尺寸qss样式代码以及图片资源,可以参考本项目的最后一篇文章中提供的源码连接。下面我们正式开始本项目的学习。
本文章结束时最终实现的首页布局与播放界面布局如下:
播放界面布局如下:
全屏之后的效果如下:
一.仓库搭建
因为本项目我们需要实现服务端与客户端,代码量比较大,所以我们先在gitee上创建一个仓库,同时为该仓库创建两个保护分支,一个master分支,一个develop分支。然后使用git将远端仓库拉到我们自己项目代码的文件夹后执行下面操作:
git bash操作:
git clone 项⽬仓库连接
git branch #查看本地仓库
git branch -r #查看远程仓库
git checkout -b develop origin/develop #基于远程origin/develop分⽀,创建本地
develop分⽀
git checkout -b feature/startupUI develop #基于本地develop创建feature/startupUI分
⽀
二.项目构建
三.启动界面
因为我们最后视频播放器需要从远端拉取数据,所以我们需要设置一个启动页面来让我们的资源拉取完毕之后再展示主界面,具体效果如图,很简单:
其实就是一个QWidget与一个QLabel的组合,这部分我们给出具体的实现方法:
界⾯内部只有⼀个QLabel显⽰logo。具体如下:
① 界⾯为模态对话框,没有标题栏,并且窗⼝图标也不会在任务栏显⽰
② 界⾯尺⼨:⻓1450,宽860,背景⾊为纯⽩⾊#FFFFFF
③ 创建⼀个QLabel⽤来显⽰Logo,QLabel在界⾯中的位置:(524, 374)
然后我们需要设置让其两秒之后消失然后显示主窗口,实现方法如下:
#include "startpage.h"
#include <QLabel>
#include <QTimer>
StartPage::StartPage(QWidget *parent)
: QDialog(parent)
{
//设置尺寸大小与背景颜色,同时设置tool保证
setFixedSize(697,392);
setStyleSheet("background-color : #FFFFFF;");
setWindowFlags(Qt::FramelessWindowHint | Qt::Tool);
//设置Label显示图片
QLabel* imageLabel = new QLabel(this);
imageLabel->setPixmap(QPixmap(":/images/startupPage/limeplayer.png"));
imageLabel->move(148,118);
}
void StartPage::startUp()
{
QTimer* timer = new QTimer(this);
connect(timer,&QTimer::timeout,this,[=](){
timer->stop();
close();
timer->deleteLater();
});
timer->start(2000);
}
因为qt6.5之后如果我们不在main函数中设置时,它的默认窗口颜色会跟随系统浅色或深色主题进行设置,以及窗口大小会跟随屏幕分辨率进行变化,我们本项目不需要这两个属性,所以我们在main函数中设置下:
#include "limeplayer.h"
#include "startpage.h"
#include <QApplication>
#include <QStyleFactory>
int main(int argc, char *argv[])
{
// 禁⽌窗⼝按照分辨率百分⽐缩放,必须套放在程序第⼀句
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
Qt::HighDpiScaleFactorRoundingPolicy::Floor);
//让它不跟随系统设置的深色模式-设置为保持浅色模式
qputenv("QT_QPA_PLATFORM","windows:darkmode=0");
QApplication a(argc, argv);
//显示启动窗口
StartPage startPage;
startPage.startUp();
startPage.exec();
LimePlayer w;
w.show();
return a.exec();
}
四.主界面布局
4.1主界面外观布局实现
仔细观察发现:
a. 主窗⼝没有标题栏
b. 程序运⾏时在任务栏有特定图标
c. 窗⼝的边沿有点模糊⿊⾊阴影效果
上述特征在窗⼝显⽰出来已经存在来,所以在主界⾯创建时,应该完成上述需求。再仔细观察主界面我们可以得到主界面一共分为三个部分,head,bodyleft及bodyRight。所以我们可以这样布局:
控件嵌套关系如下:
接下来当然是实现界面阴影效果了,还是像之前一样,我们把初始化ui界面的逻辑和连接信号槽的所有逻辑统一放到两个函数中并在构造函数中调用,实现逻辑如下:
void LimePlayer::initUi()
{
setWindowIcon(QIcon(":/images/homePage/logo.png"));
//必须要无边框和透明才能设置阴影效果
setWindowFlags(Qt::FramelessWindowHint);
setAttribute(Qt::WA_TranslucentBackground);
//设置阴影效果
QGraphicsDropShadowEffect* dropEffect = new QGraphicsDropShadowEffect(this);
dropEffect->setBlurRadius(20);//设置阴影半径
dropEffect->setColor(Qt::black);//设置阴影颜色
dropEffect->setOffset(0,0);//设置阴影偏移量
//注意这里需要设置到背景widget上,不然会出现脏坐标错误
ui->background->setGraphicsEffect(dropEffect);
// 设置bodyLeft中⾃定义按钮⽂本和图⽚-同时设置默认字体颜色
ui->homePageBtn->setFontColor("#000000");
ui->homePageBtn->setImageTextAndButtonType(":/images/homePage/shouyexuan.png", "⾸⻚",HomePageBtn);
ui->myPageBtn->setFontColor("#999999");
ui->myPageBtn->setImageTextAndButtonType(":/images/homePage/wode.png", "我的",MyPageBtn);
ui->sysPageBtn->setFontColor("#999999");
ui->sysPageBtn->setImageTextAndButtonType(":/images/homePage/admin.png", "系统",SysPageBtn);
// 默认情况下,选中⾸⻚,系统管理⻚⾯隐藏
ui->stackedWidget->setCurrentIndex(HomePageBtn);
}
void LimePlayer::connectSignalAndSlot()
{
connect(ui->minBtn,&QPushButton::clicked,this,&LimePlayer::showMinimized);//最小化
connect(ui->quitBtn,&QPushButton::clicked,this,&LimePlayer::close);//关闭窗口
connect(ui->homePageBtn,&PageSwitchButton::btClicked,this,&LimePlayer::switchPage);
connect(ui->myPageBtn,&PageSwitchButton::btClicked,this,&LimePlayer::switchPage);
connect(ui->sysPageBtn,&PageSwitchButton::btClicked,this,&LimePlayer::switchPage);
}
当然去掉边框后窗口无法再移动,我们也需要手动处理下:
//////limePlayer.h/////////////////
class LimePlayer : public QWidget
{
Q_OBJECT
public:
LimePlayer(QWidget *parent = nullptr);
//初始化界面Ui
void initUi();
//管理所有connect的函数
void connectSignalAndSlot();
//实现拖拽效果-只允许在head部分进行拖拽
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
~LimePlayer();
private:
Ui::LimePlayer *ui;
QPoint dragPos;//记录鼠标拖动时的位置
};
#endif // LIMEPLAYER_H
/////////////对应cpp文件///////////////
void LimePlayer::mouseMoveEvent(QMouseEvent *event)
{
QPoint point = event->position().toPoint();
//如果说鼠标点击范围在head中且为左键点击
if(ui->head->rect().contains(point) && event->buttons() == Qt::LeftButton)
{
move(event->globalPosition().toPoint() - dragPos);
return;
}
//其余事件交给上层处理
QWidget::mouseMoveEvent(event);
}
void LimePlayer::mousePressEvent(QMouseEvent *event)
{
QPoint point = event->position().toPoint();
//如果说鼠标点击范围在head中且为左键点击
if(ui->head->rect().contains(point) && event->button() == Qt::LeftButton)
{
dragPos = event->globalPosition().toPoint() - geometry().topLeft();
return;
}
//其余事件交给上层处理
QWidget::mousePressEvent(event);
}
4.2主界面功能逻辑实现
4.2.1minBtn和quitBtn添加槽函数
minBtn和quitBtn的作⽤是最⼩化和关闭窗⼝,这两个功能⽗类已经实现,所以给按钮绑定
QPushButton::clicked信号时,槽函数直接使⽤⽗类QWdiget的即可。上面的connect中我们已经连接相关信号槽。
4.2.2点击bodyLeft中的按钮实现页面切换
我们新建造一个自定义普通类为PageSwitchButton,后面将侧边栏的三个按钮提升上来即可:
///pageswitchbutton.h
#ifndef PAGESWITCHBUTTON_H
#define PAGESWITCHBUTTON_H
#include <QPushButton>
#include <QLabel>
//标记按钮类型
enum ButtonType{
HomePageBtn,
MyPageBtn,
SysPageBtn
};
class PageSwitchButton : public QPushButton
{
Q_OBJECT
public:
explicit PageSwitchButton(QWidget *parent = nullptr);
// 设置按钮上的⽂本和图⽚
void setImageTextAndButtonType(const QString &imagePath, const QString &text,ButtonType buTy);
//设置hover效果
void enterEvent(QEnterEvent *event) override;
void leaveEvent(QEvent *event) override;
//设置字体颜色
void setFontColor(const QString& color);
//切换图片
void setImage(const QString& imagePath);
protected:
// 重写QWidget中⿏标按下事件函数
void mousePressEvent(QMouseEvent* event) override;
private:
QLabel* btnImage; // 按钮上标签
QLabel* btnTittle; // 按钮上⽂本
ButtonType buttonType;//按钮类型
signals:
void btClicked(ButtonType buttonPage);//当按钮被按下时发射信号
};
#endif // PAGESWITCHBUTTON_H
////pageswitchbutton.cpp
#include "pageswitchbutton.h"
PageSwitchButton::PageSwitchButton(QWidget *parent)
:QPushButton{parent}
{
// 设置按钮的⼤⼩
setFixedSize(QSize(48, 46));
// QLabel⽤来显⽰图⽚
btnImage = new QLabel(this);
btnImage->setGeometry((48-24)/2, 0, 24, 24);
// QLabel⽤来显⽰按钮⽂本
btnTittle = new QLabel(this);
btnTittle->setGeometry(0, 30, 49, 16);
}
void PageSwitchButton::setImageTextAndButtonType(const QString &imagePath, const QString &text,ButtonType buTy)
{
btnImage->setPixmap(QPixmap(imagePath));
btnTittle->setText(text);
btnTittle->setAlignment(Qt::AlignHCenter);//设置文字水平居中
this->buttonType = buTy;
}
void PageSwitchButton::enterEvent(QEnterEvent *event)
{
setStyleSheet("border-radius : 5px;background-color : #C0C0C0");
}
void PageSwitchButton::leaveEvent(QEvent *event)
{
setStyleSheet("background-color : #FFFFFF");//清空样式表
}
void PageSwitchButton::setFontColor(const QString &color)
{
//设置字体颜色
btnTittle->setStyleSheet("font-style : normal;color :" + color + ";");
}
void PageSwitchButton::setImage(const QString &imagePath)
{
btnImage->setPixmap(QPixmap(imagePath));
}
void PageSwitchButton::mousePressEvent(QMouseEvent *event)
{
(void)event;
//此时界面字体切换为黑色
setFontColor("#000000");
emit btClicked(buttonType);
}
////////////在limePlayer.h中新增public函数////////////////////
//切换页面的函数
void switchPage(ButtonType buttonType);
/////////对应cpp实现////////////////////////
void LimePlayer::switchPage(ButtonType buttonType)
{
//切换-同时切换图片与字体颜色
if(buttonType == HomePageBtn)
{
ui->homePageBtn->setImage(":/images/homePage/shouyexuan.png");
ui->myPageBtn->setImage(":/images/homePage/wode.png");
ui->myPageBtn->setFontColor("#999999");
ui->sysPageBtn->setImage(":/images/homePage/admin.png");
ui->sysPageBtn->setFontColor("#999999");
}
else if(buttonType == MyPageBtn)
{
ui->myPageBtn->setImage(":/images/homePage/wodexuan.png");
ui->homePageBtn->setImage(":/images/homePage/shouye.png");
ui->homePageBtn->setFontColor("#999999");
ui->sysPageBtn->setImage(":/images/homePage/admin.png");
ui->sysPageBtn->setFontColor("#999999");
}
else if(buttonType == SysPageBtn)
{
ui->sysPageBtn->setImage(":/images/homePage/adminxuan.png");
ui->homePageBtn->setImage(":/images/homePage/shouye.png");
ui->homePageBtn->setFontColor("#999999");
ui->myPageBtn->setImage(":/images/homePage/wode.png");
ui->myPageBtn->setFontColor("#999999");
}
else
{
LOG() << "未知侧边栏按钮";
}
ui->stackedWidget->setCurrentIndex(buttonType);
}
注意到上面有一个LOG(),这是我们封装的一个简单的日志,我们新建一个头文件名为util.h,以后我们写的所有的小工具都放到这个头文件中,日志封装代码如下:
////////////////////////util.h//////////////////////////
#ifndef UTIL_H
#define UTIL_H
#include <QString>
#include <QFileInfo>
static inline QString fileName(const QString& filePath)
{
QFileInfo fileInfo(filePath);
return fileInfo.fileName();
}//内敛函数以及static防止函数重定义
// noquote()是QDebug中的一个成员函数,用于制定在输出时不会自动为字符串添加引号
#define LOG() qDebug().noquote()<<QString("[%1:%2]:").arg(fileName(__FILE__),QString::number(__LINE__))
#endif // UTIL_H
五.首页布局
5.1首页外观布局
当点击布⾸⻚按钮后,会切换到⾸⻚界⾯。
仔细观察⾸⻚布局会发现:
① ⾸⻚整体可分成上下两部分组成:选择部分和视频信息显⽰部分。
② 选择搜索区域可分成左右两部分组成:分类和标签区域 和 搜索区域
③ 分类标签区域可分为上下两部分组成:分类区域 和 标签区域
④ 搜索区中的编辑器实际为⼀个⾃定义控件,QT内置的QLineEdit⽆法达到上述效果
所以我们可以新建一个设计师类名为HomePageWidget,然后在homepagewidget.ui中完成⾸⻚布局。 最终效果如下:
搜索框的qss美化代码如下:
#search {
border: 2px solid #cccccc;
border-radius: 18px;
background-color : #FFFFFF;
font-size: 14px;
selection-background-color: #0078d4;
}
#search:focus {
border-color: rgb(170, 255, 255);
outline: none;
}
#search:hover {
border-color: rgb(170, 255, 255);
}
#search::placeholder {
color: rgb(67, 67, 67);
font-style: italic;
}
界面控件嵌套关系如下:
5.2首页逻辑处理
5.2.1标签与分类按钮处理
⽤⼾可以通过点击标签或分类来筛选感兴趣的视频,因此标签和分类都是按钮,并且按钮上⽂本和⽂
本颜⾊可能不同,将来定义⼀个buildSelectBtn函数专⻔⽤来创建按钮。
由于每个分类下显⽰的标签内容是不⼀样的,点击分类后,标签位置显⽰对应该分类的标签,因此分
类和标签之间必须要有对应关系。
封装buildSelectBtn函数单独创建按钮,创建按钮时需要传递按钮的⽂本、颜⾊和⽗元素。
分类和标签创建时,先创建分类和标签按钮,分类和标签按钮⽆需绑定槽函数,具体的分类项和标签
项按钮在创建好之后,需要绑定槽函数。
//////////////////////////homepagewidget.h////////////////////////
#include <QWidget>
#include <QPushButton>
namespace Ui {
class HomePageWidget;
}
class HomePageWidget : public QWidget
{
Q_OBJECT
public:
explicit HomePageWidget(QWidget *parent = nullptr);
//初始化分类栏与标签栏
void initKindsAndTags();
//构建一个按钮对象
QPushButton* buildSelectBtn(QWidget* parent,const QString& color,const QString& text);
//重新设置标签栏
void resetTags(const QList<QString>& kindToTags);
~HomePageWidget();
private:
Ui::HomePageWidget *ui;
QHash<QString,QList<QString>> tags;
};
///////////////////////////homepagewidget.cpp///////////////////////
HomePageWidget::HomePageWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::HomePageWidget)
{
ui->setupUi(this);
initKindsAndTags();
initRefreshAndTop();
initVideos();
}
void HomePageWidget::initKindsAndTags()
{
//添加首行分类标签
QPushButton* classify = buildSelectBtn(ui->classify,"#3ECEFF","分类");
classify->setDisabled(true);
ui->classifyHLayout->addWidget(classify);
QList<QString> kinds = {"动画","游戏","音乐","电影","历史","美食","科技","运动"};
for(auto& kind : kinds)
{
QPushButton* kindBtn = buildSelectBtn(ui->classify,"#222222",kind);
ui->classifyHLayout->addWidget(kindBtn);
//连接点击事件信号槽
connect(kindBtn,&QPushButton::clicked,this,[=](){
onKindBtnClicked(kindBtn);
});
}
//分类栏按钮设置一定间距
ui->classifyHLayout->setSpacing(8);
//标签栏按钮设置一定间距
ui->labelHLayout->setSpacing(4);
//添加分类下的对应标签,初始时默认为第一个分类下标签
tags = {{"动画", {"热血", "治愈", "冒险", "搞笑", "科幻", "致郁", "神作", "日常"}},
{"游戏", {"开放世界", "角色扮演", "射击", "策略", "独立游戏", "多人联机", "魂系", "解谜"}},
{"音乐", {"流行", "摇滚", "电子", "民谣", "说唱", "古典", "爵士", "国风"}},
{"电影", {"科幻", "悬疑", "喜剧", "文艺", "动作", "犯罪", "催泪", "史诗"}},
{"历史", {"上古", "中世纪", "文艺复兴", "工业革命", "世界大战", "文明古国", "帝王将相", "考古"}},
{"美食", {"甜点", "烧烤", "家常菜", "火锅", "烘焙", "小吃", "海鲜", "无辣不欢"}},
{"科技", {"人工智能", "虚拟现实", "智能手机", "电动汽车", "航天", "编程", "区块链", "深度学习"}},
{"运动", {"篮球", "足球", "跑步", "健身", "羽毛球", "滑雪", "电竞", "户外"}}};
resetTags(tags[kinds[0]]);
}
QPushButton* HomePageWidget::buildSelectBtn(QWidget *parent, const QString &color, const QString &text)
{
QPushButton* pushButton = new QPushButton(text,parent);
pushButton->setMinimumHeight(26);
pushButton->setFixedWidth(text.size()*16+18+18);//设置按钮宽度按照字体大小及个数动态变化
pushButton->setStyleSheet("color : " + color + ";");
return pushButton;
}
void HomePageWidget::resetTags(const QList<QString> &kindToTags)
{
QList<QPushButton*> btns = ui->labels->findChildren<QPushButton*>();
//移除并释放原来所有的标签
for(int i = 0;i < btns.size();i++)
{
ui->labelHLayout->removeWidget(btns[i]);
delete btns[i];
}
QPushButton* label = buildSelectBtn(ui->labels,"#3ECEFF","标签");
label->setDisabled(true);
ui->labelHLayout->addWidget(label);
for(auto& tag : kindToTags)
{
QPushButton* tagBtn = buildSelectBtn(ui->labels,"#222222",tag);
ui->labelHLayout->addWidget(tagBtn);
connect(tagBtn,&QPushButton::clicked,this,[=](){
onTagBtnClicked(tagBtn);
});
}
}
HomePageWidget::~HomePageWidget()
{
delete ui;
}
那么当我们点击分类中的按钮时,下面的标签栏会自动切换,当标签栏中的按钮被点击后,视频列表区域的视频会进行切换。因为后者需要服务端的支持才能继续,所以我们这里先实现前者:
////////////////////////////////homepagewidget.h////////////////////////////
//新增私有槽函数
//设置分类与标签按钮被点击时的响应函数
void onKindBtnClicked(QPushButton* kindBtn);
void onTagBtnClicked(QPushButton* tagBtn);
//////////////////////////////homepagewidget.cpp///////////////////////////
void HomePageWidget::onKindBtnClicked(QPushButton *kindBtn)
{
//设置当前按钮的点击颜色,同时移除其他按钮的颜色
kindBtn->setStyleSheet("background-color: #F1FDFF;"
"color:#3ECEFF;");
QList<QPushButton*> btns = ui->classify->findChildren<QPushButton*>();
for(int i = 0;i < btns.size();i++)
{
if(btns[i] != kindBtn && btns[i]->text() != "分类")
{
btns[i]->setStyleSheet("color : #222222;");
}
}
//重置标签栏
resetTags(tags[kindBtn->text()]);
}
void HomePageWidget::onTagBtnClicked(QPushButton *tagBtn)
{
//设置当前按钮的点击颜色,同时移除其他按钮的颜色
tagBtn->setStyleSheet("background-color: #F1FDFF;"
"color:#3ECEFF;");
QList<QPushButton*> btns = ui->labels->findChildren<QPushButton*>();
for(int i = 0;i < btns.size();i++)
{
if(btns[i] != tagBtn && btns[i]->text() != "标签")
{
btns[i]->setStyleSheet("color : #222222;");
}
}
//切换视频页面-后续补充
//.....
}
5.2.2刷新与置顶按钮处理
我们发现在视频播放界面中的左下角有这样两个按钮:
他们的作用相信各位都已心知肚明,我们不再赘述。因为这两个功能也需要服务端提供支持才能实现,所以我们这里暂时搁置,只将按钮和对应槽函数给实现出来:
///////////////////////////homepagewidget.h/////////////////////////
//新增
class HomePageWidget : public QWidget
{
Q_OBJECT
public:
///......
//初始化刷新和置顶按钮
void initRefreshAndTop();
private slots:
//设置置顶和刷新按钮被点击时的响应函数
void onRefreshBtnClicked();
void onTopBtnClicked();
private:
Ui::HomePageWidget *ui;
QHash<QString,QList<QString>> tags;
};
//////////////////////////////homepagewidget.cpp//////////////////////////
void HomePageWidget::initRefreshAndTop()
{
QWidget* widget = new QWidget(this);
widget->setFixedSize(42, 94);
widget->setStyleSheet("QPushButton:hover{background-color:#666666}"
"QPushButton{"
"background-color : #DDDDDD;"
"border-radius : 21px;"
"border : none;}");
QPushButton* refresh = new QPushButton();
refresh->setFixedSize(42, 42);
refresh->setStyleSheet("border-image :url(:/images/homePage/shuaxin.png);");
QPushButton* top = new QPushButton();
top->setFixedSize(42, 42);
top->setStyleSheet("border-image : url(:/images/homePage/zhiding.png)");
QVBoxLayout* layout = new QVBoxLayout(widget);
layout->addWidget(refresh);
layout->addWidget(top);
layout->setContentsMargins(0,0,0,0);
layout->setSpacing(10);
widget->move(1285, 630);
widget->show();
//为两个按钮分别绑定槽函数
connect(refresh,&QPushButton::clicked,this,&HomePageWidget::onRefreshBtnClicked);
connect(top,&QPushButton::clicked,this,&HomePageWidget::onTopBtnClicked);
}
void HomePageWidget::onRefreshBtnClicked()
{
LOG() << "刷新按钮被按下";
}
void HomePageWidget::onTopBtnClicked()
{
LOG() << "置顶按钮被按下";
}
5.2.3视频信息框的实现
VideoBox整体为上下结构,由视频封⾯区、视频标题区、⽤⼾信息区组成;视频封⾯区⼜内嵌:播放
量、点赞和视频时⻓;⽤⼾信息区分为:⽤⼾头像、⽤⼾昵称、视频上传时间、删除按钮组成。所以我们添加⼀个设计师界⾯,命名为VideoBox,在VideoBox.ui界面中进行布局:
控件嵌套关系如下:
然后我们更新16个这样的box到首页:
//////////////////////////////// homepagewidget.h
//////////////////////////////////
//初始化首页视频列表
void initVideos();
//////////////////////////////// homepagewidget.cpp
//////////////////////////////////
void HomePageWidget::initVideos()
{
//测试
for(int i = 0;i < 16;i++)
{
VideoBox* video = new VideoBox();
ui->videoGLayout->addWidget(video,i / 4,i % 4);
}
}
5.2.4视频搜索框的实现
因为我们的搜索框中带有按钮,所以我们这里需要自定义搜索框,因为比较简单,这里我们直接新建一个c++类的头文件与cpp文件即可,自定义实现如下:
///searchlineedit.h
#ifndef SEARCHLINEEDIT_H
#define SEARCHLINEEDIT_H
#include <QLineEdit>
class SearchLineEdit : public QLineEdit
{
Q_OBJECT
public:
explicit SearchLineEdit(QWidget *parent = nullptr);
void searchBtnClicked();//处理搜索事件
signals:
};
#endif // SEARCHLINEEDIT_H
///searchlineedit.cpp
#include "searchlineedit.h"
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>
#include "util.h"
SearchLineEdit::SearchLineEdit(QWidget *parent)
: QLineEdit{parent}
{
// 搜索框图标
QLabel* searchImg = new QLabel(this);
searchImg->setFixedSize(16, 16);
searchImg->setPixmap(QPixmap(":/images/homePage/sousuo.png"));
// 搜索框按钮
QPushButton* searchBtn = new QPushButton("搜索",this);
searchBtn->setCursor(QCursor(Qt::PointingHandCursor));//设置手型光标
searchBtn->setFixedSize(62, 32);
searchBtn->setStyleSheet("background-color : #3ECEFE;"
"border-radius : 16px;"
"font-size : 14px;"
"color : #FFFFFF;"
"font-style : normal;");
this->setTextMargins(33, 0, 0, 0);//设置搜索框中的文字向右靠一些
QHBoxLayout* hLayout = new QHBoxLayout(this);
hLayout->addWidget(searchImg);
hLayout->addStretch();//添加一个水平弹簧把二者撑到两边
hLayout->addWidget(searchBtn);
hLayout->setContentsMargins(11, 0, 2, 0);//左,上,右,下
//绑定回车与点击事件
connect(searchBtn, &QPushButton::clicked, this,
&SearchLineEdit::searchBtnClicked);
connect(this, &QLineEdit::returnPressed, this,
&SearchLineEdit::searchBtnClicked);
}
void SearchLineEdit::searchBtnClicked()
{
LOG() << "搜索按钮被按下";
}
之后在首页对search进行类型提升即可。
到这里就可以看到我们最开始给出的首页样式图片了。
六.视频播放页布局
6.1视频播放页外观布局
当点击VideoBox的视频封⾯或视频标题后,表明要播放该视频,此时需要弹出视频播放窗⼝播放该视
频。此部分布局比较复杂,因为这里我们需要实现最大化效果,所以我们需要通过布局器去控制大部分控件的高度与宽度,所以很少写死某些控件的宽高。实现的时候最好耐心一些,实在比较难可以参考本项目最后一篇文章发布时给出的源码,布局完毕后的界面如下:
界面中控件的嵌套关系如下:
6.2视频播放页的逻辑及相关控件逻辑实现
6.2.1点击首页中的videoBox弹出视频播放界面
当点击⾸⻚中VideoBox的视频封⾯或标题时,应该弹出播放界⾯播放视频,因此VideoBox必须持有
PlayerPage⻚的对对象。在拦截的⿏标在视频封⾯和标题上点击的事件处理中,显⽰播放⻚⾯。
/////////////////////////////////videobox.h///////////////////////////////////
#ifndef VIDEOBOX_H
#define VIDEOBOX_H
#include <QWidget>
#include "playerpage.h"
namespace Ui {
class VideoBox;
}
class VideoBox : public QWidget
{
Q_OBJECT
public:
explicit VideoBox(QWidget *parent = nullptr);
//让videoBox支持鼠标响应事件
bool eventFilter(QObject *watched, QEvent *event) override;
~VideoBox();
private:
Ui::VideoBox *ui;
PlayerPage* videoPlayer;
};
////////////////////////////////videobox.cpp/////////////////////////////////
#include "videobox.h"
#include "ui_videobox.h"
#include "util.h"
VideoBox::VideoBox(QWidget *parent)
: QWidget(parent)
, ui(new Ui::VideoBox)
{
ui->setupUi(this);
//默认隐藏删除按钮
ui->delVideoBtn->hide();
//为imageBox与videoTitleBox安装事件拦截器
ui->imageBox->installEventFilter(this);
ui->videoTitleBox->installEventFilter(this);
//初始化视频播放窗口-默认不显示
videoPlayer = new PlayerPage();
videoPlayer->hide();
}
bool VideoBox::eventFilter(QObject *watched, QEvent *event)
{
if(watched == ui->imageBox || watched == ui->videoTitleBox)
{
//如果是鼠标键按下才播放视频
if(event->type() == QEvent::MouseButtonPress)
{
LOG() << "播放视频-打开播放窗口";
videoPlayer->show();
//事件已处理
return true;
}
}
//其他事件继续向下传递
return QObject::eventFilter(watched,event);
}
VideoBox::~VideoBox()
{
delete ui;
}
6.2.2设置模态对话框&最⼩化&最大化&切换显示/隐藏视频信息和关闭
///////////////////////////////////playerpage.h//////////////////////////////
#ifndef PLAYERPAGE_H
#define PLAYERPAGE_H
#include <QWidget>
#include <QGraphicsOpacityEffect>
#include <QPropertyAnimation>
#include <QTimer>
#include "volume.h"
#include "playspeed.h"
namespace Ui {
class PlayerPage;
}
class PlayerPage : public QWidget
{
Q_OBJECT
public:
explicit PlayerPage(QWidget *parent = nullptr);
void initPlayer();
~PlayerPage();
private slots:
void onMaxBtnClicked();
void on_hideIntroduce_clicked();
private:
Ui::PlayerPage *ui;
bool isMax;
bool isHideIntroduce;
};
#endif // PLAYERPAGE_H
///////////////////////////////playerpage.cpp////////////////////////////////
#include "playerpage.h"
#include "ui_playerpage.h"
PlayerPage::PlayerPage(QWidget *parent)
: QWidget(parent)
, ui(new Ui::PlayerPage),
isMax(false),
isHideIntroduce(false)
{
ui->setupUi(this);
initPlayer();
}
void PlayerPage::initPlayer()
{
//设置标题栏中的文字
setWindowTitle("LimePlayer");
//Qt::CustomizeWindowHint → 开启自定义窗口装饰 Qt::WindowTitleHint → 保留标题栏
setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint);
//将窗口设置为模态
setAttribute(Qt::WA_ShowModal);
//连接槽函数
connect(ui->minBtn,&QPushButton::clicked,this,&QWidget::showMinimized);
connect(ui->maxBtn,&QPushButton::clicked,this,&PlayerPage::onMaxBtnClicked);
connect(ui->quitBtn,&QPushButton::clicked,this,&QWidget::close);
}
PlayerPage::~PlayerPage()
{
delete ui;
}
void PlayerPage::onMaxBtnClicked()
{
if(!isMax)
{
showFullScreen();
}
else
{
showNormal();
}
isMax = !isMax;
}
void PlayerPage::on_hideIntroduce_clicked()
{
if(!isHideIntroduce)
{
ui->Introduce->hide();
ui->hideIntroduce->setText("显");
ui->hideIntroduce->setToolTip("显示视频介绍");
}
else
{
ui->Introduce->show();
ui->hideIntroduce->setText("隐");
ui->hideIntroduce->setToolTip("隐藏视频介绍");
}
isHideIntroduce = !isHideIntroduce;
}
6.2.3设置定时隐藏标题栏和播放控制区域
当我们的鼠标放在播放界面一段时间之后,视频的播放控制区域和标题栏会自动消失,我们这里也来实现一下,当鼠标在screen的Widget内移动时,显示二者,静止超过十秒后二者淡出,如果鼠标放在播放控制区域时,保持二者的显示(因为此时可能需要进行输入弹幕,音量调节或倍速切换等操作):
///////////////////////////////////playerpage.h//////////////////////////////
//新增内容
class PlayerPage : public QWidget
{
Q_OBJECT
public:
void initOpacityAndAnimation();
bool eventFilter(QObject *obj, QEvent *event) override;
private slots:
void onTimerTimeout();
private:
Ui::PlayerPage *ui;
bool isHide;
//head部分的动画效果
QGraphicsOpacityEffect* headOpacity;
QPropertyAnimation* headShowAnim;
QPropertyAnimation* headHideAnim;
//Control部分的动画效果
QGraphicsOpacityEffect* conOpacity;
QPropertyAnimation* conShowAnim;
QPropertyAnimation* conHideAnim;
QTimer* hideTimer; // 控制隐藏的定时器
const int HIDE_DELAY = 10000; // 隐藏延迟时间(毫秒),可调整
};
///////////////////////////////playerpage.cpp////////////////////////////////
PlayerPage::PlayerPage(QWidget *parent)
: QWidget(parent)
, ui(new Ui::PlayerPage),
isHide(false)
{
ui->setupUi(this);
initOpacityAndAnimation();
}
void PlayerPage::initPlayer()
{
//为屏幕区域安装事件过滤器
ui->screen->installEventFilter(this);
}
void PlayerPage::initOpacityAndAnimation()
{
// 设置鼠标跟踪,确保能捕获鼠标移动事件
ui->screen->setMouseTracking(true);
//初始化隐藏定时器
hideTimer = new QTimer(this);
connect(hideTimer,&QTimer::timeout,this,&PlayerPage::onTimerTimeout);
//初始化动画效果
headOpacity = new QGraphicsOpacityEffect(ui->playHead);
headOpacity->setOpacity(1.0); // 初始完全不透明
ui->playHead->setGraphicsEffect(headOpacity); // 将效果应用到控件
headShowAnim = new QPropertyAnimation(headOpacity, "opacity");
headShowAnim->setDuration(1000); // 动画持续1000毫秒(1秒)
headShowAnim->setStartValue(0.0); // 起始值:完全透明
headShowAnim->setEndValue(1.0); // 结束值:完全不透明
headShowAnim->setEasingCurve(QEasingCurve::InOutQuad); // 使用平滑的缓动曲线
//消失时反过来即可
headHideAnim = new QPropertyAnimation(headOpacity, "opacity");
headHideAnim->setDuration(1000);
headHideAnim->setStartValue(1.0);
headHideAnim->setEndValue(0.0);
headHideAnim->setEasingCurve(QEasingCurve::InOutQuad);
//控制区域动画效果与上面相同
conOpacity = new QGraphicsOpacityEffect(ui->playControlBox);
conOpacity->setOpacity(1.0); // 初始完全不透明
ui->playControlBox->setGraphicsEffect(conOpacity); // 将效果应用到控件
conShowAnim = new QPropertyAnimation(conOpacity, "opacity");
conShowAnim->setDuration(1000); // 动画持续1000毫秒(1秒)
conShowAnim->setStartValue(0.0); // 起始值:完全透明
conShowAnim->setEndValue(1.0); // 结束值:完全不透明
conShowAnim->setEasingCurve(QEasingCurve::InOutQuad); // 使用平滑的缓动曲线
//消失时反过来即可
conHideAnim = new QPropertyAnimation(conOpacity, "opacity");
conHideAnim->setDuration(1000);
conHideAnim->setStartValue(1.0);
conHideAnim->setEndValue(0.0);
conHideAnim->setEasingCurve(QEasingCurve::InOutQuad);
}
bool PlayerPage::eventFilter(QObject *obj, QEvent *event)
{
if(obj == ui->screen && event->type() == QEvent::MouseMove)
{
if(!isHide)
{
headShowAnim->start();
conShowAnim->start();
isHide = !isHide;
//设置定时器-重复移动则是重置定时器
hideTimer->start(HIDE_DELAY);
}
}
// 将事件传递给父类处理
return QObject::eventFilter(obj, event);
}
PlayerPage::~PlayerPage()
{
delete ui;
}
void PlayerPage::onTimerTimeout()
{
//配合淡入淡出达到定时隐藏效果-不在播放控制区域时启动隐藏效果
if (!ui->playControlBox->underMouse() && !ui->videoSilder->underMouse()) {
headHideAnim->start();
conHideAnim->start();
isHide = !isHide;
hideTimer->stop();
}
else{
//重置定时器
hideTimer->start();
}
}
6.2.4进度条以及seek功能实现
这个部分在音乐播放器部分我们实现的已经很熟悉了,这里便不再赘述,新建一个设计师类名为PlaySlider,然后在ui界面中进行布局,最终效果如下:
控件嵌套关系如下:
seek功能逻辑实现如下:
///////////////////////////////////playslider.h////////////////////////////////
#ifndef PLAYSLIDER_H
#define PLAYSLIDER_H
#include <QWidget>
namespace Ui {
class PlaySlider;
}
class PlaySlider : public QWidget
{
Q_OBJECT
public:
explicit PlaySlider(QWidget *parent = nullptr);
//实现seek功能
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void moveSlider();
void mousePos(int pos);
~PlaySlider();
private:
Ui::PlaySlider *ui;
int playGrogress;
};
#endif // PLAYSLIDER_H
//////////////////////////////////playslider.cpp///////////////////////////////
#include "playslider.h"
#include "ui_playslider.h"
#include <QResizeEvent>
PlaySlider::PlaySlider(QWidget *parent)
: QWidget(parent)
, ui(new Ui::PlaySlider)
{
ui->setupUi(this);
}
void PlaySlider::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
//防止进度条拖动的范围超出界限
mousePos(event->pos().x());
moveSlider();
return;
}
QWidget::mousePressEvent(event);
}
void PlaySlider::mouseReleaseEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
mousePos(event->pos().x());
moveSlider();
return;
}
QWidget::mousePressEvent(event);
}
void PlaySlider::mouseMoveEvent(QMouseEvent *event)
{
//鼠标不再当前进度条框中
if(!this->rect().contains(event->pos()))
{
return;
}
if(event->buttons() == Qt::LeftButton)
{
mousePos(event->pos().x());
moveSlider();
return;
}
QWidget::mousePressEvent(event);
}
void PlaySlider::moveSlider(){
// 根据playGrogress或⿏标的x坐标位置设置outLine的geometry,以突出播放进度
ui->outLine->setGeometry(ui->outLine->x(), ui->outLine->y(),
playGrogress, ui->outLine->height());
}
void PlaySlider::mousePos(int pos)
{
if(pos < 0)
pos = 0;
if(pos > width())
pos = width();
playGrogress = pos;
}
PlaySlider::~PlaySlider()
{
delete ui;
}
然后在PlayerPage页面提升videoSilder为该类即可有进度条效果。这里有些美中不足的是,当进度条静止不动时,最大化页面后进度条长度跟最大化之前一样没有发生变化。应该通过等比例进行放大的,但是不知道为什么放大时有问题尺寸始终不对,不过因为后面进度条是跟着视频播放进度走的,会以播放进度百分比修改长度,所以这个问题只会出现在暂停播放同时缩小或放大窗口时才会出现,不影响大体体验,这个问题有余力的读者可以自行进行解决下,这里我们不再解决。
6.2.5音量调节按钮的实现
跟之前音乐播放器一样,我们新建一个设计师类名为Volume,然后在其ui界面中进行布局,布局完成之后的最终结果如下:
控件嵌套关系如下:
然后我们为此控件实现拖拽功能:
#ifndef VOLUME_H
#define VOLUME_H
#include <QWidget>
namespace Ui {
class Volume;
}
class Volume : public QWidget
{
Q_OBJECT
public:
explicit Volume(QWidget *parent = nullptr);
//实现拖动音量条的功能
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void moveVolumeSlider();
void mousePos(int pos);
//当鼠标离开调节区域时自动隐藏窗口
void leaveEvent(QEvent *event) override;
~Volume();
private:
Ui::Volume *ui;
int volumeGrogress;
};
#endif // VOLUME_H
////////////////////////////volume.cpp///////////////////////////
#include "volume.h"
#include "ui_volume.h"
#include <QMouseEvent>
Volume::Volume(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Volume)
{
ui->setupUi(this);
// Qt::FramelessWindowHint : 去掉窗⼝边框
// Qt::NoDropShadowWindowHint : 去掉窗⼝阴影
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint |
Qt::NoDropShadowWindowHint);
// 设置窗⼝背景透明,主要需要设置Qt::FramelessWindowHint标志,否则窗⼝背景是⿊⾊的
setAttribute(Qt::WA_TranslucentBackground);
}
void Volume::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
mousePos(event->pos().y());
moveVolumeSlider();
return;
}
//其他事件默认处理
QWidget::mousePressEvent(event);
}
void Volume::mouseReleaseEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
{
mousePos(event->pos().y());
moveVolumeSlider();
return;
}
//其他事件默认处理
QWidget::mousePressEvent(event);
}
void Volume::mouseMoveEvent(QMouseEvent *event)
{
if(!this->rect().contains(event->pos()))
{
//鼠标此时不在调节框范围内
return;
}
if(event->buttons() == Qt::LeftButton)
{
mousePos(event->pos().y());
moveVolumeSlider();
}
//其他事件默认处理
QWidget::mousePressEvent(event);
}
void Volume::moveVolumeSlider()
{
//根据鼠标的y坐标调整按钮位置以及outLine的长度与位置
ui->outLine->setGeometry(ui->outLine->x(),volumeGrogress,ui->outLine->width(),ui->inLine->y() + ui->inLine->height() - volumeGrogress);
ui->volumeBtn->move(ui->volumeBtn->x(),ui->outLine->y() - ui->volumeBtn->height() / 2);
}
void Volume::mousePos(int pos)
{
if(pos < ui->inLine->y())
pos = ui->inLine->y();
if(pos > ui->inLine->y() + ui->inLine->height())
pos = ui->inLine->y() + ui->inLine->height();
volumeGrogress = pos;
}
void Volume::leaveEvent(QEvent *event)
{
hide();
}
Volume::~Volume()
{
delete ui;
}
当然,我们也需要在播放界面添加对应槽函数让其点击之后会显示该弹窗:
/////playerpage.h
private slots:
void onVolumeBtnClicked();//音量调节
private:
Volume* volume;//音量弹窗
/////playerpage.cpp
void PlayerPage::onVolumeBtnClicked()
{
//显示音量调节窗口并设置其位置
QPoint globalPos = ui->volumeBtn->mapToGlobal(QPoint(0,0));
int volumeX = globalPos.x() - (volume->width() - ui->volumeBtn->width()) / 2;
int volumeY = globalPos.y() - volume->height();
volume->move(volumeX,volumeY);
volume->show();
}
6.2.6倍速播放窗口的实现
倍数播放窗⼝和⾳量调节窗⼝类似,都是弹出窗⼝。倍数播放窗⼝实际上内部只包含4个QPushButton的按钮即可,添加⼀个设计师界⾯,命名为PlaySpeed,在该类的ui界面中进行布局,最终效果如上,控件布局如下:
其类中我们给他添加外观属性以及连接相应槽函数:
///playspeed.h
#ifndef PLAYSPEED_H
#define PLAYSPEED_H
#include <QWidget>
namespace Ui {
class PlaySpeed;
}
class PlaySpeed : public QWidget
{
Q_OBJECT
public:
explicit PlaySpeed(QWidget *parent = nullptr);
void leaveEvent(QEvent *event);
~PlaySpeed();
private:
void onPlay05Speed();
void onPlay10Speed();
void onPlay15Speed();
void onPlay20Speed();
private:
Ui::PlaySpeed *ui;
};
#endif // PLAYSPEED_H
///playspeed.cpp
#include "playspeed.h"
#include "ui_playspeed.h"
#include "util.h"
PlaySpeed::PlaySpeed(QWidget *parent)
: QWidget(parent)
, ui(new Ui::PlaySpeed)
{
ui->setupUi(this);
// Qt::FramelessWindowHint : 去掉窗⼝边框
// Qt::NoDropShadowWindowHint : 去掉窗⼝阴影
setWindowFlags(Qt::Popup | Qt::FramelessWindowHint |
Qt::NoDropShadowWindowHint);
// 设置窗⼝背景透明,主要需要设置Qt::FramelessWindowHint标志,否则窗⼝背景是⿊⾊的
setAttribute(Qt::WA_TranslucentBackground);
connect(ui->speed05, &QPushButton::clicked, this,
&PlaySpeed::onPlay05Speed);
connect(ui->speed10, &QPushButton::clicked, this,
&PlaySpeed::onPlay10Speed);
connect(ui->speed15, &QPushButton::clicked, this,
&PlaySpeed::onPlay15Speed);
connect(ui->speed20, &QPushButton::clicked, this,
&PlaySpeed::onPlay20Speed);
}
void PlaySpeed::leaveEvent(QEvent *event)
{
hide();
}
PlaySpeed::~PlaySpeed()
{
delete ui;
}
void PlaySpeed::onPlay05Speed()
{
LOG()<<"0.5倍速播放";
}
void PlaySpeed::onPlay10Speed()
{
LOG()<<"1.0倍速播放";
}
void PlaySpeed::onPlay15Speed()
{
LOG()<<"1.5倍速播放";
}
void PlaySpeed::onPlay20Speed()
{
LOG()<<"2.0倍速播放";
}
当然我们也需要在播放界面对倍速切换按钮进行点击时能够弹出该自定义的控件:
/////playerpage.h
private slots:
void onSpeedBtnClicked();//倍速播放
private:
PlaySpeed* playSpeed;//倍速播放弹窗
/////playerpage.cpp
void PlayerPage::onSpeedBtnClicked()
{
//显示倍速调节窗口并设置其位置
QPoint globalPos = ui->speedBtn->mapToGlobal(QPoint(0,0));
int speedX = globalPos.x() - (playSpeed->width() - ui->speedBtn->width()) / 2;
int speedY = globalPos.y() - playSpeed->height();
playSpeed->move(speedX,speedY);
playSpeed->show();
}
6.2.7弹幕编辑框实现
Qt内置的QLineEdit⽆法达到预期效果,因此弹幕编辑框也需要⾃定义。弹幕编辑框的布局也⽐较简
单,实际就是在编辑框右侧放置⼀个按钮,将编辑框、按钮以及⽂本的样式设置好即可。
添加⼀个.h和.cpp⽂件,类命名为BarrageEdit,在该类中完成弹幕编辑框的实现:
///barrageedit.h
#ifndef BARRAGEEDIT_H
#define BARRAGEEDIT_H
#include <QLineEdit>
#include <QPushButton>
class BarrageEdit : public QLineEdit
{
Q_OBJECT
public:
explicit BarrageEdit(QWidget *parent = nullptr);
private:
QPushButton* sendBSBtn;
};
#endif // BARRAGEEDIT_H
///barrageedit.cpp
#include "barrageedit.h"
#include <QHBoxLayout>
BarrageEdit::BarrageEdit(QWidget *parent)
: QLineEdit{parent}
{
this->setFixedHeight(32);
this->setPlaceholderText("新的风暴已经出现,你的弹幕为何迟迟还未出现...");
this->setMaxLength(30); // 最多输⼊30个字符
this->setTextMargins(12, 6, 0, 7); // 左 上 右 下
QHBoxLayout* layout = new QHBoxLayout(this);
layout->addStretch();//添加一个水平弹簧
sendBSBtn = new QPushButton();
sendBSBtn->setFixedSize(58, 28);
sendBSBtn->setText("发送");
sendBSBtn->setCursor(QCursor(Qt::PointingHandCursor));
sendBSBtn->setStyleSheet("background-color : #3ECEFE;"
"border-radius : 4px;"
"color : #FFFFFF;"
"font-size : 14px;");
layout->addWidget(sendBSBtn);
layout->setContentsMargins(0,2,2,2);
}
接下来在playerpage页面将弹幕编辑框提升为上面的类即可有下面的效果: