C# - 捕获鼠标光标图像

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/918990/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 03:11:48  来源:igfitidea点击:

C# - Capturing the Mouse cursor image

c#mousecursoriconsscreenshot

提问by namenlos

BACKGROUND

背景

MY PROBLEM

我的问题

  • Code works fine when the mouse cursor is the normal pointer or hand icon - the mouse is rendered correctly on the screenshot
  • However, when the mouse cursor is changed to the insertion point (the "I-beam" cursor) - for example typing in NOTEPAD - then code doesn't work - the result is that I get a faint image of the cursor - like a very translucent (gray) version of it instead of the blank & white one would expect.
  • 当鼠标光标是普通指针或手形图标时,代码工作正常 - 鼠标在屏幕截图上正确呈现
  • 但是,当鼠标光标更改为插入点(“工字梁”光标)时 - 例如在记事本中输入 - 然后代码不起作用 - 结果是我得到了一个模糊的光标图像 - 就像它的非常半透明(灰色)版本,而不是人们期望的空白和白色。

MY QUESTION

我的问题

  • How can I capture the mouse cursor image when the image is one of these "I-beam"-type images
  • NOTE: If you click on the original article someone offers a suggestion - it doesn't work
  • 当图像是这些“工字梁”型图像之一时,如何捕获鼠标光标图像
  • 注意:如果您单击原始文章,有人会提供建议 - 它不起作用

SOURCE

来源

This is from the original article.

这是来自原始文章。

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

采纳答案by Tarsier

While I can't explain exactly why this happens, I think I can show how to get around it.

虽然我无法确切解释为什么会发生这种情况,但我想我可以展示如何解决它。

The ICONINFO struct contains two members, hbmMask and hbmColor, that contain the mask and color bitmaps, respectively, for the cursor (see the MSDN page for ICONINFOfor the official documentation).

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

When you call GetIconInfo() for the default cursor, the ICONINFO struct contains both valid mask and color bitmaps, as shown below (Note: the red border has been added to clearly show the image boundaries):

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

Default Cursor Mask Bitmapdefault cursor mask bitmap image

默认光标掩码位图默认光标遮罩位图图像

Default Cursor Color Bitmapdefault cursor color bitmap image

默认光标颜色位图默认光标颜色位图图像

When Windows draws the default cursor, the mask bitmap is first applied with an AND raster operation, then the color bitmap is applied with an XOR raster operation. This results in an opaque cursor and a transparent background.

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

When you call GetIconInfo() for the I-Beam cursor, though, the ICONINFO struct only contains a valid mask bitmap, and no color bitmap, as shown below (Note: again, the red border has been added to clearly show the image boundaries):

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

I-Beam Cursor Mask Bitmapibeam cursor mask bitmap image

I-Beam 光标遮罩位图ibeam 光标遮罩位图图像

According to the ICONINFO documentation, the I-Beam cursor is then a monochrome cursor. The top half of the mask bitmap is the AND mask, and the bottom half of the mask bitmap is the XOR bitmap. When Windows draws the I-Beam cursor, the top half of this bitmap is first drawn over the desktop with an AND raster operation. The bottom half of the bitmap is then drawn over top with an XOR raster operation. Onscreen, The cursor will appear as the inverse of the content behind it.

根据 ICONINFO 文档,I-Beam 光标是单色光标。掩码位图的上半部分是 AND 掩码,掩码位图的下半部分是 XOR 位图。当 Windows 绘制 I-Beam 光标时,该位图的上半部分首先通过 AND 光栅操作绘制在桌面上。然后使用 XOR 光栅操作将位图的下半部分绘制到顶部。在屏幕上,光标将显示为它后面内容的倒数。

One of the commentsfor the original article that you linked mentions this. On the desktop, since the raster operations are applied over the desktop content, the cursor will appear correct. However, when the image is drawn over no background, as in your posted code, the raster operations that Windows performs result in a faded image.

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

That being said, this updated CaptureCursor() method will handle both color and monochrome cursors, supplying a plain black cursor image when the cursor is monochrome.

也就是说,这个更新的 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();
}

There are some issues with the code that may or may not be a problem.

代码存在一些问题,可能是也可能不是问题。

  1. The check for a monochrome cursor simply tests whether the height is twice the width. While this seems logical, the ICONINFO documentation does not mandate that only a monochrome cursor is defined by this.
  2. There is probably a better way to render the cursor that the BitBlt() - BitBlt() - MakeTransparent() combination of method calls I used.
  1. 检查单色光标只是测试高度是否是宽度的两倍。虽然这看起来合乎逻辑,但 ICONINFO 文档并未强制要求仅定义单色光标。
  2. 我使用的方法调用的 BitBlt() - BitBlt() - MakeTransparent() 组合可能有更好的方法来呈现光标。

回答by nvuono

Your description of a translucent 'gray' version of the I-beam cursor makes me wonder if you're encountering an issue with image scaling or mispositioning of the cursor.

您对工字形光标的半透明“灰色”版本的描述让我怀疑您是否遇到了图像缩放或光标定位错误的问题。

One of the people posting on that site provided a (broken) link to a report with peculiar behavior that I've tracked down to: http://www.efg2.com/Lab/Graphics/CursorOverlay.htm

在该网站上发布的一个人提供了一个(损坏的)链接,指向我追踪到的具有特殊行为的报告:http: //www.efg2.com/Lab/Graphics/CursorOverlay.htm

The examples on that page are not in C# but the author of the codeproject solution may have been doing something similar and I know I've screwed up my scaling when using the graphics object on plenty of occassions myself:

该页面上的示例不在 C# 中,但 codeproject 解决方案的作者可能已经做了类似的事情,我知道我自己在很多场合使用图形对象时已经搞砸了我的缩放:

In any ImageMouseDown event once an image is loaded, the CusorBitmap is drawn with transparency on top of the bitmap using the Canvas.Draw method. Note some coordinate adjustments (rescaling) are needed in case the bitmap is stretched to fit in the TImage.

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

回答by Dimitar

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

回答by johv

Here's a modified version of Dimitar's response (using DrawIconEx) that worked for me on multiple screens:

这是 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;
    }
}

回答by Nicke Manarin

This is the patched version with all fixes for the bugs presented on this page:

这是修补版本,其中包含针对此页面上出现的错误的所有修复程序:

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

This version works with:

此版本适用于:

  1. I-Beam cursors.
  2. Black cursors.
  3. Normal cursors.
  4. Inverted cursors.
  1. I-Beam 光标。
  2. 黑色光标。
  3. 普通光标。
  4. 反向游标。

See working, here: https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Util/Native.cs#L991

在这里查看工作:https: //github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Util/Native.cs#L991

回答by Daniel

Based on the other answers I made a version without all the Windows API stuff (for the monochrome part) because the solutions did not work for all monochrome cursors. I create the cursor from the mask by combining the two mask parts.

根据其他答案,我制作了一个没有所有 Windows API 内容的版本(对于单色部分),因为这些解决方案不适用于所有单色光标。我通过组合两个蒙版部分从蒙版创建光标。

My solution:

我的解决方案:

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