英文:
How can I avoid garbled text and vector graphics in Java Swing with UI scaling and off-screen buffering?
问题
我正在开发一个Java应用程序,其中有几个JPanel
(不是 JFrame
),它们具有复杂的动画效果,需要在绘制到屏幕之前绘制到离屏缓冲区。我遇到的问题是,Swing在高DPI屏幕上执行UI缩放,而离屏缓冲区(一个光栅)不“意识到”缩放。因此,当文本或图形被渲染到缓冲区,并且缓冲区被绘制到JPanel
时,Swing将图形缩放为光栅,结果看起来像垃圾。
一个简单的例子是:
import java.awt.*;
import java.awt.geom.Line2D;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class Main {
public static void main(String[] args) {
JFrame jf = new JFrame("Demo");
Container cp = jf.getContentPane();
MyCanvas tl = new MyCanvas();
cp.add(tl);
jf.setSize(500, 250);
jf.setVisible(true);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
class MyCanvas extends JComponent {
@Override
public void paintComponent(Graphics g) {
if (g instanceof Graphics2D g2) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setFont(Font.decode("Times New Roman-26"));
g2.drawString("The poorly-scaled cake is a lie.", 70, 40);
g2.setStroke(new BasicStroke(2.3f));
g2.draw(new Line2D.Double(420, 10, 425, 70));
Image I = createImage(500, 150);
Graphics2D g2_ = (Graphics2D) I.getGraphics();
g2_.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2_.setColor(Color.BLACK);
g2_.setFont(Font.decode("Times New Roman-26"));
g2_.drawString("The poorly-scaled cake is a lie.", 70, 40);
g2_.setStroke(new BasicStroke(2.3f));
g2_.draw(new Line2D.Double(420, 10, 425, 70));
g2_.dispose();
g2.drawImage(I, 0, 130, null);
}
}
}
在我的Windows 11机器上使用JDK 20编译,得到的结果如下图所示:
顶部是直接渲染到JPanel
的文本和图形。底部是通过中间图像渲染的相同内容。
理想情况下,我希望有一个方法,比如Image createScalingAwareBuffer(JPanel jp, int width, int height)
,返回一个图像I
,与JPanel.createImage(...)
类似,但返回的Image
是矢量缩放感知的,这样jp.drawImage(I)
或等效的方法可以以与上部内容完全相同的方式显示下部图形内容。
我怀疑在双缓冲的Swing组件中渲染到后备缓冲区具有这种“意识”,但在我的情况下这不是一个选项,因为我需要精确控制何时在每个面板上进行缓冲区翻转,而(据我所知)在Swing中这是不可能的。
是否有任何解决方案,而不需要彻底重写(即迁移到Swing之外等)?
我还应该注意,我不希望禁用UI缩放(例如,在VM选项中使用-Dsun.java2d.uiScale=1
),因此“只需禁用UI缩放”并不是一个真正的解决方案。
英文:
I'm developing a Java application where several JPanel
s (not JFrame
s) have complex animations that necessitate drawing to an off-screen buffer before blitting to the display surface. A problem I'm having is that Swing is performing UI scaling for high-DPI screens, and the off-screen buffer (a raster) isn't "aware" of the scaling. Consequently, when text or graphics are rendered to the buffer, and the buffer is blitted to the JPanel
, Swing scales the graphic as a raster and the result looks like garbage.
A simple example is:
import java.awt.*;
import java.awt.geom.Line2D;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class Main {
public static void main(String[] args) {
JFrame jf = new JFrame("Demo");
Container cp = jf.getContentPane();
MyCanvas tl = new MyCanvas();
cp.add(tl);
jf.setSize(500, 250);
jf.setVisible(true);
jf.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
}
}
class MyCanvas extends JComponent {
@Override
public void paintComponent(Graphics g) {
if( g instanceof Graphics2D g2 ) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setFont( Font.decode( "Times New Roman-26" ) );
g2.drawString("The poorly-scaled cake is a lie.",70,40);
g2.setStroke( new BasicStroke( 2.3f ) );
g2.draw( new Line2D.Double( 420, 10, 425, 70 ) );
Image I = createImage( 500, 150 );
Graphics2D g2_ = (Graphics2D)I.getGraphics();
g2_.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2_.setColor( Color.BLACK );
g2_.setFont( Font.decode( "Times New Roman-26" ) );
g2_.drawString( "The poorly-scaled cake is a lie.",70,40 );
g2_.setStroke( new BasicStroke( 2.3f ) );
g2_.draw( new Line2D.Double( 420, 10, 425, 70 ) );
g2_.dispose();
g2.drawImage( I, 0, 130, null );
}
}
}
From this, compiling with JDK 20 on my Windows 11 machine, I get:
On the top is text and graphics rendered directly to the JPanel
. On the bottom is the same content rendered via an intermediary image.
Ideally, I'm looking for a method, e.g., Image createScalingAwareBuffer( JPanel jp, int width, int height )
that returns an image I
, in the same vein as JPanel.createImage( ... )
but where the returned Image
is vector scaling aware, such that jp.drawImage( I )
or equivalent displays the lower graphic content identically to the upper content.
I suspect that rendering to the back buffer in a double-buffered Swing component has this kind of "awareness", but this isn't an option in my case since I need to precisely control when buffer flips occur on a panel-by-panel basis, which (insofar as I know) is impossible in Swing.
Is there any solution for this without a radical rewrite (i.e., migrating away from Swing, etc.)?
I should also note that I don't want to disable the UI scaling (e.g., using -Dsun.java2d.uiScale=1
in VM options), hence "just disable UI scaling" isn't really a solution.
答案1
得分: 2
这段代码是一个Java程序,主要涉及到图像的渲染和缩放。其中使用了RenderableImage
接口来实现图像的渲染和缩放操作。
RenderableImage
接口定义了一些方法,其中最重要的是三个create...
方法。这些方法用于创建渲染图像的实例。
代码中还包含了一些图形绘制的操作,如绘制线条、绘制文本等。
整个程序的入口是main
方法,它创建了一个窗口并显示图像。
如果你有具体的问题或需要进一步解释,请告诉我。
英文:
There is something like scalingAwareBuffer. It’s the RenderableImage interface.
I’m not certain this will help, but in theory it should. RenderableImage looks like it has a lot of methods, but most of them are very simple. The important ones are the three create… methods.
Even this won’t produce an identical copy from the image (due to the use of the GPU when drawing directly to the screen?), but the image should at least scale properly.
import java.util.Vector;
import java.util.Map;
import java.awt.Container;
import java.awt.Color;
import java.awt.Font;
import java.awt.Image;
import java.awt.Shape;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.BasicStroke;
import java.awt.RenderingHints;
import java.awt.EventQueue;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderableImage;
import java.awt.image.renderable.RenderContext;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class ScaledImageRenderExample1 {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame jf = new JFrame("Demo");
Container cp = jf.getContentPane();
MyCanvas tl = new MyCanvas();
cp.add(tl);
jf.setSize(500, 250);
jf.setLocationByPlatform( true );
jf.setVisible(true);
jf.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
});
}
static class MyCanvas extends JComponent {
private static final long serialVersionUID = 1;
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
if( g instanceof Graphics2D g2 ) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setFont( Font.decode( "Times New Roman-26" ) );
g2.drawString("The poorly-scaled cake is a lie.",70,40);
g2.setStroke( new BasicStroke( 2.3f ) );
g2.draw( new Line2D.Double( 420, 10, 425, 70 ) );
g2.drawRenderableImage(new CakeImage(),
AffineTransform.getTranslateInstance(0, 130));
}
}
}
static class CakeImage implements RenderableImage {
private static final int DEFAULT_WIDTH = 500;
private static final int DEFAULT_HEIGHT = 150;
private static final RenderingHints DEFAULT_HINTS =
new RenderingHints(Map.of(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON,
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON,
RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_ON,
RenderingHints.KEY_RESOLUTION_VARIANT,
RenderingHints.VALUE_RESOLUTION_VARIANT_SIZE_FIT,
RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY,
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC,
RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE,
RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_QUALITY
));
private final float x;
private final float y;
CakeImage() {
this(0, 0);
}
CakeImage(float x,
float y) {
this.x = x;
this.y = y;
}
@Override
public float getMinX() {
return x;
}
@Override
public float getMinY() {
return y;
}
@Override
public float getWidth() {
return DEFAULT_WIDTH;
}
@Override
public float getHeight() {
return DEFAULT_HEIGHT;
}
@Override
public boolean isDynamic() {
return false;
}
@Override
public Object getProperty(String name) {
return Image.UndefinedProperty;
}
@Override
public String[] getPropertyNames() {
return new String[0];
}
@Override
public Vector<RenderableImage> getSources() {
return null;
}
private void drawIn(Graphics2D g2) {
g2.setColor( Color.BLACK );
g2.setFont( Font.decode( "Times New Roman-26" ) );
g2.drawString( "The poorly-scaled cake is a lie.",70,40 );
g2.setStroke( new BasicStroke( 2.3f ) );
g2.draw( new Line2D.Double( 420, 10, 425, 70 ) );
}
@Override
public RenderedImage createDefaultRendering() {
BufferedImage image = new BufferedImage(
DEFAULT_WIDTH, DEFAULT_HEIGHT, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
RenderingHints hints = g2.getRenderingHints();
hints.putAll(DEFAULT_HINTS);
g2.setRenderingHints(hints);
drawIn(g2);
g2.dispose();
return image;
}
@Override
public RenderedImage createScaledRendering(int width,
int height,
RenderingHints hints) {
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setRenderingHints(hints);
g2.scale((double) width / DEFAULT_WIDTH,
(double) height / DEFAULT_HEIGHT);
drawIn(g2);
g2.dispose();
return image;
}
@Override
public RenderedImage createRendering(RenderContext context) {
Point2D size = new Point2D.Float(getWidth(), getHeight());
Shape shape = context.getAreaOfInterest();
if (shape != null) {
Rectangle2D bounds = shape.getBounds2D();
size = new Point2D.Double(
bounds.getWidth(), bounds.getHeight());
}
context.getTransform().transform(size, size);
BufferedImage image = new BufferedImage(
(int) Math.ceil(size.getX()),
(int) Math.ceil(size.getY()),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
RenderingHints hints = context.getRenderingHints();
if (hints != null) {
g2.setRenderingHints(hints);
} else {
hints = g2.getRenderingHints();
hints.putAll(DEFAULT_HINTS);
g2.setRenderingHints(hints);
}
g2.setTransform(context.getTransform());
drawIn(g2);
g2.dispose();
return image;
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论