diff --git a/README.md b/README.md index 8666745..fac3d87 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,35 @@ Eine Vorschau erzeugst du im Tab `Layout` mit dem Button `Vorschau`. Zuerst wird } ``` -Noch nicht enthalten sind MySQL-Worker und echtes Drucken. +## Etappe 4 + +Die vierte Etappe ergänzt den Testdruck über installierte Windows-Drucker: + +- Druckerliste mit Standarddrucker-Erkennung +- Prüfung, ob der konfigurierte Drucker noch vorhanden ist +- Testdruck im Tab `Drucker` +- Testdruck im Tab `Layout` direkt aus dem aktuell bearbeiteten JSON +- Ausgabe des gerenderten Bitmaps über `System.Drawing.Printing.PrintDocument` +- benutzerdefiniertes Papierformat aus dem Layout, beim Beispiel `57 x 32 mm` +- keine zusätzlichen Druckränder; der Layout-Rand steckt bereits im gerenderten Bitmap + +Für den Dymo LabelWriter muss der Drucker in Windows bereits als normaler Windows-Drucker eingerichtet sein. Stelle im Dymo-Treiber möglichst ebenfalls das Etikettenformat `57 x 32 mm` bzw. das passende Dymo-Label ein. Die App sendet ein fertiges Bild an den Windows-Drucker; es wird kein ZPL, EPL oder TSPL verwendet. + +Einen Testdruck machst du so: + +1. Im Tab `Drucker` den Dymo LabelWriter auswählen. +2. `Speichern` klicken. +3. `Testdruck` klicken, um das ausgewählte Beispiel-Layout zu drucken. +4. Alternativ im Tab `Layout` das JSON bearbeiten und dort `Testdruck` klicken. + +Typische Fehler: + +- Falscher Drucker gewählt: im Tab `Drucker` den Dymo LabelWriter auswählen. +- Falsches Etikettenformat im Treiber: im Windows-Druckertreiber `57 x 32 mm` bzw. das passende Label einstellen. +- Ausdruck zu groß oder zu klein: prüfen, ob Treiber-Skalierung deaktiviert ist und das Layout `57 x 32 mm` verwendet. +- Etikett wird gedreht: Treiber-Orientierung und Layout-Orientation `landscape` prüfen. + +Noch nicht enthalten sind MySQL-Worker und automatische Datenbankabfrage. ## Startanleitung @@ -68,6 +96,7 @@ Noch nicht enthalten sind MySQL-Worker und echtes Drucken. 3. Das Tray-Symbol anklicken oder per Kontextmenü `Einstellungen` öffnen. 4. Im Tab `Drucker` einen installierten Windows-Drucker auswählen und speichern. 5. Im Tab `Layout` das Beispiel-Layout prüfen, bearbeiten und speichern. +6. Im Tab `Layout` oder `Drucker` einen Testdruck auslösen. Beim ersten Start werden die Programmdatenordner und das Beispiel-Layout automatisch angelegt. diff --git a/src/LabelPrintAgent/App/SettingsForm.cs b/src/LabelPrintAgent/App/SettingsForm.cs index b9a190a..8cf3b01 100644 --- a/src/LabelPrintAgent/App/SettingsForm.cs +++ b/src/LabelPrintAgent/App/SettingsForm.cs @@ -127,8 +127,8 @@ internal sealed class SettingsForm : Form panel.Controls.Add(Row(60, Label("Breite", 120), _labelWidth, Label("mm", 40))); panel.Controls.Add(Row(100, Label("Höhe", 120), _labelHeight, Label("mm", 40))); panel.Controls.Add(ButtonAt("Drucker neu laden", 140, 150, (_, _) => LoadPrinters())); - panel.Controls.Add(ButtonAt("Speichern", 300, 150, (_, _) => SavePrinter())); - panel.Controls.Add(LabelAt("Testdruck folgt in Etappe 2.", 140, 198, 340)); + panel.Controls.Add(ButtonAt("Speichern", 300, 150, (_, _) => SavePrinter(showMessage: true))); + panel.Controls.Add(ButtonAt("Testdruck", 420, 150, async (_, _) => await PrintSelectedLayoutTestAsync())); return new TabPage("Drucker") { Controls = { panel } }; } @@ -149,6 +149,7 @@ internal sealed class SettingsForm : Form panel.Controls.Add(ButtonAt("Validieren", 20, 595, (_, _) => ValidateLayout(showSuccessMessage: true))); panel.Controls.Add(ButtonAt("Speichern", 130, 595, (_, _) => SaveLayout())); panel.Controls.Add(ButtonAt("Vorschau", 240, 595, (_, _) => RenderLayoutPreview())); + panel.Controls.Add(ButtonAt("Testdruck", 350, 595, async (_, _) => await PrintCurrentLayoutTestAsync())); return new TabPage("Layout") { Controls = { panel } }; } @@ -181,16 +182,27 @@ internal sealed class SettingsForm : Form { var selected = _settingsStore.Load().Printer.PrinterName; _printer.Items.Clear(); - foreach (var printer in _printerService.GetInstalledPrinters()) + var printers = _printerService.GetInstalledPrinters(); + foreach (var printer in printers) { _printer.Items.Add(printer); } - if (!string.IsNullOrWhiteSpace(selected) && _printer.Items.Contains(selected)) + var configuredPrinter = printers.FirstOrDefault(printer => string.Equals(printer.Name, selected, StringComparison.OrdinalIgnoreCase)); + if (configuredPrinter is not null) { - _printer.SelectedItem = selected; + _printer.SelectedItem = configuredPrinter; + return; } - else if (_printer.Items.Count > 0) + + var defaultPrinter = printers.FirstOrDefault(printer => printer.IsDefault); + if (defaultPrinter is not null) + { + _printer.SelectedItem = defaultPrinter; + return; + } + + if (_printer.Items.Count > 0) { _printer.SelectedIndex = 0; } @@ -261,14 +273,17 @@ internal sealed class SettingsForm : Form MessageBox.Show("Datenbankeinstellungen gespeichert.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); } - private void SavePrinter() + private void SavePrinter(bool showMessage) { var settings = _settingsStore.Load(); - settings.Printer.PrinterName = _printer.SelectedItem?.ToString() ?? string.Empty; + settings.Printer.PrinterName = GetSelectedPrinterName() ?? string.Empty; settings.Printer.LabelWidthMm = _labelWidth.Value; settings.Printer.LabelHeightMm = _labelHeight.Value; _settingsStore.Save(settings); - MessageBox.Show("Druckereinstellungen gespeichert.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + if (showMessage) + { + MessageBox.Show("Druckereinstellungen gespeichert.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + } } private bool ValidateLayout(bool showSuccessMessage, out LabelLayout? layout) @@ -351,6 +366,104 @@ internal sealed class SettingsForm : Form } } + private async Task PrintSelectedLayoutTestAsync() + { + try + { + SavePrinter(showMessage: false); + var layout = LoadSelectedLayoutForTest(); + await PrintLayoutAsync(layout); + } + catch (Exception ex) + { + Log.Error(ex, "Could not start printer-tab test print"); + MessageBox.Show($"Testdruck fehlgeschlagen: {ex.Message}", Text, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private async Task PrintCurrentLayoutTestAsync() + { + try + { + SavePrinter(showMessage: false); + if (!ValidateLayout(showSuccessMessage: false, out var layout) || layout is null) + { + return; + } + + await PrintLayoutAsync(layout); + } + catch (Exception ex) + { + Log.Error(ex, "Could not start layout-tab test print"); + MessageBox.Show($"Testdruck fehlgeschlagen: {ex.Message}", Text, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private async Task PrintLayoutAsync(LabelLayout layout) + { + var printerName = GetSelectedPrinterName(); + if (string.IsNullOrWhiteSpace(printerName)) + { + MessageBox.Show("Bitte zuerst einen Drucker auswählen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + if (!_printerService.IsPrinterAvailable(printerName)) + { + MessageBox.Show($"Der Drucker '{printerName}' ist nicht verfügbar.", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + using var renderResult = _labelRenderer.Render(layout, PreviewDataProvider.CreatePayload()); + var printResult = await _printerService.PrintAsync( + renderResult.Image, + printerName, + layout.WidthMm, + layout.HeightMm); + + if (printResult.Success) + { + _layoutValidationErrors.Text = renderResult.Warnings.Count == 0 + ? string.Empty + : string.Join(Environment.NewLine, renderResult.Warnings); + MessageBox.Show("Testdruck wurde an den Drucker gesendet.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + MessageBox.Show($"Testdruck fehlgeschlagen: {printResult.ErrorMessage}", Text, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + + private LabelLayout LoadSelectedLayoutForTest() + { + if (_layoutKeys.SelectedItem is not null) + { + var key = _layoutKeys.SelectedItem.ToString(); + if (!string.IsNullOrWhiteSpace(key)) + { + return _layoutStore.LoadByKey(key.Replace(" (Standard)", string.Empty)); + } + } + + var firstKey = _layoutStore.GetLayoutKeys().FirstOrDefault(); + if (firstKey is null) + { + throw new InvalidOperationException("Es ist kein Layout für den Testdruck vorhanden."); + } + + return _layoutStore.LoadByKey(firstKey); + } + + private string? GetSelectedPrinterName() + { + return _printer.SelectedItem switch + { + PrinterInfo printerInfo => printerInfo.Name, + string printerName => printerName, + _ => null + }; + } + private bool IsAutostartEnabled() { using var key = Registry.CurrentUser.OpenSubKey(AutoStartRunKey, writable: false); diff --git a/src/LabelPrintAgent/Printing/PrintResult.cs b/src/LabelPrintAgent/Printing/PrintResult.cs new file mode 100644 index 0000000..dd53c6b --- /dev/null +++ b/src/LabelPrintAgent/Printing/PrintResult.cs @@ -0,0 +1,29 @@ +namespace LabelPrintAgent.Printing; + +public sealed class PrintResult +{ + public bool Success { get; init; } + public string? ErrorMessage { get; init; } + public string? PrinterName { get; init; } + public DateTime? PrintedAt { get; init; } + + public static PrintResult Ok(string printerName) + { + return new PrintResult + { + Success = true, + PrinterName = printerName, + PrintedAt = DateTime.Now + }; + } + + public static PrintResult Fail(string? printerName, string errorMessage) + { + return new PrintResult + { + Success = false, + PrinterName = printerName, + ErrorMessage = errorMessage + }; + } +} diff --git a/src/LabelPrintAgent/Printing/PrinterInfo.cs b/src/LabelPrintAgent/Printing/PrinterInfo.cs new file mode 100644 index 0000000..970d94c --- /dev/null +++ b/src/LabelPrintAgent/Printing/PrinterInfo.cs @@ -0,0 +1,20 @@ +namespace LabelPrintAgent.Printing; + +public sealed class PrinterInfo +{ + public PrinterInfo(string name, bool isDefault, bool isAvailable) + { + Name = name; + IsDefault = isDefault; + IsAvailable = isAvailable; + } + + public string Name { get; } + public bool IsDefault { get; } + public bool IsAvailable { get; } + + public override string ToString() + { + return IsDefault ? $"{Name} (Standard)" : Name; + } +} diff --git a/src/LabelPrintAgent/Printing/PrinterService.cs b/src/LabelPrintAgent/Printing/PrinterService.cs index 37535ce..620bdb0 100644 --- a/src/LabelPrintAgent/Printing/PrinterService.cs +++ b/src/LabelPrintAgent/Printing/PrinterService.cs @@ -1,11 +1,56 @@ +using System.Drawing; using System.Drawing.Printing; namespace LabelPrintAgent.Printing; public sealed class PrinterService { - public IReadOnlyList GetInstalledPrinters() + private readonly WindowsImagePrinter _imagePrinter = new(); + + public IReadOnlyList GetInstalledPrinters() { - return PrinterSettings.InstalledPrinters.Cast().OrderBy(name => name).ToList(); + var defaultPrinter = GetDefaultPrinterName(); + return PrinterSettings.InstalledPrinters + .Cast() + .OrderBy(name => name) + .Select(name => new PrinterInfo(name, string.Equals(name, defaultPrinter, StringComparison.OrdinalIgnoreCase), true)) + .ToList(); + } + + public string? GetDefaultPrinterName() + { + using var document = new PrintDocument(); + return document.PrinterSettings.IsDefaultPrinter ? document.PrinterSettings.PrinterName : null; + } + + public bool IsPrinterAvailable(string? printerName) + { + if (string.IsNullOrWhiteSpace(printerName)) + { + return false; + } + + return PrinterSettings.InstalledPrinters + .Cast() + .Any(name => string.Equals(name, printerName, StringComparison.OrdinalIgnoreCase)); + } + + public PrinterInfo GetPrinterInfo(string printerName) + { + var defaultPrinter = GetDefaultPrinterName(); + return new PrinterInfo( + printerName, + string.Equals(printerName, defaultPrinter, StringComparison.OrdinalIgnoreCase), + IsPrinterAvailable(printerName)); + } + + public Task PrintAsync( + Bitmap bitmap, + string printerName, + double labelWidthMm, + double labelHeightMm, + CancellationToken cancellationToken = default) + { + return _imagePrinter.PrintAsync(bitmap, printerName, labelWidthMm, labelHeightMm, cancellationToken); } } diff --git a/src/LabelPrintAgent/Printing/WindowsImagePrinter.cs b/src/LabelPrintAgent/Printing/WindowsImagePrinter.cs new file mode 100644 index 0000000..9ff1734 --- /dev/null +++ b/src/LabelPrintAgent/Printing/WindowsImagePrinter.cs @@ -0,0 +1,98 @@ +using System.Drawing; +using System.Drawing.Printing; +using Serilog; + +namespace LabelPrintAgent.Printing; + +public sealed class WindowsImagePrinter +{ + public Task PrintAsync( + Bitmap bitmap, + string printerName, + double labelWidthMm, + double labelHeightMm, + CancellationToken cancellationToken = default) + { + return Task.Run(() => Print(bitmap, printerName, labelWidthMm, labelHeightMm, cancellationToken), cancellationToken); + } + + private static PrintResult Print( + Bitmap bitmap, + string printerName, + double labelWidthMm, + double labelHeightMm, + CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + if (bitmap is null) + { + return PrintResult.Fail(printerName, "Das Druckbild ist leer."); + } + + if (string.IsNullOrWhiteSpace(printerName)) + { + return PrintResult.Fail(printerName, "Es wurde kein Drucker ausgewählt."); + } + + using var document = new PrintDocument(); + document.PrinterSettings.PrinterName = printerName; + if (!document.PrinterSettings.IsValid) + { + return PrintResult.Fail(printerName, $"Drucker '{printerName}' ist nicht verfügbar."); + } + + var paperSize = new PaperSize( + "Label 57x32mm", + MmToHundredthsOfInch(labelWidthMm), + MmToHundredthsOfInch(labelHeightMm)); + + document.DocumentName = "LabelPrintAgent Testdruck"; + document.PrintController = new StandardPrintController(); + document.OriginAtMargins = false; + document.DefaultPageSettings.PaperSize = paperSize; + document.DefaultPageSettings.Landscape = labelWidthMm > labelHeightMm; + document.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0); + + document.PrintPage += (_, args) => + { + if (cancellationToken.IsCancellationRequested) + { + args.Cancel = true; + return; + } + + if (args.Graphics is null) + { + args.Cancel = true; + return; + } + + args.Graphics.PageUnit = GraphicsUnit.Display; + args.Graphics.TranslateTransform(-args.PageSettings.HardMarginX, -args.PageSettings.HardMarginY); + var targetBounds = new Rectangle(0, 0, args.PageBounds.Width, args.PageBounds.Height); + args.Graphics.DrawImage(bitmap, targetBounds); + args.HasMorePages = false; + }; + + document.Print(); + return PrintResult.Ok(printerName); + } + catch (OperationCanceledException) + { + return PrintResult.Fail(printerName, "Druckauftrag wurde abgebrochen."); + } + catch (Exception ex) + { + Log.Error(ex, "Could not print label image on printer {PrinterName}", printerName); + return PrintResult.Fail(printerName, ex.Message); + } + } + + private static int MmToHundredthsOfInch(double mm) + { + return (int)Math.Round(mm / 25.4d * 100d); + } +}