Ejemplo de modelo de árbol editable
Este ejemplo muestra cómo implementar un sencillo modelo de árbol basado en ítems que puede utilizarse con otras clases en el marco modelo/vista.

El modelo admite elementos editables, cabeceras personalizadas y la posibilidad de insertar y eliminar filas y columnas. Con estas características, también es posible insertar nuevos elementos hijo, y esto se muestra en el código de ejemplo de apoyo.
Visión general
Como se describe en la Referencia de subclases de modelos, los modelos deben proporcionar implementaciones para el conjunto estándar de funciones de modelo: flags(), data(), headerData(), columnCount() y rowCount(). Además, los modelos jerárquicos, como éste, deben proporcionar implementaciones de index() y parent().
Un modelo editable necesita proporcionar implementaciones de setData() y setHeaderData(), y debe devolver una combinación adecuada de banderas desde su función flags().
Dado que este ejemplo permite cambiar las dimensiones del modelo, también debemos implementar insertRows(), insertColumns(), removeRows(), y removeColumns().
Diseño
Al igual que en el ejemplo del modelo de árbol simple, el modelo actúa simplemente como una envoltura alrededor de una colección de instancias de una clase TreeItem. Cada TreeItem está diseñado para contener los datos de una fila de elementos en una vista de árbol, por lo que contiene una lista de valores correspondientes a los datos mostrados en cada columna.
Dado que QTreeView proporciona una vista orientada a filas sobre un modelo, es natural elegir un diseño orientado a filas para las estructuras de datos que suministrarán datos a través de un modelo a este tipo de vista. Aunque esto hace que el modelo de árbol sea menos flexible, y posiblemente menos útil para su uso con vistas más sofisticadas, lo hace menos complejo de diseñar y más fácil de implementar.
![]() | Relaciones entre elementos internos Cuando se diseña una estructura de datos para su uso con un modelo personalizado, es útil exponer el padre de cada elemento a través de una función como TreeItem::parent() porque facilitará la escritura de la propia función parent() del modelo. Del mismo modo, una función como TreeItem::child() es útil cuando se implementa la función index() del modelo. Como resultado, cada El diagrama muestra cómo se conectan las instancias de En el ejemplo mostrado, dos elementos de nivel superior, A y B, pueden obtenerse a partir del elemento raíz llamando a su función child(), y cada uno de estos elementos devuelve el nodo raíz desde sus funciones parent(), aunque esto sólo se muestra para el elemento A. |
Cada TreeItem almacena los datos de cada columna de la fila que representa en su miembro privado itemData (una lista de objetos QVariant ). Dado que existe un mapeo uno a uno entre cada columna de la vista y cada entrada de la lista, proporcionamos una función simple data() para leer las entradas de la lista itemData y una función setData() para permitir su modificación. Al igual que con otras funciones del elemento, esto simplifica la implementación de las funciones data() y setData() del modelo.
Colocamos un ítem en la raíz del árbol de ítems. Este elemento raíz corresponde al índice nulo del modelo, QModelIndex(), que se utiliza para representar al padre de un elemento de nivel superior cuando se manejan índices del modelo. Aunque el elemento raíz no tiene una representación visible en ninguna de las vistas estándar, utilizamos su lista interna de objetos QVariant para almacenar una lista de cadenas que se pasarán a las vistas para su uso como títulos de cabecera horizontales.
![]() | Acceso a los datos a través del modelo En el caso que se muestra en el diagrama, la información representada por a puede obtenerse utilizando la API modelo/vista estándar: QVariant a = model->index(0, 0, QModelIndex()).data(); Dado que cada elemento contiene datos para cada columna de una fila determinada, puede haber muchos índices de modelo que se correspondan con el mismo objeto QVariant b = model->index(1, 0, QModelIndex()).data(); Se accedería al mismo |
En la clase modelo, TreeModel, relacionamos los objetos TreeItem con los índices modelo pasando un puntero para cada elemento cuando creamos su correspondiente índice modelo con QAbstractItemModel::createIndex() en nuestras implementaciones de index() y parent(). Podemos recuperar los punteros almacenados de esta forma llamando a la función internalPointer() en el índice del modelo correspondiente - creamos nuestra propia función getItem() para que haga el trabajo por nosotros, y la llamamos desde nuestras implementaciones data() y parent().
Almacenar punteros a elementos es conveniente cuando controlamos cómo se crean y destruyen, ya que podemos asumir que una dirección obtenida de internalPointer() es un puntero válido. Sin embargo, algunos modelos necesitan manejar elementos que se obtienen de otros componentes de un sistema, y en muchos casos no es posible controlar completamente cómo se crean o destruyen los elementos. En tales situaciones, un enfoque basado puramente en punteros debe complementarse con salvaguardas que garanticen que el modelo no intenta acceder a elementos que han sido eliminados.
| Almacenamiento de información en la estructura de datos subyacente Varios datos se almacenan como objetos QVariant en el miembro El diagrama muestra cómo las piezas de información, representadas por las etiquetas a, b y c en los dos diagramas anteriores, se almacenan en los elementos A, B y C de la estructura de datos subyacente. Obsérvese que los datos de una misma fila del modelo se obtienen todos del mismo elemento. Cada elemento de una lista corresponde a una información expuesta por cada columna de una fila determinada del modelo. | ![]() |
Dado que la implementación de TreeModel se ha diseñado para su uso con QTreeView, hemos añadido una restricción a la forma en que utiliza las instancias de TreeItem: cada elemento debe exponer el mismo número de columnas de datos. Esto hace que la visualización del modelo sea coherente, permitiéndonos utilizar el elemento raíz para determinar el número de columnas de cualquier fila, y sólo añade el requisito de que creemos elementos que contengan suficientes datos para el número total de columnas. Como resultado, insertar y eliminar columnas son operaciones que llevan mucho tiempo, ya que tenemos que recorrer todo el árbol para modificar cada elemento.
Un enfoque alternativo sería diseñar la clase TreeModel de forma que trunque o amplíe la lista de datos en instancias individuales de TreeItem a medida que se modifican los elementos de datos. Sin embargo, este enfoque de redimensionamiento "perezoso" sólo nos permitiría insertar y eliminar columnas al final de cada fila y no permitiría insertar o eliminar columnas en posiciones arbitrarias de cada fila.
![]() | Relacionar elementos mediante índices del modelo Al igual que en el ejemplo del modelo de árbol simple, En el diagrama se muestra cómo la implementación de parent() del modelo obtiene el índice del modelo correspondiente al padre de un elemento suministrado por el llamante, utilizando los elementos mostrados en un diagrama anterior. A partir del índice del modelo correspondiente se obtiene un puntero al elemento C mediante la función QModelIndex::internalPointer(). El puntero se almacenó internamente en el índice cuando se creó. Dado que el hijo contiene un puntero a su padre, utilizamos su función parent() para obtener un puntero al elemento B. El índice del modelo padre se crea utilizando la función QAbstractItemModel::createIndex(), pasando el puntero al elemento B como puntero interno. |
Definición de la clase TreeItem
La clase TreeItem proporciona elementos simples que contienen varios datos, incluyendo información sobre sus elementos padre e hijo:
class TreeItem { public: explicit TreeItem(QVariantList data, TreeItem *parent = nullptr); TreeItem *child(int number); int childCount() const; int columnCount() const; QVariant data(int column) const; bool insertChildren(int position, int count, int columns); bool insertColumns(int position, int columns); TreeItem *parent(); bool removeChildren(int position, int count); bool removeColumns(int position, int columns); int row() const; bool setData(int column, const QVariant &value); private: std::vector<std::unique_ptr<TreeItem>> m_childItems; QVariantList itemData; TreeItem *m_parentItem; };
Hemos diseñado la API para que sea similar a la proporcionada por QAbstractItemModel, dotando a cada elemento de funciones para devolver el número de columnas de información, leer y escribir datos, e insertar y eliminar columnas. Sin embargo, hacemos explícita la relación entre elementos proporcionando funciones para tratar con "hijos" en lugar de "filas".
Cada elemento contiene una lista de punteros a elementos hijos, un puntero a su elemento padre y una lista de objetos QVariant que corresponden a la información contenida en las columnas de una fila determinada del modelo.
Implementación de la clase TreeItem
Cada TreeItem se construye con una lista de datos y un elemento padre opcional:
TreeItem::TreeItem(QVariantList data, TreeItem *parent) : itemData(std::move(data)), m_parentItem(parent) {}
Inicialmente, cada elemento no tiene hijos. Éstos se añaden al miembro interno childItems del elemento mediante la función insertChildren() que se describe más adelante.
Los hijos se almacenan en std::unique_ptr para garantizar que cada hijo añadido al elemento se elimina cuando se elimina el propio elemento.
Dado que cada elemento almacena un puntero a su padre, la función parent() es trivial:
TreeItem *TreeItem::parent() { return m_parentItem; }
Tres funciones proporcionan información sobre los hijos de un elemento. child() devuelve un hijo específico de la lista interna de hijos:
TreeItem *TreeItem::child(int number) { return (number >= 0 && number < childCount()) ? m_childItems.at(number).get() : nullptr; }
La función childCount() devuelve el número total de hijos:
int TreeItem::childCount() const { return int(m_childItems.size()); }
La función row() se utiliza para determinar el índice del hijo en la lista de hijos de su padre. Para obtener esta información, accede directamente al miembro childItems del padre:
int TreeItem::row() const { if (!m_parentItem) return 0; const auto it = std::find_if(m_parentItem->m_childItems.cbegin(), m_parentItem->m_childItems.cend(), [this](const std::unique_ptr<TreeItem> &treeItem) { return treeItem.get() == this; }); if (it != m_parentItem->m_childItems.cend()) return std::distance(m_parentItem->m_childItems.cbegin(), it); Q_ASSERT(false); // should not happen return -1; }
El elemento raíz no tiene elemento padre; para este elemento, devolvemos cero para ser coherentes con los demás elementos.
La función columnCount() simplemente devuelve el número de elementos de la lista interna itemData de objetos QVariant:
int TreeItem::columnCount() const { return int(itemData.count()); }
Los datos se recuperan utilizando la función data(), que accede al elemento apropiado de la lista itemData:
QVariant TreeItem::data(int column) const { return itemData.value(column); }
Los datos se establecen mediante la función setData(), que sólo almacena valores en la lista itemData para índices de lista válidos, correspondientes a valores de columna en el modelo:
bool TreeItem::setData(int column, const QVariant &value) { if (column < 0 || column >= itemData.size()) return false; itemData[column] = value; return true; }
Para facilitar la implementación del modelo, devolvemos true para indicar que los datos se han establecido correctamente.
Los modelos editables a menudo necesitan ser redimensionables, permitiendo insertar y eliminar filas y columnas. La inserción de filas por debajo de un índice dado en el modelo conlleva la inserción de nuevos elementos hijo en el elemento correspondiente, gestionada por la función insertChildren():
bool TreeItem::insertChildren(int position, int count, int columns) { if (position < 0 || position > qsizetype(m_childItems.size())) return false; for (int row = 0; row < count; ++row) { QVariantList data(columns); m_childItems.insert(m_childItems.cbegin() + position, std::make_unique<TreeItem>(data, this)); } return true; }
Esto garantiza que los nuevos elementos se crean con el número de columnas requerido y se insertan en una posición válida en la lista interna childItems. Los elementos se eliminan con la función removeChildren():
bool TreeItem::removeChildren(int position, int count) { if (position < 0 || position + count > qsizetype(m_childItems.size())) return false; for (int row = 0; row < count; ++row) m_childItems.erase(m_childItems.cbegin() + position); return true; }
Como se ha comentado anteriormente, las funciones para insertar y eliminar columnas se utilizan de forma diferente a las funciones para insertar y eliminar elementos hijos, ya que se espera que se llamen en cada elemento del árbol. Para ello, se llama recursivamente a esta función en cada elemento hijo:
bool TreeItem::insertColumns(int position, int columns) { if (position < 0 || position > itemData.size()) return false; for (int column = 0; column < columns; ++column) itemData.insert(position, QVariant()); for (auto &child : std::as_const(m_childItems)) child->insertColumns(position, columns); return true; }
Definición de la clase TreeModel
La clase TreeModel proporciona una implementación de la clase QAbstractItemModel, exponiendo la interfaz necesaria para un modelo que puede ser editado y redimensionado.
class TreeModel : public QAbstractItemModel { Q_OBJECT public: Q_DISABLE_COPY_MOVE(TreeModel) TreeModel(const QStringList &headers, const QString &data, QObject *parent = nullptr); ~TreeModel() override;
El constructor y el destructor son específicos de este modelo.
QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = {}) const override; int columnCount(const QModelIndex &parent = {}) const override;
Los modelos de árbol de sólo lectura sólo necesitan proporcionar las funciones anteriores. Las siguientes funciones públicas permiten editar y cambiar el tamaño:
Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::EditRole) override; bool insertColumns(int position, int columns, const QModelIndex &parent = {}) override; bool removeColumns(int position, int columns, const QModelIndex &parent = {}) override; bool insertRows(int position, int rows, const QModelIndex &parent = {}) override; bool removeRows(int position, int rows, const QModelIndex &parent = {}) override; private: void setupModelData(const QList<QStringView> &lines); TreeItem *getItem(const QModelIndex &index) const; std::unique_ptr<TreeItem> rootItem; };
Para simplificar este ejemplo, los datos expuestos por el modelo se organizan en una estructura de datos mediante la función setupModelData() del modelo. Muchos modelos del mundo real no procesarán los datos en bruto en absoluto, sino que simplemente trabajarán con una estructura de datos existente o una API de biblioteca.
Implementación de la clase TreeModel
El constructor crea un elemento raíz y lo inicializa con los datos de cabecera suministrados:
TreeModel::TreeModel(const QStringList &headers, const QString &data, QObject *parent) : QAbstractItemModel(parent) { QVariantList rootData; for (const QString &header : headers) rootData << header; rootItem = std::make_unique<TreeItem>(rootData); setupModelData(QStringView{data}.split(u'\n')); }
Llamamos a la función interna setupModelData() para convertir los datos textuales suministrados en una estructura de datos que podamos utilizar con el modelo. Otros modelos pueden inicializarse con una estructura de datos ya preparada, o utilizar una API de una biblioteca que mantenga sus propios datos.
TreeModel::~TreeModel() = default;
El destructor sólo tiene que borrar el elemento raíz, lo que provocará que todos los elementos hijos se borren recursivamente. Esto lo hace automáticamente el destructor por defecto, ya que el elemento raíz se almacena dentro de un unique_ptr.
Dado que la interfaz del modelo con los otros componentes del modelo/vista se basa en índices de modelo, y dado que la estructura de datos interna se basa en ítems, muchas de las funciones implementadas por el modelo necesitan ser capaces de convertir cualquier índice de modelo dado en su correspondiente ítem. Por comodidad y coherencia, hemos definido una función getItem() para realizar esta tarea repetitiva:
TreeItem *TreeModel::getItem(const QModelIndex &index) const { if (index.isValid()) { if (auto *item = static_cast<TreeItem*>(index.internalPointer())) return item; } return rootItem.get(); }
Cada índice de modelo pasado a esta función debe corresponder a un elemento válido en memoria. Si el índice no es válido, o su puntero interno no se refiere a un elemento válido, se devuelve el elemento raíz en su lugar.
La implementación del modelo rowCount() es sencilla: primero utiliza la función getItem() para obtener el elemento correspondiente; después devuelve el número de hijos que contiene:
int TreeModel::rowCount(const QModelIndex &parent) const { if (parent.isValid() && parent.column() > 0) return 0; const TreeItem *parentItem = getItem(parent); return parentItem ? parentItem->childCount() : 0; }
Por el contrario, la implementación de columnCount() no necesita buscar un elemento en particular porque todos los elementos están definidos para tener el mismo número de columnas asociadas a ellos.
int TreeModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return rootItem->columnCount(); }
Como resultado, el número de columnas puede obtenerse directamente del elemento raíz.
Para que los elementos puedan editarse y seleccionarse, la función flags() debe implementarse de modo que devuelva una combinación de indicadores que incluya los indicadores Qt::ItemIsEditable y Qt::ItemIsSelectable, así como Qt::ItemIsEnabled:
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::NoItemFlags; return Qt::ItemIsEditable | QAbstractItemModel::flags(index); }
El modelo debe ser capaz de generar índices de modelo para permitir que otros componentes soliciten datos e información sobre su estructura. Esta tarea la realiza la función index(), que se utiliza para obtener los índices del modelo correspondientes a los hijos de un elemento padre dado:
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const { if (parent.isValid() && parent.column() != 0) return {};
En este modelo, sólo devolvemos índices de modelo para elementos hijos si el índice padre no es válido (correspondiente al elemento raíz) o si tiene un número de columna cero.
Utilizamos la función personalizada getItem() para obtener una instancia de TreeItem que corresponda al índice del modelo suministrado, y solicitamos su elemento hijo que corresponda a la fila especificada.
TreeItem *parentItem = getItem(parent); if (!parentItem) return {}; if (auto *childItem = parentItem->child(row)) return createIndex(row, column, childItem); return {}; }
Dado que cada elemento contiene información para toda una fila de datos, creamos un índice de modelo para identificarlo de forma única llamando a createIndex() con los números de fila y columna y un puntero al elemento. En la función data( ), utilizaremos el puntero al elemento y el número de columna para acceder a los datos asociados al índice del modelo; en este modelo, el número de fila no es necesario para identificar los datos.
La función parent() proporciona índices de modelo para los elementos padre encontrando el elemento correspondiente para un índice de modelo dado, utilizando su función parent() para obtener su elemento padre y, a continuación, creando un índice de modelo para representar al elemento padre. (Véase el diagrama anterior).
QModelIndex TreeModel::parent(const QModelIndex &index) const { if (!index.isValid()) return {}; TreeItem *childItem = getItem(index); TreeItem *parentItem = childItem ? childItem->parent() : nullptr; return (parentItem != rootItem.get() && parentItem != nullptr) ? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{}; }
Los elementos sin padre, incluido el elemento raíz, se tratan devolviendo un índice de modelo nulo. En caso contrario, se crea un índice modelo y se devuelve como en la función index(), con un número de fila adecuado, pero con un número de columna cero para ser coherente con el esquema utilizado en la implementación de index().
Prueba del modelo
Implementar correctamente un modelo de ítems puede ser todo un reto. La clase QAbstractItemModelTester del módulo Qt Test comprueba la coherencia del modelo, como la creación del índice del modelo y las relaciones padre-hijo.
Puedes probar tu modelo simplemente pasando una instancia del modelo al constructor de la clase, por ejemplo como parte de una prueba unitaria de Qt:
class TestEditableTreeModel : public QObject {Q_OBJECTprivate slots: void testTreeModel(); };void TestEditableTreeModel::testTreeModel() { constexpr auto fileName = ":/default.txt"_L1; QFile file(fileName); QVERIFY2(file.open(QIODevice::SóloLectura | QIODevice::Texto), qPrintable(fileName + " cannot be opened: "_L1 + file.errorString())); const QStringList headers{"columna1"_L1, "columna2"_L1}; TreeModel model(headers, QString::fromUtf8(file.readAll())); QAbstractItemModelTester tester(&model); } QTEST_APPLESS_MAIN(TestEditableTreeModel)#include "test.moc"
Para crear una prueba que se pueda ejecutar con el ejecutable ctest, se utiliza add_test():
# Unit Test
include(CTest)
qt_add_executable(editabletreemodel_tester
test.cpp
treeitem.cpp treeitem.h
treemodel.cpp treemodel.h)
target_link_libraries(editabletreemodel_tester PRIVATE
Qt6::Core
Qt6::Test
)
if(ANDROID)
target_link_libraries(editabletreemodel_tester PRIVATE
Qt6::Gui
)
endif()
qt_add_resources(editabletreemodel_tester "editabletreemodel_tester"
PREFIX
"/"
FILES
${editabletreemodel_resource_files}
)
add_test(NAME editabletreemodel_tester
COMMAND editabletreemodel_tester)© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.



