TL;DR version of Name Value Pairs in ComboBoxes and Kinfolk

Did you read Name Value Pairs in ComboBoxes and Kinfolk?

Too long?

Yeah. You’re right. I realized that after I published it.

Here’s the TL;DR version of it.


You have been tasked with adding a simple ComboBox to a form and load its list of values from a configuration file containing a list of name value pairs.

Getting the name value pairs from the file into a string list is a “been there, done that” breeze kind of thing. Getting a ComboBox to display the contents should be just as simple. It takes a TStrings for its Items property after all.

begin
  // Simulate loading from some configuration file
  FMyNameValuePairs := TStringList.Create;
  FMyNameValuePairs.Add('Jansen=100');
  FMyNameValuePairs.Add('Petersen=200');
  FMyNameValuePairs.Add('Gerritsen=300');
  FMyNameValuePairs.Add('Dirksen=400');
  FMyNameValuePairs.Add('Karelsen=500');

  // Assign the "configuration" contents of the string list to the ComboBox
  ComboBox1.Items := FMyNameValuePairs;
end;

Yay! That’s another item of your todo list.

ComboBox naively filled with name value pairs

Oops?!

Why are those numbers showing? They are not supposed to be there. Doesn’t a ComboBox know to use only the names when its Items holds a list of name value pairs?

In one word? No.

In more words? No, it doesn’t. The DrawItems method of TCustomComboBox simply draws the entire contents of its items:

FCanvas.TextOut(Rect.Left + 2, Rect.Top, Items[Index]);

Bugger

So how do you get around this unfortunate limitation?

StringList gold

Something very well known to Delphi “old hands”, but which comes as a surprise to many less experienced Delphi developers is that a TStringList is not “just” a list of strings. It actually is a list of records that hold both a string and an object reference.

To make that little bit of gold work to your advantage, all you need to do is

  1. store the value of your items in some object instance, and
  2. remove the name-value separator and the value from the items in your StringList

before you assign your StringList to the Items property of a ComboBox, or any other control that handles StringLists.

To store the values of your names in a StringList’s Objects, you need a class that can at least hold a string value. String because when you read configuration files into a StringList, the values come in as strings. Any conversion to the actual type of the values can be done later or elsewhere.

That said, when your Delphi version has extended RTTI or generics, why not put it to good use?

For my TValueObject class I have opted to use the TValue record that comes with extended RTTI. You could also use generics, but then you would have to code all the “To String” conversions yourself. I prefer to leverage the built-in conversions of the TValue record.

type
  TValueObject = class(TObject)
  strict private
    FValue: TValue;
  public
    constructor Create(const aValue: TValue);
    property Value: TValue read FValue;
  end;
  
  { TValueObject }

constructor TValueObject.Create(const aValue: TValue);
begin
  FValue := aValue;
end;

Storing the value of each name and stripping everything but the name part from the items in your StringList is pretty simple:

  // Convert the contents so both the ComboBox and Memo can show just the names
  // and the values are still associated with their items using actual object
  // instances.
  for idx := 0 to FMyNameValuePairs.Count - 1 do
  begin
    FMyNameValuePairs.Objects[idx] := 
      TValueObject.Create(FMyNameValuePairs.ValueFromIndex[idx]);

    FMyNameValuePairs.Strings[idx] := FMyNameValuePairs.Names[idx];
  end;

Getting the name and value for an item is just as easy:

begin
  Name_Text.Caption := FMyNameValuePairs.Items[idx];
  Value_Text.Caption := TValueObject(FMyNameValuePairs.Objects[idx]).Value.AsString;
end;

When you now set your ComboBox’ items to your StringList, the ComboBox will only show your names but it will also have all the names’ values available.

Bear in mind that setting your ComboBox’ items to your StringList does an Assign – a shallow copy – so you have to ensure that the ComboBox can no longer access its items’ values before you free your StringList.

Oh and of course you want to instantiate your StringList setting its OwnsObject property to True so that when you free it, all instances stored in its Objects are freed automatically.

Putting it all together to show just the names in your ComboBox and still have access to each name’s value:

procedure TGold_Form.ComboBox1Select(Sender: TObject);
begin
  Name_Text.Caption := ComboBox1.Items[ComboBox1.ItemIndex];
  Value_Text.Caption := 
    TValueObject(ComboBox1.Items.Objects[ComboBox1.ItemIndex]).Value.AsString;
end;

procedure TGold_Form.FormCreate(Sender: TObject);
var
  idx: Integer;
begin
  // Simulate loading from some configuration file
  // We set OwnsObjects to True so that any object instances we add will be
  // freed automatically when we free the string list.
  FMyNameValuePairs := TStringList.Create({OwnsObjects}True);
  FMyNameValuePairs.Add('Jansen=Aquarius');
  FMyNameValuePairs.Add('Petersen=Capricorn');
  FMyNameValuePairs.Add('Gerritsen=Pisces');
  FMyNameValuePairs.Add('Dirksen=Libra');
  FMyNameValuePairs.Add('Karelsen=Scorpio');

  // Convert the contents so both the ComboBox and Memo can show just the names
  // and the values are still associated with their items using actual object
  // instances.
  for idx := 0 to FMyNameValuePairs.Count - 1 do
  begin
    FMyNameValuePairs.Objects[idx] :=
      TValueObject.Create(FMyNameValuePairs.ValueFromIndex[idx]);

    FMyNameValuePairs.Strings[idx] := FMyNameValuePairs.Names[idx];
  end;

  // Show the contents of the "configuration" in a memo
  Memo1.Text := FMyNameValuePairs.Text;

  // Load the "configuration" contents of the string list into the combo box
  ComboBox1.Items := FMyNameValuePairs; // Does an Assign!
end;

procedure TGold_Form.FormDestroy(Sender: TObject);
begin
  // Ensure the ComboBox can no longer reference the items in FMyNameValuePairs
  ComboBox1.Clear;

  // Free the "configuration" string list.
  // As OwnsObjects is True, we don't need to free the Objects ourselves.
  // It is done for us.
  FMyNameValuePairs.Free;
end;

ComboBox and Memo filled with names of name value pairs allowing values of any type

Enjoy!

Posted in Software Development
Tags: , , , , , , ,
4 comments on “TL;DR version of Name Value Pairs in ComboBoxes and Kinfolk
  1. Rob says:

    So how do you get around this unfortunate limitation?
    Just override the DrawItem of the combo- or listbox and
    set the property Style on lbOwnerDrawFixed.

    procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
    Rect: TRect; State: TOwnerDrawState);
    begin
    with Control as TListBox do
    Canvas.TextOut(Rect.Left + 2, Rect.Top, Items.Names[Index]);
    end;

    • Marjan Venema says:

      Yes that works, but…
      If you had read the longer version of this post (the one linked at the top), you would have know why I don’t want to use this approach 🙂 In short: no events for stuff I expect to have to do at multiple locations and no custom controls because I like to leave graphics to the RTL/VCL and component vendors.

  2. Klaus says:

    Nice idea!!
    But since I’m very lazy, I wonder why I should have to wirte

    TValueObject.Create(FMyNameValuePairs.ValueFromIndex[idx])

    I’d like to have here just

    TValueObject.Create(FMyNameValuePairs[idx]).

    • Marjan Venema says:

      Thanks.
      Have you tried?
      FMyNameValuePairs[idx] returns the entire "name=value" string, not just the value.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Show Buttons
Hide Buttons