WPFでUSB/HDMIを実行中だけ無効化して終了時に戻す方法|管理者権限・非管理者運用(サービス/タスク)まで解説

WPFアプリ(.NET Framework 4.7.2)の実行中だけUSBやHDMIを使わせたくても、「管理者権限がない端末でも動かしたい」という条件が付くと一気に難易度が上がります。Windowsの仕組み上できること/できないことを整理し、現実的に“終了時に必ず元に戻す”まで含めた実装パターンをまとめます。

目次

やりたいことを「Windowsで実現できる形」に言い換える

まず押さえたいのが、Windowsでは「USBポートをOFF」「HDMIポートをOFF」という“物理ポート単位”の制御は、一般的なアプリからはできないという点です。実際にできるのは、ポートにぶら下がるデバイス(PnPデバイス)を無効化したり、特定クラスの機能を制限したり、表示構成を切り替える、といった間接的な制御になります。

やりたいこと(言葉)Windowsでの現実的な制御対象代表的な副作用・注意点
USBポートを使えなくしたいUSBホストコントローラ/USBハブ/特定USBデバイス/USBストレージドライバ(USBSTOR)コントローラやハブを無効化するとキーボード・マウスも死ぬ危険。USBストレージだけ止める方が安全。
USBメモリだけ禁止したいUSBストレージ(USBSTOR)・リムーバブルストレージポリシー管理者での設定が必要。反映に再接続/再起動が絡むことがある。
HDMIを使えなくしたい外部ディスプレイ出力(表示構成)/ディスプレイ・アダプタ・音声出力デバイスデバイス無効化は画面が消えるなど致命的。表示構成切替の方が実運用向き。

結論:USB/HDMIの「デバイス無効化」は原則として管理者権限が必要

USBやHDMIの“使用不可”をデバイス無効化で実現する場合、WindowsのPnP(Plug and Play)管理やドライバ制御に関わるため、基本的に管理者権限(UAC昇格)が必要になります。非管理者のままアプリ単体で直接制御するのは、通常できない(もしくは環境依存で不安定)と考えるのが安全です。

ここでいう「非管理者でも実現したい」は、次のどちらかの意味に分解すると設計が進みます。

  • アプリの起動操作は一般ユーザーで行いたい(ただし裏側に管理者コンポーネントがいても良い)
  • OS上の権限的にも一般ユーザーのみで完結させたい(この条件は“デバイス無効化”ではほぼ不可)

実現パターン比較:何を優先するかで最適解が変わる

要件に対して現実的な選択肢を並べると、以下のようになります。

パターン非管理者での起動制御の確実性ユーザー体験おすすめ度
WPFアプリ自体を管理者で起動(UAC昇格)×(UACで昇格が必要)高い毎回UACが出る小規模・単発なら最短
管理者権限のWindowsサービスに制御を委譲(WPFは一般ユーザー)○高いUAC不要(初回導入時のみ管理者)長期運用の定番
タスクスケジューラ(最高権限)で制御用EXEを起動○中〜高UAC不要(登録時のみ管理者)導入が比較的楽
USBストレージだけをポリシー/レジストリで禁止(恒久設定)○高い“アプリ実行中だけ”は苦手目的が合えば強い
HDMIはデバイス無効化ではなく表示構成を「内蔵画面のみ」に切替○(多くの環境で可能)中画面が一瞬切り替わるHDMI対策の現実解

対象デバイスの特定:まず「どれを無効化するか」を決める

USBやHDMIは“範囲”が広いので、闇雲に無効化すると事故ります。推奨は、以下の順で対象デバイスを絞り込むことです。

  • デバイスマネージャーで対象のカテゴリとデバイス名を確認する
  • プロパティから「デバイス インスタンス パス(インスタンスID)」を控える
  • アプリ側ではそのIDをキーにして、無効化/有効化を行う

特にUSBは「USBコントローラ」「USBルートハブ」「汎用USBハブ」「USB大容量記憶装置」など層が分かれており、上流を無効化すると入力機器まで止まります。まずは“USBメモリだけ”なのか、“特定のUSB機器だけ”なのかを明確にしてください。

WMIでデバイス一覧を列挙する(特定フェーズ)

WMIは「列挙」には便利です。WPFからも System.Management で利用できます。以下はUSB系のPnPデバイスをざっと列挙するサンプルです(無効化は別手段が必要になりやすい点に注意)。

using System;
using System.Management;

public static class PnpListSample
{
    public static void ListUsbPnP()
    {
        // USB関連だけ拾いたい場合は PNPClass='USB' などで絞る
        var query = "SELECT Name, DeviceID, PNPClass FROM Win32_PnPEntity WHERE PNPClass='USB'";
        using (var searcher = new ManagementObjectSearcher(query))
        using (var results = searcher.Get())
        {
            foreach (ManagementObject mo in results)
            {
                var name = mo["Name"]?.ToString();
                var id = mo["DeviceID"]?.ToString(); // 例: USB\\VID_XXXX&PID_YYYY\\....
                Console.WriteLine($"{name} | {id}");
            }
        }
    }
}

ここで得られる DeviceID は、後述するSetupAPI側で扱う「デバイス インスタンスID」と近い形で取得できることが多いですが、環境やデバイスによって表記が揺れます。最終的にはデバイスマネージャーで確認したインスタンスパスを“正”として扱うのが安全です。

有効化/無効化の実装:WMIよりSetupAPI/ConfigManagerが確実

USB/HDMIを“デバイスとして無効化”するなら、Windowsが提供するデバイス管理API(SetupAPI / ConfigManager)を使うのが王道です。WMIの InvokeMethod でうまくいくケースもありますが、クラスやデバイスに依存しやすく、実運用で詰まりがちです。

実装方針のイメージ

  • 列挙:SetupAPIでデバイス一覧を取り、インスタンスIDを取得する
  • 操作:該当デバイスに対して「有効/無効」を適用する(DIF_PROPERTYCHANGEなど)
  • 前提:原則として管理者権限が必要(サービス化/タスク化で回避)

SetupAPIで「デバイスを有効/無効」にする例(インスタンスID指定)

以下は概念が伝わるように、インスタンスID一致で対象を探し、DIF_PROPERTYCHANGEで状態変更する流れをまとめた例です。実際の製品コードでは、例外処理、ログ、対象の複数一致、再試行、64bit/32bitの扱いなどを厚くしてください。

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;

public static class DeviceEnableDisable
{
    private const uint DIGCF_PRESENT = 0x00000002;
    private const uint DIGCF_ALLCLASSES = 0x00000004;

    private const uint DIF_PROPERTYCHANGE = 0x00000012;
    private const uint DICS_ENABLE = 0x00000001;
    private const uint DICS_DISABLE = 0x00000002;
    private const uint DICS_FLAG_GLOBAL = 0x00000001;

    [StructLayout(LayoutKind.Sequential)]
    private struct SP_DEVINFO_DATA
    {
        public int cbSize;
        public Guid ClassGuid;
        public uint DevInst;
        public IntPtr Reserved;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct SP_CLASSINSTALL_HEADER
    {
        public int cbSize;
        public uint InstallFunction;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct SP_PROPCHANGE_PARAMS
    {
        public SP_CLASSINSTALL_HEADER ClassInstallHeader;
        public uint StateChange;
        public uint Scope;
        public uint HwProfile;
    }

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern IntPtr SetupDiGetClassDevs(
        IntPtr ClassGuid,
        string Enumerator,
        IntPtr hwndParent,
        uint Flags);

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern bool SetupDiEnumDeviceInfo(
        IntPtr DeviceInfoSet,
        uint MemberIndex,
        ref SP_DEVINFO_DATA DeviceInfoData);

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern bool SetupDiGetDeviceInstanceId(
        IntPtr DeviceInfoSet,
        ref SP_DEVINFO_DATA DeviceInfoData,
        StringBuilder DeviceInstanceId,
        int DeviceInstanceIdSize,
        out int RequiredSize);

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern bool SetupDiSetClassInstallParams(
        IntPtr DeviceInfoSet,
        ref SP_DEVINFO_DATA DeviceInfoData,
        ref SP_PROPCHANGE_PARAMS ClassInstallParams,
        int ClassInstallParamsSize);

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern bool SetupDiCallClassInstaller(
        uint InstallFunction,
        IntPtr DeviceInfoSet,
        ref SP_DEVINFO_DATA DeviceInfoData);

    [DllImport("setupapi.dll", SetLastError = true)]
    private static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);

    public static void SetEnabledByInstanceId(string instanceId, bool enable)
    {
        if (string.IsNullOrWhiteSpace(instanceId))
            throw new ArgumentException("instanceId is empty.");

        IntPtr infoSet = SetupDiGetClassDevs(IntPtr.Zero, null, IntPtr.Zero, DIGCF_PRESENT | DIGCF_ALLCLASSES);
        if (infoSet == IntPtr.Zero || infoSet.ToInt64() == -1)
            throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiGetClassDevs failed.");

        try
        {
            var devInfo = new SP_DEVINFO_DATA();
            devInfo.cbSize = Marshal.SizeOf(typeof(SP_DEVINFO_DATA));

            for (uint i = 0; ; i++)
            {
                if (!SetupDiEnumDeviceInfo(infoSet, i, ref devInfo))
                {
                    int err = Marshal.GetLastWin32Error();
                    // ERROR_NO_MORE_ITEMS = 259
                    if (err == 259) break;
                    throw new Win32Exception(err, "SetupDiEnumDeviceInfo failed.");
                }

                var sb = new StringBuilder(1024);
                if (!SetupDiGetDeviceInstanceId(infoSet, ref devInfo, sb, sb.Capacity, out _))
                    continue;

                var currentId = sb.ToString();
                if (!currentId.Equals(instanceId, StringComparison.OrdinalIgnoreCase))
                    continue;

                var header = new SP_CLASSINSTALL_HEADER
                {
                    cbSize = Marshal.SizeOf(typeof(SP_CLASSINSTALL_HEADER)),
                    InstallFunction = DIF_PROPERTYCHANGE
                };

                var propChange = new SP_PROPCHANGE_PARAMS
                {
                    ClassInstallHeader = header,
                    StateChange = enable ? DICS_ENABLE : DICS_DISABLE,
                    Scope = DICS_FLAG_GLOBAL,
                    HwProfile = 0
                };

                if (!SetupDiSetClassInstallParams(infoSet, ref devInfo, ref propChange, Marshal.SizeOf(propChange)))
                    throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiSetClassInstallParams failed.");

                if (!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, infoSet, ref devInfo))
                    throw new Win32Exception(Marshal.GetLastWin32Error(), "SetupDiCallClassInstaller failed.");

                return; // 見つけて処理したら終了
            }

            throw new InvalidOperationException("Device not found. instanceId=" + instanceId);
        }
        finally
        {
            SetupDiDestroyDeviceInfoList(infoSet);
        }
    }
}

重要:この種のAPI呼び出しは、非管理者で実行すると ERROR_ACCESS_DENIED になりやすいです。つまり「WPFアプリ単体で非管理者のままUSB/HDMIを無効化する」は、ここで詰まるのが自然です。

プロトタイピングはコマンドで検証すると速い

いきなりP/Invokeで作り込む前に、「そのデバイスが本当に無効化できるか」「どのインスタンスIDが正しいか」をコマンドで試すと調査が速くなります。代表的な流れは次の通りです。

  • デバイス列挙:pnputil などで確認(OS標準)
  • 無効化・有効化:devcon(WDK/ツール)などで事前検証

ただし、無効化系コマンドは基本的に管理者で実行する必要があります。

USB対策は「目的別」に分けると事故が減る

USBを止めたい理由はさまざまです。目的によって最適解が変わるため、最初に選択肢を整理しておくのが安全です。

目的おすすめ手段管理者権限“アプリ実行中だけ”適性注意点
USBメモリ等の持ち出し防止USBストレージ(USBSTOR)無効/リムーバブルストレージポリシー必要低〜中反映のタイミングが読みづらい。恒久対策向き。
特定のUSB機器だけ禁止そのデバイスのインスタンスIDを無効化必要高対象の取り違えに注意。IDが変わる機器もある。
新規USB接続を抑止デバイスインストール制限(ポリシー)必要低運用ポリシー設計が必要。アプリ単体の切替は不向き。
USB全般を止めたいUSBコントローラ/ハブを無効化必要中入力機器が止まり復旧困難になる可能性。基本非推奨。

レジストリでUSBストレージを止める案の位置づけ

よく挙がるのが HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR の Start を変更してUSBストレージドライバを無効化する方法です。これは「USBポートそのもの」ではなく、USBストレージのドライバを止めるアプローチです。

  • メリット:狙いが「USBメモリ禁止」なら目的に合う
  • デメリット:管理者権限が必須、反映タイミングが環境依存、アプリ実行中だけのON/OFFは運用が難しい

アプリ起動中だけの一時的制御に寄せるなら、レジストリ切替よりも「対象デバイス無効化」または「管理者サービスに委譲」の方が設計しやすいことが多いです。

HDMI対策は「デバイス無効化」より「外部表示を使えない状態」を作る

HDMIはUSBよりもさらに注意が必要です。HDMIを“デバイス無効化”で潰そうとすると、GPU、モニタ、オーディオ出力(HDMIオーディオ)など複数のデバイスが絡みます。誤ると自分の画面が消える、RDP/リモート操作も困難になる、などの事故に直結します。

実運用で現実的:表示構成を「PC画面のみ(内蔵のみ)」に切り替える

外部モニタを使わせないことが目的なら、ポートを物理的に殺すのではなく、Windowsの表示構成を切り替えて「内蔵ディスプレイのみ」を強制するのが現実的です。手軽な手段として DisplaySwitch.exe があり、次のように実行できます。

using System.Diagnostics;

public static class DisplaySwitchUtil
{
    // 外部ディスプレイを使わせたくない場合の一例
    public static void ToInternalOnly()
    {
        Process.Start(new ProcessStartInfo
        {
            FileName = "displayswitch.exe",
            Arguments = "/internal",
            UseShellExecute = false,
            CreateNoWindow = true
        });
    }

    // 例:拡張に戻す(元の状態復元は本来はQuery/SetDisplayConfigで保存復元が理想)
    public static void ToExtend()
    {
        Process.Start(new ProcessStartInfo
        {
            FileName = "displayswitch.exe",
            Arguments = "/extend",
            UseShellExecute = false,
            CreateNoWindow = true
        });
    }
}

ポイント:DisplaySwitch.exe は切替はできても「元の構成を自動で復元」する仕組みが弱いです。厳密に“元に戻す”までやるなら、起動時に QueryDisplayConfig で現在の構成を保存し、終了時に SetDisplayConfig で復元する設計を検討してください(P/Invoke量は増えますが再現性は上がります)。

「HDMI音声だけ禁止」など目的がズレているケース

HDMIで困っているのが映像ではなく音声出力(HDMI Audio)だけ、ということもあります。その場合は「既定の再生デバイスを内蔵スピーカーに戻す」などのソフト制御で足りることがあります。ただし既定デバイスの切替はAPIがやや面倒で、端末差もあるため、運用要件(本当に禁止したいのか、単に切替したいだけか)を先に固めるのがおすすめです。

非管理者でも“操作としては”実現する定番構成:管理者サービス/タスクに委譲

「一般ユーザーがWPFを起動するだけで、実行中はUSB/HDMIを制限し、終了したら元に戻す」を実現するなら、結局ここに行き着くことが多いです。

  • WPFアプリ:一般ユーザー権限で動作(UI担当)
  • 制御コンポーネント:管理者権限で常駐(Windowsサービス)または必要時起動(タスクスケジューラ)
  • WPFは「開始」「終了」「生存確認(ハートビート)」だけを送る

なぜこの構成が強いのか

  • 権限問題を“設計で分離”できる(UIは一般権限、危険な操作だけ特権)
  • 終了時復元を堅牢にできる(WPFが落ちてもサービス側で復旧できる)
  • 監査・ログ・制御範囲の固定がしやすい(勝手に別デバイスを触れない)

設計の肝:落ちても必ず復元する「フェイルセーフ」

“終了時に元に戻す”で最も怖いのは、アプリが異常終了したときに戻らないことです。そこで、サービス(または制御側プロセス)に以下の責務を持たせると堅牢になります。

仕組み概要効果
状態のスナップショット制限開始前に「対象デバイスが有効だったか」を保存終了時に“元の状態”へ確実に戻せる
ハートビートWPFが数秒ごとに「生きてる」信号を送るプロセス強制終了でも一定時間で復旧
タイムアウト復旧一定期間ハートビートが来なければ自動復旧戻し忘れの最終安全弁
排他/参照カウント複数起動時に制御が競合しないよう管理誤って復旧してしまう事故を防ぐ
操作ログいつ誰が開始/終了したかを記録運用トラブル時に原因追跡できる

通信はNamed Pipeが扱いやすい(ローカル限定で高速)

.NET Framework 4.7.2のWPFとWindowsサービスの組み合わせなら、Named Pipe(名前付きパイプ)が扱いやすい選択肢です。ネットワーク越しではなくローカルPC内で完結し、認可(どのユーザーが接続できるか)も比較的設計しやすいからです。

イメージは次の通りです。

  • WPF起動 → サービスへ「BEGIN」送信 → サービスが対象デバイスを無効化
  • WPF実行中 → 定期的に「PING」送信 → サービスは監視を継続
  • WPF終了 → サービスへ「END」送信 → サービスが元の状態へ復元
  • WPFが落ちる → 「PING」が止まる → サービスがタイムアウトで復元

以下はWPF側が送信する最小例です(実運用では署名付きメッセージ、ユーザー識別、リプレイ対策などのセキュリティも検討してください)。

using System;
using System.IO;
using System.IO.Pipes;
using System.Text;

public static class DeviceControlClient
{
    public static void Send(string message)
    {
        using (var client = new NamedPipeClientStream(".", "DeviceControlPipe", PipeDirection.Out))
        {
            client.Connect(2000);
            using (var writer = new StreamWriter(client, new UTF8Encoding(false)) { AutoFlush = true })
            {
                writer.WriteLine(message);
            }
        }
    }
}

// 例:アプリ開始時
// DeviceControlClient.Send("BEGIN");

// 例:定期ハートビート(DispatcherTimerなどで)
// DeviceControlClient.Send("PING");

// 例:終了時
// DeviceControlClient.Send("END");

WPFアプリ側で「終了時に戻す」を強くする実装ポイント

WPFだけで完結させると、強制終了やクラッシュで復旧処理が走らない可能性が残ります。それでもアプリ側でできることはやっておくと事故率が下がります。

  • App.OnExit で必ず「END」を送る
  • DispatcherUnhandledException で例外ログを残しつつ「END」を試みる
  • AppDomain.CurrentDomain.UnhandledException や ProcessExit でも可能な限り復旧要求を出す
  • 制御開始直後にタイムアウト復旧のためのハートビートを開始する
using System;
using System.Windows;

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        this.DispatcherUnhandledException += (s, ex) =>
        {
            try { DeviceControlClient.Send("END"); } catch { /* 握りつぶさずログ推奨 */ }
            // 必要ならログ出力
        };

        AppDomain.CurrentDomain.UnhandledException += (s, ex) =>
        {
            try { DeviceControlClient.Send("END"); } catch { }
        };

        AppDomain.CurrentDomain.ProcessExit += (s, ex) =>
        {
            try { DeviceControlClient.Send("END"); } catch { }
        };

        // 開始要求
        DeviceControlClient.Send("BEGIN");
        // ここからタイマーでPINGを送る設計にする
    }

    protected override void OnExit(ExitEventArgs e)
    {
        try { DeviceControlClient.Send("END"); } catch { }
        base.OnExit(e);
    }
}

ただし、これらは“最後の努力”であり、強制終了やOSクラッシュまで含めるなら、やはり特権側(サービス/タスク)でのタイムアウト復旧が本命です。

「元に戻す」を正確にするための状態管理

終了時の復元でありがちな失敗は、「とにかく有効化すればOK」と思い込んでしまうことです。実際には、アプリ起動前から既に無効だったデバイスを、終了時に有効化してしまう事故が起こり得ます。そこで、制御開始時に次の情報を保存します。

  • 対象デバイスのインスタンスID
  • 開始時点での状態(有効/無効)
  • 操作結果(無効化に成功したか)
  • 復元時に戻すべき状態

運用が複雑な場合は、サービス側でJSONなどにして一時保存し、サービス再起動時でも復旧できるようにする設計も有効です(ただし保存先の権限と改ざん耐性は要検討です)。

トラブルシューティング:詰まりやすいポイントと対策

アクセス拒否(Access Denied)が出る

  • 原因:無効化/有効化が管理者権限を要求している
  • 対策:WPFをUAC昇格で起動する、または制御をサービス/タスクへ移す

インスタンスIDが一致しない/見つからない

  • 原因:WMIのDeviceIDと、SetupAPIのインスタンスIDの形式が異なる、または大文字小文字・エスケープが違う
  • 対策:デバイスマネージャーの「デバイス インスタンス パス」を正として保持する。接続ポートが変わるとIDが変わる機器もあるため、運用側で固定する。

USBを止めたらキーボード/マウスが効かなくなった

  • 原因:USBコントローラやUSBハブなど上流を無効化した
  • 対策:原則として上流は触らない。禁止したいのがUSBストレージなら、ストレージ限定の対策に寄せる。

HDMI無効化で画面が消えた

  • 原因:GPU/ディスプレイ関連デバイスを無効化してしまった
  • 対策:デバイス無効化ではなく表示構成切替(内蔵のみ)に切り替える。復元手段(リモート、セーフモードなど)も事前に用意する。

最終的なおすすめ設計

「WPFアプリ実行中だけUSB/HDMIを制限し、終了時に元へ戻す」を、管理者・非管理者の両方で破綻なく運用したいなら、次の方針が堅実です。

  • USB:目的がUSBストレージ禁止ならストレージ限定の対策を優先。どうしてもデバイス無効化が必要なら、対象IDを厳密に固定し、制御は管理者サービス/タスクへ委譲する。
  • HDMI:デバイス無効化で潰すより、表示構成を「内蔵のみ」に切り替える方が安全。厳密に復元したいなら表示構成の保存・復元(Query/SetDisplayConfig)を検討する。
  • 非管理者運用:WPFは一般権限のまま、特権操作だけをサービス/タスクに分離する。ハートビート+タイムアウト復旧で“必ず戻す”を担保する。

この構成なら、権限の壁に正面からぶつからず、運用としても「いつの間にか戻らない」「端末が操作不能になる」といった事故を最小化できます。セキュリティ要件が絡む場合は、端末管理(GPO/MDM/Defender等)での恒久対策と、アプリ実行中だけの一時対策を分けて設計するとさらに安定します。

この記事を書いた人

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

コメント

コメントする

目次