1. 程式人生 > 其它 >C# 收銀機列印

C# 收銀機列印

前兩天領導說不用web端的列印外掛(C-Lodop),想用C#來實現,讓我研究一下呼叫印表機的方法,就有了這篇總結

.NET Core或.NET 5的話要引用一下NuGet包 System.Drawing.Common

獲取印表機列表

PrinterSettings.InstalledPrinters.Cast<string>();

定義列印介面

public interface IPrinter
{
  /// <summary>
  /// 設定頁面大小
  /// </summary>
  /// <param name="paperWidth"></param>
  /// <param name="paperHight"></param>
  void SetPageSize(double paperWidth, int? paperHight);

  /// <summary>
  /// 列印文字
  /// </summary>
  /// <param name="content"></param>
  /// <param name="fontSize"></param>
  /// <param name="stringAlignment"></param>
  /// <param name="width"></param>
  /// <param name="offset"></param>
  void PrintText(
      string content,
      FontSize fontSize = FontSize.Normal,
      StringAlignment stringAlignment = StringAlignment.Near,
      float width = 1,
      float offset = 0
      );

  /// <summary>
  /// 列印圖片
  /// </summary>
  /// <param name="image"></param>
  /// <param name="stringAlignment"></param>
  void PrintImage(Image image, StringAlignment stringAlignment = StringAlignment.Near);

  /// <summary>
  /// 列印一行
  /// </summary>
  void PrintSolidLine();

  /// <summary>
  /// 列印一行虛線
  /// </summary>
  /// <param name="fontSize"></param>
  void PrintDottedLine();

  /// <summary>
  /// 新起一行
  /// </summary>
  void NewLine();

  /// <summary>
  /// 開始列印
  /// </summary>
  void Print();
}

列印介面的實現

public class Printer : IPrinter
    {

        #region fields
        private PrintDocument _printDoc = new PrintDocument();

        /// <summary>
        /// 列印物件列印寬度(根據英寸換算而來,paperWidth * 3.937)
        /// </summary>
        private int _paperWidth;
        private int _paperHight;
        private const float _charProportion = 0.7352f;
        private const float _lineHeightProportion = 1.6f;
        private const string _fontName = "SimHei";
        private int _printIndex = 0;
        /// <summary>
        /// 列印模式:1、自動分頁模式  2、連續不分頁模式
        /// </summary>
        private int _mode;
        private IList<Action<Graphics>> _printActions = new List<Action<Graphics>>();

        /// <summary>
        /// 當前的列印高度,當呼叫換行或者圖片列印時會增加此欄位值
        /// </summary>
        private float _currentHeight = 0;

        public float NewLineOffset { get; set; } = (int)FontSize.Normal * _lineHeightProportion;

        #endregion

        #region ctor

        /// <summary>
        /// 初始化機列印物件
        /// </summary>
        /// <param name="printerName">印表機名稱</param>
        /// <param name="paperWidth">列印紙寬度</param>
        /// <param name="paperHight">列印紙高度</param>
        internal Printer(string printerName)
        {
            _printDoc.PrinterSettings.PrinterName = printerName;
            _printDoc.PrintPage += PrintPageDetails;

            //預設為58mm
            SetPageSize(58, null);            

            _printDoc.PrintController = new StandardPrintController();
        }


        #endregion

        #region eventHandler
        void PrintPageDetails(object sender, PrintPageEventArgs e)
        {
            while (_printIndex < _printActions.Count)
            {
                var originHeight = _currentHeight;
                _printActions[_printIndex](e.Graphics);
                _printIndex++;
                if (_currentHeight > e.PageBounds.Height - 20)
                {
                    if (_mode == 1)
                    {
                        _currentHeight = 0;
                        // HasMorePages 設定為true後,會繼續觸發PrintPage,因此_printIndex需要在外部進行定義
                        e.HasMorePages = true;
                        return;
                    }
                }
            }

            e.HasMorePages = false;
        }
        #endregion

        #region IPrinterImplement

        /// <summary>
        /// 設定頁面大小
        /// </summary>
        /// <param name="paperWidth"></param>
        /// <param name="paperHight"></param>
        public void SetPageSize(double paperWidth, int? paperHight)
        {
            switch (paperWidth)
            {
                case 80:
                    //80列印紙扣去兩邊內距實際可打的寬度為72.1
                    paperWidth = 72.1;
                    break;
                case 76:
                    //76列印紙扣去兩邊內距實際可打的寬度為63.5
                    paperWidth = 63.5;
                    break;
                case 58:
                    //58列印紙扣去兩邊內距實際可打的寬度為48
                    paperWidth = 48;
                    break;
                default:
                    paperWidth = paperWidth - 10;
                    break;
            }

            _paperWidth = Convert.ToInt32(Math.Ceiling(paperWidth * 3.937));
            _mode = 1;
            if (!paperHight.HasValue)
            {
                paperHight = 297;
                _mode = 2; // 設定為連續不分頁模式
            }

            _paperHight = Convert.ToInt32(Math.Ceiling(paperHight.Value * 3.937));
            _printDoc.DefaultPageSettings.PaperSize = new PaperSize("", _paperWidth, _paperHight);
        }

        /// <summary>
        /// 開始列印
        /// </summary>
        public void Print()
        {
            _printIndex = 0;
            _currentHeight = 0;
            _printDoc.EndPrint += (_, _) => Console.WriteLine("列印完成!");
            if (_mode == 2)
            {
                // 通過下面的方式計算出實際頁面高度
                using (Bitmap img = new Bitmap(_paperWidth, _paperHight))
                {
                    var g = Graphics.FromImage(img);
                    foreach (var item in _printActions)
                    {
                        item(g);
                    }
                    _paperHight = Convert.ToInt32(Math.Ceiling(_currentHeight)) + 5;
                    _printDoc.DefaultPageSettings.PaperSize = new PaperSize("", _paperWidth, _paperHight);
                    _printIndex = 0;
                    _currentHeight = 0;
                }
            }
            _printDoc.Print();
            _printDoc.Dispose();
            _printDoc = new PrintDocument();
            _printActions.Clear();
        }

        /// <summary>
        /// 新起一行
        /// </summary>
        public void NewLine()
        {
            _printActions.Add(g =>
            {
                _currentHeight += NewLineOffset;
                NewLineOffset = (int)FontSize.Normal * _lineHeightProportion;
            });
        }

        /// <summary>
        /// 列印文字
        /// </summary>
        /// <param name="content"></param>
        /// <param name="fontSize"></param>
        /// <param name="alignment"></param>
        /// <param name="width"></param>
        /// <param name="offset"></param>
        public void PrintText(string content, FontSize fontSize = FontSize.Normal, StringAlignment alignment = StringAlignment.Near, float width = 1, float offset = 0)
        {
            _printActions.Add(g =>
            {
                float contentWidth = width == 1 ? _paperWidth * (1 - offset) : width * _paperWidth;
                string newContent = ContentWarp(content, fontSize, contentWidth, out var rowNum);
                var font = new Font(_fontName, (int)fontSize, FontStyle.Regular);
                var point = new PointF(offset * _paperWidth, _currentHeight);
                var size = new SizeF(contentWidth, (int)fontSize * _lineHeightProportion * rowNum);
                var layoutRectangle = new RectangleF(point, size);
                var format = new StringFormat
                {
                    Alignment = alignment,
                    FormatFlags = StringFormatFlags.NoWrap
                };
                g.DrawString(newContent, font, Brushes.Black, layoutRectangle, format);
                float thisHeightOffset = rowNum * (int)fontSize * _lineHeightProportion;
                if (thisHeightOffset > NewLineOffset) NewLineOffset = thisHeightOffset;
            });
        }

        /// <summary>
        /// 列印圖片
        /// </summary>
        /// <param name="image"></param>
        /// <param name="stringAlignment"></param>
        public void PrintImage(Image image, StringAlignment stringAlignment = StringAlignment.Near)
        {
            _printActions.Add(g =>
            {
                int x = 0;
                switch (stringAlignment)
                {
                    case StringAlignment.Near:
                        break;
                    case StringAlignment.Center:
                        x = (_paperWidth - image.Width) / 2;
                        break;
                    case StringAlignment.Far:
                        x = _paperWidth - image.Width;
                        break;
                    default:
                        break;
                }
                var point = new Point(x, Convert.ToInt32(_currentHeight));
                var size = new Size(image.Width, image.Height);
                var rectangle = new Rectangle(point, size);
                g.DrawImage(image, rectangle);
                NewLineOffset = image.Height;
            });
        }

        /// <summary>
        /// 列印一行實線
        /// </summary>
        public void PrintSolidLine()
        {
            _printActions.Add(g =>
            {
                var pen = new Pen(new SolidBrush(Color.Black));
                pen.Width = 1f;
                g.DrawLine(pen, new Point(0, Convert.ToInt32(Math.Ceiling(_currentHeight))), new Point(_paperWidth, Convert.ToInt32(Math.Ceiling(_currentHeight))));
                _currentHeight += 3;
            });
        }

        /// <summary>
        /// 列印一行虛線
        /// </summary>
        public void PrintDottedLine()
        {
            var fontSize = FontSize.Normal;
            int charNum = (int)(_paperWidth / ((int)fontSize * _charProportion));
            var builder = new StringBuilder();
            for (int i = 0; i < charNum; i++)
            {
                builder.Append('-');
            }
            PrintText(builder.ToString(), fontSize, StringAlignment.Center);
        }
        #endregion

        #region methods
        /// <summary>
        /// 對內容進行分行,並返回行數
        /// </summary>
        /// <param name="content">內容</param>
        /// <param name="fontSize">文字大小</param>
        /// <param name="width">內容區寬度</param>
        /// <returns>行數</returns>
        private static string ContentWarp(string content, FontSize fontSize, float width, out int row)
        {
            content = content.Replace(Environment.NewLine, string.Empty);

            //0.7282 字元比例
            var builder = new StringBuilder();
            float nowWidth = 0;
            row = 1;
            foreach (char item in content)
            {
                int code = Convert.ToInt32(item);
                float charWidth = code < 128 ? _charProportion * (int)fontSize : _charProportion * (int)fontSize * 2;
                nowWidth += charWidth;
                if (nowWidth > width)
                {
                    builder.Append(Environment.NewLine);
                    nowWidth = charWidth;
                    row++;
                }
                builder.Append(item);
            }
            return builder.ToString();
        }

        #endregion
    }

建立印表機的工廠類

public static class PrinterFactory
{
    public static IEnumerable<string> GetAllPrints()
    {
        return PrinterSettings.InstalledPrinters.Cast<string>();
    }

    public static Printer GetPrinter(string printerName)
    {
        if (string.IsNullOrEmpty(printerName)) throw new ArgumentException(nameof(printerName));
        return new Printer(printerName);
    }
}

使用

static void Main(string[] args)
{
    // Microsoft XPS Document Writer 是測試時使用的,實際使用中,要替換成真正的印表機
    var printer = PrinterFactory.GetPrinter("Microsoft XPS Document Writer");
    printer.SetPageSize(80, null);
    printer.NewLine();
    var img = GetLogo();
    printer.PrintImage(img, StringAlignment.Center);
    printer.NewLine();
    printer.NewLine();
    printer.PrintText("永輝超市", FontSize.Large, alignment: StringAlignment.Center);
    printer.NewLine();
    printer.NewLine();
    printer.PrintText("單號:XD000269");
    printer.PrintText("流水號:000269", offset: 0.5f);
    printer.NewLine();
    printer.PrintText("收銀員:***");
    printer.PrintText("日期:" + DateTime.Now.ToString("yyyy/MM/dd"), offset: 0.5f);
    printer.NewLine();
    printer.PrintText("VIP客戶卡號:001");
    printer.NewLine();
    printer.PrintSolidLine();
    printer.NewLine();
    printer.PrintText("名稱");
    printer.PrintText("單價", offset: 0.35f);
    printer.PrintText("數量", offset: 0.65f);
    printer.PrintText("金額", alignment: StringAlignment.Far);
    printer.NewLine();
    printer.PrintText("芹菜", width: 0.35f);
    printer.PrintText("2.9", width: 0.2f, offset: 0.35f);
    printer.PrintText("1", width: 0.2f, offset: 0.65F);
    printer.PrintText("2.9", alignment: StringAlignment.Far);
    printer.NewLine();
    printer.PrintDottedLine();
    printer.NewLine();
    printer.PrintText("合計");
    printer.PrintText("1", offset: 0.65f);
    printer.PrintText("2.90", alignment: StringAlignment.Far);
    printer.NewLine();
    printer.PrintText("滿0.00減0.00折扣");
    printer.PrintText("-0.00", alignment: StringAlignment.Far);
    printer.NewLine();
    printer.PrintText("優惠金額:2.90");
    printer.PrintText("實收金額:0", offset: 0.5f);
    printer.NewLine();
    printer.PrintText("收款金額:0.00");
    printer.PrintText("找零金額:-2.90", offset: 0.5f);
    printer.NewLine();
    printer.PrintDottedLine();
    printer.NewLine();
    printer.PrintText("會員卡:001");
    printer.NewLine();
    printer.PrintText("本次積分:");
    printer.PrintText("會員餘額:43.87", offset: 0.5f);
    printer.NewLine();
    printer.PrintText("可用積分:");
    printer.NewLine();
    printer.PrintSolidLine();
    printer.NewLine();
    printer.PrintText("永輝超市", FontSize.Large, alignment: StringAlignment.Center);
    printer.NewLine();
    printer.PrintText("歡迎光臨,謝謝惠顧!", FontSize.Large, alignment: StringAlignment.Center);
    printer.NewLine();

    printer.Print();
    GC.Collect();
    
    Console.ReadKey();
}

private static Image GetLogo()
{
    var path = Path.Combine(Directory.GetCurrentDirectory(), "logo.jpg");
    using (var fileStream = File.OpenRead(path))
    {
        fileStream.Seek(0, SeekOrigin.Begin);
        return Image.FromStream(fileStream);
    }
}

效果如下

demo已上傳至 github