Отключение формы по-прежнему позволяет элементам управления childs получать вход

У меня много головной боли в последние дни с delphi, что я пытаюсь сделать, это очень просто, заблокировать интерфейс в какой-то точке и включить после некоторой другой точки.

Но так же, как это звучит, я не мог понять, почему что-то разрешено дизайном, поэтому уточнить:

1) создать проект

2) в форме положить редактирование и кнопку, порядок вкладок редактирования должен быть первым

3) настройте событие OnExit для редактирования и записи:

Enabled := False; 

4) настройте событие OnClick кнопки и напишите:

ShowMessage('this is right?');

в основном это он, теперь скомпилируйте, фокус будет на редактировании, нажмите вкладку, и форма будет отключена по мере того, как мы потребовали, поэтому в соответствии с порядком табуляции следующим элементом управления, чтобы получить фокус, является кнопка (но мы отключить форму), теперь нажмите пробел, и сообщение должно появиться.

так что вопрос: это правильно? Каково логическое объяснение этого поведения?

спасибо заранее.

Ответ 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...