Ответ 1
Оба TButton
и TEdit
являются TWinControl
потомками - это означает, что они являются оконными элементами управления. Когда они создаются, они выделяют свои собственные HWND
, и операционная система отправляет сообщения им непосредственно, когда у них есть фокус. Отключение их содержащей формы не позволяет основной форме получать входные сообщения или получать фокус, но не отключает какой-либо другой оконный контроль, если он уже имеет фокус ввода.
Если эти элементы управления не имеют фокуса ввода, то ответственность за перенос фокуса ввода на них зависит от того, когда вводится пользовательский ввод (щелчок, клавиша табуляции и т.д.). Если форма отключена, и эти элементы управления не сфокусированы, форма не получит входные сообщения, которые позволят ей перенести фокус. Однако если фокус переносится в оконный элемент управления, то все пользовательские вводные данные поступают непосредственно на этот элемент управления, даже если их родительское управляющее окно отключено - они фактически являются их отдельными окнами.
Я не уверен, что поведение, которое вы наблюдали, является ошибкой - это, возможно, не ожидается, но это стандартное поведение. Обычно нет ожиданий, что отключение одного окна также отключит других в одном приложении.
Проблема в том, что в игре есть две отдельные иерархии. На уровне VCL кнопка является дочерним элементом управления и имеет родительский элемент (форму). Однако на уровне ОС оба являются отдельными окнами, а родительские/дочерние отношения (компонентный уровень) не знакомы с ОС. Это будет аналогичная ситуация:
procedure TForm1.Button1Click(Sender: TObject);
var
form2 : TForm1;
begin
self.Enabled := false;
form2 := TForm1.Create(self);
try
form2.ShowModal;
finally
form2.Free;
end;
end;
Неужели вы ожидаете, что form2
будет отключен, когда он будет показан, просто потому, что его владелец TComponent
Form1
? Наверняка нет. Оконные элементы управления практически одинаковы.
У самих Windows также могут быть отношения родительские/дочерние, но это отдельно от владения компонентом (VCL parent/child) и не обязательно ведет себя одинаково. Из MSDN:
Система передает входные сообщения дочернего окна непосредственно на дочернее окно; сообщения не передаются через родительское окно. Единственное исключение - если дочернее окно отключеноВключить функцию. В этом случае система передает любой вход сообщения, которые попали бы в дочернее окно в родительское окно вместо. Это позволяет родительскому окну проверять входные сообщения и при необходимости включите дочернее окно.
Акцент мой - если вы отключите дочернее окно, тогда его сообщения будут перенаправлены родительскому лицу для возможности проверить и действовать на них. Обратное неверно - отключенный родитель не будет препятствовать тому, чтобы ребенок получал сообщения.
Довольно утомительное обходное решение может заключаться в том, чтобы сделать собственный набор TWinControl
, который ведет себя следующим образом:
TSafeButton = class(TButton)
protected
procedure WndProc(var Msg : TMessage); override;
end;
{...}
procedure TSafeButton.WndProc(var Msg : TMessage);
function ParentForm(AControl : TWinControl) : TWinControl;
begin
if Assigned(AControl) and (AControl is TForm) then
result := AControl
else
if Assigned(AControl.Parent) then
result := ParentForm(AControl.Parent)
else result := nil;
end;
begin
if Assigned(ParentForm(self)) and (not ParentForm(self).Enabled) then
Msg.Result := 0
else
inherited;
end;
Это позволяет подобрать родительское дерево VCL до тех пор, пока не найдет форму - если это произойдет, и форма будет отключена, то она также отклонит ввод в оконный элемент управления. Беспокойный и, вероятно, может быть более избирательным (возможно, некоторые сообщения не следует игнорировать...), но это было бы началом того, что могло бы работать.
Копаем дальше, это, похоже, не соответствует с документацией:
Только одно окно за раз может получать ввод с клавиатуры; это окно сказал, что имеет фокус клавиатуры. Если приложение использует Функция EnableWindow для отключения окна фокусировки клавиатуры, окна теряет фокус клавиатуры в дополнение к отключению. EnableWindow затем устанавливает фокус клавиатуры в значение NULL, а это означает, что окно не имеет фокуса. Если дочернее окно или другое дочернее окно имеет фокус клавиатуры, окно потомка теряет фокус, когда родительское окно отключен. Дополнительные сведения см. В разделе "Ввод клавиатуры".
Это, похоже, не происходит, даже явно устанавливая окно кнопки дочерним с:
oldParent := WinAPI.Windows.SetParent(Button1.Handle, Form1.Handle);
// here, in fact, oldParent = Form1.Handle, so parent/child HWND
// relationship is correct by default.
Немного больше (для воспроизведения) - тот же сценарий Edit
вкладки сфокусированы на кнопку, обработчик завершения позволяет TTimer. Здесь форма отключена, но кнопка сохраняет фокус, хотя это, похоже, подтверждает, что Form1 HWND действительно является родительским окном кнопки, и он должен потерять фокус.
procedure TForm1.Timer1Timer(Sender: TObject);
var
h1, h2, h3 : cardinal;
begin
h1 := GetFocus; // h1 = Button1.Handle
h2 := GetParent(h1); // h2 = Form1.Handle
self.Enabled := false;
h3 := GetFocus; // h3 = Button1.Handle
end;
В случае, когда мы перемещаем кнопку в панель, все, кажется, работает (в основном), как и ожидалось. Панель отключена, и кнопка теряет фокус, но фокус затем переходит к родительской форме (WinAPI предполагает, что он должен быть NULL).
procedure TForm1.Timer1Timer(Sender: TObject);
var
h1, h2, h3 : cardinal;
begin
h1 := GetFocus; // h1 = Button1.Handle
h2 := GetParent(h1); // h2 = Panel1.Handle
Panel1.Enabled := false;
h3 := GetFocus; // h3 = Form1.Handle
end;
Часть проблемы, кажется, здесь - похоже, что верхняя форма сама берет на себя ответственность за дефокусировку элементов управления. Это работает, за исключением случаев, когда сама форма является отключенной:
procedure TWinControl.CMEnabledChanged(var Message: TMessage);
begin
if not Enabled and (Parent <> nil) then RemoveFocus(False);
// ^^ False if form itself is being disabled!
if HandleAllocated and not (csDesigning in ComponentState) then
EnableWindow(WindowHandle, Enabled);
end;
procedure TWinControl.RemoveFocus(Removing: Boolean);
var
Form: TCustomForm;
begin
Form := GetParentForm(Self);
if Form <> nil then Form.DefocusControl(Self, Removing);
end
Где
procedure TCustomForm.DefocusControl(Control: TWinControl; Removing: Boolean);
begin
if Removing and Control.ContainsControl(FFocusedControl) then
FFocusedControl := Control.Parent;
if Control.ContainsControl(FActiveControl) then SetActiveControl(nil);
end;
Это частично объясняет описанное выше поведение - фокус перемещается в родительский элемент управления, а активное управление теряет фокус. Он по-прежнему не объясняет, почему "EnableWindow" не удается убить фокус для дочернего окна кнопки. Это начинает казаться проблемой WinAPI...