DOBON.NET DOBON.NETプログラミング掲示板過去ログ

GetOpenFileNameで複数のファイル名を取得する方法

環境/言語:[OS : Windows XP Professional / 言語 : Visual Basic .NET / .NET Framework : 2.0]
分類:[.NET]

現在、文字エンコードの指定ができるOpenFileDialogを作っています。
そこで、OFN_ALLOWMULTISELECTを指定して、複数のファイル名を取得しようとしても、ディレクトリ名しか取得できません。

いくつかのサイトで調べてみたところ、本来は
「ディレクトリ名+NULL文字+ファイル名1+NULL文字 ... ファイル名N+NULL文字+NULL文字」
という形式で取得できるようです。
どこに問題があるか、ご教授願えませんでしょうか?

尚、1つのファイル名ならば問題なく取得できます。

以下は、必要だと思われる部分を取り出したコードです。

Imports System.IO
Imports System.Windows.Forms
Imports System.Runtime.InteropServices

Public Class xOpenFileDialog

    <DllImport("Comdlg32.dll", CharSet:=CharSet.Auto, SetLastError:=True)> _
    Private Shared Function GetOpenFileName( _
        <MarshalAs(UnmanagedType.Struct)> ByRef lpofn As OPENFILENAME) As Boolean
    End Function

    Private Const OFN_ALLOWMULTISELECT As Integer = &H200
    Private Const OFN_EXPLORER As Integer = &H80000
    Private Const OFN_HIDEREADONLY As Integer = &H4

    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
    Private Structure OPENFILENAME
        Public lStructSize As Integer
        Public hwndOwner As IntPtr
        Public hInstance As Integer
        <MarshalAs(UnmanagedType.LPTStr)> _
        Public lpstrFilter As String
        <MarshalAs(UnmanagedType.LPTStr)> _
        Public lpstrCustomFilter As String
        Public nMaxCustFilter As Integer
        Public nFilterIndex As Integer
        <MarshalAs(UnmanagedType.LPTStr)> _
        Public lpstrFile As String
        Public nMaxFile As Integer
        <MarshalAs(UnmanagedType.LPTStr)> _
        Public lpstrFileTitle As String
        Public nMaxFileTitle As Integer
        <MarshalAs(UnmanagedType.LPTStr)> _
        Public lpstrInitialDir As String
        <MarshalAs(UnmanagedType.LPTStr)> _
        Public lpstrTitle As String
        Public Flags As Integer
        Public nFileOffset As Short
        Public nFileExtension As Short
        <MarshalAs(UnmanagedType.LPTStr)> _
        Public lpstrDefExt As String
        Public lCustData As Integer
        Public lpfnHook As Integer
        <MarshalAs(UnmanagedType.LPTStr)> _
        Public lpTemplateName As String
        'only if on nt 5.0 or higher
        Public pvReserved As Integer
        Public dwReserved As Integer
        Public FlagsEx As Integer
    End Structure

    Private m_FileNames() As String = Nothing

    Public Property FileName() As String
        Get
            If m_FileNames Is Nothing Then
                Return ""
            Else
                Return m_FileNames(0)
            End If
        End Get
        Set(ByVal value As String)
            m_FileNames = New String() {value}
        End Set
    End Property

    Public Property FileNames() As String()
        Get
            Return m_FileNames
        End Get
        Set(ByVal value As String())
            m_FileNames = value
        End Set
    End Property

    Public Function ShowDialog() As DialogResult
        Dim ofn As OPENFILENAME = Nothing

        ofn.lStructSize = Marshal.SizeOf(ofn)
        ofn.lpstrFilter = "All Files(*.*)" & vbNullChar & "*.*" & vbNullChar & vbNullChar
        ofn.nFilterIndex = 1
        ofn.lpstrFile = New String(vbNullChar, 512)
        ofn.nMaxFile = ofn.lpstrFile.Length
        ofn.lpstrFileTitle = New String(vbNullChar, 512)
        ofn.nMaxFileTitle = ofn.lpstrFileTitle.Length
        ofn.hwndOwner = Form.ActiveForm.Handle
        ofn.Flags = OFN_EXPLORER Or OFN_HIDEREADONLY Or OFN_ALLOWMULTISELECT

        If GetOpenFileName(ofn) = 0 Then
            Return DialogResult.Cancel
        End If

        Console.WriteLine(ofn.lpstrFile.Length)

        Dim doubleNullIndex As Integer = ofn.lpstrFile.IndexOf(vbNullChar & vbNullChar)
        If doubleNullIndex < 0 Then
            m_FileNames = New String() {ofn.lpstrFile}
        Else
            '複数選択の場合
            ofn.lpstrFile = ofn.lpstrFile.Substring(0, ofn.lpstrFile.IndexOf(vbNullChar & vbNullChar))
            Dim fName() As String = ofn.lpstrFile.Split(vbNullChar)
            ReDim m_FileNames(fName.Length - 2)
            Dim i As Integer
            For i = 0 To m_FileNames.Length - 2
                m_FileNames(i) = Path.Combine(fName(0), fName(i + 1))
            Next
        End If

        Return DialogResult.OK
    End Function
End Class
2007/05/02(Wed) 00:37:38 編集(投稿者)
2007/05/01(Tue) 18:44:56 編集(投稿者)

>「ディレクトリ名+NULL文字+ファイル名1+NULL文字 ... ファイル名N+NULL文字+NULL文字」
から
結果を受けるところはString型を使うべきではないでしょう。
(内部でNULL文字がでたら、そこまでを文字列として扱うとかしてそうだから)
> <MarshalAs(UnmanagedType.LPTStr)> _
> Public lpstrFile As String
ここかな。

代わりにIntPtr型にして、適当な領域を確保して(Marshal.AllocHGlobal)渡して、
それをByte型配列に入れて(Marshal.Copy)から、
配列の頭から1バイトずつ,NULL文字のチェックをして分割した後、
String型に入れる。(場合によってはエンコードが必要かも)

>CharSet.Auto
だと、GetOpenFileNameAなのかGetOpenFileNameWなのか微妙。。。


追記
> Byte型配列に入れて
ではなくChar型配列のほうが扱いやすいですね。
2007/05/02(Wed) 13:55:29 編集(投稿者)

早速の回答ありがとうございます。

lpstrFile をIntPtr型に変更し、Char型配列(またはByte型配列)にセットしてから取り出してみたところ、
複数のファイル名を取得することができました。本当にありがとうございました。

変更したコードは以下の通りです。

    Private Const FILENAMES_BYTES As Integer = 512

    Public Function ShowDialog() As DialogResult
        Dim ofn As OPENFILENAME = Nothing
        Dim pFile As IntPtr = Marshal.StringToHGlobalUni(New String(vbNullChar, FILENAMES_BYTES))
        Dim fList As New List(Of String)

        ofn.lStructSize = Marshal.SizeOf(ofn)
        ofn.lpstrFilter = "All Files(*.*)" & vbNullChar & "*.*" & vbNullChar & vbNullChar
        ofn.nFilterIndex = 1
        ofn.lpstrFile = pFile
        ofn.nMaxFile = FILENAMES_BYTES
        ofn.lpstrFileTitle = New String(vbNullChar, FILENAMES_BYTES)
        ofn.nMaxFileTitle = ofn.lpstrFileTitle.Length
        ofn.hwndOwner = Form.ActiveForm.Handle
        ofn.Flags = OFN_EXPLORER Or OFN_HIDEREADONLY Or OFN_ALLOWMULTISELECT

        If GetOpenFileName(ofn) = 0 Then
      Marshal.FreeHGlobal(pFile)
            Return DialogResult.Cancel
        End If

        fList = GetFileList(pFile)

        Marshal.FreeHGlobal(pFile)

        If fList.Count >= 2 Then
            Dim i As Integer
            ReDim m_FileNames(fList.Count - 2)
            For i = 0 To fList.Count - 2
                m_FileNames(i) = Path.Combine(fList(0), fList(i + 1))
            Next
        Else
            m_FileNames = New String() {fList(0)}
        End If

        Return DialogResult.OK
    End Function

    Private Function GetFileList(ByVal p As IntPtr) As List(Of String)
        Dim cFileNames(FILENAMES_BYTES / 2 - 1) As Char
        Dim tempFileName As String = ""
        Dim files As New List(Of String)
        Dim i As Integer

        Marshal.Copy(p, cFileNames, 0, FILENAMES_BYTES / 2)
        For i = 0 To FILENAMES_BYTES / 2 - 1
            If cFileNames(i) = vbNullChar Then
                'NULL文字を見つけたらリストに追加
                files.Add(tempFileName)
                'NULL文字が連続したら終了
                If cFileNames(i + 1) = vbNullChar Then
                    Exit For
                End If
                tempFileName = ""
            Else
                tempFileName &= cFileNames(i).ToString()
            End If
        Next

        Return files
    End Function
解決済み!
一文字ずつToStringして&でくっつけていくよりも
Stringクラスのコンストラクタを使ったほうが効率よさそう。

Dim start As Integer = 0
Dim length As Integer = 0

For i As Integer = 0 To  FILENAMES_BYTES / 2 - 1
    If cFileNames(i) = vbNullChar Then
        files.Add(New String(cFileNames, start, length))
        start = i + 1
        length = 0
        If cFileNames(i + 1) = vbNullChar Then
            Exit For
        End If
    Else
        length = length + 1
    End If
Next
解決済み!
2007/05/04(Fri) 10:19:37 編集(投稿者)

■No19648に返信(Blueさんの記事)
> 一文字ずつToStringして&でくっつけていくよりも
> Stringクラスのコンストラクタを使ったほうが効率よさそう。
確かにそうですね。
実は、当初のイメージではそうするつもりだったのですが、Char型配列から指定した
位置の文字列を簡単に取り出す方法を思いつかなかったので、やむを得ず一文字ずつ
処理する方法にしました。

Stringのコンストラクタにこんな便利なオーバーロードがあるとは知りませんでした。
よい勉強になりました。ありがとうございます。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板