C#으로 ZPL(Zebra Programming Language) 바코드 프린터 제어하기

ZPL(Zebra Programming Language)은 바코드 프린터 제조사인 Zebra에서 자체 개발한 언어입니다. Zebra 프린터는 매우 보편적이므로, 바코드 프린터 제어는 사실상 ZPL 사용법을 익히는 것과 같습니다.

ZPL을 C#으로 제어하는 과정은 크게 두 단계로 나눌 수 있습니다:

  1. ZPL 명령어를 작성합니다.
  2. 작성된 ZPL 명령어를 C# 문자열로 변환하여 프린터 포트로 전송합니다.

1. 로컬 프린터 직접 제어 (병렬 포트 LPT1)

이 방식은 프린터가 로컬 컴퓨터에 연결되어 있고, 주로 병렬 포트(LPT1)를 사용하는 경우에 해당합니다. 다음 C# 클래스는 병렬 포트 통신을 캡슐화합니다.


using System;
using System.Runtime.InteropServices;

namespace BarcodePrint
{
    /// <summary>
    /// ZPL 명령어 문자열을 인자로 받아 프린터로 전송하는 클래스입니다.
    /// </summary>
    class ParallelPortPrinter
    {
        // Win32 API 호출을 위한 구조체 및 함수 선언
        [StructLayout(LayoutKind.Sequential)]
        private struct Overlapped
        {
            int Internal;
            int InternalHigh;
            int Offset;
            int OffSetHigh;
            int hEvent;
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr CreateFile(
            string lpFileName,
            uint dwDesiredAccess,
            int dwShareMode,
            IntPtr lpSecurityAttributes,
            int dwCreationDisposition,
            int dwFlagsAndAttributes,
            IntPtr hTemplateFile);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool WriteFile(
            IntPtr hFile,
            byte[] lpBuffer,
            int nNumberOfBytesToWrite,
            out int lpNumberOfBytesWritten,
            out Overlapped lpOverlapped);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(IntPtr hObject);

        private IntPtr fileHandle = IntPtr.Zero;
        private const uint GENERIC_WRITE = 0x40000000;
        private const int OPEN_EXISTING = 3;

        /// <summary>
        /// LPT1 포트를 엽니다.
        /// </summary>
        /// <returns>성공 시 true, 실패 시 false</returns>
        public bool Open()
        {
            fileHandle = CreateFile("LPT1", GENERIC_WRITE, 0, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
            return fileHandle != IntPtr.Zero && fileHandle != new IntPtr(-1);
        }

        /// <summary>
        /// ZPL 명령어를 프린터로 전송합니다.
        /// </summary>
        /// <param name="zplCommand">전송할 ZPL 명령어 문자열</param>
        /// <returns>성공 시 true, 실패 시 false</returns>
        /// <exception cref="InvalidOperationException">포트가 열리지 않았을 경우 발생합니다.</exception>
        public bool Write(string zplCommand)
        {
            if (fileHandle == IntPtr.Zero || fileHandle == new IntPtr(-1))
            {
                throw new InvalidOperationException("프린터 포트가 열리지 않았습니다.");
            }

            byte[] commandBytes = System.Text.Encoding.Default.GetBytes(zplCommand);
            int bytesWritten;
            Overlapped overlapped;
            return WriteFile(fileHandle, commandBytes, commandBytes.Length, out bytesWritten, out overlapped);
        }

        /// <summary>
        /// 프린터 포트를 닫습니다.
        /// </summary>
        /// <returns>성공 시 true, 실패 시 false</returns>
        public bool Close()
        {
            if (fileHandle != IntPtr.Zero && fileHandle != new IntPtr(-1))
            {
                bool result = CloseHandle(fileHandle);
                fileHandle = IntPtr.Zero;
                return result;
            }
            return true; // 이미 닫혔거나 열리지 않았으면 성공으로 간주
        }
    }
}

사용 예시:


// ParallelPortPrinter 클래스를 인스턴스화합니다.
ParallelPortPrinter lpt = new ParallelPortPrinter();

// 전송할 ZPL 명령어 문자열을 정의합니다.
// 예: 레이블 시작, 30단계 인쇄 농도, 좌측 상단 여백 60,10,
// 폰트, 바코드 비율, 높이, Code128 바코드, 데이터 내용, 레이블 종료
string zplCmd = "^XA^MD30^LH60,10^FO20,10^ACN,18,10^BY1.4,3,50^BC,,Y,N^FD01008D004Q-0^FS^XZ";

// 프린터 포트를 엽니다.
if (!lpt.Open())
{
    // TODO: 사용자에게 오류 메시지 표시 (예: Response.Write)
    Console.WriteLine("프린터 연결에 실패했습니다. 프린터가 올바르게 설치되었고 전원이 켜져 있는지 확인하십시오.");
    return;
}

try
{
    // ZPL 명령어를 프린터로 전송합니다.
    lpt.Write(zplCmd);
}
finally
{
    // 프린터 포트를 닫습니다.
    lpt.Close();
}

ZPL 명령어 설명

아래는 예시 ZPL 명령어의 각 줄에 대한 설명입니다:


^XA      : ZPL 명령어 블록의 시작을 나타냅니다.
^MD30    : 인쇄 농도(색상 깊이)를 설정합니다. 값의 범위는 -30에서 30까지이며, 30은 가장 진한 농도입니다.
^LH60,10 : 레이블의 좌측 상단 여백을 설정합니다 (X=60, Y=10).
^FO20,10 : 필드(데이터)의 시작 위치를 설정합니다 (X=20, Y=10).
^ACN,18,10: 폰트 종류(N), 높이(18), 폭(10)을 설정합니다. 이 폰트는 바코드 아래 표시될 텍스트에 사용됩니다.
^BY1.4,3,50: 바코드의 배율(1.4), 막대 비율(3), 높이(50)를 설정합니다.
^BC,,Y,N : Code128 바코드 형식을 지정합니다. Y는 바코드 위/아래에 텍스트를 표시함을 의미하며, N은 추가 옵션입니다.
^FD01008D004Q-0^FS: 인쇄할 바코드 데이터를 지정합니다. ^FS는 필드 종료를 의미합니다.
^XZ      : ZPL 명령어 블록의 끝을 나타냅니다.

ZPL 명령어는 직관적이며, 간단한 바코드 인쇄 요구사항에는 위에서 설명한 명령어들만으로 충분한 경우가 많습니다. 복잡한 그래픽이나 패턴 인쇄도 가능하며, 미리 프린터에 업로드된 그래픽을 불러와 사용할 수도 있습니다.

2. 원격 또는 드라이버를 통한 프린터 제어

프린터가 네트워크에 있거나 병렬 포트가 아닌 다른 인터페이스(USB, 이더넷 등)를 사용하는 경우, 운영체제에 설치된 프린터 드라이버를 통해 명령어를 전송해야 합니다. 이 방식은 다음과 같은 C# 클래스를 사용합니다.


using System;
using System.IO;
using System.Runtime.InteropServices;

public class RemotePrinter
{
    // Windows API 함수 및 구조체 정의
    [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;
    }

    [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>
    /// 프린터 이름과 바이트 배열을 받아 프린터로 전송합니다.
    /// </summary>
    /// <param name="szPrinterName">프린터 이름</param>
    /// <param name="pBytes">전송할 데이터 포인터</param>
    /// <param name="dwCount">전송할 데이터 크기</param>
    /// <returns>성공 시 true, 실패 시 false</returns>
    public static bool SendBytesToPrinter(string szPrinterName, IntPtr pBytes, Int32 dwCount)
    {
        Int32 dwError = 0, dwWritten = 0;
        IntPtr hPrinter = IntPtr.Zero;
        DocInfoA di = new DocInfoA
        {
            pDocName = "ZPL Print Job",
            pOutputFile = null,
            pDataType = "RAW" // RAW 모드로 ZPL 명령 직접 전송
        };
        bool bSuccess = false;

        // 프린터 열기
        if (OpenPrinter(szPrinterName, out hPrinter, IntPtr.Zero))
        {
            // 문서 시작
            if (StartDocPrinter(hPrinter, 1, di))
            {
                // 페이지 시작
                if (StartPagePrinter(hPrinter))
                {
                    // 데이터 쓰기
                    bSuccess = WritePrinter(hPrinter, pBytes, dwCount, out dwWritten);
                    EndPagePrinter(hPrinter);
                }
                EndDocPrinter(hPrinter);
            }
            ClosePrinter(hPrinter);
        }

        if (!bSuccess)
        {
            dwError = Marshal.GetLastWin32Error();
        }
        return bSuccess;
    }

    /// <summary>
    /// 프린터 이름과 파일 경로를 받아 파일 내용을 프린터로 전송합니다.
    /// </summary>
    /// <param name="szPrinterName">프린터 이름</param>
    /// <param name="szFileName">전송할 파일 경로</param>
    /// <returns>성공 시 true, 실패 시 false</returns>
    public static bool SendFileToPrinter(string szPrinterName, string szFileName)
    {
        try
        {
            using (FileStream fs = new FileStream(szFileName, FileMode.Open))
            using (BinaryReader br = new BinaryReader(fs))
            {
                int nLength = (int)fs.Length;
                byte[] bytes = br.ReadBytes(nLength);
                IntPtr pUnmanagedBytes = Marshal.AllocCoTaskMem(nLength);
                Marshal.Copy(bytes, 0, pUnmanagedBytes, nLength);
                bool success = SendBytesToPrinter(szPrinterName, pUnmanagedBytes, nLength);
                Marshal.FreeCoTaskMem(pUnmanagedBytes);
                return success;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"파일 전송 오류: {ex.Message}");
            return false;
        }
    }

    /// <summary>
    /// 프린터 이름과 문자열을 받아 프린터로 전송합니다.
    /// </summary>
    /// <param name="szPrinterName">프린터 이름</param>
    /// <param name="szString">전송할 문자열 (ZPL 명령어)</param>
    /// <returns>성공 시 true, 실패 시 false</returns>
    public static bool SendStringToPrinter(string szPrinterName, string szString)
    {
        IntPtr pBytes = Marshal.StringToCoTaskMemAnsi(szString); // ANSI로 변환
        bool success = SendBytesToPrinter(szPrinterName, pBytes, szString.Length);
        Marshal.FreeCoTaskMem(pBytes);
        return success;
    }
}

사용 예시:

운영체제에 설치된 프린터 드라이버에 등록된 프린터 이름을 사용해야 합니다. 예를 들어, 프린터 이름이 "Zebra ZT410"이라면 다음과 같이 호출합니다.


string printerName = "Your Printer Name"; // 실제 프린터 이름으로 변경하세요.
string zplCommand = "^XA^FO50,50^A0N,30,30^FDHello ZPL^FS^XZ";

// 문자열(ZPL 명령어)을 프린터로 전송
bool result = RemotePrinter.SendStringToPrinter(printerName, zplCommand);

if (result)
{
    Console.WriteLine("ZPL 명령어가 성공적으로 전송되었습니다.");
}
else
{
    Console.WriteLine("ZPL 명령어 전송에 실패했습니다.");
}

위의 `RemotePrinter` 클래스를 사용하면 ZPL 명령어를 텍스트 파일로 저장한 뒤 해당 파일을 프린터로 보내거나, 직접 문자열 형태로 프린터 드라이버를 통해 전송할 수 있습니다.

태그: C# ZPL 바코드 프린터 병렬포트

11월 15일 08:21에 게시됨