Related
After reading the docs and examples of QAbstractItemModel and QModelIndex, I am still confused on how to properly implement the model for a QTreeView.
Since I want to provide a model for an existing hierarchical data structure, I'm avoiding using QTreeWidget or QStandardItemModel and the related issues with data duplication and synchronization. However, I failed to implement a working item model (too many issues, it is not useful to post my code).
After reading this answer it seems clear that QModelIndex does not contain hierarchical information, but simply relies on the model to tell the parent of a given index. Consequently it does not seem possible to provide an abstract tree model for an existing data structure without defining at least another helper class for storing such relation. But still, I cannot implement properly the model.
Suppose the data structure is this simple one:
struct Property {
QString name;
QString value;
};
struct Node {
QString name;
QVector<Property> properties;
};
struct Track {
int length;
QString channel;
};
struct Model {
QVector<Node> nodes;
QVector<Track> tracks;
};
where Model is the toplevel one, and it resembles a tree.
The tree displayed in the QTreeView could look like this:
Model
├─Nodes
│ ├─Node "node1"
│ │ └─Properties
│ │ ├─Property property1 = value1
│ │ └─Property property2 = value2
│ └─Node "node2"
│ └─Properties
│ └─Property property1 = someValue
└─Tracks
├─Track 1, ...
├─Track 2, ...
└─Track 3, ...
How the QAbstractItemModel subclass should be implemented to access the existing data without copy?
Here's my solution to the problem.
First of all, my initial guess that QModelIndex is incapable of storing the parent-child relationship is correct. In fact the method QModelIndex::parent simply calls QAbstractItemModel::parent, and the task of implementing the parent method is left to the model class. When the underlying model is a proper tree, pointer to tree nodes can be stored in the QModelIndex class, but in my case we are dealing with a "virtual" tree and this relationship is not available. Thus we are forced to introduce some kind of extra storage to be able to tell where we are in the tree. If QModelIndex natively supported having a pointer to the parent index, this problem would have been solved much more easily. But since QModelIndex is a value class, we cannot have a pointer to parent, but rather we have to store all the parent indices inside the QModelIndex class, and maybe the Qt developers had some good way to not do so. So I stored a QVector<QModelIndex> in the internal-pointer field of QModelIndex. There are some things to take care of, like avoiding allocating more than necessary of such indices, and also to remember freeing the memory when those are no more needed (we can't use QObject hierarchy here). There may be additional problems to take care of when the model is read-write, but in this case I'm dealing with a read-only model.
My implementation follows. Methods rowCount and data define this specific virtual tree. The other methods can be abstracted away in a class that can be re-used.
class MyModel : public QAbstractItemModel
{
Q_OBJECT
private:
struct IndexData
{
QVector<QModelIndex> parents;
};
public:
explicit MyModel(QObject *parent = nullptr);
~MyModel();
QVariant data(const QModelIndex &index, int role) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
protected:
IndexData * indexData(const QModelIndex &index) const;
QList<int> indexPath(const QModelIndex &index) const;
QString indexString(const QModelIndex &index) const;
QString indexString(int row, int column, const QModelIndex &parent) const;
public:
int indexDepth(const QModelIndex &index) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
private:
QMap<QString, IndexData*> indexData_;
Model model;
};
implementation:
MyModel::MyModel(QObject *parent)
: QAbstractItemModel(parent)
{
model.nodes.resize(2);
model.nodes[0].name = "node1";
model.nodes[0].properties.resize(2);
model.nodes[0].properties[0].name = "property1";
model.nodes[0].properties[0].value = "value1";
model.nodes[0].properties[1].name = "property2";
model.nodes[0].properties[1].value = "value2";
model.nodes[1].name = "node2";
model.nodes[1].properties.resize(1);
model.nodes[1].properties[0].name = "property1";
model.nodes[1].properties[0].value = "someValue";
model.tracks.resize(3);
model.tracks[0].length = 2;
model.tracks[0].channel = "A";
model.tracks[1].length = 4;
model.tracks[1].channel = "B";
model.tracks[2].length = 3;
model.tracks[2].channel = "C";
}
MyModel::~MyModel()
{
for(auto v : indexData_) delete v;
}
QVariant MyModel::data(const QModelIndex &index, int role) const
{
if(!index.isValid() || role != Qt::DisplayRole) return {};
int d = indexDepth(index);
auto path = indexPath(index);
if(d == 1) return "Model";
if(d == 2 && path[0] == 0 && path[1] == 0) return "Nodes";
if(d == 2 && path[0] == 0 && path[1] == 1) return "Tracks";
if(d == 3 && path[0] == 0 && path[1] == 0) return QString("Node \"%1\"").arg(model.nodes[path[2]].name);
if(d == 4 && path[0] == 0 && path[1] == 0) return "Properties";
if(d == 5 && path[0] == 0 && path[1] == 0 && path[3] == 0) return QString("Property %1 = %2").arg(model.nodes[path[2]].properties[path[4]].name, model.nodes[path[2]].properties[path[4]].value);
if(d == 3 && path[0] == 0 && path[1] == 1) return QString("Track %1...").arg(index.row() + 1);
return {};
}
QModelIndex MyModel::index(int row, int column, const QModelIndex &parent) const
{
QString dataKey = indexString(row, column, parent);
auto it = indexData_.find(dataKey);
IndexData *data;
if(it == indexData_.end())
{
data = new IndexData;
const_cast<MyModel*>(this)->indexData_.insert(dataKey, data);
if(parent.isValid())
{
data->parents.append(parent);
data->parents.append(indexData(parent)->parents);
}
}
else
{
data = it.value();
}
return createIndex(row, column, data);
}
QModelIndex MyModel::parent(const QModelIndex &index) const
{
if(!index.isValid()) return {};
auto data = indexData(index);
if(data->parents.empty()) return {};
return data->parents.at(0);
}
MyModel::IndexData * MyModel::indexData(const QModelIndex &index) const
{
if(!index.internalPointer()) return nullptr;
return reinterpret_cast<IndexData*>(index.internalPointer());
}
QList<int> MyModel::indexPath(const QModelIndex &index) const
{
QList<int> path;
auto data = indexData(index);
for(int i = data->parents.size() - 1; i >= 0; i--)
path.push_back(data->parents[i].row());
path.push_back(index.row());
return path;
}
QString MyModel::indexString(const QModelIndex &index) const
{
return indexString(index.row(), index.column(), index.parent());
}
QString MyModel::indexString(int row, int column, const QModelIndex &parent) const
{
QString pre = parent.isValid() ? indexString(parent) + "." : "";
return pre + QString("[%1,%2]").arg(row).arg(column);
}
int MyModel::indexDepth(const QModelIndex &index) const
{
if(!index.isValid()) return 0;
return 1 + indexDepth(index.parent());
}
int MyModel::rowCount(const QModelIndex &parent) const
{
if(!parent.isValid()) return 1; // root item
int d = indexDepth(parent);
auto path = indexPath(parent);
//if(d == 0) return 1; // root item
if(d == 1) return 2;
if(d == 2 && path[0] == 0 && path[1] == 0) return model.nodes.size();
if(d == 2 && path[0] == 0 && path[1] == 1) return model.tracks.size();
if(d == 3 && path[0] == 0 && path[1] == 0) return 1;
if(d == 4 && path[0] == 0 && path[1] == 0 && path[3] == 0) return model.nodes[path[2]].properties.size();
return 0;
}
int MyModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
if(index.isValid()) return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
return {};
}
You are incorrect, you can totally expose any tree structure as QAbstractItemModel without any additional structuring information. You do however have to bend the rules and abuse things a little. You can in fact have a perfectly usable model with an implementation of parent() that returns a default constructed invalid index, while it is an abstract method and it has to be implemented, it doesn't really need to do actually do anything meaningul.
As you already mentioned - the index does give you an opaque pointer to store a data reference. And since tree models have a fixed column count of 1, this leaves the column in the index free to use as some typeid or index. Now you have a way of knowing what each of those opaque pointers actually points to, so you can select the proper code path to retrieve row count and data. Those are the ones that you really do need to implement. Most likely index() too if you want to use some stock model view, but if you explicitly request children indices in a parent delegate, you can even get away with a dud implementation for index() as well.
Now, if your tree nodes do not keep references to their parents, obviously you will not be able to go down the tree directly. What you can do tho is, whatever your current index is, do not have it just free floating, but keep a stack of all the parent indices up from the root node. And now you can go down, by simply keeping breadcrumbs as you went up. You may not even need an explicit stack, in most cases you will be able to get the relevant information to construct the parent index from the parent delegate.
I have a QList of custom structs and i'm using custom model class (subclass of QAbstractListModel) to display those structs in 1-dimensional QListView. I have overriden the methodsrowCount, flags and data to construct a display string from the struct elements.
Now i would like to enable internal drag&drop to be able to reorder the items in the list by dragging them and dropping them between some other items, but this task seems unbeliavably complicated. What exactly do i need to override and what parameters do i need to set? I tried a lot of things, i tried
view->setDragEnabled( true );
view->setAcceptDrops( true );
view->setDragDropMode( QAbstractItemView::InternalMove );
view->setDefaultDropAction( Qt::MoveAction );
I tried
Qt::DropActions supportedDropActions() const override {
return Qt::MoveAction;
}
Qt::ItemFlags flags( const QModelIndex & index ) const override{
return QAbstractItemModel::flags( index ) | Qt::ItemIsDragEnabled;
}
I tried implementing insertRows and removeRows, but it still doesn't work.
I haven't found a single example of a code doing exactly that. The official documentation goes very deeply into how view/model pattern works and how to make drag&drops from external apps or from other widgets, but i don't want any of that. I only want simple internal drag&drop for manual reordering of the items in that one list view.
Can someone please help me? Or i'll get nuts from this.
EDIT: adding insertRows/removeRows implementation on request:
bool insertRows( int row, int count, const QModelIndex & parent ) override
{
QAbstractListModel::beginInsertRows( parent, row, row + count - 1 );
for (int i = 0; i < count; i++)
AObjectListModel<Object>::objectList.insert( row, Object() );
QAbstractListModel::endInsertRows();
return true;
}
bool removeRows( int row, int count, const QModelIndex & parent ) override
{
if (row < 0 || row + count > AObjectListModel<Object>::objectList.size())
return false;
QAbstractListModel::beginRemoveRows( parent, row, row + count - 1 );
for (int i = 0; i < count; i++)
AObjectListModel<Object>::objectList.removeAt( row );
QAbstractListModel::endRemoveRows();
return true;
}
objectList is QList where Object is template parameter.
In addition to the Romha's great answer, i would like to supplement few more details about how it works and what's confusing on it.
The official documentation says the QAbstractItemModel has default implementations of mimeTypes, mimeData and dropMimeData which should work for internal move and copy operations as long as you correctly implement data, setData, insertRows and removeRows.
And from certain point of view, they were right. It does work without overriding mimeData and dropMimeData, but only when your underlying data structure contains only single strings, those that are returned from data and received in setData as DisplayRole. When you have a list of compound objects (like i have) with multiple elements, only one of which is used for the DisplayRole, for example
struct Elem {
QString name;
int i;
bool b;
}
QVariant data( const QModelIndex & index, int role ) const override
{
return objectList[ index.row() ].name;
}
bool setData( const QModelIndex & index, const QVariant & value, int role ) override
{
objectList[ index.row() ].name = value.toString();
}
then the default implementations will actually do this
QVariant data = data( oldIndex, Qt::DisplayRole );
insertRows( newIndex, 1 )
setData( newIndex, data, Qt::DisplayRole )
removeRows( oldIndex, 1 )
and therefore only correctly move the names and leave the rest of the struct as is. Which makes sense now, but the system is so complicated that i didn't realize it before.
Therefore custom mimeData and dropMimeData are required to move the whole content of the structs
When you want to reorganize items in a custom model, you have to implement all needed actions:
- how to insert and remove a row
- how to get and set data
- how to serialize items (build the mimedata)
- how to unserialize items
An example with a custom model with a QStringList as data source:
The minimal implementation of the model should be:
class CustomModel: public QAbstractListModel
{
public:
CustomModel()
{
internalData = QString("abcdefghij").split("");
}
int rowCount(const QModelIndex &parent) const
{
return internalData.length();
}
QVariant data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.parent().isValid())
return QVariant();
if (role != Qt::DisplayRole)
return QVariant();
return internalData.at(index.row());
}
private:
QStringList internalData;
};
We have to add the way to insert/remove rows and set the data:
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole)
{
if (role != Qt::DisplayRole)
return false;
internalData[index.row()] = value.toString();
return true;
}
bool insertRows(int row, int count, const QModelIndex &parent)
{
if (parent.isValid())
return false;
for (int i = 0; i != count; ++i)
internalData.insert(row + i, "");
return true;
}
bool removeRows(int row, int count, const QModelIndex &parent)
{
if (parent.isValid())
return false;
beginRemoveRows(parent, row, row + count - 1);
for (int i = 0; i != count; ++i)
internalData.removeAt(row);
endRemoveRows();
return true;
}
For the drag and drop part:
First, we need to define a mime type to define the way we will deserialize the data:
QStringList mimeTypes() const
{
QStringList types;
types << CustomModel::MimeType;
return types;
}
Where CustomModel::MimeType is a constant string like "application/my.custom.model"
The method canDropMimeData will be used to check if the dropped data are legit or not. So, we can discard external data:
bool canDropMimeData(const QMimeData *data,
Qt::DropAction action, int /*row*/, int /*column*/, const QModelIndex& /*parent*/)
{
if ( action != Qt::MoveAction || !data->hasFormat(CustomModel::MimeType))
return false;
return true;
}
Then, we can create our mime data based on the internal data:
QMimeData* mimeData(const QModelIndexList &indexes) const
{
QMimeData* mimeData = new QMimeData;
QByteArray encodedData;
QDataStream stream(&encodedData, QIODevice::WriteOnly);
for (const QModelIndex &index : indexes) {
if (index.isValid()) {
QString text = data(index, Qt::DisplayRole).toString();
stream << text;
}
}
mimeData->setData(CustomModel::MimeType, encodedData);
return mimeData;
}
Now, we have to handle the dropped data. We have to deserialize the mime data, insert a new row to set the data at the right place (for a Qt::MoveAction, the old row will be automaticaly removed. That why we had to implement removeRows):
bool dropMimeData(const QMimeData *data,
Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
if (!canDropMimeData(data, action, row, column, parent))
return false;
if (action == Qt::IgnoreAction)
return true;
else if (action != Qt::MoveAction)
return false;
QByteArray encodedData = data->data("application/my.custom.model");
QDataStream stream(&encodedData, QIODevice::ReadOnly);
QStringList newItems;
int rows = 0;
while (!stream.atEnd()) {
QString text;
stream >> text;
newItems << text;
++rows;
}
insertRows(row, rows, QModelIndex());
for (const QString &text : qAsConst(newItems))
{
QModelIndex idx = index(row, 0, QModelIndex());
setData(idx, text);
row++;
}
return true;
}
If you want more info on the drag and drop system in Qt, take a look at the documentation.
Here is a evidenced example for you ,but in Python:
import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import (Qt, QStringListModel, QModelIndex,
QMimeData, QByteArray, QDataStream, QIODevice)
from PySide6.QtWidgets import (QApplication, QMainWindow, QListView, QAbstractItemView, QPushButton, QVBoxLayout, QWidget)
class DragDropListModel(QStringListModel):
def __init__(self, parent=None):
super(DragDropListModel, self).__init__(parent)
# self.myMimeTypes = 'application/vnd.text.list' # 可行
# self.myMimeTypes = "text/plain" # 可行
self.myMimeTypes = 'application/json' # 可行
def supportedDropActions(self):
# return Qt.CopyAction | Qt.MoveAction # 拖动时复制并移动相关项目
return Qt.MoveAction # 拖动时移动相关项目
def flags(self, index):
defaultFlags = QStringListModel.flags(self, index)
if index.isValid():
return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags
else:
return Qt.ItemIsDropEnabled | defaultFlags
def mimeTypes(self):
return [self.myMimeTypes]
# 直接将indexes里面对应的数据取出来,然后打包进了QMimeData()对象,并返回
def mimeData(self, indexes):
mmData = QMimeData()
encodedData = QByteArray()
stream = QDataStream(encodedData, QIODevice.WriteOnly)
for index in indexes:
if index.isValid():
text = self.data(index, Qt.DisplayRole)
stream << text # 测试,也行
# stream.writeQString(str(text)) # 原始, 可行
mmData.setData(self.myMimeTypes, encodedData)
return mmData
def canDropMimeData(self, data, action, row, column, parent):
if data.hasFormat(self.myMimeTypes) is False:
return False
if column > 0:
return False
return True
def dropMimeData(self, data, action, row, column, parent):
if self.canDropMimeData(data, action, row, column, parent) is False:
return False
if action == Qt.IgnoreAction:
return True
beginRow = -1
if row != -1: # 表示
print("case 1: ROW IS NOT -1, meaning inserting in between, above or below an existing node")
beginRow = row
elif parent.isValid():
print("case 2: PARENT IS VALID, inserting ONTO something since row was not -1, "
"beginRow becomes 0 because we want to "
"insert it at the beginning of this parents children")
beginRow = parent.row()
else:
print("case 3: PARENT IS INVALID, inserting to root, "
"can change to 0 if you want it to appear at the top")
beginRow = self.rowCount(QModelIndex())
print(f"row={row}, beginRow={beginRow}")
encodedData = data.data(self.myMimeTypes)
stream = QDataStream(encodedData, QIODevice.ReadOnly)
newItems = []
rows = 0
while stream.atEnd() is False:
text = stream.readQString()
newItems.append(str(text))
rows += 1
self.insertRows(beginRow, rows, QModelIndex()) # 先插入多行
for text in newItems: # 然后给每一行设置数值
idx = self.index(beginRow, 0, QModelIndex())
self.setData(idx, text)
beginRow += 1
return True
class DemoDragDrop(QWidget):
def __init__(self, parent=None):
super(DemoDragDrop, self).__init__(parent)
# 设置窗口标题
self.setWindowTitle('drag&drop in PySide6')
# 设置窗口大小
self.resize(480, 320)
self.initUi()
def initUi(self):
self.vLayout = QVBoxLayout(self)
self.listView = QListView(self)
self.listView.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.listView.setDragEnabled(True)
self.listView.setAcceptDrops(True)
self.listView.setDropIndicatorShown(True)
self.ddm = DragDropListModel() # 该行和下面4行的效果类似
# self.listView.setDragDropMode(QAbstractItemView.InternalMove)
# self.listView.setDefaultDropAction(Qt.MoveAction)
# self.listView.setDragDropOverwriteMode(False)
# self.ddm = QStringListModel()
self.ddm.setStringList(['Item 1', 'Item 2', 'Item 3', 'Item 4'])
self.listView.setModel(self.ddm)
self.printButton = QPushButton("Print")
self.vLayout.addWidget(self.listView)
self.vLayout.addWidget(self.printButton)
self.printButton.clicked.connect(self.printModel)
def printModel(self): # 验证移动view中项目后,背后model中数据也发生了移动
print(self.ddm.data(self.listView.currentIndex()))
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle('fusion')
window = DemoDragDrop()
window.show()
sys.exit(app.exec_())
Why does the following code crash when I call idx.data()?
QVariant ApplicantTableModel::data(const QModelIndex &idx, int role) const
{
if (!idx.isValid()) return QVariant();
if (idx.column() == 10 && role == Qt::DisplayRole)
if(idx.data() == "0")
return "-";
else return "+";
else return QSqlTableModel::data(idx,role);
}
If idx is an index of the same ApplicantTableModel instance (which it should be, otherwise the usage is incorrect) idx.data() will call idx.model()->data(), i.e. the very same ApplicantTableModel::data() function we’re looking at => infinite recursion, which leads to stack overflow/crash.
From your code I guess what you want might be
QVariant ApplicantTableModel::data(const QModelIndex &idx, int role) const
{
if (!idx.isValid())
return QVariant();
if (idx.column() == 10 && role == Qt::DisplayRole) {
if(QSqlTableModel::data(idx,role).toString() == “0")
return "-";
else
return "+";
}
return QSqlTableModel::data(idx, role);
}
I.e. call get the value of data from the base class implementation and change that in this special case.
I wanted to have a tree view which shows the item name, the item description, and two related Boolean values in respective columns. I started by modifying the Editable Tree Mode example, so there's a TreeModel that keeps track of a group of TreeItems, each of which not only has a list of child TreeItems, but also a list of QVariants which stores a set of values that can later be displayed in columns in the QTreeView.
I managed to add two more columns for two Boolean values. I also searched through the net on how to add checkboxes for QTreeView and QAbstractItemModel. I managed to have the checkboxes on the two Boolean columns working okay, as well as the rest of the tree hierarchy. Yet all the items in each column renders a checkbox and a line of text now.
Here's the parts where I've modified from the example, mainly within TreeModel.
treemodel.cpp:
bool TreeModel::isBooleanColumn( const QModelIndex &index ) const
{
bool bRet = false;
if ( !index.isValid() )
{
}
else
{
bRet = ( index.column() == COLUMN_BOL1 ) || ( index.column() == COLUMN_ BOL2 );
}
return bRet;
}
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return 0;
if ( isBooleanColumn( index ) )
{
return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable;
}
else
{
return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
}
QVariant TreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::CheckStateRole )
return QVariant();
TreeItem *item = getItem(index);
if ( role == Qt::CheckStateRole && isBooleanColumn( index ) )
{
Qt::CheckState eChkState = ( item->data( index.column() ).toBool() ) ? Qt::Checked : Qt::Unchecked;
return eChkState;
}
return item->data(index.column());
}
bool TreeModel::setData(const QModelIndex &index, const QVariant &value,
int role)
{
if (role != Qt::EditRole && role != Qt::CheckStateRole )
return false;
TreeItem *item = getItem(index);
bool result;
if ( role == Qt::CheckStateRole && isBooleanColumn( index ) )
{
Qt::CheckState eChecked = static_cast< Qt::CheckState >( value.toInt() );
bool bNewValue = eChecked == Qt::Checked;
result = item->setData( index.column(), bNewValue );
}
else
{
result = item->setData(index.column(), value);
}
if (result)
emit dataChanged(index, index);
return result;
}
mainwindow.cpp:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
…
QStringList headers;
headers << tr("Title") << tr("Description") << tr("Hide") << tr("Lock");
QFile file(":/default.txt");
file.open(QIODevice::ReadOnly);
TreeModel *model = new TreeModel(headers, file.readAll());
file.close();
…
}
The checkboxes under the non-Boolean columns don't respond to user input, and the text under the Boolean columns aren't editable. So functionality-wise there's nothing wrong, but it's still bothersome as far as UI goes.
I'm moving onto having QTreeWidget do the same thing. Meanwhile, I'm couldn't help but wonder if there's something else I'm missing here. I heard one solution is to have a custom delegate; is that the only option?
If there's anyone who can point out what else I need to do, or provide a similar example, I will greatly appreciate it.
I think the problem is in the Data method. You should return QVariant() When the role is CheckStateRole but the column is not boolean.
I had this problem. It occurred in TreeModel::parent() method due to passing child.column() value to createIndex() method. It should be 0 instead. So, instead of
createIndex(parentItem->childNumber(), child.column(), parentItem);
should be
createIndex(parentItem->childNumber(), 0, parentItem);
The reason this is happening is related to a "bug" in peoples implementation of the models data method.
In the example below, only column 2 should show a checkbox.
Problem code:
if role == Qt.CheckStateRole:
if index.column() == 2:
if item.checked:
return Qt.Checked
else:
return Qt.Unchecked
Correct code:
if role == Qt.CheckStateRole:
if index.column() == 2:
if item.checked:
return Qt.Checked
else:
return Qt.Unchecked
return None
In the problematic code, table cells that should not have a checkbox decorator were getting missed and processed by a catch all role handler farther down in the code.
I have a matrix of data, I simply stored it as a QList of QStringLists, all containing an equal number of QStrings. In this way, the data looks almost like a spreadsheet.
I use a QTableView to present this data to the user:
void DialogwitQTableView::setData(QList<QStringList> s)
{
/* Create the data model */
// 1. give it some headers
QStandardItemModel model = new QStandardItemModel(s.count(),25,this); //x Rows and 25 Columns
model->setHorizontalHeaderItem(0, new QStandardItem(QString("Column 1")));
model->setHorizontalHeaderItem(1, new QStandardItem(QString("Column 2")));
// ...
model->setHorizontalHeaderItem(24, new QStandardItem(QString("Column 25")));
// 2. populate the model with the data
for(int i = 0; i < s.count() ; i++)
{
for(int j = 0; j < s[i].count() ; j++)
model->setItem(i,j,new QStandardItem(QString(s[i][j])));
}
ui->NameOfTheTableView->setModel(model);
}
Now, if the user wants to change some of the data, he will just doubleclick in the QTableView in the Dialogbox and edits what he wants.
How do I get that edit also in the data? How can I adapt the QStringList with that new information?
If I search for documentation, I mostly find QTableViews linked to databases, but I don't see how this will work with a simple datastructure in memory. If I go to QtDesigner and click on "go to slots" for the TableView, I also do not see a slot called "datachanged" or anything similar.
Any thoughts? I feel pretty stuck and I am probably overviewing something, any tip is very welcome.
Looking at the doco, a QTableView inherits 6 signals from QAbstractItemView
http://doc.qt.digia.com/qt/qabstractitemview.html#signals
This class has all sorts of functionality for capturing edits, and edit triggers.
Once you can catch when the data is changed you can recommit it back to your model if you are using an MVC view. I am sure there are a lot of examples.
Hope that helps.
I think that for more complicated cases it's always best to use the abstract classes, more specifically QAbstractTableModel in this case.
Looking at this file, I just replaced Contact with StringList and changed the getters and setters. Check it out:
https://doc.qt.io/qt-5/qtwidgets-itemviews-addressbook-tablemodel-cpp.html
TableModel::TableModel(QObject *parent) :
QAbstractTableModel(parent)
{
}
TableModel::TableModel(QList<QStringList> stringLists, QObject *parent) :
QAbstractTableModel(parent),
stringLists(stringLists)
{
}
int TableModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return stringLists.size();
}
int TableModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 2; // put the amount of columns here
}
QVariant TableModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) return QVariant();
if (index.row() >= stringLists.size() || index.row() < 0) return QVariant();
if (role == Qt::DisplayRole) {
const auto &strings = stringLists.at(index.row());
if (index.column() == 0)
return strings[0];
else if (index.column() == 1)
return strings[1];
}
return QVariant();
}
QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole)
return QVariant();
if (orientation == Qt::Horizontal) {
switch (section) {
case 0:
return tr("String 1");
case 1:
return tr("String 2");
default:
return QVariant();
}
}
return QVariant();
}
bool TableModel::insertRows(int position, int rows, const QModelIndex &index)
{
Q_UNUSED(index);
beginInsertRows(QModelIndex(), position, position + rows - 1);
for (int row = 0; row < rows; ++row)
stringLists.insert(position, { QString(), QString() });
endInsertRows();
return true;
}
bool TableModel::removeRows(int position, int rows, const QModelIndex &index)
{
Q_UNUSED(index);
beginRemoveRows(QModelIndex(), position, position + rows - 1);
for (int row = 0; row < rows; ++row)
stringLists.removeAt(position);
endRemoveRows();
return true;
}
bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (index.isValid() && role == Qt::EditRole) {
int row = index.row();
auto strings = stringLists.at(row);
if (index.column() == 0)
strings[0] = value.toString();
else if (index.column() == 1)
contact[1] = value.toString();
else
return false;
stringLists.replace(row, contact);
emit dataChanged(index, index, {role});
return true;
}
return false;
}
Qt::ItemFlags TableModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) return Qt::ItemIsEnabled;
return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
}
QList<QStringList> TableModel::getStringLists() const
{
return stringLists;
}
I also highly recommend that you read this:
https://doc.qt.io/qt-5/modelview.html
Hope it helps.
If I search for documentation, I mostly find QTableViews linked to
databases, but I don't see how this will work with a simple
datastructure in memory.
QTableView is a part of Qt's Model/View framework. There are bunch of examples of model-views.
How do I get that edit also in the data? How can I adapt the
QStringList with that new information?
At least these solutions exists:
You can grab all data from QStandardItemModel via item method.
Connect to QStandardItemModel::itemChanged signal.
You can make your own model via subclassing (and I suggest to base on QAbstractTableModel) and implement several methods (data, setData + several utility methods).