C# - захват изображения курсора мыши
ФОН
- Я пишу приложение для захвата экрана
- Мой код основан на этом проекте: http://www.codeproject.com/KB/cs/DesktopCaptureWithMouse.aspx?display=Print
- Обратите внимание, что код также захватывает курсор мыши (что для меня желательно)
МОЯ ПРОБЛЕМА
- Код работает нормально, когда курсор мыши является обычным указателем или значком руки - мышь отображается правильно на скриншоте
- Однако, когда курсор мыши изменяется на точку вставки (курсор "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;
}
6 ответов
Хотя я не могу точно объяснить, почему это происходит, я думаю, что могу показать, как это обойти.
Структура ICONINFO содержит два члена, hbmMask и hbmColor, которые содержат соответственно маску и цветные растровые изображения для курсора (официальную документацию см. На странице MSDN для ICONINFO).
Когда вы вызываете GetIconInfo() для курсора по умолчанию, структура ICONINFO содержит как действительные маски, так и цветовые растровые изображения, как показано ниже (Примечание. Красная граница была добавлена для четкого отображения границ изображения):
Растровое изображение маски курсора по умолчанию Растровое изображение маски курсора по умолчанию http://img4.imageshack.us/img4/1108/arrowmask.png
Цвет курсора по умолчанию для курсора Цвет изображения по http://img191.imageshack.us/img191/7680/arrowcolor.png
Когда Windows рисует курсор по умолчанию, растровое изображение маски сначала применяется с растровой операцией AND, а затем растровое изображение цвета применяется с растровой операцией XOR. Это приводит к непрозрачному курсору и прозрачному фону.
Однако, когда вы вызываете GetIconInfo() для курсора I-Beam, структура ICONINFO содержит только действительное растровое изображение маски и не содержит растровое растровое изображение, как показано ниже (Примечание: снова была добавлена красная граница, чтобы четко показать границы изображения):
Растровая маска курсора I-Beam http://img14.imageshack.us/img14/6025/ibeammask.png
Согласно документации ICONINFO, курсор I-Beam является монохромным курсором. Верхняя половина битовой карты маски - это маска AND, а нижняя половина битовой карты маски - битовая карта XOR. Когда Windows рисует курсор I-Beam, верхняя половина этого растрового изображения сначала рисуется поверх рабочего стола с помощью растровой операции AND. Затем нижняя половина растрового изображения рисуется поверх растровой операции XOR. На экране, курсор будет отображаться как обратный контент за ним.
Один из комментариев к оригинальной статье, которую вы связали, упоминает об этом. На рабочем столе, поскольку растровые операции применяются к содержимому рабочего стола, курсор будет отображаться правильно. Однако, когда изображение рисуется без фона, как в опубликованном коде, растровые операции, выполняемые 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();
}
Есть некоторые проблемы с кодом, которые могут или не могут быть проблемой.
- Проверка монохромного курсора просто проверяет, равна ли высота двойной ширине. Хотя это кажется логичным, документация ICONINFO не требует, чтобы этим определялся только монохромный курсор.
- Вероятно, есть лучший способ визуализации курсора, который я использовал комбинацией вызовов методов BitBlt() - BitBlt() - MakeTransparent().
[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;
}
Вот модифицированная версия ответа Димитара (с использованием 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;
}
}
Основываясь на других ответах, я сделал версию без всего 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();
}
Это исправленная версия со всеми исправлениями ошибок, представленных на этой странице:
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;
}
Эта версия работает с:
- I-Beam курсоры.
- Черные курсоры.
- Обычные курсоры.
- Перевернутые курсоры.
Смотрите работу здесь: https://github.com/NickeManarin/ScreenToGif/blob/master/ScreenToGif/Util/Native.cs#L991
Ваше описание полупрозрачной "серой" версии двутаврового курсора заставляет меня задуматься, сталкивались ли вы с проблемой масштабирования изображения или неправильного расположения курсора.
Один из людей, размещающих на этом сайте, предоставил (неработающую) ссылку на отчет со странным поведением, которое я отслеживал по адресу: http://www.efg2.com/Lab/Graphics/CursorOverlay.htm
Примеры на этой странице не на C#, но автор решения codeproject, возможно, делал нечто подобное, и я знаю, что я испортил свое масштабирование при использовании графического объекта во многих случаях сам:
В любом событии ImageMouseDown после загрузки изображения CusorBitmap рисуется с прозрачностью поверх растрового изображения, используя метод Canvas.Draw. Обратите внимание, что некоторые корректировки координат (изменение масштаба) необходимы в том случае, если растровое изображение растянуто до размера TImage.