为什么 Delphi TOpenDialog 在初始目录中无法打开?
Why does Delphi TOpenDialog fail to open in the Initial Directory?
我正在使用 TOpenDialog
(在 Delphi 10.4 中)向用户显示我在他们的文档文件夹中为他们安装的 PDF 文件。在该文件夹中,我创建了一个文件夹 MyFolder10.2
并将 PDF 文件复制到那里。
代码很简单,过去一直有效,即使现在它仍然可以在我较慢的旧 Win10 机器上工作。但是在我更新更快的 Win10 计算机上,它只能在某些时候工作。当它不起作用时,会打开一个文件对话框,但在其他目录中(不确定它来自哪里),并且它不会过滤在 TOpenDialog
中设置的文件类型 (.pdf
)组件。
有什么办法可以追查这个谜团吗?
docPath:= GetEnvironmentVariable('USERPROFILE') + '\Documents\MyFolder10.2\';
OpenDocsDlg.InitialDir := docPath;
OpenDocsDlg.Execute;
在 Vista 之前,TOpenDialog
是 GetOpenFileName()
API 的包装器,其中 TOpenDialog.InitialDir
映射到 OPENFILENAME.lpstrInitialDir
字段。
在 Vista 和更高版本上,TOpenDialog
(通常,取决于配置)包裹 IFileDialog
/IFileOpenDialog
API instead, where TOpenDialog.InitialDir
maps to the IFileDialog.SetFolder()
method (not to IFolderDialog.SetDefaultFolder()
,正如人们所期望的那样。
根据 OPENFILENAME
文档:
lpstrInitialDir
Type: LPCTSTR
The initial directory. The algorithm for selecting the initial directory varies on different platforms.
Windows 7:
- If
lpstrInitialDir
[TOpenDialog.InitialDir
] has the same value as was passed the first time the application used an Open or Save As dialog box, the path most recently selected by the user is used as the initial directory.
- Otherwise, if
lpstrFile
[TOpenDialog.FileName
] contains a path, that path is the initial directory.
- Otherwise, if
lpstrInitialDir
is not NULL [TOpenDialog.InitialDir
is not empty], it specifies the initial directory.
- If
lpstrInitialDir
is NULL [TOpenDialog.InitialDir
is empty] and the current directory contains any files of the specified filter types, the initial directory is the current directory.
- Otherwise, the initial directory is the personal files directory of the current user.
- Otherwise, the initial directory is the Desktop folder.
Windows 2000/XP/Vista:
- If
lpstrFile
[TOpenDialog.FileName
] contains a path, that path is the initial directory.
- Otherwise,
lpstrInitialDir
[TOpenDialog.InitialDir
] specifies the initial directory.
- Otherwise, if the application has used an Open or Save As dialog box in the past, the path most recently used is selected as the initial directory. However, if an application is not run for a long time, its saved selected path is discarded.
- If
lpstrInitialDir
is NULL [TOpenDialog.InitialDir
is empty] and the current directory contains any files of the specified filter types, the initial directory is the current directory.
- Otherwise, the initial directory is the personal files directory of the current user.
- Otherwise, the initial directory is the Desktop folder.
根据 Common Item Dialog 文档:
Controlling the Default Folder
Almost any folder in the Shell namespace can be used as the default folder for the dialog (the folder presented when the user chooses to open or save a file). Call IFileDialog::SetDefaultFolder
prior to calling Show
[TOpenDialog.Execute()
] to do so.
The default folder is the folder in which the dialog starts the first time a user opens it from your application. After that, the dialog will open in the last folder a user opened or the last folder they used to save an item. See State Persistence for more details.
You can force the dialog to always show the same folder when it opens, regardless of previous user action, by calling IFileDialog::SetFolder
[TOpenDialog.InitialDir
]. However, we do not recommended doing this. If you call SetFolder
before you display the dialog box, the most recent location that the user saved to or opened from is not shown. Unless there is a very specific reason for this behavior, it is not a good or expected user experience and should be avoided. In almost all instances, IFileDialog::SetDefaultFolder
is the better method.
When saving a document for the first time in the Save dialog, you should follow the same guidelines in determining the initial folder as you did in the Open dialog. If the user is editing a previously existing document, open the dialog in the folder where that document is stored, and populate the edit box with that document's name. Call IFileSaveDialog::SetSaveAsItem()
with the current item prior to calling Show
[TOpenDialog.Execute()
].
TOpenDialog
和 TFileOpenDialog
have properties that map to the IFileDialog.SetDefaultFolder()
or IFileDialog.SetSaveAsItem()
methods. However, the TFileOpenDialog.Dialog
属性 都不会让您访问底层 IFileDialog
,因此您可以手动调用这些方法,例如在 TFileOpenDialog.OnExecute
事件.
@Remy Lebeau 的详细回答促使我再次尝试解决一个有问题的常见场景,尽管我尝试了很多次,但我以前没有设法解决这个问题:
我有一个带有 TFileOpenDialog 和 TFileSaveDialog 的图像编辑 VCL 应用程序,通常用于在连接的 Android 设备上一个接一个地编辑一堆屏幕截图,这些屏幕截图保存在台式计算机上。
问题是 FileOpenDialog 的 DefaultFolder 在计算机上保存后没有坚持打开的 Android 文件夹。现在已根据我的喜好确定了该解决方案,需要使用对话框的人可能会对解决方案感兴趣。
测试代码:
unit Main;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
Winapi.ShlObj, Winapi.KnownFolders, Winapi.ActiveX;
type
TMainForm = class(TForm)
FileOpenDialog1: TFileOpenDialog;
FileSaveDialog1: TFileSaveDialog;
OpenButton: TButton;
SaveButton: TButton;
Memo1: TMemo;
procedure FileOpenDialog1Execute(Sender: TObject);
procedure FileSaveDialog1Execute(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure OpenButtonClick(Sender: TObject);
procedure SaveButtonClick(Sender: TObject);
private
FOpenShellItem: IShellItem;
FSaveShellItem: IShellItem;
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
function GetItemName(ShellItem: IShellItem; const Flags: Cardinal): string;
var
pszItemName: LPCWSTR;
begin
Result := '';
if ShellItem.GetDisplayName(Flags, pszItemName) = S_OK then
begin
Result := pszItemName;
CoTaskMemFree(pszItemName);
end;
end;
procedure TMainForm.FormCreate(Sender: TObject);
var
pch: PChar;
begin
DesktopFont := True;
if SHGetKnownFolderPath(FOLDERID_Pictures, 0, 0, pch) = S_OK then
begin
FileOpenDialog1.DefaultFolder := pch;
FileSaveDialog1.DefaultFolder := pch;
CoTaskMemFree(pch);
end;
end;
procedure TMainForm.OpenButtonClick(Sender: TObject);
var
ShellItem: IShellItem;
ParentItem: IShellItem;
begin
if FileOpenDialog1.Execute(Handle) then
begin
if fdoAllowMultiSelect in FileOpenDialog1.Options then
FileOpenDialog1.ShellItems.GetItemAt(0, ShellItem)
else
ShellItem := FileOpenDialog1.ShellItem;
if ShellItem.GetParent(ParentItem) = S_OK then
FOpenShellItem := ParentItem;
Memo1.Lines.Add('Opened');
Memo1.Lines.Add('ItemParsingName: ' +
GetItemName(ShellItem, SIGDN_DESKTOPABSOLUTEPARSING));
Memo1.Lines.Add('ItemNormalName: ' +
GetItemName(ShellItem, SIGDN_NORMALDISPLAY));
Memo1.Lines.Add('ParentItemParsingName: ' +
GetItemName(ParentItem, SIGDN_DESKTOPABSOLUTEPARSING));
Memo1.Lines.Add('ParentItemNormalame: ' +
GetItemName(ParentItem, SIGDN_NORMALDISPLAY));
Memo1.Lines.Add('----------');
end;
end;
procedure TMainForm.SaveButtonClick(Sender: TObject);
var
ParentItem: IShellItem;
begin
if FileSaveDialog1.Execute(Handle) then
begin
if FileSaveDialog1.ShellItem.GetParent(ParentItem) = S_OK then
FSaveShellItem := FileSaveDialog1.ShellItem;
Memo1.Lines.Add('Saved');
Memo1.Lines.Add('ItemParsingName: ' +
GetItemName(FileSaveDialog1.ShellItem, SIGDN_DESKTOPABSOLUTEPARSING));
Memo1.Lines.Add('ItemNormalName: ' +
GetItemName(FileSaveDialog1.ShellItem, SIGDN_NORMALDISPLAY));
Memo1.Lines.Add('ParentItemParsingName: ' +
GetItemName(ParentItem, SIGDN_DESKTOPABSOLUTEPARSING));
Memo1.Lines.Add('ParentItemNormalName: ' +
GetItemName(ParentItem, SIGDN_NORMALDISPLAY));
Memo1.Lines.Add('----------');
end;
end;
procedure TMainForm.FileOpenDialog1Execute(Sender: TObject);
begin
if Assigned(FOpenShellItem) then
FileOpenDialog1.Dialog.SetFolder(FOpenShellItem);
end;
procedure TMainForm.FileSaveDialog1Execute(Sender: TObject);
begin
//Note the IFileSaveDialog typecast
if Assigned(FSaveShellItem) then
IFileSaveDialog(FileSaveDialog1.Dialog).SetSaveAsItem(FSaveShellItem);
end;
end.
我正在使用 TOpenDialog
(在 Delphi 10.4 中)向用户显示我在他们的文档文件夹中为他们安装的 PDF 文件。在该文件夹中,我创建了一个文件夹 MyFolder10.2
并将 PDF 文件复制到那里。
代码很简单,过去一直有效,即使现在它仍然可以在我较慢的旧 Win10 机器上工作。但是在我更新更快的 Win10 计算机上,它只能在某些时候工作。当它不起作用时,会打开一个文件对话框,但在其他目录中(不确定它来自哪里),并且它不会过滤在 TOpenDialog
中设置的文件类型 (.pdf
)组件。
有什么办法可以追查这个谜团吗?
docPath:= GetEnvironmentVariable('USERPROFILE') + '\Documents\MyFolder10.2\';
OpenDocsDlg.InitialDir := docPath;
OpenDocsDlg.Execute;
在 Vista 之前,TOpenDialog
是 GetOpenFileName()
API 的包装器,其中 TOpenDialog.InitialDir
映射到 OPENFILENAME.lpstrInitialDir
字段。
在 Vista 和更高版本上,TOpenDialog
(通常,取决于配置)包裹 IFileDialog
/IFileOpenDialog
API instead, where TOpenDialog.InitialDir
maps to the IFileDialog.SetFolder()
method (not to IFolderDialog.SetDefaultFolder()
,正如人们所期望的那样。
根据 OPENFILENAME
文档:
lpstrInitialDir
Type:
LPCTSTR
The initial directory. The algorithm for selecting the initial directory varies on different platforms.
Windows 7:
- If
lpstrInitialDir
[TOpenDialog.InitialDir
] has the same value as was passed the first time the application used an Open or Save As dialog box, the path most recently selected by the user is used as the initial directory.- Otherwise, if
lpstrFile
[TOpenDialog.FileName
] contains a path, that path is the initial directory.- Otherwise, if
lpstrInitialDir
is not NULL [TOpenDialog.InitialDir
is not empty], it specifies the initial directory.- If
lpstrInitialDir
is NULL [TOpenDialog.InitialDir
is empty] and the current directory contains any files of the specified filter types, the initial directory is the current directory.- Otherwise, the initial directory is the personal files directory of the current user.
- Otherwise, the initial directory is the Desktop folder.
Windows 2000/XP/Vista:
- If
lpstrFile
[TOpenDialog.FileName
] contains a path, that path is the initial directory.- Otherwise,
lpstrInitialDir
[TOpenDialog.InitialDir
] specifies the initial directory.- Otherwise, if the application has used an Open or Save As dialog box in the past, the path most recently used is selected as the initial directory. However, if an application is not run for a long time, its saved selected path is discarded.
- If
lpstrInitialDir
is NULL [TOpenDialog.InitialDir
is empty] and the current directory contains any files of the specified filter types, the initial directory is the current directory.- Otherwise, the initial directory is the personal files directory of the current user.
- Otherwise, the initial directory is the Desktop folder.
根据 Common Item Dialog 文档:
Controlling the Default Folder
Almost any folder in the Shell namespace can be used as the default folder for the dialog (the folder presented when the user chooses to open or save a file). Call
IFileDialog::SetDefaultFolder
prior to callingShow
[TOpenDialog.Execute()
] to do so.The default folder is the folder in which the dialog starts the first time a user opens it from your application. After that, the dialog will open in the last folder a user opened or the last folder they used to save an item. See State Persistence for more details.
You can force the dialog to always show the same folder when it opens, regardless of previous user action, by calling
IFileDialog::SetFolder
[TOpenDialog.InitialDir
]. However, we do not recommended doing this. If you callSetFolder
before you display the dialog box, the most recent location that the user saved to or opened from is not shown. Unless there is a very specific reason for this behavior, it is not a good or expected user experience and should be avoided. In almost all instances,IFileDialog::SetDefaultFolder
is the better method.When saving a document for the first time in the Save dialog, you should follow the same guidelines in determining the initial folder as you did in the Open dialog. If the user is editing a previously existing document, open the dialog in the folder where that document is stored, and populate the edit box with that document's name. Call
IFileSaveDialog::SetSaveAsItem()
with the current item prior to callingShow
[TOpenDialog.Execute()
].
TOpenDialog
和 TFileOpenDialog
have properties that map to the IFileDialog.SetDefaultFolder()
or IFileDialog.SetSaveAsItem()
methods. However, the TFileOpenDialog.Dialog
属性 都不会让您访问底层 IFileDialog
,因此您可以手动调用这些方法,例如在 TFileOpenDialog.OnExecute
事件.
@Remy Lebeau 的详细回答促使我再次尝试解决一个有问题的常见场景,尽管我尝试了很多次,但我以前没有设法解决这个问题: 我有一个带有 TFileOpenDialog 和 TFileSaveDialog 的图像编辑 VCL 应用程序,通常用于在连接的 Android 设备上一个接一个地编辑一堆屏幕截图,这些屏幕截图保存在台式计算机上。 问题是 FileOpenDialog 的 DefaultFolder 在计算机上保存后没有坚持打开的 Android 文件夹。现在已根据我的喜好确定了该解决方案,需要使用对话框的人可能会对解决方案感兴趣。
测试代码:
unit Main;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
Winapi.ShlObj, Winapi.KnownFolders, Winapi.ActiveX;
type
TMainForm = class(TForm)
FileOpenDialog1: TFileOpenDialog;
FileSaveDialog1: TFileSaveDialog;
OpenButton: TButton;
SaveButton: TButton;
Memo1: TMemo;
procedure FileOpenDialog1Execute(Sender: TObject);
procedure FileSaveDialog1Execute(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure OpenButtonClick(Sender: TObject);
procedure SaveButtonClick(Sender: TObject);
private
FOpenShellItem: IShellItem;
FSaveShellItem: IShellItem;
public
{ Public declarations }
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
function GetItemName(ShellItem: IShellItem; const Flags: Cardinal): string;
var
pszItemName: LPCWSTR;
begin
Result := '';
if ShellItem.GetDisplayName(Flags, pszItemName) = S_OK then
begin
Result := pszItemName;
CoTaskMemFree(pszItemName);
end;
end;
procedure TMainForm.FormCreate(Sender: TObject);
var
pch: PChar;
begin
DesktopFont := True;
if SHGetKnownFolderPath(FOLDERID_Pictures, 0, 0, pch) = S_OK then
begin
FileOpenDialog1.DefaultFolder := pch;
FileSaveDialog1.DefaultFolder := pch;
CoTaskMemFree(pch);
end;
end;
procedure TMainForm.OpenButtonClick(Sender: TObject);
var
ShellItem: IShellItem;
ParentItem: IShellItem;
begin
if FileOpenDialog1.Execute(Handle) then
begin
if fdoAllowMultiSelect in FileOpenDialog1.Options then
FileOpenDialog1.ShellItems.GetItemAt(0, ShellItem)
else
ShellItem := FileOpenDialog1.ShellItem;
if ShellItem.GetParent(ParentItem) = S_OK then
FOpenShellItem := ParentItem;
Memo1.Lines.Add('Opened');
Memo1.Lines.Add('ItemParsingName: ' +
GetItemName(ShellItem, SIGDN_DESKTOPABSOLUTEPARSING));
Memo1.Lines.Add('ItemNormalName: ' +
GetItemName(ShellItem, SIGDN_NORMALDISPLAY));
Memo1.Lines.Add('ParentItemParsingName: ' +
GetItemName(ParentItem, SIGDN_DESKTOPABSOLUTEPARSING));
Memo1.Lines.Add('ParentItemNormalame: ' +
GetItemName(ParentItem, SIGDN_NORMALDISPLAY));
Memo1.Lines.Add('----------');
end;
end;
procedure TMainForm.SaveButtonClick(Sender: TObject);
var
ParentItem: IShellItem;
begin
if FileSaveDialog1.Execute(Handle) then
begin
if FileSaveDialog1.ShellItem.GetParent(ParentItem) = S_OK then
FSaveShellItem := FileSaveDialog1.ShellItem;
Memo1.Lines.Add('Saved');
Memo1.Lines.Add('ItemParsingName: ' +
GetItemName(FileSaveDialog1.ShellItem, SIGDN_DESKTOPABSOLUTEPARSING));
Memo1.Lines.Add('ItemNormalName: ' +
GetItemName(FileSaveDialog1.ShellItem, SIGDN_NORMALDISPLAY));
Memo1.Lines.Add('ParentItemParsingName: ' +
GetItemName(ParentItem, SIGDN_DESKTOPABSOLUTEPARSING));
Memo1.Lines.Add('ParentItemNormalName: ' +
GetItemName(ParentItem, SIGDN_NORMALDISPLAY));
Memo1.Lines.Add('----------');
end;
end;
procedure TMainForm.FileOpenDialog1Execute(Sender: TObject);
begin
if Assigned(FOpenShellItem) then
FileOpenDialog1.Dialog.SetFolder(FOpenShellItem);
end;
procedure TMainForm.FileSaveDialog1Execute(Sender: TObject);
begin
//Note the IFileSaveDialog typecast
if Assigned(FSaveShellItem) then
IFileSaveDialog(FileSaveDialog1.Dialog).SetSaveAsItem(FSaveShellItem);
end;
end.