Unable to call function correctly with for loop [duplicate] - for-loop

I am trying to create buttons in tkinter within a for loop. And with each loop pass the i count value out as an argument in the command value. So when the function is called from the command value I can tell which button was pressed and act accordingly.
The problem is, say the length is 3, it will create 3 buttons with titles Game 1 through Game 3 but when any of the buttons are pressed the printed value is always 2, the last iteration. So it appears the buttons are being made as separate entities, but the i value in the command arguments seem to be all the same. Here is the code:
def createGameURLs(self):
self.button = []
for i in range(3):
self.button.append(Button(self, text='Game '+str(i+1),
command=lambda: self.open_this(i)))
self.button[i].grid(column=4, row=i+1, sticky=W)
def open_this(self, myNum):
print(myNum)
Is there a way to get the current i value, each iteration, to stick with that particular button?
This problem can be considered a special case of Creating functions in a loop. There's also What do lambda function closures capture?, for a more technical overview.
See also How to pass arguments to a Button command in Tkinter? for the general problem of passing arguments to Button callbacks.

Change your lambda to lambda i=i: self.open_this(i).
This may look magical, but here's what's happening. When you use that lambda to define your function, the open_this call doesn't get the value of the variable i at the time you define the function. Instead, it makes a closure, which is sort of like a note to itself saying "I should look for what the value of the variable i is at the time that I am called". Of course, the function is called after the loop is over, so at that time i will always be equal to the last value from the loop.
Using the i=i trick causes your function to store the current value of i at the time your lambda is defined, instead of waiting to look up the value of i later.

This is how closures work in python. I ran into this problem myself once.
You could use functools.partial for this.
for i in range(3):
self.button.append(Button(self, text='Game '+str(i+1), command=partial(self.open_this, i)))

Simply attach your buttons scope within a lambda function like this:
btn["command"] = lambda btn=btn: click(btn) where click(btn) is the function that passes in the button itself.
This will create a binding scope from the button to the function itself.
Features:
Customize gridsize
Responsive resizing
Toggle active state
#Python2
#from Tkinter import *
#import Tkinter as tkinter
#Python3
from tkinter import *
import tkinter
root = Tk()
frame=Frame(root)
Grid.rowconfigure(root, 0, weight=1)
Grid.columnconfigure(root, 0, weight=1)
frame.grid(row=0, column=0, sticky=N+S+E+W)
grid=Frame(frame)
grid.grid(sticky=N+S+E+W, column=0, row=7, columnspan=2)
Grid.rowconfigure(frame, 7, weight=1)
Grid.columnconfigure(frame, 0, weight=1)
active="red"
default_color="white"
def main(height=5,width=5):
for x in range(width):
for y in range(height):
btn = tkinter.Button(frame, bg=default_color)
btn.grid(column=x, row=y, sticky=N+S+E+W)
btn["command"] = lambda btn=btn: click(btn)
for x in range(width):
Grid.columnconfigure(frame, x, weight=1)
for y in range(height):
Grid.rowconfigure(frame, y, weight=1)
return frame
def click(button):
if(button["bg"] == active):
button["bg"] = default_color
else:
button["bg"] = active
w= main(10,10)
tkinter.mainloop()

Related

Adding a check next to the selected item in tkinter OptionMenu

How could I add a check sign next to the currently selected item (or highlight it) in a OptionMenu in a tkinter GUI? The idea is that when I click again to select another item, I can see easily which one is selected (similar to the following picture)
I just added a new example:
from tkinter import *
OptionList = [
"Aries",
"Taurus",
"Gemini",
"Cancer"
]
app = Tk()
app.geometry('100x200')
variable = StringVar(app)
variable.set(OptionList[0])
opt = OptionMenu(app, variable, *OptionList)
opt.config(width=90, font=('Helvetica', 12))
opt.pack(side="top")
labelTest = Label(text="", font=('Helvetica', 12), fg='red')
labelTest.pack(side="top")
def callback(*args):
labelTest.configure(text="The selected item is {}".format(variable.get()))
variable.trace("w", callback)
app.mainloop()
Just use ttk widgets for this modern looking style, try saying something like:
from tkinter import ttk
....
#arguments - master variable default *values
opt = ttk.Optionmenu(app, variable, OptionList[0], *OptionList)
The effect given by this is pretty similar or maybe identical to what your trying to achieve.
You might notice an additional third positional argument here, it is actually default=OptionList[0] argument specified here(specific to just ttk.Optionmenu), it is just the default value that the optionmenu will display, ignoring this might lead to some bugs in the looks of optionmenu, like this.
And also keep in mind, it does not have a font option too. To overcome this, check this out
Hope this was of some help to you, do let me know if any errors or doubts.
Cheers
You can get similar effect using tk.OptionMenu:
from tkinter import *
OptionList = [
"Aries",
"Taurus",
"Gemini",
"Cancer"
]
app = Tk()
app.geometry('300x200')
variable = StringVar(app)
variable.set(OptionList[0])
opt = OptionMenu(app, variable, None) # need to supply at least one menu item
opt.config(width=90, font=('Helvetica', 12))
opt.pack(side="top")
# populate the menu items
menu = opt['menu']
menu.delete(0) # remove the None item
for item in OptionList:
menu.add_radiobutton(label=item, variable=variable)
labelTest = Label(text="", font=('Helvetica', 12), fg='red')
labelTest.pack(side="top")
def callback(*args):
labelTest.configure(text="The selected item is {}".format(variable.get()))
variable.trace("w", callback)
app.mainloop()

Some PyQt5 Checkboxes are unclickable after being moved

I'm creating a to-do list where a checkbox, textbox (QLineEdit), and QComboBox (to set Priority 1, 2, etc.) are added each time the "Add Task" button is clicked. When the checkbox for each corresponding task is clicked, the task would move to the bottom of the list of tasks. Conversely, if a priority is set, then the task is moved to the top of the list of tasks.
The problem is that in certain circumstances, when I click on the checkbox, nothing happens. The signal that identifies that the checkbox state has changed is never sent. It's almost like the checkbox isn't there, even though it (or at least, the image of it) is. This problem happens if none of the priorities are set with unchecking—checking any given task works, but certain tasks cannot be unchecked. Other tasks are able to be unchecked, and sometimes if I go back to the task that couldn't be unchecked after unchecking something else, it'll be checkable again. If I were to set some tasks to have a priority and then go and try to check the checkboxes, some checkboxes won't be able to be checked at all. Again, if I were to check some other task and then go back to the task that wasn't able to be checked, it might be able to be checked this time around.
I think the error might be coming from moving the checkboxes... for some reason the functionality behind some of the checkboxes is lost going through the code. One notable thing is tabbing through the items in the GUI. Once items are moved, hitting tab will go through the items in the same order that it was originally set up in (so the cursor will essentially jump around the textboxes instead of going in order from top to bottom). Not sure if that has anything to do with the error; it's just something I've noticed. I'm don't know if this is an error within PyQt5 itself, or if it's a fixable error within the code.
import sys
from PyQt5.QtWidgets import (QWidget, QComboBox,
QPushButton, QApplication, QLabel, QLineEdit, QTextEdit, QMainWindow, QAction, QShortcut, QCheckBox)
from PyQt5.QtGui import (QKeySequence)
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import pyqtSlot, QObject
class App(QMainWindow):
def __init__(self):
super().__init__()
self.i = 40
self.j = 80
self.counter = 1
self.list_eTasks = []
self.initUI()
def initUI(self):
#sets up window
self.setWindowTitle('To-Do List')
self.setGeometry(300, 300, 800, 855)
#button
self.btn1 = QPushButton("Add Task", self)
self.btn1.clicked.connect(self.add_task_button_clicked)
#close the window via keyboard shortcuts
self.exit = QShortcut(QKeySequence("Ctrl+W" or "Ctrl+Q"), self)
self.exit.activated.connect(self.close)
self.show()
#pyqtSlot()
def add_task_button_clicked(self):
self.label = QLabel(self)
self.label.setText(str(self.counter))
self.label.move(5, self.i)
self.btn1.move(50, self.j)
self.textbox = QLineEdit(self)
self.textbox.resize(280, 40)
self.checkbox = QCheckBox(self)
self.checkbox.stateChanged.connect(self.click_box)
self.combobox = QComboBox(self)
self.combobox.addItem("No Priority")
self.combobox.addItem("Priority 1")
self.combobox.addItem("Priority 2")
self.combobox.addItem("Priority 3")
self.combobox.activated[str].connect(self.combobox_changed)
self.obj = eTask(self.checkbox, self.textbox, self.combobox)
self.list_eTasks.append(self.obj)
self.textbox.show()
self.label.show()
self.checkbox.show()
self.combobox.show()
self.i += 40
self.j += 40
self.counter += 1
self.sort_eTasks()
def click_box(self, state):
self.sort_eTasks()
def move_everything(self, new_list):
count = 0
for item in new_list:
y_value = ((40)*(count + 1))
item.check.move(20, y_value)
item.text.move(50, y_value)
item.combo.move(350, y_value)
count += 1
return
def combobox_changed(self, state):
#make new list to not include any already checked off items
for task in self.list_eTasks:
if task.combo.currentText() == "Priority 1":
task.priority = 1
elif task.combo.currentText() == "Priority 2":
task.priority = 2
elif task.combo.currentText() == "Priority 3":
task.priority = 3
else:
task.priority = 10
self.sort_eTasks()
def sort_eTasks(self):
self.list_eTasks.sort(key=lambda eTask: eTask.getRank())
self.move_everything(self.list_eTasks)
for task in self.list_eTasks:
print(str(task.priority) + " pos: " + str(task.check.pos()))
print("-----------------------------------")
class eTask:
check = ""
text = ""
combo = ""
priority = 10
def __init__(self, c, t, co):
self.check = c
self.text = t
self.combo = co
def setP(self, p):
self.priority = p
def getRank(self):
if self.check.isChecked():
return self.priority + 100
else:
return self.priority
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
There are various problems with your code, I'll try to address all of them.
The sorting mechanism is not good: it only happens when an item changes the priority (while it should also apply whenever the user clicks the checkbox)
Using low values like those results in unexpected behavior: if I have a priority 1 checked its priority is augmented by 10, so why should it go under a priority 2 or even 3?
The sorting mechanism should be centralized, instead of being split into more functions that don't do anything else: "one function to rule them all" ;-) Don't check for the combobox contents and then sort basing on another external function call: just call a single function that checks for the combobox and the checkbox, then sort the contents; remember: while OOP is about modularity, you should avoid unnecessary fragmentation (remember the KISS principle);
You should probably not connect to the activated signal, and you should certainly not use the [str] overload (checking for a string is rarely a good idea for index based widgets); use currentIndex() and its currentIndexChanged() signal instead;
Using fixed geometries for child widgets is something that should be avoided as much as possible; layout managers exist for lots of very good reasons (for example, I couldn't click on the QLineEdit if the mouse cursor was too close to the check box, even if it's over the line edit); note that you should avoid using setGeometry for the top level window also;
You should never set instance attributes that are going to be overwritten by a different object: you're continuously overwriting them (self.label, self.textbox, etc.), and the result is that making them instance attributes is completely useless; since you're already creating them with a parent widget, the garbage collector won't delete them at the end of the function, so you should only use local variables instead (label = QLabel(), etc...);
Using a class like you did is not very useful: you're going to group all those widgets anyway, so it's better to use a container widget which not only will manage its children (see the point above about layouts), but will "centralize" all necessary programming logic;
Avoid similar names for class and variables if they refer to different object types: in your case, app refers to the QApplication instance, but App is a QMainWidget subclass;
Here's a possible reimplementation of your code:
from PyQt5 import QtCore, QtWidgets
class TaskWidget(QtWidgets.QWidget):
priorityChanged = QtCore.pyqtSignal()
lastChanged = QtCore.QDateTime.currentDateTime()
def __init__(self, title='', parent=None):
super().__init__(parent)
layout = QtWidgets.QHBoxLayout(self)
self.checkBox = QtWidgets.QCheckBox()
layout.addWidget(self.checkBox)
self.textBox = QtWidgets.QLineEdit(title)
layout.addWidget(self.textBox)
self.textBox.setMinimumWidth(240)
self.priorityCombo = QtWidgets.QComboBox()
layout.addWidget(self.priorityCombo)
self.priorityCombo.addItems([
'No priority',
'Priority 1',
'Priority 2',
'Priority 3',
])
self.priorityCombo.currentIndexChanged.connect(self.updatePriority)
self.checkBox.toggled.connect(self.updatePriority)
def updatePriority(self):
# update the "lastChanged" variable, ensuring that the sorting puts on
# top the last changed task
self.lastChanged = QtCore.QDateTime.currentDateTime()
self.priorityChanged.emit()
def priority(self):
priority = self.priorityCombo.currentIndex()
# set an arbitrary (but still reasonable) priority that would be fine for
# more items in the same priority
if priority > 0:
priority *= 100
else:
priority = self.priorityCombo.count() * 100
if self.checkBox.isChecked():
priority -= 1
return priority, self.lastChanged.msecsTo(QtCore.QDateTime.currentDateTime())
class TaskApp(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(central)
self.setCentralWidget(central)
self.taskLayout = QtWidgets.QGridLayout()
layout.addLayout(self.taskLayout)
self.addTaskButton = QtWidgets.QPushButton('Add task')
layout.addWidget(self.addTaskButton)
self.addTaskButton.clicked.connect(self.addTask)
self.tasks = []
self.addTask()
def addTask(self):
task = TaskWidget('Task no. {}'.format(len(self.tasks) + 1))
if not self.taskLayout.count():
# this is for the first item only; we cannot use rowCount, since it
# always returns at least 1, even if it's empty
row = 0
else:
row = self.taskLayout.rowCount()
self.taskLayout.addWidget(QtWidgets.QLabel(str(row + 1)))
self.taskLayout.addWidget(task, row, 1)
self.tasks.append(task)
task.priorityChanged.connect(self.sortTasks)
def sortTasks(self):
tasks = self.tasks[:]
self.tasks.clear()
# since we're using a QGridLayout, we don't need to relate to
# insertWidget(), which is index based and might result in some
# inconsistencies while "regenerating" the UI; in this case, the
# grid will be the same, we're only going to rearrange the child widgets
for row, taskWidget in enumerate(sorted(tasks, key=lambda w: w.priority())):
self.taskLayout.addWidget(taskWidget, row, 1)
self.tasks.append(taskWidget)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = TaskApp()
w.show()
sys.exit(app.exec_())
I just guessed the priority implementation, but that's not the point.
I also added a datetime check, so that the last changed item with the same priority will always be on top.

My toplevel window in tkinter is no longer being destroyed. It was working fine until I tried changing other aspects of my function

I'm trying to get a popup window to display random text and a picture every time a button is pressed in tkinter. My original code was going to use an if/elif statement to do this. It worked as intended but I thought it might be easier to pair the data in a dictionary since there would be 50 elif statements otherwise (is it frowned upon to use so many? I actually found it easier to read).I was able to get this working but now the toplevel window in tkinter is not being destroyed like it was in the original function. A new Label is just being created on top of it and I can't figure out why. The function code is below. Thanks in advance, any help would be appreciated!
def Add_Gemstone2():
global Addstone
#destroy the previous window if there is one.
try:
AddStone.destroy()
except(AttributeError, NameError):
pass
#create the window.
AddStone=Toplevel()
AddStone.configure(bg='White', height=200, width=325)
AddStone.geometry('325x180+10+100')
# add gemstones to list from file.
gem_stones = open('gemstones.txt')
all_gem_stones = gem_stones.readlines()
gemstones = []
for i in all_gem_stones:
gemstones.append(i.rstrip())
# Add pictures to list.
path = r'C:\Users\Slack\Desktop\PYTHON WORKS\PYTHON GUI PROJECT\gems'
gempictures = []
# r=root, d=directories, f = files
for r,d,f in os.walk(path):
for file in f:
if '.gif' in file:
gempictures.append(os.path.join(r, file))
#create dictionary from lists.
gemdiction = dict(zip(gemstones, gempictures))
key, val = random.choice(list(gemdiction.items()))
# create the labels.
glbl1 = Label(AddStone, text=key, bg='gold', wraplength=300)
glbl1.pack()
image = ImageTk.PhotoImage(Image.open(val))
glbl2 = Label(AddStone, image=image)
glbl2.image = image
glbl2.pack()

wxpython using time.sleep() without blocking complete GUI

I want to change the text displayed in my GUI at specific time intervals. After a lot of approaches, I find that, specifically to my requirements, I must use time.sleep() instead of wx.Timer, but time.sleep() freeze the complete GUI. Here's an example of my code:
import wx
import time
DWELL_TIMES = [1, 2, 1, 3]
SCREEN_STRINGS = ['nudge nudge', 'wink wink', 'I bet she does', 'say no more!']
class DM1(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
panel = wx.Panel(self)
text_display = wx.StaticText(panel, pos = (400, 150))
for dwell_time in DWELL_TIMES:
text_display.SetLabel(SCREEN_STRINGS[dwell_time])
time.sleep(float(DWELL_TIMES[dwell_time]))
app = wx.App()
DM1Frame = DM1(None, size = (800, 600))
DM1Frame.Center()
DM1Frame.Show()
app.MainLoop()
Does somebody know why this happen, and how to make the GUI doesn't block?
I guess that Threading could help me, doesn't it? If it does, which is the correct way to put threads inside this code? Is there an alternative to Threading?
Thanks a lot!
As mentioned by others, wx.CallAfter and wx.CallLater are your friends. Study them and learn them. Here is a complete, working example using wx.CallLater. I included other refactoring as I saw fit.
import wx
DATA = [
(1, 'nudge nudge'),
(2, 'wink wink'),
(1, 'I bet she does'),
(3, 'say no more!'),
]
class Frame(wx.Frame):
def __init__(self):
super(Frame, self).__init__(None)
panel = wx.Panel(self)
self.text = wx.StaticText(panel)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.AddStretchSpacer(1)
sizer.Add(self.text, 0, wx.ALIGN_CENTER)
sizer.AddStretchSpacer(1)
panel.SetSizer(sizer)
self.index = 0
self.update()
def update(self):
duration, label = DATA[self.index]
self.text.SetLabel(label)
self.index = (self.index + 1) % len(DATA)
wx.CallLater(int(duration * 1000), self.update)
if __name__ == '__main__':
app = wx.App(None)
frame = Frame()
frame.SetTitle('Example')
frame.SetSize((400, 300))
frame.Center()
frame.Show()
app.MainLoop()
If you look at the documentation for time.sleep(), you see that it basically blocks execution of that thread for the specified interval. The problem is that currently your GUI has only a single thread, so if you block the thread then you block ALL execution in that thread. This means, as you've experienced, that the GUI is unusable during the sleep.
Even using threading, the time.sleep() call can't be in the same thread as the GUI, thus trying to get your GUI to refresh after the sleep is over will be very complicated. Beyond that, it's basically reimplementing wx.Timer! No use redoing something that's already been done for you.
It seems to me that your question should be less "how do I make sleeps work?" and more "Why isn't wx.Timer working properly?" Please explain the problem you're having with wx.Timer in detail. Why won't it work? Maybe post some code. My guess is you probably aren't binding the wx.EVT_TIMER properly. Take a look at this tutorial.
Which is the correct way to put threads inside this code?
Although using wx.Timer is the correct solution to this simplified example, if your real goal is to know how to use a worker thread to do long tasks and give updates to your main GUI without freezing your whole application, here's how:
import wx
import threading
import time
class WorkerThread(threading.Thread):
DWELL_TIMES = [1, 2, 1, 3]
SCREEN_STRINGS = ['nudge nudge', 'wink wink', 'I bet she does', 'say no more!']
def __init__(self, window):
threading.Thread.__init__(self)
self.window = window
def run(self):
for i in range(len(WorkerThread.DWELL_TIMES)):
wx.CallAfter(self.window.set_text, WorkerThread.SCREEN_STRINGS[i])
time.sleep(float(WorkerThread.DWELL_TIMES[i]))
wx.CallAfter(self.window.close)
class DM1(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
panel = wx.Panel(self)
self.text_display = wx.StaticText(panel, pos = (400, 150))
self.kickoff_work()
def kickoff_work(self):
t = WorkerThread(self)
t.start()
def set_text(self, text):
self.text_display.SetLabel(text)
def close(self):
self.Close()
app = wx.App()
DM1Frame = DM1(None, size = (800, 600))
DM1Frame.Center()
DM1Frame.Show()
app.MainLoop()
You might try making a global variable that gets the time when it first starts, then having a second variable get the current time and see if the two times are far enough apart to work. Something like this:
When the text changes to something new,
global timestart
timestart = gettime()
Then, where you check if you are changing the code,
timestop = gettime()
if timestop - timestart >= timebetweenchanges:
change code
I don't understand why you can't use a timer for this. They seem to be made for the exact purpose you need them for. As acattle mentioned already, I wrote a tutorial on the subject.
He is completely right though. Using time.sleep() will freeze the GUI because it blocks wx's main event loop. If you absolutely HAVE to use time.sleep() (which I doubt), then you can use a thread. I wrote a tutorial on that subject too. In fact, I actually use time.sleep() in that example.
I might suggest you go use wx.CallLater. Refer to official doc: http://wxpython.org/docs/api/wx.CallLater-class.html
A convenience class for wx.Timer, that calls the given callable object
once after the given amount of milliseconds, passing any positional or
keyword args. The return value of the callable is availbale after it
has been run with the GetResult method.
If you don't need to get the return value or restart the timer then
there is no need to hold a reference to this object. It will hold a
reference to itself while the timer is running (the timer has a
reference to self.Notify) but the cycle will be broken when the timer
completes, automatically cleaning up the wx.CallLater object.
Possible further reference can be found in this question: Using wx.CallLater in wxPython

wxPython ListCtrl OnClick Event

So, I have a wxPython ListCtrl which contains rows of data. How can I make an event that calls a function, with the row contents, when one of the rows if clicked on?
You can use the Bind function to bind a method to an event. For example,
import wx
class MainWidget(wx.Frame):
def __init__(self, parent, title):
super(MainWidget, self).__init__(parent, title=title)
self.list = wx.ListCtrl(parent=self)
for i,j in enumerate('abcdef'):
self.list.InsertStringItem(i,j)
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnClick, self.list)
self.Layout()
def OnClick(self, event):
print event.GetText()
if __name__ == '__main__':
app = wx.App(redirect=False)
frame = MainWidget(None, "ListCtrl Test")
frame.Show(True)
app.MainLoop()
This app will print the item in the ListCtrl that is activated (by pressing enter or double-clicking). If you just want to catch a single click event, you could use wx.EVT_LIST_ITEM_SELECTED.
The important point is that the Bind function specifies the method to be called when a particular event happens. See the section in the wxPython Getting Started guide on event handling. Also see the docs on ListCtrl for the events that widget uses.

Resources