Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki
В Tkinter geometry manager занимают все необходимое место в родительском контейнере для размещения виджетов. Но если у этого контейнера фиксированный размер или же он превышает размеры экрана, то появляется область, которая не будет видна пользователям.
Скроллбары в Tkinter автоматически не добавляются, поэтому их нужно создать и добавить как и любой другой виджет. Также нужно учесть, что лишь у некоторых классов виджетов есть параметры, с помощью которых к ним можно добавить скроллбары.
Чтобы это обойти, можно воспользоваться преимуществами виджета Canvas, который позволяет добавить скроллинг в любой контейнер.
Для демонстрации совместной работы классов Canvas и Scrollbar с целью создания изменяемого фрейма со скроллбарами, создадим приложение, которое будет динамически менять размер за счет загрузки изображения.
После нажатия на кнопку «Загрузить изображение» сама она пропадает, а в Canvas загружается изображение, которое больше контейнера. Это может быть любой графический файл.

Это активирует горизонтальный и вертикальный скроллбары, которые будут автоматически подстраиваться при изменении основного окна:

У виджета Canvas есть стандартный интерфейс скроллинга, а также метод create_window(). Важно обратить внимание на то, что этот скрипт предполагает размещение файла python.gif в той же директории:
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.scroll_x = tk.Scrollbar(self, orient=tk.HORIZONTAL)
self.scroll_y = tk.Scrollbar(self, orient=tk.VERTICAL)
self.canvas = tk.Canvas(self, width=300, height=100,
xscrollcommand=self.scroll_x.set,
yscrollcommand=self.scroll_y.set)
self.scroll_x.config(command=self.canvas.xview)
self.scroll_y.config(command=self.canvas.yview)
self.frame = tk.Frame(self.canvas)
self.btn = tk.Button(self.frame, text="Загрузить изображение",
command=self.load_image)
self.btn.pack()
self.canvas.create_window((0, 0), window=self.frame,
anchor=tk.N + tk.W)
self.canvas.grid(row=0, column=0, sticky="nswe")
self.scroll_x.grid(row=1, column=0, sticky="we")
self.scroll_y.grid(row=0, column=1, sticky="ns")
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.bind("", self.resize)
self.update_idletasks()
self.minsize(self.winfo_width(), self.winfo_height())
def resize(self, event):
region = self.canvas.bbox(tk.ALL)
self.canvas.configure(scrollregion=region)
def load_image(self):
self.btn.destroy()
self.image = tk.PhotoImage(file="python.gif")
tk.Label(self.frame, image=self.image).pack()
if __name__ == "__main__":
app = App()
app.mainloop()
Как работают скроллбары в Tkinter
Первые строчки приложения создают скроллбары и присоединяют их к объекту Canvas с помощью параметров xscrollcommand и yscrollcommand, которые ссылаются на метод set() объектов scroll_x и scroll_y соответственно. Этот метод отвечает за перемещение слайдера.
Также нужно настроить параметр command каждого из скроллбаров после определения Canvas:
self.scroll_x = tk.Scrollbar(self, orient=tk.HORIZONTAL)
self.scroll_y = tk.Scrollbar(self, orient=tk.VERTICAL)
self.canvas = tk.Canvas(self, width=300, height=100,
xscrollcommand=self.scroll_x.set,
yscrollcommand=self.scroll_y.set)
self.scroll_x.config(command=self.canvas.xview)
self.scroll_y.config(command=self.canvas.yview)
Есть возможность сначала создать Canvas и настроить его параметры позже, когда уже будут созданы экземпляры скроллбаров.
Следующий шаг — добавить фрейм с помощью метода create_window(). Первый аргумент — положение, где нужно разместить виджет, который в свою очередь передается в аргументе window. Поскольку оси x и y виджета размещаются в верхнем левом углу, разместим виджет в положении (0, 0) и выровняем его в этом углу с помощью anchor=tk.NW (северо-запад):
self.frame = tk.Frame(self.canvas)
# ...
self.canvas.create_window((0, 0), window=self.frame, anchor=tk.NW)
Затем зададим переменный размер для первых строки и колонки с помощью методов rowconfigure() и columnconfigure(). Параметр weight обозначает относительную ширину, для распределения дополнительного пространства. Однако в этом примере нет колонок или рядков для изменения размера.
Связывание с событием <Configure> поможет правильно перенастроить Canvas, когда размер основного окна меняется. Обработка такого типа события работает по тому же принципу, что и события мыши и клавиатуры:
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.bind("<Configure>", self.resize)
В итоге задаем минимальный размер основного окна с текущими шириной и высотой, которые можно получить с помощью методов winfo_width() или winfo_height().
Для получения реального размера контейнера нужно сделать так, чтобы geometry manager прорисовывал все дочерние виджеты в первую очередь с помощью вызова update_idletasks(). Этот виджет доступен во всех классах виджета и он отвечает за то, чтобы Tkinter обработал все события в процессе ожидания: например, перерисовку или новые вычисления размеров:
self.update_idletasks()
self.minsize(self.winfo_width(), self.winfo_height())
Метод resize обрабатывает событие изменения размера окна и обновляет параметр scrollregion, определяющий область Canvas, которую можно скроллить. Чтобы провести вычисления заново, можно использовать метода bbox() с константой ALL. Он возвращает окружающий размер всего виджета Canvas:
def resize(self, event):
region = self.canvas.bbox(tk.ALL)
self.canvas.configure(scrollregion=region)
Tkinter автоматически вызывает несколько событий <Configure> при старте приложения, поэтому нет необходимости вызывать self.resize() в конце метода __init__.
Лишь несколько классов виджетов поддерживают стандартные параметры скроллинга: Listbox, Text и Canvas поддерживают xscrollcommand и yscrollcommand, а Entry — только xscrollcommand. На примере было разобрано, как использовать этот паттерн с Canvas, поскольку это может быть общее решение, но та же структура применима для любых виджетов.
Также нужно отметить, что в данном случае не вызывался geometry manager для прорисовки кадра, поскольку create_window() делает это автоматически. Для лучшей организации класса приложения, можно переместить всю функциональность фрейма и внутренние виджеты в отдельный подкласс Frame.





