You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows/WPF应用级鼠标光标叠加层实现方案咨询

解决方案

针对你的WPF应用需求,这里有两个简便且无闪烁的实现思路:

方案一:预合成自定义光标(无定时器,避免闪烁)

核心思路是一次性合成当前操作光标与沙漏光标,后台任务启动时直接切换到合成光标,任务结束后切回原光标,完全不需要定时器循环更新。

实现步骤:

  1. 提前准备好沙漏光标资源(可以用系统自带的Cursors.Wait,也可以自定义)。
  2. 编写光标合成方法,将原光标与沙漏光标合并为一张位图,并保留原光标的热点(Hotspot)——这是避免光标行为异常的关键。
  3. 后台任务启动时调用合成方法生成新光标并设置到当前控件/窗口;任务结束后恢复原光标。

代码示例:

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Runtime.InteropServices;
using System.Drawing;

public static Cursor CombineCursors(Cursor baseCursor, Cursor overlayCursor)
{
    // 获取原光标和叠加光标的位图
    var baseBitmap = GetCursorBitmap(baseCursor);
    var overlayBitmap = GetCursorBitmap(overlayCursor);

    // 创建合成位图,尺寸取原光标大小
    var combinedBitmap = new RenderTargetBitmap(baseBitmap.PixelWidth, baseBitmap.PixelHeight, 96, 96, PixelFormats.Pbgra32);
    var drawingVisual = new DrawingVisual();

    using (var dc = drawingVisual.RenderOpen())
    {
        // 绘制原光标
        dc.DrawImage(baseBitmap, new Rect(0, 0, baseBitmap.PixelWidth, baseBitmap.PixelHeight));
        // 绘制沙漏到右下角(可调整位置)
        dc.DrawImage(overlayBitmap, new Rect(baseBitmap.PixelWidth - overlayBitmap.PixelWidth, baseBitmap.PixelHeight - overlayBitmap.PixelHeight, overlayBitmap.PixelWidth, overlayBitmap.PixelHeight));
    }

    combinedBitmap.Render(drawingVisual);

    // 将合成位图转为Cursor,保留原光标热点
    return BitmapToCursor(combinedBitmap, baseCursor.HotSpot);
}

// 辅助方法:将Cursor转为BitmapSource
private static BitmapSource GetCursorBitmap(Cursor cursor)
{
    var cursorHandle = cursor.Handle;
    var info = new ICONINFO();
    GetIconInfo(cursorHandle, ref info);

    var bitmap = Bitmap.FromHbitmap(info.hbmColor);
    var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());

    DeleteObject(info.hbmColor);
    DeleteObject(info.hbmMask);
    bitmap.Dispose();

    return bitmapSource;
}

// 辅助方法:将BitmapSource转为Cursor
private static Cursor BitmapToCursor(BitmapSource bitmapSource, Point hotSpot)
{
    var bitmap = new Bitmap(bitmapSource.PixelWidth, bitmapSource.PixelHeight);
    var graphics = Graphics.FromImage(bitmap);
    var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    bitmapSource.CopyPixels(Int32Rect.Empty, bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride);
    bitmap.UnlockBits(bitmapData);
    graphics.Dispose();

    var iconInfo = new ICONINFO
    {
        fIcon = false,
        xHotspot = (int)hotSpot.X,
        yHotspot = (int)hotSpot.Y,
        hbmMask = bitmap.GetHbitmap(),
        hbmColor = bitmap.GetHbitmap()
    };

    var cursorHandle = CreateIconIndirect(ref iconInfo);
    DeleteObject(iconInfo.hbmMask);
    DeleteObject(iconInfo.hbmColor);
    bitmap.Dispose();

    return CursorInteropHelper.Create(new IntPtr(cursorHandle));
}

// Windows API声明
[DllImport("user32.dll")]
private static extern bool GetIconInfo(IntPtr hIcon, ref ICONINFO pIconInfo);

[DllImport("user32.dll")]
private static extern IntPtr CreateIconIndirect(ref ICONINFO pIconInfo);

[DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);

[StructLayout(LayoutKind.Sequential)]
private struct ICONINFO
{
    public bool fIcon;
    public int xHotspot;
    public int yHotspot;
    public IntPtr hbmMask;
    public IntPtr hbmColor;
}

使用方式:

// 后台任务启动前
var originalCursor = this.Cursor;
var busyCursor = CombineCursors(originalCursor, Cursors.Wait);
this.Cursor = busyCursor;

// 后台任务结束后
this.Cursor = originalCursor;

方案二:用Adorner层显示沙漏(无需修改光标,最简便)

如果不想折腾光标合成,可以直接在WPF的Adorner装饰层上显示一个跟随鼠标的沙漏图标,完全不影响原光标正常工作,也不会有闪烁问题。

实现步骤:

  1. 创建继承自Adorner的类,内部包含沙漏图标(比如用Image控件)。
  2. 监听窗口的MouseMove事件,实时更新Adorner的位置,让沙漏跟随鼠标。
  3. 后台任务启动时将Adorner添加到Adorner层,任务结束时移除。

代码示例:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

public class BusyCursorAdorner : Adorner
{
    private readonly Image _hourglassImage;

    public BusyCursorAdorner(UIElement adornedElement) : base(adornedElement)
    {
        // 加载沙漏图标(替换为你的图标路径,可使用系统资源或自定义图片)
        _hourglassImage = new Image
        {
            Width = 16,
            Height = 16,
            Source = new BitmapImage(new Uri("pack://application:,,,/Resources/Hourglass.png"))
        };

        // 设置Adorner为透明,不阻挡鼠标操作
        IsHitTestVisible = false;
    }

    protected override int VisualChildrenCount => 1;

    protected override Visual GetVisualChild(int index)
    {
        return _hourglassImage;
    }

    protected override Size MeasureOverride(Size constraint)
    {
        _hourglassImage.Measure(constraint);
        return _hourglassImage.DesiredSize;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        _hourglassImage.Arrange(new Rect(finalSize));
        return finalSize;
    }

    // 更新沙漏位置(跟随鼠标)
    public void UpdatePosition(Point mousePosition)
    {
        // 调整位置,让沙漏显示在鼠标右下角
        var offset = new Vector(18, 18);
        var newPosition = mousePosition + offset;
        Canvas.SetLeft(this, newPosition.X);
        Canvas.SetTop(this, newPosition.Y);
    }
}

使用方式:

private BusyCursorAdorner _busyAdorner;

// 后台任务启动时
private void StartBackgroundTask()
{
    var adornerLayer = AdornerLayer.GetAdornerLayer(this.MainGrid); // 替换为你的根容器
    _busyAdorner = new BusyCursorAdorner(this.MainGrid);
    adornerLayer.Add(_busyAdorner);

    // 监听鼠标移动事件
    this.MouseMove += OnMouseMove;

    // 启动后台任务
    Task.Run(() =>
    {
        // 你的复杂运算逻辑
        Thread.Sleep(5000); // 模拟耗时操作

        // 任务结束后回到UI线程移除Adorner
        Dispatcher.Invoke(() =>
        {
            adornerLayer.Remove(_busyAdorner);
            this.MouseMove -= OnMouseMove;
        });
    });
}

private void OnMouseMove(object sender, MouseEventArgs e)
{
    _busyAdorner?.UpdatePosition(e.GetPosition(this));
}

方案对比

  • 方案一:完全通过光标合成实现,视觉上是一个整体光标,适合追求原生光标体验的场景,但需要处理不同光标尺寸、热点的兼容性。
  • 方案二:实现简单,无需修改光标逻辑,灵活度高(可自定义沙漏样式、位置),几乎没有兼容性问题,推荐优先尝试。

内容的提问来源于stack exchange,提问作者msedi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 22:31:57