Projektdateien hinzufügen.

This commit is contained in:
2026-06-30 09:06:23 +02:00
parent ea1fb2895f
commit 0544fc88c2
8 changed files with 1026 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
<Solution>
<Project Path="BonNotiz/BonNotiz.csproj" />
</Solution>
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>disable</ImplicitUsings>
<StartupObject>BonNotiz.Program</StartupObject>
<ErrorReport>none</ErrorReport>
</PropertyGroup>
</Project>
+322
View File
@@ -0,0 +1,322 @@
using System;
using System.Text;
using System.IO;
using System.Drawing;
namespace KohaCompanion.Shared
{
/// <summary>
/// Stellt einen zustandsbehafteten ESC/POS-Drucker dar.
/// </summary>
/// <remarks>
/// Diese Klasse bietet Methoden, die die ESC/POS-Befehle kapseln.
/// Zum Schreiben eines Strings können Sie die Write(String)-Methode verwenden.
///
/// Bei jedem Aufruf einer Methode werden dabei die Kommandos in einen internen
/// Puffer geschrieben, der am Schluss mit GetCurrentPuffer() geholt werden
/// kann, um das Bytearray an den Drucker zu senden.
///
/// Quelle: https://www.vbarchiv.net/tipps/tipp_2375-kassenbon-drucker-mit-vbnet-oder-c-per-esc-pos-befehle-ansprechen.html
/// Benötigt nuget-Paket: System.Text.Encoding.CodePages
/// Originalautor: Konstantin Preißer
/// Originallizenz: MIT
/// Angepaßt an C# von Anna Christina Naß
/// </remarks>
public class EscPosPrinter
{
public enum Alignment
{
Left = 0,
Center,
Right
}
public class PrinterCodepage
{
public static readonly PrinterCodepage Cp437 = new PrinterCodepage(0, CodePagesEncodingProvider.Instance.GetEncoding(437));
public static readonly PrinterCodepage Cp852 = new PrinterCodepage(18, CodePagesEncodingProvider.Instance.GetEncoding(852));
public static readonly PrinterCodepage Cp866 = new PrinterCodepage(17, CodePagesEncodingProvider.Instance.GetEncoding(866));
public static readonly PrinterCodepage Cp1252 = new PrinterCodepage(16, CodePagesEncodingProvider.Instance.GetEncoding(1252));
private readonly byte _pageNumber;
private readonly Encoding? _encoding;
private PrinterCodepage(byte pageNumber, Encoding? encoding)
{
this._pageNumber = pageNumber;
this._encoding = encoding;
}
public byte PageNumber { get { return _pageNumber; } }
public Encoding Encoding { get { return _encoding; } }
}
// Steuercodes für neue Zeile
private static readonly byte[] newLineBytes = { 0xA, 0xD };
// Interner Buffer
private MemoryStream memstr = new();
// Zustand des Druckers
private PrinterCodepage printerEnc = PrinterCodepage.Cp437;
private bool underline = false;
private bool underlineDouble = false;
private bool emphasized = false;
//private bool redColor = false; * unused
private int currentFontX = 0;
private int currentFontY = 0;
//private bool panelButtonsEnabled = true; * unused
private Alignment _alignment = Alignment.Left;
/// <summary>
/// Erstellt eine neue Instanz. Dadurch wird automatisch ein
/// "Initialize Printer"-Command (ESC '@') in den Puffer geschrieben, der
/// den Drucker auf die Standardwerte zurücksetzt, damit der Zustand des
/// Druckers dem Zustand dieses Objektes entspricht.
/// </summary>
public EscPosPrinter()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Initialize Printer Command senden
byte[] buf = new byte[] { 0x1B, 0x40 };
memstr.Write(buf, 0, buf.Length);
}
/// <summary>
/// Schreibt den angegebenen String.
/// </summary>
/// <remarks>
/// Bei diesem Vorgang werden automatisch
/// Bytes, die in den ASCII-Steuerzeichenbereich fallen (0x00-0x1F sowie 0x7F),
/// durch Leerzeichen ersetzt. So wird verhindert, dass Strings aus fremden
/// Quellen den Zustand des Druckers verändern können.
///
/// Dies betrifft auch Zeilenumbrüche. Um einen Zeilenumbruch zu schreiben,
/// verwenden Sie WriteLine() oder WriteLine(String).
///
/// Beachten Sie, dass bei den meisten Bondruckern eine Zeile erst gedruckt
/// wird, sobald diese mit einem Zeilenumbruch abgeschlossen wird (auch wenn
/// die Zeile voll ist, also so viele Zeichen belegt, wie der Drucker in eine
/// Zeile drucken kann).
///
/// Beachten Sie, dass ESC/POS-Drucker standardmäßig die DOS-Codepage 437
/// verwenden, die u. a.Rahmenzeichen, einige griechische Zeichen und
/// mathematischen Symbole enthält. In den meisten Fällen empfiehlt sich für
/// Textdruck (v.a. europäische Texte) aber ein Umschalten auf die Windows-
/// Codepage 1252 (Methode SetCodepage(PrinterCodepage)), da hier mehr
/// unterschiedliche Buchstaben (z.B. "ß"), typographische Satzzeichen und
/// auch das €-Zeichen enthalten sind.
///
/// Für Texte in anderen europäischen Sprachen empfehlen sich noch die
/// Codepage 852, die Zeichen für mitteleuropäische Sprachen enthält, sowie
/// die Codepage 866 mit kyrillischen Zeichen.
/// </remarks>
/// <param name="str">der String, der ausgegeben werden soll</param>
public void Write(string str)
{
byte[] buf = printerEnc.Encoding.GetBytes(str);
// Aufpassen, dass kein ASCII-Steuerzeichen vorkommt.
// For i As Integer = 0 To buf.Length - 1
// Dim c As Byte = buf(i)
// If Not (c >= &H20 AndAlso c <> &H7F) Then
// buf(i) = CByte(AscW(" "c))
// End If
// Next
memstr.Write(buf, 0, buf.Length);
}
/// <summary>
/// Schreibt einen Zeilenumbruch (LF) zum Abschließen der aktuellen Zeile.
/// </summary>
public void WriteLine()
{
memstr.Write(newLineBytes, 0, newLineBytes.Length);
}
/// <summary>
/// Schreibt den angegebenen String und danach einen Zeilenumbruch (LF).
/// Siehe Dokumentation zur Write(String)-Methode.
/// </summary>
/// <param name="str">der String, der ausgegeben werden soll</param>
public void WriteLine(string str)
{
Write(str);
WriteLine();
}
/// <summary>
/// ESC '*' 33 nL nH d1…dk:
/// Druckt das angegebene Bild in Schwarz/Weiß. Die Höhe muss hierbei ein
/// Vielfaches von 24 sein, da das Bild wie Textzeilen ohne Zeilenabstand
/// gedruckt wird und eine Zeile (Font A) aus 24 Pixeln besteht. Die Breite
/// sollte der Druckerauflösung entsprechen (z.B. 384 Pixel beim PRP-058-
/// Drucker mit 32 Zeichen pro Zeile).
/// </summary>
/// <remarks>
/// Die Druckauflösung hängt vom Druckermodell ab und beträgt normalerweise
/// 203,2 dpi (z.B. PRP-058) oder 180 dpi.
/// </remarks>
/// <param name="bmp">Die zu druckende Bitmap</param>
public void PrintImage(Bitmap bmp)
{
if (bmp.Height % 24 != 0)
throw new ArgumentException("Die Bildhöhe muss ein Vielfaches von 24 sein.");
if (bmp.Width > 0x3FF)
throw new ArgumentException("Die Bildbreite darf nicht größer als 1023 sein.");
byte[] buf;
byte[] zeilenAnfang = new byte[] { 0x1B, 0x2A, 33, System.Convert.ToByte(bmp.Width & 0xFF), System.Convert.ToByte((bmp.Width >> 8) & 0xFF) };
byte[] bildBuf = new byte[bmp.Width * 3 - 1 + 1];
for (int i = 0; i <= bmp.Height / 24 - 1; i++)
{
// Durch die einzelnen Zeilen gehen
buf = zeilenAnfang;
memstr.Write(buf, 0, buf.Length);
buf = bildBuf;
Array.Clear(buf, 0, buf.Length);
for (int x = 0; x <= bmp.Width - 1; x++)
{
for (int y = 0; y <= 23; y++)
{
int byteIdx = y / 8 + x * 3;
Color c = bmp.GetPixel(x, i * 24 + y);
bool bit = c.GetBrightness() < 0.5F;
if (bit)
buf[byteIdx] = (byte)(buf[byteIdx] | System.Convert.ToByte(1 << (7 - (y % 8))));
}
}
memstr.Write(buf, 0, buf.Length);
if (i != bmp.Height / 24 - 1)
// ESC J n für Paper Feed (n (hier 0) müsste eigentlich 48 sein für eine
// Zeile, aber geht mit kleineren Werten auch)
// Nicht LF verwenden, weil bei LF der normale Zeilenabstand verwendet
// wird!
buf = new byte[] { 0x1B, 0x4A, 0 };
else
// Am Schluss normaler Zeilenabstand nach unten.
buf = new byte[] { 0xA };
memstr.Write(buf, 0, buf.Length);
}
}
/// <summary>
/// ESC '-' n:
/// Aktiviert oder deaktiviert den Underline-Modus.
/// </summary>
/// <param name="value">true, wenn der Underline-Modus aktiviert werden soll,
/// sonst false</param>
/// <param name="doubleThickness">true, wenn die Linie 2 Pixel statt 1 Pixel
/// dick sein soll</param>
public void SetUnderline(bool value, bool doubleThickness)
{
if (value != underline || (value & doubleThickness != underlineDouble))
{
underline = value;
underlineDouble = doubleThickness;
byte[] buf = new byte[] { 0x1B, 0x2D, System.Convert.ToByte(value ? doubleThickness ? 2 : 1 : 0) };
memstr.Write(buf, 0, buf.Length);
}
}
/// <summary>
/// ESC 'E' n:
/// Aktiviert oder deaktiviert den Fettschrift-Modus.
/// </summary>
/// <param name="value"></param>
public void SetEmphasized(bool value)
{
if (value != emphasized)
{
emphasized = value;
byte[] buf = new byte[] { 0x1B, 0x45, System.Convert.ToByte(value ? 1 : 0) };
memstr.Write(buf, 0, buf.Length);
}
}
/// <summary>
/// GS '!' n:
/// Ändert die Font-Größe (Parameter jeweils von 0-7).
/// Standardwerte: x = 0, y = 0
/// </summary>
/// <param name="x">Horizontale Größe</param>
/// <param name="y">Vertikale Größe</param>
public void SetFontSize(int x, int y)
{
if ((x < 0 || x > 7) || (y < 0 || y > 7))
throw new ArgumentException("x und y müssen im Bereich 0-7 liegen.");
if (x != currentFontX || y != currentFontY)
{
currentFontX = x;
currentFontY = y;
byte[] buf = new byte[] { 0x1D, 0x21, System.Convert.ToByte((x << 4) | y) };
memstr.Write(buf, 0, buf.Length);
}
}
/// <summary>
/// ESC 't' n:
/// Schaltet auf die angegebene Codepage um.
/// </summary>
public void SetCodepage(PrinterCodepage codepage)
{
if (printerEnc != codepage)
{
printerEnc = codepage;
byte[] buf = new byte[] { 0x1B, 0x74, codepage.PageNumber };
memstr.Write(buf, 0, buf.Length);
}
}
/// <summary>
/// ESC 'a' n:
/// Setzt die angegebene Textausrichtung.
/// </summary>
/// <param name="alignment"></param>
public void SetAlignment(Alignment alignment)
{
if (_alignment != alignment)
{
_alignment = alignment;
byte[] buf = new byte[] { 0x1B, 0x61, System.Convert.ToByte(alignment) };
memstr.Write(buf, 0, buf.Length);
}
}
/// <summary>
/// GS 'V' m:
/// Schneidet das Papier (nur bei Modellen mit einer Auto-Cut-Funktion).
/// </summary>
/// <param name="fullCut">true, wenn ein voller Schnitt durchgeführt
/// werden soll; false, wenn ein kleines Stück freigelassen
/// werden soll</param>
public void CutPaper(bool fullCut)
{
byte[] buf = new byte[] { 0x1D, 0x56, System.Convert.ToByte(fullCut ? 65 : 66), 0x40 };
memstr.Write(buf, 0, buf.Length);
}
/// <summary>
/// Gibt den aktuellen Pufferinhalt zurück und leert diesen anschließend.
/// </summary>
/// <returns></returns>
public byte[] GetCurrentBuffer()
{
byte[] buf = memstr.ToArray();
memstr.Close();
memstr = new MemoryStream();
return buf;
}
}
}
+89
View File
@@ -0,0 +1,89 @@
using System.Drawing;
using System.Windows.Forms;
namespace BonNotiz
{
partial class Main
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
TextBox = new TextBox();
PrinterSelect = new ComboBox();
Print = new Button();
SuspendLayout();
//
// TextBox
//
TextBox.AcceptsReturn = true;
TextBox.Font = new Font("Consolas", 12F, FontStyle.Regular, GraphicsUnit.Point, 0);
TextBox.Location = new Point(12, 12);
TextBox.Multiline = true;
TextBox.Name = "TextBox";
TextBox.ScrollBars = ScrollBars.Vertical;
TextBox.Size = new Size(470, 339);
TextBox.TabIndex = 0;
TextBox.Text = "Text eingeben...";
//
// PrinterSelect
//
PrinterSelect.FormattingEnabled = true;
PrinterSelect.Location = new Point(12, 357);
PrinterSelect.Name = "PrinterSelect";
PrinterSelect.Size = new Size(177, 23);
PrinterSelect.TabIndex = 2;
//
// Print
//
Print.Location = new Point(407, 357);
Print.Name = "Print";
Print.Size = new Size(75, 23);
Print.TabIndex = 3;
Print.Text = "&Drucken";
Print.UseVisualStyleBackColor = true;
Print.Click += Print_Click;
//
// Main
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(493, 391);
Controls.Add(Print);
Controls.Add(PrinterSelect);
Controls.Add(TextBox);
Name = "Main";
Text = "BonNotiz";
ResumeLayout(false);
PerformLayout();
}
#endregion
private TextBox TextBox;
private ComboBox PrinterSelect;
private Button Print;
}
}
+60
View File
@@ -0,0 +1,60 @@
using KohaCompanion.Shared;
using System;
using System.Windows.Forms;
namespace BonNotiz
{
public partial class Main : Form
{
public Main()
{
InitializeComponent();
FillPrinters();
}
private void FillPrinters()
{
// Auswahllisten für Drucker befüllen
foreach (String p in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
{
PrinterSelect.Items.Add(p);
}
foreach (String p in PrinterSelect.Items)
{
if (p.StartsWith("Epson"))
{
PrinterSelect.SelectedItem = p;
}
}
}
static void Drucken(String bondrucker, String text)
{
EscPosPrinter pos = new();
pos.Write(text);
pos.WriteLine();
pos.WriteLine();
pos.CutPaper(true);
Byte[] buf = pos.GetCurrentBuffer();
using RawPrinter prn = new(bondrucker);
using RawPrinter.RawDocumentStream doc = prn.CreateDocument("BonNotiz");
doc.Write(buf, 0, buf.Length);
}
private void Print_Click(object sender, EventArgs e)
{
if (PrinterSelect.SelectedItem == null || PrinterSelect.SelectedItem.ToString() == null)
{
MessageBox.Show("Kein Drucker ausgewählt.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
Drucken(PrinterSelect.SelectedItem.ToString()!, TextBox.Text);
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
+20
View File
@@ -0,0 +1,20 @@
using System;
using System.Windows.Forms;
namespace BonNotiz
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new Main());
}
}
}
+397
View File
@@ -0,0 +1,397 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace KohaCompanion.Shared
{
/// <summary>
/// Eine Klasse zum Kapseln eines Druckers. Mit der Methode
/// <see cref="RawPrinter.CreateDocument">CreateDocument(String)</see>
/// kann ein neues Dokument im RAW-Format erstellt werden.
///
/// Pro RawPrinter-Instanz kann immer nur eine RawDocumentStream-Instanz
/// gleichzeitig aktiv sein. Nach der Freigabe einer RawDocumentStream-
/// Instanz kann wieder ein neues Dokument erstellt werden.
/// </summary>
/// <remarks>
/// Diese Klasse ist nicht threadsicher. Wenn verschiedene Threads drucken
/// wollen, muss für jeden Thread ein eigenens RawPrinter-Objekt
/// erstellt werden.
///
/// Quelle: https://www.vbarchiv.net/tipps/tipp_2375-kassenbon-drucker-mit-vbnet-oder-c-per-esc-pos-befehle-ansprechen.html
/// Originalautor: Konstantin Preißer
/// Originallizenz: MIT
/// Angepaßt an C# von Anna Christina Naß
/// </remarks>
public class RawPrinter : IDisposable
{
// API-Deklarationen
[DllImport("winspool.Drv", EntryPoint = "OpenPrinterW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool OpenPrinter(string szPrinter, ref IntPtr hPrinter, IntPtr pd);
[DllImport("winspool.Drv", EntryPoint = "ClosePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool ClosePrinter(IntPtr hPrinter);
[DllImport("winspool.drv", EntryPoint = "GetPrinterDriver2W", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool GetPrinterDriver2(IntPtr hWnd, IntPtr hPrinter, string? pEnvironment, int Level, IntPtr pDriverInfo, int cbBuf, ref int pcbNeeded);
private const int ERROR_INSUFFICIENT_BUFFER = 122;
private const int PRINTER_DRIVER_XPS = 0x2;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private class DRIVER_INFO_8
{
public uint cVersion;
public string? pName;
public string? pEnvironment;
public string? pDriverPath;
public string? pDataFile;
public string? pConfigFile;
public string? pHelpFile;
public string? pDependentFiles;
public string? pMonitorName;
public string? pDefaultDataType;
public string? pszzPreviousNames;
public System.Runtime.InteropServices.ComTypes.FILETIME ftDriverDate;
public ulong dwlDriverVersion;
public string? pszMfgName;
public string? pszOEMUrl;
public string? pszHardwareID;
public string? pszProvider;
public string? pszPrintProcessor;
public string? pszVendorSetup;
public string? pszzColorProfiles;
public string? pszInfPath;
public uint dwPrinterDriverAttributes;
public string? pszzCoreDriverDependencies;
public System.Runtime.InteropServices.ComTypes.FILETIME ftMinInboxDriverVerDate;
public ulong dwlMinInboxDriverVerVersion;
}
// Instanz-Variablen
private readonly IntPtr hPrinter = IntPtr.Zero;
private readonly bool xpsDriver;
private RawDocumentStream? currentDoc = null;
private bool disposed = false;
private IntPtr HandlePrinter
{
get
{
if (disposed)
throw new ObjectDisposedException("RawPrinter");
return hPrinter;
}
}
/// <summary>
/// Erstellt eine neue Instanz.
/// </summary>
/// <param name="printerName">Der Name des zu verwendenden Druckers</param>
/// <exception cref="System.ComponentModel.Win32Exception">Beim Aufruf einer
/// Win32-API trat ein Fehler auf.</exception>
public RawPrinter(string printerName)
{
CheckApiCall(OpenPrinter(printerName, ref hPrinter, IntPtr.Zero));
xpsDriver = IsXpsDriver();
}
/// <summary>
/// Überprüft den Rückgabewert einer Win32-API-Funktion und wirft bei Bedarf
/// eine Exception.
/// </summary>
/// <param name="retVal"></param>
private static void CheckApiCall(bool retVal)
{
if (!retVal)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
/// <summary>
/// Gibt die nativen Resourcen dieses Objekts frei.
/// </summary>
/// <exception cref="System.ComponentModel.Win32Exception">Beim Aufruf einer
/// Win32-API trat ein Fehler auf.</exception>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
ClosePrinter(HandlePrinter);
disposed = true;
}
}
~RawPrinter()
{
Dispose(false);
}
private bool canCreateDocument = false;
/// <summary>
/// Erstellt ein neues RawPrintDocument.
/// </summary>
/// <param name="docName">Der Name des Dokuments</param>
/// <returns>Das neue Dokument</returns>
/// <exception cref="System.ComponentModel.Win32Exception">Beim Aufruf einer
/// Win32-API trat ein Fehler auf.</exception>
/// <exception cref="InvalidOperationException">Es wurde bereits ein Dokument
/// für diesen Drucker erstellt und noch nicht freigegeben.</exception>
public RawDocumentStream CreateDocument(string docName)
{
if (disposed)
throw new ObjectDisposedException("RawPrinter");
if (currentDoc != null)
throw new InvalidOperationException("Pro RawPrinter kann immer nur " + "1 RawDocumentStream-Objekt erzeugt werden.");
canCreateDocument = true;
try
{
currentDoc = new RawDocumentStream(this, docName);
return currentDoc;
}
finally
{
canCreateDocument = false;
}
}
/// <summary>
/// Überprüft, ob es sich bei dem Treiber dieses Druckers um einen
/// XPS-Treiber handelt.
/// </summary>
/// <remarks>
/// Bei Windows-Versionen kleiner als Windows 8 (NT 6.2), also
/// Windows Vista und Windows 7, wird immer
/// false zurückgegeben.
/// </remarks>
/// <returns>true, wenn es sich um einen XPS-Treiber handelt</returns>
private bool IsXpsDriver()
{
int level = 8;
// Nachschauen, wieviel Bytes als Buffer reserviert werden müssen.
// In diesem Fall muss GetPrinterDriver2 mit ERROR_INSUFFICIENT_BUFFER
// failen.
int bytesNeeded = 0;
GetPrinterDriver2(IntPtr.Zero, HandlePrinter, null, level, IntPtr.Zero, 0, ref bytesNeeded);
if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER)
CheckApiCall(false);
DRIVER_INFO_8 driverInf;
// Nativen Speicher mit geforderter Größe erstellen
int pBytesLength = Math.Max(bytesNeeded, Marshal.SizeOf(typeof(DRIVER_INFO_8)));
IntPtr pBytes = Marshal.AllocHGlobal(pBytesLength);
try
{
CheckApiCall(GetPrinterDriver2(IntPtr.Zero, HandlePrinter, null, level, pBytes, pBytesLength, ref bytesNeeded));
// Der Anfang des Bytesarrays enthält die Structure; der hintere Teil kann
// die Strings enthalten, auf die die Pointer in der Structure zeigen.
// Hier jetzt die Structure in ein verwaltetes Objekt marshallen.
driverInf = (DRIVER_INFO_8)Marshal.PtrToStructure(pBytes, typeof(DRIVER_INFO_8));
}
finally
{
Marshal.FreeHGlobal(pBytes);
pBytes = IntPtr.Zero;
}// Pointer clearen
// Prüfen, ob das Flag PRINTER_DRIVER_XPS gesetzt ist
return (driverInf.dwPrinterDriverAttributes & PRINTER_DRIVER_XPS) != 0;
}
/// <summary>
/// Ermöglicht das Senden von RAW-Dokumenten (Bytes) an den Drucker.
/// </summary>
/// <remarks>
/// Die Methoden StartPage() und EndPage() dienen zum Unterteilen des
/// Druckauftrags in Seiten (wichtig, wenn der Windows Spooler verwendet
/// wird und man Dokumente senden will, die lange zum Erstellen brauchen,
/// da dieser bis zur Fertigestellung einer Seite wartet, bevor diese
/// tatsächlich zum Drucker gesendet wird).
/// </remarks>
public class RawDocumentStream : Stream
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private class DOCINFO
{
public string? pDocName;
public string? pOutputFile;
public string? pDataType;
}
// API-Deklarationen
[DllImport("winspool.Drv", EntryPoint = "StartDocPrinterW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool StartDocPrinter(IntPtr hPrinter, Int32 level, [In()][MarshalAs(UnmanagedType.LPStruct)] DOCINFO di);
[DllImport("winspool.Drv", EntryPoint = "EndDocPrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool EndDocPrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "StartPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool StartPagePrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "EndPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool EndPagePrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "WritePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, int dwCount, ref int dwWritten);
private readonly RawPrinter printer;
private bool pageStarted = false;
private bool disposed = false;
internal RawDocumentStream(RawPrinter printer, string docName)
{
if (!printer.canCreateDocument)
// Schauen, dass das über den Printer erstellt wird
throw new InvalidOperationException();
this.printer = printer;
DOCINFO di = new DOCINFO();
// Wenn es sich um einen v4-Treiber (XPS-basiert, eingeführt mit
// Windows 8) handelt, muss "XPS_PASS" statt "RAW" verwendet werden.
// Siehe: http://support.microsoft.com/kb/2779300
di.pDataType = printer.xpsDriver ? "XPS_PASS" : "RAW";
di.pDocName = docName;
CheckApiCall(StartDocPrinter(printer.HandlePrinter, 1, di));
}
/// <summary>
/// Startet eine neue Seite (nur wichtig für den Windows Spooler).
/// </summary>
/// <exception cref="System.ComponentModel.Win32Exception">Beim
/// Aufruf einer Win32-API trat ein Fehler auf.</exception>
public void StartPage()
{
if (!pageStarted)
{
CheckApiCall(StartPagePrinter(printer.HandlePrinter));
pageStarted = true;
}
}
/// <summary>
/// Beendet die aktuelle Seite.
/// </summary>
/// <exception cref="System.ComponentModel.Win32Exception">Beim
/// Aufruf einer Win32-API trat ein Fehler auf.</exception>
public void FinishPage()
{
if (pageStarted)
{
CheckApiCall(EndPagePrinter(printer.HandlePrinter));
pageStarted = false;
}
}
protected override void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
if (pageStarted)
FinishPage();
CheckApiCall(EndDocPrinter(printer.HandlePrinter));
printer.currentDoc = null;
}
disposed = true;
}
base.Dispose(disposing);
}
public override bool CanRead { get { return false; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return true; } }
public override void Flush() { }
public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
/// <summary>
/// Sendet die angegebenen Bytes an den Drucker.
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <exception cref="System.ComponentModel.Win32Exception">Beim Aufruf
/// einer Win32-API trat ein Fehler auf.</exception>
public override void Write(byte[] buffer, int offset, int count)
{
if (disposed)
throw new ObjectDisposedException("RawDocumentStream");
if (offset < 0 || count < 0 || offset + count > buffer.Length)
throw new ArgumentException();
if (!pageStarted)
StartPage();
// Pointer auf Byte-Array holen.
// In C#: fixed (byte* pBytes = buffer) { ... },
// dann Pointerarithmetik verwenden
GCHandle hgc = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
int bytesCompleted = 0;
while (count - bytesCompleted > 0)
{
int tempBytesWritten = 0;
CheckApiCall(WritePrinter(printer.HandlePrinter, Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset + bytesCompleted), count - bytesCompleted, ref tempBytesWritten));
if (!(tempBytesWritten > 0))
break;
bytesCompleted += tempBytesWritten;
}
}
finally
{
hgc.Free();
}
}
}
}
}