WPF中的自定义光标?

57

我想在WPF应用程序中使用图像或图标作为自定义光标。 我该怎么做?

15个回答

39

您有两个基本选项:

  1. 当鼠标光标位于您的控件上方时,通过设置 this.Cursor = Cursors.None; 隐藏系统光标,并使用任何您喜欢的技术绘制自己的光标。然后,通过响应鼠标事件更新光标的位置和外观。以下是两个示例:
  1. 通过从.cur或.ani文件中加载图像创建一个新的光标对象。这些类型的文件可以在Visual Studio中创建和编辑。还有一些免费的实用工具可用于处理它们。基本上,它们是指定光标在图像中所处位置的图像(或动画图像)。

如果您选择从文件加载,则需要绝对的文件系统路径才能使用Cursor(string fileName)构造函数。遗憾的是,相对路径或打包URI不起作用。如果您需要从相对路径或从与程序集打包的资源中加载光标,则需要从文件获取流并将其传递给Cursor(Stream cursorStream)构造函数。非常烦人但却是真实存在的问题。

另一方面,在使用XAML属性加载光标时指定相对路径确实有效,您可以利用这一点将光标加载到隐藏的控件上,然后复制引用以在其他控件上使用。我没有尝试过,但应该可以行得通。


2
还要注意,您可以从任何WPF内容动态构建光标。请参见https://dev59.com/0nE85IYBdhLWcg3wUByz#2836904,了解如何执行此操作的示例。 - Ray Burns
我在之前的评论中发布的链接涉及旋转现有光标。我刚刚发布了一个新答案来回答这个问题(请参见下文),告诉你如何将任意Visual转换为Cursor。 - Ray Burns

35

就像彼得所提到的,如果你已经有了一个 .cur 文件,那么你可以通过在资源部分创建一个虚拟元素并引用虚拟元素的指针来将其用作嵌入式资源。

例如,假设你想要根据所选工具显示非标准光标。

将以下内容添加到资源中:

<Window.Resources>
    <ResourceDictionary>
        <TextBlock x:Key="CursorGrab" Cursor="Resources/Cursors/grab.cur"/>
        <TextBlock x:Key="CursorMagnify" Cursor="Resources/Cursors/magnify.cur"/>
    </ResourceDictionary>
</Window.Resources>

嵌入式光标在代码中的示例:

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;

3
为什么你在定义Cursor属性的FrameworkElement中,使用TextBlock来缓存光标引用?是否有任何原因? - PaulJ
4
没有特别的原因,使用FrameworkElement会是一个更好的选择。谢谢! - Ben McIntosh
如何在XAML中使用它?例如:<Button Content="测试" Cursor=???? /> - Mohammad Ahmadzadeh

17

有一种比自己管理光标显示或使用Visual Studio构建许多自定义光标更容易的方法。

如果您有FrameworkElement,可以使用以下代码从中构造一个光标:

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),例如:
<Grid x:Name="customCursor" Width="32" Height="32">
  ...
</Grid>

使用方法如下:

someControl.Cursor = ConvertToCursor(customCursor, new Point(0.5, 0.5));

如果你有现成的图片,你的 FrameworkElement 可能是一个 <Image> 控件;或者使用 WPF 内置的绘图工具绘制任何你想要的内容。

请注意,关于 .cur 文件格式的详细信息可以在 ICO (file format) 找到。


3
嘿,我尝试使用这段代码来定义一个带有XAML的自定义光标。不幸的是,它仅显示空白,而不是我定义的<Image />元素。在调试代码时,我意识到在运行CopyPixels()方法后,var pixels数组中每个像素都只包含0。我对CopyPixels()方法中的stride参数出现错误,所以根据我找到的其他代码片段做了一些更改: int stride = width * ((bitmapSource.Format.BitsPerPixel + 7) / 8); 除此之外,代码看起来与上面的相同。visual是:<Image Height="32" Width="32"/> - andineupert

14

要在XAML中使用自定义光标,我稍微修改了Ben McIntosh提供的代码

<Window.Resources>    
 <Cursor x:Key="OpenHandCursor">Resources/openhand.cur</Cursor>
</Window.Resources>

使用光标只需引用资源:

<StackPanel Cursor="{StaticResource OpenHandCursor}" />

使用光标资源而不是框架元素“dummy”更有意义。 - DiamondDrake
IDE提示我找不到资源xxx.cur,这是一个bug吗? - huang

12

如果有人正在寻找UIElement本身作为光标,我结合了RayArcturus的解决方案:

    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);
    }

我会在你的流周围使用“using”语句来清理它,但除此之外,我对这种方法没有任何问题(不像其他实现方式)。 - outbred
我注意到在控件上调用Arrange会导致ListBoxItems和TreeViewItems瞬间消失,只有在引起它们父布局改变后(例如展开TreeViewItem)才会重新出现。你知道这是为什么吗? - user1618054

9

一种非常简单的方法是在Visual Studio中创建一个 .cur 文件作为光标,然后将其添加到项目资源中。

接下来,在需要指定光标时,只需添加以下代码:

myCanvas.Cursor = new Cursor(new System.IO.MemoryStream(myNamespace.Properties.Resources.Cursor1));

9
我想从项目资源中加载自定义光标文件,但遇到了类似的问题。我在互联网上搜索解决方案,但没有找到我需要的:在运行时将this.Cursor设置为存储在我的项目资源文件夹中的自定义光标。我尝试了Ben的xaml解决方案,但觉得不够优雅。PeterAllen指出:

很遗憾,相对路径或Pack URI都行不通。如果您需要从相对路径或与您的程序集一起打包的资源中加载光标,则需要从文件中获取流并将其传递给Cursor(Stream cursorStream)构造函数。非常烦人,但却是真实的。

我偶然发现了一个不错的方法来解决这个问题:
    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); 

MainApp应该替换为您的应用程序名称。Resources应该替换为项目内*.cur文件的相对文件夹路径。


"MainApp" 应该被替换为您的应用程序的名称。 "Resources" 应该被替换为项目内包含 *.cur 文件的相对文件夹路径。 - Mark Miller

8

还有一个解决方案与Ray的解决方案有点相似,但它不是使用缓慢而繁琐的像素复制,而是利用了一些Windows内部机制:

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));
}

在这种情况下,我更喜欢将中间的扩展方法放在一个扩展类中:

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):

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));
}

2
这个方法很好用(可以从任何我想要的WPF可视化对象创建光标),但是每当关联的对象被销毁时,使用此方法创建的光标的dtor中都会出现SEH异常。唯一不出现异常的方法是创建光标的单例并在任何地方重复使用它。您知道会导致SEH异常的任何原因吗?我可以猜测一整天,但实际上似乎是用于创建光标图像的对象被处理掉了,而Cursor类因此崩溃了。 - outbred
一个很好的例子,但是有一个bug,即info.yHotspot = (byte)(HotSpot.X * cursor.Height);(应该是HotSpot.Y而不是HotSpot.X)。这个例子还通过缩放源位图的尺寸来改变原始热点代码的范围,因此在指定偏移量时请记住这一点。 - Mark Feldman

2
您可以通过类似以下的代码来实现此功能:
this.Cursor = new Cursor(@"<your address of icon>");

2
你可以尝试这个。
<Window Cursor=""C:\WINDOWS\Cursors\dinosaur.ani"" />

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接