Имя Значение Пары в 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.