[RESOLVED] Drag & Drop file from compressed folder
I have a VB 6 application that has a form that successfully catches the drop of files from an explorer window. However, if the explorer window is showing the contents of a compressed folder(zip) I am unable to drop this on my form.
Does anyone know if it is possible for me to handle this and if so what I would need to do.
Thanks
Last edited by andy-w; Jan 17th, 2011 at 05:40 AM.
there may well be better (proper) ways to do what you require, but as no one has suggested any, you can test this, but it has some limitations
1. if there are more than 1 shell window open to a zip file you will not know which the item is dragged from
2. it will only work for a single dragged file, but you can detect that multiple files were dragged
vb Code:
Private Sub Form_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single)
i do my best to test code works before i post it, but sometimes am unable to do so for some reason, and usually say so if this is the case. Note code snippets posted are just that and do not include error handling that is required in real world applications, but avoid On Error Resume Next
dim all variables as required as often i have done so elsewhere in my code but only posted the relevant part
come back and mark your original post as resolved if your problem is fixed
pete
Thanks, I think that would work. However, I don't think I can rely on only a single shell window being open. I either need to handle this correctly or not permit the operation.
Given the lack of responses or answers from googling, I guess its not something people generally support.
you can test if there are multiple shell windows open to zip files, then give warning, that operation can not be processed, same with multiple files, give warning then fail
i do my best to test code works before i post it, but sometimes am unable to do so for some reason, and usually say so if this is the case. Note code snippets posted are just that and do not include error handling that is required in real world applications, but avoid On Error Resume Next
dim all variables as required as often i have done so elsewhere in my code but only posted the relevant part
come back and mark your original post as resolved if your problem is fixed
pete
What I know; may be someone else can fill in the gaps
If the drag drop is coming from an Explorer window the class name of the window will be 'CabinetWClass' from;
Code:
Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hWnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Function GetWindowClass(ByVal hWnd As Long) As String
' Return the class name of the specified window
Dim sClass As String
sClass = Space$(256)
GetClassName hWnd, sClass, 255
GetWindowClass = Left$(sClass, InStr(sClass, vbNullChar) - 1)
End Function
where hwnd is supplied by a call to;
Declare Function GetForegroundWindow Lib "user32" () As Long
..because the foreground window must be the one being dragged from.
Data.GetFormat(-16248) gives me True for file(s) dragged from a compressed folder instead of the normal Data.GetFormat(vbCFFiles) when files are dragged from a normal one. This suggests use of a RegisterClipboardFormat, and a RegisterClipboardFormat that is already registered because the Windows Shell already knows how to drag and drop a compressed file to an uncompressed folder and unzip it on the fly.
It is possible to;
byteData() = Data.GetData(-16248)
and get a little sense out of the converted string, but it is spoilt by trash characters.
When dragging and dropping files from Outlook email attachements the magic argument supplied to the RegisterClipboardFormat API must be "FileGroupDescriptor", that does not appear to work for files being dragged from compressed folders. A magic string is required for data format -16248 and some rules for processing thereafter.
The Windows Shell .CopyHere Method is zip aware so will automatically zip/ unzip as files they are Copied/ dragged between Compressed/ UnCompressed folders
it may be the clipboard format to use would be CFSTR_FILEDESCRIPTOR
does the getforegroundwindow return the drag source or target window?
can not test till later
i do my best to test code works before i post it, but sometimes am unable to do so for some reason, and usually say so if this is the case. Note code snippets posted are just that and do not include error handling that is required in real world applications, but avoid On Error Resume Next
dim all variables as required as often i have done so elsewhere in my code but only posted the relevant part
come back and mark your original post as resolved if your problem is fixed
pete
Thats given me a few pointers, seems that using the code below I can now recognize a drop from a compressed folder or a zip. Although I have not yet been able to decode the FileGroupDescriptor.
I can get the foreground window, the source for the drop. Is it possible to call the CopyHere method on it directly as then I would not need to try a decode the list of files.
Code:
Private Const CFSTR_FILEGROUPDESCRIPTORW As String = "FileGroupDescriptorW"
Private hCF_FileGroupDescriptorW As Long
Private Sub Form_Load()
hCF_FileGroupDescriptorW = &H8000 Or RegisterClipboardFormat(CFSTR_FILEGROUPDESCRIPTORW) And &H7FFF&
end sub
Private Sub Form_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single)
If Data.GetFormat(hCF_FileGroupDescriptorW) Then
Endif
end sub
Is it possible to call the CopyHere method on it directly
i guess you can copyhere the selected file (one file) or all files from the folder, but i do not know how you can return all selected files to copy
i do my best to test code works before i post it, but sometimes am unable to do so for some reason, and usually say so if this is the case. Note code snippets posted are just that and do not include error handling that is required in real world applications, but avoid On Error Resume Next
dim all variables as required as often i have done so elsewhere in my code but only posted the relevant part
come back and mark your original post as resolved if your problem is fixed
pete
Option Explicit
Private Declare Function RegisterClipboardFormat Lib "user32" Alias "RegisterClipboardFormatA" (ByVal lpString As String) As Long
Private Declare Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
'typedef struct _FILEGROUPDESCRIPTOR {
' UINT cItems;
' FILEDESCRIPTOR fgd[1];
'} FILEGROUPDESCRIPTOR, *LPFILEGROUPDESCRIPTOR;
'typedef struct _FILEDESCRIPTOR {
' DWORD dwFlags;
' CLSID clsid;
' SIZEL sizel;
' POINTL pointl;
' DWORD dwFileAttributes;
' FILETIME ftCreationTime;
' FILETIME ftLastAccessTime;
' FILETIME ftLastWriteTime;
' DWORD nFileSizeHigh;
' DWORD nFileSizeLow;
' TCHAR cFileName[MAX_PATH];
'} FILEDESCRIPTOR, *LPFILEDESCRIPTOR;
Private Sub Form_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single)
Dim ICF_FILE As Long
Dim CF_FILE As Integer
Dim bdata() As Byte
Dim sNameString As String
Dim filename(), j%, i%, msg$, nfilesingroup%
ICF_FILE = RegisterClipboardFormat("FileGroupDescriptorW")
' lCF_FILE is a Long and has to be converted to an Integer for use with GetFormat like this;
MoveMemory CF_FILE, ICF_FILE, 2
'or apparently like this;
'CF_FILE = Val("&H" & Hex(lCF_FILE))
If Data.GetFormat(CF_FILE) Then
bdata() = Data.GetData(CF_FILE)
'the 1st byte in the strct_FileGroupDescriptorW returns the number of files
nfilesingroup = bdata(1)
sNameString = StrConv(bdata, vbUnicode)
sNameString = Mid$(sNameString, 3)
ReDim filename(1 To nfilesingroup)
For i = 1 To Len(sNameString) - 100 Step (UBound(bdata) - 4) / nfilesingroup 'typically 592 which equates with the length of a struct_FILEDESCRIPTOR
j = j + 1
filename(j) = Mid(sNameString, i + 74) ' the offset of the filename in the struct_FILEDESCRIPTOR
filename(j) = Left$(filename(j), InStr(filename(j), Chr$(0) & Chr$(0)) - 1)
filename(j) = Replace$(filename(j), Chr$(0), "")
msg$ = msg$ & filename(j) & vbCr
Next
MsgBox nfilesingroup & " files dropped" & vbCr & msg$
End If
End Sub
Private CF_FILECONTENTS As Integer
Private CF_FILEGROUPDESCRIPTOR As Integer
Private CF_FILEGROUPDESCRIPTORW As Integer
Private Sub Form_Load()
' Get the clipboard formats
CF_FILECONTENTS = RegisterClipboardFormat("FileContents") And &H7FFF& Or &H8000
CF_FILEGROUPDESCRIPTOR = RegisterClipboardFormat("FileGroupDescriptor") And &H7FFF& Or &H8000
CF_FILEGROUPDESCRIPTORW = RegisterClipboardFormat("FileGroupDescriptorW") And &H7FFF& Or &H8000
End Sub
Private Sub iGrid1_OLEDragDrop( _
ByRef Data As DataObject, _
ByRef Effect As Long, _
ByRef Button As Integer, _
ByRef Shift As Integer, _
ByRef X As Single, _
ByRef Y As Single)
Dim hWindow As Long
Dim i As Integer
Dim tFMT As FORMATETC
Dim tSTGM As STGMEDIUM
Dim tFGD As FILEGROUPDESCRIPTORW
Dim tFD As FILEDESCRIPTORW
Dim lptr As Long
Dim oShell As Shell
Dim oShellWindow As Object
Dim boolGotWindow As Boolean
Dim strFile As String
If Data.GetFormat(CF_FILEGROUPDESCRIPTORW) Then
hWindow = GetForegroundWindow
Set oShell = New Shell
'Find the source shell window for the drag.
For Each oShellWindow In oShell.Windows
If oShellWindow.hwnd = hWindow Then
boolGotWindow = True
Exit For
End If
Next
If boolGotWindow Then
'Get an uncounted reference to the IDataObject
Call MoveMemory(myDataObject, ByVal ObjPtr(Data) + 16, 4)
'Use the IDataObject interface
With tFMT
.cfFormat = CF_FILEGROUPDESCRIPTORW
.dwAspect = DVASPECT_CONTENT
.lIndex = -1
.TYMED = TYMED_HGLOBAL
End With
If myDataObject.GetData(tFMT, tSTGM) = S_OK Then
'Get the pointer to the data
lptr = GlobalLock(tSTGM.Data)
'Copy the data to the UDT
MoveMemory tFGD, ByVal lptr, Len(tFGD)
For i = 0 To tFGD.cItems - 1
'Get the FILEDESCRIPTOR for the files
MoveMemory tFD, ByVal lptr + 4& + Len(tFD) * i, Len(tFD)
strFile = _
Replace(Replace(Mid(oShellWindow.locationurl, 9), "/", "\"), "%20", " ") & _
"\" & _
Left$(tFD.cFileName, InStr(tFD.cFileName, vbNullChar) - 1)
Call oShell.NameSpace(myFolderPath).CopyHere(strFile)
Next
'Release the pointer
GlobalUnlock tSTGM.Data
'Release the data
ReleaseStgMedium tSTGM
End If
'Release the IDataObject
'You can't use Set = Nothing because the reference is not counted
Call MoveMemory(myDataObject, 0&, 4)
End If
End If
End Sub
Re: [RESOLVED] Drag & Drop file from compressed folder
Good. That code makes mine look like a bit of a hack but here is my final just for info...
Code:
Option Explicit
Private Declare Function RegisterClipboardFormat Lib "user32" Alias "RegisterClipboardFormatA" (ByVal lpString As String) As Long
Private Declare Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Private Declare Function GetForegroundWindow Lib "user32" () As Long
'needs a reference to Shell Controls and Automation
'typedef struct _FILEGROUPDESCRIPTOR {
' UINT cItems;
' FILEDESCRIPTOR fgd[1];
'} FILEGROUPDESCRIPTOR, *LPFILEGROUPDESCRIPTOR;
'typedef struct _FILEDESCRIPTOR {
' DWORD dwFlags;
' CLSID clsid;
' SIZEL sizel;
' POINTL pointl;
' DWORD dwFileAttributes;
' FILETIME ftCreationTime;
' FILETIME ftLastAccessTime;
' FILETIME ftLastWriteTime;
' DWORD nFileSizeHigh;
' DWORD nFileSizeLow;
' TCHAR cFileName[MAX_PATH];
'} FILEDESCRIPTOR, *LPFILEDESCRIPTOR;
Private Sub Form_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single)
Dim ICF_FILE As Long
Dim CF_FILE As Integer
Dim bdata() As Byte
Dim sNameString As String
Dim filename As String, Msg As String, Sourcepath As String
Dim j As Integer, i As Integer, nfilesingroup As Integer
Dim hWindow As Long
Dim boolGotWindow As Boolean
Dim oShell As Shell, oShellWindow As Object
hWindow = GetForegroundWindow
Set oShell = New Shell
'Find the source shell window for the drag.
For Each oShellWindow In oShell.Windows
If oShellWindow.hwnd = hWindow Then
boolGotWindow = True
Sourcepath = Replace(Replace(Mid(oShellWindow.locationurl, 9), "/", "\"), "%20", " ")
Exit For
End If
Next
If boolGotWindow Then
ICF_FILE = RegisterClipboardFormat("FileGroupDescriptorW")
' lCF_FILE is a Long and has to be converted to an Integer for use with GetFormat like this;
CF_FILE = Val("&H" & Hex(ICF_FILE)) 'or like this MoveMemory CF_FILE, ICF_FILE, 2
If Data.GetFormat(CF_FILE) Then
bdata() = Data.GetData(CF_FILE)
'the 2 bytes in the strct_FileGroupDescriptorW returns the number of files
nfilesingroup = bdata(1) + 256 * bdata(2)
sNameString = StrConv(bdata, vbUnicode)
sNameString = Mid$(sNameString, 3)
For i = 1 To Len(sNameString) - 100 Step (UBound(bdata) - 4) / nfilesingroup 'typically 592 which equates with the length of a struct_FILEDESCRIPTOR
filename = Mid(sNameString, i + 74) ' the offset of the filename in the struct_FILEDESCRIPTOR
filename = Left$(filename, InStr(filename, Chr$(0) & Chr$(0)) - 1) 'the name is terminated by 2 acsii zeros
filename = Replace$(filename, Chr$(0), "") 'the letters in the name have ascii zeroes between them
filename = Sourcepath & "\" & filename
oShell.NameSpace(CurDir$).CopyHere (filename)
Msg$ = Msg$ & filename & vbCr
Next
MsgBox nfilesingroup & " files dropped and copied to Cdir" & vbCr & Msg$
End If
End If
End Sub
Re: [RESOLVED] Drag & Drop file from compressed folder
A small qwirk; Once you have selected files in Explorer the selection remains there when the ForeGround window changes to some other window; maybe our app's window. It is then still possible to drag the selection from the Explorer window to our target window WITHOUT the Explorer window becoming the ForeGround window again; in cases like this our ForeGround window logic at the start of our routines is looking flaky; no errors it just drops through after If boolGotWindow Then
Re: [RESOLVED] Drag & Drop file from compressed folder
Yes I noticed that when going through in debug, the foreground window was that of the IDE not the shell. When checking through the window handles I was going to see if I could determine if the window was a shell/explorer. Ideally would need to find the source of the drag from the drop, but not sure there is a way to find that from the IDataObject. I have the file names from the IDataObject but don't know from which folder (namespace) without finding the shell.
Re: [RESOLVED] Drag & Drop file from compressed folder
Maybe I shouldn't be trying to find the source shell. But rather should be trying to get the file contents from a storage stream in the data object. Then creating the file and copying the stream into it. Although I don't seem to be able to google any suitable examples of doing this.
Do you mean the whole thing or just the stream thing you were discussing in your last post?
I don't seem to have a problem copying files from sub folders within the zip when they are plain folders or even zips within a zip; just as long as they are displayed in the foreground window.
you can match the shell window to the foreground window
vb Code:
hw = getforgroundwindow
for each sw in sh.windows
if sw.hwnd = hw then exit for
next
i do my best to test code works before i post it, but sometimes am unable to do so for some reason, and usually say so if this is the case. Note code snippets posted are just that and do not include error handling that is required in real world applications, but avoid On Error Resume Next
dim all variables as required as often i have done so elsewhere in my code but only posted the relevant part
come back and mark your original post as resolved if your problem is fixed
pete
Andy ref post #20 - I am not getting that behaviour. LocationUrl is returning the full path for me, although I have not tested it with a filename containing that many dots. Filenames with more than one dot can be challenging at the best of times, see how it goes without them.
Westconn1 - Contary to what I said in post #6 we estabished in post #14 that it is possible to drag a selection from an Explorer window which is not the foreground window.
Andy ref post #20 - I am not getting that behaviour under Vista or Windows 7, but I DO under XP; so weird!
I was wondering if this could be affected by the Users Folder Options but I've switched around a few of them like 'show full path in title bar' with no success.
i do my best to test code works before i post it, but sometimes am unable to do so for some reason, and usually say so if this is the case. Note code snippets posted are just that and do not include error handling that is required in real world applications, but avoid On Error Resume Next
dim all variables as required as often i have done so elsewhere in my code but only posted the relevant part
come back and mark your original post as resolved if your problem is fixed
pete
There seems to be a fair example of that at http://stackoverflow.com/questions/2...xplorer-window. But it seems to suffer from the same problem when we are in a Sub Folder under XP; the path preceding the Folder name goes missing. Incidentally it goes missing from the Explorer window's Caption too. The technique does not seem to help to identify the Explorer window being dragged from either.
No, everything seems just relative to the compressed folder.
I think I am more convinced I should be trying to handle the stream for the file as then I would have no worry about the source. I just don't seem to be able to find a working example of it. ANd the bits I have tried just give an error.
I seem to be able to access the data object, with CFSTR_FILECONTENTS and get a STGMEDIUM for the file. But I cannot seem to process this as a stream. I can't believe that someone hasn't done this before even in .net etc.
I think I am more convinced I should be trying to handle the stream for the file as then I would have no worry about the source.
probably correct
but it looks like you can get the path by
vb Code:
With sw.document.focuseditem.GetFolder.parentfolder.self
MsgBox .Parent.self.Path & "\" & .Path
End With
i do my best to test code works before i post it, but sometimes am unable to do so for some reason, and usually say so if this is the case. Note code snippets posted are just that and do not include error handling that is required in real world applications, but avoid On Error Resume Next
dim all variables as required as often i have done so elsewhere in my code but only posted the relevant part
come back and mark your original post as resolved if your problem is fixed
pete
I think it is completely possible to get the file's data and then write that data to another file somewhere. But moving it would require deletion of original & unless you come up with a idiot-proof method of locating the source path (including any subpaths within the compressed folder), then ???
The data itself (CF_FILECONTENTS) can be retrieved via an IStream (TYMED_ISTREAM) or IStorage (TYMED_ISTORAGE) object via a TLB or hard-to-use DispCallFunc API. The data should not be available via an TYMED_GLOBAL from what I've read: Windows will use IStream/IStorage. Another app might use TYMED_GLOBAL though.
Edited: Can this one be queried from the data object also? CFSTR_SHELLIDLIST
If so, maybe it can be used to transfer files via a PIDL (SHGetPathFromIDList API)
:: CFSTR_SHELLIDLIST string is "Shell IDList Array"
Follow-up. Out of curiosity, I tried it. No joy, so CFSTR_SHELLIDLIST isn't viable.
Last edited by LaVolpe; Jan 14th, 2011 at 07:31 PM.
Insomnia is just a byproduct of, "It can't be done"
Unfortunately that still only gives a path relative to the compressed folder
that code was returning the full path when i was testing
i do my best to test code works before i post it, but sometimes am unable to do so for some reason, and usually say so if this is the case. Note code snippets posted are just that and do not include error handling that is required in real world applications, but avoid On Error Resume Next
dim all variables as required as often i have done so elsewhere in my code but only posted the relevant part
come back and mark your original post as resolved if your problem is fixed
pete
Just FYI. VB only + API (no TLB) method of retrieving file content from compressed folders
Read comments in code. Create new project, one form & paste code. Drag file(s) from compressed folder to test. Caution. This was not tested to the extremes
Code:
*** removed. Updated and replaced in post #39 below
- FYI: I tried to call IStream::Stat method to get stream size vs getting file descriptors. But call kept returning error-not implemented.
Just FYI. VB only + API (no TLB) method of retrieving file content from compressed folders
Read comments in code. Create new project, one form & paste code. Drag file(s) from compressed folder to test. Caution. This was not tested to the extremes
Edited: Some more notes
- Not compatible with files > 1GB (nFileSizeHigh<>0). If to be supported, create target file with CreateFile API and read stream directly to the file with no byte array intermediary. Search forum for VB file support ~2GB
- FYI: I tried to call IStream::Stat method to get stream size vs getting file descriptors. But call kept returning error-not implemented.
Thanks,Lavolpe. It works (tested on Win7 after compiled) on ANSI (even though winzip also not support Unicode). I remember you have come out of droped unicode files routine few years ago.
Please refine the code to support Unicode and allow 2GB above.
Last edited by Jonney; Jan 15th, 2011 at 09:03 AM.
Reason: Test on Win7
Thanks,Lavolpe. It works (tested on Win7 after compiled) on ANSI. I remember you have come out of droped unicode files routine few years ago.
Please refine the code to support Unicode and allow 2GB above.
I can only assume it does support unicode. The cFileName comes in @ 2-bytes per char, not 1. However, I cannot create a unicode-compatible compressed folder on my XP machine. Error keeps occurrring: 'The compression cannot be performed for file or directory. [filename] contains characters in its name that Compressed (zipped) Folders cannot store: [then lists the unicode characters]". My version of Winzip won't zip up those either.
If anything, the code I posted won't support ANSI, but then again, compressed folders can't be created on ANSI systems, can they? Isn't that option for XP and above only?
For others: I'll leave it to you to support +~2GB files....
Edited: See post #39 for updatd code & should handle very large files
Last edited by LaVolpe; Jan 15th, 2011 at 02:34 PM.
Reason: typo
Insomnia is just a byproduct of, "It can't be done"
I can only assume it does support unicode. The cFileName comes in @ 2-bytes per char, not 1. However, I cannot create a unicode-compatible compressed folder on my XP machine. Error keeps occurrring: 'The compression cannot be performed for file or directory. [filename] contains characters in its name that Compressed (zipped) Folders cannot store: [then lists the unicode characters]". My version of Winzip won't zip up those either.
If anything, the code I posted won't support ANSI, but then again, compressed folders can't be created on ANSI systems, can they? Isn't that option for XP and above only?
I attached files with Unicode filename for you testing.
Last edited by Jonney; Jan 15th, 2011 at 09:45 AM.
Thanx, I'll give it a go.
Note that the routine I gave won't do folders within a folder that is zipped. In other words if compressed folder has inside of it a compressed folder that contains a file & you want to extract the inner folder, you will get 2 files extracted: the inner compressed folder (0 bytes), then the file. So to save, you'd have to create the subfolder first, then the file within that subfolder if you wanted to keep the folder structure. I'm playing with updating the above code to do that.
Edited: Jonney, she's a pretty lady! It worked fine. I was able to extract the data and write it to a file. The file was created with CreateFile API to support unicode characters.
Note: When I saved the zip to my drive, unicode characters changed too, but data remained ok
Last edited by LaVolpe; Jan 15th, 2011 at 10:40 AM.
Insomnia is just a byproduct of, "It can't be done"
Ok, here is the newest re-write of the code. Things I've changed...
1) Data is not written to a byte array. It is written directly to file via a mapped file
2) Creates subfolders if dragging compressed folders within compressed folders
3) Creates files/subfolders using unicode-aware APIs
4) Theoretically handles +2GB files. I'll leave that to someone else to test. Very large files are handled via a loop using a pre-defined maximum chunk size. That chunk size is currently set at approximately 100MB, but you can change it higher or lower.
For testing. Create new blank form, paste attached code & run after changing the target path in Form_OLEDragDrop.
Read comments in code
Edited Yet Again. Dang copy & paste error. All fixed below
Either re-download or make following change:
Routine: Long2Size
Change from: LongLow = CLngLong2Size - 4294967295@) - 1&
Change to: LongLow = CLng(Long2Size - 4294967295@) - 1&
See post #48 for more comments, suggested enhancements/improvements.
GO BEARS!!!!
Last edited by LaVolpe; Jan 17th, 2011 at 11:13 AM.
Insomnia is just a byproduct of, "It can't be done"