delphifocusfiremonkeydelphi-xe5

Proper way to change focus of TEdits Delphi Xe5


I've searched around and the general answer seems to place

SomeEdit2.setFocus;

in SomeEdit1.OnExit event. I have tried this (Using Delphi Xe5, developing for iOS) and it causes the application to crash. The app does not throw an error, it just blanks out and crashes. I've tried placing the same code in other events but it does not work as expected. For example, when placed in SomeEdit1.OnChange event, when a user hits 'done' on the virtual keyboard - Focus is switched to the desired control, but the keyboard does not show and stops working properly.

What is the proper way to change focus inbetween controls when a user hits the 'done' button provided on the virtual keyboard?


Solution

  • You can not compare VCL-Control behaviour with FMX-Control behaviour, because sometimes they behave different - they should not, but they do.

    In VCL you have an OnExit event and it occurs right after the focus has left the control. So this is an OnAfterExit event.

    In FMX the OnExit event is fired before the focus gets away. So this is an OnBeforeExit.

    procedure TControl.DoExit;
    begin
      if FIsFocused then
      begin
        try
          if CanFocus and Assigned(FOnExit) then
            FOnExit(Self);
          FIsFocused := False;
    

    Now, what has this to do with your current problem?

    If you set the focus to another control inside the OnExit event, the current active control DoExit method gets called, which calls the OnExit event, and you have a perfect circle.

    So you have several options to fix this

    Bug Report

    The best solution is to create a bug report and let emba fix this.

    There is already a bug report 117752 with the same reason. So I posted the solution as a comment.

    Patch FMX.Controls.pas

    Copy FMX.Controls into your project source directory and patch the buggy code (just one line)

    procedure TControl.DoExit;
    begin
      if FIsFocused then
      begin
        try
          FIsFocused := False; // thats the place to be, before firering OnExit event 
          if CanFocus and Assigned(FOnExit) then
            FOnExit(Self);
          //FIsFocused := False; <-- buggy here
    

    SetFocus to control

    To set the focus in the OnExit you have to do some more work, because the message to change the focus to the next control is already queued. You must ensure that the focus change to the desired control take place after that already queued focus change message. The simplest approach is using a timer.

    Here is an example FMX form with 3 edit controls and each of them has an OnExit event

    unit MainForm;
    
    interface
    
    uses
      System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
      FMX.Types, FMX.Graphics, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.StdCtrls,
      FMX.Edit;
    
    type
      TForm1 = class(TForm)
        Edit1: TEdit;
        Edit2: TEdit;
        Edit3: TEdit;
        EnsureActiveControl_Timer: TTimer;
        procedure EnsureActiveControl_TimerTimer(Sender: TObject);
        procedure Edit1Exit(Sender: TObject);
        procedure Edit2Exit(Sender: TObject);
        procedure Edit3Exit(Sender: TObject);
      private
        // locks the NextActiveControl property to prevent changes while performing the timer event 
        FTimerSwitchInProgress: Boolean;
        FNextActiveControl: TControl;
        procedure SetNextActiveControl(const Value: TControl);
      protected
        property NextActiveControl: TControl read FNextActiveControl write SetNextActiveControl;
      public
    
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.fmx}
    
    procedure TForm1.Edit1Exit(Sender: TObject);
    begin
      NextActiveControl := Edit3;
    end;
    
    procedure TForm1.Edit2Exit(Sender: TObject);
    begin
      NextActiveControl := Edit1;
    end;
    
    procedure TForm1.Edit3Exit(Sender: TObject);
    begin
      NextActiveControl := Edit2;
    end;
    
    procedure TForm1.EnsureActiveControl_TimerTimer(Sender: TObject);
    begin
      EnsureActiveControl_Timer.Enabled := False;
      FTimerSwitchInProgress := True;
      try
        if (Self.ActiveControl <> NextActiveControl) and NextActiveControl.CanFocus then
          NextActiveControl.SetFocus;
      finally
        FTimerSwitchInProgress := False;
      end;
    end;
    
    procedure TForm1.SetNextActiveControl(const Value: TControl);
    begin
      if FTimerSwitchInProgress 
      or (FNextActiveControl = Value) 
      or (Assigned(Value) and not Value.CanFocus) 
      or (Self.ActiveControl = Value) 
      then
        Exit;
    
      FNextActiveControl := Value;
      EnsureActiveControl_Timer.Enabled := Assigned(FNextActiveControl);
    end;
    
    end.