[Code Review] GUI zum definierte Excel-Zellen auslesen

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.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Sirius3 hat geschrieben: Mittwoch 9. November 2022, 20:26 Die Logik sollte nichts von der GUI wissen müssen. In `control_program_sequence` ist `cells_to_read` aber ein StringVar-Object, statt eines einfachen Strings.
Danke, habe ich geändert und übergebe jetzt eine Liste mit den Wörter.

@__blackjack__ Danke für die Erklärung. Queue habe ich hier im Forum schon öfters gelesen, aber noch nie benutzt. Ich schaue mal wie weit ich komme.

Grüße
Dennis

Achja, das Auslagern der Excel-Datei-Suche in eine Funktion, war so wie ich es oben gepostet habe dumm. Nach der ersten gefundenen Datei bricht 'return' die Schleife ab. Das habe ich wieder rückgängig gemacht. "Schnell was ändern" ist einfach in jeder Hinsicht doof. :oops:
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Kurzer Zwischenstand für heute.
Für den ersten Versuch benötige ich zwei Funktionen die ich übergebe und die die Queue aktualisieren. War das grundsätzlich so gedacht?

Code: Alles auswählen

#!/usr/bin/env python3
import re
import tkinter as tk
from datetime import datetime
from pathlib import Path
from tkinter import messagebox, ttk
from tkinter.filedialog import askdirectory
from collections import deque

from openpyxl import Workbook, load_workbook

WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = "Volumen, Drehzahl, Gas"


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.columnconfigure(0, weight=100)
        self.user_select = tk.StringVar()
        self.user_select.set("<Kein Oderne ausgewählt>")
        ttk.Label(self, text="Ausgewählter Ordner:").grid(row=0, column=0, sticky=tk.W)
        self.selected_folder = tk.Label(self, textvariable=self.user_select)
        self.selected_folder.grid(row=1, column=0, sticky=tk.W)
        ttk.Button(self, text="Ordner auswählen", command=self.open_dialog).grid(
            row=2, column=0, sticky=tk.W
        )
        ttk.Label(self, text="Zellennamen die ausgelesen werden sollen").grid(
            row=0, column=4, sticky=tk.W
        )
        ttk.Label(self, text="getrennt mit Komma eingeben:").grid(
            row=1, column=4, sticky=tk.W
        )
        self.cells_to_read = tk.StringVar()
        self.cells_to_read.set(DEFAULT_CELL_NAMES)
        ttk.Entry(self, textvariable=self.cells_to_read).grid(
            row=2, column=4, sticky=tk.W
        )
        ttk.Button(self, text="Auslesen", command=self.transfer_gui_data).grid(
            row=5, column=5
        )
        ttk.Button(self, text="Beenden", command=self.quit).grid(
            row=5, column=4, sticky=tk.E
        )
        self.coffee = ttk.Label(self, text="Zeit für Kaffee...")
        self.process_bar = ttk.Progressbar(self)

        self.files_to_be_processed = deque()
        self.folder_path = None

    def open_dialog(self):
        self.folder_path = Path(askdirectory(mustexist=True))
        self.user_select.set(self.folder_path.name)

    def add_files(self, file):
        self.files_to_be_processed.append(file)

    def remove_file(self):
        self.files_to_be_processed.popleft()

    def transfer_gui_data(self):
        control_program_sequence(
            self.cells_to_read, self.folder_path, self.add_files, self.remove_file
        )


def control_program_sequence(cells_to_read, folder_path, add_files, remove_file):
    info_file = False
    timestamp = datetime.now().strftime("%Y%m%d_%H%M")
    collect_xls_files(folder_path, add_files)
    keywords = keywords_to_list(cells_to_read.get().split(","))
    workbook, output_file = open_output_file(timestamp, folder_path)
    worksheet = workbook.active
    for filename in folder_path.glob("**/*.xlsx"):
        if filename.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
            keyword_to_row_content = read_excel_file(filename, keywords)
            not_found_keywords = set(name.lower() for name in keywords) - set(
                name.lower() for name in keyword_to_row_content
            )
            if not not_found_keywords:
                write_into_output_file(worksheet, keyword_to_row_content)
            else:
                write_info_file(timestamp, folder_path, filename, not_found_keywords)
                info_file = True
            remove_file()
    workbook.save(output_file)
    if info_file:
        messagebox.showinfo(
            "Info Datei",
            "Nicht alle gefundenen Dateien konnten gelesen werden, bitte Info-Datei beachten.",
        )


def collect_xls_files(folder_path, add_files):
    for file in folder_path.glob("**/*.xlsx"):
        if file.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
            add_files(file)


def find_keyword(description, keywords):
    match = re.search(rf"\b({'|'.join(keywords)})\b", description, re.IGNORECASE)
    if match:
        return match.group(1)
    return None


def keywords_to_list(cells_to_read):
    return [keyword.strip() for keyword in cells_to_read]


def open_output_file(timestamp, folder_path):
    output_file = folder_path / f"{timestamp}_{WORKBOOK_WITH_READ_DATA}"
    if output_file.exists():
        workbook = load_workbook(output_file)
    else:
        workbook = Workbook()
    return workbook, output_file


def read_excel_file(filename, keywords):
    keyword_to_row_content = {}
    worksheet = load_workbook(filename)["Tabelle1"]
    for description, content in worksheet.iter_rows(min_col=1, max_col=2):
        keyword = find_keyword(description.value, keywords)
        if keyword is not None:
            keyword_to_row_content[keyword] = content.value
    return keyword_to_row_content


def write_info_file(timestamp, folder_path, file, not_found_keywords):
    with (folder_path / f"{timestamp}_{INFO_FILE}").open(
        "a", encoding="UTF-8"
    ) as info_file:
        info_file.write(
            f'-> In der Datei "{file}" wurden folgende Suchwörter nicht gefunden: {not_found_keywords}'
        )


def write_into_output_file(worksheet, keyword_to_row_content):
    for row in keyword_to_row_content.items():
        worksheet.append(row)


def main():
    root = tk.Tk()
    root.title("Auswertung")
    app = App(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()

Jetzt habe ich wieder das Problem, das ich zwei mal über den 'folder_path' iteriere. Aber von meinem Verständnis her muss ich einmal iterieren um die Queue zu füllen, sonst weis ich nicht wieviele Dateien auf mich zu kommen. Dann muss ich jede Datei noch aufrufen. Ich denke um zwei 'for'-schleifen komme ich nicht drum rum. Wenn ich während des befüllens der Queeu noch eine Liste mit den Dateienname erstelle, dann wäre das GUI und die Logik nicht mehr sauber getrennt.

Die Abfrage über 'after' ist noch nicht drin, das mache ich dann, wenn ich das mit den Rückruffunkion(en) richtig gemacht und verstanden habe.

Vielen Dank für eure tolle Unterstützung und gue Nacht
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Logik sollte nichts von der GUI wissen müssen. In `control_program_sequence` ist `cells_to_read` aber ein StringVar-Object, statt eines einfachen Strings.

Callbacks sollten nicht so kompliziert sein. Die Datei, die remove_file löscht, hat ja erstmal nichts mit der Datei zu tun, die gerade abgearbeitet wird.
Im einfachsten Fall gibt heißt der Callback ja einfach progress und bekommt eine Prozentzahl.
Dann muß die GUI nichts über Files wissen.

Wenn Du wirklich die Dateinamen anzeigen willst, brauchst Du zwei getrennte Operationen. Erstens alle Dateinamen ermitteln, und zweitens über diese Liste dann iterierern, um sie abzuarbeiten.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

sorry dass ich den Fehler mit dem StringVar-Object nur verlagert habe.

Ich will die Dateiennamen nicht anzeigen lassen, mir würde eine progress-bar oder einfach eine Prozentanzeige reichen. Wir können ja mal mit der Prozentanzeige beginnen, um mir den Fortschritt ausrechnen zu können benötige ich erst mal die Anzahl aller Dateien und immer wenn eine fertig ist ziehe ich die wieder ab und rechne den Fortschritt erneut aus, in etwas so:

Code: Alles auswählen

 @property
    def progress(self):
        return 100 - (100 * len(self.files_to_be_processed) / self.number_of_all_files)
Im einfachsten Fall gibt heißt der Callback ja einfach progress und bekommt eine Prozentzahl.
Wie ich das umsetzen soll verstehe ich noch nicht ganz. Bis jetzt gehe ich davon aus, dass das Programm sich die Anzahl aller Dateien merken muss und wenn ich die Logik nicht in eine extra Klasse packe, dann stehe ich hier gerade etwas auf dem Schlauch.

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

@Dennis89: Die Informationen sind doch alle *in* der Funktion. Die kann die Prozentzahl berechnen und nach aussen melden, über die Rückruffunktion.

Code: Alles auswählen

def do_something(..., on_progress=lambda percentage: None):
    items = get_items()
    for number, item in enumerate(items, 1):
        on_progress(number / len(items) * 100)
        ...
    ...
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen und danke für die weiteren Hinweise.

Ich glaube jetzt habe ich es verstanden, aber urteilt am besten selbst:

Code: Alles auswählen

#!/usr/bin/env python3
import re
import tkinter as tk
from datetime import datetime
from pathlib import Path
from tkinter import messagebox, ttk
from tkinter.filedialog import askdirectory
from collections import deque

from openpyxl import Workbook, load_workbook

from time import sleep
WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = "Volumen, Drehzahl, Gas"


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.columnconfigure(0, weight=100)
        self.user_select = tk.StringVar()
        self.user_select.set("<Kein Oderne ausgewählt>")
        ttk.Label(self, text="Ausgewählter Ordner:").grid(row=0, column=0, sticky=tk.W)
        self.selected_folder = tk.Label(self, textvariable=self.user_select)
        self.selected_folder.grid(row=1, column=0, sticky=tk.W)
        ttk.Button(self, text="Ordner auswählen", command=self.open_dialog).grid(
            row=2, column=0, sticky=tk.W
        )
        ttk.Label(self, text="Zellennamen die ausgelesen werden sollen").grid(
            row=0, column=4, sticky=tk.W
        )
        ttk.Label(self, text="getrennt mit Komma eingeben:").grid(
            row=1, column=4, sticky=tk.W
        )
        self.cells_to_read = tk.StringVar()
        self.cells_to_read.set(DEFAULT_CELL_NAMES)
        ttk.Entry(self, textvariable=self.cells_to_read).grid(
            row=2, column=4, sticky=tk.W
        )
        ttk.Button(self, text="Auslesen", command=self.transfer_gui_data).grid(
            row=5, column=5
        )
        ttk.Button(self, text="Beenden", command=self.quit).grid(
            row=5, column=4, sticky=tk.E
        )
        self.coffee = ttk.Label(self, text="Zeit für Kaffee...")
        self.process_bar = ttk.Progressbar(self)

        self.process = deque()
        self.folder_path = None
        self.process_status = False

    def open_dialog(self):
        self.folder_path = Path(askdirectory(mustexist=True))
        self.user_select.set(self.folder_path.name)

    def on_progress(self, percentage):
        try:
            self.process.popleft()
        except IndexError:
            pass
        self.process.append(percentage)
        if percentage == 100:
            self.process_status = True

    def update_process(self):
        # Instead of print -> progressbar or label with percent
        print(self.process)
        if not self.process_status:
            self.after(10, self.update_process)
        else:
            self.process_status = False

    def transfer_gui_data(self):
        self.update_process()
        control_program_sequence(
            self.cells_to_read.get(), self.folder_path, self.on_progress
        )


def control_program_sequence(cells_to_read, folder_path, on_progress=lambda percentage: None):
    info_file = False
    timestamp = datetime.now().strftime("%Y%m%d_%H%M")
    keywords = keywords_to_list(cells_to_read)
    workbook, output_file = open_output_file(timestamp, folder_path)
    worksheet = workbook.active
    files = collect_files(folder_path)
    for number, filename in enumerate(files, 1):
        on_progress(number / len(files) * 100)
        keyword_to_row_content = read_excel_file(filename, keywords)
        not_found_keywords = set(name.lower() for name in keywords) - set(
            name.lower() for name in keyword_to_row_content
        )
        if not not_found_keywords:
            write_into_output_file(worksheet, keyword_to_row_content)
        else:
            write_info_file(timestamp, folder_path, filename, not_found_keywords)
            info_file = True
            sleep(0.5)
    workbook.save(output_file)
    if info_file:
        messagebox.showinfo(
            "Info Datei",
            "Nicht alle gefundenen Dateien konnten gelesen werden, bitte Info-Datei beachten.",
        )


def collect_files(folder_path):
    files = []
    for file in folder_path.glob("**/*.xlsx"):
        if file.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
            files.append(file)
    return files


def find_keyword(description, keywords):
    match = re.search(rf"\b({'|'.join(keywords)})\b", description, re.IGNORECASE)
    if match:
        return match.group(1)
    return None


def keywords_to_list(cells_to_read):
    return [keyword.strip() for keyword in cells_to_read.split(",")]


def open_output_file(timestamp, folder_path):
    output_file = folder_path / f"{timestamp}_{WORKBOOK_WITH_READ_DATA}"
    if output_file.exists():
        workbook = load_workbook(output_file)
    else:
        workbook = Workbook()
    return workbook, output_file


def read_excel_file(filename, keywords):
    keyword_to_row_content = {}
    worksheet = load_workbook(filename)["Tabelle1"]
    for description, content in worksheet.iter_rows(min_col=1, max_col=2):
        keyword = find_keyword(description.value, keywords)
        if keyword is not None:
            keyword_to_row_content[keyword] = content.value
    return keyword_to_row_content


def write_info_file(timestamp, folder_path, file, not_found_keywords):
    with (folder_path / f"{timestamp}_{INFO_FILE}").open(
        "a", encoding="UTF-8"
    ) as info_file:
        info_file.write(
            f'-> In der Datei "{file}" wurden folgende Suchwörter nicht gefunden: {not_found_keywords}'
        )


def write_into_output_file(worksheet, keyword_to_row_content):
    for row in keyword_to_row_content.items():
        worksheet.append(row)


def main():
    root = tk.Tk()
    root.title("Auswertung")
    app = App(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]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Normalerweise muß man Daten von Threads über eine richtige Queue übermitteln; das mit dem popleft ist nicht sehr schön. Queues haben eine get-Methode, so dass man in update_process weiß, wenn get etwas liefert muß ich was updaten, ansonsten versuche ich es in 10ms nochmal.
Da es sich hier aber nur um eine Zahl handelt, könnte man auch die Zahl `process` direkt setzen.
Das mit dem Flag process_status ist auch etwas umständlich. `after` liefert eine ID zurück, mit der man after-Aufruf abbrechen kann. Hier ist es aber noch viel einfacher. Sobald process 100 erreicht hat, einfach kein after mehr aufrufen.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

In `control_program_sequence()` ist auch immer noch GUI drin. Wenn ich die Funktion von einer CLI aus verwenden will, dann sollte da am Ende nicht eine Benachrichtigungsfenster aufgehen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Vielen Dank für eure Hinweise.
Was haltet ihr davon?

Code: Alles auswählen

#!/usr/bin/env python3
import re
import tkinter as tk
from datetime import datetime
from pathlib import Path
from queue import Queue
from threading import Thread
from tkinter import messagebox, ttk
from tkinter.filedialog import askdirectory

from openpyxl import Workbook, load_workbook

WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = "Volumen, Drehzahl, Gas"


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.columnconfigure(0, weight=100)
        self.user_select = tk.StringVar()
        self.user_select.set("<Kein Oderne ausgewählt>")
        ttk.Label(self, text="Ausgewählter Ordner:").grid(row=0, column=0, sticky=tk.W)
        self.selected_folder = tk.Label(self, textvariable=self.user_select)
        self.selected_folder.grid(row=1, column=0, sticky=tk.W)
        ttk.Button(self, text="Ordner auswählen", command=self.open_dialog).grid(
            row=2, column=0, sticky=tk.W
        )
        ttk.Label(self, text="Zellennamen die ausgelesen werden sollen").grid(
            row=0, column=4, sticky=tk.W
        )
        ttk.Label(self, text="getrennt mit Komma eingeben:").grid(
            row=1, column=4, sticky=tk.W
        )
        self.cells_to_read = tk.StringVar()
        self.cells_to_read.set(DEFAULT_CELL_NAMES)
        ttk.Entry(self, textvariable=self.cells_to_read).grid(
            row=2, column=4, sticky=tk.W
        )
        ttk.Button(self, text="Auslesen", command=self.transfer_gui_data).grid(
            row=5, column=5
        )
        ttk.Button(self, text="Beenden", command=self.quit).grid(
            row=5, column=4, sticky=tk.E
        )
        self.process_in_percent = tk.StringVar()
        ttk.Label(self, textvariable=self.process_in_percent).grid(row=4, column=4)

        self.process = Queue()
        self.folder_path = None

    def open_dialog(self):
        self.folder_path = Path(askdirectory(mustexist=True))
        self.user_select.set(self.folder_path.name)

    def on_progress(self, percentage):
        self.process.put(percentage)

    @staticmethod
    def show_info_file_message(status):
        if status:
            messagebox.showinfo(
                "Info Datei",
                "Nicht alle gefundenen Dateien konnten gelesen werden, bitte Info-Datei beachten.",
            )

    def update_process(self):
        percent = self.process.get(block=True)
        self.process_in_percent.set(f"{percent} %")
        if percent != 100:
            self.after(10, self.update_process)
        else:
            self.process_in_percent.set("Fertig!")

    def transfer_gui_data(self):
        Thread(
            target=control_program_sequence,
            args=[
                self.cells_to_read.get(),
                self.folder_path,
                self.on_progress,
                self.show_info_file_message,
            ],
        ).start()
        self.update_process()


def control_program_sequence(
    cells_to_read,
    folder_path,
    on_progress=lambda percentage: None,
    show_info_file_message=lambda status: False,
):
    info_file = False
    timestamp = datetime.now().strftime("%Y%m%d_%H%M")
    keywords = keywords_to_list(cells_to_read)
    workbook, output_file = open_output_file(timestamp, folder_path)
    worksheet = workbook.active
    files = collect_files(folder_path)
    for number, filename in enumerate(files, 1):
        on_progress(number / len(files) * 100)
        keyword_to_row_content = read_excel_file(filename, keywords)
        not_found_keywords = set(name.lower() for name in keywords) - set(
            name.lower() for name in keyword_to_row_content
        )
        if not not_found_keywords:
            write_into_output_file(worksheet, keyword_to_row_content)
        else:
            write_info_file(timestamp, folder_path, filename, not_found_keywords)
            info_file = True
    workbook.save(output_file)
    if info_file:
        show_info_file_message(True)
    else:
        show_info_file_message(False)


def collect_files(folder_path):
    files = []
    for file in folder_path.glob("**/*.xlsx"):
        if file.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
            files.append(file)
    return files


def find_keyword(description, keywords):
    match = re.search(rf"\b({'|'.join(keywords)})\b", description, re.IGNORECASE)
    if match:
        return match.group(1)
    return None


def keywords_to_list(cells_to_read):
    return [keyword.strip() for keyword in cells_to_read.split(",")]


def open_output_file(timestamp, folder_path):
    output_file = folder_path / f"{timestamp}_{WORKBOOK_WITH_READ_DATA}"
    if output_file.exists():
        workbook = load_workbook(output_file)
    else:
        workbook = Workbook()
    return workbook, output_file


def read_excel_file(filename, keywords):
    keyword_to_row_content = {}
    worksheet = load_workbook(filename)["Tabelle1"]
    for description, content in worksheet.iter_rows(min_col=1, max_col=2):
        keyword = find_keyword(description.value, keywords)
        if keyword is not None:
            keyword_to_row_content[keyword] = content.value
    return keyword_to_row_content


def write_info_file(timestamp, folder_path, file, not_found_keywords):
    with (folder_path / f"{timestamp}_{INFO_FILE}").open(
        "a", encoding="UTF-8"
    ) as info_file:
        info_file.write(
            f'-> In der Datei "{file}" wurden folgende Suchwörter nicht gefunden: {not_found_keywords}'
        )


def write_into_output_file(worksheet, keyword_to_row_content):
    for row in keyword_to_row_content.items():
        worksheet.append(row)


def main():
    root = tk.Tk()
    root.title("Auswertung")
    app = App(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]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

self.process.get sollte nicht blockieren.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Damit hatte ich auch schon gespielt. Das erste mal stolpert der dann über eine leere Queue, dann müsste ich eine Ausnahmebehandlung machen, in der ich eigentlich keine Ausnahme behandle:

Code: Alles auswählen

    def update_process(self):
        try:
            percent = self.process.get(block=False)
            self.process_in_percent.set(f"{percent} %")
            if percent != 100:
                self.after(10, self.update_process)
            else:
                self.process_in_percent.set("Fertig!")
        except Empty:
            pass
Oder ein kleines 'sleep' bevor die Funktion das erste mal aufgerufen wird.
Was wäre besser, bzw. wie macht man es richtig?

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Der GUI-Code darf nicht blockieren, also auch nicht schlafen. Und Du weißt ja nicht, wie schnell das Lesen der Exceldateien ist.
Und es ist ganz normal, dass man als Ausnahmebehandlung auch nichts machen darf.

Code: Alles auswählen

    def update_process(self):
        try:
            percent = self.process.get(block=False)
        except Empty:
            pass
        else:
            self.process_in_percent.set(f"{percent} %")
            if percent != 100:
                self.after(10, self.update_process)
            else:
                self.process_in_percent.set("Fertig!")
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Okay, perfekt und vielen Dank.

Wäre das Programm dann jetzt so in die Richtung wie ihr es auch schreiben würdet? Was würdet ihr anders machen, wenn man von euch so ein Programm erwarten würde?
Ich frage deshalb um mir immer nächst höhere Ziele zu setzen. Auch wenn ich mir bewusst bin, dass ich euer Niveau nicht erreiche, aber wenn ich nur Kleinigkeiten davon mitnehme und umsetzen kann, habe ich Spass am Lernerfolg.

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

Hallo,

'update_process' musste ich noch etwas abändern. Sonst würde 'after' nur aufgerufen werden, wenn die Queue nicht leer ist. Da die das aber am Anfang ist, wird die Funktion 'update_process' nur ein einzigstes mal aufgerufen.
Ich habe es jetzt so gelöst:

Code: Alles auswählen

    def update_process(self):
        try:
            percent = self.process.get(block=False)
        except Empty:
            self.after(10, self.update_process)
        else:
            self.process_in_percent.set(f"{percent:.0f} %")
            if percent != 100:
                self.after(10, self.update_process)
            else:
                self.process_in_percent.set("Fertig!")
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Oder so:

Code: Alles auswählen

    def update_process(self):
        try:
            percent = self.process.get(block=False)
        except Empty:
            pass
        else:
            if percent == 100:
                self.process_in_percent.set("Fertig!")
                return
            self.process_in_percent.set(f"{percent:.0f} %")
        self.after(10, self.update_process)
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Danke, das gefällt mir noch besser. Da muss man keine zwei 'after'-Aufrufe schreiben.

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

Ich melde mich noch mal. Mir ist eingefallen, das es ja sein könnte, das man während des Auslesens das Programm abbrechen will. Damit das Auslesen auch aufhört, muss ich den Thread beenden, was haltet ihr von der folgenden Lösung? Ich gehe davon aus, das bis man nach dem Start-Button auf den Abbrechen-Button geklickt hat, die 'for'-Schleife bereits läuft.

Die Messagebox, die erscheint wenn man keinen Ordner ausgewählt hat habe ich auch noch eingebaut.
Dann habe ich noch eine tkinter-Frage. Ich weis man gibt keine festen Größen an, damit das GUI auf jeder Bildschirmauflösung bedienbar ist. Bei mir ist jetzt alles noch etwas arg zusammengedrückt. 'ttk.Label' bietet 'width' an, das ist standardmäßig auf 20 eingestellt. Kann ich an der Schraube drehen oder verwende ich eher 'columnspan' in 'grid'? Ich hätte zwischen den Labels gerne etwas mehr Platz und das Entry-Feld würde ich gerne länger machen, damit der Benutzer nicht das Gefühl hat, da wäre kein Platz für seine Eingaben.

Code: Alles auswählen

#!/usr/bin/env python3
import re
import tkinter as tk
from datetime import datetime
from pathlib import Path
from queue import Empty, Queue
from threading import Event, Thread
from tkinter import messagebox, ttk
from tkinter.filedialog import askdirectory

from openpyxl import Workbook, load_workbook

WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = "Volumen, Drehzahl, Gas"


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.columnconfigure(0, weight=100)
        self.user_select = tk.StringVar()
        self.user_select.set("<Kein Oderne ausgewählt>")
        ttk.Label(self, text="Ausgewählter Ordner:").grid(row=0, column=0, sticky=tk.W)
        self.selected_folder = tk.Label(self, textvariable=self.user_select)
        self.selected_folder.grid(row=1, column=0, sticky=tk.W)
        ttk.Button(self, text="Ordner auswählen", command=self.open_dialog).grid(
            row=2, column=0, sticky=tk.W
        )
        ttk.Label(self, text="Zellennamen die ausgelesen werden sollen").grid(
            row=0, column=4, sticky=tk.W
        )
        ttk.Label(self, text="getrennt mit Komma eingeben:").grid(
            row=1, column=4, sticky=tk.W
        )
        self.cells_to_read = tk.StringVar()
        self.cells_to_read.set(DEFAULT_CELL_NAMES)
        ttk.Entry(self, textvariable=self.cells_to_read).grid(
            row=2, column=4, sticky=tk.W
        )
        ttk.Button(self, text="Auslesen", command=self.transfer_gui_data).grid(
            row=5, column=5
        )
        ttk.Button(self, text="Beenden", command=self.stop_program).grid(
            row=5, column=4, sticky=tk.E
        )
        self.process_in_percent = tk.StringVar()
        ttk.Label(self, textvariable=self.process_in_percent).grid(row=4, column=4)

        self.cancel = Event()
        self.process = Queue()
        self.folder_path = None

    def open_dialog(self):
        self.folder_path = Path(askdirectory(mustexist=True))
        self.user_select.set(self.folder_path.name)

    def on_progress(self, percentage):
        self.process.put(percentage)

    @staticmethod
    def show_info_file_message(status):
        if status:
            messagebox.showinfo(
                "Info Datei",
                "Nicht alle gefundenen Dateien konnten gelesen werden, bitte Info-Datei beachten.",
            )

    def stop_program(self):
        self.cancel.set()
        self.quit()

    def update_process(self):
        try:
            percent = self.process.get(block=False)
        except Empty:
            pass
        else:
            if percent == 100:
                self.process_in_percent.set("Fertig!")
                return
            self.process_in_percent.set(f"{percent:.0f} %")
        self.after(10, self.update_process)

    def transfer_gui_data(self):
        Thread(
            target=control_program_sequence,
            args=[
                self.cells_to_read.get(),
                self.folder_path,
                self.cancel,
                self.on_progress,
                self.show_info_file_message,
            ],
        ).start()
        self.update_process()


def control_program_sequence(
    cells_to_read,
    folder_path,
    cancel,
    on_progress=lambda percentage: None,
    show_info_file_message=lambda status: False,
):
    info_file = False
    timestamp = datetime.now().strftime("%Y%m%d_%H%M")
    keywords = keywords_to_list(cells_to_read)
    workbook, output_file = open_output_file(timestamp, folder_path)
    worksheet = workbook.active
    files = collect_files(folder_path)
    for number, filename in enumerate(files, 1):
        if cancel.is_set():
            break
        on_progress(number / len(files) * 100)
        keyword_to_row_content = read_excel_file(filename, keywords)
        not_found_keywords = set(name.lower() for name in keywords) - set(
            name.lower() for name in keyword_to_row_content
        )
        if not not_found_keywords:
            write_into_output_file(worksheet, keyword_to_row_content)
        else:
            write_info_file(timestamp, folder_path, filename, not_found_keywords)
            info_file = True
    workbook.save(output_file)
    if info_file:
        show_info_file_message(True)
    else:
        show_info_file_message(False)


def collect_files(folder_path):
    files = []
    for file in folder_path.glob("**/*.xlsx"):
        if file.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
            files.append(file)
    return files


def find_keyword(description, keywords):
    match = re.search(rf"\b({'|'.join(keywords)})\b", description, re.IGNORECASE)
    if match:
        return match.group(1)
    return None


def keywords_to_list(cells_to_read):
    return [keyword.strip() for keyword in cells_to_read.split(",")]


def open_output_file(timestamp, folder_path):
    output_file = folder_path / f"{timestamp}_{WORKBOOK_WITH_READ_DATA}"
    if output_file.exists():
        workbook = load_workbook(output_file)
    else:
        workbook = Workbook()
    return workbook, output_file


def read_excel_file(filename, keywords):
    keyword_to_row_content = {}
    worksheet = load_workbook(filename)["Tabelle1"]
    for description, content in worksheet.iter_rows(min_col=1, max_col=2):
        keyword = find_keyword(description.value, keywords)
        if keyword is not None:
            keyword_to_row_content[keyword] = content.value
    return keyword_to_row_content


def write_info_file(timestamp, folder_path, file, not_found_keywords):
    with (folder_path / f"{timestamp}_{INFO_FILE}").open(
        "a", encoding="UTF-8"
    ) as info_file:
        info_file.write(
            f'-> In der Datei "{file}" wurden folgende Suchwörter nicht gefunden: {not_found_keywords}'
        )


def write_into_output_file(worksheet, keyword_to_row_content):
    for row in keyword_to_row_content.items():
        worksheet.append(row)


def main():
    root = tk.Tk()
    root.title("Auswertung")
    app = App(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

es hat sich leider ein neues Problem aufgetan. Es haben sich ein paar Anforderungen geändert, ab jetzt läuft das Programm so wie es soll.
Ich habe mit auto-py-to-exe eine exe-Datei erstellt, damit der Benutzer nichts installieren muss und das Programm auch einfach auf anderen PC's ausgeführt werden kann.

Wenn ich beim erstellen der exe-Datei "One-File" wähle, dann bekomme ich eine Datei und die kann ich hinschieben wo ich will und funktioniert immer. Das ist optimal, wenn jetzt nicht das aber kommen würde.
Eine neue Anforderung war, dass die Excel-Datei mit den ausgelesenen Daten in dem Ordner gespeichert wird, in dem das Programm abgelegt ist. Das ist bei einer "normalen" py-Datei auch kein Problem, da kann ich den Pfad mit

Code: Alles auswählen

pathlib.Path(__file__).parent.absolute()
ermitteln.

Die exe-Datei erstellt aber einen Temp-Ordner im User-Verzeichnis, führt das Skript darin aus und der Ordner wird ja wieder gelöscht.
Kann ich den Pfad der exe-Datei irgendwie ermitteln?

Die Alternative, die mir aber nicht ganz so gefällt, ich kann bei auto-py-to-exe auch "One-Directory" auswählen. Dann habe ich einen Ordner mit den ganzen Python-Dateien und zwischen drin liegt die exe-Datei. Das wäre zwar einfacher den Pfad zu ermitteln, aber gefallen tut mir das nicht.

Könnt ihr mir bitte noch einmal helfen?

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14543
Registriert: Mittwoch 14. Oktober 2015, 14:29

Laut https://pyinstaller.org/en/stable/runti ... ation.html ist deine Bemerkung nicht korrekt, __file__ wird von pyinstaller korrigiert. Sollte dem nicht so sein, ist pyinstallers Doku aber eh der Ort, darueber etwas zu lernen.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo und danke für die schnelle Antwort.

In deinem Link steht:
In the bundled script, the PyInstaller bootloader always sets the __file__ variable inside the __main__ module to the absolute path inside the bundle directory, as if the byte-compiled entry-point script existed there.
Das verstehe ich, dass das genau meine Aussage wiederlegt. Und __file__ der Pfad wäre, in dem meine exe-Datei liegt?
Ich habe in der GUI ein Label erstellt und mir dort den Pfad von 'pathlib.Path(__file__).parent.absolute()' ausgeben lassen und dort wird mir der Pfad zu dem Temp-Ordner angezeigt.

Ich konnte jetzt noch nichts finden, wie ich den Pfad rausbekomme, an dem die exe-Datei liegt. Da hört bei mir auch die Vorstellung auf. Ich stelle mir vor, dass die exe den Temp-Ordner erstellt und darin die Dateien bereithält die Python und mein Skript benötigen. Also eine künstliche Umgebung. Wie soll jetzt festgestellt werden, von welchem Ort aus die Umgebung erstellt wurde?

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