Java Swing中带有缩放图形的字符串边界-可能存在错误

Java Swing中带有缩放图形的字符串边界-可能存在错误,java,swing,fonts,Java,Swing,Fonts,如a中所述: 关于计算应该绘制到Swing组件中的字符串的大小(宽度或高度),有很多问题。有很多建议的解决方案 然而,最常用和推荐的解决方案(根据我到目前为止的经验,至少在大多数情况下计算出正确的边界)在某些条件下再次显示出相当奇怪的行为 下面是一个例子,它显示了我目前认为的一个普通错误: import java.awt.Font; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.font.F

如a中所述:

关于计算应该绘制到Swing组件中的字符串的大小(宽度或高度),有很多问题。有很多建议的解决方案

然而,最常用和推荐的解决方案(根据我到目前为止的经验,至少在大多数情况下计算出正确的边界)在某些条件下再次显示出相当奇怪的行为

下面是一个例子,它显示了我目前认为的一个普通错误:

import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Locale;

public class StringBoundsBugTest
{
    public static void main(String[] args)
    {
        Font font = new Font("Dialog", Font.PLAIN, 10);
        BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = bi.createGraphics();
        g.setRenderingHint(
            RenderingHints.KEY_FRACTIONALMETRICS,  
            RenderingHints.VALUE_FRACTIONALMETRICS_ON);

        for (int i=1; i<30; i++)
        {
            double scaling = 1.0 / i;

            AffineTransform oldAt = g.getTransform();
            g.scale(scaling, scaling);
            FontRenderContext fontRenderContext = g.getFontRenderContext();
            Rectangle2D bounds = 
                font.getStringBounds("Test", fontRenderContext);
            g.setTransform(oldAt);

            System.out.printf(Locale.ENGLISH, 
                "Scaling %8.5f, width %8.5f\n",
                scaling, bounds.getWidth());
        }

    }
}
我们可以看到计算出的尺寸在18-19左右奇怪地摆动。这表明大小确实应该是“固定”的,不管应用于图形的缩放比例如何,我不介意舍入问题和与字体相关的计算的荒谬复杂性可能带来的小错误

但不可接受的是,对于某个比例因子,计算的大小明显下降到零。发生这种情况的缩放因子取决于字体大小,但即使对于较大的字体,缩放因子也较小

当然,这里有一个明显的高级解释:在与字体相关的Swing类(如
FontRenderContext
等)的深处,执行一些计算,用图形的比例因子缩放一些值,然后。。。将其强制转换为
int
。(上述问题中的问题可能也是如此)

一个明显的解决方法是创建一个固定的
FontRenderContext
,并将其用于所有与字体相关的计算。但这违背了通常绑定到
图形的字体相关计算的目的:使用与绘画不同的
FontRenderContext
进行计算可能会导致计算大小与实际绘制大小之间的偏差


是否有人有一个干净、可靠的解决方案来计算Swing中字符串的边界,而不考虑字体大小,也不考虑应用于图形的缩放因子?

实际上FontRenderContext有4个字段

public class FontRenderContext {
    private transient AffineTransform tx;
    private transient Object aaHintValue;
    private transient Object fmHintValue;
    private transient boolean defaulting;
因此,转换是上下文的一部分。如果你的分数是1/3,当然会有一些舍入

因此,您可以在获取FontRenderContext之前,将图形的AffineTransform设置为normal(例如,无平移和无缩放)

或者你可以创建自己的并在任何地方重用它

FontRenderContext frc=new FontRenderContext(g.getTransform(), //of just replace with new AffineTransform(),
            g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING),
            g.getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS));
关于矢量创建。这也可能是一种选择

检查Font.getStringBounds()源

正如您所看到的,StandardGlyphVector是为简单的大小写创建的(当文本没有RTL内容时)。在相反的情况下,使用TextLayout

结果可能是这样的

private static Rectangle2D getBounds(Graphics2D g, String text) {
    FontRenderContext frc=new FontRenderContext(new AffineTransform(),
            g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING),
            g.getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS));
    GlyphVector gv = new StandardGlyphVector(g.getFont(), text.toCharArray(), 0,
            text.length(), frc);
    return gv.getLogicalBounds();
}

这个问题可能有解决方案(顺便说一句,这个问题也有解决方案)。乍一看,它看起来有点粗糙,但我考虑了替代解决方案的优缺点:

使用
Font#getStringBounds
Graphics2D
FontRenderContext
计算边界,对于某些比例因子给出了明显错误的结果,如本问题所述

使用“默认”(未转换)
FontRenderContext
(如所建议)计算边界可能是一个选项(稍作调整),但仍然存在中所述的问题-即对于小字体(大小小于0.5)的结果是错误的

因此,我从另一个问题扩展了解决方法,一举两得:我没有使用大小为1.0的标准化字体,而是使用了一种大得离谱的字体,使用
FontMetrics
对象计算边界大小,并根据字体的原始大小缩小这些边界

此帮助器类对此进行了总结:

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

public class StringBoundsUtils
{
    private static final Graphics2D DEFAULT_GRAPHICS;
    static
    {
        BufferedImage bi = new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB);
        DEFAULT_GRAPHICS = bi.createGraphics();
        DEFAULT_GRAPHICS.setRenderingHint(
            RenderingHints.KEY_FRACTIONALMETRICS,
            RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    }

    public static Rectangle2D computeStringBounds(String string, Font font)
    {
        return computeStringBounds(string, font, new Rectangle2D.Double());
    }

    public static Rectangle2D computeStringBounds(
        String string, Font font, Rectangle2D result)
    {
        final float helperFontSize = 1000.0f;
        final float fontSize = font.getSize2D();
        final float scaling = fontSize / helperFontSize;
        Font helperFont = font.deriveFont(helperFontSize);
        FontMetrics fontMetrics = DEFAULT_GRAPHICS.getFontMetrics(helperFont);
        double stringWidth = fontMetrics.stringWidth(string) * scaling;
        double stringHeight = fontMetrics.getHeight() * scaling;
        if (result == null)
        {
            result = new Rectangle2D.Double();
        }
        result.setRect(
            0, -fontMetrics.getAscent() * scaling,
            stringWidth, stringHeight);
        return result;

    }
}
(这可以扩展/调整以使用给定的
Graphics2D
对象,但随后应验证缩放不会影响
FontMetrics

我敢肯定,在某些情况下,这是行不通的:从右到左的文本、汉字,或者所有
FontMetrics
的内部工作方式都不足以恰当地测量文本大小。但它适用于所有与我相关的情况(可能也适用于许多其他情况),而且它确实受到上述错误的影响,而且。。。很快

下面是一个非常简单的性能比较(不是真正的基准,但应该给出一个粗略的衡量):

因此
StringBoundsUtils
Font#getStringBounds
方法快5倍(对于更长的字符串,甚至更快)

上面输出中的
result
列已经表明,使用
Font#getStringBounds
计算的边界宽度与使用这些
StringBoundsTils
计算的边界宽度之间的差异可以忽略不计

然而,我想确保这不仅适用于widhts,而且适用于整个边界。因此,我创建了一个小测试:

在本例中,我们可以看到,无论缩放比例和字体大小,这两种方法的边界“实际上是相等的”——当然,
StringBoundsUtils
即使字体大小小于0.5,也会计算适当的边界

这个测试的源代码,为了完整性:(它使用了一个小的


我倾向于将
字符串
表示转换为
形状
,然后将所有转换(例如缩放)应用于
形状
@andrewhompson这可能有效,但例如创建
字形向量
并获取其“逻辑边界”(如链接问题中所做的那样)是相当昂贵的-没有人愿意做的数百或数千标签。我觉得很奇怪suc
private static Rectangle2D getBounds(Graphics2D g, String text) {
    FontRenderContext frc=new FontRenderContext(new AffineTransform(),
            g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING),
            g.getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS));
    GlyphVector gv = new StandardGlyphVector(g.getFont(), text.toCharArray(), 0,
            text.length(), frc);
    return gv.getLogicalBounds();
}
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

public class StringBoundsUtils
{
    private static final Graphics2D DEFAULT_GRAPHICS;
    static
    {
        BufferedImage bi = new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB);
        DEFAULT_GRAPHICS = bi.createGraphics();
        DEFAULT_GRAPHICS.setRenderingHint(
            RenderingHints.KEY_FRACTIONALMETRICS,
            RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    }

    public static Rectangle2D computeStringBounds(String string, Font font)
    {
        return computeStringBounds(string, font, new Rectangle2D.Double());
    }

    public static Rectangle2D computeStringBounds(
        String string, Font font, Rectangle2D result)
    {
        final float helperFontSize = 1000.0f;
        final float fontSize = font.getSize2D();
        final float scaling = fontSize / helperFontSize;
        Font helperFont = font.deriveFont(helperFontSize);
        FontMetrics fontMetrics = DEFAULT_GRAPHICS.getFontMetrics(helperFont);
        double stringWidth = fontMetrics.stringWidth(string) * scaling;
        double stringHeight = fontMetrics.getHeight() * scaling;
        if (result == null)
        {
            result = new Rectangle2D.Double();
        }
        result.setRect(
            0, -fontMetrics.getAscent() * scaling,
            stringWidth, stringHeight);
        return result;

    }
}
import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.util.Locale;

public class StringBoundsUtilsPerformance
{
    public static void main(String[] args)
    {
        String strings[] = {
            "a", "AbcXyz", "AbCdEfGhIjKlMnOpQrStUvWxYz",
            "AbCdEfGhIjKlMnOpQrStUvWxYz" +
            "AbCdEfGhIjKlMnOpQrStUvWxYz" +
            "AbCdEfGhIjKlMnOpQrStUvWxYz" +
            "AbCdEfGhIjKlMnOpQrStUvWxYz" +
            "AbCdEfGhIjKlMnOpQrStUvWxYz" };
        float fontSizes[] = { 1.0f, 10.0f, 100.0f };
        int runs = 1000000;

        long before = 0;
        long after = 0;
        double resultA = 0;
        double resultB = 0;

        for (float fontSize : fontSizes)
        {
            Font font = new Font("Dialog", Font.PLAIN, 10).deriveFont(fontSize);
            for (String string : strings)
            {
                before = System.nanoTime();
                for (int i=0; i<runs; i++)
                {
                    Rectangle2D r = computeStringBoundsDefault(string, font);
                    resultA += r.getWidth();
                }
                after  = System.nanoTime();
                resultA /= runs;
                System.out.printf(Locale.ENGLISH,
                    "A: time %14.4f result %14.4f, fontSize %3.1f, length %d\n",
                    (after-before)/1e6, resultA, fontSize, string.length());

                before = System.nanoTime();
                for (int i=0; i<runs; i++)
                {
                    Rectangle2D r =
                        StringBoundsUtils.computeStringBounds(string, font);
                    resultB += r.getWidth();
                }
                after  = System.nanoTime();
                resultB /= runs;
                System.out.printf(Locale.ENGLISH,
                    "B: time %14.4f result %14.4f, fontSize %3.1f, length %d\n",
                    (after-before)/1e6, resultB, fontSize, string.length());
            }
        }
    }

    private static final FontRenderContext DEFAULT_FONT_RENDER_CONTEXT =
        new FontRenderContext(null, true, true);
    public static Rectangle2D computeStringBoundsDefault(
        String string, Font font)
    {
        return font.getStringBounds(string, DEFAULT_FONT_RENDER_CONTEXT);
    }
}
A: time      1100.4441 result        14.7813, fontSize 1.0, length 26
B: time       218.6409 result        14.7810, fontSize 1.0, length 26
...
A: time      1167.1569 result       147.8125, fontSize 10.0, length 26
B: time       200.6532 result       147.8100, fontSize 10.0, length 26
...
A: time      1179.7873 result      1478.1253, fontSize 100.0, length 26
B: time       208.9414 result      1478.1003, fontSize 100.0, length 26
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.Locale;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import de.javagl.viewer.Painter;
import de.javagl.viewer.Viewer;

public class StringBoundsUtilsTest
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static final Font DEFAULT_FONT =
        new Font("Dialog", Font.PLAIN, 10);
    private static Font font = DEFAULT_FONT.deriveFont(10f);

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame("Viewer");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.getContentPane().setLayout(new BorderLayout());

        Viewer viewer = new Viewer();

        String string = "AbcXyz";
        viewer.addPainter(new Painter()
        {
            @Override
            public void paint(Graphics2D g, AffineTransform worldToScreen,
                double w, double h)
            {
                AffineTransform at = g.getTransform();
                g.setColor(Color.BLACK);
                g.setRenderingHint(
                    RenderingHints.KEY_FRACTIONALMETRICS,
                    RenderingHints.VALUE_FRACTIONALMETRICS_ON);
                g.setRenderingHint(
                    RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);

                Rectangle2D boundsA =
                    StringBoundsUtilsPerformance.computeStringBoundsDefault(
                        string, font);
                Rectangle2D boundsB =
                    StringBoundsUtils.computeStringBounds(string, font);

                g.setFont(new Font("Monospaced", Font.BOLD, 12));
                g.setColor(Color.GREEN);
                g.drawString(createString(boundsA), 10, 20);
                g.setColor(Color.RED);
                g.drawString(createString(boundsB), 10, 40);

                g.setFont(font);
                g.transform(worldToScreen);
                g.drawString(string, 0, 0);
                g.setTransform(at);

                g.setColor(Color.GREEN);
                g.draw(worldToScreen.createTransformedShape(boundsA));
                g.setColor(Color.RED);
                g.draw(worldToScreen.createTransformedShape(boundsB));
            }
        });
        f.getContentPane().add(viewer, BorderLayout.CENTER);

        f.getContentPane().add(
            new JLabel("Mouse wheel: Zoom, "
                + "Right mouse drags: Move, "
                + "Left mouse drags: Rotate"),
            BorderLayout.NORTH);

        JSpinner fontSizeSpinner =
            new JSpinner(new SpinnerNumberModel(10.0, 0.1, 100.0, 0.1));
        fontSizeSpinner.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                Object object = fontSizeSpinner.getValue();
                Number number = (Number)object;
                float fontSize = number.floatValue();
                font = DEFAULT_FONT.deriveFont(fontSize);
                viewer.repaint();
            }
        });
        JPanel p = new JPanel();
        p.add(new JLabel("Font size"), BorderLayout.WEST);
        p.add(fontSizeSpinner, BorderLayout.CENTER);
        f.getContentPane().add(p, BorderLayout.SOUTH);


        viewer.setPreferredSize(new Dimension(1000,500));
        viewer.setDisplayedWorldArea(-15,-15,30,30);
        f.pack();
        viewer.setPreferredSize(null);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static String createString(Rectangle2D r)
    {
        return String.format(Locale.ENGLISH,
            "x=%12.4f y=%12.4f w=%12.4f h=%12.4f",
            r.getX(), r.getY(), r.getWidth(), r.getHeight());
    }

}