Click here to Skip to main content
Click here to Skip to main content

Listview with custom scrollbars

, 4 Aug 2011
Rate this:
Please Sign up or sign in to vote.
This article shows how to replace the default listview scrollbar with a custom scrollbar and shows an custom scrollbar implementation
Listview in action

Introduction

From time to time, I come across postings of guys asking if it's possible to replace the Listview's builtin scroll bars. The answers vary in a wide range. Everything is included, from "not possible" to nice hacks. Maybe I missed something, but I have not found any nice 'download-extract-run' solution. Smile | :) Another thing that was not included (I think) in a single post was how to attach a custom scroll bar, and with custom I don't mean a user-drawn Windows scroll bar.

For the Impatient

I created a custom and styleable scroll-bar control and a custom ListView control which inherits from the original one, but has the ability to attach one or more custom scrollbars.

The style code is encapsulated in separate painters and is/can be reused in other projects or other user controls. But if you are interested in the painters, read on.

For the impatient, check out the code at git clone git://github.com/deveck/Deveck.Utils.git.

Uncomment

Example_ScrollbarTest(); 

in the Program.cs file to enable the scroll bar sample and run it.

The painter, scroll-bar and listview code is located at:

Deveck.Utils/Ui/Painters
Deveck.Utils/Ui/Scrollbar
Deveck.Utils/Ui

The sample attaches three scroll-bars with slightly different styles to a single ListView.

Details

The following sections describe the components and the implementation in detail.

The Painters

The painters are not directly associated with the scroll-bars, the ListView or anything else. In general they just paint something, or in special cases they forward filtered input data to another painter.

I encapsulated the drawing in painters because I want it to be reusable, e.g. the currently included painters are not only used to draw the scroll-bars they are also used to draw all kind of buttons, but they are currently not part of Deveck.Utils.

The painter interface looks really simple.

public abstract class Painter
 {
   public enum State
   {
    Normal,
    Pressed,
    Hover
   }

   public abstract void Paint(Graphics g, Rectangle position, 
     State state, string text, Image img, Font textFont, Rectangle? referencePosition);
    }
  • Rectangle position: The rectangle which can be used by this painter.
  • State state: One of several states (normal, hover, pressed) which may affect the colors used.
  • string text: Text to be printed. A painter is not required to print the text, because there may be another chained painter.
  • Image img: Image associated with the target. A painter is not required to paint the image.
  • Rectangle? referencePosition: This rectangle is used for font scaling. If supplied, the supplied textFont is the font for the referencePosition size and needs to be scaled to position.

The currently available painters are:

  • Office2007BlackButtonPainter
  • Office2007BlueButtonPainter
  • WindowsStyledButtonPainter
  • SymbolPainter

There are also some special painters which filter the input data, therefore called filter painters.

  • PainterFilterNoText: Removes the text and calls the subsequent painter
  • PainterFilterSize: Adjusts the passed bounds, e.g. add padding and center, max size or stretch by specified ratio and call the subsequent painter

Another special painter is the:

  • StackedPainter

It accepts multiple painters or filters and calls them in the order they were specified.

The Scroll-bar

The custom scroll-bars need to implement a simple interface ICustomScrollbar as shown below. They do not need to be derived from Control but may be required Wink | ;) . The interface should be self-explanatory. Take a look at the CustomScrollbar, but it's quite straight forward with up down buttons for small scroll changes, a slide-able thumb for easy and fast mouse scrolling and large changes by pressing the area between up-down button and thumb, just like an ordinary scroll-bar.

public delegate void ScrollValueChangedDelegate(ICustomScrollbar sender, int newValue);

public interface ICustomScrollbar
{
  event ScrollValueChangedDelegate ValueChanged;

  int LargeChange { get; set; }
  int SmallChange { get; set; }

  int Maximum { get; set; }
  int Minimum { get; set; }
  int Value { get; set; }
}

The Custom ListView

The custom list view is a bit more tricky, and requires knowledge of how Windows GUIs work, behind nice designers. Fortunately .NET provides us with the WndProc method where we can capture all events of interest, but more on that in a second.

First, we need to define the changes we need to make to the default list view control.

  • We need a reliable way to scroll the list by code
  • We need a way to get the current scroll value and number of visible items
  • We need to get rid of the builtin scroll-bar
  • We need to be informed on all actions that should change the scroll value to update the scroll-bar
  • We need to put it all together Smile | :)

How to scroll the list by code

The first approach to this problem is to simply set the TopItem of the list, and it works... sometimes but not always. Going deeper into it reveals that the .NET listview wrapper just guesses the scroll value, because the native listview does not support an operation like this. So what is the solution? Set the TopItem multiple times, till the correct item is on top, yes it is an ugly hack Wink | ;)

public void SetScrollPosition(int pos)
{
  pos = Math.Min(Items.Count - 1, pos);

  if (pos < 0 || pos >= Items.Count)
    return;
  EnsureVisible(pos);

  for (int i = 0; i < 10; i++)
  {
    if(TopItem != null && TopItem.Index != pos)
      TopItem = Items[pos];
  }
}

How to get the current scroll value and number of visible items

At first sight, this may look exactly like the first problem. We may get the current scroll value by querying the TopItem but we also need the max, min and change values. So we need to pinvoke the getScrollInfo method which returns us a SCROLLINFO structure. This structure also contains the change value, and fortunately the large change value is equal to the visible item count, because the list always scrolls a complete page if a large change is processed.

public void GetScrollPosition
(out int min, out int max, out int pos, out int smallchange, out int largechange)
{
  SCROLLINFO scrollinfo = new SCROLLINFO();
  scrollinfo.cbSize = (uint)Marshal.SizeOf(typeof(SCROLLINFO));
  scrollinfo.fMask = (int)ScrollInfoMask.SIF_ALL;
  if (GetScrollInfo(this.Handle, (int)SBTYPES.SB_VERT, ref scrollinfo))
  {
    min = scrollinfo.nMin;
    max = scrollinfo.nMax;
    pos = scrollinfo.nPos;
    smallchange = 1;
    largechange = (int)scrollinfo.nPage;
  }
  else
  {
    min = 0;
    max = 0;
    pos = 0;
    smallchange = 0;
    largechange = 0;
  }
}

How to remove the built-in scroll-bar

You may think, as I did, that this is definitely the simplest task. Just set:

list.Scrollable = false;
and you are ready. This really hides the scroll-bars, but gives you no chance to scroll the list, even by code.

So this brings the mentioned WndProc into the game. The code block below only shows a snipped of WndProc look at the source for the complete method.

We need to capture the 'calcsize' message, retrieve the window-style and disable the scrollbar if it was activated.

protected override void WndProc(ref Message m)
{
 ...
  if (m.Msg == WM_NCCALCSIZE) // WM_NCCALCSIZE
  {
    int style = (int)GetWindowLong(this.Handle, GWL_STYLE);
    if ((style & WS_VSCROLL) == WS_VSCROLL)
      SetWindowLong(this.Handle, GWL_STYLE, style & ~WS_VSCROLL);
  }
 ...
}

How to get informed on all actions of interest

We are nearly done now, we just need to update the scroll-bars if items are added or removed, the list gets cleared or the list gets scrolled by some other mechanisms. So we again hook into the WndProc method and capture the corresponding messages.

protected override void WndProc(ref Message m)
{
 ...
   if (m.Msg == WM_VSCROLL)
   {
     int max, min, pos, smallchange, largechange;
     GetScrollPosition(out min, out max, out pos, out smallchange, out largechange);

     if (ScrollPositionChanged != null)
       ScrollPositionChanged(this, pos);

     if (_vScrollbar != null)
       _vScrollbar.Value = pos;
   }
   else if (m.Msg == LVM_INSERTITEMA || m.Msg == LVM_INSERTITEMW)
     OnItemAdded();
   else if (m.Msg == LVM_DELETEITEM || m.Msg == LVM_DELETEALLITEMS)
     OnItemsRemoved();
 ...
}

Result

It works really well and looks great. Check out the source and leave your comment.

Take a look at my blog, for more information.

History

  • 8/2/2011 - Initial version

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0

About the Author

deveck
Student
Austria Austria
No Biography provided

Comments and Discussions

 
GeneralI don't believe that no body has comment for this great article !! PinmemberMazen el Senih16-Mar-12 5:57 
GeneralRe: I don't believe that no body has comment for this great article !! PinmemberPDTPGY31-Mar-12 4:59 
GeneralRe: I don't believe that no body has comment for this great article !! PinprofessionalEligio Morgado H.25-Jun-13 0:22 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 4 Aug 2011
Article Copyright 2011 by deveck
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid