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.
Related
I know there have been a lot of times question was answered on stackoverflow about how to set row height for QTableView. I'm asking one more time but my question is not exactly about "how", at least not so simple. I'm setting row height successfully with help of Qt.SizeHintRole in data method of my custom model derived from QAbstractTableModel - see code below. (Also tried very similar example but with help of sizeHint() method of QStyledItemDelegate - the result is exactly the same.)
It works pretty good when I have MODEL_ROW_COUNT about 100 as in example below. But my dataset has ~30-40 thousands of rows. As result this simple application starts about 30 seconds with MODEL_ROW_COUNT=35000 for example.
The reason of this big delay is this line of code:
self.table_view.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
Everything works really fast with MODEL_ROW_COUNT=35000 if I would comment this line. But in this case data() method is not called with Qt.SizeHintRole and I can't manipulate row height.
So, my question is - how to set row height on a per row basis for dataset with thousands of rows? Below example works but takes 30 seconds to start with 35 000 rows (after window is shown everything is fluent)...
At the same time if I use QSqlTableModel it doesn't have this problem and I may use sizeHint() of QStyledItemDelegate without big problems. But it's a mess to have too many delegates... May I subclass QStyledItemDelegate instead of QAbstractTableModel to implement my custom model? (I'm not sure that it will work as every source recomment to subclass QAbstractTableModel for custom models...)
Or I did something wrong and there is a better way than usage of QHeaderView.ResizeToContents?
P.S. I really need different heights. Some rows in database have less data and I may show them in a couple of cells. But others have more data and I need extra space to display it. The same height for all rows will mean either waste of space (a lot of white space on a screen) or lack of essential details for some data rows. I'm using contant CUSTOM_ROW_HEIGHT only too keep example as much simple as possible and reproducible with ease - you may use any DB with any large table (I think I may re-create it even without DB... will try soon)
from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QTableView, QHeaderView
from PySide2.QtSql import QSqlDatabase, QSqlQuery
from PySide2.QtCore import Qt, QAbstractTableModel, QSize
class MyWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
self.db = QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("/home/db.sqlite")
self.db.open()
self.table_model = MyModel(parent=self, db=self.db)
self.table_view = QTableView()
self.table_view.setModel(self.table_model)
# SizeHint is not triggered without this line but it causes delay
self.table_view.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
layout = QVBoxLayout(self)
layout.addWidget(self.table_view)
self.setLayout(layout)
class MyModel(QAbstractTableModel):
CUSTOM_ROW_HEIGHT = 300
MODEL_ROW_COUNT = 100
MODEL_COL_COUNT = 5
def __init__(self, parent, db):
QAbstractTableModel.__init__(self, parent)
self.query = QSqlQuery(db)
self.query.prepare("SELECT * FROM big_table")
self.query.exec_()
def rowCount(self, parent=None):
return self.MODEL_ROW_COUNT
def columnCount(self, parent=None):
return self.MODEL_COL_COUNT
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if role == Qt.DisplayRole:
if self.query.seek(index.row()):
return str(self.query.value(index.column()))
if role == Qt.SizeHintRole:
return QSize(0, self.CUSTOM_ROW_HEIGHT)
return None
def main():
app = QApplication([])
win = MyWindow()
win.show()
app.exec_()
if __name__ == "__main__":
main()
Ok, Thanks to #musicamante I realized that I missed canFetchMore() and fetchMore() methods. So, I implemented dynamic size property and these methods in MyModel class. It was not hard at all and now I have better performance than QSqlTableModel and identical visual behavior with direct conrol of visible buffer size. Below is new code of MyModel class:
class MyModel(QAbstractTableModel):
CUSTOM_ROW_HEIGHT = 300
MODEL_ROW_COUNT = 37000
MODEL_COL_COUNT = 5
PAGE_SIZE = 500
def __init__(self, parent, db):
QAbstractTableModel.__init__(self, parent)
self.query = QSqlQuery(db)
self.query.prepare("SELECT * FROM big_table")
self.query.exec_()
self._current_size = self.PAGE_SIZE
def rowCount(self, parent=None):
return self._current_size
def columnCount(self, parent=None):
return self.MODEL_COL_COUNT
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if role == Qt.DisplayRole:
if self.query.seek(index.row()):
return str(self.query.value(index.column()))
if role == Qt.SizeHintRole:
return QSize(0, self.CUSTOM_ROW_HEIGHT)
return None
def canFetchMore(self, index):
return self._current_size < self.MODEL_ROW_COUNT
def fetchMore(self, index):
self.beginInsertRows(index, self._current_size, self._current_size + self.PAGE_SIZE - 1)
self._current_size += self.PAGE_SIZE
self.endInsertRows()
Updated Question
I think my original quandary might be a result of the structure of my PyQt app. The way I've approached creating a GUI is to divide the larger widget into smaller pieces, each given their own class until the parts are simple enough. Because of this, I end up with a ton of nesting, as a large widget holds instances of smaller widgets, and those hold their own even smaller widgets. It makes it hard to navigate data around the app.
How should a PyQt app be structured so that it is simple to understand in code and yet has a structure containing very little nesting? I haven't found many examples of this around, so I'm sort of stuck. The code example in my original question shows a pretty good example of the structure I'm currently using, which has a large amount of nesting.
Info on program
The GUI is used to create a set of parameters for running a test. The options in each setting should correspond to a binary number, and all of the binary numbers indicated by each set of options are collected, formed into a single sequence of binary numbers, and passed on. Changes to settings do not have to be carried over between sessions, as each new session will most likely correspond to a new test (and thus a new set of choices for settings).
The basic flow of the app should be that upon opening it, all available settings (about 20 total) are set to their default values. A user can go through and change whatever settings they would like, and once they're done they can press a "Generate" button to gather all of the binary numbers corresponding to the settings and create the command. It would be very helpful to have a live preview of individual bits that updates as settings are changed, which is why updates must be immediate.
Some settings are dependent on other; for instance, Setting A has 4 options, and if option 3 is selected, Setting B should be made visible, otherwise it is invisible.
Original Question
I'm definitely a beginner to PyQt, so I don't quite know if I've worded my question correctly, but here goes. I've got a GUI wherein I'm attempting to take a bunch of different settings, keep track of what number was selected from each setting, and then pass the number up to an object that keeps track of all of the numbers from all of the settings. The trouble is that I don't know the best way to get all the individual settings values up my tree of classes, so to speak. Here's the structure of my GUI so far:
Bottom: individual custom QWidgets, each responsible for a single setting. Each has a signal that fires whenever the value it returns changes.
Middle: a QWidget containing ~7-10 individual settings each. These collect settings into related groups.
Top: a QTabWidget that places each instance of a setting group into an individual tab. This widget also contains an object that should ideally collect all of the settings from individual groups into it.
My question is how do I get the values from the bottom layer signals to the top layer widget? My only idea is to connect all of the signals from those small setting widgets to a signal in the middle layer, and connect the middle layer signal to something in the top layer. This sort of chaining seems crazy, though.
I'm running PyQt5 and Python 3.7.
Here's some stripped down code which hopefully shows what I want to do.
class TabWindow(QTabWidget):
def __init__(self):
super().__init__()
self.tabs = [SettingsGroup1, SettingsGroup2, SettingsGroup3]
self.setting_storage = { # dictionary is where I'd like to store all settings values
# 'setting name': setting value
}
for tab in self.tabs:
self.addTab(tab, 'Example')
class SettingsGroup(QWidget):
def __init__(self):
super().__init__()
# not shown: layout created for widget
self.settings = []
def add_to_group(self, new_setting):
self.settings.append(new_setting)
# not shown: add setting to the layout
class SettingsGroup1(SettingsGroup):
def __init__(self):
super().__init__()
self.add_to_group([Setting1, Setting2, Setting3])
class SettingsGroup2(SettingsGroup):...
class SettingsGroup3(SettingsGroup):...
class Setting(QWidget):
val_signal = pyqtSignal([int], name='valChanged')
def __init__(self, name):
self.val = None
self.name = name
def set_val(self, new_val):
self.val = new_val
self.val_signal.emit(self.val) # <-- the signal I want to pass up
class Setting1(Setting):
def __init__(self, name):
super().__init__(name)
# not shown: create custom setting layout/interface
class Setting2(Setting):...
class Setting3(Setting):...
I use a lot of inheritance (SettingsGroup -> SettingsGroup1, 2, 3) because each subclass will have its own functions and internal dependencies that are unique to it. For each Setting subclass, for instance, there is a different user interface.
Thanks for any help provided!
EDIT: The question has been updated in the meantime, I've added a solution that's more specific at the bottom of this answer.
I feel like this question is slightly "opinion based", but since I've had my share of similar situations I'd like to propose my suggestions. In these situations it's important to understand that there's not one good way to do things, but many ways to do it wrong.
Original answer
An idea could be to create a common signal interface for every "level", which will get that signal and send it back to its parent by adding its own name to keep track of the setting "path"; the topmost widget will then evaluate the changes accordingly.
In this example every tab "group" has its own valueChanged signal, which includes the group name, setting name and value; the source signal is fired from the "source" (a spinbox, in this case), then it follows its parents which, in turn "add" their name in turn.
Keep in mind that you can also just use a generalized pyqtSignal(object) for every parent and connect it with widget.valueChanged.connect(self.valueChanged), and then track its group and setting by walking by self.sender() parents backwards.
As a final notice, if you are using these values for application settings, remember that Qt already provides the QSettings API, which can be used as a common and OS-transparent interface for every configuration you need to set (and remember between sessions) in your application. I implemented it in the example, but I suggest you to read its documentation to better understand how it works.
import sys
from PyQt5 import QtCore, QtWidgets
class SettingWidget(QtWidgets.QWidget):
valueChanged = QtCore.pyqtSignal(int)
def __init__(self, name):
super().__init__()
self.settings = QtCore.QSettings()
self.val = 0
self.name = name
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
layout.addWidget(QtWidgets.QLabel(self.name))
self.spinBox = QtWidgets.QSpinBox()
layout.addWidget(self.spinBox)
self.spinBox.valueChanged.connect(self.set_val)
def set_val(self, new_val):
if self.val != new_val:
self.val = new_val
self.valueChanged.emit(self.val)
# enter a setting group, ensuring that same name settings won't
# be mismatched; this allows a single sub level setting only
self.settings.beginGroup(self.parent().name)
self.settings.setValue(self.name, new_val)
# leave the setting group. THIS IS IMPORTANT!!!
self.settings.endGroup()
class SettingWidget1(SettingWidget):
def __init__(self):
super().__init__('Setting1')
class SettingWidget2(SettingWidget):
def __init__(self):
super().__init__('Setting2')
class SettingWidget3(SettingWidget):
def __init__(self):
super().__init__('Setting3')
class SettingsGroup(QtWidgets.QWidget):
# create two signal signatures, the first sends the full "path",
# while the last will just send the value
valueChanged = QtCore.pyqtSignal([str, str, int], [int])
def __init__(self, name):
super().__init__()
self.name = name
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
def add_to_group(self, new_setting):
widget = new_setting()
# emit both signal signatures
widget.valueChanged.connect(
lambda value, name=widget.name: self.valueChanged.emit(
self.name, name, value))
widget.valueChanged.connect(self.valueChanged[int])
self.layout().addWidget(widget)
class SettingsGroup1(SettingsGroup):
def __init__(self):
super().__init__('Group1')
self.add_to_group(SettingWidget1)
self.add_to_group(SettingWidget2)
class SettingsGroup2(SettingsGroup):
def __init__(self):
super().__init__('Group2')
self.add_to_group(SettingWidget3)
class TabWidget(QtWidgets.QTabWidget):
def __init__(self):
QtWidgets.QTabWidget.__init__(self)
self.settings = QtCore.QSettings()
self.tabs = [SettingsGroup1, SettingsGroup2]
self.settingsDict = {}
for tab in self.tabs:
widget = tab()
self.addTab(widget, widget.__class__.__name__)
widget.valueChanged[str, str, int].connect(self.valueChangedFullPath)
widget.valueChanged[int].connect(self.valueChangedOnly)
def valueChangedFullPath(self, group, setting, value):
# update the settings dict; if the group key doesn't exist, create it
try:
self.settingsDict[group][setting] = value
except:
self.settingsDict[group] = {setting: value}
settingsData = [group, setting, value]
print('Full path result: {}'.format(settingsData))
# Apply setting from here, instead of using the SettingWidget
# settings.setValue() option; this allows a single sub level only
# self.applySetting(data)
def valueChangedOnly(self, value):
parent = sender = self.sender()
# sender() returns the last signal sender, so we need to track down its
# source; keep in mind that this is *not* a suggested approach, as
# tracking the source might result in recursion if the sender's sender
# is not one of its children; this system also has issues if you're
# using a Qt.DirectConnection from a thread different from the one that
# emitted it
while parent.sender() in sender.children():
parent = sender.sender()
widgetPath = []
while parent not in self.children():
widgetPath.insert(0, parent)
parent = parent.parent()
settingsData = [w.name for w in widgetPath] + [value]
print('Single value result: {}'.format(settingsData))
# similar to valueChangedFullPath(), but with this implementation more
# nested "levels" can be used instead
# self.applySetting(settingsData)
def applySetting(self, settingsData):
# walk up to the next to last of settingsData levels, assuming they are
# all parent group section names
for count, group in enumerate(settingsData[:-2], 1):
self.settings.beginGroup(group)
# set the setting name settingsData[-2] to its value settingsData[-1]
self.settings.setValue(*settingsData[-2:])
for g in range(count):
self.settings.endGroup()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
# set both Organization and Application name to make settings persistent
app.setOrganizationName('StackOverflow')
app.setApplicationName('Example')
w = TabWidget()
w.show()
sys.exit(app.exec_())
Alternate solution, based on updated answer
Since the answer has become more specific in its update, I'm adding another suggestion.
As far as we can understand now, you don't need that level of "nested" classes, but more specifically designed code that can be reused according to your purposes. Also, since you're using binary based data, it makes things a bit (pun intended) easier, as long as you know how bit operation works (which I assume you do) and the setting "widgets" don't require specific GUI customization.
In this example I created just one "setting" class and one "group" class, and their instancies are created only according to their names and default values.
import sys
from PyQt5 import QtCore, QtWidgets
defaultValues = '0010101', '1001010', '000111'
# set bit lengths for each setting; be careful in ensuring that each
# setting group has the full default value bit length!
groups = [
['Group 1', [1, 3, 2, 1]],
['Group 2', [1, 2, 2, 1, 1]],
['Group 1', [2, 1, 2, 1]],
]
class BinaryWidget(QtWidgets.QFrame):
changed = QtCore.pyqtSignal()
def __init__(self, name, index, defaults='0'):
QtWidgets.QFrame.__init__(self)
self.setFrameShape(self.StyledPanel|self.Sunken)
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.index = index
self.defaults = defaults
self.buttons = []
# use the "defaults" length to create buttons
for i in range(len(defaults)):
value = int(defaults[i], 2) & 1
# I used QToolButtons as they're usually smaller than QPushButtons
btn = QtWidgets.QToolButton()
btn.setText(str(value))
layout.addWidget(btn, 1, i)
btn.setCheckable(True)
btn.setChecked(value)
btn.toggled.connect(self.changed)
# show the binary value on change, just for conveniency
btn.toggled.connect(lambda v, btn=btn: btn.setText(str(int(v))))
self.buttons.append(btn)
layout.addWidget(QtWidgets.QLabel(name), 0, 0, 1, layout.columnCount())
def value(self):
# return the correct value of all widget's buttons; they're reversed
# because of how bit shifting works
v = 0
for i, btn in enumerate(reversed(self.buttons)):
v += btn.isChecked() << i
# bit shift again, according to the actual "setting" bit index
return v << self.index
def resetValues(self):
oldValue = self.value()
self.blockSignals(True)
for i, value in enumerate(self.defaults):
self.buttons[i].setChecked(int(self.defaults[i], 2) & 1)
self.blockSignals(False)
newValue = self.value()
# emit the changed signal only once, and only if values actually changed
if oldValue != newValue:
self.changed.emit()
class Group(QtWidgets.QWidget):
changed = QtCore.pyqtSignal()
def __init__(self, name, defaults=None, lenghts=None):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
self.name = name
self.bitLength = 0
self.widgets = []
if defaults is not None:
self.addOptions(defaults, lenghts)
def value(self):
v = 0
for widget in self.widgets:
v += widget.value()
return v
def addOption(self, name, index, default='0'):
widget = BinaryWidget(name, index, default)
self.layout().addWidget(widget)
self.widgets.append(widget)
widget.changed.connect(self.changed)
self.bitLength += len(default)
def addOptions(self, defaults, lenghts = None):
if lenghts is None:
lenghts = [1] * len(defaults)
# reverse bit order for per-setting indexing
defaultsIndex = 0
bitIndex = len(defaults)
for i, l in enumerate(lenghts):
self.addOption(
'Setting {}'.format(i + 1),
bitIndex - l,
defaults[defaultsIndex:defaultsIndex + l])
bitIndex -= l
defaultsIndex += l
def resetValues(self):
for widget in self.widgets:
widget.resetValues()
class Tester(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.tabWidget = QtWidgets.QTabWidget()
layout.addWidget(self.tabWidget)
resultLayout = QtWidgets.QHBoxLayout()
layout.addLayout(resultLayout, layout.rowCount(), 0, 1, layout.columnCount())
self.tabs = []
self.labels = []
for (group, lenghts), defaults in zip(groups, defaultValues):
tab = Group(group, defaults, lenghts)
self.tabWidget.addTab(tab, group)
tab.changed.connect(self.updateResults)
self.tabs.append(tab)
tabLabel = QtWidgets.QLabel()
self.labels.append(tabLabel)
resultLayout.addWidget(tabLabel)
self.resetButton = QtWidgets.QPushButton('Reset values')
layout.addWidget(self.resetButton)
self.resetButton.clicked.connect(lambda: [tab.resetValues() for tab in self.tabs])
self.updateResults()
def values(self):
return [tab.value() for tab in self.tabs]
def updateResults(self):
for value, tab, label in zip(self.values(), self.tabs, self.labels):
label.setText('''
{0}: <span style="font-family:monospace;">{1} <b>{1:0{2}b}</b></span>
'''.format(tab.name, value, tab.bitLength))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Tester()
w.show()
sys.exit(app.exec_())
So i have a tree viewas shown below;
#QTreeView widget
#Shows files in set directory
self.treeView = QtWidgets.QTreeView(self.centralWidget)
self.treeView.setSortingEnabled(True)
self.treeView.setObjectName("treeView")
self.horizontalLayout_4.addWidget(self.treeView)
self.file_model=QtWidgets.QFileSystemModel()
self.file_model.setRootPath('C:\My Stuff\Movies')
self.treeView.setModel(self.file_model)
self.treeView.setRootIndex(self.file_model.index('C:\My Stuff\Movies'))
self.treeView.setColumnWidth(0,275)
self.file_model.setNameFilters(self.filterList)
self.file_model.setNameFilterDisables(0)
As you can see i have a filter that hides items that dont pass the filter (e.g. *.mkv) however i have folders in my directory that contain a file that does not fit the filter requirements. The folder remains in my treeview even though it is empty, how do i remove these empty folders (Keep in mind i need to be able to show these folders when i apply a filter that allows for the file in the folder to be shown.
I am running PyQt5, Python 3.5, Windows 7.
We have the same question and this is the way I tried to solve the problem.
You need to subclass QSortFilterProxyModel and override the hasChildren and filterAcceptsRow.
Please take note that instead of calling the QFileSystemModel's setNameFilters you would need to call the subclassed QSortFilterProxyModel's nameFilters method.
This is the code I ended up basing on the implementation above:
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class DirProxy(QSortFilterProxyModel):
nameFilters = ''
def __init__(self):
super().__init__()
self.dirModel = QFileSystemModel()
self.dirModel.setFilter(QDir.NoDotAndDotDot | QDir.AllDirs | QDir.Files) # <- added QDir.Files to view all files
self.setSourceModel(self.dirModel)
def setNameFilters(self, filters):
if not isinstance(filters, (tuple, list)):
filters = [filters]
self.nameFilters = filters
self.invalidateFilter()
def hasChildren(self, parent):
sourceParent = self.mapToSource(parent)
if not self.dirModel.hasChildren(sourceParent):
return False
qdir = QDir(self.dirModel.filePath(sourceParent))
return bool(qdir.entryInfoList(qdir.NoDotAndDotDot|qdir.AllEntries|qdir.AllDirs))
def filterAcceptsRow(self, row, parent):
source = self.dirModel.index(row, 0, parent)
if source.isValid():
if self.dirModel.isDir(source):
qdir = QDir(self.dirModel.filePath(source))
if self.nameFilters:
qdir.setNameFilters(self.nameFilters)
return bool(qdir.entryInfoList(qdir.NoDotAndDotDot|qdir.AllEntries|qdir.AllDirs))
elif self.nameFilters: # <- index refers to a file
qdir = QDir(self.dirModel.filePath(source))
return qdir.match(self.nameFilters, self.dirModel.fileName(source)) # <- returns true if the file matches the nameFilters
return True
class Test(QWidget):
def __init__(self):
super().__init__()
self.dirProxy = DirProxy()
self.dirProxy.dirModel.directoryLoaded.connect(lambda : self.treeView.expandAll())
self.dirProxy.setNameFilters(['*.ai']) # <- filtering all files and folders with "*.ai"
self.dirProxy.dirModel.setRootPath(r"<Dir>")
self.treeView = QTreeView()
self.treeView.setModel(self.dirProxy)
root_index = self.dirProxy.dirModel.index(r"<Dir>")
proxy_index = self.dirProxy.mapFromSource(root_index)
self.treeView.setRootIndex(proxy_index)
self.treeView.show()
app = QApplication(sys.argv)
ex = Test()
sys.exit(app.exec_())
This is the testing I did and the result looks just fine to me:
Trial 1:
Trial 2:
Please read this question for more info.
Short version
How do you implement undo functionality for edits made on QListWidgetItems in PySide/PyQt?
Hint from a Qt tutorial?
The following tutorial written for Qt users (c++) likely has the answer, but I am not a c++ person, so get a bit lost: Using Undo/Redo with Item Views
Longer version
I am using a QListWidget to learn my way around PyQt's Undo Framework (with the help of an article on the topic). I am fine with undo/redo when I implement a command myself (like deleting an item from the list).
I also want to make the QListWidgetItems in the widget editable. This is easy enough: just add the ItemIsEditable flag to each item. The problem is, how can I push such edits onto the undo stack, so I can then undo/redo them?
Below is a simple working example that shows a list, lets you delete items,and undo/redo such deletions. The application displays both the list and the the undo stack. What needs to be done to get edits onto that stack?
Simple working example
from PySide import QtGui, QtCore
class TodoList(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.initUI()
self.show()
def initUI(self):
self.todoList = self.makeTodoList()
self.undoStack = QtGui.QUndoStack(self)
undoView = QtGui.QUndoView(self.undoStack)
buttonLayout = self.buttonSetup()
mainLayout = QtGui.QHBoxLayout(self)
mainLayout.addWidget(undoView)
mainLayout.addWidget(self.todoList)
mainLayout.addLayout(buttonLayout)
self.setLayout(mainLayout)
self.makeConnections()
def buttonSetup(self):
#Make buttons
self.deleteButton = QtGui.QPushButton("Delete")
self.undoButton = QtGui.QPushButton("Undo")
self.redoButton = QtGui.QPushButton("Redo")
self.quitButton = QtGui.QPushButton("Quit")
#Lay them out
buttonLayout = QtGui.QVBoxLayout()
buttonLayout.addWidget(self.deleteButton)
buttonLayout.addStretch()
buttonLayout.addWidget(self.undoButton)
buttonLayout.addWidget(self.redoButton)
buttonLayout.addStretch()
buttonLayout.addWidget(self.quitButton)
return buttonLayout
def makeConnections(self):
self.deleteButton.clicked.connect(self.deleteItem)
self.quitButton.clicked.connect(self.close)
self.undoButton.clicked.connect(self.undoStack.undo)
self.redoButton.clicked.connect(self.undoStack.redo)
def deleteItem(self):
rowSelected=self.todoList.currentRow()
rowItem = self.todoList.item(rowSelected)
if rowItem is None:
return
command = CommandDelete(self.todoList, rowItem, rowSelected,
"Delete item '{0}'".format(rowItem.text()))
self.undoStack.push(command)
def makeTodoList(self):
todoList = QtGui.QListWidget()
allTasks = ('Fix door', 'Make dinner', 'Read',
'Program in PySide', 'Be nice to everyone')
for task in allTasks:
todoItem=QtGui.QListWidgetItem(task)
todoList.addItem(todoItem)
todoItem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
return todoList
class CommandDelete(QtGui.QUndoCommand):
def __init__(self, listWidget, item, row, description):
super(CommandDelete, self).__init__(description)
self.listWidget = listWidget
self.string = item.text()
self.row = row
def redo(self):
self.listWidget.takeItem(self.row)
def undo(self):
addItem = QtGui.QListWidgetItem(self.string)
addItem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.listWidget.insertItem(self.row, addItem)
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
myList=TodoList()
sys.exit(app.exec_())
Note I posted an earlier version of this question at QtCentre.
That tutorial you mentioned is really not very helpful. There are indeed many approaches to undo-redo implementation for views, we just need to choose the simplest one. If you deal with small lists, the simpliest way is to save all data on each change and restore full list from scratch on each undo or redo operation.
If you still want atomic changes list, you can track user-made edits with QListWidget::itemChanged signal. There are two problems with that:
Any other item change in the list will also trigger this signal, so you need to wrap any code that changes items into QObject::blockSignals calls to block unwanted signals.
There is no way to get previous text, you can only get new text. The solution is either save all list data to variable, use and update it on change or save the edited item's text before it's edited. QListWidget is pretty reticent about its internal editor state, so I decided to use QListWidget::currentItemChanged assuming that user won't find a way to edit an item without making is current first.
So this is the changes that will make it work (besides adding ItemIsEditable flag in two places):
def __init__(self):
#...
self.todoList.itemChanged.connect(self.itemChanged)
self.todoList.currentItemChanged.connect(self.currentItemChanged)
self.textBeforeEdit = ""
def itemChanged(self, item):
command = CommandEdit(self.todoList, item, self.todoList.row(item),
self.textBeforeEdit,
"Rename item '{0}' to '{1}'".format(self.textBeforeEdit, item.text()))
self.undoStack.push(command)
def currentItemChanged(self, item):
self.textBeforeEdit = item.text()
And the new change class:
class CommandEdit(QtGui.QUndoCommand):
def __init__(self, listWidget, item, row, textBeforeEdit, description):
super(CommandEdit, self).__init__(description)
self.listWidget = listWidget
self.textBeforeEdit = textBeforeEdit
self.textAfterEdit = item.text()
self.row = row
def redo(self):
self.listWidget.blockSignals(True)
self.listWidget.item(self.row).setText(self.textAfterEdit)
self.listWidget.blockSignals(False)
def undo(self):
self.listWidget.blockSignals(True)
self.listWidget.item(self.row).setText(self.textBeforeEdit)
self.listWidget.blockSignals(False)
I would do it like this:
Create a custom QItemDelegate and use these two signals:
editorEvent
closeEditor
On editorEvent: Save current state
On closeEditor: Get new state and create a QUndoCommand that set the new state for Redo and the old state for Undo.
Each time you verify and accept the new text of the item, save it as list item data. Quasi-semi-pseudo-code:
OnItemEdited(Item* item)
{
int dataRole{ 32 }; //or greater (see ItemDataRole documentation)
if (Validate(item->text()) {
item->setData(dataRole, item->text());
} else { //Restore previous value
item->setText(item->data(dataRole).toString());
}
}
I'm sorry if it looks too much like C++.
Is there trivial or elegant way to differentiate between many same-type signal sources in PySide/PyQt?
I am learning PySide. I have written simple application, which multiplies two numbers from two different QLineEdit() objects. Result is displayed in third QLineEdit.
Multiplier and multiplicand QLineEdit.textChanged() signals are connected to one method (TxtChanged). In this method i have to differentiate between signal sources. After some trials I figured out some workaround based upon placeholder text (4 lines below "is there another way?" comment in my code)
code:
import sys
from PySide import QtGui, QtCore
class myGUI(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
self.multiplier = 0
self.multiplicand = 0
self.myGUIInit()
def myGUIInit(self):
# input forms
a1_label = QtGui.QLabel("a1")
a1_edit = QtGui.QLineEdit()
a1_edit.setPlaceholderText("a1")
a2_label = QtGui.QLabel("a2")
a2_edit = QtGui.QLineEdit()
a2_edit.setPlaceholderText("a2")
# output form
a1a2_label = QtGui.QLabel("a1*a2")
self.a1a2_edit = QtGui.QLineEdit()
self.a1a2_edit.setReadOnly(True)
# forms events
a1_edit.textChanged.connect(self.TxtChanged)
a2_edit.textChanged.connect(self.TxtChanged)
# grid
grid = QtGui.QGridLayout()
grid.setSpacing(10)
grid.addWidget(a1_label,1,0)
grid.addWidget(a1_edit,1,1)
grid.addWidget(a2_label,2,0)
grid.addWidget(a2_edit,2,1)
grid.addWidget(a1a2_label,3,0)
grid.addWidget(self.a1a2_edit,3,1)
self.setLayout(grid)
self.setGeometry(100,100,200,200)
self.setWindowTitle("a*b")
self.show()
def TxtChanged(self,text):
sender = self.sender()
sender_text = sender.text()
if sender_text == '': sender_text = '0'
# is there another way?
if sender.placeholderText() == 'a1':
self.multiplicand = sender_text
else:
self.multiplier = sender_text
product = int(self.multiplier) * int(self.multiplicand)
print(self.multiplier,self.multiplicand,product)
self.a1a2_edit.setText(str(product))
def main():
app = QtGui.QApplication(sys.argv)
mainWindow = myGUI()
sys.exit(app.exec_())
main()
best regards,
ostrzysz
You can use the functools.partial function - and therefore connect your signals to straight to your method/function but rather to a python object which will automatically call your function with some extra data you pass it:
from functools import partial
...
....
a1_edit.textChanged.connect(partial(self.TxtChanged, a1_edit))
a2_edit.textChanged.connect(partial(self.TxtChanged, a2_edit))
...
def TxtChanged(self,sender, text):
# and here you have the "sender" parameter as it was filled in the call to "partial"
...
partials is part of the stdlib, and is very readable, but one can always use lambda instead of partial for the same effect -
a1_edit.textChanged.connect(lambda text: self.TxtChanged(a1_edit, text))
In this way the object yielded by the lambda expression will be a temporary function that will use the values for "self" and "a1_edit" from the current local variables (at the time the button is clicked), and the variable named "text" will be supplied by Pyside's callback.
One thing that bugs me most in your code is that you are using placeholderText to differentiate. QObjects has another property called objectName that is more suitable for your task. And, you don't need to use sender.text() to get the text of QLineEdit. textChanged already sends it, so you will have it in your text parameter.
Also, using a dictionary instead of two separate variables (multiplier and multiplicand) will simplify your code further.
Here is the changed code:
class myGUI(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
self.data = {"multiplier": 0,
"multiplicand": 0}
self.myGUIInit()
def myGUIInit(self):
a1_label = QtGui.QLabel("a1")
a1_edit = QtGui.QLineEdit()
a1_edit.setObjectName("multiplicand")
a2_label = QtGui.QLabel("a2")
a2_edit = QtGui.QLineEdit()
a2_edit.setObjectName("multiplier")
# skipped the rest because same
def TxtChanged(self, text):
sender = self.sender()
# casting to int while assigning seems logical.
self.data[sender.objectName()] = int(text)
product = self.data["multiplier"] * self.data["multiplicand"]
print(self.data["multiplier"], self.data["multiplicand"], product)
self.a1a2_edit.setText(str(product))
Although #jsbueno and #Avaris answered your direct question about signal sources, I wouldn't relay on this sources in your concrete case. You can make instance members a1_edit and a2_edit:
...
self.a1_edit = QtGui.QLineEdit()
...
self.a2_edit = QtGui.QLineEdit()
...
It will simplify your TxtChanged function:
def TxtChanged(self,text):
try:
multiplier = int(self.a1_edit.text())
multiplicand = int(self.a2_edit.text())
except ValueError:
self.a1a2_edit.setText('Enter two numbers')
return
product = multiplier * multiplicand
print(multiplier, multiplicand, product)
self.a1a2_edit.setText(str(product))
Also, instead of handling ValueError exception, you can use QIntValidator for input controls:
self.int_validator = QtGui.QIntValidator()
self.a1_edit.setValidator(self.int_validator)
self.a2_edit.setValidator(self.int_validator)