重复造轮子系列——基于FastReport设计打印模板实现桌面端WPF套打和商超POS高度自适应小票打印

一、引言

桌面端系统经常需要对接各种硬件设备,比如扫描器、读卡器、打印机等。

这里介绍下桌面端系统打印经常使用的场景。

1、一种是类似票务方面的系统需要打印固定格式的票据。比如景点门票、车票、电影票。

这种基本是根据模板调整位置套打。

2、还有一种是交易小票,比如商超POS小票,打印长度会随着内容的大小自动伸缩。

这种就不仅仅是固定格式的套打了,还得计算数据行以适应不同的打印长度。

打印方式也有两种类型

1、指令打印,根据不同打印机可能需要对接不同的打印指令。

2、驱动打印,不同打印机都有自带安装驱动。通过驱动打印更方便。下面介绍的内容以驱动打印的方式

打印经常需要调整打印字体位置等等这些,如果有个可视化模板设计下,系统不需要任何改动就可以是最方便的,这样方便客户或者现场实施自己做调整。

想到各种客户端报表工具都有可视化的界面而且可以打印,就找了个FastReport.Net工具来做。

二、固定格式的套打

这里以门票为例,讲解怎么使用FastReport.Net工具来设计模板以及打印。

下载FastReport.Net工具,引用到这三个dll,FastReport.dll、FastReport.Bars.dll、FastReport.Editor.dll

封装一个公共的PrintHelper.cs,提供两个方法,打印和设计。代码如下:

public class PrintHelper

    {

        /// <summary>

        /// 打印

        /// </summary>

        /// <param name="printerName">打印机</param>

        /// <param name="frxPath">模板</param>

        /// <param name="dicParam">字典参数</param>

        /// <param name="dsDataSource">数据源</param>

        /// <param name="printNum">打印数量</param>

        /// <returns></returns>

        public static Tuple<bool, string> Print(string printerName, string frxPath, Dictionary<string, object> dicParam, DataSet dsDataSource, int printNum = )

        {

            bool flag = false;

            string msg = "";

            FastReport.Report report = new FastReport.Report();

            try

            {

                report.Load(frxPath);

                report.DoublePass = true;

                if (dicParam != null && dicParam.Count > )

                {

                    foreach (var item in dicParam)

                    {

                        report.SetParameterValue(item.Key, item.Value);

                    }

                }

                if (dsDataSource != null && dsDataSource.Tables.Count > )

                {

                    report.RegisterData(dsDataSource);

                    foreach (DataSourceBase dataSourceBase in report.Dictionary.DataSources)

                    {

                        dataSourceBase.Enabled = true;

                    }

                }

                report.PrintSettings.ShowDialog = false;

                report.PrintSettings.Printer = printerName;

                report.PrintSettings.PrintMode = PrintMode.Split;

                EnvironmentSettings envSet = new EnvironmentSettings();

                envSet.ReportSettings.ShowProgress = false;

                for (int i = ; i < printNum; i++)

                {

                    report.Print();

                }                

                flag = true;

                msg = "打印成功";

            }

            catch (Exception ex)

            {

                flag = false;

                msg = ex.Message;

            }

            finally

            {

                report.Dispose();

            }

            return new Tuple<bool, string>(flag, msg);

        }

        /// <summary>

        /// 设计

        /// </summary>

        /// <param name="frxPath">模板</param>

        /// <param name="dicParam">字典参数</param>

        /// <param name="dsDataSource">数据源</param>

        /// <returns></returns>

        public static Tuple<bool, string> Design(string frxPath, Dictionary<string, object> dicParam, DataSet dsDataSource)

        {

            bool flag = false;

            string msg = "";

            FastReport.Report report = new FastReport.Report();

            try

            {

                report.Load(frxPath);

                report.DoublePass = true;

                if (dicParam != null && dicParam.Count > )

                {

                    foreach (var item in dicParam)

                    {

                        report.SetParameterValue(item.Key, item.Value);

                    }

                }

                if (dsDataSource != null && dsDataSource.Tables.Count > )

                {

                    report.RegisterData(dsDataSource);

                    foreach (DataSourceBase dataSourceBase in report.Dictionary.DataSources)

                    {

                        dataSourceBase.Enabled = true;

                    }

                }

                report.Design();

                flag = true;

                msg = "设计器打开成功";

            }

            catch (Exception ex)

            {

                flag = false;

                msg = ex.Message;

            }

            finally

            {

                report.Dispose();

            }

            return new Tuple<bool, string>(flag, msg);

        }

}

Demo设计一个TicketTemp.frx模板,如下图1

图1

面板上面的排列可以任意拖动,字体样式设计,还可以360旋转。如图2

图2

普通文本标签在Parameters里面直接可以取字典传入的参数值,双击或者拖拉都可以。如图3

图3

二维码或者条码类型,FastReport有提供相应的标签,看模板左边竖着的工具条,找到Barcode,拖一个到面板上,如图4

图4

对应的属性看右下角,如图5

图5

Barcode属性值下拉,支持这么多类型的条码和二维码编码格式

如果是使用自己生成的二维码图片就可以使用左边竖着工具里面的图片标签Picture,使用这个就可以自定义图片打印(当然条码二维码也是特殊图片)。但这个时候传入的数据要使用数据源了,并且在图片标签属性里面找到这个属性值,绑定数据源里面对应的图片字段。如图6

图6

看下demo里面模板设计按钮代码,如下:

private void BtnTicketDesign_Click(object sender, RoutedEventArgs e)

        {

            if (string.IsNullOrEmpty(txtTicketTemp.Text))

            {

                MessageBox.Show("门票模板不能为空");

                return;

            }                

            var data = this.CreateTicketData();

            if (!data.Item1)

            {

                MessageBox.Show("模拟数据生成错误");

                return;

            }

            var tuple = PrintHelper.Design(txtTicketTemp.Text, data.Item2, data.Item3);

            if (!tuple.Item1)

            {

                MessageBox.Show($"打开设计器失败:{tuple.Item2}");

            }

        }

看下demo里面模板打印按钮代码,如下:

private void BtnTicketPrint_Click(object sender, RoutedEventArgs e)

        {

            if (cbxPrinter.SelectedValue == null || string.IsNullOrEmpty(cbxPrinter.SelectedValue.ToString()))

            {

                MessageBox.Show("请选择打印机");

                return;

            }

            if (string.IsNullOrEmpty(txtTicketTemp.Text))

            {

                MessageBox.Show("门票模板不能为空");

                return;

            }

            var data = this.CreateTicketData();

            if (!data.Item1)

            {

                MessageBox.Show("模拟数据生成错误");

                return;

            }

            var tuple = PrintHelper.Print(cbxPrinter.SelectedValue.ToString(), txtTicketTemp.Text, data.Item2, data.Item3);

            if (!tuple.Item1)

            {

                MessageBox.Show($"打印失败:{tuple.Item2}");

            }

        }

打印和设计共同的组装模拟数据的方法代码如下

/// <summary>

        /// 组装门票打印模拟数据

        /// </summary>

        /// <returns></returns>

        private Tuple<bool, Dictionary<string, object>, DataSet> CreateTicketData()

        {

            //注意事项

            //文本内容放入字段中,传入就可以

            //图片内容有两种方案

            //1:使用模板的Barcode标签,这个支持标准qr码,条码等等多种类型,字典或者数据源传入文本值,自动会显示qr码条码图片

            //2:使用模板的Picture标签,使用这个就是要程序生成好图片,再把图片显示,这里就不仅仅限制于二维码条码了,各种想显示的图片都可以,但需要把图片流放到数据源中传入

            Dictionary<string, object> dic = new Dictionary<string, object>();

            dic.Add("ticketModelName", "XXX景点门票");

            dic.Add("ticketModelKind", "成人票");

            dic.Add("ticketModelPrice", "¥100");

            //字典中传入二维码的文本,fastreport提供了生成qr码以及各种条码

            dic.Add("barcode", "ET2018000000000000001");

            //如果需要,组装dataset数据源,这里以传入二维码图片为例

            DataTable dtImage = new DataTable("dtBarcode");

            dtImage.Columns.Add("barcode", typeof(Byte[]));

            DataSet dsFrx = new DataSet();

            dsFrx.Tables.Add(dtImage);

            //1、把二维码码文本生成图片  这个有很多第三方库可以支持  我这里用 ThoughtWorks.QRCode

            QRCodeEncoder qrCodeEncoder = new QRCodeEncoder();

            qrCodeEncoder.QRCodeEncodeMode = QRCodeEncoder.ENCODE_MODE.BYTE;

            qrCodeEncoder.QRCodeScale = ;

            qrCodeEncoder.QRCodeVersion = ;

            qrCodeEncoder.QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.M;

            using (Image image = qrCodeEncoder.Encode("ET2018000000000000001"))

            {

                //2、生成的图片本地可以做个备份记录,也可以不需要直接将image转byte[]传人数据源就可以

                if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "BarCode")))

                {

                    Directory.CreateDirectory(Path.Combine(System.IO.Directory.GetCurrentDirectory(), "BarCode"));

                }

                string filename = DateTime.Now.ToString("yyyymmddhhmmssfff").ToString() + ".jpg";

                string filepath = Path.Combine(System.IO.Directory.GetCurrentDirectory(), "BarCode", filename);

                using (FileStream fs = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Write))

                {

                    image.Save(fs, ImageFormat.Jpeg);

                }

                //3、将image转byte[]传人数据源  注意图片传入的是字节数组byte[]  不是文本也不是图片路径!!!

                dtImage.Rows.Add(ImageToBytes(image, ImageFormat.Jpeg));

            }

            return new Tuple<bool, Dictionary<string, object>, DataSet>(true, dic, dsFrx);

        }

运行,在设计器里面也可以预览,看下最终打印效果,如图7

图7

三、动态格式的打印

这里以商超POS交易小票为例,讲解怎么使用FastReport.Net工具来设计模板以及打印。

这种小票大部分和上面说的一样,唯一不同的是有数量不固定的数据集动态数据。

Demo设计一个BillTemp.frx模板,如下图8

图8

固定大小的数据和上面的类似,放到报表头尾,或者页面头尾都可以。动态数据需要放到Data里面,点击Configure Bands,如图9

图9

这里可以添加删除Band。

Data需要绑定对应的数据源,传进来是DataSet,也可以使用多个Data这样就可以有多个DataTable,实例这里就使用了一个。看设计器右上角数据源,如图10

图10

当前的data需要指定哪个数据源,如图11

图11

动态数据每个值也是和普通的文本类似,但不是取Parameters里面,要取DataSources里面对应的数据源字段,如图12

图12

小票模板里面还有一点是特殊的,由于数据集的动态的防止打印的时候分页,需要动态的控制面板的长度,切换到code,如图13

图13

在code里面增加代码计算数据布局之后的总高度,代码如下

  public class ReportScript

  {                      

    private float pageHeader1Height;

    private float reportTitle1Height;

    private float dataHeaderHeight;

    private float data1Height;  

    private float reportSummaryHeight;

    private float pageFooter1Height;

    private void Page1_StartPage(object sender, EventArgs e)

    {

      if(Engine.FinalPass)

      {                                             

        Page1.PaperHeight = (reportTitle1Height

          + pageHeader1Height

          +dataHeaderHeight

          +data1Height

          +reportSummaryHeight

          +pageFooter1Height)/Units.Millimeters

          +Page1.TopMargin

          +Page1.BottomMargin;

      }

    }

    private void PageHeader1_AfterLayout(object sender, EventArgs e)

    {

      pageHeader1Height=PageHeader1.Height;

    }

    private void ReportTitle1_AfterLayout(object sender, EventArgs e)

    {

      reportTitle1Height=ReportTitle1.Height;

    }

    private void PageFooter1_AfterLayout(object sender, EventArgs e)

    {

      pageFooter1Height=PageFooter1.Height;  

    }

    //Data 的高度 用+=

    private void Data1_AfterLayout(object sender, EventArgs e)

    {

       data1Height+=Data1.Height;  

    }

    //DataHeader 的高度 用+=

    private void DataHeader1_AfterLayout(object sender, EventArgs e)

    {

        dataHeaderHeight+=DataHeader1.Height;

    }

    private void ReportSummary1_AfterLayout(object sender, EventArgs e)

    {

      reportSummaryHeight=ReportSummary1.Height;

    }     

  }

这段代码就是所有的band属性的AfterLayout事件,如图14

图14

算出当前band高度最后总和最为页面的高度

Demo里面小票的设计和打印代码和上面门票的类似,这里看下模拟数据组装的方法代码

        /// <summary>

        /// 组装小票打印模拟数据

        /// </summary>

        /// <returns></returns>

        private Tuple<bool, Dictionary<string, object>, DataSet> CreateBillData()

        {

            //注意事项

            //小票打印和门票一样,主要的区别是小票动态数据会变化,小票的长度也会动态改变

            //这里主要演示下  动态数据源  为了动态拉伸,除了传入数据源,在模板上面code部分需要加代码

            Dictionary<string, object> dic = new Dictionary<string, object>();

            dic.Add("billNo", "");

            dic.Add("optorName", "管理员");

            //组装dataset数据源

            DataTable dtDetail = new DataTable("dtDetail");

            dtDetail.Columns.Add("GOODSCODE");

            dtDetail.Columns.Add("GOODSNAME");

            dtDetail.Columns.Add("GOODSPRICE");

            dtDetail.Columns.Add("GOODSCOUNT");

            dtDetail.Columns.Add("PAYSUM");

            //加10种商品

            for (int i = ; i <= ; i++)

            {

                dtDetail.Rows.Add("" + , "测试商品" + i, 10.00m, , 50.00m);

            }

            DataSet dsFrx = new DataSet();

            dsFrx.Tables.Add(dtDetail);

            return new Tuple<bool, Dictionary<string, object>, DataSet>(true, dic, dsFrx);

        }

运行,在设计器里面也可以预览,看下最终打印效果,如图15

图15

四、总结

使用这个做打印模板还是比较方便的,在套打情况下要频繁调整界面布局,使用这种可视化的界面操作方便。经常客户自己就可以自定义调整。不需要程序做任何修改。

这个FastReport.Net的具体使用方法可以查看网上资料,我这里主要是作为打印模板来用。很多细节以及用法就没展开细讲。

因为FastReport是商业软件。支持软件版权。针对商业版权问题,FastReport提供了开源版本,在nuget就可以直接引用,作为报表功能有删减,但针对打印功能完全够用了。

demo下载

感谢阅读,希望这篇文章能给你带来帮助!

重复造轮子系列——基于FastReport设计打印模板实现桌面端WPF套打和商超POS高度自适应小票打印的更多相关文章

  1. 重复造轮子系列——基于Ocelot实现类似支付宝接口模式的网关

    重复造轮子系列——基于Ocelot实现类似支付宝接口模式的网关 引言 重复造轮子系列是自己平时的一些总结.有的轮子依赖社区提供的轮子为基础,这里把使用过程的一些觉得有意思的做个分享.有些思路或者方法在 ...

  2. 重复造轮子系列--内存池(C语言)

    这个代码是我上个公司工作项目的里面内存管理(基于伙伴算法)的一个简化又简化的版本. 因为没有内存边界检查: 因为没有内存使用统计: 因为没有考虑线程安全: 因为没有内存分配操作的具体文件位置信息: 因 ...

  3. 重复造轮子系列--dijkstra算法

    前年一时脑热(理想很丰满,现实很骨感),写了这个最短路径优先的低效版本,且留着回忆吧. spf.h #ifndef SPF_H_ #define SPF_H_ typedef struct { int ...

  4. 重复造轮子系列--字符串处理(C语言)

    这些字符代码是以前写的,源于很久很久以前的一个VC++项目,在当时的部门编程比赛里因为用了项目代码的xsplit函数,万万没想到,那个做了几年的项目里面居然有坑..xsplit函数居然不能split连 ...

  5. GitHub Android 最火开源项目Top20 GitHub 上的开源项目不胜枚举,越来越多的开源项目正在迁移到GitHub平台上。基于不要重复造轮子的原则,了解当下比较流行的Android与iOS开源项目很是必要。利用这些项目,有时能够让你达到事半功倍的效果。

    1. ActionBarSherlock(推荐) ActionBarSherlock应该算得上是GitHub上最火的Android开源项目了,它是一个独立的库,通过一个API和主题,开发者就可以很方便 ...

  6. 动手造轮子:基于 Redis 实现 EventBus

    动手造轮子:基于 Redis 实现 EventBus Intro 上次我们造了一个简单的基于内存的 EventBus,但是如果要跨系统的话就不合适了,所以有了这篇基于 Redis 的 EventBus ...

  7. 避免重复造轮子的UI自动化测试框架开发

    一懒起来就好久没更新文章了,其实懒也还是因为忙,今年上半年的加班赶上了去年一年的加班,加班不息啊,好了吐槽完就写写一直打算继续的自动化开发 目前各种UI测试框架层出不穷,但是万变不离其宗,驱动PC浏览 ...

  8. 第27篇 重复造轮子---模拟IIS服务器

    在写程序的时候,重复造轮子是程序员的一个大忌,很多人对重复造轮子持有反对的态度,但是我觉得这个造轮子的过程,是对于现有的知识的一个深入的探索的过程,虽然我们不可能把轮子造的那么的完善,对于现在有的东西 ...

  9. 54 个官方 Spring Boot Starters 出炉!别再重复造轮子了…….

    在之前的文章,栈长介绍了 Spring Boot Starters,不清楚的可以点击链接进去看下. 前段时间 Spring Boot 2.4.0 也发布了,本文栈长再详细总结下最新的 Spring B ...

随机推荐

  1. SYN4201型 同步分频钟

    SYN4201型 同步分频钟 产品概述 SYN4201型同步分频钟是由西安同步电子科技有限公司精心设计.自行研发生产的一款高精度分频时钟,对输入的8路10MHz正弦信号分别进行同步分频处理,相应的输出 ...

  2. C语言实现常用排序算法——插入排序

    插入排序是最基础的排序算法,原理: 首先1个元素肯定是有序的,所以插入排序从第二个元素开始遍历:内循环首先请求一个空间保存待插入元素,从当前元素向数组起始位置反向遍历:当发现有大于待插入元素的元素,则 ...

  3. screen命令使用简单说明

    首先用screen [-S]命令建立一个session,然后就可以在这个session中建立多个window了.使用screen [-S]可以建立多个session,而每个session又可以建立多个 ...

  4. 留存: struts2+jquery+json集成

    原文地址:struts2+jquery+json集成 以下采用struts2+jquery+json模拟一个案例.当点击提交按钮时会把输入的数据提交到后台,然后从后台获取数据在客户端显示. 效果如下: ...

  5. 从理论到实践,全方位认识HTTP/2

    前言   为了降低加载时间,相信大多数人都做过如下尝试   - Keep-alive: TCP持久连接,增加了TCP连接的复用性,但只有当上一个请求/响应完全 完成后,client才能发送下一个请求 ...

  6. Akka-CQRS(14)- Http标准安全解决方案:OAuth2-资源使用授权

    上一篇讨论了SSL/TLS安全连接,主要是一套在通信层面的数据加密解决方案.但我们更需要一套方案来验证客户端.要把不能通过验证的网络请求过滤掉. OAuth2是一套行业标准的网络资源使用授权协议,也就 ...

  7. 【Linux】一步一步学Linux——虚拟机简介和系统要求(04)

    目录 00. 目录 01. VMware Workstation Pro15介绍 02. Workstation Pro 的主机系统要求 03. 虚拟机网络连接支持 04. 参考 00. 目录 @ 0 ...

  8. 用 IQ分布模拟图来测试浏览器的性能

    今天天气太凉快,跟这个日历上属于夏天的那一页显得格格不入!就连我我床下那台废弃的ThinkPad,居然也十分透凉气,那外壳连我的体温高都没有,于是,我就开始想一个方法,让我那个废弃的电脑发热,顺便用它 ...

  9. Java基础介绍运行机制笔记

    1. 基础知识点图解 编程语言核心结构:变量.基本语法.分支.循环.数组.…… Java面向对象的核心逻辑:OOP.封装.继承.多态.接口…… 开发Java SE高级应用程序:异常.集合.I/O.多线 ...

  10. HDU XXXX:求[L,R]的素数数量(数位DP)

    Problem G Time Limit : 2000/1000ms (Java/Other)   Memory Limit : 131072/131072K (Java/Other) Total S ...