In C# Winforms How to Put Dotted Border Around All Controls and Show Grip Points Upon Selection of Specific Controls at Runtime

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?

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:

  • Hosting Windows Forms Designers by Tim Dawson
  • Tailor Your Application by Building a Custom Forms Designer with .NET by Sayed Y. Hashimi

You may find this post useful:

  • Hosting Windows Forms Designer - Serialize and Deserialize designer at runtime

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

Sample Image

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:

Sample Image

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

Invisible component above control to filter input

it seems that the solution has to do with to overriding the following methods:

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

in the invisible panel class I've made as mention by the comments in this example

Hosting Windows Forms Designer - Serialize designer at runtime and generate C# code

Assuming you have a DesignSurface to show a Form as root component of the designer and having some components created at run-time by using CreateComponent method of IDesignerHost, here is how I approach the problem:

  • Get an instance of IDesignerHost from DesignSurface
  • Create new DesignerSerializationManager
  • Get an instance of TypeCodeDomSerializer from serialization manager
  • Serialize the RootComponent of the IDesignerHost
  • Create an instance of CSharpCodeProvider
  • Generate code by calling GenerateCodeFromType and passing the serialized root component.

You can also extend the example a bit and use ISelectionService to get notified about selected components and change properties at run-time using a PropertyGrid:

Sample Image

Example - Generate C# code from DesignSurface at runtime

Here in this example, I'll show how you can host a windows forms designer at run-time and design a form containing some controls and components and generate C# code at run-time and run the generated code.

Please note: It's not a production code and it's just an example as a
proof of concept.

Create the DesignSurface and host the designer

You can create the design surface like this:

DesignSurface designSurface;
private void Form1_Load(object sender, EventArgs e)
{
designSurface = new DesignSurface(typeof(Form));
var host = (IDesignerHost)designSurface.GetService(typeof(IDesignerHost));
var root = (Form)host.RootComponent;
TypeDescriptor.GetProperties(root)["Name"].SetValue(root, "Form1");
root.Text = "Form1";

var button1 = (Button)host.CreateComponent(typeof(Button), "button1");
button1.Text = "button1";
button1.Location = new Point(8, 8);
root.Controls.Add(button1);

var timer1 = (Timer)host.CreateComponent(typeof(Timer), "timer1");
timer1.Interval = 2000;
var view = (Control)designSurface.View;
view.Dock = DockStyle.Fill;
view.BackColor = Color.White;
this.Controls.Add(view);
}

Generate C# code using TypeCodeDomSerializer and CSharpCodeProvider

This is how I generate code from design surface:

string GenerateCSFromDesigner(DesignSurface designSurface)
{
CodeTypeDeclaration type;
var host = (IDesignerHost)designSurface.GetService(typeof(IDesignerHost));
var root = host.RootComponent;
var manager = new DesignerSerializationManager(host);
using (manager.CreateSession())
{
var serializer = (TypeCodeDomSerializer)manager.GetSerializer(root.GetType(),
typeof(TypeCodeDomSerializer));
type = serializer.Serialize(manager, root, host.Container.Components);
type.IsPartial = true;
type.Members.OfType<CodeConstructor>()
.FirstOrDefault().Attributes = MemberAttributes.Public;
}
var builder = new StringBuilder();
CodeGeneratorOptions option = new CodeGeneratorOptions();
option.BracingStyle = "C";
option.BlankLinesBetweenMembers = false;
using (var writer = new StringWriter(builder, CultureInfo.InvariantCulture))
{
using (var codeDomProvider = new CSharpCodeProvider())
{
codeDomProvider.GenerateCodeFromType(type, writer, option);
}
return builder.ToString();
}
}

For example:

var code = GenerateCSFromDesigner(designSurface);

Run the code sing CSharpCodeProvider

Then to run it:

void Run(string code, string formName)
{
var csc = new CSharpCodeProvider();
var parameters = new CompilerParameters(new[] {
"mscorlib.dll",
"System.Windows.Forms.dll",
"System.dll",
"System.Drawing.dll",
"System.Core.dll",
"Microsoft.CSharp.dll"});
parameters.GenerateExecutable = true;
code = $@"
{code}
public class Program
{{
[System.STAThread]
static void Main()
{{
System.Windows.Forms.Application.EnableVisualStyles();
System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
System.Windows.Forms.Application.Run(new {formName}());
}}
}}";
var results = csc.CompileAssemblyFromSource(parameters, code);
if (!results.Errors.HasErrors)
{
System.Diagnostics.Process.Start(results.CompiledAssembly.CodeBase);
}
else
{
var errors = string.Join(Environment.NewLine,
results.Errors.Cast<CompilerError>().Select(x => x.ErrorText));
MessageBox.Show(errors);
}
}

For example:

Run(GenerateCSFromDesigner(designSurface), "Form1");

Adding same extensions to multiple controls in winforms

You can encapsulate the logic in a helper class deriving from NativeWindow class. This way you can do the job without creating a derived class for each control which you want to move/resize.

You can pass the control which you want to extend to the constructor of helper class and assign your control handle to the native window. Then overriding WndProc of native window will handle messages of the control.

Also other stuffs like handling event of the control is simply possible by keeping a reference to the control which you passed in constructor and assigning event handlers.

After creating such native window helper class the usage would be:

var h1 = new LiveControlHelper(this.pictureBox1);
var h2 = new LiveControlHelper(this.button1);

Or you can use the helper for all controls of a container in a loop.

Example

In below example, I refactored the code which you posted. This way you can use the code for all controls without need to inheritance.

using System;
using System.Drawing;
using System.Windows.Forms;
public class LiveControlHelper : NativeWindow
{
private Control control;
private Point cur = new Point(0, 0);
private const int grab = 16;
public LiveControlHelper(Control c)
{
control = c;
this.AssignHandle(c.Handle);
control.MouseDown += (s, e) => { cur = new Point(e.X, e.Y); };
control.MouseMove += (s, e) => {
if (e.Button == MouseButtons.Left) {
Control x = (Control)s;
x.SuspendLayout();
x.Location = new Point(x.Left + e.X - cur.X, x.Top + e.Y - cur.Y);
x.ResumeLayout();
}
};
control.Paint += (s, e) => {
var rc = new Rectangle(control.ClientSize.Width - grab,
control.ClientSize.Height - grab, grab, grab);
ControlPaint.DrawSizeGrip(e.Graphics, control.BackColor, rc);
};
control.Resize += (s, e) => { control.Invalidate(); };
}
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == 0x84) {
var pos = control.PointToClient(new Point(m.LParam.ToInt32() & 0xffff,
m.LParam.ToInt32() >> 16));
if (pos.X >= control.ClientSize.Width - grab &&
pos.Y >= control.ClientSize.Height - grab)
m.Result = new IntPtr(17);
}
}
}

Note

1- To be able to revert the control to its normal state (non-resizable non-movable) it's better to assign event handlers using methods and not using lambda. You need to revome event handlers to revert the control to its normal state. Also to do so, you need to call DestroyHanlde method of helper class.

2- I just refactored the posted code to make it reusable for controls without need to implement a derived version of all controls. But you can enhance the code by:

  • Enable moving the control and resizing it using surface, edges and corners of control by setting m.Result to suitable values.

  • Draw grab borders/handlers on control.

3- If you need to call SetStyle on control, you can simply use an extension method from this post and call it this way:

control.SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.ResizeRedraw, true);

How to Delete selected control in Winforms C#?

Firstly you need somewhere to keep track of the controls that were "selected" (clicked). So add this to the form's codebehind:

List<Control> _itemsToDelete = new List<Control>();

And you need a flag to indicate whether the user has activated garbage mode:

bool _garbageMode = false;

To activate garbage mode:

async void GarbageMode_Click(object sender, EventArgs e)
{
_garbageMode = true;
}

Now when they "select" a control you add it to the list:

async void Control_Click(object sender, EventArgs e)
{
if (_garbageMode)
{
_itemsToDelete.Add((Control)sender);
}
}

Then to delete

foreach (var control in _itemsToDelete)
{
this.Controls.Remove(control);
control.Dispose();
}
_itemsToDelete.Clear();


Related Topics



Leave a reply



Submit