ActiveX 小票打印控件开发
背景
去年帮朋友搞一个IE打印控件,在网上找了很多都不满足,要么是不能直接打印,要么是不能指定打印机,还有的是不能自定义样式。在网上折腾了一个周,还没搞定,于是想还不如自己开发一个吧。结果从现学C#到完成差不多只花了两周。过程中也遇到了很多坑,现在分享给大家,并且将项目在GitHub上开源。如果您觉得喜欢,给个Star,深表感谢。
所需软件
- 宇宙IDE Visual Studio 开发并打包控件
- Sublime Text 3 编辑 HTML 和 JavaScript 代码
- Postman 模拟推送 json 数据
总计架构
框架示意图控件开发
- 添加Guid,例如: 073A987E-2A7C-4874-8BEE-321E04F4E84E,作为控件的classid
- 代码初始化打印控件:
public Uc()
{
InitializeComponent();
printDocument = new PrintDocument();
printPreview = new PrintPreviewDialog();
Margins margin = new Margins(1, 1, 1, 1);
printDocument.DefaultPageSettings.Margins = margin;
printDocument.DefaultPageSettings.PaperSize = new PaperSize("Custum", getInch(100), getInch(110));
printDocument.PrintPage += new PrintPageEventHandler(this.printDocument_PrintPage);
foodLength = 10;
countLength = 7;
moneyLength = 8;
}
- 获取并解析json数据,这里要用到C#解析json的库Newtonsoft.Json,通过NuGet包管理器安装,
1、在工具菜单下:
NuGet位置
2、在浏览菜单中搜索安装,这里我已经安装好了:
安装Newtonsoft.Json库
3、引用库:using Newtonsoft.Json;
按照面向对象的思想,我们需要定义实体类Ticket,具体解析代码如下:
public void getData(String json)
{
deserializedTicket = JsonConvert.DeserializeObject<Ticket>(json);
isKitchen = deserializedTicket.isKitchen;
Boolean isPreview = deserializedTicket.isPreview;
String printName = deserializedTicket.printName;
printDocument.PrinterSettings.PrinterName = printName;
printPreview.Document = printDocument;
lineReader = new StringReader(isKitchen ? getKitchenPrintString(deserializedTicket) : getTicketPrintString(deserializedTicket));
try
{
if (isPreview)
{
if (printPreview.ShowDialog() == DialogResult.OK)
{
printDocument.Print();
}
}
else
{
printDocument.Print();
}
}
catch (Exception excep)
{
MessageBox.Show(excep.Message, "打印出错", MessageBoxButtons.OK, MessageBoxIcon.Error);
printDocument.PrintController.OnEndPrint(printDocument, new PrintEventArgs());
}
}
- 打印小票,通过字符串拼接实现,代码如下:
private void printDocument_PrintPage(object sender, PrintPageEventArgs e)
{
Graphics g = e.Graphics; //获得绘图对象
float yPosition = 0; //绘制字符串的纵向位置
int count = 0; //行计数器
float leftMargin = e.MarginBounds.Left; //左边距
float topMargin = e.MarginBounds.Top; //上边距
string line = null; //行字符串
Font printFont = isKitchen ? new Font(new FontFamily("宋体"), 14) : new Font(new FontFamily("宋体"), 10);
Font notesFont = new Font(new FontFamily("宋体"), 12);
Font typeFont = new Font(new FontFamily("黑体"), 20);
SolidBrush brush = new SolidBrush(Color.Black); //刷子
//逐行的循环打印
while ((line = lineReader.ReadLine()) != null)
{
yPosition = topMargin + (count * printFont.GetHeight(g));
if (line.StartsWith("注意"))
{
g.DrawString(line, notesFont, brush, leftMargin, yPosition, new StringFormat());
}
else if (line.StartsWith("点菜") || line.StartsWith("加菜") || line.StartsWith("退菜"))
{
g.DrawString(line, typeFont, brush, leftMargin, yPosition, new StringFormat());
}
else
{
g.DrawString(line, printFont, brush, leftMargin, yPosition, new StringFormat());
}
count++;
}
lineReader = new StringReader(isKitchen ? getKitchenPrintString(deserializedTicket) : getTicketPrintString(deserializedTicket));
}
public string getTicketPrintString(Ticket ticket)
{
StringBuilder sb = new StringBuilder();
string restaurant = ticket.restaurant;
string orderNo = ticket.orderNo;
string address = ticket.address;
string pay = ticket.pay;
string telephone = ticket.telephone;
string mobilephone = ticket.mobilephone;
List<Ticket.Menu> menu = ticket.menu;
int count = menu.Count;
decimal cost = 0.00M;
sb.Append(" " + restaurant + "\n");
sb.Append("\n");
sb.Append("---------------------------------------------------------------\n");
sb.Append("日期:" + DateTime.Now.ToShortDateString() + " " + "单号:" + orderNo + "\n");
sb.Append("---------------------------------------------------------------\n");
sb.Append(padRightTrueLen(" 菜名", foodLength, ' ') + padRightTrueLen("数量", countLength, ' ') +
padRightTrueLen("单价", moneyLength, ' ') + "小计" + "\n");
for (int i = 0; i < count; i++)
{
decimal sum = Decimal.Parse(menu[i].count) * Decimal.Parse(menu[i].price);
sb.Append(padRightTrueLen((menu[i].name), foodLength, ' ') +
padRightTrueLen((" " + menu[i].count), countLength, ' ') +
padRightTrueLen((menu[i].price), moneyLength, ' ') + sum + "\n");
cost += sum;
}
sb.Append("---------------------------------------------------------------\n");
sb.Append("数量:" + count + " 合计:" + cost + "\n");
sb.Append("付款: 现金" + " " + pay);
sb.Append(" 现金找零:" + " " + (Decimal.Parse(pay) - cost) + "\n");
sb.Append("---------------------------------------------------------------\n");
sb.Append("地址:" + address + "\n");
sb.Append("电话:" + telephone + " 手机:" + mobilephone + "\n");
sb.Append("\n");
sb.Append(" 谢谢惠顾,欢迎下次光临 ");
sb.Append("\n");
return sb.ToString();
}
这里只贴出了部分代码,完整代码请移步到GitHub。
打包安装文件
这里需要安装InstallShield,到官网下载安装即可。这里注意要和Visual Studio是同一个版本,安装好后新建一个InstallShield Limited Edition Project项目:
新建InstallShield Limited Edition Project
选择项目作为主输出:
主输出
在InstallShield Limited Edition Project项目上右键,生成安装文件,并安装:
编译
我们也可以看到我们生成的可执行文件:
可执行文件位置
测试
好了,终于可以测试打印了。哦,别着急,还要写HTML,不废话了,直接上效果:
界面效果
我用的是云巴推送,具体实现就不细说了,key要用你自己的了,代码如下:
<!DOCTYPE HTML>
<html>
<head>
<title>Yunba JavaScript Over Socket.IO Demo</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<object id="print" classid="clsid:073A987E-2A7C-4874-8BEE-321E04F4E84E"
width="0" height="0" ></object>
<!--[if lte IE 7]>
<script type="text/javascript" src="javascripts/json2.js"></script>
<![endif]-->
<script type="text/javascript" src="javascripts/socket.io-1.3.5.min.js"></script>
<script type="text/javascript" src="javascripts/yunba-js-sdk.js"></script>
<script type="text/javascript" src="javascripts/jquery-1.10.2.min.js"></script>
<script>
var yunba = new Yunba({appkey: '请用你的key'});
var refresh_flag = getCookie('refresh_flag') == null ? 0 : getCookie('refresh_flag');
yunba.init(function (success) {
if (success) {
$('#msg').html('<div style="color:green">已连接上 socket</div>');
$('#msg').append('<div style="color:green">SocketId: ' + yunba.socket.id) + '</div>';
mqtt_connect();
}
}, function () {
//连接断开后,调用该回调重连。
$('#msg').html('<div style="color:green">连接恢复 socket</div>');
$('#msg').append('<div style="color:green">SocketId: ' + yunba.socket.id) + '</div>';
mqtt_connect();
});
yunba.set_message_cb (function (data) {
document.getElementById("print").getData(data.msg);
$('#msg_end').before('来自频道:' + data.topic);
$('#msg_end').before(' 消息内容:' + data.msg);
console.log(data.msg);
console.log(data);
$('#msg_end').before("\<br\/\>");
if (data.presence) {
console.log(data.presence);
}
msg_end.scrollIntoView();
});
function mqtt_connect() {
yunba.connect(function (success, msg) {
if (success) {
$('#connect_status').html('Connected Success !');
$('#connect_status').css('color', 'green');
if (refresh_flag == 0) {
addCookie('refresh_flag', 1, 0);
window.location.reload();
}
} else {
alert(msg);
}
});
}
function mqtt_disconnect() {
yunba.disconnect(function (success, msg) {
if (success) {
$('#connect_status').html('Disconnected Success !');
$('#connect_status').css('color', 'red');
} else {
alert(msg);
}
});
}
function set_alias() {
if ($('#alias_set').val() == '') {
alert('请输入别名');
return false;
}
var alias = $('#alias_set').val();
yunba.set_alias({'alias': alias}, function (data) {
if (data.success) {
alert('set alias success');
//window.location.reload();
} else {
alert(data.msg);
}
})
}
function get_alias() {
yunba.get_alias( function(data) {
$('#msg_end').before('get alias:' + data.alias);
$('#msg_end').before("\<br\/\>");
msg_end.scrollIntoView();
})
}
//监听关闭浏览器窗口时要断开mqtt连接
// window.onbeforeunload = function(){
// yunba.disconnect();
// }
function addCookie(objName, objValue, objHours){//添加cookie
var str = objName + "=" + escape(objValue);
if (objHours > 0) {//为0时不设定过期时间,浏览器关闭时cookie自动消失
var date = new Date();
var ms = objHours * 3600 * 1000;
date.setTime(date.getTime() + ms);
str += "; expires=" + date.toGMTString();
}
document.cookie = str;
//alert("添加cookie成功");
}
function getCookie(objName){ //获取指定名称的cookie的值
var arrStr = document.cookie.split("; ");
for (var i = 0; i < arrStr.length; i++) {
var temp = arrStr[i].split("=");
if (temp[0] == objName)
return unescape(temp[1]);
}
}
function delCookie(name){//为了删除指定名称的cookie,可以将其过期时间设定为一个过去的时间
var date = new Date();
date.setTime(date.getTime() - 10000);
document.cookie = name + "=a; expires=" + date.toGMTString();
}
</script>
<style>
input {
height: 30px;
font-weight: bold;
}
input[type="radio"] {
height: 15px;
}
.green {
color: green;
}
fieldset {
margin-top: 20px;
}
b {
margin-left: 10px;
}
.clear {
clear: both;
}
</style>
</head>
<body>
<div style="float:left;width:50%">
<fieldset style="height:100px;">
<legend>WebSocket Info</legend>
<div id="msg"></div>
</fieldset>
</div>
<div style="float:left;width:50%">
<fieldset style="height:100px;">
<legend>Connect & Disconnect</legend>
<div id="connect_status"></div>
<input type="button" value="Connect" onclick="mqtt_connect();"/>
<input type="button" value="Disconnect" onclick="mqtt_disconnect();"/>
</fieldset>
</div>
<div class="clear"></div>
<fieldset>
<legend>Subscribe & Unsubscribe</legend>
<div style="float:left;width:50%;">
<fieldset style="height:200px;">
<legend>收到消息</legend>
<div id="msg_list" style="Overflow-y:scroll;height:180px;">
<div id="msg_end" style="height:0px; overflow:hidden"></div>
</div>
</fieldset>
</div>
</fieldset>
<fieldset>
<legend>别名</legend>
别名(Alias): <input type="text" placeholder="输入alias..." id="alias_set"/>
<input type="button" value="Set Alias" onclick="set_alias();"/>
<input type="button" value="Get Alias" onclick="get_alias();"/>
</fieldset>
</body>
<html>
开测,打开Postman,同样 appkey 和 seckey 要用你自己的了:
postman测试
奇迹发生了,打开浏览器,就能看的打印预览:
小票预览
终于结束了,最后我们来看一下,真是打印的效果:
实物图
最后的最后,完整项目GitHub地址:
https://github.com/hfrommane/ActiveX-Print
如果您喜欢,在GitHub上给个Star。如有转载,请注明出处,感谢您!
网友评论