I'm new to PyQt5 and pretty new to Python in general. I needed a keyboard layout, and in stead of manually creating a QPushButton for every letter and setting the text, coordinates and size for each i tried to automate it. My idea was to iterate through a dictionary to create a new name for each QPushButton. I then had to use something else than self.dict[x] as text for the QPushButton, because self.dict[x] was a QPushButton itself. I created a list with all the characters and used list[x] in stead. I would use the coords list to tweak the coordinates for each QPushButton through the iterations. My attempt looked like this:
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow
import sys
class Window(QMainWindow):
coords = [0, 200]
size = 50
count = 0
list = [chr(x) for x in range(65, 91)]
dict = {}
for x in range(len(list)):
dict[x] = list[x]
def __init__(self):
super(Window, self).__init__()
self.setGeometry(100, 100, 800, 500)
self.setWindowTitle('')
self.makeButtons()
def makeButtons(self):
for x in range(len(self.dict)):
self.dict[x] = QtWidgets.QPushButton(self)
self.dict[x].resize(self.size, self.size)
self.dict[x].move(self.coords[0], self.coords[1])
self.dict[x].setText(self.list[x])
self.coords[0] += self.size
self.count += 1
if self.count == 9:
self.coords = [0, 250]
if self.count == 18:
self.coords = [25, 300]
def window():
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
window()
which does create the keyboard layout i want. However, I got stuck trying to create a method for each QPushButton, so I can't add any functionalty to the buttons. Is it possible to implement the automation of creating other metods in the makeButton method like I did with the QPushButtons themselves, or do i need another strategy to automate it?
tl;dr
Use lambda and emit signals manually:
class Window(QMainWindow):
keyClicked = QtCore.pyqtSignal(object)
# ...
def makeButtons(self):
# ...
for x in range(len(self.dict)):
self.dict[x] = button = QtWidgets.QPushButton(self)
# ...
button.clicked.connect(lambda _, key=self.list[x]: self.keyClicked.emit(key))
# ...
Explanation
Whenever you have lots of widgets that behave in the same way, you should provide a common interface for their behavior. lambda functions can help you with that, but you have to be aware about the scope the variables belong to (remember, as soon as a function exits/returns, any "anonymous" variable that has no persistent relation to other objects will be garbage collected by python, which practically means "deleted").
As you can see in the example above, I'm using two arguments.
The first is the one provided whenever the signal is emitted (any QAbstractButton descendant emits a clicked signal with a bool arguments that reports the new "checked" status, so if the button is checkable the argument will be the button checked state, otherwise it will always be False). We can just ignore that, but it has to be added as an argument of the lambda function.
The latter keyword argument is provided to keep consistence with the scope of the current cycle. In this way we can ensure that the key argument of the lambda function will always be that of the current cycle (otherwise it will always be the last for value assigned to that for cycle).
Addendum
This part of the answer is totally unrequested and unnecessary for the actual purpose of this question; nonetheless, I'm assuming you're new not only to Python and PyQt in general, but to the UX side that programming almost always has to relate to. As programmers, we have to be aware about that, since the majority of users that will finally use our products are not programmers.
Creating a keyboard GUI is not an easy task.
People are accustomed to the keyboards they use everyday (guess what, smartphones). Choosig an alphabet-order based layout for a keyboard that uses a standard "three rows" layout is not only an unpopular choice, but also a wrong one. We're (luckily) far away from that gap-time that was before the 2010's, where almost nobody knew how to type at all, but we also have to consider the results of this aspect.
To add even more mess to that, consider that even if QWERTY layout is almost standard, there are other layouts that differ partially (or completely) from it. For example, French people use AZERTY, while some Central Europe countries use QWERTZ, not to mention non-latin based writings.
And all of this only accounts for the standard alphabet letters.
Finally, the code
from string import ascii_uppercase
from PyQt5 import QtCore, QtWidgets
class Keyboard(QtWidgets.QWidget):
ASCII, QWERTY, DVORAK = 0, 1, 2
KeyLayoutLetters = {
ASCII: [ascii_uppercase[r*9:r*9+9] for r in range(3)],
QWERTY: ['QWERTYUIOP', 'ASDFGHJKL', 'ZXCVBNM'],
DVORAK: ['PYFGCRL', 'AOEUIDHTNS ', 'QJKXBMWVZ'],
}
# some default special stretch set for specific keyboard layouts
KeyStretch = {
QWERTY: [(1, 1), (1, 1), (1, 2)],
DVORAK: [(2, 1), (1, 2), (3, 1)],
}
keySize = 50
spacing = 2
letterClicked = QtCore.pyqtSignal(object)
def __init__(self):
super(Keyboard, self).__init__()
self.setKeyboardLayout()
def setKeyboardLayout(self, keyLayout=None):
keyLayout = keyLayout if keyLayout is not None else self.ASCII
if self.layout() is not None:
QtWidgets.QWidget().setLayout(self.layout())
layout = QtWidgets.QVBoxLayout(self)
layout.setSpacing(self.spacing)
stretches = self.KeyStretch.get(keyLayout, [(1, 1)] * 3)
for row, rowChars in enumerate(self.KeyLayoutLetters.get(keyLayout, self.KeyLayoutLetters[self.ASCII])):
rowLayout = QtWidgets.QHBoxLayout()
rowLayout.setSpacing(self.spacing)
layout.addLayout(rowLayout)
stretchLeft, stretchRight = stretches[row]
rowLayout.addStretch(stretchLeft)
for letter in rowChars:
if not letter.strip():
spacer = QtWidgets.QWidget()
rowLayout.addWidget(spacer)
spacer.setFixedSize(self.keySize * .5, self.keySize)
continue
letterButton = QtWidgets.QPushButton(letter)
rowLayout.addWidget(letterButton, stretch=0)
letterButton.setFixedSize(self.keySize, self.keySize)
letterButton.clicked.connect(lambda _, key=letter: self.letterClicked.emit(key))
rowLayout.addStretch(stretchRight)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
self.setCentralWidget(central)
layout = QtWidgets.QVBoxLayout(central)
# just a QLineEdit widget to test the text intertion
self.lineEdit = QtWidgets.QLineEdit()
layout.addWidget(self.lineEdit)
self.keyboard = Keyboard()
layout.addWidget(self.keyboard)
self.keyboard.setKeyboardLayout(Keyboard.DVORAK)
self.keyboard.letterClicked.connect(self.lineEdit.insert)
self.keySelector = QtWidgets.QComboBox()
layout.addWidget(self.keySelector)
self.keySelector.addItems(['ASCII', 'QWERTY', 'DVORAK'])
self.keySelector.currentIndexChanged.connect(self.keyboard.setKeyboardLayout)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
keyboard = MainWindow()
keyboard.show()
sys.exit(app.exec_())
Keep in mind that this is a really simple and raw implementation of what you're probably looking for. There is a moltitude of aspects you'd need to take care of (mostly related to special characters that are typical of the user's language).
Related
I write a simple app, While drag or scale the MainView, The PartView rubberband will show scene area in PartView.But sometime the rubber-band become a line, and sometime the rubberband disappear.So How to aviod this phenomenon appear?And sometime I want the rubberband only show it's border-line, not contain it's light-blue rectangle,So how can I write code ?
My Code
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import random
import math
r = lambda : random.randint(0, 255)
r255 = lambda : (r(), r(), r())
class Scene(QGraphicsScene):
def __init__(self):
super().__init__()
for i in range(1000):
item = QGraphicsEllipseItem()
item.setRect(0, 0, r(), r())
item.setBrush(QColor(*r255()))
item.setPos(r()*100, r()*100)
self.addItem(item)
class MainView(QGraphicsView):
sigExposeRect = pyqtSignal(QRectF)
def drawBackground(self, painter: QPainter, rect: QRectF) -> None:
super().drawBackground(painter, rect)
self.sigExposeRect.emit(rect)
def wheelEvent(self, event: QWheelEvent) -> None:
factor = math.pow(2.7, event.angleDelta().y()/360)
self.scale(factor, factor)
class PartView(QGraphicsView):
def __init__(self):
super().__init__()
self.r = QRubberBand(QRubberBand.Rectangle, self)
self.r.setWindowOpacity(1)
self.r.show()
class View(QSplitter):
def __init__(self):
super().__init__()
self.m = MainView()
self.m.setMouseTracking(True)
self.m.setDragMode(QGraphicsView.ScrollHandDrag)
self.m.sigExposeRect.connect(self.onExposeRect)
self.p = PartView()
self.m.setScene(Scene())
self.p.setScene(self.m.scene())
self.p.fitInView(self.m.scene().itemsBoundingRect())
self.addWidget(self.m)
self.addWidget(self.p)
def onExposeRect(self, rect: QRectF):
prect = self.p.mapFromScene(rect).boundingRect()
self.p.r.setGeometry(prect)
app = QApplication([])
v = View()
v.show()
app.exec()
My Result
I think the problem is that the qrect passed to the drawBackground method is only includes the portion of the background that wasn't previously in the viewport. Not positive about that though.
Either way I was able to achieve your goal of avoiding only a section of the rubber band being drawn, by sending the area for the entire viewport to the onExposeRect slot.
class MainView(QGraphicsView):
sigExposeRect = pyqtSignal(QRectF)
def drawBackground(self, painter: QPainter, rect: QRectF) -> None:
# Adding this next line was the only change I made
orect = self.mapToScene(self.viewport().geometry()).boundingRect()
super().drawBackground(painter, rect)
self.sigExposeRect.emit(orect) # and passing it to the slot.
def wheelEvent(self, event: QWheelEvent) -> None:
factor = math.pow(2.7, event.angleDelta().y()/360)
self.scale(factor, factor)
A fundamental aspect about Graphics View is its high performance in drawing even thousands of elements.
To achieve this, one of the most important optimization is updating only the portions of the scene that really need redrawing, similar to what item views do, as they normally only redraw the items that actually require updates, instead of always painting the whole visible area, which can be a huge bottleneck.
This is the reason for which overriding drawBackground is ineffective: sometimes, only a small portion of the scene is updated (and, in certain situations, even no update is done at all), and the rect argument of drawBackground only includes that portion, not the whole visible area. The result is that in these situations, the signal will emit a rectangle that will not be consistent with the visible area.
Since the visible area is relative to the viewport of the scroll area, the only safe way to receive updates about that area is to connect to the horizontal and vertical scroll bars (which always work even if they are hidden).
A further precaution is to ensure that the visible rectangle is also updated whenever the scene rect is changed (since that change might not be reflected by the scroll bars), by connecting to the sceneRectChanged signal and also overriding the setSceneRect() of the source view. Considering that the changes in vertical and scroll bars might coincide, it's usually a good idea to delay the signal with a 0-delay QTimer, so that it's only sent once when more changes to the visible area happen at the same time.
Note that since you're not actually using the features of QRubberBand, there's little use in its usage, especially if you also need custom painting. Also, since the rubber band is a child of the view, it will always keep its position even if the preview view is scrolled.
In the following example I'll show two ways of drawing the "fake" rubber band (but choose only one of them, either comment one or the other to test them) that will always be consistent with both the source and target views.
class MainView(QGraphicsView):
sigExposeRect = pyqtSignal(QRectF)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signalDelay = QTimer(self, singleShot=True, interval=0,
timeout=self.emitExposeRect)
# signals might have arguments that collide with the start(interval)
# override of QTimer, let's use a basic lambda that ignores them
self.delayEmit = lambda *args: self.signalDelay.start()
self.verticalScrollBar().valueChanged.connect(self.delayEmit)
self.horizontalScrollBar().valueChanged.connect(self.delayEmit)
def emitExposeRect(self):
topLeft = self.mapToScene(self.viewport().geometry().topLeft())
bottomRight = self.mapToScene(self.viewport().geometry().bottomRight())
self.sigExposeRect.emit(QRectF(topLeft, bottomRight))
def setScene(self, scene):
if self.scene() == scene:
return
if self.scene():
try:
self.scene().sceneRectChanged.disconnect(self.delayEmit)
except TypeError:
pass
super().setScene(scene)
if scene:
scene.sceneRectChanged.connect(self.delayEmit)
def setSceneRect(self, rect):
super().setSceneRect(rect)
self.delayEmit()
def wheelEvent(self, event: QWheelEvent) -> None:
factor = math.pow(2.7, event.angleDelta().y()/360)
self.scale(factor, factor)
class PartView(QGraphicsView):
exposeRect = None
def updateExposeRect(self, rect):
if self.exposeRect != rect:
self.exposeRect = rect
self.viewport().update()
def paintEvent(self, event):
super().paintEvent(event)
if not self.exposeRect:
return
rect = self.mapFromScene(self.exposeRect).boundingRect()
# use either *one* of the following:
# 1. QStyle implementation, imitates QRubberBand
qp = QStylePainter(self.viewport())
opt = QStyleOptionRubberBand()
opt.initFrom(self)
opt.rect = rect
qp.drawControl(QStyle.CE_RubberBand, opt)
# 2. basic QPainter
qp = QPainter(self.viewport())
color = self.palette().highlight().color()
qp.setPen(self.palette().highlight().color())
# for background
bgd = QColor(color)
bgd.setAlpha(40)
qp.setBrush(bgd)
qp.drawRect(rect)
class View(QSplitter):
def __init__(self):
super().__init__()
self.m = MainView()
self.m.setMouseTracking(True)
self.m.setDragMode(QGraphicsView.ScrollHandDrag)
self.p = PartView()
self.m.setScene(Scene())
self.p.setScene(self.m.scene())
self.p.fitInView(self.m.scene().itemsBoundingRect())
self.addWidget(self.m)
self.addWidget(self.p)
self.m.sigExposeRect.connect(self.p.updateExposeRect)
PS: please use single letter variables when they actually make sense (common variables, coordinates, loop placeholders, etc.), not for complex objects, and especially for attributes: there's no benefit in using self.m or self.p, and the only result you get is to make code less readable to you and others.
I'm trying to implement a button in PyQt5 which acts identically to pressing the Tab-Key. For this, I get the focused item, and call nextInFocusChain() to get the next item in tab order and set it to focus. If it is not a QLineEdit object, I repeat.
self.focusWidget().nextInFocusChain().setFocus()
while type(self.focusWidget()) != QLineEdit:
print(str(self.focusWidget()) + "" + str(self.focusWidget().nextInFocusChain()))
self.focusWidget().nextInFocusChain().setFocus()
Sadly this snipped does not wrap around. After the last QLineEdit, it gets stuck inside the loop while continuously printing
...
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>
Here a reproducing example...
from collections import OrderedDict
from PyQt5.QtWidgets import QDialog, QFormLayout, QLineEdit, QCheckBox, QDialogButtonBox, QVBoxLayout, QPushButton
class AddressEditor(QDialog):
def __init__(self,fields:OrderedDict,parent=None):
super(AddressEditor, self).__init__(parent=parent)
layout = QVBoxLayout(self)
form = QFormLayout(self)
self.inputs = OrderedDict()
last = None
for k,v in fields.items():
self.inputs[k] = QLineEdit(v)
if last is not None:
self.setTabOrder(self.inputs[last],self.inputs[k])
last = k
form.addRow(k, self.inputs[k])
layout.addLayout(form)
button = QPushButton("TAB")
layout.addWidget(button)
button.clicked.connect(self.tabpressed)
def tabpressed(self):
self.focusWidget().nextInFocusChain().setFocus()
while type(self.focusWidget()) != QLineEdit:
print(str(self.focusWidget()) + "" + str(self.focusWidget().nextInFocusChain()))
self.focusWidget().nextInFocusChain().setFocus()
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
dialog = AddressEditor({"Text":"Default","Empty":"","True":"True","False":"False"})
if dialog.exec():
pass
exit(0)
There are two important aspects to consider when trying to achieve "manual" focus navigation:
the focus chain considers all widgets, not only those that can "visibly" accept focus; this includes widgets that have a NoFocus policy, the parent widget (including the top level window), hidden widgets, and any other widget that can be "injected" by the style, including "helpers" like QFocusFrame;
widgets can have a focus proxy which can "push back" the focus to the previous widget, and this can cause recursion issues like in your case;
Besides that, there are other issues with your implementation:
the button accepts focus, so whenever it's pressed it resets the focus chain;
to compare the class you should use isinstance and not type;
form should not have any argument since it's going to be added as a nested layout, and in any case it shouldn't be self since a layout has already been set;
the tab order must be set after both widgets are added to parents that share the same a common ancestor widget/window;
the tab order is generally automatically set based on when/where widgets are added to the parent: for a form layout is generally unnecessary as long as all fields are inserted in order;
class AddressEditor(QDialog):
def __init__(self, fields:OrderedDict, parent=None):
super(AddressEditor, self).__init__(parent=parent)
layout = QVBoxLayout(self)
form = QFormLayout() # <- no argument
layout.addLayout(form)
self.inputs = OrderedDict()
last = None
for k, v in fields.items():
new = self.inputs[k] = QLineEdit(v)
form.addRow(k, new) # <- add the widget *before* setTabOrder
if last is not None:
self.setTabOrder(last, new)
last = new
button = QPushButton("TAB")
layout.addWidget(button)
button.clicked.connect(self.tabpressed)
button.setFocusPolicy(Qt.NoFocus) # <- disable focus for the button
def tabpressed(self):
nextWidget = self.focusWidget().nextInFocusChain()
while not isinstance(nextWidget, QLineEdit) or not nextWidget.isVisible():
nextWidget = nextWidget.nextInFocusChain()
nextWidget.setFocus()
If you want to keep the focus policy for the button so that it can be reached through Tab, the only possibility is to keep track of the focus change of the application, since as soon as the button is pressed with the mouse button it will already have received focus:
class AddressEditor(QDialog):
def __init__(self, fields:OrderedDict, parent=None):
# ...
button = QPushButton("TAB")
layout.addWidget(button)
button.clicked.connect(self.tabpressed)
QApplication.instance().focusChanged.connect(self.checkField)
self.lastField = tuple(self.inputs.values())[0]
def checkField(self, old, new):
if isinstance(new, QLineEdit) and self.isAncestorOf(new):
self.lastField = new
def tabpressed(self):
nextWidget = self.lastField.nextInFocusChain()
while not isinstance(nextWidget, QLineEdit) or not nextWidget.isVisible():
nextWidget = nextWidget.nextInFocusChain()
nextWidget.setFocus()
I created a grid using wxPython and I need to monitor the data inserted by the user into one of the cells in my grid. I need to have an event due to every key press in the keyboard (like EVT_KEY_DOWN) and I can't find a way to do that.
Right now I need to use a grid for this purpose so the solution must be something that can be integrated into wx.grid.
I tried to use GridCellEditor but it only gives the first key.
Is there a way to integrate TextCtrl into a grid's cell or something like that?
As far as I can tell, unless told differently, a grid is a collection of TextCtrl's, so the key is to bind wx.EVT_KEY_DOWN to them.
Here is one way to do it:
Note: I have added a few different element types for demonstration purposes.
Hopefully this is all that you require.
import wx
import wx.grid as gridlib
class MyForm(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, wx.ID_ANY, "A key detecting grid", size=(1000,300))
panel = wx.Panel(self, wx.ID_ANY)
self.grid = gridlib.Grid(panel)
self.grid.CreateGrid(10, 8)
self.grid.Bind(wx.EVT_KEY_DOWN, self.OnKeyPress) #Required for initial key press
self.grid.Bind(gridlib.EVT_GRID_EDITOR_CREATED, self.onEditorCreated) # For subsequent key presses
# -- Additional bits only for demonstration of isolating Text fields
# Boolean field dislays as a CheckBox
crbool = wx.grid.GridCellBoolRenderer()
cebool = wx.grid.GridCellBoolEditor()
self.grid.SetCellRenderer(1, 1, crbool)
self.grid.SetCellEditor(1, 1, cebool)
# Choice field
cechoice = wx.grid.GridCellChoiceEditor(['Choice 1','Choice 2','Choice 3'], allowOthers=False)
self.grid.SetCellEditor(1, 2, cechoice)
#Load special fields
self.grid.SetCellValue(1, 1, '1')
self.grid.SetCellValue(1, 2, 'Choice 2')
self.grid.SetColSize(0,200)
self.grid.SetColSize(2,200)
# --
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.grid, 1, wx.EXPAND, 5)
panel.SetSizerAndFit(sizer)
self.Show()
def OnKeyPress(self, event):
uk = event.UnicodeKey
key = chr(event.UnicodeKey)
shift = event.shiftDown
if not shift:
key = key.lower()
print("Key", uk, key)
event.Skip()
def onEditorCreated(self,event):
#Set TextCtrl element to want all char/key events for all keys
self.cb = event.Control
if event.Control.ClassName == "wxTextCtrl":
self.cb.SetWindowStyle(wx.WANTS_CHARS) # BEWARE! - Returns Tab, Enter, Arrow keys etc
self.cb.Bind(wx.EVT_KEY_DOWN,self.OnKeyPress)
else:
print("Non text cell - bailing out")
event.Skip()
if __name__ == "__main__":
app = wx.App()
frame = MyForm()
app.MainLoop()
I am displaying data from an SQLite database in a QTableView using a QSqlTableModel. Letting the user edit this data works fine. However, for some columns I want to use QComboboxes instead of free text cells, to restrict the list of possible answers.
I have found this SO answer and am trying to implement it on my model/view setting, but I'm running into problems (so this is a follow-up).
Here's a full mini-example:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from PyQt5 import QtSql
from PyQt5.QtWidgets import (QWidget, QTableView, QApplication, QHBoxLayout,
QItemDelegate, QComboBox)
from PyQt5.QtCore import pyqtSlot
import sys
class ComboDelegate(QItemDelegate):
"""
A delegate that places a fully functioning QComboBox in every
cell of the column to which it's applied
source: https://gist.github.com/Riateche/5984815
"""
def __init__(self, parent, items):
self.items = items
QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
combo = QComboBox(parent)
li = []
for item in self.items:
li.append(item)
combo.addItems(li)
combo.currentIndexChanged.connect(self.currentIndexChanged)
return combo
def setEditorData(self, editor, index):
editor.blockSignals(True)
# editor.setCurrentIndex(int(index.model().data(index))) #from original code
editor.setCurrentIndex(index.row()) # replacement
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentIndex())
#pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
class Example(QWidget):
def __init__(self):
super().__init__()
self.resize(400, 150)
self.createConnection()
self.fillTable() # comment out to skip re-creating the SQL table
self.createModel()
self.initUI()
def createConnection(self):
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("test.db")
if not self.db.open():
print("Cannot establish a database connection")
return False
def fillTable(self):
self.db.transaction()
q = QtSql.QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Cars;")
q.exec_("CREATE TABLE Cars (Company TEXT, Model TEXT, Year NUMBER);")
q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 2009);")
q.exec_("INSERT INTO Cars VALUES ('VW', 'Golf', 2013);")
q.exec_("INSERT INTO Cars VALUES ('VW', 'Polo', 1999);")
self.db.commit()
def createModel(self):
self.model = QtSql.QSqlTableModel()
self.model.setTable("Cars")
self.model.select()
def initUI(self):
layout = QHBoxLayout()
self.setLayout(layout)
view = QTableView()
layout.addWidget(view)
view.setModel(self.model)
view.setItemDelegateForColumn(0, ComboDelegate(self, ["VW", "Honda"]))
for row in range(0, self.model.rowCount()):
view.openPersistentEditor(self.model.index(row, 0))
def closeEvent(self, e):
for row in range(self.model.rowCount()):
print("row {}: company = {}".format(row, self.model.data(self.model.index(row, 0))))
if (self.db.open()):
self.db.close()
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
In this case, I want to use a QCombobox on the "Company" column. It should be displayed all the time, so I'm calling openPersistentEditor.
Problem 1: default values I would expect that this shows the non-edited field's content when not edited (i.e. the company as it is listed in the model), but instead it apparently shows the ith element of the combobox's choices.
How can I make each combobox show the model's actual content for this field by default?
Problem 2: editing When you comment out "self.fill_table()" you can check whether the edits arrive in the SQL database. I would expect that choosing any field in the dropdown list would replace the original value. But (a) I have to make every choice twice (the first time, the value displayed in the cell remains the same), and (b) the data appears in the model weirdly (changing the first column to 'VW', 'Honda', 'Honda' results in ('1', 'VW', '1' in the model). I think this is because the code uses editor.currentIndex() in the delegate's setModelData, but I have not found a way to use the editor's content instead. How can I make the code report the user's choices correctly back to the model? (And how do I make this work on first click, instead of needing 2 clicks?)
Any help greatly appreciated. (I have read the documentation on QAbstractItemDelegate, but I don't find it particularly helpful.)
Found the solution with the help of the book Rapid GUI Programming with Python and Qt:
createEditor and setEditorData do not work as I expected (I was misguided because the example code looked like it was using the text content but instead was dealing with index numbers). Instead, they should look like this:
def setEditorData(self, editor, index):
editor.blockSignals(True)
text = index.model().data(index, Qt.DisplayRole)
try:
i = self.items.index(text)
except ValueError:
i = 0
editor.setCurrentIndex(i)
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText())
I hope this helps someone down the line.
The problem I'm having is that when I set the static text inside a panel (see #Case 2) it only renders the first letter. The code below is a super stripped down version of my actual code but it produces an identical result:
import wx
import time
class TestInterface(wx.Frame):
testStatusFlag = 0
def __init__(self, *args, **kw):
super(TestInterface, self).__init__(*args, **kw)
self.pnl = wx.Panel(self)
self.SetSize((450, 225))
self.SetTitle('example')
self.Centre()
self.Show(True)
self.indicatorFullTest = None
self.buttonFullTest = None
self.setTestStatus(status=1)
def runTest(self, ev=None):
self.setTestStatus(status=2)
#the test is a bunch of functions that take a bunch of time to run
#they're all located in separate files but all access a single piece of hardware
#so multithreading is effectively impossible (I don't want to spend days re-writing stuff to accomodate it)
time.sleep(10)
self.setTestStatus(status=3)
return 0
def setTestStatus(self, ev=None, status=None):
#Handle the optional status argument
if (status in [1,2,3]):
self.testStatusFlag = status
#Remove any old stuff since if we're calling this function they have to get removed
if (self.indicatorFullTest != None):
self.indicatorFullTest.Hide()
if (self.buttonFullTest != None):
self.buttonFullTest.Hide()
#Case 1
if (self.testStatusFlag == 1):
self.buttonFullTest = wx.Button( self.pnl, label='Run Test', pos=(125, 100), size=(250, 50))
self.buttonFullTest.Bind(wx.EVT_BUTTON, self.runTest)
#Case 2
elif (self.testStatusFlag == 2):
self.indicatorFullTest = wx.Panel( self.pnl, pos=(125, 100), size=(250, 50))
wx.StaticText(self.indicatorFullTest, wx.ID_ANY, "Full-Board Test now in progress\nAllow up to 6 min to finish...",
style=wx.ALIGN_CENTRE_HORIZONTAL, pos=(18,7))
self.indicatorFullTest.SetBackgroundColour( 'Tan' )
self.Update()
#Case 3
elif (self.testStatusFlag == 3):
self.buttonFullTest = wx.Button( self.pnl, label='Test Complete\nPress to reset GUI',
pos=(125, 100), size=(250, 50) )
self.buttonFullTest.SetBackgroundColour( (130,255,130) )
self.buttonFullTest.Bind(wx.EVT_BUTTON, self.resetGUI)
#Resets the GUI after a test is complete
def resetGUI(self, ev=None):
self.setTestStatus(status=1) #Reset the fullTest button/indicator thing
if __name__ == '__main__':
ex = wx.App()
gui = TestInterface(None)
ex.MainLoop()
Basically, how do I make the UI fully render the text? I imagine it has something to do with not going back to wx's main loop after changing that indicator, but I feel like calling self.Update() should make that unnecessary. It may also have something to do with how I'm switching between using a button and using a panel (which is probably bad but I'm not sure how else to do it). I know that I could solve this by making my test function run in a separate thread but the problem is that the test function calls separate libraries which I literally do not have the time to re-write.
Thanks
This is a bit easier when you use wxPython's sizers because then you can just Show and Hide widgets. You can see a good example in this tutorial. I would recommend learning sizers just to make your UI more dynamic when you resize the frame.
But regardless, the answer to this conundrum is pretty simple. Instead of calling self.Update() in Case #2, you need to call wx.Yield(). I swapped that one line change in and it worked for me on Linux.