Rozdział 18 · Ostatnie poprawki

Zapisywanie i ładowanie JSON

Zapisuje się nie obrazek, lecz dokument figur — Canvas potrafi odtworzyć z niego płótno w dowolnym momencie.

Canvas nie zapisuje zdjęcia jako pliku – dokument jest zapisywany

Ważne, szczere zastrzeżenie: Canvas nie potrafi zrobić tego samodzielnie Eksportuj się do zwykłych PNG lub JPEG – niezawodny, wieloplatformowy eksport rastrowy wymaga dodatkowych zależności i wykracza poza zakres tego rozdziału. Zamiast tego zapisujemy DOCUMENT to lista liczb, z których Canvas już zbudowany jest render_document() (Rozdział 18.29):

document: list[Shape]
save_document()
plik JSON- na dysku
Ochrona środowiska.
plik JSON- na dysku
load_document()
document: list[Shape]
render_document()
Canvas
Ładowanie – Zawsze kończy się ponownym rysowaniem, a nie tylko odczytem pliku.
drawing.json
{
  "version": 1,
  "canvas": {"background": "#ffffff"},
  "items": [
    {
      "kind": "line",
      "coords": [34, 52, 180, 90],
      "color": "#2563eb",
      "width": 4
    },
    {
      "kind": "rectangle",
      "coords": [80, 120, 220, 210],
      "color": "#dc2626",
      "width": 3
    }
  ]
}
sohranenie_json.py
def save_document(self):
    path_str = filedialog.asksaveasfilename(
        defaultextension=".json", filetypes=[(„Figura JSON”, "*.json")]
    )
    if not path_str:  # użytkownik kliknął "Anuluj" — path_str == ""
        return
    data = {
        "version": 1,
        "canvas": {"background": CANVAS_BG},
        "items": [shape.to_dict() for shape in self.document],
    }
    Path(path_str).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
Real Window: rysowanie z wieloma kształtami tuż przed zapisem
Prawdziwe okno: zdjęcie przed Ctrl+S — ten sam dokument będzie w pliku JSON-.
Rzeczywiste okno: ten sam obraz, odtworzony po załadowaniu JSON w nowej sesji
Realne okno: ten sam obraz po Ctrl+O w innym uruchomieniu programu — przywrócony z JSON, a nie z pamięci poprzedniego procesu.
Anulowanie dialogu nie jest błędem, lecz normalnym sposobem
I asksaveasfilename(), i askopenfilename() zwracają pusty łańcuch znaków ""jeśli użytkownik kliknie „Anuluj” None i nie rzucaj wyjątek. Kod musi to zweryfikować PRZED przekazaniem ścieżki do Path(...): Path("") jest ważnym, ale bezsensownym obiektem, który wskazuje na aktualny katalog, a nie na plik wybrany przez użytkownika.

A jeśli plik jest uszkodzony?

Użytkownik otwiera plik, co oznacza, że może wybrać dowolne: czyjąś JSON, obraz, plik niepełny w przypadku niepowodzenia, ręcznie edytowany rysunek z literówką. Rozdział 15 (Rozdział 15.25) już wykazała, że json.load() o tym informuje via json.JSONDecodeError. Tutaj jest dodawany do Wiele funkcji: Kluczowe "items" może w ogóle nie istnieć, kształt może brakować pola, a kolor może być nić, którego Tk nie zna.

Ważne jest, by nie tylko „nie upadać” lokalny lista i zastępuje dokument tylko wtedy, gdy analiza jest całkowicie skuteczna:

zagruzka_bezopasno.py
def load_from_path(self, path):
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        shapes = [Shape.from_dict(item) for item in data["items"]]
        for shape in shapes:
            self.canvas.winfo_rgb(shape.color)   # kolor istnieje?
    except json.JSONDecodeError as exc:
        messagebox.showerror(„Plik nie mógł zostać otwarty”, f"To nie jest poprawna JSON.\n{exc}")
        return
    except (KeyError, TypeError, ValueError, tk.TclError) as exc:
        messagebox.showerror(„Plik nie mógł zostać otwarty”, f"To nie jest zapisany rysunek n{exc}")
        return

    # Parsowanie zakończyło się sukcesem, tylko teraz zmieniamy stan aplikacji.
    self.document = shapes
    self.undo_stack.clear()
    self.redo_stack.clear()
    self.render_document()
Najpierw rozmontować, potem wymienić — i nie odwrotnie
Pokusa pisania self.document = [...] od razu, ale try tylko się trzymać json.loads(). Wtedy plik o nieznanym kolorze zostanie rozłożony, dokument już zostanie zastąpiony — i upadnie render_document(). Podsumowując: stary rysunek zostaje zgubiony, historia cofnięć jest czysta, a każda kolejna próba narysowania czegoś znowu się zawiesza, bo przerysowanie zawsze kończy się tym samym złym kondem. Sprawdzenie PRZED zadaniem kosztuje jedną dodatkową zmienną i oszczędza pracę użytkownika.
Sprawdzanie koloru potrafi tylko płótno
Shape świadomie nic nie wie o Tkinter (Rozdział 18.29), więc nie może sprawdzić koloru za niego "#2563eb" oraz "не-цвет" są równie podobne do koloru. Ale canvas.winfo_rgb(color) sam pyta Tk i rzuca TclErrorjeśli takiego koloru nie ma. Podział obowiązków zostaje zachowany: model sprawdza strukturę, płótno sprawdza, co należy do rysunku.
Praktyka: Zapisz i wczytaj rysunek, aby JSON
Moduł tkinter otwiera natywne okno Python — wykonaj lokalnie w VS Code, PyCharm lub Jupyter
Praktyka kursuje lokalnie
Otwórz praktykę →