“Reader Mode” – we’ve all used it, and whether we recognise it by name (or one of its many others, such as autoscroll, middle-click scroll, etc), we’ve come to expect it from the software we use the most – web browsers, text editors, e-mail clients and more. However, if you’ve been living under a rock since the mainstream adoption of the wheel mouse, let me explain how it works:

You activate Reader Mode by middle-clicking on a scrollable container/control. A glyph appears on the window in the location where you pressed the mouse button and the cursor changes to a scroll symbol. As you move the pointer away from the glyph, the display begins to automatically scroll in that direction. The further you move the pointer from the original location, the faster the display scrolls. (You can slow down the scroll rate by moving the pointer closer to the glyph.) This offers an alternative to continuously rotating the mouse wheel, allowing you to scroll through content while keeping your hand still.

Reader Mode behaves slightly differently depending on whether you release the middle button straight away, or keep it held down while scrolling. If the former is true, it goes into a sort-of “hands-free” mode where you must click again to exit. In the latter situation, it terminates when you release the middle mouse button.

DataGridViewRM example

Reader Mode in Windows Forms

Very few Windows Forms controls support Reader Mode as-is; the only ones i’m aware that do are RichTextBox and WebBrowser. Many more controls could benefit from Reader Mode support, such as:

  • Panel
  • PictureBox
  • ListView
  • TreeView
  • DataGridView

It’s the latter of those that I will focus on in this article.

Adding Reader Mode support

So, how do we add support for Reader Mode to a control that doesn’t already have it? You might be tempted to just implement it from scratch (handling mouse events, calculating offsets, etc), but there are many reasons why this is inadvisable:

  • Re-inventing the wheel
  • Subtle differences in behaviour easily introduced
  • Will not adapt to changes in the standard implementation
  • More complex using pure Windows Forms

Thankfully, there is an API for Reader Mode which can be imported from the Windows Common Controls library (comctl32.dll). It consists of:

  • READERMODEINFO structure – For configuring Reader Mode behaviour.
  • DoReaderMode() function – Puts the control into Reader Mode.
  • TranslateDispatch callback – Redirects window messages while in Reader Mode, allowing you to handle them independently of normal control behaviour.
  • ReaderScroll callback – Does the grunt work, notifying the control to start scrolling or update its scroll rate.
[DllImport("comctl32.dll", SetLastError = true, EntryPoint = "#383")]
public static extern void DoReaderMode(ref InteropTypes.READERMODEINFO prmi);

public delegate bool TranslateDispatchCallbackDelegate(ref Message lpmsg);

public delegate bool ReaderScrollCallbackDelegate(ref READERMODEINFO prmi, int dx, int dy);

[Flags]
public enum ReaderModeFlags {
    None = 0x00,
    ZeroCursor = 0x01,
    VerticalOnly = 0x02,
    HorizontalOnly = 0x04
}

[StructLayout(LayoutKind.Sequential)]
public struct READERMODEINFO {
    public int cbSize;
    public IntPtr hwnd;
    public ReaderModeFlags fFlags;
    public IntPtr prc;
    public ReaderScrollCallbackDelegate pfnScroll;
    public TranslateDispatchCallbackDelegate fFlags2;
    public IntPtr lParam;
}

The basic life cycle for Reader Mode, therefore, is:

  1. In the handler for the control’s MouseDown event, check for the middle mouse button and:
    1. Display a glyph at the location the mouse button was pressed (i’m using a PictureBox for this).
    2. Activate the mechanism used to auto-scroll the control (i’m using a Timer for this).
    3. Initialise and enter Reader Mode by calling DoReaderMode().
  2. The user will move the mouse pointer. In the ReaderScroll callback:
    1. Store the delta values (dx, dy) so that they are visible to the auto-scroll mechanism (Timer).
    2. If either of the values changes from zero, we will need to exit Reader Mode when the mouse button is released.
  3. At regular intervals (~25ms), the auto-scroll mechanism must:
    1. Increment (or decrement) the horizontal and/or vertical scroll bar offsets in proportion to the delta values from the previous step.
  4. The user will either release the mouse button or, in “hands-free” mode, click again. In the TranslateDispatch callback:
    1. If the window message corresponds to an event that will exit Reader Mode, stop the auto-scroll mechanism and hide the glyph.
    2. Depending on the type of event, either allow the default Reader Mode implementation to handle the message or handle it ourselves.
  5. Reader Mode will exit based on its own internal logic.

Default Reader Mode implementation

The default Reader Mode implementation is very simplistic. It will not draw the glyph, scroll the control (although it does generate the delta values) and it behaves slightly differently from the “standard” behaviour we see in most applications:

  • No “hands-free” mode; Reader Mode always exits when the middle mouse button is released
    • We can overcome this by preventing the default implementation from handling the MouseUp event until the delta values change
  • Stops responding to user input if the mouse pointer leaves the bounds of the control’s parent window
    • This is avoided by preventing the default implementation from handling the MouseLeave event
  • Only sends non-zero delta values when the pointer goes outside the ‘scrolling area’
    • Conventional behaviour is restored by setting the ‘scrolling area’ to the bounds* of the glyph (instead of the bounds of the control)

* – Note that the rectangle structure expected by Reader Mode is expressed in terms of left, top, right and bottom (rather than the conventional left, top, width, height representation). This gave me a nasty surprise the first time around.

Applying Reader Mode to the DataGridView

The DataGridView control is an excellent candidate for Reader Mode; scrolling through pages of tabular data can be a real pain, and the additional of Reader Mode functionality would help to make the control more user-friendly.

The easiest method is to subclass the control. I’ve created a child class called DataGridViewRM which adds the Reader Mode functionality and allows it to be toggled on/off via a design-time property. This effectively makes it a drop-in replacement for the original control.

As I eluded to above, we need to add some child controls to facilitate the behaviour; a PictureBox and a Timer. These must be initialised in the constructor and released in the control’s Dispose() method.

In terms of actually scrolling the control, the DataGridView uses its own logic (although its scroll bar controls are publicly-accessible, manipulating them does not scroll the control properly). It exposes two properties to allow the scroll offsets to be changed; HorizontalScrollingOffset and VerticalScrollingOffset. Unfortunately, through a quirk in Windows Forms, the latter property does not have a set accessor. Thankfully, it can be accessed using reflection:

// for better performance, retrieve this only once (in the constructor)
PropertyInfo setVerticalScrollingOffset 
    = GetType().GetProperty("VerticalOffset", BindingFlags.NonPublic | BindingFlags.Instance);

[Browsable(false)]
public new int VerticalScrollingOffset {
    get {
        return base.VerticalScrollingOffset;
    }
    set {
        setVerticalScrollingOffset.SetValue(this, value, null);
    }
}

All that remains is to handle the MouseDown event:

protected override void OnMouseDown(MouseEventArgs e) {
    if (ReaderModeEnabled) {
        if (e.Button == MouseButtons.Middle) {
            DataGridView.HitTestInfo hit = HitTest(e.X, e.Y);

            // reader mode should only be activated when the button 
            // is clicked over the scrollable part of the control
            switch (hit.Type) {
                case DataGridViewHitTestType.Cell:
                case DataGridViewHitTestType.HorizontalScrollBar:
                case DataGridViewHitTestType.None:
                case DataGridViewHitTestType.VerticalScrollBar:
                    // position and show the PictureBox
                    // start the Timer
                    // call DoReaderMode()
                    return;
                }
            }
        }

        // otherwise, process the event normally
        base.OnMouseDown(e);
}

The rest of the Reader Mode implementation falls within the callback methods and the Timer‘s Tick handler, as outlined earlier. You can view the complete implementation by downloading the source code below:

Download

Source code (Visual Studio 2012 solution, .NET Framework 4.0 – includes example application)

Final words

I hope you find this enhanced DataGridView control useful; it demonstrates just one of many controls that can be modified to support Reader Mode. I hope my explanation makes it easy for you to implement Reader Mode in your own controls.

Leave a reply

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

required