How to print a text file on thermal printer using PrintDocument?
Asked Answered
I

3

5

I'm creating an application using C# with Winforms and now I need to print the receipt of sale on a thermal printer. To do this I'm creating a text file and reading it to print using the PrintDocument but I cannot do this because I don't know how to configure paper size, align text center on the paper, and others configurations. When I do print the text file is printed, but all messy, and after the end print the paper isn't stopping.

How could I do this ?

trying.

private PrintDocument printDocument = new PrintDocument();
private static String RECEIPT = Environment.CurrentDirectory + @"\comprovantes\comprovante.txt";
private String stringToPrint = "";

private void button1_Click(object sender, EventArgs e)
{
    generateReceipt();
    printReceipt();
}

private void generateReceipt()
{
    FileStream fs = new FileStream(COMPROVANTE, FileMode.Create);
    StreamWriter writer = new StreamWriter(fs);
    writer.WriteLine("==========================================");
    writer.WriteLine("          NOME DA EMPRESA AQUI            ");
    writer.WriteLine("==========================================");
    writer.Close();
    fs.Close();
}

private void printReceipt()
{
    FileStream fs = new FileStream(COMPROVANTE, FileMode.Open);
    StreamReader sr = new StreamReader(fs);
    stringToPrint = sr.ReadToEnd();
    printDocument.PrinterSettings.PrinterName = DefaultPrinter.GetDefaultPrinterName();
    printDocument.PrintPage += new PrintPageEventHandler(printPage);
    printDocument.Print();
    sr.Close();
    fs.Close();
}

private void printPage(object sender, PrintPageEventArgs e)
{
    int charactersOnPage = 0;
    int linesPerPage = 0;
    Graphics graphics = e.Graphics;

    // Sets the value of charactersOnPage to the number of characters 
    // of stringToPrint that will fit within the bounds of the page.
    graphics.MeasureString(stringToPrint, this.Font,
        e.MarginBounds.Size, StringFormat.GenericTypographic,
        out charactersOnPage, out linesPerPage);

    // Draws the string within the bounds of the page
    graphics.DrawString(stringToPrint, this.Font, Brushes.Black,
        e.MarginBounds, StringFormat.GenericTypographic);

    // Remove the portion of the string that has been printed.
    stringToPrint = stringToPrint.Substring(charactersOnPage);

    // Check to see if more pages are to be printed.
    e.HasMorePages = (stringToPrint.Length > 0);
}

I'm trying create this model of receipt.

enter image description here

Using RDLC, but it is very slow to start print. I'm follow this example

//works but slow
private void Run() {
    LocalReport report = new LocalReport();
    report.ReportPath = @"..\..\reports\ReciboDeVenda.rdlc";
    Export(report);
    Print();
}

private void Export(LocalReport report) {
    string deviceInfo =
      @"<DeviceInfo>
        <OutputFormat>EMF</OutputFormat>
        <PageWidth>8.5in</PageWidth>
        <PageHeight>11in</PageHeight>
        <MarginTop>0.25in</MarginTop>
        <MarginLeft>0.25in</MarginLeft>
        <MarginRight>0.25in</MarginRight>
        <MarginBottom>0.25in</MarginBottom>
    </DeviceInfo>";
    Warning[] warnings;
    m_streams = new List<Stream>();
    report.Render("Image", deviceInfo, CreateStream,
       out warnings);
    foreach (Stream stream in m_streams)
        stream.Position = 0;
}

private Stream CreateStream(string name, string fileNameExtension, 
                            Encoding encoding, string mimeType, bool willSeek) {
        Stream stream = new MemoryStream();
        m_streams.Add(stream);
        return stream;
}

private void Print() {
    if (m_streams == null || m_streams.Count == 0)
        throw new Exception("Error: no stream to print.");
    PrintDocument printDoc = new PrintDocument();
    printDoc.PrinterSettings.PrinterName = DefaultPrinter.GetDefaultPrinterName();
    if (!printDoc.PrinterSettings.IsValid) {
        throw new Exception("Error: cannot find the default printer.");
    }else {
        printDoc.PrintPage += new PrintPageEventHandler(PrintPage);
        m_currentPageIndex = 0;
        printDoc.Print();
    }
}

private void PrintPage(object sender, PrintPageEventArgs ev) {
    Metafile pageImage = new Metafile(m_streams[m_currentPageIndex]);

    // Adjust rectangular area with printer margins.
    Rectangle adjustedRect = new Rectangle(
        ev.PageBounds.Left - (int)ev.PageSettings.HardMarginX,
        ev.PageBounds.Top - (int)ev.PageSettings.HardMarginY,
        ev.PageBounds.Width,
        ev.PageBounds.Height);

    // Draw a white background for the report
    ev.Graphics.FillRectangle(Brushes.White, adjustedRect);

    // Draw the report content
    ev.Graphics.DrawImage(pageImage, adjustedRect);

    // Prepare for the next page. Make sure we haven't hit the end.
    m_currentPageIndex++;
    ev.HasMorePages = (m_currentPageIndex < m_streams.Count);
}
Illomened answered 25/9, 2016 at 23:48 Comment(5)
Why not using RDLC reports?Meat
@RezaAghaei I tried, but using RDLC the print is very slow, for example, 10 seconds to start print and I need the print start on the time.Illomened
Trying to create a report yourself without report engins is reinventing the wheel IMO, but you know your requirement better :). Maybe something makes the report/printing such slow. I've not seen such delay in using RDLC reports, but seen such delay in a RDL report which hosted on server and the delay was at first step because of server wake up and network and database server delay.Meat
@RezaAghaei I understand you. I'll post how to I'm using RDLC to print this report and if you can give an idea I'll glad.Illomened
I didn't checked the performance issue in your code, but I shared a useful option.Meat
R
4

If you send a plain string to your printPage(object sender, PrintEventArgs e) method, it will just print plain text in all the same font which looks 'messy' (as you named it) If you want it to be printed well formatted with different fonts (bold, regular), you have to do it all manually:

List<string> itemList = new List<string>()
{
    "201", //fill from somewhere in your code
    "202"
};

private void printPage( object sender, PrintPageEventArgs e )
{
    Graphics graphics = e.Graphics;

    Font regular = new Font( FontFamily.GenericSansSerif, 10.0f, FontStyle.Regular );
    Font bold = new Font( FontFamily.GenericSansSerif, 10.0f, FontStyle.Bold );

    //print header
    graphics.DrawString( "FERREIRA MATERIALS PARA CONSTRUCAO LTDA", bold, Brushes.Black, 20, 10 );
    graphics.DrawString( "EST ENGENHEIRO MARCILAC, 116, SAO PAOLO - SP", regular, Brushes.Black, 30, 30 );
    graphics.DrawString( "Telefone: (11)5921-3826", regular, Brushes.Black, 110, 50 );
    graphics.DrawLine( Pens.Black, 80, 70, 320, 70 );
    graphics.DrawString( "CUPOM NAO FISCAL", bold, Brushes.Black, 110, 80 );
    graphics.DrawLine( Pens.Black, 80, 100, 320, 100 );

    //print items
    graphics.DrawString( "COD | DESCRICAO                      | QTY | X | Vir Unit | Vir Total |", bold, Brushes.Black, 10, 120 );
    graphics.DrawLine( Pens.Black, 10, 140, 430, 140 );

    for( int i = 0; i < itemList.Count; i++ )
    {
        graphics.DrawString( itemList[i].ToString(), regular, Brushes.Black, 20, 150 + i * 20 );
    }

    //print footer
    //...

    regular.Dispose();
    bold.Dispose();

    // Check to see if more pages are to be printed.
    e.HasMorePages = ( itemList.Count > 20 );
}

Possible improvements on my example:

  • Centering the header strings could better be done using graphics.MeasureString().
  • List of items should better be a list of a business class insted of a plain string

Because this all is a lot of work, you should really consider using RDLC or some third party software to design your documents.

Riba answered 26/9, 2016 at 6:44 Comment(2)
thanks for your attention and your example. I wanted to use RDLC but as I said before does it very slow.Illomened
e.HasMorePages = ( itemList.Count > 20 ); ! i think this will never exit from the loop if itemList.Count > 20 right ! is there any other way to handle paging in thermal printer ?Ague
M
10

If I want to create a report myself, instead of trying to create a text file, I'll use HTML for rendering. Also to make rendering logic and mixing html tags and data simpler, I'd use T4 Run-time Text Templates. Then I can pass data to the html template and render the report simply. Then it's enough to assign the output string to a DocumentText property of a WebBrowser and call its Print method.

Download

You can clone or download a working example from r-aghaei/HtmlUsingRuntimeT4.

Why HTML?

Because of simple and flexible formatting. You can use all power of html tags and css styles to format the text. It's really better than using DrawString.

Why Run-time Text Templates?

Because it makes mixing data and html really easy and you can use C# language to perform some tasks like calculating sum, iterate over model records and so on. It uses T4 templating engine and lets you to use the template at run-time and feed data to the template.

Example

1- Add a model to project:

using System;
using System.Collections.Generic;

namespace Sample
{
    public class ReportModel
    {
        public string CustomerName { get; set; }
        public DateTime Date { get; set; }
        public List<OrderItem> OrderItems { get; set; }
    }
    public class OrderItem
    {
        public string Name { get; set; }
        public int Price { get; set; }
        public int Count { get; set; }
    }
}

2- Add a Run-time Text Template (also known as Preprocessed Text Template) to the project and name it ReportTemplate.tt. Open it and add such code:

<#@ template language="C#"#>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ parameter name="Model" type="Sample.ReportModel"#>
<html>
<head>
    <title></title>
    <style type="text/css">
        body { font-family: Calibri;width:400px;}
        table { text-align:center; }
        .container {width:400px; height:100%;}
    </style>
</head>
<body>
<div class="container">
<h1 style="text-align:center;">Order</h1>
<hr/>
<table style="width:100%">
    <tr>
        <td>Customer: <#=Model.CustomerName#></td>
        <td>Order Date: <#=Model.Date#></td>
    </tr>
</table>
<hr/>
<table style="width:100%">
    <tr><th>Index</th><th>Name</th><th>Price</th><th>Count</th><th>Sum</th></tr>
    <#
    int index =1;
    foreach(var item in Model.OrderItems) 
    {
    #>
    <tr>
        <td><#=index#></td>
        <td><#=item.Name#></td>
        <td><#=item.Price#></td>
        <td><#=item.Count#></td>
        <td><#=item.Count * item.Price#></td>
    </tr>
    <#
        index++;
    }
    var total= Model.OrderItems.Sum(x=>x.Count * x.Price);
    #>
    <tr><td></td><td></td><td></td><th>Total:</th><th><#=total#></th></tr>
</table>
<div>
</body>
</html>

3- Put a WebBrowser control on the form and write such code where you want to generate the report:

var rpt = new ReportTemplate();
rpt.Session = new Dictionary<string, object>();
rpt.Session["Model"] = new Sample.ReportModel
{
    CustomerName = "Reza",
    Date = DateTime.Now,
    OrderItems = new List<Sample.OrderItem>()
    {
        new Sample.OrderItem(){Name="Product 1", Price =100, Count=2 },
        new Sample.OrderItem(){Name="Product 2", Price =200, Count=3 },
        new Sample.OrderItem(){Name="Product 3", Price =50, Count=1 },
    }
};
rpt.Initialize();
this.webBrowser1.DocumentText= rpt.TransformText();

4- If you want to print the report, you can call:

this.webBrowser1.Print();

You should call Print after the document completed. So if you want to print directly without showing output to the user, you can handle DocumentCompleted event and call Print there.

Here is the result:

enter image description here

Meat answered 26/9, 2016 at 23:21 Comment(9)
The idea is really useful for cases which you want to create a custom report template, an email template or such things. This tehnique is much better than using Graphics.DrawString or TextRenderer.DrawText. It's more flexible and more simple.Meat
Good solution. Looks more complex than XML due to extra <tr>, <td> tags everywhereStripy
@codefrenzy Thanks for the feedback, for those who are familiar with html the solution is very friendly. For example using WebForms view engine or Razor view engine you write code for Views in ASP.NET MVC exactly this way.Meat
Nice.This seems to be very user friendly and easy to customize as per needs.Arvind
@RezaAghaei Have you tried printing this on a thermal printer & does it work perfectly ?Arvind
@rohankoshti I tried it using a virtual printer and it works perfectly. The idea is to avoid using GDI+ methods and instead render the output easily. For the printer it doesn't have any difference if you print an html document or you are drawing on drawing surface of the printer in print events. The solution that I used, using T4 templates to generate html output is like an MVC pattern and helps to separate concerns about the rendering logic and the data model that you are going to render, without need to know where the data comes from.Meat
this is great solution for thermal printingSystemize
@RezaAghaei this works like a charm! was wondering if is possible to print directly to the printer without loading the data to webBrowser form. with the DocumentCompleted I still need to load it somewhere. I was thinking to print the generated report directly to a predefined printerNahshunn
@ArgurKasemi Thanks for the feedback. You don't necessarily to show the form, just wait for document completed and print the document. Look at this post for example, I print a document event without a form, then immediately close the app.Meat
R
4

If you send a plain string to your printPage(object sender, PrintEventArgs e) method, it will just print plain text in all the same font which looks 'messy' (as you named it) If you want it to be printed well formatted with different fonts (bold, regular), you have to do it all manually:

List<string> itemList = new List<string>()
{
    "201", //fill from somewhere in your code
    "202"
};

private void printPage( object sender, PrintPageEventArgs e )
{
    Graphics graphics = e.Graphics;

    Font regular = new Font( FontFamily.GenericSansSerif, 10.0f, FontStyle.Regular );
    Font bold = new Font( FontFamily.GenericSansSerif, 10.0f, FontStyle.Bold );

    //print header
    graphics.DrawString( "FERREIRA MATERIALS PARA CONSTRUCAO LTDA", bold, Brushes.Black, 20, 10 );
    graphics.DrawString( "EST ENGENHEIRO MARCILAC, 116, SAO PAOLO - SP", regular, Brushes.Black, 30, 30 );
    graphics.DrawString( "Telefone: (11)5921-3826", regular, Brushes.Black, 110, 50 );
    graphics.DrawLine( Pens.Black, 80, 70, 320, 70 );
    graphics.DrawString( "CUPOM NAO FISCAL", bold, Brushes.Black, 110, 80 );
    graphics.DrawLine( Pens.Black, 80, 100, 320, 100 );

    //print items
    graphics.DrawString( "COD | DESCRICAO                      | QTY | X | Vir Unit | Vir Total |", bold, Brushes.Black, 10, 120 );
    graphics.DrawLine( Pens.Black, 10, 140, 430, 140 );

    for( int i = 0; i < itemList.Count; i++ )
    {
        graphics.DrawString( itemList[i].ToString(), regular, Brushes.Black, 20, 150 + i * 20 );
    }

    //print footer
    //...

    regular.Dispose();
    bold.Dispose();

    // Check to see if more pages are to be printed.
    e.HasMorePages = ( itemList.Count > 20 );
}

Possible improvements on my example:

  • Centering the header strings could better be done using graphics.MeasureString().
  • List of items should better be a list of a business class insted of a plain string

Because this all is a lot of work, you should really consider using RDLC or some third party software to design your documents.

Riba answered 26/9, 2016 at 6:44 Comment(2)
thanks for your attention and your example. I wanted to use RDLC but as I said before does it very slow.Illomened
e.HasMorePages = ( itemList.Count > 20 ); ! i think this will never exit from the loop if itemList.Count > 20 right ! is there any other way to handle paging in thermal printer ?Ague
W
1

Does your Thermal Printer support OPOS?

If it does, I recommend that you try using Microsoft Point of Service

Wina answered 26/9, 2016 at 4:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.