Delphi OpenDialog filename display problem

1.3k Views Asked by At

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.

1

There are 1 best solutions below

7
On BEST ANSWER

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.