【问题标题】:Name Value Pairs in a ComboBox组合框中的名称值对
【发布时间】:2014-10-20 01:29:21
【问题描述】:

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

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

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

我希望隐藏该值,以便我可以在代码中使用该值,但用户会看到名称。

有什么想法吗?

【问题讨论】:

    标签: delphi combobox


    【解决方案1】:

    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;
    

    【讨论】:

    • 最后我只是复制了 StdCtrls TCustomComboBox.DrawItem 并替换了项目调用,反正只有 3 行。
    • JamesB - 上面陈述的答案更具未来性。当您从 StdCtrls 复制/粘贴代码相对于基本 VCL 或 Windows 通用控件而言已过期时会发生什么?
    • 粘贴的代码显示为 ...FillRect... ...TextOut... 我意识到复制和粘贴代码并不总是最好的主意,但将代码与 VCL 匹配至少可以保持外观与其他 VCL 控件的一致性。我不太担心 FillRect 或 TextOut 即将被弃用。
    【解决方案2】:

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

      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]);
    

    更新

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

    简单 - 基于扩展 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;
    

    如果您使用的是 D2010 之前的 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;
    

    或来自组合框:

    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

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

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

        所以我的想法是将 ID 与显示的文本连接起来,例如用“#”分隔,然后覆盖 DrawItem(),这样它只会绘制去除了 ID 的文本。我扩展了它以保留多个 ID,例如“Name#ID;var1;var2”形式。 “迈克尔·西蒙斯#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。

        【讨论】:

        • 我不能直接评论@warren-p 的帖子,所以我把它放在这里。我同意,对于项目中的新人来说,这段代码可能很难理解,但是如果留下适当的注释,它就不会那么难了。此外,与组合框(或任何控件)绑定的 db 内部键值是非常合理的(参见 DBComboBox 等)。在不同的地方,您希望组合值按不同的键排序,而 ItemIndex 本身与识别数据库中的某些 ID 无关。
        • 现有的 DB 控件通常查询数据库过于频繁,并且不如通常的控件灵活。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-05-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-07-21
        • 2019-06-25
        相关资源
        最近更新 更多