Implement stage 4 Windows printer test printing
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user