« Distract your WPF application users with a splash screen! | Main | WPF Application Performance »

WPF - The Ghost Cursor

I wanted a cursor that would look like some Visuals from a WPF project I was working on a while back. The challenge though was that I wanted the cursor to be visually appealing inside and outside of my WPF application. When I dragged an image from my application to another application, I didn't want the boring Drag cursor that is the default in Windows. I wanted thumbnails of what I was dragging.

I never quite completed the project or the code, but I decided to post it hoping that it might save someone else some time -- as it is close. Very close.

Here's the effect I was looking for (the grid is under the cursor):

image

Here are the originals I dragged from my application:

image

So, you can see how the mini images with a transparent gradient where created from the images in my application. The code I'm about to present is not for the faint of heart. It uses Win32 Interop. Big time. So, if you don't understand what that means, it's likely that you won't be able to make any improvements to the code without some assistance.

The code is here.

Here's how I used it:

        private void StartDrag()
        {
            WrapPanel panel = new WrapPanel();
            IList selectedItems = listPhotos.SelectedItems;
         
            foreach (object o in selectedItems)
            {
                Photo p = o as Photo;
                if (p != null)
                {
                    if (p.IsThumbnailLoaded)
                    {
                        Border b = new Border();
                        b.Margin = new Thickness(6);
                        b.Padding = new Thickness(2);
                        b.Background = new ImageBrush(p.Thumbnail);
                        b.BorderBrush = Brushes.Black;
                        b.BorderThickness = new Thickness(3);
                        b.Height = 60;
                        b.Width = 80;

panel.Children.Add(b);
}
}
}

panel.Visibility = Visibility.Visible;
canvasOffscreen.SetValue(Grid.MarginProperty, new Thickness(this.ActualWidth, this.ActualHeight, -panel.DesiredSize.Width, -panel.DesiredSize.Height));
canvasOffscreen.Children.Add(panel);
canvasOffscreen.UpdateLayout();
panel.Measure(new Size(425, 1000));
panel.Arrange(new Rect(new Size(425, 425)));

Mouse.OverrideCursor = new GhostCursor(canvasOffscreen).Cursor;

canvasOffscreen.Children.Remove(panel);
panel.Visibility = Visibility.Collapsed;

DragDrop.DoDragDrop(listPhotos, new DataObject("test"), DragDropEffects.Link);
Mouse.OverrideCursor = null;

_isMouseDown = false;
}


I had a list of Photos, each which may have had a Thumbnail. If one was present, I added a new Border containing an ImageBrush of the Thumbnail to a WrapPanel. Then, I made the WrapPanel visible. I have a canvas that is always Visible, yet off-screen, creatively called canvasOffscreen. This allows me to place the WrapPanel full of images on the screen -- force a render, and snap the resulting visuals. Without that, I couldn't get the Visuals to render.


Next, I pass the canvasOffScreen (as a Visual) to a new instance of the GhostCursor class which returns a Cursor object that can be used by WPF. Finally, I remove all of the borders (as I've grabbed the Visual and made the cursor by this point) and hide the panel. In the code above, it does a fake DragDrop.


This all was started by this code:

        void listPhotos_MouseMove(object sender, MouseEventArgs e)
{
if (_isMouseDown && IsDragGesture(e.GetPosition(GetTopContainer())))
{
StartDrag();
}
}


private bool IsDragGesture(Point point)
{
bool hGesture = Math.Abs(point.X - _dragStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance;
bool vGesture = Math.Abs(point.Y - _dragStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance;

return (hGesture | vGesture);
}


This just starts a drag action if the mouse has moved sufficiently far while the mouse button was pressed. There's a bit more code necessary to wire it all up, but you get the idea.


There are quite a few hard-coded sizes in the GhostCursor class that you'll probably want to make into properties or parameters. Feel free.


 

        private BitmapSource CaptureScreen(Visual target)
{
Rect bounds = VisualTreeHelper.GetDescendantBounds(target);

RenderTargetBitmap renderBitmap = new RenderTargetBitmap(800, 600, 96, 96, PixelFormats.Pbgra32);

DrawingVisual dv = new DrawingVisual();
using (DrawingContext ctx = dv.RenderOpen())
{
VisualBrush vb = new VisualBrush(target);
LinearGradientBrush opacityMask = new LinearGradientBrush(Color.FromArgb(255, 1, 1, 1), Color.FromArgb(0, 1, 1, 1), 30);
ctx.PushOpacityMask(opacityMask);
ctx.DrawRectangle(vb, null, new Rect(new Point(), bounds.Size));
ctx.Pop();
}
renderBitmap.Render(dv);

return renderBitmap;
}


The above code shows how to grab a visual as a Brush, and apply a linear gradient brush to the Visual and then render it to a BitmapSource.


I had to use the CursorInteropHelper class to do a little magic (I'm very glad this class existed!):

        private void CreateCursor(int width, int height, BitmapHandle dibSectionHandle)
{
BitmapHandle monoBitmapHandle = null;
try
{
monoBitmapHandle = new BitmapHandle(CreateBitmap(width, height, 1, 1, IntPtr.Zero));

ICONINFO icon = new ICONINFO();
icon.IsIcon = false;
icon.xHotspot = 0;
icon.yHotspot = 0;
icon.ColorBitmap = dibSectionHandle;
icon.MaskBitmap = monoBitmapHandle;

_iconHandle = CreateIconIndirect(ref icon);
if (!_iconHandle.IsInvalid)
{
_ghostCursor = CursorInteropHelper.Create(_iconHandle);
}

}
finally
{
// destroy the temporary mono bitmap now ...
if (monoBitmapHandle != null)
{
monoBitmapHandle.Dispose();
}
}
}


That's all for now. Enjoy.

Help support my web site by searching and buying through Amazon.com (in assocation with Amazon.com).