-1

I'm trying to create an assistant for an old game I play. This will show you the length of time left on a spell youve cast using a progress bar (tied to macros. IE the D macro I have is for a spell 3.7 seconds long as shown below). Im a bit new to programming so I have done this in stages. I have completed the program so that it searches my macros and sends the correct timer on Key event to the function. The problem is however the spells simply queue rather than interrupting when a new macro is pressed again.

I was trying to solve this in a shorter experimental script as I find it easier to do bits as learning and then combine them later. As you see here I tried to run the timer on a separate thread so I can cancel it halfway through (in the complete program this would be when a macro is pressed before its finished).

Going from examples I found on this site I thought this global variable in the thread would work but it still simply finishes running func(x) before cancelling it and printing that it has been halted. Sorry if I havent phrased this well enough - or phrased it too much. My first time posting here

Code below - Thanks in advance!

import time 
import sys
from tkinter import *
from tkinter.ttk import *
root = Tk() 
progress = Progressbar(root, orient = HORIZONTAL, length = 500, mode = 'determinate')
progress.pack()
# on key press check spells
global casting
def func(x):
    global casting  
    while casting == True and progress['value']<100:
        increments = 100/x
        progress['value'] += increments
        root.update_idletasks() 
        time.sleep(0.01) 
    else:
        progress['value'] = 0
        root.update_idletasks()
        return

#def cancel():
#   time.sleep(2)
##  global casting
#   casting = False
#   print("Halted casting")

casting = True
t1 = threading.Thread(target=func(370))
t1.start()
time.sleep(0.9)
casting = False
print("Halted")
t1.join

###### New code updated 


from tkinter import *
from tkinter.ttk import *
global casting
myMacros = {
    "my_keys": {
        "d": ["Flamestrike", 370],
        "x": ["Poison", 150],
        "q": ["Cure", 100],
        "a": ["Lightning", 250],
        "w": ["Greater Heal", 300],
        "z": ["Magic Arrow", 100],
        "e": ["Magic Reflection", 350]
    }
}

def update_progressbar(x):
    if casting is True and progress['value'] < 100:
        increments = 100/x
        progress['value'] += increments
        # schedule this function to run again in 10ms
        root.after(10, update_progressbar, x)
    else:
        progress['value'] = 0

def cancel():
    global casting
    casting = False

def cast(x):
    global casting
    casting = True
    update_progressbar(x)

def key(event):
# Adding dynamic spellcasts grabbed from dictionary
    if(event.char in myMacros["my_keys"]):
        cancel()
#       print(myMacros["my_keys"][event.char][0])
#       print(myMacros["my_keys"][event.char][1])
        cast(myMacros["my_keys"][event.char][1])

root = Tk()
progress = Progressbar(root, orient = HORIZONTAL, length = 500, mode = 'determinate')
#start_button = Button(root, text="Cast", command=cast)
#cancel_button = Button(root, text="Cancel", command=cancel)

progress.pack(side="top", fill="x")
#start_button.pack(side="left")
#cancel_button.pack(side="left")
root.bind("<Key>",key)
root.mainloop()
Gwanthen
  • 1
  • 1
  • Read [Why is Button parameter “command” executed when declared?](https://stackoverflow.com/questions/5767228/why-is-button-parameter-command-executed-when-declared), this apply to your `.Thread(target=func(370))` also. Read also [use threads to preventing main event loop from “freezing”](https://stackoverflow.com/a/16747734/7414759) as you will see a folloup error message. – stovfl Jan 01 '20 at 15:06
  • ***"I’ll take a look into lambda!"***: I recommend to use the `args=` argument, read [threading.Thread](https://docs.python.org/3/library/threading.html#threading.Thread) – stovfl Jan 01 '20 at 15:21

1 Answers1

0

For this type of problem, you don't need threads and you shouldn't be using a loop. Tkinter already has a loop running: mainloop. Tkinter isn't thread safe, and threads are an advanced topic with lots of pitfalls. In this case threads add more problems than they solve.

Instead, write a function that can be called periodically to update the progressbar, and schedule that function with after. This is the right technique when you want to run some short bit of code in a loop, though it only works if the code you want to run takes far less than a second to run. That is the case here - updating the progressbar takes only a couple milliseconds.

The function would look something like this. Notice how it does a little work, and then schedules itself to run again in the future. When the casting variable has been set to false, or progress has hit 100, the cycle is stopped.

def update_progressbar(x):
    global after_id
    if casting and progress['value'] < 100:
        increments = 100/x
        progress['value'] += increments

        # schedule this function to run again in 10ms
        after_id = root.after(10, update_progressbar, x)
    else:
        progress['value'] = 0

You then need to start this by calling update_progressbar once:

update_progressbar(370)

Here is a complete working example based off of your original code. I've added two buttons, to start and cancel the progressbar.

screenshot

from tkinter import *s
from tkinter.ttk import *
global casting
global after_id
after_id = None

def update_progressbar(x):
    global after_id
    if casting and progress['value'] < 100:
        increments = 100/x
        progress['value'] += increments

        # schedule this function to run again in 10ms
        after_id = root.after(10, update_progressbar, x)
    else:
        progress['value'] = 0


def cancel():
    global casting
    casting = False
    if after_id is not None:
        root.after_cancel(after_id)

def cast():
    global casting
    casting = True
    update_progressbar(370)

root = Tk()
progress = Progressbar(root, orient = HORIZONTAL, length = 500, mode = 'determinate')
start_button = Button(root, text="Cast", command=cast)
cancel_button = Button(root, text="Cancel", command=cancel)

progress.pack(side="top", fill="x")
start_button.pack(side="left")
cancel_button.pack(side="left")

root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • I’ll have a try at this when I get home! I honestly didn’t want to use threads myself but google seemed to indicate that was the way to go but I knew I’d be in over my head. Thanks a lot for taking the time to give me some code to work with! – Gwanthen Jan 01 '20 at 17:25
  • Hey man thanks for the help. Would appreciate it if you could clarify with the updated code underneath the original why, doing my best to implement what you said, the progressbar doesnt get interrupted properly when I press a new macro before the function has completed? Seems to just stack the loop number and increase the speed? – Gwanthen Jan 02 '20 at 02:11
  • @Gwanthen: stackoverflow isn't designed for conversations. By adding completely different code to the original question you make the existing answers confusing since they no longer apply to everything in the question. If you have another question, you should ask it as a separate question. – Bryan Oakley Jan 02 '20 at 02:33
  • @Gwanthen: that being said, I updated the code to show how to cancel a pending call to the function that was previously scheduled with `after` .`after` returns an id which you can pass to `after_cancel`. – Bryan Oakley Jan 02 '20 at 02:35
  • My apologies. I’ll remember that for next time - thanks very much all the same! – Gwanthen Jan 02 '20 at 02:40
  • Am I supposed to be defining after_id anywhere? It seems adding this to my code has made no difference – Gwanthen Jan 02 '20 at 03:48
  • @Gwanthen: I'm sorry, I forgot to add that part. – Bryan Oakley Jan 02 '20 at 03:57