C# - 捕捉鼠标光标图片

32

背景

问题描述

  • 当鼠标指针是普通指针或手型图标时,代码可以正常工作 - 鼠标在屏幕截图中呈现为正确的样子。
  • 然而,当鼠标指针变为插入点("I-beam" 光标)时 - 例如在 NOTEPAD 中输入文本 - 代码就无法正常工作了 - 结果是我得到了一个模糊的光标图像 - 它看起来像非常半透明的灰色版本,而不是我们期望的黑白版。

我的问题

  • 如何在光标图像是 "I-beam" 类型之一时捕获鼠标光标图像?
  • 注意:如果您单击原始文章,有人提供了一个建议 - 它行不通。

来源

这是来自原始文章。

    static Bitmap CaptureCursor(ref int x, ref int y)
    {
        Bitmap bmp;
        IntPtr hicon;
        Win32Stuff.CURSORINFO ci = new Win32Stuff.CURSORINFO();
        Win32Stuff.ICONINFO icInfo;
        ci.cbSize = Marshal.SizeOf(ci);
        if (Win32Stuff.GetCursorInfo(out ci))
        {
            if (ci.flags == Win32Stuff.CURSOR_SHOWING)
            {
                hicon = Win32Stuff.CopyIcon(ci.hCursor);
                if (Win32Stuff.GetIconInfo(hicon, out icInfo))
                {
                    x = ci.ptScreenPos.x - ((int)icInfo.xHotspot);
                    y = ci.ptScreenPos.y - ((int)icInfo.yHotspot);

                    Icon ic = Icon.FromHandle(hicon);
                    bmp = ic.ToBitmap(); 
                    return bmp;
                }
            }
        }

        return null;
    }

我曾经遇到过类似的问题,我们正在为应用程序文档制作自动屏幕捕获。但是,我没有使用 interop 来获取指针,而是使用了自己的指针(在 .png 文件中)。这更简单,并且我们能够获得漂亮的透明效果(指针阴影等)。 - vgru
关于dimitar的CaptureScreen,使用DrawIconEx和相关参数以保留捕获图标的尺寸,否则应用程序的自定义鼠标指针将缩小到系统指针大小。例如,进入PowerPoint并制作演示文稿,启用笔指针,使用鼠标进行屏幕截图-您会发现这个鼠标光标很奇怪。如果有超过2个监视器(扩展监视器),此方法会捕获完全透明的图像,除了绘制正常的自定义鼠标指针。 - user1191921
6个回答

32

虽然我无法解释为什么会发生这种情况,但我认为我可以展示如何避免它。

ICONINFO结构包含两个成员,hbmMask和hbmColor,分别包含光标的掩码位图和颜色位图(请参见MSDN页面ICONINFO获取官方文档)。

当您调用GetIconInfo()以获取默认光标时,ICONINFO结构包含有效的掩码和颜色位图,如下所示(注:已添加红色边框以清晰显示图像边界):

默认光标掩码位图 default cursor mask bitmap image

默认光标颜色位图 default cursor color bitmap image

当Windows绘制默认光标时,首先使用AND栅格操作应用掩码位图,然后使用XOR栅格操作应用颜色位图。这将导致不透明的光标和透明的背景。

但是,当您调用GetIconInfo()以获取I-Beam光标时,ICONINFO结构仅包含有效的掩码位图,没有颜色位图,如下所示(注:再次添加红色边框以清晰显示图像边界):

I-Beam光标掩码位图 ibeam cursor mask bitmap image

根据ICONINFO文档,I-Beam光标是一种单色光标。掩码位图的上半部分是AND掩码,下半部分是XOR位图。当Windows绘制I-Beam光标时,首先使用AND光栅操作在桌面上绘制此位图的上半部分。然后再使用XOR光栅操作在其上方绘制位图的下半部分。在屏幕上,光标将显示为其背后内容的反色。

您链接的原始文章中的comments之一提到了这一点。在桌面上,由于光栅操作应用于桌面内容上,因此光标将显示正确。但是,在像您发布的代码中没有背景的情况下绘制图像时,Windows执行的光栅操作会导致图像变淡。

话虽如此,此更新的CaptureCursor()方法将处理彩色和单色光标,并在光标为单色时提供纯黑色光标图像。

static Bitmap CaptureCursor(ref int x, ref int y)
{
  Win32Stuff.CURSORINFO cursorInfo = new Win32Stuff.CURSORINFO();
  cursorInfo.cbSize = Marshal.SizeOf(cursorInfo);
  if (!Win32Stuff.GetCursorInfo(out cursorInfo))
    return null;

  if (cursorInfo.flags != Win32Stuff.CURSOR_SHOWING)
    return null;

  IntPtr hicon = Win32Stuff.CopyIcon(cursorInfo.hCursor);
  if (hicon == IntPtr.Zero)
    return null;

  Win32Stuff.ICONINFO iconInfo;
  if (!Win32Stuff.GetIconInfo(hicon, out iconInfo))
    return null;

  x = cursorInfo.ptScreenPos.x - ((int)iconInfo.xHotspot);
  y = cursorInfo.ptScreenPos.y - ((int)iconInfo.yHotspot);

  using (Bitmap maskBitmap = Bitmap.FromHbitmap(iconInfo.hbmMask))
  {
    // Is this a monochrome cursor?
    if (maskBitmap.Height == maskBitmap.Width * 2)
    {
      Bitmap resultBitmap = new Bitmap(maskBitmap.Width, maskBitmap.Width);

      Graphics desktopGraphics = Graphics.FromHwnd(Win32Stuff.GetDesktopWindow());
      IntPtr desktopHdc = desktopGraphics.GetHdc();

      IntPtr maskHdc = Win32Stuff.CreateCompatibleDC(desktopHdc);
      IntPtr oldPtr = Win32Stuff.SelectObject(maskHdc, maskBitmap.GetHbitmap());

      using (Graphics resultGraphics = Graphics.FromImage(resultBitmap))
      {
        IntPtr resultHdc = resultGraphics.GetHdc();

        // These two operation will result in a black cursor over a white background.
        // Later in the code, a call to MakeTransparent() will get rid of the white background.
        Win32Stuff.BitBlt(resultHdc, 0, 0, 32, 32, maskHdc, 0, 32, Win32Stuff.TernaryRasterOperations.SRCCOPY);
        Win32Stuff.BitBlt(resultHdc, 0, 0, 32, 32, maskHdc, 0, 0, Win32Stuff.TernaryRasterOperations.SRCINVERT);

        resultGraphics.ReleaseHdc(resultHdc);
      }

      IntPtr newPtr = Win32Stuff.SelectObject(maskHdc, oldPtr);
      Win32Stuff.DeleteObject(newPtr);
      Win32Stuff.DeleteDC(maskHdc);
      desktopGraphics.ReleaseHdc(desktopHdc);

      // Remove the white background from the BitBlt calls,
      // resulting in a black cursor over a transparent background.
      resultBitmap.MakeTransparent(Color.White);
      return resultBitmap;
    }
  }

  Icon icon = Icon.FromHandle(hicon);
  return icon.ToBitmap();
}

代码存在一些问题,可能会有问题,也可能没有问题。

  1. 检查单色光标的方法只是测试高度是否是宽度的两倍。虽然这似乎合乎逻辑,但 ICONINFO 文档并不规定只有单色光标才能由此定义。
  2. 渲染光标可能有更好的方法,而不是我使用的 BitBlt() - BitBlt() - MakeTransparent() 方法调用组合。

3
如果你只是想捕获带有鼠标指针的屏幕截图,那么你也可以使用DrawIcon(hDC, x, y, hIcon)函数将鼠标指针直接绘制到目标设备上下文中。 - fmuecke
我不理解为什么您要使用图标大小来检查单色光标。为什么不像您提到的那样直接使用单色光标没有彩色背景的事实呢?然后测试就变得很简单:iconInfo.hbmColor == IntPtr.Zero - glopes
1
有很多内存泄漏问题,只需在进程资源管理器的“句柄”、“GDI 对象”列中查看即可。如果您循环执行此代码,它们会极度增加并导致应用程序崩溃,“常见 GDI+ 错误”。 - Snicker
“黑色”光标方案被视为I-bean光标,结果图像只是边框,而不是背景。 - Nicke Manarin
@NickeManarin 黑色光标在技术上仍然是单色的...我已经成功地使用了这个简单的测试来处理I型光标,但我想可能取决于你想要做什么。 - glopes
显示剩余3条评论

11
[StructLayout(LayoutKind.Sequential)]
struct CURSORINFO
{
    public Int32 cbSize;
    public Int32 flags;
    public IntPtr hCursor;
    public POINTAPI ptScreenPos;
}

[StructLayout(LayoutKind.Sequential)]
struct POINTAPI
{
    public int x;
    public int y;
}

[DllImport("user32.dll")]
static extern bool GetCursorInfo(out CURSORINFO pci);

[DllImport("user32.dll")]
static extern bool DrawIcon(IntPtr hDC, int X, int Y, IntPtr hIcon);

const Int32 CURSOR_SHOWING = 0x00000001;

public static Bitmap CaptureScreen(bool CaptureMouse)
{
    Bitmap result = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format24bppRgb);

    try
    {
        using (Graphics g = Graphics.FromImage(result))
        {
            g.CopyFromScreen(0, 0, 0, 0, Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);

            if (CaptureMouse)
            {
                CURSORINFO pci;
                pci.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(typeof(CURSORINFO));

                if (GetCursorInfo(out pci))
                {
                    if (pci.flags == CURSOR_SHOWING)
                    {
                        DrawIcon(g.GetHdc(), pci.ptScreenPos.x, pci.ptScreenPos.y, pci.hCursor);
                        g.ReleaseHdc();
                    }
                }
            }
        }
    }
    catch
    {
        result = null;
    }

    return result;
}

4

根据其他答案,我创建了一个版本,没有使用所有的Windows API(对于单色部分),因为这些解决方案并不能针对所有单色光标正常工作。我通过将两个掩码部分组合起来,从掩码中创建了光标。

我的解决方案:

Bitmap CaptureCursor(ref Point position)
{
   CURSORINFO cursorInfo = new CURSORINFO();
   cursorInfo.cbSize = Marshal.SizeOf(cursorInfo);
   if (!GetCursorInfo(out cursorInfo))
      return null;

   if (cursorInfo.flags != CURSOR_SHOWING)
      return null;

   IntPtr hicon = CopyIcon(cursorInfo.hCursor);
   if (hicon == IntPtr.Zero)
      return null;

   ICONINFO iconInfo;
   if (!GetIconInfo(hicon, out iconInfo))
      return null;

   position.X = cursorInfo.ptScreenPos.x - iconInfo.xHotspot;
   position.Y = cursorInfo.ptScreenPos.y - iconInfo.yHotspot;

   using (Bitmap maskBitmap = Bitmap.FromHbitmap(iconInfo.hbmMask))
   {
      // check for monochrome cursor
      if (maskBitmap.Height == maskBitmap.Width * 2)
      {
         Bitmap cursor = new Bitmap(32, 32, PixelFormat.Format32bppArgb);
         Color BLACK = Color.FromArgb(255, 0, 0, 0); //cannot compare Color.Black because of different names
         Color WHITE = Color.FromArgb(255, 255, 255, 255); //cannot compare Color.White because of different names
         for (int y = 0; y < 32; y++)
         {
            for (int x = 0; x < 32; x++)
            {
               Color maskPixel = maskBitmap.GetPixel(x, y);
               Color cursorPixel = maskBitmap.GetPixel(x, y + 32);
               if (maskPixel == WHITE && cursorPixel == BLACK)
               {
                  cursor.SetPixel(x, y, Color.Transparent);
               }
               else if (maskPixel == BLACK)
               {
                  cursor.SetPixel(x, y, cursorPixel);
               }
               else
               {
                  cursor.SetPixel(x, y, cursorPixel == BLACK ? WHITE : BLACK);
               }
            }
         }
         return cursor;
      }
   }

   Icon icon = Icon.FromHandle(hicon);
   return icon.ToBitmap();
}

这是目前为止对我来说在任何情况下都能正常工作的唯一解决方案。 - Roemer
+1不能直接使用,但这种方法很方便,因为您可以根据AND和XOR层的组合轻松地反转底层图像(无论您在光标上绘制什么)。https://learn.microsoft.com/en-us/windows-hardware/drivers/display/drawing-monochrome-pointers - caesay

4
以下是Dimitar的回答的修改版本(使用DrawIconEx),在多个屏幕上对我有效:

以下是Dimitar的回答的修改版本(使用DrawIconEx),在多个屏幕上对我有效:

public class ScreenCapturePInvoke
{
    [StructLayout(LayoutKind.Sequential)]
    private struct CURSORINFO
    {
        public Int32 cbSize;
        public Int32 flags;
        public IntPtr hCursor;
        public POINTAPI ptScreenPos;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct POINTAPI
    {
        public int x;
        public int y;
    }

    [DllImport("user32.dll")]
    private static extern bool GetCursorInfo(out CURSORINFO pci);

    [DllImport("user32.dll", SetLastError = true)]
    static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags);

    private const Int32 CURSOR_SHOWING = 0x0001;
    private const Int32 DI_NORMAL = 0x0003;

    public static Bitmap CaptureFullScreen(bool captureMouse)
    {
        var allBounds = Screen.AllScreens.Select(s => s.Bounds).ToArray();
        Rectangle bounds = Rectangle.FromLTRB(allBounds.Min(b => b.Left), allBounds.Min(b => b.Top), allBounds.Max(b => b.Right), allBounds.Max(b => b.Bottom));

        var bitmap = CaptureScreen(bounds, captureMouse);
        return bitmap;
    }

    public static Bitmap CapturePrimaryScreen(bool captureMouse)
    {
        Rectangle bounds = Screen.PrimaryScreen.Bounds;

        var bitmap = CaptureScreen(bounds, captureMouse);
        return bitmap;
    }

    public static Bitmap CaptureScreen(Rectangle bounds, bool captureMouse)
    {
        Bitmap result = new Bitmap(bounds.Width, bounds.Height);

        try
        {
            using (Graphics g = Graphics.FromImage(result))
            {
                g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);

                if (captureMouse)
                {
                    CURSORINFO pci;
                    pci.cbSize = Marshal.SizeOf(typeof (CURSORINFO));

                    if (GetCursorInfo(out pci))
                    {
                        if (pci.flags == CURSOR_SHOWING)
                        {
                            var hdc = g.GetHdc();
                            DrawIconEx(hdc, pci.ptScreenPos.x-bounds.X, pci.ptScreenPos.y-bounds.Y, pci.hCursor, 0, 0, 0, IntPtr.Zero, DI_NORMAL);
                            g.ReleaseHdc();
                        }
                    }
                }
            }
        }
        catch
        {
            result = null;
        }

        return result;
    }
}

3

这是经过修补的版本,包含了此页面中出现的所有漏洞修复:

public static Bitmap CaptureImageCursor(ref Point point)
{
    try
    {
        var cursorInfo = new CursorInfo();
        cursorInfo.cbSize = Marshal.SizeOf(cursorInfo);

        if (!GetCursorInfo(out cursorInfo))
            return null;

        if (cursorInfo.flags != CursorShowing)
            return null;

        var hicon = CopyIcon(cursorInfo.hCursor);
        if (hicon == IntPtr.Zero)
            return null;

        Iconinfo iconInfo;
        if (!GetIconInfo(hicon, out iconInfo))
        {
            DestroyIcon(hicon);
            return null;
        }

        point.X = cursorInfo.ptScreenPos.X - iconInfo.xHotspot;
        point.Y = cursorInfo.ptScreenPos.Y - iconInfo.yHotspot;

        using (var maskBitmap = Image.FromHbitmap(iconInfo.hbmMask))
        {
            //Is this a monochrome cursor?  
            if (maskBitmap.Height == maskBitmap.Width * 2 && iconInfo.hbmColor == IntPtr.Zero)
            {
                var final = new Bitmap(maskBitmap.Width, maskBitmap.Width);
                var hDesktop = GetDesktopWindow();
                var dcDesktop = GetWindowDC(hDesktop);

                using (var resultGraphics = Graphics.FromImage(final))
                {
                    var resultHdc = resultGraphics.GetHdc();

                    BitBlt(resultHdc, 0, 0, final.Width, final.Height, dcDesktop, (int)point.X + 3, (int)point.Y + 3, CopyPixelOperation.SourceCopy);
                    DrawIconEx(resultHdc, 0, 0, cursorInfo.hCursor, 0, 0, 0, IntPtr.Zero, 0x0003);

                    //TODO: I have to try removing the background of this cursor capture.
                    //Native.BitBlt(resultHdc, 0, 0, final.Width, final.Height, dcDesktop, (int)point.X + 3, (int)point.Y + 3, Native.CopyPixelOperation.SourceErase);

                    resultGraphics.ReleaseHdc(resultHdc);
                    ReleaseDC(hDesktop, dcDesktop);
                }

                DeleteObject(iconInfo.hbmMask);
                DeleteDC(dcDesktop);
                DestroyIcon(hicon);

                return final;
            }

            DeleteObject(iconInfo.hbmColor);
            DeleteObject(iconInfo.hbmMask);
            DestroyIcon(hicon);
        }

        var icon = Icon.FromHandle(hicon);
        return icon.ToBitmap();
    }
    catch (Exception ex)
    {
        //You should catch exception with your method here.
        //LogWriter.Log(ex, "Impossible to get the cursor.");
    }

    return null;
}

此版本适用于以下内容:

  1. I-Beam光标。
  2. 黑色光标。
  3. 普通光标。
  4. 反转光标。

点击此处查看示例:https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Util/Native.cs#L991


2

您所描述的I-Beam光标的半透明“灰色”版本让我想知道您是否遇到了图像缩放或光标位置不正确的问题。

在该网站发布帖子的人之一提供了一个(损坏的)链接,指向一个显示奇怪行为的报告。我已经追踪到此问题来源:http://www.efg2.com/Lab/Graphics/CursorOverlay.htm

该页面上的示例不是使用C#编写的,但是codeproject解决方案的作者可能在做类似的事情,而且我知道当我自己在使用图形对象时,缩放时会弄错很多次:

任何ImageMouseDown事件在加载图像后, CusorBitmap都将使用Canvas.Draw方法在位图顶部以透明方式绘制。 请注意,在位图被拉伸以适应TImage时需要进行某些坐标调整(重新缩放)。


扩展也是我的第一反应 - stevenrcfox
链接已失效,存档副本 - Dzmitry Paliakou

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