import tkinter as tk from tkinter import ttk, filedialog, messagebox, simpledialog import json import subprocess import os import sys # 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 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 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 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.1", 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_values = self.program_tree.item(item_id, "values") program_name = program_values[0].strip() # Leerzeichen am Anfang entfernen program_path = self.program_tree.item(item_id, "tags")[0] # Prüfen, ob das Programm Adminrechte benötigt requires_admin = False for program in self.programs: if program["Name"].strip() == program_name.strip(): requires_admin = program.get("Adminrechte", False) break try: if sys.platform == "win32" and requires_admin: try: # Direkter Aufruf über ShellExecute API mit erhöhten Rechten import ctypes # Der Parameter "runas" fordert erhöhte Rechte an result = ctypes.windll.shell32.ShellExecuteW(None, "runas", program_path, None, None, 1) # Wenn der Rückgabewert <= 32 ist, gab es einen Fehler if result <= 32: # Fallback: Starte normal ohne erhöhte Rechte subprocess.Popen(program_path) except Exception: # Wenn es fehlschlägt, starte normal subprocess.Popen(program_path) else: # Normaler Start ohne Adminrechte 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("450x280") # Etwas mehr Platz für die Checkbox 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 # Checkbox für Adminrechte admin_var = tk.BooleanVar(value=False) admin_check = ttk.Checkbutton(frame, text="Benötigt Administratorrechte", variable=admin_var) admin_check.grid(row=3, column=0, columnspan=3, sticky="w", pady=5) # Buttons button_frame = ttk.Frame(frame) button_frame.grid(row=4, 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(), admin_var.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, requires_admin, 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) # Adminrechte-Flag hinzufügen if requires_admin: new_program["Adminrechte"] = True 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("450x280") # Etwas mehr Platz für die Checkbox 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 # Checkbox für Adminrechte admin_var = tk.BooleanVar(value=self.programs[program_index].get("Adminrechte", False)) admin_check = ttk.Checkbutton(frame, text="Benötigt Administratorrechte", variable=admin_var) admin_check.grid(row=3, column=0, columnspan=3, sticky="w", pady=5) # Buttons button_frame = ttk.Frame(frame) button_frame.grid(row=4, 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(), admin_var.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, requires_admin, 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) # Adminrechte-Flag hinzufügen if requires_admin: updated_program["Adminrechte"] = True 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() if __name__ == "__main__": root = tk.Tk() app = ProgramLauncher(root) root.mainloop()