Introduction
While experimenting with Linux programming and the Qt designer, we found it interesting to port to C# the QLayout component which allows to create a layout container that automatically arranges its components on resizing. It is not in a stage where it can be used, and perhaps you will find it a bit difficult, but we still wanted to post it in order to get feedback and to show how to add components, design time behavior and functionality. Let us cross the Rubicon.
Create the Controls
First, we will create our QLayout and QSpacer components. The QLayout is the container component, while the QSpacer is just a contained component. When the QSpacer appears among the QLayout contained components, it will absorb the extra space on resizing. Our intention is only to create some similar behavior rather than cloning the QLayout component.
- Click File -> New -> Project. Create a QtLikeLayout Visual C# Class Library Project
- Delete Class1.cs from QtLikeLayout project in Solution Explorer
- In Solution Explorer, right Click QtLikeLayout project -> Add -> Add Component
- Add a QLayout.cs component class
- Switch to source code. (Ctrl+Alt+0)
Change the inheritance to System.Windows.Forms.Panel
public class QLayout : System.Windows.Forms.Panel
But in order to inherit from Forms.Panel we need to add System.Windows.Forms to our references:
- In Solution Explorer, Right click
QtLikeLayout References -> Add reference.
- In .NET tab, select System.Windows.Forms.dll click Ok.
To the using directives on top of the QLayout.cs file, add the following code:
using System.Windows.Forms;
Add a Property
In order to display the Horizontal and Vertical layout behavior of the QLayout component, we need to add a CmpLayout property exposed in designing mode, in the property editor. Given below is an explanation of how this can be done.
First we add a QLayoutProperty enumeration, just before the QLayout class definition:
public enum QLayoutProperty
{
Horizontal,
Vertical
}
We need a private property pHLayout to hold the value, and a public method CmpLayout to expose it to the designer property editor, so we add this code to the QLayout class properties and methods:
private QLayoutProperty pHLayout = QLayoutProperty.Horizontal;
[
Category("Layout"),
Description("Controls layout arrangement."),
DefaultValue(QLayoutProperty.Horizontal)
]
public QLayoutProperty CmpLayout
{
get
{
return pHLayout;
}
set
{
pHLayout = value;
this.OnLayout(new LayoutEventArgs(this,""));
}
}
When we add a custom property to a Control, it appears on the property editor at design time. But in the Miscellaneous section, we indicate a Category, Description and Default value Attribute to specify the category in which the property or event will be displayed in the visual designer, and the description and the default value it will get. Note that when we specify a default value, the designer does not add code to initialize the control. So we must be sure to initialize it in the code, either in the constructor or by assigning it a value in the declaration as we have done, and ensure that this value is the same we put in the DefaultValue attribute. In order to set the way controls are justified, we need to add a CtrlsDock property exposed in designing mode, in the property editor. Here is how this can be done.
Again we add a QDockProperty enumeration, just before the QLayout class definition:
public enum QDockProperty
{
Fill, Side, Center, UpSide }
But we also need a private property
pDock to hold the value, and a public method
CtrlsDock to expose it to the designer property editor, so we add this code to the
QLayout class properties and methods:
private QDockProperty pDock = QDockProperty.Center;
[
Category("Layout"),
Description("Controls justification."),
DefaultValue(QDockProperty.Center)
]
public QDockProperty CtrlsDock
{
get
{
return pDock;
}
set
{
pDock = value;
this.OnLayout(new LayoutEventArgs(this,""));
}
}
Now, let us program the main behavior. We do the Layout arrangement by overriding the
OnLayout Panel inherited component method:
protected override void OnLayout(LayoutEventArgs levent)
{
base.OnLayout (levent);
this.SuspendLayout();
if (pHLayout == QLayoutProperty.Horizontal)
{
int mHeight = this.Height / 2;
int ctrlCount = this.Controls.Count;
int mWidth = (ctrlCount != 0) ? this.Width / ctrlCount : 0;
int pLeft = 0;
int cntSpace = 0;
int ctrlWidths = 0;
foreach (Control ctrl in this.Controls)
{
if (ctrl.GetType().Name == "QSpacer")
cntSpace += 1;
else
ctrlWidths += ctrl.Width;
}
foreach (Control ctrl in this.Controls)
{
if (cntSpace == 0)
{
ctrl.Width = mWidth;
ctrl.Left = pLeft;
ctrlCount --;
pLeft += ctrl.Width;
}
else
{
if (ctrl.GetType().Name == "QSpacer")
{
ctrl.Width = (this.Width - ctrlWidths) / cntSpace;
((QSpacer)ctrl).CmpLayout = QLayoutProperty.Horizontal;
}
ctrl.Left = pLeft;
pLeft += ctrl.Width;
}
switch (this.pDock)
{
case QDockProperty.Fill:
ctrl.Top = 0;
ctrl.Height = this.Height;
break;
case QDockProperty.Side:
ctrl.Top = 0;
break;
case QDockProperty.Center:
ctrl.Top = mHeight - ctrl.Height / 2;
break;
case QDockProperty.UpSide:
ctrl.Top = this.Height - 1 - ctrl.Height;
break;
}
}
}
else
{
int ctrlCount = this.Controls.Count;
int mHeight = this.Height / (ctrlCount + 1);
int mWidth = this.Width / 2;
int pTop = 0;
int ctrlsHeight = 0;
int cntSpace = 0;
int ctrlHeights = 0;
foreach (Control ctrl in this.Controls)
{
if (ctrl.GetType().Name == "QSpacer")
cntSpace += 1;
else
ctrlHeights += ctrl.Height;
ctrlsHeight += ctrl.Height + 1;
}
int sHeight = (this.Height - ctrlHeights) /
(this.Controls.Count + 1);
foreach (Control ctrl in this.Controls)
{
if (cntSpace !=0)
{
if (ctrl.GetType().Name == "QSpacer")
{
ctrl.Height = (this.Height - ctrlHeights) / cntSpace;
((QSpacer)ctrl).CmpLayout = QLayoutProperty.Vertical;
}
}
else
pTop += sHeight;
switch (this.pDock)
{
case QDockProperty.Fill:
ctrl.Left = 0;
ctrl.Width = this.Width;
break;
case QDockProperty.Side:
ctrl.Left = 0;
break;
case QDockProperty.Center:
ctrl.Left = mWidth - ctrl.Width / 2;
break;
case QDockProperty.UpSide:
ctrl.Left = this.Width - 1 - ctrl.Width;
break;
}
ctrl.Top = pTop;
pTop += ctrl.Height;
}
}
this.ResumeLayout();
}
This method generates the automatic arrangement of the contained controls. In the case of Horizontal Layout, we get all components laid out horizontally one after another, and all of them absorb the width of the QLayout panel by resizing. If one or more QSpacers are found, they absorb the extra size and the components are left at the size that they are. The controls are aligned at the center or top or bottom according to the CtrlsDock justification. In the case of Vertical Layout, we get all components laid out vertically one above the other. Here, their distance is proportional to the space left, but they retain the original width. If one or more QSpacers are found, the components are aligned one above the other without any space between them, but the spaces absorb it by resizing. Now lets create the QSpacer control:
- In Solution Explorer, right Click QtLikeLayout project -> Add -> Add Component.
- Add a QSpacer.cs component class.
- Switch to source code. (Ctrl+Alt+0)
Change the inheritance to
System.Windows.Forms.Control
public class QSpacer : System.Windows.Forms.Control
Add a pHLayout private property and a CmpLayout method, to the QSpacer class methods and properties:
private QLayoutProperty pHLayout = QLayoutProperty.Horizontal;
[
Category("Layout"),
Description("Spacer layout arrangement."),
DefaultValue(QLayoutProperty.Horizontal)
]
public QLayoutProperty CmpLayout
{
get
{
return pHLayout;
}
set
{
if ((pHLayout != value) && (DesignMode))
{
pHLayout = value;
if (pHLayout != QLayoutProperty.Horizontal)
this.Width = 23;
else
this.Height = 23;
this.Invalidate();
}
}
}
The controls are usable as they are, but we want to go a step further and give the user feedback as well as more posibilities at design time.
Add a Toolbox Icon
First we will set an image for the controls to show when they appear in the toolbox window:
- In Solution Explorer, right Click QtLikeLayout project -> Add -> Add New Item.
- Expand Local Project Items -> Resources.
- Add a QLayout.bmp Bitmap resource. (Note the name is the same as the control Class)
- Right click QLayout.bmp -> Properties. Set the Build Action property to Embedded Resource.
- Add also a QSpacer.bmp Bitmap resource.
- Right click QSpacer.bmp -> Properties. Set the Build Action property to Embedded Resource.
- Edit the QLayout.bmp and QSpacer.bmp as you like. You must set its size to 16x16.
Push (F7) to build the solution.
Create a Control Designer
Now, we will create a
QSpacerComponentDesigner Control Designer for extending the design mode behavior of our
QSpacer control. We will use it to draw it like a spring just as the Qt Designer does at design time.
- In Solution Explorer, right Click the QtLikeLayout project -> Add ->Add Class.
- Add a QSpacerComponentDesigner.cs class.
Change the inheritance to System.Windows.Forms.Panel:
public class QSpacerComponentDesigner : System.Windows.Forms.Design.ControlDesigner
We need to add System.Design.dll and System.Drawing.dll to our references:
- In Solution Explorer, Right click
QtLikeLayout References -> Add reference.
- In .NET tab, select System.Design.dll and System.Drawing.dll click Ok.
To the using directives on top of the QSpacerComponentDesigner.cs file, add the following code:
using System.Windows.Forms;
using System.Drawing;
Now we will override the WndProc for handling the resizing and OnPaintAdornments method in order to draw a spring like appearance on designing. Double click the startButton button. Replace the startButton_Click method with:
protected override void WndProc(ref Message m)
{
base.WndProc (ref m);
const int WM_SIZE = 0x0005;
if (m.Msg == WM_SIZE)
this.Control.Invalidate();
}
protected override void OnPaintAdornments(PaintEventArgs pe)
{
base.OnPaintAdornments (pe);
int bias = 3;
Point[] lines = new Point[10];
if (((QSpacer)this.Control).CmpLayout == QLayoutProperty.Horizontal)
{
int wInc = this.Control.Width / (lines.Length-1);
int wX = 0;
for(int i=0; i<lines.Length; i++)
{
lines[i].X = wX;
wX += wInc;
lines[i].Y = (i % 2 == 0 ? -1 : 1) *
(this.Control.Height / bias) + (this.Control.Height / 2);
}
}
else
{
int hInc = this.Control.Height / (lines.Length-1);
int hY = 0;
for(int i=0; i<lines.Length; i++)
{
lines[i].Y = hY;
hY += hInc;
lines[i].X = (i % 2 == 0 ? -1 : 1) *
(this.Control.Width / bias) + (this.Control.Width / 2);
}
}
pe.Graphics.DrawLines(Pens.Blue,lines);
}
}
Now add the Designer Attribute on top of the QSpacer class decaration:
[DesignerAttribute(typeof(QSpacerComponentDesigner), typeof(IDesigner))]
public class QSpacer : System.Windows.Forms.Control
Also add the System.ComponentModel.Design namespace to the using directives of QSpacer.cs file:
using System.ComponentModel.Design;
Push (F7)
Test the QLikeLayout Controls
Let's test the newly created QLayout and QSpacer controls.
- In Solution Explorer, right Click QtLikeLayout solution -> Add -> Add New Project.
- Add a Tester Visual C# Windows Application project to the solution.
- Click View -> Toolbox (Ctrl+Alt+X).
- Right Click the Toolbox window -> Add/Remove items.
- Push Browse button. Browse for the QtLikeLayout.dll in QtLikeLayout/bin/Debug folder.
- Click Ok. Right Click the Tester project -> Set as Startup Project
- From the Toolbox general tab, drag a
QLayout control into the form. Right click the qlayout1 control -> Properties. Set the Dock property to Bottom.
- From the
Toolbox, drag another QLayout control into the form. Right click the qlayout2 control -> Properties. Set the CmpLayout property to Vertical; the CtrlsDock to Fill, and the Dock property to Left.
- From the Toolbox, drag another
QLayout control into the form. Right click the qlayout3 control -> Properties. Set the CmpLayout property to Vertical; the CtrlsDock to Fill, and the Dock property to Fill.
- Drag a
QSpacer and five label controls into the qLayout1 layout (left).
- Drag a
QSpacer and five text boxes into the qLayout2 layout (right).
- Select all labels in left layout and set the Height to 20. (The same as the text boxes)
- Drag three to the using directives on top of the QLayout.cs file.and two buttons alternatively into the botton layout.
- Right click the form1 form -> Properties. Set the Minimum size to 480,215.
- In the Properties window, select the
qLayout2 (Left) layout from the controls drop down list. Set With property to 90.
- Push (F5) and test resizing the form.
Points of Interest
You must have the layout intended in mind before adding
QLayouts and controls. You can arrange the controls using the bring to front, and set to back verbs of controls. In later article updates and hopefully with your feedback, we will add a
QLayoutComponentDesigner control designer that will be more helpful at design time.