Вопрос по c++, qsqltablemodel, qt, model-view – Как обновить QSqlTableModel при сохранении выделения?

11

Я используюQSqlTableModel а такжеQTableView просмотреть таблицу базы данных SQLite.

Я хотел бы, чтобы таблица автоматически обновлялась каждую секунду или около того (она не будет очень большой таблицей - пара сотен строк). И я могу сделать это - вот так:

QTimer *updateInterval = new QTimer(this);
updateInterval->setInterval(1000);
updateInterval->start();
connect(updateInterval, SIGNAL(timeout()),this, SLOT(update_table()));

...

void MainWindow::update_table()
{
    model->select(); //QSqlTableModel*
    sqlTable->reset(); //QTableView*
}

Но это удаляет любой выбор, который у меня есть, так что выбор длится только до секунды. Это раздражает, так как другая панель в графическом интерфейсе зависит от того, что выбрано. Если ничего не выбрано, то оно сбрасывается на заставку с пояснениями.

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

Я знаю, что другие классы имеютdataChanged() сигнал, который был бы идеальным.

Кто-нибудь из вас знаетhow I could have the table refresh to reflect changes to the database (либо из командной строки, либо из других экземпляров программы)AND keep the current selection?

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

РЕДАКТИРОВАТЬ: Текущая попытка решения:

void MainWindow::update_table()
{    

    QList<QModelIndex> selection = sqlTable->selectionModel()->selection().indexes();
    QList<int> selectedIDs;
    bool somethingSelected = true;

    for(QList<QModelIndex>::iterator i = selection.begin(); i != selection.end(); ++i){
        int col = i->column();
        QVariant data = i->data(Qt::DisplayRole);

    if(col == 0) {
            selectedIDs.append(data.toInt());
        }
    }

    if(selectedIDs.empty()) somethingSelected = false;

    model->select();
    sqlTable->reset();

    if(somethingSelected){
        QList<int> selectedRows;

        int rows = model->rowCount(QModelIndex());
        for(int i = 0; i < rows; ++i){
            sqlTable->selectRow(i);
            if(selectedIDs.contains(sqlTable->selectionModel()->selection().indexes().first().data(Qt::DisplayRole).toInt())) selectedRows.append(i);
    }

    for(QList<int>::iterator i = selectedRows.begin(); i != selectedRows.end(); ++i){
        sqlTable->selectRow(*i);
    }
}
}

Хорошо, теперь это более или менее работает ...

Я добавил пример, который делает это в общем виде, в зависимости от присутствия первичных ключей. Kuba Ober

Ваш Ответ

1   ответ
9

предлагают довольно замкнутый путь отQSqlTableModel::primaryKey() к списку столбцов. РезультатprimaryKey() этоQSqlRecordи вы можете перебрать егоfield()s чтобы увидеть, что они есть. Вы также можете посмотреть все поля, составляющие запрос, изQSqlTableModel::record(), Вы найдете первое в последнем, чтобы получить список столбцов модели, которые составляют запрос.

Если ваш запрос не содержит первичного ключа, вам придется разработать его самостоятельно и предложить его с использованием какого-либо протокола. Например, вы можете выбрать это, еслиprimaryKey().isEmpty() Значение true, последний столбец, возвращаемый моделью, должен использоваться в качестве первичного ключа. Вы сами должны выяснить, как ввести результат произвольного запроса.

Затем выбранные строки можно индексировать просто по их первичным ключам (список значений ячеек, составляющих ключ, - aQVariantList). Для этого вы можете использоватьcustom selection model (QItemSelectionModel) если его конструкция не была нарушена. Ключевые методы, такие какisRowSelected() не являются виртуальными, и вы не можете переопределить их :(.

Вместо этого вы можете использовать прокси-модель, которая имитирует выбор, предоставляяQt::BackgroundRole для данных. Ваша модель находится поверх модели таблицы и хранит отсортированный список выбранных ключей. Каждый раз, когда модель проксиdata() При вызове ключа вы получаете ключ строки из базовой модели запроса, а затем ищете его в своем отсортированном списке. Наконец, вы возвращаете пользовательскую фоновую роль, если элемент выбран. Вам придется написать соответствующий оператор сравнения дляQVariantList, ЕслиQItemSelectionModel был использован для этой цели, вы могли бы поместить эту функцию в повторной реализацииisRowSelected().

Модель является общей, так как вы подписываетесь на определенный протокол для извлечения ключа из модели запроса: а именно, используяprimaryKey().

Вместо явного использования первичных ключей вы также можете использовать постоянные индексы, если модель поддерживает их. Увы, пока как минимум Qt 5.3.2,QSqlTableModel не сохраняет постоянные индексы при повторном выполнении запроса. Таким образом, как только представление меняет порядок сортировки, постоянные индексы становятся недействительными.

Ниже приведен полностью проработанный пример того, как можно реализовать такого зверя:

screenshot of the example

#include <QApplication>
#include <QTableView>
#include <QSqlRecord>
#include <QSqlField>
#include <QSqlQuery>
#include <QSqlTableModel>
#include <QIdentityProxyModel>
#include <QSqlDatabase>
#include <QMap>
#include <QVBoxLayout>
#include <QPushButton>

// Lexicographic comparison for a variant list
bool operator<(const QVariantList &a, const QVariantList &b) {
   int count = std::max(a.count(), b.count());
   // For lexicographic comparison, null comes before all else
   Q_ASSERT(QVariant() < QVariant::fromValue(-1));
   for (int i = 0; i < count; ++i) {
      auto aValue = i < a.count() ? a.value(i) : QVariant();
      auto bValue = i < b.count() ? b.value(i) : QVariant();
      if (aValue < bValue) return true;
   }
   return false;
}

class RowSelectionEmulatorProxy : public QIdentityProxyModel {
   Q_OBJECT
   Q_PROPERTY(QBrush selectedBrush READ selectedBrush WRITE setSelectedBrush)
   QMap<QVariantList, QModelIndex> mutable m_selection;
   QVector<int> m_roles;
   QBrush m_selectedBrush;
   bool m_ignoreReset;
   class SqlTableModel : public QSqlTableModel {
   public:
      using QSqlTableModel::primaryValues;
   };
   SqlTableModel * source() const {
      return static_cast<SqlTableModel*>(dynamic_cast<QSqlTableModel*>(sourceModel()));
   }
   QVariantList primaryValues(int row) const {
      auto record = source()->primaryValues(row);
      QVariantList values;
      for (int i = 0; i < record.count(); ++i) values << record.field(i).value();
      return values;
   }
   void notifyOfChanges(int row) {
      emit dataChanged(index(row, 0), index(row, columnCount()-1), m_roles);
   }
   void notifyOfAllChanges(bool remove = false) {
      auto it = m_selection.begin();
      while (it != m_selection.end()) {
         if (it->isValid()) notifyOfChanges(it->row());
         if (remove) it = m_selection.erase(it); else ++it;
      }
   }
public:
   RowSelectionEmulatorProxy(QObject* parent = 0) :
      QIdentityProxyModel(parent), m_roles(QVector<int>() << Qt::BackgroundRole),
      m_ignoreReset(false) {
      connect(this, &QAbstractItemModel::modelReset, [this]{
         if (! m_ignoreReset) {
            m_selection.clear();
         } else {
            for (auto it = m_selection.begin(); it != m_selection.end(); ++it) {
               *it = QModelIndex(); // invalidate the cached mapping
            }
         }
      });
   }
   QBrush selectedBrush() const { return m_selectedBrush; }
   void setSelectedBrush(const QBrush & brush) {
      if (brush == m_selectedBrush) return;
      m_selectedBrush = brush;
      notifyOfAllChanges();
   }
   QList<int> selectedRows() const {
      QList<int> result;
      for (auto it = m_selection.begin(); it != m_selection.end(); ++it) {
         if (it->isValid()) result << it->row();
      }
      return result;
   }
   bool isRowSelected(const QModelIndex &proxyIndex) const {
      if (! source() || proxyIndex.row() >= rowCount()) return false;
      auto primaryKey = primaryValues(proxyIndex.row());
      return m_selection.contains(primaryKey);
   }
   Q_SLOT void selectRow(const QModelIndex &proxyIndex, bool selected = true) {
      if (! source() || proxyIndex.row() >= rowCount()) return;
      auto primaryKey = primaryValues(proxyIndex.row());
      if (selected) {
         m_selection.insert(primaryKey, proxyIndex);
      } else {
         m_selection.remove(primaryKey);
      }
      notifyOfChanges(proxyIndex.row());
   }
   Q_SLOT void toggleRowSelection(const QModelIndex &proxyIndex) {
      selectRow(proxyIndex, !isRowSelected(proxyIndex));
   }
   Q_SLOT virtual void clearSelection() {
      notifyOfAllChanges(true);
   }
   QVariant data(const QModelIndex &proxyIndex, int role) const Q_DECL_OVERRIDE {
      QVariant value = QIdentityProxyModel::data(proxyIndex, role);
      if (proxyIndex.row() < rowCount() && source()) {
         auto primaryKey = primaryValues(proxyIndex.row());
         auto it = m_selection.find(primaryKey);
         if (it != m_selection.end()) {
            // update the cache
            if (! it->isValid()) *it = proxyIndex;
            // return the background
            if (role == Qt::BackgroundRole) return m_selectedBrush;
         }
      }
      return value;
   }
   bool setData(const QModelIndex &, const QVariant &, int) Q_DECL_OVERRIDE {
      return false;
   }
   void sort(int column, Qt::SortOrder order) Q_DECL_OVERRIDE {
      m_ignoreReset = true;
      QIdentityProxyModel::sort(column, order);
      m_ignoreReset = false;
   }
   void setSourceModel(QAbstractItemModel * model) Q_DECL_OVERRIDE {
      m_selection.clear();
      QIdentityProxyModel::setSourceModel(model);
   }
};

int main(int argc, char *argv[])
{
   QApplication app(argc, argv);
   QWidget w;
   QVBoxLayout layout(&w);

   QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
   db.setDatabaseName(":memory:");
   if (! db.open()) return 255;

   QSqlQuery query(db);
   query.exec("create table chaps (name, age, constraint pk primary key (name, age));");
   query.exec("insert into chaps (name, age) values "
              "('Bob', 20), ('Rob', 30), ('Sue', 25), ('Hob', 40);");
   QSqlTableModel model(nullptr, db);
   model.setTable("chaps");

   RowSelectionEmulatorProxy proxy;
   proxy.setSourceModel(&model);
   proxy.setSelectedBrush(QBrush(Qt::yellow));

   QTableView view;
   view.setModel(&proxy);
   view.setEditTriggers(QAbstractItemView::NoEditTriggers);
   view.setSelectionMode(QAbstractItemView::NoSelection);
   view.setSortingEnabled(true);
   QObject::connect(&view, &QAbstractItemView::clicked, [&proxy](const QModelIndex & index){
      proxy.toggleRowSelection(index);
   });

   QPushButton clearSelection("Clear Selection");
   QObject::connect(&clearSelection, &QPushButton::clicked, [&proxy]{ proxy.clearSelection(); });

   layout.addWidget(&view);
   layout.addWidget(&clearSelection);
   w.show();
   app.exec();
}

#include "main.moc"
Если хотите, я мог бы попытаться реализовать концепцию для этого. Не должно быть слишком сложно.
Предположительно это должен быть уникальный столбец, хотя ... will
Короткий ответ: нет способа сделать это столь же простым, как «emit dataChanged ()». Как раздражает. will
Мне бы это понравилось, я использую QT или несколько недель. Вы говорите, что ему нужен первичный ключ, у всех моих таблиц есть первичные ключи, нет ли способа просто использовать этот столбец, если он не запутается? В каждой таблице это первый столбец и целое число ... will
Вы ожидаете, что Qt автоматически сделает это. Там нет автоматического способа сделать этоexpect для случаев, когда существует первичный ключ. Следует признать, что Qt может предоставить API, который либо используетprimaryKey() или позволяет вам предоставить набор столбцов, которые составляют ключ. Обратите внимание, что все, что вам нужно, это обычайQItemSelectionModelнет необходимости прикасаться к чему-либо еще.

Похожие вопросы