Rozdział 18 · Testowanie

Debug Labs typowe błędy rysowania

Każdy błąd tutaj występuje w prawdziwych projektach studenckich — naucz się rozpoznawać objaw, zanim otworzysz debugator.

Mała kolekcja typowych błędów rysowników na Canvas — każdy z objawem i poprawką. Część z nich widzieliście już wcześniej w rozdziale; tutaj są zebrane jako podręcznik.

Debug Lab 1:Y wznosi się w górę jak Turtle
turtle_privychka.py
def on_drag(event):
    # 'znana' logika: im wyżej na ekranie, tym WIĘCEJ Y
    if event.y > start_y:
        status.config(text=„Moving UP”)  # nieprawidłowe dla Canvas!
# Курсор двигается вниз по экрану, а статус говорит 'ВВЕРХ' —
# логика была написана в терминах Turtle, а не Canvas.
Co widać na ekranie

Canvas Y rośnie W DÓŁ (Rozdział 18.9) — event.y > start_y oznacza „kursor jest niższy niż wcześniej”

Poprawiony kod
canvas_napravlenie.py
def on_drag(event):
    if event.y > start_y:
        status.config(text=„Poruszamy się w DÓŁ”)  # Prawdziwe: Y Wznosi się w dół
Debug Lab 2: Nie pamiętałem punktu początkowego gestu
zabyli_start.py
def on_press(event):
    pass  # zapomnieli zapisać event.x, event.y

def on_drag(event):
    canvas.create_line(start_x, start_y, event.x, event.y)  #start_x nieokreślone
# NameError: name 'start_x' is not defined —
# программа падает на первом же движении мыши после нажатия.
Co widać na ekranie

on_press() musi zachować punkt początkowy (Rozdział 18.12) bez niego on_drag() po prostu nie wiadomo, gdzie narysować linię lub podgląd kształtu.

Poprawiony kod
sohranili_start.py
def on_press(event):
    global start_x, start_y
    start_x, start_y = event.x, event.y
Debug Lab 3: <Motion> zamiast <B1-Motion>
lyuboe_dvizhenie.py
canvas.bind("<Motion>", on_drag)   # wyzwala się przy KAŻDYM ruchu myszy
# Линия рисуется, даже если кнопка мыши вообще не нажата —
# достаточно просто провести курсором над холстом.
Co widać na ekranie

<Motion> reaguje na każdy ruch myszy nad widżetem. Aby rysować tylko podczas przeciągania, musisz <B1-Motion> – Rozdział 18.12 wyjaśnia różnicę.

Poprawiony kod
b1_motion.py
canvas.bind("<B1-Motion>", on_drag)   # tylko przy wciśniętym lewym przycisku
Debug Lab 4: Tysiące niepołączonych punktów zamiast linii
tochki_vmesto_linii.py
def on_drag(event):
    canvas.create_oval(
        event.x - 2, event.y - 2, event.x + 2, event.y + 2,
        fill="black",
    )
# Быстрое движение мыши оставляет заметные разрывы между кружками —
# штрих выглядит рваным, а не гладким.
Co widać na ekranie

Okrąg dla każdego zdarzenia indywidualnego nie uwzględnia ścieżki MIĘDZY dwoma kolejnymi punktami. Rozdział 18.14 pokazuje rozwiązanie połączenia nowego punktu z poprzednim przez create_line().

Poprawiony kod
linii_mezhdu_tochkami.py
def on_drag(event):
    canvas.create_line(last_x, last_y, event.x, event.y, width=3, capstyle=tk.ROUND)
    # last_x, last_y aktualizowane po każdym odcinku
Debug Lab 5: Podgląd jest odtwarzany przy każdym Motion bez potrzeby
peresozdanie_prevyu.py
def on_drag(event):
    canvas.delete(preview_id)
    preview_id = canvas.create_rectangle(start_x, start_y, event.x, event.y)
# Работает, но на каждое из десятков событий Motion в секунду
# Canvas выбрасывает и заново строит один и тот же элемент.
Co widać na ekranie

Usunąć i stworzyć od nowa — to nie błąd w sensie „zepsute”, ale zbędna praca: Canvas już ma coords() dokładnie do aktualizacji istniejącego elementu (sekcja 18.18) — bez ponownego tworzenia.

Poprawiony kod
coords_vmesto_peresozdaniya.py
def on_drag(event):
    canvas.coords(preview_id, start_x, start_y, event.x, event.y)
Debug Lab 6: Figurki zapowiedzi Lost id
poteryan_id.py
def on_press(event):
    canvas.create_rectangle(event.x, event.y, event.x, event.y, dash=(4, 2))
    # id nie zapisany — preview_id nigdzie się nie pojawił

def on_drag(event):
    canvas.coords(preview_id, ...)  # NameError: preview_id nie jest zdefiniowana
# NameError на первом же движении мыши —
# ссылаться не на что, id черновой фигуры потерян.
Co widać na ekranie

create_*() zwroty item_id (Rozdział 18.10), ale tylko jeśli zostanie ZAPISANE, w przeciwnym razie kształt zostanie narysowany, ale później nie będzie do czego się odwoływać.

Poprawiony kod
sohranili_id.py
def on_press(event):
    global preview_id
    preview_id = canvas.create_rectangle(event.x, event.y, event.x, event.y, dash=(4, 2))
Debug Lab 7: Prostokąt „przewraca”
ne_normalizovano.py
def get_bounds():
    return start_x, start_y, current_x, current_y  # z wyłączeniem kierunku

left, top, right, bottom = get_bounds()
if left > right:
    raise ValueError(„to niemożliwe”)  # ale przeciąganie w górę-i-lewo właśnie to daje!
# Приложение падает с ValueError, если пользователь начал
# перетаскивание из правого нижнего угла будущей фигуры.
Co widać na ekranie

Myszką można przeciągać w dowolnym z czterech kierunków (sekcja 18.16) – oczekiwanie na kod left <= right bez wcześniejszej normalizacji działa tylko dla jednego z czterech kierunków.

Poprawiony kod
normalizovano.py
left, top, right, bottom = normalize_bounds(start_x, start_y, current_x, current_y)
# jest teraz left <= right oraz top <= bottom gwarantowane, niezależnie od kierunku
Debug Lab 8: item_id używany jako indeks listy
id_kak_indeks.py
shapes = []
item_id = canvas.create_rectangle(10, 10, 50, 50)
shapes.append("rectangle")
print(shapes[item_id])   #IndexError jeśli item_id więcej niż len(shapes) - 1
# IndexError: list index out of range —
# item_id не совпадает с порядковым номером в собственном списке кода.
Co widać na ekranie

item_id — wewnętrzny numer Canvas (rozdział 18.10), a nie pozycja na własnej liście figur. Używać go jako indeksu shapes[item_id] zdarzy się tylko przypadkowo.

Poprawiony kod
id_kak_klyuch_slovarya.py
shapes_by_id = {}
item_id = canvas.create_rectangle(10, 10, 50, 50)
shapes_by_id[item_id] = "rectangle"   # słownik — id jako KLUCZ, a nie indeks
Debug Lab 9: Undo usuwa tylko ostatni segment kreski, a nie całą linię
chastichnaya_otmena.py
def undo(self):
    item_id = self.undo_stack.pop()   # zakłada JEDEN id na akcję
    self.canvas.delete(item_id)
# После Ctrl+Z длинный карандашный штрих теряет только
# последний маленький отрезок, а не исчезает целиком.
Co widać na ekranie

Jedno pociągnięcie ołówkiem to DUŻO segmentów create_line() (Rozdział 18.14), nie jeden element. Undo musi przechowywać cały lista id (lub liczby) dla KAŻDEJ akcji, a nie tylko jeden id (Rozdział 18.24).

Poprawiony kod
otmena_celym_deystviem.py
def undo(self):
    shapes = self.undo_stack.pop()   # lista postacie JEDNEGO działania
    del self.document[len(self.document) - len(shapes):]
    self.render_document()
Debug Lab 10:Redo wskrzesza niezwiązane działanie
redo_bez_ochistki.py
def _commit_action(self, shapes):
    self.document.extend(shapes)
    self.undo_stack.append(shapes)
    # zapomniałem self.redo_stack.clear()
# Undo, затем новый рисунок, затем Redo —
# и на холсте внезапно появляется фигура, никак не связанная с текущим рисунком.
Co widać na ekranie

Jeśli po Undo narysujesz coś nowego, stara gałąź redo już nie odpowiada aktualnemu dokumentowi. Rozdział 18.25 wymaga wyraźnego rozliczenia redo_stack przy każdej nowej akcji.

Poprawiony kod
redo_s_ochistkoj.py
def _commit_action(self, shapes):
    self.document.extend(shapes)
    self.undo_stack.append(shapes)
    self.redo_stack.clear()
Debug Lab 11: Oczyściłem płótno, ale nie opowieść
ochistka_bez_istorii.py
def clear_canvas(self):
    self.canvas.delete("all")
    self.document.clear()
    #undo_stack i redo_stack pozostają nietknięte!
# После 'Очистить' нажатие Ctrl+Z не делает ничего видимого —
# а следующий Ctrl+Y возвращает на холст фигуры, которые
# пользователь только что удалил кнопкой 'Очистить'.
Co widać na ekranie

Cichy błąd: del document[len(document) - len(shapes):] na pustym dokumencie — to jest del document[-3:], że nic nie usuwa ani nie psuje. Ale anulowana „akcja” redo_stack, a następny Ctrl+Y wraca na płótno kształty wymazane przyciskiem Czyść. Historia opisuje dokument, który już nie istnieje: gdy document jest oczyszczony, undo_stack oraz redo_stack musi zostać oczyszczony razem z nim (Rozdział 18.26).

Poprawiony kod
ochistka_s_istoriej.py
def clear_canvas(self):
    self.document.clear()
    self.undo_stack.clear()
    self.redo_stack.clear()
    self.render_document()
Debug Lab 12: Anuluj colorchooser zapisuje None jako kolor
otmena_colorchooser.py
def choose_custom_color(self):
    _rgb, hex_color = colorchooser.askcolor()
    self.set_color(hex_color)   # nie testowałem hex_color na None!
# Пользователь нажал 'Отмена' в диалоге —
# следующая же попытка нарисовать линию падает: fill=None недопустим.
Co widać na ekranie

askcolor() zwroty (None, None) przy anulowaniu (Rozdział 18.21) – Ustawienie tego jako aktualnego koloru bez zaznaczania oznacza przerwanie rysunku przy następnej próbie.

Poprawiony kod
proverka_otmeny.py
def choose_custom_color(self):
    _rgb, hex_color = colorchooser.askcolor()
    if hex_color is not None:
        self.set_color(hex_color)
Debug Lab 13: Anuluj dialog zapisu - Path('') zamiast wychodzić
otmena_sohraneniya.py
def save_document(self):
    path_str = filedialog.asksaveasfilename()
    Path(path_str).write_text(...)   # nie sprawdziłem pustego sznurka!
# Пользователь нажал 'Отмена' в диалоге сохранения —
# программа пытается записать файл по пути текущей директории.
Co widać na ekranie

asksaveasfilename()/askopenfilename() zwracają pusty łańcuch znaków przy anulowaniu, a nie None , a nie wyjątek (Rozdział 18.30) — kod musi to zweryfikować przed użyciem ścieżki.

Poprawiony kod
proverka_otmeny_faila.py
def save_document(self):
    path_str = filedialog.asksaveasfilename()
    if not path_str:
        return
    Path(path_str).write_text(...)
Debug Lab: JSON załadowana, ale nie przerysowana Canvas
zagruzka_bez_render.py
def load_from_path(self, path):
    data = json.loads(path.read_text(encoding="utf-8"))
    self.document = [Shape.from_dict(item) for item in data["items"]]
    #forgot self.render_document()!
# self.document правильно заполнен новыми фигурами,
# но на экране всё ещё виден старый рисунок — или пустой холст.
Co widać na ekranie

Tak samo jak w rozdziale 17: Zmiana modelu bez wywołania renderowania nie dociera do ekranu. load_from_path() musi się zakończyć render_document() (rozdział 18.30).

Poprawiony kod
zagruzka_s_render.py
def load_from_path(self, path):
    data = json.loads(path.read_text(encoding="utf-8"))
    shapes = [Shape.from_dict(item) for item in data["items"]]
    self.document = shapes
    self.undo_stack.clear()
    self.redo_stack.clear()
    self.render_document()   # Bez tej linii ekran nie będzie wiedział o pobraniu
    # (pełna wersja z uszkodzonym przetwarzaniem plików znajduje się w sekcji 18.30)
Debug Lab 16: Uszkodzony plik usuwa aktualny rysunek
zagruzka_bez_proverki.py
def load_from_path(self, path):
    data = json.loads(path.read_text(encoding="utf-8"))
    self.document = [Shape.from_dict(i) for i in data["items"]]  # już zastąpiono!
    self.undo_stack.clear()
    self.render_document()   # ... I tylko tutaj wpadamy w zły kolor
# Открыли чужой JSON — рисунок, над которым работали час, исчез,
# Ctrl+Z его не возвращает, а каждая новая линия снова падает:
# _tkinter.TclError: unknown color name "not-a-color"
Co widać na ekranie

Dokument został zastąpiony ZANIM wiadomo, że plik jest rysowany. Przeparsuj plik na zmienną lokalną i przypisz self.document dopiero po sukcesie (rozdział 18.30) — w przeciwnym razie jedna próba otwarcia niewłaściwego pliku zabiera zarówno rysunek, jak i historię oraz możliwość dalszego rysowania.

Poprawiony kod
zagruzka_s_proverkoj.py
def load_from_path(self, path):
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        shapes = [Shape.from_dict(i) for i in data["items"]]
        for shape in shapes:
            self.canvas.winfo_rgb(shape.color)
    except (json.JSONDecodeError, KeyError, TypeError, ValueError, tk.TclError) as exc:
        messagebox.showerror(„Plik nie mógł zostać otwarty”, str(exc))
        return
    self.document = shapes
    self.render_document()
Realne okno: Ta sama aplikacja powiększona – płótno zajmuje więcej miejsca, ale wcześniej narysowane kształty pozostają tego samego rozmiaru przy tych samych współrzędnych absolutnych
Prawdziwe okno po przybliżeniu: Płótno się powiększyło, ale malowane kształty nie powiększyły się razem z nim – dokładnie to, co pokazuje Debug Lab 15 poniżej.
Debug Lab 15: Zmiana rozmiaru okna nie powoduje skalowania obrazu
resize_kak_masshtab.py
#standby: jeśli rozciągniesz okno, stare kształty będą się z nim zbliżać
canvas.grid(row=1, column=0, sticky="nsew")
# ... Ale nikt nie przelicza istniejących współrzędnych create_line/rectangle/oval
# Сам виджет Canvas становится больше вместе с окном —
# но уже нарисованные фигуры остаются в прежних абсолютных координатах, не растут.
Co widać na ekranie

sticky="nsew" oraz wagi wierszy/kolumn (Rozdział 17) powodują, że sam widżet rośnie Canvas nie jest tym samym co automatyczne skalowanie współrzędnych już narysowanych kształtów. Rozdział 18.29 wyraźnie rozbija tę różnicę: większe płótno nie oznacza automatycznie większego rysunku.

Poprawiony kod
resize_kak_bolshe_mesta.py
# Fair Model: Powiększone okno daje WIĘCEJ MIEJSCA na nowe kształty,
# a nie rozciąga już istniejące — jeśli potrzebny ostatni, współrzędne
# musiałaby być przeliczana ręcznie przy zmianie rozmiaru.
Praktyka: Odtworzenie i naprawa błędów rysunku
Automatyczna kontrola — Debug Lab 11 (czyszczenie i historia) oraz Debug Lab 10 (redo po nowym działaniu)
Otwórz praktykę →