Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

LineEditor Control – Line based visual input/output

4.56/5 (6 votes)
4 Jul 2007CPOL9 min read 1   611  
A line based control for output and optional input, and a discussion of how to create a custom control from the .NET UserControl.

Introduction

A common requirement for visual applications is a 'ListBox++' - a line based log which allows for some differentiation between entries. The most common of all, and the one that the first version of this class met, is to be able to have different lines with different colours (error lines in red, etc.).

This control solves the problem in a general way by allowing lines to render themselves and control their own height. It is provided with a basic Line class that displays itself in the default font and a specified colour, a class to display lines containing images, and one that allows the user to modify its text content. It also allows you to create custom lines that can have any display form.

It is also a study in how to create a useful custom control by inheriting the UserControl class in System.Windows.Forms. To skip straight to the information on how to make a good control, go here.

Sample use of the control is available in my game lobby client. The game list, game info pane, player list, and chat/log area are all LineEditors.

Use

The control is provided in a single .cs file, and once compiled into a DLL, can be added to Visual Studio, SharpDevelop, and suchlike in the usual way, to appear as a custom control which you can drag and drop onto your form. Alternatively, you can use the constructor and add the control manually:

C#
LineEditor lineEditor = new LineEditor();
someContainer.Controls.Add(lineEditor);

The control exposes several properties that allow you to customise the visual effect and how it interacts with the user:

  • Editable: Whether the user can edit lines that accept user input. Defaults to false.
  • ShowSelection: Whether to highlight the selected line with your current Windows selection colours. Whether on or off, a dotted rectangle is placed around the selected line to identify it. Defaults to true.
  • BottomAligned: Whether the scroll is adjusted when a new line is added, to keep the same offset from the bottom of the control. This is useful for logs or command prompt style uses where the last item should be visible most of the time. Defaults to true.
  • Selectable: Whether the user can select the control.

You can also use all the standard colour and style options provided by UserControl, of course.

The simplest use of the control is just to add lines programmatically, using the Line class. If you wish to add a default line (text only in a single colour), you can use the AddLine method:

C#
lineEditor.AddLine(Color.Blue, "A test line"); // the simplest way
lineEditor.AddLine(Color.Green, "With some data", new int[]{3, 4}); // attach data

You can attach arbitrary data to a line which is not used by the control, but which you can use to store your own information with a line.

To add other sorts of lines, you use the Lines property, which is a LineCollection. (If I hadn't originally written this class a while ago under .NET 1.1, this would simply be a List<line>.) Like all collections, it provides Add, Insert, and Remove methods that take instances of Line:

C#
lineEditor.Lines.Add(new Line(Color.Blue, "Another test"));
lineEditor.Lines.Add(new ImageLine(Image.FromFile("test.png")));
// remember to set Editable to true
lineEditor.Lines.Add(new EditableLine(Color.Green, "Editable text"));
lineEditor.Lines.Insert(new Line(Color.Black, "At the front"), 0);

As well as the simple textual Line, an ImageLine (which will display any instance of an Image) and an EditableLine (which the user can modify the text of) are provided.

Custom Lines

Many specialist lines that you may want to produce can be implemented simply by creating a Bitmap, drawing to it, and using an ImageLine. However, if you wish to produce an interactive or complex line, you can inherit from the Line class and modify its behaviour. The EditableLine class is a good guide as it is a relatively complex class that modifies many facets of Line. Here are the methods you will want to override:

  • public virtual void Paint(Graphics g, Font font, Color c, int ypos)
  • The most important of the virtual methods, this paints the line. You are passed a font with which to draw, the colour associated with this line (which may be the Windows selection highlight colour if it is selected and ShowSelection is true), and the Y position at which this line begins. You should not generally paint above ypos or below ypos+GetHeight(font). It is recommended that you observe the protected indent member as the smallest X coordinate you use to ensure that your line is aligned with the others in the control.

  • public virtual int GetHeight(Font font)
  • Returns the height of the line, given a font in which to draw. By default, returns the space taken up by the text at that font setting.

  • public virtual Font GetFont(Font font)
  • Chooses a font to draw with, given the one requested by the control. Overriding this method allows you to draw a line with a different font to the others.

  • public virtual Line CopyTo(Line li) and public virtual object Clone()
  • Allows the line to be cloned. Add any properties you add to your custom class to CopyTo(), and override Clone in a similar way to EditableLine:

    C#
    public override object Clone(){ return CopyTo(new MyCustomLine(Color, Text)); }
  • public virtual void InsertText(string text)
  • Defines how text is inserted. By default, it is appended.

You can always see and modify the Text property. If you want your line to be responsive to user input, there are also some input methods you will want to override:

C#
public virtual bool OnKeyDown(KeyEventArgs e){return false;}
public virtual bool OnKeyUp(KeyEventArgs e){return false;}
public virtual bool OnKeyPress(KeyPressEventArgs e){return false;}

Return true from these methods if you handled the keystroke and wish to suppress the default action they would cause. Remember that you need to set the Editable property of the control to true to get key events. Mouse methods will be added following a similar pattern in a later version.

If you want to provide lines which the user can edit, you may want to inherit from EditableLine instead of Line.

Writing a User Control

This control inherits directly from UserControl, and it has been instructive for me to see how to write a usable control from that base. .NET makes it relatively easy to do so, but there are a few points which caused me a little difficulty.

First Steps

The first thing to do is to paint your control, by overriding OnPaint. This is called every time your control is to be painted, so I have attempted to be somewhat efficient by caching Pens and Brushes. You will also need to create an InitializeComponent method (American spelling obligatory) to ensure that the control can appear as a custom control in your IDE and call it from the constructor. In this method, you should put:

C#
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint |
         ControlStyles.DoubleBuffer | ControlStyles.Selectable | 
         ControlStyles.StandardClick , true)

Look at the documentation for Control.SetStyle to determine exactly which styles you require, but UserPaint and Selectable are essential for most controls.

If you are creating a control for which painting can be complicated, you might consider caching the entire image to be painted, and updating it separately when it needs doing. I use this technique in the map control for my geological mapper, which may have to paint several thousand Bezier segments on a complicated map, and in my Abaria game viewer, which has to paint hundreds of 3D triangles with lighting. But for most controls, such as this one, you can easily perform all the calculation in Paint.

Mouse Interaction

To allow the user to interact with your control, you need to react to the mouse. Typically, you will want to use mouse down to select an item, and possibly mouse move to drag items around or show a hover highlight (but remember that doing this forces many repaints, so make your Paint code efficient).

The methods you want to override are:

C#
protected override void OnMouseDown(MouseEventArgs e)
protected override void OnMouseUp(MouseEventArgs e) // maybe
protected override void OnMouseMove(MouseEventArgs e) // maybe

A typical mouse handler is like this one from LineEditor:

C#
protected override void OnMouseDown(MouseEventArgs e){
    base.OnMouseDown(e);
    if(!Selectable) return;
    Focus();
    SelectedIndex = GetIndexAt(e.X, e.Y);            
    Invalidate();
}

You should always call base.Whatever(e), as not doing so will cause the default behaviour not to happen and odd things can happen. The GetIndexAt method is a common requirement, returning the index of the item under the mouse.

To implement dragging, you would have a toggle that you set in OnMouseDown/Up, and perform dragging in OnMouseMove if the mouse was down. I have no C# example of the technique as it is not implemented in this control.

Setting the StandardClick style (see First Steps) causes the Click and DoubleClick events to be fired, so by setting the selected item on mouse down, you allow these events to be useful.

Keyboard

At first glance, using the keyboard is also easy. As with the mouse, there are three methods to override to gain keyboard functionality:

C#
protected override void OnKeyDown(KeyEventArgs e)
protected override void OnKeyUp(KeyEventArgs e)
protected override void OnKeyPress(KeyPressEventArgs e)

KeyPress is for printable characters and, for some reason, backspace (8) and Enter (13). KeyDown and KeyUp are provided for all keys (KeyDown repeats if the key is held down) and tell you the key and modifiers (Shift, Alt, Ctrl).

However, to accept all relevant keys and characters, you need to prevent the framework from 'hijacking' them for things like control mnemonics (the underlined letters that you can use with Alt to activate a control). To do this, you need to override two more methods:

  • protected override bool IsInputKey(Keys k)
  • Whether the given key is to be processed by this control. Return true if you would like to see this key in OnKeyUp/Down. You should always return base.IsInputKey(k) if you do not want to treat the key differently to its normal state. In the case of the LineEditor, the arrow keys return true.

  • protected override bool IsInputChar(char c)
  • Whether the given character should be processed by the control. Return true if you would like to see this key in OnKeyPress - if you don't, it can be hijacked by a control mnemonic. For a textual control like this one, you will always want to return true from this method.

Navigation within a control once you are able to handle the key events is left as an exercise for the reader - although you may be able to get some inspiration from the EditableLine.KeyDown method. The important point is to keep track of the cursor position so you can insert, remove, and modify text at the correct point. To measure partial strings for placing the caret correctly, you should use the Graphics.MeasureCharacterRanges method (not Graphics.MeasureString) as in this excerpt from EditableLine.Paint:

C#
StringFormat sf = new StringFormat();
sf.SetMeasurableCharacterRanges (
        new CharacterRange[]{ new CharacterRange(0, ci) } );
sf.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces;
cx = indent + (ci > 0 ? g.MeasureCharacterRanges(
     s, font,
     new RectangleF(0, 0, Host.Parent.ClientSize.Width, font.Height), sf
  )[0].GetBounds(g).Right : 0);

Scrolling

Another common requirement is for a scrollbar to appear when the control is made too small or its content becomes too large for the space available. My technique is to include an instance of ScrollBar in the class, which is docked appropriately (in this case, DockStyle.Right) and made visible when it is required.

C#
// in InitializeComponent
scrollbar = new VScrollBar();
scrollbar.Dock = DockStyle.Right;
scrollbar.Visible = false;
scrollbar.Scroll += new ScrollEventHandler(ScrollbarMoved);
this.Controls.Add(scrollbar);

Whenever something changes which might affect the scroll bar (in OnResize, or when modifying the content of the control; in this case, by adding or removing lines), you should call a RecalculateScrollbar method which looks something like this one from the LineEditor:

C#
private void RecalculateScrollbar(){
    int bottom = 0;
    foreach(Line line in lines) bottom += line.GetHeight(Font);
    if(bottom < ClientSize.Height){
        scrollbar.Visible = false;
        scrollbar.Value = 0;
        return;
    }
    scrollbar.Visible = true;
    scrollbar.Maximum = bottom;
    scrollbar.LargeChange = ClientSize.Height;
    scrollbar.SmallChange = Font.Height;
    lastSBValue = scrollbar.Value;
}

It is also useful to have a VisibleWidth property:

C#
public int VisibleWidth {
    get { return ClientSize.Width - (scrollbar.Visible ? scrollbar.Width : 0); }
}

... which you can use when working out where to wrap, when to add a horizontal scrollbar, and so on.

The event handler for the scrollbar can usually simply call Invalidate to force the control to redraw; the value of the scrollbar should be checked in Paint to draw items at the right place.

This example deals with a vertical scroll bar (probably the most common), but a horizontal scrollbar is very similar. The only thing to watch out for is that if you have both scrollbars, you should paint the 'dead space' in the bottom right in the background colour and shorten both scrollbars.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)