Implement stage 4 Windows printer test printing

This commit is contained in:
2026-05-07 14:39:54 +02:00
parent 812f13ebee
commit 7e61ba8cac
6 changed files with 346 additions and 12 deletions
+30 -1
View File
@@ -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.
+122 -9
View File
@@ -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);
@@ -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
};
}
}
@@ -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;
}
}
+47 -2
View File
@@ -1,11 +1,56 @@
using System.Drawing;
using System.Drawing.Printing;
namespace LabelPrintAgent.Printing;
public sealed class PrinterService
{
public IReadOnlyList<string> GetInstalledPrinters()
private readonly WindowsImagePrinter _imagePrinter = new();
public IReadOnlyList<PrinterInfo> GetInstalledPrinters()
{
return PrinterSettings.InstalledPrinters.Cast<string>().OrderBy(name => name).ToList();
var defaultPrinter = GetDefaultPrinterName();
return PrinterSettings.InstalledPrinters
.Cast<string>()
.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<string>()
.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<PrintResult> PrintAsync(
Bitmap bitmap,
string printerName,
double labelWidthMm,
double labelHeightMm,
CancellationToken cancellationToken = default)
{
return _imagePrinter.PrintAsync(bitmap, printerName, labelWidthMm, labelHeightMm, cancellationToken);
}
}
@@ -0,0 +1,98 @@
using System.Drawing;
using System.Drawing.Printing;
using Serilog;
namespace LabelPrintAgent.Printing;
public sealed class WindowsImagePrinter
{
public Task<PrintResult> 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);
}
}