3

I need to store HCURSOR in BufferedImage with its real size and color.

I have found similar questions 1 and 2 which work fine with standard 32x32 cursor, but if I change color or size then BufferedImage becomes invalid, giving me a result like this:

enter image description here

Firstly, my problem was to get a real cursor size. But then I found the way to get it via JNA from the registry.

Then I need to save it to BufferedImage. I tried to use code snippets getImageByHICON() and getIcon() from the first link above, but there's an error somewhere -- the image is still incorrect or broken. Maybe I don't understand how to use it correctly because I am not much familiar with BufferedImage creation.

How can I save HCURSOR to BufferedImage if I have cursors real size and CURSORINFO?

Here is my full code:

import com.sun.jna.Memory;
import com.sun.jna.platform.win32.*;

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;

class CursorExtractor {

    public static void main(String[] args) {

        BufferedImage image = getCursor();

        JLabel icon = new JLabel();
        icon.setIcon(new ImageIcon(image));

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(icon);
        frame.pack();
        frame.setVisible(true);

        Toolkit toolkit = Toolkit.getDefaultToolkit();
        Point pointerPos = new Point(1, 1);
        Cursor c = toolkit.createCustomCursor(image, pointerPos, "cursorname");
        frame.setCursor(c);
    }

    public static BufferedImage getCursor() {
        // Read an int (& 0xFFFFFFFFL for large unsigned int)
        int baseSize = Advapi32Util.registryGetIntValue(
                WinReg.HKEY_CURRENT_USER, "Control Panel\\Cursors", "CursorBaseSize");

        final User32.CURSORINFO cursorinfo = new User32.CURSORINFO();
        User32.INSTANCE.GetCursorInfo(cursorinfo);
        WinDef.HCURSOR hCursor = cursorinfo.hCursor;

        return getImageByHICON(baseSize, baseSize, hCursor);
    }

    public static BufferedImage getImageByHICON(final int width, final int height, final WinDef.HICON hicon) {
        final WinGDI.ICONINFO iconinfo = new WinGDI.ICONINFO();

        try {
            // get icon information

            if (!User32.INSTANCE.GetIconInfo(hicon, iconinfo)) {
                return null;
            }
            final WinDef.HWND hwdn = new WinDef.HWND();
            final WinDef.HDC dc = User32.INSTANCE.GetDC(hwdn);

            if (dc == null) {

                return null;
            }
            try {
                final int nBits = width * height * 4;
                // final BitmapInfo bmi = new BitmapInfo(1);

                final Memory colorBitsMem = new Memory(nBits);
                // // Extract the color bitmap
                final WinGDI.BITMAPINFO bmi = new WinGDI.BITMAPINFO();

                bmi.bmiHeader.biWidth = width;
                bmi.bmiHeader.biHeight = -height;
                bmi.bmiHeader.biPlanes = 1;
                bmi.bmiHeader.biBitCount = 32;
                bmi.bmiHeader.biCompression = WinGDI.BI_RGB;
                GDI32.INSTANCE.GetDIBits(dc, iconinfo.hbmColor, 0, height, colorBitsMem, bmi, WinGDI.DIB_RGB_COLORS);
                // g32.GetDIBits(dc, iconinfo.hbmColor, 0, size, colorBitsMem,
                // bmi,
                // GDI32.DIB_RGB_COLORS);
                final int[] colorBits = colorBitsMem.getIntArray(0, width * height);

                final BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
                bi.setRGB(0, 0, width, height, colorBits, 0, height);
                return bi;
            } finally {
                com.sun.jna.platform.win32.User32.INSTANCE.ReleaseDC(hwdn, dc);
            }
        } finally {
            User32.INSTANCE.DestroyIcon(new WinDef.HICON(hicon.getPointer()));
            GDI32.INSTANCE.DeleteObject(iconinfo.hbmColor);
            GDI32.INSTANCE.DeleteObject(iconinfo.hbmMask);
        }
    }
}
Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
BaLiK
  • 99
  • 7

1 Answers1

1

I originally answered this question suggesting that you use the GetSystemMetrics() function, using the constant SM_CXCURSOR (13) for the width of the cursor in pixels, and SM_CYCURSOR (14) for the height. The linked documentation states "The system cannot create cursors of other sizes."

But then I see you posted a similar question here, and stated that those values don't change from 32x32. What happens there, as noted in this answer, is that the cursor is still actually that size, but only the smaller image is displayed on the screen; the rest of the pixels are simply "invisible". The same appears to be true for "larger" images, in that internally the "icon" associated with the cursor is still the same 32x32 size, but the screen displays something else.

Interestingly, the icon when hovering over the Swing window is always 32x32. Your choice to use Cursor c = toolkit.createCustomCursor(image, pointerPos, "cursorname"); is scaling down the bitmap image to a new (smaller) cursor in the window. I can keep the default cursor with a simple:

Cursor c = Cursor.getDefaultCursor();

I made the following changes to your code to get an ugly pixellated version at the right size:

  • changed method arguments width and height to w and h: getImageByHICON(final int w, final int h, final WinDef.HICON hicon)
  • at the start of the try block, set int width = 32 and int height = 32.
  • after fetching the colorBitsMem from GetDIBits() inserted the following:
final int[] colorBitsBase = colorBitsMem.getIntArray(0, width * height);
final int[] colorBits = new int[w * h];
for (int row = 0; row < h; row++) {
    for (int col = 0; col < w; col++) {
        int r = row * 32 / h;
        int c = col * 32 / w;
        colorBits[row * w + col] = colorBitsBase[r * 32 + c];
    }
}

So with a 64x64 system icon, I see this in the swing window:

image of icon

That size matches my mouse cursor. The pixels, notsomuch.

Another option, inspired by this answer is to use a better bitmap scaling than my simple integer math with pixels. In your getCursor() method, do:

BufferedImage before = getImageByHICON(32, 32, hCursor);

int w = before.getWidth();
int h = before.getHeight();
BufferedImage after = new BufferedImage(baseSize, baseSize, BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(baseSize / 32d, baseSize / 32d);
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
after = scaleOp.filter(before, after);

return after;

Which is giving me this:

enter image description here

Yet another option, in your getCursor() class is CopyImage().

WinDef.HCURSOR hCursor = cursorinfo.hCursor;
HANDLE foo = User32.INSTANCE.CopyImage(hCursor, 2, baseSize, baseSize, 0x00004000);
return getImageByHICON(baseSize, baseSize, new WinDef.HCURSOR(foo.getPointer()));

Gives this:

image of icon

Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
  • But my problem occurred when my cursor _is bigger_ than 32x32(e.g. 256x256 or 128x128), how I should handle this situation? – BaLiK Aug 17 '20 at 09:47
  • Two thoughts. 1. Might be the same 32x32 cursor magnified. Check accessibility cursor size at `HKEY_CURRENT_USER\SOFTWARE\Microsoft\Accessibility\CursorIndicator` (default is 1, goes up to 5). 2. Could be a resource/image rather than the actual HCURSOR bitmap. Use `GetIconInfoEx()` rather than `GetIconInfo` to get this resource. I will experiment with both approaches and get back to you. – Daniel Widdis Aug 17 '20 at 15:50
  • I've already got the real cursor size from the registry via JNA. It's in `baseSize` variable. – BaLiK Aug 17 '20 at 16:48
  • Yeah, I know and am playing with that. I am still convinced the icon from Java/Swing's perspective is still 32/32, which differs from the system perspective. If you hardcode 32 into your code, the swing bitmap exactly matches system bitmap except for size *outside* the swing window. – Daniel Widdis Aug 17 '20 at 16:57
  • The pixels are because of my very hamhanded way of scaling it using integer math on rows and columns. :) The bottom line is that your code is seeing a 32x32 cursor, and the "size" appears to impact it external to information that you have access to. You can manually (like I did) scale its size up, but the `GetCursorInfo()` method is giving you a 32x32 cursor. – Daniel Widdis Aug 17 '20 at 18:19
  • Yes, got it. And the problem with swing creation of cursor is strange... Actually I need to get cursor Image for adding my custom badge near the cursor. So current work is not useless. – BaLiK Aug 17 '20 at 18:26
  • Added another option scaling the bitmap. I think that's about all I can do with this. Sorry I couldn't be more help. – Daniel Widdis Aug 17 '20 at 18:34
  • Found another option! `User32.INSTANCE.CopyImage(hCursor, 2, baseSize, baseSize, 0x00000040);` ... may be something there. – Daniel Widdis Aug 17 '20 at 19:35
  • Actually maybe it can be useful. I've created a similar sandbox via cpp. And when I paint a big cursor, it's like real. https://gist.github.com/BaLiKfromUA/669c4e6ba3fffdf7db6dfad535976d97 – BaLiK Aug 17 '20 at 20:46