My form supports drag'n'drop of files from the Windows Explorer:
uses
ShellApi, System.IOUtils;
procedure TFormMain.FormCreate(Sender: TObject);
begin
DragAcceptFiles(Self.Handle, True);
end;
procedure TFormMain.WMDropFiles(var Msg: TMessage);
var
hDrop: THandle;
FileCount, NameLen, i: Integer;
CurrFile: String;
FileSysEntries: TArray<String>;
begin
inherited;
hDrop := Msg.wParam;
try
FileCount := DragQueryFile(hDrop, $FFFFFFFF, nil, 0);
for i := 0 to FileCount - 1 do
begin
NameLen := DragQueryFile(hDrop, i, nil, 0) + 1; //+1 for NULL
SetLength(CurrFile, NameLen);
DragQueryFile(hDrop, i, PWideChar(CurrFile), NameLen);
//If I don't do this...
SetLength(CurrFile, StrLen(PWideChar(CurrFile)));
if DirectoryExists(CurrFile) then
begin
//...I get a stack overflow here!
FileSysEntries := TDirectory.GetFiles(CurrFile, '*.*', TSearchOption.soAllDirectories);
//Rest removed for clarity...
end;
end;
finally
DragFinish(hDrop);
end;
end;
Now if I don't strip the NULL (#0
) character off the CurrFile
string (see 2nd SetLength
) I get a stack overflow when I call TDirectory.GetFiles
and I'm now sure why.
Is the second SetLength
(that strips #0
) really necessary or should I do NameLen - 1
for the first SetLength
? Or maybe something else?
I see a few issues:
you are calling DragAcceptFiles()
only in the Form's OnCreate
event. If the Form's HWND
is ever re-created during the Form's lifetime (it can happen!), you will lose the ability to receive WM_DROPFILES
messages.
You would need to call DragAcceptFiles()
again with the updated HWND
. You can override the Form's virtual CreateWnd()
method to handle that.
Alternatively, you can override the Form's virtual CreateParams()
method to enable the WS_EX_ACCEPTFILES
extended window style for each HWND
that is created.
your message handler is calling inherited
. You don't need to do that. The default handler will not do anything with the message.
you are over-allocating memory for CurrFile
. You technically DO NOT need to include the null terminator when calling SetLength()
, as it will automatically allocate extra space for one (a Delphi string
is implicitly null-terminated, so that PChar
casts can be used with C-style APIs that expect null-terminated character pointers).
If you DO include the null terminator in the string
's length, you have to explicitly shrink the string
s length afterwards, which you are doing (but not as efficiently as you could be, as DragQueryFile(i)
will tell you the length to use without a null terminator, so you don't have to calculate it manually with StrLen()
). But, it is better to simply not over-allocate to begin with.
Apparently having that extra #0
in the string's length is causing problems for TDirectory.GetFiles()
(or more likely, TPath
, which TDirectory
uses internally). You should file a bug report about that. But, you do need to make sure you don't leave the terminating #0
in the string's length to begin with, since filesystem path APIs don't accept it anyway.
Try this instead:
uses
ShellApi, System.IOUtils;
procedure TFormMain.CreateWnd;
begin
inherited;
DragAcceptFiles(Self.Handle, True);
end;
procedure TFormMain.WMDropFiles(var Msg: TMessage);
var
hDrop: THandle;
FileCount, NameLen, i: Integer;
CurrFile: String;
FileSysEntries: TArray<String>;
begin
hDrop := Msg.wParam;
try
FileCount := DragQueryFile(hDrop, $FFFFFFFF, nil, 0);
for i := 0 to FileCount - 1 do
begin
NameLen := DragQueryFile(hDrop, i, nil, 0);
SetLength(CurrFile, NameLen);
DragQueryFile(hDrop, i, PChar(CurrFile), NameLen + 1);
if TDirectory.Exists(CurrFile) then
begin
FileSysEntries := TDirectory.GetFiles(CurrFile, '*.*', TSearchOption.soAllDirectories);
//...
end;
end;
finally
DragFinish(hDrop);
end;
end;