Windowsの設定画面で「接続済み」と表示されるClassic Bluetooth機器を、アプリから「未接続(ペアリング維持)」に戻したい——しかしUWPや.NETの標準APIだけでは決め手がありません。この記事では、Win32のDeviceIoControlでIOCTL_BTH_DISCONNECT_DEVICEを投げて切断要求する方法と、UWPから使うための現実的な構成をまとめます。
「切断(未接続)」と「ペアリング解除」を混同しない
Windowsの「Bluetooth とその他のデバイス」画面では、同じ機器に対しても大きく3つの状態が見えます。
- 未ペアリング:PC側が機器を覚えていない。再接続するにはペアリングからやり直し。
- ペアリング済み(未接続):機器は登録済みだが、いまは接続セッションが張られていない。
- 接続済み:いずれかのプロファイル(A2DP/HFP/HID/SPPなど)で接続中。
今回やりたいのは「登録を残したまま、接続セッションだけ落とす」ことです。Remove device(削除)相当の処理や、Bluetoothアダプタ自体をOFFにする処理とは目的が違います。
| やりたい操作 | Windowsの見え方 | 影響 | 向いている場面 |
|---|---|---|---|
| 切断(Disconnect) | ペアリング済み(未接続) | セッションのみ終了。再接続は簡単。 | 一時的に他アプリへ譲る/不調時のリセット |
| ペアリング解除(Unpair / Forget) | 一覧から消える | 鍵情報も消える。再ペアリングが必要。 | 機器を完全に忘れたい/共有PCで後片付け |
| BluetoothアダプタOFF | 全機器が切断 | 他のBT機器も巻き込む。入力機器も落ちる可能性。 | 緊急避難/検証用 |
なぜUWP/.NET標準APIだけで「Classic Bluetooth切断」が難しいのか
Bluetoothには大きくClassic(BR/EDR)とBLE(Bluetooth Low Energy)があり、WindowsのAPIも得意分野が分かれます。WinRT(UWP)側はBLE向けのAPIが充実している一方、Classic Bluetoothの「OSが管理している接続」を一括で落とすための“公式に推奨された高レベルAPI”は見つけにくいのが実情です。
特にヘッドセットやゲームパッド、HID機器、SPP(仮想COM)などは、接続を維持する主体がアプリとは限りません。OSサービスや別アプリがプロファイルを掴んでいると、あなたのアプリだけで“きれいに切断”できないことがあります。
また「接続済み」の表示は、機器が持つ複数のプロファイル(音声・入力・シリアルなど)のうち、どれか一つでもセッションが残っていると維持されることがあります。つまり“あなたのアプリが触っている通信だけ閉じた”程度だと、設定画面の表示が変わらないケースも起きます。
結論:Win32のDeviceIoControlでIOCTL_BTH_DISCONNECT_DEVICEを発行する
ワークアラウンドとしてよく使われるのが、Win32 API DeviceIoControl()からIOCTL_BTH_DISCONNECT_DEVICEを発行して、OSに「このリモートデバイスとの接続を切ってほしい」と依頼する方法です。
このIOCTLはWindowsのドライバ向けドキュメントに定義されており、入力バッファに切断したいリモートデバイスのBluetoothアドレスを渡します。出力バッファは不要です。公式ドキュメント:IOCTL_BTH_DISCONNECT_DEVICE(Microsoft Learn)
| 項目 | 内容 |
|---|---|
| 呼び出し口 | DeviceIoControl() |
| IOCTL | IOCTL_BTH_DISCONNECT_DEVICE(SDKヘッダでは 0x41000c として定義されていることが多い) |
| 入力 | リモートBluetoothアドレス(BTH_ADDR相当、8バイト) |
| 出力 | なし |
| 注意点 | プロファイルや通信状態を考慮せず切断されるため、転送中のデータは失敗し得ます(L2CAP/SCOなどの状態に関わらず切断)。 |
重要:このIOCTLは“優雅な切断”というよりOSに強めの切断要求を出すイメージです。状態によってはすぐに再接続されることもあるため、後述のチェックリストも併せて確認してください。
IOCTL定数(0x41000c)の由来を軽く押さえる
実装例でよく出てくる 0x41000c は、Windows SDKのヘッダ(bthioctl.h)で次のような形で定義されています(要点だけ抜粋)。
#define IOCTL_BTH_DISCONNECT_DEVICE CTL_CODE(FILE_DEVICE_BLUETOOTH, 0x03, METHOD_BUFFERED, FILE_ANY_ACCESS)
つまり「Bluetoothデバイス用のIOCTL」「機能番号 0x03」「METHOD_BUFFERED」「アクセスはANY」という組み立てです。値そのものをハードコードする場合でも、どのヘッダ由来の定数なのかをコメントで残しておくと、後から保守しやすくなります。
実装の全体像:必要なのは「アドレス」と「ラジオハンドル」
IOCTL_BTH_DISCONNECT_DEVICEを投げるまでに、実務上つまずきやすいポイントは次の2つです。
| 必要なもの | 用途 | 入手方法の例 |
|---|---|---|
| リモートデバイスのBluetoothアドレス | 「どの機器を切断するか」を指定 | WinRTのBluetoothDevice.BluetoothAddress/32feetで取得/文字列からパース |
| ローカルBluetoothラジオのハンドル | DeviceIoControlの対象(どのスタックに要求するか) | BluetoothFindFirstRadioで列挙/32feetのBluetoothRadio.PrimaryRadio.Handle |
切断対象のBluetoothアドレスを取得する
Bluetoothアドレスは一般的には 00:11:22:33:44:55 のような6バイト(48bit)です。APIではulong(64bit整数)で扱うことが多く、上位16bitは0になります。
方法A:Windows.Devices.Bluetooth(WinRT)から取得する
Classic Bluetooth向けのWinRTクラスに Windows.Devices.Bluetooth.BluetoothDevice があり、BluetoothAddressプロパティでアドレス(ulong)を取得できます。ドキュメント:BluetoothDevice.BluetoothAddress
デスクトップ(.NET 6/7/8など)でも、Windows SDKへの参照が入っていればWinRTを呼び出せます。UWPの場合はアプリマニフェストの機能(capabilities)にbluetoothが必要です。
using Windows.Devices.Bluetooth;
using Windows.Devices.Enumeration;
// 例:ペアリング済み(キャッシュ)機器からBluetoothアドレスを取得する
var selector = BluetoothDevice.GetDeviceSelector();
var list = await DeviceInformation.FindAllAsync(selector);
foreach (var di in list)
{
var bt = await BluetoothDevice.FromIdAsync(di.Id);
if (bt == null) continue;
// ulongで取得(48bit)
ulong addr = bt.BluetoothAddress;
// 表示用(12桁HEX): 001122334455 のように出す
string hex = addr.ToString("X12");
// ...必要ならコロン区切りへ整形
}
方法B:文字列(MAC形式)からulongへ変換する
すでにアドレス文字列が分かっている場合は、区切り文字を除去して16進数としてパースする方法が手軽です。形式の揺れ(「:」「.」「-」)だけ吸収できるようにしておくと運用が楽になります。
using System.Globalization;
static ulong ParseBluetoothAddress(string mac)
{
if (string.IsNullOrWhiteSpace(mac)) throw new ArgumentException("mac is empty");
string hex = mac.Trim()
.Replace(":", "")
.Replace("-", "")
.Replace(".", "");
// 00:11:22:33:44:55 → 001122334455 として扱う
if (hex.Length != 12) throw new FormatException("Bluetoothアドレスは12桁HEX(6バイト)で指定してください。");
return ulong.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
実務メモ:「アドレスは合っているのに切断できない」ケースの多くは、後述するハンドルの取り方か、実は別プロファイルが握っていてすぐ再接続される、のどちらかです。まずはアドレスをログに出し、Windowsの機器情報(または別ツール)で見えるアドレスと一致するか確認してください。
Bluetoothラジオのハンドルを取得する
IOCTLは“どこに投げるか”が重要です。ここで言うハンドルは、切断したい機器のローカル側スタック(Bluetoothラジオ)に紐づくデバイスハンドルです。
方法A:BluetoothFindFirstRadio(Win32)で取得する
Win32 APIの BluetoothFindFirstRadio を使うとローカルBluetoothラジオを列挙できます。ドキュメント:BluetoothFindFirstRadio
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
struct BLUETOOTH_FIND_RADIO_PARAMS
{
public int dwSize;
}
static class NativeBluetooth
{
[DllImport("bthprops.cpl", SetLastError = true)]
public static extern IntPtr BluetoothFindFirstRadio(
ref BLUETOOTH_FIND_RADIO_PARAMS pbtfrp,
out IntPtr phRadio
);
[DllImport("bthprops.cpl", SetLastError = true)]
public static extern bool BluetoothFindRadioClose(IntPtr hFind);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);
}
// 使い方(最初のラジオだけ取る例)
var p = new BLUETOOTH_FIND_RADIO_PARAMS { dwSize = Marshal.SizeOf<BLUETOOTH_FIND_RADIO_PARAMS>() };
IntPtr radio;
IntPtr hFind = NativeBluetooth.BluetoothFindFirstRadio(ref p, out radio);
if (hFind == IntPtr.Zero) throw new InvalidOperationException("Bluetoothラジオが見つかりませんでした。");
try
{
// radio を DeviceIoControl の hDevice に使う
}
finally
{
NativeBluetooth.CloseHandle(radio);
NativeBluetooth.BluetoothFindRadioClose(hFind);
}
PCに複数のBluetoothラジオ(内蔵+USBドングルなど)がある場合は、BluetoothFindNextRadioで全て試す設計にしておくとトラブルが減ります。
方法B:32feet(InTheHand)でハンドルを得る
.NETからの実装負担を下げたい場合、32feet(InTheHand)のBluetoothRadio.PrimaryRadio.Handleなどでラジオハンドルを取得し、そこへDeviceIoControlを投げる例が見つかります。参照:Stack Overflow
「列挙やハンドル取りだけライブラリに任せる」やり方は、独自P/Invokeの量を減らせるので、まず動かしたい場面で有効です。
DeviceIoControlでIOCTL_BTH_DISCONNECT_DEVICEを発行する(C#サンプル)
ここまで揃えば、やることはシンプルです。IOCTL定数(SDKヘッダでは0x41000c)を指定して、入力バッファにアドレス(8バイト)を渡します。
using System;
using System.Runtime.InteropServices;
static class NativeIoctl
{
// SDKヘッダ(bthioctl.h)で IOCTL_BTH_DISCONNECT_DEVICE は 0x41000c
private const uint IOCTL_BTH_DISCONNECT_DEVICE = 0x41000c;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool DeviceIoControl(
IntPtr hDevice,
uint dwIoControlCode,
ref ulong lpInBuffer,
uint nInBufferSize,
IntPtr lpOutBuffer,
uint nOutBufferSize,
out uint lpBytesReturned,
IntPtr lpOverlapped
);
public static void DisconnectDevice(IntPtr radioHandle, ulong bluetoothAddress)
{
uint bytesReturned;
bool ok = DeviceIoControl(
radioHandle,
IOCTL_BTH_DISCONNECT_DEVICE,
ref bluetoothAddress,
(uint)Marshal.SizeOf<ulong>(),
IntPtr.Zero,
0,
out bytesReturned,
IntPtr.Zero
);
if (!ok)
{
int err = Marshal.GetLastWin32Error();
// ここでは用途に応じて例外化/ログだけにするなど調整
throw new InvalidOperationException($"DeviceIoControl failed. Win32Error={err}");
}
}
}
「エラーにせず、すでに未接続ならOK扱いにしたい」運用も多いと思います。その場合は、例外化せずにエラーコードを見て“未接続相当”なら成功扱いにするのが実用的です(環境によって返るエラーが変わることがあるため、まずはログを取って自環境で確認すると確実です)。
UWPでinvalid handleになる場合の現実解:フル信頼プロセスに委譲する
UWPサンドボックスでは、デバイスハンドルを開いてIOCTLを投げるような操作が制限されがちです。その結果、ハンドルが取れない/invalid handle相当で失敗するケースが起きます。
この場合の現実的な落としどころは、切断処理だけを別プロセスのデスクトップアプリ(Win32/WPF/Console)に任せ、UWPはUIと操作導線に集中する構成です。MSIXで同梱し、UWPからフル信頼プロセスを起動できます。
UWPからフル信頼プロセスを起動する:FullTrustProcessLauncher
UWPから同一パッケージ内のWin32プロセスを起動するには、Windows.ApplicationModel.FullTrustProcessLauncherを使います。ドキュメント:FullTrustProcessLauncher
using Windows.ApplicationModel;
// UWP側:フル信頼プロセスを起動
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
パッケージ(MSIX)の設定ポイント
パッケージマニフェストには、フル信頼プロセスを登録する拡張(windows.fullTrustProcess)と、実行を許可する制限付き機能(runFullTrust)が必要です。公式ドキュメント:packaging extensions(windows.fullTrustProcess)
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="desktop rescap">
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
<Applications>
<Application ...>
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="BtDisconnectHelper.exe">
<desktop:FullTrustProcess />
</desktop:Extension>
</Extensions>
</Application>
</Applications>
</Package>
UWP⇔Win32の連携(AppService/ファイル/その他)
「どのデバイスを切断するか」をUWPからヘルパーへ渡す方法は、要件に合わせて選びます。まず動かすならファイル連携が単純です。リアルタイムに結果を返したいならAppServiceが便利です。
| 連携方法 | メリット | デメリット | おすすめ度 |
|---|---|---|---|
| AppService | UWPらしい双方向通信。結果やログも返しやすい。 | 実装がやや多い(マニフェスト設定も含む)。 | 運用品質重視なら◎ |
| 共有ファイル(LocalState等) | 実装が単純。UWPがアドレスを書き、ヘルパーが読む。 | 同時実行や競合制御が必要になることがある。 | まず動かすなら◎ |
| 固定パラメータ(ParameterGroup) | 起動が簡単。 | 動的にアドレスを渡しづらい(静的パラメータ向き)。 | 限定的 |
AppServiceの考え方やサンプルは、MicrosoftのドキュメントやDesktop Bridge系のサンプルが参考になります。たとえば、UWPからWin32へ処理を委譲して結果を返す流れは、レジストリ操作のサンプルなどとも共通です(仕組みだけ転用できます)。
切断できない/すぐ再接続されるときのチェックリスト
IOCTLを投げても「思った状態にならない」ことがあります。よくある原因と対策をまとめます。
| 症状 | ありがちな原因 | 対策の方向性 |
|---|---|---|
| エラー:invalid handle | UWPでラジオハンドルが取得できていない/誤ったハンドルを渡している | デスクトップ(フル信頼)側でIOCTLを実行する。BluetoothFindFirstRadioで得たハンドルを使う。 |
| エラー:アクセス拒否 | 権限不足、またはハンドルのアクセス権が足りない | 必要なら管理者実行を検証。ハンドル取得方法を見直す(ラジオ列挙経由にする)。 |
| UIが「接続済み」のまま | 別プロファイルが接続中/表示更新の遅延 | 音声・入力・仮想COMなど、どのプロファイルが掴んでいるか確認。アプリ側が保持しているソケットやCOMを先に閉じる。 |
| 切断できても即再接続される | 機器側の自動再接続/OSサービスや別アプリが再接続を試みる | 再接続を誘発する処理(オーディオ再生、HID利用、常駐アプリ)を止める。必要なら一時的にサービスを無効化する。 |
BLE(Bluetooth Low Energy)の切断は別ルート
この記事の中心はClassic Bluetooth(非LE)です。BLEの場合、接続のライフサイクルはGATTセッションなどの概念が絡み、BluetoothLEDeviceの破棄やGattSessionの設定など、別のAPI設計になります。混在機器(Dual mode)だと「どの接続を切りたいのか」がぶれやすいので、まずは対象機器がClassicのどのプロファイルで接続されているかを整理すると、設計ミスが減ります。
参考リンク
- IOCTL_BTH_DISCONNECT_DEVICE(Microsoft Learn)
- IOCTLコードの組み立て(Microsoft Learn)
- BluetoothFindFirstRadio(Microsoft Learn)
- FullTrustProcessLauncher(Microsoft Learn)
- packaging extensions / windows.fullTrustProcess(Microsoft Learn)
- C#からBluetoothを切断する例(Stack Overflow)
- UWP/.NETでClassic Bluetoothを切断したい(Microsoft Q&A)
まとめ
WindowsでClassic Bluetooth機器を「ペアリングは残したまま未接続にする」ためには、現状、Win32のDeviceIoControlでIOCTL_BTH_DISCONNECT_DEVICEを投げるワークアラウンドが最短ルートになりがちです。実装では「リモートアドレス」と「ローカルラジオハンドル」の2点を確実に押さえ、UWPでハンドルが取れない場合はMSIX+フル信頼プロセス構成で“切断だけ委譲”する設計にすると、現場で運用しやすくなります。

コメント