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?
CodePudding user response:
I see a few issues:
you are calling
DragAcceptFiles()
only in the Form'sOnCreate
event. If the Form'sHWND
is ever re-created during the Form's lifetime (it can happen!), you will lose the ability to receiveWM_DROPFILES
messages.You would need to call
DragAcceptFiles()
again with the updatedHWND
. You can override the Form's virtualCreateWnd()
method to handle that.Alternatively, you can override the Form's virtual
CreateParams()
method to enable theWS_EX_ACCEPTFILES
extended window style for eachHWND
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 callingSetLength()
, as it will automatically allocate extra space for one (a Delphistring
is implicitly null-terminated, so thatPChar
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 thestring
s length afterwards, which you are doing (but not as efficiently as you could be, asDragQueryFile(i)
will tell you the length to use without a null terminator, so you don't have to calculate it manually withStrLen()
). 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 forTDirectory.GetFiles()
(or more likely,TPath
, whichTDirectory
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;