Tkinter для утилиты: filedialog, Listbox, Checkbutton и переменные, сохранение настроек в JSON

Вспомните: в прошлом уроке вы собрали первое окно — надпись Label, поле Entry, кнопку Button с функцией в command, раскладку grid/pack и сообщение messagebox. А в главе 1 сортировщик «Загрузок» раскладывал файлы по папкам и хранил настройки в settings.json. Сейчас соединим одно с другим: у сортировщика появится окно, где папку выбирают мышкой, файлы видны списком, а режим «только показать» включается галочкой и запоминается до следующего запуска.

🎯 После урока сможете: связать поле и галочку с переменной StringVar/BooleanVar и читать её через get(), показать список файлов в Listbox и узнать, что выбрал человек, открыть стандартный диалог выбора папки filedialog.askdirectory, сохранить последнюю папку и положение галочки в JSON и восстановить их при запуске.

Окно Windows с заголовком «Сортировщик Загрузок». Верхняя строка: кнопка «Выбрать папку» и справа надпись с путём «C:/Users/ученик/Downloads». Ниже белый список на восемь строк, заполнены пять, по алфавиту: «архив.zip -> Архивы», «договор.pdf -> Документы», «кот.jpg -> Картинки», «отчёт.docx -> Документы», «фото.PNG -> Картинки». Под списком отмеченная галочка «Только показать, не переносить» и кнопка «Разобрать». Внизу строка состояния «Найдено файлов: 5».

Переменные Tkinter: StringVar и BooleanVar

До сих пор текст из поля вы забирали методом entry.get(), а надпись меняли через label.config(text=...). Когда одно и то же значение показывают несколько виджетов или его надо сохранить в файл, удобнее завести переменную Tkinter — объект, который хранит значение и сам обновляет связанные с ним виджеты.

  • tk.StringVar() — хранит строку: путь к папке, текст состояния;
  • tk.BooleanVar() — хранит True или False: включена ли галочка;
  • var.get() — прочитать значение, var.set(значение) — записать;
  • value= при создании задаёт начальное значение: tk.StringVar(value="Загрузки").

Переменную создают после root = tk.Tk(): ей нужно уже существующее окно. Связь с виджетом задаётся параметром: у Entry и Label — textvariable=. Теперь set() меняет текст на экране, а то, что человек напечатал в поле, читается через get().

import tkinter as tk

root = tk.Tk()
folder = tk.StringVar(value="Загрузки")
entry = tk.Entry(root, textvariable=folder)
status = tk.Label(root, textvariable=folder)
entry.pack()
status.pack()

print(entry.get())
folder.set("Рабочий стол")
print(entry.get(), "|", status.cget("text"))

Вывод:

Загрузки
Рабочий стол | Рабочий стол

Одна строка folder.set(...) поменяла и поле, и надпись. Метод cget("text") читает текущий текст надписи — здесь он нужен, чтобы показать это в выводе.

В примерах урока нет root.mainloop(): так их можно запустить и сразу увидеть вывод в терминале. В настоящей программе mainloop() — последняя строка, иначе окно не покажется. И root.destroy() в конце здесь можно не писать: в уроке 3.2 он аккуратно закрывал окно, но когда программа доходит до последней строки, окно закрывается вместе с ней.

Проверьте себя

Контрольный вопрос. Какой объект подходит, чтобы хранить состояние галочки «Только показать» — включена она или нет?

Аtk.StringVar
Бtk.BooleanVar
Вtk.Listbox
Гtk.Entry

Подсказка: Галочка может быть только в двух состояниях. Какая переменная хранит True или False?

Онлайн-проверка ответа появится позже

Задание. Функцию reset() программа вызвала сама, без нажатия. Что выведет print? Введите текст вывода.

Исходный код для этого задания:

import tkinter as tk

root = tk.Tk()
folder = tk.StringVar(value="Загрузки")
entry = tk.Entry(root, textvariable=folder)
entry.pack()

def reset():
    folder.set("Видео")

reset()
print(entry.get())

Подсказка: С какой переменной связано поле через textvariable и что с этой переменной сделала reset()?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Checkbutton: галочка «Только показать»

tk.Checkbutton — квадратик с подписью. Его состояние хранит переменная из параметра variable=: отмечен — в ней True, снят — False. Параметр command= работает как у кнопки: функция вызывается при каждом щелчке, после того как переменная уже поменялась.

import tkinter as tk

root = tk.Tk()
dry_run = tk.BooleanVar(value=True)

def on_toggle():
    if dry_run.get():
        print("Режим: только показать")
    else:
        print("Режим: переносить файлы")

check = tk.Checkbutton(root, text="Только показать, не переносить",
                       variable=dry_run, command=on_toggle)
check.pack()

check.invoke()   # то же, что щелчок мышью
check.invoke()

Вывод:

Режим: переносить файлы
Режим: только показать

Метод invoke() делает то же, что щелчок мышью: переключает галочку и вызывает command. Им удобно проверять программу без окна. Галочка была включена, первый щелчок её снял, второй вернул.

В variable= передают саму переменную dry_run, а не dry_run.get(). get() вернёт просто True, и галочка ни с чем не будет связана. То же правило, что с command=on_toggle без скобок: передаём объект, а не результат его вызова.

Проверьте себя

Задание. Что выведет программа? Введите значение, как его печатает Python.

Исходный код для этого задания:

import tkinter as tk

root = tk.Tk()
flag = tk.BooleanVar(value=False)
check = tk.Checkbutton(root, text="Только показать", variable=flag)
check.pack()

check.invoke()
check.invoke()
check.invoke()
print(flag.get())

Подсказка: Каждый invoke() переключает галочку на противоположное состояние. С какого значения начали?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Галочка на экране ставится и снимается, но print(dry_run.get()) в обработчике всегда печатает одно и то же. Какая строка создаёт Checkbutton правильно?

Исходный код для этого задания:

dry_run = tk.BooleanVar(value=True)
Аtk.Checkbutton(root, text=»Только показать», variable=dry_run.get())
Бtk.Checkbutton(root, text=»Только показать», variable=dry_run)
Вtk.Checkbutton(root, text=»Только показать», textvariable=dry_run)
Гtk.Checkbutton(root, text=»Только показать», command=dry_run)

Подсказка: Какой параметр связывает галочку с переменной и что в него нужно передать — объект или его значение?

Онлайн-проверка ответа появится позже

Listbox: список файлов и выбор строки

tk.Listbox показывает строки столбиком, по одной на элемент. Основные методы:

  • lb.insert(tk.END, текст) — добавить строку в конец; tk.END означает «после последней». Вместо tk.END можно дать номер: lb.insert(0, текст) вставит строку в начало;
  • lb.delete(0, tk.END) — удалить всё от первой строки до последней (очистить список перед новым заполнением);
  • lb.size() — сколько строк в списке;
  • lb.get(i) — текст строки номер i (нумерация с нуля, как у списка Python);
  • lb.curselection() — номера выделенных строк кортежем, например (2,); ничего не выделено — пустой кортеж ().

Параметр height= при создании задаёт, сколько строк видно без прокрутки. Функцию заполнения сделаем отдельной: она получает список имён и сам Listbox параметрами, поэтому её можно проверить без окна.

import tkinter as tk

root = tk.Tk()
files_list = tk.Listbox(root, height=5)
files_list.pack()

def fill_list(listbox, names):
    listbox.delete(0, tk.END)
    for name in sorted(names):
        listbox.insert(tk.END, name)

fill_list(files_list, ["отчёт.docx", "кот.jpg", "архив.zip"])
print(files_list.size(), files_list.get(0))
fill_list(files_list, ["договор.pdf"])
print(files_list.size(), files_list.get(0))

Вывод:

3 архив.zip
1 договор.pdf

Второй вызов не дописал строку к старым, а заменил список: первой строкой функции стоит очистка.

Что выбрал человек, узнают через curselection(). Щелчок мышью в примере заменяет метод selection_set(номер) — он выделяет строку из программы:

import tkinter as tk

root = tk.Tk()
files_list = tk.Listbox(root)
for name in ["договор.pdf", "кот.jpg", "архив.zip"]:
    files_list.insert(tk.END, name)

def show_selected():
    selected = files_list.curselection()
    if not selected:
        print("Ничего не выбрано")
        return
    print("Выбран:", files_list.get(selected[0]))

show_selected()
files_list.selection_set(1)
show_selected()

Вывод:

Ничего не выбрано
Выбран: кот.jpg
Listbox из трёх строк с номерами слева: 0 «договор.pdf», 1 «кот.jpg» (выделена синим), 2 «архив.zip». Справа подписи методов: insert(tk.END, …) — стрелка под последнюю строку «сюда добавится»; size() → 3; get(1) → «кот.jpg»; curselection() → (1,).

Проверьте себя

Задание. Сколько строк будет в списке после выполнения кода? Введите число.

Исходный код для этого задания:

import tkinter as tk

root = tk.Tk()
lb = tk.Listbox(root)
for name in ["a.pdf", "b.jpg", "c.zip"]:
    lb.insert(tk.END, name)
lb.delete(0, tk.END)
for name in ["d.png", "e.txt"]:
    lb.insert(tk.END, name)
lb.insert(0, "f.docx")
print(lb.size())

Подсказка: Что остаётся в списке после delete(0, tk.END)? Посчитайте только то, что добавлено после очистки.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Что выведет программа? Введите имя файла.

Исходный код для этого задания:

import tkinter as tk

root = tk.Tk()
lb = tk.Listbox(root)
lb.insert(tk.END, "отчёт.docx")
lb.insert(tk.END, "обои.png")
lb.insert(0, "договор.pdf")
lb.insert(tk.END, "архив.zip")
lb.selection_set(2)
print(lb.get(lb.curselection()[0]))

Подсказка: Выпишите строки списка по порядку после каждого insert, пронумеруйте с нуля и найдите выделенную.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Допишите функцию fill_list целиком: она очищает список и добавляет в него только файлы, для которых в RULES есть правило; расширение сравнивайте без учёта регистра (Path(name).suffix.lower() из главы 1). Запустите программу и введите число, которое она выведет. Приложите код функции в поле комментария.

Исходный код для этого задания:

import tkinter as tk

root = tk.Tk()
files_list = tk.Listbox(root)

names = ["отчёт.docx", "Кот.JPG", "readme", "фото.png", "setup.exe", "договор.pdf",
         "архив.tar.gz", "скан.PDF", "заметки.txt", "обои.jpg", "таблица.xlsx", "видео.mp4"]
RULES = {".pdf": "Документы", ".docx": "Документы", ".jpg": "Картинки", ".png": "Картинки", ".zip": "Архивы"}

def fill_list(listbox, names, rules):
    # допишите: очистить список и добавить только файлы,
    # у которых есть правило (регистр расширения не важен)
    ...

fill_list(files_list, names, RULES)
print(files_list.size())

Подсказка: Расширение берите через Path(name).suffix и приводите к нижнему регистру. Какое расширение у «архив.tar.gz» по мнению suffix?

✅ Готово, если: программа запускается без ошибок и выводит то, что просят в задании.

Онлайн-проверка ответа появится позже

filedialog.askdirectory: выбор папки мышкой

Печатать путь к папке руками неудобно и легко ошибиться. В Tkinter есть стандартные окна выбора из модуля tkinter.filedialog, его подключают отдельно: from tkinter import filedialog.

  • filedialog.askdirectory(title="…") — окно выбора папки; возвращает путь строкой, например "C:/Users/ученик/Downloads" (разделитель — прямой слеш даже на Windows);
  • filedialog.askopenfilename(title="…") — то же для одного файла;
  • параметр initialdir= — в какой папке открыть окно (удобно подставить последнюю выбранную);
  • если человек нажал «Отмена» или закрыл окно, функция возвращает пустую строку "". Это надо проверять, иначе программа начнёт читать «папку без имени».

Диалог ждёт человека, поэтому обработчик кнопки делим на две части: choose_folder только спрашивает путь, а вся работа с путём — в open_folder(path), которая получает путь параметром. Её можно вызвать из программы с любой папкой и проверить без щелчков.

import tkinter as tk
from tkinter import filedialog
from pathlib import Path

root = tk.Tk()
root.title("Сортировщик Загрузок")
folder = tk.StringVar(value="Папка не выбрана")
files_list = tk.Listbox(root, height=5)

def open_folder(path):
    if not path:                      # «Отмена» — ничего не меняем
        return
    folder.set(path)
    files_list.delete(0, tk.END)
    for item in sorted(Path(path).iterdir()):
        if item.is_file():
            files_list.insert(tk.END, item.name)

def choose_folder():
    path = filedialog.askdirectory(title="Какую папку разобрать?")
    open_folder(path)

tk.Button(root, text="Выбрать папку", command=choose_folder).grid(row=0, column=0)
tk.Label(root, textvariable=folder).grid(row=0, column=1)
files_list.grid(row=1, column=0, columnspan=2)

open_folder("")
print(folder.get())

root.mainloop()

Если запустить без mainloop, строка open_folder("") имитирует «Отмену» и печатает Папка не выбрана: пустой путь функция отбросила. В окне choose_folder вызовется при нажатии кнопки, и в open_folder придёт путь из диалога.

Три шага слева направо. 1: окно сортировщика, курсор на кнопке «Выбрать папку». 2: стандартное окно Windows «Какую папку разобрать?» со списком папок, выделена Downloads, кнопки «Выбор папки» и «Отмена». 3: две стрелки из диалога: от «Выбор папки» — строка "C:/Users/ученик/Downloads", она уходит в open_folder(path) и появляется в надписи окна; от «Отмена» — пустая строка "", open_folder сразу выходит, надпись остаётся «Папка не выбрана».

Проверьте себя

Контрольный вопрос. Человек открыл окно выбора папки и нажал «Отмена». Что вернула filedialog.askdirectory()?

Апустую строку «»
Бпуть к последней выбранной папке
Впапку, где лежит программа
ГFalse

Подсказка: Посмотрите в списке функций модуля filedialog, что происходит при закрытии окна без выбора.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Программа открывает окно выбора папки сразу при запуске, а не по нажатию кнопки. Ошибка в одном аргументе Button. Как он должен выглядеть? Выберите вариант.

Исходный код для этого задания:

import tkinter as tk
from tkinter import filedialog

root = tk.Tk()

def choose_folder():
    path = filedialog.askdirectory()
    print(path)

button = tk.Button(root, text="Выбрать папку", command=choose_folder())
button.pack()
root.mainloop()
Аcommand=choose_folder
Бcommand=choose_folder()
Вcommand=»choose_folder»
Гcommand=choose_folder(root)

Подсказка: Что Python делает со скобками после имени функции — передаёт функцию или сразу её вызывает?

Онлайн-проверка ответа появится позже

Задание. Функцию open_folder программа вызывает сама, как если бы из диалога пришли эти пути. Что выведет print? Введите текст вывода.

Исходный код для этого задания:

import tkinter as tk

root = tk.Tk()
status = tk.StringVar(value="Папка не выбрана")

def open_folder(path):
    if not path:
        return
    status.set("Папка: " + path)

open_folder("C:/Users/ученик/Pictures")
open_folder("")
open_folder("")
print(status.get())

Подсказка: Что делает функция с пустой строкой? Какой последний НЕпустой путь она получила?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Настройки в settings.json: запомнить и восстановить

Окно, которое при каждом запуске забывает папку и галочку, раздражает. Сохраним их тем же способом, что в главе 1: словарь → json.dump в файл, при запуске json.load обратно. В JSON кладём обычные значения из get() — строку и True/False; саму переменную Tkinter в JSON не записать.

При первом запуске файла ещё нет, а если его испортили руками — json.load не разберёт текст. На первый случай есть знакомое по главе 2 исключение FileNotFoundError. На второй — новое для нас json.JSONDecodeError: его выбрасывает json.load, когда текст файла — не правильный JSON (оборванная скобка, лишняя запятая). Оба ловим одним except с кортежем, как в главе 2. Тогда берём настройки по умолчанию. А чтобы не упасть, если в старом файле нет какого-то ключа, начинаем со словаря по умолчанию и обновляем его прочитанным: dict.update() перезаписывает совпавшие ключи и добавляет новые.

import json
from pathlib import Path

DEFAULTS = {"folder": "", "dry_run": True}

def load_settings(path):
    settings = dict(DEFAULTS)            # копия, DEFAULTS не портим
    try:
        with path.open(encoding="utf-8") as f:
            settings.update(json.load(f))
    except (FileNotFoundError, json.JSONDecodeError):
        pass                             # берём значения по умолчанию
    return settings

def save_settings(path, settings):
    with path.open("w", encoding="utf-8") as f:
        json.dump(settings, f, ensure_ascii=False, indent=2)

path = Path("settings.json")
print(load_settings(path))
save_settings(path, {"folder": "C:/Users/ученик/Downloads", "dry_run": False})
print(load_settings(path))

Вывод:

{'folder': '', 'dry_run': True}
{'folder': 'C:/Users/ученик/Downloads', 'dry_run': False}

Первый вызов не нашёл файла и вернул настройки по умолчанию, второй прочитал сохранённые. pass в except — «ничего не делать»: здесь это осознанно. В главе 2 мы договорились не глотать ошибки молча; исключение — когда ошибка ожидаемая и у нас уже есть запасной вариант. Так и тут: нет файла или он испорчен — значения по умолчанию уже лежат в settings, программа работает дальше.

Связываем с окном. При запуске: прочитали файл → folder.set(...), dry_run.set(...). Сохраняем в двух местах, где настройка меняется: после выбора папки и в command галочки.

def current_settings():
    return {"folder": folder.get(), "dry_run": dry_run.get()}

def on_toggle():
    save_settings(SETTINGS_PATH, current_settings())

settings = load_settings(SETTINGS_PATH)
folder.set(settings["folder"])
dry_run.set(settings["dry_run"])
Цикл из трёх блоков по кругу. «Запуск»: load_settings читает settings.json, значения уходят в folder.set и dry_run.set. «Работа»: окно с путём и галочкой. «Изменение»: выбрали папку или щёлкнули галочку — save_settings записывает словарь из folder.get() и dry_run.get() в settings.json. Справа содержимое файла: {"folder": "C:/Users/ученик/Downloads", "dry_run": false}. Отдельная ветка: файла нет или он испорчен — берутся DEFAULTS.

Проверьте себя

Задание. Что выведет программа? Введите значение, как его печатает Python.

Исходный код для этого задания:

import json

DEFAULTS = {"folder": "", "dry_run": True, "sort": "name"}
text = '{"folder": "D:/Фото", "dry_run": false}'

settings = dict(DEFAULTS)
settings.update(json.loads(text))
print(settings["dry_run"])

Подсказка: Какие ключи update() перезапишет значениями из текста? Во что json.loads превращает логические значения JSON?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Файла nastrojki.json в папке нет. Что выведет программа? Введите значение, как его печатает Python.

Исходный код для этого задания:

import json
from pathlib import Path

def load_settings(path):
    settings = {"folder": "Загрузки", "dry_run": True}
    try:
        with path.open(encoding="utf-8") as f:
            settings.update(json.load(f))
    except (FileNotFoundError, json.JSONDecodeError):
        pass
    return settings

print(load_settings(Path("nastrojki.json"))["folder"])

Подсказка: Какая ошибка возникнет при открытии отсутствующего файла и что тогда вернёт функция?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Контрольный вопрос. Программа сохраняет настройки строкой json.dump({«dry_run»: dry_run}, f) и падает с TypeError. Как записать значение правильно?

Исходный код для этого задания:

dry_run = tk.BooleanVar(value=True)
Аjson.dump({«dry_run»: dry_run.get()}, f)
Бjson.dump({«dry_run»: dry_run.set()}, f)
Вjson.dump({«dry_run»: str(dry_run)}, f)
Гjson.dump(dry_run, f)

Подсказка: В JSON можно записать только обычные значения Python. Каким методом достают значение из переменной Tkinter?

Онлайн-проверка ответа появится позже

Собираем сортировщик: предпросмотр и перенос

Осталась кнопка «Разобрать». Логику раскладки (правила RULES из главы 1) держим в функции, которая получает папку и флаг параметрами и возвращает строки для списка. Окно только передаёт ей folder.get() и dry_run.get(). При включённой галочке функция ничего не переносит, а лишь показывает план — так человек видит, что произойдёт, до того как файлы поедут.

from pathlib import Path
import shutil

RULES = {
    ".pdf": "Документы", ".docx": "Документы",
    ".jpg": "Картинки", ".png": "Картинки",
    ".zip": "Архивы",
}

def plan_lines(names, rules):
    lines = []
    for name in sorted(names):
        target = rules.get(Path(name).suffix.lower())
        if target is not None:
            lines.append(f"{name} -> {target}")
    return lines

print(plan_lines(["кот.jpg", "readme", "договор.pdf", "фото.PNG"], RULES))

Вывод:

['договор.pdf -> Документы', 'кот.jpg -> Картинки', 'фото.PNG -> Картинки']

Полная программа окна. Она разбита комментариями на четыре части: настройки, логика раскладки, окно и запуск. Настройки и логику вы уже видели выше; новое здесь — show и on_sort. Функции sort_folder(path, dry_run_value) и plan_lines не знают про окно: их можно запустить и проверить отдельно. Обработчики кнопок только читают переменные и выводят результат.

import json
import shutil
import tkinter as tk
from tkinter import filedialog, messagebox
from pathlib import Path

# ---- настройки ----
RULES = {".pdf": "Документы", ".docx": "Документы", ".jpg": "Картинки", ".png": "Картинки", ".zip": "Архивы"}
SETTINGS_PATH = Path("settings.json")
DEFAULTS = {"folder": "", "dry_run": True}

def load_settings(path):
    settings = dict(DEFAULTS)
    try:
        with path.open(encoding="utf-8") as f:
            settings.update(json.load(f))
    except (FileNotFoundError, json.JSONDecodeError):
        pass
    return settings

def save_settings(path, settings):
    with path.open("w", encoding="utf-8") as f:
        json.dump(settings, f, ensure_ascii=False, indent=2)

# ---- логика: план и перенос ----
def plan_lines(names, rules):
    lines = []
    for name in sorted(names):
        target = rules.get(Path(name).suffix.lower())
        if target is not None:
            lines.append(f"{name} -> {target}")
    return lines

def sort_folder(path, dry_run_value):
    folder_path = Path(path)
    names = [item.name for item in folder_path.iterdir() if item.is_file()]
    lines = plan_lines(names, RULES)
    if not dry_run_value:
        for name in names:
            target = RULES.get(Path(name).suffix.lower())
            if target is not None:
                (folder_path / target).mkdir(exist_ok=True)
                shutil.move(folder_path / name, folder_path / target / name)
    return lines

# ---- окно ----
root = tk.Tk()
root.title("Сортировщик Загрузок")
folder = tk.StringVar()
dry_run = tk.BooleanVar()
status = tk.StringVar(value="Выберите папку")
files_list = tk.Listbox(root, height=8, width=50)

def current_settings():
    return {"folder": folder.get(), "dry_run": dry_run.get()}

def show(lines):
    files_list.delete(0, tk.END)
    for line in lines:
        files_list.insert(tk.END, line)
    status.set(f"Найдено файлов: {len(lines)}")

def open_folder(path):
    if not path:
        return
    folder.set(path)
    save_settings(SETTINGS_PATH, current_settings())
    show(sort_folder(path, True))          # сразу показываем план, ничего не трогая

def choose_folder():
    open_folder(filedialog.askdirectory(initialdir=folder.get() or None))

def on_toggle():
    save_settings(SETTINGS_PATH, current_settings())

def on_sort():
    if not folder.get():
        messagebox.showwarning("Сортировщик", "Сначала выберите папку")
        return
    lines = sort_folder(folder.get(), dry_run.get())
    show(lines)
    if not dry_run.get():
        messagebox.showinfo("Сортировщик", f"Перенесено файлов: {len(lines)}")

tk.Button(root, text="Выбрать папку", command=choose_folder).grid(row=0, column=0)
tk.Label(root, textvariable=folder).grid(row=0, column=1)
files_list.grid(row=1, column=0, columnspan=2)
tk.Checkbutton(root, text="Только показать, не переносить", variable=dry_run,
               command=on_toggle).grid(row=2, column=0, columnspan=2)
tk.Button(root, text="Разобрать", command=on_sort).grid(row=3, column=0, columnspan=2)
tk.Label(root, textvariable=status).grid(row=4, column=0, columnspan=2)

# ---- запуск: восстановить настройки ----
settings = load_settings(SETTINGS_PATH)
dry_run.set(settings["dry_run"])
if settings["folder"] and Path(settings["folder"]).is_dir():
    open_folder(settings["folder"])

root.mainloop()

Обратите внимание на open_folder: сразу после выбора папки вызывается sort_folder(path, True) — всегда только предпросмотр, чтобы человек увидел план. Переносит файлы лишь кнопка «Разобрать» при снятой галочке. initialdir=folder.get() or None открывает диалог в последней папке; если её нет — там, где решит система.

Создайте тестовую папку с пятью-шестью пустыми файлами разных типов, запустите программу, выберите эту папку и сравните список в окне с тем, что окажется в подпапках после «Разобрать» со снятой галочкой. Закройте и откройте программу снова — папка и галочка должны вернуться.

Проверьте себя

Задание. Что выведет программа? Введите число.

Исходный код для этого задания:

from pathlib import Path

RULES = {".pdf": "Документы", ".docx": "Документы", ".jpg": "Картинки", ".png": "Картинки", ".zip": "Архивы"}

def plan_lines(names, rules):
    lines = []
    for name in sorted(names):
        target = rules.get(Path(name).suffix.lower())
        if target is not None:
            lines.append(f"{name} -> {target}")
    return lines

names = ["скан.PDF", "обои.jpeg", "кот.jpg", "backup.tar.gz", "презентация.pptx", "фото.Png", "notes", "проект.zip"]
print(len(plan_lines(names, RULES)))

Подсказка: Для каждого имени выпишите Path(name).suffix.lower() и проверьте, есть ли такой ключ в RULES. Внимательно с .jpeg и .tar.gz.

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Обработчик кнопки программа вызывает сама. Что окажется в строке состояния? Введите текст целиком.

Исходный код для этого задания:

import tkinter as tk

root = tk.Tk()
dry_run = tk.BooleanVar(value=True)
status = tk.StringVar()
check = tk.Checkbutton(root, variable=dry_run)

def on_sort():
    lines = ["кот.jpg -> Картинки", "договор.pdf -> Документы", "фото.png -> Картинки"]
    if dry_run.get():
        status.set(f"План: {len(lines)} файла")
    else:
        status.set(f"Перенесено: {len(lines)}")

check.invoke()
on_sort()
print(status.get())

Подсказка: С каким значением создана dry_run и что с ней сделал invoke() до вызова on_sort()?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Сортировщик хранит последние папки списком recent, самая свежая — первой, не больше трёх. Что напечатает программа? Введите строку вывода целиком.

Исходный код для этого задания:

recent = ["Отчёты", "Фото", "Учёба"]

for path in ["Фото", "Музыка", "Учёба"]:
    if path in recent:
        recent.remove(path)
    recent.insert(0, path)
    recent = recent[:3]

print(", ".join(recent))

Подсказка: Выписывайте recent после каждого шага цикла: remove убирает найденный путь, insert(0, …) ставит путь первым, срез [:3] отрезает всё после третьего элемента. Была ли «Учёба» в списке в тот момент, когда её выбрали?

✅ Готово, если: ты ввёл(а) верный ответ, и онлайн-проверка его приняла.

Онлайн-проверка ответа появится позже

Задание. Проектная миссия «Предпросмотр сортировщика». Соберите окно: кнопка «Выбрать папку» (filedialog.askdirectory), Listbox, галочка «Только показать, не переносить» на BooleanVar, кнопка «Разобрать», строка состояния на StringVar. Правила — RULES урока (.pdf, .docx → Документы; .jpg, .png → Картинки; .zip → Архивы), регистр расширения не важен. Добавьте в окно второй флажок «Только картинки»: когда он включён, в Listbox попадают лишь строки «имя -> Картинки». Логику держите в функции, которая получает список имён и оба флага параметрами. Проверьте на своей папке. Ниже — содержимое папки «Загрузки» коллеги, 54 имени файла, по одному в строке. Сколько строк окажется в Listbox при включённых обоих флажках? Приложите файл программы .py; в поле ответа — число.

Принято, если:
1. число совпадает с эталоном;
2. приложен .py, где функция на этом списке имён выдаёт это число;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. флажки связаны с BooleanVar через variable=, в command передана функция без скобок;
4. при нажатии «Отмена» в диалоге программа не падает и не очищает список;
5. при включённом «Только показать» ни один файл не переносится.

музыка_22.Png

резюме_62.jpeg

logo_65.exe

чек_62.pptx

backup_87.docx

backup_53.tar.gz

чек_64.PDF

logo_95.xlsx

резюме_41.Png

фото_8.exe

backup_28.docx

заметки_16.exe

обои_66.zip

setup_1.txt

чек_85.tar.gz

таблица_51.mp4

архив_88.jpg

кот_47.jpg

договор_20.docx

фото_41.pdf

кот_53.zip

заметки_26.png

презентация_59

заметки_76.pptx

backup_63.docx

отчёт_62.mp4

презентация_98.txt

фото_86.Png

отчёт_70.pptx

setup_90.jpeg

билет_94

кот_89.docx

logo_94.tar.gz

паспорт_36.xlsx

чек_53.zip

билет_80

презентация_3.mp4

setup_42.jpeg

фото_24.exe

logo_37.jpeg

заметки_55.xlsx

setup_83.txt

музыка_85.PDF

отчёт_30

договор_35.docx

музыка_16.png

счёт_26.docx

договор_88.pdf

музыка_56.txt

отчёт_20.xlsx

отчёт_12.JPG

паспорт_6.pdf

музыка_27.Png

фото_43.txt

Подсказка: Расширение берите через Path(name).suffix.lower(); проверьте, что делает suffix с .jpeg и .tar.gz.

✅ Готово, если: ответ раскрывает то, что просит критерий приёмки в задании выше.

Онлайн-проверка ответа появится позже

Задание. Проектная миссия «Недавние папки». Доработайте сортировщик: в settings.json, кроме folder и dry_run, хранится ключ recent — список из не более чем 5 последних выбранных папок, самая свежая первой. Функция remember(settings, path) получает путь параметром: пустой путь (отмена диалога) игнорирует; если путь уже есть в recent — убирает его оттуда и ставит в начало, считая это «повторным выбором»; если нет — ставит в начало и обрезает список до 5. После каждого выбора настройки сохраняются в JSON, а recent показывается в Listbox; двойного щелчка не нужно — достаточно кнопки «Открыть выбранную», которая берёт путь через curselection(). Ниже — журнал 50 выборов папки по порядку, по одному в строке; слово ОТМЕНА означает, что человек закрыл диалог и askdirectory вернул пустую строку. Начальный recent пуст. Сколько раз за журнал случился повторный выбор (путь уже был в recent в момент выбора)? Приложите файл программы .py; в поле ответа — число.

Принято, если:
1. число совпадает с эталоном;
2. приложен .py, где remember() на этом журнале выдаёт это число;

Самопроверка перед сдачей (преподаватель смотрит на занятии):
3. после перезапуска программы recent, папка и галочка восстанавливаются из settings.json;
4. удалённый или испорченный settings.json не роняет программу — берутся значения по умолчанию;
5. «Открыть выбранную» без выделенной строки не падает.

C:/Users/ученик/Pictures

E:/Флешка

D:/Работа/Отчёты

C:/Users/ученик/Downloads

C:/Users/ученик/Downloads

C:/Users/ученик/Downloads

E:/Флешка

D:/Работа/Отчёты

ОТМЕНА

C:/Users/ученик/Desktop

C:/Users/ученик/Desktop

D:/Работа/Договоры

D:/Фото/2024

C:/Users/ученик/Desktop

C:/Users/ученик/Pictures

C:/Users/ученик/Desktop

D:/Учёба

E:/Флешка

E:/Флешка

D:/Работа/Отчёты

D:/Фото/2025

C:/Users/ученик/Pictures

D:/Работа/Договоры

D:/Фото/2025

D:/Фото/2024

D:/Работа/Отчёты

C:/Users/ученик/Pictures

D:/Фото/2024

D:/Учёба

D:/Работа/Договоры

D:/Фото/2024

D:/Фото/2024

E:/Флешка

D:/Работа/Договоры

E:/Флешка

E:/Флешка

C:/Users/ученик/Desktop

D:/Фото/2025

C:/Users/ученик/Downloads

C:/Users/ученик/Downloads

C:/Users/ученик/Downloads

C:/Users/ученик/Pictures

D:/Фото/2024

C:/Users/ученик/Desktop

D:/Фото/2024

C:/Users/ученик/Desktop

D:/Работа/Отчёты

D:/Фото/2025

D:/Работа/Отчёты

ОТМЕНА

Подсказка: Храните recent обычным списком Python: проверка in, remove(), insert(0, …) и срез [:5]. Важно, был ли путь в recent именно в момент выбора — после обрезки до 5 старые пути выпадают.

✅ Готово, если: ответ раскрывает то, что просит критерий приёмки в задании выше.

Онлайн-проверка ответа появится позже

Что дальше

Теперь вы умеете связывать поля и галочки с переменными StringVar и BooleanVar, показывать и выбирать строки в Listbox, спрашивать папку через filedialog.askdirectory и не падать на «Отмене», а настройки окна сохранять в JSON и восстанавливать при запуске. Сортировщик стал программой, которой может пользоваться человек без терминала. Осталось отдать её коллеге без Python — в следующем уроке соберём из неё exe через PyInstaller.

← Назад · ↑ В начало урока · ⌂ К навигатору курса · Вперёд →

Школа Виктора Комлева