简体   繁体   English

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

[英]Bounds of a string with scaled graphics in Swing - possible bug

As already mentioned in a related question : 正如相关问题中已提到的:

There are many (many) questions about computing the size (width or height) of a string that should be painted into a Swing component. 关于计算应该绘制到Swing组件中的字符串的大小(宽度或高度)有很多(很多)问题。 And there are many proposed solutions. 并且有许多提议的解决方案。

However, the solution that is most commonly used and recommended (and that, from my experiences so far, at least computes the correct bounds for most cases) once more shows a rather odd behavior under certain conditions. 但是,最常用和推荐的解决方案(根据我迄今为止的经验,至少计算大多数情况下的正确界限)再次在某些条件下显示出相当奇怪的行为。

The following is an example that shows what I currently consider as a plain bug: 以下示例显示了我目前认为的普通错误:

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());
        }

    }
}

The program creates a Graphics2D instance (where it does not matter whether it comes from a JComponent or a BufferedImage ), then applies various scaling factors to this graphics object, and computes the bounds of a string using the FontRenderContext of the graphics object. 该程序创建一个Graphics2D实例(无论它来自JComponent还是BufferedImage ),然后将各种缩放因子应用于此图形对象,并使用图形对象的FontRenderContext计算字符串的边界。

From my understanding, the scaling factor of the graphics object should not affect the bounds (one might expect something different here, but that's what it seems to do anyhow). 根据我的理解,图形对象的缩放因子应该影响边界(人们可能期望在这里有不同的东西,但这无论如何都是这样)。

Nevertheless, the output of the above program for me (with JDK 1.8.0_31) is 不过,上面的程序输出对我来说(JDK 1.8.0_31)是

Scaling  1.00000, width 19.44824
Scaling  0.50000, width 19.44824
Scaling  0.33333, width 19.32669
Scaling  0.25000, width 19.44824
Scaling  0.20000, width 19.44824
Scaling  0.16667, width 19.32669
Scaling  0.14286, width 19.14436
Scaling  0.12500, width 19.44824
Scaling  0.11111, width 19.14436
Scaling  0.10000, width 19.44824
Scaling  0.09091, width 19.38747
Scaling  0.08333, width 18.96204
Scaling  0.07692, width 18.96204
Scaling  0.07143, width 18.71893
Scaling  0.06667, width 19.14436
Scaling  0.06250, width 19.44824
Scaling  0.05882, width 18.59738
Scaling  0.05556, width 18.59738
Scaling  0.05263, width 18.47583
Scaling  0.05000, width 19.44824
Scaling  0.04762, width  0.00000
Scaling  0.04545, width  0.00000
Scaling  0.04348, width  0.00000
Scaling  0.04167, width  0.00000
Scaling  0.04000, width  0.00000
Scaling  0.03846, width  0.00000
Scaling  0.03704, width  0.00000
Scaling  0.03571, width  0.00000
Scaling  0.03448, width  0.00000 

One can see the computed size oddly wiggling about ~18-19. 人们可以看到计算出的大小奇怪地摆动约18-19。 This indicates that the size should indeed be "fixed", regardless of the scaling that is applied to the graphics, and I would not mind the small errors that may come from rounding issues and the ridiculous complexity of font-related computations in general. 这表明大小应该确实是“固定的”,无论应用于图形的缩放,我不介意可能来自舍入问题的小错误和一般的字体相关计算的荒谬复杂性。

But what is not acceptable is that, for a certain scaling factor, the computed size plainly drops to zero. 但是,什么是不能接受的是,一定缩放因子,计算出的大小显然下降到零。 The scaling factor for which this happens depends on the font size, but even for larger fonts, it happens with smaller scaling factors, respectively. 发生这种情况的缩放因子取决于字体大小,但即使对于较大的字体,也会分别使用较小的缩放因子。

Of course, there is an obvious, high level explanation: Somewhere, deep inside the font-related Swing classes like FontRenderContext etc, some computation is performed, scaling some value with the scaling factor of the graphics and then ... casting it to int . 当然,有一个明显的,高级别的解释:某处,在字体相关的Swing类(如FontRenderContext等)深处,执行一些计算,使用图形的缩放因子缩放某些值,然后...将其转换为int (The same was likely the issue in the question linked above). (同样可能是上述问题中的问题)。

And an obvious workaround could be to create a single, fixed FontRenderContext and use this for font-related computations everywhere. 一个明显的解决方法可能是创建一个固定的FontRenderContext并将其用于各处与字体相关的计算。 But this defeats the purpose of the font-related computations usually being bound to a Graphics : Doing the computations with a different FontRenderContext than the painting may introduce deviations between the computed sizes and the actual, painted sizes. 但这会破坏通常与Graphics绑定的字体相关计算的目的:使用与绘画不同的 FontRenderContext进行计算可能会在计算的大小和实际绘制的大小之间引入偏差。

Does anybody have a clean, reliable solution for computing the bounds of strings in Swing, regardless of the font size, and regardless of the scaling factors that are applied to the graphics? 有没有人有一个干净,可靠的解决方案来计算Swing中的字符串边界,无论字体大小如何,无论应用于图形的缩放因子如何?

Actually FontRenderContext has 4 fields 实际上FontRenderContext有4个字段

public class FontRenderContext {
    private transient AffineTransform tx;
    private transient Object aaHintValue;
    private transient Object fmHintValue;
    private transient boolean defaulting;

So transform is part of context. 所以变换是背景的一部分。 If your scale 1/3 of course there is some rounding. 如果你的比例1/3当然有一些四舍五入。

So you can either set AffineTransform to normal (eg no translate and no scale) for the Graphics just before obtaining FontRenderContext. 因此,您可以在获取FontRenderContext之前为图形设置正常的AffineTransform(例如,无翻译和无缩放)。

Or you can just create own and reuse it everywhere 或者您可以创建自己并在任何地方重复使用它

FontRenderContext frc=new FontRenderContext(g.getTransform(), //of just replace with new AffineTransform(),
            g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING),
            g.getRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS));

About GlyphVector creation. 关于GlyphVector创建。 It also could be an option. 它也可以是一种选择。

Check Font.getStringBounds() source 检查Font.getStringBounds()源代码

public Rectangle2D getStringBounds( String str, FontRenderContext frc) {
    char[] array = str.toCharArray();
    return getStringBounds(array, 0, array.length, frc);
}

public Rectangle2D getStringBounds(char [] chars,
                                int beginIndex, int limit,
                                   FontRenderContext frc) {
//some checks skipped

    boolean simple = values == null ||
        (values.getKerning() == 0 && values.getLigatures() == 0 &&
          values.getBaselineTransform() == null);
    if (simple) {
        simple = ! FontUtilities.isComplexText(chars, beginIndex, limit);
    }

    if (simple) {
        GlyphVector gv = new StandardGlyphVector(this, chars, beginIndex,
                                                 limit - beginIndex, frc);
        return gv.getLogicalBounds();

So as you can see StandardGlyphVector is created for simple case (when text has no eg RTL content). 因此,您可以看到StandardGlyphVector是为简单情况创建的(当文本没有例如RTL内容时)。 In opposite case TextLayout is used. 在相反的情况下使用TextLayout。

The result could be like this 结果可能是这样的

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();
}

There may be a solution for this issue (and, by the way, also for the issue in the other question ). 对于这个问题可能有一个解决方案(顺便说一下,对于另一个问题中的问题也是如此 )。 It looks a bit hacky at the first glance, but I considered the advantages and disadvantages of the alternative solutions: 乍一看看起来有些笨拙,但我考虑了替代解决方案的优缺点:

Computing the bounds with Font#getStringBounds and the FontRenderContext of the Graphics2D gave plainly wrong results for certain scaling factors, as described in this question. 使用Font#getStringBoundsGraphics2DFontRenderContext计算边界会给某些缩放因子带来明显错误的结果,如本问题所述。

Computing the bounds using a "default" (untransformed) FontRenderContext (as suggested by StanislavL in his answer ) may be an option (with slight adjustments), but still suffered from the issue described in the other question - namely that the results are wrong for small fonts (with a size smaller than 0.5). 使用“默认”(未转换) FontRenderContext (由StanislavL在他的回答中建议)计算边界可能是一个选项(略有调整),但仍然遇到另一个问题中描述的问题 - 即结果错误小字体(大小小于0.5)。

So I extended the solution approach from the other question, killing two birds bugs with one stone hack: Instead of using a normalized font with a size of 1.0, I'm using a ridiculously large font, compute the size of the bounds with a FontMetrics object, and scale these bounds down based on the original size of the font. 所以,我从扩展其他问题解决的途径,二 的错误一 劈:除了使用尺寸为1.0之间的标准字体,我使用的是大的离谱的字体,用计算范围的大小FontMetrics对象,并根据字体的原始大小缩小这些边界。

This is summarized in this helper class: 这个助手类总结了这一点:

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;

    }
}

(This could be extended / adjusted to use a given Graphics2D object, although one should then verify that the scaling does not affect the FontMetrics as well...) (这可以扩展/调整为使用给定的Graphics2D对象,但是应该验证缩放不会影响FontMetrics ......)

I'm pretty sure that there are cases where this does not work: Right-To-Left text, chinese characters, or all cases where the internal workings of the FontMetrics are simply not sufficient for measuring the text size appropriately. 我很确定在某些情况下这不起作用:从右到左文本,中文字符,或者FontMetrics的内部工作方式不足以适当地测量文本大小的所有情况。 But it works for all cases that are relevant for me (and probably, for many others), and it does not suffer from the aforementioned bugs, and ... it's pretty fast. 但是,它适用于有关的我(和可能,对许多其他人)所有的情况下,它不会从上述错误惨了,...这是相当快的。

Here is a very simple performance comparison (not a real benchmark, but should give a rough measure) : 这是一个非常简单的性能比较(不是真正的基准,但应该给出一个粗略的衡量标准):

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);
    }
}

It computes the bounds of strings with different lengths and font sizes, and the timing results are along the lines of the following: 它计算具有不同长度和字体大小的字符串边界,时序结果如下:

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

So the StringBoundsUtils are faster than the Font#getStringBounds approach by a factor of 5 (even more for longer strings). 因此, StringBoundsUtilsFont#getStringBounds方法快5倍(对于更长的字符串甚至更多)。

The result column in the output above already indicates that the difference between the widths of the bounds computed with Font#getStringBounds and the width of the bounds computed with these StringBoundsUtils is negligible. 上面输出中的result列已经表明使用Font#getStringBounds计算的边界宽度与使用这些StringBoundsUtils计算的边界宽度之间的StringBoundsUtils可以忽略不计。

However, I wanted to make sure that this is not only true for the widhts, but for the whole bounds . 但是,我想确保这不仅适用于widhts,而且适用于整个范围 So I created a small test: 所以我创建了一个小测试:

StringBoundsUtilsTest01

In this example, one can see that the bounds are "practically equal" for both approaches, regardless of the scaling and font size - and, of course, that the StringBoundsUtils compute the proper bounds even for font sizes smaller than 0.5. 在这个例子中,无论缩放和字体大小如何,都可以看到两种方法的边界“实际上相等” - 当然,即使字体大小小于0.5, StringBoundsUtils计算出正确的边界。

The source code of this test, for completeness: (It uses a small Viewer library , the Viewer JAR is in Maven Central ) 这个测试的源代码,为了完整性:(它使用一个小的Viewer库Viewer JAR在Maven Central中

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());
    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM