QTreeView::scrollTo not working - c++

Qt 4.8
I have a QTreeView based class with an asociated QAbstractItemModel based class. If I reload the model with new information I want to expand/scroll the tree to a previous selected item.
Both clases, tree view and model are correctly created and connected using QTreeView::setSelectionModel(...) working everything properly.
After reloading the model, I get a valid index to the previous selected item and I scrollTo it:
myTreeView->scrollTo(index);
but the tree is not expanded. However, if I expand the tree manually, the item is really selected.
Tree view is initialized in contruct with:
header()->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
header()->setStretchLastSection(false);
header()->setResizeMode(0, QHeaderView::ResizeToContents);
Any idea about expanding the tree to the selection?

Even QTreeView::scrollTo documentation says:
Scroll the contents of the tree view until the given model item index is
visible. The hint parameter specifies more precisely where the item should
be located after the operation. If any of the parents of the model item
are collapsed, they will be expanded to ensure that the model item is visible.
That is not really true (I think)
If solved the problem expanding all previous tree levels manually:
// This slot is invoqued from model using last selected item
void MyTreeWidget::ItemSelectedManually(const QModelIndex & ar_index)
{
std::vector<std::pair<int, int> > indexes;
// first of all, I save all item "offsets" relative to its parent
QModelIndex indexAbobe = ar_index.parent();
while (indexAbobe.isValid())
{
indexes.push_back(std::make_pair(indexAbobe.row(), indexAbobe.column()));
indexAbobe = indexAbobe.parent();
}
// now, select actual selection model
auto model = _viewer.selectionModel()->model();
// get root item
QModelIndex index = model->index(0, 0, QModelIndex());
if (index.isValid())
{
// now, expand all items below
for (auto it = indexes.rbegin(); it != indexes.rend() && index.isValid(); ++it)
{
auto row = (*it).first;
auto colum = (*it).second;
_viewer.setExpanded(index, true);
// and get a new item relative to parent
index = model->index(row, colum, index);
}
}
// finally, scroll to real item, after expanding everything above.
_viewer.scrollTo(ar_index);
}

I just dealt with similar situation, setting model index via setModelIndex (which internally ends up with scrollTo) worked OK for one of my models, but badly for the other one.
When I forcefully expanded all level 1 items, the scrollTo worked just as described above (calling expandAll suffices).
The reason was a bug in my model class in:
QModelIndex MyBadModel::parent(const QModelIndex& index) const
and as I fixed that, things got normal there too. The bug was such that internalId of parent model index was not the same as when this same model index (for parent) is calculated "from other direction", therefore in this model index (returned by parent method) could not be found in the list of visual indices.

Everything was simple.
Just change autoExpandDelay property from -1 to 0(for example).
ui->treeView->setAutoExpandDelay(0);

QTreeView::scrollTo should expand the hierarchy appropriately.
It's likely that your QModelIndex object is being invalidated when the model is updated (and perhaps still selecting the correct row because the row information is still valid though the parentage is not, don't ask me how those internals work). From the QModelIndex documentation:
Note: Model indexes should be used immediately and then discarded. You should not rely on indexes to remain valid after calling model functions that change the structure of the model or delete items. If you need to keep a model index over time use a QPersistentModelIndex.
You can certainly look into the QPersistentModelIndex object, but like it says in its documentation:
It is good practice to check that persistent model indexes are valid before using them.
Otherwise, you can always query for that item again after the model refresh.

Recently I struggled with same problem.
It's most likely a bug in your model class implementation.
in my case the row() method (that supposed to return index of the index under its parent) was not implemented correctly.
Sadly QT doesnt complain about that, and even selects the index (if you expand manually you will notice).
So, just go though the model code and hunt for bugs in row() and parent() methods etc.

You may be calling scrollTo before the tree view has finished reacting to the changes in current index and which indices are expanded/collapsed. A possible solution may be to delay the call to scrollTo by connecting it to a single-shot timer like this:
QTimer::singleShot(0, [this]{scrollTo(index);});
Using the timer will delay the call until control is passed back to the event queue.

Related

Making only one column of QTreeWidget editable // troubleshooting

Mind that this question isn't a duplicate of question Making only one column of a QTreeWidgetItem editable, as it's proposed solution doesn't work.
Hello, so I just want to make only ONE column of my treeWidget editable.
propertyItems.push_back(new QTreeWidgetItem); //gets filled by the while-loop
propertyItems[propertyItems.size()-1]->setText(0, prop.name); //sets the text of the item
propertyItems[propertyItems.size()-1]->setText(1, prop.value);//set the text of the other item
propertyItems[propertyItems.size()-1]->setFlags(Qt::ItemIsEditable);
ui->treeWidget_3->insertTopLevelItem(ui->treeWidget_3->topLevelItemCount(), propertyItems[propertyItems.size()-1]); //appends the items
counter ++;
and
void MainWindow::onTreeWidget3ItemDoubleClicked()
{
if (ui->treeWidget_3->currentColumn() == 2) {
ui->treeWidget_3->editItem(ui->treeWidget_3->currentItem(), ui->treeWidget_3->currentColumn());
}
}
is my approach. ontreeWidget3ItemDoubleClicked is connected with treeWidget::doubleClicked, treeWidget_3 has NO edit-triggers
BUT: when I execute the programm, the QTreeView is just grayed out.
That said, I also tried
propertyItems[propertyItems.size()-1]->setFlags(propertyItems[propertyItem.size()].flags | Qt::ItemIsEditable);
The treeWidget_3 isn't grayed off anymore, but it is still uneditable...
How can I fix this?
BTW: I am a newb to Qt so I might have forgotten something crucial. Sorry in this case.
As mentioned in the documentation:
The QTreeWidgetItem class provides an item for use with the QTreeWidget convenience class.
It means that it won't work for all use cases. The solution is to create your own model and overload the flags(const QModelIndex& index) method returning the appropriate values (basically Qt:: ItemIsEnabled for read-only columns and Qt:: ItemIsEnabled | Qt::ItemIsEditable for the editable one). You can get the column from index.column().
Qt provides an example to start with trees and models.

QT: Searching through QTreeView/QFileSystemModel with a QDtring for a QModelIndex index

I'm working on a custom QDialog for the user to choose a directory on the filesystem. I'm using a QFIleSystemModel inside a QTreeView. Whenever the user selects an item in the QTreeView the directory is written to a QLineEdit. My problem is I would like to do the opposite of-sorts by expanding through the QTreeView nodes by taking the typed text and... well... obviously expanding the nodes if the text typed is an existing, absolute path.
I've searched for quite a few variations of my problem(although I know it's very loaded) and looked through a lot of the documentation of the classes but I can't find anything to really help me. I'm guessing I need to use QTreeView::expand(const QModelIndex) to expand them after finding but searching through the index is my biggest problem as of now. I'm open for any suggestions and any help is truly appreciated. Thank you in advanced and sorry for making such a wall of text.
searching through the index is my biggest problem as of now
And index is just a "pointer" to an item in the model. You can't search "through" it, because there is nothing "in" an index. It's just a pointer to exactly one item.
You should search through the model. The index(const QString & path) method does that for you. Given a path, it returns an index into the element at the end of the path. You can then iterate upwards to extend the items:
// ...because QModelIndex::operator= doesn't exist
QModelIndex & assign(QModelIndex & dst, const QModelIndex & src) {
dst.~QModelIndex();
new (&dst) QModelIndex(src);
return dst;
}
void extend(const QString & path, QTreeView * view) {
auto fs = qobject_cast<QFileSystemModel*>(view->model());
if (!fs) return;
auto idx = fs->index(path);
// ascend up from the child item and expand items in the tree
while (idx.isValid()) {
view->expand(idx);
assign(idx, idx.parent());
}
}
You can use this method with the last item removed from the path as well, since - presumably - the last item might not be valid, and thus fs->index might fail.
Have you check here?
[Model/View Programming][Insert and delete rows in QTreeView]
http://doc.qt.io/qt-4.8/model-view-programming.html
QTreeView & QAbstractItemModel & insertRow
As base design principle, View only render the data.You should not expect to modify the data via View directly.

The easiest way to make a "tracking" item selection in TreeView?

Okay, so I have a QTreeView and a Model subclassed from QAbstractItemModel set to it. So far so good.
The data of the model gets updated every constant time interval, for example, two seconds. Let's say for simplicity the items are just short integers, and have no hierarchical structure. All items are unique.
t=0:
497
1739
18
125
19
The user selects the item 18:
t=1:
497
1739
[18]
125
19
t=2, the list of items updates and changes!
497
18
[1739]
125
For example, 1739 and 18 are now swapped, and the last 19 disappears.
Whoops! 1739 is now selected, which seems legit: the user has selected the third item, and it's still being selected even after an update.
I hope the question is now obvious: I need the selection to "track" what item exactly is selected. Using some internal id of the item itself. So if the new list contains an item with the same id as the previously selected one, it gets selected.
I could change the selection myself on update, or use insertRow and removeRow, but I don't know what exactly (1739 and 18 got swapped) has changed in the list, I just receive a new list of integers.
I've looked at QItemSelectionModel, and although the Qt documentation says
The QItemSelectionModel class keeps track of a view's selected items.
, it doesn't look like what I need. I thought it is another proxy model I need to subclass, but it's not.
Next I thought my Model is the right place to write the code that will track the selection, but I've failed again: a model is unable to retrieve the selection model of the view (view->selectionModel()), because it cannot access the view(s).
I could end up writing some signals and slots in my Model so that the code outside the Model could update the selection, plus a slot outside the model connected to the view's selectionModel updating the id of the currently selected item stored in the Model, etc, etc, but before writing such things I thought of asking if there already is some practice I don't know about.
There is so few documentation and articles explaining all these stuff, and I was unable to find an answer to the above myself.
Thank you!
It would be best to detect when items are added/moved/deleted from model and emit respective signals, but this would be to complex and probably you don't need such complex functionality.
Assuming that you are interested in single selection you can do that quite simply. When current selection changes simply store selected value.
When you will be notified that model has changed just find new position of items and update selection.
void onCurrentSelectionChange(const QModelIndex & index) { // slot
selectedValue = index->data(); // update field: QVariant selectedValue
}
void onModelReset() { // slot connected to signal where model reports that it was changed
QModelIndex newSelection = yourModel->findIndexOfValue(selectedValue); // you have to provide such method in your moel
emit newSelection(newSelection); // signal connected to setCurrentIndex slot of QTreeView or selection model
}
If you want handle more complex selection it will be more trouble.
I think you provide the answer your self. You can use an internal id that is unique for each item:
template<typename T>
class Item {
public:
Item(int id, T data);
// Access methods here.
private:
int id;
T data;
};
Then, every time you receive the timeout from your timer you can update your item selection list :
vector<int> selection;
for (int &index: treeview->selectionModel()->selectedIndexes()){
selection.push_back(model[index].id); // This assumes you implement [] operator for your model.
}
// Do modify list.
// and the using QModelSelection select again every item which id is in
// `selection`
That way you can "track" the selected items.
It is not a good idea depend on the item value, since this limit the solution to a model containing uninque values. Neither is good idea use the selectionChanged signal since it will be triggered by the user but also by you when change selections programmatically, and that would generate an infinite loop.

What is the best way to be notified of changes in an QAbstractItemView

I have a QListView that has its model (derived from QAbstractItemModel) regularly changed based on a some criteria in the UI. I would like to be notified when the view itself believes a new row has been added/removed either when the current model updates or when the model is changed. I need this notification so that I can call setIndexWidget and add a custom control under a particular column. I would prefer not to call setIndexWidget repeatedly because the population of the widget is expensive. So once per row would be ideal.
I've tried rowsInserted/rowsAboutToBeRemoved and dataChanged but those don't get called if the model being set into the view already has items in it.
Any thoughts would be greatly appreciated.
You're wrong about dataChanged. If the contents of any data item change, then dataChanged is signaled. The following invariant holds, assuming that the dataChanged slot is connected to the same signal on the model.
class ... : public QObject {
QModelIndex m_index;
bool m_changed;
Q_SLOT void dataChanged(const QModelIndex & tl) {
m_changed = m_changed || tl == m_index;
}
...
};
m_index = QModelIndex(...);
QVariant before, after;
m_changed = false;
before = model->data(index);
after = model->data(index);
Q_ASSERT(before == after || m_changed);
What you're describing is most likely caused by incorrect behavior of your model. There is a model test suite you could use to verify compliance of your model with required invariants.
Addressing your question points specifically:
I would like to be notified when the view itself believes a new row has been added/removed either when the current model updates...
The view doesn't need to believe anything. Your model must be emitting relevant signals to that effect. Simply connect to those signals from some QObject. That's all the view is doing. If the signals don't fire, the view won't be notified. End of story.
... or when the model is changed.
There's no signal for that since the entire model is replaced. You're the one who calls setModel on the view, so that shouldn't be a problem. You better know when the call is made :)

Qt QTreeView: Only allow to drop on an existing item

I've got a custom model inherited from QTreeView. I've enabled drag and drop and can currently drop an item onto the tree. However, you can currently drop onto either an existing item or between items. I would like to restrict this so that you can only drop onto existing items.
I've set DragDropOverwriteMode to true (actually this is the default for QTreeView). However, this doesn't stop you from dropping between items - it just means you can also drop onto existing items.
I know that I can ignore the "insert" drops in dropMimeData (which I am reimplementing), by checking whether row and column are valid (drops onto existing items have row and column set to -1 and parent set to the current item) and I am doing this. However, I would prefer not to get these drops. Ie. I would like it so that you are always dropping over either the item above or the item below, never between items.
Any ideas?
Thanks for any advice,
Giles
You'' need to catch the drag enter events by reimplementing the dragEnterEvent method in your custom view. The example from the Qt docs is:
void Window::dragEnterEvent(QDragEnterEvent *event)
{
if (event->mimeData()->hasFormat("text/plain"))
event->acceptProposedAction();
}
In your case, you would probably need to compare the x and y position in the event with the x and y position of the closest item or something similar and reject or accept the proposed action based on that information.
From the QAbstractItemModel::dropMimeData documentation:
It is the responsibility of the view to provide a suitable location for where the data should be inserted.
Which I have interpreted to mean that the view should reject the drop if it's not something that's supported by an underlying model, such as yours.
As of Qt 5.4 (and I assume this was true even in Qt 4.8), setting DragDropOverwriteMode to true will correctly cause the drags to be droppable only on existing items and prevents the "above/below items" drop targets from appearing.
Also, unlike what the question claims, DragDropOverwriteMode is set to false by default for QTreeView (I didn't check, maybe it's newer Qt versions), so it needs to be set to true manually.
However it is still useful to be able to calculate on what positions the item can be dropped. For example in QTreeView, one cannot drop a dragged thing on the left margin of the items, i.e the red area below:
If something is dropped in the invalid red area, dropMimeData will be called with a parent argument set to NULL. So it would be useful to ignore the dragMoveEvent in advance to show a 'you can't drop here' cursor to the user so they know they can't drop there. Qt doesn't implement changing the mouse cursor on invalid areas while dragging (as of Qt 5.4), but we can do it like this:
bool SubclassedTreeView::dropResultsInValidIndex(const QPoint& pos)
{
QTreeWidgetItem* item = itemAt(pos);
if (item == NULL || !indexFromItem(item).isValid())
return false;
return visualRect(indexFromItem(item)).adjusted(-1, -1, 1, 1).contains(pos, false);
}
virtual void SubclassedTreeView::dragMoveEvent(QDragMoveEvent* event)
{
QTreeWidget::dragMoveEvent(event);
if (!event->isAccepted())
return;
if (dropResultsInValidIndex(event->pos()))
event->accept();
else
event->ignore(); //Show 'forbidden' cursor.
}
virtual bool SubclassedTreeView::dropMimeData(QTreeWidgetItem* parent, int index, const QMimeData* data, Qt::DropAction action)
{
Q_UNUSED(index);
//Modify the following action and data format checks to suit your needs:
if (parent == NULL || action != Qt::CopyAction || !data->hasFormat("my/preferred-type"))
return false;
QModelIndex modelIndex = indexFromItem(parent);
//modelIndex is where the data is dropped onto. Implement your custom drop action here...
return true;
}
The above code contains a little part visualRect….adjusted(-1, -1, 1, 1) which was stolen from QAbstractItemViewPrivate::position sources. Actually the sources of this function can be used to calculate the overwrite/insert/invalid areas for the item when QAbstractItemViewPrivate::position is false too.
I would like to suggest a solution based on the current position of the drop indicator (QAbstractItemView::DropIndicatorPosition). It's pretty simple to implement, but unfortunately requires the drop indicator to be shown. However, this might be acceptable in some cases.
TreeView::TreeView(QWidget* parent) : QTreeView(parent)
{
setDropIndicatorShown(true);
}
void TreeView::dragMoveEvent(QDragMoveEvent* event)
{
QTreeView::dragMoveEvent(event);
if (dropIndicatorPosition() != QTreeView::OnItem)
event->setDropAction(Qt::IgnoreAction);
}