Immer wieder fällt mir auf, dass zwar sehr viel über Usability gesprochen wird, aber ganz einfache Sachen, die bei Programmierung mit Win32-Sprachen üblich waren mittlerweile in der .NET-Welt kaum noch beachtet werden. Ein Punkt davon ist das Handling von scrollbaren Elementen. Genauer gesagt geht es mir um die Unterstützung des Mausrads, das eben gerne zum Scrollen in Listen verwendet wird.

Ich will mal an Hand eines kleines Beispiel-Programmes zeigen, um was es mir geht:

Wir haben also auf der linken Seite eine Liste (DataGridView) und auf der rechten Seite eine TextBox mit mehreren Zeilen Inhalt.
Wenn man jetzt in der Liste scrollen will, muss man erst in die Liste klicken, bevor man das Scrollrad verwenden kann. Ebenso verhält es sich mit der TextBox. Erst muss man der TextBox den Focus geben indem man reinklickt und dann kann man das Mausrad zum Scrollen verwenden.
Problematisch wird dieses Verhalten, wenn sich der Inhalt der rechten Seite durch die Auswahl auf der linken Seite verändert, was in der Regel der Fall sein wird.

Besser ist es doch, wenn an Hand der Position des Maus-Zeigers erkannt wird, was gescrollt werden soll. Also wenn die Maus sich über der Liste befindet, wird in der Liste gescrollt und wenn sie über der TextBox ist wird eben da drin gescrollt. Und damit auch noch das Standard-Verhalten erhalten bleibt, machen wir es auch noch so, dass das aktive Element, das den Fokus gerade hat gescrollt wird, wenn sich die Maus außerhalb eines der beiden Elemente befindet.

Prinzipiell ist das mit .NET sogar sehr einfach zu lösen. Man muss sich allerdings auch wieder ein bisschen mit dem Message-Handling von Windows auseinandersetzen, um auf die entsprechende Lösung zu kommen. Die Lösung wäre jetzt eigentlich ein globaler MessageHook, der die Message WM_MOUSEWHEEL abfängt und abhängig von der Mausposition an das entsprechende Control weiter reicht. Unter .NET gibt es noch eine schönere Möglichkeit. Es handelt sich sozusagen um einen Anwendungsweiten MessageHook, der eben nur innerhalb einer Anwendung anspringt.
Das Schlüsselwort ist das Interface IMessageFilter und die dadurch zu implementierende Methode PreFilterMessage. Mit dieser Methode werden alle Messages überprüft und können gegebenenfalls verändert oder umgeleitet werden. Also genau das Verfahren, das wir darstellen wollen. Wir müssen also lediglich in die Form das Interface implementieren.

Da .NET erst einmal recht wenig auf Messages eingeht, müssen wir auf jeden Fall erst einmal festlegen, welche Message wir verwenden wollen. Man muss das zwar nicht machen, aber rein von der Lesbarkeit des Codes ist es besser, wenn man mit sprechenden Namen arbeitet, als mit irgendwelchen Zahlenwerten.

Wir wollen mit WM_MOUSEWHEEL arbeiten, also legen wir uns eine entsprechende Konstante mit diesem Namen an. Den Wert dieser Message findet man in der MSDN (http://msdn.microsoft.com/en-us/library/ms645617(v=vs.85).aspx) Zusätzlich benötigen wir noch die Message WM_VSCROLL zum verticalen Scrollen.


public const int WM_MOUSEWHEEL = 0x020A;
public const int WM_VSCROLL = 0x115;

Bei der Message WM_VSCROLL ist es wichtig zu wissen, dass sie beim Parameter WParam einen Wert erwartet, der aussagt, wohin gescrollt werden soll. In unserem Fall ist das entweder eine Zeile nach oben oder eine Zeile nach unten, was bedeutet, dass entweder 0 oder 1 übergeben wird. Was man sonst noch alles übergeben kann, entnimmt man bitte der MSDN.

Dann widmen wir uns mal der genannten Methode, um die Messages abzufangen.

public bool PreFilterMessage(ref Message m)
{
    if (m.Msg == WM_MOUSEWHEEL)
    {
        int zDelta = HiWord((int)m.WParam);
        int xPos = LoWord((int)m.LParam);
        int yPos = HiWord((int)m.LParam);

        Point mousePosition = PointToClient(new Point(xPos, yPos));
    }
    return false;
}

Der Aufbau von PreFilterMessage ist ganz einfach. Man bekommt als Parameter die Message, die zu überprüfen ist und gibt dann entweder true oder fals zurück. True bedeutet, dass die Message abgearbeitet ist und keine weiteren Methoden durch die Message mehr aufgerufen werden sollen. False dagegen gibt die Message an die nächste Methode weiter, die für das Message-Handling zuständig ist oder sich dafür zuständig fühlt.

Wir überprüfen also, ob es sich um eine Message vom Typ WM_MOUSEWHEEL handelt und holen dann gleich mal alle Werte aus der Message, die man evtl. brauchen kann. Bei WM_MOUSEWHEEL wird die Richtung in der das Mausrad bewegt wird in WParam gespeichert. Die Mausposition dagegen findet man in LParam. Die beiden Attribute muss man mit noch mit HighOrder bzw. LowOrder entsprechend auslesen. Dafür habe ich zwei kleine Hilfsmethoden gefunden, auf die ich jetzt aber nicht näher eingehen will

public static int HiWord(int number)
{
    if ((number & 0x80000000) == 0x80000000)
        return (number >> 16);
    else
        return (number >> 16) & 0xffff;
}
public static int LoWord(int number)
{
    return number & 0xffff;
}

Zusätzlich hab ich mir auch noch eine kleine Hilfsmethode geschrieben, mit der ich überprüfen kann, ob eine Koordinate innerhalb eines Controls ist.

public static bool PointInControl(Control ctrl, Point p)
{
    return p.X >= ctrl.Location.X && p.X <= (ctrl.Location.X + ctrl.Width) &&
        p.Y >= ctrl.Location.Y && p.Y <= (ctrl.Location.Y + ctrl.Height);
}

Desweiteren sollte man noch auf eine weitere Sache eingehen, die wir benötigen werden. Damit wir den jeweiligen Controls mitteilen können, dass sie eine Zeile weiter- oder zurückscrollen sollen, müssen wir eine entsprechende Message (WM_VSCROLL) an die jeweiligen Controls schicken. Eine Möglichkeit zum versenden von Messages gibt es allerdings nicht direkt in .NET. Man muss das über die herkömmliche WinAPI-Funktion lösen. Daher müssen wir uns noch diese Methode deklarieren.

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);

Damit haben wir alle Werkzeuge, die wir für die gewünschte Funktionalität benötigen.

Fangen wir also an, die Methode PreMessageFilter mit Logik zu füllen. Zu erst einmal wenden wir uns der TextBox zu, da diese einfacher zu handlen ist. DataGridView stellt einen Spezialfall dar, wenn es um das Scrollen geht, aber das sehen wir gleich.

Wir überprüfen als, ob sich die Maus über der TextBox befindet und senden dann eine Message an die TextBox, die das Scrollen einleitet.

if (PointInControl(textBox1, mousePosition)
{
    SendMessage(textBox1.Handle, WM_VSCROLL, (IntPtr)(zDelta > 0 ? 0 : 1), (IntPtr)null);
    return true;
}

Nachdem die Message gesendet wurde, springen wir mit dem Rückgabewert true aus der Methode, da keine Abhandlungen für diese Message mehr notwendig sind.

Dann zum DataGridView. Auch hier überprüfen wir erst wieder, ob die Maus an der richtigen Stelle steht und führen nur dann die entsprechende Logik aus. Das besondere am DataGridView ist jetzt, dass es sich bei diesem Control nicht um ein alleinstehendes Control handelt, sondern um einen Container, der mehrere Controls beinhaltet. Der Container selbst, den wir direkt ansprechen können ist in diesem Fall nicht für das Scrollen zuständig. Stattdessen beinhaltet der Container noch ein Control vom Typ VScrollBar, auf das wir letztendlich losgehen müssen. Wir müssen uns also erst die Scrollbar suchen und dann die Message an diese Scrollbar schicken. Hier tritt eine weitere Besonderheit in Kraft, allerdings in Bezug auf die Message WM_VSCROLL. Da es sich bei dem zu scrollenden Element um eine Scrollbar handelt, muss man für lParam das Handle der Scrollbar übergeben.

if (PointInControl(dataGridView1, mousePosition))
{
    VScrollBar scrollbar = null;
    foreach (Control ctrl in dataGridView1.Controls)
    {
        if (ctrl is VScrollBar)
        {
            scrollbar = (VScrollBar)ctrl;
            break;
        }
        if (scrollbar != null)
            SendMessage(dataGridView1.Handle, WM_VSCROLL, (IntPtr)(zDelta > 0 ? 0 : 1), scrollbar.Handle);

        return true;
    }
}

Ansonsten läuft dann wieder alles genau wir bei der TextBox ab.

Insgesamt sieht die Methode PreFilterMessage also folgendermaßen aus:

public bool PreFilterMessage(ref Message m)
{
    if (m.Msg == WM_MOUSEWHEEL)
    {
        int zDelta = HiWord((int)m.WParam);
        int xPos = LoWord((int)m.LParam);
        int yPos = HiWord((int)m.LParam);

        Point mousePosition = PointToClient(new Point(xPos, yPos));

        if (PointInControl(dataGridView1, mousePosition))
        {
            VScrollBar scrollbar = null;
            foreach (Control ctrl in dataGridView1.Controls)
            {
                if (ctrl is VScrollBar)
                {
                    scrollbar = (VScrollBar)ctrl;
                    break;
                }
                if (scrollbar != null)
                    SendMessage(dataGridView1.Handle, WM_VSCROLL, (IntPtr)(zDelta > 0 ? 0 : 1), scrollbar.Handle);

                return true;
            }
        }
        else if (PointInControl(textBox1, mousePosition)
        {
            SendMessage(textBox1.Handle, WM_VSCROLL, (IntPtr)(zDelta > 0 ? 0 : 1), (IntPtr)null);
            return true;
        }
    }
    return false;
}

Damit jetzt auch noch dieser MessageFilter verwendet wird, müssen wir ihn noch registrieren. Idealerweise macht man das im Konstruktor der Form.

public Form1()
{
    InitializeComponent();
    Application.AddMessageFilter(this);
    ...
}

Mit dieser Methode kann man noch viel mehr Anwendungsfälle abfangen. Denkbar ist zum Beispiel auch, dass immer die Liste gescrollt werden soll, egal, welches Element gerade den Fokus hat oder wo sich die Maus befindet. Man kann sich hier alles mögliche einfallen lassen, so lange es für den End-Benutzer verständlich ist. Man kann mit einem solchen Handling auch sehr viel kaputt machen, daher gilt, dass man sich auf jeden Fall immer Gedanken darüber machen soll, was für den Benutzer sinnvoll ist und was nicht.