使用智能设备扩展在 C# 中开发自定义控件

翻译|其它|编辑:郝浩|2006-04-07 10:29:00.000|阅读 1502 次

概述:

# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>


适用于
Microsoft Windows CE .NET
Smart Device Extensions for Microsoft Visual Studio .NET

摘要学习如何使用 Smart Device Extensions for Microsoft Visual Studio .NET (SDE) 创建自定义控件。

本页内容

简介

Smart Device Extensions for Microsoft Visual Studio .NET (SDE) 提供了一种可以在应用程序中使用的很好的基本控件。遗憾的是,嵌入式设备应用程序涉及的范围非常广,这就使得开发人员几乎肯定会在某些地方缺少合适的控件,此时,基本上有两个选择:重新进行应用程序的结构设计以使用可用的控件,或者采用您自己的自定义控件。

SDE 的第一个版本不支持设计时自定义控件,这意味着为了使用它们,必须手动编写将它们放入窗体并设置其大小和属性的代码。它只需很少的额外工作量,并且只需要您接受没有可用于自定义控件的 Form Design Support 这一事实。

问题

最近,我一直在为 Visual Studio .NET 创建类库,用于包装很多硬件的功能。通过使用一个可以为他们完成所有 P/Invoking 和资源管理工作的类库,托管代码开发人员使用这个类库来访问机载微型控制器和 Microsoft Windows CE 端口就容易多了。我开发用于 Graphics Master 设备的 I/O 的类库,以便提供对两个单独的头上的引脚的读取和写入功能。

我需要一个测试和示例应用程序,该程序能够使用户轻松地通过恰当的图形接口设置或读取数字 I/O 状态并读取模拟 I/O。我希望有某个东西看起来像示意图上的接头或类似板上的物理插头。由于我要处理两个物理上不同大小的接头,所以我需要多个控件,或最好是一个可以定义大小的控件。很显然,SDE 的工具箱中没有我想要的控件。

我本来可以使用大量的 Label、CheckBox、PictureBox 和 TextBox,但是我认为这种替代方案看起来很难看。让我们尝试编写自己的控件。

对象模型

第一个任务是决定整个对象模型。我们需要什么样的组成部分,这些组成部分将如何融合在一起,它们如何相互交互,如何与它们的环境交互?

我们将创建连接器,用来包含大小可变的引脚集合,以便能够连接不同大小的接头。每个引脚必须有可以放在被显示的“引脚”的左侧或右侧(取决于它是偶数还是奇数引脚)的标识标签。每个引脚还可以是数字的或模拟的 I/O,因此每个引脚都需要有范围从零到 0xFFFF 的单独的值。最好能够一眼即可识别每个引脚的类型和值,所以将需要使用一些颜色。当然,并非接头上的所有引脚都可用于 I/O,所以我们需要能够禁用它们中的一部分,此外,我们希望引脚是交互的,这样当我们接通一个引脚时,它可以做某些操作,比如更改状态。


基于这些要求,我们提出了一个如 2 所示的对象模型。

整体的思路是,我们将有一个 Connector 基类,然后从它派生出其他几个自定义的 Connector 类。Connector 将包含一个 Pins 类,这个类只是通过从 CollectionBase 派生,使用索引器来公开 Pin 对象的 ListArray。

实现 Pin 对象

因为此控件的骨干是 Pin 对象,所以我们首先介绍它。Pin 对象将处理控件的大多数显示属性,并处理用户交互。一旦我们可以成功地在窗体上创建、显示单个引脚并与之交互,构建一个连接器将它们组合在一起就非常简单了。

Pin 对象有四个在创建它时必须设置的属性。默认的构造函数会设置它们中的每一个,但其他构造函数还可以用来允许创建者传递非默认的值。

最重要的属性是 Alignment。这个属性确定了绘制对象时文本和引脚的位置,但更重要的是,设置属性时,它将创建和放置用于绘制引脚和文本的矩形。这些矩形的使用将在随后解释 OnDraw 时进行讨论。

清单 1 显示了基本构造函数和 Alignment 属性的代码。为引脚子组件周围所定义的偏移量和边框使用了常量,但这些常量也很容易成为控件的其他属性。

清单 1. 引脚构造函数和 Alignment 属性

public Pin()
{
  showValue = false;
  pinValue = 0;
  type = PinType.Digital;
  Alignment = PinAlignment.PinOnRight;
}
public PinAlignment Alignment
{ // determines where the pin rectangle is placed set
  {
    align = value;
    if(value == PinAlignment.PinOnRight)
    {
      this.pinBorder = new Rectangle(
        this.ClientRectangle.Width - (pinSize.Width + 10),
        1,
        pinSize.Width + 9,
        this.ClientRectangle.Height - 2);
        this.pinBounds = new Rectangle(
        this.ClientRectangle.Width - (pinSize.Width + 5),
        ((this.ClientRectangle.Height -
        pinSize.Height) / 2) + 1,
        pinSize.Width,
        pinSize.Height);
        this.textBounds = new Rectangle(
        5,
        5,
        this.ClientRectangle.Width - (pinSize.Width + 10),
        20);
    }
    else
    {
      this.pinBorder = new Rectangle(
        1,
        1,
        pinSize.Width + 9,
        this.ClientRectangle.Height - 2);
        this.pinBounds = new Rectangle(
        6,
        this.ClientRectangle.Height - (pinSize.Height + 4),
        pinSize.Width,
        pinSize.Height);
        this.textBounds = new Rectangle(
        pinSize.Width + 10,
        5,
        this.ClientRectangle.Width - (pinSize.Width + 10),
        20);
    }
    this.Invalidate();
  }
  get
  {
    return align;
  }
}

由于 Pin 对象不会提供很好的用户交互或可自定义性,所以引脚的核心功能是我们将重写的绘图例程 OnDraw,重写该例程是为了可以由我们来绘制整个引脚。

每个引脚将绘制三个部分:引脚本身将是一个圆(除非它是 Pin 1,这时它将是一个方块),我们将围绕引脚绘制边框矩形,然后在引脚的左侧或右侧留出一个区域用来绘制引脚的文本。

要绘制引脚,我们首先确定表示实际引脚的圆所使用的颜色。如果引脚被禁用,它的颜色是灰色。如果启用,则要确定它是什么类型。模拟引脚将是绿色,而数字引脚根据情况而不同,如果是低 (关)则是蓝色,如果是高(开)则是橙色。

下一步,我们使用 FillEllipse 来绘制所有实际的引脚,但 PinNumber=1 时除外,这时使用 FillRectangle 绘制引脚。通过绘制在矩形 (pinBounds) 中而不是控件的边界上,我们能够在创建引脚时设置引脚的位置(左侧或右侧),并且从这一点开始,我们可以在不用关心引脚的位置的情况下进行绘制。

下一步我们绘制标签,它将是引脚的文本或引脚的值,这取决于 ShowValue 属性。

我们使用与绘制引脚时类似的策略来绘制文本,但这次我们必须计算水平和垂直偏移量,因为在 Microsoft .NET 压缩框架中,DrawText 方法不允许有 TextAlign 参数。

最终,我们通过调用 Dispose 方法清理我们手动使用的 Brush 对象。

清单 2 显示了完整的 OnDraw 例程。

清单 2. OnDraw() 方法

protected override void OnPaint(PaintEventArgs pe)
{
  Brush b;  // determine the Pin color if(this.Enabled)
  {
    if(type == PinType.Digital)
    { // digital pins have different on/off color b = new System.Drawing.SolidBrush(
      this.Value == 0 ? (digitalOffColor) : (digitalOnColor));
    }
    else
    {  // analog pin b = new System.Drawing.SolidBrush(analogColor);
    }
  }
  else
  { // disabled pin b = new System.Drawing.SolidBrush(disabledColor);
  }  // draw the pin if(this.PinNumber == 1)
    pe.Graphics.FillRectangle(b, pinBounds);
  else
    pe.Graphics.FillEllipse(b, pinBounds); // draw a border Rectangle around the pin pe.Graphics.DrawRectangle(new Pen(Color.Black), pinBorder); // draw the text centered in the text bound string drawstring; // are we showing the Text or Value? if(showValue)
    drawstring = Convert.ToString(this.Value);
  else
    drawstring = this.Text;  // determine the actual string size SizeF fs = pe.Graphics.MeasureString(
        drawstring,
        new Font(FontFamily.GenericMonospace, 8f,
                 FontStyle.Regular));  // draw the string pe.Graphics.DrawString(
      drawstring,
      new Font(FontFamily.GenericMonospace, 8f,
      FontStyle.Regular),
      new SolidBrush((showValue ? analogColor : Color.Black)),
      textBounds.X + (textBounds.Width - fs.ToSize().Width) / 2,
      textBounds.Y + (textBounds.Height - fs.ToSize().Height) /
                 2); // clean up the Brush b.Dispose();
  }
}

构建 Pin 类的最后一步是添加 Click 处理程序。对于我们的 Pin 类来说,我们将使用自定义的 EventArg,以便可以向事件处理程序传递引脚的文本和编号。要创建自定义的 EventArg,我们只是创建了一个从 EventArgs 类派生的类:

public class PinClickEventArgs : EventArgs
{ // a PinClick passes the Pin Number and the Pin's Text public int number;
  public string text;
  public PinClickEventArgs(int PinNumber, string PinText)
  {
    number = PinNumber;
    text = PinText;
  }
}

下一步,我们将一个委托添加到命名空间中:

public delegate void PinClickHandler(Pin source, PinClickEventArgs args);

现在,我们需要添加代码来确定什么时候发生单击,然后引发事件。对于我们的 Pin 类,当引脚的边框矩形内部发生 MouseDown 和 MouseUp 事件时即为一个逻辑上的单击 - 这样,如果用户单击引脚的文本部分,则不会触发 Click 事件,但如果点击表示实际引脚的区域,则触发该事件。

首先,我们需要一个公共 PinClickHandler 事件,其定义如下:

public event PinClickHandler PinClick;

我们还需要一个私有的布尔变量,我们将在 MouseDown 事件发生时设置该变量,用于指示我们正在单击过程中。然后,我们检查 MouseUp 事件的该变量,以确定事件是否是按连续的顺序发生的:

bool midClick;

下一步,我们需要为 MouseDown MouseUp 添加两个事件处理程序,如清单 3 所示。

清单 3. 用于实现 PinClick 事件的事件处理程序

private void PinMouseDown(object sender, MouseEventArgs e)
{
  if(!this.Enabled)
    return;  // if the user clicked in the "pin" rectangle, start a click process midClick = pinBorder.Contains(e.X, e.Y);
}
private void PinMouseUp(object sender, MouseEventArgs e)
{ // if we had a mousedown and then up inside the "pin" rectangle,
  // fire a click if((midClick) && (pinBorder.Contains(e.X, e.Y)))
  {
    if(PinClick != null)
      PinClick(this, new PinClickEventArgs(
               this.PinNumber, this.Text));
  }
}

最后,我们需要为每个引脚实现事件处理程序。引脚的基本构造函数是添加这些挂钩的好地方,我们可以通过直接在构造函数中添加以下代码来完成:

this.MouseDown += new MouseEventHandler(PinMouseDown);
this.MouseUp += new MouseEventHandler(PinMouseUp);

实现 Pins 类

一旦有了 Pin 类,就可以创建从 CollectionBase 派生的 Pins 类。该类的目的是提供索引器,这样我们就可以很容易在集合内添加、删除和操纵 Pin 类。

清单 4. Pins 类

public class Pins : CollectionBase
{
  public void Add(Pin PinToAdd)
  {
    List.Add(PinToAdd);
  }
  public void Remove(Pin PinToRemove)
  {
    List.Remove(PinToRemove);
  }  // Indexer for Pins public Pin this[byte Index]
  {
    get
    {
      return (Pin)List[Index];
    }
    set
    {
      List[Index] = value;
    }
  }
  public Pins(){}
}

实现 Connector 类

既然我们已经获得了 Pins 类,我们现在需要构建 Connector 类,该类将是一个简单的包装类,这个包装类包含 Pins 类,并在每个引脚和连接器容器之间封送 PinClick 事件,而且它有一个表示连接器上的引脚数的构造函数。清单 5 显示了完整的 Connector 类。

清单 5. Connector 类

public class Connector : System.Windows.Forms.Control
{
  public event PinClickHandler PinClick;
  protected Pins pins;
  byte pincount;
  public Connector(byte TotalPins)
  {
    pins = new Pins();
    pincount = TotalPins;
    InitializeComponent();
  }
  private void InitializeComponent()
  {
    for(int i = 0 ; i < pincount ; i++)
    {
      Pin p = new Pin(PinType.Digital,
            (PinAlignment)((i + 1) % 2), 0);
      p.PinClick += new PinClickHandler(OnPinClick);
      p.PinNumber = i + 1;
      p.Text = Convert.ToString(i);
      p.Top = (i / 2) * p.Height;
      p.Left = (i % 2) * p.Width;
      this.Pins.Add(p);
      this.Controls.Add(p);
    }
    this.Width = Pins[0].Width * 2;
    this.Height = Pins[0].Height * this.Pins.Count / 2;
  }
  public Pins Pins
  {
    set
    {
      pins = value;
    }
    get
    {
      return pins;
    }
  }
  private void OnPinClick(Pin sender, PinClickEventArgs e)
  {  // pass on the event if(PinClick != null)
    {
      PinClick(sender, e);
      if(sender.Type == PinType.Digital)
        sender.Value = sender.Value == 0 ? 1 : 0;
      else
        sender.DisplayValue = !sender.DisplayValue;
    }
  }
  protected override void Dispose( bool disposing )
  {
    base.Dispose( disposing );
  }
}

Connector 的 InitializeComponent 方法是创建所有被包含的 Pin 类并将其添加到连接器的控件中的地方,并且是连接器本身调整大小的地方。InitializeComponent 也是最终被 Form Designer 用来显示我们的连接器的方法。

构建自定义连接器

Connector 类本身很简单,它不会修改任何默认的引脚设置。但是,我们现在可以通过从 Connector 类派生新的类,从而构建一个自定义连接器,并修改单个引脚(例如,使某些引脚成为模拟引脚,或将其禁用)。

在示例应用程序中,我为 Applied Data Systems 的 Graphics Master 板创建了两个连接器,一个用于 J2,一个用于 J7。构造函数基于连接器设置引脚的类型以及引脚的文本。 2 是窗体上有 J2 和 J7 的示例应用程序的屏幕快照。


标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com


为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP