简介

基于前面介绍的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的构造函数中添加菜单项,并为菜单项设置信号连接,截取部分代码

    //创建菜单栏
    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最大显示

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

4 为MainWindow和菜单栏设置qss

/*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函数。

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提示用户。

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会为我们生成槽函数

//添加浏览按钮点击后选择文件夹的操作
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左侧显示树形目录,这个之后介绍。

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。 在构造函数中隐藏头部,并且注册要传递信息的类型

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

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

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节点

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里。

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和信号的连接

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

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

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样式

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构造函数添加信号和槽函数连接,并且创建导入文件的动作,并为该动作连接槽函数。

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被点击时发出。

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函数。 因为导入操作是一个耗时的操作,所以要放到单独的线程中执行,主线程启动一个进度对话框显示导入进度,同时可以控制导入的中止操作等。 在导入时弹出一个文件选择对话框,设置默认路径

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 自定义线程完成文件复制和树目录创建

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的方式从逻辑上控制退出。 至于为什么有多处判断,因为创建逻辑是递归方式,为了保证退出的效率所以在多处判断,不加锁也是为了提高程序运行的效率。

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说明取消了创建操作,那么就要把根节点移除,并删除文件夹内的文件。

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,更新进度框进度。

      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();

相关槽函数如下

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

results matching ""

    No results matching ""