Files
PANDA/Program.cs
T

1251 lines
52 KiB
C#
Raw 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.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
[assembly: AssemblyTitle("PANDA")]
[assembly: AssemblyDescription("Pseudonymisierung alphanumerischer Nutzdaten durch Alphabetverschiebung")]
[assembly: AssemblyProduct("PANDA")]
[assembly: AssemblyCompany("PANDA")]
[assembly: AssemblyVersion("1.3.0.0")]
[assembly: AssemblyFileVersion("1.3.0.0")]
namespace Panda
{
internal static class Program
{
[STAThread]
private static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (args.Length == 2 && string.Equals(args[0], "--screenshot", StringComparison.OrdinalIgnoreCase))
{
using (var form = new MainForm())
{
form.Size = new Size(1320, 820);
form.Show();
form.LoadPreviewData();
Application.DoEvents();
using (var bitmap = new Bitmap(form.Width, form.Height))
{
form.DrawToBitmap(bitmap, new Rectangle(Point.Empty, form.Size));
bitmap.Save(args[1], System.Drawing.Imaging.ImageFormat.Png);
}
form.Close();
}
return;
}
if (args.Length == 3 && string.Equals(args[0], "--wizard-screenshot", StringComparison.OrdinalIgnoreCase))
{
using (var wizard = new ImportWizardForm(args[1]))
{
wizard.Show();
Application.DoEvents();
wizard.UncheckLastColumnForPreview();
Application.DoEvents();
using (var bitmap = new Bitmap(wizard.Width, wizard.Height))
{
wizard.DrawToBitmap(bitmap, new Rectangle(Point.Empty, wizard.Size));
bitmap.Save(args[2], System.Drawing.Imaging.ImageFormat.Png);
}
wizard.Close();
}
return;
}
Application.Run(new MainForm());
}
}
internal sealed class CsvDocument
{
public List<string> Headers = new List<string>();
public List<List<string>> Rows = new List<List<string>>();
public char Delimiter;
public bool FirstRowIsHeader;
}
internal static class CsvCodec
{
public static CsvDocument Load(string path, bool firstRowIsHeader)
{
string text;
using (var reader = new StreamReader(path, Encoding.UTF8, true))
text = reader.ReadToEnd();
char delimiter = DetectDelimiter(text);
var records = Parse(text, delimiter);
var document = new CsvDocument
{
Delimiter = delimiter,
FirstRowIsHeader = firstRowIsHeader
};
int columnCount = records.Count == 0 ? 0 : records.Max(row => row.Count);
if (firstRowIsHeader && records.Count > 0)
{
for (int column = 0; column < columnCount; column++)
{
string value = column < records[0].Count ? records[0][column] : string.Empty;
document.Headers.Add(string.IsNullOrWhiteSpace(value) ? "Spalte " + (column + 1) : value);
}
records.RemoveAt(0);
}
else
{
for (int column = 0; column < columnCount; column++)
document.Headers.Add("Spalte " + (column + 1));
}
foreach (var record in records)
{
while (record.Count < columnCount)
record.Add(string.Empty);
document.Rows.Add(record);
}
return document;
}
public static void Save(string path, CsvDocument document, IList<IList<string>> rows)
{
using (var writer = new StreamWriter(path, false, new UTF8Encoding(true)))
{
if (document.FirstRowIsHeader)
WriteRecord(writer, document.Headers, document.Delimiter);
foreach (var row in rows)
WriteRecord(writer, row, document.Delimiter);
}
}
public static CsvDocument SelectColumns(CsvDocument source, IList<int> selectedColumns)
{
var result = new CsvDocument
{
Delimiter = source.Delimiter,
FirstRowIsHeader = source.FirstRowIsHeader
};
foreach (int column in selectedColumns)
{
if (column < 0 || column >= source.Headers.Count)
throw new ArgumentOutOfRangeException("selectedColumns");
result.Headers.Add(source.Headers[column]);
}
foreach (var sourceRow in source.Rows)
{
var row = new List<string>();
foreach (int column in selectedColumns)
row.Add(column < sourceRow.Count ? sourceRow[column] : string.Empty);
result.Rows.Add(row);
}
return result;
}
private static void WriteRecord(TextWriter writer, IEnumerable<string> values, char delimiter)
{
writer.WriteLine(string.Join(delimiter.ToString(), values.Select(value => Escape(value ?? string.Empty, delimiter))));
}
private static string Escape(string value, char delimiter)
{
if (value.IndexOfAny(new[] { delimiter, '"', '\r', '\n' }) < 0)
return value;
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
internal static char DetectDelimiter(string text)
{
string firstRecord = GetFirstLogicalRecord(text);
char[] candidates = { ';', ',', '\t' };
char best = ';';
int bestCount = -1;
foreach (char candidate in candidates)
{
int count = CountOutsideQuotes(firstRecord, candidate);
if (count > bestCount)
{
bestCount = count;
best = candidate;
}
}
return best;
}
private static string GetFirstLogicalRecord(string text)
{
var builder = new StringBuilder();
bool quoted = false;
for (int index = 0; index < text.Length; index++)
{
char current = text[index];
if (current == '"')
{
if (quoted && index + 1 < text.Length && text[index + 1] == '"')
{
builder.Append("\"\"");
index++;
continue;
}
quoted = !quoted;
}
if (!quoted && (current == '\r' || current == '\n'))
break;
builder.Append(current);
}
return builder.ToString();
}
private static int CountOutsideQuotes(string text, char delimiter)
{
bool quoted = false;
int count = 0;
for (int index = 0; index < text.Length; index++)
{
if (text[index] == '"')
{
if (quoted && index + 1 < text.Length && text[index + 1] == '"')
{
index++;
continue;
}
quoted = !quoted;
}
else if (!quoted && text[index] == delimiter)
{
count++;
}
}
return count;
}
internal static List<List<string>> Parse(string text, char delimiter)
{
var rows = new List<List<string>>();
var row = new List<string>();
var field = new StringBuilder();
bool quoted = false;
for (int index = 0; index < text.Length; index++)
{
char current = text[index];
if (quoted)
{
if (current == '"')
{
if (index + 1 < text.Length && text[index + 1] == '"')
{
field.Append('"');
index++;
}
else
{
quoted = false;
}
}
else
{
field.Append(current);
}
}
else if (current == '"' && field.Length == 0)
{
quoted = true;
}
else if (current == delimiter)
{
row.Add(field.ToString());
field.Clear();
}
else if (current == '\r' || current == '\n')
{
if (current == '\r' && index + 1 < text.Length && text[index + 1] == '\n')
index++;
row.Add(field.ToString());
field.Clear();
rows.Add(row);
row = new List<string>();
}
else
{
field.Append(current);
}
}
if (field.Length > 0 || row.Count > 0)
{
row.Add(field.ToString());
rows.Add(row);
}
return rows;
}
}
internal static class LetterShifter
{
public static string Shift(string value, int amount)
{
if (string.IsNullOrEmpty(value) || amount == 0)
return value;
var result = new StringBuilder(value.Length);
foreach (char character in value)
{
if (character >= 'A' && character <= 'Z')
result.Append(ShiftInRange(character, 'A', 26, amount));
else if (character >= 'a' && character <= 'z')
result.Append(ShiftInRange(character, 'a', 26, amount));
else
result.Append(character);
}
return result.ToString();
}
private static char ShiftInRange(char value, char start, int length, int amount)
{
int shifted = ((value - start + amount) % length + length) % length;
return (char)(start + shifted);
}
}
internal sealed class ImportWizardForm : Form
{
private readonly string csvPath;
private readonly Color Navy = Color.FromArgb(24, 38, 58);
private readonly Color Blue = Color.FromArgb(41, 112, 255);
private readonly Color Background = Color.FromArgb(244, 247, 251);
private readonly Color Muted = Color.FromArgb(94, 108, 128);
private readonly CheckBox headerCheckBox = new CheckBox();
private readonly Label formatLabel = new Label();
private readonly Label selectionLabel = new Label();
private readonly CheckedListBox columnList = new CheckedListBox();
private readonly DataGridView previewGrid = new DataGridView();
private readonly Button importButton = new Button();
private CsvDocument loadedDocument;
public CsvDocument SelectedDocument { get; private set; }
public ImportWizardForm(string path)
{
csvPath = path;
Text = "PANDA Import-Assistent";
StartPosition = FormStartPosition.CenterParent;
MinimumSize = new Size(900, 600);
Size = new Size(1040, 680);
BackColor = Background;
Font = new Font("Segoe UI", 9F);
Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath) ?? SystemIcons.Application;
BuildLayout();
ReloadPreview(true);
}
private void BuildLayout()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 4,
Padding = new Padding(22, 18, 22, 18),
BackColor = Background
};
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 72));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 72));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 58));
Controls.Add(root);
var titlePanel = new Panel { Dock = DockStyle.Fill };
titlePanel.Controls.Add(new Label
{
Text = "CSV-Import vorbereiten",
Font = new Font("Segoe UI Semibold", 18F),
ForeColor = Navy,
AutoSize = true,
Location = new Point(0, 0)
});
titlePanel.Controls.Add(new Label
{
Text = "Prüfe das Format und wähle die Spalten aus, die PANDA übernehmen soll.",
ForeColor = Muted,
AutoSize = true,
Location = new Point(2, 39)
});
root.Controls.Add(titlePanel, 0, 0);
var filePanel = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 3,
RowCount = 2,
BackColor = Color.White,
Padding = new Padding(14, 8, 14, 8),
Margin = new Padding(0, 0, 0, 12)
};
filePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 55));
filePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 240));
filePanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 45));
filePanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
filePanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
var stepOne = new Label
{
Text = "1 DATEI UND FORMAT",
Font = new Font("Segoe UI Semibold", 9F),
ForeColor = Blue,
AutoSize = true,
Anchor = AnchorStyles.Left
};
filePanel.Controls.Add(stepOne, 0, 0);
filePanel.SetColumnSpan(stepOne, 3);
filePanel.Controls.Add(new Label
{
Text = Path.GetFileName(csvPath),
ForeColor = Navy,
AutoEllipsis = true,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft
}, 0, 1);
headerCheckBox.Text = "Erste Zeile enthält Überschriften";
headerCheckBox.Checked = true;
headerCheckBox.AutoSize = true;
headerCheckBox.ForeColor = Navy;
headerCheckBox.Anchor = AnchorStyles.Left;
headerCheckBox.CheckedChanged += delegate { ReloadPreview(false); };
filePanel.Controls.Add(headerCheckBox, 1, 1);
formatLabel.ForeColor = Muted;
formatLabel.AutoEllipsis = true;
formatLabel.Dock = DockStyle.Fill;
formatLabel.TextAlign = ContentAlignment.MiddleRight;
filePanel.Controls.Add(formatLabel, 2, 1);
root.Controls.Add(filePanel, 0, 1);
var content = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = 2,
BackColor = Background,
Margin = new Padding(0)
};
content.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 280));
content.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
content.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
content.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
content.Controls.Add(CreateSectionHeader("2 SPALTEN AUSWÄHLEN", false), 0, 0);
content.Controls.Add(CreateSectionHeader("VORSCHAU DER DATEI", true), 1, 0);
root.Controls.Add(content, 0, 2);
var selectionPanel = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = 3,
BackColor = Color.White,
Padding = new Padding(12, 10, 12, 12),
Margin = new Padding(0, 0, 7, 0)
};
selectionPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
selectionPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
selectionPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 30));
selectionPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
selectionPanel.RowStyles.Add(new RowStyle(SizeType.Absolute, 26));
var allButton = CreateLinkButton("Alle auswählen");
allButton.Click += delegate { SetAllColumns(true); };
var noneButton = CreateLinkButton("Auswahl aufheben");
noneButton.Click += delegate { SetAllColumns(false); };
selectionPanel.Controls.Add(allButton, 0, 0);
selectionPanel.Controls.Add(noneButton, 1, 0);
columnList.Dock = DockStyle.Fill;
columnList.BorderStyle = BorderStyle.None;
columnList.CheckOnClick = true;
columnList.ForeColor = Navy;
columnList.BackColor = Color.White;
columnList.HorizontalScrollbar = true;
columnList.ItemCheck += delegate(object sender, ItemCheckEventArgs args)
{
if (args.Index >= 0 && args.Index < previewGrid.Columns.Count)
previewGrid.Columns[args.Index].Visible = args.NewValue == CheckState.Checked;
if (IsHandleCreated)
BeginInvoke(new Action(UpdateSelectionStatus));
};
selectionPanel.SetColumnSpan(columnList, 2);
selectionPanel.Controls.Add(columnList, 0, 1);
selectionLabel.ForeColor = Muted;
selectionLabel.Dock = DockStyle.Fill;
selectionLabel.TextAlign = ContentAlignment.MiddleLeft;
selectionPanel.SetColumnSpan(selectionLabel, 2);
selectionPanel.Controls.Add(selectionLabel, 0, 2);
content.Controls.Add(selectionPanel, 0, 1);
ConfigurePreviewGrid();
content.Controls.Add(previewGrid, 1, 1);
var footer = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 4,
RowCount = 1,
Padding = new Padding(0, 12, 0, 0)
};
footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140));
footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 10));
footer.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 140));
footer.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
importButton.Text = "Importieren";
importButton.Dock = DockStyle.Fill;
importButton.Margin = new Padding(0);
importButton.FlatStyle = FlatStyle.Flat;
importButton.FlatAppearance.BorderSize = 0;
importButton.BackColor = Blue;
importButton.ForeColor = Color.White;
importButton.Cursor = Cursors.Hand;
importButton.Click += delegate { FinishImport(); };
var cancelButton = new Button
{
Text = "Abbrechen",
DialogResult = DialogResult.Cancel,
Dock = DockStyle.Fill,
FlatStyle = FlatStyle.Flat,
BackColor = Color.White,
ForeColor = Navy,
Margin = new Padding(0)
};
cancelButton.FlatAppearance.BorderColor = Color.FromArgb(206, 216, 230);
footer.Controls.Add(cancelButton, 1, 0);
footer.Controls.Add(importButton, 3, 0);
root.Controls.Add(footer, 0, 3);
AcceptButton = importButton;
CancelButton = cancelButton;
}
private Panel CreateSectionHeader(string text, bool preview)
{
var panel = new Panel
{
Dock = DockStyle.Fill,
BackColor = preview ? Color.FromArgb(236, 243, 255) : Color.White,
Margin = preview ? new Padding(7, 0, 0, 0) : new Padding(0, 0, 7, 0)
};
panel.Controls.Add(new Label
{
Text = text,
Font = new Font("Segoe UI Semibold", 9F),
ForeColor = preview ? Blue : Navy,
AutoSize = true,
Location = new Point(12, 12)
});
return panel;
}
private Button CreateLinkButton(string text)
{
var button = new Button
{
Text = text,
Dock = DockStyle.Fill,
FlatStyle = FlatStyle.Flat,
BackColor = Color.White,
ForeColor = Blue,
Cursor = Cursors.Hand,
Margin = new Padding(0, 0, 4, 4)
};
button.FlatAppearance.BorderSize = 0;
return button;
}
private void ConfigurePreviewGrid()
{
previewGrid.Dock = DockStyle.Fill;
previewGrid.Margin = new Padding(7, 0, 0, 0);
previewGrid.BackgroundColor = Color.White;
previewGrid.BorderStyle = BorderStyle.None;
previewGrid.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal;
previewGrid.GridColor = Color.FromArgb(228, 234, 242);
previewGrid.RowHeadersVisible = false;
previewGrid.ColumnHeadersHeight = 36;
previewGrid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(248, 250, 253);
previewGrid.ColumnHeadersDefaultCellStyle.ForeColor = Navy;
previewGrid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI Semibold", 9F);
previewGrid.EnableHeadersVisualStyles = false;
previewGrid.DefaultCellStyle.BackColor = Color.White;
previewGrid.DefaultCellStyle.ForeColor = Navy;
previewGrid.DefaultCellStyle.SelectionBackColor = Color.White;
previewGrid.DefaultCellStyle.SelectionForeColor = Navy;
previewGrid.RowTemplate.Height = 28;
previewGrid.AllowUserToAddRows = false;
previewGrid.AllowUserToDeleteRows = false;
previewGrid.AllowUserToOrderColumns = false;
previewGrid.ReadOnly = true;
previewGrid.MultiSelect = false;
previewGrid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
}
private void ReloadPreview(bool firstLoad)
{
try
{
loadedDocument = CsvCodec.Load(csvPath, headerCheckBox.Checked);
if (loadedDocument.Headers.Count == 0)
throw new InvalidDataException("Die Datei enthält keine auswertbaren CSV-Daten.");
formatLabel.Text = loadedDocument.Rows.Count + " Zeilen • " + loadedDocument.Headers.Count + " Spalten • " + DelimiterName(loadedDocument.Delimiter);
columnList.Items.Clear();
foreach (string header in loadedDocument.Headers)
columnList.Items.Add(header, true);
PopulatePreview();
UpdateSelectionStatus();
}
catch (Exception exception)
{
if (!firstLoad)
MessageBox.Show(this, "Die Vorschau konnte nicht aktualisiert werden.\r\n\r\n" + exception.Message, "Import-Assistent", MessageBoxButtons.OK, MessageBoxIcon.Error);
importButton.Enabled = false;
}
}
private void PopulatePreview()
{
previewGrid.SuspendLayout();
previewGrid.Columns.Clear();
previewGrid.Rows.Clear();
for (int column = 0; column < loadedDocument.Headers.Count; column++)
{
previewGrid.Columns.Add("Preview" + column, loadedDocument.Headers[column]);
previewGrid.Columns[column].SortMode = DataGridViewColumnSortMode.NotSortable;
}
foreach (var row in loadedDocument.Rows.Take(50))
previewGrid.Rows.Add(row.Cast<object>().ToArray());
previewGrid.ClearSelection();
previewGrid.ResumeLayout();
}
private void SetAllColumns(bool selected)
{
for (int index = 0; index < columnList.Items.Count; index++)
columnList.SetItemChecked(index, selected);
BeginInvoke(new Action(UpdateSelectionStatus));
}
private void UpdateSelectionStatus()
{
int count = columnList.CheckedIndices.Count;
selectionLabel.Text = count + " von " + columnList.Items.Count + " Spalten ausgewählt";
importButton.Enabled = count > 0;
}
private void FinishImport()
{
var selected = columnList.CheckedIndices.Cast<int>().ToList();
if (selected.Count == 0)
{
MessageBox.Show(this, "Bitte wähle mindestens eine Spalte aus.", "Keine Spalte ausgewählt", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SelectedDocument = CsvCodec.SelectColumns(loadedDocument, selected);
DialogResult = DialogResult.OK;
Close();
}
private static string DelimiterName(char delimiter)
{
if (delimiter == ';') return "Semikolon";
if (delimiter == ',') return "Komma";
if (delimiter == '\t') return "Tabulator";
return delimiter.ToString();
}
internal void UncheckLastColumnForPreview()
{
if (columnList.Items.Count > 1)
columnList.SetItemChecked(columnList.Items.Count - 1, false);
}
}
internal sealed class MainForm : Form
{
private readonly Color Navy = Color.FromArgb(24, 38, 58);
private readonly Color Blue = Color.FromArgb(41, 112, 255);
private readonly Color PaleBlue = Color.FromArgb(236, 243, 255);
private readonly Color Background = Color.FromArgb(244, 247, 251);
private readonly Color Muted = Color.FromArgb(94, 108, 128);
private readonly DataGridView originalGrid = new DataGridView();
private readonly DataGridView resultGrid = new DataGridView();
private readonly ComboBox scopeComboBox = new ComboBox();
private readonly NumericUpDown stepNumeric = new NumericUpDown();
private readonly Label statusLabel = new Label();
private readonly Label fileLabel = new Label();
private readonly Button exportButton = new Button();
private readonly Button resetButton = new Button();
private CsvDocument document;
private string importedPath;
public MainForm()
{
Text = "PANDA Pseudonymisierung alphanumerischer Nutzdaten";
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(1050, 680);
Size = new Size(1320, 820);
BackColor = Background;
Font = new Font("Segoe UI", 9F);
Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath) ?? SystemIcons.Application;
BuildLayout();
ConfigureGrid(originalGrid, true);
ConfigureGrid(resultGrid, false);
SetDocumentControlsEnabled(false);
}
private void BuildLayout()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 4,
Padding = new Padding(20, 18, 20, 16),
BackColor = Background
};
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 68));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 92));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
Controls.Add(root);
var header = new Panel { Dock = DockStyle.Fill, BackColor = Background };
var title = new Label
{
Text = "PANDA",
ForeColor = Navy,
Font = new Font("Segoe UI Semibold", 19F),
AutoSize = true,
Location = new Point(0, 0)
};
var subtitle = new Label
{
Text = "Pseudonymisierung alphanumerischer Nutzdaten durch Alphabetverschiebung",
ForeColor = Muted,
AutoSize = true,
Location = new Point(2, 39)
};
header.Controls.Add(title);
header.Controls.Add(subtitle);
root.Controls.Add(header, 0, 0);
var toolbar = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 8,
RowCount = 2,
Padding = new Padding(12, 10, 12, 8),
BackColor = Color.White,
Margin = new Padding(0, 0, 0, 12)
};
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 148));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 160));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 135));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 135));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 108));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 122));
toolbar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
toolbar.RowStyles.Add(new RowStyle(SizeType.Absolute, 34));
toolbar.RowStyles.Add(new RowStyle(SizeType.Absolute, 28));
root.Controls.Add(toolbar, 0, 1);
var importButton = CreateButton("Importieren", Blue, Color.White);
importButton.Click += delegate { ImportCsv(); };
toolbar.Controls.Add(importButton, 0, 0);
scopeComboBox.DropDownStyle = ComboBoxStyle.DropDownList;
scopeComboBox.Items.AddRange(new object[] { "Markierte Zellen", "Aktuelle Zelle", "Alle Einträge" });
scopeComboBox.SelectedIndex = 0;
scopeComboBox.Dock = DockStyle.Fill;
scopeComboBox.Margin = new Padding(7, 3, 7, 3);
toolbar.Controls.Add(scopeComboBox, 1, 0);
stepNumeric.Minimum = 1;
stepNumeric.Maximum = 25;
stepNumeric.Value = 1;
stepNumeric.Dock = DockStyle.Fill;
stepNumeric.TextAlign = HorizontalAlignment.Center;
stepNumeric.Margin = new Padding(7, 3, 7, 3);
toolbar.Controls.Add(stepNumeric, 2, 0);
var upButton = CreateButton("Hochzählen +", Color.FromArgb(29, 157, 105), Color.White);
upButton.Click += delegate { ApplyShift((int)stepNumeric.Value); };
toolbar.Controls.Add(upButton, 3, 0);
var downButton = CreateButton("Runterzählen ", Color.FromArgb(230, 91, 84), Color.White);
downButton.Click += delegate { ApplyShift(-(int)stepNumeric.Value); };
toolbar.Controls.Add(downButton, 4, 0);
resetButton.Text = "Zurücksetzen";
StyleSecondaryButton(resetButton);
resetButton.Click += delegate { ResetResults(); };
toolbar.Controls.Add(resetButton, 5, 0);
exportButton.Text = "CSV exportieren";
StyleSecondaryButton(exportButton);
exportButton.Click += delegate { ExportCsv(); };
toolbar.Controls.Add(exportButton, 6, 0);
fileLabel.Text = "Noch keine CSV geladen";
fileLabel.ForeColor = Muted;
fileLabel.AutoEllipsis = true;
fileLabel.Dock = DockStyle.Fill;
fileLabel.TextAlign = ContentAlignment.MiddleLeft;
toolbar.SetColumnSpan(fileLabel, 4);
toolbar.Controls.Add(fileLabel, 0, 1);
var hint = new Label
{
Text = "Tipp: Strg oder Umschalt gedrückt halten, um mehrere Zellen zu markieren.",
ForeColor = Muted,
AutoEllipsis = true,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleRight
};
toolbar.SetColumnSpan(hint, 4);
toolbar.Controls.Add(hint, 4, 1);
var grids = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 2,
RowCount = 2,
BackColor = Background,
Margin = new Padding(0)
};
grids.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
grids.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
grids.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
grids.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
root.Controls.Add(grids, 0, 2);
grids.Controls.Add(CreateGridHeader("ORIGINAL", "Importierte CSV-Werte", false), 0, 0);
grids.Controls.Add(CreateGridHeader("ERGEBNIS", "Veränderte Werte", true), 1, 0);
originalGrid.Margin = new Padding(0, 0, 7, 0);
resultGrid.Margin = new Padding(7, 0, 0, 0);
grids.Controls.Add(originalGrid, 0, 1);
grids.Controls.Add(resultGrid, 1, 1);
var statusPanel = new Panel { Dock = DockStyle.Fill, BackColor = Background };
statusLabel.Text = "Bereit bitte eine CSV-Datei importieren.";
statusLabel.ForeColor = Muted;
statusLabel.AutoSize = true;
statusLabel.Location = new Point(2, 12);
statusPanel.Controls.Add(statusLabel);
root.Controls.Add(statusPanel, 0, 3);
}
private Panel CreateGridHeader(string eyebrow, string caption, bool result)
{
var panel = new Panel
{
Dock = DockStyle.Fill,
BackColor = result ? PaleBlue : Color.White,
Margin = result ? new Padding(7, 0, 0, 0) : new Padding(0, 0, 7, 0)
};
var title = new Label
{
Text = eyebrow + " " + caption,
Font = new Font("Segoe UI Semibold", 10F),
ForeColor = result ? Blue : Navy,
AutoSize = true,
Location = new Point(12, 11)
};
panel.Controls.Add(title);
return panel;
}
private Button CreateButton(string text, Color background, Color foreground)
{
var button = new Button
{
Text = text,
Dock = DockStyle.Fill,
FlatStyle = FlatStyle.Flat,
BackColor = background,
ForeColor = foreground,
Cursor = Cursors.Hand,
Margin = new Padding(4, 2, 4, 2)
};
button.FlatAppearance.BorderSize = 0;
return button;
}
private void StyleSecondaryButton(Button button)
{
button.Dock = DockStyle.Fill;
button.FlatStyle = FlatStyle.Flat;
button.BackColor = Color.White;
button.ForeColor = Navy;
button.Cursor = Cursors.Hand;
button.Margin = new Padding(4, 2, 4, 2);
button.FlatAppearance.BorderColor = Color.FromArgb(206, 216, 230);
}
private void ConfigureGrid(DataGridView grid, bool selectable)
{
grid.Dock = DockStyle.Fill;
grid.BackgroundColor = Color.White;
grid.BorderStyle = BorderStyle.None;
grid.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal;
grid.GridColor = Color.FromArgb(228, 234, 242);
grid.RowHeadersVisible = true;
grid.RowHeadersWidth = selectable ? 74 : 50;
grid.RowHeadersDefaultCellStyle.BackColor = Color.FromArgb(248, 250, 253);
grid.RowHeadersDefaultCellStyle.ForeColor = Muted;
grid.RowHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
grid.ColumnHeadersHeight = 36;
grid.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
grid.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(248, 250, 253);
grid.ColumnHeadersDefaultCellStyle.ForeColor = Navy;
grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI Semibold", 9F);
grid.ColumnHeadersDefaultCellStyle.SelectionBackColor = Color.FromArgb(248, 250, 253);
grid.EnableHeadersVisualStyles = false;
grid.DefaultCellStyle.BackColor = Color.White;
grid.DefaultCellStyle.ForeColor = Navy;
grid.DefaultCellStyle.SelectionBackColor = selectable ? Color.FromArgb(214, 227, 255) : Color.White;
grid.DefaultCellStyle.SelectionForeColor = Navy;
grid.DefaultCellStyle.Padding = new Padding(4, 2, 4, 2);
grid.RowTemplate.Height = 30;
grid.AllowUserToAddRows = false;
grid.AllowUserToDeleteRows = false;
grid.AllowUserToOrderColumns = false;
grid.AllowUserToResizeRows = false;
grid.MultiSelect = true;
grid.SelectionMode = DataGridViewSelectionMode.CellSelect;
grid.ReadOnly = true;
grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
grid.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText;
grid.DataError += delegate { };
if (selectable)
{
grid.RowPostPaint += DrawOriginalRowHeader;
grid.RowHeaderMouseClick += ToggleOriginalRow;
grid.Scroll += delegate { SyncScroll(originalGrid, resultGrid); };
}
else
{
grid.MultiSelect = false;
grid.TabStop = false;
grid.SelectionChanged += delegate
{
if (grid.SelectedCells.Count > 0)
grid.ClearSelection();
};
grid.CellMouseDown += delegate(object sender, DataGridViewCellMouseEventArgs args)
{
if (args.RowIndex < 0 || args.ColumnIndex < 0)
return;
originalGrid.ClearSelection();
originalGrid.CurrentCell = originalGrid.Rows[args.RowIndex].Cells[args.ColumnIndex];
originalGrid.CurrentCell.Selected = true;
originalGrid.Focus();
BeginInvoke(new Action(delegate
{
grid.ClearSelection();
grid.CurrentCell = null;
}));
};
grid.Scroll += delegate { SyncScroll(resultGrid, originalGrid); };
}
}
private void DrawOriginalRowHeader(object sender, DataGridViewRowPostPaintEventArgs args)
{
var grid = (DataGridView)sender;
var bounds = new Rectangle(0, args.RowBounds.Top, grid.RowHeadersWidth, args.RowBounds.Height);
using (var backgroundBrush = new SolidBrush(Color.FromArgb(248, 250, 253)))
args.Graphics.FillRectangle(backgroundBrush, bounds);
using (var linePen = new Pen(Color.FromArgb(228, 234, 242)))
args.Graphics.DrawLine(linePen, bounds.Left, bounds.Bottom - 1, bounds.Right, bounds.Bottom - 1);
bool selected = grid.Rows[args.RowIndex].Tag is bool && (bool)grid.Rows[args.RowIndex].Tag;
int checkSize = 14;
var checkPoint = new Point(8, bounds.Top + (bounds.Height - checkSize) / 2);
CheckBoxRenderer.DrawCheckBox(args.Graphics, checkPoint, selected ? System.Windows.Forms.VisualStyles.CheckBoxState.CheckedNormal : System.Windows.Forms.VisualStyles.CheckBoxState.UncheckedNormal);
var numberBounds = new Rectangle(29, bounds.Top, bounds.Width - 32, bounds.Height);
TextRenderer.DrawText(args.Graphics, (args.RowIndex + 1).ToString(), grid.Font, numberBounds, Muted, TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.NoPadding);
}
private void ToggleOriginalRow(object sender, DataGridViewCellMouseEventArgs args)
{
if (args.RowIndex < 0 || args.RowIndex >= originalGrid.Rows.Count)
return;
var row = originalGrid.Rows[args.RowIndex];
bool wasSelected = row.Tag is bool && (bool)row.Tag;
bool isSelected = !wasSelected;
row.Tag = isSelected;
foreach (DataGridViewCell cell in row.Cells)
cell.Selected = isSelected;
originalGrid.InvalidateRow(args.RowIndex);
BeginInvoke(new Action(RefreshCheckedRowSelections));
statusLabel.Text = isSelected
? "Zeile " + (args.RowIndex + 1) + " vollständig ausgewählt."
: "Zeile " + (args.RowIndex + 1) + " aus der vollständigen Auswahl entfernt.";
statusLabel.ForeColor = Muted;
}
private void RefreshCheckedRowSelections()
{
foreach (DataGridViewRow row in originalGrid.Rows)
{
bool isSelected = row.Tag is bool && (bool)row.Tag;
if (!isSelected)
continue;
foreach (DataGridViewCell cell in row.Cells)
cell.Selected = true;
}
originalGrid.Invalidate();
}
private void ImportCsv()
{
using (var dialog = new OpenFileDialog())
{
dialog.Title = "CSV-Datei importieren";
dialog.Filter = "CSV-Dateien (*.csv)|*.csv|Textdateien (*.txt)|*.txt|Alle Dateien (*.*)|*.*";
dialog.CheckFileExists = true;
dialog.Multiselect = false;
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
try
{
using (var wizard = new ImportWizardForm(dialog.FileName))
{
if (wizard.ShowDialog(this) != DialogResult.OK)
return;
document = wizard.SelectedDocument;
}
importedPath = dialog.FileName;
PopulateGrids();
fileLabel.Text = Path.GetFileName(dialog.FileName) + " • " + document.Rows.Count + " Zeilen • " + document.Headers.Count + " importierte Spalten • " + DelimiterName(document.Delimiter);
statusLabel.Text = "Import erfolgreich. Wähle links Zellen aus oder nutze Alle Einträge.";
statusLabel.ForeColor = Color.FromArgb(29, 132, 88);
SetDocumentControlsEnabled(true);
}
catch (Exception exception)
{
MessageBox.Show(this, "Die CSV-Datei konnte nicht importiert werden.\r\n\r\n" + exception.Message, "Import fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
statusLabel.Text = "Import fehlgeschlagen.";
statusLabel.ForeColor = Color.FromArgb(190, 60, 55);
}
}
}
private string DelimiterName(char delimiter)
{
if (delimiter == ';') return "Semikolon";
if (delimiter == ',') return "Komma";
if (delimiter == '\t') return "Tabulator";
return delimiter.ToString();
}
private void PopulateGrids()
{
originalGrid.SuspendLayout();
resultGrid.SuspendLayout();
originalGrid.Columns.Clear();
resultGrid.Columns.Clear();
originalGrid.Rows.Clear();
resultGrid.Rows.Clear();
for (int column = 0; column < document.Headers.Count; column++)
{
string key = "Column" + column;
originalGrid.Columns.Add(key, document.Headers[column]);
resultGrid.Columns.Add(key, document.Headers[column]);
originalGrid.Columns[column].SortMode = DataGridViewColumnSortMode.NotSortable;
resultGrid.Columns[column].SortMode = DataGridViewColumnSortMode.NotSortable;
}
foreach (var row in document.Rows)
{
originalGrid.Rows.Add(row.Cast<object>().ToArray());
resultGrid.Rows.Add(row.Cast<object>().ToArray());
}
for (int row = 0; row < document.Rows.Count; row++)
{
originalGrid.Rows[row].HeaderCell.Value = (row + 1).ToString();
originalGrid.Rows[row].Tag = false;
resultGrid.Rows[row].HeaderCell.Value = (row + 1).ToString();
}
originalGrid.ClearSelection();
resultGrid.ClearSelection();
resultGrid.CurrentCell = null;
originalGrid.ResumeLayout();
resultGrid.ResumeLayout();
}
private void ApplyShift(int amount)
{
if (document == null)
return;
var cells = GetTargetCells();
if (cells.Count == 0)
{
MessageBox.Show(this, "Bitte markiere mindestens eine Zelle in der linken Tabelle oder wähle als Bereich Alle Einträge.", "Keine Auswahl", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
int changed = 0;
foreach (var coordinate in cells)
{
var cell = resultGrid.Rows[coordinate.Item1].Cells[coordinate.Item2];
string before = Convert.ToString(cell.Value) ?? string.Empty;
string after = LetterShifter.Shift(before, amount);
cell.Value = after;
if (!string.Equals(before, after, StringComparison.Ordinal))
changed++;
}
statusLabel.Text = changed + " von " + cells.Count + " Zellen verändert (" + (amount > 0 ? "+" : string.Empty) + amount + ").";
statusLabel.ForeColor = Color.FromArgb(29, 132, 88);
}
private List<Tuple<int, int>> GetTargetCells()
{
var result = new List<Tuple<int, int>>();
if (scopeComboBox.SelectedIndex == 2)
{
for (int row = 0; row < resultGrid.Rows.Count; row++)
for (int column = 0; column < resultGrid.Columns.Count; column++)
result.Add(Tuple.Create(row, column));
return result;
}
if (scopeComboBox.SelectedIndex == 1)
{
var current = originalGrid.CurrentCell;
if (current != null)
result.Add(Tuple.Create(current.RowIndex, current.ColumnIndex));
return result;
}
foreach (DataGridViewCell cell in originalGrid.SelectedCells)
result.Add(Tuple.Create(cell.RowIndex, cell.ColumnIndex));
foreach (DataGridViewRow row in originalGrid.Rows)
{
bool wholeRow = row.Tag is bool && (bool)row.Tag;
if (!wholeRow)
continue;
for (int column = 0; column < resultGrid.Columns.Count; column++)
result.Add(Tuple.Create(row.Index, column));
}
return result.Distinct().ToList();
}
private void ResetResults()
{
if (document == null)
return;
for (int row = 0; row < document.Rows.Count; row++)
for (int column = 0; column < document.Headers.Count; column++)
resultGrid.Rows[row].Cells[column].Value = document.Rows[row][column];
statusLabel.Text = "Alle Ergebnisse wurden auf die importierten Werte zurückgesetzt.";
statusLabel.ForeColor = Muted;
}
private void ExportCsv()
{
if (document == null)
return;
using (var dialog = new SaveFileDialog())
{
dialog.Title = "Veränderte CSV exportieren";
dialog.Filter = "CSV-Dateien (*.csv)|*.csv|Alle Dateien (*.*)|*.*";
dialog.DefaultExt = "csv";
dialog.AddExtension = true;
string sourceName = string.IsNullOrEmpty(importedPath) ? "ergebnis" : Path.GetFileNameWithoutExtension(importedPath) + "_veraendert";
dialog.FileName = sourceName + ".csv";
dialog.InitialDirectory = string.IsNullOrEmpty(importedPath) ? Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) : Path.GetDirectoryName(importedPath);
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
try
{
var rows = new List<IList<string>>();
foreach (DataGridViewRow gridRow in resultGrid.Rows)
{
var values = new List<string>();
foreach (DataGridViewCell cell in gridRow.Cells)
values.Add(Convert.ToString(cell.Value) ?? string.Empty);
rows.Add(values);
}
CsvCodec.Save(dialog.FileName, document, rows);
statusLabel.Text = "Export erfolgreich: " + dialog.FileName;
statusLabel.ForeColor = Color.FromArgb(29, 132, 88);
MessageBox.Show(this, "Die veränderte CSV wurde erfolgreich gespeichert.", "Export abgeschlossen", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception exception)
{
MessageBox.Show(this, "Die CSV-Datei konnte nicht gespeichert werden.\r\n\r\n" + exception.Message, "Export fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private void SyncScroll(DataGridView source, DataGridView target)
{
if (source.Rows.Count == 0 || target.Rows.Count == 0)
return;
try
{
target.FirstDisplayedScrollingRowIndex = source.FirstDisplayedScrollingRowIndex;
target.HorizontalScrollingOffset = source.HorizontalScrollingOffset;
}
catch (ArgumentOutOfRangeException)
{
}
}
private void SetDocumentControlsEnabled(bool enabled)
{
scopeComboBox.Enabled = enabled;
if (enabled && scopeComboBox.SelectedIndex < 0 && scopeComboBox.Items.Count > 0)
scopeComboBox.SelectedIndex = 0;
stepNumeric.Enabled = enabled;
exportButton.Enabled = enabled;
resetButton.Enabled = enabled;
scopeComboBox.Refresh();
}
internal void LoadPreviewData()
{
document = new CsvDocument { Delimiter = ';', FirstRowIsHeader = true };
document.Headers.AddRange(new[] { "Kundennummer", "Vorname", "Nachname", "Ort" });
document.Rows.Add(new List<string> { "1001", "Anna", "Meyer", "Berlin" });
document.Rows.Add(new List<string> { "1002", "Jonas", "Schmidt", "Hamburg" });
document.Rows.Add(new List<string> { "1003", "Zoe", "Fischer", "München" });
document.Rows.Add(new List<string> { "1004", "Lena", "Wagner", "Köln" });
PopulateGrids();
for (int column = 0; column < document.Headers.Count; column++)
resultGrid.Rows[0].Cells[column].Value = LetterShifter.Shift(document.Rows[0][column], 1);
originalGrid.Rows[0].Tag = true;
RefreshCheckedRowSelections();
fileLabel.Text = "beispiel.csv • 4 Zeilen • 4 Spalten • Trennzeichen: Semikolon";
statusLabel.Text = "3 von 4 Zellen verändert (+1).";
statusLabel.ForeColor = Color.FromArgb(29, 132, 88);
SetDocumentControlsEnabled(true);
scopeComboBox.SelectedIndex = 0;
scopeComboBox.Refresh();
}
}
}