I have a question about how to properly address widget attributes from outside the GUI main loop. Here is the gist of my project: I have an HMI, and it runs in its own thread. Outside of the HMI, there will be another thread that is collecting data and writing to hardware. I stripped out a bunch of stuff to create a basic example so you can see what I mean. It consists of a main window, a frame where the operator can select manual or auto, and a notebook that will display temperature probe data. In the real world, there will be varying numbers of probes and additional attributes, which is why I instantiate the probes at the top of the script. In real life, this would come from a SQL database and the GUI would be built dynamically based on database properties.
from tkinter import *
import tkinter as tk
from tkinter import ttk
from threading import Thread
from time import sleep
import random
wProbes= {1: {'Title': 'Indoors','Temperature':0,'MainObject':'','TempLabel':''},2: {'Title': 'Outdoors','Temperature':0,'Object':''}}
controlTemplate={'State':0,'On':0, 'Off':0}
systemControl = {'Auto':controlTemplate,'Manual':controlTemplate}
def gui():
class SystemOperation(tk.LabelFrame):
def __init__(self,parent):
super().__init__(parent)
self.config(text='System Operation',labelanchor='n',bd=2,highlightbackground='black',relief= 'solid',font=("arial", 20))
self.place(width=500, height=900,x=150,y=100)
def autoModeClick():
self.bAutoMode.configure(background='green')
self.bManualMode.configure(background='white')
systemControl['Auto']['State'] = 1
systemControl['Manual']['State'] = 0
def manualModeClick():
self.bManualMode.configure(background='green')
self.bAutoMode.configure(background='white')
systemControl['Auto']['State'] = 0
systemControl['Manual']['State'] = 1
self.bAutoMode = Button(self, text='Auto', background='green', command=autoModeClick)
self.bAutoMode.place(x=100, y=100, width=70)
self.bManualMode = Button(self, text='Manual', command=manualModeClick)
self.bManualMode.place(x=100, y=150, width=70)
class TemperatureProbe(tk.LabelFrame):
def __init__(self,parent,inst):
super().__init__(parent)
self.config(text=wProbes[inst]['Title'],labelanchor='n',bd=2,highlightbackground='black',relief= 'solid',font=("arial", 20))
self.place(width=500, height=300)
self.currentTemp=0
self.actTemp=Label(self,text='Temperature= %d F'%self.currentTemp,width=20)
self.actTemp.place(relx=.5, y=20, anchor='center')
self.pack_propagate(0)
wProbes[inst]['TempLabel']=self.actTemp
class probeNotebook(ttk.Notebook):
def __init__(self,parent):
super().__init__(parent)
self.place(x=900,y=100,width=800,height=700)
for t in sorted(wProbes.keys()):
tabName = wProbes[t]['Title']
self.tabFrame = tk.Frame(self, width=700, height=900, bg='white', borderwidth=2, relief='solid')
self.add(self.tabFrame, text=' %s '%tabName)
self.pFB=TemperatureProbe(self.tabFrame,t)
self.pFB.place(x=100, y=20)
wProbes[t]['MainObject']=self.pFB
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.geometry("800x800")
self.title('Test Window')
SystemOperation(self)
probeNotebook(self)
if __name__=="__main__":
app=MainWindow()
app.mainloop()
threadGUI = Thread(target=gui)
threadGUI.start()
while 1:
#A bunch of stuff will be happening here that interfaces with external hardware
if wProbes[1]['MainObject']!='':
randInt=random.randint(10,200)
wProbes[1]['TempLabel'].config(text='Temperature= %d F'%randInt)
wProbes[2]['TempLabel'].config(text='Temperature= %d F' % (randInt+200))
sleep(.5)
So, you can see that a notebook is created, and the tabs are then created based on the number of probes in the probe dictionary. Also in the probe dictionary, I set a reference to the probe container, and then the probe temperature label. If you run the code, you can see the temperatures change correctly based on the probe instances with a test random int.
But what I was really hoping to do is just use the probe MainObject reference and address its child widgets that way, i.e. wProbes[1]['MainObject'].actTemp, because in reality the probe container will have more widgets such as limits and setpoints and warnings. But if creating individual references is the correct way, I'm good with that too. Am I on the right track?
Another thing I wondered about as I go forward is how I can separate the GUI from the process script completely. Imagine the GUI as a standalone app, and a second app that acts like a server and does the heavy lifting and communicates with the GUI. Any basic hints on doing this? I'll do the work in figuring it out, but if you push me in the right direction I'd appreciate it.
Also, if there are criticisms of my project approach in general, I'm all ears. I've done quite a bit of python programming, but this is my first tkinter attempt. Thanks!
(Note: I hope it formatted correctly. I just copied and pasted into my ide, and it was fine. Worst case would be that the indentation is wrong in a couple of places when copied, but give it a try.)
Related
The goal is to achieve different "screens" in TkInter and change between them. The easiest to imagine this is to think of a mobile app, where one clicks on the icon, for example "Add new", and new screen opens. The application has total 7 screens and it should be able to change screens according to user actions.
Setup is on Raspberry Pi with LCD+touchscreen attached. I am using tkinter in Python3. Canvas is used to show elements on the screen.
Since I am coming from embedded hardware world and have very little experience in Python, and generally high-level languages, I approached this with switch-case logic. In Python this is if-elif-elif...
I have tried various things:
Making global canvas object. Having a variable programState which determines which screen is currently shown. This obviously didn't work because it would just run once and get stuck at the mainloop below.
from tkinter import *
import time
root = Tk()
programState = 0
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
time.sleep(2)
canvas.delete(ALL) #delete all objects from canvas
programState = 1
elif(programState == 1):
....
....
....
root.mainloop()
Using root.after function but this failed and wouldn't show anything on the screen, it would only create canvas. I probably didn't use it at the right place.
Trying making another thread for changing screens, just to test threading option. It gets stuck at first image and never moves to second one.
from tkinter import *
from threading import Thread
from time import sleep
def threadFun():
while True:
backgroundImage = PhotoImage(file="image1.gif")
backgroundImage2 = PhotoImage(file="image2.gif")
canvas.create_image(0,0,image=backgroundImage, anchor=NW)
sleep(2)
canvas.delete(ALL)
canvas.create_image(0,0,image=backgroundImage2, anchor=NW)
root = Tk()
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
# daemon=True kills the thread when you close the GUI, otherwise it would continue to run and raise an error.
Thread(target=threadFun, daemon=True).start()
root.mainloop()
I expect this app could change screens using a special thread which would call a function which redraws elements on the canvas, but this has been failing so far. As much as I understand now, threads might be the best option. They are closest to my way of thinking with infinite loop (while True) and closest to my logic.
What are options here? How deleting whole screen and redrawing it (what I call making a new "screen") can be achieved?
Tkinter, like most GUI toolkits, is event driven. You simply need to create a function that deletes the old screen and creates the new, and then does this in response to an event (button click, timer, whatever).
Using your first canvas example
In your first example you want to automatically switch pages after two seconds. That can be done by using after to schedule a function to run after the timeout. Then it's just a matter of moving your redraw logic into a function.
For example:
def set_programState(new_state):
global programState
programState = new_state
refresh()
def refresh():
canvas.delete("all")
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
canvas.after(2000, set_programState, 1)
elif(programState == 1):
...
Using python objects
Arguably a better solution is to make each page be a class based off of a widget. Doing so makes it easy to add or remove everything at once by adding or removing that one widget (because destroying a widget also destroys all of its children)
Then it's just a matter of deleting the old object and instantiating the new. You can create a mapping of state number to class name if you like the state-driven concept, and use that mapping to determine which class to instantiate.
For example:
class ThisPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
class ThatPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
page_map = {0: ThisPage, 1: ThatPage}
current_page = None
...
def refresh():
global current_page
if current_page:
current_page.destroy()
new_page_class = page_map[programstate]
current_page = new_page_class()
current_page.pack(fill="both", expand=True)
The above code is somewhat ham-fisted, but hopefully it illustrates the basic technique.
Just like with the first example, you can call update() from any sort of event: a button click, a timer, or any other sort of event supported by tkinter. For example, to bind the escape key to always take you to the initial state you could do something like this:
def reset_state(event):
global programState
programState = 0
refresh()
root.bind("<Escape>", reset_state)
I've been struggling with this for a while. I think I'm missing some simple piece of information and I hope you guys can help clear this up for me.
I'm trying to get tkinter to display different frames which I will eventually place widgets inside of. Here's what I did:
I've made a class that is supposed to initialize the window and make all the different frames the program will run.
I've made a separate class for each frame(I'm intending to have variables associated with the different classes when the program is done), and assigned a variable that will start that class up and make it run it's init function
I ended the StartUp class by telling it to tkraise() the frame I want displayed, and that's where things stop working correctly.
I set each frame to a different color, so when you run this program you will see that they split the screen space up instead of one being raised to the top. What am I missing?
One last point, I am purposely trying to spell everything out in my program, I learn better that way. I left it so I have to type tkinter.blah-blah-blah in front of each tkinter command so I can recognize them easily, and I decided not to have my classes inherit Frame or Tk or anything. I'm trying to understand what I'm doing.
import tkinter
class StartUp:
def __init__(self):
self.root = tkinter.Tk()
self.root.geometry('300x300')
self.container = tkinter.Frame(master=self.root, bg='blue')
self.container.pack(side='top', fill='both', expand=True)
self.page1 = Page1(self)
self.page2 = Page2(self)
self.page1.main_frame.tkraise()
class Page1():
def __init__(self, parent):
self.main_frame = tkinter.Frame(master=parent.container, bg='green')
self.main_frame.pack(side='top', fill='both', expand=True)
class Page2():
def __init__(self, parent):
self.main_frame = tkinter.Frame(master=parent.container, bg='yellow')
self.main_frame.pack(side='top', fill='both', expand=True)
boot_up = StartUp()
boot_up.root.mainloop()
When you do pack(side='top', ...), top doesn't refer to the top of the containing widget, it refers to the top of any empty space in the containing widget. Page initially takes up all of the space, and then when you pack Page2, it goes below Page1 rather than being layered on top of it.
If you are using the strategy of raising one window above another, you need to either use grid or place to layer the widgets on top of each other. The layering is something pack simply can't do.
Your other choice is to call pack_forget on the current window before calling pack on the new windowl
I'm quite new to programming and trying to write a fairly simple program that records the times in between different keyboard button presses (a bit like multiple reaction time tests) and prints these times on screen in an array, then terminates and saves the array after a certain period of time is up.
I've already written most of the program in pygame after giving up on Tkinter because it seemed to be the best thing for responding to keyboard input in real time. However, now that I'm wanting the text to scroll automatically once the screen fills up, add more columns to the array, and export to Excel, I'm starting to wonder whether I'd be better off with a module more suited to text handling.
Can anyone advise me on whether I'm making a mistake attempting this in pygame and whether responding immediately to multiple keyboard inputs in Tkinter is possible? I can provide more detail if necessary.
Using Tkinter, you can bind to <Any-KeyPress>. The function that is called is passed an event object that has a timestamp. You can use that to compute the time between events.
Here's a quick example that shows how to display the time between keypresses. You can of course add your own logic to count and track and display however you want.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
self.last_event = None
tk.Frame.__init__(self, parent)
self.label = tk.Label(self, text="")
self.text = tk.Text(self, wrap="word")
self.label.pack(side="top", fill="x")
self.text.pack(fill="both", expand=True)
self.text.bind("<Any-KeyRelease>", self.on_key_release)
def on_key_release(self, event):
if self.last_event is not None:
delta = event.time - self.last_event.time
self.label.configure(text="time since last event: %s ms" % delta)
self.last_event = event
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
We have a functioning program that uses Tkinter as its GUI. Everything works fine however different branches of the code are now using different hardware which realistically need different buttons. Hence we'd like to have the main GUI import modules representing the buttons depending on what hardware is being used.
I've cut out some of the code below, I'm interested in removing the makemenu() function to a separate module, hence when it is called in the Application __init__ (self.makemenu(master)) I would like to make that a reference to a separate module. I've tried doing this and am having trouble. Is this even possible?
I'm a little confused on the parent structure, what needs to be passed to my button module, etc.? I know this is a poorly constructed question but if anyone is able to advise if this is possible and put my on the right track that would be great. For example if someone could show how to modify this code to have the buttons defined in a separate module I could figure out how to do the same in my module.
# Import necessary libraries
import sys
import os
import Tkinter as tk
class Application(tk.Frame):
##################################################################
## Final functions are designed to initialize the GUI and
## connect various mouse movements to useful functions.
##################################################################
def definevars(self):
'''Original definition of all of the key variables that
we need to keep track of while running the GUI
'''
self.disable = True
self.savimgstatus = 'off'
self.mode = 'Standby'
self.status = 'Not Ready'
def makemenu(self,master):
''' Function to create the main menu bar across
the top of the GUI.
'''
self.menubar = tk.Menu(master)
## Motor Submenu
motormenu = tk.Menu(self.menubar,tearoff=1)
motormenu.add_command(label='ALT',state='disabled')
motormenu.add_command(label='error check',
command=lambda: self.geterror('alt'))
motormenu.add_separator()
motormenu.add_command(label='AZ',state='disabled')
motormenu.add_command(label='error check',
command=lambda: self.geterror('az'))
self.menubar.add_cascade(label='Tracker Motors',menu=motormenu)
## Set the big menu as the main menu bar.
master.config(menu=self.menubar)
def __init__(self,tcpconn,DOME,TRACKERSTAGE, master=None):
'''Main function to initialize the GUI. Will scale
the size of the GUI to fit any size screen... to a
point. It will not allow it to be smaller than
600x800.
'''
self.buf = 1024
## Check resolution of screen. Make GUI 2/3rds of size
## unless that means under 600x800.
fh = round(master.winfo_screenheight()*2./3.)
fw = round(master.winfo_screenwidth()*2./3.)
if fh < 600: fh = 600
if fw < 800: fw = 800
print 'GUI resolution set to {0} x {1}'.format(fw,fh)
self.fw = fw
self.fh = fh
self.imwidth = int(0.45*self.fw)
self.imheight = int(0.45*self.fh)
self.imcentx = self.imwidth/2
self.imcenty = self.imheight/2this
## Initialize Frame
tk.Frame.__init__(self, master, height=fh,width=fw)
self.grid()
self.grid_propagate(0)
## Initialize Various variables.
self.definevars()
## Create buttons, etc.
self.createWidgets()
self.makemenu(master)
self.disableall()
## Main Loop function
self.checkoutput()
###################################################################
# Initialize GUI window.
root = tk.Tk()
root.title('Hardware') # window title
app = Application(master=root)
app.mainloop() # go into the main program loop
sys.exit()
If you want to move makemenu to a separate module, that should be pretty simple. However, you'll need to change a few things.
Since makemenu no longer has a reference to self (or has a different reference, if you implement it as a separate class), you need to replace calls like command=lambda: self.geterror('alt')) to be command=lambda: master.geterror('alt')).
The other thing I recommend is to remove the call to add the menu to the root. I believe that modules shouldn't have side effects like this -- the function should make a menu and return it, and let the caller decide how to use it, ie:
self.menubar=makemenu(master)
master.configure(menu=self.menubar)
Roughly speaking, this is a variation of the MVC (model/view/controller) architectural pattern where the Application instance is your controller (and also part of the view unless you make modules of all your UI code). The menu is part of the view, and forwards UI functions to the controller for execution.
Your application then looks something like this:
from makemenu import makemenu
class Application(...):
def __init__(...):
...
self.menubar = makemenu(master)
master.config(menu=self.menubar)
...
I'm a beginner programmer in python and have recently began using tkinter though I have come across a problem which I can't solve.
Basically I have two entry boxes.
Entry1 = message
Entry2 = no. of flashes
(This is just an example of what I need.)
All I need is a for loop for a label to pop up and flash entry1 as many times as entry2, yes I realize how to get the entry inputs but I have no idea how to get the label to continuously flash, I have tried pack_forget and .destroy methods for the label in a loop, but unfortunately it does not display as it almost instantly clears it from the screen again.
The basic idea is to create a function that does the flash (or half of a flash), and then use after to repeatedly call the function for as long as you want the flash to occur.
Here's an example that switches the background and foreground colors. It runs forever, simply because I wanted to keep the example short. You can easily add a counter, or a stop button, or anything else you want. The thing to take away from this is the concept of having a function that does one frame of an animation (in this case, switching colors), and then scheduling itself to run again after some amount of time.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.label = tk.Label(self, text="Hello, world",
background="black", foreground="white")
self.label.pack(side="top", fill="both", expand=True)
self.flash()
def flash(self):
bg = self.label.cget("background")
fg = self.label.cget("foreground")
self.label.configure(background=fg, foreground=bg)
self.after(250, self.flash)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()