I am looking for a way to implement a time picker in a tkinter application.
I was able to implement this (probably not in the best way) using the spinbox widget and also using #PRMoureu's wonderful answer for validation. What I have right now is this -
import tkinter as tk
class App(tk.Frame):
def __init__(self,parent):
super().__init__(parent)
self.reg=self.register(self.hour_valid)
self.hourstr=tk.StringVar(self,'10')
self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,validate='focusout',validatecommand=(self.reg,'%P'),invalidcommand=self.hour_invalid,textvariable=self.hourstr,width=2)
self.reg2=self.register(self.min_valid)
self.minstr=tk.StringVar(self,'30')
self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,validate='focusout',validatecommand=(self.reg2,'%P'),invalidcommand=self.min_invalid,textvariable=self.minstr,width=2)
self.hour.grid()
self.min.grid(row=0,column=1)
def hour_invalid(self):
self.hourstr.set('10')
def hour_valid(self,input):
if (input.isdigit() and int(input) in range(24) and len(input) in range(1,3)):
valid = True
else:
valid = False
if not valid:
self.hour.after_idle(lambda: self.hour.config(validate='focusout'))
return valid
def min_invalid(self):
self.minstr.set('30')
def min_valid(self,input):
if (input.isdigit() and int(input) in range(60) and len(input) in range(1,3)):
valid = True
else:
valid = False
if not valid:
self.min.after_idle(lambda: self.min.config(validate='focusout'))
return valid
root = tk.Tk()
App(root).pack()
root.mainloop()
This seems like a pretty common requirement in GUI applications so I think there must be a more standard way to achieve this. How can I implement a user picked time widget in a cleaner way?
I am asking this because the tiny feature I want implemented is when incrementing/decrementing the minute-spinbox, if it loops over, the hour-spinbox should accordingly increase/decrease.
I thought of achieving this by setting a callback function, but I would not come to know which button of the spinbox exactly was triggered (up or down).
You can trace the changes on your minutes and act accordingly. Below sample shows how to automatically increase hour when minutes increases pass 59; you can adapt and figure out how to do the decrease part.
import tkinter as tk
class App(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.hourstr=tk.StringVar(self,'10')
self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,textvariable=self.hourstr,width=2,state="readonly")
self.minstr=tk.StringVar(self,'30')
self.minstr.trace("w",self.trace_var)
self.last_value = ""
self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.minstr,width=2,state="readonly")
self.hour.grid()
self.min.grid(row=0,column=1)
def trace_var(self,*args):
if self.last_value == "59" and self.minstr.get() == "0":
self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)
self.last_value = self.minstr.get()
root = tk.Tk()
App(root).pack()
root.mainloop()
Thanks Henry for your code, it is excellant. Here is my extension for Seconds:
# You can trace the changes on your minutes and act accordingly. Below sample shows how to automatically increase hour when minutes increases pass 59 and similarly Seconds increase pass 59; you can adapt and figure out how to do the decrease part.
import tkinter as tk
class App(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.hourstr=tk.StringVar(self,'10')
self.hour = tk.Spinbox(self,from_=0,to=23,wrap=True,textvariable=self.hourstr,width=2,state="readonly")
self.minstr=tk.StringVar(self,'30')
self.min = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.minstr,width=2) # ,state="readonly"
self.secstr=tk.StringVar(self,'00')
self.sec = tk.Spinbox(self,from_=0,to=59,wrap=True,textvariable=self.secstr,width=2)
self.last_valueSec = ""
self.last_value = ""
self.minstr.trace("w",self.trace_var)
self.secstr.trace("w",self.trace_varsec)
self.hour.grid()
self.min.grid(row=0,column=1)
self.sec.grid(row=0,column=2)
def trace_var(self,*args):
if self.last_value == "59" and self.minstr.get() == "0":
self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)
self.last_value = self.minstr.get()
def trace_varsec(self,*args):
if self.last_valueSec == "59" and self.secstr.get() == "0":
self.minstr.set(int(self.minstr.get())+1 if self.minstr.get() !="59" else 0)
if self.last_value == "59":
self.hourstr.set(int(self.hourstr.get())+1 if self.hourstr.get() !="23" else 0)
self.last_valueSec = self.secstr.get()
root = tk.Tk()
App(root).pack()
root.mainloop()
Related
I'm trying to do something with classes in Python (I come from a procedural languages background). Trying to create a version of tkinter's Label widget supporting a couple of new methods to manipulate the text of the label.
My problem is that I can't get the label to actually be visible on the screen.
Here's the code:
from tkinter import *
DEFAULT_BG = '#f0f0f0'
class cngMsg(Label):
"""Message Display label"""
def __init__(self, parent, w, h):
"""Init the Message Label"""
self.parent = parent
Label.__init__(self, parent)
self.msgText = "Hello World"
self.msgLabel = Label(parent, text=self.msgText)
self.msgLabel.config(height=h, width=w, bg=DEFAULT_BG)
def clear(self):
self.msgText = ""
print(len(self.msgText))
self.msgLabel.config(text=self.msgText)
def newMessage(self, message):
print("about to display <" + message + ">")
self.msgText = message
print(len(self.msgText))
self.msgLabel.config(text=self.msgText)
def show(self, message, sameLine=None):
if (not sameLine) and len(self.msgText) > 0:
self.msgText += '/n'
print("about to show: <" + message + ">")
self.msgText = self.msgText + message
print(len(self.msgText))
self.msgLabel.config(text=self.msgText)
#Root Stuff
if __name__ == "__main__":
app = Tk()
app.title("Message Test")
# this is the start of the application
print("initialise the Message Test")
gMsg = cngMsg(app, 60, 20)
gMsg.pack()
gMsg.newMessage("new message")
gMsg.show("this is a test")
gMsg.show("second test")
app.mainloop()
The debug print messages appear on the console but the application window doesn't display the Label.
GUI programming requires using a non-procedural paradigm since they are user-input driven. The question Tkinter — executing functions over time discusses this and has some sample code.
Personally I have often found it useful when creating GUI apps to think of them as FSMs (Finite State Machines) in which user inputs cause them to change their state.
Here's how to do something similar to what I think you were trying to in your sample code which is based on the #Bryan Oakley's answer to the linked question (updated to Python 3). It also shows the proper way to subclass tkinter classes. In addition it mostly follows the PEP 8 - Style Guide for Python Code guideline, which I strongly suggest your read and start following.
from tkinter import *
DEFAULT_BG = '#f0f0f0'
DEFAULT_MSG_TEXT = "Hello World"
DELAY = 1000 # Milliseconds.
class CngMsg(Label):
"""Message Display label"""
def __init__(self, parent, w, h):
# Intialize with default text and background color.
super().__init__(parent, text=DEFAULT_MSG_TEXT, height=h, width=w, bg=DEFAULT_BG)
def clear(self):
self.config(text='')
def newMessage(self, message):
self.config(text=message)
def show(self, message, same_line=False):
text = self.cget('text') # Get value of current option.
if not same_line and text:
text += '\n'
text += message
self.config(text=text)
class MyApp(Tk):
def __init__(self):
super().__init__()
self.frame = Frame(self)
self.frame.pack()
self.test_msg = CngMsg(self.frame, 60, 20)
self.test_msg.pack()
self.state = 0
self.do_test()
def do_test(self):
if self.state == 0:
self.test_msg.newMessage("start message")
self.state = 1
elif self.state == 1:
self.test_msg.show("this is a test")
self.state = 2
elif self.state == 2:
self.test_msg.show("second test")
self.state = 3
elif self.state == 3:
self.test_msg.clear()
self.test_msg.show("TEST COMPLETED")
self.state = -1 # Enter final state.
elif self.state != -1:
self.quit() # Stop mainloop.
raise RuntimeError("Unknown state encountered")
if self.state != -1: # Not final state?
self.after(DELAY, self.do_test) # Schedule another call.
if __name__ == "__main__":
root = MyApp()
root.title("Message Test")
root.mainloop()
Your cngMsg class is creating two labels. The first is the instance of that class itself. The second is created when you do self.msgLabel = Label(parent, text=self.msgText). This second label is a child of the cngMsg label. Since you do not call pack, place, or grid on that second label it will not appear anywhere.
In your newMessage method you are updating the text in the invisible label instead of the actual label.
You don't need this second label, and in newMessage you should configure itself like this:
def newMessage(self, message):
self.msgText = msgText
self.configure(text=self.msgText)
Similarly, show and clear should be defined in a similar way:
def clear(self):
self.msgText = ""
self.config(text=self.msgText)
def show(self, message, sameLine=None):
if (not sameLine) and len(self.msgText) > 0:
self.msgText += '/n'
self.msgText = self.msgText + message
self.config(text=self.msgText)
I'm trying to make a chess AI. I need a chess set in order to play. I made the pieces (they can be dragged), board, clock, etc. Right now, I am trying to make the clock alternate when someone plays. During one of my tests to make the clock alternate, the clock for the ai suddenly disappeared. I checked if I forgot anything like did I add the grid method, put it in the root, saved, etc. I did all of them. I tried changing the numbers in row. It was originally 0. I tried 1 and it reappeared. But it was not in the place I wanted, so I changed the number again from 1 to 2 to see if it still works. But it disappeared. I put back one but it was still gone(I checked twice if I saved). Here's the code for the ai clock:
from root_frame import Root
from tkinter import Label
ai_turn = False
class AI:
def __init__(self):
self.the_screen = Root()
self.ai_label = Label(self.the_screen.root, text = "AI", font = "Helvetica 18 bold", width = 40)
self.ai_clock = Label(self.the_screen.root, font = "Helvetica 18 bold")
def set_timer(self):
self.t = 600
return self.t
def countdown(self):
global ai_turn
if self.t > 0 and ai_turn == True:
self.convert()
self.t = self.t - 1
self.ai_clock.after(1000, lambda: self.countdown())
print("it's running dum dum")
elif self.t == 0:
self.ai_clock.config(text = "ai loose")
elif ai_turn == False:
print("it's not running dum dum")
self.t = self.t
def convert(self):
self.seconds = self.t % (24 * 3600)
self.seconds %= 3600
self.minutes = self.t // 60
self.seconds %= 60
self.ai_clock.config(text = "%02d:%02d" % (self.minutes, self.seconds))
def stop(self):
global ai_turn
ai_turn = False
if ai_turn == False:
print("ai stopped, value: {}".format(ai_turn))
def go(self):
global ai_turn
ai_turn = True
if ai_turn == True:
print("ai active, value: {}".format(ai_turn))
And here is where I grid them:
class Screen:
def __init__(self):
self.AI = AI()
self.Human = Human()
self.AI.ai_clock.grid(row = 0, column = 9)
self.Human.human_clock.grid(row = 7, column = 8)
"Run functions to activate the clocks"
self.AI.set_timer()
self.AI.countdown()
self.Human.set_timer()
self.Human.countdown()
And finally, here is root_frame:
from tkinter import Tk
class Root:
root = Tk()
(There's indent because it was in a method)Thanks!
You call self.AI.countdown() exactly once. In it, you only start the timer if self.t>0 and ai_turn == True. However, ai_turn is False, so the timer never starts. If you set ai_turn to True at the start of the program the timer will work.
All you have to do to discover this is put a print statement right before your if statement to verify your assumptions about those variables. Also, if you give the label a distinct color you will see that it is on the screen but very small because by default it has no text in it.
In other words, the label appears just fine, and is updated properly as long as you have the logic in place to start the timer.
There's no help on google, I've asked some people as well but none of them seem to know how to answer my question.
I'm programming a GUI for a project, and it contains an RSS-Feed ticker.
It scrolls through the news and when it updates (every 3 seconds for obvious debug reasons) it speeds up a bit.
This means, if I run the program, after two hours the ticker is scrolling at a non-human readable speed.
The main code wasn't written by me, I modified it and added the update function.
main():
import tkinter as tk
from Press import RSSTicker
def displayRSSticker(win):
# Place RSSTicker portlet
tickerHandle = RSSTicker(win, bg='black', fg='white', highlightthickness=0, font=("avenir", 30))
tickerHandle.pack(side='bottom', fill='x', anchor='se')
def main():
# Set the screen definition, root window initialization
root = tk.Tk()
root.configure(background='black')
width, height = root.winfo_screenwidth(), root.winfo_screenheight()
root.geometry("%dx%d+0+0" % (width, height))
label = tk.Label(root, text="Monitor Dashboard", bg='black', fg='red')
label.pack(side='bottom', fill='x', anchor='se')
# Display portlet
displayRSSticker(root)
# Loop the GUI manager
root.mainloop(0)
###############################
# MAIN SCRIPT BODY PART #
###############################
if __name__ == "__main__":
main()
RSSTicker class:
import feedparser
import tkinter as tk
class RSSTicker(tk.Text):
# Class constructor
def __init__(self, parent, **params):
super().__init__(parent, height=1, wrap="none", state='disabled', **params)
self.newsFeed = feedparser.parse('http://www.repubblica.it/rss/homepage/rss2.0.xml')
self.update()
# Class methods
def update(self):
self.headlineIndex = 0
self.text = ''
self.pos = 0
self.after_idle(self.updateHeadline)
self.after_idle(self.scroll)
self.after(4000, self.update)
def updateHeadline(self):
try:
self.text += ' ' + self.newsFeed['entries'][self.headlineIndex]['title']
except IndexError:
self.headlineIndex = 0
self.text = self.feed['entries'][self.headlineIndex]['title']
self.headlineIndex += 1
self.after(5000, self.updateHeadline)
def scroll(self):
self.config(state='normal')
if self.pos < len(self.text):
self.insert('end', self.text[self.pos])
self.pos += 1
self.see('end')
self.config(state='disabled')
self.after(180, self.scroll)
I thought the problem lied in the self.pos variable, printing it out resulted in it counting up, resetting to 1 and counting up faster.. But it doesn't seem to be problem causing the acceleration of the ticker.
From what I've understood tho, the problem must be in the scroll method.
If someone understand how to keep the original scroll speed when updated, thank you.
I think you can use a couple tracking variables to make sure that update only starts the loop once and then next time update is called it will just run scroll without starting a new loop. At the same time if scroll is not called by update then it will continue looping as needed.
Change your RSSTicker class to the following:
class RSSTicker(tk.Text):
# Class constructor
def __init__(self, parent, **params):
self.scroll_started = False # Tracker for first update.
super().__init__(parent, height=1, wrap="none", state='disabled', **params)
self.newsFeed = feedparser.parse('http://www.repubblica.it/rss/homepage/rss2.0.xml')
self.update()
def update(self):
self.headlineIndex = 0
self.text = ''
self.pos = 0
self.after_idle(self.updateHeadline)
self.after_idle(lambda: self.scroll('update'))
self.after(4000, self.update)
def updateHeadline(self):
try:
self.text += ' ' + self.newsFeed['entries'][self.headlineIndex]['title']
except IndexError:
self.headlineIndex = 0
self.text = self.feed['entries'][self.headlineIndex]['title']
self.headlineIndex += 1
self.after(5000, self.updateHeadline)
def scroll(self, from_after_or_update = 'after'):
self.config(state='normal')
if self.pos < len(self.text):
self.insert('end', self.text[self.pos])
self.pos += 1
self.see('end')
self.config(state='disabled')
# Check if the loop started by after.
if from_after_or_update != 'update':
self.scroll_started = True
self.after(180, self.scroll)
# If not started by after check to see if it is the 1st time loop is started by "update".
elif self.scroll_started is False and from_after_or_update == 'update':
self.scroll_started = True
self.after(180, self.scroll)
# If neither of the above conditions then do not start loop to prevent multiple loops.
else:
print("ran scroll method without adding new after loop!")
I'm new in Python and I'm currently trying to use tkinter as first GUI. I was used to making it without classes. And it is my first time to use import tkinter as tk instead of import *
import tkinter as tk
def update():
pass
#Game.statsFrame #doesn't work Game.statsFrame.stat1_amountLabel too
#Game.stat1_amountLabel #doesnt work < want to use update_idletasks() or
#just type new cofnig...
#just errors like: "Game' has no attribute 'statsFrame" etc #Game
class character:
name = ""
experience = 0
level = 0
gold = 0
stat1 = 0
stat2 = 0
stat3 = 0
stat4 = 0
stat5 = 0
avaiblePoints = 0
def add_stat1(self):
if self.avaiblePoints >= 1:
self.stat1 += 1
self.avaiblePoints -= 1
update()
else:
pass
def add_stat2(self):
if self.avaiblePoints >= 1:
self.stat2 += 1
self.avaiblePoints -= 1
update()
[...]
myChar = character()
myChar.avaiblePoints = 3
class Game:
def __init__(self, parent):
self.myParent = parent
self.myGame = tk.Frame(parent)
self.myGame.grid()
self.statsFrame = tk.Frame(self.myGame).grid()
self.stat1Label = tk.Label(self.statsFrame)
self.stat1Label.config(text="Strength:")
self.stat1Label.grid(column=1, row=1)
self.stat1_amountLabel = tk.Label(self.statsFrame)
self.stat1_amountLabel.config(text=myChar.stat1)
self.stat1_amountLabel.grid(column=2, row=1)
self.add_stat1Button = tk.Button(self.statsFrame)
self.add_stat1Button.config(text="+", command=myChar.add_stat1)
self.add_stat1Button.grid(column=3, row=1)
root = tk.Tk()
myapp = Game(root)
root.mainloop()
But I can't get to (for example) stat1Label and change text inside it and after it use update_idletasks(). It's like it doesnt exist. Errors shows that Game has not atributtes like stat1Label etc.
I want to use it becouse I have read that __init__ method is better and I want to swtich between pages. I have no idea, when I wasn't using class in tkinter some things (like this) was easier and had no problems. I'm very confused guys.
It's excellent that you're using import tkinter as tk instead of the dreaded "star" import, and that you're trying to organize your code with classes. It can be a little confusing at first, but it makes your code more modular, which helps enormously, especially when the GUI gets large.
There are a few problems with your code. The most important one is this line:
self.statsFrame = tk.Frame(self.myGame).grid()
The .grid method (and .pack and .place) all return None. So that line saves None to self.statsFrame, not the Frame widget. So when you later try to do stuff with self.statsFrame it won't do what you expect.
Another problem is that the text attribute of your self.stat1_amountLabel doesn't track the value of myChar.stat1, so when you change the value of myChar.stat1 you need to explicitly update the Label with the new value. Alternatively, you could use the textvariable attribute with an IntVar to hold the character's stat. See the entry for textvariable in the Label config docs for info.
Your character class has a whole bunch of attributes like name, experience etc as class attributes. That's not a good idea because class attributes are shared by all instances of a class. But you probably want each character instance to have their own instance attributes. So you should give character an __init__ method where you set those attributes. OTOH, it's ok to use class attributes for default values that get overridden by instance attributes.
Anyway, here's a repaired version of your code with a Button that updates the Strength stat. I've put the stats in a list, rather than having a bunch of separate named stats that would have to be managed separately. And I've given Game a make_stat method so you can easily add rows for the other stats.
import tkinter as tk
class Character:
def __init__(self, availablePoints=0):
self.name = ""
self.experience = 0
self.level = 0
self.gold = 0
self.stats = [0] * 5
self.availablePoints = availablePoints
def add_stat(self, idx):
if self.availablePoints >= 1:
self.stats[idx] += 1
self.availablePoints -= 1
class Game:
def __init__(self, parent):
self.myParent = parent
self.myGame = tk.Frame(parent)
self.myGame.grid()
self.statsFrame = tk.Frame(self.myGame)
self.statsFrame.grid()
self.make_stat("Strength:", 0, 0)
def make_stat(self, text, idx, row):
label = tk.Label(self.statsFrame, text=text)
label.grid(column=1, row=row)
amount = tk.Label(self.statsFrame, text=myChar.stats[idx])
amount.grid(column=2, row=row)
def update():
myChar.add_stat(idx)
amount["text"] = myChar.stats[idx]
button = tk.Button(self.statsFrame, text="+", command=update)
button.grid(column=3, row=row)
myChar = Character(3)
root = tk.Tk()
myapp = Game(root)
root.mainloop()
This code is still not ideal, but it's an improvement. ;) For example, it would be good to give Game a method for creating new characters, rather than creating them in the global context. You could store them in a dict attribute of Game, using the character's name as the key.
Here's a new version that works on separate named stat attributes. As I said in the comments, doing it this way is more complicated (and less efficient) than using a list to hold the stats.
import tkinter as tk
class Character:
def __init__(self, availablePoints):
self.name = ""
self.experience = 0
self.level = 0
self.gold = 0
self.stat1 = 0
self.stat2 = 0
self.stat3 = 0
self.stat4 = 0
self.stat5 = 0
self.availablePoints = availablePoints
class Game:
def __init__(self, parent):
self.myParent = parent
self.myGame = tk.Frame(parent)
self.myGame.grid()
self.statsFrame = tk.Frame(self.myGame)
self.statsFrame.grid()
self.make_stat("Strength:", "stat1", 1, 1)
def make_stat(self, text, stat, column, row):
label = tk.Label(self.statsFrame, text=text)
label.grid(column=column, row=row)
amount = tk.Label(self.statsFrame, text=getattr(myChar, stat))
amount.grid(column=(column+1), row=row)
def update():
if myChar.availablePoints >= 1:
v = getattr(myChar, stat) + 1
setattr(myChar, stat, v)
myChar.availablePoints -= 1
amount["text"] = v
button = tk.Button(self.statsFrame, text="+", command=update)
button.grid(column=(column+2), row=row)
myChar = Character(5)
root = tk.Tk()
myapp = Game(root)
root.mainloop()
I want to set a label in Tkinter using my countdown timer function. Right now all it does is set the lable to "10" once 10 is reached and I don't really understand why. Also, even if I have the timer print to a terminal instead the "Time's up!" bit never prints.
import time
import tkinter as tk
class App():
def __init__(self):
self.root = tk.Tk()
self.label = tk.Label(text="null")
self.label.pack()
self.countdown()
self.root.mainloop()
# Define a timer.
def countdown(self):
p = 10.00
t = time.time()
n = 0
# Loop while the number of seconds is less than the integer defined in "p"
while n - t < p:
n = time.time()
if n == t + p:
self.label.configure(text="Time's up!")
else:
self.label.configure(text=round(n - t))
app=App()
Tkinter already has an infinite loop running (the event loop), and a way to schedule things to run after a period of time has elapsed (using after). You can take advantage of this by writing a function that calls itself once a second to update the display. You can use a class variable to keep track of the remaining time.
import Tkinter as tk
class ExampleApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.label = tk.Label(self, text="", width=10)
self.label.pack()
self.remaining = 0
self.countdown(10)
def countdown(self, remaining = None):
if remaining is not None:
self.remaining = remaining
if self.remaining <= 0:
self.label.configure(text="time's up!")
else:
self.label.configure(text="%d" % self.remaining)
self.remaining = self.remaining - 1
self.after(1000, self.countdown)
if __name__ == "__main__":
app = ExampleApp()
app.mainloop()