delphitopendialog

Delphi OpenDialog filename display problem


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.


Solution

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