Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

MultiColor MultiFormat Label

0.00/5 (No votes)
13 Mar 2016 1  
Labels are not only to add titles for fields in a Form. They are a usefull tool to comunicate with the application.

Introduction

Please be patient with my descriptions. English is not my best!
During the development of an application, I needed to display a large list of process comments. It was very difficult to follow de text searching for errors and other advertisements with a plain writed text.
I surfed the net looking for a label control that was able to display text in various colors and formats, with no result. In order words the unique solution proposed was to use a RichTextBox, but that was not what I was looking for, so I decided to create my own control from scratch.
After various failed intents, I decided to take the easy way and fill a UserControl with a label collection, so that each label can be formatted in a different mode.
For it, I created two classes, CustomText and TextFormat and organized them in two List<> collections from where take the elements to fill each Label. The only big difficulty was to transmit the PropertyChange events from the classes and List<> to the main class to made a really interactive procedure.
Observable Collection gives poor information over eventargs, and in any case transmits the propertychanges for its items. The solution was to extend the CollectionEditor to surround the problem.
At the end, I not only resolved my problem, I obtained a Control that resolves the problem to create MultiColor MultiFormat Labels in an easy way to create and maintain. I also have included the possibility to scroll labels with large text automatically.

Using the code

Overview

The configuration of the MultiFormat Label is grouped in a Category of the ToolBox propertygrid named "ContentAppearance".
There you can find all the properties you need to create the text and the text formats.
With the configuration of the MainText the control creates a MainFormat that can be used for other text lines in the control.

Simple MultiFormatLabel

To create a simple MultiFormatLabel, you only need to select your preferences in the same way you do it to create a standard Label control.

Enhanced MultiFormatLabel

After the selection of the basic Label properties (yellow fields), we select the content for the MainText, in this case, an Image and their position (blue fields).
For the rest of the text lines, we need to configure the CustomizedText and the CustomizedTextFormats (green fields).

For each of the lines, we select on the CustomTexts Editor, the content of the line (text and/or image) and a Textformat that will be configured in the Text Format Editor, from the dropdown List.
The order of the CustomText in the List, will determinate the order in which the lines will be displayed.

In the Text Format Editor, select the desired fields in order to give the text the desired appearance.

AddText on Runtime (OnDemand)

On Runtime you can add text lines to the control in 3 ways:

     - Using OnDemand CustomizedTexts
     - Adding free text with CustomizedTextFormats
     - Adding free text with CustomFormats you create programmatically

      - OnDemand CustomizedTexts

To add OnDemand text you must first create a CustomizedText as explained above.
In the CustomText Property Grid, change the OutputType from "Permanent" to "OnDemand".
This will hide the text line in the MultiFormatLabel, then the Label will show her appearance when it is first loaded. So, if you will control how it will show during design time, do this after finish with the label design.

The MultiFormatLabel has two methods to add OnDemand text:

     AppendCustomizedText(string CustomTextName)
     AppendCustomizedText(CustomText CText)

In this case we will use the first method, so now, in your program code, add following line when you will that the text appears in the label.

        private void button_Full_Click(object sender, EventArgs e)
        {
            this.MFL_OnDemand.AppendCustomizedText("full");
        } 
      - Free text with CustomizedTextFormats

To add a free text with an existing CustomizedTextFormat, you have 10 methods:

     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)

In the 5 first methods you don't need to indicate the TextFormat. They will be displayed using the MainFormat, that is, the format you have created for the MainText.

The next 5 methods needs the name of an preconfigured TextFormat in the MultiFormatName.

 

// 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");
      - Free text with CustomFormats you create programmatically

To add content with TextFormats you created for On Runtime use, you have 5 methods:

     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)

The methods shown above, can be used to add a free selected text and/or image, with an on Runtime created TextFormat or TextFormats obtained from the MultiFormatLabel. To create a TextFormat you can use following code:

            // 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");

And to add a free text with a on Runtime created 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);

Tip

When you create a new MultiFormatLabel you can spare time following these recommendations:

   - Open the CustomizedTextFormat Collection.

   - Create two, three or more TextFormats and click OK without making any change.

   - Open the CustomizedText Collection.

   - Add the lines you will, changing the name (if necessary), write the text and/or add the image, and assign a  TextFormat you created before from the dropdown menu to each CustomText.

   - When you are satisfied with your texts, click OK
.
   - Open now the CustomizedTextFormat Collection again.

   - Customize the TextFormats you have created before and have assigned to CustomText, viewing the effect until you are satisfied with them.

   - Give a Name to the TextFormats that lets you identificate them easily (if necessary).

   - Delete the TextFormat you haven't used, and click OK.

   - If you are satisfied with the result and you will have text lines to use OnDemand, reopen the CustomizedText Collection and change the OutputType for these lines.

Automatic Scroll

During the development of this control I had to fight with the vertical scroll bar to force his behavior in the form I wanted. From this I have deduced it were not difficult to implement the scrolling effect to read long texts when the available space was restringed. So I decided to add this facility to the control.
To make your Label scroll automatically, you only have to change following parameters.

The AutomaticScrollSpeed determinates in pixels per second the speed that the scrolling text will move up.
The value range is imitated between 5 and 500, and the default value is 25.
To control the scrolling process you have four methods:

     StartScrolling()          Start the scrolling process. Scrolling will end when the text has reached his last line and will  retain his position.
     StopScrolling()          Stops the scrolling and resets the control to her starting position.
     PauseScrolling()        Stops the scrolling at the position he has reached.
     ContinueScrolling()   Continues the scrolling from the position where it was stopped.

Demo Project

The Demo Project shows examples of how to configure and use the MultiFormatLabel control.
All labels in the forms are MultiFormatLabels.
Under the examples, you can see the Process Output Label, which shows how to use a MultiFormatLabel to obtain information from the application in an easy readable form. I think, labels are underutilized. They are static texts, but the question is to determinate when its content is created and if his appearance must be a monotone sequence of letters. Many applications need to inform the user was happens during their execution and the label is indeed the best solution, but not in her actual stand.
At the bottom of the form you can see a button that gives you the possibility to open a User Manual. When I thought on this I asked me, why not use the MultiFormatLabel to create it? Said and done. As you can see, it is possible to create short help text including images only with a label! You don't need to appeal to Help Designer to include short explanation in a program. Use a MultiFormatLabel.

Points of Interest

Now let's see a little bit of code.

The first problem I have found was to synchronize the  CollectionEditor display with the real List<> actualization. The CollectionEditor don't actualizes the List<> with public methods using instead an internal list that is maintained while the CollectionEditor is open. She creates the new items and actualizes their display in accordance but retains active the deleted items until the Editor is open actualizing only the display. If you click the OK button to close the Editor, the internal list actualizes the real List<>, adding or deleting items in accordance with what you have made during editing time.

But if you click the CANCEL button, the List<> will not be actualized.

To reproduce this behavior, the TextFormatEditorLoad and the CustomTextEditorLoad events creates copies of the significant collections in the program awaiting for what button closes the Editor.

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);
}

And for the case the CANCEL button are clicked, we added methods that resets the changes made during edition to there original state.

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;
}

But there are not a Load or CancelButton_ClickEvent in the CollectionEditor. To resolve it we must extend the CollectionEditor and the 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;
   }
}

In addition to this, we must also control if the delete button is clicked, then the destroyInstance event of the Editor will not fire while the Editor is open, and in the case of the CustomText we also must detect the position of the line in the List, then that is important for the disply order in the Label. After adding this, our CollectionEditor looks like this:

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;
   }

}

With this, we have resolved the first problem, how to capture the Collection Events. Now, we must resolve the second problem: how to capture the propertychange events for the TextFormat and the Custom Text.

In the definition of both classes I have added a PropertyChangeEventHandler:

[System.ComponentModel.Browsable(true)]
[System.ComponentModel.DesignTimeVisible(true)]
public class TextFormat : INotifyExtPropertyChanged, ICloneable
{
       [System.ComponentModel.DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public event ExtPropertyChangedEventHandler ExtPropertyChanged = delegate { };
...


And in the CollectionEditors EditValue method added the following lines for the 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);
        }
    }
...

for the CustomText and

...

    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);
        }
    }

...

for the TextFormat.

So each time the ColletionEditor opens, we add a destination method in the main Class to each Item. (The -= and += consecutively avoids eventhandler accumulation)

And following methods adds the eventhandler to every new item created during editing:

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.

The last detail we have to resolve is how to create a dropdownlist that permit the selection of a textformat by its name to assign it to the CustomText. First I created a normal dropdownlist editor.

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();
    }
}

Now in the CollectionEditor added following line and modified the EditValue and CreateInstance methods as follows:

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;
}

Now we have imported a list with the TextFormatNames in our CollectionEditor, and we can translate it to each CustomText item in a variable named Data.

We modify our TextFormatNameEditor as follows:

...
    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;
        }
    }
...

And this is all.

A last detail.

I musted expand the propertychageeventargs then the information transmited is limited to he property Name and I needed to transfer also values, specially for the Name property.

 

The extended propertyevent is as follow:

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;
    }
}

and the methos used in the TextFormat and CustomText are:

...

[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);
}

...

History

First publication: MultiFormatLabel, version:1.0.0 - 28/02/2016

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here