Вот мой минимально репрезентативный пример (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()
Вы продолжаете говорить tkinter
вызвать check_rqueue
1000 раз в секунду, пока не будут показаны все изображения. Вполне возможно, что он не сможет найти достаточно времени, чтобы правильно нарисовать ttk.Label
. Попробуйте увеличить задержку в обоих вызовах .after()
до 20. Кроме того, если у вас нет привязок, которые можно активировать во время работы .update()
, это должно быть безопасно.
@TheLizzard не должно быть большой разницы от 1 до 20, поскольку сам основной цикл просто циклически повторяется с интервалом примерно 20 мс. Только 0 или более высокий интервал будет иметь значение.
@Thingamabobs Мне удалось отказаться от использования внешнего модуля scrframe.py
, чтобы сократить MRE. Проблема сохраняется. Итак, приведенный выше сценарий — это все, что нужно изучить.
это grey patch
ваш self.label
до того, как tkiner
успеет заполнить его всеми элементами. Python не так быстр, и tkinter
не так быстр (он запускает код на языке tcl
), и у вас есть много миниатюр для отображения (я не имею в виду конвертировать, а только отображать их в окне) - поэтому tkinter
может не успеть Нарисуйте этикетку в короткие сроки. Вы пытались запустить его для меньшего количества изображений? Если вы не можете найти способ его оптимизации, возможно, вам придется использовать другую среду графического интерфейса. то есть. PyQt
@furas Я заметил, что серое пятно self.label ` всегда появляется, когда его поднимают из нижнего положения. Я узнал об этом, закомментировав первую инструкцию понижения в строке 69. При создании self.label
выглядит правильно. Не понимаю, почему self.update_idletasks
не может решить эту проблему, поскольку это проблема перерисовки. Уменьшение его до 5 строк также не может решить эту проблему.
Возможно, найду время на выходных. Но некоторые вещи я бы попробовал быстро исправить self.after(0, self.label.lift)
. Если это не сработает, почему бы не сделать эту метку окном модели, настоящим окном, чтобы исключить его из цикла перерисовки? Также закомментируйте все операторы печати во время тестирования.
Кроме того... почему бы не поднять метку и не вызвать тяжелые вычисления с помощью after
, чтобы убедиться, что метка нарисована правильно?
@Thingamabobs self.label
предназначен для того, чтобы скрыть self.vsf
переходный период его внешнего вида. self.after(0, self.label.lift)
не получилось. Если метка является частью другого окна верхнего уровня, можно ли ее поднять непосредственно перед и в конце потока? Моя цель — использовать get_thumbnails_concurrently_with_queue()
для выполнения тяжелых вычислений, и я не могу сделать это с помощью after
метода или связать его с ним.
Чтобы Tk функционировал правильно, вам необходимо обслуживать его цикл событий, и делать это практически постоянно. Это когда принимаются такие мелочи, как инструкции ОС о том, когда и где на самом деле должно быть нарисовано окно.
Вам следует вызвать функцию tk.mainloop()
.
Рекомендуемое чтение:
ОП уже выполнил root.mainloop()
в коде.
Рекомендуемая вами литература укрепила мое понимание методов update
и update_idletasks
. Спасибо. Да, используется метод mainloop
.
Спасибо @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 Кажется, моя проблема вызвана строкой 103. Без реализации вашего комментария и просто без выполнения the self.update_idletasks()
в цикле for эта проблема исчезнет. Знаете ли вы, почему это так? Кроме того, другая команда self.update_idletasks
в строке 130 кажется лишней. Короче говоря, похоже, что щедрое использование метода .update_idletasks()
тоже может быть проблематичным. Можете ли вы посоветовать, каковы общепринятые практики использования и отказа от использования метода update_idletasks()
?
Представьте, что эти команды кричат вашим сотрудникам. update_idletasks
СЕЙЧАС просто кричит! а update
больше похоже на РАБОТУ! РАБОТА! РАБОТА!. Давление на сотрудников с помощью update
приводит к ошибкам по неосторожности, при этом давая команду СЕЙЧАС! может работать на обе стороны. Однако в большинстве случаев даже команда NOW! это признак плохого управления, и вам следует подумать, нет ли лучшего способа решить эту ситуацию.
Чтобы быть менее абстрактным, я использую update_idletasks
иногда, когда запускаю новое окно, после того, как каждая инструкция помещена в очередь и окно готово/сопоставлено, я сбрасываю неактивные задачи.
Другое использование update_idletasks
: вы определяете размер контейнера и нуждаетесь в реальных значениях, вы сбрасываете задачи, чтобы получить их. Однако winfo_reqwidth()
в большинстве случаев является точным.
Кода гораздо больше, чем я готов изучить. Однако попробуйте использовать опцию состояния с
hidden
иnormal
, если это вам подходит.