ComboBox中的名称值对

时间:2010-06-11 13:03:08

标签: delphi combobox

我确信这一定是一个常见的问题,但我似乎无法找到一个简单的解决方案......

我想使用名称值对的组合框控件作为项目。 ComboBox将TStrings作为其项目,因此应该没问题。

不幸的是,组合框上的绘图方法会绘制Items [i],因此您可以在框中获得Name = Value。

我希望隐藏值,因此我可以使用代码中的值,但用户会看到名称。

任何想法?

4 个答案:

答案 0 :(得分:14)

Style设为csOwnerDrawFixed并写入

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;

答案 1 :(得分:11)

如果您的值是整数: 拆分名称值对,将名称存储在组合框的字符串中以及相应对象中的值。

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

通过这种方式,您可以继续以通用方式使用控件,并且仍然具有可用的值:

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

此方法也可用于其他对象的列表。例如,包含TPerson对象实例的TObjectList:

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

并通过以下方式检索所选项目的相应TPerson:

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

更新

更好的方法 - 而不依赖于整数值的方法 - 是预处理List,将值包装在一个简单的类中,并将其实例添加到List的Objects中。

简单 - 基于RTTI的扩展 - 包装类:

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;

如果您使用的是D1010之前版本的Delphi,只需使用string代替TValue。

预处理清单:

// 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;

将列表加载到Combo现在是一个简单的任务:

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

请记住,在内部执行分配,因此在释放List之前,最好确保组合无法再访问其列表对象的实例。

从列表中获取名称和值:

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

或来自ComboBox:

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

可以在我的博客上找到相同的信息和更全面的解释:TL;DR version of Name Value Pairs in ComboBoxes and Kinfolk

答案 2 :(得分:1)

组合框项目文本应包含显示文本。这是正确的风格。然后,使用ItemIndex属性查找内部键值。击败控件的属性以包含模型代码或数据库内部键值,这是对OOP原则的严重违反。

让我们考虑一下将来如何维护您的应用程序。您可能会自己回到这段代码并思考,“我在想什么?”。记住“最少惊奇的原则”。按照使用方式使用东西,避免自己和同事免于痛苦。

答案 3 :(得分:1)

我同意Marjan Venema的解决方案,因为它使用已经内置的支持在TStringList中存储对象。

我也处理了这个问题,我首先使用上面发布的解决方案的版本“csOwnerDrawFixed”派生了我自己的组合框组件。我实际上需要存储一个ID(通常来自数据库)和文本。该ID将对用户隐藏。我认为这是一种常见的情况。 ItemIndex仅用于从列表中检索数据,它实际上不是一个有意义的变量,就像上面发布的示例一样。

所以我的想法是将ID与显示的文本连接起来,例如用“#”分隔,并覆盖DrawItem(),这样它只会绘制剥离了ID的文本。我将其扩展为保留多个ID,格式为“Name#ID; var1; var2”,例如。 “Michael Simons#11;真实; M”。 DrawItem()会在#。

之后删除所有内容

现在,如果组合中的项目很少,那么这样做很好。但是当处理更大的列表时,集中滚动组合使用CPU,因为在每个项目绘制时,需要剥离文本。

所以,我制作的第二个版本使用了AddObject方法。交易CPU消耗了更多的内存,但这是一个公平的交易,因为事情要快得多。

用户看到的文本通常存储在combo.Items中,所有其他数据存储在与每个元素关联的TStringList中。无需覆盖DrawItem,因此您可以从例如派生。 TmxFlatComboBox并保持其平面外观。

以下是派生组件的一些最重要的功能:

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;

在这两个版本中,所有数据(甚至是ID)都表示为字符串,因为它使事情更加通用,因此在使用它们时,您需要执行大量的StrToInt,反之亦然。

用法示例:

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 !

另外,

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

方法很有用,为了检索任何ID的索引,但这个答案已经太长了,无法在此处发布。

使用相同的原理,您还可以派生自定义ListBox或CheckListBox。