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