1

Put an OpenDialog component on a new form and this code in OnCreate

opendialog1.filename:='This is a long filename.txt';
opendialog1.execute;

When the app runs the dialog appears with the filename displayed in the opendialog, but the filename is highlighted and scrolled to the right (even though there is plenty of room to show the full filename).

All it shows is "ng filename.txt" that has been highlighted.

Is there a way I can "unhighlight" the filename and get the text scrolled back to the left so it shows the full name "This is a long filename.txt"?

The problem would be fixed if I could simulate/push a press of the home key into the OpenDialog once it is shown, but none of the following options seem to work.

keybd_event(VK_HOME, 0, 0, 0);
keybd_event(VK_HOME, 0, KEYEVENTF_KEYUP, 0);

or

input.Itype := INPUT_KEYBOARD;
input.ki.wVk := 47;
input.ki.wScan := 47;
input.ki.dwFlags := 0;
input.ki.time := 0;
input.ki.dwExtraInfo := 0;
SendInput(1, input, sizeof(input));
input.ki.dwFlags := KEYEVENTF_KEYUP;
SendInput(1, input, sizeof(input));

or

PostMessage(GetParent(OpenDialog1.Handle), WM_KEYDOWN, VK_HOME, 0);
PostMessage(GetParent(OpenDialog1.Handle), WM_KEYUP, VK_HOME, 0);

If I put those snippets of code before openDialog1.execute it does seem to work on my PC, but is a bad idea as the dialog is not open yet and so may not receive the key press message(s).

Trying all of those methods after the opendialog1.execute call don't seem to do anything.

Some1Else
  • 715
  • 11
  • 26
  • This is a well-known issue. – Andreas Rejbrand Mar 08 '21 at 23:00
  • Are there any well known fixes? – Some1Else Mar 08 '21 at 23:06
  • Not that I am aware of. Cannot say I have search for any, though. – Andreas Rejbrand Mar 08 '21 at 23:08
  • Why do you want to set the filename at all? Isn't the user supposed to choose a file to open!? – Delphi Coder Mar 09 '21 at 07:36
  • It is an indication of which file was last loaded. One of a bunch of files is loaded at random when the app starts. Showing the filename lets the user know which of the files was auto-loaded. – Some1Else Mar 09 '21 at 07:46
  • @DelphiCoder: IIRC, the same issue applies to *save* dialogs, and in this case it is very common that a default file name is suggested. For instance, many text editors and word processors use (part of) the first line of text as the default file name. – Andreas Rejbrand Mar 09 '21 at 07:53
  • @AndreasRejbrand Actually save dialog does not have this same issue. Long filename is shown just fine in a save dialog. And by the way this issue does not originate from Delphi but it also present in pretty much any other language that is wrapping the new windows common file dialog. – SilverWarior Mar 09 '21 at 12:13
  • @SilverWarior: That appears to be correct. – Andreas Rejbrand Mar 09 '21 at 13:50
  • @Some1Else: "Trying all of those methods after the opendialog1.execute call don't seem to do anything." That's because the `Execute` function doesn't return until the dialog is closed. (Otherwise it would be impossible to read the chosen file name the normal way we do it!) – Andreas Rejbrand Mar 09 '21 at 13:51
  • This is the core issue: https://stackoverflow.com/questions/60182612/default-filename-appears-truncated-in-windows-ifiledialog – Andreas Rejbrand Mar 09 '21 at 13:52

1 Answers1

3

The suggested solution

keybd_event(VK_HOME, 0, 0, 0);
keybd_event(VK_HOME, 0, KEYEVENTF_KEYUP, 0);

is not safe, no matter when you execute it.

What if I press Ctrl+O in the app and switch to a different app while waiting for the dialog to show?

Then this other app will receive the HOME key. Then anything can happen. The other app might be displaying a track bar controlling the rate of flow of a patient's IV drugs, and that HOME key might set the trackbar to 0 cc/min.

More likely: you lose the selection in an Explorer window (possibly containing a thousand images), the caret pos in a document (located at a very particular place), the selected node in a tree view, etc. Or your media player restarts the current track.

And yes, many people (such as myself) really do multitask in that way!


This is a possible (safe) solution. I don't claim that it is the most elegant one, though.

The approach is as follows:

  1. I use the OnSelectionChange event to get a chance to run some code after the dialog window has been created. However, I must manually make sure that I only run my "file name fix" the very first time this event is fired. I also make sure only to run this code if the (default) file name is non-empty.

  2. The first time the event is fired, I find the edit box inside it. I "know" it is the right control because (1) it is an edit box and (2) its text is equal to the (default) file name (which is not empty).

  3. I then specifically instruct this edit box to move its caret to the first character, and then to (re)select every character.

Full code:

type
  TOpenDialogFileNameEditData = class
    FileName: string;
    Handle: HWND;
  end;

function EnumChildProc(h: HWND; lp: LPARAM): BOOL; stdcall;
var
  WndClass, WndTxt: array[0..1024] of Char;
begin
  Result := True;
  FillChar(WndClass, SizeOf(WndClass), 0);
  FillChar(WndTxt, SizeOf(WndTxt), 0);
  if GetClassName(h, WndClass, Length(WndClass)) <> 0 then
  begin
    if SameText(WndClass, 'Edit') then
    begin
      if GetWindowText(h, WndTxt, Length(WndTxt)) <> 0 then
      begin
        if WndTxt = TOpenDialogFileNameEditData(lp).FileName then
        begin
          TOpenDialogFileNameEditData(lp).Handle := h;
          Exit(False);
        end;
      end;
    end;
  end;
end;

procedure TForm1.FODE(Sender: TObject);
begin
  if Sender is TOpenDialog then
  begin
    var OpenDialog := TOpenDialog(Sender);
    if OpenDialog.Tag <> 0 then
      Exit;
    OpenDialog.Tag := 1;
    var Data := TOpenDialogFileNameEditData.Create;
    try
      Data.FileName := ExtractFileName(OpenDialog.FileName);
      if Data.FileName.IsEmpty then
        Exit;
      if OpenDialog.Handle <> 0 then
      begin
        EnumChildWindows(OpenDialog.Handle, @EnumChildProc, NativeInt(Data));
        if Data.Handle <> 0 then
        begin
          SendMessage(Data.Handle, EM_SETSEL, 0,  0); // set caret at first char
          SendMessage(Data.Handle, EM_SETSEL, 0, -1); // (re)select all
        end;
      end;
    finally
      Data.Free;
    end;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  with TOpenDialog.Create(nil) do
    try
      FileName := 'This is the story of a horse that met a cat that met a dog the day before.txt';
      OnSelectionChange := FODE;
      Execute
    finally
      Free;
    end;
end;

Before:

Screenshot of unfixed open dialog. The file name reads "e day before.txt".

After:

Screenshot of fixed open dialog. The file name is now fully visible.

Update

Upon request, I have made an easily reusable unit and function to apply this fix.

Here is the complete unit:

unit OpenDialogUpgrader;

interface

uses
  Windows, Messages, SysUtils, Types, Dialogs;

procedure FixOpenDialog(AOpenDialog: TOpenDialog);

implementation

type
  TOpenDialogFileNameEditData = class
    FileName: string;
    Handle: HWND;
    class procedure DialogSelectionChange(Sender: TObject);
  end;

procedure FixOpenDialog(AOpenDialog: TOpenDialog);
begin
  AOpenDialog.Tag := 0;
  AOpenDialog.OnSelectionChange := TOpenDialogFileNameEditData.DialogSelectionChange;
end;

{ TOpenDialogFileNameEditData }

function EnumChildProc(h: HWND; lp: LPARAM): BOOL; stdcall;
var
  WndClass, WndTxt: array[0..1024] of Char;
begin
  Result := True;
  FillChar(WndClass, SizeOf(WndClass), 0);
  FillChar(WndTxt, SizeOf(WndTxt), 0);
  if GetClassName(h, WndClass, Length(WndClass)) <> 0 then
  begin
    if SameText(WndClass, 'Edit') then
    begin
      if GetWindowText(h, WndTxt, Length(WndTxt)) <> 0 then
      begin
        if WndTxt = TOpenDialogFileNameEditData(lp).FileName then
        begin
          TOpenDialogFileNameEditData(lp).Handle := h;
          Exit(False);
        end;
      end;
    end;
  end;
end;


class procedure TOpenDialogFileNameEditData.DialogSelectionChange(Sender: TObject);
begin
  if Sender is TOpenDialog then
  begin
    var OpenDialog := TOpenDialog(Sender);
    if OpenDialog.Tag <> 0 then
      Exit;
    OpenDialog.Tag := 1;
    var Data := TOpenDialogFileNameEditData.Create;
    try
      Data.FileName := ExtractFileName(OpenDialog.FileName);
      if Data.FileName.IsEmpty then
        Exit;
      if OpenDialog.Handle <> 0 then
      begin
        EnumChildWindows(OpenDialog.Handle, @EnumChildProc, NativeInt(Data));
        if Data.Handle <> 0 then
        begin
          SendMessage(Data.Handle, EM_SETSEL, 0,  0); // set caret at first char
          SendMessage(Data.Handle, EM_SETSEL, 0, -1); // (re)select all
        end;
      end;
    finally
      Data.Free;
    end;
  end;
end;

end.

To use this unit and function in your own unit X, just add the unit (OpenDialogUpgrader) to your X unit's implementation section uses clause and change your standard

var OpenDialog := TOpenDialog.Create(nil);
try
  OpenDialog.FileName := 'This is the story of a horse that met a cat that met a dog the day before.txt';
  OpenDialog.Execute;
finally
  OpenDialog.Free;
end;

into

var OpenDialog := TOpenDialog.Create(nil);
try
  OpenDialog.FileName := 'This is the story of a horse that met a cat that met a dog the day before.txt';
  FixOpenDialog(OpenDialog); // <-- just call this prior to Execute
  OpenDialog.Execute;
finally
  OpenDialog.Free;
end;

Of course, if your application has 73 open dialogs in 20 different units, you only need a single copy of this OpenDialogUpgrader unit, but you need to add it to the implementation section uses clause in each of your 20 units. And you need to call FixOpenDialog before each TOpenDialog.Execute.

Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384
  • Simply putting keybd_event(VK_HOME, 0, 0, 0); keybd_event(VK_HOME, 0, KEYEVENTF_KEYUP, 0); inside the current OpenDialog1.OpenDialog1FolderChange event seems to fix the issue without needing a new class. Is there any possible issues I am not seeing here? It works to fix my issue in my application. – Some1Else Mar 09 '21 at 16:22
  • @Some1Else: That's not safe. What if I press Ctrl+O in my app and switch to a different app while waiting for the dialog to show? Then this other app will receive the HOME key. Then anything can happen. The other app might be displaying a track bar controlling the rate of flow of a patient's IV drugs, and that HOME key might set the trackbar to 0 cc/min. (More likely: you lose the selection in an Explorer window, the caret pos in a document, the selected node in a tree view, etc.) – Andreas Rejbrand Mar 09 '21 at 17:54
  • What about if I use SetForegroundWindow(OpenDialog1.handle); just before the keyb_event? Or, altyernatively, why doesn't SendMessage(OpenDialog1.Handle, WM_KEYDOWN, VK_HOME, 0); work? That would ensure the key down goes directly to the opendialog and not another window or application. – Some1Else Mar 09 '21 at 18:51
  • (1) `SetForegroundWindow` will not work. If a different app has keyboard focus, Windows will not let your app steal the focus. (2) `SendMessage(OpenDialog1.Handle, ...)` doesn't work because this will send the message to the open dialog window itself, and not the edit box window. Recall that every control in a dialog box is a window itself. You need to send the message to the file name edit box window. – Andreas Rejbrand Mar 09 '21 at 19:08
  • OK, last hope. Is there a way to not have to create the new class? Simpler single function to find the opendialog handle, then the edit handle, then sendmessage to the edit. Code I can self contain within the OpenDialog1.OnSelectionChange event procedure. – Some1Else Mar 09 '21 at 19:31
  • @Some1Else: Yes, it is definitely possible to put this into a neater and easily reusable package. Let's see if I manage to do that before I have to do the groceries. – Andreas Rejbrand Mar 09 '21 at 19:32
  • @Some1Else: Done! – Andreas Rejbrand Mar 09 '21 at 19:45