重复造*系列——基于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 = 1)
{
bool flag = false;
string msg = "";
FastReport.Report report = new FastReport.Report();
try
{
report.Load(frxPath);
report.DoublePass = true;
if (dicParam != null && dicParam.Count > 0)
{
foreach (var item in dicParam)
{
report.SetParameterValue(item.Key, item.Value);
}
}
if (dsDataSource != null && dsDataSource.Tables.Count > 0)
{
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 = 0; 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 > 0)
{
foreach (var item in dicParam)
{
report.SetParameterValue(item.Key, item.Value);
}
}
if (dsDataSource != null && dsDataSource.Tables.Count > 0)
{
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 = 4;
qrCodeEncoder.QRCodeVersion = 4;
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", "2018111100002222");
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 = 1; i <= 10; i++)
{
dtDetail.Rows.Add("10000" + 1, "测试商品" + i, 10.00m, 5, 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就可以直接引用,作为报表功能有删减,但针对打印功能完全够用了。
感谢阅读,希望这篇文章能给你带来帮助!