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()