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

Posted

技术标签:

【中文标题】如何像表单设计器一样将控件置于设计状态模式?【英文标题】:How to put controls into a Design State Mode just like the Form Designer does? 【发布时间】:2015-06-12 06:03:50 【问题描述】:

这个问题让我困惑了一段时间,也许答案很简单,或者它涉及更多的 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 也不是我要寻找的答案,是的,行为有点相同,但当然控件的绘制方式不同,以显示它们被禁用,这不是我想要的。

【问题讨论】:

你试过csDesigning in ComponentState吗? 我对您的问题有了更好的理解,ComponentState 不一定是解决方案。 @JerryDodge 我看了ControlStyleControlState 无法得到任何工作,除非我做错了,而且我认为csDesigning 是只读的,以后需要再看一遍现在从我的机器上。 【参考方案1】:

您要查找的不是控件本身的功能,而是表单设计器本身的实现。在设计时,用户输入在任何给定控件处理之前被截获。 VCL 定义了一个CM_DESIGNHITTEST 消息,以允许每个控件指定它是否希望在设计时接收用户输入(例如,允许列表/网格列标题的可视化调整大小)。这是一项可选功能。

不过,您可以将所需的控件放在无边框的TPanel 上,然后根据需要简单地启用/禁用TPanel 本身。这将有效地启用/禁用其子控件的所有用户输入和动画。此外,当TPanel 被禁用时,子控件不会将自己呈现为禁用状态。

【讨论】:

我想你是对的,我认为我过于关注控制级别而没有考虑到 Delphi 表单设计器可能会拦截这些消息。使用像 TPanel 这样的容器控件也是一个好主意,只是这样会禁用所有子控件 - 而不是特定的子控件。 设计器不会“包装”每个控件。设计器将 IDesignerHook 接口的实现“附加”到设计的 TCustomForm。在 VCL 内部,每个控件将通过拥有 TCustomForm 的 IDesignerHook.IsDesignMsg() 引导每个 Windows 消息。如果该函数返回 True,则该消息已被设计者“处理”。这包括鼠标和键盘消息。 我发现使用 TPanel 的问题是当Enabled := False 面板不响应诸如 OnMouseDown 之类的事件 对,因为它被禁用了,所以它不会接收任何用户输入。但是,为什么面板需要 OnMouseDown 呢?您只是将它用作容器来帮助您启用/禁用子控件。 好吧,你总是可以编写一个实现Vcl.Forms.IDesignerHook的类,并在运行时将其分配给父Form的Designer属性,然后在ComponentState属性中启用csDesigning标志每个所需的控件,然后这些控件将调用您的 IsDesignMsg() 方法,就像它们在设计时为实际的表单设计器所做的那样。然后你可以对他们做任何你想做的事情。【参考方案2】:

我不确定这是否是您所追求的,但 Greatis 有一个表单设计器组件。见:http://www.greatis.com/delphicb/formdes/

【讨论】:

也许这是一个评论【参考方案3】:

Remy Lebeau 关于将控件放入容器(例如 TPanel)然后将面板设置为 Enabled := False 的回答确实将控件置于我正在寻找的状态。我还发现覆盖控件WM_HITTEST 会将控件置于相同的状态,例如它们不接收焦点并且不能与之交互。这两个的问题是控件仍然需要能够响应MouseDownMouseMoveMouseUp 事件等,但它们不再不能。

Remy 还建议编写一个类并实现 Vcl.Forms.IDesignerHook,我还没有尝试过,因为它可能需要太多工作才能满足我的需要。

无论如何,在玩了很多之后,我找到了另一种替代方法,它涉及使用PaintTo 将控件绘制到画布上。我做的步骤如下:

使用暴露的Canvas 创建自定义TPanelFormCreate 创建自定义面板并将其与客户端对齐 在运行时向表单添加控件(将自定义面板置于最前面) 在自定义面板上调用控件PaintTo 方法Canvas

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

因为控件位于面板下方,为了让它们响应MouseDownMouseMoveMouseUp 等事件,我覆盖了面板中的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 事件,那是因为我们实际上并没有查看控件,而是查看控件的图形副本。

【讨论】:

以上是关于如何像表单设计器一样将控件置于设计状态模式?的主要内容,如果未能解决你的问题,请参考以下文章

Windows 窗体设计器 - 如何防止删除子控件

驰骋表单设计器的外部接口

Windows窗体设计器会破坏表单布局

继承的控件不在表单设计器生成的代码中生成代码

使用框架 4.7.2 运行 SDK 项目的 vs2019 中的控件和表单缺少图标和视图设计器选项

基于UEditor上开发的表单设计器--自定义文本控件