為 TreeView 建立一個簡單的模型
從 Qt 5.5 開始,我們有一個新的精彩 TreeView ,一個我們都在等待的控制元件。一個 TreeView 控制元件實現了從模型專案的樹表示。通常它看起來像其他 QML 檢視 - ListView或 TableView 。但 TreeView 的資料結構更復雜。
ListView 或 TableView 中的資料由一維節點陣列表示。在 TreeView 中,每個節點都可以包含自己的節點陣列。因此,與 TreeView 中的其他檢視不同,要獲取指定節點,我們必須知道父節點,而不僅僅是元素的行或列。
另一個主要區別是 TreeView 不支援 ListModel 。要提供資料,我們必須繼承 QAbstractItemModel 。在 Qt 中,可以使用像 QFileSystemModel 這樣的模型類來提供對本地檔案系統的訪問,或者使用 QSqlTableModel 來提供對資料庫的訪問。
在下面的示例中,我們將建立從 QAbstractItemModel 派生的此類模型。但為了使示例更加真實,我建議將模型設定為 ListModel, 但為樹指定,以便我們可以從 QML 新增節點。有必要澄清一下,模型本身不包含任何資料,只提供對它的訪問。因此,提供和組織資料完全是我們的責任。
由於模型資料是在樹中組織的,因此最簡單的節點結構如下所示,在虛擬碼中:
Node {
var data;
Node parent;
list<Node> children;
}
在 C++
中,節點宣告應如下所示:
class MyTreeNode : public QObject
{
Q_OBJECT
public:
Q_PROPERTY(QQmlListProperty<MyTreeNode> nodes READ nodes)
Q_CLASSINFO("DefaultProperty", "nodes")
MyTreeNode(QObject *parent = Q_NULLPTR);
void setParentNode(MyTreeNode *parent);
Q_INVOKABLE MyTreeNode *parentNode() const;
bool insertNode(MyTreeNode *node, int pos = (-1));
QQmlListProperty<MyTreeNode> nodes();
MyTreeNode *childNode(int index) const;
void clear();
Q_INVOKABLE int pos() const;
Q_INVOKABLE int count() const;
private:
QList<MyTreeNode *> m_nodes;
MyTreeNode *m_parentNode;
};
我們從 QObject 派生我們的類,以便能夠在 QML
中建立一個節點。所有子節點都將新增到 nodes
屬性中,因此接下來的 2 部分程式碼是相同的:
TreeNode {
nodes:[
TreeNode {}
TreeNode {}
]
}
TreeNode {
TreeNode {}
TreeNode {}
}
請參閱此文章以瞭解有關預設屬性的更多資訊。
節點類實現:
MyTreeNode::MyTreeNode(QObject *parent) :
QObject(parent),
m_parentNode(nullptr) {}
void MyTreeNode::setParentNode(MyTreeNode *parent)
{
m_parentNode = parent;
}
MyTreeNode *MyTreeNode::parentNode() const
{
return m_parentNode;
}
QQmlListProperty<MyTreeNode> MyTreeNode::nodes()
{
QQmlListProperty<MyTreeNode> list(this,
0,
&append_element,
&count_element,
&at_element,
&clear_element);
return list;
}
MyTreeNode *MyTreeNode::childNode(int index) const
{
if(index < 0 || index >= m_nodes.length())
return nullptr;
return m_nodes.at(index);
}
void MyTreeNode::clear()
{
qDeleteAll(m_nodes);
m_nodes.clear();
}
bool MyTreeNode::insertNode(MyTreeNode *node, int pos)
{
if(pos > m_nodes.count())
return false;
if(pos < 0)
pos = m_nodes.count();
m_nodes.insert(pos, node);
return true;
}
int MyTreeNode::pos() const
{
MyTreeNode *parent = parentNode();
if(parent)
return parent->m_nodes.indexOf(const_cast<MyTreeNode *>(this));
return 0;
}
int MyTreeNode::count() const
{
return m_nodes.size();
}
MyTreeNode *MyTreeModel::getNodeByIndex(const QModelIndex &index)
{
if(!index.isValid())
return nullptr;
return static_cast<MyTreeNode *>(index.internalPointer());
}
QModelIndex MyTreeModel::getIndexByNode(MyTreeNode *node)
{
QVector<int> positions;
QModelIndex result;
if(node) {
do
{
int pos = node->pos();
positions.append(pos);
node = node->parentNode();
} while(node != nullptr);
for (int i = positions.size() - 2; i >= 0 ; i--)
{
result = index(positions[i], 0, result);
}
}
return result;
}
bool MyTreeModel::insertNode(MyTreeNode *childNode, const QModelIndex &parent, int pos)
{
MyTreeNode *parentElement = getNode(parent);
if(pos >= parentElement->count())
return false;
if(pos < 0)
pos = parentElement->count();
childNode->setParentNode(parentElement);
beginInsertRows(parent, pos, pos);
bool retValue = parentElement->insertNode(childNode, pos);
endInsertRows();
return retValue;
}
MyTreeNode *MyTreeModel::getNode(const QModelIndex &index) const
{
if(index.isValid())
return static_cast<MyTreeNode *>(index.internalPointer());
return m_rootNode;
}
要通過 QQmlListProperty 向 QML 公開類似列表的屬性,我們需要接下來的 4 個函式:
void append_element(QQmlListProperty<MyTreeNode> *property, MyTreeNode *value)
{
MyTreeNode *parent = (qobject_cast<MyTreeNode *>(property->object));
value->setParentNode(parent);
parent->insertNode(value, -1);
}
int count_element(QQmlListProperty<MyTreeNode> *property)
{
MyTreeNode *parent = (qobject_cast<MyTreeNode *>(property->object));
return parent->count();
}
MyTreeNode *at_element(QQmlListProperty<MyTreeNode> *property, int index)
{
MyTreeNode *parent = (qobject_cast<MyTreeNode *>(property->object));
if(index < 0 || index >= parent->count())
return nullptr;
return parent->childNode(index);
}
void clear_element(QQmlListProperty<MyTreeNode> *property)
{
MyTreeNode *parent = (qobject_cast<MyTreeNode *>(property->object));
parent->clear();
}
現在讓我們宣告模型類:
class MyTreeModel : public QAbstractItemModel
{
Q_OBJECT
public:
Q_PROPERTY(QQmlListProperty<MyTreeNode> nodes READ nodes)
Q_PROPERTY(QVariantList roles READ roles WRITE setRoles NOTIFY rolesChanged)
Q_CLASSINFO("DefaultProperty", "nodes")
MyTreeModel(QObject *parent = Q_NULLPTR);
~MyTreeModel();
QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE;
Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE;
int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
QQmlListProperty<MyTreeNode> nodes();
QVariantList roles() const;
void setRoles(const QVariantList &roles);
Q_INVOKABLE MyTreeNode * getNodeByIndex(const QModelIndex &index);
Q_INVOKABLE QModelIndex getIndexByNode(MyTreeNode *node);
Q_INVOKABLE bool insertNode(MyTreeNode *childNode, const QModelIndex &parent = QModelIndex(), int pos = (-1));
protected:
MyTreeNode *getNode(const QModelIndex &index) const;
private:
MyTreeNode *m_rootNode;
QHash<int, QByteArray> m_roles;
signals:
void rolesChanged();
};
由於我們從抽象 QAbstractItemModel 派生出模型類,我們必須重新定義下一個函式: data()
, flags()
, index()
, parent()
, columnCount()
和 rowCount()
。為了使我們的模型能夠與 QML
一起工作,我們定義了 roleNames()
。此外,與節點類一樣,我們定義了預設屬性,以便能夠在 QML
中向模型新增節點。roles
屬性將包含角色名稱列表。
實施:
MyTreeModel::MyTreeModel(QObject *parent) :
QAbstractItemModel(parent)
{
m_rootNode = new MyTreeNode(nullptr);
}
MyTreeModel::~MyTreeModel()
{
delete m_rootNode;
}
QHash<int, QByteArray> MyTreeModel::roleNames() const
{
return m_roles;
}
QVariant MyTreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
MyTreeNode *item = static_cast<MyTreeNode*>(index.internalPointer());
QByteArray roleName = m_roles[role];
QVariant name = item->property(roleName.data());
return name;
}
Qt::ItemFlags MyTreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return 0;
return QAbstractItemModel::flags(index);
}
QModelIndex MyTreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
MyTreeNode *parentItem = getNode(parent);
MyTreeNode *childItem = parentItem->childNode(row);
if (childItem)
return createIndex(row, column, childItem);
else
return QModelIndex();
}
QModelIndex MyTreeModel::parent(const QModelIndex &index) const
{
if (!index.isValid())
return QModelIndex();
MyTreeNode *childItem = static_cast<MyTreeNode*>(index.internalPointer());
MyTreeNode *parentItem = static_cast<MyTreeNode *>(childItem->parentNode());
if (parentItem == m_rootNode)
return QModelIndex();
return createIndex(parentItem->pos(), 0, parentItem);
}
int MyTreeModel::rowCount(const QModelIndex &parent) const
{
if (parent.column() > 0)
return 0;
MyTreeNode *parentItem = getNode(parent);
return parentItem->count();
}
int MyTreeModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
QQmlListProperty<MyTreeNode> MyTreeModel::nodes()
{
return m_rootNode->nodes();
}
QVariantList MyTreeModel::roles() const
{
QVariantList list;
QHashIterator<int, QByteArray> i(m_roles);
while (i.hasNext()) {
i.next();
list.append(i.value());
}
return list;
}
void MyTreeModel::setRoles(const QVariantList &roles)
{
static int nextRole = Qt::UserRole + 1;
foreach(auto role, roles) {
m_roles.insert(nextRole, role.toByteArray());
nextRole ++;
}
}
MyTreeNode *MyTreeModel::getNodeByIndex(const QModelIndex &index)
{
if(!index.isValid())
return nullptr;
return static_cast<MyTreeNode *>(index.internalPointer());
}
QModelIndex MyTreeModel::getIndexByNode(MyTreeNode *node)
{
QVector<int> positions;
QModelIndex result;
if(node) {
do
{
int pos = node->pos();
positions.append(pos);
node = node->parentNode();
} while(node != nullptr);
for (int i = positions.size() - 2; i >= 0 ; i--)
{
result = index(positions[i], 0, result);
}
}
return result;
}
bool MyTreeModel::insertNode(MyTreeNode *childNode, const QModelIndex &parent, int pos)
{
MyTreeNode *parentElement = getNode(parent);
if(pos >= parentElement->count())
return false;
if(pos < 0)
pos = parentElement->count();
childNode->setParentNode(parentElement);
beginInsertRows(parent, pos, pos);
bool retValue = parentElement->insertNode(childNode, pos);
endInsertRows();
return retValue;
}
MyTreeNode *MyTreeModel::getNode(const QModelIndex &index) const
{
if(index.isValid())
return static_cast<MyTreeNode *>(index.internalPointer());
return m_rootNode;
}
通常,此程式碼與標準實現沒有太大區別,例如 Simple tree 示例
我們提供了一種從 QML
開始的方法,而不是在 C++
中定義角色。 TreeView 事件和方法基本上與 QModelIndex 一起使用 。我個人認為將它傳遞給 qml 沒有多大意義,因為你唯一可以做的就是將它傳遞給模型。
無論如何,我們的類提供了一種將索引轉換為節點的方法,反之亦然。為了能夠在 QML 中使用我們的類,我們需要註冊它:
qmlRegisterType<MyTreeModel>("qt.test", 1, 0, "TreeModel");
qmlRegisterType<MyTreeNode>("qt.test", 1, 0, "TreeElement");
最好的例子,我們如何在 QML
中使用我們的 TreeView 模型:
import QtQuick 2.7
import QtQuick.Window 2.2
import QtQuick.Dialogs 1.2
import qt.test 1.0
Window {
visible: true
width: 800
height: 800
title: qsTr("Tree example")
Component {
id: fakePlace
TreeElement {
property string name: getFakePlaceName()
property string population: getFakePopulation()
property string type: "Fake place"
function getFakePlaceName() {
var rez = "";
for(var i = 0;i < Math.round(3 + Math.random() * 7);i ++) {
rez += String.fromCharCode(97 + Math.round(Math.random() * 25));
}
return rez.charAt(0).toUpperCase() + rez.slice(1);
}
function getFakePopulation() {
var num = Math.round(Math.random() * 100000000);
num = num.toString().split("").reverse().join("");
num = num.replace(/(\d{3})/g, '$1,');
num = num.split("").reverse().join("");
return num[0] === ',' ? num.slice(1) : num;
}
}
}
TreeModel {
id: treemodel
roles: ["name","population"]
TreeElement {
property string name: "Asia"
property string population: "4,164,252,000"
property string type: "Continent"
TreeElement {
property string name: "China";
property string population: "1,343,239,923"
property string type: "Country"
TreeElement { property string name: "Shanghai"; property string population: "20,217,700"; property string type: "City" }
TreeElement { property string name: "Beijing"; property string population: "16,446,900"; property string type: "City" }
TreeElement { property string name: "Chongqing"; property string population: "11,871,200"; property string type: "City" }
}
TreeElement {
property string name: "India";
property string population: "1,210,193,422"
property string type: "Country"
TreeElement { property string name: "Mumbai"; property string population: "12,478,447"; property string type: "City" }
TreeElement { property string name: "Delhi"; property string population: "11,007,835"; property string type: "City" }
TreeElement { property string name: "Bengaluru"; property string population: "8,425,970"; property string type: "City" }
}
TreeElement {
property string name: "Indonesia";
property string population: "248,645,008"
property string type: "Country"
TreeElement {property string name: "Jakarta"; property string population: "9,588,198"; property string type: "City" }
TreeElement {property string name: "Surabaya"; property string population: "2,765,487"; property string type: "City" }
TreeElement {property string name: "Bandung"; property string population: "2,394,873"; property string type: "City" }
}
}
TreeElement { property string name: "Africa"; property string population: "1,022,234,000"; property string type: "Continent" }
TreeElement { property string name: "North America"; property string population: "542,056,000"; property string type: "Continent" }
TreeElement { property string name: "South America"; property string population: "392,555,000"; property string type: "Continent" }
TreeElement { property string name: "Antarctica"; property string population: "4,490"; property string type: "Continent" }
TreeElement { property string name: "Europe"; property string population: "738,199,000"; property string type: "Continent" }
TreeElement { property string name: "Australia"; property string population: "29,127,000"; property string type: "Continent" }
}
TreeView {
anchors.fill: parent
model: treemodel
TableViewColumn {
title: "Name"
role: "name"
width: 200
}
TableViewColumn {
title: "Population"
role: "population"
width: 200
}
onDoubleClicked: {
var element = fakePlace.createObject(treemodel);
treemodel.insertNode(element, index, -1);
}
onPressAndHold: {
var element = treemodel.getNodeByIndex(index);
messageDialog.text = element.type + ": " + element.name + "\nPopulation: " + element.population;
messageDialog.open();
}
}
MessageDialog {
id: messageDialog
title: "Info"
}
}
雙擊新增節點,按住節點資訊。