12

I work in a team working on a IDE similar to Visual Studio to develop custom Winform code for our local clients. In our code we have User Controls overridden to make our tasks easier but most of our controls are derived from basic C# Winform Controls.

I currently need help in implementing dotted border around all our controls, with the type of grip points as provided by Visual Studio.

Unselected Controls

enter image description here

Selected Controls

enter image description here

This feature is highly demanded as it can help in aligning without compensation on visual guidelines.

We have currently implemented a dark border around all controls, using

this.BackColor = Color.Black;
this.Height = ComboBox.Height + 4;

Which puts a black border around the generated Controls, which in the above code snippet is a ComboBox.

One member pointed us towards using Margins and Padding as shown in the Microsoft documentation: https://msdn.microsoft.com/library/3z3f9e8b(v=vs.110)

But this is mostly theory and does not seem to help much. the closest thing that has come to solve this problem so far has been an online 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);
    } 
}

I am surprized to not find a close match to my search so far, perhaps i am using the wrong terminology, as I recently got into programming in this domain.

I believe that future online searches are going to be benifitted, if this problem gets solved. Looking forward for pointers form those with experience in this problem. Really appreciate any help in this direction.

  • 4
    You need to [host windows forms designer](http://www.developerfusion.com/article/4351/hosting-windows-forms-designers/). If you need a simpler thing, take a look at [this post](http://stackoverflow.com/questions/39948820/adding-same-extensions-to-multiple-controls-in-winforms). – Reza Aghaei Oct 14 '16 at 18:29
  • Have you tried adjusting the ButtonBorderStyle from Inset to Dashed? What you're describing would likely require you creating custom controls. – Enfyve Oct 14 '16 at 18:30
  • See [here](http://stackoverflow.com/questions/17264225/how-can-user-resize-control-at-runtime-in-winforms) – TaW Oct 14 '16 at 19:20
  • If you want to use a real design surface, you should follow the [first link](http://www.developerfusion.com/article/4351/hosting-windows-forms-designers/) and host a windows forms designer. If you want to follow the post which handles `WM_NCHITTEST`, you should follow [second link](http://stackoverflow.com/questions/39948820/adding-same-extensions-to-multiple-controls-in-winforms). These are not all available options, you can also create a control which mimics that resize border behavior and connect it to your control then when the border resizes, your control will be resized too. – Reza Aghaei Oct 17 '16 at 12:16
  • What's your idea about linked posts and what do you expect from an answer? – Reza Aghaei Oct 17 '16 at 12:20
  • There can be different solutions for the problem but without describing more about the requirement and without answering comments any try for answering this question would be a shot in the dark! – Reza Aghaei Oct 20 '16 at 18:43
  • 1) I am sorry for responding to this comment chain so late, here are some more points... I do not have provision to host windows form designer, i will have to extend the project handed over to me, only a part of which I own. – Ganesh Kamath - 'Code Frenzy' Oct 21 '16 at 06:17
  • 2) I dont know about linked posts. I wanted to know if the solution to my problem had some simple solutioon, like adding some property to the control in the User Control Properties declaration which would enable a border at run time, so far, i think this is not the case, rather the case is to re-define a Pen option in the OnPaints method which is over-ridden for the sake of modifying looks [so complicated] – Ganesh Kamath - 'Code Frenzy' Oct 21 '16 at 06:20
  • 3) In the absence of a simple obvious property, I wanted to know if there was a way around implementing very complex code, because I will need to justify my answer when I implement the solution through this way. – Ganesh Kamath - 'Code Frenzy' Oct 21 '16 at 12:07
  • No problem. I believe it's a really good question but a good answer to the question would be very very long and is not suitable for stackoverflow. While I believe my first comment is what you are looking for, but as a reference for future readers, I added more description about hosting windows forms designer. Also I added another option (just for learning purpose) to draw borders around controls. You should know the designer works really more sophisticated. To learn more about how the designer works take a look at [this post](http://stackoverflow.com/a/32299687/3110834). – Reza Aghaei Oct 24 '16 at 00:27
  • Just added a link to my other answer about [Hosting Windows Forms Designer - Serialize and Deserialize designer at runtime](https://stackoverflow.com/a/59537245/3110834), it's an example for *Solution 1* as you can see in the screenshot. – Reza Aghaei Dec 31 '19 at 08:42

4 Answers4

7

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

Developing a custom form designer is not a trivial task and needs a lot of knowledge and a lot of time and I believe the best solution which you can use, is hosting windows forms designer.

It's not just about drawing selection borders:

  • Each control has it's own designer with specific features, for example some controls like MenuStrip has it's own designer which enables you to add/remove items on designer.
  • Controls may have some specific sizing and positioning rules. For example some of them are auto-sized like TextBox or docked controls can not be reposition by mouse and so on.
  • Components are not visible on your form which you may need to edit them.
  • Some properties are design-time properties.
  • Some properties are added using extender providers and you need to perform additional tasks to provide a way to change them in your custom designer.
  • And a lot of other considerations.

Solution 1 - Hosting Windows Forms Designer

To learn more about design time architecture, take a look at Design-Time Architecture. To host windows forms designer in your application, you need to implement some interfaces like IDesignerHost, IContainer, IComponentChangeService, IExtenderProvider, ITypeDescriptorFilterService, IExtenderListService, IExtenderProviderService.

For some good examples you can take a look at:

You may find this post useful:

The post contains a working example on how to host windows forms designer at run-time and generate code:

Solution 2 - Drawing selection border over a transparent panel

While I strongly recommend using the first solution, but just for learning purpose if you want to draw selection border around controls, you can add the forms which you want to edit as a control to the host form, then put a transparent panel above the form. Handle Click event of transparent Panel and find the control under mouse position and draw a selection border around it on transparent panel like this:

enter image description here

In the example, I just created a transparent panel and drew selection border. It's just an example and performing sizing and positioning is out of scope of the example. It's just to show you how you can draw selection border around controls. You also can use the idea to create a SelctionBorder control and encapsulate sizing and positioning logic in the control and instead of drawing the borders, add an instance of SelectionBorder control to transparent panel and in its sizing and positioning events, change corresponding control coordinates.

Please pay attention it's just an example and in a real designer environment you should consider a lot of important things.

Transparent Panel

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)
    {
    }
}

Host Form

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();
    }
}
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • Thank you @Reza Aghaei. I am really grateful that you took the time to understand the requirement and helped me solve this problem. – Ganesh Kamath - 'Code Frenzy' Oct 24 '16 at 05:14
  • If you dont mind, could you briefly explain what sizing and positioning would require? I read that it is out of scope of this example (which I agree completely), but just checking if there is a simple extension to this anwer – Ganesh Kamath - 'Code Frenzy' Oct 24 '16 at 05:21
  • You're welcome. I should emphasize I'll use the first solution in a real environment and as I said the second solution which I shared is just for learning purpose. To add sizing and moving support you can use the idea to create a `SelctionBorder` control and encapsulate sizing and positioning logic in the control and instead of drawing the borders, add an instance of `SelectionBorder` control to transparent panel and in its sizing and positioning events, change corresponding control coordinates. It's enough to handle `MouseDown`, `MouseMove` and `MouseUp` events. – Reza Aghaei Oct 24 '16 at 09:06
  • Also for resizing an moving `SelectionBorder` control, you can use the solution which provided by Hans in [this post](http://stackoverflow.com/questions/17264225/how-can-user-resize-control-at-runtime-in-winforms). – Reza Aghaei Oct 24 '16 at 09:08
2

Some caution would be appropriate here, modeling a UI designer after the Winforms designer is an easy decision, actually implementing it is a job that can keep you occupied for many months. Discovering that you cannot paint outside the control bounds is indeed the very first obstacle you'll run into, many more like that.

The first shortcut you might consider is to draw place-holders for the controls so you don't depend on the Control class. Works fine as long as it doesn't have to look too closely like the real control (i.e. give up on WYSIWYG) and you don't have to resize them.

But you'll surely dismiss that. You then have to do the same thing the Winforms designer does, you have to overlay a transparent window on top of the design surface. You can draw anything you want on that overlay and it provides automatic mouse and keyboard isolation so the control itself is completely oblivious of the design-time interaction. Find examples of such an overlay in this post and this post.

Last but not least, it is worth mentioning that you can leverage the existing Winforms designer in your own projects as well. You have to implement IDesignerHost. And a bunch more, unfortunately the abstraction level is fairly high and the MSDN docs rather brief. Best thing to do is to work from a sample that shows a full-featured designer. This KB article has the link. Code is excellent and well documented, you get an almost complete designer with toolbox and Properties window which de/serializes the design from/to XML and can generate C# and VB.NET code. Do look past the garish UI, it doesn't enable Visual Styles and color choices are the kind that I would make :) Making it pretty wasn't the point of the code sample.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
1

I Have Created I windows Form Application Hope this will Help you

BackEnd C# Code

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)
        {

        }
    }
}

Designer C# Code

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;
    }
}

enter image description here

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

You Can also Create Border on Selected Controls by Adding if(c is ComboBox)

or if (c.Name == "comboBox1") in foreach loop

!! Change the Color According to your Need !!

Zulqarnain Jalil
  • 1,679
  • 16
  • 26
  • The solution may be useful just in cases which you want to draw border of some controls over parent control. For example layout some combo boxes to overlap each other and see the result. – Reza Aghaei Oct 20 '16 at 19:02
  • Thank you for your effort, I wanted the border to appear when I selected the control, it seems to be a permanent border, is it possible for you to help me out in making the changes needed for it to change only when selected? – Ganesh Kamath - 'Code Frenzy' Oct 21 '16 at 12:38
0
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;
}
Frank Hagenson
  • 953
  • 8
  • 17