#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ CSV-Processor GUI v2.0: Grafische Oberfläche für CSV/Excel-Verarbeitung """ import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext, simpledialog import csv import json import os from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Tuple # Optional: Excel Support try: import openpyxl from openpyxl.styles import Font, PatternFill EXCEL_SUPPORT = True except ImportError: EXCEL_SUPPORT = False try: from odf.opendocument import OpenDocumentSpreadsheet from odf.table import Table, TableRow, TableCell from odf.text import P ODT_SUPPORT = True except ImportError: ODT_SUPPORT = False class CSVProcessorGUI: def __init__(self, root): self.root = root self.root.title("CSV-Processor v2.0") self.root.geometry("900x700") self.config_dir = Path("csv_processor_config") self.config_dir.mkdir(exist_ok=True) self.current_config = {} # Für temporäre Voreinstellungs-Daten self.headers = [] self.data = [] self.original_names = {} self.selected_columns = set() self.setup_ui() def setup_ui(self): """Hauptoberfläche erstellen""" # Menüleiste menubar = tk.Menu(self.root) self.root.config(menu=menubar) # Datei-Menü file_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Datei", menu=file_menu) file_menu.add_command(label="Datei öffnen", command=self.load_file) file_menu.add_command(label="Verarbeiten & Speichern", command=self.process_and_save) file_menu.add_separator() file_menu.add_command(label="Beenden", command=self.root.quit) # Voreinstellungen-Menü preset_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Voreinstellungen", menu=preset_menu) preset_menu.add_command(label="Laden", command=self.load_preset) preset_menu.add_command(label="Speichern", command=self.save_preset) preset_menu.add_command(label="Verwalten", command=self.manage_presets) # Hilfe-Menü help_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Hilfe", menu=help_menu) help_menu.add_command(label="Über", command=self.show_about) # Hauptcontainer main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) main_frame.rowconfigure(4, weight=1) # Datei-Auswahl file_frame = ttk.LabelFrame(main_frame, text="Quelldatei", padding="10") file_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5) file_frame.columnconfigure(1, weight=1) ttk.Label(file_frame, text="Datei:").grid(row=0, column=0, sticky=tk.W) self.file_entry = ttk.Entry(file_frame, width=50) self.file_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5) ttk.Button(file_frame, text="Durchsuchen", command=self.browse_file).grid(row=0, column=2) ttk.Button(file_frame, text="Laden", command=self.load_file).grid(row=0, column=3, padx=5) # Einstellungen settings_frame = ttk.LabelFrame(main_frame, text="Verarbeitungseinstellungen", padding="10") settings_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5) settings_frame.columnconfigure(1, weight=1) row = 0 self.has_header_var = tk.BooleanVar(value=True) ttk.Checkbutton(settings_frame, text="Datei hat Kopfzeile", variable=self.has_header_var).grid(row=row, column=0, sticky=tk.W, columnspan=2) row += 1 self.remove_empty_rows_var = tk.BooleanVar(value=True) ttk.Checkbutton(settings_frame, text="Leere Zeilen entfernen", variable=self.remove_empty_rows_var).grid(row=row, column=0, sticky=tk.W, columnspan=2) row += 1 self.remove_empty_cols_var = tk.BooleanVar(value=True) ttk.Checkbutton(settings_frame, text="Leere Spalten entfernen (automatisch vor Spaltenauswahl)", variable=self.remove_empty_cols_var).grid(row=row, column=0, sticky=tk.W, columnspan=3) # Info-Label info_label = ttk.Label(settings_frame, text="ℹ Leere Spalten werden vor der Spaltenauswahl entfernt", foreground="blue", font=('TkDefaultFont', 8)) info_label.grid(row=row+1, column=0, columnspan=3, sticky=tk.W, pady=(0,5)) row += 2 ttk.Label(settings_frame, text="Mapping-Datei:").grid(row=row, column=0, sticky=tk.W) self.mapping_entry = ttk.Entry(settings_frame) self.mapping_entry.grid(row=row, column=1, sticky=(tk.W, tk.E), padx=5) ttk.Button(settings_frame, text="...", command=self.browse_mapping, width=3).grid(row=row, column=2) row += 1 ttk.Label(settings_frame, text="Ausgabeformat:").grid(row=row, column=0, sticky=tk.W) self.format_var = tk.StringVar(value="csv") format_frame = ttk.Frame(settings_frame) format_frame.grid(row=row, column=1, sticky=tk.W, padx=5) ttk.Radiobutton(format_frame, text="CSV", variable=self.format_var, value="csv").pack(side=tk.LEFT) if EXCEL_SUPPORT: ttk.Radiobutton(format_frame, text="Excel", variable=self.format_var, value="xlsx").pack(side=tk.LEFT, padx=10) if ODT_SUPPORT: ttk.Radiobutton(format_frame, text="ODS", variable=self.format_var, value="ods").pack(side=tk.LEFT) # Spaltenauswahl columns_frame = ttk.LabelFrame(main_frame, text="Spaltenauswahl", padding="10") columns_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5) button_frame = ttk.Frame(columns_frame) button_frame.pack(fill=tk.X) ttk.Button(button_frame, text="Alle auswählen", command=self.select_all_columns).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="Alle abwählen", command=self.deselect_all_columns).pack(side=tk.LEFT) ttk.Button(button_frame, text="Auswahl umkehren", command=self.invert_selection).pack(side=tk.LEFT, padx=5) # Spalten-Liste mit Checkboxen list_frame = ttk.Frame(columns_frame) list_frame.pack(fill=tk.BOTH, expand=True, pady=5) scrollbar = ttk.Scrollbar(list_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.columns_listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE, yscrollcommand=scrollbar.set, height=10) self.columns_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.config(command=self.columns_listbox.yview) # Vorschau preview_frame = ttk.LabelFrame(main_frame, text="Datenvorschau", padding="10") preview_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=5) self.info_label = ttk.Label(preview_frame, text="Keine Datei geladen") self.info_label.pack() # Status und Log log_frame = ttk.LabelFrame(main_frame, text="Status", padding="10") log_frame.grid(row=4, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state='disabled') self.log_text.pack(fill=tk.BOTH, expand=True) # Aktionsbuttons action_frame = ttk.Frame(main_frame) action_frame.grid(row=5, column=0, sticky=(tk.W, tk.E), pady=10) ttk.Button(action_frame, text="Verarbeiten & Speichern", command=self.process_and_save).pack(side=tk.RIGHT, padx=5) ttk.Button(action_frame, text="Vorschau", command=self.show_preview).pack(side=tk.RIGHT) self.log("CSV-Processor GUI v2.0 gestartet") self.log(f"Excel-Support: {'Ja' if EXCEL_SUPPORT else 'Nein'}") self.log(f"ODS-Support: {'Ja' if ODT_SUPPORT else 'Nein'}") def log(self, message): """Nachricht im Log anzeigen""" self.log_text.config(state='normal') self.log_text.insert(tk.END, f"{datetime.now().strftime('%H:%M:%S')} - {message}\n") self.log_text.see(tk.END) self.log_text.config(state='disabled') def browse_file(self): """Datei zum Öffnen auswählen""" filetypes = [ ("Alle unterstützten Dateien", "*.csv *.xlsx *.xls"), ("CSV Dateien", "*.csv"), ] if EXCEL_SUPPORT: filetypes.append(("Excel Dateien", "*.xlsx *.xls")) filetypes.append(("Alle Dateien", "*.*")) filename = filedialog.askopenfilename( title="Quelldatei auswählen", filetypes=filetypes ) if filename: self.file_entry.delete(0, tk.END) self.file_entry.insert(0, filename) def browse_mapping(self): """Mapping-Datei auswählen""" filename = filedialog.askopenfilename( title="Mapping-Datei auswählen", filetypes=[("JSON Dateien", "*.json"), ("Alle Dateien", "*.*")] ) if filename: self.mapping_entry.delete(0, tk.END) self.mapping_entry.insert(0, filename) def load_file(self): """Datei laden""" filepath = self.file_entry.get() if not filepath: messagebox.showwarning("Warnung", "Bitte wählen Sie zuerst eine Datei aus.") return path = Path(filepath) if not path.exists(): messagebox.showerror("Fehler", f"Datei nicht gefunden: {filepath}") return try: self.log(f"Lade Datei: {path.name}") # Datei laden file_ext = path.suffix.lower() if file_ext in ['.xlsx', '.xls']: self.headers, self.data = self.read_excel(path) else: self.headers, self.data = self.read_csv(path) # Mapping anwenden mapping_file = self.mapping_entry.get() if mapping_file and Path(mapping_file).exists(): mappings = self.load_column_mappings(Path(mapping_file)) self.headers, self.data, self.original_names = self.apply_column_mappings( self.headers, self.data, mappings ) self.log(f"Mapping angewendet: {len(mappings)} Spalten umbenannt") else: self.original_names = {h: h for h in self.headers} # Spalten in Liste anzeigen self.update_columns_list() # Wenn Voreinstellung mit Spaltenauswahl geladen wurde, anwenden if hasattr(self, 'current_config') and 'selected_columns' in self.current_config: self.apply_column_selection_from_preset(self.current_config['selected_columns']) # Config zurücksetzen nach Anwendung self.current_config = {} # Info aktualisieren self.info_label.config(text=f"Geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen") self.log(f"Erfolgreich geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen") # Warnung anzeigen wenn "Leere Spalten entfernen" aktiv ist if self.remove_empty_cols_var.get(): self.log("ℹ Leere Spalten werden beim Verarbeiten automatisch entfernt") except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Laden: {str(e)}") self.log(f"FEHLER: {str(e)}") def read_csv(self, filepath: Path) -> Tuple[List[str], List[Dict]]: """CSV-Datei lesen""" encodings = ['utf-8', 'latin-1', 'cp1252'] for encoding in encodings: try: with open(filepath, 'r', encoding=encoding) as f: first_line = f.readline() f.seek(0) sniffer = csv.Sniffer() delimiter = sniffer.sniff(first_line).delimiter if self.has_header_var.get(): reader = csv.DictReader(f, delimiter=delimiter) headers = list(reader.fieldnames) data = list(reader) else: reader = csv.reader(f, delimiter=delimiter) rows = list(reader) if rows: headers = [f"Spalte_{i+1}" for i in range(len(rows[0]))] data = [] for row in rows: row_dict = {headers[i]: row[i] if i < len(row) else '' for i in range(len(headers))} data.append(row_dict) else: headers = [] data = [] return headers, data except UnicodeDecodeError: continue except Exception: continue raise Exception(f"Konnte Datei {filepath} nicht lesen") def read_excel(self, filepath: Path) -> Tuple[List[str], List[Dict]]: """Excel-Datei lesen""" if not EXCEL_SUPPORT: raise Exception("Excel-Support nicht verfügbar. Installieren Sie: pip install openpyxl") wb = openpyxl.load_workbook(filepath, data_only=True) ws = wb.active data = [] headers = None for i, row in enumerate(ws.iter_rows(values_only=True)): if i == 0 and self.has_header_var.get(): headers = [str(cell) if cell is not None else f"Spalte_{j+1}" for j, cell in enumerate(row)] else: if headers is None: headers = [f"Spalte_{j+1}" for j in range(len(row))] row_dict = {} for j, cell in enumerate(row): if j < len(headers): row_dict[headers[j]] = str(cell) if cell is not None else '' data.append(row_dict) return headers, data def load_column_mappings(self, mapping_file: Path) -> Dict[str, str]: """Spaltennamen-Zuordnungen aus JSON laden""" try: with open(mapping_file, 'r', encoding='utf-8') as f: return json.load(f) except: return {} def apply_column_mappings(self, headers: List[str], data: List[Dict], mappings: Dict[str, str]) -> Tuple[List[str], List[Dict], Dict[str, str]]: """Spaltennamen umbenennen""" new_headers = [mappings.get(h, h) for h in headers] original_names = {new_h: old_h for old_h, new_h in zip(headers, new_headers)} new_data = [] for row in data: new_row = {} for old_h, new_h in zip(headers, new_headers): new_row[new_h] = row.get(old_h, '') new_data.append(new_row) return new_headers, new_data, original_names def update_columns_list(self): """Spaltenliste aktualisieren""" self.columns_listbox.delete(0, tk.END) self.selected_columns = set(self.headers) # Alle initial ausgewählt for i, header in enumerate(self.headers): orig = self.original_names.get(header, header) if orig != header: display = f"☑ {orig} → {header}" else: display = f"☑ {header}" self.columns_listbox.insert(tk.END, display) self.columns_listbox.selection_set(i) def select_all_columns(self): """Alle Spalten auswählen""" self.columns_listbox.selection_set(0, tk.END) self.selected_columns = set(self.headers) def deselect_all_columns(self): """Alle Spalten abwählen""" self.columns_listbox.selection_clear(0, tk.END) self.selected_columns = set() def invert_selection(self): """Auswahl umkehren""" for i in range(len(self.headers)): if self.columns_listbox.selection_includes(i): self.columns_listbox.selection_clear(i) else: self.columns_listbox.selection_set(i) def get_selected_columns(self) -> List[str]: """Aktuell ausgewählte Spalten ermitteln""" selected_indices = self.columns_listbox.curselection() return [self.headers[i] for i in selected_indices] def process_data(self) -> Tuple[List[str], List[Dict]]: """Daten verarbeiten""" if not self.headers or not self.data: raise Exception("Keine Daten geladen") headers = self.headers[:] data = self.data[:] # 1. Leere Zeilen entfernen if self.remove_empty_rows_var.get(): original_count = len(data) data = [row for row in data if any(str(v).strip() for v in row.values())] removed = original_count - len(data) if removed > 0: self.log(f"Leere Zeilen entfernt: {removed}") # 2. Leere Spalten entfernen (VOR der Spaltenauswahl!) if self.remove_empty_cols_var.get(): non_empty_headers = [] for header in headers: if any(row.get(header, '').strip() for row in data): non_empty_headers.append(header) removed = len(headers) - len(non_empty_headers) if removed > 0: self.log(f"Leere Spalten entfernt: {removed}") # Zeige welche Spalten entfernt wurden removed_cols = [h for h in headers if h not in non_empty_headers] for col in removed_cols[:5]: orig = self.original_names.get(col, col) if orig != col: self.log(f" - {orig} → {col} (leer)") else: self.log(f" - {col} (leer)") if len(removed_cols) > 5: self.log(f" ... und {len(removed_cols)-5} weitere") headers = non_empty_headers data = [{h: row.get(h, '') for h in headers} for row in data] # 3. Spaltenauswahl anwenden (arbeitet mit nicht-leeren Spalten) selected = self.get_selected_columns() # Nur Spalten verwenden, die noch existieren (nicht leer) UND ausgewählt sind final_columns = [h for h in headers if h in selected] if not final_columns: raise Exception("Keine Spalten ausgewählt oder alle ausgewählten Spalten sind leer") # Zeige Info über übersprungene Spalten skipped_empty = [h for h in selected if h not in headers] skipped_deselected = [h for h in headers if h not in selected] if skipped_empty: self.log(f"Übersprungene Spalten (leer): {len(skipped_empty)}") if skipped_deselected: self.log(f"Abgewählte Spalten (rot): {len(skipped_deselected)}") headers = final_columns data = [{h: row.get(h, '') for h in headers} for row in data] self.log(f"Finale Verarbeitung: {len(headers)} Spalten, {len(data)} Zeilen") return headers, data def show_preview(self): """Vorschau der verarbeiteten Daten""" try: headers, data = self.process_data() preview_window = tk.Toplevel(self.root) preview_window.title("Datenvorschau") preview_window.geometry("800x400") frame = ttk.Frame(preview_window, padding="10") frame.pack(fill=tk.BOTH, expand=True) # Treeview für Tabelle tree = ttk.Treeview(frame, columns=headers, show='headings') for col in headers: tree.heading(col, text=col) tree.column(col, width=100) for row in data[:100]: # Max 100 Zeilen values = [row.get(col, '') for col in headers] tree.insert('', tk.END, values=values) scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=tree.yview) tree.configure(yscrollcommand=scrollbar.set) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) info_label = ttk.Label(preview_window, text=f"Zeige erste {min(len(data), 100)} von {len(data)} Zeilen") info_label.pack(pady=5) except Exception as e: messagebox.showerror("Fehler", f"Fehler bei Vorschau: {str(e)}") def process_and_save(self): """Daten verarbeiten und speichern""" try: headers, data = self.process_data() # Zieldatei wählen output_format = self.format_var.get() filetypes = [] if output_format == 'csv': filetypes = [("CSV Dateien", "*.csv")] elif output_format == 'xlsx': filetypes = [("Excel Dateien", "*.xlsx")] elif output_format == 'ods': filetypes = [("ODS Dateien", "*.ods")] filetypes.append(("Alle Dateien", "*.*")) filename = filedialog.asksaveasfilename( title="Datei speichern", defaultextension=f".{output_format}", filetypes=filetypes ) if not filename: return # Speichern target_file = Path(filename) if output_format == 'csv': self.write_csv(target_file, headers, data) elif output_format == 'xlsx': self.write_excel(target_file, headers, data) elif output_format == 'ods': self.write_ods(target_file, headers, data) self.log(f"Datei gespeichert: {target_file.name}") messagebox.showinfo("Erfolg", f"Datei erfolgreich gespeichert:\n{target_file}") except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Speichern: {str(e)}") self.log(f"FEHLER: {str(e)}") def write_csv(self, filepath: Path, headers: List[str], data: List[Dict]): """CSV-Datei schreiben""" with open(filepath, 'w', encoding='utf-8', newline='') as f: writer = csv.DictWriter(f, fieldnames=headers, delimiter=';') writer.writeheader() writer.writerows(data) def write_excel(self, filepath: Path, headers: List[str], data: List[Dict]): """Excel-Datei schreiben""" if not EXCEL_SUPPORT: raise Exception("Excel-Support nicht verfügbar") wb = openpyxl.Workbook() ws = wb.active ws.title = "Daten" # Kopfzeile for col_num, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_num, value=header) cell.font = Font(bold=True) cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid") # Daten for row_num, row_data in enumerate(data, 2): for col_num, header in enumerate(headers, 1): ws.cell(row=row_num, column=col_num, value=row_data.get(header, '')) wb.save(filepath) def write_ods(self, filepath: Path, headers: List[str], data: List[Dict]): """ODS-Datei schreiben""" if not ODT_SUPPORT: raise Exception("ODS-Support nicht verfügbar") doc = OpenDocumentSpreadsheet() table = Table(name="Daten") # Kopfzeile row = TableRow() for header in headers: cell = TableCell() cell.addElement(P(text=header)) row.addElement(cell) table.addElement(row) # Daten for row_data in data: row = TableRow() for header in headers: cell = TableCell() cell.addElement(P(text=str(row_data.get(header, '')))) row.addElement(cell) table.addElement(row) doc.spreadsheet.addElement(table) doc.save(filepath) def load_preset(self): """Voreinstellung laden""" presets = [f.stem for f in self.config_dir.glob("*.json")] if not presets: messagebox.showinfo("Info", "Keine Voreinstellungen vorhanden.") return dialog = PresetDialog(self.root, "Voreinstellung laden", presets) if dialog.result: preset_file = self.config_dir / f"{dialog.result}.json" try: with open(preset_file, 'r', encoding='utf-8') as f: config = json.load(f) # Einstellungen anwenden if 'has_header' in config: self.has_header_var.set(config['has_header']) if 'remove_empty_rows' in config: self.remove_empty_rows_var.set(config['remove_empty_rows']) if 'remove_empty' in config: self.remove_empty_cols_var.set(config['remove_empty']) # Mapping-Datei vorbelegen if 'mapping_file' in config and config['mapping_file']: self.mapping_entry.delete(0, tk.END) self.mapping_entry.insert(0, config['mapping_file']) self.log(f"Mapping-Datei vorbelegt: {config['mapping_file']}") if 'output_format' in config: self.format_var.set(config['output_format']) # Quelldatei vorbelegen (falls gespeichert) if 'source_file' in config and config['source_file']: source_path = Path(config['source_file']) if source_path.exists(): self.file_entry.delete(0, tk.END) self.file_entry.insert(0, config['source_file']) self.log(f"Quelldatei vorbelegt: {source_path.name}") # Datei automatisch laden self.load_file() # Spaltenauswahl anwenden (nach dem Laden!) if 'selected_columns' in config and self.headers: self.apply_column_selection_from_preset(config['selected_columns']) else: self.log(f"⚠ Gespeicherte Quelldatei nicht gefunden: {config['source_file']}") elif 'selected_columns' in config: # Keine Quelldatei, aber Spaltenauswahl gespeichert # Wird angewendet sobald Datei geladen wird self.current_config = config self.log(f"Spaltenauswahl wird angewendet sobald Datei geladen wird") self.log(f"Voreinstellung geladen: {dialog.result}") messagebox.showinfo("Erfolg", f"Voreinstellung '{dialog.result}' geladen.\n\n" "Hinweis: Leere Spalten werden beim Verarbeiten\n" "automatisch übersprungen (vor Spaltenauswahl).") except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Laden: {str(e)}") def apply_column_selection_from_preset(self, selected_columns: List[str]): """Spaltenauswahl aus Voreinstellung anwenden""" saved_selection = set(selected_columns) # Alle abwählen self.columns_listbox.selection_clear(0, tk.END) # Nur gespeicherte Spalten auswählen (die existieren) selected_count = 0 for i, header in enumerate(self.headers): if header in saved_selection: self.columns_listbox.selection_set(i) selected_count += 1 skipped_count = len(saved_selection) - selected_count self.log(f"Spaltenauswahl angewendet: {selected_count} Spalten ausgewählt") if skipped_count > 0: self.log(f" ⚠ {skipped_count} Spalten nicht verfügbar (leer oder nicht vorhanden)") # Info-Text anzeigen self.info_label.config( text=f"Geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen | " f"Ausgewählt: {selected_count} Spalten" ) def save_preset(self): """Voreinstellung speichern""" name = simpledialog.askstring("Voreinstellung speichern", "Name der Voreinstellung:") if not name: return # Fragen ob Quelldatei mitgespeichert werden soll save_source = False if self.file_entry.get(): save_source = messagebox.askyesno( "Quelldatei speichern?", "Möchten Sie den Pfad zur Quelldatei in der Voreinstellung speichern?\n\n" "Ja = Datei wird beim Laden der Voreinstellung automatisch geladen\n" "Nein = Nur Einstellungen werden gespeichert" ) config = { 'has_header': self.has_header_var.get(), 'remove_empty_rows': self.remove_empty_rows_var.get(), 'remove_empty': self.remove_empty_cols_var.get(), 'mapping_file': self.mapping_entry.get(), 'output_format': self.format_var.get(), 'selected_columns': self.get_selected_columns() } # Quelldatei optional speichern if save_source: config['source_file'] = self.file_entry.get() preset_file = self.config_dir / f"{name}.json" try: with open(preset_file, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2, ensure_ascii=False) info_text = f"Voreinstellung '{name}' gespeichert.\n\n" info_text += f"Enthält:\n" info_text += f"- Einstellungen: Ja\n" info_text += f"- Mapping: {'Ja' if config['mapping_file'] else 'Nein'}\n" info_text += f"- Quelldatei: {'Ja' if save_source else 'Nein'}\n" info_text += f"- Spaltenauswahl: {len(config['selected_columns'])} Spalten" self.log(f"Voreinstellung gespeichert: {name}") messagebox.showinfo("Erfolg", info_text) except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Speichern: {str(e)}") def manage_presets(self): """Voreinstellungen verwalten""" PresetManagerWindow(self.root, self.config_dir) def show_about(self): """Über-Dialog""" messagebox.showinfo( "Über CSV-Processor", "CSV-Processor v2.0\n\n" "Professionelle CSV/Excel-Verarbeitung\n\n" f"Excel-Support: {'Ja' if EXCEL_SUPPORT else 'Nein'}\n" f"ODS-Support: {'Ja' if ODT_SUPPORT else 'Nein'}" ) class PresetDialog(simpledialog.Dialog): """Dialog zur Auswahl einer Voreinstellung""" def __init__(self, parent, title, presets): self.presets = presets self.result = None super().__init__(parent, title) def body(self, master): tk.Label(master, text="Voreinstellung auswählen:").pack(pady=5) self.listbox = tk.Listbox(master) for preset in self.presets: self.listbox.insert(tk.END, preset) self.listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) return self.listbox def apply(self): selection = self.listbox.curselection() if selection: self.result = self.presets[selection[0]] class PresetManagerWindow: """Fenster zur Verwaltung von Voreinstellungen""" def __init__(self, parent, config_dir): self.config_dir = config_dir self.window = tk.Toplevel(parent) self.window.title("Voreinstellungen verwalten") self.window.geometry("400x300") frame = ttk.Frame(self.window, padding="10") frame.pack(fill=tk.BOTH, expand=True) tk.Label(frame, text="Voreinstellungen:").pack(anchor=tk.W) self.listbox = tk.Listbox(frame) self.listbox.pack(fill=tk.BOTH, expand=True, pady=5) button_frame = ttk.Frame(frame) button_frame.pack(fill=tk.X) ttk.Button(button_frame, text="Löschen", command=self.delete_preset).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="Schließen", command=self.window.destroy).pack(side=tk.RIGHT) self.refresh_list() def refresh_list(self): """Liste aktualisieren""" self.listbox.delete(0, tk.END) for preset in self.config_dir.glob("*.json"): self.listbox.insert(tk.END, preset.stem) def delete_preset(self): """Voreinstellung löschen""" selection = self.listbox.curselection() if not selection: messagebox.showwarning("Warnung", "Bitte wählen Sie eine Voreinstellung aus.") return preset_name = self.listbox.get(selection[0]) if messagebox.askyesno("Bestätigung", f"Voreinstellung '{preset_name}' wirklich löschen?"): preset_file = self.config_dir / f"{preset_name}.json" preset_file.unlink() self.refresh_list() messagebox.showinfo("Erfolg", f"Voreinstellung '{preset_name}' gelöscht.") def main(): root = tk.Tk() app = CSVProcessorGUI(root) root.mainloop() if __name__ == "__main__": main()