Kompletter Rewrite
This commit is contained in:
45
.gitignore
vendored
45
.gitignore
vendored
@@ -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
119
README.md
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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
165
config.py
Normal 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
0
data/__init__.py
Normal file
153
data/data_manager.py
Normal file
153
data/data_manager.py
Normal 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
0
gui/__init__.py
Normal file
351
gui/dialogs.py
Normal file
351
gui/dialogs.py
Normal 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
396
gui/main_window.py
Normal 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
288
gui/modern_widgets.py
Normal 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
32
main.py
Normal 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
0
models/__init__.py
Normal file
31
models/category.py
Normal file
31
models/category.py
Normal 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
39
models/program.py
Normal 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'})"
|
||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
@@ -1,2 +1,2 @@
|
||||
@echo off
|
||||
start "" /b pythonw.exe run.py
|
||||
start "" /b pythonw.exe main.py
|
||||
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
94
utils/icon_utils.py
Normal file
94
utils/icon_utils.py
Normal 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()
|
||||
Reference in New Issue
Block a user