Имя Значение Пары в ComboBox

Я убежден, что это должна быть общая проблема, но я не могу найти простое решение...

Я хочу использовать элемент управления combobox с парами значений имен в качестве элементов. ComboBox берет TStrings как свои элементы, поэтому это должно быть хорошо.

К сожалению, метод рисования в combobox рисует Items [i], поэтому вы получаете Name = Value в поле.

Я хочу, чтобы значение было скрыто, поэтому я могу работать со значением в коде, но пользователь видит имя.

Любые идеи?

Ответы

Ответ 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;

Ответ 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]);

Этот подход также может использоваться для списков других объектов. Например, TObjectList, содержащий экземпляры объекта TPerson:

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

Update

Лучший способ - и не зависящий от значений, являющихся целыми числами, - это предварительная обработка списка, перенос значений в простой класс и добавление его экземпляров в объекты списка.

Простой расширенный класс на основе 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;

Если вы используете версию Delphi до D2010, просто используйте 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 версия Пары значений значения в ComboBoxes и Kinfolk

Ответ 3

Текст элементов со списком должен содержать текст отображения. Это правильный стиль. Затем используйте свойство ItemIndex для поиска внутренних значений ключа. Преодоление свойств управления, чтобы содержать код вашей модели или внутренние ключевые значения базы данных, является огромным нарушением принципов ООП.

Давайте просто рассмотрим, как кто-то собирается поддерживать ваше приложение в будущем. Вы можете вернуться к этому коду самостоятельно и подумать: "О чем я думал?". Помните "принцип наименьшего изумления". Используйте вещи так, как они должны были использоваться, и спасайте себя и своих коллег от боли.

Ответ 4

Я согласен с решением Marjan Venema, поскольку он использует уже встроенную поддержку для хранения объектов в TStringList.

Я также занимался этим, и я впервые получил свой собственный компонент combobox, используя измененную версию вышеизложенного решения с "csOwnerDrawFixed". Мне действительно нужно было хранить идентификатор (обычно из базы данных) вместе с текстом. Идентификатор будет скрыт от пользователя. Я думаю, что это общий сценарий. ItemIndex используется только для извлечения данных из списка, это не действительно значимая переменная, как в вышеприведенном примере выше.

Итак, моя идея заключалась в том, чтобы объединить идентификатор с отображаемым текстом, например, "#" и переопределить DrawItem(), чтобы он только рисовал текст с идентификатором. Я продлил это, чтобы сохранить больше, чем идентификатор, в форме "Name # ID; var1; var2", например. "Майкл Саймонс № 11, правда, М". DrawItem() разделил бы все после #.

Теперь это хорошо, когда у вас мало элементов в комбо. Но при работе с более крупным списком, прокрутка комбо интенсивно использует процессор, так как при каждом вытягивании элемента текст нужно удалить.

Итак, вторая версия, которую я сделал, использовала метод AddObject. Это торгуемый процессор для небольшого потребления памяти, но это справедливая сделка, потому что все было намного быстрее.

Текст, который видит пользователь, обычно сохраняется в 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;

В обеих версиях все данные (даже идентификаторы) представлены в виде строк, потому что они содержат вещи более общие, поэтому при их использовании вам нужно сделать много преобразований 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.