用 Bartender 實現(xiàn)標(biāo)簽打印的工程實踐)
簡介這份資源面向使用 Delphi 進行桌面應(yīng)用開發(fā)、并需要在程序中集成標(biāo)簽打印功能的開發(fā)者核心解決如何通過 ActiveX 控件在 Delphi 環(huán)境下調(diào)用 Bartender 完成標(biāo)簽設(shè)計與打印的問題。資源包內(nèi)共 1 個 doc 文檔壓縮包約 392KB內(nèi)容圍繞 Bartender 的 ActiveX 調(diào)用方式展開涵蓋控件安裝、項目創(chuàng)建、控件引入、API 調(diào)用及功能測試等關(guān)鍵環(huán)節(jié)并梳理了標(biāo)簽設(shè)計、打印輸出、數(shù)據(jù)庫管理等主要功能模塊同時說明了該方案在提升工作效率、增強靈活性和控制成本方面的優(yōu)勢。目前已有 389 人學(xué)習(xí)瀏覽適合具備一定 Delphi 基礎(chǔ)、希望快速掌握 Bartender 集成思路的開發(fā)者參考可幫助讀者理解 ActiveX 調(diào)用流程與常見功能接口為實際項目中的標(biāo)簽打印需求提供可落地的實現(xiàn)方向。1. 從一張標(biāo)簽說起Delphi 調(diào) Bartender 到底在調(diào)什么車間里那臺老產(chǎn)線還在跑工控機上掛著一個 Delphi 寫的進銷存操作員點一下“打印標(biāo)簽”旁邊那臺條碼機就吐出一張帶批次號的標(biāo)簽。這套東西看著土但背后其實是一個很典型的組合Delphi 負(fù)責(zé)業(yè)務(wù)邏輯和界面Bartender 負(fù)責(zé)標(biāo)簽?zāi)0搴痛蛴◎?qū)動。兩者之間靠的不是什么神秘協(xié)議就是 ActiveX 自動化。很多人第一次聽到“Delphi 調(diào)用 Bartender”會以為要裝什么 SDK、寫什么驅(qū)動其實 Bartender 本身就暴露了一個 ActiveX 控件Delphi 作為老牌 COM 宿主直接導(dǎo)入類型庫就能用。核心對象就一個BarTender.Application拿到它之后打開模板、改字段、發(fā)打印指令三步走完。適合誰適合手里有存量 Delphi 項目、又不想把標(biāo)簽打印重寫成 .NET 或者 Python 的團隊。下面把我自己拆這套東西的過程和踩過的坑攤開講。2. 把 ActiveX 導(dǎo)進 Delphi類型庫導(dǎo)入與對象模型2.1 為什么是 ActiveX 而不是命令行Bartender 其實提供了好幾種被調(diào)用的方式常見的有命令行參數(shù)、ActiveX 自動化、還有 .NET 的 SDK。命令行最省事但只能傳有限參數(shù)改字段要靠/F加文件路徑動態(tài)性差.NET SDK 功能全但 Delphi 調(diào) .NET 得走 COM 互操作或者托管封裝多一層就多一層玄學(xué)。ActiveX 自動化是中間那條路Bartender 安裝時注冊了BarTender.Application這個 ProgIDDelphi 通過 COM 直接拿到接口既能讀模板里的具名字段也能觸發(fā)打印還能拿到打印狀態(tài)。選它的理由很實在不用額外裝運行時不用改項目架構(gòu)Delphi 7 到 Delphi 11 都能用。代價是 Bartender 必須是完整版或者至少是支持自動化的版本某些精簡版或者試用版在自動化接口上會有限制這個后面避坑章節(jié)會細(xì)說。2.2 在 Delphi IDE 里導(dǎo)入類型庫操作路徑不復(fù)雜但版本差異會讓人翻車。以 Delphi 10.x 為例打開項目菜單Component→Import Component。選Import a Type Library在列表里找BarTender 20xx Type Library版本號取決于你裝的 Bartender 年份。單位名默認(rèn)是BarTender_TLB別改后面引用方便。勾選Generate Component Wrapper會多生成一個組件面板頁不勾也行直接CreateUnit方式用接口。導(dǎo)入完成后Delphi 會在項目里生成一個.pas文件里面定義了IApplication、IFormat、IDocument這些接口。這里有個細(xì)節(jié)不同年份的 Bartender 類型庫接口名可能微調(diào)比如早期版本IApplication的方法叫Open新版本可能多了重載。導(dǎo)入后先別急著寫業(yè)務(wù)編譯一下看有沒有E2010 Incompatible types之類的報錯有的話說明類型庫和當(dāng)前 Delphi 的 COM 聲明有沖突常見做法是手動改一下生成的.pas里那幾個IDispatch的聲明。// 導(dǎo)入類型庫后在 uses 里加上 BarTender_TLB uses ComObj, ActiveX, BarTender_TLB; var BtApp: IApplication; BtFormat: IFormat; begin // 創(chuàng)建 Bartender 應(yīng)用實例ProgID 就是注冊表里的名字 BtApp : CoApplication.Create; try // 打開一個已經(jīng)設(shè)計好的標(biāo)簽?zāi)0迓窂綄懡^對路徑 BtFormat : BtApp.Formats.Open(D:\Labels\product_label.btw, False, ); // 改一個具名字段字段名要和模板里定義的完全一致 BtFormat.SetNamedSubStringValue(BatchNo, 20240521-001); // 打印一份到默認(rèn)打印機 BtFormat.PrintOut(False, False); finally // 退出 Bartender釋放 COM 對象 BtApp.Quit(btDoNotSaveChanges); BtApp : nil; end; end;這段代碼的邏輯很直白CoApplication.Create走的是 COM 的類工廠Formats.Open打開模板SetNamedSubStringValue改字段PrintOut觸發(fā)打印。參數(shù)說明一下Open的第二個參數(shù)是ReadOnly傳False表示可寫第三個參數(shù)是Password模板沒加密就傳空串。PrintOut第一個參數(shù)ShowPrintDialog傳False表示不彈窗直接打第二個參數(shù)WaitForCompletion傳False表示異步打完就返回不等打印機響應(yīng)。2.3 對象模型里真正要關(guān)心的三個接口導(dǎo)入之后接口一大堆但日常用的就三個接口作用關(guān)鍵方法IApplication應(yīng)用級入口Formats.Open、QuitIFormat單個標(biāo)簽?zāi)0錝etNamedSubStringValue、PrintOutIDocument打印任務(wù)Print、WaitForCompletionIApplication是根Formats是個集合Open返回IFormat。IFormat上除了改字段還能讀數(shù)據(jù)庫連接、改打印機名。IDocument一般不用手動創(chuàng)建PrintOut內(nèi)部會生成。如果你要精確控制打印份數(shù)用IFormat.PrintOut的第三個參數(shù)Copies或者拿IDocument的Print方法。3. 動態(tài)改字段與打印從模板具名到數(shù)據(jù)庫聯(lián)動3.1 具名字段和數(shù)據(jù)庫字段的區(qū)別Bartender 模板里改數(shù)據(jù)有兩種路子一種是具名字段Named SubString一種是數(shù)據(jù)庫字段綁定。具名字段是模板設(shè)計時手動在對象屬性里起的名字比如BatchNo、ProductName調(diào)用方通過SetNamedSubStringValue直接塞值。數(shù)據(jù)庫字段是模板里配了 ODBC 或者文本文件數(shù)據(jù)源打印時 Bartender 自己去查。選哪個如果數(shù)據(jù)來自 Delphi 的業(yè)務(wù)邏輯比如剛算出來的批次號用具名字段最直接不用配數(shù)據(jù)源。如果數(shù)據(jù)量大、字段多比如從 SQL Server 里拉一整批產(chǎn)品信息那在模板里配數(shù)據(jù)庫連接更省事Delphi 只負(fù)責(zé)觸發(fā)打印。我一般混合用關(guān)鍵動態(tài)字段用具名靜態(tài)的或者批量數(shù)據(jù)用數(shù)據(jù)庫綁定。// 假設(shè)模板里配了一個文本文件數(shù)據(jù)源路徑是 D:\Data\products.txt // 這里演示用具名字段覆蓋其中一列再觸發(fā)打印 BtFormat.SetNamedSubStringValue(ProductName, 工業(yè)級條碼打印機); BtFormat.SetNamedSubStringValue(Price, 1280.00); // 如果模板里綁了數(shù)據(jù)庫可以強制刷新數(shù)據(jù)源 BtFormat.Database.Refresh; // 打印兩份不彈窗異步 BtFormat.PrintOut(False, False, 2);Database.Refresh這個調(diào)用在數(shù)據(jù)庫源是文件時特別有用因為 Bartender 默認(rèn)會緩存文件內(nèi)容不刷新的話打出來的還是上一次的數(shù)據(jù)。PrintOut第三個參數(shù)是份數(shù)傳2就是打兩張。注意異步模式下如果連續(xù)快速調(diào)用PrintOutBartender 內(nèi)部會排隊但打印機可能跟不上后面避坑章節(jié)會講怎么加延遲。3.2 打印機選擇和頁面設(shè)置Bartender 模板里可以預(yù)設(shè)打印機但工控機上打印機名可能變比如從Zebra ZT230換成TSC TTP-244。這時候在 Delphi 里動態(tài)改打印機比重新設(shè)計模板快。// 獲取當(dāng)前模板的打印機設(shè)置 BtFormat.Printer TSC TTP-244 Pro; // 如果模板里頁面大小和實際標(biāo)簽紙不匹配可以改頁面設(shè)置 BtFormat.PageSetup.LabelWidth : 60; // 單位是毫米 BtFormat.PageSetup.LabelHeight : 40; // 打印前先預(yù)覽一下確認(rèn)布局沒跑偏 BtFormat.PrintPreview;Printer屬性直接賦打印機名字符串名字要和 Windows 里顯示的完全一致包括空格和大小寫。PageSetup里的寬高單位取決于 Bartender 的全局設(shè)置默認(rèn)是毫米但有些模板用的是英寸改之前先在 Bartender 界面里看一眼。PrintPreview在調(diào)試階段很有用能彈出一個預(yù)覽窗口確認(rèn)字段替換和布局都對了再正式打。3.3 異常處理和 COM 釋放COM 對象不釋放Bartender 進程會殘留在任務(wù)管理器里打幾次之后內(nèi)存就上去了。Delphi 里IApplication是接口引用計數(shù)歸零會自動釋放但前提是你沒在全局變量里一直持有它。procedure TForm1.PrintLabel(const BatchNo: string); var BtApp: IApplication; BtFormat: IFormat; begin BtApp : nil; BtFormat : nil; try BtApp : CoApplication.Create; BtFormat : BtApp.Formats.Open(D:\Labels\product_label.btw, False, ); BtFormat.SetNamedSubStringValue(BatchNo, BatchNo); BtFormat.PrintOut(False, False); except on E: Exception do begin // 記錄日志別讓異常吞掉 LogError(Bartender print failed: E.Message); end; end; // 顯式置 nil觸發(fā)引用計數(shù)釋放 BtFormat : nil; if Assigned(BtApp) then begin BtApp.Quit(btDoNotSaveChanges); BtApp : nil; end; end;這段代碼的關(guān)鍵在finally之前就把BtFormat置 nil然后Quit再置 nil。順序反了的話Quit之后BtFormat還持有引用Bartender 進程可能不會立刻退出。btDoNotSaveChanges是枚舉值表示退出時不保存模板改動避免誤改模板文件。4. 避坑與排查那些讓我加班到凌晨的報錯4.1 “Class not registered” 或 “ActiveX component cant create object”現(xiàn)象CoApplication.Create直接拋異常提示類未注冊。原因Bartender 安裝時沒有注冊 ActiveX 組件或者裝的是精簡版/試用版自動化接口被閹割。還有一種可能是 32 位和 64 位不匹配——Delphi 編譯的是 32 位程序但 Bartender 裝的是 64 位版本注冊表里 ProgID 在Wow6432Node下找不到。解決先確認(rèn) Bartender 版本完整版在安裝時會有“注冊 ActiveX 組件”的選項沒勾的話重新運行安裝程序修復(fù)。然后確認(rèn) Delphi 目標(biāo)平臺是 32 位還是 64 位和 Bartender 的位數(shù)對齊。如果實在對不齊常見做法是用一個 32 位的中間進程去調(diào) BartenderDelphi 通過命名管道或者消息和中間進程通信但這屬于繞路方案能不用就不用。4.2 打印出來字段沒變還是上一次的數(shù)據(jù)現(xiàn)象代碼里明明改了BatchNo打出來的標(biāo)簽還是舊值。原因Bartender 對模板里的數(shù)據(jù)庫源有緩存尤其是文本文件或者 Excel 數(shù)據(jù)源SetNamedSubStringValue改的是具名字段但如果模板里那個對象綁的是數(shù)據(jù)庫字段具名字段就被覆蓋了。另一種情況是模板打開時用了ReadOnlyTrue改字段被靜默忽略。解決先確認(rèn)模板里那個對象到底是具名字段還是數(shù)據(jù)庫字段在 Bartender 設(shè)計器里看對象屬性。如果是數(shù)據(jù)庫字段要么改成具名字段要么在改完之后調(diào)Database.Refresh。Open的第二個參數(shù)確保傳False。還有個小概率情況模板文件被其他進程占用Open打開的是緩存副本改完沒保存到實際打印的實例上這種就檢查文件鎖。4.3 連續(xù)打印時 Bartender 進程堆積現(xiàn)象批量打印 100 張標(biāo)簽任務(wù)管理器里出現(xiàn)幾十個 Bartender 進程內(nèi)存飆升。原因每次CoApplication.Create都新建一個 Bartender 實例如果沒正確Quit進程就殘留。異步PrintOut還沒完成時Quit被調(diào)用Bartender 可能掛起不退出。解決批量打印時不要每次新建實例復(fù)用一個IApplication打完所有標(biāo)簽再Q(mào)uit。如果必須每次新建在PrintOut之后加一個等待確認(rèn)打印任務(wù)進入隊列再退出。等待可以用IDocument.WaitForCompletion或者簡單點用Sleep(500)但 Sleep 是下策能拿IDocument就拿。// 批量打印時復(fù)用實例的寫法 BtApp : CoApplication.Create; try for i : 0 to BatchList.Count - 1 do begin BtFormat : BtApp.Formats.Open(D:\Labels\product_label.btw, False, ); BtFormat.SetNamedSubStringValue(BatchNo, BatchList[i]); BtFormat.PrintOut(False, False); BtFormat : nil; // 釋放當(dāng)前模板引用 end; finally BtApp.Quit(btDoNotSaveChanges); BtApp : nil; end;4.4 打印機名改了但打到了默認(rèn)打印機現(xiàn)象代碼里設(shè)了BtFormat.Printer : TSC TTP-244 Pro結(jié)果標(biāo)簽從辦公室那臺激光打印機出來了。原因Bartender 模板里可能勾了“使用默認(rèn)打印機”這個選項優(yōu)先級高于代碼里的Printer賦值?;蛘叽蛴C名拼寫有細(xì)微差異比如多了個空格Bartender 找不到就回退到默認(rèn)。解決在 Bartender 設(shè)計器里取消“使用默認(rèn)打印機”的勾選改成指定打印機。代碼里賦值后用BtFormat.Printer讀回來確認(rèn)一下讀回來的名字和設(shè)進去的一致才說明生效了。如果打印機是網(wǎng)絡(luò)打印機名字要用\\服務(wù)器名\打印機名的 UNC 格式。4.5 試用版打印隨機改字符現(xiàn)象用試用版 Bartender 調(diào)試打出來的標(biāo)簽偶爾某個字符變成亂碼或者問號。原因Bartender 試用版有個限制30 天到期后包含試用功能的標(biāo)簽格式在打印時會隨機更改一個字符。這個不是代碼問題是授權(quán)機制。解決換正式授權(quán)或者用 Bartender 的免費版功能有限但不會隨機改字符。如果只是內(nèi)部測試可以在虛擬機里定期重置試用期但這不是長久之計。生產(chǎn)環(huán)境必須用正式授權(quán)否則打出來的標(biāo)簽可能掃不出來這個坑我踩過一次一批貨的條碼全廢了。5. 進階用 Delphi 做一層打印隊列封裝5.1 為什么要封裝直接在每個業(yè)務(wù)按鈕里寫CoApplication.Create和PrintOut短期能跑長期會亂。打印任務(wù)多了之后你需要控制并發(fā)、重試、日志、打印機狀態(tài)回讀。我一般會在 Delphi 里封一個TLabelPrinter類把 Bartender 的調(diào)用包起來業(yè)務(wù)層只調(diào)Print(BatchNo, ProductName)這樣的方法。type TLabelPrinter class private FApp: IApplication; FTemplatePath: string; FPrinterName: string; function EnsureApp: IApplication; public constructor Create(const ATemplatePath, APrinterName: string); destructor Destroy; override; function Print(const Fields: TStrings; Copies: Integer 1): Boolean; end; function TLabelPrinter.EnsureApp: IApplication; begin if not Assigned(FApp) then FApp : CoApplication.Create; Result : FApp; end; function TLabelPrinter.Print(const Fields: TStrings; Copies: Integer): Boolean; var BtFormat: IFormat; i: Integer; begin Result : False; try BtFormat : EnsureApp.Formats.Open(FTemplatePath, False, ); BtFormat.Printer : FPrinterName; for i : 0 to Fields.Count - 1 do BtFormat.SetNamedSubStringValue(Fields.Names[i], Fields.ValueFromIndex[i]); BtFormat.PrintOut(False, False, Copies); BtFormat : nil; Result : True; except on E: Exception do LogError(Print failed: E.Message); end; end;這個封裝的邏輯是EnsureApp懶加載 Bartender 實例避免每次新建Print接收一個TStrings里面是字段名值的鍵值對循環(huán)塞進模板異常統(tǒng)一記日志返回布爾值讓業(yè)務(wù)層知道成功與否。Copies默認(rèn) 1需要多份就傳。5.2 打印狀態(tài)回讀和重試Bartender 的PrintOut異步模式下你只知道指令發(fā)出去了不知道打印機有沒有真的打。要確認(rèn)狀態(tài)得拿IDocument接口。var Doc: IDocument; begin Doc : BtFormat.PrintOut(False, True); // 第二個參數(shù) True 表示等待完成 if Doc.Status btStatusCompleted then LogInfo(Print completed) else LogWarn(Print status: IntToStr(Ord(Doc.Status))); end;PrintOut的第二個參數(shù)傳True會阻塞直到打印完成返回IDocument。Doc.Status是個枚舉btStatusCompleted表示成功其他值對應(yīng)錯誤、取消、缺紙等。重試邏輯就圍繞這個狀態(tài)寫如果狀態(tài)不是完成等兩秒再試一次最多三次。注意阻塞模式下別在主線程里調(diào)會卡界面放到線程里或者用TThread.Queue異步處理。5.3 一個我常用的調(diào)試習(xí)慣每次改完模板或者打印邏輯我不會直接上產(chǎn)線而是先在 Delphi 里調(diào)PrintPreview把預(yù)覽窗口彈出來看一眼。確認(rèn)字段替換、打印機、份數(shù)都對了再改成PrintOut正式打。這個習(xí)慣幫我省了很多廢標(biāo)簽紙。另外模板文件我會用 Git 管起來每次改動都有記錄出問題能回滾。Bartender 的.btw文件是二進制Git 不能 diff但至少能看出版本變化。從那以后我每次接新的 Bartender 調(diào)用需求都強制走一遍先導(dǎo)入類型庫編譯通過再寫最小打印 Demo確認(rèn)字段和打印機都對最后才往業(yè)務(wù)代碼里集成。希望幫到你。本文還有配套的精品資源點擊獲取