0

My high-level task - change the image on the display from the tests. I'm creating an automatic test for the device with cameras. These cameras directed to the my display. The simplest test looks like this:

  1. Show aruco-marker or picture on the monitor;
  2. Capture image from the cameras;
  3. Detect aruco-marker or expected picture on the images;

Tkinter-related part of code:

import tkinter as tk
from PIL.ImageTk import PhotoImage
from time import sleep
import threading


class StandDisplayManager(tk.Frame):
    def __init__(self):
        super().__init__()
        self.pack()
        self.bind("<<Close>>", quit)

        self.canv = tk.Canvas(self)
        self.canv.config(width=self.winfo_screenwidth(), height=self.winfo_screenheight())
        self.canv.pack()

        self.markers = list()

        global display_manager_is_ready
        display_manager_is_ready = True

    def show_aruco_marker(self, id="0001", x=300, y=300):
        img_name = env_config.TEST_DATA_PREFIX + "/aruco_id_%s.png" % (id)
        imgobj = PhotoImage(file=img_name)

        self.canv.config(bg='white')
        marker_id = self.canv.create_image(x, y, image=imgobj, anchor=tk.NW)
        self.canv.tkraise(marker_id)
        self.markers.append(marker_id)
        self.canv.pack()

        # I need this sleep to demonstrate my problem
        # I can display first marker without any problems
        # But second marker (and other markers) gone after end of this method
        # So, I'm using this sleep just to see the marker
        sleep(1)

    def hide_aruco_markers(self):
        for marker in self.markers:
            self.canv.delete(marker)
        self.markers.clear()

    def close(self):
        self.event_generate("<<Close>>")


def _run_stand_display_manager():
    global display_manager
    display_manager = StandDisplayManager()
    display_manager.mainloop()


def run_stand_display_manager_in_separate_thread():
    display_manager_thread = threading.Thread(
        target=_run_stand_display_manager)
    display_manager_thread.start()
    global display_manager_is_ready
    while not display_manager_is_ready:
        sleep(0.1)

My test:


def test_with_display_manager_example():
    global display_manager
    run_stand_display_manager_in_separate_thread()

    display_manager.show_aruco_marker("0011")
    # display shows marker 0011 and I can capture images and detect it
    sleep(3)
    display_manager.hide_aruco_markers()
    display_manager.show_aruco_marker("0012")
    # Display shows marker 0012 1 second only and hide it after end of method show_aruco_marker
    # So, I can't capture image and detect marker without "sleep" in the method show_aruco_marker
    # Test failed here
    sleep(3)
    display_manager.hide_aruco_markers()
    display_manager.show_aruco_marker("0010")
    display_manager.close()

I can't solve this issue in a pretty way. I found this solution (Update data in a Tkinter-GUI with data from a second Thread) with a queue and AsyncioThread. But I don't like this solution because I need update image on the display immediately (And I can do it immediately, in fact, with ugly "sleep" :) ).

Help me please solve this problem.

Or suggest please more appropriate tool. It will be cool also!

Vadim
  • 1
  • You can't really call tkinter from a different thread. Try changing how the data is handled or have a tkinter loop that looks at a flag/variable and changes the canvas. – TheLizzard Mar 01 '21 at 12:49
  • Thank you for answer! How I can do something in the loop from the tkinter without events? I may try to use some string variable with name of action. – Vadim Mar 01 '21 at 14:28
  • I posted an answer with an example of how you can make a tkinter loop. It still might not solve your problems. – TheLizzard Mar 01 '21 at 14:36

1 Answers1

0

You can use tkinter loops. An example of tkinter loop:

import tkinter as tk

def my_loop():
    # Do some work here:
    print("This is in a loop")
    # After 100 ms call my_loop
    root.after(100, my_loop)

def start_mainloop():
    # After 100 ms call my_loop
    root.after(100, my_loop)

# Create the window
root = tk.Tk()
# Start the loop
start_mainloop()
# You must keep calling `root.update` or call `root.mainloop` once
root.mainloop()

You can use <tkinter object>.after(time_in_ms, function, *arguments) to make a loop where the function calls itself. Note: because of how tkinter handles updates, it isn't a recursive function so you aren't going to get a RecursionError.

TheLizzard
  • 7,248
  • 2
  • 11
  • 31