Custom cursor in WPF?
我想在 WPF 应用程序中使用图像或图标作为自定义光标。我该怎么做?
你有两个基本的选择:
当鼠标光标在您的控件上时,通过设置
- http://www.xamlog.com/2006/07/17/creating-a-custom-cursor/
-
http://www.hanselman.com/blog/DeveloperDesigner.aspx
可以在此处找到其他示例: -
WPF 教程 - 如何使用自定义光标
- 将光标设置为在拖动时呈现一些文本
- 变得花哨并使用我们正在拖动的视觉反馈[而不是光标]
- 如何在数据绑定的 ItemsControls 之间拖放项目?
通过从 .cur 或 .ani 文件加载图像来创建新的 Cursor 对象。您可以在 Visual Studio 中创建和编辑这些类型的文件。还有一些免费的实用程序可以处理它们。基本上,它们是指定"热点"的图像(或动画图像),指示光标位于图像中的哪个点。
如果您选择从文件加载,请注意您需要一个绝对文件系统路径才能使用
另一方面,在使用 XAML 属性加载光标时将光标指定为相对路径确实有效,您可以使用这一事实将光标加载到隐藏控件上,然后复制引用以在另一个控件上使用。我没试过,但应该可以。
就像 Peter 提到的,如果您已经有一个 .cur 文件,您可以通过在资源部分创建一个虚拟元素,然后在需要时引用虚拟的光标,将其用作嵌入式资源。
例如,假设您想根据所选工具显示非标准光标。
添加到资源:
1 2 3 4 5 6 | <Window.Resources> <ResourceDictionary> <TextBlock x:Key="CursorGrab" Cursor="Resources/Cursors/grab.cur"/> <TextBlock x:Key="CursorMagnify" Cursor="Resources/Cursors/magnify.cur"/> </ResourceDictionary> </Window.Resources> |
代码中引用的嵌入式游标示例:
1 2 3 4 5 6 | if (selectedTool =="Hand") myCanvas.Cursor = ((TextBlock)this.Resources["CursorGrab"]).Cursor; else if (selectedTool =="Magnify") myCanvas.Cursor = ((TextBlock)this.Resources["CursorMagnify"]).Cursor; else myCanvas.Cursor = Cursor.Arrow; |
有一种比自己管理光标显示或使用 Visual Studio 构建大量自定义光标更简单的方法。
如果您有一个 FrameworkElement,您可以使用以下代码从中构造一个 Cursor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public Cursor ConvertToCursor(FrameworkElement visual, Point hotSpot) { int width = (int)visual.Width; int height = (int)visual.Height; // Render to a bitmap var bitmapSource = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32); bitmapSource.Render(visual); // Convert to System.Drawing.Bitmap var pixels = new int[width*height]; bitmapSource.CopyPixels(pixels, width, 0); var bitmap = new System.Drawing.Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb); for(int y=0; y<height; y++) for(int x=0; x<width; x++) bitmap.SetPixel(x, y, Color.FromArgb(pixels[y*width+x])); // Save to .ico format var stream = new MemoryStream(); System.Drawing.Icon.FromHandle(resultBitmap.GetHicon()).Save(stream); // Convert saved file into .cur format stream.Seek(2, SeekOrigin.Begin); stream.WriteByte(2); stream.Seek(10, SeekOrigin.Begin); stream.WriteByte((byte)(int)(hotSpot.X * width)); stream.WriteByte((byte)(int)(hotSpot.Y * height)); stream.Seek(0, SeekOrigin.Begin); // Construct Cursor return new Cursor(stream); } |
请注意,您的 FrameworkElement 的大小必须是标准光标大小(例如 16x16 或 32x32),例如:
1 2 3 | <Grid x:Name="customCursor" Width="32" Height="32"> ... </Grid> |
它会这样使用:
1 | someControl.Cursor = ConvertToCursor(customCursor, new Point(0.5, 0.5)); |
如果你有一个现有的图像,你的 FrameworkElement 显然可以是一个
请注意,有关 .cur 文件格式的详细信息可以在 ICO(文件格式)中找到。
为了在 XAML 中使用自定义光标,我稍微修改了 Ben McIntosh 提供的代码:
1 2 3 | <Window.Resources> <Cursor x:Key="OpenHandCursor">Resources/openhand.cur</Cursor> </Window.Resources> |
要使用光标,只需引用资源:
1 | <StackPanel Cursor="{StaticResource OpenHandCursor}" /> |
如果有人正在寻找 UIElement 本身作为光标,我结合了 Ray 和 Arcturus 的解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | public Cursor ConvertToCursor(UIElement control, Point hotSpot) { // convert FrameworkElement to PNG stream var pngStream = new MemoryStream(); control.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); Rect rect = new Rect(0, 0, control.DesiredSize.Width, control.DesiredSize.Height); RenderTargetBitmap rtb = new RenderTargetBitmap((int)control.DesiredSize.Width, (int)control.DesiredSize.Height, 96, 96, PixelFormats.Pbgra32); control.Arrange(rect); rtb.Render(control); PngBitmapEncoder png = new PngBitmapEncoder(); png.Frames.Add(BitmapFrame.Create(rtb)); png.Save(pngStream); // write cursor header info var cursorStream = new MemoryStream(); cursorStream.Write(new byte[2] { 0x00, 0x00 }, 0, 2); // ICONDIR: Reserved. Must always be 0. cursorStream.Write(new byte[2] { 0x02, 0x00 }, 0, 2); // ICONDIR: Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid cursorStream.Write(new byte[2] { 0x01, 0x00 }, 0, 2); // ICONDIR: Specifies number of images in the file. cursorStream.Write(new byte[1] { (byte)control.DesiredSize.Width }, 0, 1); // ICONDIRENTRY: Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels. cursorStream.Write(new byte[1] { (byte)control.DesiredSize.Height }, 0, 1); // ICONDIRENTRY: Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels. cursorStream.Write(new byte[1] { 0x00 }, 0, 1); // ICONDIRENTRY: Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette. cursorStream.Write(new byte[1] { 0x00 }, 0, 1); // ICONDIRENTRY: Reserved. Should be 0. cursorStream.Write(new byte[2] { (byte)hotSpot.X, 0x00 }, 0, 2); // ICONDIRENTRY: Specifies the horizontal coordinates of the hotspot in number of pixels from the left. cursorStream.Write(new byte[2] { (byte)hotSpot.Y, 0x00 }, 0, 2); // ICONDIRENTRY: Specifies the vertical coordinates of the hotspot in number of pixels from the top. cursorStream.Write(new byte[4] { // ICONDIRENTRY: Specifies the size of the image's data in bytes (byte)((pngStream.Length & 0x000000FF)), (byte)((pngStream.Length & 0x0000FF00) >> 0x08), (byte)((pngStream.Length & 0x00FF0000) >> 0x10), (byte)((pngStream.Length & 0xFF000000) >> 0x18) }, 0, 4); cursorStream.Write(new byte[4] { // ICONDIRENTRY: Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file (byte)0x16, (byte)0x00, (byte)0x00, (byte)0x00, }, 0, 4); // copy PNG stream to cursor stream pngStream.Seek(0, SeekOrigin.Begin); pngStream.CopyTo(cursorStream); // return cursor stream cursorStream.Seek(0, SeekOrigin.Begin); return new Cursor(cursorStream); } |
一种非常简单的方法是在 Visual Studio 中将光标创建为 .cur 文件,然后将其添加到项目资源中。
然后在要分配光标时添加以下代码:
1 | myCanvas.Cursor = new Cursor(new System.IO.MemoryStream(myNamespace.Properties.Resources.Cursor1)); |
另一种解决方案有点类似于 Ray 的解决方案,但不是缓慢而繁琐的像素复制,它使用了一些 Windows 内部结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | private struct IconInfo { public bool fIcon; public int xHotspot; public int yHotspot; public IntPtr hbmMask; public IntPtr hbmColor; } [DllImport("user32.dll")] private static extern IntPtr CreateIconIndirect(ref IconInfo icon); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetIconInfo(IntPtr hIcon, ref IconInfo pIconInfo); public Cursor ConvertToCursor(FrameworkElement cursor, Point HotSpot) { cursor.Arrange(new Rect(new Size(cursor.Width, cursor.Height))); var bitmap = new RenderTargetBitmap((int)cursor.Width, (int)cursor.Height, 96, 96, PixelFormats.Pbgra32); bitmap.Render(cursor); var info = new IconInfo(); GetIconInfo(bitmap.ToBitmap().GetHicon(), ref info); info.fIcon = false; info.xHotspot = (byte)(HotSpot.X * cursor.Width); info.yHotspot = (byte)(HotSpot.Y * cursor.Height); return CursorInteropHelper.Create(new SafeFileHandle(CreateIconIndirect(ref info), true)); } |
中间有一个扩展方法,我更喜欢在扩展类中使用这种情况:
1 2 3 4 5 6 7 8 9 | using DW = System.Drawing; public static DW.Bitmap ToBitmap(this BitmapSource bitmapSource) { var bitmap = new DW.Bitmap(bitmapSource.PixelWidth, bitmapSource.PixelHeight, DW.Imaging.PixelFormat.Format32bppPArgb); var data = bitmap.LockBits(new DW.Rectangle(DW.Point.Empty, bitmap.Size), DW.Imaging.ImageLockMode.WriteOnly, DW.Imaging.PixelFormat.Format32bppPArgb); bitmapSource.CopyPixels(Int32Rect.Empty, data.Scan0, data.Height * data.Stride, data.Stride); bitmap.UnlockBits(data); return bitmap; } |
有了这一切,它就相当简单明了。
而且,如果您碰巧不需要指定自己的热点,您甚至可以将其缩短(您也不需要结构或 P/Invokes):
1 2 3 4 5 6 7 | public Cursor ConvertToCursor(FrameworkElement cursor, Point HotSpot) { cursor.Arrange(new Rect(new Size(cursor.Width, cursor.Height))); var bitmap = new RenderTargetBitmap((int)cursor.Width, (int)cursor.Height, 96, 96, PixelFormats.Pbgra32); bitmap.Render(cursor); var icon = System.Drawing.Icon.FromHandle(bitmap.ToBitmap().GetHicon()); return CursorInteropHelper.Create(new SafeFileHandle(icon.Handle, true)); } |
我想从项目资源中加载自定义光标文件,但遇到了类似问题。我在互联网上搜索了一个解决方案,但没有找到我需要的:在运行时将
我试过 Ben 的 xaml 解决方案,但觉得它不够优雅。
彼得艾伦说:
Lamely, a relative path or Pack URI will not work. If you need to load the cursor from a relative path or from a resource packed with your assembly, you will need to get a stream from the file and pass it in to the Cursor(Stream cursorStream) constructor. Annoying but true.
我偶然发现了一个很好的方法来解决我的问题:
1 2 3 4 5 | System.Windows.Resources.StreamResourceInfo info = Application.GetResourceStream(new Uri("/MainApp;component/Resources/HandDown.cur", UriKind.Relative)); this.Cursor = new System.Windows.Input.Cursor(info.Stream); |
你可以通过像
这样的代码来做到这一点
1 | this.Cursor = new Cursor(@"<your address of icon>"); |
你可以试试这个
1 | <Window Cursor=""C:\\WINDOWS\\Cursors\\dinosaur.ani"" /> |
它可能在 Visual Studio 2017 中发生了变化,但我能够将 .cur 文件作为嵌入式资源引用:
1 2 3 | <Setter Property="Cursor" Value="/assembly-name;component/location-name/curser-name.cur" /> |
如果你使用的是visual studio,你可以
确保所有 GDI 资源(例如 bmp.GetHIcon)都被释放。否则你最终会出现内存泄漏。以下代码(图标的扩展方法)非常适用于 WPF。它会在右下角创建一个带有小图标的箭头光标。
备注:此代码使用图标来创建光标。它不使用当前的 UI 控件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | public static Cursor CreateCursor(this Icon icon, bool includeCrossHair, System.Drawing.Color crossHairColor) { if (icon == null) return Cursors.Arrow; // create an empty image int width = icon.Width; int height = icon.Height; using (var cursor = new Bitmap(width * 2, height * 2)) { // create a graphics context, so that we can draw our own cursor using (var gr = System.Drawing.Graphics.FromImage(cursor)) { // a cursor is usually 32x32 pixel so we need our icon in the lower right part of it gr.DrawIcon(icon, new Rectangle(width, height, width, height)); if (includeCrossHair) { using (var pen = new System.Drawing.Pen(crossHairColor)) { // draw the cross-hair gr.DrawLine(pen, width - 3, height, width + 3, height); gr.DrawLine(pen, width, height - 3, width, height + 3); } } } try { using (var stream = new MemoryStream()) { // Save to .ico format var ptr = cursor.GetHicon(); var tempIcon = Icon.FromHandle(ptr); tempIcon.Save(stream); int x = cursor.Width/2; int y = cursor.Height/2; #region Convert saved stream into .cur format // set as .cur file format stream.Seek(2, SeekOrigin.Begin); stream.WriteByte(2); // write the hotspot information stream.Seek(10, SeekOrigin.Begin); stream.WriteByte((byte)(width)); stream.Seek(12, SeekOrigin.Begin); stream.WriteByte((byte)(height)); // reset to initial position stream.Seek(0, SeekOrigin.Begin); #endregion DestroyIcon(tempIcon.Handle); // destroy GDI resource return new Cursor(stream); } } catch (Exception) { return Cursors.Arrow; } } } /// <summary> /// Destroys the icon. /// </summary> /// <param name="handle">The handle.</param> /// <returns></returns> [DllImport("user32.dll", CharSet = CharSet.Auto)] public extern static Boolean DestroyIcon(IntPtr handle); |
还可以查看 Scott Hanselman 的 BabySmash (www.codeplex.com/babysmash)。他使用了一种更"蛮力"的方法来隐藏窗口光标并在画布上显示他的新光标,然后将光标移动到"真正的"光标应该是
在这里阅读更多:
http://www.hanselman.com/blog/DeveloperDesigner.aspx
这将使用附加属性将存储在项目中的任何图像转换为光标。图片必须编译为资源!
例子
1 | <Button MyLibrary:FrameworkElementExtensions.Cursor=""{MyLibrary:Uri MyAssembly, MyImageFolder/MyImage.png}""/> |
FrameworkElementExtensions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using System; using System.Windows; using System.Windows.Media; public static class FrameworkElementExtensions { #region Cursor public static readonly DependencyProperty CursorProperty = DependencyProperty.RegisterAttached("Cursor", typeof(Uri), typeof(FrameworkElementExtensions), new UIPropertyMetadata(default(Uri), OnCursorChanged)); public static Uri GetCursor(FrameworkElement i) => (Uri)i.GetValue(CursorProperty); public static void SetCursor(FrameworkElement i, Uri input) => i.SetValue(CursorProperty, input); static void OnCursorChanged(object sender, DependencyPropertyChangedEventArgs e) { if (sender is FrameworkElement frameworkElement) { if (GetCursor(frameworkElement) != null) frameworkElement.Cursor = new ImageSourceConverter().ConvertFromString(((Uri)e.NewValue).OriginalString).As<ImageSource>().Bitmap().Cursor(0, 0).Convert(); } } #endregion } |
ImageSourceExtensions
1 2 3 4 5 6 7 8 | using System.Drawing; using System.Windows.Media; using System.Windows.Media.Imaging; public static class ImageSourceExtensions { public static Bitmap Bitmap(this ImageSource input) => input.As<BitmapSource>().Bitmap(); } |
BitmapSourceExtensions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System.IO; using System.Windows.Media.Imaging; public static class BitmapSourceExtensions { public static System.Drawing.Bitmap Bitmap(this BitmapSource input) { if (input == null) return null; System.Drawing.Bitmap result; using (var outStream = new MemoryStream()) { var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(input)); encoder.Save(outStream); result = new System.Drawing.Bitmap(outStream); } return result; } } |
位图扩展
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | using System; using System.Drawing; using System.Runtime.InteropServices; public static class BitmapExtensions { [StructLayout(LayoutKind.Sequential)] public struct ICONINFO { /// <summary> /// Specifies whether this structure defines an icon or a cursor. A value of TRUE specifies an icon; FALSE specifies a cursor. /// </summary> public bool fIcon; /// <summary> /// Specifies the x-coordinate of a cursor's hot spot. If this structure defines an icon, the hot spot is always in the center of the icon, and this member is ignored. /// </summary> public Int32 xHotspot; /// <summary> /// Specifies the y-coordinate of the cursor's hot spot. If this structure defines an icon, the hot spot is always in the center of the icon, and this member is ignored. /// </summary> public Int32 yHotspot; /// <summary> /// (HBITMAP) Specifies the icon bitmask bitmap. If this structure defines a black and white icon, this bitmask is formatted so that the upper half is the icon AND bitmask and the lower half is the icon XOR bitmask. Under this condition, the height should be an even multiple of two. If this structure defines a color icon, this mask only defines the AND bitmask of the icon. /// </summary> public IntPtr hbmMask; /// <summary> /// (HBITMAP) Handle to the icon color bitmap. This member can be optional if this structure defines a black and white icon. The AND bitmask of hbmMask is applied with the SRCAND flag to the destination; subsequently, the color bitmap is applied (using XOR) to the destination by using the SRCINVERT flag. /// </summary> public IntPtr hbmColor; } [DllImport("user32.dll")] static extern IntPtr CreateIconIndirect([In] ref ICONINFO piconinfo); [DllImport("user32.dll")] static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo); [DllImport("user32.dll", SetLastError = true)] public static extern bool DestroyIcon(IntPtr hIcon); public static System.Windows.Forms.Cursor Cursor(this Bitmap input, int hotX, int hotY) { ICONINFO Info = new ICONINFO(); IntPtr Handle = input.GetHicon(); GetIconInfo(Handle, out Info); Info.xHotspot = hotX; Info.yHotspot = hotY; Info.fIcon = false; IntPtr h = CreateIconIndirect(ref Info); return new System.Windows.Forms.Cursor(h); } } |
光标扩展
1 2 3 4 5 6 7 8 9 10 | using Microsoft.Win32.SafeHandles; public static class CursorExtensions { public static System.Windows.Input.Cursor Convert(this System.Windows.Forms.Cursor Cursor) { SafeFileHandle h = new SafeFileHandle(Cursor.Handle, false); return System.Windows.Interop.CursorInteropHelper.Create(h); } } |
作为
1 | public static Type As<Type>(this object input) => input is Type ? (Type)input : default; |
乌里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System; using System.Windows.Markup; public class Uri : MarkupExtension { public string Assembly { get; set; } = null; public string RelativePath { get; set; } public Uri(string relativePath) : base() { RelativePath = relativePath; } public Uri(string assembly, string relativePath) : this(relativePath) { Assembly = assembly; } static Uri Get(string assemblyName, string relativePath) => new Uri($"pack://application:,,,/{assemblyName};component/{relativePath}", UriKind.Absolute); public override object ProvideValue(IServiceProvider serviceProvider) { if (Assembly == null) return new System.Uri(RelativePath, UriKind.Relative); return Get(Assembly, RelativePath); } } |