WPF(C#/.NET)でWindowsのBluetoothをON/OFFする方法|WinRT Radio API・権限・ポリシーの落とし穴まで解説

WPF(C#/.NET)アプリから Windows の Bluetooth をプログラムで ON/OFF したい——しかし標準APIが見当たらず、環境によって動かないこともあります。本記事では WinRT の Radio API を使った現実的な実装と、権限・ポリシーで失敗する理由、代替策までまとめます。

目次

WPF(C#/.NET)からBluetoothを有効/無効にしたいときにハマる理由

「Bluetooth を ON/OFF する」操作は、単なるアプリ内設定ではなく、Windows が管理している“無線(Radio)デバイス”の状態を変更する行為です。つまり、WPF アプリが勝手に実行できるとは限らず、ユーザーの許可・組織ポリシー・ドライバやハードウェア状態に強く依存します。

さらに混乱しやすいのが、.NET の「Bluetooth 関連 API」と「Bluetooth の電源(ラジオ状態)制御」が別物だという点です。たとえば Windows.Devices.Bluetooth 系は“接続・列挙・GATT 通信”には使えても、Windows の Bluetooth を丸ごと ON/OFF するための“標準API”としてはそのまま使えません。Microsoft Q&A でも「.NET には直接 Bluetooth を制御する built-in API はない」という趣旨の回答が出ています。

結論:Bluetooth の ON/OFF は WinRT の「Radio API」が現実的

Windows では Bluetooth を含む無線機能が “Radio” として扱われます。そのため、WinRT の Windows.Devices.Radios(Radio API)を使うことで、Bluetooth ラジオを列挙し、状態(On/Off)を変更できます。

ただし重要な注意点があります。

  • 状態変更は非同期(Async)で進むため、必ず await して完了を待つ必要があります。SetStateAsync は「許可されたか」を返し、実際の状態遷移はその後に進む、という設計です。
  • 許可は常に取れるわけではありません。RequestAccessAsync の結果が Allowed 以外なら、組織ポリシーやユーザー設定により制御がブロックされている可能性があります。
  • “列挙して状態を読むだけ”は許可不要でできても、“ON/OFF 変更”は許可が必要、という点も見落とされがちです。

方式比較:Radio API と Win32(bthprops.cpl)P/Invoke の違い

方式狙いできることできない/苦手権限・運用面
WinRT:Windows.Devices.Radios(推奨)Bluetooth の ON/OFF(ラジオ状態)Bluetooth ラジオの状態を On/Off に切替Windows のUI(トレイアイコン等)を確実に消す保証はないユーザー許可・システムポリシー次第で失敗。変更操作は許可が必要
Win32:BluetoothEnableDiscovery / BluetoothEnableIncomingConnections(用途注意)検出可能/着信可の切替“見つかる/見つからない”“着信許可”の制御Bluetooth の電源 OFF とは別物(完全無効化の代替にならない場合がある)設定変更扱いで昇格が必要になるケースがある、アプリ終了で元に戻る挙動にも注意

Radio API を WPF から使うための準備(プロジェクト設定の考え方)

Radio API は WinRT API なので、WPF から使うには「Windows の WinRT API を参照できる状態」にする必要があります。やり方はプロジェクトやターゲットによって多少異なりますが、考え方は共通です。

.NET(.NET 6/7/8 など)WPF の場合

  • ターゲットを Windows 向け(例:net8.0-windows / net8.0-windows10.0.xxxxx.x)にする
  • 必要なら Windows SDK 参照(環境や CI 事情で追加の targeting pack が必要になることがある)
  • WinRT 呼び出し自体は using Windows.Devices.Radios; で書ける

もし「WinRT 型が見つからない」「参照できない」場合は、Windows SDK(WinMD)を参照する必要があります。Windows SDK のメタデータ(WinMD)は、WinRT API をコンパイル時に解決するための仕組みです。

.NET Framework WPF(4.8 等)や古い .NET Core の場合

古いプロジェクトでは、WinRT API を使うために Microsoft.Windows.SDK.Contracts などの参照追加が必要になることがあります(Windows 10 WinRT API Pack)。

また、環境によっては WinRT の非同期型を await するために System.Runtime.WindowsRuntime の参照が必要になるケースがあります(“await できない/拡張メソッドが見つからない”系のエラー対策)。※この点は採用しているフレームワーク構成で変わるため、まずはビルド構成を整理するのが近道です。

Windows App SDK を WPF で扱う選択肢

WPF を維持しつつ、Windows Runtime API を扱いやすくする方向として Windows App SDK を併用する考え方もあります(packaged / unpackaged どちらも扱い方が整理されています)。

実装の基本:Bluetooth ラジオを探して状態を切り替える

まずは最小構成の流れを押さえます。

  1. Radio.RequestAccessAsync() で制御許可を確認
  2. Radio.GetRadiosAsync() でラジオ一覧を取得
  3. RadioKind.Bluetooth を選ぶ
  4. SetStateAsync(RadioState.On/Off) を await し、戻り値(許可されたか)を確認

「await してるのに切り替わった気がしない」問題の正体

SetStateAsync は戻り値として RadioAccessStatus を返しますが、これは「要求が受け入れられたかどうか」です。実際のラジオ状態の遷移はその後に非同期で進み、ハードウェアスイッチなどでブロックされる可能性もあります。つまり、UI 上は少し待ってから反映されることがあります。

堅牢版サンプル:切り替え結果(許可)と、最終状態(On/Off)を分けて扱う

現場で事故が少ないのは、「許可が取れたか」と「状態が変わったか」を分離してログに残す設計です。以下は WPF でも使いやすい形にした例です。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Devices.Radios;

public static class BluetoothRadioController
{
    public static async Task<RadioAccessStatus> SetBluetoothAsync(
        RadioState targetState,
        TimeSpan? waitTimeout = null,
        CancellationToken cancellationToken = default)
    {
        // 1) 許可確認(状態変更には許可が必要)
        var access = await Radio.RequestAccessAsync();
        if (access != RadioAccessStatus.Allowed)
            return access;

        // 2) ラジオ列挙(Bluetooth が複数ある環境もある)
        var radios = await Radio.GetRadiosAsync();
        var btRadios = radios.Where(r => r.Kind == RadioKind.Bluetooth).ToList();

        if (btRadios.Count == 0)
            throw new InvalidOperationException("Bluetooth ラジオが見つかりません。ドライバ/デバイス構成を確認してください。");

        // 3) すべての Bluetooth ラジオに対して状態変更
        RadioAccessStatus lastStatus = RadioAccessStatus.Unspecified;

        foreach (var bt in btRadios)
        {
            cancellationToken.ThrowIfCancellationRequested();

            lastStatus = await bt.SetStateAsync(targetState);
            if (lastStatus != RadioAccessStatus.Allowed)
                return lastStatus;

            // 4) オプション:状態遷移を少し待つ(UIで“変わってない”と誤認しがちなので)
            if (waitTimeout is not null)
            {
                var timeoutAt = DateTimeOffset.UtcNow + waitTimeout.Value;
                while (bt.State != targetState && DateTimeOffset.UtcNow < timeoutAt)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    await Task.Delay(200, cancellationToken);
                }
            }
        }

        return lastStatus;
    }

    public static Task<RadioAccessStatus> EnableBluetoothAsync(CancellationToken ct = default)
        => SetBluetoothAsync(RadioState.On, TimeSpan.FromSeconds(5), ct);

    public static Task<RadioAccessStatus> DisableBluetoothAsync(CancellationToken ct = default)
        => SetBluetoothAsync(RadioState.Off, TimeSpan.FromSeconds(5), ct);

    public static async Task<RadioAccessStatus> ToggleBluetoothAsync(CancellationToken ct = default)
    {
        var radios = await Radio.GetRadiosAsync();
        var bt = radios.FirstOrDefault(r => r.Kind == RadioKind.Bluetooth);
        if (bt == null)
            throw new InvalidOperationException("Bluetooth ラジオが見つかりません。");

        var target = (bt.State == RadioState.On) ? RadioState.Off : RadioState.On;
        return await SetBluetoothAsync(target, TimeSpan.FromSeconds(5), ct);
    }
}

WPF の画面(ボタン)から安全に呼ぶ例

WPF で一番多い事故は「UI スレッドをブロックする」「連打で競合する」「例外が握りつぶされて原因が分からない」です。最低限、ボタン無効化と例外表示だけでも入れておくと、運用が楽になります。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

public partial class MainWindow : Window
{
    private CancellationTokenSource? _cts;

    public MainWindow()
    {
        InitializeComponent();
    }

    private async void ToggleBluetoothButton_Click(object sender, RoutedEventArgs e)
    {
        ToggleBluetoothButton.IsEnabled = false;
        _cts?.Cancel();
        _cts = new CancellationTokenSource();

        try
        {
            var status = await BluetoothRadioController.ToggleBluetoothAsync(_cts.Token);

            if (status == Windows.Devices.Radios.RadioAccessStatus.Allowed)
            {
                MessageBox.Show("Bluetooth の切り替え要求が受け付けられました。", "成功");
            }
            else
            {
                MessageBox.Show($"Bluetooth の切り替えが許可されませんでした: {status}", "失敗");
            }
        }
        catch (OperationCanceledException)
        {
            // 連打などでキャンセルされた場合は静かに終了
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "例外");
        }
        finally
        {
            ToggleBluetoothButton.IsEnabled = true;
        }
    }
}

「動かない」原因のほとんどはここ:症状別チェックリスト

症状よくある原因対処の方向性
RequestAccessAsync() が Allowed 以外ユーザー許可がない/組織ポリシーで制御禁止/設定でアプリの無線制御が拒否結果(DeniedByUser / DeniedBySystem / Unspecified)をログに残し、手動切替へ誘導。管理端末ならポリシー側の見直し
SetStateAsync() が Allowed 以外状態変更が拒否された(許可・ポリシー・ハード制限)戻り値を必ず評価。UI に「禁止されている可能性」を出す
GetRadiosAsync() が空(Bluetooth が見つからない)ビルドアーキテクチャ不一致(AnyCPU 等)で列挙できない事例/ドライバやデバイス未搭載x64 端末なら x64 ビルドを試す。最初に「BT ラジオが存在するか」を UI で診断表示する
await していない(または UI がすぐ閉じる)非同期完了前に処理が終わり、切り替えが反映されない必ず await。古い書き方の場合は Completed イベントなどで完了待ちを入れる(“Completed を付けたら動いた”は典型)
Off にしてもトレイの Bluetooth アイコンが消えないアイコン表示は Windows UI 仕様(状態だけで必ず消えるとは限らない)「アイコン非表示」は別レイヤーの要求として切り分け、端末管理(GPO/MDM)やユーザー設定に寄せる

Radio API の「許可(RadioAccessStatus)」をどう扱うべきか

Radio API は「できた/できない」を例外だけで表現しません。RequestAccessAsync と SetStateAsync は RadioAccessStatus を返し、ここに“拒否の理由”が含まれます。アプリ側ではこの値を UI とログの両方に残すのが重要です。

RadioAccessStatus意味合い(実務的な解釈)アプリ側の推奨対応
Allowed操作要求は受理された(状態遷移はこの後に非同期で進む)数秒の猶予を見て状態表示を更新。必要なら StateChanged で追従
DeniedByUserユーザー側の設定・許可で拒否されたユーザーに手動切替手順を提示(設定画面への誘導など)
DeniedBySystemOS / 組織ポリシー / デバイス制限で拒否された管理者(IT)にエスカレーションする導線を用意(ログ出力が特に重要)
Unspecified原因が明確でない拒否環境情報(OS、ビルド、アーキ、権限、パッケージ有無)を収集して切り分け

MSIX(パッケージ化)している場合は「radios capability」の宣言が論点になる

もし WPF アプリを MSIX 等でパッケージ化している場合、UWP の権限モデルに寄るため、マニフェストで capability を宣言しないと API の利用が制限されることがあります。Windows.Devices.Radios を使うには “Radio device capability” を宣言する、と Microsoft Learn に明記されています。

代表的には次のような宣言が話題になります(どのマニフェストにどう書くかはパッケージ構成で変わるため、まず「MSIX か/非パッケージか」を切り分けてください)。

<DeviceCapability Name="radios" />

「管理者権限がないから動かない?」への現実的な答え

結論から言うと、「一般ユーザー権限で必ず失敗する」とは限りません。しかし、環境次第で失敗する可能性は高く、特に企業端末や制限端末では“システム側の拒否”が起きやすい、というのが実務感です。

また、同じ “Bluetooth を無効化” でも、やろうとしているレベルが違うと必要権限も変わります。よく混ざる要求を分解して整理すると、判断が一気に楽になります。

やりたいこと一般ユーザーでできる可能性補足
Bluetooth を ON/OFF(ラジオ状態)にする端末設定・ポリシー次第Radio API は許可が取れれば可能。拒否される場合は DeniedBySystem/DeniedByUser になる
検出可能/着信許可を切り替える端末次第(昇格が必要になるケースあり)Win32 の BluetoothEnableDiscovery / BluetoothEnableIncomingConnections は“電源OFF”ではない。状態がアプリ生存期間に依存する点も注意
デバイスマネージャで Bluetooth アダプタ自体を無効化(ドライバ無効)基本的に難しいこれは OS 管理領域の操作になりやすく、運用・セキュリティ面でも管理部門と合意が必要
Bluetooth サービスを停止・無効化して“完全に使えない”状態に固定基本的に難しい影響範囲が大きく、キーボード/マウス等に波及することもある。端末管理(GPO/MDM)でやる領域
トレイの Bluetooth アイコンを“必ず消す”アプリだけでは困難UI 表示は Windows 側の仕様。RadioState.Off と表示は必ずしも一致しない

Microsoft Q&A のスレッドでも、bthprops.cpl 経由の関数は「通常は昇格した権限が必要になる」と案内されています。したがって、ユーザー権限で動かしたい場合は、まず Radio API で「許可が取れるか」を判定し、ダメなら端末管理側の施策に寄せるのが安全です。

方式B(用途注意):bthprops.cpl を P/Invoke して検出/着信を切り替える

検索するとよく出てくるのが、BluetoothEnableDiscovery と BluetoothEnableIncomingConnections を呼ぶ方法です。ただしこれは “Bluetooth の電源OFF” ではなく、「検出可能」「着信を受け付ける」設定を変える方向に寄った API です。期待している挙動(Windows の Bluetooth トグルそのもの)とズレることがあります。

さらに重要なのが、BluetoothEnableDiscovery は変更状態が“呼び出したアプリの生存期間”に紐づき、アプリが終わると元に戻る、という性質がドキュメントに明記されている点です。恒久的な「禁止」には向きません。

P/Invoke サンプル(何をしているかが分かる最小例)

using System;
using System.Runtime.InteropServices;

public static class BluetoothVisibilityController
{
// Win32 API の DLL は bthprops.cpl(ドキュメントの Requirements に記載)
[DllImport("bthprops.cpl", SetLastError = true)]
private static extern bool BluetoothEnableDiscovery(IntPtr hRadio, bool fEnabled);


[DllImport("bthprops.cpl", SetLastError = true)]
private static extern bool BluetoothEnableIncomingConnections(IntPtr hRadio, bool fEnabled);

public static void SetDiscoverableAndConnectable(bool enabled)
{
    // hRadio に NULL(IntPtr.Zero)を渡すと「ローカルの全ラジオに対して」試行される
    // ただし、これは“電源OFF”ではない点に注意
    var ok1 = BluetoothEnableIncomingConnections(IntPtr.Zero, enabled);
    var ok2 = BluetoothEnableDiscovery(IntPtr.Zero, enabled);

    if (!ok1 || !ok2)
        throw new InvalidOperationException("Bluetooth の検出/着信設定を変更できませんでした。権限やポリシーを確認してください。");
}


}

この方式を採用するときの実務的な注意点

  • 「非connectableは非discoverable」など、設定の前提条件がある(順序を誤ると失敗する)
  • 状態がアプリ生存期間に依存し、恒久的な無効化には向かない
  • 端末によっては昇格が必要になり、一般ユーザー権限では失敗する可能性がある

「完全に消したい」「ユーザーに触らせたくない」要求は、アプリではなく端末管理で解く

よくある追加要望が「Bluetooth を完全に消して、トレイ(通知領域)の Bluetooth アイコンも表示させたくない」「ユーザーが再び ON にできないようロックしたい」です。このレベルになると、WPF アプリ単体での実現は難易度が一気に上がります。

理由はシンプルで、

  • トレイアイコンの表示は Windows の UI 仕様やユーザー設定の範囲であり、ラジオ状態の ON/OFF と 1:1 で連動しないことがある
  • “ユーザーに触らせない”はセキュリティ/運用ポリシーの話であり、アプリで隠すより GPO/MDM のほうが筋が良い
  • サービス無効化やデバイス無効化は管理領域で、影響範囲も広く、基本的に管理者合意が必要

現実的には次のような役割分担が安定します。

要件推奨アプローチ理由
アプリから Bluetooth を切り替えたい(便利用途)Radio API で ON/OFF(許可が取れる範囲で)OS が用意する無線制御の正攻法。失敗時はユーザー誘導に回せる
企業端末で Bluetooth を禁止したい(ロック用途)GPO/MDM(Intune 等)で禁止ユーザー回避を封じるのは端末管理の仕事。監査・運用にも乗せやすい
Bluetooth を“存在しない”扱いにしたいデバイス無効化(ドライバ無効)を管理者側で実施アプリでの擬似的な隠蔽より確実。ただし影響と保守が大きい

実務で効く小技:失敗したら“設定画面”に誘導してユーザーに切り替えてもらう

組織ポリシーやユーザー設定で拒否される環境は一定数あります。その場合、アプリ側で無理に突破しようとすると、セキュリティ上も運用上も厳しくなります。

おすすめは、RadioAccessStatus が Allowed 以外なら、

  • アプリ内では「この端末ではプログラムから Bluetooth を切り替えられません」と明示
  • 設定画面(Bluetooth 設定)への誘導ボタンを用意
  • IT 管理下なら「ポリシーで無効化されている可能性」をメッセージに含める(問い合わせが早くなる)

という“落としどころ”を作ると、現場のトラブルシュートが劇的に楽になります。

まとめ:WPF で Bluetooth を ON/OFF するなら、まず Radio API を軸に設計する

WPF(C#/.NET)から Windows の Bluetooth を ON/OFF したい場合、最も現実的なのは WinRT の Windows.Devices.Radios を使う方法です。特に、

  • RequestAccessAsync / SetStateAsync の戻り値(許可)を必ず評価する
  • 状態遷移は非同期なので await を徹底し、必要なら少し待つ/StateChanged で追従する
  • “完全に禁止”“UI から消す”はアプリ単体で無理をせず、端末管理(GPO/MDM)に寄せる

この3点を押さえるだけで、「動く端末と動かない端末がある」「Completed を付けたら動いた」系の混乱をかなり減らせます。

この記事を書いた人

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

コメント

コメントする

目次