Windows下的Java多显示处理-缩放显示的错误?

Windows下的Java多显示处理-缩放显示的错误?,java,windows,awt,display,jna,Java,Windows,Awt,Display,Jna,tl;dr 在Windows 10下,如果我将次要显示放在主要显示的右侧,并对次要显示应用缩放(例如150%),则显示坐标(由Java API返回)重叠,而不是让显示边界并排放置。换句话说,如果我慢慢地将鼠标从主屏幕的左边缘移动到次屏幕的右边缘,Java的APIMouseInfo.getPointerInfo().getLocation()会将X位置从0增加到1920,然后一旦光标进入第二个屏幕,该值会跳回到1280,然后再次增加到2560。因此,对于不同的区域,1280-1920范围返回两次

tl;dr

在Windows 10下,如果我将次要显示放在主要显示的右侧,并对次要显示应用缩放(例如150%),则显示坐标(由Java API返回)重叠,而不是让显示边界并排放置。换句话说,如果我慢慢地将鼠标从主屏幕的左边缘移动到次屏幕的右边缘,Java的API
MouseInfo.getPointerInfo().getLocation()
会将X位置从0增加到1920,然后一旦光标进入第二个屏幕,该值会跳回到1280,然后再次增加到2560。因此,对于不同的区域,1280-1920范围返回两次

在文章的最后,我加入了一个(更新的)演示,使问题变得显而易见。请毫不犹豫地尝试一下,并向我们汇报

长版本:

这篇文章提供了(太)多的上下文,但也是为了分享我在搜索主题时学到的东西

首先,为什么要麻烦?因为我正在用Java构建一个屏幕捕获应用程序,它需要正确处理多显示器配置,包括应用Windows缩放功能的显示器

使用Java API(
GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()
),只要缩放为100%,就可以观察到主显示器的左上角位于原点(0,0),而其他显示器的坐标“紧挨着”主显示器

下面的图片是用文章末尾的代码制作的

例如,如果我们有两个全高清显示器,主显示器的左上角位于(0,0),而

  • 如果次节点位于同一标高的右侧,则其左上角为(1920,0):

  • 如果次节点位于同一标高的左侧,则其左上角为(-1920,0):

  • 如果次级线圈位于下方,水平对齐,则其左上角为(01080):

  • 如果辅助设备位于上方,水平对齐,则其左上角为(0,-1080):

  • 如果显示器未对齐,依此类推:

  • 或使用不同的分辨率:

但是,如果二次显示进行了缩放,则会出现问题:缩放因子似乎不仅应用于其尺寸,还应用于其原点,该原点更接近(0,0)

如果第二个在左边,这是有意义的。例如,当辅助1920x1080的缩放比例为150%时,它会使逻辑1280x720位于(-1280,0):

但是,如果次原点位于右侧,则原点也将缩放到(1280,0),从而更接近原点并使其与主原点“重叠”:

换句话说,如果鼠标位于(1800,0)-见上面的红点-我看不出有办法知道它是否位于第一个显示器的右侧(距离右边缘120px)或第二个显示器的左侧(距离左边缘520px)。在这种情况下,将鼠标从主显示器移动到次显示器时,鼠标的X位置在到达主显示器边界时“向后跳”

在屏幕上定位窗口也是如此。如果我将对话框的X位置设置为1800,我无法知道它将在哪里打开

经过多次浏览之后,一些答案表明,查询Windows缩放的唯一方法是使用本机调用。实际上,使用JNA,可以获得显示器的物理大小(尽管答案似乎表明调用应该返回逻辑大小)。也就是说,JNA调用忽略了比例因子,当比例为100%时,其行为与Java API完全相同:

我是不是错过了什么

不知道缩放因子是一个小问题,但无法分辨鼠标在哪个显示器上,或者无法在显示器上定位我想要的窗口,这对我来说是一个真正的问题。这是Java Bug吗

注意:这是上面使用的应用程序的代码,在Windows1064B上使用OpenJDK14运行。它显示了Java感知到的显示设置和鼠标位置的缩小版本。如果您在小矩形内单击并拖动,它还可以在真实屏幕上放置和移动一个小对话框。信用:UI的灵感来自WheresMyMouse代码发布的地方

按原样,代码只使用JavaAPI。 如果要与JNA进行比较,请搜索标记为“JNA_ONLY”的4个块,取消对它们的注释,然后添加JNA库。然后,演示将在JNA和JavaAPI之间切换,以便在每次右键单击时显示屏幕边界和鼠标光标。该对话框在此版本中从不使用JNA

// JNA_ONLY
//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.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

/**
 * Java multi-display detection and analysis.
 * UI idea based on WheresMyMouse - https://stackoverflow.com/a/21592711/13551878
 */
public class ShowDisplays {

    private static boolean useJna = false;

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            JFrame frame = new JFrame("Display Configuration");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLayout(new BorderLayout());
            frame.add(new TestPane());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    public static class TestPane extends JPanel {
        private List<Rectangle> screenBounds;
        JDialog dlg;

        public TestPane() {
            screenBounds = getScreenBounds();
            // refresh screen details every second to reflect changes in Windows Preferences in "real time"
            new Timer(1000, e -> screenBounds = getScreenBounds()).start();

            // Refresh mouse position at 25fps
            new Timer(40, e -> repaint()).start();

            MouseAdapter mouseAdapter = new MouseAdapter() {

                public void mouseClicked(MouseEvent e) {
                    if (e.getButton() != MouseEvent.BUTTON1) {
                        useJna = !useJna;
                        repaint();
                    }
                }

                @Override
                public void mousePressed(MouseEvent e) {
                    System.out.println(e.getButton());
                    if (e.getButton() == MouseEvent.BUTTON1) {
                        if (!dlg.isVisible()) {
                            dlg.setVisible(true);
                        }
                        moveDialogTo(e.getPoint());
                    }
                }


                @Override
                public void mouseDragged(MouseEvent e) {
                    moveDialogTo(e.getPoint());
                }


                private void moveDialogTo(Point mouseLocation) {
                    final Rectangle surroundingRectangle = getSurroundingRectangle(screenBounds);
                    double scaleFactor = Math.min((double) getWidth() / surroundingRectangle.width, (double) getHeight() / surroundingRectangle.height);

                    int xOffset = (getWidth() - (int) (surroundingRectangle.width * scaleFactor)) / 2;
                    int yOffset = (getHeight() - (int) (surroundingRectangle.height * scaleFactor)) / 2;

                    int screenX = surroundingRectangle.x + (int) ((mouseLocation.x - xOffset) / scaleFactor);
                    int screenY = surroundingRectangle.y + (int) ((mouseLocation.y - yOffset) / scaleFactor);

                    dlg.setLocation(screenX - dlg.getWidth() / 2, screenY - dlg.getHeight() / 2);
                }


            };

            addMouseListener(mouseAdapter);
            addMouseMotionListener(mouseAdapter);

            // Prepare the test dialog
            dlg = new JDialog();
            dlg.setTitle("Here");
            dlg.setSize(50, 50);
            dlg.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);

        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();

            // Mouse position
            Point mousePoint = getMouseLocation();

            g2d.setColor(Color.BLACK);
            g2d.fillRect(0, 0, getWidth(), getHeight());

            final Rectangle surroundingRectangle = getSurroundingRectangle(screenBounds);
            double scaleFactor = Math.min((double) getWidth() / surroundingRectangle.width, (double) getHeight() / surroundingRectangle.height);

            int xOffset = (getWidth() - (int) (surroundingRectangle.width * scaleFactor)) / 2;
            int yOffset = (getHeight() - (int) (surroundingRectangle.height * scaleFactor)) / 2;

            g2d.setColor(Color.BLUE);
            g2d.fillRect(xOffset, yOffset, (int) (surroundingRectangle.width * scaleFactor), (int) (surroundingRectangle.height * scaleFactor));

            Font defaultFont = g2d.getFont();
            for (int screenIndex = 0; screenIndex < screenBounds.size(); screenIndex++) {
                Rectangle screen = screenBounds.get(screenIndex);
                Rectangle scaledRectangle = new Rectangle(
                        xOffset + (int) ((screen.x - surroundingRectangle.x) * scaleFactor),
                        yOffset + (int) ((screen.y - surroundingRectangle.y) * scaleFactor),
                        (int) (screen.width * scaleFactor),
                        (int) (screen.height * scaleFactor));

                // System.out.println(screen + " x " + scaleFactor + " -> " + scaledRectangle);
                g2d.setColor(Color.DARK_GRAY);
                g2d.fill(scaledRectangle);
                g2d.setColor(Color.GRAY);
                g2d.draw(scaledRectangle);

                // Screen text details
                g2d.setColor(Color.WHITE);

                // Display number
                final Font largeFont = new Font(defaultFont.getName(), defaultFont.getStyle(), (int) (screen.height * scaleFactor) / 2);
                g2d.setFont(largeFont);
                String label = String.valueOf(screenIndex + 1);
                FontRenderContext frc = g2d.getFontRenderContext();
                TextLayout layout = new TextLayout(label, largeFont, frc);
                Rectangle2D bounds = layout.getBounds();
                g2d.setColor(Color.WHITE);
                g2d.drawString(
                        label,
                        (int) (scaledRectangle.x + (scaledRectangle.width - bounds.getWidth()) / 2),
                        (int) (scaledRectangle.y + (scaledRectangle.height + bounds.getHeight()) / 2)
                );

                // Resolution + corner
                final Font smallFont = new Font(defaultFont.getName(), defaultFont.getStyle(), (int) (screen.height * scaleFactor) / 10);
                g2d.setFont(smallFont);

                // Resolution
                String resolution = screen.width + "x" + screen.height;
                layout = new TextLayout(resolution, smallFont, frc);
                bounds = layout.getBounds();
                g2d.drawString(
                        resolution,
                        (int) (scaledRectangle.x + (scaledRectangle.width - bounds.getWidth()) / 2),
                        (int) (scaledRectangle.y + scaledRectangle.height - bounds.getHeight())
                );

                // Corner
                String corner = "(" + screen.x + "," + screen.y + ")";
                g2d.drawString(
                        corner,
                        scaledRectangle.x,
                        (int) (scaledRectangle.y + bounds.getHeight() * 1.5)
                );

            }

            g2d.setFont(defaultFont);
            FontMetrics fm = g2d.getFontMetrics();

            if (mousePoint != null) {
                g2d.fillOval(xOffset + (int) ((mousePoint.x - surroundingRectangle.x) * scaleFactor) - 2,
                        yOffset + (int) ((mousePoint.y - surroundingRectangle.y) * scaleFactor) - 2,
                        4,
                        4
                );
                g2d.drawString("Mouse pointer is at (" + mousePoint.x + "," + mousePoint.y + ")", 4, fm.getHeight());
            }

            g2d.drawString("Click and drag in this area to move a dialog on the actual screens", 4, fm.getHeight() * 2);

            // JNA_ONLY
            // g2d.drawString("Now using " + (useJna ? "JNA" : "Java API") + ". Right-click to toggle", 4, fm.getHeight() * 3);

            g2d.dispose();
        }
    }

    public static Rectangle getSurroundingRectangle(List<Rectangle> screenRectangles) {
        Rectangle surroundingBounds = null;
        for (Rectangle screenBound : screenRectangles) {
            if (surroundingBounds == null) {
                surroundingBounds = new Rectangle(screenRectangles.get(0));
            }
            else {
                surroundingBounds.add(screenBound);
            }
        }
        return surroundingBounds;
    }

    private static Point getMouseLocation() {
        // JNA_ONLY
//        if (useJna) {
//            final WinDef.POINT point = new WinDef.POINT();
//            if (User32.INSTANCE.GetCursorPos(point)) {
//                return new Point(point.x, point.y);
//            }
//            else {
//                return null;
//            }
//        }
        return MouseInfo.getPointerInfo().getLocation();
    }

    public static List<Rectangle> getScreenBounds() {
        List<Rectangle> screenBounds;

        // JNA_ONLY
//        if (useJna) {
//            screenBounds = new ArrayList<>();
//            // Enumerate all monitors, and call a code block for each of them
//            // See https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumdisplaymonitors
//            // See http://www.pinvoke.net/default.aspx/user32/EnumDisplayMonitors.html
//            User32.INSTANCE.EnumDisplayMonitors(
//                    null, // => the virtual screen that encompasses all the displays on the desktop.
//                    null, // => don't clip the region
//                    (hmonitor, hdc, rect, lparam) -> {
//                        // For each found monitor, get more information
//                        // See https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmonitorinfoa
//                        // See http://www.pinvoke.net/default.aspx/user32/GetMonitorInfo.html
//                        WinUser.MONITORINFOEX monitorInfoEx = new WinUser.MONITORINFOEX();
//                        User32.INSTANCE.GetMonitorInfo(hmonitor, monitorInfoEx);
//                        // Retrieve its coordinates
//                        final WinDef.RECT rcMonitor = monitorInfoEx.rcMonitor;
//                        // And convert them to a Java rectangle, to be added to the list of monitors
//                        screenBounds.add(new Rectangle(rcMonitor.left, rcMonitor.top, rcMonitor.right - rcMonitor.left, rcMonitor.bottom - rcMonitor.top));
//                        // Then return "true" to continue enumeration
//                        return 1;
//                    },
//                    null // => No additional info to pass as lparam to the callback
//            );
//            return screenBounds;
//        }

        GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice[] screenDevices = graphicsEnvironment.getScreenDevices();
        screenBounds = new ArrayList<>(screenDevices.length);
        for (GraphicsDevice screenDevice : screenDevices) {
            GraphicsConfiguration configuration = screenDevice.getDefaultConfiguration();
            screenBounds.add(configuration.getBounds());
        }
        return screenBounds;
    }

}
//仅限JNA\u
//导入com.sun.jna.platform.win32.User32;
//导入com.sun.jna.platform.win32.WinDef;
//导入com.sun.jna.platform.win32.WinUser;
导入javax.swing.*;
导入java.awt.*;
导入java.awt.event.MouseAdapter;
导入java.awt.event.MouseEvent;
导入java.awt.font.FontRenderContext;
导入java.awt.font.TextLayout;
导入java.awt.geom.Rectangle2D;
导入java.util.ArrayList;
导入java.util.List;
/**
*Java多显示检测与分析。
*UI理念基于WheresMyMouse-https://stackoverflow.com/a/21592711/13551878
*/
公开课展览{
私有静态布尔useJna=false;
公共静态void main(字符串[]args){
EventQueue.invokeLater(()->{
JFrame=新JFrame(“显示配置”);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(新的BorderLayout());
frame.add(newtestpane());
frame.pack();
frame.setLocationRelativeTo(空);
frame.setVisible(true);
});
}
公共静态类TestPane扩展了JPanel{
私人列表屏幕