Search code examples
delphivcl

Remove and Replace a visual component at runtime


Is it possible to, for instance, replace and free a TEdit with a subclassed component instantiated (conditionally) at runtime? If so, how and when it should be done? I've tried to set the parent to nil and to call free() in the form constructor and AfterConstruction methods but in both cases I got a runtime error.


Being more specific, I got an Access violation error (EAccessViolation). It seems François is right when he says that freeing components at frame costruction messes with Form controls housekeeping.


Solution

  • This more generic routine works either with a Form or Frame (updated to use a subclass for the new control):

    function ReplaceControlEx(AControl: TControl; const AControlClass: TControlClass; const ANewName: string; const IsFreed : Boolean = True): TControl;
    begin
      if AControl = nil then
      begin
        Result := nil;
        Exit;
      end;
      Result := AControlClass.Create(AControl.Owner);
      CloneProperties(AControl, Result);// copy all properties to new control
      // Result.Left := AControl.Left;   // or copy some properties manually...
      // Result.Top := AControl.Top;
      Result.Name := ANewName;
      Result.Parent := AControl.Parent; // needed for the InsertControl & RemoveControl magic
      if IsFreed then
        FreeAndNil(AControl);
    end;
    
    function ReplaceControl(AControl: TControl; const ANewName: string; const IsFreed : Boolean = True): TControl;
    begin
      if AControl = nil then
        Result := nil
      else
        Result := ReplaceControlEx(AControl, TControlClass(AControl.ClassType), ANewName, IsFreed);
    end;
    

    using this routine to pass the properties to the new control

    procedure CloneProperties(const Source: TControl; const Dest: TControl);
    var
      ms: TMemoryStream;
      OldName: string;
    begin
      OldName := Source.Name;
      Source.Name := ''; // needed to avoid Name collision
      try
        ms := TMemoryStream.Create;
        try
          ms.WriteComponent(Source);
          ms.Position := 0;
          ms.ReadComponent(Dest);
        finally
          ms.Free;
        end;
      finally
        Source.Name := OldName;
      end;
    end;
    

    use it like:

    procedure TFrame1.AfterConstruction;
    var
      I: Integer;
      NewEdit: TMyEdit;
    begin
      inherited;
      NewEdit := ReplaceControlEx(Edit1, TMyEdit, 'Edit2') as TMyEdit;
      if Assigned(NewEdit) then
      begin
        NewEdit.Text := 'My Brand New Edit';
        NewEdit.Author := 'Myself';
      end;
      for I:=0 to ControlCount-1 do
      begin
        ShowMessage(Controls[I].Name);
      end;
    end;
    

    CAUTION: If you are doing this inside the AfterConstruction of the Frame, beware that the hosting Form construction is not finished yet.
    Freeing Controls there, might cause a lot of problems as you're messing up with Form controls housekeeping.
    See what you get if you try to read the new Edit Caption to display in the ShowMessage...
    In that case you would want to use
    ...ReplaceControl(Edit1, 'Edit2', False)
    and then do a
    ...FreeAndNil(Edit1)
    later.