VB.NETでUSBドライブの取り外し(Eject)が動作しない原因と解決策【Shell.Application.NameSpace(17)】

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) の戻り値が Nothing
  • Nothing.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 を表示し、ログにも残す

「オブジェクト変数が設定されていません」を潰すチェックリスト

実際にデバッグするときのチェックポイントを順に並べると、次のようになります。

チェックする対象条件問題があったときの想定原因
objShellobjShell 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) に変えるところから試してみてください。

この記事を書いた人

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

コメント

コメントする

目次