A full but slightly swampy-feeling virtual mouse delta solution is at the bottom. Doesn't need libraries, tested on Java 8/9/11 on Windows 10 with desktop scaling 200%.
This solution by Tejas Shah works for me 99%. (I'm still looking for my jaw, it must be here somewhere.)
Here's my commented Java adaptation from what I suspect to be Kotlin. It works on my 4K 200% Desktop in Java 8 and 9. My diagonal mouse move loop still looks a wee bit weird, but the mouse goes 99% where you want it instead of jumping around.
It's not 100%! On Java 8, when I keep putting the mouse at a fixed position every 20 ms on Swing thread, the mouse will sometimes be off by 1 pixel if I have moved it a certain way. This is probably a rounding issue. (Using Math.round below does not solve it.) Similarly, on Java 9 the mouse will either be in that place, or it will keep jumping between that place and one pixel to the left/top of that place.
To get the correct scaling factor (e.g. 2
on a Windows system at 200% scaling), you can use my crappy but very elaborate GUIScaling class, which also supports custom graphics scaling, mouse scaling to and from, etc., and keeps it all consistent on Java 8&9 (or other) between 100% and 200% (or other) scaling.
final private Robot robot;
private void mouseMoveFixed(final int x,
final int y) {
final double scaleFactor = GUIScaling.GUISCALINGFACTOR_SYSTEM; // If your Windows system is at 200% scaling, put a 2 here.
final double modFactor = 1 - (1 / scaleFactor);
final Point origin = MouseInfo.getPointerInfo().getLocation();
final int deltaX = x - origin.x;
final int deltaY = y - origin.y;
final int finalX = (int) (x - deltaX * modFactor); // I don't know if this needs to be rounded.
final int finalY = (int) (y - deltaY * modFactor); // I couldn't spot a difference if Math.round() was used.
robot.mouseMove(finalX, finalY);
}
In the future, if ever you need a "virtual mouse" delta, just call this static method:
JavaRobotMouseDelta.calcVirtualMouseDelta()
It will return the delta from screen center and set the mouse to that location. If there is no delta to be returned, the method returns null.
If desktop scaling is not 100%, the method will use different approaches depending on Java 8, 9, and 11+. All of which are imprecise and thus will give a slightly swampy mouse feeling. But it's better than nothing, and it's all encapsulated. Enjoy!
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* [v1, 2022-01-15 12!00 UTC] by Dreamspace President
* <p>
* Uses 2 values from my GUIScaling class, see https://stackoverflow.com/questions/43057457/jdk-9-high-dpi-disable-for-specific-panel/46630710#46630710
*/
final public class JavaRobotMouseDelta {
/**
* @return NULL if there is no delta and thus no action should be taken.
*/
public static Dimension calcVirtualMouseDelta() {
final int centerX;
final int centerY;
int deltaX;
int deltaY;
{
final PointerInfo pointerInfo = MouseInfo.getPointerInfo();
final float halfScaled = (float) (2 * GUIScaling.GUISCALINGFACTOR_FONTINCUSTOMGRAPHICSCONTEXT);
final DisplayMode displayMode = pointerInfo.getDevice().getDisplayMode();
centerX = Math.round(displayMode.getWidth() / halfScaled);
centerY = Math.round(displayMode.getHeight() / halfScaled);
final Point mouseLocation = pointerInfo.getLocation();
deltaX = mouseLocation.x - centerX;
deltaY = mouseLocation.y - centerY;
}
if (GUIScaling.GUISCALINGFACTOR_SYSTEM == 1) {
if (deltaX == 0 && deltaY == 0) {
// Ignoring mouse move event caused by Robot centering call.
return null;
}
ROBOT.mouseMove(centerX, centerY);
return new Dimension(deltaX, deltaY);
} else {
// The following comments and logic are for desktop scaling 200%.
// I did not yet bother trying to figure this out for other values.
// Java 11 etc. finally have a FIXED Robot.mouseMove()! HOWEVER: You will be blindfolded,
// so you get a pixel offset of 1 every once in a while because moving the mouse from center to the LEFT OR TOP
// will INSTANTLY cause a mouse event with a new position. Moving it right or bottom will NOT cause an event,
// because e.g. a 3840x2160 (twice 1920x1080) display will ALL THROUGHOUT inside Java have HALVED coordinates,
// e.g. you set window width 800, reality will be 1600, but you will have getWidth 800, and the mouse coordinates
// will also be treated like this - EVEN FOR THE ROBOT!
// Java 8/9 (and below) have a broken Robot.mouseMove() under Windows!=100%, but thanks to Tejas Shah's solution below,
// we can work around that. Without JNA or looping tricks.
// HOWEVER: We need to lose 1 pixel of motion. And we need to thoroughly rape the resulting delta DEPENDING ON JAVA VERSION.
// Rant: I want an additional mouseMove method that uses TRUE pixels. And I want access to the HARDWARE MOUSE DELTA,
// scaled by system (acceleration) as well as unscaled. And I want access to scanCode (KeyEvent) without Reflection
// and on ALL systems, not just Windows! ... Rant over.
if (deltaX < 0) {
deltaX += 1;
} else if (deltaX > 0) {
deltaX -= 1;
}
if (deltaY < 0) {
deltaY += 1;
} else if (deltaY > 0) {
deltaY -= 1;
}
if (deltaX == 0 && deltaY == 0) {
// Ignoring mouse move event caused by Robot centering call.
return null;
}
final int deltaXRaped;
final int deltaYRaped;
if (JAVAVERSIONMAJOR >= 11) {
deltaXRaped = deltaX < 0 ? deltaX : Math.round(deltaX * (1 + RAND.nextFloat() * 0.75f));
deltaYRaped = deltaY < 0 ? deltaY : Math.round(deltaY * (1 + RAND.nextFloat() * 0.75f));
ROBOT.mouseMove(centerX, centerY);
} else if (JAVAVERSIONMAJOR >= 9) {
deltaXRaped = deltaX > 0 ? deltaX : Math.round(deltaX * (1 + RAND.nextFloat() * 0.75f));
deltaYRaped = deltaY > 0 ? deltaY : Math.round(deltaY * (1 + RAND.nextFloat() * 0.75f));
mouseMoveFixed(centerX, centerY);
} else {
deltaXRaped = deltaX < 0 ? deltaX : Math.round(deltaX * (1 + RAND.nextFloat() * 0.75f));
deltaYRaped = deltaY < 0 ? deltaY : Math.round(deltaY * (1 + RAND.nextFloat() * 0.75f));
mouseMoveFixed(centerX, centerY);
}
return new Dimension(deltaXRaped, deltaYRaped);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// UTILITIES NEEDED ABOVE.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
final public static int JAVAVERSIONMAJOR = getJavaVersionMajor();
final public static Robot ROBOT = getRobot();
final public static Random RAND = new Random();
private static Robot getRobot() {
try {
return new Robot();
} catch (AWTException e) {
e.printStackTrace();
System.exit(-1);
throw new Error();
}
}
// From https://stackoverflow.com/a/2591122/3500521 (Aaron Digulla)
private static int getJavaVersionMajor() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf(".");
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
// Converted from the ?Kotlin? original at https://gist.github.com/tejashah88/201daacada3a785f86b8cf069ead63f5 (Tejas Shah)
public static void mouseMoveFixed(final int x,
final int y) {
final double scaleFactor = GUIScaling.GUISCALINGFACTOR_SYSTEM; // If your Windows system is at 200% scaling, put a 2 here.
final double modFactor = 1 - (1 / scaleFactor);
final Point origin = MouseInfo.getPointerInfo().getLocation();
final int deltaX = x - origin.x;
final int deltaY = y - origin.y;
int finalX = (int) (x - deltaX * modFactor); // I don't know if this needs to be rounded.
int finalY = (int) (y - deltaY * modFactor); // I couldn't spot a difference if Math.round() was used.
ROBOT.mouseMove(finalX, finalY);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// BONUS, YOU CAN DELETE THIS.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
final private static Cursor INVISIBLE_CURSOR;
static {
final BufferedImage cursorImg = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
INVISIBLE_CURSOR = Toolkit.getDefaultToolkit().createCustomCursor(cursorImg,
new Point(0, 0),
"invisibleCursor");
}
public static void setMouseBusy(final Component windowOrComponent) {
if (windowOrComponent != null) {
windowOrComponent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
}
}
public static void setMouseIdle(final Component windowOrComponent) {
if (windowOrComponent != null) {
windowOrComponent.setCursor(Cursor.getDefaultCursor());
}
}
public static void setMouseInvisible(final Component windowOrComponent) {
if (windowOrComponent != null) {
windowOrComponent.setCursor(INVISIBLE_CURSOR);
}
}
public static void setMouseCrossHair(final Component windowOrComponent) {
if (windowOrComponent != null) {
windowOrComponent.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
}
}
}