
简介这份 Microsoft Barcode Control 9.0 示例资料面向基于 .NET Framework 的 Windows Forms 开发者围绕条形码生成与打印需求提供可运行的集成参考。压缩包共 30 个文件约 57KB包含 cs 源代码、dll 组件、exe 程序、resx 资源及 csproj/sln 工程文件等既有窗体设计代码也保留编译输出与项目配置便于直接打开解决方案对照控件用法。已有 6438 人学习说明其在条码控件入门与项目实践中具有一定参考价值。通过 Form1 等示例代码读者可了解 Code 39、Code 128、EAN-13 等条码类型的属性设置、数据绑定、绘制事件与打印接口调用并观察校验位处理和动态数据更新方式从而把条码生成逻辑迁移到自己的 Windows 应用中减少从零摸索控件集成的成本。1. 一套十几年前的控件为什么今天还在被翻出来上周帮人看一条标签线现场机器还是 Win7软件是十多年前写完就没再动过的 WinForms。客户要求加个条码负责的工程师试了三个开源库有的 Code128 打出来扫码枪读三次错两次有的中文批号一贴就比例失真。最后翻出来的还是Barcode Control 9.0。这个 ActiveX 条码控件确实不新鲜但它把“出码”和“打纸”两件事绑在同一套对象模型里属性赋值完直接往打印机 DC 上画省掉了位图导出、缩放、DPI 对齐这一串麻烦。它适合的场景很具体老系统加条码、固定格式标签批量打印、下游不是报表引擎而是打印机 DC。如果正好卡在“代码能生成、纸上扫不出”这一步这个包值得拆开看一遍。2. 控件对象模型与码制选型先算清模块宽度再谈打印条码打印失败的原因八九成不在控件本身而在动手写代码之前就没把两件事定下来模块宽度多大用哪种码制。这两个参数一错后面打印链路调得再顺纸上的条码照样是废的。所以先把模型和选型掰开说清楚再谈怎么调用。2.1 三个属性能决定条码能不能被扫出来这套控件的属性加起来几十个真正决定“扫得出扫不出”的其实只有三个模块宽度通常叫BarWidth或NarrowBarWidth、条码高度、静区留白。模块宽度的单位是缇twip1 缇 1/1440 英寸不是像素也不是毫米。很多人第一次用的时候看到BarWidth 2觉得挺宽其实才 0.035 毫米打出来整条码糊成一片黑。拿 300 DPI 的打印机算一下一个点约等于 0.0847 毫米Code128 的模块宽度至少要覆盖 2 个打印点才算稳也就是 0.17 毫米左右换算成缇是 9.6取 10 比较合适。我一般会写个换算函数把毫米当唯一输入别在代码里到处填缇// 缇转毫米毫米转缇统一从这里走避免不同页面重复算错 private const double TwipsPerInch 1440.0; private static int MmToTwips(double mm) (int)Math.Round(mm / 25.4 * TwipsPerInch); private static double TwipsToMm(int twips) twips / TwipsPerInch * 25.4;高度和宽度要分开看。高度不够低于 8 毫米扫码枪在斜角方向读不到高度够但模块宽度不够问题更深肉眼看条码还在设备却读不出来。静区是最容易漏的Code128 左右各留 10 倍模块宽度控件一般有LeftMargin/RightMargin这类属性但不少版本默认是 0生成出来贴在标签边缘扫码枪直接读不到起始符。所以静区要么在控件里设要么在排版时留出足够空白别指望编码器兜底。还有一个经常被忽略的点模块宽度是相对打印机 DPI 的不是相对屏幕的。屏幕预览看着完美打到纸上变窄一半因为预览按 96 DPI 渲染打印机是 203 或 300 DPI。正确做法是先确定目标打印机 DPI反推出毫米值再换算成缇屏幕预览只用来核对内容不用来核对尺寸。2.2 码制选型Code128、Code39、EAN-13 怎么挑控件的Style或Symbology属性是一个枚举各版本编号不一样我在代码里通常自己包一层常量不直接写数字。选型上常见就四种对照一下需求就够码制字符集密度校验位典型用途Code128全 ASCII含大小写与控制符高同长度最窄自动内嵌物流面单、内部批号、SKUCode39数字、大写字母、少量符号低比 Code128 宽 30%50%可选老产线设备、只认 39 的扫描器EAN-13定长 13 位数字固定必带零售商品条码ITF-14定长 14 位数字中必带外箱、托盘标签默认推荐 Code128除非下游明确只认 Code39。从 Code39 换到 Code128 最常见的代价不在条码宽度而在扫描器配置有些老设备的键盘仿真层会把 Code128 的某些控制字符吞掉扫出来的字符串跑到输入框里就少几位。这种情况我一般现场拿扫码枪回读一次跟原字符串逐字符比对包括校验位和不可见字符比猜快得多。EAN-13 和 ITF-14 有个坑要注意校验位通常不是控件替你算而是要求你传入完整 13 位或 14 位数字其中最后一位是校验位。控件里有CheckDigit开关设成自动计算会截掉你传的末位自己重算设成手动传入的字符串不对就直接拒码。两种模式没有对错取决于数据源头——批号从数据库来、校验位上游已经算好就别开自动否则出现“我传的对出来却不对”的情况。还有一类如果编码内容里含%、*、空格这类字符Code39 在某些实现里会被当成起止符解释或者在*两侧拼接时出错。这种情况不是编码逻辑问题是字符集不支持换成 Code128 基本能绕开。2.3 控件在 .NET 里的三层包装关系很多人第一次把这套控件引到 WinForms 里就晕设计器拖出来的类型叫AxBarcodeCtl代码里又能声明BarcodeCtl到底该用哪个。它是一个 COM/ActiveX 控件.ocx在 .NET 里要经过两步转换才可用先由tlbimp或 IDE 自动生成互操作程序集得到接口类型再由aximp生成 Ax 宿主包装负责窗口句柄和事件转发。结果就是一个控件对应两个程序集、两个类型AxBarcodeCtl继承自AxHost带窗口句柄能拖到窗体上、能响应事件、能直接DrawToDC。BarcodeCtl接口类型不能直接可视化但可以用来读取属性、或者在非可视化场景下做编码计算。要往界面上放用 Ax 类型要纯计算条码内容不显示用接口类型。搞混的典型症状是new BarcodeCtl()之后调用绘制方法报“对象未初始化”或者句柄无效——因为它没有真正的窗口DrawToDC没地方画。这一条记牢后面运行时创建的代码就不会踩。3. WinForm 落地注册、引用到运行时出码从拿到包到跑出第一张标签实际要过三步注册控件、让项目认识它、再在代码里创建并赋值。三步里最容易翻车的是第一步因为注册失败时 IDE 给你的报错非常含糊只说“未能加载”不告诉你为什么。所以这一段我按操作顺序写每步都标出失败时该看什么。3.1 注册与引用OCX 到 Ax 包装的三步先在管理员权限的命令提示符里注册注意路径里别带中文和空格注册失败多半是路径问题:: 以管理员身份运行换成你解压后的实际文件名 cd /d D:\tools\barcode9 regsvr32 /s BarcodeCtl9.ocx :: 先卸旧版再注册新版两台机器上都装着不同版本时尤其要执行 regsvr32 /u /s BarcodeCtl9.ocx/s是静默模式不弹成功框。装完想确认注册表里有没有落到正确位置查一下 CLSID 有没有被写进去:: 能查到 ProgID 说明注册成功查不到就是注册失败别急着开 IDE reg query HKCR\BarcodeCtl9.BarcodeCtl /s注册成功之后在 WinForms 项目里走“添加引用 → COM 选项卡”勾选对应的控件条目IDE 会自动生成AxBarcodeCtl和BarcodeCtl两个包装。生成的 Interop 程序集会被拷到输出目录这一点很重要——后面部署到客户机时如果只在目标机regsvr32注册了 ocx 而没把 Interop 程序集带过去照样报类未注册。如果你的环境是 32 位和 64 位混着的注册时用哪个regsvr32决定注册到哪个注册表视图。32 位系统只有一种64 位系统要分清楚:: 64 位系统里32 位控件用 SysWOW64 下的注册器别用 System32 那个 C:\Windows\SysWOW64\regsvr32.exe /s BarcodeCtl9.ocx3.2 运行时创建控件并赋值出图不是所有场景都适合设计器拖拽动态生成多个条码、按数据行批量出码、或者把控件放在没有界面的服务里跑都得运行时创建。下面这段是能直接抄的骨架using System; using System.Drawing; using System.Drawing.Imaging; using System.Windows.Forms; using AxBarcodeLib; // IDE 生成的 Ax 包装命名空间实际名称以你项目为准 public class BarcodeRenderer : IDisposable { private AxBarcodeCtl _ctl; public void Init(Form host) { _ctl new AxBarcodeCtl(); // 关键BeginInit/EndInit 之间不能访问句柄相关属性 ((ISupportInitialize)_ctl).BeginInit(); host.Controls.Add(_ctl); // 不挂到某个容器上CreateControl 会失败 ((ISupportInitialize)_ctl).EndInit(); _ctl.CreateControl(); // 显式创建句柄服务端无窗体场景下尤其必要 // 下面这些属性名以对象浏览器里实际显示的为准各家版本拼写略有差异 _ctl.Style 7; // 码制Code128枚举值查控件文档 _ctl.BarWidth MmToTwips(0.25); // 模块宽度 0.25mm约合 300DPI 下 3 个点 _ctl.Height MmToTwips(12); // 条码高度 12mm _ctl.ShowText true; // 是否在下方显示明文 _ctl.TextPosition 2; // 0 不显示 1 上方 2 下方 _ctl.Rotation 0; // 0 度正向 _ctl.MarginLeft MmToTwips(2.5); // 左静区10 倍模块宽度 _ctl.MarginRight MmToTwips(2.5); } public void RenderTo(string data, string filePath) { _ctl.Value data; // 待编码内容赋值即完成编码 using (var bmp new Bitmap(600, 240)) using (var g Graphics.FromImage(bmp)) { g.Clear(Color.White); IntPtr hdc g.GetHdc(); try { // 把控件画到内存 DCx/y 单位同样按控件约定一般是缇或像素 _ctl.DrawToDC(hdc.ToInt32(), MmToTwips(5), MmToTwips(5)); } finally { g.ReleaseHdc(hdc); // 必须成对释放否则下次 GetHdc 会失败 } bmp.Save(filePath, ImageFormat.Png); } } public void Dispose() _ctl?.Dispose(); }这段代码里真正容易出错的只有三处第一CreateControl()不能省。控件是窗口化的 ActiveX没有句柄时内部很多绘制状态是空的直接调DrawToDC要么抛异常要么画出来是一张白图。第二GetHdc/ReleaseHdc必须成对。少写一次释放下一次调用会拿到一个失效句柄表现是“第一张图正常第二张开始全白”非常有迷惑性。第三BarWidth填的是缇Height也是缇。我见过有人把像素直接塞进去屏幕预览正常因为控件会把缇当像素处理一部分一上打印机高度只剩三分之一。3.3 批量生成从数据表循环出码单张出码只是验证真正上线都是几万条批号一次跑。这时候要把“编码失败”和“渲染失败”分开处理别让一条脏数据把整批打断。using System.Collections.Generic; using System.Data; using System.Text.RegularExpressions; public Liststring BatchRender(DataTable table, string outDir) { var failed new Liststring(); foreach (DataRow row in table.Rows) { string code (row[SkuCode] ?? ).ToString().Trim(); // 编码前先做字符集与长度预检比让控件抛异常再 catch 干净得多 if (code.Length 0 || code.Length 48 || !Regex.IsMatch(code, ^[\x20-\x7E]$)) { failed.Add(code); continue; } try { RenderTo(code, ${outDir}\{code}.png); } catch (Exception ex) { // 记下具体行方便回头只重跑失败的那几条而不是整批重来 failed.Add(${code}\t{ex.Message}); } } return failed; }参数上要注意长度Code128 在常见控件实现里有个内部缓冲区上限超长字符串会被截断而且不报错打出来的条码少一段扫码枪读到的内容和预期对不上排查起来很费时间。我一般按业务规则硬性卡在 48 字符以内超长的走分段或者换码制。字符集预检用的是可打印 ASCII 范围这一条挡掉了中文和全角符号。Code128 本身能编码扩展字符但不少控件实现只保证 ASCII 的正确性中文进得去出不来出来的是一条内容错乱的条码。如果业务上确实要打中文批号正确做法是把中文放在明文区域条码只编码数字或英文短码两者用同一个主键关联。4. 打印链路从屏幕预览到标签纸的三条出口生成图片只是半程纸上的条码能不能扫取决于你怎么把它送到打印机。三条常见路径各有适用范围选错了再调参数也是白费力气。4.1 走 PrintDocument把控件当绘制器这是最贴合控件设计意图的用法。PrintDocument的PrintPage事件里拿到的是打印机 DC直接让控件往上画中间不经过位图缩放尺寸最准。private void OnPrintPage(object sender, PrintPageEventArgs e) { // 一页三个标签横向 45mm 步进位置全部用毫米算别写死像素 for (int i 0; i 3; i) { int x MmToPx(e.Graphics, 8 i * 45); int y MmToPx(e.Graphics, 10); IntPtr hdc e.Graphics.GetHdc(); try { _ctl.DrawToDC(hdc.ToInt32(), x, y); } finally { e.Graphics.ReleaseHdc(hdc); } } e.HasMorePages false; } // 毫米转像素按当前 Graphics 的水平分辨率算屏幕和打印机自动适配 private static int MmToPx(Graphics g, double mm) (int)Math.Round(mm / 25.4 * g.DpiX);这里的MmToPx用的是g.DpiX所以同一段代码在屏幕上预览是 96 DPI打到 203 DPI 的标签机上会自动放大到正确尺寸不需要写两套坐标。需要注意PrintPage事件的坐标系原点在可打印区域的左上角不是纸张物理边缘。不同打印机不可打印边距差几毫米很正常标签纸上位置整体偏一点点八成就是这个原因。稳妥做法是先打一张定位图画十字线到四个角的已知距离量出来再统一加偏移。4.2 导出图片再交给报表模板如果标签版式复杂、有 logo、有多列数据用控件直接画太累常见做法是先用控件出图再交给报表工具或者 FastReport 这类模板引擎排。这条路的代价是图片分辨率必须和打印机 DPI 对齐。用 96 DPI 出的图塞进 300 DPI 的模板里会被插值放大模块边缘发虚扫码枪读起来就吃力了。正确做法是生成时按目标 DPI 反推像素尺寸// 按目标打印机 DPI 生成位图避免模板里二次插值 double targetDpi 300; double widthMm 60, heightMm 25; int pxW (int)Math.Round(widthMm / 25.4 * targetDpi); int pxH (int)Math.Round(heightMm / 25.4 * targetDpi); using (var bmp new Bitmap(pxW, pxH)) { bmp.SetResolution((float)targetDpi, (float)targetDpi); // 关键写进图片元数据 // 后续绘制同 3.2 节 }SetResolution这一行不写图片默认是 96 DPI 元数据某些报表引擎会按 96 去算物理尺寸贴到纸上的条码凭空缩小三倍。这类问题在预览里看不出来只有打出来才发现返工成本很高。4.3 标签打印机指令打印和图形打印的分界这是选型上最容易走偏的一环。标签打印机通常有两条路一是把整页当图片发给打印机图形模式二是发字体/条码指令让打印机自己渲染指令模式比如 ZPL 这类。用 ActiveX 控件出图本质上是图形模式。它能保证屏幕所见即所得但有两个限制一是分辨率受图片本身限制图片做多细纸上就多细二是每张标签都要传一张图行数多的时候传输量不小。指令模式生成的是矢量条码打印机用自己的固件渲染边缘更锐利、速度快但版式控制权从你手里交出去了对齐、旋转、多行文本都得用打印机自己的语言写。我的判断标准很简单条码是整张标签的主体、版式简单、数量大走指令标签里有复杂图文混排、需要复用现有模板、或者打印机品牌杂走图形。两套混着上的情况也有——主体条码走指令图片和说明文字走图形叠加前提是打印机支持混合渲染。5. 避坑与排查五个高频翻车点这一章只写现场撞见过的每条按“现象、原因、解决”的顺序能对号入座的直接照做。5.1 扫不出来八成是模块宽度被打印链路改过现象控件里量着模块宽度 0.25 毫米扫十次错八次读出来的字符串还经常少一位。原因中间经过了缩放。要么是图片被报表模板按比例拉伸要么是打印机的“适合页面”选项开着把整张图缩小了 80%。模块宽度一旦低于一个打印点相邻条空在物理上就糊在一起边缘对比度不够扫描器判不出边界。解决先别改控件的值直接拿卡尺量纸上实际的黑条宽度和理论值对一下。差了就往上找缩放源头把“适合页面”“自动缩放”全部关掉图片按 1:1 输出。确认没有缩放之后再把模块宽度往上调到 0.3 毫米试一般能解决。5.2 位置漂移控件坐标和可打印区域不是一回事现象屏幕上位置对着打出来整条码往右偏了五六毫米换一台同型号打印机又对上了。原因DrawToDC的坐标是相对当前 DC 的原点而打印机 DC 的原点在可打印区域左上角不同机型、不同纸边距这个点不一样。控件本身不知道纸的物理边缘在哪。解决别在代码里硬编码偏移先打一张带十字标记的校准页量出实际偏移量做成打印机型号对应的配置项读进来。同型号批量部署时这套配置可以直接复用跨型号再校准一次。5.3 换台机器就报“类未注册”现象开发机跑得好好的拷到客户机上启动就抛COMException提示类未注册或 ProgID 找不到重装软件也没用。原因regsvr32注册的是 ocx 本身但 .NET 项目运行时还需要 IDE 生成的 Interop 程序集。发布时如果只拷了 exe 和 dllInterop 那个程序集没跟着走运行时找不到包装类型。另一种情况是注册到了错误的注册表视图32 位控件用 64 位注册器注册64 位进程读得到、32 位进程读不到。解决先把 Interop 程序集设为“复制到本地”确认输出目录里有再用对应位数的注册器重新注册一次注册完用reg query确认 CLSID 落到了哪。这两步都做完还报错看一眼应用程序日志里的事件 ID一般会指明具体是哪个 CLSID 失败。5.4 64 位进程里加载失败现象项目平台目标设成 AnyCPU在 64 位系统上跑创建控件时抛BadImageFormatException或者直接崩掉。原因这类 ActiveX 控件绝大多数只有 32 位版本64 位进程加载不了 32 位组件。AnyCPU 在 64 位系统上默认按 64 位跑正好踩中。解决把平台目标钉死成 x86。改项目文件比改 IDE 里的下拉框更可靠因为有些构建配置会把它覆盖掉PropertyGroup PlatformTargetx86/PlatformTarget !-- 如果项目里有 Prefer32Bit 且是 exe这里要明确关掉避免行为和预期不一致 -- Prefer32Bitfalse/Prefer32Bit /PropertyGroup改完记得清理重建只做增量编译有时旧的目标框架信息还留在中间文件里。5.5 特殊字符把整批打挂现象单独测试某一条批号时抛异常日志里只有一句“参数无效”看不出是哪个字符的问题批量跑的时候整批中断。原因编码内容里混进了全角字符、制表符、或者超过控件缓冲区长度的串。控件在赋值阶段就拒绝异常信息没带上具体数据定位很困难。解决编码前统一做一次清洗和校验长度和字符集都卡死脏数据不进控件。批量处理时逐条 try/catch失败的行写进单独的结果文件跑完只重跑失败的那几条不整批重来。这条看着笨但省下来的时间比任何调试技巧都多。6. 上线前的三重验证回读、静区与模块宽度条码这东西有个特点肉眼看着好好的设备读不出来或者反过来扫码枪能读但换一台型号稍老的设备就废了。所以只靠“看一眼觉得没问题”是不够的我习惯在批量投产前跑三道验证。第一道是回读。拿现场实际使用的那把扫码枪不是办公室里随便找的一把对同一张标签连扫十次逐字符比对读回值和原字符串包括校验位和不可见字符。这一道最容易抓到字符集问题——键盘仿真层会把某些控制字符吞掉屏幕上完全看不出异常读回来少两位才知道。验证项工具判定标准不通过时先改哪里回读一致性现场扫码枪连扫 10 次全部匹配原字符串码制、字符集清洗模块宽度卡尺或放大镜实测值与理论值偏差 5%打印缩放设置静区留白目视 卡尺左右各 ≥ 10 倍模块宽度排版边距、控件 Margin高度卡尺≥ 10 毫米控件 Height边缘清晰度放大镜黑条边缘无锯齿粘连输出分辨率、DPI 元数据第二道是静区。量一下条码左右到标签边缘的实际距离和模块宽度乘 10 对一下。这一项在密集排版里最容易被挤掉尤其是标签宽度本来就紧张的时候设计师为了字大一点把条码往边上挪了两毫米扫码枪立刻变挑剔。第三道是模块宽度实测。不用每次都上卡尺但新打印机、新纸、新模板第一次上线时必须量一次。量出来的值和理论值差 5% 以内可以接受超了就回头查缩放链路别指望调控件参数能补回来。三道都过了再批量跑。听起来麻烦实际做下来一轮不到十分钟比起打了几千张发现扫不出、整批返工重印这点时间花得值。从那以后我每批标签上线前都会先打三张、扫十次、拿卡尺量一遍模块宽度全部过了才敢放开批量跑。这个习惯帮我拦下过好几次看似正常的模板改动。希望帮到你。本文还有配套的精品资源点击获取