830 lines
34 KiB
Python
830 lines
34 KiB
Python
#!/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()
|