QTreeView Move Item with Button - c++

I'm using a QTreeView with a QStandardItemModel and I'm trying to figure out how to move items up and down the tree using buttons . I can do drag and drop no problem, but what I would like to do is have some buttons associated with "move up" and "move down" functions. I just cant find anything on the subject. There seems to be a "moveRow()" function for the model object, but I cant find any documentation on it so I'm not sure if its what I need. Any information you could give to point me in the right direction would be greatly appreciated!
PS Here are my QT Creator stats:
Qt Creator 2.6.2
Based on Qt 5.0.1 (64 bit)

Your hunch is correct. moveRow() is the right function to call.
To move items within one parent (it's a tree, after all), you'd do moveRow(parent, index.row(), parent, index.row() + delta), where delta is set to 1 or -1 depending on whether you move down or up, respectively.
If you want to allow items to be moved between parents, you'll need additional logic to figure out the destination parent if the item would be moved past its parent.
Do note that it's considered bad design if the move button are separate from the items to be moved. Your delegate should display up and down arrows for each item, in its row, so that you can move things with one click. When there is a contiguous selection, the delegates should merge the up/down arrows to cover all of the items. When the selection is non-contiguous, the up/down arrows should disappear.
With separate buttons, you need two clicks: first select the item, then click up/down. This sucks from user experience point of view.

moveRow doesn't seem to work in QTreeView.
Here's a simple (PyQt5) solution for moving an item "up one" relative to its sibling(s). It must have at least one sibling "above". Also this is only for moving between siblings, not where you want to move a row to a different parent. However, I think that these issues could be engineered without difficulty on the basis of this code.
One thing to be aware of here is that the QStandardItem doesn't change in operations like this, but that the associated QModelIndex does.
# in almost all QTreeView implementations the "tree" structure is at column 0...
curr_index = self.selectionModel().currentIndex().siblingAtColumn(0)
if curr_index.isValid():
curr_item = self.model().itemFromIndex(curr_index)
curr_row = curr_index.row()
# if this is the top sibling of its parent it cant be moved up...
if curr_row > 0:
parent_item = self.model().itemFromIndex(curr_index.parent())
# NB parent_item is None in the case of root children:
# in that case you therefore use "takeRow" and "insertRow" methods of the model, not the item!
take_row = self.model().takeRow if parent_item == None else parent_item.takeRow
insert_row = self.model().insertRow if parent_item == None else parent_item.insertRow
row_to_move = take_row(curr_row)
insert_row(curr_row - 1, row_to_move)
new_index = self.model().indexFromItem(curr_item)
# now set the (single) selection, and set current, back to the moved item
# (if you are implementing single-selection, obviously)
flag = QtCore.QItemSelectionModel.SelectionFlag
self.selectionModel().setCurrentIndex(new_index, flag.Clear | flag.SelectCurrent)

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

MFC How to make ctreectrl virtual?

My application is displaying a Hugh amount of files system entries held in memory using a ctreectrl , adding all the items takes ~20seconds even when using SetRedraw(False) , so how to make a completely virtual(breadth,depth) ctreectrl & how to populate it ?
Edit#1
I want to display the displayed portion items expanded from the beginning , but I don't want to store them in the tree, for example
Root-->
Child1-->
SubChile1
Child2
Child3
you must not add all items at once. you must add only top level items with cChildren = I_CHILDRENCALLBACK
and handle WM_NOTIFY
with code == TVN_GETDISPINFO if mask & TVIF_CHILDREN set
cChildren (TRUE or FALSE)
with code == TVN_ITEMEXPANDING, action == TVE_EXPAND - expand
node - add only direct child items (one level) again with
cChildren = I_CHILDRENCALLBACK
and possible
with code == TVN_ITEMEXPANDED, action == TVE_COLLAPSE - collapse
node - remove all childs
sense of cChildren = I_CHILDRENCALLBACK - if you add folder to list, you not need at once initialize it (open handle, enum childs) - only when you first time got I_CHILDRENCALLBACK (this when your item become visible, but if containing folder large enough(like system32) - it have too many items, but visible only several top at begin, new begin visible when user scroll down)- open folder, determinate are it have subitems (and based on this set cChildren) but not full enumerate it (do this only on <TVN_ITEMEXPANDING, TVE_EXPAND>
I have no advice to make it virtual.
I use for large tree structures the possibility of collecting a child branch only when it is need. I trap TVN_ITEMEXPANDING
So how to do it: First read the first level (root), than keep all root node collapsed and read all child nodes of the root (just 1 level deep) and populate them.
When a node expands, you already have the nodes, now read the next level below the childs of the expanding node.
So you see only the expanded nodes plus one invisible level.
I do this in this way to show all nodes that are expandable with the + sign. All nodes without children nodes are shown without it as leafs.
The second way is not to fill the string data and let the tree load it via callback. But the impact is small. The real problem with the speed are the number of nodes.

Forcing QGraphicsItem to use z value and ignore the parent child relationship

I have problem using QGraphicsItem class.
There is a base_parent, base_part, part_1, part_2. The base_parent is the parent of the base_part. part_1 and part_2 are children of the base_part. What I want to do is to set the Z value of this items in a way that part_1 and part_2 stack behind the base_parent and the base_part stacks in front of the base_parent. So what I've implemented is something like this:
base_part->setParentItem(base_parent);
part_1->setParentItem(base_part);
part_2->setParentItem(base_part);
At first the tried setting the z value using setZValue() but obviously it didn't work.
base_parent->setZValue(0);
base_part->setZValue(1);
part_1->setZValue(-1);
part_2->setZValue(-2);
In the documentation it is said that:
An item's children are stacked on top of the parent
I was giving up but then I saw another parameter.
You can call setZValue() on an item to explicitly stack it on top of,
or under, other sibling items. The default Z value for an item is 0.
Items with the same Z value are stacked by insertion order.
You can call stackBefore() to reorder the list of children. This will
directly modify the insertion order.
You can set the ItemStacksBehindParent flag to stack a child item
behind its parent.
So I tried setting the ItemStacksBehindParent flag but it moved the stacked the whole child behind, meaning the base_part, part_1 and part_2 were stacked behind the base_parent.
Afterwards I found another flag, ItemNegativeZStacksBehindParent.
The item automatically stacks behind it's parent if it's z-value is negative. This flag enables setZValue() to toggle ItemStacksBehindParent. This flag was introduced in Qt 4.6.
So I tried again:
base_part->setFlag(QGraphicsItem::ItemNegativeZStacksBehindParent);
part_1->setFlag(QGraphicsItem::ItemNegativeZStacksBehindParent);
part_2->setFlag(QGraphicsItem::ItemNegativeZStacksBehindParent);
Using this flag stacks part_1 and part_2 behind the base_part but they are still stack in front of the base_parent.
I want to know if there's a way to force items to use only Z value and ignore the parent child relationship.
The main reason for insisting on hierarchical structure is that I want to animate the base_part and the part_1 and part_2 must move along the base_part, Of course I know my way around this problem, If only I set parent of the part_1 and part_2 to base_parent, I'll be able to get the result I wanted, but then I have to animate part_1 and part_2 as well which is a little nasty.

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.

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);
}