I used transparent click-through overlay based on the answer (that uses jna to enable click-through)
https://stackoverflow.com/a/28772306/1093872.
However the actual visible overlay is always behind one step, ie. when a repaint() gets called on the component, the update that should have been shown in the previous update is shown.
At first I suspected memory inconsistency issue because of the different thread. But I also tried the sure-to-be-correct approach using SwingWorker, and still faced the same problem.
AWTUtilities.setWindowOpaque(w, false); seems to be causing the problem in some way, because after I comment that out or set it to true, the problem disappears, but unfortunately the window is also not transparent (obviously).
I suspect the problem might be something related to double-buffering (the buffer is not swapped after painting only before next paint), but I don't really know. I also tried calling setDoubleBuffered(false) and setDoubleBuffered(true) on the component, but that does not change anything.
Also I realized that the problem isn't related to the jna part, because after removing that, the problem remains exactly the same.
Note that the index printing occurs at the same time as the drawing, so I know the paintComponent gets called in time, but the visible update only happens on the next call to it.
Any ideas on what can be the cause, and how to fix this?
import com.sun.awt.AWTUtilities;
import com.sun.jna.Native;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinUser;
import javax.swing.*;
import java.awt.*;
public class OverlayUpdateTest {
private static MyJComponent myJComponent;
public static void main(String[] args) {
setupOverlayWindow();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
myJComponent.increaseIndex();
myJComponent.repaint();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
private static void setupOverlayWindow() {
Window w = new Window(null);
myJComponent = new MyJComponent();
w.add(myJComponent);
w.pack();
w.setLocationRelativeTo(null);
w.setVisible(true);
w.setAlwaysOnTop(true);
/**
* This sets the background of the window to be transparent.
*/
AWTUtilities.setWindowOpaque(w, false);
setTransparent(w);
}
private static void setTransparent(Component w) {
WinDef.HWND hwnd = getHWnd(w);
int wl = User32.INSTANCE.GetWindowLong(hwnd, WinUser.GWL_EXSTYLE);
wl = wl | WinUser.WS_EX_LAYERED | WinUser.WS_EX_TRANSPARENT;
User32.INSTANCE.SetWindowLong(hwnd, WinUser.GWL_EXSTYLE, wl);
}
/**
* Get the window handle from the OS
*/
private static WinDef.HWND getHWnd(Component w) {
WinDef.HWND hwnd = new WinDef.HWND();
hwnd.setPointer(Native.getComponentPointer(w));
return hwnd;
}
private static class MyJComponent extends JComponent {
private int index = 0;
/**
* This will draw a black cross on screen.
*/
protected void paintComponent(Graphics g) {
g.setColor(Color.BLACK);
g.fillRect(0, getHeight() / 2 - 10, getWidth(), 20);
g.fillRect(getWidth() / 2 - 10, 0, 20, getHeight());
g.setColor(Color.RED);
g.drawString("i: " + index, 10, getFont().getSize() + 10);
System.out.println("i: " + index);
}
public Dimension getPreferredSize() {
return new Dimension(100, 100);
}
void increaseIndex() {
index++;
}
}
}
After much trial and error I found a working solution...
Changing Window w = new Window(null); to Window w = new JWindow(); solved the problem. I don't know why, but I'm happy with it.
Related
I'm trying to set the icon of a Java AWT application so it renders in native resolution on the Windows 10 taskbar (including when desktop scaling is set above 100%). It seems that by default, if an executable embeds an icon containing multiple sizes, Windows seems to pick a size larger than the actual size of taskbar icons and downsize it (at 100% scale it resizes the 32 pixel icon to 24, even if a 24 pixel icon is supplied, and similarly for other scales.)
I've solved this problem for C++ MFC applications by loading just the correctly sized icon as a resource and sending a WM_SETICON message to the window, which results in a nice sharp icon on the taskbar and alt-tab dialog.
smallIcon = (HICON)LoadImage( myInstance, MAKEINTRESOURCE(smallIconRes), IMAGE_ICON, smallIconSize, smallIconSize, LR_DEFAULTCOLOR );
SendMessage(hWnd, WM_SETICON, ICON_SMALL, (LPARAM)smallIcon);
bigIcon = (HICON)LoadImage( myInstance, MAKEINTRESOURCE(bigIconRes), IMAGE_ICON, bigIconSize, bigIconSize, LR_DEFAULTCOLOR );
SendMessage(hWnd, WM_SETICON, ICON_BIG, (LPARAM)bigIcon);
That approach doesn't seem to work for Java applications - a WM_SETICON message with wParam set to ICON_SMALL works fine, but the equivalent with ICON_BIG is ignored.
If I try to use Java's API to set the icon, by doing this
List<Image> icons = new ArrayList<Image>();
icons.add(windowIcons.getIcon(20)); // small icons are 20x20 pixels
icons.add(windowIcons.getIcon(30)); // large are 30x30 at 125% scale
setIconImages(icons);
the correct icon is used but it appears blurry, as if something has resized it to the "expected" size and then resized it back. Left here is how it appears, right is the contents of the icon file.
So, my question is: what can I do in this Java application to make Windows render the icon I give it on the taskbar without scaling it and blurring the details?
There is indeed a scaling function called getScaledIconImage() in sun.awt.SunToolkit which is is always used when setting the icons. You must bypass this function in order to get an unaliased icon. So what you need is a replacement for java.awt.Window.setIconImages() method.
Provided several icon images Icon16x16.png, Icon24x24.png, etc. This is an example of a customSetIconImages() which puts a crisp 24x24 pixels icon in the taskbar of Windows 10.
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.ImageIcon;
import java.awt.peer.WindowPeer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
#SuppressWarnings("serial")
public class MyFrame extends Frame implements WindowListener {
final Image i16, i24, i32, i48;
MyFrame() throws Exception {
i16 = Toolkit.getDefaultToolkit().getImage("Icon16x16.png");
i24 = Toolkit.getDefaultToolkit().getImage("Icon24x24.png");
i32 = Toolkit.getDefaultToolkit().getImage("Icon32x32.png");
i48 = Toolkit.getDefaultToolkit().getImage("Icon48x48.png");
addWindowListener(this);
setSize(500,300);
setTitle("Unaliased icon example");
setLayout(new FlowLayout());
setVisible(true);
}
public synchronized void customSetIconImages(java.util.List<Image> icons) throws Exception {
Field windowIcons = Class.forName("java.awt.Window").getDeclaredField("icons");
windowIcons.setAccessible(true);
windowIcons.set(this, new ArrayList<Image>(icons));
if (getPeer() != null)
updateIconImages(i24, 24, 24, i24, 24, 24);
firePropertyChange("iconImage", null, null);
}
public void updateIconImages(Image big, int bw, int bh, Image small, int sw, int sh) throws Exception {
DataBufferInt iconData = getUnscaledIconData(big, bw, bh);
DataBufferInt iconSmData = getUnscaledIconData(small, sw, sh);
WindowPeer peer = (WindowPeer) getPeer();
Method setIconImagesData = Class.forName("sun.awt.windows.WWindowPeer").getDeclaredMethod("setIconImagesData", int[].class, int.class, int.class, int[].class, int.class, int.class);
setIconImagesData.setAccessible(true);
setIconImagesData.invoke(peer, iconData.getData(), bw, bh, iconSmData.getData(), sw, sh);
}
public static DataBufferInt getUnscaledIconData(Image image, int w, int h) {
Image temporary = new ImageIcon(image).getImage();
BufferedImage buffImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = buffImage.createGraphics();
g2d.drawImage(temporary, 0, 0, null);
g2d.dispose();
Raster raster = buffImage.getRaster();
DataBuffer buffer = raster.getDataBuffer();
return (DataBufferInt) buffer;
}
#Override
public void windowOpened(WindowEvent arg0) {
try {
customSetIconImages(Arrays.asList(i24));
} catch (Exception e) {
System.err.println(e.getClass().getName()+" "+e.getMessage());
}
}
#Override
public void windowActivated(WindowEvent arg0) {
}
#Override
public void windowClosed(WindowEvent arg0) {
}
#Override
public void windowClosing(WindowEvent arg0) {
dispose();
}
#Override
public void windowDeactivated(WindowEvent arg0) {
}
#Override
public void windowDeiconified(WindowEvent arg0) {
}
#Override
public void windowIconified(WindowEvent arg0) {
}
public static void main(String args[]) throws Exception {
MyFrame fr = new MyFrame();
}
}
As #df778899 said, inside sun.awt.windows.WWindowPeer there are four private native methods which you can call t determine system icons size. You can combine the information returned by these methods with your own version getScaledIconImage() that performs unaliasing or not as yoou wish.
Last, note that this is a very dirty hack just for getting an unaliased icon. I've only tested in in Java 8 and Windows 10. And there are high chances that it doesn't work in newer versions of Java.
This won't be the answer you're hoping for, but this looks like a problem at the JDK level.
The window icons are handled by the sun.awt.windows.WWindowPeer class, which in turn makes a few native method calls, but there is enough to see in the source for this to point to the problem. Please read the important bit here.
Essentially, regardless of how many icon image sizes are provided, it will only pick out two sizes - for the WWindowPeer.getSysIconWidth() and getSysSmIconWidth() - to pass into the native setIconImagesData() method.
The getSysIconWidth() and getSysSmIconWidth() methods are also native, but it is possible to directly check their return values:
JFrame frame = new JFrame();
runOnPeer(frame, "getSysIconWidth");
runOnPeer(frame, "getSysIconHeight");
runOnPeer(frame, "getSysSmIconWidth");
runOnPeer(frame, "getSysSmIconHeight");
private void runOnPeer(JFrame frame, String methodName) {
//JDK8 style
//ComponentPeer peer = frame.getPeer();
//JDK11 style
Field peerField = Component.class.getDeclaredField("peer");
peerField.setAccessible(true);
Object peer = peerField.get(frame);
Method method = Class.forName("sun.awt.windows.WWindowPeer")
.getDeclaredMethod(methodName);
method.setAccessible(true);
System.out.println(methodName + "()=" + method.invoke(peer));
}
... which returns this on Windows 10 ...
getSysIconWidth()=32
getSysIconHeight()=32
getSysSmIconWidth()=16
getSysSmIconHeight()=16
As you say, clearly one of these image sizes is then being scaled for the taskbar.
I'm migrating my Swing app to Java 11 to take advantage of the HiDPI display support. I'm using a Samsung monitor with resolution set to 3840x2160, scaling at 125%, with Windows 10.
Although java 9 and above are advertised as properly handling HiDPI scaling, when displaying a simple JTable, the gridlines appear of different thickness, as shown here:
Here's the code for this:
import javax.swing.*;
public class TestTable {
public static void main(String[] args) {
new TestTable();
}
public TestTable() {
JTable table = new JTable(12,6);
JDialog dialog = new JDialog();
JScrollPane sp = new JScrollPane(table);
table.setShowGrid(true);
table.setRowHeight(25);
dialog.setContentPane(sp);
dialog.setSize(300,300);
dialog.setVisible(true);
dialog.setLocationRelativeTo(null);
}
}
However, when setting the Nimbus L&F, the problem goes away:
import javax.swing.*;
public class TestTable {
public static void main(String[] args) {
try {
for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
if ("Nimbus".equals(info.getName())) {
UIManager.setLookAndFeel(info.getClassName());
break;
}
}
} catch (Exception e) { }
new TestTable();
}
public TestTable() {
JTable table = new JTable(12,6);
JDialog dialog = new JDialog();
JScrollPane sp = new JScrollPane(table);
table.setShowGrid(true);
table.setRowHeight(25);
dialog.setContentPane(sp);
dialog.setSize(300,300);
dialog.setVisible(true);
dialog.setLocationRelativeTo(null);
}
}
How can I achieve the same with the default Windows L&F ?
(Same behavior is observed with java 9 & 10)
The difference is how the two look and feels render their grid lines.
The default look and feel MetalLookAndFeel (and the WindowsLookAndFeel) is based around BasicLookAndFeel which uses the BasicTableUI class to render the JTable. In BasicTableUI.paintGrid() it calls such as SwingUtilities2.drawHLine() - which actually calls Graphics.fillRect() which is the problem.
The Nimbus look and feel uses the SynthTableUI class. In SynthTableUI.paintGrid() it does ultimately call Graphics.drawLine(), which clearly draws a cleaner line under scaling.
As you say, that sounds like a bug in the main look and feels under HiDPI.
It is possible to create a workaround for this, though it's not particularly elegant.
With a custom version of the Graphics that is being used, it's possible to override fillRect() to use drawLine() instead, if the width or height is 1. This custom Graphics can be introduced specifically when painting the table:
JTable table = new JTable(12, 6) {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(new GraphicsWorkaround(g));
}
};
(An anonymous subclass is just used for brevity).
Then the GraphicsWorkaround class is written as a wrapper to the true g that was passed in. Subclassing DebugGraphics here is just a trick to save having to write delegate calls in all the other methods in Graphics:
import java.awt.Graphics;
import javax.swing.DebugGraphics;
public class GraphicsWorkaround extends DebugGraphics {
private final Graphics g;
public GraphicsWorkaround(Graphics g) {
super(g);
this.g = g;
}
#Override
public Graphics create() {
return new GraphicsWorkaround(g.create());
}
#Override
public void fillRect(int x, int y, int width, int height) {
if (width == 1)
g.drawLine(x, y, x, y + height - 1);
else if (height == 1)
g.drawLine(x, y, x + width - 1, y);
else
super.fillRect(x, y, width, height);
}
}
(The create() method is there to handle the internal scratchGraphics clone created in JComponent.paintComponent() ).
This then enables drawLine() to be called after all, which looked much better at 125% scaling.
I have a simple physics loop that does a calculation for a time interval, waits for the interval to pass, and then renders the results on the screen. It's very simple code (even though the timing is probably wrong, but that's exactly what I'm trying to learn about) and works well when I am moving the mouse around the screen.
package physicssim;
import java.awt.Graphics;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class PhysicsSim extends JFrame {
private static class PhysicsObject {
public PhysicsObject(double x, double y, double v_x, double v_y) {
this.x = x;
this.y = y;
this.v_x = v_x;
this.v_y = v_y;
}
public double x;
public double y;
public double v_x;
public double v_y;
}
PhysicsObject particle;
boolean running = true;
DrawPane drawPane;
public PhysicsSim() {
particle = new PhysicsObject(10,10, .1, .2);
drawPane = new DrawPane(particle);
this.setSize(800,600);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setContentPane(drawPane);
this.setVisible(true);
}
private static class DrawPane extends JPanel {
PhysicsObject p;
public DrawPane(PhysicsObject p) {
this.p = p;
}
#Override
public void paint(Graphics g) {
super.paint(g); //To change body of generated methods, choose Tools | Templates.
g.fillOval((int)p.x, (int) p.y, 10, 10);
}
}
public void start() {
int FPS = 60;
long TIME_BETWEEN_FRAMES_NS = 1000000000/FPS;
// Initial draw
drawPane.repaint();
long lastDrawTime = System.nanoTime();
while(running) {
// Update physics
particle.x+=particle.v_x*(TIME_BETWEEN_FRAMES_NS*.0000001);
particle.y+=particle.v_y*(TIME_BETWEEN_FRAMES_NS*.0000001);
// While there is time until the next draw wait
while(TIME_BETWEEN_FRAMES_NS > (System.nanoTime()-lastDrawTime)) {
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
Logger.getLogger(PhysicsSim.class.getName()).log(Level.SEVERE, null, ex);
}
}
drawPane.repaint();
long currentTime = System.nanoTime();
System.out.println(currentTime - lastDrawTime);
lastDrawTime = currentTime;
}
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
PhysicsSim sim = new PhysicsSim();
sim.start();
}
}
The last bit about printing the time difference was just a sanity check to make sure that it was in fact calling around the requested interval. The results are fairly consistent so I don't see why there should be any choppiness.
As I mentioned above, this code works great if I a moving the mouse around the screen, everything is smooth.
If I am not moving the mouse it becomes very choppy until I start moving the mouse over the application.
I assume this is something simple, but I hope that you guys can help me. Thank you.
Alright, it looks like my problem was I was drawing directly to g in paint(). After replacing with the following everything worked correctly.
#Override
public void paint(Graphics g) {
BufferedImage img = new BufferedImage(800, 600, BufferedImage.TYPE_3BYTE_BGR);
img.getGraphics().fillOval((int) p.x, (int) p.y, 10, 10);
g.drawImage(img, 0, 0, null);
}
I was considering deleting this code snippet because it's rough and shameful, but maybe it will help someone else. Happy coding.
Is there a way for me to get the X and Y values of a window in java? I read that I'll have to use runtime, since java can't mess directly, however I am not so sure of how to do this. Can anyone point me some links/tips on how to get this?
To get the x and y position of "any other unrelated application" you're going to have to query the OS and that means likely using either JNI, JNA or some other scripting utility such as AutoIt (if Windows). I recommend either JNA or the scripting utility since both are much easier to use than JNI (in my limited experience), but to use them you'll need to download some code and integrate it with your Java application.
EDIT 1
I'm no JNA expert, but I do fiddle around with it some, and this is what I got to get the window coordinates for some named window:
import java.util.Arrays;
import com.sun.jna.*;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.win32.*;
public class GetWindowRect {
public interface User32 extends StdCallLibrary {
User32 INSTANCE = (User32) Native.loadLibrary("user32", User32.class,
W32APIOptions.DEFAULT_OPTIONS);
HWND FindWindow(String lpClassName, String lpWindowName);
int GetWindowRect(HWND handle, int[] rect);
}
public static int[] getRect(String windowName) throws WindowNotFoundException,
GetWindowRectException {
HWND hwnd = User32.INSTANCE.FindWindow(null, windowName);
if (hwnd == null) {
throw new WindowNotFoundException("", windowName);
}
int[] rect = {0, 0, 0, 0};
int result = User32.INSTANCE.GetWindowRect(hwnd, rect);
if (result == 0) {
throw new GetWindowRectException(windowName);
}
return rect;
}
#SuppressWarnings("serial")
public static class WindowNotFoundException extends Exception {
public WindowNotFoundException(String className, String windowName) {
super(String.format("Window null for className: %s; windowName: %s",
className, windowName));
}
}
#SuppressWarnings("serial")
public static class GetWindowRectException extends Exception {
public GetWindowRectException(String windowName) {
super("Window Rect not found for " + windowName);
}
}
public static void main(String[] args) {
String windowName = "Document - WordPad";
int[] rect;
try {
rect = GetWindowRect.getRect(windowName);
System.out.printf("The corner locations for the window \"%s\" are %s",
windowName, Arrays.toString(rect));
} catch (GetWindowRect.WindowNotFoundException e) {
e.printStackTrace();
} catch (GetWindowRect.GetWindowRectException e) {
e.printStackTrace();
}
}
}
Of course for this to work, the JNA libraries would need to be downloaded and placed on the Java classpath or in your IDE's build path.
This is easy to do with the help of the end user. Just get them to click on a point in a screen shot.
E.G.
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
/** Getting a point of interest on the screen.
Requires the MotivatedEndUser API - sold separately. */
class GetScreenPoint {
public static void main(String[] args) throws Exception {
Robot robot = new Robot();
final Dimension screenSize = Toolkit.getDefaultToolkit().
getScreenSize();
final BufferedImage screen = robot.createScreenCapture(
new Rectangle(screenSize));
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JLabel screenLabel = new JLabel(new ImageIcon(screen));
JScrollPane screenScroll = new JScrollPane(screenLabel);
screenScroll.setPreferredSize(new Dimension(
(int)(screenSize.getWidth()/2),
(int)(screenSize.getHeight()/2)));
final Point pointOfInterest = new Point();
JPanel panel = new JPanel(new BorderLayout());
panel.add(screenScroll, BorderLayout.CENTER);
final JLabel pointLabel = new JLabel(
"Click on any point in the screen shot!");
panel.add(pointLabel, BorderLayout.SOUTH);
screenLabel.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent me) {
pointOfInterest.setLocation(me.getPoint());
pointLabel.setText(
"Point: " +
pointOfInterest.getX() +
"x" +
pointOfInterest.getY());
}
});
JOptionPane.showMessageDialog(null, panel);
System.out.println("Point of interest: " + pointOfInterest);
}
});
}
}
Typical output
Point of interest: java.awt.Point[x=342,y=43]
Press any key to continue . . .
A little late to the party here but will add this to potentially save others a little time. If you are using a more recent version of JNA then WindowUtils.getAllWindows() will make this much easier to accomplish.
I am using the most recent stable versions as of this post from the following maven locations:
JNA Platform - net.java.dev.jna:jna-platform:5.2.0
JNA Core - net.java.dev.jna:jna:5.2.0
Java 8 Lambda (Edit: rect is a placeholder and will need to be final or effectively final to work in a lambda)
//Find IntelliJ IDEA Window
//import java.awt.Rectangle;
final Rectangle rect = new Rectangle(0, 0, 0, 0); //needs to be final or effectively final for lambda
WindowUtils.getAllWindows(true).forEach(desktopWindow -> {
if (desktopWindow.getTitle().contains("IDEA")) {
rect.setRect(desktopWindow.getLocAndSize());
}
});
Other Java
//Find IntelliJ IDEA Window
Rectangle rect = null;
for (DesktopWindow desktopWindow : WindowUtils.getAllWindows(true)) {
if (desktopWindow.getTitle().contains("IDEA")) {
rect = desktopWindow.getLocAndSize();
}
}
Then within a JPanel you can draw a captured image to fit (Will stretch image if different aspect ratios).
//import java.awt.Robot;
g2d.drawImage(new Robot().createScreenCapture(rect), 0, 0, getWidth(), getHeight(), this);
When I try to use myCustomPanel.add(someComponent) it does not add...
Here is my custom JPanel class:
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JPanel;
/**
*
* #author Jacob
*/
public class OSXMainPanel extends JPanel {
public static final long serialVersionUID = 24362462L;
private Image image;
public OSXMainPanel() {
super.setOpaque(true);
try {
image = javax.imageio.ImageIO.read(new java.net.URL(getClass().getResource("/assets/background.png"), "background.png"));
} catch (Exception e) {}
}
#Override
protected void paintComponent(Graphics g) {
if (isOpaque())
{
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
}
for(int w = 0; w < this.getWidth(); w = w + 50) {
for(int h = 0; h < this.getHeight(); h = h + 50) {
g.drawImage(image, w, h, 50, 50, this);
}
}
}
}
The reason this isn't working is because your paintComponent method isn't painting the added components. Calling super.paintComponent(g) at the start of the paintComponent method should fix this.
It should not be necessary to call super.paintComponent(Graphics g) to paint child components. The call is useful, to draw the background, but not strictly necessary.
I tested the code on Java 6 and it worked fine for me. The only modification I made was to add the following line in the constructor:
add(new JLabel("Test"));
I do not have the background image file so the image drawing code was doing nothing. Either the background image is somehow obscuring the child components or there is a bug in the code that adds a child component. Try commenting out the drawImage call and see if child components become visible.
I would call updateUI()
myPanel.add(new JLabel("wanna see it"));
// change of look and feel
myPanel.updateUI();
after adding the component - if you want to update whole look and feel. Otherwise use revalidate().
myPanel.add(new JLabel("wanna see this"));
myPanel.revalidate();