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.
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.
Canvas Y rośnie W DÓŁ (Rozdział 18.9) — event.y > start_y oznacza „kursor jest niższy niż wcześniej”
def on_drag(event):
if event.y > start_y:
status.config(text=„Poruszamy się w DÓŁ”) # Prawdziwe: Y Wznosi się w dół
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 —
# программа падает на первом же движении мыши после нажатия.
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.
def on_press(event):
global start_x, start_y
start_x, start_y = event.x, event.y
canvas.bind("<Motion>", on_drag) # wyzwala się przy KAŻDYM ruchu myszy
# Линия рисуется, даже если кнопка мыши вообще не нажата —
# достаточно просто провести курсором над холстом.
<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ę.
canvas.bind("<B1-Motion>", on_drag) # tylko przy wciśniętym lewym przycisku
def on_drag(event):
canvas.create_oval(
event.x - 2, event.y - 2, event.x + 2, event.y + 2,
fill="black",
)
# Быстрое движение мыши оставляет заметные разрывы между кружками —
# штрих выглядит рваным, а не гладким.
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().
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
def on_drag(event):
canvas.delete(preview_id)
preview_id = canvas.create_rectangle(start_x, start_y, event.x, event.y)
# Работает, но на каждое из десятков событий Motion в секунду
# Canvas выбрасывает и заново строит один и тот же элемент.
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.
def on_drag(event):
canvas.coords(preview_id, start_x, start_y, event.x, event.y)
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 черновой фигуры потерян.
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ć.
def on_press(event):
global preview_id
preview_id = canvas.create_rectangle(event.x, event.y, event.x, event.y, dash=(4, 2))
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, если пользователь начал
# перетаскивание из правого нижнего угла будущей фигуры.
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.
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
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 не совпадает с порядковым номером в собственном списке кода.
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.
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
def undo(self):
item_id = self.undo_stack.pop() # zakłada JEDEN id na akcję
self.canvas.delete(item_id)
# После Ctrl+Z длинный карандашный штрих теряет только
# последний маленький отрезок, а не исчезает целиком.
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).
def undo(self):
shapes = self.undo_stack.pop() # lista postacie JEDNEGO działania
del self.document[len(self.document) - len(shapes):]
self.render_document()
def _commit_action(self, shapes):
self.document.extend(shapes)
self.undo_stack.append(shapes)
# zapomniałem self.redo_stack.clear()
# Undo, затем новый рисунок, затем Redo —
# и на холсте внезапно появляется фигура, никак не связанная с текущим рисунком.
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.
def _commit_action(self, shapes):
self.document.extend(shapes)
self.undo_stack.append(shapes)
self.redo_stack.clear()
def clear_canvas(self):
self.canvas.delete("all")
self.document.clear()
#undo_stack i redo_stack pozostają nietknięte!
# После 'Очистить' нажатие Ctrl+Z не делает ничего видимого —
# а следующий Ctrl+Y возвращает на холст фигуры, которые
# пользователь только что удалил кнопкой 'Очистить'.
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).
def clear_canvas(self):
self.document.clear()
self.undo_stack.clear()
self.redo_stack.clear()
self.render_document()
def choose_custom_color(self):
_rgb, hex_color = colorchooser.askcolor()
self.set_color(hex_color) # nie testowałem hex_color na None!
# Пользователь нажал 'Отмена' в диалоге —
# следующая же попытка нарисовать линию падает: fill=None недопустим.
askcolor() zwroty (None, None) przy anulowaniu (Rozdział 18.21) – Ustawienie tego jako aktualnego koloru bez zaznaczania oznacza przerwanie rysunku przy następnej próbie.
def choose_custom_color(self):
_rgb, hex_color = colorchooser.askcolor()
if hex_color is not None:
self.set_color(hex_color)
def save_document(self):
path_str = filedialog.asksaveasfilename()
Path(path_str).write_text(...) # nie sprawdziłem pustego sznurka!
# Пользователь нажал 'Отмена' в диалоге сохранения —
# программа пытается записать файл по пути текущей директории.
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.
def save_document(self):
path_str = filedialog.asksaveasfilename()
if not path_str:
return
Path(path_str).write_text(...)
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 правильно заполнен новыми фигурами,
# но на экране всё ещё виден старый рисунок — или пустой холст.
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).
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)
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"
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.
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()
#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 становится больше вместе с окном —
# но уже нарисованные фигуры остаются в прежних абсолютных координатах, не растут.
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.
# 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.