|
Yeah, I knew that. Slip of the brain when I typed it.
|
|
|
|
|
OK, I'm finally getting this to work. I took the suggestion more seriously that GetLastError() might provide some insight into the problem. It wasn't anything that explicitly told me what was wrong, but it did cause me to take a much closer look at all of my declarations, and realizing that using "\\?\UNC\" was not correct...It needs to be just "\\?\".
But also took to heart your comments on strings and pointers. In that regard, I found some typing problems here and there, and missed some lines that were still not rewritten for pointers.
I'll post the resulting code here, shortly, after I copy my test code over to my final demo program and fix some remaining issues.
|
|
|
|
|
One hickup...I need to deal with a C type, LPWSTR. When I research how to get this in vb.Net, I do not find any consistent explanation of how to deal with marshaling unmanaged types. There are no real-world examples and what MSDN offers is cryptic at best. Here is my best guess that does not show any syntax errors:
Dim LPWSTR As UnmanagedType
Dim MyUnicodePointer As New MarshalAsAttribute(LPWSTR)
|
|
|
|
|
An LPWSTR is just a Long Pointer to a Wide character String. You're just marshalling a normal string from .NET to the function your calling. Usually, you don't have to do anything special in your function Declaration other than tell it that the paramter is a String .
|
|
|
|
|
I'm confused. The function requires a pointer to a wide string, so you can't use 'As String' as a passed argument to the function FindFirstFileEx(). You have to use 'As LWPSTR' or, if I understand you correctly, 'As Long'. If that is true, then I have to wrap the string in a 'long' pointer, which for a 64bit platform would translate to Int32 (or rather, IntPtr), since a win32 function can only accept 32bits arguments and parameters.
If that is true, then my existing marshal should be correct:
Dim MyStringPtr As Long = Marshal.StringToHGlobalAuto(MyString)
But when I pass MyStringPtr to FindFileEx(), I only get back the first character in a Unicode folder/file name, and then, only when the first character in the string happens to be in the ANSI range.
|
|
|
|
|
I haven't read all of this thread but I don't see either the Auto or Unicode character set modifiers in your external procedure declarations, which means that the default of Ansi is being applied. This could be the root cause of your string problems as UTF16 encoded strings treated as Ansi will get truncated at the first null, i.e. after the first character.
See Declare statement[^] and also Calling Windows APIs[^] for examples using both Declare and DllImport.
Alan.
|
|
|
|
|
That basically has been my conclusion, though I don't know how to fix it yet. I figured SOMETHING had to be cutting the returned string short, and I know that the input string is getting to memory correctly, because I can retrieve it just fine. So that leaves the WIN32_FIND_DATA structure as the culprit. Documentation says the following declaration is required:
<VBFixedString(MAX_PATH), MarshalAs(UnmanagedType.ByValTStr, SizeConst:=MAX_PATH)> Public cFileName As String
And for my string pointer, I used:
Dim lpFileName_IntPtr As Long = Marshal.StringToHGlobalAuto(lpFileName_str)
I experimented with changing MAX_PATH to 32767 rather than 260, but alone by itself it does nothing. And anyway, that is a max character length issue, not an ANSI vs. wide character issue. So I feel that the problem is with declaring cFileName, correctly. That was where I was at when I saw your post. I do not know how to squeeze some martialing in there to account for a wide string.
modified 6-Jun-13 20:22pm.
|
|
|
|
|
Hi treddie,
Based on my understanding of your code, I see no reason to use FindFirstFileEx versus FindFirstFile. Also, the directory filter flag option can fail silently if it is not supported and you need to check the attributes anyways if it was your intent to just recover directories. But it appears as your code is also doing these checks.
Just a word of caution about interop examples you find on the web; many of these were originally done in VB6 and may not have been properly modified for DotNet types.
Here are links to a reference and a tool that you may find useful in working with interop.
Miscellaneous Marshaling (look at the links under "See Also") Samples[^]
PInvoke Interop Assistant[^]
I was bored so I tried putting together a simple TreeView file explorer that bypasses the 260 character limit. It may serve as an example for what you are trying to do. Just place a TreeView control on new WinForm project and paste this code.
I used the command prompt "Subst" command to create a drive reference to a path that reached the 260 character barier and then added some more directories under that substituted drive to get to a file that had 400+ characters in it's path to test this. It appears to work.
Edit: There appears to be a length limit on codeblock code.
Imports System.Runtime.InteropServices
Public Class Form1
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
TreeView1.Nodes.Add(DriveToTreeNode("c"c))
End Sub
Private Sub TreeView1_AfterCollapse(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeViewEventArgs) Handles TreeView1.AfterCollapse
e.Node.Nodes.Clear()
e.Node.Nodes.Add(New TreeNode With {.Tag = New NodeData With {.IsDirectory = True}}) ' add dummy node
End Sub
Private Sub TreeView1_BeforeExpand(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeViewCancelEventArgs) Handles TreeView1.BeforeExpand
' only directories can have nodes
e.Node.Nodes.Clear()
EnumerateDirectory(e.Node)
End Sub
Private Sub TreeView1_MouseDoubleClick(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles TreeView1.MouseDoubleClick
If e.Button = Windows.Forms.MouseButtons.Left Then
Dim hti As TreeViewHitTestInfo = TreeView1.HitTest(TreeView1.PointToClient(MousePosition))
If CType(hti.Node.Tag, NodeData).IsFile Then
Dim fullpath As String = CType(hti.Node.Parent.Tag, NodeData).DirectoryPath & hti.Node.Text
MessageBox.Show("Long Path: " & fullpath & vbCrLf & "Path Length: " & fullpath.Length.ToString() & _
vbCrLf & "Short Name: " & GetShortPathName(fullpath), _
hti.Node.Text, MessageBoxButtons.OK)
End If
End If
End Sub
#Region "Drive Enumeration"
Private Function DriveToTreeNode(ByVal drive As Char) As TreeNode
Dim node As New TreeNode
node.Text = drive.ToString().ToUpper()
node.Tag = New NodeData With {.IsDirectory = True, .DirectoryPath = node.Text & ":\"}
' recursing all filedirectories would take too long for a drive
' so will handle it on each directory node expansion
EnumerateDirectory(node, recursedirectories:=False)
Return node
End Function
' see: Naming Files, Paths, and Namespaces
' Maximum Path Length Limitation
' http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx#maxpath
Private Const LongPathPrefix As String = "\\?\"
Private Sub EnumerateDirectory(ByVal node As TreeNode, Optional ByVal recursedirectories As Boolean = False)
Dim path As String = CType(node.Tag, NodeData).DirectoryPath
Dim findData As New WIN32_FIND_DATA
Dim hFile As Microsoft.Win32.SafeHandles.SafeFileHandle
hFile = FindFirstFile(LongPathPrefix & path & "*", findData)
If hFile.DangerousGetHandle.ToInt32 = -1 Then
' just ignoring errors for this demo, most common is 5 - Access Denied
' System Error Codes
' http://msdn.microsoft.com/en-us/library/windows/desktop/ms681381%28v=vs.85%29.aspx
'Dim [error] As Int32 = Marshal.GetLastWin32Error()
'Stop
Return
Else
Do
If IsFlagSet(findData.dwFileAttributes, IO.FileAttributes.Directory) Then
If findData.cFileName = "." Then Continue Do
If findData.cFileName = ".." Then Continue Do
Dim subdir As New TreeNode With {.Text = findData.cFileName, _
.Tag = New NodeData With {.IsDirectory = True, _
.DirectoryPath = path & findData.cFileName & "\" _
} _
}
If recursedirectories Then
EnumerateDirectory(subdir, recursedirectories)
Else
' need to add a dummy node to make it expandable
subdir.Nodes.Add(New TreeNode With {.Tag = New NodeData With {.IsDirectory = True}})
End If
node.Nodes.Add(subdir)
Else ' Filename
node.Nodes.Add(New TreeNode With {.Text = findData.cFileName, .Tag = New NodeData With {.IsDirectory = False}})
End If
Loop While FindNextFile(hFile, findData)
FindClose(hFile) ' make sure to close the handle
End If
End Sub
Private Function IsFlagSet(ByVal value As System.ValueType, ByVal testflag As System.ValueType) As Boolean
Dim val As UInt32 = CType(value, UInt32)
Dim flag As UInt32 = CType(testflag, UInt32)
Return (val And flag) = flag
End Function
<DllImport("kernel32.dll", CharSet:=CharSet.Unicode, SetLastError:=True, EntryPoint:="FindFirstFile")> _
Private Shared Function FindFirstFile(<System.Runtime.InteropServices.In(), MarshalAs(UnmanagedType.LPWStr)> ByVal fileName As String, _
<[In](), Out(), MarshalAs(UnmanagedType.LPStruct)> ByVal data As WIN32_FIND_DATA) As Microsoft.Win32.SafeHandles.SafeFileHandle
End Function
<DllImport("kernel32.dll", CharSet:=CharSet.Unicode, SetLastError:=True, EntryPoint:="FindNextFileW")> _
Private Shared Function FindNextFile(ByVal hFindFile As Microsoft.Win32.SafeHandles.SafeFileHandle, _
<[In](), Out(), MarshalAs(UnmanagedType.LPStruct)> ByVal ByvallpFindFileData As WIN32_FIND_DATA) As Boolean
End Function
<DllImport("kernel32.dll")> _
Private Shared Function FindClose(ByVal hFindFile As Microsoft.Win32.SafeHandles.SafeFileHandle) As Boolean
End Function
<DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function GetShortPathName(ByVal longPath As String, _
<MarshalAs(UnmanagedType.LPTStr)> ByVal ShortPath As System.Text.StringBuilder, _
<MarshalAs(Runtime.InteropServices.UnmanagedType.U4)> ByVal bufferSize As Integer) As Integer
End Function
2nd part as text (sorry about format)
Private Shared Function GetShortPathName(ByVal path As String) As String
Dim sb As New System.Text.StringBuilder(1000)
Dim len As Int32 = GetShortPathName(LongPathPrefix & path, sb, sb.Capacity)
If len > sb.Capacity Then
sb.Capacity = len
GetShortPathName(LongPathPrefix & path, sb, sb.Capacity)
End If
Return sb.ToString().Replace(LongPathPrefix, "")
End Function
<structlayout(layoutkind.sequential, charset:="CharSet.Unicode)," bestfitmapping(false)=""> _
Private Class WIN32_FIND_DATA
<marshalas(unmanagedtype.u4)> Public dwFileAttributes As IO.FileAttributes
<marshalas(unmanagedtype.u4)> Private ftCreationTime_dwLowDateTime As UInt32
<marshalas(unmanagedtype.u4)> Private ftCreationTime_dwHighDateTime As UInt32
<marshalas(unmanagedtype.u4)> Private ftLastAccessTime_dwLowDateTime As UInt32
<marshalas(unmanagedtype.u4)> Private ftLastAccessTime_dwHighDateTime As UInt32
<marshalas(unmanagedtype.u4)> Private ftLastWriteTime_dwLowDateTime As UInt32
<marshalas(unmanagedtype.u4)> Private ftLastWriteTime_dwHighDateTime As UInt32
<marshalas(unmanagedtype.u4)> Public nFileSizeHigh As UInt32
<marshalas(unmanagedtype.u4)> Public nFileSizeLow As UInt32
<marshalas(unmanagedtype.u4)> Public dwReserved0 As UInt32
<marshalas(unmanagedtype.u4)> Public dwReserved1 As UInt32
' Note: by changing the cFileName size constant from 260 to 32767 to handle long
' path names, it appears that cAlternateFileName always returns blank
' if the ShortPathName is needed, then use the GetShortPathName function
' http://msdn.microsoft.com/en-us/library/windows/desktop/aa364989%28v=vs.85%29.aspx
<marshalas(unmanagedtype.byvaltstr, sizeconst:="32767)"> Public cFileName As String
<marshalas(unmanagedtype.byvaltstr, sizeconst:="14)"> Public cAlternateFileName As String
Public Function CreationTime() As DateTime
Return DateTime.FromFileTime(MergeToInt64(ftCreationTime_dwLowDateTime, ftCreationTime_dwHighDateTime))
End Function
Public Function LastAccessTime() As DateTime
Return DateTime.FromFileTime(MergeToInt64(ftLastAccessTime_dwLowDateTime, ftLastAccessTime_dwHighDateTime))
End Function
Public Function LastWriteTime() As DateTime
Return DateTime.FromFileTime(MergeToInt64(ftLastWriteTime_dwLowDateTime, ftLastWriteTime_dwHighDateTime))
End Function
Public Function FileSizeInBytes() As UInt64
Return (CULng(nFileSizeHigh) << 32) Or CULng(nFileSizeLow)
End Function
Private Function MergeToInt64(ByVal low As UInt32, ByVal high As UInt32) As Int64
Dim bighigh As UInt64 = high
bighigh <<= 32
bighigh = bighigh Or CULng(low)
Return CLng(bighigh)
End Function
End Class ' WIN32_FIND_DATA
#End Region
Private Class NodeData
Private _IsDirectory As Boolean
Public Property IsDirectory() As Boolean
Get
Return _IsDirectory
End Get
Set(ByVal value As Boolean)
_IsDirectory = value
End Set
End Property
Public ReadOnly Property IsFile() As Boolean
Get
Return Not IsDirectory
End Get
End Property
Public DirectoryPath As String
End Class 'NodeData
End Class
modified 6-Jun-13 0:59am.
|
|
|
|
|
But FindFirstFile() cannot handle Unicode, as far as MSDN seems to suggest.
|
|
|
|
|
Where in the documentation does it suggest that?
Quite the opposite is suggested in http://msdn.microsoft.com/en-us/library/windows/desktop/aa364418%28v=vs.85%29.aspx[^]
Quote: In the ANSI version of this function, the name is limited to MAX_PATH characters. To extend this limit to 32,767 wide characters, call the Unicode version of the function and prepend "\\?\" to the path. For more information, see Naming a File.
|
|
|
|
|
Damn. I swear to gawd I saw it say you needed to use the Ex version to make it happen. Somewhere along the line I got messed up there. Too many questions all at once, I guess.
But now that I know that, my overly-verbose usage should still work...I just have to keep track of extra arguments. Also, it is a good exercise in understanding both, since I may in the future need to use the Ex function.
I haven't had a chance yet, today, to run your example.
|
|
|
|
|
Quote: Damn. I swear to gawd I saw it say you needed to use the Ex version to make it happen.
I blame it all on micro-blackhole that make one jump from one parallel universe to another just to screw with your mind when the minor differences bite you in the behind.
All I can say is that you are making an effort to learn and that is a rare and beautiful thing to see in a forum these days.
|
|
|
|
|
Thanks, man!
It can be frustrating, but it is more frustrating to stay confused and not see the frikkin light!
|
|
|
|
|
I built your project, but when run, the treeview stays blank.
|
|
|
|
|
Edit: I deleted the upload, forgot I was in the middle of trying an ASCII version. Will repost a link once I get it back to Unicode.
That's strange. I tried running it on a low privilege account and it run fine on Vista/32 bit.
In case anything got messed up while I was trying to post the code(I had some issues) I've uploaded the project (VS2008) to: http://sdrv.ms/18SnASR
This version is a bit different because I've been playing with it bit this evening. You will also find the syntax for the FindFirstFileEx function in the scraps.txt file.
Try un-commenting the code in this block:
If hFile.DangerousGetHandle.ToInt32 = -1 Then
Dim [error] As Int32 = Marshal.GetLastWin32Error()
Stop
Return
Else
and see what error code, if any, you are getting.
modified 6-Jun-13 21:43pm.
|
|
|
|
|
|
That one worked great. Now I need to examine what the heck you did!
|
|
|
|
|
So the problem WAS in my version of the WIN32_FIND_DATA structure.
I copied your declarations over to replace mine and other than having to provide something other than null for the initial values, nothing else was required. After doing that, my program works fine.
But I do not understand why my declarations were wrong.
-------------------------------------------------------------------------------------------------
Edit: vb had a banner over my function declaration that was telling me that I might have to martial the WIN32_FIND_DATA structure. I thought it was only referring to the two strings. But why was martialing required everywhere else?
-------------------------------------------------------------------------------------------------
The last two for the strings were what vb.Net suggested when I originally converted my vb6 program over to net, which apparently, in this case, were not the correct solutions. For the other declarations, FILETIME did not work, and the one that bothers me the worst is that Uint32 did not work. Isn't U4 (Unsigned 4 bytes) the same in principal as Uint32 (Unsigned 4 bytes)?
My old structure:
Private Structure WIN32_FIND_DATA
Dim dwFileAttributes As UInt32
Dim ftCreationTime As FILETIME
Dim ftLastAccessTime As FILETIME
Dim ftLastWriteTime As FILETIME
Dim nFileSizeHigh As UInt32
Dim nFileSizeLow As UInt32
Dim dwReserved0 As UInt32
Dim dwReserved1 As UInt32
<VBFixedString(MAX_PATH), MarshalAs(UnmanagedType.ByValTStr, SizeConst:=MAX_PATH)> Public cFileName As String
<VBFixedString(14), MarshalAs(UnmanagedType.ByValTStr, SizeConst:=14)> Public cAlternate As String
End Structure
Your structure:
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode), BestFitMapping(False)> _
Private Class WIN32_FIND_DATA
<MarshalAs(UnmanagedType.U4)> Public dwFileAttributes As IO.FileAttributes = 8208
<MarshalAs(UnmanagedType.U4)> Private ftCreationTime_dwLowDateTime As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Private ftCreationTime_dwHighDateTime As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Private ftLastAccessTime_dwLowDateTime As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Private ftLastAccessTime_dwHighDateTime As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Private ftLastWriteTime_dwLowDateTime As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Private ftLastWriteTime_dwHighDateTime As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Public nFileSizeHigh As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Public nFileSizeLow As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Public dwReserved0 As UInt32 = 0
<MarshalAs(UnmanagedType.U4)> Public dwReserved1 As UInt32 = 0
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32767)> Public cFileName As String = " "
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=14)> Public cAlternateFileName As String = " "
modified 7-Jun-13 2:52am.
|
|
|
|
|
TnTinMn,
I am curios about your usage of U4. Isn't U4 (Unsigned 4 bytes) the same in principal as Uint32 (Unsigned 4 bytes)?
|
|
|
|
|
Like most people, I have some bad habits.
I will decorate the fields with MarshalAs even if it is not needed. It is usually an indication that my first attempt failed (i.e. I copied the declaration for PInvoke.com and it was incorrect) and then I checked the un-managed declaration for the expected type or that I wrote the declaration myself based on the specification.
You are correct concerning the U4 usage. It is not needed when using a UInt32 for a dword field.
see: Blittable and Non-Blittable Types
|
|
|
|
|
Got you. Thanks for the learning. I will say that data typing can be pretty difficult. All it takes is screwing up a ByRef for a ByVal and not figuring it out until an hour later. So I can thoroughly understand using marshaling just to be on the safe side. I wish they would simplify the syntax, however. Like, maybe, "U4ToUInt32(parameter name)". That way, a quick marshaling could be done without having to worry about a long verbose syntax, let alone trying to remember the path of the namespace.
|
|
|
|
|
Quote: ... let alone trying to remember the path of the namespace. Organize your code and move all interop to a separate file and set the proper Imports statements at the top of the code.
Then to really mess with those who read your code but never the MSDN language specification , set an alias for the Imports.
Imports sri = System.Runtime.InteropServices
Imports lok = System.Runtime.InteropServices.LayoutKind
Imports umt = System.Runtime.InteropServices.UnmanagedType
<sri.StructLayout(lok.Sequential)> _
Public Class Class1
<sri.MarshalAs(umt.U4)> Public Fred As UInt32
End Class
Just be careful with the aliasing, there is glitch in their implementation that lets you alias all the way to the class/type name. That is just plain wrong as discussed here.[^]
|
|
|
|
|
I certainly would not like to have to go looking for the an alias, except when lines start getting too long and too numerous. That means I have to jump all over the place to find it, and then I might lose my train of thought. That's true of my own code, too, especially after having not looked at it in ages.
That one example reminds me of someone deliberately trying to make their code obscure so that people will have a hard time understanding it. Works fine, unless you forgot you did it and you come back later and say, "What the F!".
I don't understand why the IDE doesn't see something crazy-wrong like using string = int32 .
|
|
|
|
|
Hey, has anyone heard of the Delimon.win32.IO library?
I just ran into it at MS and it it is a direct replacement for the System.IO functions, with same function names, to boot. The great part is that it crosses the MAX_PATH barrier by design. It requires NET Framework 4. So that means VS2010, unless there is a way to get VS2008 to see it.
If someone mentioned this earlier, I apologize. But I did not find anything on it when doing a search here.
I have added a reference to it in my project and am about to test it.
UPDATE: Is NOT a direct replacement! Going through the properties/methods, a lot are simply not there, or stuff has been moved around and called by different names. Not sure if it completely covers all the functionality of System.IO.
Also, since it requires framework 4, I don't think WinXP can use it. Which is a bummer since there are still a lot of XP systems out there.
UPDATE 2: It MAY be a direct replacement after all...I might have attempted to type in methods while the dll reference was not properly set up. At any rate, this is a brand new library apparently, and it IS buggy. For instance, Directory.Exists and File.Exists methods do not filter correctly...Each returns "True" regardless of whether the resource is a folder or a file.
modified 13-Jun-13 14:41pm.
|
|
|
|
|
To CodeProject,
This is regarding a project that I'm trying to implement - quick chronological file opener script with retrieving 'FileDate' and 'ObjFilePath' and sorting its data into application calendar menu with qick links to exisiting files.
Plese give some suggestions on file date and time attributes inserting into form style widget
Regards, Nik
|
|
|
|
|