构建类似 Qt 的布局, 以在面板调整大小时自动排列控件






4.75/5 (6投票s)
通过创建一个行为类似于 Qt 布局的容器组件,添加设计功能和视觉反馈。在容器调整大小时自动调整包含组件的大小和位置。

引言
在试验 Linux 编程和 Qt 设计器时,我们觉得将 QLayout
组件移植到 C# 很有趣,它允许创建一个布局容器,该容器在调整大小时会自动排列其组件。它尚未达到可用的阶段,而且您可能会觉得它有点困难,但我们仍然想发布它,以便获得反馈并展示如何添加组件、设计时行为和功能。让我们跨越卢比孔。
创建控件
首先,我们将创建 QLayout
和 QSpacer
组件。QLayout
是容器组件,而 QSpacer
只是一个被包含的组件。当 QSpacer
出现在 QLayout
的包含组件中时,它将在调整大小时吸收额外的空间。我们的意图只是创建一些类似的行为,而不是克隆 QLayout
组件。
- 单击 文件 -> 新建 -> 项目。创建一个 *QtLikeLayout* Visual C# 类库项目
- 在解决方案资源管理器中,删除 *QtLikeLayout* 项目中的 *Class1.cs*
- 在解决方案资源管理器中,右键单击 *QtLikeLayout* 项目 -> 添加 -> 添加组件
- 添加一个 *QLayout.cs* 组件类
- 切换到源代码。(Ctrl+Alt+0)
将继承更改为 System.Windows.Forms.Panel
public class QLayout : System.Windows.Forms.Panel
但是,为了继承自 Forms.Panel
,我们需要在我们的引用中添加 System.Windows.Forms
- 在解决方案资源管理器中,右键单击
QtLikeLayout
引用 -> 添加引用。 - 在 .NET 选项卡中,选择 *System.Windows.Forms.dll*,然后单击确定。
在 *QLayout.cs* 文件顶部的 using 指令中,添加以下代码
using System.Windows.Forms;
添加属性
为了显示 QLayout
组件的水平和垂直布局行为,我们需要在属性编辑器中添加一个在设计模式下公开的 CmpLayout
属性。下面是对如何完成此操作的说明。
首先,我们在 QLayout
类定义之前添加一个 QLayoutProperty
枚举
public enum QLayoutProperty
{
Horizontal,
Vertical
}
我们需要一个私有属性 pHLayout
来保存值,以及一个公共方法 CmpLayout
来将其公开给设计器属性编辑器,因此我们将此代码添加到 QLayout
类的属性和方法中
private QLayoutProperty pHLayout = QLayoutProperty.Horizontal;
[
Category("Layout"),
Description("Controls layout arrangement."),
DefaultValue(QLayoutProperty.Horizontal)
]
public QLayoutProperty CmpLayout
{
get
{
return pHLayout;
}
set
{
pHLayout = value;
// Rearrange components
this.OnLayout(new LayoutEventArgs(this,""));
}
}
当我们向控件添加自定义属性时,它会在设计时出现在属性编辑器中。但在“杂项”部分,我们指定一个 Category
、Description
和 Default
值属性,以指定属性或事件将在可视化设计器中显示的类别,以及它将获得的描述和默认值。请注意,当我们指定默认值时,设计器不会添加代码来初始化控件。因此,我们必须确保在代码中初始化它,要么在构造函数中,要么在声明中像我们一样赋值,并确保该值与我们在 DefaultValue
属性中设置的值相同。为了设置控件的对齐方式,我们需要在属性编辑器中添加一个在设计模式下公开的 CtrlsDock
属性。下面是实现方法。
再次,我们在 QLayout
类定义之前添加一个 QDockProperty
枚举
public enum QDockProperty
{
Fill, // Default, the widgets fill upto container walls
Side, // Right or Top justification
Center, // Center justification
UpSide // Left or botton justifcation
}
但我们也需要一个私有属性 pDock
来保存值,以及一个公共方法 CtrlsDock
来将其公开给设计器属性编辑器,因此我们将此代码添加到 QLayout
类的属性和方法中 private QDockProperty pDock = QDockProperty.Center;
[
Category("Layout"),
Description("Controls justification."),
DefaultValue(QDockProperty.Center)
]
public QDockProperty CtrlsDock
{
get
{
return pDock;
}
set
{
pDock = value;
// Rearrange components
this.OnLayout(new LayoutEventArgs(this,""));
}
}
现在,让我们编写主要行为。我们通过重写 OnLayout
Panel 继承组件方法来执行布局安排 protected override void OnLayout(LayoutEventArgs levent)
{
// We need to handle the Docking and layout changes performed
// by the base clase. Maybe we should inherit from a more generic
// control.
base.OnLayout (levent);
this.SuspendLayout();
if (pHLayout == QLayoutProperty.Horizontal)
{
// controls are horizontally arranged
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)
{
// there is no spacer
if (cntSpace == 0)
{
ctrl.Width = mWidth;
ctrl.Left = pLeft;
ctrlCount --;
// if ctrl does not allow resizing
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;
// count spacers and meassure
foreach (Control ctrl in this.Controls)
{
if (ctrl.GetType().Name == "QSpacer")
cntSpace += 1;
else
ctrlHeights += ctrl.Height;
ctrlsHeight += ctrl.Height + 1;
}
// dump spacer high;
int sHeight = (this.Height - ctrlHeights) /
(this.Controls.Count + 1);
foreach (Control ctrl in this.Controls)
{
// there is at least one Spacer
if (cntSpace !=0)
{
if (ctrl.GetType().Name == "QSpacer")
{
// the spacer fill the space betwen
ctrl.Height = (this.Height - ctrlHeights) / cntSpace;
((QSpacer)ctrl).CmpLayout = QLayoutProperty.Vertical;
}
}
// is it just like there is one spacer between every control
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();
}
此方法生成对包含的控件的自动排列。对于水平布局,我们将所有组件一个接一个地水平排列,并且所有组件都通过调整大小来吸收 QLayout
面板的宽度。如果找到一个或多个 QSpacers
,它们将吸收额外的空间,而组件将保持其大小。根据 CtrlsDock
的对齐方式,控件将居中、顶部或底部对齐。对于垂直布局,我们将所有组件一个接一个地垂直排列。在这里,它们的距离与剩余空间成比例,但它们保留原始宽度。如果找到一个或多个 QSpacers
,组件将一个接一个地排列,中间没有任何空间,但它们会通过调整大小来吸收空间。现在让我们创建 QSpacer
控件
- 在解决方案资源管理器中,右键单击 *QtLikeLayout* 项目 -> 添加 -> 添加组件。
- 添加一个 *QSpacer.cs* 组件类。
- 切换到源代码。(Ctrl+Alt+0)
System.Windows.Forms.Control
public class QSpacer : System.Windows.Forms.Control
将一个 pHLayout
私有属性和一个 CmpLayout
方法添加到 QSpacer
类的 C# 方法和属性中
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();
}
}
}
控件本身就可以使用,但我们想更进一步,为用户提供反馈以及在设计时更多的可能性。
添加工具箱图标
首先,我们将为控件设置一个图像,以便在它们出现在工具箱窗口中时显示- 在解决方案资源管理器中,右键单击 *QtLikeLayout* 项目 -> 添加 -> 添加新项。
- 展开“本地项目项” -> “资源”。
- 添加一个 *QLayout.bmp* 位图资源。(请注意,名称与控件类相同)
- 右键单击 *QLayout.bmp* -> 属性。将“生成操作”属性设置为“嵌入式资源”。
- 另外添加一个 *QSpacer.bmp* 位图资源。
- 右键单击 *QSpacer.bmp* -> 属性。将“生成操作”属性设置为“嵌入式资源”。
- 根据您的喜好编辑 *QLayout.bmp* 和 *QSpacer.bmp*。必须将其大小设置为 16x16。
创建控件设计器
现在,我们将创建一个QSpacerComponentDesigner
控件设计器,用于扩展我们 QSpacer
控件的设计模式行为。我们将使用它来像 Qt 设计器在设计时那样将其绘制成弹簧的样子。- 在解决方案资源管理器中,右键单击 *QtLikeLayout* 项目 -> 添加 -> 添加类。
- 添加一个 *QSpacerComponentDesigner.cs* 类。
将继承更改为 System.Windows.Forms.Panel
public class QSpacerComponentDesigner : System.Windows.Forms.Design.ControlDesigner
我们需要在我们的引用中添加 *System.Design.dll* 和 *System.Drawing.dll*
- 在解决方案资源管理器中,右键单击
QtLikeLayout
引用 -> 添加引用。 - 在 .NET 选项卡中,选择 *System.Design.dll* 和 *System.Drawing.dll*,然后单击确定。
在 *QSpacerComponentDesigner.cs* 文件顶部的 using 指令中,添加以下代码
using System.Windows.Forms;
using System.Drawing;
现在我们将重写 WndProc
以处理调整大小,以及 OnPaintAdornments
方法,以便在设计时绘制类似弹簧的外观。双击 startButton 按钮。将 startButton_Click
方法替换为
// repaint control on Resize
protected override void WndProc(ref Message m)
{
base.WndProc (ref m);
// see Winuser.h for other message const
// int WM_MOVE = 0x0003;
const int WM_SIZE = 0x0005;
//
if (m.Msg == WM_SIZE)
this.Control.Invalidate();
}
// Occurs after the designed Control has painted itself
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);
}
}
现在,在 QSpacer
类声明的顶部添加 Designer Attribute
// Associates the designer class QSpacerComponentDesigner
// with QSpacer control.
[DesignerAttribute(typeof(QSpacerComponentDesigner), typeof(IDesigner))]
public class QSpacer : System.Windows.Forms.Control
还将 System.ComponentModel.Design
命名空间添加到 *QSpacer.cs* 文件的 using 指令中
using System.ComponentModel.Design;
按 F7 键
测试 QLikeLayout 控件
让我们测试新创建的 QLayout
和 QSpacer
控件。
- 在解决方案资源管理器中,右键单击 *QtLikeLayout* 解决方案 -> 添加 -> 添加新项目。
- 向解决方案中添加一个 Tester Visual C# Windows 应用程序项目。
- 单击 查看 -> 工具箱。(Ctrl+Alt+X)。
- 右键单击工具箱窗口 -> 添加/删除项。
- 按“浏览”按钮。浏览到 *QtLikeLayout/bin/Debug* 文件夹中的 *QtLikeLayout.dll*。
- 单击确定。右键单击 Tester 项目 -> 设置为启动项目
- 从工具箱的“常规”选项卡中,将一个
QLayout
控件拖到窗体上。右键单击qlayout1
控件 -> 属性。将Dock
属性设置为Bottom
。 - 从
Toolbox
中,将另一个QLayout
控件拖到窗体上。右键单击qlayout2
控件 -> 属性。将CmpLayout
属性设置为Vertical
;将CtrlsDock
设置为Fill
;并将Dock
属性设置为Left
。 - 从工具箱中,将另一个
QLayout
控件拖到窗体上。右键单击qlayout3
控件 -> 属性。将CmpLayout
属性设置为Vertical
;将CtrlsDock
设置为Fill
;并将Dock
属性设置为Fill
。 - 将一个
QSpacer
和五个标签控件拖到qLayout1
布局(左侧)。 - 将一个
QSpacer
和五个文本框控件拖到qLayout2
布局(右侧)。 - 选择左侧布局中的所有标签,并将高度设置为 20。(与文本框相同)
- 将三个控件拖到 *QLayout.cs* 文件顶部的 using 指令中,并将两个按钮交替放入底部布局。
- 右键单击 form1 窗体 -> 属性。将“最小大小”设置为 480,215。
- 在“属性”窗口中,从控件下拉列表中选择
qLayout2
(左侧)布局。将“宽度”属性设置为 90。 - 按 F5 键并测试调整窗体大小。
关注点
在添加QLayouts
和控件之前,您必须有自己心中想要的布局。您可以使用控件的“置于顶层”和“置于底层”动词来排列控件。在后续的文章更新中,并希望能得到您的反馈,我们将添加一个 QLayoutComponentDesigner
控件设计器,它在设计时将更有帮助。