Programme/csv_processor/csv_processor_gui.py

830 lines
34 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()