Search code examples
pythontkinterpython-3.12

How to update the main window/canvas from a parallel process. Python 3.12


There is a program that simulates the construction of a graph in real time. To update the schedule and speed up the work of the program as a whole, it was decided to use multiprocessing.

The problem is that there is no way to update the main window/canvas from a parallel process.

So, maybe someone has come across something like this or knows how to solve it?

import time
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import pandas as pd
import os
import sys
import inspect
import multiprocessing


def get_script_dir(follow_symlinks=True):
    if getattr(sys, 'frozen', False):
        path = os.path.abspath(sys.executable)
    else:
        path = inspect.getabsfile(get_script_dir)
    if follow_symlinks:
        path = os.path.realpath(path)
    return os.path.dirname(path)


def open_file():
    data_frame = ns.daf
    filepath = filedialog.askopenfilename(initialdir=get_script_dir())
    if filepath != "":
        data_frame = pd.read_csv(filepath, sep="\t")
    ns.daf = data_frame


def set_graf_data(nas, curva):
    # window = ns.window
    graf_value = [[] for _ in range(9)]
    x_value = [[] for _ in range(9)]
    data_frame = nas.daf
    axes = nas.axes
    delay = nas.delay

    line_object = [axes.plot([], [])[0] for _ in range(9)]
    print(data_frame)
    for i in range(len(data_frame[data_frame.columns.tolist()[1]])):
        if len(x_value[1]) == 1000:
            del x_value[1][0]
        x_value[1].append(float(data_frame[data_frame.columns.tolist()[1]].values[i]) * 86400 - float(data_frame[data_frame.columns.tolist()[1]].values[0]) * 86400)
        for j in range(9):
            if len(graf_value[j]) == 1000:
                del graf_value[j][0]

            graf_value[j].append(float(data_frame[data_frame.columns.tolist()[j + 2]].values[i]))

            line_object[j].set_data(x_value[1], graf_value[j])
        axes.relim()
        axes.autoscale()
        axes.set_xlim(x_value[1][-1] - 1000, x_value[1][-1] + 100)
        curva.update()
        time.sleep(1/delay)


def choose_click():
    data_frame = ns.daf
    choose_window = Tk()
    choose_window.columnconfigure(index=0, weight=1)
    choose_window.rowconfigure(index=0, weight=50)
    choose_window.rowconfigure(index=1, weight=1)
    choose_window.title("Choose")
    grafes = data_frame.columns.tolist()[2:]
    grafes_var = Variable(choose_window, value=grafes)
    grafes_listbox = Listbox(choose_window, listvariable=grafes_var)
    grafes_listbox.grid(row=0, column=0, sticky=NSEW)
    btn_choose = ttk.Button(choose_window, text="Confirm", cursor="hand2",
                            command=lambda: (choose_window.destroy()))
    btn_choose.grid(row=1, column=0, sticky=NSEW)
    ns.daf = data_frame


def confirm_time():
    d = float(entry_time.get())
    ns.delay = d


if __name__ == '__main__':

    root = Tk()
    root.title("Emulator")
    # root.geometry("250x200")

    fig, ax = plt.subplots(1, 1)
    plt.ion()

    canvas = FigureCanvasTkAgg(fig, master=root)
    canvas.get_tk_widget().grid(row=2, column=2, columnspan=3, rowspan=40, sticky=NSEW)

    delay_time = 10

    df = pd.DataFrame()

    mgr = multiprocessing.Manager()
    ns = mgr.Namespace()
    ns.daf = df
    ns.axes = ax
    ns.delay = delay_time
    # ns.window = root
    ns.can = canvas

    p1 = multiprocessing.Process(target=set_graf_data, args=(ns, canvas, ))

    btn_open = ttk.Button(text='Open file', command=open_file)
    btn_open.grid(column=0, row=0, sticky=NSEW)

    btn_check = ttk.Button(text='Check', command=choose_click)
    btn_check.grid(column=1, row=0, sticky=NSEW)

    btn_confirm_time = ttk.Button(text='Confirm time', command=confirm_time)
    btn_confirm_time.grid(row=1, column=1, sticky=NSEW)

    entry_time = ttk.Entry()
    entry_time.insert(0, str(1))
    entry_time.grid(row=1, column=0, sticky=E)

    btn_set_data = ttk.Button(text='Start', command=lambda: p1.start())
    btn_set_data.grid(column=2, row=0, sticky=NSEW)

    root.mainloop()

Были попытки передать в параллельный процесс само окно или отдельно canvas, но выскакивает ошибка:

Traceback (most recent call last):
  File "C:\Users\egorov22\PycharmProjects\emulator\main.py", line 104, in <module>
    ns.can = canvas
    ^^^^^^
  File "C:\Users\egorov22\AppData\Local\Programs\Python\Python312\Lib\multiprocessing\managers.py", line 1129, in __setattr__
    return callmethod('__setattr__', (key, value))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\egorov22\AppData\Local\Programs\Python\Python312\Lib\multiprocessing\managers.py", line 820, in _callmethod
    conn.send((self._id, methodname, args, kwds))
  File "C:\Users\egorov22\AppData\Local\Programs\Python\Python312\Lib\multiprocessing\connection.py", line 206, in send
    self._send_bytes(_ForkingPickler.dumps(obj))
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\egorov22\AppData\Local\Programs\Python\Python312\Lib\multiprocessing\reduction.py", line 51, in dumps
    cls(buf, protocol).dump(obj)
TypeError: cannot pickle '_tkinter.tkapp' object

Solution

  • You cannot use or interact with tkinter widgets across processes. That's simply not possible.

    The simplest solution is to set up a multiprocessing queue. The worker process can write to the queue when there is data that needs to be updated, and the UI process can poll that queue using after to pull the data off of the queue and update the UI.

    You have to take care that your polling of the queue doesn't block the UI thread for more than 100ms or so, otherwise the UI will appear sluggish.