当前位置: 首页 > news >正文

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函数

思路图:
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/e409a752724146b18b6daaa36d825f8d.png

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源码地址点击此处
后续持续更新…

相关文章:

  • PowerBI 表格显示无关联的表数据
  • 智能产线07期-能耗监控:数据驱动的智慧能源管理系统
  • 2025年03月中国电子学会青少年软件编程(Python)等级考试试卷(一级)真题
  • 如何实现采购数字化?
  • 智能翻译播放器,让无字幕视频不再难懂
  • 4.18学习总结
  • 从数据集到开源模型,覆盖无机材料设计/晶体结构预测/材料属性记录等
  • 从瀑布到敏捷:我是如何学习PSM完成转型的
  • Oceanbase单机版上手示例
  • WiFi“管家”------hostapd的工作流程
  • pdfjs库使用3
  • 语音合成之二TTS模型损失函数进化史
  • Nacos安装及数据持久化
  • YOLOv5、YOLOv6、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLOv11、YOLOv12的网络结构图
  • 【教程】无视硬件限制强制升级Windows 11
  • 用 NLP + Streamlit,把问卷变成能说话的反馈
  • PyCharm入门导览
  • 深度学习-全连接神经网络-1
  • 解析:深度优先搜索、广度优先搜索和回溯搜索
  • 通信算法之269 : OFDM信号的循环自相关特性用于无人机图传信号识别
  • 全国首家由司法行政部门赋码登记的商事调解组织落户上海
  • 撤销逾千名留学生签证,特朗普政府面临集体诉讼
  • 安徽省合肥市人大常委会原副主任杜平太接受审查调查
  • 守护体面的保洁员,何时能获得体面?|离题
  • 新华每日电讯写在浦东开发开放35周年之际:改革开放为笔,绘就崭新传奇
  • 4月沪市公司新增回购70家,金额上限已超222亿元