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?
Asked Answered
G

4

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.

Gainly answered 14/10, 2016 at 18:6 Comment(11)
You need to host windows forms designer. If you need a simpler thing, take a look at this post.Lugworm
Have you tried adjusting the ButtonBorderStyle from Inset to Dashed? What you're describing would likely require you creating custom controls.Damle
See hereRearward
If you want to use a real design surface, you should follow the first link and host a windows forms designer. If you want to follow the post which handles WM_NCHITTEST, you should follow second link. 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.Lugworm
What's your idea about linked posts and what do you expect from an answer?Lugworm
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!Lugworm
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.Lockup
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]Lockup
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.Lockup
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.Lugworm
Just added a link to my other answer about Hosting Windows Forms Designer - Serialize and Deserialize designer at runtime, it's an example for Solution 1 as you can see in the screenshot.Lugworm
L
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();
    }
}
Lugworm answered 24/10, 2016 at 0:1 Comment(4)
Thank you @Reza Aghaei. I am really grateful that you took the time to understand the requirement and helped me solve this problem.Lockup
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 anwerLockup
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.Lugworm
Also for resizing an moving SelectionBorder control, you can use the solution which provided by Hans in this post.Lugworm
O
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.

Olshausen answered 14/10, 2016 at 18:6 Comment(1)
Thank you for the info, looks like I have still a long way to go before I get to the actual solution (^_^)Lockup
C
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 !!

Catechist answered 20/10, 2016 at 12:17 Comment(2)
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.Lugworm
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?Lockup
S
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;
}
Surveillance answered 18/10, 2016 at 3:52 Comment(5)
My response to your solution will be a little delayed due to a personal emergency, let me try this solution later on and get back. Thank you very much for your response.Lockup
It would be helpful if whoever downvoted my answer would at least have the common decency to leave a comment saying what the problem with it is.Surveillance
I've edited my answer to remove the message loop hooking. That's not necessary. The containing form is just drawing on it's own canvas. Also, it now highlights the active control grab handles.Surveillance
Hi Frank, sorry for the delay. I want to know if you can make the outer border as Hatch Pen only when the control is selected. Otherwise could it turn to no outer border?Lockup
For what it's worth, I've updated my answer to do this.Surveillance

© 2022 - 2024 — McMap. All rights reserved.