C# Windowsフォームで複数HIDバーコードリーダーの入力元をRaw Input APIで識別する方法

1台のWindows PCに複数のUSBバーコードリーダーを接続すると、どのバーコードが「どのリーダー」から送られたのか分からなくなりがちです。特に生産ラインのように工程ごとに役割が分かれている現場では、入力元を誤認すると品質にも直結します。本記事では、C# WindowsフォームアプリでWindowsのRaw Input APIを使い、複数のHIDバーコードリーダーをデバイス単位で確実に識別する方法を詳しく解説します。

目次

課題の背景:複数のHIDバーコードリーダーを1台のPCで使うと何が起きるか

USB接続のバーコードリーダーの多くは「HIDキーボード互換デバイス」として認識されます。つまり、OSから見ると「ただのキーボード」と同じ扱いで、読み取ったバーコードをキーストロークの連続として送信します。

ところが、生産ラインなどで次のような構成をとることは珍しくありません。

  • 工程1:部材投入チェック用スキャナ(スキャナA)
  • 工程2:組立完了チェック用スキャナ(スキャナB)
  • 工程3:出荷ラベル貼付チェック用スキャナ(スキャナC)

これら3台を同じPCにUSB接続すると、標準的な KeyPress / KeyDown イベントからは、どのキー入力が「どのバーコードリーダー」から来たものなのか区別できません。結果として、次のような問題が発生します。

  • 工程1のバーコードが、工程2の処理として誤って扱われる
  • オペレーターが誤って別工程のスキャナを手に取っても、アプリ側で気づけない
  • 工程ごとのログ・トレーサビリティが曖昧になる

従来よく行われる回避策として、

  • スキャナ側でプレフィックス(前置文字列)を設定し、「A:XXXX」「B:XXXX」のように識別子を付ける
  • 別ウィンドウをアクティブにしてから読み取ってもらう

といった方法がありますが、設定変更や操作ミスに弱く、長期運用にはあまり向きません。

方法概要問題点
プレフィックス方式スキャナ設定で識別用文字列を先頭に付ける設定変更されると破綻、オペレーターの操作で簡単にずれる
アクティブウィンドウ切替工程ごとに入力フォーカスを切り替える操作負担が大きく、誤操作しやすい
キーボードフックグローバルフックでキーを監視入力元デバイス自体は区別できない

根本的な解決策は、「キーストローク」ではなく「物理デバイス単位」で入力を扱うことです。そのために利用できるのが、WindowsのRaw Input APIです。

解決策:Raw Input API で「どのキーボード(スキャナ)からの入力か」を取得する

Raw Input API は、キーボードやマウス、HIDデバイスなどの入力を「生の状態(Raw)」でアプリケーションに届けるための仕組みです。通常の KeyDown / KeyPress では、OSがアクティブウィンドウに対して抽象化されたキーイベントを投げてくれますが、Raw Inputでは次の情報を取得できます。

  • どの物理デバイス(ハンドル)から来たイベントか(hDevice)
  • 仮想キーコード、スキャンコード
  • キー押下/キー解放などのフラグ

特に重要なのは RAWINPUTHEADER.hDevice です。これは「接続されている物理デバイスごとに一意のハンドル」として扱われるため、

  • デバイスハンドル → 工程名(工程1/2/3)
  • デバイスハンドル → スキャナ種別(部材スキャナ/出荷スキャナ)

といったマッピングをアプリケーション側に持てば、「どのスキャナからのバーコードなのか」を確実に識別できます。

観点従来のKeyDown/KeyPressRaw Input
入力単位論理キー(ウィンドウに対するメッセージ)物理デバイス+生のキー状態
複数デバイスの区別不可(全てまとめて同じキーボード)hDevice でデバイスごとに区別可能
フォーカスの影響アクティブウィンドウのみRIDEV_INPUTSINK で非アクティブでも受信可能

実装の全体像:C# Windowsフォームでの手順

Raw Input APIを使って複数バーコードリーダーを識別する際の、実装フローの全体像をまとめると次のようになります。

手順概要ポイント
1RegisterRawInputDevices でキーボード用途のデバイスを登録するUsage Page 0x01(Generic Desktop)、Usage 0x06(Keyboard)
2フォームの WndProc をオーバーライドして WM_INPUT を受信するGetRawInputData で RAWINPUT を取得
3初回入力時に GetRawInputDeviceInfo でデバイス情報を取得し、アプリ内テーブルに登録ベンダーID、プロダクトID、シリアル番号、デバイス名など
4hDevice をキーにして、工程ごとに入力を振り分けるデバイスごとの入力バッファを用意し、Enterでバーコード確定

以下では、C#(WinForms)での具体的なコード例と実装時のポイントを順に解説します。

準備:基本的なP/Invoke定義

C#からRaw Input APIを利用するには、User32.dllの関数や構造体をP/Invokeで定義する必要があります。ここではキーボード(バーコードリーダー)に必要な最低限の定義に絞って紹介します。

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

internal static class NativeMethods
{
    public const int RIDEV_INPUTSINK = 0x00000100;
    public const int RID_INPUT = 0x10000003;
    public const int RIM_TYPEKEYBOARD = 1;
    public const int WM_INPUT = 0x00FF;

    public const int RIDI_PREPARSEDDATA = 0x20000005;
    public const int RIDI_DEVICENAME    = 0x20000007;
    public const int RIDI_DEVICEINFO    = 0x2000000b;

    [StructLayout(LayoutKind.Sequential)]
    public struct RAWINPUTDEVICE
    {
        public ushort usUsagePage;
        public ushort usUsage;
        public int dwFlags;
        public IntPtr hwndTarget;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RAWINPUTHEADER
    {
        public int dwType;
        public int dwSize;
        public IntPtr hDevice;
        public IntPtr wParam;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RAWKEYBOARD
    {
        public ushort MakeCode;
        public ushort Flags;
        public ushort Reserved;
        public ushort VKey;
        public uint   Message;
        public uint   ExtraInformation;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RAWINPUT
    {
        public RAWINPUTHEADER header;
        public RAWINPUTUNION data;
    }

    // キーボードだけを扱う前提なので簡略化版
    [StructLayout(LayoutKind.Explicit)]
    public struct RAWINPUTUNION
    {
        [FieldOffset(0)]
        public RAWKEYBOARD keyboard;
        // マウスやHIDが必要ならここに追加
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RID_DEVICE_INFO_KEYBOARD
    {
        public uint dwType;
        public uint dwSubType;
        public uint dwKeyboardMode;
        public uint dwNumberOfFunctionKeys;
        public uint dwNumberOfIndicators;
        public uint dwNumberOfKeysTotal;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RID_DEVICE_INFO
    {
        public uint cbSize;
        public uint dwType;
        public RID_DEVICE_INFO_KEYBOARD keyboard;
        // 実際はunionだが、ここではキーボードのみを想定して簡略化
    }

    [DllImport("User32.dll", SetLastError = true)]
    public static extern bool RegisterRawInputDevices(
        [In] RAWINPUTDEVICE[] pRawInputDevices,
        uint uiNumDevices,
        uint cbSize);

    [DllImport("User32.dll", SetLastError = true)]
    public static extern uint GetRawInputData(
        IntPtr hRawInput,
        uint uiCommand,
        IntPtr pData,
        ref uint pcbSize,
        uint cbSizeHeader);

    [DllImport("User32.dll", SetLastError = true)]
    public static extern uint GetRawInputDeviceInfo(
        IntPtr hDevice,
        uint uiCommand,
        IntPtr pData,
        ref uint pcbSize);
}

実案件ではエラー処理や他デバイスへの対応などでもう少し定義が増えますが、ひとまずキーボード用スキャナ3台を識別するだけであればこの程度から始められます。

ステップ1:Raw Inputデバイス(キーボード)を登録する

まず、アプリケーション(フォーム)のウィンドウハンドルに対して、「キーボードのRaw Inputを送ってほしい」とOSに登録します。これはフォームのハンドルが確定したタイミング(OnHandleCreated など)で実行するのが安全です。

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        RegisterForRawInput();
    }

    private void RegisterForRawInput()
    {
        var rid = new NativeMethods.RAWINPUTDEVICE[1];

        // Usage Page 0x01: Generic Desktop Controls
        // Usage 0x06    : Keyboard
        rid[0].usUsagePage = 0x01;
        rid[0].usUsage     = 0x06;
        rid[0].dwFlags     = NativeMethods.RIDEV_INPUTSINK; // 非アクティブでも受信
        rid[0].hwndTarget  = this.Handle;

        if (!NativeMethods.RegisterRawInputDevices(
                rid,
                (uint)rid.Length,
                (uint)Marshal.SizeOf<NativeMethods.RAWINPUTDEVICE>()))
        {
            throw new System.ComponentModel.Win32Exception(
                Marshal.GetLastWin32Error());
        }
    }
}

RIDEV_INPUTSINK を指定することで、フォームが非アクティブのときでもバーコードリーダーからの入力を受け取ることができます。生産ラインのように別ソフトと並行運用する場合にも有効です。

ステップ2:WndProcでWM_INPUTを受け取り、RAWINPUTを取り出す

次に、フォームの WndProc をオーバーライドし、WM_INPUT メッセージを処理します。ここではRaw Inputの生データを取得し、デバイスハンドルおよびキー情報を取り出します。

public partial class MainForm : Form
{
    // デバイスハンドル → 入力バッファ
    private readonly Dictionary<IntPtr, StringBuilder> _deviceBuffers
        = new Dictionary<IntPtr, StringBuilder>();

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == NativeMethods.WM_INPUT)
        {
            ProcessRawInput(m.LParam);
        }

        base.WndProc(ref m);
    }

    private void ProcessRawInput(IntPtr lParam)
    {
        uint dwSize = 0;

        // 1. 必要なバッファサイズを取得
        uint headerSize = (uint)Marshal.SizeOf<NativeMethods.RAWINPUTHEADER>();
        if (NativeMethods.GetRawInputData(
                lParam,
                NativeMethods.RID_INPUT,
                IntPtr.Zero,
                ref dwSize,
                headerSize) != 0)
        {
            return; // 異常系:本来はログなどを出した方がよい
        }

        IntPtr buffer = Marshal.AllocHGlobal((int)dwSize);
        try
        {
            // 2. 実データを取得
            if (NativeMethods.GetRawInputData(
                    lParam,
                    NativeMethods.RID_INPUT,
                    buffer,
                    ref dwSize,
                    headerSize) != dwSize)
            {
                return;
            }

            var raw = Marshal.PtrToStructure<NativeMethods.RAWINPUT>(buffer);

            // キーボード以外は無視
            if (raw.header.dwType != NativeMethods.RIM_TYPEKEYBOARD)
                return;

            IntPtr deviceHandle = raw.header.hDevice;
            var kb = raw.data.keyboard;

            HandleKeyboardInput(deviceHandle, kb);
        }
        finally
        {
            Marshal.FreeHGlobal(buffer);
        }
    }
}

ここまでで、どのデバイスから来たキーボード入力か を示す deviceHandle を取得できるようになりました。次は、このデバイスハンドルを「工程1用スキャナ」「工程2用スキャナ」といった論理的な役割に紐づけます。

ステップ3:デバイス情報を取得して工程用スキャナと紐付ける

RAWINPUTHEADER.hDevice は「そのPCに接続されたデバイスの中で一意のハンドル」です。とはいえ、ハンドルそのものは毎回同じ値になるとは限りませんし、人間にとって意味のある値でもありません。そこで、初回入力時に GetRawInputDeviceInfo を使って、ベンダーIDやプロダクトID、デバイス名などを取得し、アプリ側のマスタと紐付けておくのが実践的です。

スキャナ情報クラスの例

public class ScannerInfo
{
    public IntPtr DeviceHandle { get; set; }
    public string DeviceName  { get; set; } = string.Empty;
    public string LogicalName { get; set; } = string.Empty; // 工程1/工程2 など
    public uint VendorId      { get; set; }
    public uint ProductId     { get; set; }
}

フォーム側では、デバイスハンドルをキーにしたディクショナリを持ちます。

private readonly Dictionary<IntPtr, ScannerInfo> _scannerMap
    = new Dictionary<IntPtr, ScannerInfo>();

GetRawInputDeviceInfoでデバイス情報を取得する

初めて見つかったデバイスハンドルに対して、デバイス名と詳細情報を取得するコード例は次の通りです。

private ScannerInfo GetOrCreateScannerInfo(IntPtr deviceHandle)
{
    if (_scannerMap.TryGetValue(deviceHandle, out var info))
    {
        return info;
    }

    info = new ScannerInfo
    {
        DeviceHandle = deviceHandle
    };

    // 1. デバイス名の取得
    uint size = 0;
    NativeMethods.GetRawInputDeviceInfo(
        deviceHandle,
        NativeMethods.RIDI_DEVICENAME,
        IntPtr.Zero,
        ref size);

    if (size > 0)
    {
        IntPtr nameBuffer = Marshal.AllocHGlobal((int)size);
        try
        {
            if (NativeMethods.GetRawInputDeviceInfo(
                    deviceHandle,
                    NativeMethods.RIDI_DEVICENAME,
                    nameBuffer,
                    ref size) > 0)
            {
                info.DeviceName = Marshal.PtrToStringUni(nameBuffer) ?? string.Empty;
            }
        }
        finally
        {
            Marshal.FreeHGlobal(nameBuffer);
        }
    }

    // 2. デバイス詳細情報の取得(ここではキーボードのみを想定)
    uint infoSize = (uint)Marshal.SizeOf<NativeMethods.RID_DEVICE_INFO>();
    var di = new NativeMethods.RID_DEVICE_INFO
    {
        cbSize = infoSize
    };

    IntPtr diBuffer = Marshal.AllocHGlobal((int)infoSize);
    try
    {
        Marshal.StructureToPtr(di, diBuffer, false);
        if (NativeMethods.GetRawInputDeviceInfo(
                deviceHandle,
                NativeMethods.RIDI_DEVICEINFO,
                diBuffer,
                ref infoSize) > 0)
        {
            di = Marshal.PtrToStructure<NativeMethods.RID_DEVICE_INFO>(diBuffer);
            info.VendorId  = di.keyboard.dwSubType; // ベンダーID/プロダクトIDを使う場合は実際の定義に合わせて調整
            info.ProductId = di.keyboard.dwNumberOfKeysTotal;
        }
    }
    finally
    {
        Marshal.FreeHGlobal(diBuffer);
    }

    // 3. 論理名の付与(ここでは仮に手動で割り当てる例)
    info.LogicalName = DecideLogicalName(info);

    _scannerMap[deviceHandle] = info;
    return info;
}

private string DecideLogicalName(ScannerInfo info)
{
    // 実際には、設定ファイルやDBに登録したルールで判定するのがおすすめ
    // ここでは単純に最初に見つかった順に工程1/2/3を割り当てる簡易例

    int count = _scannerMap.Count;
    return count switch
    {
        0 => "工程1スキャナ",
        1 => "工程2スキャナ",
        2 => "工程3スキャナ",
        _ => "その他スキャナ"
    };
}

実運用では、取得した DeviceName(HIDパス)やベンダーID/プロダクトIDを設定画面に一覧表示し、管理者が「これは工程1」「これは工程2」とマッピングを行うUIを用意すると、スキャナの交換にも柔軟に対応できます。

ステップ4:デバイスごとにバーコード文字列を組み立てて処理する

Raw Input APIでは、1つのバーコードが「大量のキー押下イベント」の連続として届きます。そこで、デバイスごとに入力バッファを用意し、

  • 数字や英字のキーで文字を追加
  • Enter(または設定した終了キー)で1バーコードの終端とみなす

というシンプルなロジックでバーコード文字列を組み立てます。

キー入力を処理してバーコードを組み立てる例

private void HandleKeyboardInput(IntPtr deviceHandle, NativeMethods.RAWKEYBOARD kb)
{
    // キーアップは無視(Flags 0x0001 がキーリリース)
    const ushort RI_KEY_BREAK = 0x0001;
    if ((kb.Flags & RI_KEY_BREAK) == RI_KEY_BREAK)
    {
        return;
    }

    var scanner = GetOrCreateScannerInfo(deviceHandle);

    // バッファ取得(なければ作成)
    if (!_deviceBuffers.TryGetValue(deviceHandle, out var sb))
    {
        sb = new StringBuilder();
        _deviceBuffers[deviceHandle] = sb;
    }

    // 仮想キーコードから文字に変換(ここでは簡易的に数字とEnterのみ)
    Keys key = (Keys)kb.VKey;

    if (key == Keys.Enter)
    {
        if (sb.Length > 0)
        {
            string barcode = sb.ToString();
            sb.Clear();

            // 工程別に処理を振り分け
            ProcessBarcode(scanner, barcode);
        }
        return;
    }

    // バーコードは数字のみ想定(必要に応じて拡張)
    if (key >= Keys.D0 && key <= Keys.D9)
    {
        char c = (char)('0' + (key - Keys.D0));
        sb.Append(c);
    }
    else if (key >= Keys.NumPad0 && key <= Keys.NumPad9)
    {
        char c = (char)('0' + (key - Keys.NumPad0));
        sb.Append(c);
    }

    // 必要に応じて英字や機能キーへの対応を追加
}

private void ProcessBarcode(ScannerInfo scanner, string barcode)
{
    // scanner.LogicalName(工程1/2/3)で処理を分岐
    switch (scanner.LogicalName)
    {
        case "工程1スキャナ":
            HandleStep1(barcode);
            break;
        case "工程2スキャナ":
            HandleStep2(barcode);
            break;
        case "工程3スキャナ":
            HandleStep3(barcode);
            break;
        default:
            HandleUnknownScanner(barcode, scanner);
            break;
    }
}

private void HandleStep1(string barcode)
{
    // 工程1用のロジック(部材の受入チェックなど)
}

private void HandleStep2(string barcode)
{
    // 工程2用のロジック(中間検査など)
}

private void HandleStep3(string barcode)
{
    // 工程3用のロジック(出荷ラベルの照合など)
}

private void HandleUnknownScanner(string barcode, ScannerInfo scanner)
{
    // 想定外のスキャナから読まれた場合の処理(アラートなど)
}

このように、バーコード処理ロジックを「工程別」に分離しておけば、新しい工程が追加された際にも ScannerInfo.LogicalName と処理メソッドを追加するだけで拡張できます。

既存のRaw Inputラッパライブラリを使う場合

P/Invokeの定義や構造体の扱いに不安がある場合、Raw Inputを扱いやすい形にラップしたオープンソースライブラリを利用する方法もあります。これらのライブラリを使うと、

  • デバイスの列挙
  • デバイスごとのイベント購読
  • スキャンコードからキー文字列への変換

などを比較的簡単に利用できるようになり、アプリケーション側の実装は「デバイスIDごとのバーコードバッファ管理」に集中できます。自前でP/Invokeを書く場合と比べて導入は楽になりますが、ライブラリのライセンスやメンテナンス状況は事前に確認しておくと安心です。

代替案との比較:プレフィックス方式・COMポート方式

Raw Input以外にも、複数スキャナを区別する方法はいくつか存在します。それぞれの長所/短所を整理しておきます。

方式概要メリットデメリット
Raw Input方式WindowsのRaw Input APIでデバイスハンドルを取得ソフトウェア側でデバイス識別が完結 設定変更に強く、スキャナ入れ替えも柔軟 1台のPCで複数工程を安全に扱えるWin32 API/P/Invokeの知識が必要 実装のコード量はやや多い
プレフィックス方式スキャナ側設定で先頭に識別文字列を付加実装が簡単(通常のKeyPressで処理可能) 既存アプリにも組み込みやすい設定変更されると動作が破綻 オペレーターが誤って設定を変えるリスク
COMポート方式スキャナを仮想COMポートとして認識させるデバイスごとに別ポートとして扱える キーボードとは完全に分離できるスキャナの動作モード設定が必要 シリアル通信処理の実装・エラー対応が必要

特に生産ライン用アプリケーションのように、現場でスキャナ設定を触られるリスクがある場合には、「設定依存度の低さ」という観点からRaw Input方式を採用する価値が高いと言えます。

運用上の注意点とトラブルシューティング

OSバージョンとhDeviceの一意性

  • Windows 10 / 11 では、一般的なHIDキーボード互換バーコードリーダーであれば、ほぼ問題なくデバイスごとに固有の hDevice が得られます。
  • 古いOS(特にWindows 7以前)では、一部のデバイスでハンドルの扱いが不安定だったり、キーボード以外のHIDでハンドルが共有されるケースが報告されています。

可能であれば、ターゲット環境のOSバージョンで事前検証を行い、「USBポートを挿し替えても同じスキャナとして認識されるか」「新しいスキャナに交換した際にマッピングし直せるか」を確認しておくと安心です。

通常キーボードとの共存

Raw Inputでキーボードを登録すると、バーコードリーダーだけでなく通常のキーボード入力もRaw Inputとして届きます。アプリ側で「どのデバイスをバーコードリーダーとして扱うか」を明確に定義し、それ以外のデバイス(通常キーボード)は無視するようにします。

  • 設定画面で、「このデバイスIDはバーコードリーダーとして使用する」というチェックボックスを用意する
  • バーコードリーダーでのみ入力される特定のキー(F13など)を使って、初期登録ウィザードを提供する

といった工夫を行うことで、誤ってキーボードから入力された文字列をバーコードとして解釈してしまう事故を防げます。

例外・異常系の扱い

現場運用では、次のような事象が必ず発生します。

  • スキャナのケーブル抜け/接触不良
  • PC起動後にスキャナを後から挿す/別のポートに挿し替える
  • 誤って別メーカーのスキャナを接続する

このため、アプリケーション側では少なくとも以下のようなログやUIフィードバックを用意しておくと運用が安定します。

  • 不明なデバイスから入力された場合のログ(ハンドル、デバイス名、時刻)
  • 設定されていないスキャナから読み取りがあった場合に、画面上で分かりやすく警告表示
  • スキャナ一覧表示画面で「オンライン/オフライン」を見える化

パフォーマンスとUIスレッド

WM_INPUT はフォームのメッセージループ上で処理されるため、Raw Inputの処理が重いとUI全体のレスポンスが悪化します。次のような工夫をしておくと安全です。

  • Raw Input処理(バーコード確定)まではUIスレッドで行うが、その後の重い処理(DB書き込み、外部サービス連携など)は別スレッド(Task.Run)で行う
  • ログ出力や画面更新を必要最低限に抑える(1バーコードあたり1回程度)
  • 過度に長いバーコードは一定長で切り捨てるなど、異常入力のガードを入れる

まとめ:Raw Inputで「工程ごとに正しいスキャナを使う」文化をつくる

本記事では、C# Windowsフォームアプリで複数台のHIDバーコードリーダーを同一PCに接続しつつ、どのバーコードがどのリーダーから送られたのかを厳密に識別する方法を紹介しました。

  • 通常の KeyPress / KeyDown では、複数のバーコードリーダーを区別できない
  • WindowsのRaw Input APIを使うことで、物理デバイスごとのハンドル(hDevice)を取得できる
  • GetRawInputDeviceInfo でデバイス情報を取得し、工程ごとの論理スキャナにマッピングできる
  • デバイスごとの入力バッファを持ち、Enterキーでバーコード確定することで、工程別の処理を安全に実装できる
  • プレフィックス方式やCOMポート方式と比べて、設定変更に強く、運用の安定性が高い

生産ライン用アプリの品質とトレーサビリティを高めるには、「誰が・どの工程で・どのスキャナを使って読み取ったのか」をソフトウェア側で確実に把握することが重要です。Raw Input APIを活用したデバイス単位の管理は、その強力な土台となります。新規開発だけでなく、既存のWindowsフォームアプリへの段階的な組み込みも十分可能ですので、複数スキャナ運用で課題を抱えている場合はぜひ検討してみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次