Как обновить QSqlTableModel при сохранении выделения?

Я использую 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(), который был бы идеальным.

Кто-нибудь из вас знает , как я мог бы обновить таблицу, чтобы отразить изменения в базе данных (из использования командной строки или других экземпляров программы) И сохранить текущий выбор

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

EDIT: Текущая попытка решения:

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);
    }
}
}

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

Ответ 1

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

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

Выбранные строки затем могут быть проиндексированы просто их первичными ключами (список значений ячеек, которые содержат ключ - a QVariantList). Для этого вы можете использовать модель пользовательского выбора (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"