在工業自動化、POS 系統開發中,並口(LPT)雖屬傳統介面,但因成本低、相容性好仍廣泛用於印表機、舊式儀器控制。.NET Framework 提供 SerialPort 類別支援序列埠,卻缺乏並口原生支援。本文介紹結合 inpout32.dll、Win32 API 與 WMI (WQL) 查詢動態取得並口基址,構建完整可靠的並口讀寫元件。
ParallelPort 對應 SerialPortWriteFile,狀態埠需 inpout32.dll 讀取| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| Win32 API (CreateFile/WriteFile) | 無需第三方 DLL,系統原生 | 無法讀取狀態埠資料 | 單向輸出(如單純列印) |
| inpout32.dll (Inp32/Out32) | 可讀寫所有埠 | 需知基址,需簽署驅動程式(x64) | 雙向通訊、狀態讀取 |
| WQL 查詢 Win32_PortResource | 動態取得實際基址 | 需 System.Management 參考,權限要求較高 | 解決基址不固定問題 |
using System;
using System.Management;
using System.Runtime.InteropServices;
using System.Threading;
public class ParallelPort : IDisposable
{
// ===== inpout32.dll 匯入 =====
[DllImport("inpout32.dll", EntryPoint = "Out32")]
public static extern void Output(uint address, int value);
[DllImport("inpout32.dll", EntryPoint = "Inp32")]
public static extern int Input(uint address);
// ===== Win32 API 匯入 =====
[DllImport("kernel32.dll", CharSet = CharSet.Auto, 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, IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
private const uint GENERIC_WRITE = 0x40000000;
private const uint OPEN_EXISTING = 3;
private const int INVALID_HANDLE_VALUE = -1;
private IntPtr _handle = (IntPtr)INVALID_HANDLE_VALUE;
private uint _basePort = 0;
private bool _isWorking = false;
private Thread _monitorThread;
private int _lastStatus = 0;
public string PortName { get; private set; }
public bool IsOpen => _handle != (IntPtr)INVALID_HANDLE_VALUE;
public event EventHandler StatusChanged;
public ParallelPort(string portName)
{
PortName = portName;
DetectBasePortViaWQL();
}
// ===== WQL 動態偵測並口基址 =====
private void DetectBasePortViaWQL()
{
// 查詢並口裝置關聯的資源
string wql = $"ASSOCIATORS OF {{Win32_ParallelPort.DeviceID='{PortName}'}}";
using (var searcher = new ManagementObjectSearcher(wql))
{
foreach (ManagementObject obj in searcher.Get())
{
if (obj.ClassPath.ClassName == "Win32_PortResource")
{
_basePort = Convert.ToUInt32(obj.Properties["StartingAddress"].Value);
break;
}
}
}
if (_basePort == 0)
throw new InvalidOperationException($"無法取得 {PortName} 的基址");
}
// ===== 開啟並口(使用 Win32 CreateFile 取得 handle)=====
public void Open()
{
_handle = CreateFile(PortName, GENERIC_WRITE, 0, IntPtr.Zero,
(int)OPEN_EXISTING, 0, IntPtr.Zero);
if (_handle == (IntPtr)INVALID_HANDLE_VALUE)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(),
$"無法開啟 {PortName}");
_isWorking = true;
_monitorThread = new Thread(MonitorStatusPort) { IsBackground = true };
_monitorThread.Start();
}
// ===== 寫入資料埠 (Base + 0) =====
public bool Write(byte[] data)
{
if (!IsOpen) return false;
int written;
return WriteFile(_handle, data, data.Length, out written, IntPtr.Zero);
}
// ===== 讀取狀態埠 (Base + 1) 使用 inpout32 =====
public byte[] ReadStatus(int length)
{
byte[] result = new byte[length];
for (int i = 0; i < length; i++)
result[i] = (byte)Input(_basePort + 1); // 狀態埠
return result;
}
// ===== 背景執行緒監測狀態變化 =====
private void MonitorStatusPort()
{
while (_isWorking)
{
int current = Input(_basePort + 1);
if (current != _lastStatus)
{
_lastStatus = current;
StatusChanged?.Invoke(this, new PortStatusEventArgs(current));
}
Thread.Sleep(500);
}
}
public void Close()
{
_isWorking = false;
_monitorThread?.Join(1000);
if (_handle != (IntPtr)INVALID_HANDLE_VALUE)
{
CloseHandle(_handle);
_handle = (IntPtr)INVALID_HANDLE_VALUE;
}
}
public void Dispose() => Close();
}
public class PortStatusEventArgs : EventArgs
{
public int StatusByte { get; }
public PortStatusEventArgs(int status) => StatusByte = status;
}
ASSOCIATORS OF {Win32_ParallelPort.DeviceID='LPT1'} 回傳該並口關聯的所有資源物件,篩選 Win32_PortResource 類別即可取得 StartingAddress(基址)。此法解決多並口、PCIe 轉接卡導致基址漂移問題。
CreateFile("LPT1") + WriteFile,支援緩衝、重疊 I/O,相容性佳ReadFile 讀 LPT 較不穩定,改用 inpout32.dll 的 Inp32(Base+1) 直接讀取狀態埠inpout32.dll 必須使用簽署版本(如 inpoutx64.dll),否則載入失敗DllImport 條件編譯
var port = new ParallelPort("LPT1");
port.StatusChanged += (s, e) =>
Console.WriteLine($"狀態變更: 0x{e.StatusByte:X2}");
port.Open();
port.Write(new byte[] { 0x1B, 0x40 }); // ESC @ 初始化印表機
port.Write(System.Text.Encoding.GetEncoding(950).GetBytes("測試列印內容\n\n"));
Thread.Sleep(2000);
port.Close();
| 現象 | 可能原因 | 解決方案 |
|---|---|---|
| CreateFile 回傳 -1 | 埠被佔用/不存在/權限不足 | 檢查裝置管理員、以管理員執行、確認 PortName 正確 |
| inpout32.dll 載入失敗 | x64 程序載入 x86 DLL / 未簽署 | 使用簽署版 inpoutx64.dll 或啟用 Prefer 32-bit |
| WQL 查無結果 | WMI 服務未啟動/權限不足 | 啟動 Winmgmt 服務、加入 Administrators 群組 |
| 列印亂碼 | 編碼不匹配 | 確認印表機代碼頁,使用正確 Encoding (Big5/UTF-8) |
透過 WQL 動態查詢並口基址,結合 Win32 API 處理資料輸出、inpout32.dll 處理狀態讀取,並以背景執行緒監測狀態變化,即可構建穩健、可部署於現代 Windows 環境的並口控制元件。此架構已在多套 POS 系統、儀器控制專案中驗證可靠。