TechTricks
Technical answers from the trenches 
 
 
 
 

     
   
Obtaining Names of Enumerated Values
 
   
 Posted: 23 April 2003
 
   
 
 Applies to: Delphi 2 and later
 
   
 
Audience: Intermediate
 
       
   

Question: Is there a way to convert the value of an enumerated type back to its name?

Answer: Yes, using Delphi's Runtime Type Information (RTTI). Specifically, the GetEnumName() function from the TYPINFO unit returns the name of a enumerated type constant as a string. It requires two parameters, a pointer to the enumerated type containing the constant and an integer value corresponding to the constant's value.

This article demonstrates GetEnumName() in some detail. It shows how to:

  • Return the names of all constants in an enumerated set
  • Use GetEnumName() to create clearer documentation
  • Write safety nets into your enumerated type processing code
  • Resolve common errors encountered with GetEnumName()

Returning the name of an enumerated constant

To illustrate, consider the following code sample. It populates a memo with the names of the default AVI files supported by the TAnimate component:

uses
   ComCtrls,
   TypInfo;
// ...

procedure TForm1.Button1Click(Sender: TObject);
var
   si  : SmallInt;
   str : String;

begin
   with Memo1.Lines do
   begin
      Clear;
      for si := ord( low( TCommonAVI ) ) to
                ord( high( TCommonAVI ) ) do
      begin
         str := getEnumName( typeInfo( tCommonAVI ), si );
         add( format( '%2d = %s', [ si, str ] ) );
      end;
   end;
end;

Using GetEnumName to create clearer documentation

GetEnumName() also provides interesting opportunities for creating clearer documentation routines. For example, consider the following procedure taken from a DataModule from one of our recent applications:
uses
   TypInfo;
// ...

function TdmMainData.getStructures: TStrings;
var
   siCom,            // Component loop counter
   siFld : SmallInt; // Field loop counter
   str   : String;   // Output string
begin

  with Result do
  for siCom := 0 to ComponentCount - 1 do
    if ( Components[ siCom ] is TAdoDataSet ) then
      with ( Components[ siCom ] as tAdoDataSet ) do
      begin

        add( upperCase( CommandText ) + '.DBF:' );
        for siFld := 0 to FieldCount - 1 do

          with Fields[ siFld ] do
          begin

            str := GetEnumName( TypeInfo( TFieldType ),
                                  Ord( DataType ) );

            add( format( '%2d. %s (%s, %d)',
                         [ siFld + 1, FieldName,
                           str, DataSize ] ) );

          end;
        add( '' );  // White space
      end;
end;

This particular example generates a simple report listing the names of the tables in the data module and their field structures. GetEnumName() reports the data type of each field using the associate tFieldType constant, rather than its ordinal value. As a result, the final report is easier to read for the average Delphi programmer.

To use the getStructures() example in an application, use something similar to the following:

uses maindata;
// ...

procedure TfrmMainForm.Button1Click(Sender: TObject);
begin
   Memo1.Lines.Clear;
   Memo1.Lines.addStrings( dmMainData.getStructures );
end;

Safer programming with GetEnumName()

If you maintain your applications over time, it's easy to forget to update your code to handle new values added to your enumerated types. The resulting bugs can be hard to detect because your code doesn't respond to supported values. With GetEnumName(), you can add exception handlers that detect this sort of problem and provide detailed troubleshooting information.

For example, suppose you're using an enumerated set to control the state of a button in a dialog box, as shown in the following code sample:

type tInfoButtonState = ( ibsHidden, ibsDisabled, ibsEnabled );
// ...

procedure TfrmInfoForm.setButtonState( const aState: tInfoButtonState );

   procedure setButton( const IsVisible, isEnabled : Boolean );
   begin
      with sbnCancel do
      begin
         Visible := isVisible;
         Enabled := isEnabled;
         if not isVisible then
         stxMessage.Width := Left + Width;
      endIf
   end;

begin
   case aState of
      ibsHidden   : setButton( FALSE, FALSE );
      ibsDisabled : setButton( TRUE, FALSE );
      ibsEnabled  : setButton( TRUE, TRUE );
   end; // case
end;

As you can probably, this code uses an enumerated set to control a SpeedButton's Visible and Enabled properties. While it works as written, suppose you later decide to turn the button into a toggle button (by setting its GroupIndex property to a non-zero value). But, you're in a hurry. You add ibsPressed and ibsUnpressed to the tInfoButtonState, but forget to update the setButtonState method to handle the new values.

When you run your code, you find that nothing happens when you call SetButtonState() with one of your new values. You spend valuable time reviewing your code, trying desparately to locate the problem.

(Granted, the example seems forced when presented like this, however, similar situations arise more frequently than you might imagine.)

The main problem with this code is not that values have been added to the underlying type, but that the processing code (the switch statement) doesn't provide a "safety net" for unhandled values. Else clauses prevent this by "catching" unhandled conditions and let you provide detailed troublshooting information, as shown in the this revision:

type tInfoButtonState = ( ibsHidden, ibsDisabled, ibsEnabled,
                          ibsPressed, ibsUnPressed );
// ...

procedure TfrmInfoForm.setButtonState( const aState: tInfoButtonState );

   procedure setButton( const IsVisible, isEnabled : Boolean );
   begin
      with sbnCancel do
      begin
         Visible := isVisible;
         Enabled := isEnabled;
         if not isVisible then
         stxMessage.Width := Left + Width;
      endIf
   end;
var
   strConst,
   strError : String;

begin
   case aState of
      ibsHidden   : setButton( FALSE, FALSE );
      ibsDisabled : setButton( TRUE, FALSE );
      ibsEnabled  : setButton( TRUE, TRUE );
   else
      begin

        strConst := getEnumName(
                      typeInfo( tInfoButtonState ),
                      Ord( aState ) );
        strError := format(

           'Can''t Change Button State.' + #10#10 +
           'Reason: %s''s setButtonState() method ' + #10 +
           'does not support the %s state.' + #10#10 +
           'Please contact Support for assistance.',
           [ Self.ClassName, strConst ] );

        raise Exception.create( strError );
      end;
   end; // case
end;

This version raises an Exception when unsupported values are detected; the details of the error message use GetEnumName() to provide the specific name of the unhandled value. This provides more complete troubleshooting information and helps keep your debugging time focused. (You'll also note that the resulting exception is phrased professionally, just in case a problem gets past testing and appears to an end-user.)

Common Errors and Their Solutions

This section outlines errors you may encounter when trying to use getEnumName(). While we can't possibly predict all possible situations, we hope this helps get you started.

  • Compilation error: Undeclared identifier: 'getEnumName'

    You need to add the TypInfo unit to an appropriate Uses block.

  • Compilation error: '.' expected but '(' found

    If the cursor is located in the first parameter of GetEnumName, you may have mistyped "typeInfo" (the typecast function) as "typInfo" (the name of the supporting unit).

  • Compilation error: Incompatible types: 'Integer' and 'TEnumeratedType'

    This usually appears when you pass an enumerated constant, instead of its ordinal integer value. Use Ord() to return the integer. (Note that "TEnumeratedType" will be the name of the enumerated set.)

  • GetEnumName() returns a blank value

    The second parameter contains an integer value that is not part of the enumerated set.

  • EAccess Violations generally appear when you pass negative integers as the second parameter or otherwise provide inappropriate values.

Summary

While there are several ways to extend and improve the examples in this article, we they begin to illustrate some of the possibilities for Delphi's RTTI. If you have a version of Delphi that includes the VCL source code, take some time to read the TYPINFO unit for additional information and ideas.

P.S. We know we've used a lot of extraneous variables in this article's examples; however, we felt they helped clarify the examples. If you disagree, please feel free to let us know via Feedback.

 

       

Top

Feedback About Paradox Delphi Assorted Web Stuff
 
 
Copyright © 2000-2004, techtricks.com; All Rights Reserved.
Acknowledgements, Disclaimers, Terms and Conditions.
Article last updated on 01 June 2003

 

Other Sites: Paradox, Delphi, Perl, Web Stuff, and More


 

[- End -]