Introduction
This article describes a ListView
derived custom control within which users can drag to scroll its content. It is designed to make user interfaces more intuitive, and it could be a good fit for some touch screen applications.
The demo EXE requires either Windows XP or Windows Vista with .NET 3.5 SP1 installed, and the source code was compiled and tested against Visual Studio 2008 SP1.
Background
Here is a brief description of the design goals for DraggableListView
:
- Hide horizontal and vertical scrollbars, and add new functionality to allow scrolling by tracking the
PreviewMouseDown
, PreviewMouseMove
, and PreviewMouseUp
mouse events.
- Add four new Dependency Properties
VerticalCurrentPage
, VerticalTotalPage
, HorizontalCurrentPage
, and HorizontalTotalPage
to help tracking while scrolling. Since there is no horizontal or vertical scrollbars, it may be hard for users to tell the whereabouts without these four properties.
- Add algorithm to calculate dragging speed from the
PreviewMouseDown
event to the PreviewMouseUp
event. Based on that speed, scrolling after the MouseUp
event should be either stopping immediately, gradually slowing down, or continuing to scroll until it reaches the other end.
- While scrolling after the
MouseUp
event, the user should be able to stop scrolling immediately with a single mouse click.
- Select an item by double-clicking instead of single-clicking; this will prevent selection changes while scrolling.
Using the Code
To use the control, you can simply copy the file DraggableListView.cs into your own project and include the following lines:
xmlns:slv="clr-namespace:DraggableListViewLib"
...
<slv:DraggableListView>
...
</slv:DraggableListView>
...
Also, you can check the demo source code on how to use the control and set its properties.
How it Works
Here, I will briefly describe how this control is actually coded:
1. Access the ScrollViewer inside ListView
In order to programmatically scroll the content of a ListView
, we need to have a reference to the ScrollViewer
object inside the ListView
. I did this in the function OnApplyTemplate()
by calling GetVisualChild()
, as follows:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
DependencyObject border = GetTemplateChild("Bd");
listViewScrollViewer = GetVisualChild<ScrollViewer>(border);
if (listViewScrollViewer != null)
{
listViewScrollViewer.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
listViewScrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
listViewScrollViewer.CanContentScroll = false;
listViewScrollViewer.ScrollChanged +=
new ScrollChangedEventHandler(listViewScrollViewer_ScrollChanged);
}
}
From the code above, you can also see that I hide the ScrollBar
s and set CanContentScroll
to false
after I have a valid reference to the ScrollViewer
object.
2. Logic to calculate the scrolling speed
In order to best estimate the mouse dragging speed, I use the following local variables:
...
private Point mouseDownPoint;
private Point mouseMovePoint1;
private Point mouseMovePoint2;
private Point mouseMovePoint3;
private DateTime mouseDownTime;
private DateTime mouseMoveTime1;
private DateTime mouseMoveTime2;
private DateTime mouseMoveTime3;
...
The variables mouseDownPoint
and mouseDownTime
are initially saved when the user starts to drag the mouse, and move point 1 to move point 3 are the last three points during a mouse drag movement. If a mouse drag on the control is like a straight line, we can calculate the scrolling speed simply using mouseDownPoint
and mouseMovePoint3
. But if the user zigzagged on the control, the best estimate for the scrolling speed is to use mouseMovePoint1
and mouseMovePoint3
.
Here are the lines of code I use to estimate the scrolling speed:
double speed = 0.0, speedX = 0.0, speedY = 0.0, speed1 = 0.0, speed2 = 0.0;
double mouseScrollTime = 0.0;
double mouseScrollDistance = 0.0;
Direction leftOrRight, upOrDown;
if (mouseMoveTime1 != mouseDownTime && mouseMoveTime2 !=
mouseDownTime && mouseMoveTime3 != mouseDownTime)
{
mouseScrollTime = mouseMoveTime3.Subtract(mouseMoveTime1).TotalSeconds;
mouseScrollDistance =
Math.Sqrt(Math.Pow(mouseMovePoint3.X - mouseMovePoint1.X, 2.0) +
Math.Pow(mouseMovePoint3.Y - mouseMovePoint1.Y, 2.0));
if (mouseScrollTime != 0 && mouseScrollDistance != 0)
speed1 = mouseScrollDistance / mouseScrollTime;
mouseScrollTime = mouseMoveTime3.Subtract(mouseDownTime).TotalSeconds;
mouseScrollDistance =
Math.Sqrt(Math.Pow(mouseMovePoint3.X - mouseDownPoint.X, 2.0) +
Math.Pow(mouseMovePoint3.Y - mouseDownPoint.Y, 2.0));
if (mouseScrollTime != 0 && mouseScrollDistance != 0)
speed2 = mouseScrollDistance / mouseScrollTime;
if (speed1 > speed2)
{
speed = speed1;
leftOrRight = (mouseMovePoint3.X > mouseMovePoint1.X) ?
Direction.Left : Direction.Right;
upOrDown = (mouseMovePoint3.Y > mouseMovePoint1.Y) ?
Direction.Up : Direction.Down;
speedX = Math.Abs(mouseMovePoint3.X - mouseMovePoint1.X) /
mouseMoveTime3.Subtract(mouseMoveTime1).TotalSeconds;
speedY = Math.Abs(mouseMovePoint3.Y - mouseMovePoint1.Y) /
mouseMoveTime3.Subtract(mouseMoveTime1).TotalSeconds;
}
else
{
speed = speed2;
leftOrRight = (mouseMovePoint3.X > mouseDownPoint.X) ?
Direction.Left : Direction.Right;
upOrDown = (mouseMovePoint3.Y > mouseDownPoint.Y) ?
Direction.Up : Direction.Down;
speedX = Math.Abs(mouseMovePoint3.X - mouseDownPoint.X) /
mouseMoveTime3.Subtract(mouseDownTime).TotalSeconds;
speedY = Math.Abs(mouseMovePoint3.Y - mouseDownPoint.Y) /
mouseMoveTime3.Subtract(mouseDownTime).TotalSeconds;
}
...
3. Stop scrolling immediately with a mouse click
Scrolling after the MouseUp
event is implemented using a Storyboard
object. I added logic in the PreviewMouseDown
event to check whether the Storyboard
object is still active. If it is, I will first pause the object, save the ScrollViewer
's current horizontal and vertical offsets, then clear out the value provided by the animation, and finally, set back the horizontal and vertical offsets.
You can check the relevant code here:
...
if (storyboard != null)
{
if (storyboard.GetCurrentState(this) == ClockState.Active)
{
storyboard.Pause(this);
}
double tempHorizontalOffset = listViewScrollViewer.HorizontalOffset;
double tempVerticalOffset = listViewScrollViewer.VerticalOffset;
this.BeginAnimation(DraggableListView.ScrollViewerHorizontalOffsetProperty, null);
listViewScrollViewer.ScrollToHorizontalOffset(tempHorizontalOffset);
this.BeginAnimation(DraggableListView.ScrollViewerVerticalOffsetProperty, null);
listViewScrollViewer.ScrollToVerticalOffset(tempVerticalOffset);
storyboard = null;
}
...
4. Select an item by double-click
In order to achieve selecting/unselecting an item by double-click instead of single-click, we need to hide the SelectionChanged
event for single mouse click from DraggableListView
and only fire the event caused by other situations like double mouse click, etc.
I use the local variables selectedItemPrivate
and selectedItemsPrivate
to keep a copy of the SelectedItem
and SelectedItems
properties in case they get out of synchronization. I also use a few other boolean variables to keep track of the different situations.
...
private Object selectedItemPrivate = null;
private ArrayList selectedItemsPrivate = null;
private bool isMouseDoubleClick = false;
private bool isMouseDown = false;
private bool mayBeOutOfSync = false;
...
The function OnSelectionChanged()
actually decides whether to hide the SelectionChanged
event or not:
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (isMouseDown == true && isMouseDoubleClick == false)
{
mayBeOutOfSync = true;
e.Handled = true;
}
else
{
this.selectedItemPrivate = this.SelectedItem;
if (this.SelectedItems != null)
this.selectedItemsPrivate = new ArrayList(this.SelectedItems);
else
this.selectedItemsPrivate = null;
}
base.OnSelectionChanged(e);
}
And finally, I synchronize the local copy of selectedItemPrivate
and selectedItemsPrivate
back to the SelectedItem
and SelectedItems
properties in the functions draggableListView_MouseButtonDown()
, OnPreviewMouseMove()
, and OnPreviewMouseUp()
, with the following lines of code.
...
if (mayBeOutOfSync)
{
if (this.SelectionMode == SelectionMode.Single)
{
this.SelectedItem = this.selectedItemPrivate;
}
else
SetSelectedItems(selectedItemsPrivate);
mayBeOutOfSync = false;
}
...
5. Logic when dragging reaches an edge
When dragging reaches either edge, the content will stop moving while the user continues to drag the mouse. This will cause the previously saved variable initialOffset
to be invalid, and we need logic in the function OnPreviewMouseMove()
to detect whenever we reach an edge and re-save all the necessary information.
...
Point delta = new Point(this.mouseDownPoint.X - mouseCurrentPoint.X,
this.mouseDownPoint.Y - mouseCurrentPoint.Y);
if ((listViewScrollViewer.ScrollableHeight > 0 &&
((listViewScrollViewer.VerticalOffset == 0 && delta.Y < 0) ||
(listViewScrollViewer.VerticalOffset ==
listViewScrollViewer.ScrollableHeight && delta.Y > 0))) ||
listViewScrollViewer.ScrollableWidth > 0 &&
((listViewScrollViewer.HorizontalOffset == 0 && delta.X < 0) ||
(listViewScrollViewer.HorizontalOffset ==
listViewScrollViewer.ScrollableWidth && delta.X > 0)))
{
initialOffset.X = listViewScrollViewer.HorizontalOffset;
initialOffset.Y = listViewScrollViewer.VerticalOffset;
mouseDownPoint = mouseMovePoint1 = mouseMovePoint2 =
mouseMovePoint3 = e.GetPosition(this);
mouseDownTime = mouseMoveTime1 =
mouseMoveTime2 = mouseMoveTime3 = DateTime.Now;
}
else
{
listViewScrollViewer.ScrollToHorizontalOffset(this.initialOffset.X + delta.X);
listViewScrollViewer.ScrollToVerticalOffset(this.initialOffset.Y + delta.Y);
}
...
Limitation
Quote from MSDN: "If you require physical scrolling instead of logical scrolling... set its CanContentScroll
property to false
", and so I set CanContentScroll
to false
in the function OnApplyTemplate()
. But setting CanContentScroll
to false
also turns off virtualizing, which makes this class less useful if you have lots of rows to display. Also because of this, you will notice there is a pause when you switch to the "Editable ListView Sample" for the first time.
Feel free to use the DraggableListView
in your WPF applications. I hope you find WPF as cool as I do.
History
- Oct. 03, 2008 - Initial release.