VB .NET の Windows フォームから USB メモリを「安全な取り外し(Eject)」したいのに、Shell.Application.NameSpace(17).ParseName(…).InvokeVerb(“Eject”) でエラーが出て動かない――そんなときの原因と、実務で安心して使える修正版コードをまとめて解説します。
VB .NET で USB の Eject が失敗する典型パターン
まずは問題の状況を整理します。多くの場合、次のようなコードを書いています。
Dim objShell As Object = CreateObject("Shell.Application")
objShell.NameSpace(17).ParseName("H:\").InvokeVerb("Eject")
このコードを VB .NET の Windows Forms アプリから実行すると、多くの環境で次のエラーが発生します。
- オブジェクト変数または With ブロック変数が設定されていません。
実際には、NameSpace(17) が Nothing を返し、その Nothing に対して .ParseName(...) を呼んでいるために VB 側でこの例外が投げられています。
ところが、まったく同じロジックを PowerShell で書くと正常に Eject できる、という事例が多く報告されています。
$shell = New-Object -ComObject Shell.Application
$folder = $shell.NameSpace(17)
$item = $folder.ParseName("H:")
$item.InvokeVerb("Eject")
では、なぜ VB .NET では失敗し、PowerShell では成功するのでしょうか。
原因:Shell.NameSpace の引数型(VARIANT)と VB の Integer
ポイントは、COM メソッドの引数は VARIANT、VB .NET からは Object として扱う必要があるという点です。
Shell.Application.NameSpace の正体
Shell.Application の NameSpace メソッドは、型ライブラリ上では次のようなイメージで定義されています。
Folder NameSpace(VARIANT vDir);
つまり、引数は「何でも入る箱」である VARIANT 型であり、.NET の世界では Object として扱います。
ところが VB .NET で 17(= ssfDRIVES)をそのまま渡すと、Integer 型のまま COM に渡され、環境によっては COM 側での解釈に失敗し、NameSpace(17) が Nothing を返してしまうことがあります。
これが、次のエラーメッセージにつながります。
NameSpace(17)の戻り値がNothingNothing.ParseName("H:")を呼ぶ- オブジェクト変数または With ブロック変数が設定されていません 例外
なぜ PowerShell では動くのか
PowerShell は、COM 呼び出し時の型変換(Variant への変換)をかなり柔軟に行います。整数リテラル 17 も、自然に VARIANT へ変換され、ssfDRIVES として正しく解釈されます。
一方 VB .NET は、オーバーロード解決や COM インターフェイスのラップの影響で、そのまま Integer を渡すと期待通りにマッピングされないケースがある、という違いがあります。
解決策の全体像
この問題を解決するためのポイントを先に一覧で整理しておきます。
| 対策 | 具体的な内容 | 期待できる効果 |
|---|---|---|
| 1. 引数を Object にキャスト | objShell.NameSpace(CType(17, Object)) として VARIANT と整合させる | NameSpace が正しくドライブフォルダを返す |
| 2. ドライブ文字列の正規化 | "H:\" ではなく "H:" のように末尾 \ を除く | ParseName の解決が安定し、環境差を軽減 |
| 3. Nothing チェック | objShell / NameSpace / ParseName それぞれを検査 | 原因の切り分けがしやすく、ユーザーに丁寧なメッセージを表示できる |
以下では、これらをすべて反映した VB .NET の実装例と、設計上のポイントを詳しく解説します。
実装例:VB .NET Windows Forms で USB を取り外す
まずは、最小限+実務で使いやすいレベルまで整えたコード例です。Windows 10 / Windows 11 を想定しています。
フォームロード時に USB ドライブを列挙する
Imports System.IO
Public Class Form1
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
CBDrives.Items.Clear()
For Each d In DriveInfo.GetDrives()
If d.DriveType = DriveType.Removable AndAlso d.IsReady Then
' "H:\" → "H:" に正規化
CBDrives.Items.Add(d.Name.TrimEnd("\"c))
End If
Next
End Sub
ここでのポイントは次のとおりです。
DriveType.RemovableかつIsReady = Trueのものだけを対象にすることで、挿入済みでマウントされている USB メモリのみをコンボボックスに出す。d.Nameは通常"H:\"という形式なので、TrimEnd("\"c)で"H:"にしておく。
ボタンから Eject を呼び出す
Private Sub BtnEj_Click(sender As Object, e As EventArgs) Handles BtnEj.Click
Dim drive = CBDrives.Text.Trim()
If drive = String.Empty Then
MessageBox.Show("取り外す USB ドライブを選択してください。",
"USB 取り外し",
MessageBoxButtons.OK,
MessageBoxIcon.Information)
Return
End If
Try
EjectUsb(drive)
' 必要なら、再列挙して本当に消えたかを確認してからメッセージを出す
If IsDriveDisconnected(drive) Then
MessageBox.Show(String.Format("{0} を安全に取り外しました。", drive),
"USB 取り外し",
MessageBoxButtons.OK,
MessageBoxIcon.Information)
Else
MessageBox.Show(String.Format(
"{0} の取り外しコマンドを送信しましたが、" &
"まだシステムがドライブを保持している可能性があります。", drive),
"USB 取り外し",
MessageBoxButtons.OK,
MessageBoxIcon.Warning)
End If
Catch ex As Exception
MessageBox.Show("USB ドライブの取り外しに失敗しました。" & Environment.NewLine & ex.Message,
"USB 取り外しエラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error)
End Try
End Sub
Eject を実行するメソッド(本体)
Private Sub EjectUsb(driveLetter As String)
' 引数の正規化("H:\" → "H:")
Dim normalizedDrive = driveLetter.Trim().TrimEnd("\"c)
If normalizedDrive.Length < 2 OrElse Not normalizedDrive.EndsWith(":") Then
Throw New ArgumentException(
String.Format("ドライブ文字の形式が不正です: {0}", driveLetter),
NameOf(driveLetter))
End If
Dim objShell As Object = Nothing
Try
objShell = CreateObject("Shell.Application")
' ★ ここが最大のポイント:ssfDRIVES(=17) を Object にキャストして渡す
Dim drivesFolder = objShell.NameSpace(CType(17, Object))
If drivesFolder Is Nothing Then
Throw New InvalidOperationException(
"Shell.NameSpace(17) が Nothing を返しました。" & Environment.NewLine &
"Windows のシェルが正しく動作していない可能性があります。")
End If
Dim item = drivesFolder.ParseName(normalizedDrive)
If item Is Nothing Then
Throw New ArgumentException(
String.Format("指定したドライブが見つかりません: {0}", normalizedDrive),
NameOf(driveLetter))
End If
' コンテキストメニューの「取り出し(Eject)」を実行
item.InvokeVerb("Eject")
Finally
' COM オブジェクトを明示的に解放しておくと安心
If objShell IsNot Nothing AndAlso
System.Runtime.InteropServices.Marshal.IsComObject(objShell) Then
System.Runtime.InteropServices.Marshal.ReleaseComObject(objShell)
End If
End Try
End Sub
取り外し後にドライブが消えたか確認する補助関数
Private Function IsDriveDisconnected(driveLetter As String) As Boolean
Dim normalized = driveLetter.Trim().TrimEnd("\"c).ToUpperInvariant()
For Each d In DriveInfo.GetDrives()
Dim name = d.Name.TrimEnd("\"c).ToUpperInvariant()
If name = normalized Then
Return False
End If
Next
Return True
End Function
End Class
この構成であれば、
- COM 呼び出しの引数型不一致を回避
- ドライブ文字列の揺れ(
H:\/H:)を吸収 - Nothing チェック+例外メッセージで原因が掴みやすい
- ユーザーに対しても「取り外し成功/疑わしい」などのフィードバックを返せる
といったメリットがあります。
よくある失敗例と修正パターン
もう少し具体的に、「こう書くと失敗しがち」「こう直すとよい」という形でまとめます。
| よくあるコード | 問題点 | 修正版の例 |
|---|---|---|
objShell.NameSpace(17) | 引数が Integer のまま渡され、環境によっては Nothing が返る | objShell.NameSpace(CType(17, Object)) |
ParseName("H:\") | 環境差・実装差で正しく解決できないことがある | ParseName("H:")(TrimEnd("\"c) で事前に正規化) |
NameSpace(17).ParseName(usb).InvokeVerb("Eject") | NameSpace や ParseName が Nothing でも気づかず例外になる | drivesFolder, item に変数を分けて Nothing チェック |
| Try~Catch を書かない | ユーザーには「動かない」だけで、原因がわからない | Catch で ex.Message を表示し、ログにも残す |
「オブジェクト変数が設定されていません」を潰すチェックリスト
実際にデバッグするときのチェックポイントを順に並べると、次のようになります。
| チェックする対象 | 条件 | 問題があったときの想定原因 |
|---|---|---|
objShell | objShell Is Nothing になっていないか | CreateObject("Shell.Application") の失敗。権限やレジストリ破損の可能性 |
drivesFolder = objShell.NameSpace(CType(17, Object)) | drivesFolder Is Nothing かどうか | Shell のドライブビュー取得に失敗。Windows のシェル周辺の不具合や COM 登録の問題 |
item = drivesFolder.ParseName(drive) | item Is Nothing かどうか | 該当ドライブが存在しない、または drive 文字列が不正 |
item.InvokeVerb("Eject") | 例外が発生しないか | ドライブ使用中、ポリシーやウイルス対策ソフトによるブロックなど |
この順番でブレークポイントを置きながら確認していくと、どこでつまずいているかを簡単に特定できます。
「安全に取り外せます」トーストが出ない理由
InvokeVerb("Eject") 自体は Windows に「取り外し要求」を送りますが、
- 常に「安全に取り外せます」トーストが出るわけではない
- 通知設定やエクスプローラーのバージョンによって挙動が変わる
といった点には注意が必要です。
そのため、アプリ側では次のような方針が現実的です。
- Eject 実行時点では「取り外し要求を送信しました」と表示する
- 少し待ってからドライブ一覧を再列挙し、対象ドライブが消えていれば「安全に取り外しました」と表示
- 残っている場合は「まだ使用中の可能性があります」などと警告する
前述の IsDriveDisconnected 関数は、そのチェックを簡単に行うためのユーティリティです。
USB ドライブが Eject できない代表的なケース
コードは正しいのに Eject 自体が失敗することもあります。その場合は、次のような要因を疑います。
| 現象 | 主な原因候補 | 対処のヒント |
|---|---|---|
| Eject を実行してもドライブが消えない | ファイルを開いたまま、Explorer でドライブを開いたままなど | アプリ側で「使用中のファイルを閉じてください」とガイダンスを表示 |
| 「このデバイスは現在使用中です」のエラー | バックアップソフト、ウイルス対策ソフトなどがアクセス中 | 常駐ソフトを一時停止、または対象フォルダーを除外設定にする |
| 特定の USB だけ Eject できない | USB 側のファームウェアやドライバーの癖、固定ディスク扱いのデバイス | 別ポート・別 PC で挙動を確認。Windows 上は「取り外し不可」なデバイスも存在 |
| 管理者権限がない環境で失敗する | 企業ポリシーでリムーバブルの取り外しが制限されている | システム管理者に仕様と要件を確認。アプリ側ではエラー内容をそのまま通知 |
PowerShell 呼び出しを VB から使う代替案
既に PowerShell でのスクリプトが安定して動いているなら、VB .NET から PowerShell を呼び出す、という割り切った方法もあります。
PowerShell スクリプト例
$drive = $args[0]
$shell = New-Object -ComObject Shell.Application
$folder = $shell.NameSpace(17)
$item = $folder.ParseName($drive)
$item.InvokeVerb("Eject")
これを eject-usb.ps1 として保存し、VB 側では System.Diagnostics.Process から起動します。
Private Sub EjectUsbViaPowerShell(driveLetter As String)
Dim psi As New ProcessStartInfo() With {
.FileName = "powershell.exe",
.Arguments = String.Format("-ExecutionPolicy Bypass -File ""{0}"" ""{1}""",
"C:\Path\To\eject-usb.ps1",
driveLetter.Trim().TrimEnd("\"c)),
.UseShellExecute = False,
.CreateNoWindow = True
}
Using p = Process.Start(psi)
p.WaitForExit()
If p.ExitCode <> 0 Then
Throw New InvalidOperationException(
String.Format("PowerShell による Eject に失敗しました。ExitCode={0}", p.ExitCode))
End If
End Using
End Sub
もちろん、外部プロセス起動のオーバーヘッドや、実行ポリシーの問題などがあるため、
- まずは VB から直接 Shell.Application を正しく呼ぶ方法(本記事のメイン)
- どうしても環境依存の問題が解消しない場合の逃げ道として PowerShell 呼び出し
という位置づけにするのが現実的です。
より低レベルな API(CfgMgr32 / SetupAPI)を使うべきか?
ネット検索をしていると、
CM_Request_Device_Eject(CfgMgr32)- SetupAPI 経由で物理デバイスを列挙して Eject
といった低レベル API を VB から P/Invoke している例も見つかります。
これらは、
- USB デバイス単位で細かく制御したい
- ドライブレターではなくデバイス ID ベースで扱いたい
といった高度な要件には有用ですが、次のようなデメリットもあります。
- P/Invoke 定義が煩雑で、バージョン間差異も大きい
- 管理者権限が必要になりやすく、失敗時のメッセージもわかりにくい
- USB 以外のデバイスにも影響を与えうるため、安全性に配慮が必要
通常の「USB メモリの取り外し」用途であれば、まずは本記事で紹介した Shell.Application + InvokeVerb(“Eject”) 方式をきちんと安定させる方が、工数・安全性のバランスが良いことがほとんどです。
VB .NET と COM を仲良くさせるためのポイント
今回の問題は、一言でいうと「COM の VARIANT 引数に VB 側の型がきれいにはまらなかった」ことが原因です。同様のトラブルを避けるために、次のような習慣をつけておくと役立ちます。
1. COM 引数は「とりあえず Object」で渡す
今回の NameSpace のように、
- 型ライブラリで
VARIANTが使われている - .NET 側では
Objectとして見えている
というメソッドには、
objShell.NameSpace(CType(ssfDRIVES, Object))
のように CType(…, Object) をかませておくと安全です。
2. マジックナンバーは定数化しておく
17 という数字だけが散らばっていると、後から見返したときに「何の数値だっけ?」となってしまいます。
Private Const ssfDRIVES As Integer = 17
' 呼び出し側
Dim drivesFolder = objShell.NameSpace(CType(ssfDRIVES, Object))
このように定数化しておくだけでも、メンテナンス性が大きく向上します。
3. Option Strict On でコンパイルする
COM 周りでは、暗黙の型変換が「たまたま動く」ことが多いのですが、長期的にはバグの温床になります。
Option Strict Onをプロジェクト全体で有効化- 必要な箇所だけ
CTypeやDirectCastで明示的に変換
とすることで、今回のような「うっかり型が合わずに Nothing が返る」といった問題を早期に防ぎやすくなります。
実装全体のサンプル(まとめ版)
ここまでの内容を踏まえて、フォーム全体のサンプルコードを再掲しておきます。自分のプロジェクトに貼り付けて調整しやすいよう、1 ファイルに収めています。
Imports System.IO
Imports System.Runtime.InteropServices
Public Class Form1
Private Const ssfDRIVES As Integer = 17
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
CBDrives.Items.Clear()
For Each d In DriveInfo.GetDrives()
If d.DriveType = DriveType.Removable AndAlso d.IsReady Then
CBDrives.Items.Add(d.Name.TrimEnd("\"c)) ' "H:\" → "H:"
End If
Next
End Sub
Private Sub BtnEj_Click(sender As Object, e As EventArgs) Handles BtnEj.Click
Dim drive = CBDrives.Text.Trim()
If drive = String.Empty Then
MessageBox.Show("取り外す USB ドライブを選択してください。",
"USB 取り外し",
MessageBoxButtons.OK,
MessageBoxIcon.Information)
Return
End If
Try
EjectUsb(drive)
If IsDriveDisconnected(drive) Then
MessageBox.Show(String.Format("{0} を安全に取り外しました。", drive),
"USB 取り外し",
MessageBoxButtons.OK,
MessageBoxIcon.Information)
Else
MessageBox.Show(String.Format(
"{0} の取り外しコマンドを送信しましたが、" &
"まだシステムがドライブを保持している可能性があります。", drive),
"USB 取り外し",
MessageBoxButtons.OK,
MessageBoxIcon.Warning)
End If
Catch ex As Exception
MessageBox.Show("USB ドライブの取り外しに失敗しました。" & Environment.NewLine & ex.Message,
"USB 取り外しエラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error)
End Try
End Sub
Private Sub EjectUsb(driveLetter As String)
Dim normalizedDrive = driveLetter.Trim().TrimEnd("\"c)
If normalizedDrive.Length < 2 OrElse Not normalizedDrive.EndsWith(":") Then
Throw New ArgumentException(
String.Format("ドライブ文字の形式が不正です: {0}", driveLetter),
NameOf(driveLetter))
End If
Dim objShell As Object = Nothing
Try
objShell = CreateObject("Shell.Application")
' ssfDRIVES を Object 経由で渡す
Dim drivesFolder = objShell.NameSpace(CType(ssfDRIVES, Object))
If drivesFolder Is Nothing Then
Throw New InvalidOperationException(
"Shell.NameSpace(17) が Nothing を返しました。" & Environment.NewLine &
"Windows のシェルが正しく動作していない可能性があります。")
End If
Dim item = drivesFolder.ParseName(normalizedDrive)
If item Is Nothing Then
Throw New ArgumentException(
String.Format("指定したドライブが見つかりません: {0}", normalizedDrive),
NameOf(driveLetter))
End If
item.InvokeVerb("Eject")
Finally
If objShell IsNot Nothing AndAlso Marshal.IsComObject(objShell) Then
Marshal.ReleaseComObject(objShell)
End If
End Try
End Sub
Private Function IsDriveDisconnected(driveLetter As String) As Boolean
Dim normalized = driveLetter.Trim().TrimEnd("\"c).ToUpperInvariant()
For Each d In DriveInfo.GetDrives()
Dim name = d.Name.TrimEnd("\"c).ToUpperInvariant()
If name = normalized Then
Return False
End If
Next
Return True
End Function
End Class
まとめ:Eject が動かない時はまず「17 を Object にする」
最後に、本記事の要点をコンパクトに振り返ります。
- 現象:
NameSpace(17).ParseName(...).InvokeVerb("Eject")で「オブジェクト変数または With ブロック変数が設定されていません」 - 直接原因:
NameSpace(17)がNothingを返し、Nothing.ParseNameを呼んでいる - 背景:
NameSpaceの引数は COM のVARIANT(= .NET のObject)。VB のIntegerをそのまま渡すと型解決に失敗することがある - 決定的な対策:
objShell.NameSpace(CType(17, Object))のように Object 経由で渡す - あわせて行うべきこと:
- ドライブ文字列は
H:形式に正規化 objShell/NameSpace/ParseNameに対してNothingチェック- Eject 実行後にドライブ一覧を再列挙し、ユーザーに結果をフィードバック
- ドライブ文字列は
これらを押さえておけば、VB .NET の Windows フォームから USB メモリを安全に取り外す処理を、安定して実装できるようになります。既存コードがうまく動いていない場合は、まずは 17 を CType(17, Object) に変えるところから試してみてください。

コメント