SHCChangeNotifyRegisterを用いたリムーバブルディスク監視


蒼月  2010-07-11 21:12:24  No: 146969

いつも参考にさせて頂いています。
早速ですが、質問させていただきます。

環境:XP ProS P3
      VB2008にて.netFrameWork2.0を使用して開発

私は、今、SDカードの抜き差しをトリガーとしたプログラムを作成しています。

調べたところ、ウィンドウメッセージのWM_DEVICECHANGEを捕捉すれば、できそうだとわかったので、作成をしてみました。
ですが、USB接続のリーダライタを使用するとき、元からSDがささったものを接続すれば、メッセージが発生するのですが、リーダライタをさした後にSDを差し込んでもメッセージが発生しませんでした。

そこで、もう少し調べてみたところ、題名にもあるように「SHCChangeNotifyRegister」を使えば、WM_DEVICECHANGEで捕捉できない抜き差しも捕捉できるとのことなので、下記のウェブサイト(C#の例)を参考にVBに移植を行いました。

参考にしたサイト
http://d.hatena.ne.jp/ohyajapan/20081123/p2

ですが、SHCChangeNotifyRegister実行時に、「AccessViolationException  保護されているメモリに読み取りまたは書き込み操作を行おうとしました。他のメモリが壊れていることが考えられます。」が発生してしまいます。

理由を探してはみたのですが、SHCChangeNotifyRegisterに関しての記述自体が少ないため、ここで質問させていただきました。

ウィンドウが必要とのことなので、フォームのLoadイベントにてSHCChangeNotifyRegisterを使用しています。
Activedなどにも移してみましたが、変わりませんでした。

よろしくお願いします。


魔界の仮面弁士  2010-07-12 18:19:19  No: 146970

>下記のウェブサイト(C#の例)を参考にVBに移植を行いました。
現象を再現可能な移植後のコードを提示できますか?


蒼月  2010-07-13 06:46:35  No: 146971

遅くなって申し訳ありません。
移植後のソースを抜粋して、提示致します。

Imports System.IO
Imports System.Runtime.InteropServices
Imports Microsoft.Win32.SafeHandles

Public Class Form1
    <DllImport("shell32.dll", EntryPoint:="#2", CharSet:=CharSet.Auto)> _
    Private Shared Function SHChangeNotifyRegister( _
        ByVal hWnd As IntPtr, _
        ByVal fSources As SHCNF, _
        ByVal fEvents As SHCNE, _
        ByVal wMsg As UInt32, _
        ByVal cEntries As Integer, _
        ByVal pFsne As SHChangeNotifyEntry) As UInt32
    End Function

    <DllImport("shell32.dll", EntryPoint:="#4", CharSet:=CharSet.Auto)> _
    Private Shared Function SHChangeNotifyUnregister( _
        ByVal hNotify As UInteger) As Boolean
    End Function

    <DllImport("shell32.dll", CharSet:=CharSet.Auto)> _
    Private Shared Function SHGetPathFromIDList( _
        ByVal pidl As IntPtr, _
        ByVal StringBuilder As Path) As Boolean
    End Function

    Private Enum SHCNF As UInteger
        SHCNF_IDLIST = &H0
        SHCNF_PATHA = &H1
        SHCNF_PRINTERA = &H2
        SHCNF_DWORD = &H3
        SHCNF_PATHW = &H5
        SHCNF_PRINTERW = &H6
        SHCNF_TYPE = &HFF
        SHCNF_FLUSH = &H1000
        SHCNF_FLUSHNOWAIT = &H2000
    End Enum

    Private Enum SHCNE As UInteger
        SHCNE_MEDIAINSERTED = &H20
        SHCNE_MEDIAREMOVED = &H40
    End Enum

    Private Structure SHNOTIFYSTRUCT
        Public dwItem1 As UInteger
        Public dwItem2 As UInteger
    End Structure

    Private Structure SHChangeNotifyEntry
        Public pIdl As IntPtr
        Public Recursively As Boolean
    End Structure

    Private Enum WM As UInteger
        WM_SHNOTIFY = &H401
        WM_DEVICECHANGE = &H219
        WM_CONTEXTMENU = &H7B
        WM_SYSCOMMAND = &H112
        WM_COPYDATA = &H4A
    End Enum

    Private notifyId As UInteger
    Private isActived As Boolean = False

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        Dim notifyEntry = New SHChangeNotifyEntry()
        notifyEntry.pIdl = IntPtr.Zero
        notifyEntry.Recursively = True

ここでエラー→        Dim notifyIdLocal = SHChangeNotifyRegister(Handle, _
                                          SHCNF.SHCNF_TYPE Or SHCNF.SHCNF_IDLIST, _
                                          SHCNE.SHCNE_MEDIAINSERTED Or SHCNE.SHCNE_MEDIAREMOVED, _
                                          CUInt(WM.WM_SHNOTIFY), 1, notifyEntry)

        notifyId = CType(notifyIdLocal, UInteger)
    End Sub
End Class

Dim notifyIdLocal = SHChangeNotifyRegisterの部分でエラーが発生します。


魔界の仮面弁士  2010-07-13 20:10:01  No: 146972

元の定義が LONG なものを、わざわざ UInteger にしているのは何故でしょうか?
サイズが一緒なので型自体の互換性は保たれていますが、不自然な印象を受けますし、
符号無し整数型にするメリットも思い当たりません(逆パターンならまだ分かりますが)。

たとえば、SHCNE_MEDIAINSERTED の元の定義は
  #define SHCNE_MEDIAINSERTED       0x00000020L
となっています。0x00000020L であって 0x00000020UL ではありません。
またそれを利用しようとしている第 3 引数も、元の定義は ULONG ではなく LONG です。

もうひとつ。「UInteger」だったり「UInt32」だったりといった表記揺れが気になります。
意味としては一緒ですが、統一しておいた方が良いと思いますよ。

で。何よりも問題なのは第 6 引数です。
そもそも、SHChangeNotifyRegister の定義は
  戻り値: ULONG
  引数1: HWND
  引数2: int
  引数3: LONG
  引数4: UINT
  引数5: int
  引数6: SHChangeNotifyEntry*
ですよね。
http://msdn.microsoft.com/en-us/library/bb762120%28VS.85%29.aspx

一方、蒼月さんの定義は、
  戻り値: UInt32
  引数1: ByVal IntPtr
  引数2: ByVal SHCNF(As UInteger)
  引数3: ByVal SHCNE(As UInteger)
  引数4: ByVal UInt32
  引数5: ByVal Integer
  引数6: ByVal SHChangeNotifyEntry(Structure)
となっています。少なくとも第 6 引数はこのままだとマズイでしょう。
とりあえず、SHChangeNotifyEntry の定義を修正して、

<StructLayout(LayoutKind.Sequential)> Private Class SHChangeNotifyEntry
  Public pIdl As IntPtr
  <MarshalAs(UnmanagedType.Bool)> Public Recursively As Boolean
End Class

のようにクラスとして用意してみてください。
とりあえずこれで、AccessViolationException は回避できると思います。

>    Public Recursively As Boolean
上記にも書きましたが、属性 <MarshalAs(UnmanagedType.Bool)> を付与しましょう。
SHChangeNotifyUnregister や SHGetPathFromIDList の戻り値についても同様です。

また、SHGetPathFromIDList の定義も大幅に間違っています。引数に System.IO.Path クラスを
受け取ることは(アンマネージ API でもマネージ API でも)まずありえないです。


蒼月  2010-07-17 09:29:57  No: 146973

>魔界の仮面弁士 様

レス、ありがとうございます。
遅くなってしまい、申し訳ありません。

まだ、アドバイス頂いた内容を試せていない状態です。
明日、明後日くらいまでには、試すことができると思いますので、結果をお伝え致します。

ありがとうございました。


蒼月  2010-07-18 18:30:41  No: 146974

>魔界の仮面弁士 様

ご指摘頂いた内容を確認してみました。

>とりあえず、SHChangeNotifyEntry の定義を修正して、

><StructLayout(LayoutKind.Sequential)> Private Class >SHChangeNotifyEntry
>  Public pIdl As IntPtr
>  <MarshalAs(UnmanagedType.Bool)> Public Recursively As Boolean
>End Class

>のようにクラスとして用意してみてください。
>とりあえずこれで、AccessViolationException は回避できると思います。

ひとまずこれを反映させたところ、エラーの発生が無くなり、メッセージの取得が可能となりました。

また、定義があいまいだという件については、修正中です。
少し時間を頂くかもしれませんが、修正が終わり次第、
修正後のソースを掲載したいと思います。


※返信する前に利用規約をご確認ください。




  


  このエントリーをはてなブックマークに追加