如何刷新QSqlTableModel同时保留选择? [英] How to refresh a QSqlTableModel while preserving the selection?

查看:971
本文介绍了如何刷新QSqlTableModel同时保留选择?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用QSqlTableModel和QTableView查看SQLite数据库表。



我想让表自动刷新每秒左右是一个非常大的表 - 几百行)。我可以这样做 - 像这样:

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

...

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

但这会删除我所有的选择,持续长达一秒钟。这是恼人的,因为GUI中的另一个窗格取决于所选的内容。



然后,我尝试了一个有点黑客的方法,它获取所选的行号,重置表,然后重新设置。选择该行。但是这也不行,因为选择的行可以基于表的添加向上或向下移动。



我知道其他类有一个 dataChanged()信号,这将是理想的。



任何人都知道更改数据库(来自命令行用法或程序的其他实例)并保留当前选择?



我知道我可以从当前选择中获取数据,然后重置搜索同一行,然后重新选择,但这似乎是一个计数器生产和坏的解决方案的问题。



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

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

现在工作...

解决方案

真正的交易是查询结果的主键。 Qt的API提供了一个相当迂回的路由从 QSqlTableModel :: primaryKey()到列表的列表。 primaryKey()的结果是 QSqlRecord ,您可以遍历其字段)s ,看看它们是什么。您还可以从 QSqlTableModel :: record()中查找构成查询的所有字段。



如果您的查询不包含主键,您将需要设计一个自己,并提供使用一些协议。例如,如果 primaryKey()。isEmpty()为true,则模型返回的最后一列将用作主键。这是由你来找出如何键入任意查询的结果。



然后可以通过它们的主键(值的列表组成键的单元格 - QVariantList )。为此,如果其设计没有损坏,您可以使用自定义选择模型 QItemSelectionModel )。关键方法,例如 isRowSelected()不是虚拟的,您不能重新实现它们:。



<相反,你可以使用一个代理模型,通过为数据提供一个自定义的 Qt :: BackgroundRole 来模仿选择。你的模型位于表模型的顶部,每次调用代理模型的 data()时,您将从底层查询模型中获取该行的键,然后在已排序的列表中搜索它。如果 QItemSelectionModel ,则必须为 QVariantList 编写相关的比较运算符c $ c>可用于此目的,您可以将此功能重新实现为 isRowSelected()



该模型是通用的,因为您订阅了从查询模型中提取密钥的特定协议:即使用 primaryKey()



而不是显式地使用主键,如果模型支持它们,也可以使用持久索引。唉,直到到目前为止Qt 5.3.2, QSqlTableModel 不保留持久索引,当查询重新运行。因此,只要视图改变排序顺序,持久索引就会变得无效。



下面是一个完整的例子,说明如何实现这样的野兽: / p>

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

//变体列表的词典比较
bool operator<(const QVariantList& a,const QVariantList& b){
int count = std :: max .count(),b.count());
//对于字典比较,null在所有其他之前
Q_ASSERT(QVariant()< QVariant :: fromValue(-1));
for(int i = 0; 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;
类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();
返回值;
}
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>结果;
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 ;
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数据(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()){
//更新缓存
if(!it-> isValid())* it = proxyIndex;
//返回背景
if(role == Qt :: BackgroundRole)return m_selectedBrush;
}
}
返回值;
}
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布局(& w);

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

QSqlQuery查询(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模型(nullptr,db);
model.setTable(chaps);

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

QTableView视图;
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(清除选择);
QObject :: connect(& clearSelection,& QPushButton :: clicked,[& proxy] {proxy.clearSelection();});

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

#includemain.moc


I am using a QSqlTableModel and QTableView to view an SQLite database table.

I would like to have the table auto refresh every second or so (it's not going to be a very large table - a couple of hundred rows). And i can do this - like so:

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*
}

But this removes any selection I have, so the selections only last for up to a second. This is annoying, as another pane in the GUI depends on what is selected. If nothing is selected, then it resets to an explanation splash page.

I then tried a somewhat hacky approach, which gets the selected row number, resets the table, and then selects that row. But this doesn't work either, as the selected row can move up or down based on additions to the table.

I know other classes have a dataChanged() signal, which would be ideal.

Do any of you know how I could have the table refresh to reflect changes to the database (from either command line usage, or other instances of the program) AND keep the current selection?

I know I could get data from the current selection, and then after the reset search for the same row and then reselect it, but this seems like a counter productive and bad solution to the problem.

EDIT: Current attempt at solution:

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

Okay so this more or less works now...

解决方案

The real deal is the primary key on the result of your query. Qt's APIs offer a rather circuitous route from QSqlTableModel::primaryKey() to a list of columns. The result of primaryKey() is a QSqlRecord, and you can iterate over its field()s to see what they are. You can also look up all of the fields that comprise the query proper from QSqlTableModel::record(). You find the former in the latter to get a list of model columns that comprise the query.

If your query doesn't contain a primary key, you'll have to design one yourself and offer it using some protocol. For example, you can choose that if primaryKey().isEmpty() is true, the last column returned by the model is to be used as the primary key. It's up to you to figure out how to key the result of an arbitrary query.

The selected rows can then be indexed simply by their primary keys (a list of values of the cells that comprise the key -- a QVariantList). For this, you could use a custom selection model (QItemSelectionModel) if its design wasn't broken. The key methods, such as isRowSelected() aren't virtual, and you can't reimplement them :(.

Instead, you can use a proxy model that mimicks selection by providing a custom Qt::BackgroundRole for the data. Your model sits on top of the table model, and keeps a sorted list of selected keys. Each time the proxy model's data() is called, you get the row's key from underlying query model, then search for it in your sorted list. Finally, you return a custom background role if the item is selected. You'll have to write relevant comparison operator for QVariantList. If QItemSelectionModel was usable for this purpose, you could put this functionality into a reimplementation of isRowSelected().

The model is generic since you subscribe to a certain protocol for extracting the key from a query model: namely, using primaryKey().

Instead of using primary keys explicitly, you can also use persistent indices if the model supports them. Alas, until at lest Qt 5.3.2, QSqlTableModel does not preserve the persistent indices when the query is rerun. Thus, as soon as the view changes the sort order, the persistent indices become invalid.

Below is a fully worked out example of how one might implement such a beast:

#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"

这篇关于如何刷新QSqlTableModel同时保留选择?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆