After scouring the articles online I have come up with this design for a winforms based touchscreen app that needs smartphone like scrolling. The app itself will run on a ta
7 Years later but for anyone that is looking for a neat and tidy winforms solution:
using System;
using System.Drawing;
using System.Windows.Forms;
/// <summary>
/// Pass the panel into constructor and the control will be turned into a touch scrollable control.
/// </summary>
public class TouchScroll
{
private Point mouseDownPoint;
private Panel parentPanel;
/// <summary>
/// pass in the panel you would like to be touch scrollable and it will be handled here.
/// </summary>
/// <param name="panel">The root panel you need to scroll with</param>
public TouchScroll(Panel panel)
{
parentPanel = panel;
AssignEvents(panel);
}
private void AssignEvents(Control control)
{
control.MouseDown += MouseDown;
control.MouseMove += MouseMove;
foreach (Control child in control.Controls)
AssignEvents(child);
}
private void MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
this.mouseDownPoint = Cursor.Position;
}
private void MouseMove(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left)
return;
Point pointDifference = new Point(Cursor.Position.X - mouseDownPoint.X, Cursor.Position.Y - mouseDownPoint.Y);
if ((mouseDownPoint.X == Cursor.Position.X) && (mouseDownPoint.Y == Cursor.Position.Y))
return;
Point currAutoS = parentPanel.AutoScrollPosition;
parentPanel.AutoScrollPosition = new Point(Math.Abs(currAutoS.X) - pointDifference.X, Math.Abs(currAutoS.Y) - pointDifference.Y);
mouseDownPoint = Cursor.Position; //IMPORTANT
}
}
}
And as a Component:
public partial class TouchableFlowLayoutPanel : FlowLayoutPanel
{
private bool _doTouchScroll;
private Point _mouseStartPoint = Point.Empty;
private Point _panelStartPoint = Point.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="TouchableFlowLayoutPanel" /> class.
/// </summary>
public TouchableFlowLayoutPanel()
{
InitializeComponent();
Program.mouseFilter.MouseFilterDown += mouseFilter_MouseFilterDown;
Program.mouseFilter.MouseFilterMove += mouseFilter_MouseFilterMove;
Program.mouseFilter.MouseFilterUp += mouseFilter_MouseFilterUp;
}
/// <summary>
/// Initializes a new instance of the <see cref="TouchableFlowLayoutPanel" /> class.
/// </summary>
/// <param name="container">The container.</param>
public TouchableFlowLayoutPanel(IContainer container)
{
container.Add(this);
InitializeComponent();
Program.mouseFilter.MouseFilterDown += mouseFilter_MouseFilterDown;
Program.mouseFilter.MouseFilterMove += mouseFilter_MouseFilterMove;
Program.mouseFilter.MouseFilterUp += mouseFilter_MouseFilterUp;
}
/// <summary>
/// Handles the MouseFilterDown event of the mouseFilter control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="MouseFilterEventArgs" /> instance containing the event data.
/// </param>
private void mouseFilter_MouseFilterDown(object sender, MouseFilterEventArgs e)
{
if (!_doTouchScroll && e.Button == MouseButtons.Left)
{
_mouseStartPoint = new Point(e.X, e.Y);
_panelStartPoint = new Point(-AutoScrollPosition.X,
-AutoScrollPosition.Y);
}
}
/// <summary>
/// Handles the MouseFilterMove event of the mouseFilter control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="MouseFilterEventArgs" /> instance containing the event data.
/// </param>
private void mouseFilter_MouseFilterMove(object sender, MouseFilterEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (!_mouseStartPoint.Equals(Point.Empty))
{
int dx = (e.X - _mouseStartPoint.X);
int dy = (e.Y - _mouseStartPoint.Y);
if (_doTouchScroll)
{
AutoScrollPosition = new Point(_panelStartPoint.X - dx,
_panelStartPoint.Y - dy);
}
else if (Math.Abs(dx) > 10 || Math.Abs(dy) > 10)
{
_doTouchScroll = true;
}
}
}
}
/// <summary>
/// Handles the MouseFilterUp event of the mouseFilter control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="MouseFilterEventArgs" /> instance containing the event data.
/// </param>
private void mouseFilter_MouseFilterUp(object sender, MouseFilterEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (_doTouchScroll && !AutoScrollPosition.Equals(_panelStartPoint) &&
!_panelStartPoint.Equals(Point.Empty))
{
// don't fire Click-Event
e.Handled = true;
}
_doTouchScroll = false;
_mouseStartPoint = Point.Empty;
_panelStartPoint = Point.Empty;
}
}
}
internal class MouseFilter : IMessageFilter
{
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_LBUTTONUP = 0x0202;
private const int WM_MOUSEMOVE = 0x0200;
/// <summary>
/// Filters a message before sending it
/// </summary>
/// <param name="m">The message to be sent.This message can not be changed.</param>
/// <returns>
/// true to filter the message and prevent it from being sent. false to allow the message to be sent to the next filter or control.
/// </returns>
public bool PreFilterMessage(ref Message m)
{
Point mousePosition = Control.MousePosition;
var args = new MouseFilterEventArgs(MouseButtons.Left, 0, mousePosition.X, mousePosition.Y, 0);
switch (m.Msg)
{
case WM_MOUSEMOVE:
if (MouseFilterMove != null)
{
MouseFilterMove(Control.FromHandle(m.HWnd), args);
}
break;
case WM_LBUTTONDOWN:
if (MouseFilterDown != null)
{
MouseFilterDown(Control.FromHandle(m.HWnd), args);
}
break;
case WM_LBUTTONUP:
if (MouseFilterUp != null)
{
MouseFilterUp(Control.FromHandle(m.HWnd), args);
}
break;
}
// Always allow message to continue to the next filter control
return args.Handled;
}
/// <summary>
/// Occurs when [mouse filter up].
/// </summary>
public event MouseFilterEventHandler MouseFilterUp;
/// <summary>
/// Occurs when [mouse filter down].
/// </summary>
public event MouseFilterEventHandler MouseFilterDown;
/// <summary>
/// Occurs when [mouse filter move].
/// </summary>
public event MouseFilterMoveEventHandler MouseFilterMove;
}
internal delegate void MouseFilterEventHandler(object sender, MouseFilterEventArgs args);
internal delegate void MouseFilterMoveEventHandler(object sender, MouseFilterEventArgs args);
internal class MouseFilterEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="MouseFilterEventArgs" /> class.
/// </summary>
/// <param name="mouseButton">The mouse button.</param>
/// <param name="clicks">The clicks.</param>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <param name="delta">The delta.</param>
public MouseFilterEventArgs(MouseButtons mouseButton, int clicks, int x, int y, int delta)
{
Button = mouseButton;
Clicks = clicks;
X = x;
Y = y;
Delta = delta;
Handled = false;
}
/// <summary>
/// Gets or sets the button.
/// </summary>
/// <value>
/// The button.
/// </value>
public MouseButtons Button { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="MouseFilterEventArgs" /> is handled.
/// </summary>
/// <value>
/// <c>true</c> if handled; otherwise, <c>false</c>.
/// </value>
public bool Handled { get; set; }
/// <summary>
/// Gets or sets the X.
/// </summary>
/// <value>
/// The X.
/// </value>
public int X { get; set; }
/// <summary>
/// Gets or sets the Y.
/// </summary>
/// <value>
/// The Y.
/// </value>
public int Y { get; set; }
/// <summary>
/// Gets or sets the clicks.
/// </summary>
/// <value>
/// The clicks.
/// </value>
public int Clicks { get; set; }
/// <summary>
/// Gets or sets the delta.
/// </summary>
/// <value>
/// The delta.
/// </value>
public int Delta { get; set; }
}
static class Program
{
public static MouseFilter mouseFilter = new MouseFilter();
/// <summary>
/// Der Haupteinstiegspunkt für die Anwendung.
/// </summary>
[STAThread]
static void Main()
{
Application.AddMessageFilter(mouseFilter);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
To the original post, considering Cheap Funeral answer.
This version is more simple and more fluid to the touch (considers the finger move speed but not considering the effect of still scrolling after removing the finger, like moving but speeding down(maybe i'll do that whem i have time))
Works for a panel or FlowLayoutPanel
Create a form and insert a FlowLayoutPanel on it.
Form code:
Public Class Form1
Private Function GenerateButton(pName As String) As Button
Dim mResult As New Button
With mResult
.Name = pName
.Text = pName
.Width = FlowPanel.Width
.Height = 100
.Margin = New Padding(0)
.Padding = New Padding(0)
.BackColor = Color.CornflowerBlue
AddHandler .MouseDown, AddressOf Button_MouseDown
AddHandler .MouseMove, AddressOf Button_MouseMove
End With
Return mResult
End Function
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
FlowPanel.Padding = New Padding(0)
FlowPanel.Margin = New Padding(0)
Dim i As Integer
For i = 1 To 100
FlowPanel.Controls.Add(GenerateButton("btn" & i.ToString))
Next
End Sub
Dim myMouseDownPoint As Point
Dim myCurrAutoSMouseDown As Point
Private Sub Button_MouseDown(sender As Object, e As MouseEventArgs) Handles FlowPanel.MouseDown
myMouseDownPoint = PointToClient(Cursor.Position)
myCurrAutoSMouseDown = FlowPanel.AutoScrollPosition
End Sub
Private Sub Button_MouseMove(sender As Object, e As MouseEventArgs) Handles FlowPanel.MouseMove
If e.Button = Windows.Forms.MouseButtons.Left Then
Dim mLocation As Point = PointToClient(Cursor.Position)
If myMouseDownPoint <> mLocation Then
Dim mCurrAutoS As Point
Dim mDeslocation As Point = myMouseDownPoint - mLocation
mCurrAutoS.X = Math.Abs(myCurrAutoSMouseDown.X) + mDeslocation.X
mCurrAutoS.Y = Math.Abs(myCurrAutoSMouseDown.Y) + mDeslocation.Y
FlowPanel.AutoScrollPosition = mCurrAutoS
End If
End If
End Sub
TIP: To hide the scrolls of the flowlayoutpanel(or panel), create a usercontrol that inherits from flowlayoutpanel(or panel) and:
Imports System.Runtime.InteropServices
Public Class FlowLayoutPanelExt
Inherits FlowLayoutPanel
<DllImport("user32.dll")> _
Private Shared Function ShowScrollBar(hWnd As IntPtr, wBar As Integer, bShow As Boolean) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
Private Enum ScrollBarDirection
SB_HORZ = 0
SB_VERT = 1
SB_CTL = 2
SB_BOTH = 3
End Enum
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
If Me.Visible Then
ShowScrollBar(Me.Handle, CInt(ScrollBarDirection.SB_BOTH), False)
MyBase.WndProc(m)
End If
End Sub
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.AutoScroll = True
End Sub
End Class
I used the code the OP posted but found it doesnt work if there are things with in the panel like labels. To make this work more smoothly I changed .
e.Location
to
PointToClient(Cursor.Position)
and then had all objects inside the panel also call the mousedown and mousemove events. This way not matter where you click it should move.
This´s my way using IMessageFilter. For everyone who´s looking for a solutiion. First you have to implement a Filter that will listen for all Application Events:
internal class MouseFilter : IMessageFilter
{
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_LBUTTONUP = 0x0202;
private const int WM_MOUSEMOVE = 0x0200;
/// <summary>
/// Filtert eine Meldung, bevor sie gesendet wird.
/// </summary>
/// <param name="m">Die zu sendende Meldung. Diese Meldung kann nicht geändert werden.</param>
/// <returns>
/// true, um die Meldung zu filtern und das Senden zu verhindern. false, um das Senden der Meldung bis zum nächsten Filter oder Steuerelement zu ermöglichen.
/// </returns>
public bool PreFilterMessage(ref Message m)
{
Point mousePosition = Control.MousePosition;
var args = new MouseFilterEventArgs(MouseButtons.Left, 0, mousePosition.X, mousePosition.Y, 0);
switch (m.Msg)
{
case WM_MOUSEMOVE:
if (MouseFilterMove != null)
{
MouseFilterMove(Control.FromHandle(m.HWnd), args);
}
break;
case WM_LBUTTONDOWN:
if (MouseFilterDown != null)
{
MouseFilterDown(Control.FromHandle(m.HWnd), args);
}
break;
case WM_LBUTTONUP:
if (MouseFilterUp != null)
{
MouseFilterUp(Control.FromHandle(m.HWnd), args);
}
break;
}
// Always allow message to continue to the next filter control
return args.Handled;
}
/// <summary>
/// Occurs when [mouse filter up].
/// </summary>
public event MouseFilterEventHandler MouseFilterUp;
/// <summary>
/// Occurs when [mouse filter down].
/// </summary>
public event MouseFilterEventHandler MouseFilterDown;
/// <summary>
/// Occurs when [mouse filter move].
/// </summary>
public event MouseFilterMoveEventHandler MouseFilterMove;
}
internal delegate void MouseFilterEventHandler(object sender, MouseFilterEventArgs args);
internal delegate void MouseFilterMoveEventHandler(object sender, MouseFilterEventArgs args);
internal class MouseFilterEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="MouseFilterEventArgs" /> class.
/// </summary>
/// <param name="mouseButton">The mouse button.</param>
/// <param name="clicks">The clicks.</param>
/// <param name="x">The x.</param>
/// <param name="y">The y.</param>
/// <param name="delta">The delta.</param>
public MouseFilterEventArgs(MouseButtons mouseButton, int clicks, int x, int y, int delta)
{
Button = mouseButton;
Clicks = clicks;
X = x;
Y = y;
Delta = delta;
Handled = false;
}
/// <summary>
/// Gets or sets the button.
/// </summary>
/// <value>
/// The button.
/// </value>
public MouseButtons Button { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="MouseFilterEventArgs" /> is handled.
/// </summary>
/// <value>
/// <c>true</c> if handled; otherwise, <c>false</c>.
/// </value>
public bool Handled { get; set; }
/// <summary>
/// Gets or sets the X.
/// </summary>
/// <value>
/// The X.
/// </value>
public int X { get; set; }
/// <summary>
/// Gets or sets the Y.
/// </summary>
/// <value>
/// The Y.
/// </value>
public int Y { get; set; }
/// <summary>
/// Gets or sets the clicks.
/// </summary>
/// <value>
/// The clicks.
/// </value>
public int Clicks { get; set; }
/// <summary>
/// Gets or sets the delta.
/// </summary>
/// <value>
/// The delta.
/// </value>
public int Delta { get; set; }
}
Then you have to register this Filter to you Program:
static class Program
{
public static MouseFilter mouseFilter = new MouseFilter();
/// <summary>
/// Der Haupteinstiegspunkt für die Anwendung.
/// </summary>
[STAThread]
static void Main()
{
Application.AddMessageFilter(mouseFilter);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
And now you can listen for global Mouse Events and scroll your scrollable Panel like this:
public partial class MainForm : Form
{
private bool _doTouchScroll;
private Point _mouseStartPoint = Point.Empty;
private Point _yourScrollablePanelStartPoint = Point.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="MainForm" /> class.
/// </summary>
public MainForm()
{
InitializeComponent();
Program.mouseFilter.MouseFilterDown += mouseFilter_MouseFilterDown;
Program.mouseFilter.MouseFilterMove += mouseFilter_MouseFilterMove;
Program.mouseFilter.MouseFilterUp += mouseFilter_MouseFilterUp;
}
/// <summary>
/// Handles the MouseFilterDown event of the mudmFilter control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="MouseFilterEventArgs" /> instance containing the event data.
/// </param>
private void mouseFilter_MouseFilterDown(object sender, MouseFilterEventArgs e)
{
if (!_doTouchScroll && e.Button == MouseButtons.Left)
{
_mouseStartPoint = new Point(e.X, e.Y);
_yourScrollablePanelStartPoint = new Point(-yourScrollablePanel.AutoScrollPosition.X,
-yourScrollablePanel.AutoScrollPosition.Y);
}
}
/// <summary>
/// Handles the MouseFilterMove event of the mudmFilter control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="MouseFilterEventArgs" /> instance containing the event data.
/// </param>
private void mouseFilter_MouseFilterMove(object sender, MouseFilterEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (!_mouseStartPoint.Equals(Point.Empty))
{
int dx = (e.X - _mouseStartPoint.X);
int dy = (e.Y - _mouseStartPoint.Y);
if (_doTouchScroll)
{
yourScrollablePanel.AutoScrollPosition = new Point(_yourScrollablePanelStartPoint.X - dx,
_yourScrollablePanelStartPoint.Y - dy);
}
else if (Math.Abs(dx) > 10 || Math.Abs(dy) > 10)
{
_doTouchScroll = true;
}
}
}
}
/// <summary>
/// Handles the MouseFilterUp event of the mudmFilter control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
/// The <see cref="MouseFilterEventArgs" /> instance containing the event data.
/// </param>
private void mouseFilter_MouseFilterUp(object sender, MouseFilterEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (_doTouchScroll && !yourScrollablePanel.AutoScrollPosition.Equals(_yourScrollablePanelStartPoint) &&
!_yourScrollablePanelStartPoint.Equals(Point.Empty))
{
// dont fire Click-Event
e.Handled = true;
}
_doTouchScroll = false;
_mouseStartPoint = Point.Empty;
_yourScrollablePanelStartPoint = Point.Empty;
}
}
}
This solution seems to be the best one out there and the most commonly accepted one - however, if you scroll to the bottom and touch a the actual flowcontrol behind the buttons (I tried to make this so that there would be empty space), you then have to double tap-and-hold the button for the scrolling to resume. Restarting the application restores the phone-like scrolling functionality. I am wondering if anyone else has seen this or figured it out - try it with your apps and see if it is the case as well. I modified the snippet above so that you can start a new project, copy and paste this into form1's code, and hit run.
Public Class Form1
Dim FlowPanel As New FlowLayoutPanel
Private Function GenerateButton(ByVal pName As String) As Button
Dim mResult As New Button
With mResult
.Name = pName
.Text = pName
.Width = 128
.Height = 128
.Margin = New Padding(0)
.Padding = New Padding(0)
.BackColor = Color.CornflowerBlue
AddHandler .MouseDown, AddressOf Button_MouseDown
AddHandler .MouseMove, AddressOf Button_MouseMove
End With
Return mResult
End Function
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
Me.Width = 806
Me.Height = 480
FlowPanel.Padding = New Padding(0)
FlowPanel.Margin = New Padding(0)
' FlowPanel.ColumnCount = Me.Width / (128 + 6)
FlowPanel.Dock = DockStyle.Fill
FlowPanel.AutoScroll = True
Me.Controls.Add(FlowPanel)
Dim i As Integer
For i = 1 To 98
FlowPanel.Controls.Add(GenerateButton("btn" & i.ToString))
Next
End Sub
Dim myMouseDownPoint As Point
Dim myCurrAutoSMouseDown As Point
Private Sub Button_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs)
myMouseDownPoint = PointToClient(Cursor.Position)
myCurrAutoSMouseDown = FlowPanel.AutoScrollPosition
End Sub
Private Sub Button_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs)
If e.Button = Windows.Forms.MouseButtons.Left Then
Dim mLocation As Point = PointToClient(Cursor.Position)
If myMouseDownPoint <> mLocation Then
Dim mCurrAutoS As Point
Dim mDeslocation As Point = myMouseDownPoint - mLocation
mCurrAutoS.X = Math.Abs(myCurrAutoSMouseDown.X) + mDeslocation.X
mCurrAutoS.Y = Math.Abs(myCurrAutoSMouseDown.Y) + mDeslocation.Y
FlowPanel.AutoScrollPosition = mCurrAutoS
End If
End If
End Sub
End Class