wxPython listctrl insertitem and SetItem all at once - python-3.x

I have a listctrl,
self.list = wx.ListCtrl(panel, -1, style=wx.LC_REPORT | wx.LC_NO_HEADER)
self.list.InsertColumn(col=0, heading='', format=wx.LIST_FORMAT_CENTER, width=150)
self.list.InsertColumn(col=1, heading='', format=wx.LIST_FORMAT_CENTER, width=450)
for person in people:
#this is the issue right here
index = self.list.InsertItem(0, person.age) #one line to insert an item and get the index, and the next line to update the item at that index... two lines to actually put a full entry in.
self.list.SetItem(index=index, column=1, label=person.name)
This works fine setting up the listctrl initially in the constructor, but what if I want to dynamically add/remove items in the listctrl at runtime?
I've come across wx.CallAfter, wx.CallLater, startWorker (from wx.lib.delayedresult), and wx.Timer
If you look at the example above the issue is that I've got one line that inserts the item, and another line that updates the item to have the correct name on the item that was just inserted. So if I've got threads that are taking turns removing and adding items to the listctrl, if I insert an item and another thread inserts an item at the same time, the index that I just got won't be relevant for updating. i.e. I need an atomic operation for inserting an item that includes inserting both the person's age and the person's name. So my first question is, is there a way to insert all information of a list item in one line?
If I cannot do that, then my next question is how could I accomplish the prescribed behavior? For example, suppose there are threads randomly coloring the top row, adding, and deleting:
self.color_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.item_colorizer, self.color_timer)
self.red_shown = True
self.color_timer.Start(500)
self.delete_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.item_deleter, self.delete_timer)
self.delete_timer.Start(500)
self.adder_timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.item_queuer, self.adder_timer)
self.adder_timer.Start(400)
Then here are my methods that are used to add people, delete people, and color the top row:
def item_queuer(self, event):
startWorker(consumer=self.item_adder,
workerFn=self.person_generator)
def person_generator(self):
return {'person':random.choice(people)}
def item_adder(self, result):
res = result.get()
person = res['person']
wx.CallAfter(self.list.InsertItem, 0, person.name) # this is the problem right here. If another thread does a color swap in between this line and the next, then this won't work.
wx.CallAfter(self.list.SetItem, index=0, column=1, label=person.name)
def item_deleter(self, event):
wx.CallAfter(self.list.DeleteItem, 0)
def item_colorizer(self, event):
if self.color_timer.IsRunning():
if not self.red_shown:
wx.CallAfter(self.list.SetItemBackgroundColour, 0, wx.RED)
wx.CallAfter(self.list.SetItemTextColour, 0, wx.WHITE)
self.red_shown = True
else:
wx.CallAfter(self.list.SetItemBackgroundColour, 0, wx.BLUE)
wx.CallAfter(self.list.SetItemTextColour, 0, wx.BLACK)
self.red_shown = False
What actually happens when I run this is that I end up having rows where the person is partially inserted (just the age), and the color changes before the name is inserted. I've noticed that the InsertItem method on the listctrl is overloaded and offers one signature where I can insert a ListItem, but I cannot get that to work either.
item1 = wx.ListItem()
item1.SetBackgroundColour(wx.GREEN)
item1.SetColumn(0)
item1.SetText(32)
item1.SetColumn(1)
item1.SetText('John')
self.list.InsertItem(item1)
wx._core.wxAssertionError: C++ assertion "rv != -1" failed at ....\src\msw\listctrl.cpp(1730) in wxListCtrl::InsertItem(): failed to insert an item in wxListCtrl

Related

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.

Accessing dynamically created Variables set in a for loop

TLDR FIX: put your objects(or list of objects) into a dictionary
Simply put, i am creating a bunch of pages in a wxpython notebook dynamically and on each page i am placing further widgets and sizers and such. my issue is that if i want to access a variable set in the for loop when it creates the page, i am unable to because once the for loops runs again the variable will be reset and used to populate the new page.
something along the lines of this
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
wx.Frame.__init__(self, *args, **kwds)
self.SetSize((400, 300))
self.notebook_1 = wx.Notebook(self, wx.ID_ANY)
self.text_button = wx.Button(self, wx.ID_ANY, "get text")
self.Bind(wx.EVT_BUTTON, self.get_text_handler, self.text_button)
self.__set_properties()
sizer_1 = wx.BoxSizer(wx.VERTICAL)
sizer_2 = wx.BoxSizer(wx.HORIZONTAL)
sizer_1.Add(self.text_button, 0, 0, 0)
sizer_1.Add(self.notebook_1, 1, wx.EXPAND, 0)
self.SetSizer(sizer_1)
self.Layout()
numlist = (1, 2, 3, 4)
#global page_dictionary
#page_dictionary = {}
for i in numlist:
self.notebook_1_pane_2 = wx.Panel(self.notebook_1, wx.ID_ANY)
self.notebook_1_pane_2.SetSizer(sizer_2)
self.notebook_1.AddPage(self.notebook_1_pane_2, str(i))
self.label_1 = wx.TextCtrl(self.notebook_1_pane_2, wx.ID_ANY, str(i))
sizer_2.Add(self.label_1, 0, 0, 0)
self.Layout()
#page_k = self.label_1.GetValue()
#page_k_good = page_k + str(i)
#page_dictionary[page_k_good] = i
#print(page_dictionary)
def __set_properties(self):
self.SetTitle("frame")
def get_text_handler(self, event): # wxGlade: MyFrame.<event_handler>
print(self.label_1.GetValue())
the only difference between this and what im actually running, is that the numlist variable is actually generating a random number of values dynamically
my issue is that if i want to reference any of the labels ive set in previous pages, i can't because the self.label_1 variable gets reset to the entry on the last page. so if i want to access the label on say page number 1 the label variable is set to the values of the last page created in the for loop. its like once the label is placed and populated on the wxNotebook page its just gone and what is in that widget isn't accessible anymore.
so how would i, say for example, print(self.label_1) of page 1 in the notebook when self.label_1 was rewritten to match the label in the last page generated?
was messing around with dictionaries so i commented that out.
how would i be able to click that button, and have it get the text from the boxes in current pages?
Final edit hopefully:
how would i go about assigning multiple objects into the dictionary? would i need to just assign the value i as a list (label_1, Label_2) then when needing to call methods on them just reference them from the list, that is now in the dictionary?
yup, that did it, put them into the dictionary as a list, can pull the list when needed.
I am still at a loss as to exactly what you are trying to do, but based on an educated guess I think you are trying to preserve the identity of objects created in a loop so that they maybe accessed outside of the loop. With this in mind, here is an example of how this can be done. Here is a very simplistic example:
import random as rnd #used to create a random number for each object
# just a list of names to be scanned in for loop
lst = ["a", "b", "c", "d"]
# Just an illustrative object for demo purposes
class SillyClass:
def __init__(self, name, nmb):
self._id = name
self._nmb = nmb
#property
def name(self):
return self._id
#property
def number(self):
return self._nmb
# create a list of objects for later reference
def build_objects(lst):
rslt = []
for itm in lst:
rslt.append(SillyClass(itm, rnd.randrange(5, 200)))
return rslt
# show use of list to print object data
obj_lst = build_objects(lst)
# Show the contents of obj_lst
print(obj_lst, "\n")
# show access of object attributes
for obj in obj_lst:
print(f"Object: {obj.name} contains {obj.number}")
The result of running this code is:
[<__main__.SillyClass object at 0x0000022C5CB9E9D0>, <__main__.SillyClass object at 0x0000022C5CB9EFD0>, <__main__.SillyClass object at 0x0000022C5CB9E670>, <__main__.SillyClass object at 0x0000022C5CC42370>]
Object: a contains 187
Object: b contains 164
Object: c contains 97
Object: d contains 52
This illustrates that obj_lst contains a list of objects, which then can be used to access the attributes of the object. You can utilize this approach or possibly associate the object to a specific identifier using a dict structure. What ever you need to best serve your purposes.

Return old value to combobox with dynamic search and autocompletion

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).

tkinker optionmenu not showing chosen result

import tkinter
window = tkinter.Tk()
def abc(event):
ans=0
numberss=['7','8','9']
omenu2['menu'].delete(0, 'end')
for number in numberss:
omenu2['menu'].add_command(label=numberss[ans], command=efg)
ans=ans+1
def efg(event=None):
print('yee')
numbers = ['1','2', '3']
number=['4','5','6']
var = tkinter.StringVar(window)
var1 = tkinter.StringVar(window)
omenu = tkinter.OptionMenu(window, var, *numbers, command = abc)
omenu.grid(row=1)
omenu2 = tkinter.OptionMenu(window, var1, *number, command = efg)
omenu2.grid(row=2)
after you have entered the first option menu, it will update the second one. when you enter data into the second one, it runs the command, but doesn't show you what you entered. i do not want to include a button, and i know that the command works and not on the second
i found some code that changed the options of the second menu, however when i ran this, the command wouldn't work as it was changed to tkinter.setit (i would also like to know what is does. i do not currently understand it)
omenu2['menu'].add_command(label=numberss[ans], command=tkinter._setit(var1, number))
this has been taken from a larger piece of code, and has thrown the same error
You should set your StringVar(var1) new value.
def abc(event):
numberss=['7','8','9']
omenu2['menu'].delete(0, 'end')
for number in numberss:
omenu2['menu'].add_command(label=number, command=lambda val=number: efg(val))
def efg(val, event=None):
print('yee')
var1.set(val)
You are using for loop so you don't need ans(at least not in this code) since it iterates over items themselves.

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