Wie Scrollbar mit tkinter

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
nichtluke
User
Beiträge: 2
Registriert: Mittwoch 27. Dezember 2023, 07:07

Hallo zusammen!
ich lerne gerade etwas Python, um genauer zu sein bin ich absoluter Anfänger. Habe jetzt mein erstes Programm mit tkinter-GUI gebastelt.

Wie kann ich eine Scrollbar einbauen? Es soll auch nur soweit Scrollen können, wie Inhalt auf der Seite ist (also nicht drüber hinaus).

Im Allgemeinen generiert hier nur ein Gitter aus Bildern, das bei Klick den Zustand ändert und diesen in einer .txt-File abspeichert.

Danke schonmal!

Code: Alles auswählen

import tkinter as tk
from PIL import Image, ImageTk
import os

def on_image_click(row, col):
    images[row][col]["grayed"] = not images[row][col]["grayed"]
    update_image_state(row, col)
    save_state()

def save_state():
    with open("state.txt", "w") as file:
        for row in images:
            for cell in row:
                file.write(str(int(cell["grayed"])) + " ")

def update_image_state(row, col):
    if images[row][col]["grayed"]:
        images[row][col]["label"].config(image=gray_image)
    else:
        images[row][col]["label"].config(image=photo_images[row][col])

# Initialize the main window
root = tk.Tk()
root.title("JackOfAllChamps v0.0.1")
root.iconbitmap("l.ico")
root.geometry("625x300")
root.configure(bg='#aaf0d1')

# Create a frame for the grid
grid_frame = tk.Frame(root, bg='#aaf0d1', bd=0, relief="solid", highlightbackground='#aaf0d1', highlightcolor='#aaf0d1')
grid_frame.grid(row=1, column=0, columnspan=8, pady=10)

# Add the image above the grid
headline_image = ImageTk.PhotoImage(Image.open(os.path.join("src", "headlineJoac.png")))
headline_label = tk.Label(grid_frame, image=headline_image, bg='#aaf0d1')
headline_label.grid(row=0, column=0, columnspan=8, pady=10)

# Load images and create labels
images = []
gray_image = ImageTk.PhotoImage(Image.open(os.path.join("src", "gray_image.jpeg")))
photo_images = [[None] * 8 for _ in range(3)]

# Load the state
try:
    with open("state.txt", "r") as file:
        state = [int(bit) for bit in file.read().split()]
except FileNotFoundError:
    state = [0] * (3 * 8)

bit_index = 0

for row in range(3):
    row_images = []
    for col in range(8):
        if bit_index < len(state):
            cell = {"grayed": bool(state[bit_index]), "row": row, "col": col}
            photo_images[row][col] = ImageTk.PhotoImage(Image.open(os.path.join("src", f"image_{row}_{col}.jpeg")))
            label = tk.Label(grid_frame, image=photo_images[row][col], bg='#aaf0d1')
            label.bind("<Button-1>", lambda event, r=row, c=col: on_image_click(r, c))
            label.grid(row=row + 1, column=col, padx=5, pady=5)
            cell["label"] = label
            row_images.append(cell)
        bit_index += 1
    images.append(row_images)

# Update the state for correct initial display
for row in range(3):
    for col in range(8):
        update_image_state(row, col)

# Start the GUI loop
root.mainloop()
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@nichtluke: Bevor man da einen Scrollbalken einbaut, sollte man das erst einmal aufräumen.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Wenn man das Hauptprogramm in eine Funktion gesteckt hat, stellt man fest das die vorhanden Funktionen alle das Problem haben, das sie auf globalen Zustand zugreifen. Alle drei brauchen `images` als Argument, und zwei davon auch noch `gray_image` und `photo_images`.

Das könnte man auch über den Default-Argument-Hack bei dem ``lambda``-Ausdruck lösen, aber genau für so etwas ist `functools.partial()` gedacht.

Das werden dann schon ganz schön viele Argumente die da herum gereicht werden. Das ist so knapp an der Grenze wo ich sagen würde das ist schon nicht mehr sinnvoll sich hier um objektorientierte Programmierung zu drücken. Da kommt man bei GUI-Programmierung nicht weit.

Andererseits wird da auch mehr durch die Gegend gereicht als notwendig ist, denn statt alle Bilder und den Zeilen- und Spaltenindex zu übergeben um dann in den Funktionen über Zeilen- und Spaltenindex auf die Daten zuzugreifen, könnte man sich diese Indirektion sparen. Und `images` dann auch als einfache, flache Liste verwalten, statt als zweidimensionale Liste. Diese Eigenschaft wird nirgends wirklich gebraucht, macht aber den Code umständlicher.

Die Bilddaten in dem Wörterbuch brauchen "row" und "col" nicht, da wird nie drauf zugegriffen. An der passenden Stelle im Quelltext kann man das auch komplett erstellen, ohne später noch das "label" ergänzen zu müssen. Und es würde Sinn machen das `PhotoImage`-Objekt mit in das Wörterbuch zu packen um das nicht separat übergeben zu müssen.

Die leere `images`-Liste wird ziemlich früh erstellt. Recht weit weg von der Stelle im Code wo die dann endlich gefüllt wird.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Wenn es im `tkinter`-Modul Konstanten für Zeichenketten mit einer besonderen Bedeutung gibt, sollte man die benutzten, statt eine Zeichenkette mit dem Wert. Und auch selber Konstanten definieren für wiederkehrende Werte mit gleicher Bedeutung.

`grid_frame` wird als *einziges* in das Hauptfenster gesetzt, da macht also weder ``row=1`` Sinn, noch ``columnspan=8``.

Zum erstellen von `PhotoImage`-Objekten wird mehrmals ein fast identischer Ausdruck verwendet, für den es sich IMHO schon lohnt eine Funktion zu schreiben.

Statt `os.path` & Co würde man in neuem Code eher `pathlib` verwenden.

Das `state` und `bit_index` in den verschachtelten Schleifen zum Erstellen von den Anzeigeelementen für das Grid manuell verwaltet werden, ist unnötig unübersichtlich und Fehleranfällig. Da würde man besser die Bitwerte mit `enumerate()` aufzählen und Spalten- und Zeilenindex daraus berechnen. Das spart auch noch drei Ebenen Einrückung für den Code der ein einzelnes Anzeigeelement erzeugt.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial
from pathlib import Path

from PIL import Image, ImageTk

STATE_FILE_PATH = Path("state.txt")
IMAGE_PATH = Path("src")

IMAGES_PER_ROW = 8
IMAGE_ROW_COUNT = 3
BACKGROUND_COLOR = "#aaf0d1"


def load_state():
    try:
        return list(
            map(int, STATE_FILE_PATH.read_text(encoding="ascii").split())
        )
    except FileNotFoundError:
        return [0] * (IMAGE_ROW_COUNT * IMAGES_PER_ROW)


def save_state(images_data):
    STATE_FILE_PATH.write_text(
        " ".join(str(int(image_data["grayed"])) for image_data in images_data)
        + "\n",
        encoding="ascii",
    )


def load_photo_image(file_path):
    return ImageTk.PhotoImage(Image.open(file_path))


def update_image_state(image_data, gray_image):
    image_data["label"].configure(
        image=gray_image if image_data["grayed"] else image_data["photo_image"]
    )


def on_image_click(images_data, image_data, gray_image, _event=None):
    image_data["grayed"] = not image_data["grayed"]
    update_image_state(image_data, gray_image)
    save_state(images_data)


def main():
    root = tk.Tk()
    root.title("JackOfAllChamps v0.0.1")
    root.iconbitmap("l.ico")
    root.configure(bg=BACKGROUND_COLOR)

    grid_frame = tk.Frame(
        root,
        bg=BACKGROUND_COLOR,
        bd=0,
        relief=tk.SOLID,
        highlightbackground=BACKGROUND_COLOR,
        highlightcolor=BACKGROUND_COLOR,
    )
    grid_frame.grid(row=0, column=0, pady=10)

    headline_image = load_photo_image(IMAGE_PATH / "headlineJoac.png")
    tk.Label(grid_frame, image=headline_image, bg=BACKGROUND_COLOR).grid(
        row=0, column=0, columnspan=IMAGES_PER_ROW, pady=10
    )

    gray_image = load_photo_image(IMAGE_PATH / "gray_image.jpeg")
    images_data = []
    for i, bit_value in enumerate(load_state()):
        row_index, column_index = divmod(i, IMAGES_PER_ROW)
        photo_image = load_photo_image(
            IMAGE_PATH / f"image_{row_index}_{column_index}.jpeg"
        )
        label = tk.Label(grid_frame, image=photo_image, bg=BACKGROUND_COLOR)
        image_data = {
            "grayed": bool(bit_value),
            "label": label,
            "photo_image": photo_image,
        }
        update_image_state(image_data, gray_image)
        label.bind(
            "<Button-1>",
            partial(on_image_click, images_data, image_data, gray_image),
        )
        label.grid(row=row_index + 1, column=column_index, padx=5, pady=5)
        images_data.append(image_data)

    root.mainloop()


if __name__ == "__main__":
    main()
Wörterbücher die immer einen festen Satz an Schlüsseln haben sind eigentlich ein Objekt und sollten in der Regel als Klasse modelliert werden.

Und es ist eine GUI, also auch da sollte man eher eine Klasse schreiben, als das über `partial()` zu lösen.

Scrollbalken für ”beliebigen” Inhalt, macht man in Tk mit `Canvas`. Da sollten Beispiele im Netz zu finden sein.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
nichtluke
User
Beiträge: 2
Registriert: Mittwoch 27. Dezember 2023, 07:07

Wow, vielen Dank für deine Mühe!

Funktioniert tatsächlich einwandfrei. Habe nun auch die Scrollbar hinzugefügt.
Jedoch stelle ich mich etwas doof an, eine weitere Zeile im Gitter zu generieren.
Ein einfaches Setzen von

Code: Alles auswählen

IMAGE_ROW_COUNT = 3
zu

Code: Alles auswählen

IMAGE_ROW_COUNT = 4
funktioniert leider nicht.
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@nichtluke: Die Anzahl der Zeilen ergibt sich aus der Anzahl der Bits in der Zustandsdatei, beziehungsweise was `load_state()` zurück liefert. Das sollte im Original doch eigentlich auch so sein, weil da die Zelleninhalte nur erzeugt werden, solange ``bit_index < len(state)`` gilt. `IMAGE_ROW_COUNT` wird im überarbeiteten Quelltext nur verwendet wenn die Datei nicht gefunden werden konnte.

Edit: Spasseshalber mit Objekt statt Wörterbuch für die einzelnen Bildanzeigen (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial
from pathlib import Path

import PIL

STATE_FILE_PATH = Path("state.txt")
IMAGE_PATH = Path("src")

IMAGES_PER_ROW = 8
IMAGE_ROW_COUNT = 3
BACKGROUND_COLOR = "#aaf0d1"


def load_state():
    try:
        return list(
            map(int, STATE_FILE_PATH.read_text(encoding="ascii").split())
        )
    except FileNotFoundError:
        return [0] * (IMAGE_ROW_COUNT * IMAGES_PER_ROW)


def save_state(images):
    STATE_FILE_PATH.write_text(
        " ".join(str(int(image.is_grayed)) for image in images) + "\n",
        encoding="ascii",
    )


def load_photo_image(file_path):
    return PIL.ImageTk.PhotoImage(PIL.Image.open(file_path))


class Image(tk.Label):
    def __init__(self, master, gray_image, photo_image, is_grayed, **kwargs):
        tk.Label.__init__(self, master, **kwargs)
        self.gray_image = gray_image
        self.photo_image = photo_image
        self.is_grayed = is_grayed
        self.update_display()

    def update_display(self):
        self["image"] = self.gray_image if self.is_grayed else self.photo_image

    def toggle_state(self):
        self.is_grayed = not self.is_grayed


def on_image_click(images, image, _event=None):
    image.toggle_state()
    save_state(images)


def main():
    root = tk.Tk()
    root.title("JackOfAllChamps v0.0.1")
    root.iconbitmap("l.ico")
    root.configure(bg=BACKGROUND_COLOR)

    grid_frame = tk.Frame(
        root,
        bg=BACKGROUND_COLOR,
        bd=0,
        relief=tk.SOLID,
        highlightbackground=BACKGROUND_COLOR,
        highlightcolor=BACKGROUND_COLOR,
    )
    grid_frame.grid(row=0, column=0, pady=10)

    headline_image = load_photo_image(IMAGE_PATH / "headlineJoac.png")
    tk.Label(grid_frame, image=headline_image, bg=BACKGROUND_COLOR).grid(
        row=0, column=0, columnspan=IMAGES_PER_ROW, pady=10
    )

    gray_image = load_photo_image(IMAGE_PATH / "gray_image.jpeg")
    images = []
    for i, bit_value in enumerate(load_state()):
        row_index, column_index = divmod(i, IMAGES_PER_ROW)
        image = Image(
            grid_frame,
            gray_image,
            load_photo_image(
                IMAGE_PATH / f"image_{row_index}_{column_index}.jpeg"
            ),
            bit_value,
            bg=BACKGROUND_COLOR,
        )
        image.bind("<Button-1>", partial(on_image_click, images, image))
        image.grid(row=row_index + 1, column=column_index, padx=5, pady=5)
        images.append(image)

    root.mainloop()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten