Windows позволяет с каждой строкой списка элементов управления
ListBoх
и
ComboBox
связать либо число, либо указатель (точнее — некоторую четырехбайтную величину, которую программа может трактовать как число, как указатель или как что-либо еще). В VCL эта возможность обеспечивает привязку к строкам списка объектов (четырёхбайтная величина по умолчанию трактуется как
TObject
). Доступ к этим объектам осуществляется через свойства
TComboBox.Items.Objects[Index]
и
TListBox.Items.Objects[Index]
.
Иногда
все-таки требуется привязывать к строкам не объекты, а числа. Для этого можно воспользоваться явным приведением типов, например:
ComboBox1.Items.Objects[I] := TObject(17);
I := Integer(ComboBox1.Items.Objects[I]);
Если таким образом связать со строкой значение -1, то при попытке получить его во всех версиях Delphi до 7-й включительно возникнет исключение
EStringListError
с комментарием "List index out of bounds". Рассмотрим следующий код (листинг 3.56. пример ListIndex на компакт-диске).
Листинг 3.56. Пример кода, вызывающего исключение
EStringListError
procedure TForm1.Button1Click(Sender: TObject);
var
I: Integer;
begin
ComboBox1.Items.Clear;
ComboBox1.Items.AddObject('Text', TObject(-1));
I := Integer(ComboBox1.Items.Objects[0]); { * }
Label1.Caption := IntToStr(I);
end;
Исключение возникнет при попытке выполнить строку, отмеченную звездочкой, хотя очевидно, что индекс в данном случае корректен. Чтобы понять причину ошибки, необходимо рассмотреть, как осуществляется чтение значения, привязанного к строке, на уровне Windows API. Рассмотрим это на примере
TComboBox
. Для получения значения необходимо послать окну
ComboBox
сообщение
CB_GETITEMDATA
. Результатом обработки этого сообщения будет значение, связанное с указанной строкой, или
CB_ERR
, если при обработке сообщения возникнет ошибка. При этом документация не уточняет, какие именно ошибки могут в принципе возникнуть и как узнать, какая из них произошла.
Метод
TComboBoxStrings.GetObject
, через который читается значение свойства
Objects
, в Delphi 7 и более ранних версиях интерпретирует получение
CB_ERR
однозначно: генерирует исключение
EStringListError
с комментарием "List index out of bounds".
Проблема заключается в том, что константа
CB_ERR
имеет численное значение -1. Поэтому и в случае ошибки, и в случае, когда строке сопоставлено значение -1, системный обработчик сообщения
CB_GETITEMDATA
вернет одинаковый результат. И метод
TComboBoxStrings.GetObject
интерпретирует его как ошибку. (А что ему еще остается делать?)
Аналогичная проблема по тем же причинам возникает и для
ListBox
(аналогичная по смыслу константа
LB_ERR
также имеет значение -1). Это прямое следствие документированных особенностей работы Windows и модели работы VCL встречается во всех версиях Windows. Та же проблема возникает при попытке указать значение 4 294 967 295, т.к. на двоичном уровне это число записывается той же комбинацией битов, что и -1.
При использовании свойства
Objects
по прямому назначению, т.е. для хранения объектов, эта проблема не может возникнуть, потому что $FFFFFFFF — это адрес самого старшего байта в четырехгигабайтном виртуальном адресном пространстве программы. Эта область адресного пространства зарезервирована системой, и менеджер памяти Delphi не может выделить память для объекта в этой области. Рекомендуемые способы решения проблемы:
1. Пересмотреть алгоритм и отказаться от связывания значения -1 со строками.
2. Напрямую посылать
CB_GETITEMDATA
окну
ComboBox
, а попадание индекса в диапазон контролировать самостоятельно другими методами. Приведенный в листинге 3.57 код иллюстрирует последний совет.
Листинг 3.57. Получение связанного с элементом значения вручную
procedure TForm1.Button2Click(Sender: TObject);
var
I: Integer;
begin
ComboBox1.Items.Clear;
ComboBox1.Items.AddObject('Text', TObject(-1));
I := SendMessage(ComboBox1.Handle, CB_GETITEMDATA, 0, 0);
Label1.Caption := IntToStr(I);
end;
Как уже было отмечено ранее, в BDS 2006 и более поздних версиях исключение не возникает. Это связано с новой реализацией метода
TCustomComboBoxStrings.GetObject
, который отвечает за получение значения свойства
Items.Object
(листинг 3.58).
Листинг 3.58. Получение значения свойства
Items.Object
в BDS 2006 и выше
function TCustomComboBoxStrings.GetObject(Index: Integer): TObject;
begin
Result := TObject(SendMessage(ComboBox.Handle, CB_GETITEMDATA, Index, 0));
// Do additional checking on Count and Index here is so in the event
// the object being retrieved is the integer -1 the call will succeed
if (Longint(Result) = CB_ERR) and ((Count = 0) or
(Index < 0) or (Index > Count)) then
Error(SListIndexError, Index);
end;
Решение спорное, т.к. проверка корректности системой дополняется собственной проверкой индекса, и не совсем понятно, что делать в том случае, если система фиксирует какую-либо ошибку, не связанную с индексом. Но здесь Windows ставит разработчика в такие условия, что любое решение будет спорным, так что упреком по отношению к разработчикам VCL такая оценка их решения не является.