1025 lines
44 KiB
Python
1025 lines
44 KiB
Python
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("<<ListboxSelect>>", 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("<Double-1>", 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("<Button-1>", 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() |