Click here to Skip to main content
15,890,185 members
Articles / Programming Languages / C#
Article

Ruler Control

Rate me:
Please Sign up or sign in to vote.
4.88/5 (82 votes)
8 Aug 20066 min read 385.8K   19.4K   192   138
Ruler control in C#

Sample Image - Ruler.jpg

Introduction

We're building a layout surface and need a ruler by which a user of an application containing the layout surface can judge the size of artifacts such as images, lines and boxes. Though I looked in CodeProject and other places I could not find any rulers written in C#. There are one or two in C++ and in VB but since we want all managed C# code these options did not fit the need so we are writing our own. Its a piece that will probably change as the layout surface project progresses but it may have use for some as it stands.

The image illustrates some sample rulers and the demo allows you to play with various properties to examine their effects.

Note that the ruler is not intended to be a replacement for a phyical ruler. It is not the purpose of the control that, somehow, an inch will be an inch on any display. Rather it is intended to provide a relative measurement. However, 72 points equals 1 inch so assuming that the printer is up to it, a ruler showing 6 inches and printed out should be 6 inches on paper.

Features

The control inherits from System.Windows.Forms.Control and the appearance of the ruler is controlled by 14 exposed a properties in addition to those of the underlying control:

  • BorderStyle (System.Windows.Forms.Border3DStyle enumerated type)
    A ruler can be displayed with any of the border styles supported by the Forms namespace. By default an etched border is drawn.
  • DivisionMarkFactor/MiddleMarkFactor (int)
    The division marks are displayed in the ForeColor selected. There are two different marks: a "middle" mark and the rest. A middle mark is displayed only when there are an even number of sub-divisions and never when RulerAlignment.raMiddle is used. The height or length of the marks is controlled by these factors that represent the proportion of the height (or width) to use. For example, a MiddleMarkFactor of 3 will give middle marks a length of 1/3 of the height (or width) of the control.
  • Divisions (int)
    This value determines the number of sub-divisions to display between each MajorInterval. The approriate number will depend on thew scale mode chosen along with application or aesthetic parameters.
  • MajorInterval (int)
    This value how frequently the markings repeat and how frequently numbers are displayed. For example, the usual MajorInterval for Inches and Centimetres is 1. That is, the number will be display per unit. For Points, a more appropriate MajorInterval will be 36 or 72 while for Pixel it might 100. The MajorInterval value for Centimetres might be 2 in which case the displayed numbers will be 2, 4, 6, etc and the middle mark will represent the odd numbered centimetres. There is no upper limit to the MajorInterval.
  • MouseLocation (Read-only)
    If mouse tracking is enabled and the mouse is within the x-axis bounds (for a horizontal ruler) or y-axis bounds (for a vertical ruler) this property will display the pixel offset into the ruler of the current mouse position. Note that the mouse does not have to be over the ruler, though it can be, just within the the bounds though anywhere on the screen.

    If mouse tracking is not enabled or the mouse is outside the respective bounds, this property will return -1.
  • MouseTrackingOn (bool)
    When enabled, the ruler will display a line in the ruler indicating the current position of the mouse. The control will also generate an event so that the containing form or control can track the mouse position and react appropriately. The demo uses this event to display the mouse position in terms of pixels and in terms of scale units.
  • Orientation (enumerated type)
    The available options are Horizontal and vertical
  • RulerAlignment (enumerated type)
    The marks on the ruler can be displayed on the top, middle or bottom of a horizontal ruler or the left, middle, right of a vertical ruler.
  • ScaleMode (enumerated type)
    Valid scales are Pixels, Points, Centimetres and Inches
  • ScaleValue (Read-only - double)
    The value of the current mouse position expressed in scale units
  • StartValue (double)
    The number, in scale units, from which the ruler should start displaying marks.
  • VerticalNumbers (bool)
    If set to true the numbers displayed on the ruler will be presented vertically.
  • ZoomFactor (double)
    The ruler has been developed to participate in a layout control that may, like a Word document, be zoomed. As the layout or document is zoomed, so the ruler must change its relative scale. The factor specified determines the level of apparent zooming. The control is constrained between 0.5 and 2 (50% and 200%).

Areas of interest

IMessageFilter

A control will, ordinarily, only receive mouse events when the mouse is over it. In this case, the control must receive mouse event information at all times while mouse tracking is enabled. To do this, the control also implements the interface IMessageFilter and calls Application.AddMessageFilter(this) to hook the message queue. The IMessageFilter interface requires the implementation of one method:

C#
public void PreFilterMessage(Message m) {} 

The implementation in this control acts on all WM_MOUSEMOVE message to determine whether the mouse is within the control x-axis (horizontal ruler) or y-axis bounds (vertical ruler) but not both. If the mouse is with bounds, then:

  • the position of the mouse is retrieved in screen coordinates;
  • it is saved in client coordinates;
  • the control is repainted so that the mouse tracking line can be updated and
  • an event is raised

There are some rulers written in VB that purport to track the mouse. Conveniently VB fires a controls MouseMove() event whereever a mouse moves. However the coordinates are relative to any control the mouse is over so it works fine while over another control that has an origin at the let or top egde of a ruler. But if it is not (like the ruler in Word) then such a control is much less useful. Morever, if the mouse passes over another, contained control, the problem get worse.

These issues do not affect this implementation because the control is looking at all mouse messages and retrieving the mouse position in screen corrdinate so is unaffected by the presence (or not) or ther controls.

Spreading

The main function in the control is the private function DrawControl(). This function draws the control using double-buffering and does so respecting the various property settings. As a result of the settings is it highly likely that the marks to be displayed do not fit conveniently into the number of pixels available. For example, if it is assumed that 53 pixels equals 1 centimeter and the user asks for 8 divisions, there are going to be 5 "spare" pixels. Allocating these pixels to, say, the last division guarantees that this division is going to look odd. Instead, this implementation uses a spreading algorithm such that spare pixels are allocated to divisions more evenly. The basic algorithm is:

C#
int iRemaining = iWholeAmount;
for(i=0; i<iDivisionCount; i++)
{
 int iDivisionsToAllocate = (iDivisionCount-i);
 iAllocation = Convert.ToInt32(Math.Round((double)iRemaining/
   (double)iDivisionsToAllocate, 0));
 iRemaining -= iAllocation;
} 
Applying the algoritm to the example above and you get:
C#
iDivisionCount=8; iWholeAmount=50;

i=0; iDivisionsToAllocate=8; iAllocation=7; iRemaining=46;
i=1; iDivisionsToAllocate=7; iAllocation=7; iRemaining=39;
i=2; iDivisionsToAllocate=6; iAllocation=7; iRemaining=32;
i=3; iDivisionsToAllocate=5; iAllocation=6; iRemaining=26;
i=4; iDivisionsToAllocate=4; iAllocation=7; iRemaining=19;
i=5; iDivisionsToAllocate=3; iAllocation=6; iRemaining=13;
i=6; iDivisionsToAllocate=2; iAllocation=7; iRemaining= 6;
i=7; iDivisionsToAllocate=1; iAllocation=6; iRemaining= 0; 
The actual implementation looks a little more complicated because the user might select a starting value of 1.4. In such a case the spreading must take place from division one but the effect cannot be taken into account until the amount spread is greater than the starting value.

Compiler constants

This code is usually compiled into a project and takes advantage of controls in related libraries. However these cannot be distributed. So the code uses a conditional compilation constant to exclude or replace code as necessary. This constant is FRAMEWORKMENUS and the code will not compile without it being defined.

Updates

  • 2003-05-20 - Thanks to Max Haenel for spotting a potential problem that could arise because the RemoveMessageFilter call will not be made until until the application terminates thereby consume more memory than necessary.
  • 2006-08-07 - Updated source code download

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


Written By
Web Developer
United Kingdom United Kingdom
Independent software development for clients including Microsoft. Design and development of modules for financial reporting and business intelligence.

Comments and Discussions

 
GeneralRe: The control request too much OnPaint during mouse moves and freezes other controls Pin
Bill Seddon19-Dec-13 1:00
Bill Seddon19-Dec-13 1:00 
BugThe ruler rounds the scale, so it is wrong for a range of values Pin
Pedro77k7-Dec-13 10:10
Pedro77k7-Dec-13 10:10 
GeneralRe: The ruler rounds the scale, so it is wrong for a range of values Pin
Bill Seddon7-Dec-13 10:51
Bill Seddon7-Dec-13 10:51 
GeneralRe: The ruler rounds the scale, so it is wrong for a range of values Pin
Pedro77k7-Dec-13 12:14
Pedro77k7-Dec-13 12:14 
GeneralRe: The ruler rounds the scale, so it is wrong for a range of values Pin
Bill Seddon7-Dec-13 12:41
Bill Seddon7-Dec-13 12:41 
GeneralRe: The ruler rounds the scale, so it is wrong for a range of values Pin
Pedro77k7-Dec-13 16:18
Pedro77k7-Dec-13 16:18 
GeneralRe: The ruler rounds the scale, so it is wrong for a range of values Pin
Bill Seddon7-Dec-13 23:36
Bill Seddon7-Dec-13 23:36 
GeneralRe: The ruler rounds the scale, so it is wrong for a range of values Pin
Pedro77k8-Dec-13 2:35
Pedro77k8-Dec-13 2:35 
Bill, I think you didn't understand the problem. The image (or text) and the ruler must be on the same scale, synchronized, as you said. At this range: 1.04 <= scale < 1.05 the ruler size will be the same, but the image/text and window don't.

Image sample: OMG | :OMG:
http://i.imgur.com/m2cVJ7F.png[^]

I know the control have been used for a long time, maybe nobody have noticed that. It is a small detail, but for me it is important. I just want to improve it.

Anyway, the first test version of the fix is here for anyone is interest on it. It is not 100% tested yet.

C#
private void DrawControl2(Graphics graphics)
{
    Graphics g = null;

    if (!this.Visible) return;

    // Bug reported by Kristoffer F
    if (this.Width < 1 || this.Height < 1)
    {
        return;
    }

    if (_bitmap == null)
    {
        int valueOffset = 0;
        int scaleStartValue;

        // Create a bitmap
        _bitmap = new Bitmap(this.Width, this.Height);

        g = Graphics.FromImage(_bitmap);

        try
        {
            // Wash the background with BackColor
            g.FillRectangle(new SolidBrush(this.BackColor), 0, 0, _bitmap.Width, _bitmap.Height);

            if (StartValue >= 0)
            {
                scaleStartValue = Convert.ToInt32(StartValue * _scale / _majorInterval);  // Convert value to pixels
            }
            else
            {
                // If the start value is -ve then assume that we are starting just above zero
                // For example if the requested value -1.1 then make believe that the start is
                // +0.9.  We can fix up the printing of numbers later.
                double startValue = Math.Ceiling(Math.Abs(_startValue)) - Math.Abs(_startValue);

                // Compute the offset that is to be used with the start point is -ve
                // This will be subtracted from the number calculated for the display numeral
                scaleStartValue = Convert.ToInt32(startValue * _scale / _majorInterval);  // Convert value to pixels
                valueOffset = Convert.ToInt32(Math.Ceiling(Math.Abs(_startValue)));
            };

            // Paint the lines on the image
            //int scale;// = _scale;

            double start = Start();  // iStart is the pixel number on which to start.
            int end = (this.Orientation == Orientation.Horizontal) ? Width : Height;

            //#if DEBUG
            //                    if (this.Orientation == Orientation.Vertical)
            //                    {
            //                        System.Diagnostics.Debug.WriteLine("Vert");
            //                    }
            //#endif

            for (int j = 0; j * _scale + start <= end; j++)
            {
                double left = _scale;  // Make an assumption that we're starting at zero or on a major increment
                double jOffset = start + scaleStartValue + j * _scale;
                int point = Convert.ToInt32(start + j * _scale);

                //scale = Convert.ToInt32(jOffset - start) % Convert.ToInt32(_scale);  // Get the mod value to see if this is "big line" opportunity

                // If it is, draw big line
                if (true)//scale == 0)
                {
                    if (_RulerAlignment != RulerAlignment.Middle)
                    {
                        if (this.Orientation == Orientation.Horizontal)
                            Line(g, point, 0, point, Height);
                        else
                            Line(g, 0, point, Width, point);
                    }

                    left = _scale;     // Set the for loop increment
                }
                else
                {
                    //left = _scale - Math.Abs(scale);     // Set the for loop increment
                }

                int space = Convert.ToInt32(_scale);// Convert.ToInt32(left);

                int iValue = Convert.ToInt32((((jOffset - start) / _scale) + 1) * _majorInterval);

                // Accommodate the offset if the starting point is -ve
                iValue -= valueOffset;
                DrawValue(g, iValue, Convert.ToInt32(j * _scale - start), space);

                double used = start;
                double smallMarkPoint;
                // TO DO: This must be wrong when the start is negative and not a whole number
                //Draw small lines
                for (int i = 0; i < _numberOfDivisions; i++)
                {
                    // Get the increment for the next mark
                    smallMarkPoint = (_scale - (double)(used-start)) / (_numberOfDivisions - i); // Use a spreading algorithm rather that using expensive floating point numbers

                    // So the next mark will have used up
                    used += smallMarkPoint;

                    if (used >= (_scale - left))
                    {
                        int iX = Convert.ToInt32(used + j * _scale - (_scale - left));

                        // Is it an even number and, if so, is it the middle value?
                        bool bMiddleMark = ((_numberOfDivisions & 0x1) == 0) & (i + 1 == _numberOfDivisions / 2);
                        bool bShowMiddleMark = bMiddleMark;
                        bool bLastDivisionMark = (i + 1 == _numberOfDivisions);
                        bool bLastAlignMiddleDivisionMark = bLastDivisionMark & (_RulerAlignment == RulerAlignment.Middle);
                        bool bShowDivisionMark = !bMiddleMark & !bLastAlignMiddleDivisionMark;

                        if (bShowMiddleMark)
                        {
                            DivisionMark(g, iX, _middleMarkFactor);  // Height or Width will be 1/3
                        }
                        else if (bShowDivisionMark)
                        {
                            DivisionMark(g, iX, _divisionMarkFactor);  // Height or Width will be 1/5
                        }
                    }
                }
            }

            if (_i3DBorderStyle != Border3DStyle.Flat)
                ControlPaint.DrawBorder3D(g, this.ClientRectangle, this._i3DBorderStyle);

        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.Message);
        }
        finally
        {
            g.Dispose();
        }
    }

    g = graphics;

    try
    {

        // Always draw the bitmap
        g.DrawImage(_bitmap, this.ClientRectangle);

        RenderTrackLine(g);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
    //finally
    //{
    //    GC.Collect();
    //}


}

Pedro

QuestionThe scale value is not correct ? Pin
leanhtan8623-Oct-13 18:08
leanhtan8623-Oct-13 18:08 
AnswerRe: The scale value is not correct ? Pin
Pedro77k7-Dec-13 10:02
Pedro77k7-Dec-13 10:02 
AnswerRe: The scale value is not correct ? Pin
Bill Seddon7-Dec-13 10:39
Bill Seddon7-Dec-13 10:39 
QuestionWidescreen Resolution causes scale to be off Pin
Galstaph25-Jul-12 5:35
Galstaph25-Jul-12 5:35 
Question“Error: The type or namespace name 'RulerControl' could not be found.” Pin
jiming liu10-Apr-12 12:57
jiming liu10-Apr-12 12:57 
NewsModified it to become a Timeline control Pin
Rob_Jurado3-Mar-12 19:04
Rob_Jurado3-Mar-12 19:04 
GeneralMy vote of 5 Pin
Manoj Kumar Choubey20-Feb-12 19:33
professionalManoj Kumar Choubey20-Feb-12 19:33 
QuestionIs smMillimetre a valid scale ? Pin
ravi.g12-Sep-11 3:54
ravi.g12-Sep-11 3:54 
AnswerRe: Is smMillimetre a valid scale ? Pin
Bill Seddon12-Sep-11 4:52
Bill Seddon12-Sep-11 4:52 
GeneralRe: Is smMillimetre a valid scale ? Pin
ravi.g12-Sep-11 10:16
ravi.g12-Sep-11 10:16 
GeneralRe: Is smMillimetre a valid scale ? Pin
Mohd Azzam25-Jan-12 20:35
Mohd Azzam25-Jan-12 20:35 
GeneralRe: Is smMillimetre a valid scale ? Pin
Bill Seddon25-Jan-12 23:25
Bill Seddon25-Jan-12 23:25 
QuestionI am trying to use rulercontrol.dll in a vb net 2008 3.5framework x86 app Pin
Member 767985415-Aug-11 9:01
Member 767985415-Aug-11 9:01 
AnswerRe: I am trying to use rulercontrol.dll in a vb net 2008 3.5framework x86 app Pin
Bill Seddon15-Aug-11 9:34
Bill Seddon15-Aug-11 9:34 
GeneralRe: I am trying to use rulercontrol.dll in a vb net 2008 3.5framework x86 app Pin
Member 767985416-Aug-11 5:07
Member 767985416-Aug-11 5:07 
GeneralRe: I am trying to use rulercontrol.dll in a vb net 2008 3.5framework x86 app Pin
Bill Seddon16-Aug-11 5:09
Bill Seddon16-Aug-11 5:09 
GeneralRe: I am trying to use rulercontrol.dll in a vb net 2008 3.5framework x86 app Pin
Member 767985422-Aug-11 19:34
Member 767985422-Aug-11 19:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.