1
0

Kompletter Rewrite

This commit is contained in:
Akamaru
2025-09-08 17:19:22 +02:00
parent edf0012917
commit 7e377363c6
18 changed files with 1688 additions and 1052 deletions

45
.gitignore vendored
View File

@@ -1,3 +1,48 @@
# User data
data.json
program_list.json
# Archive files
Programm-Shortcut.7z
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
.venv/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
*.tmp
*.log
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

119
README.md
View File

@@ -1,17 +1,18 @@
# Programm-Shortcut
# Programm-Shortcut v2.0
Ein Programm-Launcher für Windows mit Kategorieverwaltung und Programm-Icons.
Ein moderner Programm-Launcher für Windows mit Kategorieverwaltung, Programm-Icons und verbesserter Benutzeroberfläche.
![Screenshot des Programm-Shortcut](screenshot.png)
## Features
- Organisiere deine Programme in benutzerdefinierten Kategorien
- Programme werden mit ihren Original-Icons angezeigt
- Sortiere Programme nach Name oder Kategorie
- Einfaches Hinzufügen, Bearbeiten und Entfernen von Programmen
- Einstellungen werden automatisch gespeichert
- Kompaktes und benutzerfreundliches Interface
- **Organisiere Programme** in benutzerdefinierten Kategorien
- **Original-Icons** werden automatisch aus den Programmen extrahiert
- **Flexible Sortierung** nach Name oder Kategorie
- **Intuitive Bedienung** mit modernem Interface
- **Automatische Speicherung** aller Einstellungen
- **Administratorrechte-Unterstützung** für Programme, die diese benötigen
- **Konsistente Icons** in allen Fenstern und Dialogen
## Installation
@@ -19,7 +20,7 @@ Ein Programm-Launcher für Windows mit Kategorieverwaltung und Programm-Icons.
2. Klone das Repository oder lade es herunter
3. Installiere die erforderlichen Abhängigkeiten:
```
```bash
pip install pywin32 Pillow
```
@@ -27,37 +28,103 @@ pip install pywin32 Pillow
Starte die Anwendung mit:
```bash
python main.py
```
python run.py
```
oder mit der start.bat
oder verwende die mitgelieferte `start.bat` Datei.
### Programme hinzufügen:
1. Klicke auf "Hinzufügen"
2. Gebe den Programmnamen ein
1. Klicke im Menü auf "Programm" → "Hinzufügen..." oder drücke `Ctrl+N`
2. Gib den Programmnamen ein
3. Wähle den Programmpfad über "Durchsuchen"
4. Wähle eine Kategorie oder erstelle eine neue
5. Klicke auf "Speichern"
5. Aktiviere "Benötigt Administratorrechte" falls erforderlich
6. Klicke auf "Speichern"
### Programme starten:
1. Wähle eine Kategorie aus der linken Seitenleiste
1. Wähle eine Kategorie aus dem Menü "Kategorie"
2. Wähle ein Programm aus der Liste
3. Klicke auf "Starten"
3. Doppelklicke oder drücke `Enter` zum Starten
## Abhängigkeiten
### Tastaturkürzel:
- Python 3.6+
- tkinter (in der Standardinstallation von Python enthalten)
- pywin32 (für das Extrahieren von Programm-Icons)
- Pillow (für die Bildverarbeitung)
- `Ctrl+N`: Neues Programm hinzufügen
- `F2`: Ausgewähltes Programm bearbeiten
- `Del`: Ausgewähltes Programm entfernen
- `Enter`: Ausgewähltes Programm starten
## Architektur v2.0
Das Programm wurde komplett in eine modulare, wartbare Architektur überarbeitet:
```
├── main.py # Haupteinstiegspunkt
├── config.py # Zentrale Konfiguration und Themes
├── models/ # Datenmodelle
│ ├── program.py # Program-Klasse mit Admin-Unterstützung
│ └── category.py # CategoryManager
├── data/ # Datenverarbeitung
│ └── data_manager.py # JSON-Operationen und Migration
├── utils/ # Hilfsfunktionen
│ └── icon_utils.py # Icon-Extraktion und -Verwaltung
└── gui/ # GUI-Komponenten
├── main_window.py # Hauptfenster mit moderner UI
├── dialogs.py # Alle Dialog-Komponenten
└── modern_widgets.py # Moderne UI-Komponenten
```
## Was ist neu in v2.0
### 🎨 Verbessertes Design
- **Moderne Themes**: Drei verschiedene Farbschemas (Modern Dark, Modern Light, Gradient Blue)
- **Konsistente Icons**: Alle Fenster und Dialoge verwenden das Anwendungsicon
- **Verbesserte Typography**: Professionelle Schriftarten und Größen
- **Responsive Layout**: Bessere Anpassung an verschiedene Fenstergrößen
### 🔧 Technische Verbesserungen
- **Eigene Dialog-Klassen**: Alle System-Dialoge wurden durch eigene Implementierungen ersetzt
- **Bessere Icon-Behandlung**: Optimierte Icon-Extraktion und -Caching
- **Robuste Pfad-Auflösung**: Verbesserte Kompatibilität mit WSL und verschiedenen Umgebungen
- **Admin-Rechte-Unterstützung**: Programme können als Administrator gestartet werden
### 🏗️ Architektur-Verbesserungen
- **Separation of Concerns**: Klare Trennung von GUI, Daten und Geschäftslogik
- **Modulare Komponenten**: Wiederverwendbare UI-Elemente
- **Erweiterte Konfiguration**: Zentrale Theme- und Einstellungsverwaltung
- **Bessere Testbarkeit**: Kleinere, fokussierte Module
### Vorteile der neuen Architektur:
- **Wartbarkeit**: Übersichtliche Codestruktur mit klaren Verantwortlichkeiten
- **Erweiterbarkeit**: Einfache Integration neuer Features
- **Stabilität**: Robuste Fehlerbehandlung und Validierung
- **Performance**: Optimierte Icon-Verwaltung und UI-Updates
## Migration von v1.x
Die Migration erfolgt automatisch beim ersten Start von v2.0:
- Bestehende `data.json` Dateien werden automatisch geladen
- Alte `program_list.json` Dateien werden migriert
- Alle Benutzereinstellungen bleiben erhalten
- Keine manuelle Konfiguration erforderlich
## Systemanforderungen
- **Python**: 3.6 oder höher
- **Betriebssystem**: Windows (Icon-Unterstützung), Linux/WSL (grundlegende Funktionalität)
- **Abhängigkeiten**:
- `tkinter` (standardmäßig in Python enthalten)
- `pywin32` (für Icon-Extraktion unter Windows)
- `Pillow` (für Bildverarbeitung)
## Hinweise
- Icon-Unterstützung funktioniert nur unter Windows
- Ohne die optionalen Abhängigkeiten (pywin32 und Pillow) funktioniert das Programm weiterhin, jedoch ohne Icon-Anzeige
- **Icon-Unterstützung**: Funktioniert vollständig nur unter Windows
- **WSL-Kompatibilität**: Grundlegende Funktionalität verfügbar
- **Fallback-Modus**: Ohne optionale Abhängigkeiten läuft das Programm ohne Icons
## Info
## Entwicklung
- Erstellt mit Hilfe von Claude
Erstellt mit Hilfe von Claude AI und kontinuierlich weiterentwickelt.

165
config.py Normal file
View File

@@ -0,0 +1,165 @@
from typing import Dict, Any
class Config:
# Application settings
APP_NAME = "Programm-Shortcut"
APP_VERSION = "2.0"
APP_AUTHOR = "© 2025 Akamaru"
APP_DESCRIPTION = "Erstellt mit Hilfe von Claude"
SOURCE_URL = "https://git.ponywave.de/Akamaru/Programm-Shortcut"
# Window settings
DEFAULT_WINDOW_WIDTH = 700
DEFAULT_WINDOW_HEIGHT = 550
MIN_WINDOW_WIDTH = 600
MIN_WINDOW_HEIGHT = 450
# GUI settings
TREEVIEW_ROW_HEIGHT = 24
CATEGORY_LIST_HEIGHT = 15
CATEGORY_LIST_WIDTH = 15
PROGRAM_NAME_PADDING = " " # 3 spaces for icon alignment
# Icon settings
ICON_SIZE = (16, 16)
ICON_FILE = "icon.ico"
# File settings
DATA_FILE = "data.json"
LEGACY_FILE = "program_list.json"
# Modern color themes
THEMES = {
"modern_dark": {
"bg_primary": "#2b2b2b",
"bg_secondary": "#3c3c3c",
"bg_tertiary": "#4a4a4a",
"accent": "#007acc",
"accent_hover": "#1e90ff",
"success": "#28a745",
"success_hover": "#34ce57",
"danger": "#dc3545",
"text_primary": "#ffffff",
"text_secondary": "#cccccc",
"text_muted": "#888888",
"border": "#555555",
"selection": "#007acc",
"selection_hover": "#1e90ff"
},
"modern_light": {
"bg_primary": "#ffffff",
"bg_secondary": "#f8f9fa",
"bg_tertiary": "#e9ecef",
"accent": "#007acc",
"accent_hover": "#1e90ff",
"success": "#28a745",
"success_hover": "#34ce57",
"danger": "#dc3545",
"text_primary": "#212529",
"text_secondary": "#495057",
"text_muted": "#6c757d",
"border": "#dee2e6",
"selection": "#007acc",
"selection_hover": "#1e90ff"
},
"gradient_blue": {
"bg_primary": "#f0f2f5",
"bg_secondary": "#ffffff",
"bg_tertiary": "#e3f2fd",
"accent": "#1976d2",
"accent_hover": "#1565c0",
"success": "#4caf50",
"success_hover": "#43a047",
"danger": "#f44336",
"text_primary": "#263238",
"text_secondary": "#546e7a",
"text_muted": "#90a4ae",
"border": "#cfd8dc",
"selection": "#1976d2",
"selection_hover": "#1565c0"
}
}
# Current theme (can be changed)
CURRENT_THEME = "gradient_blue"
@classmethod
def get_theme(cls) -> dict:
return cls.THEMES[cls.CURRENT_THEME]
# Modern typography
FONTS = {
"heading": ("Segoe UI", 18, "bold"),
"subheading": ("Segoe UI", 14, "bold"),
"body": ("Segoe UI", 11),
"body_small": ("Segoe UI", 10),
"caption": ("Segoe UI", 9),
"button": ("Segoe UI", 10, "bold"),
"link": ("Segoe UI", 10, "underline")
}
# Compact UI Spacing
SPACING = {
"xs": 2,
"sm": 4,
"md": 6,
"lg": 8,
"xl": 12,
"xxl": 16
}
# Compact button styles
BUTTON_STYLES = {
"primary": {"style": "primary", "width": 80, "padding": (6, 4)},
"secondary": {"style": "secondary", "width": 70, "padding": (4, 3)},
"small": {"style": "small", "width": 24, "padding": (2, 2)},
"icon": {"style": "icon", "width": 28, "padding": (3, 3)}
}
# Border radius for modern look
BORDER_RADIUS = 8
# Compact card styling
CARD_PADDING = 8
CARD_SHADOW = {"x": 0, "y": 1, "blur": 3}
# Sort options
SORT_OPTIONS = [
"Keine",
"Programm (A-Z)",
"Programm (Z-A)",
"Kategorie (A-Z)",
"Kategorie (Z-A)"
]
# Messages
MESSAGES = {
"no_program_selected": "Bitte wählen Sie ein Programm aus.",
"no_category_selected": "Bitte wählen Sie eine Kategorie zum Entfernen aus.",
"cannot_remove_all": "Die Kategorie 'Alle' kann nicht entfernt werden.",
"fill_all_fields": "Bitte füllen Sie alle Felder aus.",
"path_not_exists": "Der angegebene Pfad existiert nicht.",
"category_exists": "Die Kategorie '{}' existiert bereits.",
"program_not_found": "Programm '{}' konnte nicht gefunden werden.",
"error_starting_program": "Fehler beim Starten des Programms: {}",
"error_loading_data": "Fehler beim Laden der Daten: {}\nEine neue Liste wird erstellt.",
"error_saving_data": "Fehler beim Speichern der Daten: {}",
"error_opening_url": "Fehler beim Öffnen der URL: {}",
"icon_libraries_missing": "Warnung: Die Bibliotheken für Programm-Icons fehlen. Installieren Sie diese mit: pip install pywin32 Pillow",
"confirm_remove_category": "Möchten Sie die Kategorie '{}' wirklich entfernen?\n\nAlle Programme in dieser Kategorie werden ohne Kategorie gespeichert.",
"confirm_remove_program": "Möchten Sie '{}' wirklich entfernen?",
"help_text": "Wählen Sie ein Programm zum Starten oder fügen Sie ein neues hinzu"
}
@classmethod
def get_default_options(cls) -> Dict[str, Any]:
return {
"default_sort_column": None,
"default_sort_reverse": False,
"remember_last_category": False,
"last_category": "Alle",
"window_width": cls.DEFAULT_WINDOW_WIDTH,
"window_height": cls.DEFAULT_WINDOW_HEIGHT
}

0
data/__init__.py Normal file
View File

153
data/data_manager.py Normal file
View File

@@ -0,0 +1,153 @@
import json
import os
from typing import List, Dict, Any
from models.program import Program
from models.category import CategoryManager
class DataManager:
def __init__(self, json_file: str = 'data.json'):
self.json_file = json_file
self.category_manager = CategoryManager()
self.programs: List[Program] = []
self.options: Dict[str, Any] = self._get_default_options()
def _get_default_options(self) -> Dict[str, Any]:
return {
"default_sort_column": None,
"default_sort_reverse": False,
"remember_last_category": False,
"last_category": "Alle",
"window_width": 700,
"window_height": 550
}
def load_data(self) -> bool:
try:
# Handle legacy file migration
if not os.path.exists(self.json_file) and os.path.exists('program_list.json'):
self._migrate_from_legacy_file()
return True
if not os.path.exists(self.json_file):
self._create_empty_file()
return True
with open(self.json_file, encoding='utf-8') as f:
data = json.load(f)
# Handle old format without options
if "options" not in data:
data["options"] = self._get_default_options()
self._load_from_data(data)
return True
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error loading data: {e}")
self._create_empty_file()
return False
def _migrate_from_legacy_file(self):
with open('program_list.json', encoding='utf-8') as f:
old_data = json.load(f)
if isinstance(old_data, list):
# Old format: just a list of programs
# Extract categories from programs
categories = []
for program_data in old_data:
if "Kategorie" in program_data and program_data["Kategorie"] not in categories:
categories.append(program_data["Kategorie"])
data = {
"categories": categories,
"programs": old_data,
"options": self._get_default_options()
}
else:
# Newer format but without options
old_data["options"] = self._get_default_options()
data = old_data
self._load_from_data(data)
self.save_data()
def _create_empty_file(self):
data = {
"categories": [],
"programs": [],
"options": self._get_default_options()
}
self._load_from_data(data)
self.save_data()
def _load_from_data(self, data: Dict[str, Any]):
self.category_manager.load_from_data(data.get("categories", []))
self.programs = [Program.from_dict(p) for p in data.get("programs", [])]
self.options = data.get("options", self._get_default_options())
def save_data(self) -> bool:
try:
data = {
"categories": self.category_manager.to_list(),
"programs": [p.to_dict() for p in self.programs],
"options": self.options
}
with open(self.json_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"Error saving data: {e}")
return False
def add_program(self, program: Program) -> bool:
if program.category and not self.category_manager.has_category(program.category):
self.category_manager.add_category(program.category)
self.programs.append(program)
return self.save_data()
def remove_program(self, program_name: str) -> bool:
for i, program in enumerate(self.programs):
if program.name.strip() == program_name.strip():
del self.programs[i]
return self.save_data()
return False
def update_program(self, old_name: str, updated_program: Program) -> bool:
for i, program in enumerate(self.programs):
if program.name.strip() == old_name.strip():
if updated_program.category and not self.category_manager.has_category(updated_program.category):
self.category_manager.add_category(updated_program.category)
self.programs[i] = updated_program
return self.save_data()
return False
def get_programs_by_category(self, category: str = None) -> List[Program]:
if not category or category == "Alle":
return self.programs
return [p for p in self.programs if p.category == category]
def add_category(self, category: str) -> bool:
if self.category_manager.add_category(category):
return self.save_data()
return False
def remove_category(self, category: str) -> bool:
if not self.category_manager.remove_category(category):
return False
# Remove category from all programs
for program in self.programs:
if program.category == category:
program.category = None
return self.save_data()
def update_options(self, **kwargs):
self.options.update(kwargs)
return self.save_data()

0
gui/__init__.py Normal file
View File

351
gui/dialogs.py Normal file
View File

@@ -0,0 +1,351 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import os
import webbrowser
from typing import Callable, Optional, Dict, Any, List
from models.program import Program
from config import Config
from utils.icon_utils import IconManager
from gui.modern_widgets import ModernStyle, ModernCard, ModernButton
class BaseDialog:
def __init__(self, parent: tk.Widget, title: str, width: int = 500, height: int = 400):
self.parent = parent
self.result = None
self.modern_style = ModernStyle()
self.icon_manager = IconManager()
theme = Config.get_theme()
self.window = tk.Toplevel(parent)
self.window.title(title)
self.window.geometry(f"{width}x{height}")
self.window.minsize(width, height) # Mindestgröße setzen
self.window.resizable(True, True) # Größe änderbar machen
self.window.grab_set()
self.window.configure(bg=theme["bg_primary"])
# Set icon
try:
self.window.iconbitmap(self.icon_manager.resource_path(Config.ICON_FILE))
except:
pass
# Configure modern styling
style = ttk.Style()
self.modern_style.configure_styles(style)
self._center_window()
self._create_widgets()
def _center_window(self):
self.window.update_idletasks()
width = self.window.winfo_width()
height = self.window.winfo_height()
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
y = (self.window.winfo_screenheight() // 2) - (height // 2)
self.window.geometry(f"{width}x{height}+{x}+{y}")
def _create_widgets(self):
pass
def show(self):
self.window.wait_window()
return self.result
class ProgramDialog(BaseDialog):
def __init__(self, parent: tk.Widget, title: str, categories: List[str],
program: Optional[Program] = None):
self.categories = categories
self.program = program
self.is_edit = program is not None
super().__init__(parent, title, 450, 280)
def _create_widgets(self):
# Simple container
container = ttk.Frame(self.window, padding="20")
container.pack(fill=tk.BOTH, expand=True)
# Form fields using grid
self._create_form_fields(container)
# Buttons
self._create_buttons(container)
def _create_form_fields(self, parent):
# Name field
ttk.Label(parent, text="Programmname:").grid(row=0, column=0, sticky="w", pady=5)
self.name_entry = ttk.Entry(parent, width=30)
self.name_entry.grid(row=0, column=1, columnspan=2, sticky="we", pady=5)
# Path field
ttk.Label(parent, text="Programmpfad:").grid(row=1, column=0, sticky="w", pady=5)
self.path_entry = ttk.Entry(parent, width=30)
self.path_entry.grid(row=1, column=1, sticky="we", pady=5)
browse_button = ttk.Button(parent, text="Durchsuchen", command=self._browse_file)
browse_button.grid(row=1, column=2, padx=5, pady=5)
# Category field
ttk.Label(parent, text="Kategorie:").grid(row=2, column=0, sticky="w", pady=5)
self.category_combobox = ttk.Combobox(parent, width=28, values=sorted(self.categories))
self.category_combobox.grid(row=2, column=1, columnspan=2, sticky="we", pady=5)
# Admin rights checkbox
self.admin_var = tk.BooleanVar(value=False)
admin_check = ttk.Checkbutton(parent, text="Benötigt Administratorrechte",
variable=self.admin_var)
admin_check.grid(row=3, column=0, columnspan=3, sticky="w", pady=5)
# Fill fields if editing
if self.program:
self.name_entry.insert(0, self.program.name)
self.path_entry.insert(0, self.program.path)
if self.program.category:
self.category_combobox.set(self.program.category)
elif self.categories:
self.category_combobox.current(0)
self.admin_var.set(self.program.requires_admin)
elif self.categories:
self.category_combobox.current(0)
self.name_entry.focus_set()
# Make column 1 expandable
parent.columnconfigure(1, weight=1)
def _create_buttons(self, parent):
# Button frame
button_frame = ttk.Frame(parent)
button_frame.grid(row=4, column=0, columnspan=3, pady=15)
ttk.Button(button_frame, text="Speichern", command=self._save).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Abbrechen", command=self.window.destroy).pack(side=tk.LEFT, padx=5)
def _browse_file(self):
if not self.is_edit:
self.path_entry.delete(0, tk.END)
filename = filedialog.askopenfilename(
filetypes=[
("Ausführbare Dateien", "*.exe"),
("Batch-Dateien", "*.bat;*.cmd"),
("Alle ausführbaren Dateien", "*.exe;*.bat;*.cmd"),
("Alle Dateien", "*.*")
]
)
if filename:
if not self.is_edit:
self.path_entry.insert(0, filename)
else:
self.path_entry.delete(0, tk.END)
self.path_entry.insert(0, filename)
def _save(self):
name = self.name_entry.get().strip()
path = self.path_entry.get().strip()
category = self.category_combobox.get().strip()
requires_admin = self.admin_var.get()
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
self.result = Program(
name=name,
path=path,
category=category if category else None,
requires_admin=requires_admin
)
self.window.destroy()
class OptionsDialog(BaseDialog):
def __init__(self, parent: tk.Widget, current_options: Dict[str, Any]):
self.current_options = current_options.copy()
super().__init__(parent, "Programmoptionen", 400, 320)
def _create_widgets(self):
# Simple container with normal padding
container = ttk.Frame(self.window, padding="20")
container.pack(fill=tk.BOTH, expand=True)
# Title
title_label = ttk.Label(container, text="Programmoptionen",
font=("Segoe UI", 12, "bold"))
title_label.grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 15))
# Sort option
ttk.Label(container, text="Standardsortierung:").grid(row=1, column=0, sticky="w", pady=5)
self.sort_var = tk.StringVar(value=self._get_sort_option_text())
sort_combobox = ttk.Combobox(container, textvariable=self.sort_var,
values=Config.SORT_OPTIONS,
width=20)
sort_combobox.grid(row=1, column=1, sticky="we", pady=5)
sort_combobox.configure(state="readonly")
# Remember category option
self.remember_category_var = tk.BooleanVar(
value=self.current_options.get("remember_last_category", False)
)
remember_category_check = ttk.Checkbutton(
container,
text="Letzte ausgewählte Kategorie merken",
variable=self.remember_category_var
)
remember_category_check.grid(row=2, column=0, columnspan=2, sticky="w", pady=10)
# Separator
separator = ttk.Separator(container, orient="horizontal")
separator.grid(row=3, column=0, columnspan=2, sticky="ew", pady=10)
# Info section
info_frame = ttk.Frame(container)
info_frame.grid(row=4, column=0, columnspan=2, sticky="ew", pady=5)
ttk.Label(info_frame, text=f"Version: {Config.APP_VERSION}",
font=("Segoe UI", 9)).pack(anchor="w")
ttk.Label(info_frame, text=f"{Config.APP_AUTHOR}",
font=("Segoe UI", 9)).pack(anchor="w")
ttk.Label(info_frame, text=f"{Config.APP_DESCRIPTION}",
font=("Segoe UI", 9)).pack(anchor="w")
# Link
link_label = ttk.Label(info_frame, text="Quellcode auf git.ponywave.de",
font=("Segoe UI", 9, "underline"),
foreground="blue", cursor="hand2")
link_label.pack(anchor="w", pady=(5, 0))
link_label.bind("<Button-1>", lambda e: self._open_url(Config.SOURCE_URL))
# Buttons
button_frame = ttk.Frame(container)
button_frame.grid(row=5, column=0, columnspan=2, pady=20)
ttk.Button(button_frame, text="Speichern", command=self._save).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Abbrechen", command=self.window.destroy).pack(side=tk.LEFT, padx=5)
# Make column 1 expandable
container.columnconfigure(1, weight=1)
def _get_sort_option_text(self) -> str:
column = self.current_options.get("default_sort_column")
reverse = self.current_options.get("default_sort_reverse", False)
if column == "name":
return "Programm (Z-A)" if reverse else "Programm (A-Z)"
elif column == "category":
return "Kategorie (Z-A)" if reverse else "Kategorie (A-Z)"
else:
return "Keine"
def _save(self):
sort_option = self.sort_var.get()
remember_category = self.remember_category_var.get()
# Translate sort options
if sort_option == "Programm (A-Z)":
sort_column, sort_reverse = "name", False
elif sort_option == "Programm (Z-A)":
sort_column, sort_reverse = "name", True
elif sort_option == "Kategorie (A-Z)":
sort_column, sort_reverse = "category", False
elif sort_option == "Kategorie (Z-A)":
sort_column, sort_reverse = "category", True
else: # "Keine"
sort_column, sort_reverse = None, False
self.result = {
"default_sort_column": sort_column,
"default_sort_reverse": sort_reverse,
"remember_last_category": remember_category
}
self.window.destroy()
def _open_url(self, url: str):
try:
webbrowser.open(url)
except Exception as e:
messagebox.showerror("Fehler", f"Fehler beim Öffnen der URL: {str(e)}")
class CategoryDialog(BaseDialog):
def __init__(self, parent: tk.Widget, dialog_type: str = "add", categories: List[str] = None):
self.dialog_type = dialog_type
self.categories = categories or []
if dialog_type == "add":
title = "Neue Kategorie"
width, height = 400, 150
else: # delete
title = "Kategorie löschen"
width, height = 450, 200
super().__init__(parent, title, width, height)
def _create_widgets(self):
container = ttk.Frame(self.window, padding="20")
container.pack(fill=tk.BOTH, expand=True)
if self.dialog_type == "add":
# Label
ttk.Label(container, text="Geben Sie den Namen der neuen Kategorie ein:").pack(pady=(0, 10))
# Entry
self.name_entry = ttk.Entry(container, width=30)
self.name_entry.pack(pady=(0, 15))
self.name_entry.focus_set()
# Bind Enter key
self.name_entry.bind("<Return>", lambda e: self._ok())
else: # delete
# Label
label_text = f"Verfügbare Kategorien: {', '.join(sorted(self.categories))}\n\nWelche Kategorie soll gelöscht werden?"
ttk.Label(container, text=label_text, wraplength=400).pack(pady=(0, 10))
# Combobox for category selection
self.category_combobox = ttk.Combobox(container, values=sorted(self.categories), width=30)
self.category_combobox.pack(pady=(0, 15))
self.category_combobox.focus_set()
if self.categories:
self.category_combobox.current(0)
# Buttons
button_frame = ttk.Frame(container)
button_frame.pack()
ttk.Button(button_frame, text="OK", command=self._ok).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Abbrechen", command=self.window.destroy).pack(side=tk.LEFT, padx=5)
def _ok(self):
if self.dialog_type == "add":
self.result = self.name_entry.get().strip()
else: # delete
self.result = self.category_combobox.get().strip()
self.window.destroy()
@staticmethod
def add_category(parent: tk.Widget) -> Optional[str]:
dialog = CategoryDialog(parent, "add")
return dialog.show()
@staticmethod
def delete_category(parent: tk.Widget, categories: List[str]) -> Optional[str]:
dialog = CategoryDialog(parent, "delete", categories)
return dialog.show()
@staticmethod
def confirm_remove_category(parent: tk.Widget, category: str) -> bool:
return messagebox.askyesno("Bestätigung",
Config.MESSAGES["confirm_remove_category"].format(category),
parent=parent)
@staticmethod
def confirm_remove_program(parent: tk.Widget, program_name: str) -> bool:
return messagebox.askyesno("Bestätigung",
f"Möchten Sie '{program_name}' wirklich entfernen?",
parent=parent)

396
gui/main_window.py Normal file
View File

@@ -0,0 +1,396 @@
import tkinter as tk
from tkinter import ttk, messagebox
import subprocess
import sys
import os
import ctypes
from typing import Optional, List
from config import Config
from data.data_manager import DataManager
from utils.icon_utils import IconManager
from gui.dialogs import ProgramDialog, OptionsDialog, CategoryDialog
from gui.modern_widgets import (ModernStyle, ModernCard, ModernButton,
ModernListbox, ModernTreeview, IconButton)
from models.program import Program
class MainWindow:
def __init__(self, root: tk.Tk):
self.root = root
self.data_manager = DataManager()
self.icon_manager = IconManager()
self.sort_column: Optional[str] = None
self.sort_reverse: bool = False
self.modern_style = ModernStyle()
self._setup_window()
self._load_data()
self._create_widgets()
self._populate_program_list()
self._apply_options()
self._center_window()
def _setup_window(self):
theme = Config.get_theme()
self.root.title(Config.APP_NAME)
self.root.geometry(f"{Config.DEFAULT_WINDOW_WIDTH}x{Config.DEFAULT_WINDOW_HEIGHT}")
self.root.minsize(Config.MIN_WINDOW_WIDTH, Config.MIN_WINDOW_HEIGHT)
self.root.configure(bg=theme["bg_primary"])
# Load icon if available
try:
self.root.iconbitmap(self.icon_manager.resource_path(Config.ICON_FILE))
except:
pass
def _load_data(self):
if not self.data_manager.load_data():
messagebox.showwarning("Warnung", Config.MESSAGES["error_loading_data"].format("JSON-Fehler"))
def _create_widgets(self):
self._configure_styles()
self._create_menubar()
self._create_program_list()
def _configure_styles(self):
style = ttk.Style()
self.modern_style.configure_styles(style)
def _create_menubar(self):
# Windows-style menu bar
self.menubar = tk.Menu(self.root)
self.root.config(menu=self.menubar)
# Program menu
program_menu = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Programm", menu=program_menu)
program_menu.add_command(label="Hinzufügen...", command=self._add_program, accelerator="Ctrl+N")
program_menu.add_command(label="Bearbeiten...", command=self._edit_program, accelerator="F2")
program_menu.add_command(label="Entfernen", command=self._remove_program, accelerator="Del")
program_menu.add_separator()
program_menu.add_command(label="Starten", command=self._start_program, accelerator="Enter")
# Category menu
self.category_menu = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Kategorie", menu=self.category_menu)
self._create_category_menu(self.category_menu)
# Tools menu
tools_menu = tk.Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Extras", menu=tools_menu)
tools_menu.add_command(label="Optionen...", command=self._show_options)
# Bind keyboard shortcuts
self.root.bind("<Control-n>", lambda e: self._add_program())
self.root.bind("<F2>", lambda e: self._edit_program())
self.root.bind("<Delete>", lambda e: self._remove_program())
self.root.bind("<Return>", lambda e: self._start_program())
def _create_category_menu(self, menu):
# Add categories to menu with radio buttons
self.category_var = tk.StringVar(value="Alle")
menu.add_radiobutton(label="Alle", variable=self.category_var,
value="Alle", command=self._on_category_change)
if self.data_manager.category_manager.categories:
menu.add_separator()
for category in sorted(self.data_manager.category_manager.categories):
menu.add_radiobutton(label=category, variable=self.category_var,
value=category, command=self._on_category_change)
menu.add_separator()
menu.add_command(label="Neue Kategorie...", command=self._add_category)
menu.add_command(label="Kategorie löschen...", command=self._remove_category)
def _on_category_change(self):
self._populate_program_list()
def _update_category_menu(self):
# Clear and recreate category menu
self.category_menu.delete(0, 'end')
self._create_category_menu(self.category_menu)
def _create_program_list(self):
# Simple program list taking full window
list_frame = ttk.Frame(self.root, style="Modern.TFrame")
list_frame.pack(fill=tk.BOTH, expand=True, padx=Config.SPACING["sm"],
pady=Config.SPACING["sm"])
# Scrollbar
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Program treeview
columns = ("name", "category")
show_option = "tree headings" if self.icon_manager.icon_support else "headings"
self.program_tree = ModernTreeview(list_frame, yscrollcommand=scrollbar.set,
columns=columns, show=show_option)
# Headers without emojis for clean look
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))
# Better column sizing
self.program_tree.column("name", width=400, anchor="w", minwidth=200)
self.program_tree.column("category", width=150, anchor="w", minwidth=80)
self.program_tree.column("#0", width=24, stretch=False, anchor="w")
self.program_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.program_tree.yview)
# Bindings
self.program_tree.bind("<Double-1>", lambda event: self._start_program())
self.program_tree.bind("<Return>", lambda event: self._start_program())
def _populate_program_list(self):
# Clear existing items
for item in self.program_tree.get_children():
self.program_tree.delete(item)
# Get selected category from menu
selected_category = self.category_var.get()
# Get programs for category
if selected_category == "Alle":
programs = self.data_manager.programs
else:
programs = self.data_manager.get_programs_by_category(selected_category)
# Sort programs
sorted_programs = self._sort_programs(programs)
# Add programs to tree
for program in sorted_programs:
self._add_program_to_tree(program)
def _sort_programs(self, programs: List[Program]) -> List[Program]:
if self.sort_column == "name":
return sorted(programs, key=lambda x: x.name.lower(), reverse=self.sort_reverse)
elif self.sort_column == "category":
return sorted(programs, key=lambda x: (x.category or "").lower(), reverse=self.sort_reverse)
else:
# Default sort: category then name
return sorted(programs, key=lambda x: ((x.category or "").lower(), x.name.lower()))
def _add_program_to_tree(self, program: Program):
program_name = Config.PROGRAM_NAME_PADDING + program.name
category = program.category or ""
# Get icon if supported
icon = None
if self.icon_manager.icon_support:
icon = self.icon_manager.get_icon(program.path)
# Add to tree
if self.icon_manager.icon_support and icon:
self.program_tree.insert("", tk.END, text="",
values=(program_name, category),
tags=(program.path,), image=icon)
else:
self.program_tree.insert("", tk.END, text="",
values=(program_name, category),
tags=(program.path,))
def _sort_treeview(self, column: str, reverse: bool):
if self.sort_column == column:
reverse = not self.sort_reverse
self.sort_column = column
self.sort_reverse = reverse
self._populate_program_list()
def _get_selected_program(self) -> Optional[Program]:
selected_items = self.program_tree.selection()
if not selected_items:
return None
item_id = selected_items[0]
program_values = self.program_tree.item(item_id, "values")
program_name = program_values[0].strip()
# Find program in data
for program in self.data_manager.programs:
if program.name.strip() == program_name:
return program
return None
def _start_program(self):
program = self._get_selected_program()
if not program:
messagebox.showinfo("Information", Config.MESSAGES["no_program_selected"])
return
try:
# Set working directory to the program's directory
program_dir = os.path.dirname(program.path)
# Check if it's a batch file
is_batch_file = program.path.lower().endswith(('.bat', '.cmd'))
if sys.platform == "win32" and program.requires_admin:
try:
# For admin programs, use ShellExecuteW with working directory
result = ctypes.windll.shell32.ShellExecuteW(
None, "runas", program.path, None, program_dir, 1)
if result <= 32:
# Fallback with working directory
if is_batch_file:
subprocess.Popen(['cmd', '/c', program.path], cwd=program_dir)
else:
subprocess.Popen(program.path, cwd=program_dir)
except Exception:
# Fallback with working directory
if is_batch_file:
subprocess.Popen(['cmd', '/c', program.path], cwd=program_dir)
else:
subprocess.Popen(program.path, cwd=program_dir)
else:
# Normal start with working directory
if is_batch_file:
# Batch files need to be run through cmd
subprocess.Popen(['cmd', '/c', program.path], cwd=program_dir)
else:
# Regular executable
subprocess.Popen(program.path, cwd=program_dir)
except Exception as e:
messagebox.showerror("Fehler",
Config.MESSAGES["error_starting_program"].format(str(e)))
def _add_program(self):
dialog = ProgramDialog(self.root, "Programm hinzufügen",
self.data_manager.category_manager.categories)
program = dialog.show()
if program:
if self.data_manager.add_program(program):
self._update_category_menu()
self._populate_program_list()
def _edit_program(self):
program = self._get_selected_program()
if not program:
messagebox.showinfo("Information",
"Bitte wählen Sie ein Programm zum Bearbeiten aus.")
return
dialog = ProgramDialog(self.root, "Programm bearbeiten",
self.data_manager.category_manager.categories,
program)
updated_program = dialog.show()
if updated_program:
if self.data_manager.update_program(program.name, updated_program):
self._update_category_menu()
self._populate_program_list()
def _remove_program(self):
program = self._get_selected_program()
if not program:
messagebox.showinfo("Information",
"Bitte wählen Sie ein Programm zum Entfernen aus.")
return
if CategoryDialog.confirm_remove_program(self.root, program.name):
if self.data_manager.remove_program(program.name):
self._populate_program_list()
def _add_category(self):
category_name = CategoryDialog.add_category(self.root)
if not category_name:
return
if self.data_manager.category_manager.has_category(category_name):
messagebox.showwarning("Warnung",
Config.MESSAGES["category_exists"].format(category_name))
return
if self.data_manager.add_category(category_name):
self._update_category_menu()
self._populate_program_list()
def _remove_category(self):
# Show list of categories to delete
categories = self.data_manager.category_manager.categories
if not categories:
messagebox.showinfo("Information", "Keine Kategorien zum Löschen vorhanden.")
return
# Use custom dialog for category deletion
category = CategoryDialog.delete_category(self.root, list(categories))
if not category or category not in categories:
return
if CategoryDialog.confirm_remove_category(self.root, category):
if self.data_manager.remove_category(category):
# Reset to "Alle" if deleted category was selected
if self.category_var.get() == category:
self.category_var.set("Alle")
self._update_category_menu()
self._populate_program_list()
def _show_options(self):
dialog = OptionsDialog(self.root, self.data_manager.options)
result = dialog.show()
if result:
self.sort_column = result["default_sort_column"]
self.sort_reverse = result["default_sort_reverse"]
self.data_manager.update_options(**result)
self._apply_options()
self._populate_program_list()
def _apply_options(self):
# Apply sort settings
if self.data_manager.options.get("default_sort_column"):
self.sort_column = self.data_manager.options["default_sort_column"]
self.sort_reverse = self.data_manager.options["default_sort_reverse"]
# Apply category selection
if (self.data_manager.options.get("remember_last_category") and
self.data_manager.options.get("last_category")):
last_category = self.data_manager.options["last_category"]
# Check if category still exists
all_categories = ["Alle"] + self.data_manager.category_manager.categories
if last_category in all_categories:
self.category_var.set(last_category)
self._populate_program_list()
# Apply window size
if (self.data_manager.options.get("window_width") and
self.data_manager.options.get("window_height")):
width = self.data_manager.options["window_width"]
height = self.data_manager.options["window_height"]
self.root.geometry(f"{width}x{height}")
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 on_closing(self):
# Save current state
self.data_manager.update_options(
window_width=self.root.winfo_width(),
window_height=self.root.winfo_height()
)
# Save last category if option is enabled
if self.data_manager.options.get("remember_last_category"):
self.data_manager.update_options(last_category=self.category_var.get())
self.root.destroy()

288
gui/modern_widgets.py Normal file
View File

@@ -0,0 +1,288 @@
import tkinter as tk
from tkinter import ttk
from typing import Optional, Callable, Any
from config import Config
class ModernStyle:
"""Manages modern styling for tkinter widgets"""
def __init__(self):
self.theme = Config.get_theme()
self.fonts = Config.FONTS
self.spacing = Config.SPACING
def configure_styles(self, style: ttk.Style):
"""Configure modern ttk styles"""
theme = self.theme
# Configure main frame
style.configure("Modern.TFrame",
background=theme["bg_primary"],
relief="flat")
# Configure card frames
style.configure("Card.TFrame",
background=theme["bg_secondary"],
relief="flat",
borderwidth=1)
# Configure modern buttons
style.configure("Modern.TButton",
background=theme["accent"],
foreground=theme["text_primary"] if "dark" in Config.CURRENT_THEME else "white",
font=self.fonts["button"],
borderwidth=0,
focuscolor="none",
relief="flat",
padding=(12, 8))
style.map("Modern.TButton",
background=[("active", theme["accent_hover"]),
("pressed", theme["accent"])],
foreground=[("active", "white"),
("pressed", "white")])
# Configure success buttons
style.configure("Success.TButton",
background=theme["success"],
foreground="white",
font=self.fonts["button"],
borderwidth=0,
focuscolor="none",
relief="flat",
padding=(12, 8))
style.map("Success.TButton",
background=[("active", theme["success_hover"]),
("pressed", theme["success"])],
foreground=[("active", "white"),
("pressed", "white")])
# Configure secondary buttons
style.configure("Secondary.TButton",
background=theme["bg_tertiary"],
foreground=theme["text_primary"],
font=self.fonts["body"],
borderwidth=1,
focuscolor="none",
relief="flat",
padding=(8, 6))
style.map("Secondary.TButton",
background=[("active", theme["border"]),
("pressed", theme["bg_tertiary"])],
relief=[("pressed", "flat")])
# Configure icon buttons
style.configure("Icon.TButton",
background=theme["bg_tertiary"],
foreground=theme["text_primary"],
font=self.fonts["subheading"],
borderwidth=1,
focuscolor="none",
relief="flat",
padding=(8, 8))
style.map("Icon.TButton",
background=[("active", theme["border"]),
("pressed", theme["bg_tertiary"])],
relief=[("pressed", "flat")])
# Configure labels
style.configure("Heading.TLabel",
background=theme["bg_primary"],
foreground=theme["text_primary"],
font=self.fonts["heading"])
style.configure("Subheading.TLabel",
background=theme["bg_primary"],
foreground=theme["text_secondary"],
font=self.fonts["subheading"])
style.configure("Body.TLabel",
background=theme["bg_primary"],
foreground=theme["text_secondary"],
font=self.fonts["body"])
style.configure("Caption.TLabel",
background=theme["bg_primary"],
foreground=theme["text_muted"],
font=self.fonts["caption"])
# Configure modern treeview
style.configure("Modern.Treeview",
background=theme["bg_secondary"],
foreground=theme["text_primary"],
fieldbackground=theme["bg_secondary"],
borderwidth=0,
relief="flat",
font=self.fonts["body"])
style.configure("Modern.Treeview.Heading",
background=theme["bg_tertiary"],
foreground=theme["text_primary"],
font=self.fonts["subheading"],
relief="flat",
borderwidth=0)
style.map("Modern.Treeview",
background=[("selected", theme["selection"])],
foreground=[("selected", "white")])
style.map("Modern.Treeview.Heading",
background=[("active", theme["border"])])
# Configure modern listbox (via tk styling)
# Note: Listbox doesn't support ttk styling, will be handled in widget creation
# Configure modern entry
style.configure("Modern.TEntry",
fieldbackground=theme["bg_secondary"],
foreground=theme["text_primary"],
borderwidth=1,
relief="flat",
insertcolor=theme["text_primary"],
font=self.fonts["body"])
style.map("Modern.TEntry",
focuscolor=[("focus", theme["accent"])],
bordercolor=[("focus", theme["accent"])])
# Configure modern combobox
style.configure("Modern.TCombobox",
fieldbackground=theme["bg_secondary"],
foreground=theme["text_primary"],
background=theme["bg_secondary"],
borderwidth=1,
relief="flat",
font=self.fonts["body"])
# Configure modern checkbutton
style.configure("Modern.TCheckbutton",
background=theme["bg_secondary"],
foreground=theme["text_primary"],
font=self.fonts["body"],
focuscolor="none")
style.map("Modern.TCheckbutton",
background=[("active", theme["bg_secondary"]),
("pressed", theme["bg_secondary"])])
class ModernCard(ttk.Frame):
"""A modern card-like container with shadow effect simulation"""
def __init__(self, parent, padding=None, **kwargs):
super().__init__(parent, style="Card.TFrame",
padding=padding or Config.CARD_PADDING, **kwargs)
class ModernButton(ttk.Button):
"""Enhanced button with modern styling and hover effects"""
def __init__(self, parent, text="", style_type="primary", icon=None,
command=None, **kwargs):
# Determine style based on type
if style_type == "primary":
style = "Modern.TButton"
elif style_type == "success":
style = "Success.TButton"
elif style_type == "secondary":
style = "Secondary.TButton"
else:
style = "Modern.TButton"
super().__init__(parent, text=text, style=style, command=command, **kwargs)
# Add hover effects
self.bind("<Enter>", self._on_enter)
self.bind("<Leave>", self._on_leave)
def _on_enter(self, event):
"""Handle mouse enter"""
self.configure(cursor="hand2")
def _on_leave(self, event):
"""Handle mouse leave"""
self.configure(cursor="")
class ModernListbox(tk.Listbox):
"""Modern styled listbox with custom colors"""
def __init__(self, parent, **kwargs):
theme = Config.get_theme()
# Configure modern listbox appearance
defaults = {
"background": theme["bg_secondary"],
"foreground": theme["text_primary"],
"selectbackground": theme["selection"],
"selectforeground": "white",
"activestyle": "none",
"relief": "flat",
"borderwidth": 0,
"highlightthickness": 1,
"highlightcolor": theme["accent"],
"highlightbackground": theme["border"],
"font": Config.FONTS["body"]
}
# Merge with user provided kwargs
defaults.update(kwargs)
super().__init__(parent, **defaults)
class ModernTreeview(ttk.Treeview):
"""Enhanced treeview with modern styling"""
def __init__(self, parent, **kwargs):
super().__init__(parent, style="Modern.Treeview", **kwargs)
# Configure modern row height
style = ttk.Style()
style.configure("Modern.Treeview", rowheight=32)
class ModernScrollbar(ttk.Scrollbar):
"""Modern styled scrollbar"""
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
# Configure modern scrollbar
style = ttk.Style()
theme = Config.get_theme()
style.configure("Modern.Vertical.TScrollbar",
background=theme["bg_tertiary"],
troughcolor=theme["bg_primary"],
borderwidth=0,
arrowcolor=theme["text_muted"],
darkcolor=theme["bg_tertiary"],
lightcolor=theme["bg_tertiary"])
class IconButton(ttk.Button):
"""Button designed specifically for icons"""
def __init__(self, parent, icon_text="", command=None, **kwargs):
super().__init__(parent, text=icon_text, style="Icon.TButton",
command=command, **kwargs)
self.configure(width=3)
# Add hover effects
self.bind("<Enter>", self._on_enter)
self.bind("<Leave>", self._on_leave)
def _on_enter(self, event):
"""Handle mouse enter"""
self.configure(cursor="hand2")
def _on_leave(self, event):
"""Handle mouse leave"""
self.configure(cursor="")

32
main.py Normal file
View File

@@ -0,0 +1,32 @@
import tkinter as tk
import sys
import os
# Add current directory to Python path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gui.main_window import MainWindow
from utils.icon_utils import IconManager
from config import Config
def main():
root = tk.Tk()
# Set icon for main window
icon_manager = IconManager()
try:
root.iconbitmap(icon_manager.resource_path(Config.ICON_FILE))
except:
pass
app = MainWindow(root)
# Handle window closing
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()
if __name__ == "__main__":
main()

0
models/__init__.py Normal file
View File

31
models/category.py Normal file
View File

@@ -0,0 +1,31 @@
from typing import List, Set
class CategoryManager:
def __init__(self):
self._categories: Set[str] = set()
@property
def categories(self) -> List[str]:
return sorted(list(self._categories))
def add_category(self, category: str) -> bool:
if not category or category in self._categories:
return False
self._categories.add(category)
return True
def remove_category(self, category: str) -> bool:
if category not in self._categories:
return False
self._categories.remove(category)
return True
def has_category(self, category: str) -> bool:
return category in self._categories
def load_from_data(self, categories: List[str]):
self._categories = set(categories)
def to_list(self) -> List[str]:
return self.categories

39
models/program.py Normal file
View File

@@ -0,0 +1,39 @@
from dataclasses import dataclass
from typing import Optional
@dataclass
class Program:
name: str
path: str
category: Optional[str] = None
requires_admin: bool = False
def __post_init__(self):
if not self.name:
raise ValueError("Program name cannot be empty")
if not self.path:
raise ValueError("Program path cannot be empty")
def to_dict(self) -> dict:
result = {
"Name": self.name,
"Pfad": self.path
}
if self.category:
result["Kategorie"] = self.category
if self.requires_admin:
result["Adminrechte"] = True
return result
@classmethod
def from_dict(cls, data: dict) -> 'Program':
return cls(
name=data["Name"],
path=data["Pfad"],
category=data.get("Kategorie"),
requires_admin=data.get("Adminrechte", False)
)
def __str__(self) -> str:
return f"{self.name} ({self.category or 'Ohne Kategorie'})"

1025
run.py

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,2 +1,2 @@
@echo off
start "" /b pythonw.exe run.py
start "" /b pythonw.exe main.py

0
utils/__init__.py Normal file
View File

94
utils/icon_utils.py Normal file
View File

@@ -0,0 +1,94 @@
import sys
import os
from typing import Optional, Dict, Any
try:
import win32ui
import win32gui
import win32con
import win32api
from PIL import Image, ImageTk
ICON_SUPPORT = True
except ImportError:
print("Warnung: Die Bibliotheken für Programm-Icons fehlen. Installieren Sie diese mit: pip install pywin32 Pillow")
ICON_SUPPORT = False
class IconManager:
def __init__(self):
self.icon_cache: Dict[str, Any] = {}
self.icon_support = ICON_SUPPORT
def resource_path(self, relative_path: str) -> str:
"""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(self, exe_path: str) -> Optional[Any]:
"""Extrahiert das Icon aus einer EXE-Datei"""
if not self.icon_support:
return None
try:
# Icon-Handle erhalten
large, small = win32gui.ExtractIconEx(exe_path, 0)
# Wir verwenden das größere Icon
if large:
# Wir nehmen das erste Icon
ico_x = win32api.GetSystemMetrics(win32con.SM_CXICON)
ico_y = win32api.GetSystemMetrics(win32con.SM_CYICON)
# Icon in DC (Device Context) zeichnen
hdc = win32ui.CreateDCFromHandle(win32gui.GetDC(0))
hbmp = win32ui.CreateBitmap()
hbmp.CreateCompatibleBitmap(hdc, ico_x, ico_y)
hdc = hdc.CreateCompatibleDC()
hdc.SelectObject(hbmp)
hdc.DrawIcon((0, 0), large[0])
# Bitmap in ein Python-Image-Objekt umwandeln
bmpinfo = hbmp.GetInfo()
bmpstr = hbmp.GetBitmapBits(True)
img = Image.frombuffer(
'RGBA',
(bmpinfo['bmWidth'], bmpinfo['bmHeight']),
bmpstr, 'raw', 'BGRA', 0, 1
)
# Aufräumen
win32gui.DestroyIcon(large[0])
for icon in small:
if icon:
win32gui.DestroyIcon(icon)
# Bild auf die gewünschte Größe skalieren
img = img.resize((16, 16), Image.LANCZOS)
# In PhotoImage umwandeln für Tkinter
return ImageTk.PhotoImage(img)
return None
except Exception as e:
print(f"Fehler beim Extrahieren des Icons: {str(e)}")
return None
def get_icon(self, program_path: str) -> Optional[Any]:
"""Holt ein Icon aus dem Cache oder extrahiert es neu"""
if not self.icon_support:
return None
if program_path in self.icon_cache:
return self.icon_cache[program_path]
icon = self.extract_icon_from_exe(program_path)
self.icon_cache[program_path] = icon
return icon
def clear_cache(self):
"""Leert den Icon-Cache"""
self.icon_cache.clear()