I need to determine which rows are selected in a QTableView associated with a QStandardItemModel. From the view, I call the function selectionModel() to get the selection. This function returns a QSelectionModel object. From that object, I want to call isRowSelected() function. This function takes two arguments: the row I want to test, and a parent argument, which is a QModelIndex. This is where I'm lost. What is this parent argument for? Where does it come from? Conceptually, I don't understand why I'd need this parameter, and concretely, I don't know what value I should pass to the function to make it work.
You'll find the parent useful in a QTreeView, for example. For your use case, this are the relevant parts of the documentation:
The index is used by item views, delegates, and selection models to
locate an item in the model... Invalid indexes are often used as parent indexes when referring to top-level items in a model."
With QtCore.QModelIndex() you'll create an invalid index, that's the argument you are looking for. In this example, you can use the context menu to print the selection state of the rows:
#!/usr/bin/env python
#-*- coding:utf-8 -*-
from PyQt4 import QtGui, QtCore
class MyWindow(QtGui.QTableView):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.modelSource = QtGui.QStandardItemModel(self)
for rowNumber in range(3):
items = []
for columnNumber in range(3):
item = QtGui.QStandardItem()
item.setText("row: {0} column: {0}".format(rowNumber, columnNumber))
items.append(item)
self.modelSource.appendRow(items)
self.actionSelectedRows = QtGui.QAction(self)
self.actionSelectedRows.setText("Get Selected Rows")
self.actionSelectedRows.triggered.connect(self.on_actionSelectedRows_triggered)
self.contextMenu = QtGui.QMenu(self)
self.contextMenu.addAction(self.actionSelectedRows)
self.setModel(self.modelSource)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.horizontalHeader().setStretchLastSection(True)
self.customContextMenuRequested.connect(self.on_customContextMenuRequested)
#QtCore.pyqtSlot(bool)
def on_actionSelectedRows_triggered(self, state):
for rowNumber in range(self.model().rowCount()):
info = "Row {0} is ".format(rowNumber)
if self.selectionModel().isRowSelected(rowNumber, QtCore.QModelIndex()):
info += "selected"
else:
info += "not selected"
print info
#QtCore.pyqtSlot(QtCore.QPoint)
def on_customContextMenuRequested(self, pos):
self.contextMenu.exec_(self.mapToGlobal(pos))
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
app.setApplicationName('MyWindow')
main = MyWindow()
main.resize(333, 222)
main.show()
sys.exit(app.exec_())
Related
I use PyQt5 and the QTreeview to show an xml tree (and some of its attributes as columns) loaded from a file. This works as expected. But I'm now struggling with two things:
The itemChange event. The tree node should be editable and I need to do some checks after editing. The itemChange is called but it is called N times when editing one node.
Starting at the edited item I need to go up and down in the tree and check the child and parent nodes. I expected this to be simple like getParent() for one and a recursive getChildren() for the other direction. But how do I get form the itemChange events QStandardItem to the parent and child items?
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
import xml.etree.ElementTree as ET
tree = ET.ElementTree(file='data.xml')
class MainFrame(QtWidgets.QWidget):
def __init__(self, parent=None):
super(MainFrame, self).__init__(parent)
self.tree = QtWidgets.QTreeView(self)
layout = QtWidgets.QHBoxLayout(self)
layout.addWidget(self.tree)
root_model = QtGui.QStandardItemModel()
root_model.setHorizontalHeaderLabels(['Label', 'privilege', 'uid', 'docId'])
self.tree.setModel(root_model)
self.tree.setUniformRowHeights(True)
root = tree.getroot()
self._populateTree(root, root_model.invisibleRootItem())
self.tree.expandAll()
self.tree.resizeColumnToContents(0)
self.tree.resizeColumnToContents(1)
self.tree.resizeColumnToContents(2)
self.move(0, 0)
self.resize(800, 1000)
#self.tree.itemChanged.connect(self.edit)
def _populateTree(self, root, parent):
child_item = QtGui.QStandardItem(root.tag)
child_item.setData(root)
privilege = ''
uid = ''
docId = ''
privilege_item = QtGui.QStandardItem(privilege)
uid_item = QtGui.QStandardItem(uid)
docId_item = QtGui.QStandardItem(docId)
parent.appendRow([child_item, privilege_item, uid_item, docId_item])
privilege_item.model().itemChanged.connect(self.change_privilege)
child_item.model().itemChanged.connect(self.change_privilege)
for elem in root.getchildren():
self._populateTree(elem, child_item)
def change_privilege(self, item):
print(item)
print(item.row(), item.column())
def check_parent(self, item):
# get parent of item and check value
# check_parent(...)
def check_child(self, item):
# get children of item and check value
# for child in item.children():
# check_child(child)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main = MainFrame()
main.show()
sys.exit(app.exec_())
To answer the first question: every time you call MainFrame._populateTree you create a signal-slot connection between the signals child_item.model().itemChanged and privilege_item.model().itemChanged and the slot MainFrame.change_privilege. However, since all child items and privelege item belong to the same model (i.e. root_model) you are effectively creating multiple connections between the same signal and slot. This means that when any item in the model is changed, the slot is called multiple times as well. Easiest way around this is to create one connection in __init__ or so.
To access the parent of an item you can indeed use item.parent(). The children of an item can be accessed one-by-one via item.child(row, column) where row is in the range range(item.rowCount()) and column in the range range(item.columnCount()), e.g.
def change_privilege(self, item):
print(item.row(), item.column(), item.text())
self.check_parent(item)
self.check_child(item)
def check_parent(self, item):
if item.parent():
print('Parent:', item.parent().text())
else:
print('No parent')
def check_child(self, item):
if item.hasChildren():
print('Children:')
for row in range(item.rowCount()):
print('\t', row, end='')
for col in range(item.columnCount()):
print('\t', item.child(row, col).text(), end='')
print()
else:
print('No children')
I'm fairly new to python, and I feel this is an advanced question, with that in mind it might be out of the scope of Stack Exchange. Please bear with me.
I have a QTreeWidget and QStackedWidget. I have populated the QTreeWidget using a tuple
TreeList = ({
'Header1': ((
'Item11',
'Item12',
)),
'Header2': ((
'Item21',
'Item22'
))
})
for key, value in TreeList.items():
root = QTreeWidgetItem(self.QTreeWidget, [key])
for val in value:
root.addChild(QTreeWidgetItem([val]))
I would like to use this same tuple to populate a QStackedWidget with pages that correspond with the QTreeWidget and establish signals and slots with in the same loop. I have tried using:
for key in TreeList['Item1']:
page = QWidget()
page.setObjectName(key)
self.QStackedWidget.addWidget(page)
print(self.QStackedWidget)
but all of the objects point to the same hex value, which I assume means they are not unique. Perhaps I need to increment the index? In either case I have not been able to figure out how to generate the signals and slots while doing this, since they are being dynamically created and I need unique but predictable names.
The idea is that by simply adding a new category or item to the tuple, the QTreeView, QStackedWidget, and signals / slots are all generated at runtime without having to modify any source code beyond the tuple.
What would be the best way to accomplish this?
Sorry if this is too much, but I figured it might be a good question in case anyone else attempts the same thing at a later time.
EDIT: I'm using Python 3.6 and PyQt5
The idea is to associate the index of the QStackedWidget with the QTreeWidgetItem, so we can use the setData() method of QTreeWidgetItem to save the index, and then recover it with the data() method:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Widget(QWidget):
def __init__(self, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
self.tree = QTreeWidget(self)
self.stack = QStackedWidget(self)
lay = QHBoxLayout(self)
lay.addWidget(self.tree)
lay.addWidget(self.stack)
for key, value in TreeList.items():
root = QTreeWidgetItem(self.tree, [key])
for val in value:
item = QTreeWidgetItem([val])
root.addChild(item)
widget = QLabel(val, self)
ix = self.stack.addWidget(widget)
item.setData(0, Qt.UserRole, ix)
self.tree.expandAll()
self.tree.itemClicked.connect(self.onItemClicked)
def onItemClicked(self, item, column):
val = item.data(0, Qt.UserRole)
if val is not None:
self.stack.setCurrentIndex(val)
TreeList = ({
'Header1': ((
'Item11',
'Item12',
)),
'Header2': ((
'Item21',
'Item22'
))
})
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
I have a QListView which displays a list of items using PyQt in Python. How can I get it to return a qlistview specific item when searched for?
For example, if I have the following Qlistview with 4 items, how can I get the item which contains text = dan? or bring it to the top of the list. Also, the search doesn't need to be completely specific, If I type "da" I'd like it to return dan or items that starts with "da" and possibly bring it to the top of the list
My Qlistview is defined as follows:
from PyQt4 import QtCore, QtGui
import os
import sys
class AppView(QtGui.QDialog):
def __init__(self, parent=None):
super(AppView, self).__init__(parent)
self.resize(400, 400)
self.ShowItemsList()
def ShowItemsList(self):
self.setWindowTitle("List")
buttonBox = QtGui.QDialogButtonBox(self)
buttonBox.setOrientation(QtCore.Qt.Horizontal)
buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Ok)
listview = QtGui.QListView(self)
verticalLayout = QtGui.QVBoxLayout(self)
verticalLayout.addWidget(listview)
verticalLayout.addWidget(buttonBox)
buttonBox.accepted.connect(self.close)
model = QtGui.QStandardItemModel(listview)
with open("names-list.txt") as input:
if input is not None:
item = input.readlines()
for line in item:
item = QtGui.QStandardItem(line)
item.setCheckable(True)
item.setCheckState(QtCore.Qt.PartiallyChecked)
model.appendRow(item)
listview.setModel(model)
listview.show()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
view = AppView()
view.show()
sys.exit(app.exec_())
I fixed it like this. I made my model an instance variable beginning with self so that I can access it from another function.
def searchItem(self):
search_string = self.searchEditText.text() # Created a QlineEdit to input search strings
items = self.model.findItems(search_string, QtCore.Qt.MatchStartsWith)
if len(items) > 0:
for item in items:
if search_string:
self.model.takeRow(item.row()) #take row of item
self.model.insertRow(0, item) # and bring it to the top
else:
print "not found"
I am designing a program composed of a 3D viewer and a table with Python 3.4 and PySide bindings.
I have created a TableView with this class:
from PySide import QtGui
from PySide.QtCore import Qt
class MyTableView(QtGui.QWidget):
def __init__(self, parent=None):
super(MyTableView, self).__init__()
self.parent = parent
self.title = "Results"
self.initUI()
def initUI(self):
self.grid = QtGui.QGridLayout(self)
self.table = QtGui.QTableView()
self.grid.addWidget(self.table, 0, 0)
# Configure table
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setDefaultAlignment(Qt.AlignLeft)
self.table.setSortingEnabled(True)
self.table.setAlternatingRowColors(True)
self.table.setShowGrid(False)
self.table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
and the model with this other class:
class MyModel(QStandardItemModel):
def __init__(self, path, *args, **kwargs):
super(MyModel, self).__init__()
self.path = path
self.parse()
def parse(self):
with open(self.path) as f:
self.mydata = yaml.load(f)
self.setColumnCount(len(self.mydata['headers']) + 1)
self.setHorizontalHeaderLabels(
['ID'] + self.mydata['headers'])
row = 0
for ind, val in self.mydata['rows'].items():
col = 0
self.insertRow(row)
self.setItem(row, col, QStandardItem(ind))
for v in val:
col += 1
self.setItem(row, col, QStandardItem(str(v)))
row += 1
which are then tied together in this controller:
from PySide.QtCore import Qt
class MyController(object):
def __init__(self, model, view):
self.model = model
self.tableview = view.table
self.fill_table()
self.connect_signals()
def fill_table(self):
self.tableview.setModel(self.model)
self.tableview.sortByColumn(0, Qt.AscendingOrder)
def connect_signals(self):
selectionModel = self.tableview.selectionModel()
selectionModel.selectionChanged.connect(self.selection_changed)
def selection_changed(self, selected, deselected):
print("Selection changed.")
Then, the program is executed through this script:
def main():
app = QtGui.QApplication(sys.argv)
MyController(MyModel(sys.argv[1]), MyView())
sys.exit(app.exec_())
if __name__ == '__main__':
main()
(Note that I haven't posted the main window class, but you get the idea)
The table gets rendered OK, but I am not able to connect the selectionChanged signal to the handler (which should udpate the viewer, but for testing purposes it's only a print statement).
What am I doing wrong? Thanks!
[EDIT]
I have discovered that it works if I use a lambda function to call the handler method. Can someone explain why?!
selectionModel.selectionChanged.connect(lambda: self.selection_changed(selectionModel.selectedRows()))
I tried to implement what you wrote and it worked - so I can't be 100% sure of why you are having trouble. But I suspect it is because of what I had to fix to get it to work at all: I had to sort out some problems you have with garbage collection.
In the example code you give, you create a MyController, a MyModel and a MyView. But they will all then be garbage collected (in CPython) since you don't keep a reference to them. If you add a reference to MyController
my_controller = MyController(MyModel(sys.argv[1]), MyView())
you are almost there, but I think the MyTableView might also then be garbage collected since the controller only keeps a reference to the QTableVIew not the MyTableView.
Presumably using the lanbda function changes the references you are preserving - it preserves the controller and the selection model - and that may be why it is working in that case.
Generally it's a good idea to use the Qt parenting mechanism. If you simply parented all these objects on the main window (or their natural parent widget) that would have prevented most of these problems.
If a list selection does not exist for filtered results then I would like to automatically highlight the first item. I created the method force_selection() that highlight's the first item if nothing is selected. I am using the QListView.selectionModel() to determine the selection index. I have tried connecting force_selection() to the QLineEdit slots: textEdited(QString) and textChanged(QString). However, it appears that there is a timing issue between textChanged and the proxy refreshing the QListView. Sometimes the selection is made while other times it disappears.
So how would I go about forcing a selection (blue highlight) during a proxy filter if the user has not made a selection yet? The idea behind my code is that the user searches for an item, the top item is the best result so it is selected (unless they manually select another item in the filter view).
You can find an image of the problem here.
Recreate issue:
Execute sample script with Python 2.7
Do not select anything in the list (QLineEdit should have focus)
Search for 'Red2', slowly type 'R', 'e', 'd' --> Red1 and Red2 are visible and Red1 is highlighted
Finish the search by typing the number '2' --> Red2 is no longer highlighted/selected
Final solution:
from PySide import QtCore
from PySide import QtGui
class SimpleListModel(QtCore.QAbstractListModel):
def __init__(self, contents):
super(SimpleListModel, self).__init__()
self.contents = contents
def rowCount(self, parent):
return len(self.contents)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
return str(self.contents[index.row()])
class Window(QtGui.QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
data = ['Red1', 'Red2', 'Blue', 'Yellow']
self.model = SimpleListModel(data)
self.view = QtGui.QListView(self)
self.proxy = QtGui.QSortFilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.proxy.setDynamicSortFilter(True)
self.proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.view.setModel(self.proxy)
self.search = QtGui.QLineEdit(self)
self.search.setFocus()
layout = QtGui.QGridLayout()
layout.addWidget(self.search, 0, 0)
layout.addWidget(self.view, 1, 0)
self.setLayout(layout)
# Connect search to proxy model
self.connect(self.search, QtCore.SIGNAL('textChanged(QString)'),
self.proxy.setFilterFixedString)
# Moved after connect for self.proxy.setFilterFixedString
self.connect(self.search, QtCore.SIGNAL('textChanged(QString)'),
self.force_selection)
self.connect(self.search, QtCore.SIGNAL('returnPressed()'),
self.output_index)
# #QtCore.Slot(QtCore.QModelIndex)
#QtCore.Slot(str)
def force_selection(self, ignore):
""" If user has not made a selection, then automatically select top item.
"""
selection_model = self.view.selectionModel()
indexes = selection_model.selectedIndexes()
if not indexes:
index = self.proxy.index(0, 0)
selection_model.select(index, QtGui.QItemSelectionModel.Select)
def output_index(self):
print 'View Index:',self.view.currentIndex().row()
print 'Selected Model Current Index:',self.view.selectionModel().currentIndex()
print 'Selected Model Selected Index:',self.view.selectionModel().selectedIndexes()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
The problem is the order of connect calls. You connect textChanged to force_selection first, so it's called first. But at that time, filter is not processed and proxy is not updated. So you select an item that might soon be removed by filtering.
Just switch the order of connect calls.
By the way, you might want to reconsider your logic in force_selection. currentIndex doesn't necessarily correspond to selected indexes. You can observe that by typing red2 and deleting 2. You'll get both Red1 and Red2 selected. If you want to deal with currentIndex use setCurrentIndex instead of select. If you want to deal with selected indexes, then your condition should be based on selectedRows or selectedIndexes.