使用上下文菜单捕获HWND

3

我正在使用WinAPI在Java中捕获窗口。

public static BufferedImage capture(Pointer hWnd2, String wText) {
   HWND hWnd = new HWND(hWnd2);
   boolean success;
   if(User32Extra.INSTANCE.IsIconic(hWnd))
   {
       success= User32Extra.INSTANCE.ShowWindow(hWnd, 4);
       System.err.println(success);
       System.err.println(wText+Native.getLastError());
   }
   HDC hdcWindow2 = User32.INSTANCE.GetDC(hWnd);
   HDC hdcMemDC = GDI32.INSTANCE.CreateCompatibleDC(hdcWindow2);
   success = User32Extra.INSTANCE.PrintWindow(hWnd,hdcWindow2,1);
   RECT bounds = new RECT();
   RECT bounds1 = new RECT();
   User32Extra.INSTANCE.GetWindowRect(hWnd, bounds);
   User32Extra.INSTANCE.GetClientRect(hWnd, bounds1);

   int extraGap = (bounds.right-bounds.left-bounds1.right);
   int width = bounds.right-bounds.left-extraGap;
   int height = bounds.bottom-bounds.top-extraGap ;
   HBITMAP hBitmap = GDI32.INSTANCE.CreateCompatibleBitmap(hdcWindow2, width, height);

   HANDLE hOld = GDI32.INSTANCE.SelectObject(hdcMemDC, hBitmap);
   GDI32Extra.INSTANCE.BitBlt(hdcMemDC,0, 0, width, height, hdcWindow2, bounds.left+bounds1.right-bounds.right+extraGap, bounds.top+bounds1.bottom-bounds.bottom+extraGap, WinGDIExtra.SRCCOPY);
   GDI32.INSTANCE.SelectObject(hdcMemDC, hOld);

   BITMAPINFO bmi = new BITMAPINFO();
   bmi.bmiHeader.biWidth = width;
   bmi.bmiHeader.biHeight = -height;
   bmi.bmiHeader.biPlanes = 1;
   bmi.bmiHeader.biBitCount = 32;
   bmi.bmiHeader.biCompression = WinGDI.BI_RGB;
   Memory buffer = new Memory(width * height * 4);
   GDI32.INSTANCE.GetDIBits(hdcWindow2, hBitmap, 0, height, buffer, bmi, WinGDI.DIB_RGB_COLORS);

   BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

   GDI32.INSTANCE.DeleteObject(hBitmap);
   GDI32.INSTANCE.DeleteDC(hdcMemDC);
   User32.INSTANCE.ReleaseDC(hWnd, hdcWindow2);

   return image;
}

这段代码捕获指定窗口的HWND。但是,如果用户打开上下文菜单或菜单栏选项,则不会被捕获。

如何捕获窗口的上下文菜单?


也许这与你的问题无关,但是为什么要使用PrintWindow在窗口本身上打印,然后再将其复制到内存中呢?你应该打印到内存DC或者复制到内存DC。也许结果会有所不同。 - ElderBug
1个回答

1

我会使用GetDesktopWindow()来捕获整个屏幕,然后将窗口坐标传递给BitBlt,以仅复制所有叠加层(包括上下文菜单)的目标窗口。

public static BufferedImage capture(Pointer hWnd2, String wText) {
   HWND hWnd = new HWND(hWnd2);
   boolean success;
   if(User32Extra.INSTANCE.IsIconic(hWnd))
   {
       success= User32Extra.INSTANCE.ShowWindow(hWnd, 4);
       System.err.println(success);
       System.err.println(wText+Native.getLastError());
   }
   // Pass GetDesktopWindow instead of target HWND to GetDC
   HDC hdcWindow2 = User32.INSTANCE.GetDC(User32.INSTANCE.GetDesktopWindow());
   HDC hdcMemDC = GDI32.INSTANCE.CreateCompatibleDC(hdcWindow2);

   RECT bounds = new RECT();
   //RECT bounds1 = new RECT();
   User32Extra.INSTANCE.GetWindowRect(hWnd, bounds);
   //User32Extra.INSTANCE.GetClientRect(hWnd, bounds1);

   //int extraGap = (bounds.right-bounds.left-bounds1.right);
   int width = bounds.right-bounds.left;
   int height = bounds.bottom-bounds.top;
   HBITMAP hBitmap = GDI32.INSTANCE.CreateCompatibleBitmap(hdcWindow2, width, height);

   HANDLE hOld = GDI32.INSTANCE.SelectObject(hdcMemDC, hBitmap);
   GDI32Extra.INSTANCE.BitBlt(hdcMemDC,0, 0, width, height, hdcWindow2, bounds.left, bounds.top, WinGDIExtra.SRCCOPY);
   GDI32.INSTANCE.SelectObject(hdcMemDC, hOld);

   BITMAPINFO bmi = new BITMAPINFO();
   bmi.bmiHeader.biWidth = width;
   bmi.bmiHeader.biHeight = -height;
   bmi.bmiHeader.biPlanes = 1;
   bmi.bmiHeader.biBitCount = 32;
   bmi.bmiHeader.biCompression = WinGDI.BI_RGB;
   Memory buffer = new Memory(width * height * 4);
   GDI32.INSTANCE.GetDIBits(hdcWindow2, hBitmap, 0, height, buffer, bmi, WinGDI.DIB_RGB_COLORS);

   BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

   GDI32.INSTANCE.DeleteObject(hBitmap);
   GDI32.INSTANCE.DeleteDC(hdcMemDC);
   User32.INSTANCE.ReleaseDC(hWnd, hdcWindow2);

   return image;
}

这种方法可能会捕获其他窗口,如果它们与目标窗口重叠。如果你知道窗口是活动的或者上下文菜单已经打开(因为这将强制它成为前景),那么这可能已经足够了。
另一种方法有点黑客,但适用于其他场景。我们可以检查是否有任何类名为#32768的窗口(用于弹出菜单的类)。如果有,我们检查我们的目标窗口是否是前景窗口,然后我们可以将其复制到我们的目标位图中,覆盖在我们的窗口顶部。
public static BufferedImage capture(Pointer hWnd2, String wText) {
    HWND hWnd = new HWND(hWnd2);
    boolean success;
    if(User32Extra.INSTANCE.IsIconic(hWnd))
    {
        success= User32Extra.INSTANCE.ShowWindow(hWnd, 4);
        System.err.println(success);
        System.err.println(wText+Native.getLastError());
    }
    HDC hdcWindow2 = User32.INSTANCE.GetDC(hWnd);
    final HDC hdcMemDC = GDI32.INSTANCE.CreateCompatibleDC(hdcWindow2);

    RECT bounds = new RECT();
    RECT bounds1 = new RECT();
    User32Extra.INSTANCE.GetWindowRect(hWnd, bounds);
    User32Extra.INSTANCE.GetClientRect(hWnd, bounds1);

    int extraGap = (bounds.right-bounds.left-bounds1.right);
    int width = bounds.right-bounds.left-extraGap;
    int height = bounds.bottom-bounds.top-extraGap ;
    HBITMAP hBitmap = GDI32.INSTANCE.CreateCompatibleBitmap(hdcWindow2, width, height);

    HANDLE hOld = GDI32.INSTANCE.SelectObject(hdcMemDC, hBitmap);
    GDI32Extra.INSTANCE.BitBlt(hdcMemDC,0, 0, width, height, hdcWindow2, bounds.left+bounds1.right-bounds.right+extraGap, bounds.top+bounds1.bottom-bounds.bottom+extraGap, WinGDIExtra.SRCCOPY);

    // Popup menu doesn't have parent, so we could encounter situation when context menu from another window is overlapping our target window
    // Since context menu enforces our window to focus, we can check here if our window is foreground window (otherwise it is not possible to have opened context menu)
    if (User32.INSTANCE.GetForegroundWindow() == hWnd)
    {
        final RECT windowBounds = bounds;
        // Enum all windows and check if they have class #32768
        User32.INSTANCE.EnumWindows(new WNDENUMPROC() {
            @Override
            public boolean callback(HWND hMenuWnd, Pointer arg1) {
                byte[] windowClass = new byte[8];
                User32.INSTANCE.GetClassNameA(hMenuWnd, windowClass, 8);
                String wClass = Native.toString(windowClass);
                // #32768 is class name for popup menus.
                if (wClass.equals("#32768"))
                {
                    HDC hdcWindowMenu = User32.INSTANCE.GetDC(hMenuWnd);
                    RECT menuBounds = new RECT();
                    User32Extra.INSTANCE.GetWindowRect(hMenuWnd, menuBounds);
                    GDI32Extra.INSTANCE.BitBlt(hdcMemDC, menuBounds.left - windowBounds.left, menuBounds.top - windowBounds.top, menuBounds.right-menuBounds.left, menuBounds.bottom-menuBounds.top, hdcWindowMenu, 0, 0, WinGDIExtra.SRCCOPY);
                    User32.INSTANCE.ReleaseDC(hMenuWnd, hdcWindowMenu);
                }
                return true;
            }
        }, null);
    }

    GDI32.INSTANCE.SelectObject(hdcMemDC, hOld);

    BITMAPINFO bmi = new BITMAPINFO();
    bmi.bmiHeader.biWidth = width;
    bmi.bmiHeader.biHeight = -height;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = WinGDI.BI_RGB;
    Memory buffer = new Memory(width * height * 4);
    GDI32.INSTANCE.GetDIBits(hdcWindow2, hBitmap, 0, height, buffer, bmi, WinGDI.DIB_RGB_COLORS);

    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

    GDI32.INSTANCE.DeleteObject(hBitmap);
    GDI32.INSTANCE.DeleteDC(hdcMemDC);
    User32.INSTANCE.ReleaseDC(hWnd, hdcWindow2);

    return image;
}

对于EnumWindows你需要添加以下导入:

import com.sun.jna.platform.win32.WinUser.WNDENUMPROC;

和定义:

boolean EnumWindows(WinUser.WNDENUMPROC lpEnumFunc, Pointer arg);
int GetClassNameA(HWND hWnd, byte[] lpString, int nMaxCount);

你尝试过我的代码吗?它仅捕获您指定的窗口,但从屏幕 HWND 而不是窗口 HWND 复制它。这样将包括所有覆盖层,包括上下文菜单。它不会捕获整个屏幕。 - AdrianEddy
我正在使用它进行屏幕共享。如果我使用它,我的屏幕将被指定窗口覆盖。 - Vishnu
为什么会这样呢?这段代码并没有绘制任何东西,它只是将屏幕上的位图复制到 BufferedImage 中。请注意,我删除了 PrintWindow 调用,因为捕获窗口并不是必要的。如果这不是您想要的,请澄清您的需求。 - AdrianEddy
它复制的是屏幕上可见的最顶部窗口,而不是指定的窗口。注意:最顶部的窗口不是指定的窗口。我已经尝试过了。 - Vishnu
谢谢!这个方法可行,但是如果我打开子菜单,只有子菜单被捕获了,因为findwindow只能找到一个窗口,即最顶层的窗口。如何获取所有上下文菜单? - Vishnu
显示剩余6条评论

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