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

基于文本的姓名列表 + 查找控件

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.64/5 (7投票s)

2004 年 1 月 21 日

4分钟阅读

viewsIcon

72445

downloadIcon

706

一个可用的示例, 展示了如何构建一个类似 Outlook 的姓名查找控件

Sample Image - TextObjectList.jpg

引言

您是否曾经想过创建一个类似 Outlook 的文本框,该文本框会解析用户输入的内容并根据某些数据源进行验证?嗯,这是我为此类控件实现的一个版本。

步骤 A - 所需的内部对象

我试图使对象模型尽可能通用,以适应大多数需求。作为基础,我使用了 RichTextBox 控件,并继承了其中的编辑和格式化文本显示功能。

该控件的功能:我允许使用标准分隔符“;”和“,” - 但由于这是一个数组,您可以根据需要添加或更改它们。现在,由于我不想编写文本编辑器,而是专注于查找功能,因此我创建了一个类 (RichTextList),该类继承自 System.Windows.Forms.RichTextBox,并封装了一些用于在其中选择和单击的逻辑。这样,我也有一种本机方法可以使用格式化(颜色、粗体、下划线等)打印列表。当然,RTF 控件并不是真正轻量级的,所以我们可能会考虑放弃内联渲染,但另一方面,它包含在 .NET Framework 中,因此无论如何都不包含在二进制文件中。

  //
  // This class is only used internally -
  // out control will not expose any of it directly
  //
  class RichTextList : System.Windows.Forms.RichTextBox {
    ...
    public char[] SeparationChars = new char[] {';',','}; 
    // which chars are interpreted as object-separators
    ...

我们重写了一些继承的事件,以便我们可以捕获用户尝试单击或选择某项内容。无论用户单击何处,我们都尝试选择用户单击的项。我们不希望用户能够更改已验证项的输入。

    protected override void OnSelectionChanged(EventArgs e) {
      if (this.NoSelChangeEvent) return;
      //at end of list
      if (base.SelectionStart == base.Text.Length) return;
      //selected whole list
      if (base.SelectionLength == base.Text.Length) return;
      if (base.SelectionLength == 0 //at sep-char
        && base.Text.IndexOfAny(this.SeparationChars,
        base.SelectionStart,1) > -1) return;
      this.MarkItem(base.SelectionStart); //within an item >> select it!
      base.OnSelectionChanged(e);
      if (base.SelectedText.Length > 0 && this.ItemSelected != null)
        this.ItemSelected(base.SelectedText);
    }

这是我们的选择逻辑,它确定当前单击/选择的项的开始和结束位置。也许,我有些老套,应该使用 RegExp - 不确定哪种方式性能更好。

    // this function actually marks the item in the textbox
    private void MarkItem(int Pos) {
      this.NoSelChangeEvent = true;
      /* Find first pos */
      if (Pos == base.Text.Length) Pos--;
      int x1 = base.Text.LastIndexOfAny(this.SeparationChars, Pos, Pos)+1;
      base.SelectionStart = x1;

      /* Find last pos */
      int x2 = base.Text.IndexOfAny(this.SeparationChars, Pos+1);
      base.SelectionLength = (x2<0?base.Text.Length:x2)-base.SelectionStart;
      this.NoSelChangeEvent = false;
    }

现在,我们创建一个轻量级对象来表示列表中的一个项。该对象将用于/实例化用户输入的每个解析出的项。基本上,它允许开发人员定义已验证项的显示颜色。当然,为了通用性,该控件本身无法验证任何输入。它通过 ValidateItems 事件通知容器,只有当列表中有任何未经验证的项时才会引发该事件。

我们进行验证的时间点是 OnValidate 事件,该事件在失去焦点或当父窗体/窗体请求验证时自动引发。

  //
  // a low-weight Class to hold all parsed elements
  //
  public class ObjectItem {
    ...
    public System.Drawing.Color TextColor = System.Drawing.Color.Blue; 
    // the default color of a validated item

当引发 ValidateItems 事件时,开发人员会遍历集合(见下文)来确定未经验证的项。为了验证它们,他只需处理输入的文本并根据某些后端逻辑进行验证,后端逻辑通常会返回某个对象或该对象的唯一标识符(例如数据库 ID、用户对象、DataRowGUID 或其他任何内容)。无论返回什么,都可以通过将其分配给 ObjectRef 来将其挂接到 ObjectItem。如果 ObjectRef 不为 null,则这些项将被视为已经验证 - 非常基础,非常简单,非常有效:)。

    // wether this item has been validated or not
    public bool Validated {
      get { return (this.ObjectRef != null); }
    }

    // a reference to the validated source.
    // can be an ID value, an object-reference or any
    // other means of locating the resource. If this object is null,
    // then Validated returns false, else it returns true.
    public object ObjectRef;

但是,由于列表中很可能不止一项,我们需要一个集合来容纳所有项。我们将此对象称为 ObjectItemCollection - 因为它是 ObjectItem 的集合。

让我重点介绍这些重要的实现

每当从列表中移除一项时,我们都想知道!通常,开发人员也希望从后端资源(例如数据库或业务对象)中移除该项,因此当这种情况发生时会引发一个事件。现在,由于所有目前和之前解释的对象都不包含我们正在构建的主要控件,您将在下面看到它们之间的关系。

  //
  // The collection which holds all entered elements
  //
  public class ObjectItemCollection : CollectionBase {

    ...
    // we have the UI control raise an event, if an item 
    // has been removed from the collection
    protected override void OnRemoveComplete(int index, object value) {
      base.OnRemoveComplete (index, value);
      this.Textbox.OnRemoveItem((ObjectItem)value);
    }

当然,开发人员可以随时向我们的控件添加项 - 这些项通常已经验证,因此他也可以在此处提供 ObjectRef。以编程方式添加未经验证的项通常没有意义。即使如此,您只需为 objRef 提供 NULL

    // implementing code can add items to the Text/Listbox using this
    // add method
    public ObjectItem Add(string itemName, object objRef) {
      ObjectItem it = new ObjectItem(itemName, objRef);
      it.isNew = true;
      List.Add(it);
      return it;
    }

步骤 B - 最后,'TextObjectList' UserControl 本身

接下来,我们构建控件本身,它将引发事件并管理列表的解析和构建。我将此类命名为 TextObjectList,它将是控件的类(因此,它是 public),它必须继承自 System.Windows.Forms.UserControl

此处声明的事件是您将绑定到的事件。上述子对象只会向该控件报告其操作 - 它决定如何进行并发出指令。

    // our event delegates
    public delegate void ObjectItemRemovedEvent(TextObjectList list, 
                                                       ObjectItem item);
    public delegate void ObjectItemClickedEvent(TextObjectList list, 
                                    ObjectItem item, MouseEventArgs ev);
    public delegate void ValidateObjectItemsEvent(ObjectItemCollection col);

    public event ObjectItemClickedEvent ObjectItemClicked;
    public event ObjectItemRemovedEvent ObjectItemRemoved;
    public event ValidateObjectItemsEvent ValidateItems;

    // this collection holds all entered items - validated and not
    public ObjectItemCollection ObjectItems;

我们重写了 Validate 事件,以便我们可以响应用户输入(实际上,响应失去焦点或窗体手动请求 Validate)。

    // we create our own validation code
    public override bool Validate() {
      base.Validate();
      bool AllValid = true;
      string txtEntered = this.NList.Text;
      string intSep = "";
      foreach (char sepChar in this.intSepChar) {
        intSep += sepChar.ToString();
      }

      /* Replace all allowed Sep-Chars with our internal one
       * so we can split the input */
      foreach (char sepChar in this.SeparationChars) {
        txtEntered = txtEntered.Replace(sepChar.ToString(), intSep);
      }

      /* Now split the input */
      string[] txtItems = txtEntered.Split(this.intSepChar);

      /* Then parse each item */
      ArrayList idxs = new ArrayList();
      foreach (string txtItem in txtItems) {
        if (txtItem.Trim() == string.Empty) continue;
        Debug.WriteLine(" .. parsing txtItem " + txtItem.Trim(), 
                                           "TextObjectList.Validate");
        if (this.ObjectItems.Contains(txtItem.Trim())) {
          idxs.Add( this.ObjectItems.IndexOf(
            this.ObjectItems.FindByName(txtItem.Trim())
            ));
          continue;
        }
        //not in collection yet, add it!
        ObjectItem it = new ObjectItem(txtItem.Trim());
        this.ObjectItems.Add(it);
        idxs.Add( this.ObjectItems.IndexOf(it) );
      }

      /* Now remove all items not in array */
      for (int i = this.ObjectItems.Count-1; i >= 0; i--) {
        if (idxs.Contains(i)) continue;
        if (this.ObjectItems.Item(i).isNew) continue;
        this.ObjectItems.RemoveAt(i);
      }

      /* Something to validate by host? */
      AllValid = true;
      foreach (ObjectItem it in this.ObjectItems) {
        if (!it.Validated) AllValid = false;
      }

      /* Now have the host validate all new items */
      if (!AllValid && this.ValidateItems != null)
        this.ValidateItems(this.ObjectItems);

      /* Finally visually display all items */
      AllValid = true;
      string newRtf = "";
      string colTbl = BuildColorTable();
      foreach (ObjectItem it in this.ObjectItems) {
        it.isNew = false;
        if (it.Validated) {
          newRtf += @"\cf" + this.colors[it.TextColor.ToArgb()] 
          + @"\ul\b " + it.ItemName + @"\b0\ulnone\cf0";
        } else {
          newRtf += @"\cf1 " + it.ItemName + @"\cf0";
          AllValid = false;
        }
        newRtf += " " + this.SeparationChars[0].ToString();
      }
      this.NList.Rtf = @"{\rtf1\ansi\ansicpg1252\deff0\deflang3079" +
        @"{\fonttbl{\f0\fswiss\fcharset0 Arial;}}" +
        @"{\colortbl ;\red255\green0\blue0;" + colTbl + "}" +
        @"{\*\generator TextObjectList.NET;}\viewkind4\uc1\pard\f0\fs20 " 
        + newRtf + @"\par}";
      return AllValid;
    }

以下是从下方对象引发的事件 - 我们会相应地捕获和处理它们。

    // ah, an item in the textbox has been clicked,
     // we check which one it is in our 
    // collection and raise the appropriate event
    protected void NList_ItemClicked(string ItemName, MouseEventArgs e) {
      if (this.ObjectItemClicked == null) return;
      if (!this.ObjectItems.Contains(ItemName)) return;
      this.ObjectItemClicked(this, this.ObjectItems.FindByName(ItemName), e);
    }

    // our UI textbox wants to validate -
    // so we check all items and don't let the textbox
    // loose focus if an item in it could not be validated
    protected void NList_Validating(object sender, 
            System.ComponentModel.CancelEventArgs e) {
      e.Cancel = (!this.Validate());
    }


    // fire the event, if an item has been removed from the collection
    internal void OnRemoveItem(ObjectItem value) {
      if (this.ObjectItemRemoved != null)
        this.ObjectItemRemoved(this, value);
    }

关注点

此示例实际上为您提供了以下主题和技术的快速介绍:继承、委托和事件、控件构建、集合构建。由于我使用 ObjectItem 对象来处理对象,您可以直接增强它,或者从它继承您自己的扩展对象,以为其添加更多功能或根据您的需求进行调整。

希望这些代码对您有所帮助。由于这里只展示了部分代码,请使用上面的链接下载源代码。如果您有任何疑问,请随时与我联系。

© . All rights reserved.