Как правильно уничтожить визуальный компонент с использованием события?
Создаю свой VCL на базе TScrollBox для удобной работы с множеством последовательностей ДНК.
type
TSeqPanel = class (TPanel)
protected
editBtn, delBtn:TButton;
procedure Resize; override;
public
constructor Create(AOwner:TComponent); virtual;
destructor Destroy; override;
procedure Resort; virtual;
end;
TSeqBox = class(TScrollBox)
public
EditAction, DelAction:TNotifyEvent;
destructor Destroy; override;
constructor Create(AOwner:TComponent); virtual;
procedure AddPanel(name,hint:string; tag:integer);
procedure DeletePanel(tag:integer);
protected
private
panels:array of TSeqPanel;
end;
Фактически TSeqBox - контейнер для панелей с кнопками, которые создаются динамически (используется массив panels для хранения объектов). Событию OnClick кнопок editBtn, delBtn также динамически присваиваются методы из основной формы:
panels[l].editBtn.OnClick:=EditAction;
panels[l].delBtn.OnClick:=DelAction;
В частности, DelAction, это:
procedure TForm1.DelSequence(Sender: TObject);
var
l:integer;
begin
l:=(Sender as TButton).Tag;
//тут идёт удаление данных ДНК
seqBox.DeletePanel(l);
end;
Добавлю для полноты картины метод DeletePanel и деструктор TSeqPanel:
procedure TSeqBox.DeletePanel(tag: integer);
var
i:integer;
begin
panels[tag].Free;
if tag < high(panels) then
for I := tag to High(panels)-1 do
panels[i]:=panels[i+1];
SetLength(panels,high(panels));
// код для перерисовки панелей
end;
destructor TSeqPanel.Destroy;
begin
delBtn.Free;
editBtn.Free;
inherited;
end;
Проблема: если я из кода основной формы просто вызываю метод DeletePanel, необходимая панель замечательно удаляется без ошибок. Если же посредством нажатия кнопки delBtn на панели через вызов события, то вылезает ошибка c0000005 Access violation. Как я понимаю, кнопка delBtn удаляется вместе с родительской панелью раньше, чем происходит какое-то обращение к какому-то методу этой кнопки или панели.
Как тогда правильно организовать передачу события на удвление?
Ответы (2 шт):
Можно послать сообщение родительскому окну, передав tag для идентификации - кого именно убивать. Тогда удаление произойдёт после выхода из обработчика (Post-, не SendMessage)
PostMessage(Form1.Handle, WM_USER+666, tag, 0);
Пример привёл для формы, т.к. тут легче всего организовать приём сообщений, но компонент должен всё содержать в себе, и обрабатывать эти сообщения от деток сам.
Классическая задача вида "удалить себя из своего метода".
Общего решения под все версии Delphi и все поддерживаемые платформы, к сожалению, нет.
Потенциально - наиболее общим можно бы считать аналог решения из ответа @MBo, только использующее вместо прямой отправки сообщения - таймер, но оно в большинстве случаев будет слишком избыточно и громоздко.
Посему, если вы используете последние версии Delphi, то наиболее правильно будет воспользоваться методом TThread.ForceQueue:
TThread.ForceQueue(nil,
procedure
begin
seqBox.DeletePanel(i); // этот код выполнится "когда-нибудь потом", когда приложение
// будет бездействовать
end);
Однако, здесь есть подводный камень, скрывающийся именно в выполнении задачи "потом": если в промежутке между постановкой кода в очередь на выполнение (ForceQueue) и реальным его выполнением объект, содержащий seqBox, будет уничтожен - выйдет ошибка доступа к памяти.
Во избежание подобного, при уничтожении формы крайне желательно озаботиться тем, чтобы все поставленные в очередь отложенные выполнения уже выполнились:
destructor TForm1.Destroy;
begin
CheckSynchronize(0); // здесь будут вызваны и выполнены все участки кода, заявленные
// на отложенное выполнение через ForceQueue, Queue, Synchronize
inherited;
end;
Если же в вашей версии Delphi метод TThread.ForceQueue не поддерживается (а приложение разрабатывается под ОС Windows) - используйте код из ответа @MBo (он лишен указанного выше подводного камня)
