Name Value Pairs in a ComboBox

后端 未结 4 1598
醉话见心
醉话见心 2021-02-01 06:42

I\'m convinced this must be a common problem, but I can\'t seem to find a simple solution...

I want to use a combobox control with name value pairs as the items. ComboBo

相关标签:
4条回答
  • 2021-02-01 07:02

    The combobox items text should have contained the display text. That is the proper style. Then, use the ItemIndex property to look up the internal key values. Defeating the control's properties to contain your model code or database internal key values, is a huge violation of OOP principles.

    Let's just consider how someone is going to maintain your application in the future. You might come back to this code yourself and think, "what was I thinking?". Remember the "principle of least amazement". Use things the way they were meant to be used, and save yourself and your co-workers from pain.

    0 讨论(0)
  • 2021-02-01 07:04

    Set Style to csOwnerDrawFixed and write

    procedure TForm1.ComboBox1DrawItem(Control: TWinControl; Index: Integer;
      Rect: TRect; State: TOwnerDrawState);
    begin
      ComboBox1.Canvas.TextRect(Rect, Rect.Left, Rect.Top, ComboBox1.Items.Names[Index]);
    end;
    
    0 讨论(0)
  • 2021-02-01 07:06

    I agree with Marjan Venema's solution, as it uses already built-in support for storing objects in a TStringList.

    I've also dealt with this and I first derived my own combobox component using a tweaked version of the posted solution above with "csOwnerDrawFixed". I actually needed to store an ID (usually from a database) along with a text. The ID would be hidden from the user. I think this is a common scenario. The ItemIndex is used just to retrieve data from the list, it isn't really a meaningful variable, like in the posted example above.

    So my idea was to concatenate the ID with the displayed text, separated by "#" for example, and override DrawItem() so it would only paint the text with the ID stripped. I extended this to keep more than an ID, in the form "Name#ID;var1;var2" eg. "Michael Simons#11;true;M". DrawItem() would strip everything after #.

    Now that is good to start with, when you have few items in a combo. But when dealing with a larger list, scrolling the combo intensively uses CPU, as at every item draw, the text needs to be stripped.

    So, the second version I made used the AddObject method. That traded CPU for a little more memory consumption, but It's a fair trade, because things were a lot faster.

    Text that the user sees is stored normally in combo.Items, and all other data is stored in a TStringList associated with every element. No need to override DrawItem, so you can derive from eg. TmxFlatComboBox and keep its flat look as is.

    Here's some of the most important functions of the derived component:

    procedure TSfComboBox.AddItem(Item: string; Lista: array of string);
    var ListaTmp: TStringList;
        i: integer;
    begin
      ListaTmp:= TStringList.Create;
      if High(Lista)>=0 then
        begin
        for i:=0 to High(Lista) do
          ListaTmp.Add(Lista[i]);
        end;
      Items.AddObject(Item, ListaTmp);  
      //ListaTmp.Free; //no freeing here! we override .Clear() also and the freeing is done there
    end;
    
    function TSfComboBox.SelectedId: string;
    begin
      Result:= GetId(ItemIndex, 0);
    end;
    
    function TSfComboBox.SelectedId(Column: integer): string;
    begin
      Result:= GetId(ItemIndex, Column);
    end;
    
    function TSfComboBox.GetId(Index: integer; Column: integer = 0): string;
    var ObiectTmp: TObject;
    begin
      Result:= '';
      if (Index>=0) and (Items.Count>Index) then
        begin
        ObiectTmp:= Items.Objects[Index];
        if (ObiectTmp <> nil) and (ObiectTmp is TStringList) then
          if TStringList(ObiectTmp).Count>Column then
            Result:= TStringList(ObiectTmp)[Column];
        end;
    end;
    
    function TSfComboBox.SelectedText: string;
    begin
      if ItemIndex>=0
        then Result:= Items[ItemIndex]
        else Result:= '';    
    end;
    
    procedure TSfComboBox.Clear;
    var i: integer;
    begin
      for i:=0 to Items.Count-1 do
        begin
        if (Items.Objects[i] <> nil) and (Items.Objects[i] is TStringList) then
          TStringList(Items.Objects[i]).Free;
        end;
      inherited Clear;
    end;
    
    procedure TSfComboBox.DeleteItem(Index: Integer);
    begin
      if (Index < 0) or (Index >= Items.Count) then Exit;
      if (Items.Objects[Index] <> nil) and (Items.Objects[Index] is TStringList) then
        TStringList(Items.Objects[Index]).Free;
      Items.Delete(Index);
    end;
    

    In both versions, all data (even ID's) are represented as strings, because it keeps things more general, so when using them, you need to do a lot of StrToInt and vice versa conversions.

    Usage example:

    combo1.AddItem('Michael Simons', ['1', '36']); // not using Items.Add, but .AddItem !
    combo1.AddItem('James Last', ['2', '41']);
    intSelectedID:= StrToIntDef(combo1.SelectedId, -1); // .ItemIndex would return -1 also, if nothing is selected
    intMichaelsId:= combo1.GetId(0);
    intMichaelsAge:= combo1.GetId(0, 1); // improperly said GetId here, but you get the point
    combo1.Clear; // not using Items.Clear, but .Clear directly !
    

    Also, a

    GetIndexByValue(ValueToSearch: string, Column: integer = 0): integer
    

    method is useful, in order to retrieve the index of any ID, but this answer is already too long to post it here.

    Using the same principle, you can also derive a custom ListBox or CheckListBox.

    0 讨论(0)
  • 2021-02-01 07:23

    If your values are integers: Split the name value pairs, store the names in the strings of the combobox and the values in the corresponding objects.

      for i := 0 to List.Count - 1 do
        ComboBox.AddItem(List.Names[i], TObject(StrToInt(List.ValueFromIndex[i], 0)));
    

    This way you can keep using your controls in a generic way and still have the value avalaible through:

    Value := Integer(ComboBox.Items.Objects[ComboBox.ItemIndex]);
    

    This approach can also be used for lists of other objects. For example a TObjectList containing TPerson object instances:

    var
      i: Integer;
      PersonList: TObjectList;
    begin
      for i := 0 to PersonList.Count - 1 do
        ComboBox.AddItem(TPerson(PersonList[i]).Name, PersonList[i]);
    

    and retrieve the corresponding TPerson of the selected item through:

    Person := TPerson(ComboBox.Items.Objects[ComboBox.ItemIndex]);
    

    Update

    A better way - and one not dependent on the values being integers - is to pre-process the List, wrapping the values in a simple class and adding instances thereof to the List's Objects.

    Simple - extended RTTI based - wrapper class:

    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;
    

    If you are using a pre-D2010 version of Delphi, just use string instead of TValue.

    Pre-processing the list:

    // 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 List.Count - 1 do
    begin
      List.Objects[idx] := 
        TValueObject.Create(List.ValueFromIndex[idx]);
    
      List.Strings[idx] := List.Names[idx];
    end;
    

    Loading the list into the Combo is now a simple assignment:

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

    Do bear in mind that internally this does an assign, so you had better make sure that the combo can no longer access the instances its list's objects before you free List.

    Getting the name and value from the list:

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

    or from the ComboBox:

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

    The same information with more comprehensive explanation can be found on my blog: TL;DR version of Name Value Pairs in ComboBoxes and Kinfolk

    0 讨论(0)
提交回复
热议问题