Tkinter: configure multiple widgets simultaneously - python

I want to set the background of multiple widgets in a row at once. However, in the example you can see that sometimes the colorization is ugly: Widgets change color in separate times. How to update the appearance (configuration) of multiple widgets simultaneously? Virtual events would solve this issue?
import tkinter
class Application(tkinter.Tk):
TITLE = 'Simultaneous Widget Blink Test'
BLINK_INT = 75
def __init__(self, rows=4, cols=10):
super().__init__()
self.rows, self.cols = rows, cols
self.frames = [[] for _ in range(rows)]
self.labels = [[] for _ in range(rows)]
self.withdraw() # assembling in background...
self.resizable(width=False, height=False)
self.title(self.TITLE)
self.build()
for ri in range(self.rows):
self.curr_ri = ri
self.set_row_style()
self.curr_ri, self.curr_n = 0, 0
self.deiconify() # showing it
self.blink_again()
def build(self):
for ri in range(self.rows):
for ci in range(self.cols):
f = tkinter.Frame(self, width=40, height=40)
self.frames[ri].append(f)
f.grid(row=ri, column=ci, sticky='news')
l = tkinter.Label(f, text='{}{}'.format(ri, ci))
self.labels[ri].append(l)
l.place(relx=0.5, rely=0.5, anchor='c')
def blink_again(self):
if self.curr_n == 10:
self.curr_n = 0
self.curr_ri += 1
if self.curr_ri == self.rows:
self.curr_ri = 0
self.curr_n += 1
self.highlight_row()
def highlight_row(self):
self.set_row_style(bg='orange')
self.after(self.BLINK_INT, self.restore_row)
def restore_row(self):
self.set_row_style()
self.after(self.BLINK_INT, self.blink_again)
def set_row_style(self, bg='white'):
for w in self.frames[self.curr_ri]:
w.configure(bg=bg)
for w in self.labels[self.curr_ri]:
w.configure(bg=bg)
if __name__ == '__main__':
app = Application()
app.mainloop()
Thanks in advance!
EDIT: More simplified example code.

The way you are updating the widgets is the proper way to modify multiple widgets at once. As long as you don't have any calls to update or update_idletask in the loop that modifies all of the widgets, the modifications should all appear simultaneously the next time there is a refresh on the screen (ie: when your function returns control to the event loop).
When I run your code I see no flickering. It all behaves as I expect.

Related

tkinter: clear inheritanced widget

how to remove the page1's widgets when the next button is pressed, so that only the page2's widgets is shown.
and vice versa if the back button is pressed on page 2, so the widgets don't overlap
from tkinter import *
class Buttons(Button):
def __init__(self,master,**kwargs):
super().__init__(master=master,**kwargs)
self.look = {"fg":"ghost white","bg":"DarkBlue"}
self.config(self.look)
def makeButton(self,name,texts,wide,rows,cols,com):
self.name = name
self.texts = texts
self.wide = wide
self.rows = rows
self.cols = cols
self.com = com
self.name = Buttons(root,text=self.texts,width=self.wide,command=self.com)
self.name.place(x=self.rows,y=self.cols)
class make(Buttons):
def __init__(self, mainFrame):
super().__init__(mainFrame)
self.main_frame = Frame(mainFrame, width=400, height=300)
self.main_frame.place()
self.page1()
def page1(self):
self.makeButton("name1","Page1-widgets1",15,125,30,None)
self.makeButton("name2","Page1-widgets2",15,125,80,None)
self.makeButton("name3","Next",15,125,130,self.page2)
self.makeButton("name4","Exit",15,125,180,exit)
def page2(self):
self.makeButton("name5","Page2-widgets1",15,135,40,None)
self.makeButton("name6","Page2-widgets2",15,135,90,None)
self.makeButton("name7","Page2-widgets3",15,135,140,None)
self.makeButton("name8","Back",15,135,210,self.page1)
def main():
global root
root = Tk()
root.geometry('400x300+50+50')
script = make(root)
root.mainloop()
if __name__ == '__main__':
main()
was gonna comment but not enough reps, so gonna ask my question here. I copied your script and produced same error too. Firstly its not clear what you want to achieve with script so comments between definitions would help a lot!. When continue button is pressed you want current page to be destroyed and new one to be created right ?
enter code here
def frame_elements_remove(self, elements):
self.elements = elements
for self.element in self.elements:
self.element.destroy()
To destroy button it has to object. When I try to check for type of element in list ;
self.frame_elements = []
with
def mainPage(self):
self.frame_elements_remove(self.frame_elements)
self.makeButton("Button1", "Continue", 10, 10, self.page1)
self.makeButton("Button2", "Exit", 10, 80, quit)
self.controler = 1
self.frame_elements = [self.makeButton]
#just put here print for type check
print(type(self.frame_elements[0]))
it returns;
<class 'method'>
So its seems, its not destroyable because it is not object but a method.
Hope it helps!

After() not reaching next iteration in splash window

I have a splash window in a tkinter application, which is a child class of Toplevel. My target is to show a GIF moving, thus I am trying with a recurrent .after() function to update a Label. My code for the Splash window is:
import tkinter as tk
class Splash(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.overrideredirect(True)
self.parent = parent
frames = [tk.PhotoImage(file="myfile.gif",format = 'gif -index %i' %(i)) for i in range(89)] #it has 89 frames
def animate(ind, label):
frame = frames[ind]
ind += 1
if ind>88: #With this condition it will play gif infinitely
ind = 0
label.configure(image=frame)
label.update()
self.after(110, animate, ind, label)
label = tk.ttk.Label(self)
label.pack(expand=1)
self.after(0, animate, 0, label)
self.parent.overrideredirect(True)
self.update()
self.lift()
Now: I tried to also put a print function before the next after call, and I could see that it reached that step. However, if I place a print(str(ind)) at the beginning of the animate function, I see only the first index appearing.
How can I actually loop in a Toplevel window in the correct way?
EDIT:
To add context to this, I have the main class App where I do the following:
from ttkthemes import ThemedStyle, ThemedTk
import GUI.splash as sp
class App:
def __init__(self, ...):
self.interface = ThemedTk()
self.interface.iconify()
splash = sp.Splash(self.interface)
...
self.interface.after(3000, splash.destroy) #Thanks #CoolCloud
self.interface.deiconify()
self.interface.mainloop()
EDIT 2:
I have tried with a minimum reproducible example:
import tkinter as tk
class Splash(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.overrideredirect(True)
self.parent = parent
frames = [tk.PhotoImage(file="C:/temp/splash.gif",format = 'gif -index %i' %(i)) for i in range(89)] #it has 89 frames
def animate(ind, label):
frame = frames[ind]
ind += 1
if ind>88: #With this condition it will play gif infinitely
ind = 0
label.configure(image=frame)
label.update()
self.after(110, animate, ind, label)
label = tk.Label(self)
label.pack(expand=1)
self.after(0, animate, 0, label)
self.update()
self.lift()
class App:
def __init__(self):
self.interface = tk.Tk()
self.interface.iconify()
splash = Splash(self.interface)
self.interface.after(3000, splash.destroy)
self.interface.deiconify()
self.interface.mainloop()
if __name__ == "__main__":
App()
It looks like the actual problem is the size of my underlying window, or at least that's what I would think, since in my case I load multiple frames (one on top of the other one) and switch between them depending on an OptionMenu. Maybe this is too much, however it works much better than destroying and rebuilding everything.
Btw, keeping my app heavy as it is, is there a better way to produce a splash screen to load my interface and show a gif moving as in my minimal example? The gif can be downloaded here: https://images.app.goo.gl/Bpi6EvpJgodiQs2e7

Python self.after() changes running order

I am (still!) writing a Noughts and Crosses program using Tkinter in Python 3.
I wanted to implement a short pause between the player's move and the computer's move, so in the add_move_comp() method implemented self.after() function.
I expected this to pause the program for 1000ms then continue by 'drawing' on the computer's move.
However when I run my code, the program only recognises the computer's move the next move after, causing problems with identifying a 3-in-a-row.
I think this is due to line 62 running after 1000ms while the rest of the program continues running, but I don't know how to stop the whole program and continue running with line 62 after the pause.
from tkinter import *
from functools import partial
from itertools import *
import random
class Window(Frame):
def __init__(self, master = None): # init Window class
Frame.__init__(self, master) # init Frame class
self.master = master # allows us to refer to root as master
self.rows = 3
self.columns = 3
self.guiGrid = [[None for x in range(self.rows)] for y in range(self.columns)] # use this for the computer's moves
self.noText = StringVar(value = '')
self.cross = StringVar(value = 'X')
self.nought = StringVar(value = 'O')
self.button_ij = None
self.myMove = True
self.create_window()
self.add_buttons()
def create_window(self):
self.master.title('Tic Tac Toe')
self.pack(fill = BOTH, expand = 1)
for i in range(0,3): # allows buttons to expand to frame size
self.grid_columnconfigure(i, weight = 1)
self.grid_rowconfigure(i, weight = 1)
def add_buttons(self):
rows = 3
columns = 3
for i in range (rows):
for j in range(columns):
self.button_ij = Button(self, textvariable = self.noText, command = lambda i=i, j=j: self.add_move(i,j))
self.guiGrid[i][j] = self.button_ij
self.button_ij.grid(row = i,column = j, sticky =E+W+S+N)
def add_move(self, i,j):
# player's move
while self.myMove:
pressedButton = self.guiGrid[i][j]
self.guiGrid[i][j].config(textvariable = self.cross)
# computer's turn
self.myMove = False
while (self.myMove == False):
self.add_move_comp()
def add_move_comp(self):
repeat = True
while repeat:
i = random.randint(0,0) # row - only allow O to be drawn on 1st row for testing purposes
j = random.randint(0,2) # column
testText = self.guiGrid[i][j].cget('text')
if testText == '':
self.after(1000,lambda i = i, j = j :self.guiGrid[i][j].config(textvariable = self.nought)) # search up rules with returning values using lambda
print('Should plot O at row: ',i,' column: ',j)
print('TEXT ACTUALLY PLOTTED IS: ',self.guiGrid[i][j].cget('text'))
self.myMove = True
repeat = False
print(self.guiGrid[0][0].cget('text'), self.guiGrid[0][1].cget('text'), self.guiGrid[0][2].cget('text')+'THIS IS PRINTING')
root = Tk() # creating Tk instance
rootWidth = '500'
rootHeight = '500'
root.geometry(rootWidth+'x'+rootHeight)
ticTacToe = Window(root) # creating Window object with root as master
root.mainloop() # keeps program running
This is due to the fact that self.after imeadiately returns and starts the given function after passing trough a queque to prevent the actual Window from beeing unable to responde because there is some code running. A way to work around this is to create a BooleanVariable for validation and using self.wait_variable as follows:
from tkinter import *
from functools import partial
from itertools import *
import random
class Window(Frame):
def __init__(self, master = None): # init Window class
Frame.__init__(self, master) # init Frame class
self.master = master # allows us to refer to root as master
self.rows = 3
self.columns = 3
self.guiGrid = [[None for x in range(self.rows)] for y in range(self.columns)] # use this for the computer's moves
self.noText = StringVar(value = '')
self.cross = StringVar(value = 'X')
self.nought = StringVar(value = 'O')
self.button_ij = None
self.myMove = True
self.validationVar = BooleanVar(self)
self.create_window()
self.add_buttons()
def create_window(self):
self.master.title('Tic Tac Toe')
self.pack(fill = BOTH, expand = 1)
for i in range(0,3): # allows buttons to expand to frame size
self.grid_columnconfigure(i, weight = 1)
self.grid_rowconfigure(i, weight = 1)
def add_buttons(self):
rows = 3
columns = 3
for i in range (rows):
for j in range(columns):
self.button_ij = Button(self, textvariable = self.noText, command = lambda i=i, j=j: self.add_move(i,j))
self.guiGrid[i][j] = self.button_ij
self.button_ij.grid(row = i,column = j, sticky =E+W+S+N)
def add_move(self, i,j):
# player's move
while self.myMove:
pressedButton = self.guiGrid[i][j]
self.guiGrid[i][j].config(textvariable = self.cross)
# computer's turn
self.myMove = False
while (self.myMove == False):
self.add_move_comp()
def add_move_comp(self):
repeat = True
while repeat:
i = random.randint(0,0) # row - only allow O to be drawn on 1st row for testing purposes
j = random.randint(0,2) # column
testText = self.guiGrid[i][j].cget('text')
if testText == '':
self.after(1000, self.validationVar.set, True)
self.wait_variable(self.validationVar)
self.guiGrid[i][j].config(textvariable = self.nought) # search up rules with returning values using lambda
print('Should plot O at row: ',i,' column: ',j)
print('TEXT ACTUALLY PLOTTED IS: ',self.guiGrid[i][j].cget('text'))
self.myMove = True
repeat = False
print(self.guiGrid[0][0].cget('text'), self.guiGrid[0][1].cget('text'), self.guiGrid[0][2].cget('text')+'THIS IS PRINTING')
root = Tk() # creating Tk instance
rootWidth = '500'
rootHeight = '500'
root.geometry(rootWidth+'x'+rootHeight)
ticTacToe = Window(root) # creating Window object with root as master
root.mainloop() # keeps program running

PyQt5: How to reset the content of a QtWidgets.QGroupBox

I have a RadioButtonWidget class that receives a list of names (button_list) and a QtWidgets.QGroupBox (radio_group_box) and creates a radio button for each name. The problem I have is that after creating the buttons, I cannot change them. That is if I call the class again with another list of names, nothing changes. I need to create a function inside my class to remove any existing radio buttons so that I can add a new list inside it.
I tried to do radio_group_box.deleteLater() outside the class but this removes the whole box.
class RadioButtonWidget(QtWidgets.QWidget):
def __init__(self, radio_group_box, button_list):
super().__init__()
self.radio_group_box = radio_group_box
self.radio_button_group = QtWidgets.QButtonGroup()
#create the radio buttons
self.radio_button_list = []
for each in button_list:
self.radio_button_list.append(QtWidgets.QRadioButton(each))
if button_list != []:
#set the default checked item
self.radio_button_list[0].setChecked(True)
#create layout for radio buttons and add them
self.radio_button_layout = QtWidgets.QVBoxLayout()
# add buttons to the layout and button group
counter = 1
for each in self.radio_button_list:
self.radio_button_layout.addWidget(each)
self.radio_button_group.addButton(each)
self.radio_button_group.setId(each,counter)
counter += 1
# add radio buttons to the group box
self.radio_group_box.setLayout(self.radio_button_layout)
def selected_button(self):
return self.radio_button_group.checkedId()
Instead of removing the radio buttons, you can create a whole new radio button layout and set it for the group box exactly as you did in the constructor. Here is an example where the function set_group_box_buttons will remove the existing layout from radio_group_box (which is done by setting it to a temp widget), and add a new one with the new buttons.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class RadioButtonWidget(QWidget):
def __init__(self, radio_group_box, button_list):
super().__init__()
self.radio_group_box = radio_group_box
self.set_group_box_buttons(button_list)
grid = QGridLayout(self)
grid.addWidget(self.radio_group_box)
def selected_button(self):
return self.radio_button_group.checkedId()
def set_group_box_buttons(self, button_list):
self.radio_button_group = QButtonGroup()
self.radio_button_list = [QRadioButton(x) for x in button_list]
if button_list:
self.radio_button_list[0].setChecked(True)
if self.radio_group_box.layout():
QWidget().setLayout(self.radio_group_box.layout())
self.radio_button_layout = QVBoxLayout()
for i, v in enumerate(self.radio_button_list):
self.radio_button_layout.addWidget(v)
self.radio_button_group.addButton(v)
self.radio_button_group.setId(v, i)
self.radio_group_box.setLayout(self.radio_button_layout)
class Template(QWidget):
def __init__(self):
super().__init__()
self.rbw = RadioButtonWidget(QGroupBox('Radio Buttons'), ['Radio 1', 'Radio 2', 'Radio 3'])
self.box = QLineEdit()
self.box.returnPressed.connect(self.replace_buttons)
grid = QGridLayout(self)
grid.addWidget(self.rbw, 0, 0)
grid.addWidget(self.box, 0, 1)
def replace_buttons(self):
self.rbw.set_group_box_buttons(self.box.text().split(', '))
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = Template()
gui.show()
sys.exit(app.exec_())
To demonstrate, I added a QLineEdit which will update the names when you press enter. Before:
After:
There's a conceptual error in your code: you are creating a new RadioButtonGroup, which is a widget, but you are not using it.
As long as each group box will only contain the radio buttons, there is no need to create a new widget (especially if you're not actually using it); you just have to create a layout if the groupbox doesn't have one yet.
There are at least two possible approaches to your question.
For both of them I always use existing radios if possible, to avoid unnecessary object destruction each time the options change, so that they are removed only when the number of options decreases. This also avoids unnecessary layout updates (especially if the number of options is the same).
I also kept the logical "interface" consistent, providing the same method and behavior of update_options(groupBox, options).
QObject based group
With this implementation, I'm creating an object that acts as an interface responsible of creating a QButtonGroup and setting the options, while also providing signals for the state change or the current checked radio.
class RadioButtonGroup(QtCore.QObject):
optionToggled = QtCore.pyqtSignal(object, int, bool)
optionChanged = QtCore.pyqtSignal(object, int)
def __init__(self, radio_group_box, button_list):
super().__init__()
self.groupBox = radio_group_box
layout = radio_group_box.layout()
self.buttonGroup = QtWidgets.QButtonGroup(self)
self.buttonGroup.buttonToggled[int, bool].connect(self.changed)
if layout is None:
layout = QtWidgets.QVBoxLayout(radio_group_box)
for i, text in enumerate(button_list, 1):
radio = QtWidgets.QRadioButton(text)
layout.addWidget(radio)
self.buttonGroup.addButton(radio, i)
def button(self, id):
return self.buttonGroup.button(id)
def changed(self, i, state):
self.optionToggled.emit(self, i, state)
if state:
self.optionChanged.emit(self, i)
def selected_button(self):
return self.buttonGroup.checkedId()
def update_options(self, button_list):
layout = self.groupBox.layout()
# this method will keep the current checked radio as checked, if you want
# to reset it everytime, just uncomment the next commented lines
#self.buttonGroup.setExclusive(False)
for i, text in enumerate(button_list, 1):
radio = self.buttonGroup.button(i)
if radio:
#radio.setChecked(False)
radio.setText(text)
else:
radio = QtWidgets.QRadioButton(text)
layout.addWidget(radio)
self.buttonGroup.addButton(radio, i)
#self.buttonGroup.setExclusive(True)
if len(button_list) == len(self.buttonGroup.buttons()):
return
# there are more radios than needed, remove them
for radio in self.buttonGroup.buttons():
id = self.buttonGroup.id(radio)
if id > i:
self.buttonGroup.removeButton(radio)
radio.deleteLater()
class ObjectBased(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('buttongroup.ui', self)
self.pushButton.clicked.connect(self.setOptions)
self.groupBoxes = self.groupBox1, self.groupBox2, self.groupBox3
self.radioButtonGroups = []
for box in self.groupBoxes:
group = RadioButtonGroup(box,
['Option {}'.format(o + 1) for o in range(randrange(1, 10))])
self.radioButtonGroups.append(group)
group.optionChanged.connect(self.optionChanged)
def setOptions(self):
buttonGroup = self.radioButtonGroups[self.comboBox.currentIndex()]
options = ['Option {}'.format(o + 1) for o in range(self.spinBox.value())]
buttonGroup.update_options(options)
def optionChanged(self, radioButtonGroup, id):
groupBox = radioButtonGroup.groupBox
print('{} checked {} ({})'.format(
groupBox.title(), id, radioButtonGroup.button(id).text()))
Self contained
In this mode, the logic is all within the window class. While this approach is slightly simpler than the other one, we're missing an unique "interface", which might be useful for access from external objects instead.
class SelfContained(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('buttongroup.ui', self)
self.pushButton.clicked.connect(self.setOptions)
self.radioButtonGroups = []
for g, groupBox in enumerate((self.groupBox1, self.groupBox2, self.groupBox3)):
buttonGroup = QtWidgets.QButtonGroup(self)
self.radioButtonGroups.append((groupBox, buttonGroup))
buttonGroup.buttonToggled[int, bool].connect(lambda id, state, g=g: self.optionChanged(g, id, state))
self.update_options(g, ['Option {}'.format(o + 1) for o in range(randrange(1, 10))])
def update_options(self, groupId, button_list):
groupBox, buttonGroup = self.radioButtonGroups[groupId]
layout = groupBox.layout()
if layout is None:
layout = QtWidgets.QVBoxLayout(groupBox)
# as above...
#buttonGroup.setExclusive(False)
for i, text in enumerate(button_list, 1):
radio = buttonGroup.button(i)
if radio:
#radio.setChecked(False)
radio.setText(text)
else:
radio = QtWidgets.QRadioButton(text)
layout.addWidget(radio)
buttonGroup.addButton(radio, i)
#buttonGroup.setExclusive(True)
if len(button_list) == len(buttonGroup.buttons()):
return
for radio in buttonGroup.buttons():
id = buttonGroup.id(radio)
if id > i:
buttonGroup.removeButton(radio)
radio.deleteLater()
def setOptions(self):
groupId = self.comboBox.currentIndex()
options = ['Option {}'.format(o + 1) for o in range(self.spinBox.value())]
self.update_options(groupId, options)
def optionChanged(self, groupId, id, state):
if state:
groupBox, buttonGroup = self.radioButtonGroups[groupId]
print('{} checked {} ({})'.format(groupBox.title(), id, buttonGroup.button(id).text()))

generating animation in tkinter

I wrote a small program using tkinter to generate a figure based on parameters I'm giving the program (through gui interface). I am drawing the figure directly with tkinter using Canvas. Now I want to generate a series of figures and put them in animated loop. I can generate a gif animation and load it into the figure but the question is if I can do this directly in tkinter. I can put everything in loop, but then I'll lose the event handler. Any other option?
Edit
Here is a simple script:
#!/usr/bin/python
from Tkinter import *
import ttk
import numpy as np
colors = {1:'#F00',
2:'#0F0',
3:'#00F',
4:'#FF0',
5:'#0FF'}
root = Tk()
canvas = Canvas(root,width=400,height=400)
label = Label(root,text='seed')
values = StringVar()
combo = ttk.Combobox(root)
def painter(event):
seed = int(combo.get())
np.random.seed(seed)
nrows = 10
ncols = 10
Y = 0
dx = 400/nrows
dy = 400/ncols
for i in range(nrows):
X = 0
for j in range(ncols):
n = np.random.randint(1,5)
col = colors[n]
canvas.create_rectangle(X,Y,X+dx,Y+dy,width=1,fill=col)
X += dx
Y += dy
canvas.grid(column=0,row=0,rowspan=2)
combo.grid(row=1,column=1)
label.grid(row=0,column=1)
combo['values'] = [1,2,3,4]
combo.bind('<<ComboboxSelected>>',painter)
combo.current(0)
painter(None)
root.mainloop()
I could change the painter function to include an infinite loop but this way I never reach the mainloop(), so I need to call this only after the main loop is created, but how?
a simple example of some animation using tkinter canvas:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.title("Canvas animation example")
self.c = tk.Canvas(self, width=400, height=400)
self.c.pack()
self.f_index = 0 # index so we know which frame to draw next
# array to hold our frame data,
# you'll probably need this to hold more than
# just a set of coordinates to draw a line...
self.f_data = []
for num in range(0, 400, 5): # make up a set of fake data
self.f_data.append([num, num, num+10, num+10])
def next_frame(self):
data = self.f_data[self.f_index] # fetch frame data
self.c.delete('all') # clear canvas
self.c.create_line(*data) # draw new frame data
self.f_index += 1 # increment frame index
if (self.f_index >= len(self.f_data)): # check and wrap if at end of sequence
self.f_index = 0
self.c.after(50, self.next_frame) # call again after 50ms
if __name__ == "__main__":
app = App()
app.next_frame() # called manually once to start animation
# could be started with 'after' instead if desired
app.mainloop()
note that the more items you want to delete and redraw the slower this code will perform. you need to have realistic expectations, drawing a few 10's of items will be fine, a few 100's might be ok on a decent pc, but 1000's of items would be very poorly performing.

Categories