我想在 WPF 应用程序中使用图像或图标作为自定义光标。我怎样才能做到这一点?
15 回答
您有两个基本选择:
当鼠标光标在您的控件上时,通过设置隐藏系统光标
this.Cursor = Cursors.None;
并使用您喜欢的任何技术绘制您自己的光标。然后,通过响应鼠标事件来更新光标的位置和外观。这里有两个例子:通过从 .cur 或 .ani 文件加载图像来创建新的 Cursor 对象。您可以在 Visual Studio 中创建和编辑这些类型的文件。还有一些免费的实用程序可以处理它们。基本上,它们是指定“热点”的图像(或动画图像),指示光标位于图像中的哪个点。
如果您选择从文件加载,请注意您需要一个绝对文件系统路径才能使用Cursor(string fileName)
构造函数。Lamely,相对路径或 Pack URI 将不起作用。如果您需要从相对路径或与程序集一起打包的资源加载光标,则需要从文件中获取流并将其传递给Cursor(Stream cursorStream)
构造函数。烦人但真实。
另一方面,在使用 XAML 属性加载光标时将光标指定为相对路径确实有效,您可以使用这一事实将光标加载到隐藏控件上,然后复制引用以在另一个控件上使用。我没有尝试过,但它应该可以工作。
就像Peter 提到的那样,如果您已经有一个 .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;
有一种比自己管理光标显示或使用 Visual Studio 构建大量自定义光标更简单的方法。
如果您有一个 FrameworkElement,您可以使用以下代码从中构造一个 Cursor:
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(文件格式)中找到。
为了在 XAML 中使用自定义光标,我稍微修改了Ben McIntosh 提供的代码:
<Window.Resources>
<Cursor x:Key="OpenHandCursor">Resources/openhand.cur</Cursor>
</Window.Resources>
要使用游标,只需引用资源:
<StackPanel Cursor="{StaticResource OpenHandCursor}" />
如果有人正在寻找 UIElement 本身作为光标,我结合了Ray和Arcturus的解决方案:
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 文件,然后将其添加到项目资源中。
然后在要分配光标时添加以下代码:
myCanvas.Cursor = new Cursor(new System.IO.MemoryStream(myNamespace.Properties.Resources.Cursor1));
我想从项目资源中加载一个自定义光标文件并遇到了类似的问题。我在互联网上搜索了一个解决方案,但没有找到我需要的:this.Cursor
在运行时将 设置为存储在我的项目的资源文件夹中的自定义光标。我试过 Ben 的 xaml 解决方案,但觉得它不够优雅。彼得艾伦说:
Lamely,相对路径或 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 文件的相对文件夹路径。
另一种解决方案有点类似于 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));
}
你可以试试这个
<Window Cursor=""C:\WINDOWS\Cursors\dinosaur.ani"" />
你可以通过像这样的代码来做到这一点
this.Cursor = new Cursor(@"<your address of icon>");
还可以查看 Scott Hanselman 的 BabySmash (www.codeplex.com/babysmash)。他使用了一种更“蛮力”的方法来隐藏 windows 光标并在画布上显示他的新光标,然后将光标移动到“真正的”光标
在这里阅读更多: http ://www.hanselman.com/blog/DeveloperDesigner.aspx
确保所有 GDI 资源(例如 bmp.GetHIcon)都被释放。否则你最终会出现内存泄漏。以下代码(图标的扩展方法)非常适用于 WPF。它会在右下角创建一个带有小图标的箭头光标。
备注:此代码使用图标来创建光标。它不使用当前的 UI 控件。
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);
如果您使用的是视觉工作室,您可以
- 新建一个游标文件
- 复制/粘贴图像
- 将其保存到 .cur 文件。
它可能已随 Visual Studio 2017 更改,但我能够将 .cur 文件引用为嵌入式资源:
<Setter
Property="Cursor"
Value="/assembly-name;component/location-name/curser-name.cur" />
这将使用附加属性将存储在项目中的任何图像转换为光标。图像必须编译为资源!
例子
<Button MyLibrary:FrameworkElementExtensions.Cursor=""{MyLibrary:Uri MyAssembly, MyImageFolder/MyImage.png}""/>
框架元素扩展
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
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
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;
}
}
位图扩展
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);
}
}
光标扩展
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);
}
}
作为
public static Type As<Type>(this object input) => input is Type ? (Type)input : default;
乌里
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);
}
}