目录

Qt动态添加和删除布局(仿jupyter)

前言

老师给了一个任务,仿照jupyter notebook中可以动态添加、删除的cell,用c++的qt做一个类似的界面。具体来说,一个cell中有一个输入框,一个输出框,还有四个按钮,分别是运行添加删除清空。此外,为了突出正在编辑的cell,要实现点击到cell的空白部分时,将背景颜色高亮。实现的效果如下:https://myblog-1257298572.cos.ap-shanghai.myqcloud.com/img/ezgif.com-gif-maker.gif

虽然我压根就没学过qt,但是刚接到任务时,我觉得这玩意逻辑挺简单的,应该不难实现。但是后来发现我错了,要做这么一个小东西要用到很多知识。

实现

因为ui比较简单,所以用代码实现,没用designer。先整一个自定义组件叫cell(也就是一个类,继承自QWidget),cell里面包含了两个输入框和四个按钮,在public中定义这四个按钮,保证这四个按钮可以被外部访问到。

用一个QList把所有的cell存储起来,界面和这个QList一一对应,这样,添加和删除cell,就等于添加和删除QList。写一个渲染函数render,把已有的布局全删了,根据QList重新构建,每次进行一个操作之后都重新render。

怎么连接信号与槽呢?因为要实现的逻辑与全局有关,不能在cell内写connect,只能在外面的mainwindow里写,这就是为什么要保证四个按钮可以被外部访问到。因为按钮长得都一模一样,所以要区分按下的是哪一个cell的按钮,所以得用到传参的信号与槽,参数就是按钮的索引位置。这些都是写在render函数里的,别忘了在connect前先disconnect一下,清除原有的信号与槽。

最后,怎么点击背景改变颜色呢?先用groupbox把四个按钮和两个输入输出框包裹起来,然后想到直接用groupbox的click信号,但是我发现这玩意不生效(毕竟人家本来的作用不是拿来干这个的),所以换一种方法。我定义了一个myGroupBox类,继承自QGroupBox,然后重写了它的QMousePressEvent函数,点击就能发出bg_clicked信号(自定义信号),这个信号被mainwindow接收到就可以控制它的stylesheet啦。

代码:

cell.h

#ifndef CELL_H
#define CELL_H

#include <QWidget>
#include <QTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QTextEdit>
#include "mygroupbox.h"

class Cell: public QWidget
{
    Q_OBJECT
public:
    explicit Cell(QWidget *parent = nullptr);
    ~Cell();

    // 一些控件
    myGroupBox *m_box;
    QPushButton *m_btnAdd;
    QPushButton *m_btnDelete;
    QPushButton *m_btnClear;
    QPushButton *m_btnRun;
    QTextEdit *m_input;
    QTextEdit *m_output;

    // 属性:是否被选中
    bool is_clicked=false;

private:
    void init(); // 初始化内部控件

signals:

public slots:

};

#endif // CELL_H

mygroupbox.h

#ifndef MYGROUPBOX_H
#define MYGROUPBOX_H

#include <QGroupBox>

class myGroupBox : public QGroupBox
{
    Q_OBJECT
public:
    explicit myGroupBox(QWidget *parent = nullptr);

    // 鼠标点击
    virtual void mousePressEvent(QMouseEvent *ev);



signals:
    void bg_clicked(); // 背景(groupbox)被点击后变色

public slots:
};

#endif // MYGROUPBOX_H

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QTextEdit>
#include "mygroupbox.h"
#include "cell.h"


QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    // 设置一个主widget
    QWidget *widget;
    // 设置一个mainLayout
    QVBoxLayout *mainLayout;
    // 绘制
    void render();
    // 递归的清除所有布局以及控件
    void deleteAllitemsOfLayout(QLayout* layout);
    // 设置一个list存储所有的cell
    QList<Cell*> cells;

    // 按钮所触发的函数,传入按钮相对应的索引
    void on_btnAdd_clicked(int i);
    void on_btnDelete_clicked(int i);
    void on_btnClear_clicked(int i);
    void on_btnRun_clicked(int i);
    void on_box_clicked(int i);


private slots:


private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

cell.cpp

#include "cell.h"
#include <QDebug>

Cell::Cell(QWidget *parent) : QWidget(parent)
{
    init();
}

Cell::~Cell()
{
}

void Cell::init()
{
    m_box = new myGroupBox;
    QHBoxLayout *hlay = new QHBoxLayout;
    QVBoxLayout *vlay1 = new QVBoxLayout;
    QVBoxLayout *vlay2 = new QVBoxLayout;
    // 把按钮、文本框等控件添加进布局
    m_input = new QTextEdit;
    m_output = new QTextEdit;
    m_output->setStyleSheet("background-color:rgb(230,230,230);");
    m_output->setReadOnly(true);
    vlay2->addWidget(m_input);
    vlay2->addWidget(m_output);
    m_btnRun = new QPushButton("Run", this);
    vlay1->addWidget(m_btnRun);
    m_btnAdd = new QPushButton("Add", this);
    vlay1->addWidget(m_btnAdd);
    m_btnDelete = new QPushButton("Delete");
    vlay1->addWidget(m_btnDelete);
    m_btnClear = new QPushButton("Clear");
    vlay1->addWidget(m_btnClear);
    vlay1->addStretch();
    hlay->addLayout(vlay1);
    hlay->addLayout(vlay2);
    m_box->setLayout(hlay);
    QHBoxLayout *out = new QHBoxLayout;
    out->addWidget(m_box);
    setLayout(out);
}


mygroupbox.cpp

#include "mygroupbox.h"
#include <QDebug>

myGroupBox::myGroupBox(QWidget *parent) : QGroupBox(parent)
{

}

void myGroupBox::mousePressEvent(QMouseEvent *ev)
{
//    // 新思路:发一个信号发出去给mainwindow,然后在外面处理这个
//    qDebug() << "背景被点击";
    emit this->bg_clicked();

}

mainwindow.cpp

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 设置centralwidget和mainlayout
    widget = new QWidget();
    this->setCentralWidget(widget);
    mainLayout = new QVBoxLayout;
    widget->setLayout(mainLayout);

    // 初始化一个界面
    on_btnAdd_clicked(0);
    render();

}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::render()
{
    // 清除所有布局
    deleteAllitemsOfLayout(mainLayout);
    delete mainLayout->layout();

    // 新建布局,并添加控件
    mainLayout = new QVBoxLayout;

    for(int i=0; i<cells.length(); i++){
        mainLayout->addWidget(cells.at(i));

        // 被选中的要改变背景颜色
        if( cells.at(i)->is_clicked ){
            cells.at(i)->m_box->setStyleSheet("QGroupBox{background:rgb(250,250,250)}");
        }else{
            cells.at(i)->m_box->setStyleSheet("");
        }

        // 删除原有连接
        cells.at(i)->m_btnAdd->disconnect();
        cells.at(i)->m_btnDelete->disconnect();
        cells.at(i)->m_btnClear->disconnect();
        cells.at(i)->m_btnRun->disconnect();
        cells.at(i)->m_box->disconnect();

        // 连接btnAdd
        connect(cells.at(i)->m_btnAdd, &QPushButton::clicked, this, [ = ] {
            this->on_btnAdd_clicked(i);
        });
        // 连接btnDelete
        connect(cells.at(i)->m_btnDelete, &QPushButton::clicked, this, [ = ] {
            on_btnDelete_clicked(i);
        });
        // 连接btnClear
        connect(cells.at(i)->m_btnClear, &QPushButton::clicked, this, [ = ] {
            on_btnClear_clicked(i);
        });
        // 连接btnRun
        connect(cells.at(i)->m_btnRun, &QPushButton::clicked, this, [ = ] {
            on_btnRun_clicked(i);
        });
        // 连接m_box,用于改变背景颜色
        connect(cells.at(i)->m_box, &myGroupBox::bg_clicked, this, [ = ] {
            on_box_clicked(i);
        });
    }

    mainLayout->addStretch(); // 加一个弹簧
    widget->setLayout(mainLayout);
//    widget->show();

}

void MainWindow::deleteAllitemsOfLayout(QLayout *layout)
{
    QLayoutItem *child;
            while ((child = layout->takeAt(0)) != nullptr)
            {
                ///setParent为NULL,防止删除之后界面不消失
                if(child->widget())
                {
                    child->widget()->setParent(nullptr);
                }else if(child->layout()){
                    deleteAllitemsOfLayout(child->layout());
                }
                delete child;
            }
}

void MainWindow::on_btnAdd_clicked(int i)
{
    qDebug() << "添加:" << i;
    Cell *new_cell = new Cell;
    cells.append(new_cell);
    this->render();
}

void MainWindow::on_btnDelete_clicked(int i)
{
    qDebug() << "删除:" << i;
    QWidget* w = cells.at(i);
    delete w;
    cells.removeAt(i);
    this->render();
}

void MainWindow::on_btnClear_clicked(int i)
{
    qDebug() << "清空:" << i;
    cells.at(i)->m_input->clear();
}

void MainWindow::on_btnRun_clicked(int i)
{
    qDebug() << "Run第" << i << "个";
}

void MainWindow::on_box_clicked(int i)
{
    qDebug() << "第" << i << "个box被点击";
    cells.at(i)->is_clicked = true;
    for( int j=0; j<cells.length(); j++ ){
        if( cells.at(j)->is_clicked && j!=i){
            cells.at(j)->is_clicked = false;
        }
    }
    this->render();

}

涉及到项目其它内容,mainwindow.cpp就截选了一部分,仅供学习。

学习历程

虽然我没有学过qt,但是我学过vue,前端的框架在设计理念上都是相通的,所以我对qt中的一些概念能很快理解,比如信号与槽,控件等等。但是web前端的编程环境跟c++大不一样,所以适应起来还是需要一段时间的。

刚开始学一门技术,并且立刻拿它来做项目确实是很头疼的,但这也是最锻炼人的方式。恭喜我,在堕落的寒假的尾声终于又点亮了一颗技能树。