БлогNot. QT: делаем редактируемое дерево строк с произвольным количеством столбцов у элем...

QT: делаем редактируемое дерево строк с произвольным количеством столбцов у элемента

В отличие от этого примера, мы не будем работать с "поэлементным" Item-Based виджетом Tree View, а делаем всё по "канонам MVC" с помощью Model-Based виджета Tree Widget. Это позволит нам отделить логическую модель данных от их внешего представления (вида). Я не изобретал "велосипед", а основывался вот на этих примерах из документации к QT5: простое дерево, редактируемое дерево.

Виджет QTreeModel создадим на основе шаблона QWidget.

Данные об узлах дерева будем хранить в обычном текстовом файле, который подключим к ресурсам проекта.

Чтобы добавить в проект ресурсы, нужно:

  • щёлкнув правой кнопкой на корневой папке проекта в окне "Проекты", выберем пункты Добавить новый – QT – Файл ресурсов QT, введём имя simpletreemodel.qrc и добавим файл в проект;
  • в редакторе ресурсов добавим корневой префикс "/":

Добавить префикс и затем файл ресурсов в QT Creator
Добавить префикс и затем файл ресурсов в QT Creator

После этого добавим в ресурсы файл с именем default.txt и следующим содержимым:

Узел 1                  :::  описание узла 1
 Узел 1-1               :::  описание узла 1-1
 Узел 1-2               :::  описание узла 1-2
Узел 2                  :::  описание узла 2
 Узел 2-1               :::  описание узла 2-1
  Узел 2-1-1            :::  описание узла 2-1-1
  Узел 2-1-2            :::  описание узла 2-1-2
Узел 3                  :::  описание узла 3

Здесь вложение узлов дерева показано пробельными отступами слева, а переход к новому столбцу – символами :::, кодировка файла – Юникод (UTF-8). Естественно, впоследствии мы предусмотрим в коде разбор этого формата файла.

Чтобы использовать данные ресурса, приложению достаточно инициализировать его по ранее указанному имени:

Q_INIT_RESOURCE(simpletreemodel);

и затем обратиться к нужному ресурсу, используя его префикс и имя файла:

":/default.txt"

Ресурсами могут быть файловые данные любых типов.

Наше дерево будет представлять собой список строк, некоторые из которых могут быть подчинены строкам, находящимся выше по тексту. Данные каждой строки при этом могут содержать несколько столбцов. Для моделирования отдельного элемента такой структуры данных разработаем и добавим в проект класс TreeItem:

Файл treeitem.h
#ifndef TREEITEM_H
#define TREEITEM_H

#include <QList>
#include <QVector>
#include <QVariant>

class TreeItem {
public:
 explicit TreeItem (const QVector<QVariant> &data,
 TreeItem *parentItem = 0); //Конструктор узла дерева
 ~TreeItem();               //...и деструктор

 void appendChild(TreeItem *child); //Добавить узел-потомок
 TreeItem *child(int row); //Вернуть дочерний элемент
 int childCount() const; //Количество дочерних элементов
 int columnCount() const; //Вернуть количество столбцов элемента
 QVariant data(int column) const; //Вернуть данные указанного столбца
 int childNumber() const; //Вернуть номер строки элемента

 TreeItem *parentItem(); //Вернуть родительский элемент
 bool insertChildren(int position, int count, int columns); //Вставить потомков (строки)
 bool insertColumns(int position, int columns); //Вставить столбцы
 bool removeChildren(int position, int count);  //Удалить потомков
 bool removeColumns(int position, int columns); //Удалить столбцы
 bool setData(int column, const QVariant &value); //Установить данные
private: //Внутреннее представление данных:
 QList <TreeItem*> m_childItems; //Список дочерних элементов
 QVector <QVariant> m_itemData; //Список данных текущего узла
 TreeItem *m_parentItem; //Ссылка на родительский узел
};
#endif // TREEITEM_H
Файл treeitem.cpp
#include "treeitem.h"

TreeItem::TreeItem (const QVector<QVariant> &data, TreeItem *parent) {
 //Конструктору узла нужно передать данные и ссылку на родителя
 m_parentItem = parent;
 m_itemData = data;
}

TreeItem::~TreeItem() { qDeleteAll(m_childItems); }

/*
 Методы класса служат, по сути, интерфейсом к
 соответствующим методам стандартного класса QVector:
*/

void TreeItem::appendChild(TreeItem *item) {
 m_childItems.append(item);
 //Добавить узел в список потомков
}

TreeItem *TreeItem::child (int row) {
 return m_childItems.value(row);
 //По номеру строки выбрать нужного потомка из списка
}

int TreeItem::childCount() const {
 return m_childItems.count();
 //Количество потомков узла = длине списка потомков
}

int TreeItem::columnCount() const {
 return m_itemData.count();
 //Количество столбцов в узле = длине списка данных узла
}

QVariant TreeItem::data (int column) const {
 return m_itemData.value(column);
 //Взять данные из нужного столбца
}

TreeItem *TreeItem::parentItem() {
 return m_parentItem; //Вернуть ссылку на родителя
}

int TreeItem::childNumber() const {
 //Если есть родитель - найти свой номер в списке его потомков
 if (m_parentItem) return m_parentItem->m_childItems.indexOf(const_cast<TreeItem*>(this));
 return 0; //Иначе вернуть 0
}

/*
 Следующие 4 метода просто управляют контейнерами класса m_childItems и m_itemData,
 предназначенными для хранения данных
*/

bool TreeItem::insertChildren(int position, int count, int columns) {
 if (position < 0 || position > m_childItems.size()) return false;
 for (int row = 0; row < count; ++row) {
  QVector<QVariant> data(columns);
  TreeItem *item = new TreeItem(data, this);
  m_childItems.insert(position, item);
 }
 return true;
}

bool TreeItem::insertColumns(int position, int columns) {
 if (position < 0 || position > m_itemData.size()) return false;
 for (int column = 0; column < columns; ++column) m_itemData.insert(position, QVariant());
 foreach (TreeItem *child, m_childItems) child->insertColumns(position, columns);
 return true;
}

bool TreeItem::removeChildren(int position, int count) {
 if (position < 0 || position + count > m_childItems.size()) return false;
 for (int row = 0; row < count; ++row) delete m_childItems.takeAt(position);
 return true;
}

bool TreeItem::removeColumns(int position, int columns) {
 if (position < 0 || position + columns > m_itemData.size()) return false;
 for (int column = 0; column < columns; ++column) m_itemData.removeAt(position);
 foreach (TreeItem *child, m_childItems) child->removeColumns(position, columns);
 return true;
}

//А этот метод ставит значение value в столбец column элемента:
bool TreeItem::setData(int column, const QVariant &value) {
 if (column < 0 || column >= m_itemData.size()) return false;
 m_itemData[column] = value;
 return true;
}

За модель (а именно, за предоставление модельных индексов) будет отвечать класс TreeModel. Его нужно сделать потомком QAbstractItemModel, содержащего прототипы всех методов, нужных для работы с моделью элемента данных.

Файл treemodel.h
#ifndef TREEMODEL_H
#define TREEMODEL_H
#include <QAbstractItemModel>
#include <QModelIndex>
#include "treeitem.h"

class TreeModel : public QAbstractItemModel {
 Q_OBJECT
public:
 TreeModel(const QStringList &headers, const QString &data, QObject *parent = 0);
 ~TreeModel();
 /*
  Уточняем заголовки методов правильными ключевыми словами C++:
  const - функция не меняет объект, для которого вызывается
  override - функция переопределяет вирутальный метод базового класса
 */
 QVariant data(const QModelIndex &index, int role) const override;
  //получить данные из модельного индекса index с ролью role
 Qt::ItemFlags flags(const QModelIndex &index) const override;
  //получить флаги выбора
 QVariant headerData(int section, Qt::Orientation orientation,
  int role = Qt::DisplayRole) const override;
  //получить данные заголовка
 QModelIndex index(int row, int column,
  const QModelIndex &parent = QModelIndex()) const override;
  //получить модельный индекс по строке и столбцу
 QModelIndex parent(const QModelIndex &index) const override;
  //получить модельный индекс родителя
 int rowCount(const QModelIndex &parent = QModelIndex()) const override;
 int columnCount(const QModelIndex &parent = QModelIndex()) const override;
  //получить количество строк и столбцов для элемента с заданным модельным индексом
 bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
  //установить данные узла с индексом index в значение value
 bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value,
  int role = Qt::EditRole) override;
  //установить данные заголовка столбца
 bool insertColumns(int position, int columns,const QModelIndex &parent = QModelIndex()) override;
 bool removeColumns(int position, int columns, const QModelIndex &parent = QModelIndex()) override;
 bool insertRows(int position, int rows, const QModelIndex &parent = QModelIndex()) override;
 bool removeRows(int position, int rows, const QModelIndex &parent = QModelIndex()) override;
  //вставка и удаление столбцов и строк
private:
 void setupModelData(const QStringList &lines, TreeItem *parent);
  //внутренний метод для установки данных модели
 TreeItem *getItem(const QModelIndex &index) const;
  //внутренний метод для получения элемента
 TreeItem *rootItem; //ссылка на корневой узел
};
#endif // TREEMODEL_H
Файл treemodel.cpp
#include "treeitem.h"
#include "treemodel.h"

TreeModel::TreeModel(const QStringList &headers, const QString &data, QObject *parent)
    : QAbstractItemModel(parent) {
 QVector <QVariant> rootData;
 rootData << "Узел" << "Описание"; //В модели будет 2 столбца
 rootItem = new TreeItem(rootData);
  //Создали корневой элемент
 setupModelData(data.split(QString("\n")), rootItem);
  //Данные о строках модели разделены переводом строки
}

TreeModel::~TreeModel() { delete rootItem; }

int TreeModel::columnCount(const QModelIndex &parent) const {
 return rootItem->columnCount();
}

QVariant TreeModel::data (const QModelIndex &index, int role) const {
 if (!index.isValid()) return QVariant();
 if (role != Qt::DisplayRole  && role != Qt::EditRole) return QVariant();
 TreeItem *item = getItem(index);
 return item->data(index.column());
}

TreeItem *TreeModel::getItem(const QModelIndex &index) const {
 if (index.isValid()) {
  TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
  if (item) return item;
 }
 return rootItem;
}

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const {
 if (!index.isValid()) return 0;
 return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}

QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const {
 if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return rootItem->data(section);
 return QVariant();
}

QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const {
 if (parent.isValid() && parent.column() != 0) return QModelIndex();
 TreeItem *parentItem = getItem(parent);
 TreeItem *childItem = parentItem->child(row);
 if (childItem) return createIndex(row, column, childItem);
 else return QModelIndex();
}

QModelIndex TreeModel::parent(const QModelIndex &index) const {
 if (!index.isValid()) return QModelIndex();
 TreeItem *childItem = getItem(index);
 TreeItem *parentItem = childItem->parentItem();
 if (parentItem == rootItem) return QModelIndex();
 return createIndex(parentItem->childNumber(), 0, parentItem);
}

int TreeModel::rowCount(const QModelIndex &parent) const {
 TreeItem *parentItem = getItem(parent);
 return parentItem->childCount();
}

void TreeModel::setupModelData(const QStringList &lines, TreeItem *parent) {
 QList<TreeItem*> parents;
 QList<int> indentations;
 parents << parent;
 indentations << 0;
 int number = 0;
 //Ищем первый непробельный символ с номером position
 while (number < lines.count()) {
  int position = 0;
  while (position < lines[number].length()) {
  if (lines[number].at(position) != ' ') break;
  position++;
 }
 //Отрезаем пробельное начало строки
 QString lineData = lines[number].mid(position).trimmed();
 if (!lineData.isEmpty()) {
  //Читаем остальную часть строки, если она есть
  QStringList columnStrings = lineData.split(":::", QString::SkipEmptyParts);
   //Учитываем разделитель столбцов
  QVector <QVariant> columnData; //Список данных столбцов
  for (int column = 0; column < columnStrings.count(); ++column)
   columnData << columnStrings[column];
  if (position > indentations.last()) {
   //Последний потомок текущего родителя теперь будет новым родителем,
   //пока у текущего родителя нет потомков
   if (parents.last()->childCount() > 0) {
    parents << parents.last()->child(parents.last()->childCount()-1);
    indentations << position;
   }
  }
  else {
   while (position < indentations.last() && parents.count() > 0) {
    parents.pop_back();
    indentations.pop_back();
   }
  }
  //Добавить новый узел в список потомков текущего родителя
  parents.last()->appendChild(new TreeItem(columnData, parents.last()));
 }
 ++number;
 }
}

bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role) {
 if (role != Qt::EditRole) return false;
 TreeItem *item = getItem(index);
 bool result = item->setData(index.column(), value);
 if (result) {
  emit dataChanged(index, index);
 }
 return result;
}

bool TreeModel::setHeaderData(int section, Qt::Orientation orientation,
                              const QVariant &value, int role) {
 if (role != Qt::EditRole || orientation != Qt::Horizontal) return false;
 bool result = rootItem->setData(section, value);
 if (result) {
  emit headerDataChanged(orientation, section, section);
 }
 return result;
}

bool TreeModel::insertColumns(int position, int columns, const QModelIndex &parent) {
 bool success;
 beginInsertColumns(parent, position, position + columns - 1);
 success = rootItem->insertColumns(position, columns);
 endInsertColumns();
 return success;
}

bool TreeModel::insertRows(int position, int rows, const QModelIndex &parent) {
 TreeItem *parentItem = getItem(parent);
 bool success;
 beginInsertRows(parent, position, position + rows - 1);
 success = parentItem->insertChildren(position, rows, rootItem->columnCount());
 endInsertRows();
 return success;
}

bool TreeModel::removeColumns(int position, int columns, const QModelIndex &parent) {
 bool success;
 beginRemoveColumns(parent, position, position + columns - 1);
 success = rootItem->removeColumns(position, columns);
 endRemoveColumns();
 if (rootItem->columnCount() == 0) removeRows(0, rowCount());
 return success;
}

bool TreeModel::removeRows(int position, int rows, const QModelIndex &parent) {
 TreeItem *parentItem = getItem(parent);
 bool success = true;
 beginRemoveRows(parent, position, position + rows - 1);
 success = parentItem->removeChildren(position, rows);
 endRemoveRows();
 return success;
}

Основной виджет предоставит слоты для действий, выполняемых по нажатию кнопок и реализует обновление их состояния. Интерфейс приложения выполнен во встроенном дизайнере форм, вот как он выглядит:

Интерфейс приложения QT для редактирования дерева строк
Интерфейс приложения QT для редактирования дерева строк
Файл widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QtWidgets>
#include <QFile>
#include <QDir>

namespace Ui { class Widget; }
class Widget : public QWidget {
    Q_OBJECT
    Ui::Widget *ui;
public:
    Widget(QWidget *parent = 0);
    void updateActions(); //слот для обновления состояния кнопок
private slots: //слоты для действий, выполняемых по кнопкам
    void insertChild();
    bool insertColumn();
    void insertRow();
    bool removeColumn();
    void removeRow();
public slots: //для реализации сигнала selectionChanged у QTreeView::selectionModel
    void updateActions(const QItemSelection &,const QItemSelection &);
};
#endif // WIDGET_H
Файл widget.cpp
#include "widget.h"
#include "treemodel.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
 ui->setupUi(this);
 //Инициализируем ресурсы:
 Q_INIT_RESOURCE(simpletreemodel);
 //Получаем предустановленное "дерево" в file:
 QFile file(":/default.txt");
 file.open(QIODevice::ReadOnly);
 //Создаем заголовки столбцов:
 QStringList headers;
 headers << tr("Заголовок") << tr("Описание");
 //Загружаем данные в модель:
 TreeModel *model = new TreeModel(headers, file.readAll());
 file.close();
 ui->treeView->setModel(model);
 for (int column = 0; column < model->columnCount(); ++column) ui->treeView->resizeColumnToContents(column);

 //Осталось соединить сигналы со слотами:
 connect(ui->treeView->selectionModel(),SIGNAL(selectionChanged(const QItemSelection&,const QItemSelection&)),
         this, SLOT(updateActions(const QItemSelection&,const QItemSelection&)));
 connect(ui->insertRowAction,SIGNAL(clicked()),this,SLOT(insertRow()));
 connect(ui->insertColumnAction,SIGNAL(clicked()),this,SLOT(insertColumn()));
 connect(ui->removeRowAction,SIGNAL(clicked()),this,SLOT(removeRow()));
 connect(ui->removeColumnAction,SIGNAL(clicked()),this,SLOT(removeColumn()));
 connect(ui->insertChildAction,SIGNAL(clicked()),this,SLOT(insertChild()));
 //и обновить состояние кнопок:
 updateActions();
}

void Widget::insertChild() {
 //Получаем модельный индекс и модель элемента:
 QModelIndex index = ui->treeView->selectionModel()->currentIndex();
 QAbstractItemModel *model = ui->treeView->model();
 //Вставляем данные:
 if (model->columnCount(index) == 0) {
  if (!model->insertColumn(0, index)) return;
 }
 if (!model->insertRow(0, index)) return;
 //Инициализируем их:
 for (int column = 0; column < model->columnCount(index); ++column) {
  QModelIndex child = model->index(0, column, index);
  model->setData(child, QVariant("Данные"), Qt::EditRole);
  if (!model->headerData(column, Qt::Horizontal).isValid())
   model->setHeaderData(column, Qt::Horizontal, QVariant("Столбец"), Qt::EditRole);
  }
 //Выбираем вставленный узел:
 ui->treeView->selectionModel()->setCurrentIndex(model->index(0, 0, index),
  QItemSelectionModel::ClearAndSelect);
 //Меняем состояние кнопок:
 updateActions();
}

bool Widget::insertColumn() {
 QAbstractItemModel *model = ui->treeView->model();
 int column = ui->treeView->selectionModel()->currentIndex().column();
 bool changed = model->insertColumn(column + 1);
 if (changed)
  model->setHeaderData(column + 1, Qt::Horizontal, QVariant("Столбец"), Qt::EditRole);
 updateActions();
 return changed;
}

void Widget::insertRow() {
 QModelIndex index = ui->treeView->selectionModel()->currentIndex();
 QAbstractItemModel *model = ui->treeView->model();
 if (!model->insertRow(index.row()+1, index.parent())) return;
 updateActions();
 for (int column = 0; column < model->columnCount(index.parent()); ++column) {
  QModelIndex child = model->index(index.row()+1, column, index.parent());
  model->setData(child, QVariant("Данные"), Qt::EditRole);
 }
}

bool Widget::removeColumn() {
 QAbstractItemModel *model = ui->treeView->model();
 int column = ui->treeView->selectionModel()->currentIndex().column();
 bool changed = model->removeColumn(column); //Удалить столбец для каждого потомка
 if (changed) updateActions();
 return changed;
}

void Widget::removeRow() {
 QModelIndex index = ui->treeView->selectionModel()->currentIndex();
 QAbstractItemModel *model = ui->treeView->model();
 if (model->removeRow(index.row(), index.parent())) updateActions();
}

void Widget::updateActions(const QItemSelection &selected,const QItemSelection &deselected) {
 updateActions();
}

void Widget::updateActions() {
 //Обновим состояние кнопок:
 bool hasSelection = !ui->treeView->selectionModel()->selection().isEmpty();
 ui->removeRowAction->setEnabled(hasSelection);
 ui->removeColumnAction->setEnabled(hasSelection);
 bool hasCurrent = ui->treeView->selectionModel()->currentIndex().isValid();
 ui->insertRowAction->setEnabled(hasCurrent);
 ui->insertColumnAction->setEnabled(hasCurrent);
 //Покажем информацию в заголовке окна:
 if (hasCurrent) {
  ui->treeView->closePersistentEditor(ui->treeView->selectionModel()->currentIndex());
  int row = ui->treeView->selectionModel()->currentIndex().row();
  int column = ui->treeView->selectionModel()->currentIndex().column();
  if (ui->treeView->selectionModel()->currentIndex().parent().isValid())
   this->setWindowTitle(tr("(row,col)=(%1,%2)").arg(row).arg(column));
  else
   this->setWindowTitle(tr("(row,col)=(%1,%2) ВЕРХ").arg(row).arg(column));
 }
}

Главный файл main.cpp, а также сам файл проекта полностью шаблонны, не привожу их текстом. Вот вид приложения в работе и проект в архиве.

Вид окна виджета QTreeModel
Вид окна виджета QTreeModel

 Скачать папку проекта QT5 QTreeModel в архиве .zip (9 Кб)

02.05.2017, 15:44 [17255 просмотров]


теги: textprocessing c++ программирование список qt

К этой статье пока нет комментариев, Ваш будет первым