Я знаю, что в Java 9 исправлены проблемы с масштабированием и разрешением экрана, которые присутствовали в Swing в Java 8. Однако у меня возникла ситуация, когда один и тот же код отображается четко при запуске на Java 8, но размыто при использовании любой версии Java 9 и после (включая самые последние версии LTS Java 21 и Java 22). Загвоздка в том, что это происходит только в Windows, если в настройках дисплея Windows установлено масштабирование 125%.
Похоже, что для того, чтобы это произошло, все условия должны соблюдаться одновременно:
drawImage().Если я просто рисую линии и текст непосредственно на объекте Graphics2D (тот, который мне предоставила функция JComponentpaint(Graphics g)), то все работает нормально даже на Java 9+ с масштабированием Windows 125%. Однако если я визуализирую ту же самую графику, но использую метод drawImage(), она будет размытой.
Итак, если я запускаю свой код на Java 8 при любом из вышеперечисленных условий (масштаб Windows установлен на 100% или 125%, использование объекта Graphics напрямую или использование drawImage() не имеет значения), все отображается четко (т. е. без каких-либо тип сглаживания или дизеринга или что-то еще происходит).
Если я запускаю свой код на любой версии Java 9+ с масштабированием Windows не 125%, все отображается четко (так же, как на Java 8). Если я запускаю свой код на любой версии Java 9+ с Windows с масштабированием 125%, но напрямую использую объект Graphics (а не drawImage()), то все отображается четко. Только если я воспользуюсь методом drawImage(), дела пойдут наперекосяк.
У меня есть минимальный воспроизводимый пример:
Вот как он отображается на Java 22 с Windows при масштабировании 125 % без использования метода drawImage():
Вот как он отображается на Java 22 с Windows при масштабировании 125% с использованием метода drawImage():
Однако если я использую Java 8, то нет никакой разницы между использованием drawImage() и отсутствием drawImage().
Вот код:
public class MainWindow extends JFrame implements ActionListener {
DrawPanel dp;
public static void main(String[] args) {
new MainWindow();
}
public MainWindow() {
super("Graphics Test");
setLayout(new BorderLayout());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton button = new JButton("Redraw");
button.addActionListener(this);
add(button, BorderLayout.PAGE_START);
JPanel center = new JPanel(new FlowLayout());
dp = new DrawPanel();
center.add(dp);
add(center, BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
dp.drawOnImage = (e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK;
dp.repaint();
}
}
И кастомный компонент. Обратите внимание, что удержание CTRL вызовет рендеринг с использованием drawImage(), тогда как без CTRL будет напрямую использоваться объект Graphics:
public class DrawPanel extends JComponent {
Graphics2D gSave;
BufferedImage bi;
boolean drawOnImage = false;
public DrawPanel() {
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
@Override
public void paint(Graphics gr) {
Graphics2D g = (Graphics2D) gr;
gSave = g;
if (drawOnImage) {
bi = new BufferedImage(getSize().width, getSize().height, BufferedImage.TYPE_INT_ARGB);
g = bi.createGraphics();
}
Font font = new Font("SansSerif", Font.BOLD, 36);
g.setFont(font);
g.setColor(Color.BLACK);
g.drawString("Test String", 5, 40);
g.setStroke(new BasicStroke(6.0001f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0f, null, 0f));
g.drawRect(100, 70, 100, 100);
font = new Font("SansSerif", Font.PLAIN, 16);
g.setFont(font);
g.drawString("More Text to test the resolution", 5, 200);
Rectangle rect2 = new Rectangle(300, 40, 100, 100);
g.rotate(Math.toRadians(45));
g.draw(rect2);
if (drawOnImage) {
g.dispose();
gSave.drawImage(bi, null, 0, 0);
}
}
}
Итак, вопрос заключается в следующем: почему в более старой версии Java с этим не было проблем, а в более новых версиях производительность, похоже, снизилась? Есть ли что-то, что drawImage() делает по-другому, что я могу смягчить, чтобы не получить этот результат?
(И на неизбежный вопрос о том, зачем я вообще это делаю и зачем это необходимо (например, если это работает, почему бы вам просто не рисовать напрямую на объекте Graphics, а не использовать метод drawImage()?), отмечу, что фактический код, над которым я работаю, пытается рисовать полупрозрачные перекрывающиеся фигуры при перетаскивании мышью, поэтому был использован этот BufferedImage подход с использованием drawImage(). Возможно, есть другой способ сделать это, который не требует использования drawImage(), но я еще не использовал. нашел.)
Что касается вопроса «почему/как»? В The Atlantic есть отличная статья о проблеме CrowdStrike, которая разрушила Интернет, и я считаю, что она уместна здесь. По сути, проблема в том, что наши системы становятся настолько сложными, что инженеры работают над исправив одну проблему, можно легко сломать другую: theatlantic.com/technology/archive/2024/07/…
Ваш графический интерфейс масштабируется до 125%. Java 9 была первой версией Java Swing, масштабируемой с помощью масштабирования Windows. Если вы этого не хотите, добавьте строку System.setProperty("sun.java2d.uiScale", "1"); в качестве первой строки вашего кода.




Почему в старой версии Java с этим не было проблем, а в новых версиях производительность, похоже, снизилась?
Я думаю, ответ таков: (очень) старые версии Java вообще не учитывали разрешение монитора. Поэтому, даже если ваш монитор настроен на 125%, я думаю, что приложение запустится на 100%. Это простительно, когда разница составляет 125% против 100%, но разница становится действительно резкой, если на вашем мониторе установлено значение 200% или 250%, и вы запускаете старое приложение, которое отображает 100%.
Например, вот как ваше приложение отображается для меня с использованием старой и новой JRE:
Если мы хотим сохранить разрешение 125%, проблема заключается в том, как мы все масштабируем.
По умолчанию Graphics2D, который вы получаете от Swing, имеет AffineTransform, масштабированный до 1,25. Вот почему, когда вы рисуете рисунок без BufferedImage, он «просто работает».
Все становится сложнее, когда вы добавляете BufferedImage. Вы просите BufferedImage иметь размер 400x400. Это виртуальный размер вашего окна. Но при разрешении 125% окно размером 400x400 пикселей действительно занимает 500x500 пикселей. Таким образом, для хорошей визуализации ваше изображение должно иметь размер 500x500 пикселей.
Нам нужно изображение размером 500х500 пикселей.
Вот модифицированная копия вашего приложения, которая должна работать намного лучше. При этом учитывается коэффициент трансформации/масштаба вашего пункта назначения.
(Есть много места для обсуждения дизайна, но для краткости это краткое исправление.)
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
public class MainWindow extends JFrame implements ActionListener {
DrawPanel dp;
public static void main(String[] args) {
new MainWindow();
}
public MainWindow() {
super("Graphics Test");
setLayout(new BorderLayout());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton button = new JButton("Redraw");
button.addActionListener(this);
add(button, BorderLayout.PAGE_START);
JPanel center = new JPanel(new FlowLayout());
dp = new DrawPanel();
center.add(dp);
add(center, BorderLayout.CENTER);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
dp.drawOnImage = (e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK;
dp.repaint();
}
}
class DrawPanel extends JComponent {
boolean drawOnImage = false;
public DrawPanel() {
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
@Override
public void paint(Graphics gr) {
Graphics2D g = (Graphics2D) gr;
if (drawOnImage) {
ScaledBufferedImage bi = new ScaledBufferedImage(getSize().width, getSize().height, BufferedImage.TYPE_INT_ARGB, Math.sqrt(g.getTransform().getDeterminant()));
Graphics2D imgG = bi.createGraphics();
imgG.setRenderingHints(g.getRenderingHints());
paintImage(imgG);
imgG.dispose();
g.drawImage(bi, 0, 0, getSize().width, getSize().height,
0, 0, bi.getWidth(), bi.getHeight(), null);
} else {
paintImage(g);
}
}
private void paintImage(Graphics2D g) {
Font font = new Font("SansSerif", Font.BOLD, 36);
g.setFont(font);
g.setColor(Color.BLACK);
g.drawString("Test String", 5, 40);
g.setStroke(new BasicStroke(6.0001f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0f, null, 0f));
g.drawRect(100, 70, 100, 100);
font = new Font("SansSerif", Font.PLAIN, 16);
g.setFont(font);
g.drawString("More Text to test the resolution", 5, 200);
Rectangle rect2 = new Rectangle(300, 40, 100, 100);
g.rotate(Math.toRadians(45));
g.draw(rect2);
}
}
class ScaledBufferedImage extends BufferedImage {
final int virtualWidth, virtualHeight;
final double scaleFactor;
public ScaledBufferedImage(int width, int height, int imageType, double scaleFactor) {
super( (int) Math.round(width * scaleFactor), (int) Math.round(height * scaleFactor), imageType);
virtualHeight = height;
virtualWidth = width;
this.scaleFactor = scaleFactor;
}
public Graphics2D createGraphics() {
Graphics2D returnValue = super.createGraphics();
returnValue.scale(scaleFactor, scaleFactor);
return returnValue;
}
}
Спасибо, это было то, что я искал. Кажется, это все исправит!
Я бы не стал использовать определитель в качестве масштабного коэффициента. Просто используйте getScaleX() и getScaleY() на экранной графике, используйте их для адаптации физической ширины и высоты буферизованного изображения и передайте эти два значения в метод scale графики изображения. Хотя масштабирование экрана обычно одинаково для x и y, вы не получите никакого преимущества, задав один и тот же коэффициент для обоих направлений. Кстати, компоненты Swing уже имеют двойную буферизацию. Нет смысла добавлять еще один буфер в процесс рисования.
Если бы я разрабатывал настольное приложение, я бы определенно хотел разработать/обсудить это дальше. Привлекательность getDeterminant в том, что он лучше поддерживает вращение/переворот. (Например: если я добавлю g.rotate(Math.PI/2.0, getWidth()/2.0, getHeight()/2.0); к Graphics2D компонента, тогда этот код будет работать, но transform.getScaleX() и transform.getScaleY() будут равны нулю.) Однако я только что проверил свой монитор Windows, и если я поверну/перевернул свой монитор: AffineTransform моего JComponent НЕ включает это преобразование, так что этот момент может (?) никогда не стать проблемой в реальном мире.
В первом изображении не используется метод
drawImage(), а во втором изображении используется методdrawImage(). Оба используют Java 22. Если я использую Java 8, изображения будут идентичны первому изображению. Я подумал, что было бы излишним предоставлять кучу изображений, которые все выглядят одинаково.