为什么 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 之前,TOpenDialogGetOpenFileName() 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:

  1. 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.
  2. Otherwise, if lpstrFile [TOpenDialog.FileName] contains a path, that path is the initial directory.
  3. Otherwise, if lpstrInitialDir is not NULL [TOpenDialog.InitialDir is not empty], it specifies the initial directory.
  4. 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.
  5. Otherwise, the initial directory is the personal files directory of the current user.
  6. Otherwise, the initial directory is the Desktop folder.

Windows 2000/XP/Vista:

  1. If lpstrFile [TOpenDialog.FileName] contains a path, that path is the initial directory.
  2. Otherwise, lpstrInitialDir [TOpenDialog.InitialDir] specifies the initial directory.
  3. 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.
  4. 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.
  5. Otherwise, the initial directory is the personal files directory of the current user.
  6. 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()].

TOpenDialogTFileOpenDialog 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.