I'm trying to use curses for a class project where we need to use python
The premise is a server/client chat. I was trying to implement it such that the incoming messages from the server would be displayed above the message prompt. I was able to circumvent the python input() function pausing the program with a second thread displaying the messages I decided to use curses to print things at different parts of the screen.
when I tried multi threading with curses I it had a lot of issues with the input cursor. It used displayed the characters that were being input to the program right next to the incoming chat text despite the cursor visually moving back on the screen to the input area
I've isolated the code that's supposed to make this happen:
import threading
from time import sleep
import curses
screen = curses.initscr()
screen.clear()
screen.refresh()
num_rows, num_cols = screen.getmaxyx()
#prints what would be chat to the top of the screen
def thread_delay(thread_name, delay):
i = 0
while i < 10:
#is supposed to save the cursor's location before moving it
global x_location, y_location
x_location, y_location = curses.getsyx()
screen.addstr(0+i, 0, "stuff")
screen.refresh()
#is supposed to restore the cursor to the input location
curses.setsyx(x_location, y_location)
curses.doupdate()
sleep(1)
i+=1
#gets and displays input to the bottom of the screen
def my_raw_input(stdscr, r, c, prompt_string):
curses.echo()
stdscr.addstr(r, c, prompt_string)
stdscr.refresh()
global input
input = stdscr.getstr(r + 1, c, 20)
return # ^^^^ reading input at next line
#threads
t1 = threading.Thread(target = thread_delay, args = ('t1', 1,))
t2 = threading.Thread(target = my_raw_input, args = (screen, num_rows - 4, 0, "type what you want"))
t1.start()
t2.start()
t1.join()
t2.join()
curses.napms(3000)
curses.endwin()
#for debugging
print(input.decode('utf-8'))
Picture of what is happening vs what's supposed to happen:
I guess your class is over, so it might not be useful anymore...
There are several problems....
first problem is that you are not accepting curses text input, and instead using standard input. The curses module, works by moving the cursor around and adding a character, so wherever your last addstr or addch command ends is where the cursor is positioned, and will next add text. So you could possibly move the cursor back to where you want the input to go, after writing to the screen or you could use curses to capture keyboard input.
Secondly using curses in two processes at the same time is probably not a good idea, since it bypasses the GIL. Instead, you could split your code threads into a server and a curses TUI, and share a global send queue and a receive queue from the multiprocessing.Manager to transfer messages.
Then your server thread can send messages from the send_queue with a .get() and receive messages and putting them in the receive_queue with a .put(). Conversely your curses thread can display messages from the recv_queue with a .get() and put a string composed from curses.getch() or curses.getkey() into the send_queue with a .put()
also consider using the curses.wrapper to simplify entering and exiting your curses features. I think you probably didn't use this, because you tried to use curses over multiple processes in which case a wrapper might not work so well.
I'm sure there are other and probably better ways to do it, but this is just one of many. But I hope this helps someone, or encourages someone more experienced to contribute a better answer.
Related
I wanted to write a program that moves the mouse cursor and presses the shift key so that the computer won't be locked. The Problem is I want that if the user doesn't set two variables they should be automatically 0 and 1 but it seems I am missing something. The other thing that I can't understand is when I start the program if I don't click on it everything is ok, but if I click on the program's window it says "Not Responding" but the program is running. I can't understand what I am doing wrong here. Below is my code:
#!/usr/bin/python
import pyautogui
import time
import sys
import os
import PySimpleGUI as sg
from datetime import datetime
def main():
sg.theme('DarkAmber')
layout = [ [sg.Text('Please enter the time intervall between the movements:', size = (45,1)), sg.Input(key='-IT-', enable_events=True)],
[sg.Text('Please enter how long should the script run:', size = (45,1)), sg.Input(key='-DURATION-', enable_events=True)],
[sg.Button('Start'), sg.Button('Stop')],
[sg.Output(size=(60,15))] ]
window = sg.Window('Press to start to move!', layout, size=(450,250), element_justification='right')
while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Stop'):
break
if event == '-IT-' and values['-IT-'] and values['-IT-'][-1] not in ('0123456789'):
window['-IT-'].update(values['-IT-'][:-1])
if event == '-DURATION-' and values['-DURATION-'] and values['-DURATION-'][-1] not in ('0123456789'):
window['-DURATION-'].update(values['-DURATION-'][:-1])
elif event == 'Start':
if values['-IT-'] == "" and values['-DURATION-'] == "":
window['-IT-'].update(1)
window['-DURATION-'].update(0)
elif values['-IT-'] != "" and values['-DURATION-'] == "":
window['-DURATION-'].update(0)
elif values['-IT-'] == "" and values['-DURATION-'] != "":
window['-IT-'].update(1)
move(numMin=int('0'+values['-IT-']), numDuration=int('0'+values['-DURATION-']))
window.close()
main()
Edit:
I've just cut out the unnecessary part of the code.
So if I'm not misunderstanding, I need to use a thread, the second question is for me unclear can I typecast an empty string without adding zero (0) to it? Because when I try just typecast it doesn't work, and if I add a zero the behavior of the code is changing.
Operations That Take a "Long Time"
If you're a Windows user you've seen windows show in their title bar "Not Responding" which is soon followed by a Windows popup stating that "Your program has stopped responding". Well, you too can make that message and popup appear if you so wish! All you need to do is execute an operation that takes "too long" (i.e. a few seconds) inside your event loop.
You have a couple of options for dealing this with. If your operation can be broken up into smaller parts, then you can call Window.Refresh() occasionally to avoid this message. If you're running a loop for example, drop that call in with your other work. This will keep the GUI happy and Window's won't complain.
If, on the other hand, your operation is not under your control or you are unable to add Refresh calls, then the next option available to you is to move your long operations into a thread.
The "Old Way"
There are a couple of demo programs available for you to see how to do this. You basically put your work into a thread. When the thread is completed, it tells the GUI by sending a message through a queue. The event loop will run with a timer set to a value that represents how "responsive" you want your GUI to be to the work completing.
The "New Way" - Window.write_event_value
This new function that is available currently only in the tkinter port as of July 2020 is exciting and represents the future way multi-threading will be handled in PySimpleGUI (or so is hoped).
Previously, a queue was used where your event loop would poll for incoming messages from a thread.
Now, threads can directly inject events into a Window so that it will show up in the window.read() calls. This allows a your event loop to "pend", waiting for normal window events as well as events being generated by threads.
You can see this new capability in action in this demo: Demo_Multithreaded_Write_Event_Value.py
Here is that program for your inspection and education. It's SO nice to no longer poll for threaded events.
import threading
import time
import PySimpleGUI as sg
"""
Threaded Demo - Uses Window.write_event_value communications
Requires PySimpleGUI.py version 4.25.0 and later
This is a really important demo to understand if you're going to be using multithreading in PySimpleGUI.
Older mechanisms for multi-threading in PySimpleGUI relied on polling of a queue. The management of a communications
queue is now performed internally to PySimpleGUI.
The importance of using the new window.write_event_value call cannot be emphasized enough. It will hav a HUGE impact, in
a positive way, on your code to move to this mechanism as your code will simply "pend" waiting for an event rather than polling.
Copyright 2020 PySimpleGUI.org
"""
THREAD_EVENT = '-THREAD-'
cp = sg.cprint
def the_thread(window):
"""
The thread that communicates with the application through the window's events.
Once a second wakes and sends a new event and associated value to the window
"""
i = 0
while True:
time.sleep(1)
window.write_event_value('-THREAD-', (threading.current_thread().name, i)) # Data sent is a tuple of thread name and counter
cp('This is cheating from the thread', c='white on green')
i += 1
def main():
"""
The demo will display in the multiline info about the event and values dictionary as it is being
returned from window.read()
Every time "Start" is clicked a new thread is started
Try clicking "Dummy" to see that the window is active while the thread stuff is happening in the background
"""
layout = [ [sg.Text('Output Area - cprint\'s route to here', font='Any 15')],
[sg.Multiline(size=(65,20), key='-ML-', autoscroll=True, reroute_stdout=True, write_only=True, reroute_cprint=True)],
[sg.T('Input so you can see data in your dictionary')],
[sg.Input(key='-IN-', size=(30,1))],
[sg.B('Start A Thread'), sg.B('Dummy'), sg.Button('Exit')] ]
window = sg.Window('Window Title', layout, finalize=True)
while True: # Event Loop
event, values = window.read()
cp(event, values)
if event == sg.WIN_CLOSED or event == 'Exit':
break
if event.startswith('Start'):
threading.Thread(target=the_thread, args=(window,), daemon=True).start()
if event == THREAD_EVENT:
cp(f'Data from the thread ', colors='white on purple', end='')
cp(f'{values[THREAD_EVENT]}', colors='white on red')
window.close()
if __name__ == '__main__':
main()
Multithreaded Programs
While on the topic of multiple threads, another demo was prepared that shows how you can run multiple threads in your program that all communicate with the event loop in order to display something in the GUI window. Recall that for PySimpleGUI (at least the tkinter port) you cannot make PySimpleGUI calls in threads other than the main program thread.
The key to these threaded programs is communication from the threads to your event loop. The mechanism chosen for these demonstrations uses the Python built-in queue module. The event loop polls these queues to see if something has been sent over from one of the threads to be displayed.
You'll find the demo that shows multiple threads communicating with a single GUI is called:
Demo_Multithreaded_Queued.py
Once again a warning is in order for plain PySimpleGUI (tkinter based) - your GUI must never run as anything but the main program thread and no threads can directly call PySimpleGUI calls.
I have an issue with Pool.map in combination with the curses module of Python. Whenever I compute bigger workloads with Pool.map my curses UI breaks: it does not react upon the default screen's getch anymore. Instead of reading in any pressed key instantly (and continuing with parsing it) I can press any amount of keys until I hit enter. Sometimes (in addition to that) even the UI breaks (like showing a fraction of my normal shell).
Curses UI wrapper
This is a wrapper class (Screen) that handles the curses UI stuff for me:
# -*- coding: utf-8 -*-
import curses
class Screen(object):
def __init__(self):
# create a default screen
self.__mainscr = curses.initscr()
self.__stdscr = curses.newwin(curses.LINES - 2, curses.COLS - 2, 1, 1)
self.__max_height, self.__max_width = self.__stdscr.getmaxyx()
# start coloring
curses.start_color()
curses.use_default_colors()
# define colors
curses.init_pair(1, 197, -1) # red
curses.init_pair(2, 227, -1) # yellow
curses.init_pair(3, curses.COLOR_MAGENTA, -1)
curses.init_pair(4, curses.COLOR_GREEN, -1) # darkgreen
curses.init_pair(5, curses.COLOR_BLUE, -1)
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_WHITE)
curses.init_pair(7, curses.COLOR_WHITE, -1)
curses.init_pair(8, curses.COLOR_CYAN, -1)
curses.init_pair(9, 209, -1) # orange
curses.init_pair(10, 47, -1) # green
def add_str_to_scr(self, add_str: str, colorpair: int = 7):
self.__stdscr.addstr(str(add_str), curses.color_pair(colorpair))
def linebreak(self):
self.__stdscr.addstr("\n")
def clear_screen(self):
self.__stdscr.clear()
def refresh_screen(self):
self.__stdscr.refresh()
def wait_for_enter_or_esc(self):
curses.noecho()
while True:
c = self.__stdscr.getch()
if c == 10 or c == 27: # 10: Enter, 27: ESC
break
curses.echo()
def get_user_input_chr(self) -> str:
return chr(self.__stdscr.getch())
def get_user_input_str(self) -> str:
return self.__stdscr.getstr().decode(encoding="utf-8")
Actual program
I've written a small example, since the mentioned failure always happen when I combine Pool.map within a curses UI and have high workload. The code just computes some useless mult and add stuff on a numpy array.
import curses
from screen import Screen
from multiprocessing import Pool, cpu_count
import numpy as np
s = Screen() # initializing my Screen wrapper
np.random.seed(1234) # setting the rng fixed to make results comparable
# worker function to simulate workload
def worker(arr):
return arr * 2 + 1
s.clear_screen() # cleans the screen
s.refresh_screen() # displays current buffer's content
s.add_str_to_scr("Start processing data...")
s.linebreak()
s.linebreak()
s.refresh_screen()
# data to feed worker function with (sliced by rows)
data_arr = np.random.rand(8, int(1e7)) # <-- big array for high workload
with Pool(cpu_count()) as p:
buffer = p.map(worker, [data_arr[row] for row in np.ndindex(data_arr.shape[0])])
s.add_str_to_scr("...finished processing:")
s.linebreak()
s.linebreak()
s.refresh_screen()
for row in buffer:
s.add_str_to_scr(row[0:3])
s.linebreak()
s.refresh_screen()
# *Here* the program should wait until the user presses *any* key
# and continue INSTANTLY when any key gets pressed.
# However, for big workloads, it does not react to single key presses,
# but wait for any amount of keys pressed until you hit 'Enter'
s.get_user_input_chr()
curses.endwin()
Now, when I execute the code with a high workload (i.e. crunching an array of shape (8, int(1e7) equals 8 rows with 10,000,000 columns) curse's getch breaks and I get this behavior:
As you can see, I can hit q (or any other key) as often as I want, but curse's getch does not react. I have to press the Enter key to make it recognize the input.
Moreover the first line gets overwritten with my original shell's output for some reason.
This behavior only happens, when the calculation of Pool.map roughly needs 1 second or longer.
When I set data_arr to a small array like np.random.rand(8, 100) everything works like a charm, but as soon as I feed big arrays where the computation takes like >= 1second, this weird bug appears and breaks my curses UI.
Any ideas?
Is Pool.map not joining the worker processes correctly somehow?
The program is doing what you told it to do:
you're calling initscr (and ignoring the fact that curses creates a top-level window),
then creating a subwindow which covers most of the screen,
printing several lines on the screen, refreshing the display after each line, and
finally at the end, waiting for input from the subwindow.
However, your program doesn't call cbreak, raw, etc., which would let you read an unbuffered (no "Enter" pressed) character. Also, the program doesn't turn off echo. If the load is light, you won't notice, since response is fast. But under heavy load, e.g., swapping or high memory/CPU usage, it'll still be recovering when it gets to the prompt. So you notice.
Regarding the screen size, perhaps you meant
self.__stdscr = curses.newwin(curses.LINES - 1, curses.COLS - 1, 0, 0)
but supposing that you intended to leave "empty" space around the window, you could improve things by doing
self.__mainscr.refresh()
immediately after initscr (which would erase the screen).
I am attempting for a homework assignment to implement Simon Says in python. I'm trying to do it using the turtle library (a requirement).
However, I've run into a stumbling block in that while I can get the screen to register click events (currently just printing the x,y coordinates) I can't get it to wait for a click event.
Specifically what I'm planning on doing is having areas on the screen that when they click within that location it is considered as if they had clicked a button. Screen clears and game does whatever.
However, in experiments in trying to get a working 'button' all that it does is set it so it prints the x,y coordinates but the rest of the program finishes. Didn't wait for the user to click anything. I tried a blocking method of...
while clicked == False:
pass
or
while clicked == False:
time.sleep(1)
but both methods hangs the program until I manually interrupt and then it'll print the clicks.
Am I missing an option somewhere?
Turtles don´t have buttons, but they do have callbacks for clicks.
Furthermore, you should use onclick for Screen to detect general clicks and onclick for turtle to detect clicking in turtles. You can, for example, make a 4 BIG turtles with different colors by using a dynamic shape.
Also, turtle is based on Tk, so you must be aware of things like mainloop()
The following program give some hints for Python 2.7.5.
import turtle as t
from random import randint
class MyTurtle(t.Turtle) :
def __init__(self,**args) :
t.Turtle.__init__(self,**args)
def mygoto(self,x,y) :
t1.goto(x,y)
print x,y
def randonics(self,x,y) :
self.left(randint(90,270))
def minegoto(x,y) :
print x,y
t1.goto(x,y)
wt=t.Screen()
t1=MyTurtle()
wt.register_shape("big",((0,0),(30,0),(30,30),(0,30)))
t1.shape("big")
wt.onclick(t1.mygoto,btn=1)
wt.onclick(minegoto,btn=2)
t1.onclick(t1.randonics,btn=3)
t1.goto(100,100)
t.mainloop()
So after extensive search there isn't necessarily a way pause execution of the code in python while using turtle to wait for some click event. Maybe in Tk I could do that but not in turtle.
However, there is a way to get around that. As an example. A method sets up the fake button on the screen, sets the click event, and terminates. The click event when clicked calls the next method needed for execution. So until the button is clicked the actual code isn't doing anything but remains in memory for use.
So more specifically.
1. Create a 'button'.
2. Have your program behave normally until it needs to wait for a click event.
3. Set up the on screen click (or on turtle) in such a way when the 'button' is clicked the next part of the code is run.
Special note. The code in question can't depend on waiting for a click event for later on in code. Instead, the click causes the next part of the execution of your code.
You can make the function registered with onclick() test the x,y position. If it is inside some region you do whatever you must.
I don´t see the difference between what you want to do and what this code does, the modification of turtle position is just an example, you can do anything when a click is captured by onclick(), even start a thread if you really need it (using Creating Threads in python)
import turtle as t
from random import randint
from threading import Thread
from time import sleep
def threaded_function(arg,t1):
for i in range(arg):
print "running",i
sleep(1)
t1.forward(i*10)
def minegoto(x,y) :
print x,y
t1.goto(x,y)
thread = Thread(target = threaded_function, args = (10,t1 ))
thread.start()
thread.join()
print "thread finished...exiting"
wt=t.Screen()
t1=t.Turtle()
wt.register_shape("big",((0,0),(30,0),(30,30),(0,30)))
t1.shape("big")
wt.onclick(minegoto,btn=1)
t1.goto(100,100)
t.mainloop()
I'm trying to learn how to use the thread module. I followed along with the instructions here: http://effbot.org/zone/tkinter-threads.htm
My hope is the test script will:
Print out the "count" every two seconds
Show a pop-up dialog window (also every 2 seconds)
The pop-ups should be allowed to accumulate (if I don't click "OK" for a while, there should be
multiple pop-ups)
However, when I run this script it will freeze the main window and after a while crash. I think I'm not implementing the thread module correctly.
Could someone please have a look and point out what I'm doing wrong?
Here is what I've tried so far:
from Tkinter import *
import thread
import Queue
import time
class TestApp:
def __init__(self, parent):
self.super_Parent = parent
self.main_container = Frame(parent)
self.main_container.pack()
self.top_frame = Frame(self.main_container)
self.top_frame.pack(side=TOP)
self.bottom_frame = Frame(self.main_container)
self.bottom_frame.pack(side=TOP)
self.text_box = Text(self.top_frame)
self.text_box.config(height=20, width=20)
self.text_box.pack()
self.queue = Queue.Queue()
self.update_me()
def show_popup(self):
self.my_popup = Toplevel(self.main_container)
self.my_popup.geometry('100x100')
self.popup_label = Label(self.my_popup, text="Hello!")
self.popup_label.pack(side=TOP)
self.pop_button = Button(self.my_popup, text="OK", command=self.my_popup.destroy)
self.pop_button.pack(side=TOP)
def write(self, line):
self.queue.put(line)
def update_me(self):
try:
while 1:
line = self.queue.get_nowait()
if line is None:
self.text_box.delete(1.0, END)
else:
self.text_box.insert(END, str(line))
self.text_box.see(END)
self.text_box.update_idletasks()
except Queue.Empty:
pass
self.text_box.after(100, self.update_me)
def pipeToWidget(input, widget):
widget.write(input)
def start_thread():
thread.start_new(start_test, (widget,))
def start_test(widget):
count = 0
while True:
pipeToWidget(str(count) + "\n", widget)
count += 1
time.sleep(2)
widget.show_popup()
root = Tk()
widget = TestApp(root)
start_button = Button(widget.bottom_frame, command=start_thread)
start_button.configure(text="Start Test")
start_button.pack(side=LEFT)
root.title("Testing Thread Module")
root.mainloop()
I can't reproduce your problem, but I can see why it would happen.
You're using the queue to pass messages from the background thread to the main thread for updating text_box, which is correct. But you're also calling widget.show_popup() from the background thread, which means it creates and displays a new Toplevel in the background thread. That's not correct.
All UI code must run in the same thread—not all UI code for each top-level window, all UI code period. On some platforms, you may get away with running each window in its own thread (or even free-threading everything), but that isn't supposed to work, and definitely will crash or do improper things on some platforms. (Also, that single UI thread has to be the initial thread on some platforms, but that isn't relevant here.)
So, to fix this, you need to do the same dance for creating the popups that you do for updating the textbox.
The obvious way to do that is to move the widget.show_popup() to the loop in update_me(). If you want it to happen 2 seconds after the textbox updates, just add self.top_frame.after(2000, self.show_popup) to the method.
But I'm guessing you're trying to teach yourself how to have multiple independent updating mechanisms, so telling you "just use a single update queue for everything" may not be a good answer. In that case, just create two queues, and a separate update method servicing each queue. Then, do your pipeToWidget, sleep 2 seconds, then pipeToPopup.
Another way around this is to use mtTkinter. It basically does exactly what you're doing, but makes it automatic, pushing each actual Tk GUI call onto a queue to be run later by the main loop. Of course your objects themselves have to be thread-safe, and this also means that you have to deal with the GUI calls from one thread getting interleaved with calls from another thread. But as long as neither of those is a problem (and they don't seem to be in your case), it's like magic.
If you want to know why this is freezing and/or crashing for you on Win7 and not for me on OS X 10.8… well, you really need to look into a mess of Tcl, C, and Python code, and also at how each thing is built. And, unless it's something simple (like your Tk build isn't free-threaded), it wouldn't tell you much anyway. The code isn't supposed to work, and if it seems to work for me… that probably just means it would work every time until the most important demo of my career, at which point it would fail.
Using Win32GUI and Watsup, I'm writing a bit of Python code to automate a search across a database that is accessed through a program that doesn't come with an interface for it. As such, I can take a string from a list and then input it into the search box and press 'lookup'.
However, when the search returns more than 1000 results, the program throws a warning dialog --which is simply a notification of the number of results--which halts the execution of the Python code. I can't get the code to progress past the line where it presses lookup.
At a guess, this would be because it doesn't expect a window or know how to handle a warning--but I don't either, other than manually accepting it. Below is the relevent sample of code, though it's probably not very enlightening. After "clickButton(LookupButton)", the execution halts.
LookupButtonlocation = elemstring.find("Lookup", AuthNameFieldlocation) - 15
#Use Regex search to find handles
number_regex = re.compile(';(\d+);')
AuthNameEdit = int(number_regex.search(elemstring[AuthNameFieldlocation:]).group(1))
LookupButton = int(number_regex.search(elemstring[LookupButtonlocation:]).group(1))
#Input new Author into Edit Field
setEditText(AuthNameEdit, "John Campbell")
#Click lookup button
clickButton(LookupButton)
I'm not a WATSUP user, but I do something very similar using pywinauto - in my case I'm running a number of automated tests that open various 3rd party programs that, in a similar way, throw up inconvenient warning dialogs. It's a bit difficult to deal with dialogs that you don't know about, however if you do know which dialogs appear, but not when they appear, you can start a thread to just deal with those pop-ups. The following is a simple example from what I'm doing, and uses pywinauto but you could adapt the approach for WATSUP:
import time
import threading
class ClearPopupThread(threading.Thread):
def __init__(self, window_name, button_name, quit_event):
threading.Thread.__init__(self)
self.quit_event = quit_event
self.window_name = window_name
self.button_name = button_name
def run(self):
from pywinauto import application, findwindows
while True:
try:
handles = findwindows.find_windows(title=self.window_name)
except findwindows.WindowNotFoundError:
pass #Just do nothing if the pop-up dialog was not found
else: #The window was found, so click the button
for hwnd in handles:
app = application.Application()
app.Connect(handle=hwnd)
popup = app[self.window_name]
button = getattr(popup, self.button_name)
button.Click()
if self.quit_event.is_set():
break
time.sleep(1) #should help reduce cpu load a little for this thread
Essentially this thread is just an infinite loop that looks for a pop-up window by name, and if it finds it, it clicks on a button to close the window. If you have many pop-up windows you can open one thread per popup (bug that's not overly efficient, though). Because it's an infinite loop, I have the thread looking to see if an event is set, to allow me to stop the thread from my main program. So, in the main program I do something like this:
#Start the thread
quit_event = threading.Event()
mythread = ClearPopupThread('Window Popup Title', 'Yes button', quit_event)
# ...
# My program does it's thing here
# ...
# When my program is done I need to end the thread
quit_event.set()
This is not necessarily the only way to deal with your issue, but is a way that's worked for me. Sorry I can't really help you much with dealing with WATSUP (I always found pywinauto a bit easier to use), but I noticed on the WATSUP homepage (http://www.tizmoi.net/watsup/intro.html), Example 2 does something similar without using threads, i.e., looks for a named window and clicks a specific button on that window.