I want two columns to be displayed in my Combobox. How could I do it without going too far into code. This code works but I have only achieved it with one column.
sqlcom2="SELECT cod,name FROM product"
data2 = cur.execute(sqlcom2)
result2 = cur.fetchall()
result22 = [i[0] for i in result2]
self.comboBox.addItems(result22)
QComboBox doesn't normally allow to show more than one column of items as its view() is normally a QListView. In order to show more contents, you can use a QTableView instead, but this requires adding more items for each row, so the basic addItem() or addItems() cannot be used, so items must be added to the combobox model instead.
Even if QComboBox currently uses a QStandardItemModel, this might change in the future, so it's better to create your own QStandardItemModel and set that model for the combo.
Then, QComboBox is able to use models with multiple columns (the default is column 0) and the currently used column can be changed using setModelColumn(), but you need to ensure that whenever the user selects an item in a different column the combobox column is update accordingly. In order to do that, a subclass of QTableView is usually the best approach, as it allows to override the methods required to notify the combo about the column change.
There's also another problem: QComboBox uses its own width to automatically resize the popup width. While for single column popup this is not a big problem, if multiple columns are going to be shown, it's possible that only the first column will be shown.
To solve this issue you can set a minimum size for the table view within the showEvent of the table; note that this is generally not a suggested approach, but it is acceptable for this specific case and for the sake of simplicity. Also consider that I'm using the defaultSectionSize() property, and you might prefer to ensure that the columns resize themselves according to the contents, but since that is a more and slightly unrelated topic that might add more complexity to the implementation, I'll not address it in this answer.
class ComboTableView(QtWidgets.QTableView):
def __init__(self, comboBox):
super().__init__()
self.comboBox = comboBox
self.horizontalHeader().hide()
self.verticalHeader().hide()
self.activated.connect(self.updateComboColumn)
def updateComboColumn(self, index):
if (index.isValid() and
index.flags() & QtCore.Qt.ItemIsEnabled and
index.flags() & QtCore.Qt.ItemIsSelectable):
self.comboBox.setModelColumn(index.column())
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.updateComboColumn(self.indexAt(event.pos()))
super().mousePressEvent(event)
def showEvent(self, event):
w = self.horizontalHeader().defaultSectionSize() * self.model().columnCount()
if self.model().rowCount() > self.comboBox.maxVisibleItems():
w += self.verticalScrollBar().sizeHint().width()
self.setMinimumWidth(w)
class SomeWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.comboTableView = ComboTableView(self.comboBox)
self.comboBox.setView(self.comboTableView)
comboModel = QtGui.QStandardItemModel()
self.comboBox.setModel(comboModel)
# ...
sqlcom2="SELECT cod,name FROM product"
data2 = cur.execute(sqlcom2)
result2 = cur.fetchall()
for rowValues in result2:
items = []
for value in rowValues:
items.append(QtGui.QStandardItem(value))
comboModel.appendRow(items)
Finally, you can also implement "horizontal" keyboard navigation within the combo so that the user can switch between columns using the arrow keys, and that can be done by installing an event filter that manages arrow keys.
class SomeWindow(QtWidgets.QMainWindow):
def __init__(self):
# ...
self.comboBox.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.comboBox and event.type() == QtCore.QEvent.KeyPress:
if event.key() == QtCore.Qt.Key_Right:
column = min(
self.comboBox.modelColumn() + 1,
self.comboBox.model().columnCount() - 1
)
self.comboBox.setModelColumn(column)
elif event.key() == QtCore.Qt.Key_Left:
column = max(0, self.comboBox.modelColumn() - 1)
self.comboBox.setModelColumn(column)
return super().eventFilter(source, event)
Related
There are rows in QTreeView, each row has two columns.
The first column carries a name, second column carries a preview.
Questions:
How to get click on row signal? If row is selected by single mouse click, how to get signal? I use QTreeView.doubleClicked.connect() now, but is there way how to do?
I follow up on the first question. If I choose by clicking on the item in the second column how to get item name from the first column? (if is not possible to perform the action with a single mouse click, how to do it with QTreeView.doubleClicked.connect()? )
How to get information about category, subcategories, row, column of item?
Thanks for hints and help.
Code preview:
QTreeView
self.tree = QtWidgets.QTreeView()
self.model = QtGui.QStandardItemModel(self.tree)
self.tree.setDragEnabled(True)
self.tree.doubleClicked.connect(self.getValue)
Filling QTreeView
self.model.setHorizontalHeaderLabels(('Category / Subcategories / Name', 'Preview'))
self.tree.setModel(self.model)
self.tree.setColumnWidth(0, 200)
#root = self.tree.rootIndex()
for row, (keys, vals) in enumerate(self.dataStatObj.items()):
if row == 0:
kategorie = keys[0]
podkategorie = keys[1]
categoryTV = QtGui.QStandardItem(keys[0])
subcategoriesTV = QtGui.QStandardItem(keys[1])
self.model.appendRow(categoryTV)
for iimg in listImagesNameSuffix:
if iimg[1] == keys[2]:
print(iimg[1])
print(keys[2])
imgName = iimg[0]
imgTV = StandardItemObr()
imgTV.setIcon(QtGui.QIcon(pathToImages + imgName))
imgTV.setEditable(False)
nameImgTV = QStandardItem(keys[2])
nameImgTV.setEditable(False)
categoryTV.appendRow(subcategoriesTV)
categoryTV.setEditable(False)
subcategoriesTV.appendRow([nameImgTV, imgTV])
subcategoriesTV.setEditable(False)
...
...
self.tree.expandAll()
self.tree.setIconSize(QtCore.QSize(100, 100))
getValue (self.tree.doubleClicked.connect(self.getValue))
def getValue(self, val):
print(val.data(), 'If I click on the first column, the text will appear.'
' When I click on the second column I need to return the same as for the first column ')
if val.data() is None:
print('None')
print(val.parent(), "PARENT")
Preview
QTreeView, like other views such as QListView or QTableView, inherits from QAbstractItemView, and if you look at the Signals section you'll see that there are other signals other than doubleClicked. What you need, obviously, is the clicked signal.
That signal, like most of the others, returns the model index
(a QModelIndex instance) that has been clicked, which is an object that is used to locate the data within the model, including its "coordinates": row, column and parent (the last is fundamental for tree models).
The QModelIndex class has various convenience functions that allow easy access to the relation with the model:
data(), which you're already using, that returns the value(s) associated to that specific index in the model; it's actually a "shortcut" to model.data(index, role);
parent() is pretty explanatory; same as model.parent(index);
sibling(), returns an index at the specified row or column that shares the same parent of the given one; it's the same as model.sibling(row, column, index);
Considering the above, if you want to get data about the first column after a click, you need to connect the function to the clicked signal; then, in order to get information about any possible parent, you can use a basic recursive function:
def getValue(self, index):
if index.column():
index = index.sibling(index.row(), 0)
# since Qt 5.11 you can use `index.siblingAtColumn(0)`
def getValueRecursive(index):
info = ['"{}" (row {}, col {})'.format(
index.data(), index.row(), index.column())]
if index.parent().isValid():
info.extend(getValueRecursive(index.parent()))
return info
print('Index clicked!')
for indexInfo in reversed(getValueRecursive(index)):
print(indexInfo)
Note that the clicked signal will obviously only work for mouse clicks: if the current index is changed using the keyboard, it will not be emitted, so you should better use the selectionModel of the view and use the selectionChanged or currentChanged signals instead.
I strongly recommend you to carefully and patiently study the classes you're using, including the inherited ones; for instance, if you're using QTreeView, you should consider studying all the documentation about: QAbstractItemView, QAbstractScrollArea, QFrame, QWidget and, finally, QObject; since you're using models, you also need to know more about QModelIndex, QAbstractItemModel and read the documentation about the model/view framework.
I have QTableWidget which is non editable.(i had setup noEditTriggers while creating Ui file). I want to make particular cell editable from each row. how i can get this done?
I looked into several answers on SO and other platforms but didn't get anything working for me.
currently I am using this piece of code. it doesnt give an error but i still could not edit that cell value.
self.item = QTableWidgetItem('Hi')
flags = self.item.flags()
flags ^= QtCore.Qt.ItemIsEditable
self.item.setFlags(flags)
self.table.setItem(row, column, self.item)
EDIT::
Using the same fundament for the #musicamante answer is to create a delegate that only returns one editor in the specific column, the advantage is that you don't need to subclassify QTableWidget and the logic can be used in other types of views:
class Delegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
if index.column() == 2:
return super(Delegate, self).createEditor(parent, option, index)
delegate = Delegate(self.table)
self.table.setItemDelegate(delegate)
Update:
If you want the cells with NN to be editable then you must return the editor when it meets that condition: index.data() == "NN"
import random
import sys
from PyQt5 import QtWidgets
class Delegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
if index.data() == "NN":
return super(Delegate, self).createEditor(parent, option, index)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
texts = ["Hello", "Stack", "Overflow", "NN"]
table = QtWidgets.QTableWidget(10, 5)
delegate = Delegate(table)
table.setItemDelegate(delegate)
for i in range(table.rowCount()):
for j in range(table.columnCount()):
text = random.choice(texts)
it = QtWidgets.QTableWidgetItem(text)
table.setItem(i, j, it)
table.resize(640, 480)
table.show()
sys.exit(app.exec_())
You could set the flags for each item, while leaving the default edit triggers, but this is not very good approach, since you could have a very large table, some items could be changed/added/removed and you might forget to set/reset the flags.
A better approach could be to override the edit() method, and execute the default implementation (which creates the item editor and starts the editing) by manually setting the edit trigger.
This requires to leave the default edit triggers (or at least one trigger method) set.
class TableWidget(QtWidgets.QTableWidget):
def edit(self, index, trigger, event):
# editing is allowed only for the third column
if index.column() != 2:
trigger = self.NoEditTriggers
return super().edit(index, trigger, event)
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_())
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++.
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.