65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (6投票s)

2007年3月10日

CPOL

7分钟阅读

viewsIcon

46698

downloadIcon

595

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

Screenshot - qtlikelayout.jpg

引言

在试验 Linux 编程和 Qt 设计器时,我们觉得将 QLayout 组件移植到 C# 很有趣,它允许创建一个布局容器,该容器在调整大小时会自动排列其组件。它尚未达到可用的阶段,而且您可能会觉得它有点困难,但我们仍然想发布它,以便获得反馈并展示如何添加组件、设计时行为和功能。让我们跨越卢比孔。

创建控件

首先,我们将创建 QLayoutQSpacer 组件。QLayout 是容器组件,而 QSpacer 只是一个被包含的组件。当 QSpacer 出现在 QLayout 的包含组件中时,它将在调整大小时吸收额外的空间。我们的意图只是创建一些类似的行为,而不是克隆 QLayout 组件。

  1. 单击 文件 -> 新建 -> 项目。创建一个 *QtLikeLayout* Visual C# 类库项目
  2. 在解决方案资源管理器中,删除 *QtLikeLayout* 项目中的 *Class1.cs*
  3. 在解决方案资源管理器中,右键单击 *QtLikeLayout* 项目 -> 添加 -> 添加组件
  4. 添加一个 *QLayout.cs* 组件类
  5. 切换到源代码。(Ctrl+Alt+0)

将继承更改为 System.Windows.Forms.Panel

public class QLayout : System.Windows.Forms.Panel

但是,为了继承自 Forms.Panel,我们需要在我们的引用中添加 System.Windows.Forms

  1. 在解决方案资源管理器中,右键单击 QtLikeLayout 引用 -> 添加引用。
  2. 在 .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,""));
      }
   }

当我们向控件添加自定义属性时,它会在设计时出现在属性编辑器中。但在“杂项”部分,我们指定一个 CategoryDescriptionDefault 值属性,以指定属性或事件将在可视化设计器中显示的类别,以及它将获得的描述和默认值。请注意,当我们指定默认值时,设计器不会添加代码来初始化控件。因此,我们必须确保在代码中初始化它,要么在构造函数中,要么在声明中像我们一样赋值,并确保该值与我们在 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 控件

  1. 在解决方案资源管理器中,右键单击 *QtLikeLayout* 项目 -> 添加 -> 添加组件。
  2. 添加一个 *QSpacer.cs* 组件类。
  3. 切换到源代码。(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();
         }
      }
   }

控件本身就可以使用,但我们想更进一步,为用户提供反馈以及在设计时更多的可能性。

添加工具箱图标

首先,我们将为控件设置一个图像,以便在它们出现在工具箱窗口中时显示
  1. 在解决方案资源管理器中,右键单击 *QtLikeLayout* 项目 -> 添加 -> 添加新项。
  2. 展开“本地项目项” -> “资源”。
  3. 添加一个 *QLayout.bmp* 位图资源。(请注意,名称与控件类相同)
  4. 右键单击 *QLayout.bmp* -> 属性。将“生成操作”属性设置为“嵌入式资源”。
  5. 另外添加一个 *QSpacer.bmp* 位图资源。
  6. 右键单击 *QSpacer.bmp* -> 属性。将“生成操作”属性设置为“嵌入式资源”。
  7. 根据您的喜好编辑 *QLayout.bmp* 和 *QSpacer.bmp*。必须将其大小设置为 16x16。
按 F7 键生成解决方案。

创建控件设计器

现在,我们将创建一个 QSpacerComponentDesigner 控件设计器,用于扩展我们 QSpacer 控件的设计模式行为。我们将使用它来像 Qt 设计器在设计时那样将其绘制成弹簧的样子。
  1. 在解决方案资源管理器中,右键单击 *QtLikeLayout* 项目 -> 添加 -> 添加类。
  2. 添加一个 *QSpacerComponentDesigner.cs* 类。

将继承更改为 System.Windows.Forms.Panel

   public class QSpacerComponentDesigner : System.Windows.Forms.Design.ControlDesigner

我们需要在我们的引用中添加 *System.Design.dll* 和 *System.Drawing.dll*

  1. 在解决方案资源管理器中,右键单击 QtLikeLayout 引用 -> 添加引用。
  2. 在 .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 控件

让我们测试新创建的 QLayoutQSpacer 控件。

  1. 在解决方案资源管理器中,右键单击 *QtLikeLayout* 解决方案 -> 添加 -> 添加新项目。
  2. 向解决方案中添加一个 Tester Visual C# Windows 应用程序项目。
  3. 单击 查看 -> 工具箱。(Ctrl+Alt+X)。
  4. 右键单击工具箱窗口 -> 添加/删除项。
  5. 按“浏览”按钮。浏览到 *QtLikeLayout/bin/Debug* 文件夹中的 *QtLikeLayout.dll*。
  6. 单击确定。右键单击 Tester 项目 -> 设置为启动项目
  7. 从工具箱的“常规”选项卡中,将一个 QLayout 控件拖到窗体上。右键单击 qlayout1 控件 -> 属性。将 Dock 属性设置为 Bottom
  8. Toolbox 中,将另一个 QLayout 控件拖到窗体上。右键单击 qlayout2 控件 -> 属性。将 CmpLayout 属性设置为 Vertical;将 CtrlsDock 设置为 Fill;并将 Dock 属性设置为 Left
  9. 从工具箱中,将另一个 QLayout 控件拖到窗体上。右键单击 qlayout3 控件 -> 属性。将 CmpLayout 属性设置为 Vertical;将 CtrlsDock 设置为 Fill;并将 Dock 属性设置为 Fill
  10. 将一个 QSpacer 和五个标签控件拖到 qLayout1 布局(左侧)。
  11. 将一个 QSpacer 和五个文本框控件拖到 qLayout2 布局(右侧)。
  12. 选择左侧布局中的所有标签,并将高度设置为 20。(与文本框相同)
  13. 将三个控件拖到 *QLayout.cs* 文件顶部的 using 指令中,并将两个按钮交替放入底部布局。
  14. 右键单击 form1 窗体 -> 属性。将“最小大小”设置为 480,215。
  15. 在“属性”窗口中,从控件下拉列表中选择 qLayout2(左侧)布局。将“宽度”属性设置为 90。
  16. 按 F5 键并测试调整窗体大小。

关注点

在添加 QLayouts 和控件之前,您必须有自己心中想要的布局。您可以使用控件的“置于顶层”和“置于底层”动词来排列控件。在后续的文章更新中,并希望能得到您的反馈,我们将添加一个 QLayoutComponentDesigner 控件设计器,它在设计时将更有帮助。
© . All rights reserved.