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.
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
- store the value of your items in some object instance, and
- 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;
Enjoy!
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;
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.
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]).
Thanks.
Have you tried?
FMyNameValuePairs[idx]
returns the entire"name=value"
string, not just the value.