如何将控件置于设计状态模式中,就像表单设计器一样?

4
这个问题已经困扰我一段时间了,也许答案很简单,或者需要更多的VCL技巧或魔法来实现我想要的,但无论如何,我都不知道如何解决我的问题。
如果您查看Delphi表单设计器,您会发现当鼠标移动到它们上面时,控件都不会有任何动画效果,也不能接收焦点或输入(例如您不能在TEdit中键入内容、点击TCheckBox或移动TScrollBar等),只有在运行时,控件才能正常地响应用户交互。
我想知道如何在运行时实现此类行为,例如将控件设置为像设计状态模式一样。但是,控件仍然应该响应鼠标事件,例如OnMouseDownOnMouseMoveOnMouseUp等,以便可以根据需要移动和调整大小。
这是我最接近的实现:
procedure SetControlState(Control: TWinControl; Active: Boolean);
begin
  SendMessage(Control.Handle, WM_SETREDRAW, Ord(Active), 0);
  InvalidateRect(Control.Handle, nil, True);
end;

可以简单地称之为:
procedure TForm1.chkActiveClick(Sender: TObject);
begin
  SetControlState(Button1, chkActive.Checked);
  SetControlState(Button2, chkActive.Checked);
  SetControlState(Edit1, chkActive.Checked);
end;

例如,表单上的所有控件:
procedure TForm1.chkActiveClick(Sender: TObject);
var
  I: Integer;
  Ctrl: TWinControl;
begin
  for I := 0 to Form1.ControlCount -1 do
  begin
    if Form1.Controls[I] is TWinControl then
    begin
      Ctrl := TWinControl(Form1.Controls[I]);
      if (Ctrl <> nil) and not (Ctrl = chkActive) then
      begin
        SetControlState(Ctrl, chkActive.Checked);
      end;
    end;
  end;
end;

我注意到以上代码存在两个问题:尽管控件似乎变成了设计状态,但某些控件(如TButton)仍然具有动画效果。另一个问题是,当控件处于设计状态时,按下左Alt键会导致它们消失。
所以我的问题是,我如何在运行时将控件放入类似Delphi表单设计器的设计状态模式中,其中这些控件不会动画(基于Windows主题),也不能接收焦点或输入?
为了使这一点更清楚,请看一下基于上面的代码示例的此示例图像,其中控件不再处于活动状态,但TButton的动画绘制仍处于活动状态:
但实际上应该是:
从上面的两个图像中,只有TCheckBox控件可以交互。
是否有一个隐藏的程序可以改变控件的状态?或者可能更适合实现这一点的方法?迄今为止,我设法获得的代码只会出现更多问题。
将控件设置为Enabled := False也不是我正在寻找的答案,是的,行为有点相同,但是控件的绘制方式当然会不同以显示它们已被禁用,这不是我要找的。

1
你尝试过使用 ComponentState 中的 csDesigning 吗? - Jerry Dodge
我刚刚更好地理解了你的问题,“ComponentState”并不一定是解决方案。 - Jerry Dodge
@JerryDodge 我看了一下 ControlStyleControlState,除非我做错了什么,否则无法让它们正常工作。此外,我认为 csDesigning 是只读的,因为我现在离开了我的机器,所以稍后需要再次查看。 - Craig
3个回答

5
您要查找的并不是控件本身的功能,而是表单设计器本身的实现。在设计时,用户输入会在任何给定控件处理之前被拦截。VCL定义了一个CM_DESIGNHITTEST消息,允许每个控件指定它是否想在设计时接收用户输入(例如允许视觉调整列表/网格列标题)。这是一项自愿选择的功能。
但您可以将所需的控件放置在无边框的TPanel上,然后根据需要简单地启用/禁用TPanel本身。这将有效地启用/禁用其子控件的所有用户输入和动画。此外,在禁用TPanel时,子控件将不会呈现为已禁用状态。

我想你是对的,我认为我过于专注于控制级别,没有考虑到 Delphi 表单设计器可能会截取这些消息。使用像 TPanel 这样的容器控件也是一个好主意,只是这会禁用所有子控件 - 而不是特定的控件。 - Craig
4
设计师不会“包装”每个控件。设计师会向设计的 TCustomForm“附加”一个 IDesignerHook 接口的实现。在 VCL 的内部,每个控件将通过拥有者 TCustomForm 的 IDesignerHook.IsDesignMsg() 将每个 Windows 消息传递。如果该函数返回 True,则消息已由设计师“处理”。这包括鼠标和键盘消息。 - Allen Bauer
我发现使用 TPanel 的问题是,当 Enabled := False 时,面板不会响应 OnMouseDown 等事件。 - Craig
对,因为它被禁用了,所以它不会接收任何用户输入。但是你为什么需要在面板上使用OnMouseDown呢?你只是将其作为一个容器来帮助你启用/禁用子控件。 - Remy Lebeau
好的,虽然使用容器(如面板)是一个可行的解决方案,并且基本上可以实现我所要求的功能,但我应该包括这样一个事实:我需要能够响应鼠标消息。这可能是非用户交互混淆的地方,我的意思是用户不能按按钮或单击像TEdit这样的控件,而是仍然应该能够选择控件,就像Delphi表单设计器一样(这样我就可以移动和调整它们)。我不需要一个表单设计器,只需要控件的行为像Delphi表单设计器控件一样(设计状态)。 - Craig
1
你可以编写一个实现了 Vcl.Forms.IDesignerHook 接口的类,并在运行时将其分配给父窗体的 Designer 属性,然后在每个所需控件的 ComponentState 属性中启用 csDesigning 标志,这样这些控件就会像设计时的实际窗体设计器一样调用你的 IsDesignMsg() 方法。然后你可以对它们进行任何操作。 - Remy Lebeau

1
Remy Lebeau的回答提到将控件放入容器(例如TPanel),然后设置面板为Enabled := False可以使控件处于我要求的状态。我还发现,覆盖控件的WM_HITTEST也会使控件处于相同的状态,即它们不接收焦点,也不能与之交互。这两种方法的问题在于,控件仍然需要能够响应MouseDownMouseMoveMouseUp等事件,但它们不再能够响应。
Remy还建议编写一个类并实现Vcl.Forms.IDesignerHook,这是我尚未尝试过的,因为可能需要太多的工作。
无论如何,在进行了大量尝试之后,我找到了另一种替代方法,它涉及使用PaintTo将控件绘制到画布上。我所做的步骤如下:
  • 创建具有公开Canvas的自定义TPanel
  • 在FormCreate中创建并将自定义面板对齐到客户端
  • 在运行时向表单添加控件(将自定义面板置于最前)
  • 调用控件的PaintTo方法到自定义面板的Canvas上

本质上,这是创建组件并使用表单作为父级,我们的自定义面板位于顶部。然后,控件被绘制到面板的画布上,这使得它看起来像控件在面板上,实际上它坐落在表单下方而不受干扰。

因为控件位于面板下方,为了使它们响应诸如MouseDown、MouseMove和MouseUp等事件,我在面板中重写了WM_NCHitTest并将结果设置为HTTRANSPARENT。

代码如下:

自定义面板:

type
  TMyPanel = class(TPanel)
  protected
     procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHitTest;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    property Canvas;
  end;

{ TMyPanel }

constructor TMyPanel.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  Align := alClient;
  BorderStyle := bsNone;
  Caption := '';
end;

destructor TMyPanel.Destroy;
begin
  inherited Destroy;
end;

procedure TMyPanel.WMNCHitTest(var Message: TWMNCHitTest);
begin
  Message.Result := HTTRANSPARENT;
end;

表单:

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    FMyPanel: TMyPanel;
    procedure ControlMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
  public
    { Public declarations }
  end;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  FMyPanel := TMyPanel.Create(nil);
  FMyPanel.Parent := Form1;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FMyPanel.Free;
end;

procedure TForm1.ControlMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Sender is TWinControl then
  begin
    ShowMessage('You clicked: ' + TWinControl(Sender).Name);
  end;
end;

在表单中添加TButton的示例:
procedure TForm1.Button1Click(Sender: TObject);
var
  Button: TButton;
begin
  Button := TButton.Create(Form1);
  Button.Parent := Form1;

  FMyPanel.BringToFront;

  with Button do
  begin
    Caption := 'Button';
    Left := 25;
    Name := 'Button';
    Top  := 15;
    OnMouseDown := ControlMouseDown;

    PaintTo(FMyPanel.Canvas, Left, Top);
    Invalidate;
  end;
end;

如果您尝试运行上述代码,您会发现我们创建的 TButton 没有动画效果或接收焦点,但是它可以响应我们在上面附加的 MouseDown 事件,这是因为我们实际上并没有查看控件,而是在查看控件的图形副本。

0

1
也许这是作为注释意思的。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接