« June 2007 | Main | August 2007 »

July 23, 2007

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.

July 4, 2007

Distract your WPF application users with a splash screen!

If you've written any WPF applications -- I bet you've had these feelings:

  • Worry: How patient will your users be? The startup time for your application is much longer than you'd like. (Do you hate the start up time for a WPF application as much as me? )
  • Pain: Annoyed by the long PAUSE that happens the first time you start/launch your WPF application.
  • Concern: How many times will the user double-click your application icon before your application shows up? Will they wonder if they actually started the application only to start several copies after a series of double-clicks?
  • Stressed: You've just bought a new computer with the fastest processor, the fastest disk, and more RAM than is rational, and you convinced your financial officer or IT shop (in my case, it might be my wife!) that this computer would be so fast at everything it did that you can't imagine needing a new computer for many many years! Everything else performs awesome -- just these little WPF applications launch so slowly .........

If you've had those or similar feelings, then what you are in need of is a splash screen!

(Seriously, first try to determine if there's ways of loading your application faster: like minimizing the number and size of assemblies that need to be loaded initially; reduce the amount of JITing that must be done -- less code is often better; use a profiler to find hot spots, use a two stage application launch (a mini-loader followed by the real application); and etc. A splash screen at least makes users feel like something is happening. They received a response back from the system -- it acknowledged their actions and is working.)

One of the typical ways of adding a splash screen would be to add a Window to your project and show it before the main window shows -- you'd probably leave the border off, consider using transparency, it would look awesome. That would work. HOWEVER, it will still take too long to show up -- as there's just too much startup needed by a WPF application before that would be made visible (and the fancier it is, the slower it will load). So, the solution is to build something that relies very little on .NET -- so that it can show BEFORE most of the WPF assemblies and .NET assemblies are loaded and needed.

It's not that difficult to do if you're familiar with Windows 32 programming. I'm talking bare to the metal Win32 programming -- not even the thin wrapper than is offered by Windows Forms or any of the associated Drawing assemblies that are commonly used in a WinForm application: that's too much bloat.

So, what does that mean? It means that this splash screen shows up FAST. If your main EXE is small (maybe just a shim for launching the real application), then it will be even faster!

In the attached ZIP file, I've included the assembly you'll need, a demo project, and a project that is used to create the necessary DLL containing the Bitmap. When I said bare to the metal, I meant it. My splash screen component requires a Native Windows 32 bitmap resource to be made available. You can't use a .NET assembly resource as those are just too slow to load.

Also included is a sample Bitmap that I'm sure you'll want to replace:

image

The bitmap can be of any size (although splash screens typically aren't very large... I like to keep them about 400x250 usually).

There are a few differences about a WPF application that uses this component. First -- you can't allow the default project to create the main window for you automatically.

namespace Demo
{
  public class App : System.Windows.Application
  {
    [STAThreadAttribute()]
    public static void Main()
    {
      Splashing splasher = new Splashing("TheSplash.dll", typeof(App).Module, 101);
      splasher.Show();
      App app = new App();
      app.Run(new Window1(splasher));
    }
  }
}

I've removed the App.xaml and default App.xaml.cs files in my project completely and replaced them with a single App.cs file. In this file, I create an instance of the Splasher component, show it, and then Run the application with a new instance of my main window, Window1. I'm passing the instance of the Splasher to Window1 so the splash screen can be hidden when Window1 has shown (or has progressed far enough to warrant closing).

If you have resources in App.xaml, then you might want to move them to a new file(name), and be sure that the Class reference is removed as well from the xml header. Then add code to the Main method above (before the run and after the App instance is created):

    ResourceDictionary dictionary = new ResourceDictionary();
    dictionary.Source = new Uri("/GlobalResources.xaml", UriKind.Relative);
    app.Resources.MergedDictionaries.Add(dictionary);

Of course, adjust this based on your naming conventions and needs.

The parameters to the Splashing class are:

  • Resource file name: The name of the DLL containing the native Win32 resource Bitmap you want to load. It needs to be in the current directory or use a relative path.
  • Module: Used internally - just pass it the module from the Application object -- that's what it really wants. :)
  • Resource ID: Win32 resources are all assigned integer IDs. This number needs to match the resource ID assigned to your splash screen bitmap. VC++ starts at 101 -- in my sample project, it's the first resource, and it's ID is 101.

You'll need to close/hide the splash screen of course. I use the event IsVisibleChanged in the demo as a signal to hide the splash screen:

    void Window1_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
      if (_splasher != null)
      {
        Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, 
new ArgsDelegate0(delegate() { _splasher.Close(this); _splasher.Dispose(); _splasher = null; })); } }

The Close method optionally takes a Window object -- it pops the Window to the front (otherwise you may find that the application when run outside of Visual Studio will not retain it's z-order properly). It's bizarre when you leave that out!

What I haven't done in the demo is attach to the various global/local exception handlers to make sure the splash screen goes away properly. Nothing more embarrassing than a splash screen covering an exception dialog (not that you should have that happen ... but ....).

I've included in the zip file the VC++ project needed to build the resource DLL. If you don't have VC++ ... hmmm. I'm not sure what you should do since I don't have access easily to a system that only has, for example, Visual C# Express installed, on it to know what alternatives you might have. There may be some free options on the web that are easy enough. All you need to do is replace the embedded bitmap with an image of your own. In my sample project the Bitmap is called Splash.bmp. As I mentioned before, it can be any size you want. The side benefit of the bitmap living in a secondary DLL is that it can be unloaded and not continue to consume resources like .NET assemblies containing a Bitmap resource would typically do -- once loaded, always loaded.

image

Once built, just copy it to your output folder. There's no real need to include it in your solution as it's not changing.

If persuaded sufficiently, I might add the ability to add changeable text to the splash screen. :)

By clicking on this download link and using the component, you agree to the following terms:

Copyright (c) 2007, WiredPrairie.Us

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the name of the WiredPrairie.Us nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

If you use it in a project, please write and tell me (coder @ this web site).

I agree, now download it.

Comments or questions, etc. - email me or leave a comment. Enjoy.

Technorati tags: , , , ,

Only humans allowed ... how unfair!

"The content of message is intended to be understood by humans."

image

From the Intellisense documentation for FileNotFoundException in .NET 2.0.

 

From a usability perspective, not sure I'd want to see the text of any exception leak to the end user. Certainly not very often. Even if I wrote an end to end application, I'd handle the exception and display it in a more friendly easy to digest manner (and even better, try to prevent it from ever becoming a problem).

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