QTableView + custom TableModel + lazy loading - qt

I need to load heavy dataset into QTableView. Dataset is no less then 700Mb in memory and I don't want to load all it to memory.
QSqlQueryModel is not ideal for me for 2 reasons - it is not editable and it is not realy load-on-demand (because fetching whole data to memory).
What I want to get
I want to store only some part of data in memory. Just for
displaying and maybe some buffer for fast scrolling.
Model should be editable
It should be low-memory-consumption
Should have no freezes
How I am trying to solve this (straightforward model of my code)
Custom QTableView (tableView)
Custom TableModel (model)
Model wrapper. (wrapper)
Model wrapper select rows count from database, and set this value to model. Now model can answer for int rowCount().
This same value is set for tableView.verticalScrollBar().
tableView.verticalScrollBar signal(valueChanged) is connected to tableview slot(on_valueChanged)
Some code
tableView::on_valueChanged(value)
{
wrapper.changeOffset(value);
}
wrapper::changeOffset(value)
{
if (_offset == value){
return;
}
_selectQuery->seek(value);
int endValue = qMin(value + _cacheSize, model->rowCount());
_list.clear();
for(int i = value; i < endValue-1; i++){
_list.append(_selectQuery->record());
}
model->setRecordList(_list);
_offset = value;
model->setOffset(_offset);
}
_selectQuery in wrapper::changeOffset is previosly executed QSqlQuery cursor for select query results.
I also implemented several methods in model
QVariant SqlRecModel::data(const QModelIndex &index, int role) const
{
int row = index.row() - _offset;
if (row > m_recList.size() || row < 0){
return QVariant();
}
if (role == Qt::DisplayRole)
{
QVariant value = m_recList.at(row).value(index.column());
return value;
}
return QVariant();
}
Setter for model data storage
void SqlRecModel::setRecordList(const QList<QSqlRecord> &records)
{
qDebug() << "r:";
emit layoutAboutToBeChanged();
m_recList = records;
emit layoutChanged();
}
Problem
I can scroll _cacheSize rows, but I get crash (The program has unexpectedly finished.) after going out of old cacheRange.
Any advice? I don't know where to dig. Thanks!

Sorry for disturbing.
Error was in other code block.
This way is just okay to solve the task I have.
btw: if you play with cache buffer you can achive more smooth scroll.

Related

How to limit the number of QCheckboxes checked at the same time?

I'm in the process of creating a QT application which is using multiple (14) qcheckboxes. I need to have a limit (preferably set as a variable that i can change) to the number of checkboxes that can be checked at the same time, is there any way to achieve this cleanly ? Thanks for your time.
There is no simple way of doing this, you have to write your code to do it.
I suppose you have the checkboxes in some parent widget class. So I would create a slot which looks like this.
void SomeParentWidget::onCheckBoxToggled(bool value)
{
// when we unchecked the checkbox,
// we do not need to count the number of checked ones
if (!value)
return;
int total = 0;
int limit = 15; // your "magic" number of maximum checked checkboxes
for (auto chb : allCheckBoxes()) // allCheckBoxes() is some method which returns all the checkboxes in consideration
{
if (chb->isChecked())
{
++total;
if (total > limit)
{
// too many checkboxes checked! uncheck the sender checkbox
// Note: you may want to add some nullptr checks or asserts to the following line for better robustness of your code.
qobject_cast<QCheckBox*>(sender())->setChecked(false);
return;
}
}
}
}
And when creating each of your checkboxes inside some parent widget, connect this slot to their signal:
auto chb = new QCheckBox();
connect(chb, &QCheckBox::toggled, this, &SomeParentWidget::onCheckBoxToggled);
Implementation of allCheckBoxes() is up to you, I do not know how you can retrieve the collection of all your check boxes. Depends on your design.
I found another, even simpler solution. Use this slot.
void SomeParentWidget::onCheckBoxToggled(bool value)
{
static int totalChecked = 0; // static! the value is remembered for next invocation
totalChecked += value ? 1 : -1;
Q_ASSERT(totalChecked >= 0);
int maxChecked = 15; // any number you like
if (value && totalChecked > maxChecked)
{
qobject_cast<QCheckBox*>(sender())->setChecked(false);
}
}
... and connect it to checkboxes' toggled() signal. Note that in order to work correctly, all check boxes must be unchecked at the time when you make the signal-slot connection because this function starts counting from zero (0 is the initial value of the static variable).
You can store all your checkboxes in a map (either in an std::map, an std::unordered_map or an QMap). Your keys will be your checkboxes, and your values will be their states, so something like this:
std::unordered_map<QCheckBox*, bool> m_checkBoxStates;
Here's what your connected to your toggled signal of all your checkboxes look like (keep in mind that all the signals will be connected to the same slot):
void MainWindow::onToggled(bool checked) {
QCheckBox* checkBox = sender(); //the checkbox that has been toggled
m_checkBoxStates[checkBox] = checked;
if (!checked) {
return;
}
const int count = std::count_if(m_checkBoxStates.begin(), m_checkBoxStates.end(),
[](const auto pair) {
return pair.second == true;
});
if (count > maxCount) {
checkBox->setChecked(false);
}
}

QSortFilterProxyModel: inconsistent changes reported by source model

So I have a QTreeView widget that has a custom QSortFilterProxyModel as a source model, which itself wraps a custom QAbstractItemModel called: sourceModel.
My tree displays files and folders. If a file removal leaves a folder empty, the folder is automatically deleted. The implementation is below:
bool sourceModel::removeRows(int row, int count, const QModelIndex& parent)
{
if (parent.isValid())
{
auto parent_node = static_cast<Node*>(parent.internalPointer());
if (!parent.data(rootNode).toBool())
{
beginRemoveRows(parent, row, row + count - 1);
for (int i = 0; i < count; ++i)
parent_node->removeChild(row + i);
endRemoveRows();
if (parent_node->isType<Folder>() && parent_node->children() == 0)
{
removeRows(parent_node->row(), 1, parent.parent());
}
return true;
}
}
}
This works fine when removeRows is called via the proxy model, but in another instance when sourceModel calls removeRows directly, I get:
QSortFilterProxyModel: inconsistent changes reported by source model
It's as if the QSortFilterProxyModel isn't receiving or handling beginRemoveRows/endRemoveRows properly.
I've resolved this, the fix had nothing to do with the models themselves and the code posted works fine. I was calling a method directly from a context menu workflow and this was causing some sort of race condition between the proxy model and a selection model.

Specializing a QAbstractProxyModel for adding a column: the table cells becomes empty

I have created a mixin-like proxy model (Qt5) which just adds an extra first column to another proxy model, for adding a QToolBar of actions to each row of the table view (for example, a "delete" button). The model just provides a way of populating a QList<QVariant> for the first column. The delegate must know what is the meaning of each QVariant (usually ints/enums identifying actions), and populate the QToolBar accordingly. As last feature, if there's no actions, no extra column is added (it behaves like a QIdentityProxyModel in that case). Once added, actions cannot be removed. That's a feature for another day.
The problem of today is that, when I insert actions (which I do before setting the model to the view), the cells are all blanks. So, I'm doing something wrong with the signals or who knows with what (I think the mistake is in the add_action function, at the end of the snippet):
template<class proxy_model>
class action_model : public proxy_model
{
QList<QVariant> l_actions;
public:
using base_t = proxy_model;
using base_t::base_t; // Inheriting constructors.
QModelIndex mapFromSource(const QModelIndex& source_idx) const override
{
if (!l_actions.empty() and source_idx.isValid())
return this->createIndex(source_idx.row(),
source_idx.column() + 1);
else // identity proxy case
return base_t::mapFromSource(source_idx);
} // same for mapToSource but with - 1 instead of + 1.
int columnCount(const QModelIndex& parent = QModelIndex()) const override
{ return this->base_t::columnCount() + !l_actions.empty(); }
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
{
if (!l_actions.empty()) {
if (orientation == Qt::Horizontal and section == 0
and role == Qt::DisplayRole)
return "Actions"; // Testing.
else
return base_t::headerData(section - 1, orientation, role);
} else // identity proxy case
return base_t::headerData(section, orientation, role);
}
QVariant data(const QModelIndex& idx, int role) const override
{
if (!l_actions.empty()) {
if (idx.column() == 0 and role = Qt::DisplayRole)
return l_actions; // All the actions for drawing.
else
return QVariant();
} else // identity proxy case
return base_t::data(idx, role);
}
Qt::ItemFlags flags(QModelIndex const& idx) const
{
if (!l_actions.empty() and idx.column() == 0)
return Qt::NoItemFlags; // No editable or selectable
else
return base_t::flags(idx);
}
// And here, I think, is where the fun starts:
// The action could be added before or after the sourceModel
// is set or this model is connected to a view, but I don't
// how that cases are supposed to be managed.
void add_action(QVariant const& action)
{
bool was_empty = l_actions.empty();
l_actions << action;
if (was_empty and !this->insertColumns(0, 1))
throw std::logic_error("Something went wrong");
Q_EMIT this->dataChanged
(this->createIndex(0, 0),
this->createIndex(this->rowCount(), 0),
{ Qt::DisplayRole });
}
};
Without setting actions, the model works fine, both with QAbstractIdentityProxyModel and QSortFilterProxyModel as proxy_model. But, when setting actions, the view shows every cell blank, both with QSortFilterProxyModel and QAbstractIdentityProxyModel.
Here is a user-land code:
enum sql_action { DELETE };
auto* table_model = /* My QSqlTableModel */;
auto* view_model = new action_model<QIdentityProxyModel>(my_parent);
auto* table_view = new QTableView;
view_model->add_action(static_cast<int>(sql_action::DELETE));
view_model->setSourceModel(table_model);
table_view->setModel(view_model);
table_view->setSortingEnabled(true);
table_view->setAlternatingRowColors(true);
// The last column is printed in white, not with alternate colors.
table_view->show();
table_model->select();
The delegates are not a problem because I have set no one. I expect a first column with white cells, but I get an entirely white table. The column names are shown fine, except the last one, which prints just 0 as column name.
What am I doing wrong?
The problem is in your data() method.
You do not compare role to Qt::DisplayRole you assign to role
If you have actions, you either return the action entry or QVariant(), never any data

QTableView: dataChanged event clears cell being edited

Working with a QTableView and QAbstractTableModel - when the model emits a dataChanged event for the cell being edited, the string the user has typed in the cell (but not pressed enter to 'commit' the edit) is erased.
Example: Click a cell, type '123', cell is still in edit mode waiting for more text, dataChanged is emitted and the '123' is erased, leaving an empty cell in edit mode.
Does anyone know how to stop this behaviour, or how the model can detect when the cell is being edited to prevent dataChanged events being raised for that cell?
I had the same problem. The thing is, that data() function is called with different role parameter. For displaying role==Qt::DisplayRoleand while editing it is called with role==Qt::EditRole. For example try changing
QVariant MyModel::data(const QModelIndex & index, int role) const
{
if (role == Qt::DisplayRole)
return QString("Text to Edit");
}
to
QVariant MyModel::data(const QModelIndex & index, int role) const
{
if (role == Qt::DisplayRole || role == Qt::EditRole)
return QString("Text to Edit");
}
that should do the trick
I had the same problem and found an approach without writing my own Delegate:
The problem is exactly how you described it: The data gets updated in the Background and everything you Edit gets cleared out because the dataChanged Event updates all values thus calling the data function, which returns an empty QVariant() Object if nothing is specified for the Qt::EditRole. Even Leonid's answer would always overwrite your edits with the same QString("Text to Edit").
So what I did was this:
Introduce a member variable and dafine it mutable so it can be changed by the const data function:
mutable bool m_updateData = true;
In your background data updating function, check for m_update date before emmitting the dataChanged Signal:
if (m_updateData)
emit(dataChanged(index, index));
In your data function, check for the edit role and set m_updateData to false:
if (role == Qt::EditRole)
{
m_updateData = false;
}
After the Edit is finished, the setData function is called, where you update the data in your model. Reset m_updateDate to true after you have done that.
This works perfectly for me :)
Check your model class, you should override the setData method in your model. If every thing is correct it will update model after editing data... please let me know if you have another implementation
bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (index.isValid() && role == Qt::EditRole) {
int row = index.row();
int col = index.column();
//// change data
emit(dataChanged(index, index));
return true;
}
return false;
}
I think that you should use dataChanged event only for indexes that are not being edited or only for Qt::ItemDataRole::DisplayRole.
E.g. to update only second column for every row use:
emit dataChanged(index(0, 1),
index(rowCount() - 1, 1),
QVector<int>{ Qt::ItemDataRole::DisplayRole });

QAbstactTableModel insert at top

I have addFile function in my TableModel class which inserts a new record at the end.
void TableModel::addFile(const QString &path)
{
beginInsertRows(QModelIndex(), list.size(),list.size());
TableItem item;
item.filename = path;
QFile file(path);
item.size = file.size();
item.status = StatusNew;
list << item;
endInsertRows();
}
This function works fine but instead of appending record at the end I would like to insert it at the top. Any pointers on how to update my existing function ?
I have already tried some combinations but there is no Luck.
There are two things that you need to do. First is to adjust the call to beginInsertRows. Because it is here that we are telling the model that we are adding rows, where they will go, and how many we are adding. Here is the method description:
void QAbstractItemModel::beginInsertRows ( const QModelIndex & parent,
int first, int last )
So in your case since you want to add a row at the first index, and only one row, we pass 0 as the index of the first item, and 0 which is the index of the last item we are adding (because of course we are only adding one item).
beginInsertRows(modelIndex(), 0, 0);
Next we have to provide the data for the item. I assume that 'list' is a QList (if not it is probably similar). So we want to call the 'insert' method.
list.insert(0, item);
And that should be it.
For display you can try delegates as explained in the link (I haven't tried the example though). It will help the community if you can add your observations.
Thanks to everyone for replying. I have found the solution by my own:
In case if anyone is interested
void TableModel::addFile(const QString &path)
{
beginInsertRows(QModelIndex(), list.size(), list.size());
TableItem item;
item.filename = path;
QFile file(path);
item.size = file.size();
item.status = StatusNew;
list << item; // Why Assign first? Maybe not required
for (int i = list.size() - 1; i > 0; i--)
{
list[i] = list[i-1];
}
list[0] = item; // set newly added item at the top
endInsertRows();
}

Resources