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 0000000..6223c15 Binary files /dev/null and b/icon.ico differ 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 0000000..d47f2ea Binary files /dev/null and b/screenshot.png differ 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