【问题标题】:Raw printing directly to a USB printer, bypassing Windows spooler绕过 Windows spooler 直接将原始打印到 USB 打印机
【发布时间】:2011-09-11 20:58:37
【问题描述】:

我正在尝试使用Zebra TTP8200 热敏打印机。对于我的应用程序,我需要连续打印绘图仪类型的轨迹,直到用户点击停止按钮。我玩过 ZPL 语言,我可以成功地生成位图数据,并通过将 ZPL 作为原始数据输出,一次转储一行(或几行)位图。

我正在使用一些Microsoft demo code 将原始数据输出到打印机,这很好用,除了一个问题:假脱机程序。事实证明,每次我使用 MS rawprn.exe 代码输出一些数据时,它实际上都是作为打印作业假脱机,然后传输到打印机。这需要长达 10 秒才能通过假脱机程序,显然太慢了。在驱动程序中禁用假脱机没有帮助,它只是意味着当作业通过假脱机程序并完成打印时程序挂起。

有没有办法绕过后台处理程序并将数据直接输出到此 USB 打印机?到目前为止,我的研究还没有发现任何可能在 Windows API 中出现的东西。理想情况下,我希望能够像使用串行打印机一样使用打印机 - 打开端口并将数据推入。

非常感谢任何提示!

【问题讨论】:

标签: windows printing zebra-printers


【解决方案1】:

下面的 C# 类是我改编自 Microsoft 知识库文章的内容。此类中有一些方法可以将打印作业作为stringbyte[] 发送。请注意,那里有一些对 log4net 的引用,可以用您选择的日志框架删除/替换。 :

/// <summary>
/// Class used to aid in sending raw printer data (PS, PRN, etc) directly to the printer.
/// This class was taken from http://support.microsoft.com/kb/322091
/// </summary>
public class PrintQueueUtility
{
    private static ILog log = LogManager.GetLogger(typeof(PrintQueueUtility));

    [DllImport("winspool.Drv", EntryPoint = "OpenPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool OpenPrinter([MarshalAs(UnmanagedType.LPStr)] string szPrinter, out IntPtr hPrinter, IntPtr pd);

    [DllImport("winspool.Drv", EntryPoint = "ClosePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool ClosePrinter(IntPtr hPrinter);

    [DllImport("winspool.Drv", EntryPoint = "StartDocPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool StartDocPrinter(IntPtr hPrinter, Int32 level, [In, MarshalAs(UnmanagedType.LPStruct)] DOCINFOA di);

    [DllImport("winspool.Drv", EntryPoint = "EndDocPrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool EndDocPrinter(IntPtr hPrinter);

    [DllImport("winspool.Drv", EntryPoint = "StartPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool StartPagePrinter(IntPtr hPrinter);

    [DllImport("winspool.Drv", EntryPoint = "EndPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool EndPagePrinter(IntPtr hPrinter);

    [DllImport("winspool.Drv", EntryPoint = "WritePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
    public static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, Int32 dwCount, out Int32 dwWritten);

    /// <summary>Method which sends a <see langword="byte"/> array to a printer queue with a specific document name.</summary>
    /// <param name="bytes">Byte array to send to the printer.</param>
    /// <param name="printerName">Name of the printer to send the <paramref name="bytes"/> to.</param>
    /// <param name="documentName">The document Name.</param>
    /// <returns><see cref="bool"/> indicating whether or not the method succeeded at adding something to the print queue.</returns>
    public static bool SendBytesToPrinter(byte[] bytes, string printerName, string documentName)
    {
        bool success;

        // Allocate some unmanaged memory for those bytes into an unmanaged pointer.
        IntPtr unmanagedBytes = Marshal.AllocCoTaskMem(bytes.Length);

        // Copy the managed byte array into the unmanaged array.
        Marshal.Copy(bytes, 0, unmanagedBytes, bytes.Length);

        // Send the unmanaged bytes to the printer.
        success = SendUnmanagedBytesToPrinter(unmanagedBytes, printerName, documentName, bytes.Length);

        // Free the unmanaged memory that you allocated earlier.
        Marshal.FreeCoTaskMem(unmanagedBytes);

        return success;
    }

    /// <summary>Method which sends a string to the printer queue with a specific document name.</summary>
    /// <param name="data"><see cref="String"/> data to send to the printer.</param>
    /// <param name="printerName">Name of the printer to send the data to.</param>
    /// <param name="documentName">Name of the document in the printer queue.</param>
    /// <returns><see cref="bool"/> indicating whether or not the method succeeded at adding something to the print queue.</returns>
    public static bool SendStringToPrinter(string data, string printerName, string documentName)
    {
        bool success;
        IntPtr unmanagedBytes;

        // How many characters are in the string?
        var characterCount = data.Length;

        // Assume that the printer is expecting ANSI text, and then convert
        // the string to ANSI text.
        unmanagedBytes = Marshal.StringToCoTaskMemAnsi(data);

        // Send the converted ANSI string to the printer.
        success = SendUnmanagedBytesToPrinter(unmanagedBytes, printerName, documentName, characterCount);
        Marshal.FreeCoTaskMem(unmanagedBytes);

        return success;
    }

    private static bool SendUnmanagedBytesToPrinter(IntPtr unmanagedBytes, string printerName, string documentName, int count)
    {
        int error; 
        int written;
        IntPtr printer;
        var di = new DOCINFOA();
        var success = false;

        di.pDocName = documentName;
        di.pDataType = "RAW";

        // Open the printer.
        if (OpenPrinter(printerName.Normalize(), out printer, IntPtr.Zero))
        {
            // Start a document.
            if (StartDocPrinter(printer, 1, di))
            {
                // Start a page.
                if (StartPagePrinter(printer))
                {
                    // Write the bytes.
                    success = WritePrinter(printer, unmanagedBytes, count, out written);
                    EndPagePrinter(printer);
                }

                EndDocPrinter(printer);
            }

            ClosePrinter(printer);
        }

        // If you did not succeed, GetLastError may give more information
        // about why not.
        if (!success)
        {
            error = Marshal.GetLastWin32Error();

            log.ErrorFormat("Sending bytes to printer {0} failed. Last Win32 error = {1}", printerName, error);
        }

        return success;
    }

    // Structure and API declarations:
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public class DOCINFOA
    {
        [MarshalAs(UnmanagedType.LPStr)]
        public string pDocName;

        [MarshalAs(UnmanagedType.LPStr)]
        public string pOutputFile;

        [MarshalAs(UnmanagedType.LPStr)]
        public string pDataType;
    }

【讨论】:

    【解决方案2】:

    感谢cmets。

    经过一番挖掘,我发现this interesting article 使用usbprint.sys 提供的Windows 打印机功能。稍微修改一下示例代码似乎就可以了。我想我会走这条路。

    有文中给出的最终代码:

    /* Code to find the device path for a usbprint.sys controlled
    * usb printer and print to it
    */
    
    #include <usb.h>
    #include <usbiodef.h>
    #include <usbioctl.h>
    #include <usbprint.h>
    #include <setupapi.h>
    #include <devguid.h>
    #include <wdmguid.h>
    
    /* This define is required so that the GUID_DEVINTERFACE_USBPRINT variable is
     * declared an initialised as a static locally, since windows does not include it
     * in any of its libraries
     */
    
    #define SS_DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
    static const GUID name \
    = { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }
    
    SS_DEFINE_GUID(GUID_DEVINTERFACE_USBPRINT, 0x28d78fad, 0x5a12, 0x11D1, 0xae,
                   0x5b, 0x00, 0x00, 0xf8, 0x03, 0xa8, 0xc2);
    
    void SomeFunctionToWriteToUSB()
    {
      HDEVINFO devs;
      DWORD devcount;
      SP_DEVINFO_DATA devinfo;
      SP_DEVICE_INTERFACE_DATA devinterface;
      DWORD size;
      GUID intfce;
      PSP_DEVICE_INTERFACE_DETAIL_DATA interface_detail;
    
      intfce = GUID_DEVINTERFACE_USBPRINT;
      devs = SetupDiGetClassDevs(&intfce, 0, 0, DIGCF_PRESENT |
                                 DIGCF_DEVICEINTERFACE);
      if (devs == INVALID_HANDLE_VALUE) {
        return;
      }
      devcount = 0;
      devinterface.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
      while (SetupDiEnumDeviceInterfaces(devs, 0, &intfce, devcount, &devinterface)) {
        /* The following buffers would normally be malloced to he correct size
         * but here we just declare them as large stack variables
         * to make the code more readable
         */
        char driverkey[2048];
        char interfacename[2048];
        char location[2048];
        char description[2048];
    
        /* If this is not the device we want, we would normally continue onto the
         * next one or so something like
         * if (!required_device) continue; would be added here
         */
        devcount++;
        size = 0;
        /* See how large a buffer we require for the device interface details */
        SetupDiGetDeviceInterfaceDetail(devs, &devinterface, 0, 0, &size, 0);
        devinfo.cbSize = sizeof(SP_DEVINFO_DATA);
        interface_detail = calloc(1, size);
        if (interface_detail) {
          interface_detail->cbSize = sizeof (SP_DEVICE_INTERFACE_DETAIL_DATA);
          devinfo.cbSize = sizeof(SP_DEVINFO_DATA);
          if (!SetupDiGetDeviceInterfaceDetail(devs, &devinterface, interface_detail,
                                               size, 0, &devinfo)) {
        free(interface_detail);
        SetupDiDestroyDeviceInfoList(devs);
        return;
          }
          /* Make a copy of the device path for later use */
          strcpy(interfacename, interface_detail->DevicePath);
          free(interface_detail);
          /* And now fetch some useful registry entries */
          size = sizeof(driverkey);
          driverkey[0] = 0;
          if (!SetupDiGetDeviceRegistryProperty(devs, &devinfo, SPDRP_DRIVER, &dataType,
                                                (LPBYTE)driverkey, size, 0)) {
        SetupDiDestroyDeviceInfoList(devs);
        return;
          }
          size = sizeof(location);
          location[0] = 0;
          if (!SetupDiGetDeviceRegistryProperty(devs, &devinfo,
                                                SPDRP_LOCATION_INFORMATION, &dataType,
                                                (LPBYTE)location, size, 0)) {
        SetupDiDestroyDeviceInfoList(devs);
        return;
          }
          usbHandle = CreateFile(interfacename, GENERIC_WRITE, FILE_SHARE_READ,
                     NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL |
                     FILE_FLAG_SEQUENTIAL_SCAN, NULL);
          if (usbHandle != INVALID_HANDLE_VALUE) {
        /* Now perform all the writing to the device ie.
         * while (some condition) WriteFile(usbHandle, buf, size, &bytes_written);
         */
        CloseHandle(usbHandle);
          }
        }
      }
      SetupDiDestroyDeviceInfoList(devs);
    }
    

    再次感谢您的建议。

    【讨论】:

    • -1:此答案中没有代码,还有一个死链接:(添加一些代码,我会将-1更改为+1!
    • 嗨,Al,我很快就要试试这个了。你知道,Argox 打印机实用程序可以直接向 USB 发送 eltron/datamax 命令,无需任何打印机驱动程序。所以我想知道。
    • 任何认为此答案有帮助的人也可能会发现帖子stackoverflow.com/questions/40775369/… 中的讨论也很有帮助。
    【解决方案3】:

    有没有办法绕过后台处理程序并将数据直接输出到此 USB 打印机?

    是的,当然。它内置在大多数操作系统中,通过 USB 进行原始打印只是比以太网和 COM/LPT 不那么明显。请注意,许多应用程序(例如记事本)无法打印原始文件,因此您的应用程序也需要支持这一点。

    1. 为您的 USB 打印机安装适当的驱动程序。检查打印机属性以查看它正在使用哪个 USB 端口。可能是USB001等。
    2. 使用设备和打印机,添加第二台打印机。本地端口,选择刚刚创建的端口(即 USB001) 注意:某些版本的 Windows 有一个自动检测复选框,如果有,请取消选中。
    3. 制造商:通用,打印机:通用/纯文本
    4. 使用当前安装的驱动程序
    5. 为打印机指定一个名称,以区别于已创建的打印机,即 Zebra TTP8200 Raw。
    6. 请勿分享
    7. 不打印测试页,完成

    现在使用您的原始打印应用程序,使用新创建的打印机。

    附: 作为 Java 开源原始打印教程的一部分,这些说明也可在此处获得,并附有屏幕截图。该项目还提供其他平台(Ubuntu、OS X)的教程。

    http://qzindustries.com/TutorialRawWin

    -特雷斯

    【讨论】:

      【解决方案4】:

      如果 USB 打印机可用作 COM 端口,您只需写入 COM 端口即可。像这样,在 DOS 提示符下:

      dir > com1
      

      前面的例子会将dir命令的结果输出到打印机。

      或者,这是另一个例子:

      copy file.txt com1
      

      前面的例子会将file.txt的内容输出到打印机。

      输出格式正确的 ZPL 数据将比纯文本更难。但是,我已经使用 Ruby(和 Epson/ESC 命令)在 Linux 上让它工作了。

      【讨论】:

      • 您好 Teddy,谢谢,它没有显示为 COM 端口。如果这样做,这一切都会像您建议的那样简单!如果有一个替代的驱动程序让它成为一个假的串行接口,那就太好了。
      • 制造商网站说打印机支持并行打印。您可以像写入 COM 端口一样写入 LPT1 端口。在我的示例中,只需将 COM1 替换为 LPT1。
      • 嗨 Teddy,我很确定我拥有的版本只有 USB(但我会检查明天什么时候回到它前面)。对于这个应用程序,我们实际上将 PC 的并行端口用于另一个设备,我宁愿不使用附加 PCI 卡 - 这似乎太 1990 年代!
      • 如果手动在网络上共享USB打印机,可以mapped to a local LPT port,使用方式与本方案相同。
      • @QZSupport 您不必在网络上共享它,因为您可以通过链接问题中的环回卡访问它,或者使用 \\localhost
      猜你喜欢
      • 1970-01-01
      • 2011-06-15
      • 1970-01-01
      • 1970-01-01
      • 2011-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多