Login


A Scrolling Status Control

By Jonathan Wood on 2/9/2011
Language: C#
Technology: .NETWinForms
Platform: Windows
License: CPOL
Views: 19,957
Desktop Development » Controls » User Controls » A Scrolling Status Control

Screenshot of Demo Project

Download Source Code Download Source Code

Introduction

When you want to keep the user informed about what your application is doing, you probably print a message to a text box or label control, or perhaps you display the message in a status bar.

But what happens when there are a lot of messages and you'd like the user to be able to see more than just the most recent?

For example, consider the Output Window in Visual Studio. Certainly, there can be a quick flurry of messages sent to this window. If each message overwrote the previous one, many messages would go by without giving the user a chance to see them. The way this window displays multiple lines makes sense because it allows the user to get a better feel for the types of messages that are being generated.

I've seen applications take various techniques to address the situation described above. Some used a regular multiline text box control, while still others used a list box control. Both are horribly inefficient.

In the case of a text box, the code must get the entire text from the control, append the new line, and then write it back to the control. Not only is this approach inefficient but the text keeps getting longer, making it even less efficient until some technique has to be employed to prevent the text from growing infinitely long.

This task requires efficiency and good performance because some applications can produce many messages in very quick succession. So I decided to write a simple user control that displays multiple lines of messages.

The ScrollingStatus User Control

Listing 1 shows my ScrollingStatus user control. The Write() method adds a new message to the control. Messages are displayed at the bottom, causing older messages to scroll up.

Each message string is stored separately in a collection, so the control avoids the overhead of appending strings into new, longer strings. The LineBufferSize specifies the maximum number of messages to hold. Once this many messages are stored, adding a new one causes the oldest one to be removed.

Messages are stored in a LinkedList<> collection. This makes adding messages to the end while removing them from the start very efficient.

Overall, this is a very simple control. However, I took a little more time to make it as efficient as possible. For example, the code calls the Windows API function ScrollWindow(). This function shifts a portion of the window to another location and I used it to scroll existing lines up, when needed, to avoid having to redraw those lines every time a new message is added. (Note that you'll need to modify the interoperability statements if you are compiling for 64-bit Windows.)

I also added a context menu with two commands and corresponding methods. Copy() copies the entire contents of the control onto the clipboard in the form of a multi-line string. Clear() clears the window and any messages currently stored in the message collection.

Listing 1: The ScrollingStatus User Control

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace TestScrollingStatus
{
    /// <summary>
    /// Implements scrolling status control
    /// </summary>
    public partial class ScrollingStatus : UserControl
    {
        [DefaultValue(100)]
        public int LineBufferSize { get; set; }

        LinkedList<string> _lines;
        private int _maxRows;

        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int left;
            public int top;
            public int right;
            public int bottom;
        }

        [DllImport("user32.dll")]
        public static extern int
            ScrollWindow(IntPtr hwnd, int cx, int cy,
            ref RECT rectScroll, IntPtr rectClip);

        public ScrollingStatus()
        {
            InitializeComponent();

            _lines = new LinkedList<string>();
            LineBufferSize = 100;
        }

        /// <summary>
        /// Writes a new line to the control.
        /// </summary>
        /// <param name="text">Line of text to add</param>
        public void Write(string text)
        {
            // Add text to line list
            _lines.AddLast(text);

            // Don't exceed maximum number of lines
            if (_lines.Count > LineBufferSize)
                _lines.RemoveFirst();

            // Update display
            int rowHeight = Font.Height;
            if (_lines.Count > _maxRows)
            {
                // Speed updates by scrolling current display
                RECT rect = new RECT();
                rect.left = 0;
                rect.top = rowHeight;
                rect.right = ClientSize.Width;
                rect.bottom = rowHeight * GetVisibleRows();
                ScrollWindow(Handle, 0, -rowHeight, ref rect, (IntPtr)0);
                Update();
            }
            else
            {
                // Speed updates by repainting only new line
                Rectangle rect = ClientRectangle;
                rect.Y = (_lines.Count - 1) * rowHeight;
                rect.Height = rowHeight;
                Invalidate(rect);
            }
        }

        /// <summary>
        /// Writes a new line to the control with formatting.
        /// </summary>
        /// <param name="format">Format string</param>
        /// <param name="args">Format arguments</param>
        public void Write(string format, params object[] args)
        {
            Write(String.Format(format, args));
        }

        /// <summary>
        /// Clears all lines from control.
        /// </summary>
        public void Clear()
        {
            _lines.Clear();
            Invalidate();
        }

        /// <summary>
        /// Gets all text as a multi-line string.
        /// </summary>
        public override string Text
        {
            get
            {
                StringBuilder builder = new StringBuilder();
                foreach (string line in _lines)
                    builder.AppendLine(line);
                return builder.ToString();
            }
        }

        /// <summary>
        /// Copies all text as a multi-line string.
        /// </summary>
        public void Copy()
        {
            Clipboard.Clear();
            Clipboard.SetText(Text);
        }

        /// <summary>
        /// Recalculates the number of rows that will fit
        /// in the control window.
        /// </summary>
        private void CalculateMaxRows()
        {
            _maxRows = ClientRectangle.Height / Font.Height;
        }

        /// <summary>
        /// Returns the number of rows visible within the
        /// control window.
        /// </summary>
        /// <returns></returns>
        private int GetVisibleRows()
        {
            return Math.Min(_lines.Count, _maxRows);
        }

        // Paint background
        protected override void OnPaintBackground(PaintEventArgs e)
        {
            e.Graphics.FillRectangle(new SolidBrush(BackColor), e.ClipRectangle);
        }

        // Paint control
        protected override void OnPaint(PaintEventArgs e)
        {
            // Get number of visible rows
            int rows = GetVisibleRows();
            if (rows > 0)
            {
                int rowHeight = Font.Height;
                int clipTop = e.ClipRectangle.Top;

                // Find first node and calculate position
                LinkedListNode<string> node = _lines.Last;
                int yPos = rowHeight * (rows - 1);
                while (yPos > clipTop)
                {
                    node = node.Previous;
                    yPos -= rowHeight;
                }

                // Draw lines
                using (Brush brush = new SolidBrush(ForeColor))
                {
                    PointF point = new PointF(0, yPos);
                    while (node != null)
                    {
                        e.Graphics.DrawString(node.Value, Font, brush, point);
                        point.Y += rowHeight;
                        node = node.Next;
                    }
                }
            }
        }

        // Adjust for resized window
        protected override void OnSizeChanged(EventArgs e)
        {
            CalculateMaxRows();
            Invalidate();
        }

        private void copyToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Copy();
        }

        private void clearToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Clear();
        }
    }
}

Room for Improvements

As you can see, I spent a little time making the control fast in the case where a new message is being added to the control. When resized, the control ensures everything is repainted, although not quite as efficiently. For my purposes, that's all I needed. However, there is certainly room for more features.

The most glaring feature would be a scroll bar to view older items still in the message buffer. Ideally, a vertical scroll bar would become visible when there are older items to view, and scrolling would cause those items to become visible. While you're at it, you could also add a horizontal scroll bar to handle cases where some messages are too wide to fit the control's client area.

Be aware, however, that the scroll bars associated with the user control's AutoScroll property will make the control considerably less efficient than I have it now. You also need to consider what happens when the user scrolls up and is viewing older messages when they are removed as a result of newer messages being added.

Other features that come to mind include the ability to select parts of the text so you can copy selected messages to the clipboard, and even the ability to allow the user to type in text, similar to a console application.

Conclusion

For me personally, this is a control I have many uses for. I have some applications that run for hours processing data. This control would be a nice addition to some of the forms in those applications. Perhaps you can use it as well. And be sure to let me know if you add some of those improvements I mentioned.

End-User License

Use of this article and any related source code or other files is governed by the terms and conditions of The Code Project Open License.

Author Information

Jonathan Wood

I'm a software/website developer working out of the greater Salt Lake City area in Utah. I've developed many websites including Black Belt Coder, Insider Articles, and others.