i have a lot of data that i represent as QStandardItems inside two QTreeViews. I want to retrieve the visual position of an item inside the left tree in order to align the selection of it with the item inside the right tree. The user should be able to double-click on the desired item inside the left tree, afterwards which the right tree should represent the item corresponding to his selection. So far im using a function with the doubleClicked signal of the left tree as starting point for the logic.
self.tree_view_left.doubleClicked.connect(self.double_clicked)
Inside of double_clicked im retrieving the current index of the left tree and then im searching for his equivalent inside the right tree (NOTE i want to match only roots - not child elements).
def double_clicked(self):
index = self.selection_model.currentIndex()
tree_left = self.tree_view_left
tree_right = self.tree_view_right
if index.isValid():
item = index.model().itemFromIndex(index)
if item.hasChildren():
item_search = tree_right .model().findItems(item.text(),
Qt.MatchRecursive)
# TODO: Find/Calc visual position of StandardItem inside TreeView
left_vis_item = tree_left.indexAt(tree_left.rect().topLeft())
right_vis_item = tree_left.indexAt(tree_left.rect().bottomRight())
# TODO: Move vertical scrollbar of right TreeView based on that visual position
# Change right slider position
tree_right.scrollTo(item_search[0].index())
tree_right.selectionModel().select(item_search[0].index(), QItemSelectionModel.Select)
I have found similiar issues in the forum here, here and here, but either they had a used a different widget (QTreeWidget) or they used a function which i cannot implement (QAbstractItemView::visualRect. My approach was at first to retrieve the visual position by finding out the visible indices (left_vis_item, right_vis_item), but then i thought of using the mouse position. The last one is new to me. Any ideas?
EDIT: I have now added a MRE with a bare minimum of data from a mockup created with this website. The MainWindow Class, which holds (here minimum) main features, looks like this:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Example")
window_geometry = self.frameGeometry()
center_screen = QDesktopWidget().availableGeometry().center()
window_geometry.moveCenter(center_screen)
self.move(window_geometry.topLeft())
group_box_left = QGroupBox('1. Tree view')
group_box_right = QGroupBox('2. Tree view')
self.tree_view_left = QTreeView(self)
self.tree_view_left.setEditTriggers(QAbstractItemView.NoEditTriggers)
tree_view_left_header = self.tree_view_left.header()
tree_view_left_header.setDefaultSectionSize(200)
tree_model_left = QStandardItemModel()
self.tree_view_right = QTreeView(self)
tree_model_right = QStandardItemModel()
# Add data
with open("mockup.json", 'r') as f:
data = json.load(f)
self.import_data(data, tree_model_left)
self.import_data(data, tree_model_right)
# Use filled model for tree view
self.tree_view_left.setModel(tree_model_left)
self.tree_view_left.setVisible(True)
self.tree_view_left.setExpandsOnDoubleClick(False)
self.tree_view_right.setModel(tree_model_right)
self.selection_model = self.tree_view_left.selectionModel()
self.tree_view_left.doubleClicked.connect(self.double_clicked)
# Layout
hor_box_left = QVBoxLayout()
hor_box_left.addWidget(self.tree_view_left)
group_box_left.setLayout(hor_box_left)
hor_box_right = QVBoxLayout()
hor_box_right.addWidget(self.tree_view_right)
group_box_right.setLayout(hor_box_right)
main_layout = QHBoxLayout()
main_layout.addWidget(group_box_left)
main_layout.addWidget(group_box_right)
main_layout.setContentsMargins(100, 0, 100, 0)
main_layout.setSpacing(50)
widget = QWidget()
widget.setLayout(main_layout)
self.setCentralWidget(widget)
And the function for data import is:
def import_data(self, data, model):
for i in range(0, len(data)):
root = QStandardItem(str(i))
root.appendRows([QStandardItem(data[i]["Firstname"]), QStandardItem(data[i]["Lastname"]),
QStandardItem(data[i]["City"])])
model.appendRow(root)
I will explain the issue with snapshots. In the first snapshot you can see that there is an offset between the selected item in the left Tree and the right.. The desired behaviour would be to remove this offset. The current implementation works with no offset ONLY if the items are collapsed (which will not always be the case) . I will also provide the used data for this example below:
[
{
"Firstname": "Dotty",
"Lastname": "O'Neill",
"City": "Tucson"
},
{
"Firstname": "Libbie",
"Lastname": "Shuler",
"City": "Surat Thani"
},
{
"Firstname": "Caressa",
"Lastname": "Henebry",
"City": "Tucson"
},
{
"Firstname": "Larine",
"Lastname": "Schalles",
"City": "Gdańsk"
},
{
"Firstname": "Maryellen",
"Lastname": "Bord",
"City": "Palembang"
},
{
"Firstname": "Genovera",
"Lastname": "Desai",
"City": "Basra"
},
{
"Firstname": "Fina",
"Lastname": "Thad",
"City": "Mecca"
},
{
"Firstname": "Emylee",
"Lastname": "Flita",
"City": "Vancouver"
},
{
"Firstname": "Aurore",
"Lastname": "Lipson",
"City": "Makati City"
},
{
"Firstname": "Florie",
"Lastname": "Hachmin",
"City": "Douala"
},
{
"Firstname": "Brooks",
"Lastname": "Grayce",
"City": "Birmingham"
},
{
"Firstname": "Kristan",
"Lastname": "Allina",
"City": "Bahía Blanca"
},
{
"Firstname": "Dorothy",
"Lastname": "Remmer",
"City": "Nouméa"
},
{
"Firstname": "Kittie",
"Lastname": "Kussell",
"City": "Detroit"
},
{
"Firstname": "Lexine",
"Lastname": "Letsou",
"City": "Palikir"
},
{
"Firstname": "Madelle",
"Lastname": "Joseph",
"City": "Mwanza"
},
{
"Firstname": "Olivette",
"Lastname": "Faust",
"City": "Pelotas"
},
{
"Firstname": "Damaris",
"Lastname": "Slifka",
"City": "Graz"
},
{
"Firstname": "Dennie",
"Lastname": "Merat",
"City": "Abuja"
},
{
"Firstname": "Augustine",
"Lastname": "Burnside",
"City": "Arbil"
}
]
Qt doesn't provide a way to position an item at a specific position. But scrollTo()
actually has a second optional argument that allows to ensure that the item is shown at a "reference" position.
Knowing that, we can try to scroll to the index and show it at the bottom, then manually update the vertical scroll bar value until it reaches the required position.
WARNING: this can only work properly as long as:
ScrollPerItem
);If any of the above is not respected, you'll get unexpected results. There might also be inconsistencies in case one of the view is showing the horizontal scroll bar and the other isn't, specifically when the left view is scrolled to the bottom and when the horizontal scroll bar is shown the viewport cannot show full items only.
I modified your function also adding the index
parameter that the doubleClicked
signal automatically provides.
I also changed the search function to the more conventional QAbstractItemModel.match()
, removing the Qt.MatchRecursive
flag, since you only want to match top level items of both the source and destination models.
Finally, you generally only need to use setCurrentIndex()
, and using the Select
flag alone will update the selection, possibly showing multiple selected items (the proper flag would be ClearAndSelect
). Also, you should never use persistent references to the selection model (the reason is explained in the setModel()
) documentation).
def double_clicked(self, index):
if not index.isValid() or index.parent().isValid():
return
right_model = self.tree_view_right.model()
matches = right_model.match(right_model.index(0, 0),
Qt.DisplayRole, index.data())
if not matches:
return
right_index = matches[0]
self.tree_view_right.setCurrentIndex(right_index)
self.tree_view_right.scrollTo(right_index,
QAbstractItemView.PositionAtBottom)
leftRect = self.tree_view_left.visualRect(index)
sb = self.tree_view_right.verticalScrollBar()
sbMax = sb.maximum()
while sb.value() < sbMax:
rightRect = self.tree_view_right.visualRect(right_index)
if (
leftRect.contains(rightRect) # the visual rects match
or leftRect.y() > rightRect.y() # the right rect is above
):
break
sb.setValue(sb.value() + 1)
Note that the leftRect.y() > rightRect.y()
condition is required in case the source item is next the first top rows of the model and any item above it is expanded, while the left match is not; consider this case:
scrollTo(index, PositionAtBottom)
can only try to show the index at the bottom of the view, but since the views can only show 10 items, the eighth item on the right will never be at the bottom;