1

In a 32-bit VCL Application in Windows 10 in Delphi 11 Alexandria, I am trying to write a small app that finds the list of ANCESTORS from a class name that the user inputs in an Edit box:

procedure TForm1.DoShowAncestors(const aClassName: string);
var
  ClassRef: TClass;
begin
   lstAncestors.Clear;

   // Does not work:
   //ClassRef := TClass.Create;
   //ClassRef.ClassName := aClassName;

   // [dcc32 Error] E2076 This form of method call only allowed for class methods or constructor:
   ClassRef := TClass(aClassName).ClassType;

   while ClassRef <> nil do
   begin
     lstAncestors.Items.Add(ClassRef.ClassName);
     ClassRef := ClassRef.ClassParent;
   end;
end;

procedure TForm1.edtClassNameKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if Key = VK_RETURN then
  begin
    DoShowAncestors(Trim(edtClassName.Text));
  end;
end;

However, the problem is to transform the input string into a TClass object. See the above error comments.

user1580348
  • 5,721
  • 4
  • 43
  • 105
  • 1
    Actually, this Q has been asked several times before: https://stackoverflow.com/questions/29471798/get-class-by-its-name-in-delphi, https://stackoverflow.com/questions/29471798/get-class-by-its-name-in-delphi – Andreas Rejbrand Jul 17 '22 at 14:42

2 Answers2

4

Since Delphi is a compiled language, obtaining a class (or object) by name is not a natural operation, but requires some kind of framework.

Fortunately, modern RTTI (uses RTTI) can easily handle this for you:

procedure ShowAncestors(const AClass: string);
begin

  var Ctx := TRttiContext.Create;
  try
    var LType := Ctx.FindType(AClass);
    if LType is TRttiInstanceType then
    begin
      var R := TRttiInstanceType(LType).MetaclassType;
      while Assigned(R) do
      begin
        ShowMessage(R.ClassName);
        R := R.ClassParent;
      end;
    end;
  finally
    Ctx.Free; // actually, just to make the code "look" right!
  end;

end;

Try it with

ShowAncestors('Vcl.Forms.TForm')

for instance.

(Of course, this only works for classes actually included in the final EXE.)

Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384
  • As this only works for classes included in the final EXE, and I need to get the ancestors from ANY class name, how can I get the ancestors from any class name? – user1580348 Jul 17 '22 at 14:53
  • 1
    @user1580348: You can make sure to "mention" every class you want to be included: https://stackoverflow.com/a/10613212/282848. Other than that, there is no way. It is mathematically impossible to obtain a class reference to a class that doesn't exist! It is even more impossible to obtain such a class reference by name, since the name isn't included in the EXE, either. But of course you can compile your own RTL and VCL class hierarchy tree by hand and include it as a (text?) resource and parse that at run-time to get the required info (but obv. not actual metaclasses). – Andreas Rejbrand Jul 17 '22 at 14:57
  • Could a `ShowAncestors` routine running INSIDE THE IDE (e.g., as an addon/expert) access all types known to the IDE? – user1580348 Jul 17 '22 at 18:22
  • @user1580348: Don't know much about those, but probably not. It seems to me like you need a Pascal parser that you can feed the RTL/VCL source code. – Andreas Rejbrand Jul 17 '22 at 19:04
  • Well, I should mention one thing: With the `{$STRONGLINKTYPES ON}` directive in your *.dpr, even unused classes will be included in the EXE. However, it is still true that classes that are present in units not `used` will be absent, so you still need to add all your units to the program! But at least you don't need to "mention" every class in those units. Of course, your EXE will become very big (and maybe slow) if you include everything in it. – Andreas Rejbrand Jul 17 '22 at 20:04
  • Andreas, this doesn't seem to work: https://www.screencast.com/t/LhM5KJMR83b – user1580348 Jul 18 '22 at 17:56
  • @user1580348: That's too far down. Put it on the second line, immediately after `program myapp;` – Andreas Rejbrand Jul 18 '22 at 19:26
0

Now there is no more need to enter a fully qualified class name, and now there is a visual feedback validation of the class name in the edit:

enter image description here

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.Classes, Vcl.StdCtrls,
  Vcl.ExtCtrls;

type
  TForm1 = class(TForm)
    edtClassName: TEdit;
    lstAncestors: TListBox;
    pnlEdit: TPanel;
    procedure edtClassNameChange(Sender: TObject);
    procedure edtClassNameKeyDown(Sender: TObject; var Key: Word; Shift:
        TShiftState);
    procedure edtClassNameKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormActivate(Sender: TObject);
  private
    FDontDoIt: Boolean;
    function CheckEmptyEdit: Boolean;
    procedure DoShowAncestors(const aClassName: string);
    function GetMatchingTypeName: string;
    procedure SetEditBorder;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses
  System.StrUtils,
  System.RTTI;

function FindMyClass(const aName: string): TClass;
var
  ctx: TRttiContext;
  ThisType: TRttiType;
  ThisList: TArray<TRttiType>;
  FPos: Integer;
begin
  Result := nil;
  ctx := TRttiContext.Create;
  try
    ThisList := ctx.GetTypes;
    for ThisType in ThisList do
    begin
      if ThisType.IsInstance and (EndsText(aName, ThisType.Name)) then
      begin
        Result := ThisType.AsInstance.MetaClassType;
        BREAK;
      end;
    end;
  finally
    ctx.Free;
  end;
end;

procedure TForm1.edtClassNameChange(Sender: TObject);
var
  ctx: TRttiContext;
  ThisType: TRttiType;
  ThisList: TArray<TRttiType>;
  InputStr: string;
  FPos: Integer;
begin
  if CheckEmptyEdit then
    EXIT;

  if FDontDoIt then
  begin
    FDontDoIt := False;
    EXIT;
  end;

  FPos := edtClassName.SelStart;

  var ThisMatchingTypeName := GetMatchingTypeName;
  FDontDoIt := True;
  try
    if ThisMatchingTypeName <> '' then
      edtClassName.Text := ThisMatchingTypeName;
  finally
    FDontDoIt := False;
  end;

  SetEditBorder;

  if pnlEdit.Color <> clRed then
  begin
    edtClassName.SelStart :=  FPos;
    edtClassName.SelLength := Length(ThisMatchingTypeName) - FPos;
  end;
end;

procedure TForm1.SetEditBorder;
begin
  if FindMyClass(Trim(edtClassName.Text)) = nil then
  begin
    pnlEdit.Color := clRed;
    lstAncestors.Clear;
  end
  else
    pnlEdit.Color := clGreen;
end;

function TForm1.GetMatchingTypeName: string;
var
  ctx: TRttiContext;
  ThisType: TRttiType;
  ThisList: TArray<TRttiType>;
  InputStr: string;
begin
  Result := '';
  InputStr := Trim(edtClassName.Text);
  if InputStr = '' then EXIT;
  ctx := TRttiContext.Create;
  try
    ThisList := ctx.GetTypes;
    for ThisType in ThisList do
    begin
      if ThisType.IsInstance and (StartsText(InputStr, ThisType.Name)) then
      begin
        Result := ThisType.Name;
        BREAK;
      end;
    end;
  finally
    ctx.Free;
  end;
end;

procedure TForm1.DoShowAncestors(const aClassName: string);
var
  ClassRef: TClass;
begin
   lstAncestors.Items.BeginUpdate;
   try
     lstAncestors.Clear;

     ClassRef := FindMyClass(aClassName);

     while ClassRef <> nil do
     begin
       lstAncestors.Items.Add(ClassRef.ClassName);
       ClassRef := ClassRef.ClassParent;
     end;
   finally
     lstAncestors.Items.EndUpdate;
   end;
end;

procedure TForm1.edtClassNameKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  case Key of
    VK_BACK: FDontDoIt := True;
    VK_DELETE: FDontDoIt := True;
  end;
end;

procedure TForm1.edtClassNameKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  case Key of
    VK_RETURN: DoShowAncestors(Trim(edtClassName.Text));
    VK_BACK:
      begin
        FDontDoIt := False;
        SetEditBorder;
        CheckEmptyEdit;
      end;
    VK_DELETE:
      begin
        FDontDoIt := False;
        SetEditBorder;
        CheckEmptyEdit;
      end;
  end;
end;

function TForm1.CheckEmptyEdit: Boolean;
begin
  Result := False;
  if Trim(edtClassName.Text) = '' then
  begin
    pnlEdit.Color := clGray;
    lstAncestors.Clear;
    Result := True;
  end;
end;

procedure TForm1.FormActivate(Sender: TObject);
begin
  edtClassName.SetFocus;
end;

end.

And here is the DFM:

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Show Class Ancestors'
  ClientHeight = 300
  ClientWidth = 434
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -15
  Font.Name = 'Segoe UI'
  Font.Style = []
  Position = poScreenCenter
  ShowHint = True
  OnActivate = FormActivate
  PixelsPerInch = 120
  TextHeight = 20
  object lstAncestors: TListBox
    AlignWithMargins = True
    Left = 16
    Top = 55
    Width = 402
    Height = 229
    Margins.Left = 16
    Margins.Top = 16
    Margins.Right = 16
    Margins.Bottom = 16
    Align = alClient
    ItemHeight = 20
    TabOrder = 0
    ExplicitTop = 60
    ExplicitHeight = 224
  end
  object pnlEdit: TPanel
    AlignWithMargins = True
    Left = 16
    Top = 16
    Width = 402
    Height = 23
    Margins.Left = 16
    Margins.Top = 16
    Margins.Right = 16
    Margins.Bottom = 0
    Align = alTop
    BevelOuter = bvNone
    Caption = 'pnlEdit'
    Color = clGray
    ParentBackground = False
    TabOrder = 1
    object edtClassName: TEdit
      AlignWithMargins = True
      Left = 1
      Top = 1
      Width = 400
      Height = 21
      Hint = 'Enter a known Class Name and then press the Enter/Return key.'
      Margins.Left = 1
      Margins.Top = 1
      Margins.Right = 1
      Margins.Bottom = 1
      Align = alClient
      BorderStyle = bsNone
      TabOrder = 0
      OnChange = edtClassNameChange
      OnKeyDown = edtClassNameKeyDown
      OnKeyUp = edtClassNameKeyUp
      ExplicitLeft = 0
      ExplicitTop = 0
      ExplicitWidth = 402
      ExplicitHeight = 28
    end
  end
end
user1580348
  • 5,721
  • 4
  • 43
  • 105
  • You need `BeginUpdate..EndUpdate` (protected with `try..finally`). – Andreas Rejbrand Jul 17 '22 at 15:17
  • I have added the `BeginUpdate..EndUpdate`. But for what do I need it? – user1580348 Jul 17 '22 at 15:28
  • Create a new VCL app, drop a `TListBox` and a `TButton` on the main form, and add the following code on `TButton.OnClick`: `ListBox1.Clear; for var i := 1 to 10000 do ListBox1.Items.Add(i.ToString);` Run the program and click the button. It takes 2 seconds on my computer to fill the list box. Now add `BeginUpdate..EndUpdate`. On my computer, it now takes 0.046 seconds. That's close to a factor 50. – Andreas Rejbrand Jul 17 '22 at 15:51
  • Fortunately, Delphi classes usually do not have more than 999 ancestors. – user1580348 Jul 17 '22 at 15:56
  • 2
    No, but it is a good habit always to use this pattern when you populate a `TStrings`. And even at 1000 items, there is a noticeable difference. (And you save energy, battery life, etc.!) – Andreas Rejbrand Jul 17 '22 at 15:59
  • Not to speak of computers which (for any valid reason) just are very slow - it could still be a 20 years old CPU. – AmigoJack Jul 17 '22 at 16:31