重复造轮子系列——基于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. epoll模型的探索与实践

    我们知道nginx的效率非常高,能处理上万级的并发,其之所以高效离不开epoll的支持, epoll是什么呢?,epoll是IO模型中的一种,属于多路复用IO模型; 到这里你应该想到了,select, ...

  2. thinkphp5ajax分頁&&搜索後分頁

    //控制器層 //分頁 public function list_january_table(){ //設置當前頁 $page = input("post.page") ? inp ...

  3. Spark学习之路(十)—— Spark SQL 外部数据源

    一.简介 1.1 多数据源支持 Spark支持以下六个核心数据源,同时Spark社区还提供了多达上百种数据源的读取方式,能够满足绝大部分使用场景. CSV JSON Parquet ORC JDBC/ ...

  4. vscode+vagrant+xdebug调试

    配置: { "name": "Listen for XDebug", "type": "php", "requ ...

  5. CLR 垃圾回收算法

    c#相较于c,c++而言,在内存管理上为程序员提供了极大的方便,解放了程序员与内存地址打交道,提高了程序员的工作效率.比如c中分配的malloc堆空间没有释放导致的内存泄露,数组越界导致的踩内存错误, ...

  6. Java学习笔记——Linux下安装配置tomcat

    朝辞白帝彩云间,千里江陵一日还. 两岸猿声啼不住,轻舟已过万重山. ——早发白帝城 首先需要安装配置JDK,这里简单回顾下.Linux下用root身份在/opt/文件夹下创建jvm文件夹,然后使用ta ...

  7. 浅谈block, inline和inline-block的区别

    block 块元素    inline 内联元素 常见的块元素有:div, p, h1~h6, table, form, ol, ul等 常见的内联元素有:span, a, strong, em, l ...

  8. HTML5新增的标签与属性

    一.关于DTD HTML5 不基于 SGML,所以不需要引用 DTD(HTML 4.01 基于 SGML) 二.HTML5结构标签 <header> 标记定义一个页面或一个区域的头部 &l ...

  9. PHP弱类型总结

    0x01:“==”和“===” PHP中有两种比较符号,“==”与“===”.“==”我们称之为等值符,当等号两边为相同类型时,直接比较值是否相等:当等号两边类型不同时,先转换为相同的类型,再对转换后 ...

  10. [apue] 使用 popen/pclose 的一点疑问

    当我们需要将输出作为标准输入传递给一个命令,或者将一个命令的输出作为标准输入来读取, 一般会想到使用pipe与fork相结合的方式,来重定向标准输入/输出给指定命令. popen/pclose 帮助我 ...