Pass a variable between two scripts? - python

I'm having issues passing variables between two scripts in python without ending the imported script. I would like for the status bar in my tkinter GUI to update to reflect what number the counter in script2 is on. I've included the code below:
from tkinter import *
import script2
root = Tk()
canvas = Canvas(root, width=300, height=300)
canvas.pack()
def runScript():
var = script2.information()
button1 = Button(root, text="Click Me!", command=runScript)
button1.pack()
var = StringVar()
var.set('Waiting for Input...')
status = Label(root, textvariable=var, bd=1, relief=SUNKEN, anchor=W)
status.pack(side=BOTTOM, fill=X)
root.mainloop()
script2.py would be:
#script2
import time
def information():
variable = 0
while variable < 500:
yield variable
variable += 1
print(variable)
time.sleep(1)
I understand that this has to do with generators, but my understanding was that yield would pause the loop, send the data to the original program, let it process it, and then un-pause the loop. Is there a better way of understanding this process?

For one thing, script2.information() is a generator function which means you will have to manually iterate the generator object returned on the first call it to get successive values.
For another, tkinter doesn't support multithreading. One thing you can do is schedule for a function to be called after a certain amount of time using the universal widget method after(). In this case, it can be used to schedule a call to an (added) function that iterates the generator object after it's been created by calling script2.information() and updates the StringVar() widget accordingly each time it's called.
You also need to change script2.py so it doesn't call time.sleep(). Doing so will make your tkinter GUI program hang whenever it's called (since it temporarily interrupts execution of the tkinter mainloop()).
Modified script2.py:
import time
def information():
variable = 0
while variable < 60: # changed from 500 for testing
yield variable
variable += 1
print(variable)
# time.sleep(1) # don't call in tkinter programs
main script:
from tkinter import *
import script2
DELAY = 100 # in millisecs
root = Tk()
canvas = Canvas(root, width=300, height=300)
canvas.pack()
def do_update(gen, var):
try:
next_value = next(gen)
except StopIteration:
var.set('Done!')
else:
var.set(next_value)
root.after(DELAY, do_update, gen, var) # call again after delay
def run_script(var):
gen = script2.information() # create generator object
do_update(gen, var) # start iterating generator and updating var
var = StringVar()
var.set('Waiting for Input...')
button1 = Button(root, text="Run script!", command=lambda: run_script(var))
button1.pack()
status = Label(root, textvariable=var, bd=1, relief=SUNKEN, anchor=W)
status.pack(side=BOTTOM, fill=X)
root.mainloop()

Your information generator returns an iterator, you will need to save the iterator instance and call next() on it to get the next value. To change the var, you will need to use .set() to change it.
from tkinter import *
import script2
root = Tk()
canvas = Canvas(root, width=300, height=300)
canvas.pack()
def runScript():
var.set(next(info)) # set the variable as the "next" result of the info generator
info = script2.information() # the info is an iterator
button1 = Button(root, text="Click Me!", command=runScript)
button1.pack()
var = StringVar()
var.set('Waiting for Input...')
status = Label(root, textvariable=var, bd=1, relief=SUNKEN, anchor=W)
status.pack(side=BOTTOM, fill=X)
root.mainloop()
The script2 can stay the same. But I recommend you to not use time.sleep, it wouldn't work as you expect it to! It will just make your program not respond for the given seconds.
You will need to use root.after if you want to increment to continue till StopIteration, just change runScript to this::
def runScript():
global info
try:
var.set(next(info)) # set the variable as the "next" result of the info generator
except StopIteration:
var.set('Finished')
info = script2.information()
else:
root.after(1, runScript)

Related

How to make a "while True" loop work in a tkinter-based game?

I want to make a simple clicker game
and one of my upgrades needs to be constantly running in background.
I tried while True: loop but it just doesn't work and python crushes.
What's wrong?
from time import sleep
import tkinter as tk
window=tk.Tk()
window.columnconfigure([0,4], minsize=60,)
def click():
points["text"]=1+int(points["text"])
def up3():
if int(points["text"])>=int(cost3["text"]):
lvl3["text"]=int(lvl3["text"])+1
points["text"]=int(points["text"])-int(cost3["text"])
cost3["text"]=int(int(cost3["text"])*2)
while True:
points["text"]=int(points["text"])+100*int(lvl3["text"])
sleep(1)
# points and click button
points=tk.Label(text="10000000")
points.grid(row=0,column=0,columnspan=5,sticky="WE")
tk.Button(text="click!",command=click).grid(row=1,column=0,columnspan=5,sticky="WE")
# upgrade 3
tk.Button(text="passive points",command=up3).grid(row=5,sticky="WE",column=0)
tk.Label(text="+100/s").grid(row=5,column=1)
cost3=tk.Label(text="10000")
cost3.grid(row=5,column=3)
lvl3=tk.Label(text="1")
lvl3.grid(row=5,column=4)
window.mainloop()
Here's how to use the universal widget method after() to effectively perform a while True: infinite loop that won't interfere with the execution of the rest of your tkinter app.
I've added a function called updater() which performs the actions you want and then reschedules another call to itself after a 1000 milliseconds (i.e. 1 second) delay.
This is what some of the answers to the linked question in #Random Davis's comment suggest doing, as do I.
import tkinter as tk
window=tk.Tk()
window.columnconfigure([0,4], minsize=60,)
def click():
points["text"] = int(points["text"]) + 1
def up3():
if int(points["text"]) >= int(cost3["text"]):
lvl3["text"] = int(lvl3["text"]) + 1
points["text"] = int(points["text"]) - int(cost3["text"])
cost3["text"] = int(int(cost3["text"]) * 2)
updater() # Start update loop.
def updater():
points["text"] = int(points["text"]) + 100*int(lvl3["text"])
window.after(1000, updater) # Call again in 1 sec.
# points and click button
points = tk.Label(text="10000000")
points.grid(row=0, column=0, columnspan=5, sticky="WE")
tk.Button(text="click!",command=click).grid(row=1, column=0, columnspan=5, sticky="WE")
# upgrade 3
tk.Button(text="passive points", command=up3).grid(row=5, sticky="WE", column=0)
tk.Label(text="+100/s").grid(row=5, column=1)
cost3 = tk.Label(text="10000")
cost3.grid(row=5, column=3)
lvl3 = tk.Label(text="1")
lvl3.grid(row=5, column=4)
window.mainloop()

How can i update a variable after pressing buttons on tkinter?

There's an example of my code below.
I am trying to make a GUI with tkinter, in python. I want an app that has a variable, let's say var_list, that is introduced into a function as a parameter.I run this function using a button with command=lambda: analize(var_list)
I want to be able to modify the variable by pressing buttons (buttons to add strings to the list). And I have a function for that aswell:
def button_clicked(e):
if ((e["text"]).lower()) in var_list:
var_list.pop(var_list.index((e["text"]).lower())) #this adds a string to the list
else:
var_list.append((e["text"]).lower()) #this deletes the string from the list if it was already there
The function works, I tried printing the var_list and it gets updated everytime I press a button.
The problem is that I have to create the var_list as an empty list before, and when I run the function analize(var_list), it uses the empty list instead of the updated one.
Any idea on how to update the global var everytime I add/delete something from the list?
from tkinter import *
from PIL import ImageTk
def show_frame(frame):
frame.tkraise()
def button_clicked(e):
if ((e["text"]).lower()) in var_list:
var_list.pop(var_list.index((e["text"]).lower()))
else:
var_list.append((e["text"]).lower())
def analize(x):
#does stuff with the list
window = Tk()
frame1 = Frame(window)
frame2 = Frame(window)
canvas1 = Canvas(frame1,width = 1280, height = 720)
canvas1.pack(expand=YES, fill=BOTH)
image = ImageTk.PhotoImage(file="background.png")
var_list = []
button1 = Button(canvas1, text="Analize",font=("Arial"),justify=CENTER, width=10, command=lambda: [show_frame(frame2),analize(x=var_list)])
button1.place(x=(1280/2)-42, y=400)
button2 = Button(canvas1, text="String1",font=("Arial"),justify=CENTER, width=10, command=lambda: button_clicked(button2))
button2.place(x=(1280/2)-42, y=450)
button3 = Button(canvas1, text="String2",font=("Arial"),justify=CENTER, width=10, command=lambda: button_clicked(button3))
button3.place(x=(1280/2)-42, y=500)
Thank you
you can make a global variable eg:-global var
Now you can access it within other defination to manipulate the variable like this
global var
var = 0 # if you want to set a default value to the variable before calling the
function
def change_var():
global var
var = 1
USE OF GLOBAL
using global is highly recommended and is quite necessary if you are working with functions that contain or has the need to manipulate the variable
If global is not given inside the function, the variable will live inside the function and it cannot be accessed outside the function.
Hope this answer was helpful, btw, I am not sure if this the answer you are looking for as your question is not clear, maybe give a situation where you might think it might be necessary to change or update the variable
Sorry, I did not understand you but I guess this example will help you -
import tkinter as tk
root = tk.Tk()
var_list = []
def change_val(n):
var_list.append(n)
label1.config(text=var_list)
def remove():
try:
var_list.pop()
label1.config(text=var_list)
except:
pass
label1 = tk.Label(root,text=var_list)
label1.pack()
button1 = tk.Button(root,text='1',command=lambda:change_val(1))
button1.pack()
button2 = tk.Button(root,text='2',command=lambda:change_val(2))
button2.pack()
button3 = tk.Button(root,text='3',command=lambda:change_val(3))
button3.pack()
button4 = tk.Button(root,text='Pop Element',command=remove)
button4.pack()
root.mainloop()

Python Tkinter Entry.get() gives previous input

I am working on a relatively large project, but have managed to recreate my problem in just a few lines:
import tkinter as tk
root = tk.Tk()
root.geometry('200x200')
def doStuff():
pass
sv = tk.StringVar()
def callback():
print(E.get())
doStuff()
return True
E = tk.Entry(root, bg="white", fg="black", width=24, textvariable=sv, validate="key",validatecommand=callback)
E.grid(row=0, column=0, padx=30, pady=5, sticky=tk.E)
root.mainloop()
The desired output would be that every time the user changes the entry in this Entrywidget, a function is called.
This works just fine, but using E.get() returns the 'previous' entry, for example:
-entry is 'boo'
-E.get() is 'bo'
Python seems to run Callback() before the Entry Widget has been changed.
Validation by design happens before the text has been inserted or deleted. For validation to work, it must be able to prevent the data from being changed.
If you aren't doing validation, but instead just want to call a function when the value changes, the best way to do that is to put a trace on the associated variable.
def callback(*args):
print(E.get())
doStuff()
return True
sv = tk.StringVar()
sv.trace_add("write", callback)

Why is my threaded process still freezing?

I'm writing an app using tkinter for the GUI, and I want an indeterminate progress bar to be going back and forth while the main function is running, which sometimes takes a few seconds, depending on user input. Normally, the whole program freeze while the main function is running, so I am trying to establish a threaded process for the progress bar so it moves while main() is doing its thing (both functions are called withinscan_and_display()).
from tkinter import filedialog
from tkinter import *
from tkinter.ttk import Progressbar
from main import main
import graphs
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from PIL import ImageTk, Image
import threading
root = Tk()
launch_frame = Frame(root)
button_frame = Frame(root)
graph_frame = Frame(root, height=1000, width=1200)
# log directory button/text
logDirButton = Button(master=launch_frame, text='Select log storage location...',
command=lambda: get_directory(log_text), width=22)
log_text = Text(master=launch_frame, height=1, width=25)
logDirButton.grid(row=1, column=0)
log_text.grid(row=1, column=1)
# scan directory button/text
dirButton = Button(master=launch_frame, text="Select scan directory...", command=lambda: get_directory(t), width=22)
t = Text(master=launch_frame, height=1, width=25)
dirButton.grid(row=2, column=0)
t.grid(row=2, column=1)
# main scan button
mainButton = Button(master=launch_frame, text="SCAN!", state=DISABLED,
width=50, height=10, bg='#27b355')
mainButton.grid(row=3, column=0, columnspan=2)
# progress bar
progress = Progressbar(launch_frame, orient=HORIZONTAL, length=100, mode='indeterminate')
progress.grid(row=4, column=0, columnspan=2)
launch_frame.grid(row=0, column=0, sticky=NW)
def get_directory(text):
# first clear form if it already has text
try:
text.delete("1.0", END)
except AttributeError:
pass
directory = filedialog.askdirectory()
# store the first directory for later specific reference
text.insert(END, directory)
# disable scan button until user has given necessary info to run (log storage location, scan directory)
enable_scan_button(log_text, t)
return directory
def enable_scan_button(logText, dirText):
if logText.get("1.0", END) != '\n' and dirText.get('1.0', END) != '\n':
mainButton['state'] = NORMAL
mainButton['command'] = lambda: scan_and_display()
else:
mainButton['state'] = DISABLED
def scan_and_display():
threading.Thread(target=bar_start).start()
# get scan directory and log directory from text fields
log_directory = log_text.get("1.0", END)[:-1]
scan_directory = t.get("1.0", END)[:-1]
# store the initial scan directory for later reference
top_dir = scan_directory
# runs the main scan function. Passes scan_directory and log_directory arguments
data, scanDate = main(log_directory, scan_directory)
display(scan_directory, data, scanDate, top_dir)
def bar_start():
print("BAR START CALLED")
progress.start(10)
With this setup (and various other configurations I've trieD), the bar still freezes while main() is doing it's thing, and I need it to move to indicate to the user that something is happening.
You need to thread the long-running function, not the progressbar.
def scan_and_display():
# get scan directory and log directory from text fields
# It's best to do all tkinter calls in the main thread
log_directory = log_text.get("1.0", END)[:-1]
scan_directory = t.get("1.0", END)[:-1]
th = threading.Thread(target=bar_start, args=(log_directory, scan_directory))
th.start()
progress.start()
def bar_start(log_directory, scan_directory):
print("BAR START CALLED")
# store the initial scan directory for later reference
top_dir = scan_directory
# runs the main scan function. Passes scan_directory and log_directory arguments
data, scanDate = main(log_directory, scan_directory)
display(scan_directory, data, scanDate, top_dir)
progress.stop()
EDIT: here's a MCVE:
import tkinter as tk
from tkinter.ttk import Progressbar
import time
from threading import Thread
def long_running_function(arg1, arg2, result_obj):
"""accept some type of object to store the result in"""
time.sleep(3) # long running function
result_obj.append(f'DONE at {int(time.time())}')
root.event_generate("<<LongRunningFunctionDone>>") # trigger GUI event
def long_func_start():
x = 'spam'
y = 'eggs'
t = Thread(target=long_running_function, args=(x,y,data))
t.start()
result.config(text='awaiting result')
progress.start()
def long_func_end(event=None):
progress.stop()
result.config(text=f"process finished with result:\n{data[-1]}")
root = tk.Tk()
root.geometry('200x200')
btn = tk.Button(root, text='start long function', command=long_func_start)
btn.pack()
progress = Progressbar(root, orient=tk.HORIZONTAL, length=100, mode='indeterminate')
progress.pack()
result = tk.Label(root, text='---')
result.pack()
root.bind("<<LongRunningFunctionDone>>", long_func_end) # tell GUI what to do when thread ends
data = [] # something to store data
root.mainloop()

Global Variable Undefined? Can't get label to display global variable from function

This is a little program I am making to learn Python which is a NPC Trait Generator that generates random words from a text file.
My random generator works, but I can't get it to display properly in the label. I currently have pertrait set to global but it doesn't seem to pick up the variable to display in the label? I tried setting up StringVar but I couldn't seem to get that to work properly either.
import tkinter as tk
from tkinter import font
# Random Personality generation from text list
def gen():
global pertrait
pertrait = print(random.choice(open(
'F:\\Desktop\\python\\RandomGenerator py\\CharTraitList.txt').read(
).split()).strip())
root = tk.Tk()
root.title("NPC Trait Generator")
root.geometry("500x200")
frame = tk.Frame(root)
frame.pack()
# define font
fontStyle = font.Font(family='Courier', size=44, weight='bold')
# background image variable and import
paper = tk.PhotoImage(
file='F:\\Desktop\\python\\RandomGenerator py\\papbckgrd.png')
butQuit = tk.Button(frame,
text="Quit",
fg="red",
command=quit)
butQuit.pack(side=tk.LEFT)
ButGen = tk.Button(frame,
text="Generate",
command=gen)
ButGen.pack(side=tk.RIGHT)
# Label generation
Trait1 = tk.Label(root,
compound=tk.CENTER,
text=pertrait,
font=fontStyle)
# image=paper) .pack(side="right")
Trait1.pack()
root.mainloop()
print() doesn’t return a value, and you neglected to call gen()!
Change:
pertrait = print(random.choice(open(
'F:\\Desktop\\python\\RandomGenerator py\\CharTraitList.txt').read(
).split()).strip())
To:
pertrait = random.choice(open(
'F:\\Desktop\\python\\RandomGenerator py\\CharTraitList.txt').read(
).split()).strip()
But really it would be much better t avoid the global - make gen() return the value and assign the result of gen() ot pertrait

Categories