Following is my code to validate an account code, and add dashes at certain intervals. An example account code is 140-100-1000-6610-543. The code takes the regex: \d{3}-\d{3}-\d{4}-\d{4}-\d{3] and allows the user to type in numbers, and where there is a dash in the regex, it places the dash for the user. Or rather, it should. On our production server (('Qt version:', '4.8.1'), ('SIP version:', '4.13.2'), ('PyQt version:', '4.9.1')) it does work. Our dev server is upgraded to ('Qt version:', '4.8.6'), ('SIP version:', '4.15.5'), ('PyQt version:', '4.10.4') and it doesn't work there.
On each server, I type in 140. On the production (older version), the line edit value changes to 140-. On the newer version development server, it does not add the dash.
Please let me know if you see my issue, and let me know if this is a PyQt issue, or a Qt issue.
import sys
from PyQt4 import QtGui, QtCore
DEBUG = True
class acValidator(QtGui.QRegExpValidator):
def __init__(self, regexp, widget):
QtGui.QRegExpValidator.__init__(self, regexp, widget)
self.widget = widget
def validate(self, text, pos):
'''function to decide if this account code is valid'''
valid, _npos = QtGui.QRegExpValidator.validate(self, text, pos)
if valid != QtGui.QValidator.Acceptable:
self.fixup(text)
print 'acWidget.validate result of fixup', text
# move position to the end, if fixup just added a dash
# to the end
newstr = self.widget.text()
newpos = len(str(newstr))
if pos + 1 == newpos and newstr[pos:newpos] == '-':
pos = newpos
# return the valid variable, and the current position
return (valid, pos)
def fixup(self, text):
'''place dashes, if we can'''
# pylint: disable=no-member
if DEBUG:
print 'acWidget.py fixup'
reParts = self.regExp().pattern().split('-')
if DEBUG:
print list(reParts)
newreg = ''
for i in range(len(reParts)):
newreg += reParts[i]
if DEBUG:
print i, reParts[i]
nr = QtCore.QRegExp(newreg)
# pylint: disable=no-member
if nr.exactMatch(text) and not self.regExp().exactMatch(text):
if DEBUG:
print 'adding dash'
text += '-'
return
newreg += '-'
def isValid(self):
'''return a true or false for the validity based on whether the
widget is a lineEdit or a comboBox (acCB). true only if the
validator returns QtGui.QValidator.Acceptable
'''
valid, _npos = QtGui.QRegExpValidator.validate(self,
self.widget.text(),
self.widget.cursorPosition())
if valid == QtGui.QValidator.Acceptable:
return True
return False
class acWidget(QtGui.QLineEdit):
def __init__(self, parent=None):
QtGui.QLineEdit.__init__(self, parent)
self.regex = r'\d{3}-\d{3}-\d{4}-\d{4}-\d{3}'
self.setMinimumWidth(200)
self.setValidator(acValidator(QtCore.QRegExp(self.regex),
self))
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
form = QtGui.QDialog()
layout = QtGui.QVBoxLayout(form)
a = acWidget(form)
layout.addWidget(a)
form.setLayout(layout)
form.setMinimumWidth(400)
form.setMinimumHeight(200)
form.show()
app.exec_()
The problem is caused by the C++ signatures of fixup and validate requiring that the text argument is modifiable. If you're using Python 2, this distinctly unpythonic way of doing things is honoured by PyQt; whereas with Python 3, the signatures have been changed so that the methods simply return the modified values.
The specific issue in your code can be found here:
def fixup(self, text):
...
text += '-'
It seems that, in earlier versions of PyQt, augmented assignment on a QString did an implicit in-place mutation. But in more recent versions, it works more like normal python augmented assignment and simply re-binds the local variable like this:
text = text + '-'
To work around this, you can do an explict in-place mutation:
text.append('-')
Related
I'm very new to QT so please assume I know nothing. I want an auto-complete where it only matches against the text after the last comma.
e.g. if my word bank is ["alpha", "beta", "vector space"], and the user currently has typed "epsilon,dog,space" then it should match against "vector space" since "space" is a substring of "vector space".
I'm using PyQt6 and my current code looks something like this (adapted from a YouTube tutorial):
line_of_text = QLineEdit("")
word_bank = [name for name,value in names_dict.items()]
completer = QCompleter(word_bank)
completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
completer.setFilterMode(Qt.MatchContains)
line_of_text.setCompleter(completer)
So currently, if word_bank is ["alpha", "beta", "vector space"], then if line_of_text had the string "epsilon,dog,space" then it wouldn't match against anything because "epsilon,dog,space" isn't a substring of "alpha" nor "beta" nor "vector space".
How can I alter my code to achieve what I would like to achieve? -- I'm experienced with programming, just not with Qt.
PS: I have tried doing
line_of_text.textChanged[str].connect(my_function)
where my_function takes only the substring of line_of_text after the last comma and feeds it to completer.setCompletionPrefix and then calls completer.complete().
This simply does not work. I assume the reason is that completer.complete() updates the completion prefix from line_of_text, causing the call to completer.setCompletionPrefix to be overwritten immediately after.
One solution is to use the textChanged signal to set the completion prefix, and bypass the built-in behaviour of the line-edit to allow greater control of when and how the completions happen.
Below is a basic implementation that shows how to do that. By default, it only shows completions when the text after the last comma has more than single character - but that can be easily adjusted:
from PyQt6 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.edit = QtWidgets.QLineEdit()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.edit)
word_bank = ['alpha', 'beta', 'vector space']
self.completer = QtWidgets.QCompleter(word_bank)
self.completer.setCaseSensitivity(
QtCore.Qt.CaseSensitivity.CaseInsensitive)
self.completer.setFilterMode(QtCore.Qt.MatchFlag.MatchContains)
self.completer.setWidget(self.edit)
self.completer.activated.connect(self.handleCompletion)
self.edit.textChanged.connect(self.handleTextChanged)
self._completing = False
def handleTextChanged(self, text):
if not self._completing:
found = False
prefix = text.rpartition(',')[-1]
if len(prefix) > 1:
self.completer.setCompletionPrefix(prefix)
if self.completer.currentRow() >= 0:
found = True
if found:
self.completer.complete()
else:
self.completer.popup().hide()
def handleCompletion(self, text):
if not self._completing:
self._completing = True
prefix = self.completer.completionPrefix()
self.edit.setText(self.edit.text()[:-len(prefix)] + text)
self._completing = False
if __name__ == '__main__':
app = QtWidgets.QApplication(['Test'])
window = Window()
window.setGeometry(600, 100, 300, 50)
window.show()
app.exec()
(Assuming str is the string from the text input) Have you tried separating str using the , character, then iterating over each of the words separated by a comma.
for word in str.split(","):
for check in word_bank:
etc...
the connected function will be called in your case whenever the string entered changes.
I am trying to have a series of QTableView created at runtime and added to newly created pages of a multipage QTabWidget.
All seems to go fine, but the QTableView don't show up.
The QTabWidget gets zeroed (reset to no pages) and refurbished (...) flawlessly (at least it looks like so) depending on the selection of a combobox (and the dictionaries therein related).
I am also using a delegate callback to include a column of checkboxes to the QTableView (thanks to https://stackoverflow.com/a/50314085/7710452), which works fine stand alone.
Here is the code.
Main Window
EDIT
as recommended by eyllanesc, here is the standalone module (jump to the end of the post for details on the part I think is problematic):
"""
qt5 template
"""
import os
import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtGui as qtg
from PyQt5 import uic
from configparser import ConfigParser, ExtendedInterpolation
from lib.SearchControllers import findGuis, get_controller_dict, show_critical, show_exception
import resources.resources
from lib.CheckBoxesDelegate import CheckBoxDelegate
myForm_2, baseClass = uic.loadUiType('./forms/setup.ui')
class MainWindow(baseClass):
def __init__(self, config_obj: ConfigParser,
config_name: str,
proj_name: str,
*args,
**kwargs):
super().__init__(*args, **kwargs)
self.ui = myForm_2()
self.ui.setupUi(self)
# your code begins here
self.setWindowTitle(proj_name + " Setup")
self.ui.logo_lbl.setPixmap(qtg.QPixmap(':/logo_Small.png'))
self.config_obj = config_obj
self.config_name = config_name
self.proj_filename = proj_name
self.proj_config = ConfigParser(interpolation=ExtendedInterpolation())
self.proj_config.read(proj_name)
self.guis_dict = {}
self.components = {}
self.cdp_signals = {}
self.root_path = self.config_obj['active']['controllers']
self.tableViews = []
self.tabs = []
self.iniControllersBox()
self.setActSignals()
self.load_bulk()
self.set_signals_table()
self.update_CurController_lbl()
self.update_ControllersTab() # here is where the action gets hot
# your code ends here
self.show() # here crashes if I passed the new tab to the instance of
# QTabView. otherwise it shows empty tabs
#########################################################
def load_bulk(self):
# get the list of running components into a dictionary
for i in self.list_controllers:
i_path = os.path.join(self.root_path, i)
print(i)
self.components[i] = get_controller_dict(i_path,
self.config_obj,
'Application.xml',
'Subcomponents/Subcomponent',
'Name',
'src')
for j in self.components[i]:
print(j)
signals_key = (i , j)
tgt = os.path.join(self.root_path, self.components[i][j])
self.cdp_signals[signals_key] = get_controller_dict(i_path,
self.config_obj,
self.components[i][j],
'Signals/Signal',
'Name',
'Type',
'Routing')
def set_signals_table(self):
self.ui.MonitoredDevicesTable.setHorizontalHeaderItem(0, qtw.QTableWidgetItem('GUI caption'))
self.ui.MonitoredDevicesTable.setHorizontalHeaderItem(1, qtw.QTableWidgetItem('Monitored Signal'))
def setActSignals(self):
self.ui.controllersBox.currentIndexChanged.connect(self.update_guis_list)
self.ui.controllersBox.currentIndexChanged.connect(self.update_CurController_lbl)
self.ui.controllersBox.currentIndexChanged.connect(self.update_ControllersTab)
def update_ControllersTab(self):
self.ui.componentsTab.clear() # this is the QTabWidget
self.tabs = []
self.tableViews = []
curr_controller = self.ui.controllersBox.currentText()
for i in self.components[curr_controller]:
if len(self.cdp_signals[curr_controller, i]) == 0:
continue
self.tabs.append(qtw.QWidget())
tabs_index = len(self.tabs) - 1
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model = qtg.QStandardItemModel(len(self.cdp_signals[curr_controller, i]), 5)
model.setHorizontalHeaderLabels(header_labels)
# in the next line I try to create a new QTableView passing
# the last tab as parameter, in the attempt to embed the QTableView
# into the QWidget Tab
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
tbw_Index = len(self.tableViews) - 1
self.tableViews[tbw_Index].setModel(model)
delegate = CheckBoxDelegate(None)
self.tableViews[tbw_Index].setItemDelegateForColumn(0, delegate)
rowCount = 0
for row in self.cdp_signals[curr_controller, i]:
for col in range(len(self.cdp_signals[curr_controller, i][row])):
index = model.index(rowCount, col, qtc.QModelIndex())
model.setData(index, self.cdp_signals[curr_controller, i][row][col])
try:
self.ui.componentsTab.addTab(self.tabs[tabs_index], i) # no problems, some controllers ask up to
except Exception as ex:
print(ex)
def update_CurController_lbl(self):
self.ui.active_controller_lbl.setText(self.ui.controllersBox.currentText())
def iniControllersBox(self):
self.list_controllers = [os.path.basename(f.path) for f in os.scandir(self.root_path) if f.is_dir() and str(
f.path).upper().endswith('NC')]
self.ui.controllersBox.addItems(self.list_controllers)
for i in range(self.ui.controllersBox.count()):
self.ui.controllersBox.setCurrentIndex(i)
newKey = self.ui.controllersBox.currentText()
cur_cntrlr = os.path.join(self.config_obj['active']['controllers'], self.ui.controllersBox.currentText())
self.guis_dict[newKey] = findGuis(cur_cntrlr, self.config_obj)
self.ui.controllersBox.setCurrentIndex(0)
self.update_guis_list()
def update_guis_list(self, index=0):
self.ui.GuisListBox.clear()
self.ui.GuisListBox.addItems(self.guis_dict[self.ui.controllersBox.currentText()])
if __name__ == '__main__':
config = ConfigParser()
config.read('./config.ini')
app = qtw.QApplication([sys.argv])
w = MainWindow(config, './config.ini',
'./test_setup_1.proj')
sys.exit(app.exec_())
and here the external to add the checkboxes column:
class CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
if event.type() == QtCore.QEvent.MouseButtonPress or event.type() == QtCore.QEvent.MouseMove:
return False
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole)
The 1st picture shows the layout in QTDesigner, the 2nd the result (emtpy tabs) when avoiding the crashing.
the QTabWidget has no problems in zeroing, or scale up, back to as many tab as I need, it's just that I have no clue on how to show the QTabview. My approach was to try to embed the QTabView in the tabpage passing it as parameter to the line creating the new QTabView.
Since I am using rather convoluted dictionaries, calling an XML parser to fill them up, not to mention the config files, I know even this version of my script is hardly reproduceable/runnable.
If someone had the patience of focusing on the update_ControllersTab method though, and tell me what I am doing wrong handling the QWidgets, it'd be great.
Again the basic idea is to clear the QTabWidget any time the user selects a different controller (combo box on the left):
self.ui.componentsTab.clear() # this is the QTabWidget
self.tabs = [] # list to hold QTabView QWidgets (pages) throughout the scope
self.tableViews = [] # list to hold QTabView(s) thorughout the scope
count how many tabs (pages) and hence embedded TabViews I need with the new controllers selected.
and then for each tab needed:
create a new tab (page)
self.tabs.append(qtw.QWidget())
tabs_index = len(self.tabs) - 1
create a new QTabView using a model:
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model = qtg.QStandardItemModel(len(self.cdp_signals[curr_controller, i]), 5)
model.setHorizontalHeaderLabels(header_labels)
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
tbw_Index = len(self.tableViews) - 1
self.tableViews[tbw_Index].setModel(model)
populate the TableView with data, and then finally add the tab widget (with the suppposedly embedded QTableView to the QTabWidget (the i argument is a string from my dbases Names:
self.ui.componentsTab.addTab(self.tabs[tabs_index], i)
This method is called also by the __init__ to initialize and apparently all goes error free, until the last 'init' statement:
`self.show()`
at which point the app crashes with:
Process finished with exit code 1073741845
on the other hand, if here instead of trying to embed the QTableView:
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
I omit the parameter, that is:
self.tableViews.append(qtw.QTableView())
the app doesn't crash anymore, but of course no QtableViews are shown, only empty tabpages:
As stupid as this may sound the problem is in... the delegate class that creates the checkboxes in the first column (see https://stackoverflow.com/a/50314085/7710452)
I commented out those two lines:
delegate = CheckBoxDelegate(None)
self.tableViews[tbw_Index].setItemDelegateForColumn(0, delegate)
and... bingo!
the CheckBoxDelegate works fine in the example shown in the post (a single QTableView form). I also tinkered around adding columns and rows, and moving the checkbox column back and forth with no problems. In that standalone. But as soon as I add the class and set the delegate, i am back at square zero, app crashing with:
Process finished with exit code 1073741845
so I am left with this problem now. Thnx to whomever read this.
Problem solved, see comment to post above.
I'm using a QLineEdit widget to enter email addresses and I set up a QRegExpValidor to validate the entry.
The validator is working as far as preventing the input of characters not allowed in the QRegExp, but funny enough, it allows to enter intermediate input, either by pushing the enter key or by firing the "editingfinished" signal.
I verified the return of the validator which is correct (Intermediate or Acceptable).
Checking the PyQt5 documentation it seams that an intermediate state of the validator does not prevent the focus to change to another widget. Furthermore, it stops the firing of the editingFinished and returnedPressed signals so the user may enter wrong address as far as it marches partially the RegExp.
Ref: https://doc.qt.io/qt-5/qlineedit.html#acceptableInput-prop
I was able to solve my needs by removing the validator from the QLineEdit widget, and placing a method "checkValidator" linked to the cursorPositionChanged of the QLineEdit widget discarting the last character entered when nonacceptable, and setting the focus back to the QLineEdit when it validats intermmediate. It works just fine but when the the focus was reset, the others signals on other widgets were fired one at a time. Momentarily, I handle this by checking if the sender has focus at the start of the methods (see lookForFile)
Even though I could handle the issue, I appreciate very much anybody explaining me the proper way to use the RegExpValidator and or why resetting the focus fires the other signals out of the blue.
def setUI(self):
self.setWindowTitle("EMail Settings")
self.setModal(True)
rx = QRegExp(r"[a-z0-9_%]+#[a-z0-9%_]+\.[a-z0-9%_]{3,3}")
lblAddress = QLabel("EMail Address")
self.lineAddress = QLineEdit(self)
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
#self.lineAddress.setValidator(self.mailValidator)
self.lineAddress.cursorPositionChanged.connect(self.checkValidator)
self.lineAddress.returnPressed.connect(self.checkValidator)
lblPassword = QLabel("Password")
self.linePwd = QLineEdit()
self.linePwd.setEchoMode(QLineEdit.PasswordEchoOnEdit)
lblOauth2 = QLabel("Oauth2 Token")
self.lineOauth = QLineEdit()
pushOauth = QPushButton("...")
pushOauth.setObjectName("token")
pushOauth.clicked.connect(self.lookForFile)
pushOauth.setFixedWidth(30)
#pyqtSlot()
def checkValidator(self):
self.lineAddress.blockSignals(True)
v = self.mailValidator.validate(self.lineAddress.text(), len(self.lineAddress.text()))
if v[0] == 0:
self.lineAddress.setText(self.lineAddress.text()[:-1])
elif v[0] == 1:
self.lineAddress.setFocus()
elif v[0] == 2:
pass
print("validates", v)
self.lineAddress.blockSignals(False)
#pyqtSlot()
def lookForFile(self):
try:
if not self.sender().hasFocus():
return
baseDir = "C"
obj = self.sender()
if obj.objectName() == "Draft":
capt = "Email Draft"
baseDir = os.getcwd() + "\\draft"
fileType = "Polo Management Email (*.pad)"
dialog = QFileDialog(self, directory=os.getcwd())
dialog.setFileMode(QFileDialog.Directory)
res = dialog.getExistingDirectory()
elif obj.objectName() == "token":
capt = "Gmail Outh2 token File"
fileType = "Gmail token Files (*.json)"
baseDir = self.lineOauth.text()
res = QFileDialog.getOpenFileName(self, caption=capt, directory=baseDir, filter=fileType)[0]
fileName = res
if obj.objectName() == "Draft":
self.lineDraft.setText(fileName)
elif obj.objectName() == "tokenFile":
self.lineOauth.setText(fileName)
except Exception as err:
print("settings: lookForFile", err.args)
Hope to answer #eyllanesc and Qmusicmante request with this minimal reproducible example. I change the regex for a simple one allowing for a string of lower case a-z follow by a dot and three more lowercase characters.
What I intend is the validator not allowing the user to enter a wrong input. The example allows for "xxxzb.ods" but also allows "xxxzb" or "xxxzb.o" for instance.
In short, not allowing the user to enter a wrong input.
This is my minimal reproducible example:
class CheckValidator(QDialog):
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setUI()
def setUI(self):
self.setWindowTitle("EMail Settings")
self.setModal(True)
rx = QRegExp(r"[a-z]+\.[a-z]{3}")
lblAddress = QLabel("Check Line")
self.lineAddress = QLineEdit()
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
self.lineAddress.setValidator(self.mailValidator)
self.lineAddress.cursorPositionChanged[int, int].connect(lambda
oldPos, newPos: self.printValidator(newPos))
lblCheck = QLabel("Check")
lineCheck = QLineEdit()
formLayout = QFormLayout()
formLayout.addRow(lblAddress, self.lineAddress)
formLayout.addRow(lblCheck, lineCheck)
self.setLayout(formLayout)
#pyqtSlot(int)
def printValidator(self, pos):
print(self.mailValidator.validate(self.lineAddress.text(), pos))
if __name__ == '__main__':
app = QApplication(sys.argv)
tst = CheckValidator()
tst.show()
app.exec()
I found a solution and I post it here for such a case it may help somebody else.
First I remove the QRegExpValidator from the QLineEdit widget. The reason being is that QLineEdit only fires editingFinished (and we'll need it ) only when QRegExpValidator returns QValidator.Acceptable while the validator is present.
Then we set a method fired by the 'cursorPositionchanged' signal of QlineEdit widget. On this method, using the QRegExpValidator we determine if the last character entered is a valid one. If not we remove it.
Finally, I set a method fired by the 'editingFinished' signal using the RegEx exactMatch function to determine if the entry is valid. If it is not, we give the user the option to clear the entry or to return to the widget to continue entering data. The regex used is for testing purposes only, for further information about email validation check #Musicamante comments.
This is the code involved:
def setUI(self):
................
................
rx = QRegExp(r"[a-z0-9_%]+#[a-z0-9%_]+\.[a-z0-9%_]{3,3}")
lblAddress = QLabel("EMail Address")
self.lineAddress = QLineEdit(self)
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
self.lineAddress.cursorPositionChanged[int, int].connect(lambda oldPos,
newPos: self.checkValidator(newPos))
self.lineAddress.editingFinished.connect(lambda : self.checkRegExp(rx))
#pyqtSlot(int)
def checkValidator(self, pos):
v = self.mailValidator.validate(self.lineAddress.text(), pos ))
if v[0] == 0:
self.lineAddress.setText(self.lineAddress.text()[:-1])
#pyqtSlot(QRegExp)
def checkRegExp(self, rx):
if not rx.exactMatch(self.lineAddress.text()) and self.lineAddress.text():
if QMessageBox.question(self, "Leave the Address field",
"The string entered is not a valid email address! \n"
"Do you want to clear the field?", QMessageBox.Yes|QMessageBox.No) ==
QMessageBox.Yes:
self.lineAddress.clear()
else:
self.lineAddress.setFocus()
I’ve got a problem with QDoubleSpinBox. Editing behavior of “backspace” key somehow depends on the size of the suffix.
If I set “m” as suffix, then set the cursor at the end of the spinbox and press “backspace”, the cursor jumps over the “m” suffix to the value which then can be edited with further “backspaces”.
If I set the suffix to “mm” or any double-lettered word, the cursor remains at the end of the spinbox no matter how many “backspaces” I press.
I tried to debug what comes into “validate” method, and got a peculiar result:
When the “backspace” is pressed while cursor is at the end of "0,00m", validate receives "0,00m".
When the “backspace” is pressed while cursor is at the end of "0,00_m" validate receives "0,00__m"
When the “backspace” is pressed while cursor is at the end of "0,00_mm" validate receives "0,00_m_mm"
What is the cause of such behavior and how can I overcome it?
# coding=utf-8
from PyQt5 import QtWidgets
class SpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self):
super().__init__()
def validate(self, text, index):
res = super().validate(text, index)
print(text, res, self.text())
return res
if __name__ == "__main__":
q_app = QtWidgets.QApplication([])
sb = SpinBox()
sb.setSuffix(" m")
sb.show()
q_app.exec_()
The source code for QDoubleSpinBox/QAbstractSpinBox is extremely convoluted when it comes to key-event handling - I couldn't work out what default behaviour is supposed to be, or even where it might be implemented. There might be a bug somewhere, but I wouldn't want to bet on it.
It looks like the only option is to reimplement keyPressEvent:
class SpinBox(QtWidgets.QDoubleSpinBox):
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Backspace:
suffix = self.suffix()
if suffix:
edit = self.lineEdit()
text = edit.text()
if (text.endswith(suffix) and
text != self.specialValueText()):
pos = edit.cursorPosition()
end = len(text) - len(suffix)
if pos > end:
edit.setCursorPosition(end)
return
super().keyPressEvent(event)
I have a QTreeView, and I've figured out how to set its style by using setStyleSheet in my main class:
self.view.setStyleSheet("""
QTreeView::item {
margin: 2px;
}
""")
That will style the entire QTreeView. But I want certain items in the tree to be bolded. When I create the branches (using [the parent widget].appendRow("the name of the item")), is there a way to 'tag' or isolate specific items so it can be styled the same way? I think the answer has something to do with the 'AccessibleName' or 'ObjectName' properties, but I'm having trouble finding documentation on it.
Update: This is what I have so far:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from future_builtins import *
import os, sys
from PySide.QtCore import *
from PySide.QtGui import *
path_to_media = '/Volumes/show/vfx/webisodes/%d1%/seq/%d2%/%d3%/renders/2d/comp/'
class FileTree(QTreeView):
"""Main file tree element"""
def __init__(self):
QTreeView.__init__(self)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Space or event.key() == Qt.Key_Return:
index = self.selectedIndexes()[0]
crawler = index.model().itemFromIndex(index)
if crawler.uri:
print("launching", crawler.uri)
p = os.popen(('open -a "RV.app" "'+ crawler.uri) +'"', "r")
QTreeView.keyPressEvent(self, event)
class Branch(QStandardItem):
"""Branch element"""
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
class AppForm(QMainWindow):
def __init__(self, parent = None):
super(AppForm, self).__init__(parent)
self.model = QStandardItemModel()
self.view = FileTree()
self.view.setStyleSheet("""
QTreeView::item {
margin: 2px;
}
""")
self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.view.setModel(self.model)
self.setCentralWidget(self.view)
self.Grow()
# self.view.setSortingEnabled(True)
def Grow(self):
"""Populates FileTree (using BuildBranch())"""
global path_to_media
self.path = {}
self.path['source'] = path_to_media
self.path['parts'] = []
self.path['depth'] = 0
self.path['crawl'] = {}
for i in self.path['source'].split('%'):
if i[0] == "d" and i[1].isdigit():
self.path['depth'] += 1
else:
self.path['parts'].append(i)
self.BuildBranch(self.path['parts'], self.path['depth'], parentWidget = self.model.invisibleRootItem())
def BuildBranch(self, parts, depth, uri = '', count = 0, parent = '', parentWidget = ''):
"""Recursively crawls folder structure and adds appropriate branches"""
if not uri: uri = parts[0]
else: uri += parent + parts[count]
try:
if os.listdir(uri):
for i in os.listdir(uri):
if i[0] != '.':
if count != depth:
if os.path.isdir(uri):
thisWidget = Branch(i)
parentWidget.appendRow(thisWidget)
self.BuildBranch(parts, depth, uri, count + 1, i, parentWidget = thisWidget)
else:
thisWidget = Branch(i)
parentWidget.appendRow(thisWidget)
elif count == depth:
thisWidget = Branch(i, uri + i, 'media')
parentWidget.appendRow(thisWidget)
else:
print("nothing here; nuking " + parent)
# Need to add code to nuke unused branch
except OSError:
print("Folder structure error... nuking the branch")
# Need to add code to nuke unused branch
def main():
app = QApplication(sys.argv)
form = AppForm()
form.resize(800, 600)
form.setWindowTitle('Qt Dailies')
form.show()
app.exec_()
if __name__ == "__main__":
main()
Update 2: Okay, I modified my Branch class so that if 'bold' is passed to it, it makes the branch bold (in theory)...
class Branch(QStandardItem):
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
if tag == 'bold':
self.setData(self.createBoldFont(), Qt.FontRole)
def createBoldFont(self):
if self.font: return self.font
self.font = QFont()
self.font.setWeight(QFont.Bold)
return self.font
... but while the code runs, nothing seems to happen. What am I still not getting?
Qt's model-view architecture allows for data that describes the different roles being performed. For example, there is a role for editing data, displaying data, etc. You're interested in the font role (i.e. Qt::FontRole) as the font has a weight enum of which bold is one value.
When you build your branches you first need to identify which items should be bolded. I'll assume you have a method like such that can identify whether or not they should be bold:
def should_be_bolded(self, item):
return 1 # your condition checks
Now just set the weight on the font and set the font role of the item using its setData method:
def BuildBranch(...):
thisWidget = Branch(i)
if self.should_be_bolded(thisWidget):
thisWidget.setData(self.createBoldFont(), Qt.FontRole)
def createFont(self):
if self.font: return self.font
self.font = QFont()
self.font.setWeight(QFont.Bold)
return self.font
But wait... you already have a subclass of QtandardItem, so you can use that:
class Branch(QStandardItem):
"""Branch element"""
def __init__(self, label, uri = None, tag = None):
QStandardItem.__init__(self, label)
self.uri = uri
if self.should_be_bolded():
self.bold_myself()
You'll have to fix the should_be_bolded and bold_myself methods, cleaning up accordingly, but hopefully you get the point.
Stephen pointed out that you can also subclass one of the QAbstractItemModels, like the QStandardItemModel you're using, and return a specific Qt.FontRole. His way makes that knowledge implicit in the model. Decide where that knowledge best belongs and place it in the most appropriate place, whether it's in the item, the tree-creation algorithm, the model, or even a view model.
In your model's data() method you can add code to set the font depending on the content of the item. For example, if you wanted to bold everything in one particular row,
def data(self, index, role):
if role == QtCore.Qt.FontRole:
if index.row() == 1:
boldFont = QtGui.QFont()
boldFont.setBold(True)
return boldFont
You just need a way to retrieve the name of your branch when given an index for it. That depends on the implementation of your tree model.
The Qt Model/View tutorial has a good example, although it's in C++. Look at section 2.2 (Extending the Read Only Example with Roles).