Created
July 6, 2017 11:59
-
-
Save mamidenn/9eaa09695ba4d0a5762a2b74eee34d7f to your computer and use it in GitHub Desktop.
A ListView with rearrangeable items. Uses a DropIndicator object to provide visual clues as to where a dragged item will be dropped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
using System.Windows; | |
using System.Windows.Documents; | |
using System.Windows.Media; | |
namespace de.dennhardt | |
{ | |
/// <summary> | |
/// Eine Himmelsrichtung. Richtungen lassen sich per logischer | |
/// Oder-Operation kombinieren. | |
/// </summary> | |
public enum Direction | |
{ | |
None = 0, | |
North = 1, | |
Northeast = 3, | |
East = 2, | |
Southeast = 6, | |
South = 4, | |
Southwest = 12, | |
West = 8, | |
Northwest = 9 | |
} | |
/// <summary> | |
/// Ein Adorner, der visuelles Feedback bei Drag and Drop-Operationen bietet | |
/// </summary> | |
public class DropIndicator : Adorner | |
{ | |
/// <summary> | |
/// Richtung, in der der Indikator angezeigt wird | |
/// </summary> | |
public Direction Direction; | |
/// <summary> | |
/// Initialisiert einen neuen Indikator | |
/// </summary> | |
/// <param name="adornedElement">Das Element, an das der Indikator angehängt wird</param> | |
/// <param name="direction">Die Richtung, in der der Indikator angehängt wird</param> | |
public DropIndicator(UIElement adornedElement, Direction direction) | |
: base(adornedElement) | |
{ | |
this.Direction = direction; | |
this.IsHitTestVisible = false; | |
} | |
/// <summary> | |
/// Wird beim Rendern ausgeführt. Rendert den Indikator | |
/// </summary> | |
/// <param name="context">Kontext, in dem der Indikator gerendert wird</param> | |
protected override void OnRender(DrawingContext context) | |
{ | |
Rect rect = new Rect(this.AdornedElement.RenderSize); | |
Pen pen = new Pen(new SolidColorBrush(Colors.Black), 2); | |
if ((Direction & Direction.West) > 0) | |
{ | |
drawFeatheredLine(context, pen, rect.TopLeft, rect.BottomLeft); | |
} | |
if ((Direction & Direction.East) > 0) | |
{ | |
drawFeatheredLine(context, pen, rect.TopRight, rect.BottomRight); | |
} | |
} | |
private void drawFeatheredLine(DrawingContext context, Pen pen, Point p0, Point p1) | |
{ | |
context.DrawLine(pen, p0.Add(-2, 0), p0.Add(0,2)); | |
context.DrawLine(pen, p0.Add(2, 0), p0.Add(0,2)); | |
context.DrawLine(pen, p0.Add(0, 2), p1.Add(0,-2)); | |
context.DrawLine(pen, p1.Add(-2, 0), p1.Add(0, -2)); | |
context.DrawLine(pen, p1.Add(2, 0), p1.Add(0, -2)); | |
} | |
} | |
/// <summary> | |
/// Bietet zusätzliche Methoden | |
/// </summary> | |
public static partial class ExtensionMethods | |
{ | |
/// <summary> | |
/// Bildet die Summe aus einem Punkt und einem durch zwei Skalare definierten | |
/// Punkt. Erleichtert die lesbarkeit der Point.Add()-Methode. | |
/// </summary> | |
/// <param name="p">Punkt</param> | |
/// <param name="x">X-Koordinate</param> | |
/// <param name="y">Y-Koordinate</param> | |
/// <returns></returns> | |
public static Point Add(this Point p, double x, double y) | |
{ | |
return new Point(p.X + x, p.Y + y); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Windows; | |
using System.Windows.Controls; | |
using System.Windows.Controls.Primitives; | |
using System.Windows.Documents; | |
using System.Windows.Input; | |
using System.Windows.Media; | |
namespace de.dennhardt | |
{ | |
/// <summary> | |
/// A ListView with rearrangeable items. Uses a DropIndicator object to | |
/// provide visual clues as to where a dragged item will be dropped. As generic | |
/// as possible to enable usage in many circumstances. | |
/// | |
/// Proudly presented by Martin Dennhardt | |
/// </summary> | |
class OrderableListView : ListView | |
{ | |
public DropIndicator DropIndicator; | |
private bool prepareDrag = false; | |
private Type itemType { | |
// get the type of the generic list items | |
get | |
{ | |
var genericArguments = ItemsSource.GetType().GetGenericArguments(); | |
if (genericArguments.Length == 1) | |
{ | |
return genericArguments[0]; | |
} | |
else | |
{ | |
return typeof(object); | |
} | |
} | |
} | |
public OrderableListView() | |
{ | |
AllowDrop = true; | |
} | |
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) | |
{ | |
var item = getItemAtPosition(e.GetPosition(this)); | |
// clear selection if clicked in empty space | |
if (item == null) | |
{ | |
if (getItemAtPosition<ScrollBar>(e.GetPosition(this)) == null) | |
{ | |
// deselect all Items, but only if we're not using a scrollbar | |
Focus(); | |
SelectedItem = null; | |
} | |
return; | |
} | |
prepareDrag = true; | |
// don't drag and drop if clicked in textbox | |
if (getItemAtPosition<TextBoxBase>(e.GetPosition(this)) != null) | |
{ | |
prepareDrag = false; | |
} | |
// suppress item selection on mousedown | |
// instead this is handled @ mouse up event | |
if (!item.IsSelected) | |
{ | |
// but only if we haven't clicked a button! | |
if (getItemAtPosition<ButtonBase>(e.GetPosition(this)) == null) | |
{ | |
e.Handled = true; | |
} | |
} | |
} | |
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e) | |
{ | |
prepareDrag = false; | |
// let other logic handle it, if we clicked a button | |
if (getItemAtPosition<ButtonBase>(e.GetPosition(this)) != null) | |
{ | |
return; | |
} | |
// focus item if selection has changed | |
var item = getItemAtPosition(e.GetPosition(this)); | |
if (item != null && SelectedItem != ItemContainerGenerator.ItemFromContainer(item)) | |
{ | |
item.Focus(); | |
SelectedItem = ItemContainerGenerator.ItemFromContainer(item); | |
} | |
} | |
protected override void OnMouseLeave(MouseEventArgs e) | |
{ | |
prepareDrag = false; | |
} | |
protected override void OnMouseMove(MouseEventArgs e) | |
{ | |
removeDropIndicator(); | |
// drag if we're already waiting for it | |
if (prepareDrag) | |
{ | |
prepareDrag = false; | |
onDrag(e); | |
} | |
} | |
protected sealed override void OnDragOver(DragEventArgs e) | |
{ | |
// check if dragged element is of correct type | |
// prevent dropping if it is not | |
if (!e.Data.GetDataPresent(itemType)) | |
{ | |
e.Effects = DragDropEffects.None; | |
e.Handled = true; | |
return; | |
} | |
// get ListViewItem below pointer | |
var item = getItemAtPosition(e.GetPosition(this)); | |
if (item == null) | |
{ | |
return; | |
} | |
// remove old drop indicator | |
removeDropIndicator(); | |
// get octant of pointer position | |
var direction = getDirectionFromPosition(item, e.GetPosition(item)); | |
// add new drop indicator | |
DropIndicator = new DropIndicator(item, direction); | |
var newAdornerLayer = AdornerLayer.GetAdornerLayer(item); | |
newAdornerLayer.Add(DropIndicator); | |
} | |
protected override void OnDrop(DragEventArgs e) | |
{ | |
var dropIndex = ((IList)ItemsSource).Count; | |
if (DropIndicator != null) | |
{ | |
// get the item next to the drop indicator | |
var item = DropIndicator.AdornedElement as ListViewItem; | |
var direction = DropIndicator.Direction; | |
// remove drop indicator | |
removeDropIndicator(); | |
// figure out where to insert the item | |
// TODO generify for vertical Lists! | |
var addBelow = (direction & Direction.East) > 0 ? 1 : 0; | |
dropIndex = ItemContainerGenerator.IndexFromContainer(item) + addBelow; | |
} | |
// get dragged item and its index | |
var draggedItem = e.Data.GetData(itemType); | |
var draggedIndex = ((IList)ItemsSource).IndexOf(draggedItem); | |
// decrement if we're moving up to compensate for index recalculation | |
if (draggedIndex >= 0) | |
{ | |
dropIndex -= (dropIndex > draggedIndex) ? 1 : 0; | |
} | |
// alway select new items | |
var wasSelected = true; | |
if (((IList)ItemsSource).Contains(draggedItem)) | |
{ | |
// preserve selection if item was already in list | |
wasSelected = ((ListViewItem)ItemContainerGenerator.ContainerFromItem(draggedItem)).IsSelected; | |
// remove item from list | |
((IList)ItemsSource).Remove(draggedItem); | |
} | |
else | |
{ | |
// clear selection if we add a new item | |
SelectedItem = null; | |
} | |
// insert item at new location | |
((IList)ItemsSource).Insert(dropIndex, draggedItem); | |
// restore item selection state | |
var itemContainer = ItemContainerGenerator.ContainerFromItem(draggedItem) as ListViewItem; | |
if (itemContainer != null) | |
{ | |
itemContainer.IsSelected = wasSelected; | |
} | |
} | |
private void onDrag(MouseEventArgs e) | |
{ | |
// get ListViewItem below pointer | |
var item = getItemAtPosition(e.GetPosition(this)); | |
if (item == null) | |
{ | |
return; | |
} | |
// get corresponding data object | |
var data = ItemContainerGenerator.ItemFromContainer(item); | |
// start drag and drop | |
DragDrop.DoDragDrop(item, data, DragDropEffects.Move); | |
} | |
private void removeDropIndicator() | |
{ | |
if (DropIndicator != null) | |
{ | |
var layer = AdornerLayer.GetAdornerLayer(DropIndicator.AdornedElement); | |
if (layer != null) | |
{ | |
layer.Remove(DropIndicator); | |
} | |
DropIndicator = null; | |
} | |
} | |
private ListViewItem getItemAtPosition(Point p) | |
{ | |
return getItemAtPosition<ListViewItem>(p); | |
} | |
/// <summary> | |
/// Get item of type T at supplied point that is farthest down in the | |
/// tree or null if no such item exists. | |
/// </summary> | |
/// <typeparam name="T">Type of required item</typeparam> | |
/// <param name="p">Point where to look for the item</param> | |
/// <returns>Required item or null</returns> | |
private T getItemAtPosition<T>(Point p) where T : FrameworkElement | |
{ | |
var item = InputHitTest(p) as FrameworkElement; | |
while (item as T == null && item != this) | |
{ | |
item = VisualTreeHelper.GetParent(item) as FrameworkElement; | |
} | |
return item as T; | |
} | |
private Direction getDirectionFromPosition(ListViewItem item, Point p) | |
{ | |
Direction direction = Direction.None; | |
direction |= p.Y < item.ActualHeight / 2 ? Direction.North : Direction.South; | |
direction |= p.X < item.ActualWidth / 2 ? Direction.West : Direction.East; | |
return direction; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment