這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、小票列印 目前市面上的小票印表機大多採用的列印指令集為ESC/POS指令,它可以使用ASCII碼、十進位、十六進位來控制列印,我們可以使用它來控制字體大小、列印排版、字體加粗、下劃線、走紙、切紙、控制錢箱等,下麵以初始化印表機為例: ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
一、小票列印
目前市面上的小票印表機大多採用的列印指令集為ESC/POS指令,它可以使用ASCII碼、十進位、十六進位來控制列印,我們可以使用它來控制字體大小、列印排版、字體加粗、下劃線、走紙、切紙、控制錢箱等,下麵以初始化印表機為例:
ASCII碼 ESC @ 十進位碼 27 64 十六進位 1B 40
小票列印紙的寬度一般可分58mm和80mm,這裡指的是列印紙的寬度,但是在實際列印的時候,有效列印區域並沒有這麼寬。
印表機紙寬58mm,頁的寬度384,字元寬度為1,每行最多盛放32個字元 印表機紙寬80mm,頁的寬度576,字元寬度為1,每行最多盛放48個字元
上面說的字元指的是列印到小票上的內容,其中數字和字母占1個字元,中文占2個字元,也就是說,如果使用58mm的列印紙,一行最多可以列印16個漢字或者32個數字 。
當然這是在不改變字體大小的情況下,如果我們改變了字體大小,那麼一行盛放的內容也會改變。
//控制字元大小 ASCII碼 GS ! n 十進位碼 29 33 n 十六進位 1D 21 n
1.這裡的n是一個變數, 0 ≤ n ≤ 255
2.用二進位表示,n的取值範圍就是00000000到11111111,其中二進位的前四位用來控制寬度,後四位用來控制高度。0000表示不變,0001表示放大2倍,0002表示放大3倍,以此類推
3.該命令對所有字元(英數字元和漢字) 有效。
4.預設值:n = 0
下麵我們來看一下字元的不同放大倍數(這裡的1倍,表示使用預設大小):
放大倍數 | n(二進位) | n(十進位) |
---|---|---|
寬度1倍,高度1倍 | 00000000 | 0 |
寬度1倍,高度2倍 | 00000001 | 1 |
寬度1倍,高度3倍 | 00000002 | 2 |
寬度2倍,高度1倍 | 00010000 | 16 |
寬度2倍,高度2倍 | 00010001 | 17 |
寬度2倍,高度3倍 | 00010002 | 18 |
寬度3倍,高度1倍 | 00020000 | 32 |
寬度3倍,高度2倍 | 00020001 | 33 |
寬度3倍,高度3倍 | 00020002 | 34 |
PS:列印紙時間有些長,字跡有些模糊,見諒
列印指令封裝
// 印表機紙寬58mm,頁的寬度384,字元寬度為1,每行最多盛放32個字元 // 印表機紙寬80mm,頁的寬度576,字元寬度為1,每行最多盛放48個字元 const PAGE_WIDTH = 576; const MAX_CHAR_COUNT_EACH_LINE = 48; //字元串轉位元組序列 function stringToByte(str) { var bytes = new Array(); var len, c; len = str.length; for (var i = 0; i < len; i++) { c = str.charCodeAt(i); if (c >= 0x010000 && c <= 0x10FFFF) { bytes.push(((c >> 18) & 0x07) | 0xF0); bytes.push(((c >> 12) & 0x3F) | 0x80); bytes.push(((c >> 6) & 0x3F) | 0x80); bytes.push((c & 0x3F) | 0x80); } else if (c >= 0x000800 && c <= 0x00FFFF) { bytes.push(((c >> 12) & 0x0F) | 0xE0); bytes.push(((c >> 6) & 0x3F) | 0x80); bytes.push((c & 0x3F) | 0x80); } else if (c >= 0x000080 && c <= 0x0007FF) { bytes.push(((c >> 6) & 0x1F) | 0xC0); bytes.push((c & 0x3F) | 0x80); } else { bytes.push(c & 0xFF); } } return bytes; } //位元組序列轉ASCII碼 //[0x24, 0x26, 0x28, 0x2A] ==> "$&C*" function byteToString(arr) { if (typeof arr === 'string') { return arr; } var str = '', _arr = arr; for (var i = 0; i < _arr.length; i++) { var one = _arr[i].toString(2), v = one.match(/^1+?(?=0)/); if (v && one.length == 8) { var bytesLength = v[0].length; var store = _arr[i].toString(2).slice(7 - bytesLength); for (var st = 1; st < bytesLength; st++) { store += _arr[st + i].toString(2).slice(2); } str += String.fromCharCode(parseInt(store, 2)); i += bytesLength - 1; } else { str += String.fromCharCode(_arr[i]); } } return str; } //居中 function Center() { var Center = []; Center.push(27); Center.push(97); Center.push(1); var strCenter = byteToString(Center); return strCenter; } //居左 function Left() { var Left = []; Left.push(27); Left.push(97); Left.push(0); var strLeft = byteToString(Left); return strLeft; } //居右 function Right() { var right = []; Left.push(27); Left.push(97); Left.push(2); var strRight = byteToString(right); return strRight; } //標準字體 function Size1() { var Size1 = []; Size1.push(29); Size1.push(33); Size1.push(0); var strSize1 = byteToString(Size1); return strSize1; } //大號字體 /* 放大1倍 n = 0 * 長寬各放大2倍 n = 17 */ function Size2(n) { var Size2 = []; Size2.push(29); Size2.push(33); Size2.push(n); var strSize2 = byteToString(Size2); return strSize2; } // 字體加粗 function boldFontOn() { var arr = [] arr.push(27) arr.push(69) arr.push(1) var cmd = byteToString(arr); return cmd } // 取消字體加粗 function boldFontOff() { var arr = [] arr.push(27) arr.push(69) arr.push(0) var cmd = byteToString(arr); return cmd } // 列印並走紙n行 function feedLines(n = 1) { var feeds = [] feeds.push(27) feeds.push(100) feeds.push(n) var printFeedsLines = byteToString(feeds); return printFeedsLines } // 切紙 function cutPaper() { var cut = [] cut.push(29) cut.push(86) cut.push(49) var cutType = byteToString(cut); return cutType } // 開錢箱 function open_money_box() { var open = [] open.push(27) open.push(112) open.push(0) open.push(60) open.push(255) var openType = byteToString(open) return openType } // 初始化印表機 function init() { var arr = [] arr.push(27) arr.push(68) arr.push(0) var str = byteToString(arr) return str } /* 設置左邊距 len: */ function setLeftMargin(len = 1) { var arr = [] arr.push(29) arr.push(76) arr.push(len) var str = byteToString(arr) return str } // 設置列印區域寬度 function setPrintAreaWidth(width) { var arr = [] arr.push(29) arr.push(87) arr.push(width) var str = byteToString(arr) return str } /** * @param str * @returns {boolean} str是否全是中文 */ function isChinese(str) { return /^[\u4e00-\u9fa5]$/.test(str); } // str是否全含中文或者中文標點 function isHaveChina(str) { if (escape(str).indexOf("%u") < 0) { return 0 } else { return 1 } } /** * 返回字元串寬度(1個中文=2個英文字元) * @param str * @returns {number} */ function getStringWidth(str) { let width = 0; for (let i = 0, len = str.length; i < len; i++) { width += isHaveChina(str.charAt(i)) ? 2 : 1; } return width; } /** * 同一行輸出str1, str2,str1居左, str2居右 * @param {string} str1 內容1 * @param {string} str2 內容2 * @param {string} fillWith str1 str2之間的填充字元 * @param {number} fontWidth 字元寬度 1/2 * */ function inline(str1, str2, fillWith = ' ', fontWidth = 1) { const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth; // 需要填充的字元數量 let fillCount = lineWidth - (getStringWidth(str1) + getStringWidth(str2)) % lineWidth; let fillStr = new Array(fillCount).fill(fillWith.charAt(0)).join(''); return str1 + fillStr + str2; } /** * 用字元填充一整行 * @param {string} fillWith 填充字元 * @param {number} fontWidth 字元寬度 1/2 */ function fillLine(fillWith = '-', fontWidth = 1) { const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth; return new Array(lineWidth).fill(fillWith.charAt(0)).join(''); } /** * 文字內容居中,左右用字元填充 * @param {string} str 文字內容 * @param {number} fontWidth 字元寬度 1/2 * @param {string} fillWith str1 str2之間的填充字元 */ function fillAround(str, fillWith = '-', fontWidth = 1) { const lineWidth = MAX_CHAR_COUNT_EACH_LINE / fontWidth; let strWidth = getStringWidth(str); // 內容已經超過一行了,沒必要填充 if (strWidth >= lineWidth) { return str; } // 需要填充的字元數量 let fillCount = lineWidth - strWidth; // 左側填充的字元數量 let leftCount = Math.round(fillCount / 2); // 兩側的填充字元,需要考慮左邊需要填充,右邊不需要填充的情況 let fillStr = new Array(leftCount).fill(fillWith.charAt(0)).join(''); return fillStr + str + fillStr.substr(0, fillCount - leftCount); }
也就是說,如果我們使用的印表機採用的是ESC/POS指令集(我這裡使用過佳博、芯燁、斯普瑞特印表機),只要我們想辦法把列印指令發送給印表機,印表機就可以識別到並且進行列印等操作。那麼我們該如何發送呢?
1.藍牙印表機
參考掘金 zgt_不夢的文章 微信小程式連接藍牙印表機列印圖片示例
- 初始化藍牙模塊 wx.openBluetoothAdapter()
- 初始化完成後搜尋附近的藍牙設備 wx.startBluetoothDevicesDiscovery()
- 監聽尋找到新設備的事件 wx.onBluetoothDeviceFound()
- 在監聽尋找到新設備的事件回調中獲取所有藍牙設備列表 wx.getBluetoothDevices()
- 連接低功耗藍牙設備 wx.createBLEConnection()
- 連接成功後獲取藍牙設備服務 wx.getBLEDeviceServices()
- 在服務中取(notify=true || indicate=true) && write=true 的特征值的 uuid: wx.getBLEDeviceCharacteristics()
- 完成後停止搜尋 wx.stopBluetoothDevicesDiscovery()
- 向低功耗藍牙設備特征值中寫入二進位數據 wx.writeBLECharacteristicValue()
- 離開頁面時取消藍牙連接 wx.closeBLEConnection()
- 關閉藍牙模塊 wx.closeBluetoothAdapter()
親測,好使!在uniapp也可以,只需替換對應的API即可
2.網口印表機
這裡我使用的scoket連接,相比於USB列印,這裡需要保證印表機和安卓設備在同一區域網下。好處是安卓設備可以和印表機距離較遠(比如廚房列印)。這裡以斯普瑞特印表機為例:[斯普瑞特官網 www.sprinter.com.cn/在進行數據通信之前,我們需要知道印表機在此區域網下的 IP, 下圖為“一鍵配網”工具
通過這個工具我們可以方便快捷的查詢到印表機的IP,或者可以根據空閑的網段來修改預設分配的IP,斯普瑞特POS印表機的埠是9100。
如果是其他品牌的印表機,我們也可以使用arp命令來查看當前區域網下的IP
拿到印表機的IP之後我們怎麼來測試一下印表機呢?
我們可以使用telnet命令(這個在Windows系統一般預設是關閉的,需要我們手動打開)
//telnet + 空格 + ip + 空格 + 埠號 telnet 192.168.5.6 9100
打開命令行視窗輸入telnet命令,按下回車
如果埠關閉或者無法連接,則顯示不能打開到主機的鏈接,鏈接失敗;埠打開的情況下,鏈接成功,則進入telnet頁面(全黑的),證明埠可用。
連接成功後,我們輸入任何內容後,按下回車,印表機就會列印我們剛纔輸入的內容。
接下來我們要使用scoket來連接安卓設備和印表機,這裡我使用的是uniapp
/** * 調用tcp通信進行列印 * @param {buffer} buffer 列印數據 * @param {object} printerInfo 印表機對象{IP:'',PORT:''} */ function tcpWrite(buffer, printerInfo) { var Socket = plus.android.importClass("java.net.Socket"); var PrintWriter = plus.android.importClass("java.io.PrintWriter"); var BufferedWriter = plus.android.importClass("java.io.BufferedWriter"); var OutputStreamWriter = plus.android.importClass("java.io.OutputStreamWriter"); var BufferedReader = plus.android.importClass("java.io.BufferedReader"); var InputStreamReader = plus.android.importClass("java.io.InputStreamReader"); var InetSocketAddress = plus.android.importClass("java.net.InetSocketAddress"); //連接 註意:這裡的埠一定是數字類型 var sk = null try { sk = new Socket(printerInfo.IP, Number(printerInfo.PORT)); sk.setSoTimeout(5000); } catch (e) { console.log(e, 'ee') uni.showToast({ icon: 'none', title: '印表機連接失敗' }) } //發送 try { var outputStreamWriter = new OutputStreamWriter(sk.getOutputStream(), "GBK"); var bufferWriter = new BufferedWriter(outputStreamWriter); var out = new PrintWriter(bufferWriter, true); out.println(buffer); //關閉tcp連接 out.close(); } catch (e) { console.log(e, 'ee') uni.showToast({ icon: 'none', title: '印表機數據傳輸失敗' }) } }
列印小票
目前我們已經可以開心的使用列印功能了,只需要組合一下列印指令即可。這裡需要註意的是,如果我們在此之前設置了字元大小寬高均放大2倍,那麼後面列印的字元都會被放大,所以如果後面我們想使用預設字元大小,我們還需要再次設置字元大小為預設來覆蓋之前的指令
//這裡的EscPosUtil.js就是上面封裝的列印指令 import Esc from './EscPosUtil.js'; // 列印文字格式 let strCenter = Esc.Center(); //文字居中 let strLeft = Esc.Left(); //文字靠左 let strSize1 = Esc.Size1(); //預設文字 let strSize2 = Esc.Size2(17); //文字放大兩倍(長寬均為兩倍) let printerInfo = { IP:'192.168.5.6', PORT: 9100 } let strCmd = strCenter + Esc.Size2(17) + Esc.boldFontOn() + '測試門店'+ "\n"; strCmd += strSize1 + Esc.fillLine(' ') + "\n" strCmd += strCenter + Esc.Size2(17) + Esc.boldFontOn() + '結賬單-堂食' + "\n"; strCmd += strSize1 + Esc.fillLine(' ') + "\n" strCmd += strLeft + Esc.Size2(17) + "取餐號:" + '62' + "\n"; strCmd += Esc.inline('桌號:' + '牡丹廳', '人數:' + '6', ' ', 2) + "\n" strCmd += Esc.boldFontOff() + strSize1 + Esc.fillLine(' ') + "\n" strCmd += strLeft + strSize1 + "訂單號:" + '202305171749110001' + "\n"; // 商品信息 strCmd += Esc.fillAround('商品') + "\n" // 票尾 strCmd += Esc.fillLine(' ') + "\n" strCmd += strCenter + '歡迎下次光臨!' + "\n"; strCmd += Esc.feedLines(4) + "\n" // 切紙 strCmd += Esc.cutPaper() tcpWrite(strCmd, printerInfo)
列印效果(這裡僅為展示,非上述代碼列印)
3.USB印表機
這裡我使用的是uniapp插件市場的插件,如果你瞭解安卓原生開發,你也可以自己製作一個原生插件,或者使用Native.js開發。使用原生插件在本地調試需要先打包“自定義調試基座”,在本地測試後再打正式包。
在使用USB插件後,我們可以監聽USB設備的插入和拔出,在初始化之後,我們可以進行數據通信,將上面封裝的列印指令傳給印表機即可
二、網頁列印
由於是網頁運行在瀏覽器中,所以我們只能使用瀏覽器給我們提供的API
1.windows.print()
這個API在不同的瀏覽器中會有差異,其作用就是可以把網頁中的body元素列印出來,如果我們不想列印整個body元素,則需要將body的innerHTML替換。使用這種方式有時有些頁面樣式會和列印出來的不一樣,那麼我們就要使用其他方式來優化。
//使用方法 document.body.innerHTML = newstr; // 把需要列印的指定內容賦給body window.print();
1.1使用媒體查詢
@media print { //把需要列印時才用到的樣式寫到這裡 p{ font-size:16px; } }同理,你也可以直接在CSS文件或者style標簽中加上 media="print"
<style media="print"> //CSS代碼 </style>
1.2監聽列印事件
//監聽列印之前的事件 window.onbeforeprint = function() { //可以修改元素樣式 } //監聽列印之後的事件 window.onafterprint = function() { //恢復之前的樣式 }
1.3分頁符
1.3.1 page-break-before 指定元素前插入分頁符
1.3.2 page-break-after 指定元素後插入分頁符
值 | 描述 |
---|---|
auto | 預設。如果必要則在元素後插入分頁符。 |
always | 在元素後插入分頁符。 |
avoid | 避免在元素後插入分頁符。 |
left | 在元素之後足夠的分頁符,一直到一張空白的左頁為止。 |
right | 在元素之後足夠的分頁符,一直到一張空白的右頁為止。 |
inherit | 規定應該從父元素繼承 page-break-after 屬性的設置。 |
1. 您不能對絕對定位的元素使用此屬性。
2. 請儘可能少地使用分頁屬性,並且避免在表格、浮動元素、帶有邊框的塊元素中使用分頁屬性。
3. 任何版本的Internet Explorer(包括IE8)支持屬性值"left","right",和"inherit"。
4. Firefox,Chrome和Safari不支持屬性值"avoid","left"和"right"。.
@media print { footer {page-break-after: always;} }
1.3.3 page-break-inside 設置是否在指定元素中插入分頁符
值 | 描述 |
---|---|
auto | 預設。如果必要則在元素內部插入分頁符。 |
avoid | 避免在元素內部插入分頁符。 |
inherit | 規定應該從父元素繼承 page-break-inside 屬性的設置。 |
- 您不能對絕對定位的元素使用此屬性。
- 請儘可能少地使用分頁屬性,並且避免在表格、浮動元素、帶有邊框的塊元素中使用分頁屬性。
- IE8 及更早IE版本不支持 "inherit" 屬性。
- Firefox, Chrome, 以及 Safari 不支持屬性值 "avoid".
//避免在 <pre> 與 <blockquote> 元素中插入分頁符: @media print { pre, blockquote {page-break-inside: avoid;} }
1.4設置紙張
@page: 用來設置頁面大小、邊距、方向等
//portrait:縱向; landscape: 橫向 @page { size: A4 portrait; //設置紙張及其方向 這裡表示使用A4紙張,列印方向為縱向 margin: 3.7cm 2.6cm 3.5cm; //設置紙張外邊距 } // 去除頁眉 @page { margin-top: 0; } // 去除頁腳 @page { margin-bottom: 0; }
值得註意的是,如果我們使用的印表機是黑白列印的,比如針式印表機,那麼我們使用的顏色最好是 #000,如果使用 #999這種灰色,列印效果會很不清晰