在 C# Winforms 中,有没有一种方法可以在所有控件周围放置虚线边框,并在运行时选择特定控件时显示控制点?

In C# Winforms is there a way to put dotted border around all controls and show grip points upon selection of specific controls at runtime?

我在一个 IDE 类似于 Visual Studio 的团队中工作,为我们的本地客户开发自定义 Winform 代码。在我们的代码中,我们覆盖了用户控件以使我们的任务更容易,但我们的大部分控件都派生自基本的 C# Winform 控件。

我目前需要帮助在所有控件周围实现虚线边框,使用 Visual Studio 提供的抓点类型。

未选择的控件

选定控件

此功能需求量很大,因为它可以帮助在不补偿视觉准则的情况下进行对齐。

我们目前使用

在所有控件周围实现了深色边框
this.BackColor = Color.Black;
this.Height = ComboBox.Height + 4;

它在生成的控件周围放置了一个黑色边框,在上面的代码片段中是一个 ComboBox。

一位成员指出我们使用 Microsoft 文档中所示的边距和填充:https://msdn.microsoft.com/library/3z3f9e8b(v=vs.110)

但这主要是理论,似乎没有多大帮助。到目前为止,最接近解决这个问题的是在线 CodeProject link:

public class MyGroupBox : GroupBox
{
    protected override void OnPaint(PaintEventArgs e)
    {
    base.OnPaint(e);
    ControlPaint.DrawBorder(e.Graphics, ClientRectangle,
        Color.Black, BORDER_SIZE, ButtonBorderStyle.Inset,
        Color.Black, BORDER_SIZE, ButtonBorderStyle.Inset,
        Color.Black, BORDER_SIZE, ButtonBorderStyle.Inset,
        Color.Black, BORDER_SIZE, ButtonBorderStyle.Inset);
    } 
}

我很惊讶到目前为止没有找到与我的搜索相近的匹配项,也许我使用了错误的术语,因为我最近进入了这个领域的编程。

我相信,如果这个问题得到解决,未来的在线搜索将会受益。期待在这个问题上有经验的人指点。非常感谢这方面的任何帮助。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        hatchedPen = (Pen)SystemPens.ControlDarkDark.Clone();
        hatchedPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        // Clear any existing grab handles
        using (Graphics g = Graphics.FromHwnd(this.Handle))
        {

            foreach (Control ctrl in Controls)
            {
                var rect = GetGrabBounds(ctrl);
                g.FillRectangle(SystemBrushes.ButtonFace, rect);
            }
        }

        // Need to draw grab handles?
        if (ActiveControl != null && e.ClipRectangle.IntersectsWith(GetGrabBounds(ActiveControl)))
        {
            DrawGrabHandles(ActiveControl);
        }
    }

    private void DrawGrabHandles(Control ctrl)
    {
        using (Graphics g = Graphics.FromHwnd(this.Handle))
        {
            Rectangle bounds = GetGrabRect(ctrl);
            g.DrawRectangle(hatchedPen, bounds);
            foreach (Point pt in new Point[]
                {
                    new Point(bounds.Left, bounds.Top),
                    new Point(bounds.Left + bounds.Width / 2, bounds.Top),
                    new Point(bounds.Right, bounds.Top),
                    new Point(bounds.Left, bounds.Top + bounds.Height / 2),
                    new Point(bounds.Right, bounds.Top + bounds.Height / 2),
                    new Point(bounds.Left, bounds.Bottom),
                    new Point(bounds.Left + bounds.Width / 2, bounds.Bottom),
                    new Point(bounds.Right, bounds.Bottom),

                })
            {
                Rectangle r = new Rectangle(pt, new Size(5, 5));
                r.X = r.X - 2;
                r.Y = r.Y - 2;
                g.FillRectangle(SystemBrushes.ButtonFace, r);
                g.DrawRectangle(SystemPens.ControlDarkDark, r);
            }
        }
    }

    private static Rectangle GetGrabRect(Control ctrl)
    {
        var result = ctrl.Bounds;
        result = Rectangle.Inflate(result, 4, 4);
        result.X--;
        result.Y--;
        return result;
    }

    private static Rectangle GetGrabBounds(Control ctrl)
    {
        var result = GetGrabRect(ctrl);
        result.Inflate(4, 4);
        return result;
    }

    private Pen hatchedPen;
}

我已经创建了 windows 表格申请希望这对您有所帮助

后端 C# 代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication2
{

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.Paint += new PaintEventHandler(this_Paint);
        }
        private void this_Paint(object sender, PaintEventArgs e)
        {
            Pen pen = new Pen(Color.Green, 2.0F);
            pen.DashStyle = DashStyle.Dash;
            foreach (Control c in groupBox1.Controls)
            {
                e.Graphics.DrawRectangle(pen, (groupBox1.Location.X + c.Location.X)-1, (groupBox1.Location.Y + c.Location.Y)-1, c.Width + 2, c.Height + 2);
            }
            pen.Dispose();
        }
        private void Form1_Load(object sender, EventArgs e)
        {

        }
    }
}

设计器 C# 代码

namespace WindowsFormsApplication2
{
    partial class Form1
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.groupBox1 = new System.Windows.Forms.GroupBox();
            this.comboBox1 = new System.Windows.Forms.ComboBox();
            this.comboBox2 = new System.Windows.Forms.ComboBox();
            this.comboBox3 = new System.Windows.Forms.ComboBox();
            this.comboBox4 = new System.Windows.Forms.ComboBox();
            this.groupBox1.SuspendLayout();
            this.SuspendLayout();
            // 
            // groupBox1
            // 
            this.groupBox1.BackColor = System.Drawing.Color.Transparent;
            this.groupBox1.Controls.Add(this.comboBox4);
            this.groupBox1.Controls.Add(this.comboBox3);
            this.groupBox1.Controls.Add(this.comboBox2);
            this.groupBox1.Controls.Add(this.comboBox1);
            this.groupBox1.Location = new System.Drawing.Point(33, 36);
            this.groupBox1.Name = "groupBox1";
            this.groupBox1.Size = new System.Drawing.Size(193, 184);
            this.groupBox1.TabIndex = 0;
            this.groupBox1.TabStop = false;
            this.groupBox1.Text = "groupBox1";
            // 
            // comboBox1
            // 
            this.comboBox1.FormattingEnabled = true;
            this.comboBox1.Location = new System.Drawing.Point(36, 40);
            this.comboBox1.Name = "comboBox1";
            this.comboBox1.Size = new System.Drawing.Size(121, 21);
            this.comboBox1.TabIndex = 0;
            // 
            // comboBox2
            // 
            this.comboBox2.FormattingEnabled = true;
            this.comboBox2.Location = new System.Drawing.Point(36, 67);
            this.comboBox2.Name = "comboBox2";
            this.comboBox2.Size = new System.Drawing.Size(121, 21);
            this.comboBox2.TabIndex = 1;
            // 
            // comboBox3
            // 
            this.comboBox3.FormattingEnabled = true;
            this.comboBox3.Location = new System.Drawing.Point(36, 94);
            this.comboBox3.Name = "comboBox3";
            this.comboBox3.Size = new System.Drawing.Size(121, 21);
            this.comboBox3.TabIndex = 1;
            // 
            // comboBox4
            // 
            this.comboBox4.FormattingEnabled = true;
            this.comboBox4.Location = new System.Drawing.Point(36, 121);
            this.comboBox4.Name = "comboBox4";
            this.comboBox4.Size = new System.Drawing.Size(121, 21);
            this.comboBox4.TabIndex = 1;
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(284, 261);
            this.Controls.Add(this.groupBox1);
            this.Name = "Form1";
            this.Text = "Form1";
            this.Load += new System.EventHandler(this.Form1_Load);
            this.groupBox1.ResumeLayout(false);
            this.ResumeLayout(false);

        }

        #endregion

        private System.Windows.Forms.GroupBox groupBox1;
        private System.Windows.Forms.ComboBox comboBox1;
        private System.Windows.Forms.ComboBox comboBox4;
        private System.Windows.Forms.ComboBox comboBox3;
        private System.Windows.Forms.ComboBox comboBox2;
    }
}

Make GroupBox1 backgroundcolour 'Transparent' because I am drawing on Form not on GroupBox

您还可以通过添加 if(c is ComboBox)

在选定的控件上创建边框

if (c.Name == "comboBox1") 在 foreach 循环中

!!根据您的需要更改颜色!!

这里有些谨慎是适当的,在 Winforms 设计器之后建模 UI 设计器是一个容易的决定,实际实现它是一项可以让您忙上好几个月的工作。发现你不能在控制范围外绘画确实是你 运行 进入的第一个障碍,还有更多类似的障碍。

您可能考虑的第一个快捷方式是为控件绘制占位符,这样您就不会依赖控件 class。只要它不必看起来太像真实控件(即放弃所见即所得)并且您不必调整它们的大小,就可以正常工作。

但你肯定会忽略这一点。然后你必须做 Winforms 设计器做的同样的事情,你必须 overlay 一个透明的 window 在设计表面上。您可以在该覆盖层上绘制任何您想要的东西,并且它提供了自动鼠标和键盘隔离,因此控件本身完全忽略了设计时交互。在 this post and this post.

中查找此类叠加的示例

最后但同样重要的是,您也可以在自己的项目中利用现有的 Winforms 设计器。您必须实施 IDesignerHost。还有更多,不幸的是抽象级别相当高,MSDN 文档相当简短。最好的办法是从显示功能齐全的设计器的样本开始工作。 This KB article 有 link。代码非常好并且有据可查,您将获得一个几乎完整的设计器,其中包含工具箱和属性 window,其中 de/serializes 设计 from/to XML 并且可以生成 C# 和 VB.NET代码。不要看那些花哨的 UI,它不会启用视觉样式和颜色选择是我会选择的那种 :) 让它漂亮不是代码示例的重点。

I work in a team working on a IDE similar to Visual Studio ....

开发自定义表单设计器并非易事,需要大量知识和大量时间,我相信您可以使用的最佳解决方案是托管 windows 表单设计器。

这不仅仅是绘制选区边框:

  • 每个控件都有自己的具有特定功能的设计器,例如 MenuStrip 等某些控件有自己的设计器,使您可以 add/remove 设计器上的项目。
  • 控件可能有一些特定的大小和定位规则。例如,其中一些是自动调整大小的 TextBox 或停靠控件无法通过鼠标重新定位等等。
  • 组件在您的表单上不可见,您可能需要对其进行编辑。
  • 一些属性是设计时属性。
  • 一些属性是使用扩展器提供程序添加的,您需要执行额外的任务才能提供在自定义设计器中更改它们的方法。
  • 还有很多其他的考虑。

解决方案 1 - 托管 Windows Forms Designer

要了解有关设计时体系结构的更多信息,请查看 Design-Time Architecture。要在您的应用程序中托管 windows 表单设计器,您需要实现一些接口,例如 IDesignerHostIContainerIComponentChangeServiceIExtenderProviderITypeDescriptorFilterServiceIExtenderListServiceIExtenderProviderService

一些好的例子你可以看看:

您可能会发现此 post 有用:

post 包含有关如何在 运行 时托管 windows 表单设计器并生成代码的工作示例:

解决方案 2 - 在透明面板上绘制选择边框

虽然我强烈建议使用第一种解决方案,但出于学习目的,如果您想在控件周围绘制选择边框,您可以将要编辑的表单作为控件添加到宿主表单中,然后将窗体上方的透明面板。处理透明面板的 Click 事件并找到鼠标位置下的控件并在透明面板上围绕它绘制一个选择边框,如下所示:

在示例中,我只是创建了一个透明面板并绘制了选择边框。这只是一个示例,执行大小调整和定位超出了示例的范围。它只是向您展示如何在控件周围绘制选择边框。您还可以使用该想法创建一个 SelctionBorder 控件并在控件中封装大小和定位逻辑,而不是绘制边框,将 SelectionBorder 控件的实例添加到透明面板及其大小和定位中事件,改变相应的控制坐标。

请注意,这只是一个示例,在真实的设计器环境中,您应该考虑很多重要的事情。

透明面板

using System.Windows.Forms;
public class TransparentPanel : Panel
{
    const int WS_EX_TRANSPARENT = 0x20;
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ExStyle = cp.ExStyle | WS_EX_TRANSPARENT;
            return cp;
        }
    }
    protected override void OnPaintBackground(PaintEventArgs e)
    {
    }
}

主机表单

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
public partial class HostForm : Form
{
    private Panel containerPanel;
    private TransparentPanel transparentPanel;
    private PropertyGrid propertyGrid;
    public HostForm()
    {
        this.transparentPanel = new TransparentPanel();
        this.containerPanel = new Panel();
        this.propertyGrid = new PropertyGrid();
        this.SuspendLayout();
        this.propertyGrid.Width = 200;
        this.propertyGrid.Dock = DockStyle.Right;
        this.transparentPanel.Dock = System.Windows.Forms.DockStyle.Fill;
        this.transparentPanel.Name = "transparentPanel";
        this.containerPanel.Dock = System.Windows.Forms.DockStyle.Fill;
        this.containerPanel.Name = "containerPanel";
        this.ClientSize = new System.Drawing.Size(450, 210);
        this.Controls.Add(this.transparentPanel);
        this.Controls.Add(this.propertyGrid);
        this.Controls.Add(this.containerPanel);
        this.Name = "HostForm";
        this.Text = "Host";
        this.Load += this.HostForm_Load;
        this.transparentPanel.MouseClick += this.transparentPanel_MouseClick;
        this.transparentPanel.Paint += this.transparentPanel_Paint;
        this.ResumeLayout(false);
    }
    private void HostForm_Load(object sender, EventArgs e)
    {
        this.ActiveControl = transparentPanel;
        /**************************************/
        /*Load the form which you want to edit*/
        /**************************************/   
        var f = new Form(); 
        f.Location = new Point(8, 8);
        f.TopLevel = false;
        this.containerPanel.Controls.Add(f);
        SelectedObject = f;
        f.Show();
    }
    Control selectedObject;
    Control SelectedObject
    {
        get { return selectedObject; }
        set
        {
            selectedObject = value;
            propertyGrid.SelectedObject = value;
            this.Refresh();
        }
    }
    void transparentPanel_MouseClick(object sender, MouseEventArgs e)
    {
        if (this.Controls.Count == 0)
            return;
        SelectedObject = GetAllControls(this.containerPanel)
            .Where(x => x.Visible)
            .Where(x => x.Parent.RectangleToScreen(x.Bounds)
                .Contains(this.transparentPanel.PointToScreen(e.Location)))
            .FirstOrDefault();
        this.Refresh();
    }
    void transparentPanel_Paint(object sender, PaintEventArgs e)
    {
        if (SelectedObject != null)
            DrawBorder(e.Graphics, this.transparentPanel.RectangleToClient(
                SelectedObject.Parent.RectangleToScreen(SelectedObject.Bounds)));
    }
    private IEnumerable<Control> GetAllControls(Control control)
    {
        var controls = control.Controls.Cast<Control>();
        return controls.SelectMany(ctrl => GetAllControls(ctrl)).Concat(controls);
    }
    void DrawBorder(Graphics g, Rectangle r)
    {
        var d = 4;
        r.Inflate(d, d);
        ControlPaint.DrawBorder(g, r, Color.Black, ButtonBorderStyle.Dotted);
        var rectangles = new List<Rectangle>();
        var r1 = new Rectangle(r.Left - d, r.Top - d, 2 * d, 2 * d); rectangles.Add(r1);
        r1.Offset(r.Width / 2, 0); rectangles.Add(r1);
        r1.Offset(r.Width / 2, 0); rectangles.Add(r1);
        r1.Offset(0, r.Height / 2); rectangles.Add(r1);
        r1.Offset(0, r.Height / 2); rectangles.Add(r1);
        r1.Offset(-r.Width / 2, 0); rectangles.Add(r1);
        r1.Offset(-r.Width / 2, 0); rectangles.Add(r1);
        r1.Offset(0, -r.Height / 2); rectangles.Add(r1);
        g.FillRectangles(Brushes.White, rectangles.ToArray());
        g.DrawRectangles(Pens.Black, rectangles.ToArray());
    }
    protected override bool ProcessTabKey(bool forward)
    {
        return false;
    }
    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        this.Refresh();
    }
}