Поднятый виджет ttk.Label не может оперативно перерисоваться?

Вот мой минимально репрезентативный пример (MRE) того, как я создаю ttk.Button виджеты с изображением с помощью многопоточного подхода. Однако у меня возникла проблема с задачей, которая возникает перед задачей многопоточности. Всякий раз, когда виджет self.label поднимается, его нельзя быстро перерисовать; на короткое время появляется серое пятно, прежде чем self.label появится полностью. Запуск self.update_idletasks() (см. строку 97) не может решить эту проблему. Только запуск self.update() может решить эту проблему (необходимо раскомментировать строку 98). Однако некоторые полагают, что использование self.update() может быть вредным. Можно ли решить эту проблему без использования self.update()? Если да, то как? Не могли бы вы также объяснить, почему возникает эта проблема? Спасибо.

Демо-версия выпуска:

Демонстрация желаемого результата:

МРЭ:

Пожалуйста, сохраните любой имеющийся у вас файл .jpg в том же каталоге/папке, что и этот скрипт, и переименуйте его в testimage.jpg. Как работает этот графический интерфейс? Нажмите кнопку Run, чтобы начать многопоточность. Для повторного запуска сначала необходимо нажать кнопку Reset, а затем кнопку Run. НЕ НАЖИМАЙТЕ Reset, когда цепочка сообщений продолжается, и наоборот.

# Python modules
import tkinter as tk
import tkinter.ttk as ttk
import concurrent.futures as cf
import queue
import threading
from itertools import repeat
import random
from time import sleep

# External modules
from PIL import Image, ImageTk


def get_thumbnail_c(gid: str, fid: str, fpath: str, psize=(100, 100)):
    # print(f"{threading.main_thread()=} {threading.current_thread()=}")
    with Image.open(fpath) as img:
        img.load()
    img.thumbnail(psize)
    return gid, fid, img


def get_thumbnails_concurrently_with_queue(
        g_ids: list, f_ids: list, f_paths: list, rqueue: queue.Queue,
        size: tuple):
    futures = []
    job_fn = get_thumbnail_c

    with cf.ThreadPoolExecutor() as vp_executor:
        for gid, fids, fpath in zip(g_ids, f_ids, f_paths):
            for gg, ff, pp in zip(repeat(gid, len(fids)), fids,
                                  repeat(fpath, len(fids))):
                job_args = gg, ff, pp, size
                futures.append(vp_executor.submit(job_fn, *job_args))
    for future in cf.as_completed(futures):
        rqueue.put(("thumbnail", future.result()))
        futures.remove(future)
        if not futures:
            print(f'get_thumbnails_concurrently has completed!')
            rqueue.put(("completed", ()))


class GroupNoImage(ttk.Frame):
    def __init__(self, master, gid, fids):
        super().__init__(master, style='gframe.TFrame')
        self.bns = {}
        self.imgs = {}
        for i, fid in enumerate(fids):
            self.bns[fid] = ttk.Button(self, text=f"{gid}-P{i}", compound = "top",
                                       style = "imgbns.TButton")
            self.bns[fid].grid(row=0, column=i, stick = "nsew")

class App(ttk.PanedWindow):
    def __init__(self, master, **options):
        super().__init__(master, **options)
        self.master = master
        self.groups = {}
        self.rqueue = queue.Queue()

        self.vsf = ttk.Frame(self)
        self.add(self.vsf)

        self.label = ttk.Label(
            self, style = "label.TLabel", width=7, anchor = "c", text = "ttk.Label",
            font=('Times', '70', ''))
        self.label.place(
            relx=0.5, rely=0.5, relwidth=.8, relheight=.8, anchor = "center",
            in_=self.vsf)
        self.label.lower(self.vsf)

    def create_grpsframe(self):
        self.grpsframe = ttk.Frame(self.vsf, style='grpsframe.TFrame')
        self.grpsframe.grid(row=0, column=0, sticky = "nsew")

    def run(self, event):
        self.create_grpsframe()
        gids = [f"G{i}" for i in range(50)]
        random.seed()
        fids = []
        for gid in gids:
            f_ids = []
            total = random.randint(2,10)
            for i in range(total):
                f_ids.append(f"{gid}-P{i}" )
            fids.append(f_ids)
        fpaths = ["testimage.jpg" for i in range(len(gids))]
        self.create_groups_concurrently(gids, fids, fpaths)

    def reset(self, event):
        self.grpsframe.destroy()
        self.groups.clear()

    def create_groups_concurrently(self, gids, fids, fpaths):
        print(f"\ncreate_groups_concurrently")

        self.label.lift(self.vsf)
        # self.update_idletasks()  # Can't fix self.label appearance issue
        # self.update()  # Fixed self.label appearance issue

        for i, (gid, f_ids) in enumerate(zip(gids, fids)):
            self.groups[gid] = GroupNoImage(self.grpsframe, gid, f_ids)
            self.groups[gid].grid(row=i, column=0, sticky = "nsew")
            self.update_idletasks()
        # sleep(3)
        print(f"\nStart thread-queue")

        jthread = threading.Thread(
            target=get_thumbnails_concurrently_with_queue,
            args=(gids, fids, fpaths, self.rqueue, (100,100)),
            name = "jobthread")
        jthread.start()
        self.check_rqueue()

    def check_rqueue(self):
        # print(f"\ndef _check_thread(self, thread, start0):")
        duration = 1  # millisecond
        try:
            info = self.rqueue.get(block=False)
            # print(f"{info=}")
        except queue.Empty:
            self.after(1, lambda: self.check_rqueue())
        else:
            match info[0]:
                case "thumbnail":
                    gid, fid, img = info[1]
                    print(f"{gid=} {fid=}")
                    grps = self.groups
                    grps[gid].imgs[fid] = ImageTk.PhotoImage(img)
                    grps[gid].bns[fid]["image"] = grps[gid].imgs[fid]
                    self.update_idletasks()
                    self.after(duration, lambda: self.check_rqueue())
                case "completed":
                    print(f'Completed')
                    self.label.lower(self.vsf)

class ButtonGroups(ttk.Frame):
    def __init__(self, master, **options):
        super().__init__(master, style='bnframe.TFrame', **options)
        self.master = master
        self.bnrun = ttk.Button(
            self, text = "Run", width=10, style='bnrun.TButton')
        self.bnreset = ttk.Button(
            self, text = "Reset", width=10, style='bnreset.TButton')

        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=1)
        self.bnrun.grid(row=0, column=0, sticky = "nsew")
        self.bnreset.grid(row=0, column=1, sticky = "nsew")


if __name__ == "__main__":
    root = tk.Tk()
    root.geometry('1300x600')
    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)

    ss = ttk.Style()
    ss.theme_use('default')
    ss.configure(".", background = "gold")
    ss.configure("TPanedwindow", background = "red")
    ss.configure('grpsframe.TFrame', background='green')
    ss.configure('gframe.TFrame', background='yellow')
    ss.configure('imgbns.TButton', background='orange')
    ss.configure("label.TLabel", background = "cyan")
    ss.configure('bnframe.TFrame', background='white')
    ss.configure('bnrun.TButton', background='violet')
    ss.configure('bnreset.TButton', background='green')

    app = App(root)
    bns = ButtonGroups(root)
    app.grid(row=0, column=0, sticky = "nsew")
    bns.grid(row=1, column=0, sticky = "nsew")

    bns.bnrun.bind("<B1-ButtonRelease>", app.run)
    bns.bnreset.bind("<B1-ButtonRelease>", app.reset)

    root.mainloop()

Кода гораздо больше, чем я готов изучить. Однако попробуйте использовать опцию состояния с hidden и normal, если это вам подходит.

Thingamabobs 17.04.2024 21:39

Вы продолжаете говорить tkinter вызвать check_rqueue 1000 раз в секунду, пока не будут показаны все изображения. Вполне возможно, что он не сможет найти достаточно времени, чтобы правильно нарисовать ttk.Label. Попробуйте увеличить задержку в обоих вызовах .after() до 20. Кроме того, если у вас нет привязок, которые можно активировать во время работы .update(), это должно быть безопасно.

TheLizzard 17.04.2024 21:39

@TheLizzard не должно быть большой разницы от 1 до 20, поскольку сам основной цикл просто циклически повторяется с интервалом примерно 20 мс. Только 0 или более высокий интервал будет иметь значение.

Thingamabobs 17.04.2024 21:47

@Thingamabobs Мне удалось отказаться от использования внешнего модуля scrframe.py, чтобы сократить MRE. Проблема сохраняется. Итак, приведенный выше сценарий — это все, что нужно изучить.

Sun Bear 17.04.2024 23:39

это grey patch ваш self.label до того, как tkiner успеет заполнить его всеми элементами. Python не так быстр, и tkinter не так быстр (он запускает код на языке tcl), и у вас есть много миниатюр для отображения (я не имею в виду конвертировать, а только отображать их в окне) - поэтому tkinter может не успеть Нарисуйте этикетку в короткие сроки. Вы пытались запустить его для меньшего количества изображений? Если вы не можете найти способ его оптимизации, возможно, вам придется использовать другую среду графического интерфейса. то есть. PyQt

furas 17.04.2024 23:45

@furas Я заметил, что серое пятно self.label ` всегда появляется, когда его поднимают из нижнего положения. Я узнал об этом, закомментировав первую инструкцию понижения в строке 69. При создании self.label выглядит правильно. Не понимаю, почему self.update_idletasks не может решить эту проблему, поскольку это проблема перерисовки. Уменьшение его до 5 строк также не может решить эту проблему.

Sun Bear 18.04.2024 00:26

Возможно, найду время на выходных. Но некоторые вещи я бы попробовал быстро исправить self.after(0, self.label.lift). Если это не сработает, почему бы не сделать эту метку окном модели, настоящим окном, чтобы исключить его из цикла перерисовки? Также закомментируйте все операторы печати во время тестирования.

Thingamabobs 18.04.2024 00:40

Кроме того... почему бы не поднять метку и не вызвать тяжелые вычисления с помощью after, чтобы убедиться, что метка нарисована правильно?

Thingamabobs 18.04.2024 00:48

@Thingamabobs self.label предназначен для того, чтобы скрыть self.vsf переходный период его внешнего вида. self.after(0, self.label.lift) не получилось. Если метка является частью другого окна верхнего уровня, можно ли ее поднять непосредственно перед и в конце потока? Моя цель — использовать get_thumbnails_concurrently_with_queue() для выполнения тяжелых вычислений, и я не могу сделать это с помощью after метода или связать его с ним.

Sun Bear 18.04.2024 01:15
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
9
117
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Чтобы Tk функционировал правильно, вам необходимо обслуживать его цикл событий, и делать это практически постоянно. Это когда принимаются такие мелочи, как инструкции ОС о том, когда и где на самом деле должно быть нарисовано окно.

Вам следует вызвать функцию tk.mainloop().


Рекомендуемое чтение:

ОП уже выполнил root.mainloop() в коде.

acw1668 18.04.2024 10:48

Рекомендуемая вами литература укрепила мое понимание методов update и update_idletasks. Спасибо. Да, используется метод mainloop.

Sun Bear 18.04.2024 18:18
Ответ принят как подходящий

Спасибо @Thingamabobs. Ваш комментарий сработал:

почему бы не поднять метку и не вызвать тяжелые вычисления с помощью after to убедиться, что этикетка нарисована правильно?

Моя поправка приведена ниже. Дополнительные вызовы методов self.update_idletasks() и self.update() даже не потребовались. При этом основной цикл событий tkinter также был оперативно обслужен. Хотя я получил желаемый результат, используя метод .after_idle() для этого MRE, я заметил, что метод .after() дает более надежный результат в моем реальном коде.

Поправки:

def run(self, event):
    self.create_grpsframe()
    gids = [f"G{i}" for i in range(50)]
    random.seed()
    fids = []
    for gid in gids:
        f_ids = []
        total = random.randint(2,10)
        for i in range(total):
            f_ids.append(f"{gid}-P{i}" )
        fids.append(f_ids)
    fpaths = ["testimage.jpg" for i in range(len(gids))]
    self.label.lift(self.vsf)  # Put this statement here instead of in the self.create_groups_concurrently method.
    self.after_idle(self.create_groups_concurrently, gids, fids, fpaths)  # Then use the after_idle() method to call the method that start the threads.

Рад видеть, что это сработало для вас.

Thingamabobs 18.04.2024 20:16

@Thingamabobs Кажется, моя проблема вызвана строкой 103. Без реализации вашего комментария и просто без выполнения the self.update_idletasks() в цикле for эта проблема исчезнет. Знаете ли вы, почему это так? Кроме того, другая команда self.update_idletasks в строке 130 кажется лишней. Короче говоря, похоже, что щедрое использование метода .update_idletasks() тоже может быть проблематичным. Можете ли вы посоветовать, каковы общепринятые практики использования и отказа от использования метода update_idletasks()?

Sun Bear 20.04.2024 05:09

Представьте, что эти команды кричат ​​вашим сотрудникам. update_idletasks СЕЙЧАС просто кричит! а update больше похоже на РАБОТУ! РАБОТА! РАБОТА!. Давление на сотрудников с помощью update приводит к ошибкам по неосторожности, при этом давая команду СЕЙЧАС! может работать на обе стороны. Однако в большинстве случаев даже команда NOW! это признак плохого управления, и вам следует подумать, нет ли лучшего способа решить эту ситуацию.

Thingamabobs 20.04.2024 11:46

Чтобы быть менее абстрактным, я использую update_idletasks иногда, когда запускаю новое окно, после того, как каждая инструкция помещена в очередь и окно готово/сопоставлено, я сбрасываю неактивные задачи.

Thingamabobs 20.04.2024 11:52

Другое использование update_idletasks: вы определяете размер контейнера и нуждаетесь в реальных значениях, вы сбрасываете задачи, чтобы получить их. Однако winfo_reqwidth() в большинстве случаев является точным.

Thingamabobs 20.04.2024 12:07

Другие вопросы по теме

Похожие вопросы