多色多格式标签





5.00/5 (4投票s)
标签不仅仅是为表单中的字段添加标题。它们是与应用程序通信的有用工具。
引言
请耐心对待我的描述。英语不是我的强项!
在开发一个应用程序的过程中,我需要显示一个包含大量进程注释的列表。使用纯文本很难跟踪错误和其他广告信息。
我在网上搜索能够显示各种颜色和格式文本的标签控件,但一无所获。换句话说,唯一的解决方案是使用 RichTextBox,但这并不是我想要的,所以我决定从头开始创建自己的控件。
经过几次失败的尝试后,我决定采取简单的办法,用一个标签集合填充一个 UserControl,这样每个标签都可以以不同的模式进行格式化。
为此,我创建了两个类 CustomText 和 TextFormat,并将它们组织在两个 List<> 集合中,从那里获取元素来填充每个 Label。唯一的难点是将 PropertyChange 事件从类和 List<> 传递到主类,以实现真正的交互式过程。
Observable Collection 对 eventargs 提供的信息很少,并且在任何情况下都不会传递其项的属性更改。解决方案是扩展 CollectionEditor 来规避这个问题。
最终,我不仅解决了我的问题,还获得了一个控件,可以轻松创建和维护多色多格式的标签。我还加入了自动滚动显示长文本标签的可能性。
使用代码
概述
MultiFormat Label 的配置分组在工具箱属性网格的“ContentAppearance”类别中。
在那里,您可以找到创建文本和文本格式所需的所有属性。
通过配置MainText,控件会创建一个MainFormat,该格式可用于控件中的其他文本行。
简单的 MultiFormatLabel
要创建简单的 MultiFormatLabel,您只需像创建标准 Label 控件一样选择您的偏好设置。
增强型 MultiFormatLabel
在选择基本 Label 属性(黄色字段)后,我们选择 MainText 的内容,在这种情况下,是一个图像及其位置(蓝色字段)。
对于其余的文本行,我们需要配置 CustomizedText 和 CustomizedTextFormats(绿色字段)。
对于每一行,我们在 CustomTexts Editor 中选择行的内容(文本和/或图像)以及一个将在 Text Format Editor 中配置的 Textformat,从下拉列表中选择。
CustomText 在列表中的顺序将决定行的显示顺序。
在 Text Format Editor 中,选择所需的字段以赋予文本所需的样式。
运行时添加文本 (OnDemand)
在运行时,您可以通过 3 种方式向控件添加文本行
- 使用 OnDemand CustomizedTexts
- 使用 CustomizedTextFormats 添加自由文本
- 使用程序创建的 CustomFormats 添加自由文本
- OnDemand CustomizedTexts
要添加 OnDemand 文本,您必须先按照上述说明创建一个 CustomizedText。
在 CustomText Property Grid 中,将 OutputType 从“Permanent”更改为“OnDemand”。
这将隐藏 MultiFormatLabel 中的文本行,然后标签将在首次加载时显示其外观。因此,如果您想在设计时控制其显示方式,请在完成标签设计后再执行此操作。
MultiFormatLabel 有两种方法可以添加 OnDemand 文本
AppendCustomizedText(string CustomTextName)
AppendCustomizedText(CustomText CText)
在这种情况下,我们将使用第一种方法,所以现在,在您的程序代码中,当您希望文本出现在标签时,添加以下行。
private void button_Full_Click(object sender, EventArgs e) { this.MFL_OnDemand.AppendCustomizedText("full"); }
- 使用 CustomizedTextFormats 添加自由文本
要使用现有的 CustomizedTextFormat 添加自由文本,您有 10 种方法
AppendContent(string text)
AppendContent(string[] TextLines)
AppendContent(Image image)
AppendContent(string text, Image image)
AppendContent(string[] TextLines, Image image)
AppendContent(string text, string TextFormatName)
AppendContent(string[] TextLines, string TextFormatName)
AppendContent(Image image, string TextFormatName)
AppendContent(string text, Image image, string TextFormatName)
AppendContent(string[] TextLines, Image image, string TextFormatName)
在前 5 种方法中,您无需指定 TextFormat。它们将使用 MainFormat 显示,即您为 MainText 创建的格式。
接下来的 5 种方法需要 MultiFormatName 中预先配置的 TextFormat 名称。
// Filling an MultiFormatLabel with on runtime created text using a preconfigured CustomizedTextFormat else if (radioButton_blue.Checked) this.MFL_OnDemand.AppendContent(this.textBox_VarText.Text, "Blue");
- 使用程序创建的 CustomFormats 添加自由文本
要添加您为运行时使用的 TextFormats 创建的内容,您有 5 种方法
AppendContent(string text, TextFormat TFormat)
AppendContent(string[] TextLines, TextFormat TFormat)
AppendContent(Image image, TextFormat TFormat)
AppendContent(string text, Image image, TextFormat TFormat)
AppendContent(string[] TextLines, Image image, TextFormat TFormat)
上述方法可用于添加自由选定的文本和/或图像,以及运行时创建的 TextFormat 或从 MultiFormatLabel 获取的 TextFormats。要创建 TextFormat,您可以使用以下代码
// On Runtime created TextFormat to use outside of the created in the MultiFormatLabel this.MyFormat = new TextFormat( Color.Black, new Font("Book Antiqua", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))), Color.Yellow, BorderStyle.None, ContentAlignment.MiddleCenter, ContentAlignment.MiddleCenter, new Padding(0), "MyFormat");
并添加使用运行时创建的 TextFormat 添加自由文本
// Filling an MultiFormatLabel with on runtime created text and CustomizedTextFormat if (radioButton_myformat.Checked) this.MFL_OnDemand.AppendContent(this.textBox_VarText.Text, this.MyFormat);
提示
当您创建新的 MultiFormatLabel 时,遵循以下建议可以节省时间
- 打开 CustomizedTextFormat Collection。
- 创建两个、三个或更多 TextFormats,然后单击 OK,无需进行任何更改。
- 打开 CustomizedText Collection。
- 添加您想要的行,更改名称(如果需要),编写文本和/或添加图像,并从下拉列表中为每个 CustomText 分配您之前创建的 TextFormat。
- 当您对文本满意时,单击 OK。
.
- 现在再次打开 CustomizedTextFormat Collection。
- 自定义您之前创建并分配给 CustomText 的 TextFormats,观察效果直到您满意为止。
- 为 TextFormats 命名,以便轻松识别它们(如果需要)。
- 删除未使用的 TextFormat,然后单击 OK。
- 如果您对结果满意并且您将有 OnDemand 可用的文本行,请重新打开 CustomizedText Collection 并为这些行更改 OutputType。
自动滚动
在开发此控件的过程中,我不得不与垂直滚动条斗争,以强制其按我想要的方式行为。由此我推断,要实现滚动效果以在可用空间受限时阅读长文本并不难。所以我决定将此功能添加到控件中。
要使您的 Label 自动滚动,只需更改以下参数。
AutomaticScrollSpeed 以每秒像素为单位决定滚动文本向上移动的速度。
值范围在 5 到 500 之间,默认值为 25。
您有四种方法可以控制滚动过程
StartScrolling() 开始滚动过程。滚动将在文本滚动到最后一行时结束,并保持其位置。
StopScrolling() 停止滚动并将控件重置到其初始位置。
PauseScrolling() 停止滚动,停留在已到达的位置。
ContinueScrolling() 从停止的位置继续滚动。
演示项目
演示项目展示了如何配置和使用 MultiFormatLabel 控件的示例。
表单中的所有标签都是 MultiFormatLabels。
在示例下方,您可以看到 Process Output Label,它展示了如何使用 MultiFormatLabel 以易于阅读的方式获取应用程序信息。我认为标签的利用率不足。它们是静态文本,但问题在于确定其内容何时创建以及其外观是否必须是单调的字母序列。许多应用程序需要告知用户其执行过程中发生了什么,而标签确实是最佳解决方案,但不是它目前的标准状态。
在表单底部,您可以看到一个按钮,可以打开用户手册。当我想起这一点时,我问自己,为什么不使用 MultiFormatLabel 来创建它呢?说到做到。正如您所见,只需一个标签就可以创建包含图像的简短帮助文本!您无需求助于 Help Designer 来在程序中包含简短的解释。使用 MultiFormatLabel。
关注点
现在让我们看一些代码。
我发现的第一个问题是将 CollectionEditor 的显示与实际的 List<> 更新同步起来。CollectionEditor 不使用公共方法更新 List<>,而是使用一个内部列表,该列表在 CollectionEditor 打开期间进行维护。它创建新项并相应地更新它们的显示,但保留已删除的项的活动状态,直到编辑器关闭并仅更新显示。如果您单击 OK 按钮关闭编辑器,内部列表将更新实际的 List<>,根据您在编辑期间所做的操作添加或删除项。
但是,如果您单击 CANCEL 按钮,List<> 将不会被更新。
为了重现此行为,TextFormatEditorLoad 和 CustomTextEditorLoad 事件会创建程序中重要集合的副本,等待编辑器关闭的按钮。
internal void TextFormatEditorLoad(object sender, EventArgs e) { this.textFormatNamesCop = this.cloneNames(this.textFormatNames); this.textFormatsCop = this.cloneTextFormats(this.CustomizedTextFormats); this.customTextNamesCop = this.cloneNames(this.customTextNames); this.customLinesCop = this.cloneCustomTexts(this.CustomizedTexts); this.controlsCop = this.cloneControls(this.Controls); } internal void CustomTextEditorLoad(object sender, EventArgs e) { this.customTextNamesCop = this.cloneNames(this.customTextNames); this.customLinesCop = this.cloneCustomTexts(this.CustomizedTexts); this.controlsCop = this.cloneControls(this.Controls); }
并且在单击 CANCEL 按钮的情况下,我们添加了将编辑期间所做的更改重置为其原始状态的方法。
internal void TextFormatEditorCancel(object sender, EventArgs e) { this.textFormatNames = this.cloneNames(this.textFormatNamesCop); this._customizedTextFormats = this.cloneTextFormats(this.textFormatsCop); this.customTextNames = this.cloneNames(this.customTextNamesCop); this._customLines = this.cloneCustomTexts(this.customLinesCop); this.Controls.Clear(); this.Controls.AddRange(this.controlsCop); this.textFormatNamesCop = null; this.textFormatsCop = null; this.customTextNamesCop = null; this.customLinesCop = null; this.controlsCop = null; } internal void CustomTextEditorCancel(object sender, EventArgs e) { this._customLines = this.cloneCustomTexts(this.customLinesCop); this.customTextNames = this.cloneNames(this.customTextNamesCop); this.Controls.Clear(); this.Controls.AddRange(this.controlsCop); this.customTextNamesCop = null; this.customLinesCop = null; this.controlsCop = null; }
但是 CollectionEditor 中没有 Load 或 CancelButton_ClickEvent。要解决此问题,我们必须扩展 CollectionEditor 和 CollectionForm。
public class CustomTextCollectionEditor : CollectionEditor { private MultiFormatLabel mflabel; public CustomTextCollectionEditor(Type type) : base(type) { } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { if (provider != null && context != null) { if (context.Instance is MultiFormatLabel) { this.mflabel = (MultiFormatLabel)context.Instance; } } return base.EditValue(context, provider, value); } protected override CollectionForm CreateCollectionForm() { CollectionForm collectionForm = base.CreateCollectionForm(); Form cForm = collectionForm as Form; if (cForm != null) { cForm.Load += new EventHandler(this.mflabel.CustomTextEditorLoad); Button Cancel = cForm.CancelButton as Button; Cancel.Click += new EventHandler(this.mflabel.CustomTextEditorCancel); } return collectionForm; } }
除此之外,我们还必须控制是否单击了删除按钮,因为在编辑器打开时 DestroyInstance 事件不会触发,对于 CustomText,我们还必须检测行在 List 中的位置,因为这对于 Label 中的显示顺序很重要。添加此功能后,我们的 CollectionEditor 如下所示
public class CustomTextCollectionEditor : CollectionEditor { private MultiFormatLabel mflabel; private ListBox listBox; private object previous = null; private object actual = ""; public CustomTextCollectionEditor(Type type) : base(type) { } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { if (provider != null && context != null) { if (context.Instance is MultiFormatLabel) { this.mflabel = (MultiFormatLabel)context.Instance; } } return base.EditValue(context, provider, value); } protected override CollectionForm CreateCollectionForm() { CollectionForm collectionForm = base.CreateCollectionForm(); Form cForm = collectionForm as Form; if (cForm != null) { if (cForm.Controls[0] is TableLayoutPanel) { TableLayoutPanel tlpanel = cForm.Controls[0] as TableLayoutPanel; if (tlpanel.Controls[0] is Button) { Button downButton = tlpanel.Controls[0] as Button; downButton.Click += new EventHandler(this.MoveDown); } if (tlpanel.Controls[7] is Button) { Button upButton = tlpanel.Controls[7] as Button; upButton.Click += new EventHandler(this.MoveUp); } if (tlpanel.Controls[1] is TableLayoutPanel) { TableLayoutPanel tlpanel1 = tlpanel.Controls[1] as TableLayoutPanel; if (tlpanel1.Controls[1] is Button) (tlpanel1.Controls[1] as Button).Click += new EventHandler(this.deleteItem); } if (tlpanel.Controls[4] is ListBox) { this.listBox = tlpanel.Controls[4] as ListBox; listBox.SelectedIndexChanged += listBox_SelectedIndexChanged; } if (tlpanel.Controls[5] is PropertyGrid) { PropertyGrid pGrid = tlpanel.Controls[5] as PropertyGrid; pGrid.HelpVisible = true; } } cForm.Load += new EventHandler(this.mflabel.CustomTextEditorLoad); Button Cancel = cForm.CancelButton as Button; Cancel.Click += new EventHandler(this.mflabel.CustomTextEditorCancel); } return collectionForm; } private void deleteItem(object sender, EventArgs e) { PropertyInfo pInfo = this.previous.GetType().GetProperty("Value"); if (pInfo == null || this.listBox.SelectedItem == null) { pInfo = this.actual.GetType().GetProperty("Value"); if (pInfo != null) { CustomText ctext = pInfo.GetValue(this.actual, null) as CustomText; this.mflabel.RemoveCustomText(ctext); } return; } if (pInfo.GetValue(this.previous, null) is CustomText) { CustomText ctext = pInfo.GetValue(this.previous, null) as CustomText; this.mflabel.RemoveCustomText(ctext); } } private void MoveUp(object sender, EventArgs e) { PropertyInfo p = this.actual.GetType().GetProperty("Value"); if (p.GetValue(this.actual, null) is CustomText) { CustomText ctext = p.GetValue(this.actual, null) as CustomText; this.mflabel.CustomTextmoveUp(ctext); } } private void MoveDown(object sender, EventArgs e) { PropertyInfo p = this.actual.GetType().GetProperty("Value"); if (p.GetValue(this.actual, null) is CustomText) { CustomText ctext = p.GetValue(this.actual, null) as CustomText; this.mflabel.CustomTextmoveDown(ctext); } } private void listBox_SelectedIndexChanged(object sender, EventArgs e) { if (this.listBox.SelectedItem != null) { this.previous = this.actual; this.actual = this.listBox.SelectedItem; } } protected override bool CanSelectMultipleInstances() { return false; } }
这样,我们就解决了第一个问题,即如何捕获 Collection 事件。现在,我们必须解决第二个问题:如何捕获 TextFormat 和 Custom Text 的属性更改事件。
在两个类的定义中,我都添加了一个 PropertyChangeEventHandler
[System.ComponentModel.Browsable(true)] [System.ComponentModel.DesignTimeVisible(true)] public class TextFormat : INotifyExtPropertyChanged, ICloneable { [System.ComponentModel.DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public event ExtPropertyChangedEventHandler ExtPropertyChanged = delegate { }; ...
并在 CollectionEditors EditValue 方法中添加了以下行以供 CustomText 使用
... if (value is List<CustomText>) { List<CustomText> customtexts = (List<CustomText>)value; ... foreach (CustomText ctext in customtexts) { ... ctext.ExtPropertyChanged -= new ExtPropertyChangedEventHandler(this.mflabel.CustomTextHasChanged); ctext.ExtPropertyChanged += new ExtPropertyChangedEventHandler(this.mflabel.CustomTextHasChanged); } } ...
为 CustomText 和
... if (value is List<TextFormat>) { List<TextFormat> textformats = (List<TextFormat>)value; foreach (TextFormat textF in textformats) { textF.ExtPropertyChanged -= new ExtPropertyChangedEventHandler(this.mflabel.TextFormatHasChanged); textF.ExtPropertyChanged += new ExtPropertyChangedEventHandler(this.mflabel.TextFormatHasChanged); } } ...
为 TextFormat。
因此,每次 CollectionEditor 打开时,我们都会向主类的每个 Item 添加一个目标方法。(-= 和 += 连续避免了事件处理器的累积)
以下方法将事件处理器添加到编辑期间创建的每个新项
protected override object CreateInstance(Type itemType) { CustomText ctext = (CustomText)base.CreateInstance(itemType); ... ctext.ExtPropertyChanged += new ExtPropertyChangedEventHandler(this.mflabel.CustomTextHasChanged); this.mflabel.AddCustomText(ctext); return ctext; }
protected override object CreateInstance(Type itemType) { TextFormat textF = (TextFormat)base.CreateInstance(itemType); ... textF.ExtPropertyChanged += new ExtPropertyChangedEventHandler(this.mflabel.TextFormatHasChanged); this.mflabel.AddTextFormat(textF.Name); return textF; } This allows redirecting "on the fly" all the propertychange occurrences to the main Class.
最后一个细节是,我们需要解决如何创建一个下拉列表,该列表允许按名称选择文本格式,并将其分配给 CustomText。首先,我创建了一个普通的下拉列表编辑器。
public sealed class TextFormatNameEditor : System.Drawing.Design.UITypeEditor { private System.Windows.Forms.Design.IWindowsFormsEditorService edSvc = null; private ListBox TextFormatNamesList; public TextFormatNameEditor() { this.TextFormatNamesList = new ListBox(); this.TextFormatNamesList.BorderStyle = BorderStyle.FixedSingle; this.TextFormatNamesList.Size = new Size(80, 150); this.TextFormatNamesList.ItemHeight = 5; this.TextFormatNamesList.SelectedIndexChanged += new System.EventHandler(this.ObjectList_SelectedIndexChanged); } public override Object EditValue(ITypeDescriptorContext context, System.IServiceProvider provider, Object value) { if (context != null && context.Instance != null && provider != null) { this.edSvc = ((System.Windows.Forms.Design.IWindowsFormsEditorService)(provider.GetService(typeof(System.Windows.Forms.Design.IWindowsFormsEditorService)))); value = this.TextFormatNamesList.SelectedItem; } return value; } public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) { return UITypeEditorEditStyle.DropDown; } private void ObjectList_SelectedIndexChanged(Object sender, System.EventArgs e) { if (this.TextFormatNamesList.SelectedItem == null) return; this.edSvc.CloseDropDown(); } }
现在,在 CollectionEditor 中添加以下行,并按如下方式修改 EditValue 和 CreateInstance 方法
private string[] TextFormatNames; public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { if (provider != null && context != null) { if (context.Instance is MultiFormatLabel) { this.mflabel = (MultiFormatLabel)context.Instance; this.TextFormatNames = mflabel.textFormatNames.ToArray(); if (value is List<CustomText>) { List<CustomText> customtexts = (List<CustomText>)value; foreach (CustomText ctext in customtexts) { ctext.Data = this.TextFormatNames; ctext.ExtPropertyChanged -= new ExtPropertyChangedEventHandler(this.mflabel.CustomTextHasChanged); ctext.ExtPropertyChanged += new ExtPropertyChangedEventHandler(this.mflabel.CustomTextHasChanged); } } } } return base.EditValue(context, provider, value); } protected override object CreateInstance(Type itemType) { CustomText ctext = (CustomText)base.CreateInstance(itemType); string dText = this.GetDisplayText(ctext); dText = dText.Substring(dText.LastIndexOf(".") + 1); int counter = 0; do { counter++; } while (this.mflabel.customTextNames.Contains(dText + counter.ToString())); ctext.Name = ctext.Text = dText + counter.ToString(); ctext.Data = this.TextFormatNames; ctext.ExtPropertyChanged += new ExtPropertyChangedEventHandler(this.mflabel.CustomTextHasChanged); this.mflabel.AddCustomText(ctext); return ctext; }
现在,我们在 CollectionEditor 中导入了一个包含 TextFormatNames 的列表,并将其转换为一个名为 Data 的变量,该变量可以转换为每个 CustomText 项。
我们按如下方式修改 TextFormatNameEditor
... if (this.edSvc != null) { this.TextFormatNamesList.Items.Clear(); if (context.Instance is CustomText) { CustomText customtext = (CustomText)context.Instance; this.TextFormatNamesList.Items.AddRange(customtext.Data); if (value != null && value.ToString() != "") this.TextFormatNamesList.SelectedItem = value; this.edSvc.DropDownControl(this.TextFormatNamesList); value = this.TextFormatNamesList.SelectedItem; } } ...
这就是全部。
最后一个细节。
我必须扩展 propertychageeventargs,因为传输的信息仅限于属性名称,而我还需要传输值,特别是对于 Name 属性。
扩展的属性事件如下
public delegate void ExtPropertyChangedEventHandler(object sender, ExtPropertyChangedEventArgs e);
public interface INotifyExtPropertyChanged { event ExtPropertyChangedEventHandler ExtPropertyChanged; } public class ExtPropertyChangedEventArgs : PropertyChangedEventArgs { public object Value { get; private set; } public object OldValue { get; private set; } public object NewValue { get; private set; } public ExtPropertyChangedEventArgs(string propertyName) : base(propertyName) { } public ExtPropertyChangedEventArgs(string propertyName, object value) : base(propertyName) { Value = value; } public ExtPropertyChangedEventArgs(string propertyName, object oldValue, object newValue) : base(propertyName) { OldValue = oldValue; NewValue = newValue; } }
以及 TextFormat 和 CustomText 中使用的方法
... [Category("Appearance")] public string Name { get { return this._Name; } set { if (this._Name != value) { string temp = this._Name; this._Name = value; this.ValueChanged("Name", temp, value); } } } [Category("Appearance")] [Editor("System.ComponentModel.Design.MultilineStringEditor", typeof(System.Drawing.Design.UITypeEditor))] [Description("Text to display. If it is left blank and Image is null, line will not be shown.")] public string Text { get { return this._Text; } set { if (this._Text != value) { this._Text = value; this.ValueChanged("Text"); } } } private void ValueChanged(string PropName) { ExtPropertyChangedEventArgs args = new ExtPropertyChangedEventArgs(PropName); this.OnValueChanged(this, args); } private void ValueChanged(string PropName, string OldValue, string NewValue) { ExtPropertyChangedEventArgs args = new ExtPropertyChangedEventArgs(PropName, OldValue, NewValue); this.OnValueChanged(this, args); } public virtual void OnValueChanged(object sender, ExtPropertyChangedEventArgs e) { if (ExtPropertyChanged != null) ExtPropertyChanged.Invoke(sender, e); } ...
历史
首次发布:MultiFormatLabel,版本:1.0.0 - 2016/02/28