0
votes

I'm trying to build Inventory like interface for project that I'm creating. Idea is to have list of images that can be dragged to players as shown below:

enter image description here

Images are loaded from directory and displayed inside ListView, list of players is displayed in ListBox.

My XAML looks like this:

<Window x:Class="DynamicImagesDrag.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:dynamicImagesDrag="clr-namespace:DynamicImagesDrag"
        Title="MainWindow"
        Height="405"
        Width="719.162"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <dynamicImagesDrag:StringToImageConverter x:Key="StringToImageConverter" />
    </Window.Resources>
    <Grid>
        <ListView Name="MyList"
                  ItemsSource="{Binding Images}"
                  PreviewMouseLeftButtonDown="UIElement_OnPreviewMouseLeftButtonDown"
                  PreviewMouseMove="UIElement_OnPreviewMouseMove"
                  ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                  VerticalAlignment="Stretch"
                  HorizontalAlignment="Left"
                  Margin="10" Width="250">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel />
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>

            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate TargetType="ListViewItem">
                                <ContentPresenter/>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ListView.ItemContainerStyle>

            <ListView.ItemTemplate>
                <DataTemplate>
                    <DockPanel Width="50" Height="50">
                        <DockPanel.Background>
                            <ImageBrush ImageSource="BG1.png"/>
                        </DockPanel.Background>
                        <Image Source="{Binding Path, Converter={StaticResource StringToImageConverter} }"
                               Height="32"
                               Width="32" />
                    </DockPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <ListBox ItemsSource="{Binding People}" HorizontalAlignment="Right" HorizontalContentAlignment="Stretch" Margin="10" VerticalAlignment="Stretch"
                 Width="200">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Vertical" AllowDrop="True" PreviewDrop="UIElement_OnPreviewDrop">
                        <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                        <ProgressBar Height="20" Value="{Binding Points}" Margin="0" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

Code behind:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace DynamicImagesDrag
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow
    {
        private readonly ObservableCollection<MyImage> _images = new ObservableCollection<MyImage>();

        public ObservableCollection<MyImage> Images
        {
            get { return _images; }
        }

        private readonly ObservableCollection<Person> _people = new ObservableCollection<Person>();

        public ObservableCollection<Person> People { get { return _people; } }


        public MainWindow()
        {
            InitializeComponent();

            _people.Add(new Person() { Name = "Person1", Points = 10 });
            _people.Add(new Person() { Name = "Person2", Points = 0 });
            _people.Add(new Person() { Name = "Person3", Points = 40 });

            string appPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            if (appPath != null)
            {
                string imagePath = Path.Combine(appPath, "Images");
                if (Directory.Exists(imagePath))
                {
                    var images = Directory
                        .EnumerateFiles(imagePath)
                        .Where(file => file.ToLower().EndsWith("jpg") || file.ToLower().EndsWith("png"))
                        .ToList();

                    foreach (string image in images)
                    {
                        _images.Add(new MyImage
                        {
                            Name = Path.GetFileName(image),
                            Path = image,
                            Points = Convert.ToInt32(Path.GetFileNameWithoutExtension(image))
                        });
                    }
                }
            }
        }

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool GetCursorPos(ref Win32Point pt);

        [StructLayout(LayoutKind.Sequential)]
        internal struct Win32Point
        {
            public Int32 X;
            public Int32 Y;
        };

        public static Point GetMousePosition()
        {
            Win32Point w32Mouse = new Win32Point();
            GetCursorPos(ref w32Mouse);
            return new Point(w32Mouse.X, w32Mouse.Y);
        }


        private void UIElement_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            _startPoint = e.GetPosition(null);
        }

        #region Field and Properties

        private bool _dragHasLeftScope;
        private Point _startPoint;
        public bool IsDragging { get; set; }

        DragAdorner _adorner;
        AdornerLayer _layer;
        public FrameworkElement DragScope { get; set; }

        #endregion // Field and Properties



        private void UIElement_OnPreviewMouseMove(object sender, MouseEventArgs e)
        {
            // Ensure that the user does not drag by accident
            if (e.LeftButton == MouseButtonState.Pressed && !IsDragging)
            {
                Point position = e.GetPosition(null);

                if (Math.Abs(position.X - _startPoint.X) > SystemParameters.MinimumHorizontalDragDistance ||
                    Math.Abs(position.Y - _startPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
                {
                    StartDragInProcAdorner(e);
                }
            }
        }

        void DragScope_DragLeave(object sender, DragEventArgs e)
        {
            if (e.OriginalSource == DragScope)
            {
                Point p = e.GetPosition(DragScope);
                Rect r = VisualTreeHelper.GetContentBounds(DragScope);
                if (!r.Contains(p))
                {
                    _dragHasLeftScope = true;
                    e.Handled = true;
                }
            }
        }

        void Window1_DragOver(object sender, DragEventArgs args)
        {
            if (_adorner == null) return;
            _adorner.LeftOffset = args.GetPosition(DragScope).X /* - _startPoint.X */ ;
            _adorner.TopOffset = args.GetPosition(DragScope).Y /* - _startPoint.Y */ ;
        }

        void DragScope_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
        {
            if (_dragHasLeftScope)
            {
                e.Action = DragAction.Cancel;
                e.Handled = true;
            }
        }

        private void StartDragInProcAdorner(MouseEventArgs e)
        {
            DragScope = Application.Current.MainWindow.Content as FrameworkElement;

            bool previousDrop = DragScope.AllowDrop;
            DragScope.AllowDrop = true;
            try
            {
                DragEventHandler draghandler = Window1_DragOver;
                DragScope.PreviewDragOver += draghandler;
                DragEventHandler dragleavehandler = DragScope_DragLeave;
                DragScope.DragLeave += dragleavehandler;
                QueryContinueDragEventHandler queryhandler = DragScope_QueryContinueDrag;
                DragScope.QueryContinueDrag += queryhandler;

                DragScope.GiveFeedback+=DragScope_GiveFeedback;

                FrameworkElement dr = MyList.ItemContainerGenerator.ContainerFromItem(MyList.SelectedItem) as FrameworkElement;

                if (dr == null)
                    return;
                _adorner = new DragAdorner(DragScope, dr, true, 0.5);
                _layer = AdornerLayer.GetAdornerLayer(DragScope);
                _layer.Add(_adorner);


                IsDragging = true;
                _dragHasLeftScope = false;

                DataObject data = new DataObject(MyList.SelectedItem as MyImage);
                DragDropEffects de = DragDrop.DoDragDrop(MyList, data, DragDropEffects.Move);

                DragScope.AllowDrop = previousDrop;
                AdornerLayer.GetAdornerLayer(DragScope).Remove(_adorner);
                _adorner = null;

                DragScope.DragLeave -= dragleavehandler;
                DragScope.QueryContinueDrag -= queryhandler;
                DragScope.PreviewDragOver -= draghandler;

                IsDragging = false;
            }
            catch
            {
                DragScope.AllowDrop = previousDrop;
                AdornerLayer.GetAdornerLayer(DragScope).Remove(_adorner);
                _adorner = null;
                IsDragging = false;

            }
        }

        private void DragScope_GiveFeedback(object sender, GiveFeedbackEventArgs e)
        {


        }

        private void UIElement_OnPreviewDrop(object sender, DragEventArgs e)
        {
            var stackPanel = sender as StackPanel;
            if (stackPanel == null) return;
            var student = stackPanel.DataContext as Person;

            MyImage myImage = e.Data.GetData(typeof(MyImage)) as MyImage;
            if (student != null) student.Points += myImage.Points;
        }
    }

    public class StringToImageConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            try
            {
                return new BitmapImage(new Uri((string)value));
            }
            catch
            {
                return new BitmapImage();
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    public class Person : INotifyPropertyChanged
    {
        private string _name;
        private int _points;

        public string Name
        {
            get { return _name; }
            set
            {
                if (value == _name) return;
                _name = value;
                OnPropertyChanged();
            }
        }

        public int Points
        {
            get { return _points; }
            set
            {
                if (value == _points) return;
                _points = value;
                if (_points >= 100)
                {
                    _points -= 100;
                    Debug.WriteLine("100!");
                }
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class MyImage
    {
        public string Path { get; set; }
        public string Name { get; set; }

        public int Points { get; set; }
    }
}

and DragAdorner (taken from http://www.infragistics.com/community/blogs/alex_fidanov/archive/2009/07/28/drag-amp-drop-with-datapresenter-family-controls.aspx)

class DragAdorner : Adorner
{
    public DragAdorner(UIElement owner) : base(owner) { }

    public DragAdorner(UIElement owner, UIElement adornElement, bool useVisualBrush, double opacity)
        : base(owner)
    {
        _owner = owner;
        VisualBrush _brush = new VisualBrush
        {
            Opacity = opacity,
            Visual = adornElement
        };

        DropShadowEffect dropShadowEffect = new DropShadowEffect
        {
            Color = Colors.Black,
            BlurRadius = 15,
            Opacity = opacity
        };

        Rectangle r = new Rectangle
        {
            RadiusX = 3,
            RadiusY = 3,
            Fill = _brush,
            Effect = dropShadowEffect,
            Width = adornElement.DesiredSize.Width,
            Height = adornElement.DesiredSize.Height
        };


        XCenter = adornElement.DesiredSize.Width / 2;
        YCenter = adornElement.DesiredSize.Height / 2;

        _child = r;
    }

    private void UpdatePosition()
    {
        AdornerLayer adorner = (AdornerLayer)Parent;
        if (adorner != null)
        {
            adorner.Update(AdornedElement);
        }
    }

    #region Overrides

    protected override Visual GetVisualChild(int index)
    {
        return _child;
    }
    protected override int VisualChildrenCount
    {
        get
        {
            return 1;
        }
    }
    protected override Size MeasureOverride(Size finalSize)
    {
        _child.Measure(finalSize);
        return _child.DesiredSize;
    }
    protected override Size ArrangeOverride(Size finalSize)
    {

        _child.Arrange(new Rect(_child.DesiredSize));
        return finalSize;
    }
    public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
    {
        GeneralTransformGroup result = new GeneralTransformGroup();

        result.Children.Add(base.GetDesiredTransform(transform));
        result.Children.Add(new TranslateTransform(_leftOffset, _topOffset));
        return result;
    }

    #endregion

    #region Field & Properties

    public double scale = 1.0;
    protected UIElement _child;
    protected VisualBrush _brush;
    protected UIElement _owner;
    protected double XCenter;
    protected double YCenter;
    private double _leftOffset;
    public double LeftOffset
    {
        get { return _leftOffset; }
        set
        {
            _leftOffset = value - XCenter;
            UpdatePosition();
        }
    }
    private double _topOffset;
    public double TopOffset
    {
        get { return _topOffset; }
        set
        {
            _topOffset = value - YCenter;

            UpdatePosition();
        }
    }

    #endregion
}

Drag works almost fine:

enter image description here

except adorner is visible only in source list and target list, it isn't displayed during whole drag.

My questions are:

  • How can I fix drag and drop to see adorner whole time?
  • How can I display image instead selectedItem inside adorner? Right now inside adorner is that brown background, I'd like to get only transparent image.
  • How can I show if dragtarget is correct inside adorner instead of changing cursor? I'd like to change opacity of adorner if target is correct.
  • I'd like to get drag and drop working with touch events, @KOTIX suggested using Gong WPF dragdrop, will it work fine on touch enabled screens?
  • Currently I'm setting AllowDrop on StackPanel inside ListBox ItemTemplate, should it stay there or maybe I should set in on ListBox?

I've searched over internet (including SO) for solution, but I couldn't find anything that fits my needs.

I found some great articles:
http://www.codeproject.com/Articles/37161/WPF-Drag-and-Drop-Smorgasbord
http://www.zagstudio.com/blog/488#.VgHPyxHtmkp
http://nonocast.cn/adorner-in-wpf-part-5-drag-and-drop/
https://blogs.claritycon.com/blog/2009/03/generic-wpf-drag-and-drop-adorner/

Last one was very interesting, but I wasn't able to modify it in a way to add points to players instead of moving items. In my case I want items on left to stay, I just want to update list on right based on dragged item.

2

2 Answers

1
votes

I don't have all the answers yet but it may lead you to your goals.

  1. How can I fix drag and drop to see adorner whole time?

    Your DragLayer is Transparent, just set a value for the Background property of your Grid

  2. How can I display image instead selectedItem inside adorner? Right now inside adorner is that brown background, I'd like to get only transparent image.

The best I've come to is to explore the VisualTree to get the Image inside the template :

//Get the `ListViewItem`
FrameworkElement dr = MyList.ItemContainerGenerator.ContainerFromItem(MyList.SelectedItem) as FrameworkElement;
//Explore the VisualTree to get the grand-child
//This should be refactored to a Func<UIElement,UIElement> to accord to templates changes
UIElement el = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(dr, 0), 0) as UIElement;
//Create the DragAdorner using the found UIElement
_adorner = new DragAdorner(DragScope, el, true, 1d);
  1. How can I show if dragtarget is correct inside adorner instead of changing cursor? I'd like to change opacity of adorner if target is correct.

To show some feedback, you need to... GiveFeedBack

Here is your handler :

private void DragScope_GiveFeedback(object sender, GiveFeedbackEventArgs e)
{
    if (_adorner == null) return;
    if (e.Effects == DragDropEffects.Copy)
    {
        _adorner.Opacity = 1d;
        e.Handled = true;
    }
    else
    {
        _adorner.Opacity = 0.5d;
        e.Handled = true;
    }
}

Now, you have to set the desired effect in 2 places : the Person template and the DragLayer :

private void StackPanel_DragOver(object sender, DragEventArgs e)
{
    e.Effects = DragDropEffects.Copy;
    e.Handled = true;
}
void Window1_DragOver(object sender, DragEventArgs args)
{
    if (_adorner == null) return;
    _adorner.LeftOffset = args.GetPosition(DragScope).X;
    _adorner.TopOffset = args.GetPosition(DragScope).Y;
    if (!args.Handled)
        args.Effects = DragDropEffects.Move;
}

In order for these to work, you have to allow those 2 effects when initiating the DragDrop :

DragDropEffects de = DragDrop.DoDragDrop(MyList, data, DragDropEffects.Move | DragDropEffects.Copy);

Also, to avoid cumulative opacity reduction, use 1 for the DragAdorner opacity in constructor.

  1. I'd like to get drag and drop working with touch events, @KOTIX suggested using Gong WPF dragdrop, will it work fine on touch enabled screens?

This solution is fully native, you should get it to work with touch devices.

  1. Currently I'm setting AllowDrop on StackPanel inside ListBox ItemTemplate, should it stay there or maybe I should set in on ListBox?

If your goal is to add MyImage.Points to the drop target, the AllowDrop must be set on the StackPanel.

1
votes

I would suggest using Gong WPF dragdrop

This library always worked great for me and way better than built-in support. It also adds great support for MVVM design.

Good luck!