案例效果图
- 先看我们要实现的效果
下面提供的代码将会以伪代码的形式表现,主要是提供一个思路给大家做参考
基础概念
- 想要做出该案例效果,需要对基本概念有一定理解,若对基础概念不理解,委托与代理的基本概念可以看我的这篇博客或者自行百度。
框架
- 案例中我们可以看到左侧为树状结构,右侧为表格结构,对树可以增删查操作,对表可以增删改操作。要实现这些功能,我们需要俩个代理一个为树的一个为表的,他们都是继承于
QAbstractItemModel
。树于表的代理模型都需要设置自己单独的结构体用于操作数据,其中树相对于表就复杂多。 想要实现树的查找我们还需要设置一个用于过滤的代理模型QSortFilterProxyModel
,当然这只是现实过滤的一种方法,还有更简单的方法就是用正则表达式去查找保存所有数据的链表。而设置表的时候想得到不同类型的单元格,则需要为表设置委托QStyledItemDelegate
。 - 准备好这些基本结构就可以
new
好一个QTableView
与一个QTreeView
操作了。
代码结构介绍
代理模型与其结构体设计
上面提到结构体是用与保存/操作数据的
- 表结构体
struct DeveloperTableItem {
DeveloperTableItem(const QString& key, const QVariant& value) : m_key(key), m_value(value) { }
QString m_key;
QVariant m_value;
};
可以看到表只需要一个键值对就行
2. 树结构体
// 信息
typedef struct Node_t {
QString Node; // 节点
QString Type; // 节点类型
Node_t()
{
Node = "";
Type = "";
}
} Node;
class TreeItem
{
public:
// 节点类型
enum Type
{
Unknown,
HeadNode, // 头节点
BranchNode, // 分支节点
LeafNode, // 叶子节点
};
//*相关功能函数略*//
public:
QString m_uuid;
Type m_type;
private:
QList<TreeItem*> m_children; // 子节点
TreeItem* m_parent; // 父节点
int m_row; // 此item位于父节点中第几个
Node* _ptr; // 存储数据的指针
};
树的结构相对就复杂很多,它需要保存自己的节点信息(子节点,父节点,节点详细信息)
3. 表的代理
class TableModel : public QAbstractTableModel
{
public:
enum Cloum
{
Key = 0,
Value
};
TableModel(QObject* parent = nullptr);
void addRow(const QString& key, const QVariant& value);
void setTreeItem(TreeItem* item) { m_item = item; }
//*功能函数-略*//
protected:
virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
private:
/// @brief 数据链表
QList<DeveloperTableItem*> m_data;
/// @brief 表头
QStringList m_listHeader;
TreeItem* m_item;
};
简单介绍一下上面的关键函数
m_data
使用一个QList
保存所有的数据。
addRow
函数是用于往结构m_data
中加入数据的,setTreeItem
函数用于设置为该表所属的树节点。
那些保护函数都是重写QAbstractTableModel
类的,可以自行百度。
4. 树的代理
class TreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
explicit TreeModel(QObject* parent = nullptr);
//*功能函数-略*//
protected:
QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override;
int rowCount(const QModelIndex& parent) const override;
QVariant data(const QModelIndex& index, int role) const override;
QModelIndex parent(const QModelIndex& index) const override;
int columnCount(const QModelIndex& parent) const override;
virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
private:
/// @brief 根节点
TreeItem* m_rootItem;
/// @brief 表头
QStringList m_header;
};
简单介绍一下上面的关键函数
树中我们只需要使用m_rootItem
保存根节点就可以保存所有数据,详细的数据都保存在上述树的的结构体中。
addRow
函数是用于往结构m_data
中加入数据的,setTreeItem
函数用于设置为该表所属的树节点。
那些保护函数都也是重写QAbstractTableModel
类的,可以自行百度。
委托与过滤模型
- 表的委托
class MyDelegate : public QStyledItemDelegate
{
public:
MyDelegate(QObject* parent = 0) : QStyledItemDelegate(parent) { }
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void setEditorData(QWidget* editor, const QModelIndex& index) const override;
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option,
const QModelIndex& index) const override;
};
都是熟面孔了,我称之为委托三部曲
createEditor
:用于创建一个编辑器部件,用于编辑指定的数据项
setEditorData
:用于将数据从数据模型中的指定索引复制到编辑器部件中,以便用户对其进行编辑。
setModelData
:用于将编辑器部件中的数据复制回数据模型中的指定索引,以便更新模型中的数据。
最后还有个updateEditorGeometry
用于更新编辑器部件的位置和大小,以便与其所在的视图或窗口的布局相匹配。
2. 树的过滤模型
class TreeViewProxyModel : public QSortFilterProxyModel
{
public:
explicit TreeViewProxyModel(QObject* parent = nullptr);
void setFilterString(const QString& strFilter = QString());
void begin() { beginResetModel(); }
void end() { endResetModel(); }
void expandFilteredNodes(const QModelIndex& parent = QModelIndex());
protected:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
private:
QString m_strFilterString;
ModelTreeView* mtree;
};
简单介绍一下
setFilterString
:修改文本时调用的函数,其会调用invalidateFilter
函数触发过滤器
filterAcceptsRow
:重写的函数,为过滤的规则,通过调用invalidateFilter
函数触发
expandFilteredNodes
:递归展开筛选后的节点
视图
- 视图没啥好介绍的,就设置一下样式,把代理和委托设置好, 各种功能函数实现。
部分详细的功能点介绍
- 在树中我们可以看到有俩列,他们是保持一定的占比的,当我们变换窗口大小时,占比也是不会变得,具体实现为如下代码。
// 此函数用于调整列宽
auto adjustColumnWidth = [=]() {
int totalWidth = viewport()->width(); // 获取视图的总宽度
int firstColumnWidth = static_cast<int>(totalWidth * 0.8); // 计算出第一列的宽度
setColumnWidth(0, firstColumnWidth); // 设置第一列的宽度
};
// 在视图第一次显示的时候可能需要调整列宽
adjustColumnWidth();
// 为了响应后续的尺寸变化,比如用户调整窗口大小,我们需要连接 resize 事件
QObject::connect(header(), &QHeaderView::sectionResized, this, adjustColumnWidth);
- 如何在树中设置菜单,如下代码
//视图构造函数中定义
this->setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &ModelTreeView::customContextMenuRequested, this, &ModelTreeView::onTreeContextMenu);
//实现函数
void ModelTreeView::onTreeContextMenu(const QPoint& pos)
{
m_menu->clear();
QModelIndex index = indexAt(pos);
if (!index.isValid()) {
return;
}
//通过Index得到节点信息
TreeItem* item = getItemByIndex(index);
//有了节点信息既可以设置相应的右键菜单
switch (item->m_type) {
case TreeItem::HeadNode: {
m_menu->addAction("添加组", this, SLOT(onAddGroup()));
} break;
case TreeItem::BranchNode: {
//略
} break;
case TreeItem::LeafNode: {
//略
} break;
default:
break;
}
m_menu->exec(cursor().pos());
}
- 表的菜单,与树的菜单做法差不多
// 设置允许自定义上下文菜单
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &QWidget::customContextMenuRequested, this, &TableView::showContextMenu);
void TableView::showContextMenu(const QPoint& pos)
{
QModelIndex index = indexAt(pos);
QMenu contextMenu(tr("菜单"), this);
QAction addAction(tr("添加键值"), this);
QAction removeAction(tr("删除"), this);
// 只有在存在有效行时添加删除选项
if (index.isValid()) {
contextMenu.addAction(&removeAction);
}
contextMenu.addAction(&addAction);
// 显示菜单
contextMenu.exec(mapToGlobal(pos));
}
- 每次点击树节点右侧出现相对应的表,先获取点击的节点信息,清空原有的表代理数据,为什么要清除呢,我使用的思想是多对一,多个树节点对应一个表,因为数据都存放在树的结构体中,所以每次就是清除上一次数据,把这一次数据重新刷新到表格中。数据与界面是要分开的,这是我们使用model-view这个设计模式的核心思想 如下代码实现。
// 连接树的信号到插槽,以对项目点击作出响应
connect(dataPtr->treeView, &QTreeView::clicked, this, &DeveloperModeWidget::onTreeClicked);
void DeveloperModeWidget::onTreeClicked(const QModelIndex& index)
{
// 根据index获取被点击的项
auto item = dataPtr->treeView->getItemByIndex(index);
if (item && item->parent()) {
dataPtr->tableModel->cleraData();//清除原有数据
setTabelData(item);//设置表格数据
dataPtr->tableView->update(); // 刷新表格视图
}
}
- 我们给树设置了了一个过滤模型,那么在点击节点时要获取正确的节点,需要进行映射。
TreeItem* ModelTreeView::getItemByIndex(const QModelIndex& index)
{
auto mapIndex = proxymodel->mapToSource(index);
return m_treeModel->itemFromIndex(mapIndex);
}