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

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.

Related

QTreeWidgetItem setting not selectable clears the selection

I have a QTreeWidget and I want certain rows to be non select-able, which can be achieved by QTreeWidgetItem::setFlags(treeWidgetItem->flags() & ~Qt::ItemIsSelectable).
The problem is that I have an existing row that is already selected and later I click on the non select-able row, selectedItems() returns an empty list. I want the selected row to keep its selection if the user tries to select a non select-able row.
Should I keep track of the selection and handle this scenario in the code, or this can be achieved somehow else. I'd rather not reinvent the wheel.
Thank you.
Cause
Calling QTreeView::mousePressEvent(event) clears the selection when clicked on a non-selectable item if the selection mode is set to QAbstractItemView::SingleSelection.
Solution
My solution would be to either:
Set the selection mode to QAbstractItemView::MultiSelection,
or (in case this is not desired):
Reimplement the mouse events in a subclass of QTreeWidget in order to bypass the default behavior.
Note: In either case, use the QItemSelectionModel::selectionChanged signal to get the list of the selected items.
Example
Here is an example re-implementation of the mouse events in MyTreeWidget preventing the selection of being cleared by clicking a non-selectable item. The top item is expanded/collapsed on a double click:
void MyTreeWidget::mousePressEvent(QMouseEvent *event)
{
if (indexAt(event->pos())->flags() & Qt::ItemIsSelectable)
QTreeWidget::mousePressEvent(event);
}
void MyTreeWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
QTreeWidget::mouseDoubleClickEvent(event);
QTreeWidgetItem *item = itemAt(event->pos());
if (item && item->childCount())
item->setExpanded(!item->isExpanded());
}
The modified in the described manner version of the provided example is available on GitHub.
Improvements
Special thanks to #eyllanesc for making this example more waterproof by:
adding a check if item is not NULL
replacing itemAt with indexAt

Finding a wxMenu's Selected Radio Item

Let's say that I have a group of radio items in a wxMenu. I know that exactly one of these will be checked at any given time.
Does the wxMenu or some other construct hold onto the index of the checked item, or do I need to call the isChecked on each radio item till I find the checked element to find it's index?
I've asked this question about how to do that, but I'd much prefer wxWidgets saved me from doing that everywhere.
No, saving the index of the last selected item (as shown in ravenspoint's answer) or using wxMenuBarBase::IsChecked() until you find the selected radio button is the only way to do it.
For wxWidgets to provide access to the currently selected button it would need not only to store it (which means not forgetting to update not only when the selected changes, but also when items are inserted into/deleted from the menu, so it's already not completely trivial), but to somehow provide access to the radio items group you're interested in, which would require being able to identify it and currently there is no way to do it and adding it isn't going to be particularly simple.
What could be done easily, however, is writing a reusable function int GetIndexOfSelectedRadioItem(int firstItem) that would start at the given item and call IsChecked() on subsequent items until it returns true and return the offset of the item. You should be able to do it in your own code, but if you'd like to include such function in wxWidgets itself (as a static wxMenuBar method, probably), please don't hesitate to send patches/pull requests doing it!
It is easy enough to roll your own.
Bind an event handler to wxEVT_COMMAND_RADIOBUTTON_SELECTED for every button. In the handler, extract the ID of the selected radio button and store it somewhere.
Like this:
ResolMenu = new wxMenu();
ResolMenu->AppendRadioItem(idRcvLoRez,"Low Resolution");
ResolMenu->AppendRadioItem(idRcvMeRez,"Medium Resolution");
ResolMenu->AppendRadioItem(idRcvHiRez,"High Resolution");
ResolMenu->Check( idRcvLoRez, true );
Bind(wxEVT_MENU,&cFrame::onRcvRez,this,idRcvLoRez);
Bind(wxEVT_MENU,&cFrame::onRcvRez,this,idRcvMeRez);
Bind(wxEVT_MENU,&cFrame::onRcvRez,this,idRcvHiRez);
void onRcvRez( wxCommandEvent& event )
{
myRezID = event.GetId();

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

QTreeView::scrollTo not working

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.

How to thread the sorting of a QSortFilterProxyModel?

The main view of my application contains a one-level (no children) QTreeView that displays on average 30,000 items. Due to the way the items are created, they are inserted into the model unsorted. This means that on application startup I have to sort the items in the view alphabetically, which takes nearly 1 second, leaving an unresponsive grey screen until it is done. (Since the window hasn't painted yet)
Is there any way I could get the sorting of a QSortFilerProxyModel into a separate thread, or are there any other alternative ways to approach this problem?
Here's my lessThan() code, for reference: (left and right are the two QModelIndexes passed to the function)
QString leftString = left.data(PackageModel::NameRole).toString();
QString rightString = right.data(PackageModel::NameRole).toString();
return leftString < rightString;
Thanks in advance.
Don't sort the items in the view. Add them to a temporary list and sort that list using QtConcurrent::run. When done (use a QFutureWatcher to know when), set up your model. While sorting is being performed, you can display a "please wait" message or a throbber.