Qt signal emit in addressbook - qt

I am learning the Model-View in Qt by the AddressBook example. https://doc.qt.io/qt-5/qtwidgets-itemviews-addressbook-example.html
And I find something interesting. The code construct a TableModel class besed on QAbstractTableModel. In the override setData function, it emit the dataChanged signal. But, there is no signal emit in removeRows/insertRows. Then, how can these function update the View.
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) {
listOfPairs.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();
QPair<QString, QString> p = listOfPairs.value(row);
if (index.column() == 0)
p.first = value.toString();
else if (index.column() == 1)
p.second = value.toString();
else
return false;
listOfPairs.replace(row, p);
emit(dataChanged(index, index));
return true;
}
return false;
}

Make note of function calls beginRemoveRows() and endRemoveRows() in the function removeRows() of your posted code.
The beginRemoveRows() function, emits a signal rowsAboutToBeRemoved(). This is how connected views can know about the deletion and the underlying connected views must handle before data is removed.
Look the note in below documentation:
https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows

Related

Qt: How to implement simple internal drag&drop for reordering items in QListView using a custom model

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_())

QTreemodel multiple QVariant role

I am using this Example http://doc.qt.io/qt-5/qtwidgets-itemviews-editabletreemodel-example.html and need to pass a Color as Forgoundroll to the data, but cant figure it out.
In the treemodel.cpp i have altered the data as following..
QVariant TreeModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::ForegroundRole)
return QVariant();
TreeItem *item = getItem(index);
if(role==Qt::ForegroundRole) {
QBrush redBackground(QColor(Qt::red));
return redBackground;
} else
return item->data(index.column());
}
... which works (items get the red color, but need to control the color from the mainwindow.cpp and let user set it and have different colors per column/row. Apparently i need to alter the Treemodel:setdata method, but cant figure it out.
So seeking for the setdata method..
bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (role != Qt::EditRole && role != Qt::ForegroundRole )
return false;
TreeItem *item = getItem(index);
bool result;
if(role==Qt::ForegroundRole ) {
//do what ???????
} else {
result= item->setData(index.column(), value);
}
if (result) emit dataChanged(index, index);
return result;
}
From mainwindow.cpp i need to set it like ..
model->setData(child, QVariant(rowdata.at(column)), Qt::EditRole); // this provides the text of the inserted row
model->setData(child, QVariant(QBrush(Qt::red)), Qt::ForegroundRole); // this to provide its color
... but i get the text of the color #ffffff instead (in red color though).
Any help would be appreciated. Thanks
You have to save the color somewhere. One option is to add it to the TreeItem:
class TreeItem
{
public:
...
void setColor(const QColor& color) { this->color = color; }
const QColor& getColor() const { return color; }
private:
...
QColor color;
};
And in the model, you simply set the color if it's the appropriate role, something along these lines:
bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
TreeItem *item = getItem(index);
bool result = false;
switch (role)
{
case Qt::EditRole:
result = item->setData(index.column(), value);
break;
case Qt::ForegroundRole:
item->setColor(value.value<QColor>());
result = true;
break;
default:
break;
}
if (result)
emit dataChanged(index, index);
return result;
}
And similarly in getData(), you return something like item->getColor().
Also, you don't have to use QBrush, AFAIK you can simply return QColor as a ForegroundRole.

Qt - QTreeView and custom model with checkbox columns

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.

using QList<QStringList> to populate a QTableView. When something gets changed in the View, how do I get it back in the data?

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).

HowTo restore QTreeView last expanded state?

What I have:
QTreeView class with table data
And connected QAbstractTableModel model
Question: how to save expanded state of items? Is some one have finished solutions?
PS: I know, that I can do this code by myself, but I don't have much time, and this is not the major problem of our project, but still we need it, because app contain a lot of such tables, and every time expanding tree items is annoyed process...
First, thanks to Razi for persistentIndexList and isExpanded way.
Second, here is the code which works for me just fine :-)
dialog.h file:
class Dialog : public QDialog
{
Q_OBJECT;
TreeModel *model;
TreeView *view;
public:
Dialog(QWidget *parent = 0);
~Dialog(void);
void reload(void);
protected:
void createGUI(void);
void closeEvent(QCloseEvent *);
void saveState(void);
void restoreState(void);
};
dialog.cpp file:
Dialog::Dialog(QWidget *parent)
{
createGUI();
reload();
}
Dialog::~Dialog(void) {};
void Dialog::reload(void)
{
restoreState();
}
void Dialog::createGUI(void)
{
QFile file(":/Resources/default.txt");
file.open(QIODevice::ReadOnly);
model = new TreeModel(file.readAll());
file.close();
view = new TreeView(this);
view->setModel(model);
QVBoxLayout *mainVLayout = new QVBoxLayout;
mainVLayout->addWidget(view);
setLayout(mainVLayout);
}
void Dialog::closeEvent(QCloseEvent *event_)
{
saveState();
}
void Dialog::saveState(void)
{
QStringList List;
// prepare list
// PS: getPersistentIndexList() function is a simple `return this->persistentIndexList()` from TreeModel model class
foreach (QModelIndex index, model->getPersistentIndexList())
{
if (view->isExpanded(index))
{
List << index.data(Qt::DisplayRole).toString();
}
}
// save list
QSettings settings("settings.ini", QSettings::IniFormat);
settings.beginGroup("MainWindow");
settings.setValue("ExpandedItems", QVariant::fromValue(List));
settings.endGroup();
}
void Dialog::restoreState(void)
{
QStringList List;
// get list
QSettings settings("settings.ini", QSettings::IniFormat);
settings.beginGroup("MainWindow");
List = settings.value("ExpandedItems").toStringList();
settings.endGroup();
foreach (QString item, List)
{
// search `item` text in model
QModelIndexList Items = model->match(model->index(0, 0), Qt::DisplayRole, QVariant::fromValue(item));
if (!Items.isEmpty())
{
// Information: with this code, expands ONLY first level in QTreeView
view->setExpanded(Items.first(), true);
}
}
}
Have a nice day!)
PS: this example based on C:\Qt\4.6.3\examples\itemviews\simpletreemodel code.
Thanks to Razi and mosg I was able to get this working. I made it restore the expanded state recursively so I thought I would share that part.
void applyExpandState_sub(QStringList& expandedItems,
QTreeView* treeView,
QAbstractItemModel* model,
QModelIndex startIndex)
{
foreach (QString item, expandedItems)
{
QModelIndexList matches = model->match( startIndex, Qt::UserRole, item );
foreach (QModelIndex index, matches)
{
treeView->setExpanded( index, true );
applyExpandState_sub(expandedItems,
treeView,
model,
model->index( 0, 0, index ) );
}
}
}
Then use like:
void myclass::applyExpandState()
{
m_treeView->setUpdatesEnabled(false);
applyExpandState_sub( m_expandedItems,
m_treeView,
m_model,
m_model->index( 0, 0, QModelIndex() ) );
m_treeView->setUpdatesEnabled(true);
}
I am using the Qt::UserRole here because multiple items in my model can have the same display name which would mess up the expand state restoration, so the UserRole provides a unique identifier for each item to avoid that problem.
These two function by using a loop should do that for you:
QModelIndexList QAbstractItemModel::persistentIndexList () const
bool isExpanded ( const QModelIndex & index ) const
Here is a general approach that should work with any QTreeView based widget, that uses some sort of ID system to identify elements (I am assuming the ID is an int, which is stored inside the Qt::UserRole):
void MyWidget::saveExpandedState()
{
for(int row = 0; row < tree_view_->model()->rowCount(); ++row)
saveExpandedOnLevel(tree_view_->model()->index(row,0));
}
void Widget::restoreExpandedState()
{
tree_view_->setUpdatesEnabled(false);
for(int row = 0; row < tree_view_->model()->rowCount(); ++row)
restoreExpandedOnLevel(tree_view_->model()->index(row,0));
tree_view_->setUpdatesEnabled(true);
}
void MyWidget::saveExpandedOnLevel(const QModelIndex& index)
{
if(tree_view_->isExpanded(index)) {
if(index.isValid())
expanded_ids_.insert(index.data(Qt::UserRole).toInt());
for(int row = 0; row < tree_view_->model()->rowCount(index); ++row)
saveExpandedOnLevel(index.child(row,0));
}
}
void MyWidget::restoreExpandedOnLevel(const QModelIndex& index)
{
if(expanded_ids_.contains(index.data(Qt::UserRole).toInt())) {
tree_view_->setExpanded(index, true);
for(int row = 0; row < tree_view_->model()->rowCount(index); ++row)
restoreExpandedOnLevel(index.child(row,0));
}
}
Instead of MyWidget::saveExpandedState() and MyWidget::saveExpandedState() one could also directly call MyWidget::saveExpandedOnLevel(tree_view_->rootIndex()) and MyWidget::restoreExpandedOnLevel(tree_view_->rootIndex()). I only used the above implementation because the for loop will be called anyway and MyWidget::saveExpandedState() and MyWidget::saveExpandedState() looked cleaner with my SIGNAL and SLOT design.
I have reworked iforce2d's solution into this:
void ApplyExpandState(QStringList & nodes,
QTreeView * view,
QAbstractItemModel * model,
const QModelIndex startIndex,
QString path)
{
path+=QString::number(startIndex.row()) + QString::number(startIndex.column());
for(int i(0); i < model->rowCount(startIndex); ++i)
{
QModelIndex nextIndex = model->index(i, 0, startIndex);
QString nextPath = path + QString::number(nextIndex.row()) + QString::number(nextIndex.column());
if(!nodes.contains(nextPath))
continue;
ApplyExpandState(nodes, view, model, model->index(i, 0, startIndex), path);
}
if(nodes.contains(path))
view->setExpanded( startIndex.sibling(startIndex.row(), 0), true );
}
void StoreExpandState(QStringList & nodes,
QTreeView * view,
QAbstractItemModel * model,
const QModelIndex startIndex,
QString path)
{
path+=QString::number(startIndex.row()) + QString::number(startIndex.column());
for(int i(0); i < model->rowCount(startIndex); ++i)
{
if(!view->isExpanded(model->index(i, 0, startIndex)))
continue;
StoreExpandState(nodes, view, model, model->index(i, 0, startIndex), path);
}
if(view->isExpanded(startIndex))
nodes << path;
}
This way there is no need to match data. Obviously - for this approach to work, tree needs to stay relatively unchanged. If you somehow change the order of tree items - it will expand wrong nodes.
Here is a version which doesn't rely on nodes having a unique Qt::UserRole or Qt::DisplayRole - it just serialises the entire QModelIndex
header:
#pragma once
#include <QTreeView>
class TreeView : public QTreeView
{
Q_OBJECT
public:
using QTreeView::QTreeView;
QStringList saveExpandedState(const QModelIndexList&) const;
void restoreExpandedState(const QStringList&);
};
source:
#include "tree_view.h"
#include <QAbstractItemModel>
namespace
{
std::string toString(const QModelIndex& index)
{
std::string parent = index.parent().isValid() ? toString(index.parent()) : "X";
char buf[512];
sprintf(buf, "%d:%d[%s]", index.row(), index.column(), parent.c_str());
return buf;
}
QModelIndex fromString(const std::string& string, QAbstractItemModel& model)
{
int row, column;
char parent_str[512];
sscanf(string.c_str(), "%d:%d[%s]", &row, &column, parent_str);
QModelIndex parent = *parent_str == 'X' ? QModelIndex() : fromString(parent_str, model);
return model.index(row, column, parent);
}
}
QStringList TreeView::saveExpandedState(const QModelIndexList& indices) const
{
QStringList list;
for (const QModelIndex& index : indices)
{
if (isExpanded(index))
{
list << QString::fromStdString(toString(index));
}
}
return list;
}
void TreeView::restoreExpandedState(const QStringList& list)
{
setUpdatesEnabled(false);
for (const QString& string : list)
{
QModelIndex index = fromString(string.toStdString(), *model());
setExpanded(index, true);
}
setUpdatesEnabled(true);
};
For a QFileSystemModel, you can't use persistentIndexList().
Here is my work around. It works pretty well, even if I do say so myself. I haven't tested to see what happens if you have a slow loading filesystem, or if you remove the file or path.
// scrolling code connection in constructor
model = new QFileSystemModel();
QObject::connect(ui->treeView, &QTreeView::expanded, [=](const QModelIndex &index)
{
ui->treeView->scrollTo(index, QAbstractItemView::PositionAtTop);//PositionAtCenter);
});
// save state, probably in your closeEvent()
QSettings s;
s.setValue("header_state",ui->treeView->header()->saveState());
s.setValue("header_geometry",ui->treeView->header()->saveGeometry());
if(ui->treeView->currentIndex().isValid())
{
QFileInfo info = model->fileInfo(ui->treeView->currentIndex());
QString filename = info.absoluteFilePath();
s.setValue("last_directory",filename);
}
// restore state, probably in your showEvent()
QSettings s;
ui->treeView->header()->restoreState(s.value("header_state").toByteArray());
ui->treeView->header()->restoreGeometry(s.value("header_geometry").toByteArray());
QTimer::singleShot(1000, [=]() {
QSettings s;
QString filename = s.value("last_directory").toString();
QModelIndex index = model->index(filename);
if(index.isValid())
{
ui->treeView->expand(index);
ui->treeView->setCurrentIndex(index);
ui->treeView->scrollTo(index, QAbstractItemView::PositionAtCenter);
qDebug() << "Expanded" << filename;
}
else
qDebug() << "Invalid index" << filename;
} );
Hope that helps someone.
My approach was to save the list of expanded items (as pointers) and when restoring, only set as expanded only the items in this list.
In order to use the code below, you may need to replace TreeItem * to a constant pointer to your object (that doesn't change after a refresh).
.h
protected slots:
void restoreTreeViewState();
void saveTreeViewState();
protected:
QList<TargetObject*> expandedTreeViewItems;
.cpp
connect(view->model(), SIGNAL(modelAboutToBeReset()), this, SLOT(saveTreeViewState()));
connect(view->model(), SIGNAL(modelReset()), this, SLOT(restoreTreeViewState()));
...
void iterateTreeView(const QModelIndex & index, const QAbstractItemModel * model,
const std::function<void(const QModelIndex&, int)> & fun,
int depth=0)
{
if (index.isValid())
fun(index, depth);
if (!model->hasChildren(index) || (index.flags() & Qt::ItemNeverHasChildren)) return;
auto rows = model->rowCount(index);
auto cols = model->columnCount(index);
for (int i = 0; i < rows; ++i)
for (int j = 0; j < cols; ++j)
iterateTreeView(model->index(i, j, index), model, fun, depth+1);
}
void MainWindow::saveTreeViewState()
{
expandedTreeViewItems.clear();
iterateTreeView(view->rootIndex(), view->model(), [&](const QModelIndex& index, int depth){
if (!view->isExpanded(index))
{
TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
if(item && item->getTarget())
expandedTreeViewItems.append(item->getTarget());
}
});
}
void MainWindow::restoreTreeViewState()
{
iterateTreeView(view->rootIndex(), view->model(), [&](const QModelIndex& index, int depth){
TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
if(item && item->getTarget())
view->setExpanded(index, expandedTreeViewItems.contains(item->getTarget()));
});
}
I think this implementation gives extra flexibility compared to some of the others here. At least, I could not make it work with my custom model.
If you want to keep new items expanded, change the code to save the collapsed items instead.

Resources