QT项目----电子相册(2)
文章目录
- 前言
- 一.添加目录树ProTree类
- 大致思路图
- 1.加入目录树
- 二.加入相册创建功能
- 1.wizard中点击完成后返回路径和名字
- 详解dynamic_cast<>
- 2.实现槽函数AddProTree
- 详解疑点
- 3.思路分析:
- 4.结果
- 三.弹出菜单功能
- 1.代码思路以及解析
- 四.菜单功能一 导入文件
- 1.大致思路
- 2.先根据思路完成部分SlotImport函数
- 3.创建线程QThread
- 1.构造函数
- 2.run函数
- 1.CreateProtree
- 3.实现构造函数和析构函数
- 4.实现CreateProTree函数
- 5.完成run函数
- 4.实现进度条更新
- 5.实现完成和取消
前言
提示:这里可以添加本文要记录的大概内容:
我们接着前面
这次我们把左侧的目录树给实现了,接着上文点击确定后,应该要用一个相册的文件被添加到左侧的目录中
备注!!!!目录树的qss样式已经写好放在GitHub中,此处不再提及!!
提示:以下是本篇文章正文内容,下面案例可供参考
一.添加目录树ProTree类
1 创建Qt设计师界面类,名字为ProTree,基类选择QDialog,ProTree中添加一个垂直布局,布局内添加一个QLabel和一个QTreeWidget,最后将ProTree设置为垂直布局
2 考虑到QTreeWidget功能有限,我们需要继承QTreeWidget重新实现一个新的类ProTreeWidget,所以在项目中新增C++类ProTreeWidget继承自QTreeWidget
同时将ProTree布局中的QTreeWidget提升为ProTreeWidget
3 同样的道理为了便于操作定义ProTreeItem继承QTreeWidgetItem
大致思路图
1.加入目录树
在mainwindow.h中加入一个成员变量
QWidget * _protree;
这里有基类是因为降低类与类之间的耦合性 如果这里用protree类型,protree中也用到mainwindow 可能会引发互引用问题
//加入目录树_protree=new ProTree();ui->proLayout->addWidget(_protree);
将目录树放入左侧的区域
放入这个位置
二.加入相册创建功能
1.wizard中点击完成后返回路径和名字
当创建点击完成时
//实现完成按钮
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);//如果重写了基类的虚函数//后续还想再用其原本的功能 需要再次调用
}
会发送一个信号SigProSettings
这个信号会我们在wizard.h中自定义的
将名字与路径返回
此时我们在mainwindow中要接收这个信号做出响应
详解dynamic_cast<>
🧠 一、dynamic_cast 是什么?
✅ 它是 C++ 中的运行时类型转换运算符,专用于指针或引用类型的“安全”向下转型(多态)
📌 举个例子:
Base* base = new Derived();
Derived* d = dynamic_cast<Derived*>(base); // 安全地向下转型
相比 static_cast,dynamic_cast 会在运行时做类型检查,如果类型不对,会返回 nullptr(指针)或者抛异常(引用),更安全!
📦 二、此处 dynamic_cast<ProTree*>(_protree) 是什么意思?
👉 表示将 _protree 这个指针(类型是基类指针)转换成 ProTree 类型*
ProTree* proTreePtr = dynamic_cast<ProTree*>(_protree);
_protree 是一个 基类指针(比如 QTreeWidget*);
ProTree 是你写的自定义子类;
转换后才能调用子类中定义的 AddProTree() 函数
✅ 三、那为啥 _protree 是用基类类型声明的?(QTreeWidget* _protree;)
为了降低耦合度、增强扩展性!
📦 这样设计有什么好处?
QTreeWidget* _protree; 方便以后替换其它自定义树控件
ProTree* _protree; 强依赖 ProTree 类,耦合性高,不利于拓展和复用
这样设计的最大优点是:用基类来存储,用子类来实现具体功能,只在真正需要用到子类方法时才 dynamic_cast 一下
2.实现槽函数AddProTree
void ProTree::AddProTree(const QString name, const QString path)
{ui->treeWidget->AddProTree(name,path);
}
这里我们转到treeWidget中,到ProTreeWidget中调用AddProTree
void ProTreeWidget::AddProTree(const QString &name,
const QString &path)//创建相册到目录中
{QDir dir(path);QString _path=dir.absoluteFilePath(name);if(_set_path.find(_path)!=_set_path.end())//有这个路径了{return;}QDir p_dir(_path);if(!p_dir.exists())//如果文件夹不存在 就创建一个{bool p=p_dir.mkpath(_path);// mkpath() 会递归创建路径中不存在的部分,//比如: /a/b/c 如果 /a/b 不存在,也会一并创建if(!p){return;}}_set_path.insert(_path);auto item=new ProTreeItem(this,name,_path,TreeItemPro);item->setData(0,Qt::DisplayRole,name);item->setData(0,Qt::DecorationRole,QIcon(":/photo/e94dcc6cc2a06e03229c65a6d1a7e70.jpg"));item->setData(0,Qt::ToolTipRole,_path);this->addTopLevelItem(item);
}
详解疑点
这里可能会有一个疑惑,为什么触发信号后调用槽函数,槽函数中又要掉用到自定义的基类中使用函数AddProTree呢???
✅ 一、调用流程总结
connect(&wizard, &Wizard::SigProSettings,dynamic_cast<ProTree*>(_protree),&ProTree::AddProTree);
这个连接的意思是:
当 Wizard 中配置完成发出 SigProSettings(name, path) 信号时,调用 ProTree 的 AddProTree(name, path) 槽函数。
🔁 然后在 ProTree::AddProTree 中又写了:
void ProTree::AddProTree(const QString name, const QString path)
{ui->treeWidget->AddProTree(name, path);
}
也就是说:
Wizard 发出信号 →
ProTree::AddProTree() 处理一下 →
再让真正的 ProTreeWidget 来完成相册的添加(构造、显示、注册路径等)
🧠 二、那为啥不直接 connect 到 ProTreeWidget::AddProTree?
👉 因为在做一件很重要的事情:模块解耦 + 逻辑中转
📦 ProTree 的职责:
是业务层控制器(或界面类),它掌控界面逻辑;
所以你不希望让 Wizard 直接操作 ProTreeWidget 这种 UI 控件;
因为 Wizard 是独立的,不应该知道界面内部怎么实现(这是解耦)。
📦 ProTreeWidget 的职责:
真正负责显示树形结构、插入节点、修改 UI;
是纯粹的视图控件类。
所以通过 ProTree::AddProTree() 中转,让 Wizard 只和业务类打交道,不和具体控件耦合
🔍 三、深入分析 ProTreeWidget::AddProTree(…)
这个函数的核心功能是:
拼接完整路径(name + path)
判断路径是否已存在,防止重复
自动创建文件夹(相册)
添加 ProTreeItem(你自定义的类,继承自 QTreeWidgetItem)
设置图标、提示、名字
添加到树中显示
这部分纯粹是 UI 操作逻辑,因此它在 ProTreeWidget 类中实现
🎯 四、为什么这样设计更优雅?
模块 说明
Wizard 弹出配置窗口,负责采集用户输入,不关心 UI
ProTree 接收信号,作为业务调度中间层
ProTreeWidget 真正操作 UI,添加项目节点
ProTreeItem 树中每一个节点的模型封装
3.思路分析:
1.先判断完整路径是否存在
完整路径是path+name
路径用QDir类型显示
如果路径不存在 我们走下一步
此时判断文件夹是否存在 如果不存在,就用mkpath递归的创建
然后将此路径加入_set_path中
最后将这个item设置一下显示信息
item->setData(0, Qt::DisplayRole, name);
✅ 作用:
在第 0 列显示这个项目的“名字”。
📦 参数解释:
0:列索引,表示设置第 0 列(通常是最左边那列)。
Qt::DisplayRole:显示文本的角色(显示内容)。
name:你传进来的相册/文件夹的名字
item->setData(0, Qt::DecorationRole, QIcon(“:/photo/xxx.jpg”));
✅ 作用:
在第 0 列左边的图标区域,显示一张图片作为图标(装饰)。
📦 参数解释:
0:还是第 0 列。
Qt::DecorationRole:装饰角色,用于设置图标、图片等。
QIcon(“:/photo/xxx.jpg”):一个资源图片,用作显示的图标
item->setData(0, Qt::ToolTipRole, _path);
✅ 作用:
设置鼠标悬停时的提示文本(ToolTip),显示完整路径。
📦 参数解释:
0:第 0 列。
Qt::ToolTipRole:鼠标悬停提示的角色。
_path:你设置进去的完整路径,比如 D:/Photos/相册1
最后this->addTopLevelItem(item)将这个item加入
4.结果
这样在wizard点击完成时触发done函数,进而发送信号触发ProTree的AddProToTree函数了,从而生成一个项目目录的item。
效果如下:
三.弹出菜单功能
我们要在生成的ProTreeWidget的项目root item中点击右键,弹出菜单,然后选择导入文件夹,将文件夹中的目录和文件递归的导入我们创建的项目目录,并且在root下生成item节点。
类似于这种效果:
当右键的时候会跳出来一个菜单
itemPressed信号是从QTreeWidget基类继承而来的,在QTreeWidget中的item被点击时发出
我们实现一个槽函数即可
1.代码思路以及解析
首先我们利用QGuiApplication::mouseButtons()& Qt::RightButton判断是否为右键点击
然后判断是否为根目录,我们取出*item的type()就行,然后我们创建了一个const.h的目录,存储了一些常量,我们在里面定义的TreeItemPro为根节点,所以我们拿获取到的type与这个比较即可,然后还有一个点就是new了一个对象记得销毁
private slots:void SlotItemPress(QTreeWidgetItem *item, int column);//点击时触发这个槽函数
实现这个槽函数
void ProTreeWidget::SlotItemPress(QTreeWidgetItem *item, int column)//右键打开菜单
{if(QGuiApplication::mouseButtons()& Qt::RightButton)//如果是右键{int tmp_type=item->type();if(tmp_type!=TreeItemPro){return;//不是根目录}auto menu=new QMenu(this);menu->addAction(_action_import);menu->addAction(_action_setstart);menu->addAction(_action_slideshow);menu->addAction(_action_closerpro);menu->exec(QCursor::pos());//控制菜单弹出位置delete menu;}
}
这里是加入了4个动作
这四个动作事先先定义为成员变量了
效果如下:
四.菜单功能一 导入文件
1.大致思路
接下来点击导入文件动作之后执行SlotImport函数。
因为导入操作是一个耗时的操作,所以要放到单独的线程中执行,主线程启动一个进度对话框显示导入进度,同时可以控制导入的中止操作等。
在导入时弹出一个文件选择对话框,设置默认路径
2.先根据思路完成部分SlotImport函数
我们点击导入文件以后,应该要弹出一个文件夹,然后用户选择文件夹,我们遍历此文件夹中内容将图片遍历一遍导入,这个操作部分很耗时,所以我们单独开一个线程来完成这个功能,不直接写入主线程中
void ProTreeWidget::SlotImport()//菜单选项一 导入文件
{QFileDialog file_log;file_log.setFileMode(QFileDialog::Directory);//文件夹模式file_log.setViewMode(QFileDialog::Detail);//详细模式file_log.setWindowTitle(tr("选择导入文件夹"));QString file_log_path=dynamic_cast<ProTreeItem*>(_right_btn_item)->GetPath();file_log.setDirectory(file_log_path);//打开路径QStringList _list;if(file_log.exec()){_list=file_log.selectedFiles();//获取用户选中的文件然后存储在_list中}if(_list.length()==0)//用户没有选择任何文件{return;}QString first_path=_list.at(0);int file_count=0;//记录文件中图片有多少个
}
此时我们还要创建一个模态对话框,显示加载进度
//主页面创建一个模态对话框显示加载进度_dialog_progress=new QProgressDialog(this);//给对话框初始化_dialog_progress->setWindowTitle(tr("Please wait..."));_dialog_progress->setFixedWidth(PROGRESS_WIDTH);_dialog_progress->setRange(0,PROGRESS_WIDTH);_dialog_progress->exec();
这个PROGRESS_WIDTH是定义的一个常量300,我们这里把对话框_dialog_progress设置为了一个成员变量,因为在线程中还会用到,所以为了方便,就先设置好
3.创建线程QThread
1.构造函数
我们先理清思路,我们肯定要确定要导入的文件的原地址,然后再确定我们要导入在哪,这是目的地址,然后我们还要确定导入的文件是在哪个父节点之下,然后我们还要确定一个文件数,还要确定一个根节点
同时我们看原本的QThread函数中构造函数中有哪些
我们把这个复制进来就行
ProTreeThread(const QString & src_path,const QString & dis_path,
QTreeWidgetItem* parent_item,const int file_count,QTreeWidget* self,QTreeWidgetItem* root,QObject *parent = nullptr);
这就是线程的构造函数
src_path:要导入文件的地址
dis_path :导入文件去哪
parent_item 将文件导入在哪个父节点下
file_count文件中图片数
self就是左侧这一块目录树
root是最顶部的根节点
最后一个参数是原本QThread中自带的
最后别忘记补上析构函数
~ProTreeThread();
2.run函数
如果我们想让线程跑起来有什么功能的话,必须将线程中run函数重写
同时在重写基类的虚函数的时候,尽量用protected
仅限于父类子类的调用
protected:virtual void run();
1.CreateProtree
同时我们还要写一个CreateProtree函数,在run函数中调用这个函数,来创建树
//封装一个函数 run中调用这个函数
private:void CreateProTree(const QString& src_path,const QString& dist_path,QTreeWidgetItem* parent_item,int& file_count,QTreeWidget* self, QTreeWidgetItem* root,QTreeWidgetItem* preitem=nullptr);
每个参数含义与上面的一样,这里多了一个参数preitem 代表前向节点,因为后面我们要把图片一个一个按顺序排列
我们这里添加一些成员变量
//成员变量QString _src_path;//原地址QString _dist_path;//要复制到的地址int _file_count;QTreeWidgetItem* _parent_item;//父节点QTreeWidget* _self;//当前节点QTreeWidgetItem* _root;bool _bstop;//控制线程退出
3.实现构造函数和析构函数
ProTreeThread::ProTreeThread(const QString & src_path,const QString& dist_path,QTreeWidgetItem* parent_ite,int file_count,QTreeWidget* self,QTreeWidgetItem* root,QObject* parent):QThread(parent),_src_path(src_path),_dist_path(dist_path),_parent_item(parent_ite),_self(self),_root(root),_bstop(false)
{}
这里主要是将成员变量初始化
这里我们要调用线程自己原本的构造函数QThread()然后传入参数就行
原因:
析构函数
ProTreeThread::~ProTreeThread()
{}
4.实现CreateProTree函数
思路图:
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);import_dir.setFilter(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot);//保留这些import_dir.setSorting(QDir::Name);QFileInfoList _list=import_dir.entryInfoList();for(int i=0;i<_list.size();i++){QFileInfo file_info=_list.at(i);if(_bstop)//用户点击了取消{return;}if(file_info.isDir())//是文件夹{file_count++;emit SigUpdateProgress(_file_count);QDir dir_path(dist_path);QString t_path=dir_path.absoluteFilePath(file_info.fileName());QDir t_dir(t_path);if(!t_dir.exists())//不存在就创建{bool isok=t_dir.mkpath(t_path);if(!isok){continue;}}auto *item=new ProTreeItem(parent_item,file_info.fileName(),t_path,root,TreeItemDir);item->setData(0,Qt::DisplayRole,file_info.fileName());item->setData(0,Qt::ToolTipRole,t_path);item->setData(0,Qt::DecorationRole,QIcon(":/photo/8ce90598bd8ddfdc3408f8781e6dd96.jpg"));// if (!parent_item && self) {// self->addTopLevelItem(item);// }CreateProTree(file_info.absoluteFilePath(),t_path,item,file_count,self,root,preitem);}else//文件{if(file_info.completeSuffix()!="png"&&file_info.completeSuffix()!="jpeg"&&file_info.completeSuffix()!="jpg"){continue;//不是图片}_file_count++;emit SigUpdateProgress(_file_count);if(!needcopy){continue;}QDir dir_path(dist_path);QString dir_path_t=dir_path.absoluteFilePath(file_info.fileName());if(!QFile::copy(file_info.absoluteFilePath(),dir_path_t)){continue;}auto *item=new ProTreeItem(parent_item,file_info.fileName(),dir_path_t,root,TreeItemPic);item->setData(0,Qt::DisplayRole,file_info.fileName());item->setData(0,Qt::ToolTipRole,dir_path_t);item->setData(0,Qt::DecorationRole,QIcon(":/photo/8ce90598bd8ddfdc3408f8781e6dd96.jpg"));/*f (!parent_item && self) {self->addTopLevelItem(item);}*///更新链表if(preitem)//不为空{ProTreeItem *pre_item=dynamic_cast<ProTreeItem*>(preitem);//为了使用SetNextItem这个函数pre_item->SetNextItem(item);}item->SetPreItem(preitem);preitem=item;}}parent_item->setExpanded(true);//可展开
}
5.完成run函数
void ProTreeThread::run()
{CreateProTree(_src_path,_dist_path,_parent_item,_file_count,_self,_root);if(_bstop){auto path=dynamic_cast<ProTreeItem*>(_root)->GetPath();//获取路径//树中删除int dex=_self->indexOfTopLevelItem(_root);delete _self->takeTopLevelItem(dex);//删除文件夹QDir dir(path);dir.removeRecursively();return;}emit SigFinishProgress(_file_count);
}
4.实现进度条更新
我们在主界面中要先用一个智能指针存储线程
使用 std::shared_ptr 的主要目的是管理 ProTreeThread 对象的生命周期
_thread_create_p =std::
make_shared<ProTreeThread>(std::ref(first_path),std::ref(file_log_path),_right_btn_item,file_count,this,_right_btn_item,nullptr);
connect(_thread_create_p.get(),&ProTreeThread::SigUpdateProgress,
this,&ProTreeWidget::SlotUpdateProgress);//连接的时候用裸指针
每次线程中file_count++之后我们都发送一个信号 交给主界面,主界面做实现这个槽函数,让窗口的进度条更新
//进度条更新
void ProTreeWidget::SlotUpdateProgress(int count)
{//qDebug()<<"接收更新信号1"<<Qt::endl;if(!_dialog_progress)//未打开{return;}qDebug()<<count<<Qt::endl;if(count>=PROGRESS_MAX){_dialog_progress->setValue(count%PROGRESS_MAX);}else{_dialog_progress->setValue(count);}
}
5.实现完成和取消
当完成后,自动关闭窗口
点击取消,我们要发送一个信号通知线程,然后线程中实现一个槽函数将_bstop变为true
connect(_thread_create_p.get(),&ProTreeThread::SigFinishProgress,this,&ProTreeWidget::SlotFinishProgress);//完成关闭窗口connect(_dialog_progress,&QProgressDialog::canceled,this,&ProTreeWidget::SlotCanceled);//点击取消按钮connect(this,&ProTreeWidget::SigCanceled,_thread_create_p.get(),&ProTreeThread::SlotCanceled);//取消通知线程
//完成的时候关闭窗口就行
void ProTreeWidget::SlotFinishProgress()
{_dialog_progress->setValue(PROGRESS_MAX);_dialog_progress->deleteLater();
}
void ProTreeWidget::SlotCanceled()//通知线程取消
{emit SigCanceled();delete _dialog_progress;_dialog_progress=nullptr;
}
void ProTreeThread::SlotCanceled()
{this->_bstop=true;
}
所有源码我放入Github中同步更新
Github源码地址点击此处
后续持续更新…