From 1bd34ec5bf1474ad01d731ae43fb3d4a9825d9de Mon Sep 17 00:00:00 2001 From: Akamaru Date: Wed, 23 Apr 2025 23:03:55 +0200 Subject: [PATCH] =?UTF-8?q?Komplett=20=C3=BCberarbeitet=20(v1.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue JSON-Struktur - GUI komplett überarbeitet - Kategorien hinzugefügt - Optionen um Kategorien und Programme hinzuzufügen - Optionen zum Sortieren der Listr - Starten über start.bat ohne CDM-Fenster - README aktualisiert --- .gitignore | 2 + README.md | 67 ++- icon.ico | Bin 0 -> 67646 bytes program_list.json | 14 - run.py | 1006 +++++++++++++++++++++++++++++++++++++++++++-- screenshot.png | Bin 0 -> 32834 bytes start.bat | 2 + 7 files changed, 1036 insertions(+), 55 deletions(-) create mode 100644 .gitignore create mode 100644 icon.ico delete mode 100644 program_list.json create mode 100644 screenshot.png create mode 100644 start.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c26d32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +data.json diff --git a/README.md b/README.md index f1cdc0e..608b250 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,63 @@ -# Programm-Öffner +# Programm-Shortcut -## Beschreibung -Programm-Öffner ist ein einfaches Python-Projekt, das als Test für ein Shortcut-Programm dient. Mit dieser Anwendung kannst du "schnell" installierte Programme auf deinem Computer öffnen. +Ein Programm-Launcher für Windows mit Kategorieverwaltung und Programm-Icons. -## Funktionen -- Öffnen von installierten Programmen über einen Shortcut. +![Screenshot des Programm-Shortcut](screenshot.png) -## Anforderungen -- Python 3.x \ No newline at end of file +## Features + +- Organisiere deine Programme in benutzerdefinierten Kategorien +- Programme werden mit ihren Original-Icons angezeigt +- Sortiere Programme nach Name oder Kategorie +- Einfaches Hinzufügen, Bearbeiten und Entfernen von Programmen +- Einstellungen werden automatisch gespeichert +- Kompaktes und benutzerfreundliches Interface + +## Installation + +1. Stelle sicher, dass Python 3.6 oder höher installiert ist +2. Klone das Repository oder lade es herunter +3. Installiere die erforderlichen Abhängigkeiten: + +``` +pip install pywin32 Pillow +``` + +## Verwendung + +Starte die Anwendung mit: + +``` +python run.py +``` +oder mit der start.bat + +### Programme hinzufügen: + +1. Klicke auf "Hinzufügen" +2. Gebe den Programmnamen ein +3. Wähle den Programmpfad über "Durchsuchen" +4. Wähle eine Kategorie oder erstelle eine neue +5. Klicke auf "Speichern" + +### Programme starten: + +1. Wähle eine Kategorie aus der linken Seitenleiste +2. Wähle ein Programm aus der Liste +3. Klicke auf "Starten" + +## Abhängigkeiten + +- Python 3.6+ +- tkinter (in der Standardinstallation von Python enthalten) +- pywin32 (für das Extrahieren von Programm-Icons) +- Pillow (für die Bildverarbeitung) + +## Hinweise + +- Icon-Unterstützung funktioniert nur unter Windows +- Ohne die optionalen Abhängigkeiten (pywin32 und Pillow) funktioniert das Programm weiterhin, jedoch ohne Icon-Anzeige + +## Info + +- Erstellt mit Hilfe von Claude \ No newline at end of file diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6223c159d4620c2757da2261c6e6ac35ab0bb430 GIT binary patch literal 67646 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 literal 0 HcmV?d00001 diff --git a/program_list.json b/program_list.json deleted file mode 100644 index 25e9369..0000000 --- a/program_list.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "Name": "Crunchyroll Downloader", - "Pfad": "C:\\Users\\Akamaru\\AppData\\Local\\Programs\\crunchyroll-downloader\\Crunchyroll Downloader.exe" - }, - { - "Name": "clrmamepro", - "Pfad": "D:\\Portable\\clrmamepro\\cmpro64.exe" - }, - { - "Name": "Programm 3", - "Pfad": "E:\\Pfad\\zum\\Programm3.exe" - } -] diff --git a/run.py b/run.py index 30f08ad..2b0543a 100644 --- a/run.py +++ b/run.py @@ -1,43 +1,981 @@ import tkinter as tk +from tkinter import ttk, filedialog, messagebox, simpledialog import json import subprocess +import os +import sys -def open_program(program_path): - print(f"Attempting to open: {program_path}") - subprocess.Popen(program_path) +# Versuche die Icon-Bibliotheken zu importieren +try: + import win32ui + import win32gui + import win32con + import win32api + from PIL import Image, ImageTk + ICON_SUPPORT = True +except ImportError: + # Wir können messagebox hier nicht verwenden, da es aus tkinter importiert wird + print("Warnung: Die Bibliotheken für Programm-Icons fehlen. Installieren Sie diese mit: pip install pywin32 Pillow") + ICON_SUPPORT = False -def load_programs(): - with open('program_list.json') as f: - data = json.load(f) - return data +def resource_path(relative_path): + """Get absolute path to resource, works for dev and for PyInstaller""" + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + return os.path.join(base_path, relative_path) -def program_clicked(program_path): - open_program(program_path) +def extract_icon_from_exe(exe_path): + """Extrahiert das Icon aus einer EXE-Datei""" + if not ICON_SUPPORT: + return None + + try: + # Icon-Handle erhalten + large, small = win32gui.ExtractIconEx(exe_path, 0) + + # Wir verwenden das größere Icon + if large: + # Wir nehmen das erste Icon + ico_x = win32api.GetSystemMetrics(win32con.SM_CXICON) + ico_y = win32api.GetSystemMetrics(win32con.SM_CYICON) + + # Icon in DC (Device Context) zeichnen + hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0)) + hbmp = win32ui.CreateBitmap() + hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_y) + hdc = hdc.CreateCompatibleDC() + + hdc.SelectObject(hbmp) + hdc.DrawIcon((0, 0), large[0]) + + # Bitmap in ein Python-Image-Objekt umwandeln + bmpinfo = hbmp.GetInfo() + bmpstr = hbmp.GetBitmapBits(True) + img = Image.frombuffer( + 'RGBA', + (bmpinfo['bmWidth'], bmpinfo['bmHeight']), + bmpstr, 'raw', 'BGRA', 0, 1 + ) + + # Aufräumen + win32gui.DestroyIcon(large[0]) + for icon in small: + if icon: + win32gui.DestroyIcon(icon) + + # Bild auf die gewünschte Größe skalieren + img = img.resize((16, 16), Image.LANCZOS) + + # In PhotoImage umwandeln für Tkinter + return ImageTk.PhotoImage(img) + return None + except Exception as e: + print(f"Fehler beim Extrahieren des Icons: {str(e)}") + return None -def populate_listbox(programs): - for program in programs: - program_name = program['Name'] - program_path = program['Pfad'] - listbox.insert(tk.END, program_name) - listbox.bind('<>', on_select) +class ProgramLauncher: + def __init__(self, root): + self.root = root + self.root.title("Programm-Shortcut") + self.root.geometry("700x550") + self.root.minsize(600, 450) + self.root.configure(bg="#f0f0f0") + + # Lade das Symbol, falls vorhanden + try: + self.root.iconbitmap(resource_path("icon.ico")) + except: + pass + + # Initialisiere Standardwerte + self.programs = [] + self.categories = ["Spiele", "Werkzeuge", "Office", "Multimedia", "Internet"] + self.options = self.get_default_options() + self.sort_column = None + self.sort_reverse = False + + # Icon-Cache initialisieren + self.icon_cache = {} + + # Laden der Daten aus der JSON-Datei + self.json_file = 'data.json' + self.load_data() + + self.create_widgets() + self.populate_program_list() + + # Wende gespeicherte Optionen an + self.apply_options() + + # Zentriere das Fenster + self.center_window() + + def center_window(self): + self.root.update_idletasks() + width = self.root.winfo_width() + height = self.root.winfo_height() + x = (self.root.winfo_screenwidth() // 2) - (width // 2) + y = (self.root.winfo_screenheight() // 2) - (height // 2) + self.root.geometry(f"{width}x{height}+{x}+{y}") + + def load_data(self): + """Lädt Programme und Kategorien aus der JSON-Datei""" + try: + # Prüfen, ob die alte Datei existiert und umbenannt werden muss + if not os.path.exists(self.json_file) and os.path.exists('program_list.json'): + # Wandle altes in neues Format um und speichere es + with open('program_list.json', encoding='utf-8') as f: + old_data = json.load(f) + + # Wenn es sich um das alte Format handelt (nur Liste von Programmen) + if isinstance(old_data, list): + self.data = { + "categories": self.categories, + "programs": old_data, + "options": self.get_default_options() + } + # Kategorien aus Programmen extrahieren und mit Standardkategorien zusammenführen + for program in old_data: + if "Kategorie" in program and program["Kategorie"] not in self.data["categories"]: + self.data["categories"].append(program["Kategorie"]) + else: + # Wenn es bereits das neue Format hat, füge nur Optionen hinzu + old_data["options"] = self.get_default_options() + self.data = old_data + + # Speichere in neuer Datei + with open(self.json_file, 'w', encoding='utf-8') as f: + json.dump(self.data, f, indent=2, ensure_ascii=False) + elif not os.path.exists(self.json_file): + # Erstelle eine neue Datei mit leeren Werten und Standardoptionen + self.data = { + "categories": self.categories, + "programs": [], + "options": self.get_default_options() + } + self.save_data() + else: + # Datei laden + with open(self.json_file, encoding='utf-8') as f: + data = json.load(f) + + # Überprüfe, ob es sich um das alte Format handelt (ohne options) + if "options" not in data: + data["options"] = self.get_default_options() + + # Verwende das neue Format + self.data = data + + # Aktualisiere die Werte für den einfacheren Zugriff + self.programs = self.data["programs"] + self.categories = self.data["categories"] + self.options = self.data.get("options", self.get_default_options()) + + except (FileNotFoundError, json.JSONDecodeError) as e: + messagebox.showwarning("Warnung", f"Fehler beim Laden der Daten: {str(e)}\nEine neue Liste wird erstellt.") + self.data = { + "categories": self.categories, + "programs": [], + "options": self.get_default_options() + } + # Speichere die neue Liste + self.save_data() + + # Stelle sicher, dass die Variablen immer richtig gesetzt sind + self.programs = self.data["programs"] + self.categories = self.data["categories"] + self.options = self.data["options"] + + def get_default_options(self): + """Gibt Standardoptionen zurück""" + return { + "default_sort_column": None, + "default_sort_reverse": False, + "remember_last_category": False, + "last_category": "Alle", + "window_width": 700, + "window_height": 550 + } + + def apply_options(self): + """Wendet gespeicherte Optionen an""" + # Wenn eine Standardsortierung gespeichert ist, anwenden + if self.options.get("default_sort_column"): + self.sort_column = self.options["default_sort_column"] + self.sort_reverse = self.options["default_sort_reverse"] + self.populate_program_list() + + # Wenn die letzte Kategorie gespeichert werden soll, auswählen + if self.options.get("remember_last_category") and self.options.get("last_category"): + # Finde die Kategorie in der Listbox + for i in range(self.category_listbox.size()): + if self.category_listbox.get(i) == self.options["last_category"]: + self.category_listbox.selection_clear(0, tk.END) + self.category_listbox.selection_set(i) + self.on_category_select(None) + break + + # Fenstergröße anwenden, falls gespeichert + if self.options.get("window_width") and self.options.get("window_height"): + self.root.geometry(f"{self.options['window_width']}x{self.options['window_height']}") + + def save_data(self): + """Speichert Programme und Kategorien in der JSON-Datei""" + try: + # Aktuelle Optionen aktualisieren + if hasattr(self, 'sort_column'): + self.options["default_sort_column"] = self.sort_column + self.options["default_sort_reverse"] = self.sort_reverse + + # Fenstergröße speichern + self.options["window_width"] = self.root.winfo_width() + self.options["window_height"] = self.root.winfo_height() + + # Aktuelle Kategorie speichern, wenn Option aktiviert ist + if self.options.get("remember_last_category"): + try: + index = self.category_listbox.curselection()[0] + self.options["last_category"] = self.category_listbox.get(index) + except (IndexError, AttributeError): + pass + + # Stelle sicher, dass die Optionen im Hauptdatenobjekt aktualisiert werden + self.data["options"] = self.options + + # In Datei speichern + with open(self.json_file, 'w', encoding='utf-8') as f: + json.dump(self.data, f, indent=2, ensure_ascii=False) + + # Debug: Dateiinhalt überprüfen + # print(f"Saved options: {self.options}") + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Speichern der Daten: {str(e)}") + + def create_widgets(self): + # Hauptframe + main_frame = ttk.Frame(self.root, padding="10") + main_frame.pack(fill=tk.BOTH, expand=True) + + # Stil konfigurieren + style = ttk.Style() + style.configure("TFrame", background="#f0f0f0") + style.configure("TButton", background="#4CAF50", foreground="black", padding=5) + style.configure("TLabel", background="#f0f0f0", foreground="#333333", font=("Segoe UI", 10)) + style.configure("Title.TLabel", background="#f0f0f0", foreground="#333333", font=("Segoe UI", 14, "bold")) + style.configure("Category.TLabel", background="#f0f0f0", foreground="#555555", font=("Segoe UI", 12, "bold")) + + # Titel + title_label = ttk.Label(main_frame, text="Programm-Shortcut", style="Title.TLabel") + title_label.pack(pady=10) + + # Erklärungstext + help_text = ttk.Label(main_frame, text="Wählen Sie ein Programm zum Starten oder fügen Sie ein neues hinzu") + help_text.pack(pady=5) + + # Horizontaler Frame für Kategorieauswahl und Programmliste + h_frame = ttk.Frame(main_frame) + h_frame.pack(fill=tk.BOTH, expand=True, pady=10) + + # Linker Frame für Kategorien (vertikal ausgerichtet) + category_frame = ttk.Frame(h_frame, padding="5") + category_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) + + # Container für Kategorien-Bereich als vertikalen Stack + category_content = ttk.Frame(category_frame) + category_content.pack(fill=tk.BOTH, expand=True) + + # Kategorie-Label + cat_label = ttk.Label(category_content, text="Kategorien", style="Category.TLabel") + cat_label.pack(pady=(0, 5), anchor="w") + + # Container für Listbox und Scrollbar + list_container = ttk.Frame(category_content) + list_container.pack(fill=tk.BOTH, expand=True) + + # Scrollbar für Kategorieliste + cat_scrollbar = ttk.Scrollbar(list_container) + cat_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Listbox für Kategorien + self.category_listbox = tk.Listbox(list_container, height=15, width=15, + yscrollcommand=cat_scrollbar.set, + font=("Segoe UI", 10), + selectbackground="#4CAF50", + activestyle="none", + exportselection=0) + self.category_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + cat_scrollbar.config(command=self.category_listbox.yview) + + # Füllen der Kategorie-Listbox + for category in sorted(self.categories): + self.category_listbox.insert(tk.END, category) + + # Standardmäßig "Alle" auswählen + self.category_listbox.insert(0, "Alle") + self.category_listbox.selection_set(0) + self.category_listbox.bind("<>", self.on_category_select) + + # Kategorie-Buttons-Container erstellen (unterhalb der Listbox) + button_container = ttk.Frame(category_content) + button_container.pack(fill=tk.X, pady=10) + + # Container für die Buttons, damit sie zentriert werden können + buttons_center = ttk.Frame(button_container) + buttons_center.pack(anchor=tk.CENTER) + + # Die Buttons nebeneinander anordnen + self.add_cat_button = ttk.Button(buttons_center, text="+", width=3, command=self.add_category) + self.add_cat_button.pack(side=tk.LEFT, padx=5) + + self.remove_cat_button = ttk.Button(buttons_center, text="-", width=3, command=self.remove_category) + self.remove_cat_button.pack(side=tk.LEFT, padx=5) + + # Rechter Frame für Programme + program_frame = ttk.Frame(h_frame) + program_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # Frame für Programmliste und Scrollen + list_frame = ttk.Frame(program_frame) + list_frame.pack(fill=tk.BOTH, expand=True) + + # Scrollbar für die Treeview + scrollbar = ttk.Scrollbar(list_frame) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Anzeige von Icons aktivieren und Platz dafür schaffen + style.configure("Treeview", rowheight=24) # Mehr Platz für Icons + + # Statt ein Zellenpadding zu verwenden, verwenden wir einen zusätzlichen Abstand im Text + style.configure("Treeview.Cell", padding=(0, 0)) + + # Treeview für Programme + # Wenn Icon-Unterstützung aktiviert ist, verwenden wir ein anderes Format + if ICON_SUPPORT: + self.program_tree = ttk.Treeview(list_frame, yscrollcommand=scrollbar.set, + columns=("name", "category"), + show="tree headings") + else: + self.program_tree = ttk.Treeview(list_frame, yscrollcommand=scrollbar.set, + columns=("name", "category"), + show="headings") + + # Setze Spaltenüberschriften mit Sortierfunktion + self.program_tree.heading("name", text="Programm", command=lambda: self.sort_treeview("name", False)) + self.program_tree.heading("category", text="Kategorie", command=lambda: self.sort_treeview("category", False)) + + # Konfiguriere Spaltenbreiten und Ausrichtung + self.program_tree.column("name", width=220, anchor="w", minwidth=150) + self.program_tree.column("category", width=100, anchor="w") + self.program_tree.column("#0", width=30, stretch=False, anchor="w") # Icon-Spalte + + self.program_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + scrollbar.config(command=self.program_tree.yview) + + # Frame für Buttons + button_frame = ttk.Frame(main_frame) + button_frame.pack(fill=tk.X, pady=10) + + # Buttons + self.start_button = ttk.Button(button_frame, text="Starten", command=self.start_program) + self.start_button.pack(side=tk.LEFT, padx=5) + + self.add_button = ttk.Button(button_frame, text="Hinzufügen", command=self.add_program) + self.add_button.pack(side=tk.LEFT, padx=5) + + self.remove_button = ttk.Button(button_frame, text="Entfernen", command=self.remove_program) + self.remove_button.pack(side=tk.LEFT, padx=5) + + self.edit_button = ttk.Button(button_frame, text="Bearbeiten", command=self.edit_program) + self.edit_button.pack(side=tk.LEFT, padx=5) + + self.options_button = ttk.Button(button_frame, text="Optionen", command=self.show_options_dialog) + self.options_button.pack(side=tk.RIGHT, padx=5) + + # Doppelklick auf einen Eintrag + self.program_tree.bind("", lambda event: self.start_program()) + + # Sortierungszustand + self.sort_column = self.options.get("default_sort_column") + self.sort_reverse = self.options.get("default_sort_reverse", False) + + def show_options_dialog(self): + """Zeigt einen Dialog mit Programmoptionen an""" + options_window = tk.Toplevel(self.root) + options_window.title("Programmoptionen") + options_window.geometry("450x350") + options_window.resizable(False, False) + options_window.grab_set() # Modal machen + + # Zentriere das Dialogfenster + options_window.update_idletasks() + width = options_window.winfo_width() + height = options_window.winfo_height() + x = (options_window.winfo_screenwidth() // 2) - (width // 2) + y = (options_window.winfo_screenheight() // 2) - (height // 2) + options_window.geometry(f"{width}x{height}+{x}+{y}") + + # Frame + frame = ttk.Frame(options_window, padding="20") + frame.pack(fill=tk.BOTH, expand=True) + + # Überschrift + ttk.Label(frame, text="Programmoptionen", font=("Segoe UI", 12, "bold")).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 15)) + + # Option für Standardsortierung + ttk.Label(frame, text="Standardsortierung:").grid(row=1, column=0, sticky="w", pady=5) + sort_var = tk.StringVar(value=self.get_sort_option_text()) + sort_combobox = ttk.Combobox(frame, textvariable=sort_var, values=[ + "Keine", + "Programm (A-Z)", + "Programm (Z-A)", + "Kategorie (A-Z)", + "Kategorie (Z-A)" + ]) + sort_combobox.grid(row=1, column=1, sticky="we", pady=5) + sort_combobox.configure(state="readonly") + + # Option zum Speichern der letzten Kategorie + remember_category_var = tk.BooleanVar(value=self.options.get("remember_last_category", False)) + remember_category_check = ttk.Checkbutton( + frame, + text="Letzte ausgewählte Kategorie merken", + variable=remember_category_var + ) + remember_category_check.grid(row=2, column=0, columnspan=2, sticky="w", pady=5) + + # Trennlinie + separator = ttk.Separator(frame, orient="horizontal") + separator.grid(row=3, column=0, columnspan=2, sticky="ew", pady=10) + + # Info-Bereich + info_frame = ttk.Frame(frame) + info_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=5) + + # Version + version_label = ttk.Label(info_frame, text="Version: 1.0", font=("Segoe UI", 9)) + version_label.pack(anchor="w") + + # Copyright & Autor Info + author_label = ttk.Label(info_frame, text="© 2025 Akamaru", font=("Segoe UI", 9)) + author_label.pack(anchor="w") + + created_label = ttk.Label(info_frame, text="Erstellt mit Hilfe von Claude", font=("Segoe UI", 9)) + created_label.pack(anchor="w") + + # Link zum Quellcode mit Unterstreichung + link_style = ttk.Style() + link_style.configure("Link.TLabel", foreground="blue", font=("Segoe UI", 9, "underline")) + + link_label = ttk.Label(info_frame, text="Quellcode auf git.ponywave.de verfügbar", + style="Link.TLabel", cursor="hand2") + link_label.pack(anchor="w", pady=(5, 0)) + link_label.bind("", lambda e: self.open_url("https://git.ponywave.de/Akamaru/Programm-Shortcut")) + + # Buttons + button_frame = ttk.Frame(frame) + button_frame.grid(row=5, column=0, columnspan=2, pady=20) + + save_button = ttk.Button( + button_frame, + text="Speichern", + command=lambda: self.save_options( + sort_combobox.get(), + remember_category_var.get(), + options_window + ) + ) + save_button.pack(side=tk.LEFT, padx=5) + + cancel_button = ttk.Button(button_frame, text="Abbrechen", command=options_window.destroy) + cancel_button.pack(side=tk.LEFT, padx=5) + + def get_sort_option_text(self): + """Gibt den Text für die aktuelle Sortierungsoption zurück""" + if self.sort_column == "name": + if self.sort_reverse: + return "Programm (Z-A)" + else: + return "Programm (A-Z)" + elif self.sort_column == "category": + if self.sort_reverse: + return "Kategorie (Z-A)" + else: + return "Kategorie (A-Z)" + else: + return "Keine" + + def save_options(self, sort_option, remember_category, window): + """Speichert die Programmoptionen""" + # Sortieroptionen übersetzen + if sort_option == "Programm (A-Z)": + self.options["default_sort_column"] = "name" + self.options["default_sort_reverse"] = False + self.sort_column = "name" + self.sort_reverse = False + elif sort_option == "Programm (Z-A)": + self.options["default_sort_column"] = "name" + self.options["default_sort_reverse"] = True + self.sort_column = "name" + self.sort_reverse = True + elif sort_option == "Kategorie (A-Z)": + self.options["default_sort_column"] = "category" + self.options["default_sort_reverse"] = False + self.sort_column = "category" + self.sort_reverse = False + elif sort_option == "Kategorie (Z-A)": + self.options["default_sort_column"] = "category" + self.options["default_sort_reverse"] = True + self.sort_column = "category" + self.sort_reverse = True + else: # "Keine" + self.options["default_sort_column"] = None + self.options["default_sort_reverse"] = False + self.sort_column = None + self.sort_reverse = False + + # Kategorie-Speicher-Option setzen + self.options["remember_last_category"] = remember_category + + # Aktuelle Kategorie speichern, wenn Option aktiviert ist + if remember_category: + try: + index = self.category_listbox.curselection()[0] + self.options["last_category"] = self.category_listbox.get(index) + except (IndexError, AttributeError): + pass + + # Stelle sicher, dass die Optionen im Hauptdatenobjekt aktualisiert werden + self.data["options"] = self.options + + # Änderungen speichern + self.save_data() + + # Liste neu sortieren + self.populate_program_list() + + # Fenster schließen + window.destroy() + + def open_url(self, url): + """Öffnet eine URL im Standard-Browser""" + try: + import webbrowser + webbrowser.open(url) + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Öffnen der URL: {str(e)}") + + def on_category_select(self, event): + # Aktualisiere die Programmliste basierend auf der ausgewählten Kategorie + self.populate_program_list() + + def add_category(self): + """Fügt eine neue Kategorie hinzu""" + # Dialog für neue Kategorie + category_name = simpledialog.askstring("Neue Kategorie", "Geben Sie den Namen der neuen Kategorie ein:") + if not category_name: + return + + # Prüfe, ob die Kategorie bereits existiert + if category_name in self.categories: + messagebox.showwarning("Warnung", f"Die Kategorie '{category_name}' existiert bereits.") + return + + # Füge die Kategorie zur Liste hinzu + self.categories.append(category_name) + + # Aktualisiere die Kategorie-Listbox + self.category_listbox.delete(1, tk.END) # Lösche alle außer "Alle" + for category in sorted(self.categories): + self.category_listbox.insert(tk.END, category) + + # Speichere die aktualisierte Kategorienliste + self.save_data() + + def remove_category(self): + """Entfernt eine Kategorie""" + # Hole die ausgewählte Kategorie + try: + index = self.category_listbox.curselection()[0] + category = self.category_listbox.get(index) + except IndexError: + messagebox.showinfo("Information", "Bitte wählen Sie eine Kategorie zum Entfernen aus.") + return + + # "Alle" kann nicht entfernt werden + if category == "Alle": + messagebox.showinfo("Information", "Die Kategorie 'Alle' kann nicht entfernt werden.") + return + + # Bestätigung einholen + confirm = messagebox.askyesno("Bestätigung", + f"Möchten Sie die Kategorie '{category}' wirklich entfernen?\n\n" + "Alle Programme in dieser Kategorie werden ohne Kategorie gespeichert.") + if not confirm: + return + + # Programme ohne Kategorie speichern + for program in self.programs: + if program.get("Kategorie") == category: + if "Kategorie" in program: + del program["Kategorie"] + + # Entferne die Kategorie aus der Liste + self.categories.remove(category) + + # Aktualisiere die Kategorie-Listbox + self.category_listbox.delete(1, tk.END) # Lösche alle außer "Alle" + for category in sorted(self.categories): + self.category_listbox.insert(tk.END, category) + + # Wähle "Alle" aus + self.category_listbox.selection_set(0) + + # Speichere die Änderungen + self.save_data() + self.populate_program_list() + + def populate_program_list(self): + # Lösche alle bestehenden Einträge + for item in self.program_tree.get_children(): + self.program_tree.delete(item) + + # Hole die ausgewählte Kategorie + try: + index = self.category_listbox.curselection()[0] + selected_category = self.category_listbox.get(index) + except IndexError: + selected_category = "Alle" + + # Sortiere Programme nach Kategorie und Name + def get_category(program): + return program.get("Kategorie", "").lower() if "Kategorie" in program else "" + + # Nach dem aktuellen Sortierzustand sortieren + if self.sort_column == "name": + # Nach Programmname sortieren (Groß-/Kleinschreibung ignorieren) + sorted_programs = sorted(self.programs, key=lambda x: x["Name"].lower(), reverse=self.sort_reverse) + elif self.sort_column == "category": + # Nach Kategorie sortieren (Groß-/Kleinschreibung ignorieren) + sorted_programs = sorted(self.programs, key=lambda x: get_category(x), reverse=self.sort_reverse) + else: + # Standardsortierung: Kategorie dann Name (Groß-/Kleinschreibung ignorieren) + sorted_programs = sorted(self.programs, key=lambda x: (get_category(x), x["Name"].lower())) + + # Füge Programme hinzu + for program in sorted_programs: + category = program.get("Kategorie", "") if "Kategorie" in program else "" + + # Filtere nach Kategorie, wenn nicht "Alle" ausgewählt ist + if selected_category != "Alle" and category != selected_category: + continue + + program_path = program["Pfad"] + program_name = " " + program["Name"] # Fester Abstand mit 3 Leerzeichen + + # Hole das Icon nur, wenn Icon-Unterstützung aktiviert ist + icon = None + if ICON_SUPPORT: + # Hole das Icon, wenn es noch nicht im Cache ist + if program_path in self.icon_cache: + icon = self.icon_cache[program_path] + else: + # Versuche, das Icon zu extrahieren + icon = extract_icon_from_exe(program_path) + # Speichere im Cache für zukünftige Verwendung + self.icon_cache[program_path] = icon + + # Füge Programm hinzu + if ICON_SUPPORT and icon: + # Mit Icon in der #0 Spalte + item_id = self.program_tree.insert("", tk.END, text="", values=(program_name, category), + tags=(program_path,), image=icon) + else: + # Ohne Icon, normaler Eintrag + item_id = self.program_tree.insert("", tk.END, text="", values=(program_name, category), + tags=(program_path,)) + + def start_program(self): + selected_items = self.program_tree.selection() + if not selected_items: + messagebox.showinfo("Information", "Bitte wählen Sie ein Programm aus.") + return + + item_id = selected_items[0] + program_path = self.program_tree.item(item_id, "tags")[0] + + try: + subprocess.Popen(program_path) + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Starten des Programms: {str(e)}") + + def add_program(self): + add_window = tk.Toplevel(self.root) + add_window.title("Programm hinzufügen") + add_window.geometry("450x250") + add_window.resizable(False, False) + add_window.grab_set() # Modal machen + + # Zentriere das Dialogfenster + add_window.update_idletasks() + width = add_window.winfo_width() + height = add_window.winfo_height() + x = (add_window.winfo_screenwidth() // 2) - (width // 2) + y = (add_window.winfo_screenheight() // 2) - (height // 2) + add_window.geometry(f"{width}x{height}+{x}+{y}") + + # Frame + frame = ttk.Frame(add_window, padding="20") + frame.pack(fill=tk.BOTH, expand=True) + + # Eingabefelder + ttk.Label(frame, text="Programmname:").grid(row=0, column=0, sticky="w", pady=5) + name_entry = ttk.Entry(frame, width=30) + name_entry.grid(row=0, column=1, columnspan=2, sticky="we", pady=5) + + ttk.Label(frame, text="Programmpfad:").grid(row=1, column=0, sticky="w", pady=5) + path_entry = ttk.Entry(frame, width=30) + path_entry.grid(row=1, column=1, sticky="we", pady=5) + + browse_button = ttk.Button(frame, text="Durchsuchen", + command=lambda: self.browse_file_and_clear(path_entry)) + browse_button.grid(row=1, column=2, padx=5, pady=5) + + ttk.Label(frame, text="Kategorie:").grid(row=2, column=0, sticky="w", pady=5) + + # Combobox für Kategorien + category_combobox = ttk.Combobox(frame, width=28, values=sorted(self.categories)) + category_combobox.grid(row=2, column=1, columnspan=2, sticky="we", pady=5) + if self.categories: + category_combobox.current(0) # Erste Kategorie auswählen, falls vorhanden + + # Buttons + button_frame = ttk.Frame(frame) + button_frame.grid(row=3, column=0, columnspan=3, pady=15) + + save_button = ttk.Button(button_frame, text="Speichern", + command=lambda: self.save_new_program( + name_entry.get(), + path_entry.get(), + category_combobox.get(), + add_window)) + save_button.pack(side=tk.LEFT, padx=5) + + cancel_button = ttk.Button(button_frame, text="Abbrechen", command=add_window.destroy) + cancel_button.pack(side=tk.LEFT, padx=5) + + # Focus auf erstes Eingabefeld + name_entry.focus_set() + + def browse_file_and_clear(self, entry_widget): + """Durchsucht nach einer Datei und löscht vorher den Inhalt des Eingabefelds""" + entry_widget.delete(0, tk.END) + filename = filedialog.askopenfilename(filetypes=[("Ausführbare Dateien", "*.exe"), ("Alle Dateien", "*.*")]) + if filename: + entry_widget.insert(0, filename) + + def save_new_program(self, name, path, category, window): + if not name or not path: + messagebox.showwarning("Warnung", "Bitte füllen Sie alle Felder aus.") + return + + if not os.path.exists(path): + messagebox.showwarning("Warnung", "Der angegebene Pfad existiert nicht.") + return + + # Neues Programm hinzufügen (nur mit Kategorie, wenn eine ausgewählt wurde) + new_program = {"Name": name, "Pfad": path} + if category: + new_program["Kategorie"] = category + + # Füge Kategorie hinzu, falls sie noch nicht existiert + if category not in self.categories: + self.categories.append(category) + + # Aktualisiere die Kategorie-Listbox + self.category_listbox.delete(1, tk.END) # Lösche alle außer "Alle" + for cat in sorted(self.categories): + self.category_listbox.insert(tk.END, cat) + + self.programs.append(new_program) + self.save_data() + self.populate_program_list() + window.destroy() + + def remove_program(self): + selected_items = self.program_tree.selection() + if not selected_items: + messagebox.showinfo("Information", "Bitte wählen Sie ein Programm zum Entfernen aus.") + return + + item_id = selected_items[0] + program_values = self.program_tree.item(item_id, "values") + program_name = program_values[0].strip() # Leerzeichen am Anfang entfernen + + # Bestätigung + confirm = messagebox.askyesno("Bestätigung", f"Möchten Sie '{program_name}' wirklich entfernen?") + if not confirm: + return + + # Finde den Index des zu entfernenden Programms + found = False + for i, program in enumerate(self.programs): + if program["Name"] == program_name: + del self.programs[i] + found = True + break + + # Wenn kein exakter Treffer gefunden wurde, versuche es ohne Leerzeichen + if not found: + for i, program in enumerate(self.programs): + if program["Name"].strip() == program_name: + del self.programs[i] + break + + self.save_data() + self.populate_program_list() + + def edit_program(self): + selected_items = self.program_tree.selection() + if not selected_items: + messagebox.showinfo("Information", "Bitte wählen Sie ein Programm zum Bearbeiten aus.") + return + + item_id = selected_items[0] + program_values = self.program_tree.item(item_id, "values") + program_name = program_values[0].strip() # Leerzeichen am Anfang entfernen + program_category = program_values[1] if len(program_values) > 1 else "" + program_path = self.program_tree.item(item_id, "tags")[0] + + # Finde den Index des zu bearbeitenden Programms + program_index = None + for i, program in enumerate(self.programs): + if program["Name"] == program_name.strip(): # Vergleich ohne führende Leerzeichen + program_index = i + break + + if program_index is None: + # Wenn kein exakter Treffer gefunden wird, versuche ohne Leerzeichen zu vergleichen + for i, program in enumerate(self.programs): + if program["Name"].strip() == program_name.strip(): # Beide Namen ohne Leerzeichen vergleichen + program_index = i + break + + if program_index is None: + messagebox.showinfo("Information", f"Programm '{program_name}' konnte nicht gefunden werden.") + return + + # Bearbeitungsfenster + edit_window = tk.Toplevel(self.root) + edit_window.title("Programm bearbeiten") + edit_window.geometry("450x250") + edit_window.resizable(False, False) + edit_window.grab_set() # Modal machen + + # Zentriere das Dialogfenster + edit_window.update_idletasks() + width = edit_window.winfo_width() + height = edit_window.winfo_height() + x = (edit_window.winfo_screenwidth() // 2) - (width // 2) + y = (edit_window.winfo_screenheight() // 2) - (height // 2) + edit_window.geometry(f"{width}x{height}+{x}+{y}") + + # Frame + frame = ttk.Frame(edit_window, padding="20") + frame.pack(fill=tk.BOTH, expand=True) + + # Eingabefelder + ttk.Label(frame, text="Programmname:").grid(row=0, column=0, sticky="w", pady=5) + name_entry = ttk.Entry(frame, width=30) + name_entry.insert(0, program_name.strip()) # Original-Programmnamen ohne Leerzeichen einfügen + name_entry.grid(row=0, column=1, columnspan=2, sticky="we", pady=5) + + ttk.Label(frame, text="Programmpfad:").grid(row=1, column=0, sticky="w", pady=5) + path_entry = ttk.Entry(frame, width=30) + path_entry.insert(0, program_path) + path_entry.grid(row=1, column=1, sticky="we", pady=5) + + browse_button = ttk.Button(frame, text="Durchsuchen", + command=lambda: self.browse_file(path_entry)) + browse_button.grid(row=1, column=2, padx=5, pady=5) + + ttk.Label(frame, text="Kategorie:").grid(row=2, column=0, sticky="w", pady=5) + + # Combobox für Kategorien + category_combobox = ttk.Combobox(frame, width=28, values=sorted(self.categories)) + category_combobox.grid(row=2, column=1, columnspan=2, sticky="we", pady=5) + if program_category: + category_combobox.set(program_category) + elif self.categories: + category_combobox.current(0) # Erste Kategorie auswählen, falls vorhanden + + # Buttons + button_frame = ttk.Frame(frame) + button_frame.grid(row=3, column=0, columnspan=3, pady=15) + + save_button = ttk.Button(button_frame, text="Speichern", + command=lambda: self.update_program( + program_index, + name_entry.get(), + path_entry.get(), + category_combobox.get(), + edit_window)) + save_button.pack(side=tk.LEFT, padx=5) + + cancel_button = ttk.Button(button_frame, text="Abbrechen", command=edit_window.destroy) + cancel_button.pack(side=tk.LEFT, padx=5) + + def browse_file(self, entry_widget): + filename = filedialog.askopenfilename(filetypes=[("Ausführbare Dateien", "*.exe"), ("Alle Dateien", "*.*")]) + if filename: + entry_widget.delete(0, tk.END) + entry_widget.insert(0, filename) + + def update_program(self, index, name, path, category, window): + if not name or not path: + messagebox.showwarning("Warnung", "Bitte füllen Sie alle Felder aus.") + return + + if not os.path.exists(path): + messagebox.showwarning("Warnung", "Der angegebene Pfad existiert nicht.") + return + + # Programm aktualisieren (nur mit Kategorie, wenn eine ausgewählt wurde) + updated_program = {"Name": name, "Pfad": path} + if category: + updated_program["Kategorie"] = category + + # Füge Kategorie hinzu, falls sie noch nicht existiert + if category not in self.categories: + self.categories.append(category) + + # Aktualisiere die Kategorie-Listbox + self.category_listbox.delete(1, tk.END) # Lösche alle außer "Alle" + for cat in sorted(self.categories): + self.category_listbox.insert(tk.END, cat) + + self.programs[index] = updated_program + self.save_data() + self.populate_program_list() + window.destroy() + + def sort_treeview(self, column, reverse): + """Sortiert die Treeview nach der angegebenen Spalte""" + if self.sort_column == column: + # Wenn dieselbe Spalte erneut geklickt wird, umkehren + reverse = not self.sort_reverse + + # Speichern des aktuellen Sortierzustands + self.sort_column = column + self.sort_reverse = reverse + + # Aktualisiere die Programmliste mit dem neuen Sortierzustand + self.populate_program_list() -def on_select(event): - index = listbox.curselection()[0] - selected_program = programs[index] - program_clicked(selected_program['Pfad']) - -# Erstelle das Hauptfenster -root = tk.Tk() - -# Erstelle eine Listbox -listbox = tk.Listbox(root) -listbox.pack() - -# Lade die Programme aus der JSON-Datei -programs = load_programs() - -# Fülle die Listbox mit den Programmen -populate_listbox(programs) - -# Starte die GUI-Schleife +if __name__ == "__main__": + root = tk.Tk() + app = ProgramLauncher(root) root.mainloop() \ No newline at end of file diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d47f2eaad2135c38b65b3359fd030538ea8554de GIT binary patch literal 32834 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 F`5zUs=qUgI literal 0 HcmV?d00001 diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..9daf64a --- /dev/null +++ b/start.bat @@ -0,0 +1,2 @@ +@echo off +start "" /b pythonw.exe run.py \ No newline at end of file