QT 实现电子相册(一)--目录树和向导

简介

基于前面介绍的QT知识,做一个电子相册,总结前文介绍的各类知识,将用到QListWidget,QTreeWidget,双缓冲绘图,信号槽,动画效果,绘图事件,鼠标事件,qss等知识,算是对之前知识的一个总结。

效果如下https://cdn.llfc.club/lv_0_20230118144805.gif

MainWindow设计

1 MainWindow.ui的centralWidget中添加水平布局horizontalLayout,在该布局中添加两个垂直布局proLayout和picLayout。
horizontalLayout设置layoutStretch比例为1比4, 同时为MainWindow.ui添加manubar,效果是这个样子的
https://cdn.llfc.club/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87_20230118162036.png
2 在MainWindow的构造函数中添加菜单项,并为菜单项设置信号连接,截取部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//创建菜单栏
QMenu * menu_file = menuBar()->addMenu(tr("文件(&F)"));
//创建项目动作
QAction * act_create_pro = new QAction(QIcon(":/icon/createpro.png"), tr("创建项目"),this);
act_create_pro->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_N));
menu_file->addAction(act_create_pro);

//打开项目动作
QAction * act_open_pro = new QAction(QIcon(":/icon/openpro.png"), tr("打开项目"),this);
act_open_pro->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_O));
menu_file->addAction(act_open_pro);

//创建设置菜单
QMenu * menu_set = menuBar()->addMenu(tr("设置(&S)"));
//设置背景音乐
QAction * act_music = new QAction(QIcon(":/icon/music.png"), tr("背景音乐"),this);
act_music->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_M));
menu_set->addAction(act_music);

//连接创建项目槽函数
connect(act_create_pro, &QAction::triggered, this, &MainWindow::SlotCreatePro);
//连接打开项目的槽函数
connect(act_open_pro, &QAction::triggered, this, &MainWindow::SlotOpenPro);

3 main函数设置mainwindow最大显示

1
2
w.setWindowTitle("Album");
w.showMaximized();

4 为MainWindow和菜单栏设置qss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*mainwindow 样式*/
MainWindow {
/* 背景色 */
background-color:rgb(46,47,48);
}

/*菜单栏基本样式*/
QMenuBar{
color:rgb(231,231,231);
background-color:rgb(46,47,48);
}

/*菜单基本样式*/
QMenu{
color:rgb(231,231,231);
background-color:rgb(55,55,55);
}


/* 菜单栏选中条目时 */

QMenuBar::item:selected {
background-color:rgb(80,80,80);
}

/*菜单选中条目*/
QMenu::item:selected {
background-color:rgb(39,96,154);
}

向导类Wizard

1 添加设计师界面类Wizard
Wizard类用来响应创建项目菜单被点击后弹出向导框,其继承于QWizard。
2 添加两个向导页面类ConfirmPage和ProSetPage类,基类选择QWizardPage类,并在Wizard.ui里添加两个wizardpage,将这两个wizardpage升级为ProSetPage和ConfirmPage。ProSetPage类用来设置创建项目的属性,我们先点击其ui文件为其添加网格布局gridLayout,然后将ProSetPage设置为网格布局,设置gridLayout的margin为5,在gridLayout中添加控件,形成如下布局
https://cdn.llfc.club/1674091337745.jpg
3 将两个lineEdit注册为wizard的field,保证两个lineEdit是空的时候无法点击下一步,将QLineEdit的textEdited信号和ProSetPage的completeChanged信号连接起来,这样在lineEdit编辑的时候就会发送textEdited信号,进而触发ProSetPage发送completeChanged信号。
setClearButtonEnabled设置为true可以在lineEdit输入数据后显示清除按钮,直接清除已录入的字符。
completeChanged信号是从proSetPage的基类QWizardPage类继承而来的。completeChanged信号发出后会触发QWizardPage类的isComplete函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ProSetPage::ProSetPage(QWidget *parent) :
QWizardPage(parent),
ui(new Ui::ProSetPage)
{
ui->setupUi(this);
registerField("proPath", ui->lineEdit_2);
registerField("proName*", ui->lineEdit);

connect(ui->lineEdit, &QLineEdit::textEdited, this, &ProSetPage::completeChanged);
connect(ui->lineEdit_2, &QLineEdit::textEdited, this, &ProSetPage::completeChanged);
QString curPath = QDir::currentPath();
ui->lineEdit_2->setText(curPath);
ui->lineEdit_2->setCursorPosition( ui->lineEdit_2->text().size());
ui->lineEdit->setClearButtonEnabled(true);
ui->lineEdit_2->setClearButtonEnabled(true);
}

为了实现特定的判断,我们重写isComplete函数。这样我们就能判断文件夹是否合理以及是否已经有项目路径了。
可以根据不满足的条件设置tips提示用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool ProSetPage::isComplete() const
{
if(ui->lineEdit->text() == "" || ui->lineEdit_2->text() == ""){
return false;
}

//判断是否文件夹是否合理
QDir dir(ui->lineEdit_2->text());
if(!dir.exists())
{
//qDebug()<<"file path is not exists" << endl;
ui->tips->setText("project path is not exists");
return false;
}

//判断路径是否存在
QString absFilePath = dir.absoluteFilePath(ui->lineEdit->text());
// qDebug() << "absFilePath is " << absFilePath;

QDir dist_dir(absFilePath);
if(dist_dir.exists()){
ui->tips->setText("project has exists, change path or name!");
return false;
}

ui->tips->setText("");
return QWizardPage::isComplete();
}

4 为浏览按钮添加点击后选择文件夹操作,在prosetpage.ui文件里右键点击browse按钮,选择转到槽,QT会为我们生成槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//添加浏览按钮点击后选择文件夹的操作
void ProSetPage::on_pushButton_clicked()
{
QFileDialog file_dialog;
file_dialog.setFileMode(QFileDialog::Directory);
file_dialog.setWindowTitle("选择导入的文件夹");
auto path = QDir::currentPath();
file_dialog.setDirectory(path);
file_dialog.setViewMode(QFileDialog::Detail);

QStringList fileNames;
if (file_dialog.exec()){
fileNames = file_dialog.selectedFiles();
}

if(fileNames.length() <= 0){
return;
}

QString import_path = fileNames.at(0);
qDebug() << "import_path is " << import_path << endl;
ui->lineEdit_2->setText(import_path);
}

5 在ProSetPage页面点击下一步会跳转到下一页。ConfirmPage没什么代码,在ui文件里添加提示即可。在完成时我们可以重写QWidzard的done函数。
将页面设置的项目名称和路径传递给ProTree类,ProTree类用来在MainWindow左侧显示树形目录,这个之后介绍。

1
2
3
4
5
6
7
8
9
10
11
void Wizard::done(int result)
{
if(result == QDialog::Rejected){
return QWizard::done(result);
}

QString name, path;
ui->wizardPage1->GetProSettings(name, path);
emit SigProSettings(name, path);
QWizard::done(result);
}

项目目录树ProTree类

1 创建Qt设计师界面类,名字为ProTree,基类选择QDialog,ProTree中添加一个垂直布局,布局内添加一个QLabel和一个QTreeWidget,最后将ProTree设置为垂直布局。
https://cdn.llfc.club/1674871455133.jpg
2 考虑到QTreeWidget功能有限,我们需要继承QTreeWidget重新实现一个新的类ProTreeWidget,所以在项目中新增C++类ProTreeWidget继承自QTreeWidget。
在构造函数中隐藏头部,并且注册要传递信息的类型

1
2
3
qRegisterMetaType<QVector<int> >("QVector<int>");
//隐藏表头
this->header()->hide();

同时将ProTree布局中的QTreeWidget提升为ProTreeWidget
3 同样的道理为了便于操作定义ProTreeItem继承QTreeWidgetItem,相关的成员变量和函数省略,这里简单介绍下构造函数

1
2
3
4
5
6
ProTreeItem::ProTreeItem(QTreeWidget *view, const QString &name,
const QString &path, int type):QTreeWidgetItem (view, type),
_path(path),_name(name),_root(this),_pre_item(nullptr),_next_item(nullptr)
{

}

view和type传递给基类,其他参数_path表示项目路径,_name表示项目名称,_root表示根节点,_pre_item表示前一个节点,_next_item表示后一个节点。
还有第二个重载版本的构造函数,可以通过根节点构造新的item节点

1
2
3
4
5
6
7
ProTreeItem::ProTreeItem(QTreeWidgetItem *parent, const QString &name,
const QString &path, QTreeWidgetItem* root,int type):QTreeWidgetItem(parent,type),
_path(path),_name(name),_root(root),_pre_item(nullptr),_next_item(nullptr)

{

}

4 ProTreeWidget添加槽函数AddProToTree
AddProToTree函数里判断路径和名字是否准确,然后创建一个item插入到treewidget里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void ProTreeWidget::AddProToTree(const QString &name, const QString &path)
{
qDebug() << "ProTreeWidget::AddProToTree name is " << name << " path is " << path << endl;
QDir dir(path);
QString file_path = dir.absoluteFilePath(name);
//检测重名,判断路径和名字都一样则拒绝加入
if(_set_path.find(file_path) != _set_path.end()){
qDebug() << "file has loaded" << endl;
return;
}
//构造项目用的文件夹
QDir pro_dir(file_path);
//如果文件夹不存在则创建
if(!pro_dir.exists()){
bool enable = pro_dir.mkpath(file_path);
if(!enable){
qDebug() << "pro_dir make path failed" << endl;
return;
}
}

_set_path.insert(file_path);
auto * item = new ProTreeItem(this, name, file_path, TreeItemPro);
item->setData(0,Qt::DisplayRole, name);
item->setData(0,Qt::DecorationRole, QIcon(":/icon/dir.png"));
item->setData(0,Qt::ToolTipRole, file_path);
}

5 在MainWindow中串联创建项目逻辑
因为在MainWindow的构造函数中已经添加了SlotCreatePro和信号的连接

1
2
//连接创建项目槽函数
connect(act_create_pro, &QAction::triggered, this, &MainWindow::SlotCreatePro);

所以这里实现点击创建项目后设置向导的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
void MainWindow::SlotCreatePro(bool){
qDebug() << "slot create pro triggered" << endl;
Wizard wizard(this);
wizard.setWindowTitle(tr("创建项目"));
auto *page = wizard.page(0);
page->setTitle(tr("设置项目配置"));
//连接信号和槽
connect(&wizard, &Wizard::SigProSettings, dynamic_cast<ProTree*>(_protree),&ProTree::AddProToTree);

wizard.show();
wizard.exec();
disconnect(&wizard);
}

我们在qss中设置ProTree样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ProTree {
border-color: #9F9F9F;
border-style: solid;
border-width: 1px 1px 1px 1px;
padding-right: 10px;
}

QLabel#label_pro {
color: rgb(231,231,231);
border-color: #9F9F9F;
border-style: dotted;
border-width: 0 0 1px 0;
padding-bottom: 10px;
margin-bottom: 5px;
}

这样在wizard点击完成时触发done函数,进而发送信号触发ProTree的AddProToTree函数了,从而生成一个项目目录的item。
效果如下
https://cdn.llfc.club/1674874650492.jpg

文件夹导入功能

我们要在生成的ProTreeWidget的项目root item中点击右键,弹出菜单,然后选择导入文件夹,将文件夹中的目录和文件递归的导入我们创建的项目目录,并且在root下生成item节点。
1 ProTreeWidget构造函数添加信号和槽函数连接,并且创建导入文件的动作,并为该动作连接槽函数。

1
2
3
connect(this, &ProTreeWidget::itemPressed, this, &ProTreeWidget::SlotItemPressed);
_action_import = new QAction(QIcon(":/icon/import.png"),tr("导入文件"), this);
connect(_action_import, &QAction::triggered, this, &ProTreeWidget::SlotImport);

itemPressed信号是从QTreeWidget基类继承而来的,在QTreeWidget中的item被点击时发出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ProTreeWidget::SlotItemPressed(QTreeWidgetItem *pressedItem, int column)
{
qDebug() << "ProTreeWidget::SlotItemPressed" << endl;
if(QGuiApplication::mouseButtons() == Qt::RightButton) //判断是否为右键
{
QMenu menu(this);
qDebug() << "menu addr is " << &menu << endl;
int itemtype = (int)(pressedItem->type());
if (itemtype == TreeItemPro)
{
_right_btn_item = pressedItem;
menu.addAction(_action_import);
menu.exec(QCursor::pos()); //菜单弹出位置为鼠标点击位置
}
}
}

TreeItemPro是我们在const.h中定义的类型,在SlotItemPressed函数中判断是否为右键点击,如果是再根据item的类型判断是root节点,则在菜单中添加动作。
接下来点击导入文件动作之后执行SlotImport函数。
因为导入操作是一个耗时的操作,所以要放到单独的线程中执行,主线程启动一个进度对话框显示导入进度,同时可以控制导入的中止操作等。
在导入时弹出一个文件选择对话框,设置默认路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void ProTreeWidget::SlotImport()
{
QFileDialog file_dialog;
file_dialog.setFileMode(QFileDialog::Directory);
file_dialog.setWindowTitle("选择导入的文件夹");
QString path = "";
if(!_right_btn_item){
qDebug() << "_right_btn_item is empty" << endl;
path = QDir::currentPath();
return ;
}

path = dynamic_cast<ProTreeItem*>(_right_btn_item)->GetPath();

file_dialog.setDirectory(path);
file_dialog.setViewMode(QFileDialog::Detail);

QStringList fileNames;
if (file_dialog.exec()){
fileNames = file_dialog.selectedFiles();
}

if(fileNames.length() <= 0){
return;
}

QString import_path = fileNames.at(0);
// qDebug() << "import_path is " << import_path << endl;
}

文件选择对话框选择要导入的文件夹,返回路径,我们根据这个路径做copy操作,将文件夹内的文件和文件夹都copy到之前设置的项目路径里。
这是个耗时的操作,那我们重新实现一个线程继承自QThread类,简单看一下构造函数
2 自定义线程完成文件复制和树目录创建

1
2
3
4
5
6
7
8
ProTreeThread::ProTreeThread(const QString &src_path, const QString &dist_path,
QTreeWidgetItem *parent_item, int &file_count,
QTreeWidget *self, QTreeWidgetItem *root, QObject *parent)
:QThread (parent),_src_path(src_path),_dist_path(dist_path),
_file_count(file_count),_parent_item(parent_item),_self(self),_root(root),_bstop(false)
{

}

parent传递给父类构造函数,_src_path表示打开的文件夹路径,_dist_path表示我们创建的项目路径,_file_count表示文件数,用来和进度框交互,_parent_item新创建节点的父节点,_self表示QProTreeWidget对象,_root表示新创建节点隶属于哪个根节点,便于后期做交互。_bstop表示是否停止,如果为true则线程终止。
3 实现copy文件功能和目录树创建

(1) 根据文件类型(文件夹还是文件)执行不同的逻辑,如果是文件则创建item添加到父节点下。如果是文件夹类型,则递归进入创建逻辑,直到所有的文件和文件夹被遍历完成。
(2) 如果_bstop被设置为true,则退出创建逻辑。
(3) 统计文件数,发信号SigUpdateProgress通知进度框更新进度

浅谈一下_bstop的设计逻辑。因为QTread类提供了terminate和quit函数,这些只能从机制上保证线程退出,并不能保证逻辑的准确性,所以我并没有采用这个机制,而是通过_bstop的方式从逻辑上控制退出。
至于为什么有多处判断,因为创建逻辑是递归方式,为了保证退出的效率所以在多处判断,不加锁也是为了提高程序运行的效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
void ProTreeThread::CreateProTree(const QString &src_path, const QString &dist_path,
QTreeWidgetItem *parent_item, int &file_count,
QTreeWidget *self, QTreeWidgetItem *root, QTreeWidgetItem* preItem)
{
if(_bstop){
return;
}
bool needcopy = true;
if(src_path == dist_path){
needcopy = false;
}

QDir import_dir(src_path);
qDebug() << "src_path is " << src_path << "dis_path is " << dist_path << endl;
//设置文件过滤器
QStringList nameFilters;
import_dir.setFilter(QDir::Dirs|QDir::Files|QDir::NoDotAndDotDot);//除了目录或文件,其他的过滤掉
import_dir.setSorting(QDir::Name);//优先显示名字
QFileInfoList list = import_dir.entryInfoList();
qDebug() << "list.size " << list.size() << endl;
for(int i = 0; i < list.size(); i++){
if(_bstop){
return;
}
QFileInfo fileInfo = list.at(i);
bool bIsDir = fileInfo.isDir();
if (bIsDir)
{
if(_bstop){
return;
}
file_count++;
emit SigUpdateProgress(file_count);
QDir dist_dir(dist_path);
//构造子目的路径
QString sub_dist_path = dist_dir.absoluteFilePath(fileInfo.fileName());
qDebug()<< "sub_dist_path " << sub_dist_path;
//子目的目录
QDir sub_dist_dir(sub_dist_path);
//不能存在则创建
if(!sub_dist_dir.exists()){
//可以创建多级目录
bool ok = sub_dist_dir.mkpath(sub_dist_path);
if(!ok){
qDebug()<< "sub_dist_dir mkpath failed"<< endl;
continue;
}
}

auto * item = new ProTreeItem(parent_item, fileInfo.fileName(),
sub_dist_path, root,TreeItemDir);
item->setData(0,Qt::DisplayRole, fileInfo.fileName());
item->setData(0,Qt::DecorationRole, QIcon(":/icon/dir.png"));
item->setData(0,Qt::ToolTipRole, sub_dist_path);
;
CreateProTree(fileInfo.absoluteFilePath(), sub_dist_path, item, file_count, self,root,preItem);

}else{
if(_bstop){
return;
}
const QString & suffix = fileInfo.completeSuffix();
if(suffix != "png" && suffix != "jpeg" && suffix != "jpg"){
qDebug() << "suffix is not pic " << suffix << endl;
continue;
}
file_count++;
emit SigUpdateProgress(file_count);
if(!needcopy){
continue;
}

QDir dist_dir(dist_path);
QString dist_file_path = dist_dir.absoluteFilePath(fileInfo.fileName());
if(!QFile::copy(fileInfo.absoluteFilePath(), dist_file_path)){
qDebug() << "file src to dist copy failed" << endl;
continue;
}

auto * item = new ProTreeItem(parent_item, fileInfo.fileName(),
dist_file_path, root,TreeItemPic);
item->setData(0,Qt::DisplayRole, fileInfo.fileName());
item->setData(0,Qt::DecorationRole, QIcon(":/icon/pic.png"));
item->setData(0,Qt::ToolTipRole, dist_file_path);

if(preItem){
auto* pre_proitem = dynamic_cast<ProTreeItem*>(preItem);
pre_proitem->SetNextItem(item);
}

item->SetPreItem(preItem);
preItem = item;
}
}
parent_item->setExpanded(true);
}

4 重写线程run函数
run函数就是线程启动后执行的函数,如果CreateProTree运行结束,判断_bstop是否为true,如果为true说明取消了创建操作,那么就要把根节点移除,并删除文件夹内的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void ProTreeThread::run()
{
CreateProTree(_src_path,_dist_path,_parent_item,_file_count,_self,_root);
if(_bstop){
auto path = dynamic_cast<ProTreeItem*>(_root)->GetPath();
auto index = _self->indexOfTopLevelItem(_root);
delete _self->takeTopLevelItem(index);
QDir dir(path);
dir.removeRecursively();
return;
}

emit SigFinishProgress(_file_count);
}

5 完善ProTreeWidget的SlotImport函数

创建进队对话框,然后连接线程和对话框的信号和槽
(1) 当对话框被取消时发出QProgressDialog::canceled信号,被ProTreeWidget::SlotCancelProgress捕获。(对话框取消,ProTreeWidget做回收操作并发送SigCancelProgress)
(2) ProTreeWidget发出SigCancelProgress信号,被ProTreeThread::SlotCancelProgress捕获。(对话框取消,线程终止)
(3) 连接ProTreeThread::SigFinishProgress和ProTreeWidget::SlotFinishProgress,进度框响应线程完成操作。
(4) 连接ProTreeThread::SigUpdateProgress和ProTreeWidget::SlotUpdateProgress,更新进度框进度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  int file_count = 0;

//创建模态对话框
_dialog_progress = new QProgressDialog(this);

//耗时操作放在线程中操作

_thread_create_pro = std::make_shared<ProTreeThread>(std::ref(import_path), std::ref(path),
_right_btn_item,
std::ref(file_count), this,_right_btn_item,nullptr);
//连接更新进度框操作
connect(_thread_create_pro.get(), &ProTreeThread::SigUpdateProgress,
this, &ProTreeWidget::SlotUpdateProgress);

connect(_thread_create_pro.get(), &ProTreeThread::SigFinishProgress, this,
&ProTreeWidget::SlotFinishProgress);

connect(_dialog_progress, &QProgressDialog::canceled, this, &ProTreeWidget::SlotCancelProgress);
connect(this, &ProTreeWidget::SigCancelProgress, _thread_create_pro.get(),
&ProTreeThread::SlotCancelProgress);
_thread_create_pro->start();

//连接信号和槽
_dialog_progress->setWindowTitle("Please wait...");
_dialog_progress->setFixedWidth(PROGRESS_WIDTH);
_dialog_progress->setRange(0, PROGRESS_MAX);
_dialog_progress->exec();

相关槽函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void ProTreeWidget::SlotUpdateProgress(int count)
{
qDebug() << "count is " << count;
if(!_dialog_progress){
qDebug() << "dialog_progress is empty!!!" << endl;
return;
}

if(count >= PROGRESS_MAX){
_dialog_progress->setValue(count%PROGRESS_MAX);
}else{
_dialog_progress->setValue(count%PROGRESS_MAX);
}

}

void ProTreeWidget::SlotCancelProgress()
{
// _thread_create_pro->terminate();
emit SigCancelProgress();
delete _dialog_progress;
_dialog_progress =nullptr;
}

void ProTreeWidget::SlotFinishProgress()
{
_dialog_progress->setValue(PROGRESS_MAX);
_dialog_progress->deleteLater();
}

运行导入文件效果如下
https://cdn.llfc.club/1674892256936.jpg
为了完善共功能,在之前的ProTreeWidget构造函数里添加其他的几个动作,包括设置活动项目,关闭项目,开启轮播等,这里不再赘述。

源码链接

源码链接
https://gitee.com/secondtonone1/qt-learning-notes