在上一篇博客中,使用C++和Qt框架完成了Wallpaper的雏形,现于之前的基础上,完善更多功能。
之前已经完成了打开文件,播放暂停,设置父窗体的功能,本文将不再详细说明。
最终效果(视频来自“大污师”的鲸落)
这个头文件里专门放宏定义内容,便于修改
#ifndef SUPPORT_H
#define SUPPORT_H
#define PLAYER_FILTER "视频(*.mp4 *.avi *.wmv);;图片(*.jpg *.png *.bmp);;所有文件(*.*)"
#define BUTTON_TEXT_PLAY "播放"
#define BUTTON_TEXT_PAUSE "暂停"
#define HEKY_AUTORUN "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"
#define OPEN_BROWSER_FILE_PATH "OpenBrowser.exe"
#define OPEN_REFRESHBAKGROUND_FILE_PATH "ReFreshBackground.exe"
#define VOLUME_HIGH "image: url(:/Image/volume-high.png);"
#define VOLUME_LOW "image: url(:/Image/volume-low.png);"
#define VOLUME_OFF "image: url(:/Image/volume-off.png);"
#define SHARED_MEMORY_KEY "wallpaper_by_dearxuan"
#endif // SUPPORT_H
显然每次开机都要手动打开软件非常麻烦,如果能让软件开机自启,就会方便很多
实现开机自启需要读写注册表
HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run
左边是是键名,右边是键值,键值指向文件地址,系统会在开机后执行对应文件,只要向注册表写入自己程序的地址,就可以实现开机自启。当关闭开机自启时,只要删除这一项就可以
//设置开机自启,flag表示是否开机自启
void Widget::AutoRun(bool flag) {
QString appName = QApplication::applicationName();
QString appPath = QApplication::applicationFilePath();
appPath = appPath.replace("/", "\\");
//打开注册表
QSettings *reg = new QSettings(
HEKY_AUTORUN,//宏定义,自启动的注册表位置,详细内容在头文件support.h里
QSettings::NativeFormat);
QString val = reg->value(appName).toString();//根据appName获取键值
if (flag) {//打开开机自启
if (val != appPath) {//键值不是本程序地址或者不存在,则写入键值
reg->setValue(appName, appPath);
}
QString v = reg->value(appName).toString();//读取刚刚写入的键值
if (v != appPath) {//写入失败
ui->checkBox_1->setChecked(false);
QMessageBox::warning(this, "拒绝访问", "添加注册表失败.", QMessageBox::Ok, QMessageBox::Ok);
}
} else {//关闭开机自启
reg->remove(appName);//移除键值
QString v = reg->value(appName).toString();
if (v != "") {//键值仍然存在,移除失败
ui->checkBox_1->setChecked(true);
QMessageBox::warning(this, "拒绝访问", "移除注册表失败.", QMessageBox::Ok, QMessageBox::Ok);
}
}
reg->deleteLater();//延迟删除
}
同时启动软件时还要判断是否打开了开机自启,如果打开了,则自动把checkbox1设置成勾选状态
//判断是否打开开机自启
bool isAutoRun() {
QString appName = QApplication::applicationName();
QString appPath = QApplication::applicationFilePath();
appPath = appPath.replace("/", "\\");
QSettings *reg = new QSettings(
HEKY_AUTORUN,//宏定义,在头文件support.h里面
QSettings::Registry64Format);
//如果注册表键值就是本程序的地址,表示打开了开机自启
QString val = reg->value(appName).toString();
return val == appPath;
}
当点击“叉叉”时,关闭窗口,但是不退出程序,用户通过点击任务栏角标再次打开窗口
在Widget.h里定义QSystemTrayIcon
public:
Widget(QWidget *parent = nullptr);
~Widget();
About about;
QVideoWidget videoWidget;
QMediaPlayer player;
QSystemTrayIcon tray;//任务栏托盘角标
QMediaPlaylist *list;
在程序启动时添加菜单,绑定事件,并显示
tray.setToolTip("Wallpaper");//任务栏托盘小图标的标题
tray.setIcon(QIcon(":Icon/wallpaper.ico"));//任务栏托盘小图标
QMenu *menu = new QMenu();
QAction *action_showDialog = new QAction("打开主窗口");
QAction *action_exit = new QAction("退出");
menu->addAction(action_showDialog);//添加菜单
menu->addAction(action_exit);//添加菜单
tray.setContextMenu(menu);//绑定菜单
//绑定托盘小图标点击事件
connect(&tray, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(OnSystemTrayClicked(QSystemTrayIcon::ActivationReason)));
//绑定菜单点击事件
connect(action_showDialog, SIGNAL(triggered(bool)), this, SLOT(onShowDialogClick()));
connect(action_exit, SIGNAL(triggered(bool)), this, SLOT(on_exitButton_clicked()));
tray.show();//显示小图标
点击“打开主窗口”时显示窗口
//点击了任务栏托盘小图标
void Widget::OnSystemTrayClicked(QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger) {
this->showNormal();//如果点击了任务栏托盘小图标,则显示本窗口
}
}
点击“退出”时相当于点击主窗口里面的“退出”按钮,所以直接绑定到“退出”按钮的点击事件
//退出按钮点击事件,同时任务栏小图标的”退出“菜单也会执行这个函数
void Widget::on_exitButton_clicked() {
if (!firstPlay) {//还没有打开视频,不需要刷新背景
ReFreshBackground();
}
this->hide();//隐藏窗口
WriteSetting();//写入设置
qApp->exit(0);//退出
}
在程序退出时,自动保存设置,包括:是否自动播放上一次视频,当前音量,当前视频文件地址。
用txt来保存虽然写入很方便,但是读取时候需要自己转换,非常痛苦,而且txt能被人轻易编辑,造成程序崩溃,所以采用二进制方法,防止用户随意编辑。设置将会被保存到当前路径下的setting文件(没有后缀)。
//读取设置
void Widget::ReadSetting() {
QFile file("setting");//设置文件,不需要后缀
bool n = false;//是否加载上一次视频
int v = 100;//音量
if (file.open(QIODevice::ReadOnly)) {//文件存在
QDataStream input(&file);
input >> n >> v >> path;//读取
}
file.close();
ui->checkBox_2->setChecked(n);
ui->volumeSlider->setValue(v);
if(!n) path = "";
}
//写入设置
void Widget::WriteSetting() {
QFile file("setting");
if (file.open(QIODevice::WriteOnly)) {
QDataStream output(&file);
output << ui->checkBox_2->isChecked() << ui->volumeSlider->value() << path;
}
file.close();
}
在程序打开时现执行ReadSetting()来读取设置,并自动设置音量,路径等。
添加volumeSlider滑块来控制音量,将范围设置成0~1000,这样可以减小滑动时的卡顿感,但是在处理数据时要把数值除以10,并在用户释放鼠标时自动修正滑块位置,比如把998修正为1000
//音量滑块值改变事件
void Widget::on_volumeSlider_valueChanged(int value) {
//音量滑块的范围为0~1000,音量有效值是0~100,设置1000是为了减小卡顿感觉
value = (value + 5) / 10;//四舍五入除以10
ui->volumeLabel->setText(QString::number(value));
if (value > 50 && VolumeNowPic != 2) {//高音量
VolumeNowPic = 2;
ui->volumePic->setStyleSheet(VOLUME_HIGH);
} else if (value > 0 && value < 50 && VolumeNowPic != 1) {//低音量
ui->volumePic->setStyleSheet(VOLUME_LOW);
VolumeNowPic = 1;
} else if (value == 0 && VolumeNowPic != 0) {//关闭音量
ui->volumePic->setStyleSheet(VOLUME_OFF);
VolumeNowPic = 0;
}
player.setVolume(value);//设置音量
}
//音量滑块鼠标释放事件
void Widget::on_volumeSlider_sliderReleased() {
//当滑块位置在999时,此时音量是100,当用户释放鼠标后自动把滑块位置设置成1000
ui->volumeSlider->setValue(player.volume() * 10);
}
当用户点击音量图标时,用户希望能够立即静音,但是又不能改变滑块位置,如果用户再次点击或者移动滑块,则关闭静音,类似windows的音量设置那样。
上面的代码里使用了VolumeNowPic这个变量,这个是用来保存当前图标的,0,1,2分别表示关,低音量,高音量。引入这个变量是用来判断当前图标和修改后的图标,如果一致就放弃修改。
音量图标使用QLabel来放置
由于QLabel不支持点击事件,所以我们要自己添加。
ui->volumePic->installEventFilter(this);//注册音量Label的点击事件
Widget.h里写上
在cpp里写上
//事件监听
bool Widget::eventFilter(QObject *obj, QEvent *event) {
//文件Label被点击
if (obj == ui->fileLabel && event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouse = static_cast(event);
if (mouse->button() == Qt::LeftButton && path != "") {
//如果是左键,则打开文件位置
QProcess process;
QString p = path.replace("/", "\\");
QString cmd = "explorer /select," + p;
process.startDetached(cmd);
}
}
//音量Label被点击
if (obj == ui->volumePic && event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouse = static_cast(event);
if (mouse->button() == Qt::LeftButton) {
if (player.volume() == 0) {
//音量是0,恢复volumeSlider上的正常音量
on_volumeSlider_valueChanged(ui->volumeSlider->value());
} else {
//音量不是0,静音
player.setVolume(0);
ui->volumePic->setStyleSheet(VOLUME_OFF);
VolumeNowPic = 0;
}
}
}
return false;
}
上面那个是点击文件QLabel打开文件位置的函数,第二个if语句才是重点,先判断音量是否为0,如果是0,则执行一次value_changed事件,这样就会自动根据滑块位置设置音量大小,如果滑块就在0的位置上,那么执行这个函数也什么都不会发生。如果音量不是0,则把音量改成0,同时修改VolumeNowPic为0,表示当前显示的是静音图片。代码里的VOLUME_OFF是我自己定义的,在头文件support.h里面
与音量滑块同理,速度滑动栏的范围是50~1000,最终结果除以50,因为速度不能为0,所以实际值应该在0.1~2.0。当切换速度时,视频会明显黑屏以下,因此切换次数越少越好,当用户滑动滑块时,会不断触发value_changed,所以首先想到把切换速度写在释放鼠标的函数里,但是用户使用滚轮时,不会触发鼠标事件,所以还是需要在value_changed里写。我们可以设置一个布尔变量,来指示value_changed是否可用,当用户按下鼠标时,把这个值设置成false,这样就不会切换速度,如果用户使用滚轮或者点击滑动栏,仍然会执行切换速度,但是切换次数远少于移动鼠标时的value_changed次数,这是可以接受的。
bool sliderEnable = true;//速度滑块的value_changed监听是否可用
//播放速度滑块鼠标释放事件
void Widget::on_rateSlider_sliderReleased() {
int rate = (ui->rateSlider->value() + 25) / 50;//24舍25入除以50,此时rate的范围是1~20
ui->rateSlider->setValue(rate * 50);//乘上50,校准滑块位置
sliderEnable = true;//value监听可用
on_rateSlider_valueChanged(ui->rateSlider->value());//执行一次value监听事件,修改player的播放速度
}
//播放速度滑块value改变事件
void Widget::on_rateSlider_valueChanged(int value) {
//范围是50~1000,对应速度0.1~2.0,默认为1.0,设置1000是为了减小卡顿感
int rate = (value + 25) / 50;//24舍25入除以50,此时rate的范围是1~20
ui->rateLabel->setText(QString::number(rate / 10.0, 'f', 1));//显示数值的Label,把rate除以10并取小数点后一位
if (sliderEnable) {//如果value监听可用
//在修改播放速度时会有明显黑屏,为了防止过多黑屏影响视觉感受,当用户滑动滑块时不改变速度,当用户释放鼠标时再改变
//当用户使用滚轮,或者点击滑动栏,触发stepadd或stepsub时,需要改变播放速度
player.setPlaybackRate(rate / 10.0);//设置播放速度
}
}
//速度滑块点击事件
void Widget::on_rateSlider_sliderPressed() {
sliderEnable = false;//value监听不可用,防止过多黑屏,当用户使用滚轮时不触发本事件,因此value监听有效
}
在之前已经实现了加载视频的功能,现在原有基础上进行扩展。
之前直接把整个过程写到了按钮事件里,是因为当时还没有自动加载视频功能,必须通过按钮打开视频,而现在程序支持自动打开上一次视频,这就要求我们把加载过程单独写成一个函数。
//加载视频文件
void Widget::LoadFile(const QString file) {
QFile video(file);
if (!video.exists()) {
//文件不存在
ui->fileLabel->setText("未打开文件");
ui->playButton->setText(BUTTON_TEXT_PLAY);
ui->playButton->setEnabled(false);
path = "";
ui->fileLabel->setToolTip(NULL);
ui->fileLabel->setCursor(Qt::ArrowCursor);//默认鼠标样式
return;
}
QFileInfo fi = QFileInfo(path);//文件信息
this->ui->fileLabel->setText("正在加载:" + fi.fileName());//文件Label显示文件名
path = file;
if (firstPlay) {//是否是第一次播放
firstPlay = false;
HWND hwnd = (HWND) videoWidget.winId();//获取视频窗体的句柄
SetBackground(hwnd);//把视频窗体设置成背景窗体的子窗体
videoWidget.setWindowFlags(Qt::FramelessWindowHint);
videoWidget.setAspectRatioMode(Qt::IgnoreAspectRatio);
videoWidget.showFullScreen();
list = new QMediaPlaylist;
}
//清理原有的播放列表,加入视频
list->clear();
list->addMedia(QMediaContent(QUrl::fromLocalFile(file)));
list->setPlaybackMode(QMediaPlaylist::CurrentItemInLoop);//循环播放
player.setPlaylist(list);
player.setVideoOutput(&videoWidget);//设置输出窗体
player.play();
ui->playButton->setText(BUTTON_TEXT_PAUSE);
ui->playButton->setEnabled(true);
ui->fileLabel->setText("正在播放:" + fi.fileName());
ui->fileLabel->setToolTip(path.replace("/", "\\"));
ui->fileLabel->setCursor(Qt::PointingHandCursor);//“指向”鼠标样式
}
在按钮事件里调用该函数
//点击了“打开”按钮,打开文件
void Widget::on_openButton_clicked() {
QString file = QFileDialog::getOpenFileName(
this,
"打开文件",
path,//初始地址
PLAYER_FILTER);//过滤器,仅显示支持的格式
if (!file.isEmpty()) {
LoadFile(file);
}
}
“关于”窗体里面添加作者信息,并增加“刷新壁纸”和“清理注册表”功能
当程序异常退出,会导致背景显示异常,点击“刷新壁纸”可以让背景正常显示。
卸载软件之前,可以取消开机自启,或者点击“清理注册表”,来删除启动项。
EXE文件:https://dearx.lanzoui.com/iLUQGs80y2h
源代码:https://dearx.lanzoui.com/iw8x8s80u5g