Читаем О чём не пишут в книгах по Delphi полностью

Чтобы убедиться, насколько некорректно реализовано удаление компонентов в VCL, рассмотрим еще один пример (на компакт-диске он называется FrameDel). В этом примере на форму помещается фрейм с одним компонентом типа TComboBox. Код фрейма показан в листинге 3.66.

Листинг 3.66. Код фрейма

type

 TFrame1 = class(TFrame)

  ComboBox1: TComboBox;

 private

  { Private declarations }

 public

  destructor Destroy; override;

  procedure AddComboItem;

 end;

destructor TFrame1.Destroy;

var

 I: Integer;

begin

 for I := 0 to ComboBox1.Items.Count - 1 do

  if Assigned(ComboBox1.Items.Objects[I]) then

   Dispose(PDateTime(ComboBox1.Items.Objects[I]));

 inherited;

end;

procedure TFrame1.AddComboItem;

var

 P: PDateTime;

begin

 New(P);

 P^:= Now;

 ComboBox1.Items.AddObject('Item ' + TimeToStr(P^), TObject(P));

end;

На форму в обработчике события OnShow помещается такой фрейм и вызывается его метод AddComboItem, чтобы в компоненте ComboBox1 появился один элемент в списке. Если закрыть такую форму, никаких исключений не возникает, все выглядит нормально. Но при трассировке можно заметить, что цикл внутри деструктора не выполняется ни разу, потому что ComboBox1.Items.Count возвращает 0. Это происходит потому, что на момент вызова этого деструктора и окно фрейма, и окно ComboBox1 уже не существуют, в чем легко убедиться, проверив в деструкторе значение поля ComboBox1.FHandle (до обращения к свойству ComboBox1.Items.Count оно равно нулю). А при обращении к этому свойству происходит попытка создать окно. Так как свойство TComboBox1.Parent в этот момент еще не обнулено, предпринимается попытка создать заново и фрейм тоже, и эта попытка становится успешной. К этому моменту свойство Parent фрейма уже обнулено, но метод TCustomFrame.CreateParams реализован таким образом, что родителем всех фреймов, для которых родитель не задан явно, становится невидимое окно приложения, которое на этот момент еще не разрушено. Таким образом, окно фрейма и окно компонента TComboBox1 успешно создаются заново, и им можно посылать сообщения.

Ранее мы говорили, что код компонента TComboBox обеспечивает перенос элементов при удалении и последующем создании окна. Но в данном случае этот код даже не догадывается, что после удаления окно может быть создано ещё раз, и потому механизм переноса не задействуется. Вновь созданное окно компонента ComboBox1 не получает в свой список ни одного элемента, что и приводит к тому, что свойство Items.Count равно нулю. Но динамическая память, выделенная в методе AddComboItem остается не освобождённой. В результате имеем утечку памяти вместо исключения. Кроме того, имеем утечку и других ресурсов, т.к. код, ответственный за удаление окна фрейма, на этот момент уже отработал и не будет запущен еще раз, чтобы удалить вновь созданное окно.

Решением проблемы может стать уже опробованный способ: нужно обрабатывать сообщение WM_DESTROY, посылаемое фрейму, выполнять в нем все те же проверки, что и в листинге 3.65, и при необходимости освобождать память, которую нельзя освободить в деструкторе.

Примечание

Если бы мы попытались использовать наследника от класса, например, TPanel вместо TFrame, ошибка при завершении работы программы возникла бы, компоненту TPanel не назначается родитель по умолчанию, и попытка создания его окна в деструкторе закончилась бы неудачей. Назначение родителя по умолчанию приводит еще к одному интересному эффекту: мы можем добавлять в ComboBox1 элементы до того, как будет назначено свойство Parent фрейма. Ошибки не возникнет, потому что окно фрейма будет создано успешно, а при последующем назначении свойства Parent фрейма в компоненте ComboBox1 сработает механизм переноса элементов при создании нового окна, и пользователь увидит добавленные элементы.

Перейти на страницу:
Нет соединения с сервером, попробуйте зайти чуть позже