Return old value to combobox with dynamic search and autocompletion - python-3.x

I have a reimplemented comboBox that performs dynamic search and autocompletion (code isn't mine). The problem is when I type something, that doesn't match any value in combobox list and press enter - I receive an empty string. But I wish to receive instead an old value, that was in combobox before I started to type other value. Could anybody help me with that?
Also I want to ask the meaning of 2 strings in ExtendedComboBox class (as long as code isn't mine):
inside function on_completer_activated there is expression if text: ; I can't understand what does it mean, because I always write the whole expression (like if text == True: or something like that)
I don't understand the meaning of [str] in line self.activated[str].emit(self.itemText(index)). I have never seen this kind of construction in pyqt when something in square brackets comes directly after a signal.
code:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class ExtendedComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super(ExtendedComboBox, self).__init__(parent)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setEditable(True)
# add a filter model to filter matching items
self.pFilterModel = QtCore.QSortFilterProxyModel(self)
self.pFilterModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.pFilterModel.setSourceModel(self.model())
# add a completer, which uses the filter model
self.completer = QtWidgets.QCompleter(self.pFilterModel, self)
# always show all (filtered) completions
self.completer.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion)
self.setCompleter(self.completer)
# connect signals
self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
self.completer.activated.connect(self.on_completer_activated)
# on selection of an item from the completer, select the corresponding item from combobox
def on_completer_activated(self, text):
if text:
index = self.findText(text)
self.setCurrentIndex(index)
self.activated[str].emit(self.itemText(index))
# on model change, update the models of the filter and completer as well
def setModel(self, model):
super(ExtendedComboBox, self).setModel(model)
self.pFilterModel.setSourceModel(model)
self.completer.setModel(self.pFilterModel)
# on model column change, update the model column of the filter and completer as well
def setModelColumn(self, column):
self.completer.setCompletionColumn(column)
self.pFilterModel.setFilterKeyColumn(column)
super(ExtendedComboBox, self).setModelColumn(column)
class ComboBox_Model(QtCore.QAbstractListModel):
def __init__(self, data_list = [], parent = None):
super(ComboBox_Model, self).__init__()
self.data_list = data_list
def rowCount(self, parent):
return len(self.data_list)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
value = self.data_list[row]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
value = self.data_list[row]
return value
class Mainwindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.combobox = ExtendedComboBox()
self.layout_1 = QtWidgets.QHBoxLayout()
self.layout_1.addWidget(self.combobox)
self.setLayout(self.layout_1)
data = ['some text to display', 'other text to display', 'different text']
self.model = ComboBox_Model(data)
self.combobox.setModel(self.model)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())

When a combobox is set as editable, by default allows insertion of non existing items at the bottom of the current model when pressing return. Since the model used in that code is not editable, when pressing return with unrecognized text the combobox is unable to add the new item (and select it), which results in setting the index to -1.
You can connect to the embedded QLineEdit returnPressed signal and check whether the current index is valid or not; this is possible because the signal is also previously connected to the combobox insertion, so when you receive the signal the combo has already tried to add the new item and eventually set the (possibly) invalid index.
In order to store the previous index, just connect to the currentIndexChanged() and save it as long as it's greater or equal to 0.
class ExtendedComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
# ...
self.lineEdit().returnPressed.connect(self.returnPressed)
self.currentIndexChanged.connect(self.storePreviousIndex)
self.previousIndex = self.currentIndex()
def storePreviousIndex(self, index):
if index >= 0:
self.previousIndex = index
def returnPressed(self):
if self.currentIndex() < 0 or self.currentText() != self.itemText(self.currentIndex()):
self.setCurrentIndex(self.previousIndex)
Note that the second comparison in returnPressed is to add compatibility to the default internal model, in case setModel() is not called and the insertion policy is NoInsert.
About the two final questions:
the if statement checks if the condition is true or not, or, if you want, the condition is not false, as in "not nothing" (aka, False, 0, None); you can do some experiments with simple statements to better understand: if True:, if 1:, if 'something': will all result as valid conditions, while if False:, if 0: or if '': not.
some signals have multiple signatures for their arguments, meaning that the same signal can be emitted more than once, each time with different types of arguments; for example the activated signal of QComboBox is emitted twice, the first time as int with the new current index, then with the new current text; whenever you want to connect to (or emit) an overload that is not the default one, you need to specify the signature in brackets. In the case above, the signal is explicitly emitted for the str signature only (I don't know why the int was not, though). Note that overloaded signals are being gradually removed in Qt (in fact, the [str] signature of activated() is considered obsolete since Qt 5.14).

Related

Connecting comboboxes created in a loop in PyQt5

I'm trying to build a GUI that is generated dynamically based on some input dictionary. I'm using the GridLayout, iterate over the dictionary keys and generate per iteration/grid row the key of the dictionary as a QLineEdit (to give some background color as a visual cue) and two comboboxes next to it. Ideally, the second combobox would change its items based on what is selected in the first combobox. Here's the relevant part of my code:
from PyQt5.QtCore import Qt
from PyQt5.Widgets import QApplication, QMainWindow, QVBoxLayout, QGridLayout, QLineEdit, QComboBox
class GUI(QMainWindow):
"""App View"""
def __init__(self, data):
super().__init__()
self.data = data
self.generalLayout = QVBoxLayout()
self._displayview()
def _displayview(self):
self.layout = QGridLayout()
subdict = self.data
combolist = ['Auto', 'Select value', 'Calculate']
maxwidth = sum([len(key) for key in subdict.keys()])
self.count = 0
for item in subdict.keys():
color = subdict[item]['Color']
# Display tags and bg-color
display = QLineEdit()
display.setMaximumWidth(maxwidth-maxwidth//4)
display.setStyleSheet("QLineEdit"
"{"
f"background : {color}"
"}")
display.setText(item)
display.setAlignment(Qt.AlignLeft)
display.setReadOnly(True)
# Combobox action
self.cb_action = QComboBox()
if color == 'Lightgreen':
self.cb_action.addItem('Auto')
self.cb_action.setEnabled(False)
else:
self.cb_action.addItems(combolist)
# Combobox value
self.cb_value = QComboBox()
self.cb_value.addItem('Text')
self.cb_action.currentIndexChanged.connect(self.react)
self.layout.addWidget(display, self.count, 0)
self.layout.addWidget(self.cb_action, self.count, 1)
self.layout.addWidget(self.cb_value, self.count, 2)
self.count += 1
self.generalLayout.addLayout(self.layout)
def react(self):
... # my humble approaches here
Basically, the first combobox has the three options: Auto, Select value and Calculate and based on that selection, the combobox next to it should present different options (right now, it only has 'Text' for testing purposes).
I tried different approaches in the self.react(), e.g. a simple self.cb_value.addItem('something'), which would however add the item in the last combobox (which makes sense). I also tried to simply build a new combobox with self.layout.addWidget(), however without an index, that won't work. Lastly, I tried to simply create that second column of comboboxes anew in another iteration using self.cb_action.currentText() as help, however, that again returns only the text from the last combobox.
I understand that due to the nature of creating everything while iterating I get these problems. It's not unlikely that I haven't fully understood the concept of widgets. Would anybody be so kind to point me in the right direction how I would do this with variable input data? I'd probably face the same issue when I tried to extract all these information from the comboboxes to get some output I can work with.
Thank you and have a good day.

Updating and deleting the MySQLite database through dynamic button on QTableWidget

I have created a fairly simple app that takes three input parameter from the LineEdit's and displays it in the QTablewidget through the Button placed side it. In QTableWidget dynamic update button and removes button are created as the rows values are filled. Whenever QTableWidget's cell is changed and the update button is clicked, It updated the value in the database. Removes button helps to remove the specific row entry from the database.I am able to remove from value from QTableWidget but not from database.
ui,_ = loadUiType('drake.ui')
from db_new import DatabaseNew
db_new = DatabaseNew('database-punk-2')
class LoginNew(QMainWindow, ui):
def __init__(self):
QMainWindow.__init__(self)
self.setupUi(self)
self.show_database()
self.pushButton.clicked.connect(self.addToTableWidget)
def addToTableWidget(self):
self.row_data = []
self.val1 = self.lineEdit.text()
self.row_data.append(self.val1)
self.val2 = self.lineEdit_2.text()
self.row_data.append(self.val2)
self.val3 = self.lineEdit_3.text()
self.row_data.append(self.val3)
row = self.tableWidget.rowCount()
self.tableWidget.setRowCount(row+1)
col = 0
for item in self.row_data:
cell = QTableWidgetItem(str(item))
self.tableWidget.setItem(row, col, cell)
col += 1
db_new.insert(self.val1,self.val2, self.val3)
for index in range(self.tableWidget.rowCount()):
self.btx = QPushButton(self.tableWidget)
self.btn = QPushButton(self.tableWidget)
self.btx.setText("Update")
self.btn.setIcon(QIcon(QPixmap("delete.png")))
self.btn.setIconSize(QSize(35,35))
self.btx.clicked.connect(self.update_pos)
self.btn.clicked.connect(self.delete_pos)
self.tableWidget.setCellWidget(index,3, self.btx)
self.tableWidget.setCellWidget(index,4,self.btn)
def show_database(self):
res = db_new.fetch_data()
self.tableWidget.setRowCount(0)
for row_number, row_data in enumerate(res):
self.tableWidget.insertRow(row_number)
for column_number, data in enumerate(row_data):
self.tableWidget.setItem(row_number, column_number, QTableWidgetItem(str(data)))
def update_pos(self):
self.button =self.focusWidget()
self.index = self.tableWidget.indexAt(self.button.pos())
self.button.clicked.connect(self.btn_trigger)
def btn_trigger(self):
QMessageBox.information(self, "Update Data", f' Value is {self.index.row()} {self.index.column()}')
# db_new.update(self.index.row()-1,self.val1,self.val2,self.val3)
# Unable to find appropiate method for updating the values from the database.
def delete_pos(self):
rows = set()
print("First row Value ")
print(rows)
for indexes in self.tableWidget.selectedIndexes():
rows.add(indexes.row())
for row in sorted(rows, reverse=True):
self.tableWidget.removeRow(row)
# Unable to find the appropiate logic for removing from database
def main():
app = QApplication(sys.argv)
win = LoginNew()
win.show()
app.exec_()
if __name__ =='__main__':
main()
Database File
UI file image
I am unable to update the database or delete a specific row using the row-delete button, I a'm unable to do it.
From what I am seeing I understand the following:
With show_database() -> fetch_data() you are only getting the fields description_one, description_two, and status from your database.
The information which is lacking here is "ID". This field is required by your remove and update functions. You try to emulate this with self.index.row()-1, however this is bound to fail as IDs are typically auto-incrementing and hence not necessarily in a 1-2-3-fashion anymore.
So I would suggest adding an ID-column to the table, and getting this field with fetch_data as well. Once you have that, you should be able to use your remove and update functions in a straight forward way, directly implementing the fetched ID.
If you wand to declutter the interface, you can hide this column, as it does not contain any viable information for a potential user of the interface.

What's difference setData, setItemData and setIem method of QStandardItemModel?

I am confused how to use setData, setItemData and setItem method of QStandardItemModel, these method seem have the same effect, i wonder to know which method should i choose to use will be best?
class DemoD(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
table = QTableView()
model = QStandardItemModel(4, 2)
table.setModel(model)
# delegate = SpinBoxDelegate()
# table.setItemDelegate(delegate)
for row in range(4):
for col in range(2):
item = QStandardItem('china')
model.setItem(row, col, item)
index = model.index(row, col)
value = QVariant((row + 1)*(col + 1))
model.setData(index, value)
model.setItemData(index, {1: 'a', 2: 'b'})
self.setCentralWidget(table)
self.resize(400, 300)
app = QApplication([])
demo = DemoD()
demo.show()
app.exec()
If you want to understand the concepts of a Qt model you should read the following guides:
Model/View Programming
Model/View Tutorial
Why Qt is misusing model/view terminology?
Documentation of each method: setData(), setItemData() and setItem().
Previous concepts:
QStandarItemModel: It is a class that inherits from QAbstractItemModel that allows to store any type of information unlike QAbstractItemModel that only defines the behavior.
Considering that you have read the previous links carefully, we will try to explain the difference between the different methods that you indicate:
setData(): Every Qt model inherits from QAbstractItemModel so this class defines the general behavior, in this case it is defined that the setData() model is responsible for modifying the information of a role associated with a QModelIndex. In other words, it is the generic method that you have to implement if you want to implement an editable model, for example QStringListModel is not an editable model so it does not implement it but in the case of QStandardItemModel it is editable so you can modify the information of the model through of that method.
setItem(): QStandardItem is a concept of QStandardItemModel that is conceptually similar to QModelIndex. This element allows you to easily interact with the QModelIndex. If a QStandardItem is not associated with a model it will only store the information, at the time a model is assigned all information is passed to the model, and the model informs you of any changes that can be made by other methods such as setData. An equivalent to setData of the model is the setData method of QStandardItem but the latter does not need to provide the QModelIndex since that information is internally available or can be obtained when a model is established.
For example:
it.setText("foo")
it.setTextAlignment(QtCore.Qt.AlignCenter)
is equivalent to
it.model().setData(it.index(), "foo", QtCore.Qt.DisplayRole)
it.model().setData(it.index(), QtCore.Qt.AlignCenter, QtCore.Qt.TextAlignmentRole)
As you can see, QStandardItem allows you to modify the information of the item in a simple way, and in a simple way you can say that it is an item of the model.
setItemData(): It is a method that allows you to modify the information of several roles associated to a QModelIndex by checking if the roles are valid, in general if you use an invalid model the method will not update the information but in the case of QStandardItemModel that handles the generic information it is established that everything Role is valid for what will always work.
In the case of QStandardItemModel the following codes are equivalent:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
model = QtGui.QStandardItemModel(1, 1)
it = QtGui.QStandardItem()
model.setItem(0, 0, it)
# The following lines modify the text shown
# to be related to the Qt::DisplayRole role and
# the QModelIndex associated with the QStandardItem:
it.setText("foo")
it.setData("foo", QtCore.Qt.DisplayRole)
model.setData(it.index(), "foo", QtCore.Qt.DisplayRole)
model.setItemData(it.index(), {QtCore.Qt.DisplayRole: "foo"})
# The same as the previous lines but in the case of
# the background and the text colors of the item.
it.setForeground(QtGui.QColor("red"))
it.setBackground(QtGui.QColor("blue"))
it.setData(QtGui.QColor("red"), QtCore.Qt.ForegroundRole)
it.setData(QtGui.QColor("blue"), QtCore.Qt.BackgroundRole)
model.setData(it.index(), QtGui.QColor("red"), QtCore.Qt.ForegroundRole)
model.setData(it.index(), QtGui.QColor("blue"), QtCore.Qt.BackgroundRole)
model.setItemData(
it.index(),
{
QtCore.Qt.ForegroundRole: QtGui.QColor("red"),
QtCore.Qt.BackgroundRole: QtGui.QColor("blue"),
},
)
Both setData and setItemData are very similar.
What you have to understand is that Qt models use roles to assign certain data to each "index". This means that each index (a reference to a model row and column, possibly including a parent if the model supports trees) can have different data attached to it. The most commonly used data role is the "DisplayRole", which is what an item view usually shows as text; but other data is usually implemented (see ItemDataRole, which helps an item view to correctly show the model data to the user.
The most important difference between setData and setItemData is the mapping. What you're doing does not work as the keywords you're using are not recognized as usable roles.
In your example ({1: 'a', 2: 'b'}), 1 maps to DecorationRole (which is used from item views to show a decoration - an icon) and 2 maps to EditRole, which is used whenever the user wants to edit the contents of that item, something that can differ from what's displayed (think about entering a date in a short form such as "10/11", that can be an actual date that is finally shown as "november 10 2019").
Finally, setItem is a special function of QStandardItemModel that creates a new item (or overwrites an existing one) with the new provided QStandardItem object.
I'm providing a test example that will better show what happens in all three situations.
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.table = QtWidgets.QTableView()
layout.addWidget(self.table)
# hide headers, we're not interested
self.table.horizontalHeader().setVisible(False)
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.model = QtGui.QStandardItemModel()
self.table.setModel(self.model)
for item in range(1, 6):
item = QtGui.QStandardItem('item {}'.format(item))
self.model.appendRow(item)
toolLayout = QtWidgets.QHBoxLayout()
layout.addLayout(toolLayout)
self.itemTextEdit = QtWidgets.QLineEdit('text')
toolLayout.addWidget(self.itemTextEdit)
self.itemSetTextButton = QtWidgets.QPushButton('Set text')
toolLayout.addWidget(self.itemSetTextButton)
self.itemSetTextButton.clicked.connect(self.setText)
toolLayout.addSpacing(5)
self.itemAlignCombo = QtWidgets.QComboBox()
toolLayout.addWidget(self.itemAlignCombo)
for alignText in ('Left', 'Center', 'Right'):
alignment = QtCore.Qt.AlignVCenter | getattr(QtCore.Qt, 'Align{}'.format(alignText))
self.itemAlignCombo.addItem(alignText, alignment)
self.itemSetAlignButton = QtWidgets.QPushButton('Set alignment')
toolLayout.addWidget(self.itemSetAlignButton)
self.itemSetAlignButton.clicked.connect(self.setAlignment)
self.table.setCurrentIndex(self.model.index(0, 0))
toolLayout.addSpacing(5)
self.setDataButton = QtWidgets.QPushButton('SetItemData()')
toolLayout.addWidget(self.setDataButton)
self.setDataButton.clicked.connect(self.setItemData)
setItemLayout = QtWidgets.QHBoxLayout()
layout.addLayout(setItemLayout)
self.itemRowSpin = QtWidgets.QSpinBox()
setItemLayout.addWidget(self.itemRowSpin)
self.itemRowSpin.setRange(1, self.model.rowCount() + 1)
self.itemRowSpin.setValue(self.itemRowSpin.maximum())
self.setItemButton = QtWidgets.QPushButton('SetItem()')
setItemLayout.addWidget(self.setItemButton)
self.setItemButton.clicked.connect(self.setItem)
def setText(self):
# set the text of the current item
index = self.table.currentIndex()
self.model.setData(index, self.itemTextEdit.text())
def setAlignment(self):
# set the alignment of the current item
index = self.table.currentIndex()
self.model.setData(index, self.itemAlignCombo.currentData(), QtCore.Qt.TextAlignmentRole)
def setItemData(self):
# set *both* text and alignment of the current item
index = self.table.currentIndex()
self.model.setItemData(index, {
QtCore.Qt.DisplayRole: self.itemTextEdit.text(),
QtCore.Qt.TextAlignmentRole: self.itemAlignCombo.currentData()
})
def setItem(self):
# set a new item for the selected row with the selected text and alignment
item = QtGui.QStandardItem()
item.setText(self.itemTextEdit.text())
item.setTextAlignment(QtCore.Qt.Alignment(self.itemAlignCombo.currentData()))
self.model.setItem(self.itemRowSpin.value() - 1, 0, item)
self.itemRowSpin.setMaximum(self.model.rowCount() + 1)
self.itemRowSpin.setValue(self.itemRowSpin.maximum())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

Struggling to understand this Tkinter entry validation code [duplicate]

What is the recommended technique for interactively validating content in a tkinter Entry widget?
I've read the posts about using validate=True and validatecommand=command, and it appears that these features are limited by the fact that they get cleared if the validatecommand command updates the Entry widget's value.
Given this behavior, should we bind on the KeyPress, Cut, and Paste events and monitor/update our Entry widget's value through these events? (And other related events that I might have missed?)
Or should we forget interactive validation altogether and only validate on FocusOut events?
The correct answer is, use the validatecommand attribute of the widget. Unfortunately this feature is severely under-documented in the Tkinter world, though it is quite sufficiently documented in the Tk world. Even though it's not documented well, it has everything you need to do validation without resorting to bindings or tracing variables, or modifying the widget from within the validation procedure.
The trick is to know that you can have Tkinter pass in special values to your validate command. These values give you all the information you need to know to decide on whether the data is valid or not: the value prior to the edit, the value after the edit if the edit is valid, and several other bits of information. To use these, though, you need to do a little voodoo to get this information passed to your validate command.
Note: it's important that the validation command returns either True or False. Anything else will cause the validation to be turned off for the widget.
Here's an example that only allows lowercase. It also prints the values of all of the special values for illustrative purposes. They aren't all necessary; you rarely need more than one or two.
import tkinter as tk # python 3.x
# import Tkinter as tk # python 2.x
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
# valid percent substitutions (from the Tk entry man page)
# note: you only have to register the ones you need; this
# example registers them all for illustrative purposes
#
# %d = Type of action (1=insert, 0=delete, -1 for others)
# %i = index of char string to be inserted/deleted, or -1
# %P = value of the entry if the edit is allowed
# %s = value of entry prior to editing
# %S = the text string being inserted or deleted, if any
# %v = the type of validation that is currently set
# %V = the type of validation that triggered the callback
# (key, focusin, focusout, forced)
# %W = the tk name of the widget
vcmd = (self.register(self.onValidate),
'%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
self.entry = tk.Entry(self, validate="key", validatecommand=vcmd)
self.text = tk.Text(self, height=10, width=40)
self.entry.pack(side="top", fill="x")
self.text.pack(side="bottom", fill="both", expand=True)
def onValidate(self, d, i, P, s, S, v, V, W):
self.text.delete("1.0", "end")
self.text.insert("end","OnValidate:\n")
self.text.insert("end","d='%s'\n" % d)
self.text.insert("end","i='%s'\n" % i)
self.text.insert("end","P='%s'\n" % P)
self.text.insert("end","s='%s'\n" % s)
self.text.insert("end","S='%s'\n" % S)
self.text.insert("end","v='%s'\n" % v)
self.text.insert("end","V='%s'\n" % V)
self.text.insert("end","W='%s'\n" % W)
# Disallow anything but lowercase letters
if S == S.lower():
return True
else:
self.bell()
return False
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
For more information about what happens under the hood when you call the register method, see Why is calling register() required for tkinter input validation?
For the canonical documentation see the Validation section of the Tcl/Tk Entry man page
After studying and experimenting with Bryan's code, I produced a minimal version of input validation. The following code will put up an Entry box and only accept numeric digits.
from tkinter import *
root = Tk()
def testVal(inStr,acttyp):
if acttyp == '1': #insert
if not inStr.isdigit():
return False
return True
entry = Entry(root, validate="key")
entry['validatecommand'] = (entry.register(testVal),'%P','%d')
entry.pack()
root.mainloop()
Perhaps I should add that I am still learning Python and I will gladly accept any and all comments/suggestions.
Use a Tkinter.StringVar to track the value of the Entry widget. You can validate the value of the StringVar by setting a trace on it.
Here's a short working program that accepts only valid floats in the Entry widget.
try:
from tkinter import *
except ImportError:
from Tkinter import * # Python 2
root = Tk()
sv = StringVar()
def validate_float(var):
new_value = var.get()
try:
new_value == '' or float(new_value)
validate_float.old_value = new_value
except:
var.set(validate_float.old_value)
validate_float.old_value = '' # Define function attribute.
# trace wants a callback with nearly useless parameters, fixing with lambda.
sv.trace('w', lambda nm, idx, mode, var=sv: validate_float(var))
ent = Entry(root, textvariable=sv)
ent.pack()
ent.focus_set()
root.mainloop()
Bryan's answer is correct, however no one mentioned the 'invalidcommand' attribute of the tkinter widget.
A good explanation is here:
http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/entry-validation.html
Text copy/pasted in case of broken link
The Entry widget also supports an invalidcommand option that specifies a callback function that is called whenever the validatecommand returns False. This command may modify the text in the widget by using the .set() method on the widget's associated textvariable. Setting up this option works the same as setting up the validatecommand. You must use the .register() method to wrap your Python function; this method returns the name of the wrapped function as a string. Then you will pass as the value of the invalidcommand option either that string, or as the first element of a tuple containing substitution codes.
Note:
There is only one thing that I cannot figure out how to do: If you add validation to an entry, and the user selects a portion of the text and types a new value, there is no way to capture the original value and reset the entry. Here's an example
Entry is designed to only accept integers by implementing 'validatecommand'
User enters 1234567
User selects '345' and presses 'j'. This is registered as two actions: deletion of '345', and insertion of 'j'. Tkinter ignores the deletion and acts only on the insertion of 'j'. 'validatecommand' returns False, and the values passed to the 'invalidcommand' function are as follows: %d=1, %i=2, %P=12j67, %s=1267, %S=j
If the code does not implement an 'invalidcommand' function, the 'validatecommand' function will reject the 'j' and the result will be 1267. If the code does implement an 'invalidcommand' function, there is no way to recover the original 1234567.
Define a function returning a boolean that indicates whether the input is valid.Register it as a Tcl callback, and pass the callback name to the widget as a validatecommand.
For example:
import tkinter as tk
def validator(P):
"""Validates the input.
Args:
P (int): the value the text would have after the change.
Returns:
bool: True if the input is digit-only or empty, and False otherwise.
"""
return P.isdigit() or P == ""
root = tk.Tk()
entry = tk.Entry(root)
entry.configure(
validate="key",
validatecommand=(
root.register(validator),
"%P",
),
)
entry.grid()
root.mainloop()
Reference.
While studying Bryan Oakley's answer, something told me that a far more general solution could be developed. The following example introduces a mode enumeration, a type dictionary, and a setup function for validation purposes. See line 48 for example usage and a demonstration of its simplicity.
#! /usr/bin/env python3
# https://stackoverflow.com/questions/4140437
import enum
import inspect
import tkinter
from tkinter.constants import *
Mode = enum.Enum('Mode', 'none key focus focusin focusout all')
CAST = dict(d=int, i=int, P=str, s=str, S=str,
v=Mode.__getitem__, V=Mode.__getitem__, W=str)
def on_validate(widget, mode, validator):
# http://www.tcl.tk/man/tcl/TkCmd/ttk_entry.htm#M39
if mode not in Mode:
raise ValueError('mode not recognized')
parameters = inspect.signature(validator).parameters
if not set(parameters).issubset(CAST):
raise ValueError('validator arguments not recognized')
casts = tuple(map(CAST.__getitem__, parameters))
widget.configure(validate=mode.name, validatecommand=[widget.register(
lambda *args: bool(validator(*(cast(arg) for cast, arg in zip(
casts, args)))))]+['%' + parameter for parameter in parameters])
class Example(tkinter.Frame):
#classmethod
def main(cls):
tkinter.NoDefaultRoot()
root = tkinter.Tk()
root.title('Validation Example')
cls(root).grid(sticky=NSEW)
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(0, weight=1)
root.mainloop()
def __init__(self, master, **kw):
super().__init__(master, **kw)
self.entry = tkinter.Entry(self)
self.text = tkinter.Text(self, height=15, width=50,
wrap=WORD, state=DISABLED)
self.entry.grid(row=0, column=0, sticky=NSEW)
self.text.grid(row=1, column=0, sticky=NSEW)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
on_validate(self.entry, Mode.key, self.validator)
def validator(self, d, i, P, s, S, v, V, W):
self.text['state'] = NORMAL
self.text.delete(1.0, END)
self.text.insert(END, 'd = {!r}\ni = {!r}\nP = {!r}\ns = {!r}\n'
'S = {!r}\nv = {!r}\nV = {!r}\nW = {!r}'
.format(d, i, P, s, S, v, V, W))
self.text['state'] = DISABLED
return not S.isupper()
if __name__ == '__main__':
Example.main()
import tkinter
tk=tkinter.Tk()
def only_numeric_input(e):
#this is allowing all numeric input
if e.isdigit():
return True
#this will allow backspace to work
elif e=="":
return True
else:
return False
#this will make the entry widget on root window
e1=tkinter.Entry(tk)
#arranging entry widget on screen
e1.grid(row=0,column=0)
c=tk.register(only_numeric_input)
e1.configure(validate="key",validatecommand=(c,'%P'))
tk.mainloop()
#very usefull for making app like calci
Here's an improved version of #Steven Rumbalski's answer of validating the Entry widgets value by tracing changes to a StringVar — which I have already debugged and improved to some degree by editing it in place.
The version below puts everything into a StringVar subclass to encapsulates what's going on better and, more importantly allow multiple independent instances of it to exist at the same time without interfering with each other — a potential problem with his implementation because it utilizes function attributes instead of instance attributes, which are essentially the same thing as global variables and can lead to problems in such a scenario.
try:
from tkinter import *
except ImportError:
from Tkinter import * # Python 2
class ValidateFloatVar(StringVar):
"""StringVar subclass that only allows valid float values to be put in it."""
def __init__(self, master=None, value=None, name=None):
StringVar.__init__(self, master, value, name)
self._old_value = self.get()
self.trace('w', self._validate)
def _validate(self, *_):
new_value = self.get()
try:
new_value == '' or float(new_value)
self._old_value = new_value
except ValueError:
StringVar.set(self, self._old_value)
root = Tk()
ent = Entry(root, textvariable=ValidateFloatVar(value=42.0))
ent.pack()
ent.focus_set()
ent.icursor(END)
root.mainloop()
This code can help if you want to set both just digits and max characters.
from tkinter import *
root = Tk()
def validate(P):
if len(P) == 0 or len(P) <= 10 and P.isdigit(): # 10 characters
return True
else:
return False
ent = Entry(root, validate="key", validatecommand=(root.register(validate), '%P'))
ent.pack()
root.mainloop()
Responding to orionrobert's problem of dealing with simple validation upon substitutions of text through selection, instead of separate deletions or insertions:
A substitution of selected text is processed as a deletion followed by an insertion. This may lead to problems, for example, when the deletion should move the cursor to the left, while a substitution should move the cursor to the right. Fortunately, these two processes are executed immediately after one another.
Hence, we can differentiate between a deletion by itself and a deletion directly followed by an insertion due to a substitution because the latter has does not change the idle flag between deletion and insertion.
This is exploited using a substitutionFlag and a Widget.after_idle().
after_idle() executes the lambda-function at the end of the event queue:
class ValidatedEntry(Entry):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tclValidate = (self.register(self.validate), '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W')
# attach the registered validation function to this spinbox
self.config(validate = "all", validatecommand = self.tclValidate)
def validate(self, type, index, result, prior, indelText, currentValidationMode, reason, widgetName):
if typeOfAction == "0":
# set a flag that can be checked by the insertion validation for being part of the substitution
self.substitutionFlag = True
# store desired data
self.priorBeforeDeletion = prior
self.indexBeforeDeletion = index
# reset the flag after idle
self.after_idle(lambda: setattr(self, "substitutionFlag", False))
# normal deletion validation
pass
elif typeOfAction == "1":
# if this is a substitution, everything is shifted left by a deletion, so undo this by using the previous prior
if self.substitutionFlag:
# restore desired data to what it was during validation of the deletion
prior = self.priorBeforeDeletion
index = self.indexBeforeDeletion
# optional (often not required) additional behavior upon substitution
pass
else:
# normal insertion validation
pass
return True
Of course, after a substitution, while validating the deletion part, one still won’t know whether an insert will follow.
Luckily however, with:
.set(),
.icursor(),
.index(SEL_FIRST),
.index(SEL_LAST),
.index(INSERT),
we can achieve most desired behavior retrospectively (since the combination of our new substitutionFlag with an insertion is a new unique and final event.

tkinter GUI design: managing variables from multiple widgets/toolbars

{Edit: the answer by Bryan Oakley in the suggested duplicate question enter link description here a) fires a response on change to the array variable (arrayvar.trace mode="w"), and I need it triggered on FocusOut, as described in my original question; b) works for Python 2, but I'm having trouble converting it to work in Python 3.5. I'm currently using his and pyfunc's answers as leads and trying to figure out a similar solution using a FocusOut event.}
I am working on a tkinter GUI that lets a user select a particular type of calculation, using a pair of radio button lists. Based on the selections, a tool bar is populated with multiple modular entry widgets, one for each variable the calculation requires. The goal is to have the numerical entry values passed to the model, which will return data to be graphed on a canvas or matplotlib widget.
My question is: what typical strategy is used for gathering and continually refreshing values from multiple widgets, in order to update displays and to pass them on to the model? The trick here is that there will be a large number of possible calculation types, each with their own toolbar. I'd like the active toolbar to be "aware" of its contents, and ping the model on every change to a widget entry.
I think the widgets and the toolbar would have to be classes, where the toolbar can query each widget for a fresh copy of its entry values when a change is detected, and store them as some collection that is passed to the model. I'm not entirely sure how it can track changes to the widgets. Using a "validate='focusout' " validation on the entry widget (e.g. as in
this validation reference )
suggests itself, but I already use "validate='key' " to limit all entries to numbers. I don't want to use "validate=all" and piggyback onto it because I don't want to continually ask the model to do a lengthy calculation on every keypress.
I'm new to GUI programming, however, so I may be barking up the wrong tree. I'm sure there must be a standard design pattern to address this, but I haven't found it.
Below is a screenshot of a mockup to illustrate what I want the GUI to do. The Task radiobutton controls which secondary button menu appears below. The selection in the second menu populates the top toolbar with the necessary entry widgets.
The following code does (mostly) what I want. The ToolBar frame objects will store the values from its contained widgets, and call the appropriate model as needed. The VarBox objects are Entry widgets with extra functionality. Hitting Tab or Return refreshes the data stored in the ToolBar dictionary, tells the ToolBar to send data to the model, and shifts focus to the next VarBox widget.
from tkinter import *
# Actual model would be imported. "Dummy" model for testing below.
def dummy_model(dic):
"""
A "dummy" model for testing the ability for a toolbar to ping the model.
Argument:
-dic: a dictionary whose values are numbers.
Result:
-prints the sum of dic's values.
"""
total = 0
for value in dic.values():
total += value
print('The total of the entries is: ', total)
class ToolBar(Frame):
"""
A frame object that contains entry widgets, a dictionary of
their current contents, and a function to call the appropriate model.
"""
def __init__(self, parent=None, **options):
Frame.__init__(self, parent, **options)
self.vars = {}
def call_model(self):
print('Sending to dummy_model: ', self.vars)
dummy_model(self.vars)
class VarBox(Frame):
"""
A customized Frame containing a numerical entry box
Arguments:
-name: Name of the variable; appears above the entry box
-default: default value in entry
"""
def __init__(self, parent=None, name='', default=0.00, **options):
Frame.__init__(self, parent, relief=RIDGE, borderwidth=1, **options)
Label(self, text=name).pack(side=TOP)
self.widgetName = name # will be key in dictionary
# Entries will be limited to numerical
ent = Entry(self, validate='key') # check for number on keypress
ent.pack(side=TOP, fill=X)
self.value = StringVar()
ent.config(textvariable=self.value)
self.value.set(str(default))
ent.bind('<Return>', lambda event: self.to_dict(event))
ent.bind('<FocusOut>', lambda event: self.to_dict(event))
# check on each keypress if new result will be a number
ent['validatecommand'] = (self.register(self.is_number), '%P')
# sound 'bell' if bad keypress
ent['invalidcommand'] = 'bell'
#staticmethod
def is_number(entry):
"""
tests to see if entry is acceptable (either empty, or able to be
converted to a float.)
"""
if not entry:
return True # Empty string: OK if entire entry deleted
try:
float(entry)
return True
except ValueError:
return False
def to_dict(self, event):
"""
On event: Records widget's status to the container's dictionary of
values, fills the entry with 0.00 if it was empty, tells the container
to send data to the model, and shifts focus to the next entry box (after
Return or Tab).
"""
if not self.value.get(): # if entry left blank,
self.value.set(0.00) # fill it with zero
# Add the widget's status to the container's dictionary
self.master.vars[self.widgetName] = float(self.value.get())
self.master.call_model()
event.widget.tk_focusNext().focus()
root = Tk() # create app window
BarParentFrame = ToolBar(root) # holds individual toolbar frames
BarParentFrame.pack(side=TOP)
BarParentFrame.widgetName = 'BarParentFrame'
# Pad out rest of window for visual effect
SpaceFiller = Canvas(root, width=800, height=600, bg='beige')
SpaceFiller.pack(expand=YES, fill=BOTH)
Label(BarParentFrame, text='placeholder').pack(expand=NO, fill=X)
A = VarBox(BarParentFrame, name='A', default=5.00)
A.pack(side=LEFT)
B = VarBox(BarParentFrame, name='B', default=3.00)
B.pack(side=LEFT)
root.mainloop()

Resources