Berechnungstool VBA zu Python mit Laufzeit Problemen

mit matplotlib, NumPy, pandas, SciPy, SymPy und weiteren mathematischen Programmbibliotheken.
Antworten
vantom
User
Beiträge: 1
Registriert: Mittwoch 2. Juli 2025, 09:07

Hallo liebe Devs,

ich freue mich hier selber mal ein kleines Problemchen zu posten.

Ich habe ein kleines Script von einem Kollegen aus VBA in Python übernommen und ein paar dinge angepasst. Jetzt ist jedoch immer noch das Problem, da es teilweise zu einer aufwändigen Berechnung kommt, dass sich das Programm schnell mal aufhängt.

An sich soll das Programm eine Kombination aus verschiedenen Wandkassetten berechnen, ich habe dazu auch noch implimentiert, dass sich das Programm auf Datenbanken stützen kann um noch bessere kombinationen zu finden.

Hat jemand eine Ahnung wie ich solch einen Berechnungsprozess mit bspw. 10 Kassetten oder der Datenbank schnell rechnen lassen kann.

Mfg Jonas

Code: Alles auswählen

import tkinter as tk
from tkinter import messagebox
import os

RAL_7000 = "#7E8B92"
RAL_7001 = "#8A9597"  # Helleres Grau für Felder und Buttons

class KassetteApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Kassetten Kombi Rechner")
        # Setze das Fenster-Icon
        icon_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "LOGO-TOOLKKR.ico"))
        try:
            self.root.iconbitmap(icon_path)
        except Exception:
            pass

        self.root.configure(bg=RAL_7000)
        self.current_page = 0
        self.inputs = {
            "ziel": tk.StringVar(),
            "toleranz": tk.StringVar(),
            "kassetten": tk.StringVar(),
            "priorisiert": tk.StringVar()
        }
        self.beste_kombi_text = ""
        self.alle_kombis = []
        self.datenbank_einbeziehen = tk.BooleanVar(value=False)
        self.db_dateien = []
        self.db_auswahl = tk.StringVar()

        # Kein Kopfbereich/Titel mehr!

        self.pages = [
            self.seite_zielmass,
            self.seite_toleranz,
            self.seite_kassetten,
            self.seite_prioritaet,
            self.seite_ergebnis
        ]

        self.page_frame = tk.Frame(root, bg=RAL_7001)
        self.page_frame.pack(padx=30, pady=20, fill="both", expand=True)

        self.navigation_frame = tk.Frame(root, bg=RAL_7001)
        self.navigation_frame.pack(pady=(0, 20))

        self.btn_left = tk.Button(
            self.navigation_frame, text="←", width=12, command=self.zurueck,
            font=("Segoe UI", 12), bg=RAL_7001, fg="#222", relief="groove", bd=2, highlightbackground=RAL_7001
        )
        self.btn_left.grid(row=0, column=0, padx=20, ipadx=5, ipady=5)

        self.btn_right = tk.Button(
            self.navigation_frame, text="→", width=12, command=self.vor,
            font=("Segoe UI", 12), bg=RAL_7001, fg="#222", relief="groove", bd=2, highlightbackground=RAL_7001
        )
        self.btn_right.grid(row=0, column=1, padx=20, ipadx=5, ipady=5)

        # Tastatur-Shortcuts
        self.root.bind('<Return>', lambda event: self.btn_right.invoke())
        self.root.bind('<KP_Enter>', lambda event: self.btn_right.invoke())
        self.root.bind('<Escape>', lambda event: self.btn_left.invoke())

        self.show_page()

    def show_page(self):
        for widget in self.page_frame.winfo_children():
            widget.destroy()
        self.pages[self.current_page]()

        self.btn_left.config(
            bg=RAL_7001, highlightbackground=RAL_7001,
            text="←" if self.current_page > 0 else "✖ Beenden",
            command=self.zurueck if self.current_page > 0 else self.root.destroy
        )
        self.btn_right.config(
            bg=RAL_7001, highlightbackground=RAL_7001,
            text="→" if self.current_page < len(self.pages) - 1 else "✅ Berechnen",
            command=self.vor if self.current_page < len(self.pages) - 1 else self.berechnen
        )

    def seite_zielmass(self):
        tk.Label(self.page_frame, text="Gesamtmaß in mm:", font=("Segoe UI", 12), bg=RAL_7001, fg="#222").pack(pady=(30, 5))
        entry = tk.Entry(self.page_frame, textvariable=self.inputs["ziel"], font=("Segoe UI", 12), justify="center", bg="#fff", fg="#222")
        entry.pack(ipady=4, pady=(0, 10))
        entry.focus_set()

    def seite_toleranz(self):
        tk.Label(self.page_frame, text="Toleranz in mm:", font=("Segoe UI", 12), bg=RAL_7001, fg="#222").pack(pady=(30, 5))
        entry = tk.Entry(self.page_frame, textvariable=self.inputs["toleranz"], font=("Segoe UI", 12), justify="center", bg="#fff", fg="#222")
        entry.pack(ipady=4, pady=(0, 10))
        entry.focus_set()

    def seite_kassetten(self):
        tk.Label(self.page_frame, text="Kassettenlängen (mit , getrennt):", font=("Segoe UI", 12), bg=RAL_7001, fg="#222").pack(pady=(30, 5))
        entry = tk.Entry(self.page_frame, textvariable=self.inputs["kassetten"], font=("Segoe UI", 12), justify="center", bg="#fff", fg="#222")
        entry.pack(ipady=4, pady=(0, 10))
        entry.focus_set()
        # Checkbox unter dem Eingabefeld
        cb = tk.Checkbutton(
            self.page_frame,
            text="Datenbank einbeziehen",
            variable=self.datenbank_einbeziehen,
            font=("Segoe UI", 11),
            bg=RAL_7001,
            fg="#222",
            selectcolor=RAL_7001,
            activebackground=RAL_7001,
            command=self.show_page  # Seite neu zeichnen, wenn Haken gesetzt/entfernt wird
        )
        cb.pack(pady=(10, 0))

        # Dropdown für Datenbankauswahl, nur wenn Haken gesetzt
        if self.datenbank_einbeziehen.get():
            self.db_dateien = self.finde_db_dateien()
            wk_liste = [f.replace("kassetten_db_", "").replace(".csv", "") for f in self.db_dateien]
            if wk_liste:
                if not self.db_auswahl.get() or self.db_auswahl.get() not in wk_liste:
                    self.db_auswahl.set(wk_liste[0])
                tk.Label(self.page_frame, text="Datenbank auswählen:", font=("Segoe UI", 11), bg=RAL_7001, fg="#222").pack(pady=(10, 2))
                tk.OptionMenu(self.page_frame, self.db_auswahl, *wk_liste).pack()
            else:
                tk.Label(self.page_frame, text="Keine Datenbankdateien gefunden!", font=("Segoe UI", 11), bg=RAL_7001, fg="red").pack(pady=(10, 2))

    def seite_prioritaet(self):
        tk.Label(self.page_frame, text="Bevorzugte Länge in mm:", font=("Segoe UI", 12), bg=RAL_7001, fg="#222").pack(pady=(30, 5))
        entry = tk.Entry(self.page_frame, textvariable=self.inputs["priorisiert"], font=("Segoe UI", 12), justify="center", bg="#fff", fg="#222")
        entry.pack(ipady=4, pady=(0, 10))
        entry.focus_set()

    def seite_ergebnis(self):
        tk.Label(self.page_frame, text="Beste Kombination:", anchor="center", justify="center",
                 font=("Segoe UI", 13, "bold"), bg=RAL_7001, fg="#222").pack(anchor="center", pady=(30, 5))
        for line in self.beste_kombi_text.split('\n'):
            tk.Label(self.page_frame, text=line, anchor="center", justify="center",
                     font=("Segoe UI", 12), bg=RAL_7001, fg="#222").pack(anchor="center")
        tk.Label(self.page_frame, text="\nAlle gültigen Kombinationen:", font=("Segoe UI", 12, "bold"),
                 bg=RAL_7001, fg="#222").pack(pady=(20, 5))
        text_widget = tk.Text(self.page_frame, height=15, width=60, wrap="word", font=("Segoe UI", 11), bg="#f5f7f8", fg="#222", relief="flat")
        text_widget.pack(pady=(0, 10))
        text_widget.insert("1.0", "\n".join(self.alle_kombis))
        text_widget.config(state="disabled")

    def zurueck(self):
        if self.current_page > 0:
            self.current_page -= 1
            self.show_page()

    def vor(self):
        if self.current_page < len(self.pages) - 1:
            self.current_page += 1
            self.show_page()

    def berechnen(self):
        try:
            ziel = int(self.inputs["ziel"].get())
            toleranz = int(self.inputs["toleranz"].get())
            kassetten = [int(x.strip()) for x in self.inputs["kassetten"].get().split(",") if x.strip().isdigit()]
            priorisiert = int(self.inputs["priorisiert"].get())
        except:
            messagebox.showerror("Fehler", "Bitte überprüfe deine Eingaben!")
            return

        db_kassetten = []
        if self.datenbank_einbeziehen.get():
            if not self.db_dateien:
                messagebox.showerror("Fehler", "Keine Datenbankdateien gefunden!")
                return
            # Finde die gewählte Datei
            wk = self.db_auswahl.get()
            db_file = f"kassetten_db_{wk}.csv"
            db_path = os.path.abspath(os.path.join(r"N:\Public\CO_Bau\_Program\Daten", db_file))
            if not os.path.exists(db_path):
                messagebox.showerror("Fehler", f"Datenbankdatei nicht gefunden: {db_path}")
                return
            import csv
            with open(db_path, newline="", encoding="utf-8") as f:
                reader = csv.DictReader(f)
                for row in reader:
                    try:
                        laenge = int(row["laenge"])
                        db_kassetten.append(laenge)
                    except Exception:
                        continue

        beste, priorisiert_anzahl, gesamt, abw, alle = self.finde_kombinationen(
            ziel, toleranz, kassetten, priorisiert, db_kassetten if self.datenbank_einbeziehen.get() else None
        )
        if not beste:
            self.beste_kombi_text = "Keine gültige Kombination gefunden."
        else:
            if self.datenbank_einbeziehen.get():
                # alle_kassetten ist die Reihenfolge von beste
                alle_kassetten = list(sorted(set(db_kassetten).union(set(kassetten))))
                self.beste_kombi_text = "  ".join(
                    f"{anz}x{k}" for anz, k in zip(beste, alle_kassetten) if anz > 0
                )
            else:
                self.beste_kombi_text = "  ".join(
                    f"{anz}x{k}" for anz, k in zip(beste, kassetten) if anz > 0
                )
            self.beste_kombi_text += f"\nGesamt: {gesamt} Stück | Abweichung: {abw} mm"

        self.alle_kombis = alle
        self.show_page()

    def finde_kombinationen(self, ziel, toleranz, pflicht_kassetten, priorisiert, db_kassetten=None):
        if db_kassetten is not None:
            alle_kassetten = list(sorted(set(db_kassetten).union(set(pflicht_kassetten))))
        else:
            alle_kassetten = pflicht_kassetten

        max_anzahl = [(ziel + toleranz) // k for k in alle_kassetten]
        beste_kombi = None
        beste_priorisiert = -1
        beste_gesamt = float("inf")
        beste_abweichung = None
        alle_kombis = []

        def rekursiv(index, aktuelle):
            nonlocal beste_kombi, beste_priorisiert, beste_gesamt, beste_abweichung

            if index >= len(alle_kassetten):
                summe = sum(anz * kas for anz, kas in zip(aktuelle, alle_kassetten))
                abw = summe - ziel
                if 0 <= abw <= toleranz:  # Nur positive Abweichung zulassen
                    gesamt = sum(aktuelle)
                    anz_prior = aktuelle[alle_kassetten.index(priorisiert)] if priorisiert in alle_kassetten else 0
                    if db_kassetten is not None:
                        for k in pflicht_kassetten:
                            idx = alle_kassetten.index(k)
                            if aktuelle[idx] < 1:
                                return
                    kombi_str = "  ".join(f"{anz}x{k}" for anz, k in zip(aktuelle, alle_kassetten) if anz > 0)
                    kombi_str += f" | Gesamt: {gesamt} | Abweichung: {abw} mm"
                    alle_kombis.append(kombi_str)

                    if (anz_prior > beste_priorisiert) or (anz_prior == beste_priorisiert and gesamt < beste_gesamt):
                        beste_kombi = aktuelle[:]
                        beste_priorisiert = anz_prior
                        beste_gesamt = gesamt
                        beste_abweichung = abw
                return

            for i in range(max_anzahl[index] + 1):
                aktuelle[index] = i
                rekursiv(index + 1, aktuelle)

        rekursiv(0, [0] * len(alle_kassetten))
        return beste_kombi, beste_priorisiert, beste_gesamt, beste_abweichung, alle_kombis

    def finde_db_dateien(self):
        pfad = r"N:\Public\CO_Bau\_Program\Daten"
        files = []
        for f in os.listdir(pfad):
            if f.startswith("kassetten_db_WK") and f.endswith(".csv"):
                files.append(f)
        return files

    def neu_starten(self):
        for var in self.inputs.values():
            var.set("")
        self.datenbank_einbeziehen.set(False)
        self.db_auswahl.set("")
        self.current_page = 0
        self.beste_kombi_text = ""
        self.alle_kombis = []
        self.show_page()

# App starten
if __name__ == "__main__":
    root = tk.Tk()
    app = KassetteApp(root)
    root.mainloop()

nezzcarth
User
Beiträge: 1752
Registriert: Samstag 16. April 2011, 12:47

Kannst du bitte das konkrete algorithmische Problem, das du lösen möchtest, genauer beschreiben? Es gibt viele Ansätze für Optimierungsprobleme, aber um was vorzuschlagen, muss man besser verstehen, was du vorhast.

Generell würde ich in solchen Fällen die GUI erst einmal weglassen und gucken, dass man das Kernproblem sauber gelöst bekommt. Anschließend kannst du dann wieder eine GUI darum packen. Das führt dann auch fast von alleine zu einer Struktur, bei der die Logik und die Darstellung besser von einander getrennt sind, als das bei deinem Code im Moment der Fall ist.
Zuletzt geändert von nezzcarth am Mittwoch 2. Juli 2025, 20:17, insgesamt 1-mal geändert.
Benutzeravatar
__blackjack__
User
Beiträge: 14019
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Im Grunde ist das ja schon getrennt, denn die ”Methode” `finde_kombinationen()` ist gar keine Methode sondern eine eigentlich von der Klasse unabhängige Funktion, die aus irgendwelchen Gründen in der Klasse steckt. Einfach aus der Klasse raus nehmen und isoliert betrachten.

Gilt auch für `finde_db_dateien()`.
“The best book on programming for the layman is »Alice in Wonderland«; but that's because it's the best book on anything for the layman.” — Alan J. Perlis
Sirius3
User
Beiträge: 18260
Registriert: Sonntag 21. Oktober 2012, 17:20

Man benutzt keine kryptischen Abkürzungen, was ist eine `btn` oder eine `kas`? Nur um ein paar Buchstaben zu sparen, sollte man nicht die Lesbarkeit opfern.
Importe gehören an den Anfang der Datei.
Alle konstanten Werte sollte man als Konstanten am Anfang der Datei definieren, wie Du es ja schon mit RAL_7000 und RAL_7001 gemacht hast.
Statt os.path benutzt man heutzutage pathlib. Die Methode `finde_db_dateien` benutzt `self` nicht, ist also eine eigenständige Funktion. Auch andere Funktionen sollten unabhängig von der GUI arbeiten, wie das Laden der Datenbanken, oder das Berechnen der Kombinationen.
`continue` sollte man nicht verwenden. Exceptions sollten immer so konkret wie möglich angegeben werden, z.B. `ValueError` statt `Exception` :

Code: Alles auswählen

import tkinter as tk
from tkinter import messagebox
from pathlib import Path
import csv

RAL_7000 = "#7E8B92"
RAL_7001 = "#8A9597"  # Helleres Grau für Felder und Buttons
ICON_PATH = Path(__file__).parents[2] / "LOGO-TOOLKKR.ico"
DATA_PATH = Path("N:/Public/CO_Bau/_Program/Daten")

def find_db_files():
    return {
        path.stem.removeprefix('kassetten_db_'): path
        for path in DATA_PATH.glob("kassetten_db_WK*.csv")
    }

def load_db_file(db_filepath):
    with db_filepath.open(newline="", encoding="utf-8") as file:
        reader = csv.DictReader(file)
        result = []
        for row in reader:
            try:
                result.append(int(row["laenge"]))
            except ValueError:
                # TODO: sollen die Zeilen einfach ignoriert werden?
                pass
    return result

def find_combinations(...):
    ...
Statt ständig Widgets zu löschen und neuzuerzeugen, macht man das am Anfang einmal und zeigt nur die passenden Frames an.
Langlaufende Berechnungen dürfen nicht innerhalb der GUI laufen, weil diese sonst nicht mehr reagiert, sondern müssen in einem eigenen Thread gestartet werden.
Antworten