Checkbuttons soll verschwinden bei User-Aktion

Fragen zu Tkinter.
Antworten
Benutzeravatar
Dennis89
User
Beiträge: 1153
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen zusammen,

ich benötige mal wieder euere Hilfe.
Ich hatte kürzlich mal ein Thread eröffnet in dem es um Messwerte ging. Da ich noch keine aktuellen Daten habe, wollte ich das GUI mal vorbereiten.

Ich hätte gerne folgendes:
Der Benutzer kann eine oder mehrere Datein auswählen. Der Name oder die Namen der ausgewählten Dateien werden als Checkboxen aufgelistet.
Wenn dann eine Checkbox angewählt wird, dann sollen die Spalten, die sich in der Datei befinden, aufgelistet werden. Wieder als Checkboxen.

So weit funktioniert das auch, aber es wäre schön wenn die angezeigten Spalten wieder verschwinden, wenn ich den Hacken der ausgewählten Datei wieder wegmache.
Das funktioniert bei mir nicht, weil ich die Checkboxen an keinen Namen gebunden habe und ich nicht weis wie ich die sinnvoll an Namen binden soll, da die Anzahl nicht fest vorgegeben wird und ich ja nicht einfach mal 20 Checkboxen vorbelegen will/kann/soll/darf.

Ich habe hier mal ein Beispielcode, der zeigt jetzt zwar keine Spalten an, aber das Prinzip ist das gleiche, die erstellten Checkboxen sollen verschwinden, wenn ich die Datei wieder abwähle.

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk
from tkinter.filedialog import askopenfilenames
from pathlib import Path


class SensorData(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.user_choices = {}
        ttk.Label(self, text="Datei(en) mit Messwerte auswählen").grid(row=0, column=0)
        ttk.Button(self, text="Öffnen", command=self.show_files).grid(row=1, column=0)

    @staticmethod
    def select_files():
        return [Path(file) for file in askopenfilenames()]

    def read_files(self):
        return self.select_files()

    def show_files(self):
        sensor_files = self.read_files()
        for index, file in enumerate(sensor_files, 2):
            self.user_choices[file] = tk.IntVar()
            ttk.Checkbutton(
                self,
                text=str(file.stem),
                onvalue=True,
                offvalue=False,
                variable=self.user_choices[file],
                command=self.update_selected_files,
            ).grid(row=index, column=0)

    def update_selected_files(self):
        for index, (file, selected) in enumerate(self.user_choices.items(), 2):
            if selected.get():
                ttk.Checkbutton(self, text=file).grid(row=1, column=index)


def main():
    root = tk.Tk()
    root.title("Sensordaten auslesen")
    app = SensorData(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Wenn ich die Checkboxen an Namen binde, dann könnte ich als Namen den Dateinamen verwenden, aber ich würde die Namen das auserhalb der '__init__' erstellen. Soll ich in der '__init__' eine leere Liste für die Checkboxen erstellen, so wie ich dass auch mit dem Wörterbuch gemacht habe? Aber das wäre ja auch schon fast ein Drama, dann füge ich jede Checkbox der Liste hinzu, dann muss ich noch einmal durch die Liste gehen wegen 'grid', weil das ja None zurück gibt und meine Liste sonst aus None anstatt aus Checkbox-Objekten bestehen würde.
Das wäre dann so:

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk
from tkinter.filedialog import askopenfilenames
from pathlib import Path


class SensorData(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.user_choices = {}
        self.checkboxes = []
        ttk.Label(self, text="Datei(en) mit Messwerte auswählen").grid(row=0, column=0)
        ttk.Button(self, text="Öffnen", command=self.show_files).grid(row=1, column=0)

    @staticmethod
    def select_files():
        return [Path(file) for file in askopenfilenames()]

    def read_files(self):
        return self.select_files()

    def show_files(self):
        sensor_files = self.read_files()
        for index, file in enumerate(sensor_files, 2):
            self.user_choices[file] = tk.IntVar()
            ttk.Checkbutton(
                self,
                text=str(file.stem),
                onvalue=True,
                offvalue=False,
                variable=self.user_choices[file],
                command=self.update_selected_files,
            ).grid(row=index, column=0)

    def update_selected_files(self):
        for index, (file, selected) in enumerate(self.user_choices.items(), 2):
            if selected.get():
                self.checkboxes.append(ttk.Checkbutton(self, text=file))
                self.checkboxes[index - 2].grid(row=1, column=index)
            else:
                try:
                    self.checkboxes[index - 2].destroy()
                except IndexError:
                    pass


def main():
    root = tk.Tk()
    root.title("Sensordaten auslesen")
    app = SensorData(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Aber das haut nur in einem Durchgang hin, weil ich ja immer weitere Objekte an die Liste anhänge, dann passt mein Index nicht mehr. Und Objekte aus der Liste löschen bringt auch alles durcheinander.
Nach dem ich nun lange genug eure Lösungen gesehen habe, fühlt sich das falsch/kompliziert an. Daher bitte ich euch um ein zwei Hinweise, wie man das machen würde.

Vielleicht noch ein paar Hintergrundinfos wieso ich das so haben will. Die Dateien mit den Messwerten sind csv-Dateien und ich will die Messwerte grafisch darstellen. Dabei soll der Benutzer auswählen können, welche Messwerte er angezeigt haben will. Entweder in einzelnen Diagrammen nebeneinander oder beispielsweise Messwerte mit der gleichen Einheiten und dem gleichen Zeitstempel in einem Diagramm. Als Beispiel, ich habe zwei Versuche, also zwei Dateien, nun will ich in einem Diagramm sehen wie sich der Druck voneinander unterscheidet. Dann wähle ich beide Dateien aus, wähle dann in beiden Spalten den Druck aus und dann könnte es noch einen Button oder Checkbox geben, mit der Option in einem Diagramm anzeigen.

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Habe jetzt nicht in den Code geschaut, aber wenn man eine vorher unbekannte Anzahl von Werten hat, kann man die in einer Liste speichern. Und falls man eine Abbildung von einem Wert, zum Beispiel einem Dateinamen, auf andere Werte haben will, gibt es Wörterbücher. Und beides lässt sich auch kombinieren.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1153
Registriert: Freitag 11. Dezember 2020, 15:13

Danke für die Antwort. Das ist für mich soweit verständlich und auch umsetzbar. Mein Problem war eher das ich nicht wusste wie ich die Checkbuttons, die ich in der Liste gespeichert habe wie der elegant raus bekomme.
Ich habe mir jetzt so beholfen:

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk
from tkinter.filedialog import askopenfilenames
from pathlib import Path


class SensorData(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.user_choices = {}
        self.checkboxes = []
        ttk.Label(self, text="Datei(en) mit Messwerte auswählen").grid(row=0, column=0)
        ttk.Button(self, text="Öffnen", command=self.show_files).grid(row=1, column=0)

    @staticmethod
    def select_files():
        return [Path(file) for file in askopenfilenames()]

    def read_files(self):
        return self.select_files()

    def show_files(self):
        sensor_files = self.read_files()
        for index, file in enumerate(sensor_files, 2):
            self.user_choices[file] = tk.IntVar()
            ttk.Checkbutton(
                self,
                text=str(file.stem),
                onvalue=True,
                offvalue=False,
                variable=self.user_choices[file],
                command=self.update_selected_files,
            ).grid(row=index, column=0)

    def update_selected_files(self):
        for index, (file, selected) in enumerate(self.user_choices.items(), 2):
            if selected.get():
                self.checkboxes.append(ttk.Checkbutton(self, text=file))
                self.checkboxes[index - 2].grid(row=1, column=index)
            else:
                try:
                    self.checkboxes[index - 2].destroy()
                    self.checkboxes[index - 2] = None
                except IndexError:
                    pass
        self.checkboxes = [checkbox for checkbox in self.checkboxes if checkbox is not None]


def main():
    root = tk.Tk()
    root.title("Sensordaten auslesen")
    app = SensorData(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Es fühlt sich aber nicht richtig an.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1153
Registriert: Freitag 11. Dezember 2020, 15:13

Update:
Mit einem Dictonary könnte es so aussehen, das gefällt mir schon besser:

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk
from tkinter.filedialog import askopenfilenames
from pathlib import Path
from functools import partial


class SensorData(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.user_choices = {}
        self.checkboxes = {}
        ttk.Label(self, text="Datei(en) mit Messwerte auswählen").grid(row=0, column=0)
        ttk.Button(self, text="Öffnen", command=self.show_files).grid(row=1, column=0)

    @staticmethod
    def select_files():
        return [Path(file) for file in askopenfilenames()]

    def read_files(self):
        return self.select_files()

    def show_files(self):
        sensor_files = self.read_files()
        for index, file in enumerate(sensor_files, 2):
            self.user_choices[file] = tk.IntVar()
            ttk.Checkbutton(
                self,
                text=str(file.stem),
                onvalue=True,
                offvalue=False,
                variable=self.user_choices[file],
                command=partial(self.update_selected_files, file, index),
            ).grid(row=index, column=0)

    def update_selected_files(self, file, index):
        if self.user_choices[file].get():
            self.checkboxes[file] = ttk.Checkbutton(self, text=file)
            self.checkboxes[file].grid(row=1, column=index)
        else:
            self.checkboxes[file].destroy()


def main():
    root = tk.Tk()
    root.title("Sensordaten auslesen")
    app = SensorData(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: `read_files()` ist ja letztlich nur ein anderer Name für `select_files()`. Da hätte man also auch einfach ``read_files = select_files`` schreiben können um diese Methode zu definieren. Oder gleich die `select_files()` als `read_files()` benennen können. Wobei `read_files()` eher ein Name für eine Methode wäre die Dateien liest, also deren Inhalte.

Das was da immer `file` heisst ist keine Datei sondern ein Datei*name* oder *-pfad*. Bei `file` würde man Methoden wie `read()`/`write()` und `close()` erwarten.

Man muss den „Öffnen“-Button deaktivieren wenn er benutzt wurde, oder den Code so erweitern, dass kein Murks passiert, wenn der Benutzer den ein zweites mal benutzt.

`update_selected_files()` ist falsch benannt, weil ja nur eine Datei betroffen ist.

Das ”ständige” erstellen und zerstören von Anzeigeelementen würde man eher nicht machen. Man würde die erstellen und dann entsprechend anzeigen und verstecken.

Das `index` bei 2 anfängt ist verwirrend. So entsteht in Gridzeile 1 eine Lücke weil zwischen dem „Öffnen“-Button und den Pfaden mit den Checkboxen eine Spalte unbenutzt ist, und das hängt auch von den Anzeigeelementen vor dem dynamischen Inhalt ab. Da würde man messer noch mal `Frame`-Objekte als Container einbauen, damit egal was sonst noch so angezeigt wird, der Index bei 0 anfängt. Dann lässt sich ein erneutes „Öffnen“ auch leichter implementieren, weil man dann einfach diese Frames komplett leeren kann, ohne den statischen Teil mit zu löschen.

Code: Alles auswählen

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


def select_files():
    return [Path(file) for file in askopenfilenames()]


class SensorData(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        self.user_choices = {}
        self.checkboxes = {}
        ttk.Label(self, text="Datei(en) mit Messwerte auswählen").grid(
            row=0, column=0
        )
        self.open_button = ttk.Button(
            self, text="Öffnen", command=self.show_files
        )
        self.open_button.grid(row=1, column=0)
        self.files_frame = ttk.Frame(self)
        self.files_frame.grid(row=2, column=0)
        self.frame_in_need_of_a_better_name = ttk.Frame(self)
        self.frame_in_need_of_a_better_name.grid(row=1, column=1)

    def update_selected_file(self, file_path, index):
        checkbox = self.checkboxes[file_path]
        if self.user_choices[file_path].get():
            checkbox.grid(row=0, column=index)
        else:
            checkbox.grid_forget()

    def show_files(self):
        for index, file_path in enumerate(select_files()):
            self.user_choices[file_path] = tk.BooleanVar()
            self.checkboxes[file_path] = ttk.Checkbutton(
                self.frame_in_need_of_a_better_name, text=file_path
            )
            ttk.Checkbutton(
                self.files_frame,
                text=str(file_path.stem),
                onvalue=True,
                offvalue=False,
                variable=self.user_choices[file_path],
                command=partial(self.update_selected_file, file_path, index),
            ).grid(row=index, column=0, sticky=tk.W)

        self.open_button["state"] = tk.DISABLED


def main():
    root = tk.Tk()
    root.title("Sensordaten auslesen")
    app = SensorData(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1153
Registriert: Freitag 11. Dezember 2020, 15:13

Vielen Dank.
Wobei `read_files()` eher ein Name für eine Methode wäre die Dateien liest, also deren Inhalte
Dafür war die Funktion auch gedacht. Hier sollten später die csv-Dateien in Pandas-DataFrames gewandelt werden. Ob ich das an der Stelle schon mache oder erst wenn der Hacken gesetzt wurde weis ich noch nicht. Aber ich denke schon davor. Wenn man ein paar Dateien öffnet, rechnet man damit, dass das Program Zeit braucht und man nicht gleich weiter machen kann. Wenn man einen "einfachen" Hacken setzt, sollte es nicht zu Verzögerungen kommen.

Einen besseren Namen für 'frame_in_need_of_a_better_name' werde ich mir natürlich überlegen 😄

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten