2015-10-21 45 views
4

如已經在related question提到:界 - 可能的錯誤

有關於計算,應該是一個字符串的大小(寬度或高度)許多(許多)的問題畫成一個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()); 
     } 

    } 
} 

該程序創建一個Graphics2D實例(它不要緊,無論它來自JComponentBufferedImage)然後將各種縮放因子應用於此圖形對象,並使用圖形對象的FontRenderContext來計算字符串的邊界。

從我的理解來看,圖形對象的縮放因子應該是而不是會影響界限(人們可能期望在這裏有所不同,但這就是它看起來要做的)。

然而,上述程序的用於我的輸出(與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 

人們可以看到計算的大小奇怪擺動約18-19〜。這表明尺寸應該確實是「固定的」,而不管應用於圖形的縮放比例如何,我不介意可能來自四捨五入問題的小錯誤以及一般字體相關計算的荒謬複雜性。

而不是什麼是可接受的是,對於某個比例因子,計算的大小明顯下降到零。發生這種情況的縮放因子取決於字體大小,但即使對於較大的字體,它也會分別發生在較小的縮放因子下。

當然,有一個明顯的,高層次的解釋:某處深處的字體相關的Swing類,如FontRenderContext等,執行一些計算,用圖形的縮放因子縮放一些值,然後...將它鑄造成int。 (上面提到的問題也可能是這個問題)。

而且一個明顯的解決辦法可能是建立一個單一的,固定的FontRenderContext到處都用這個字體相關的計算。但是,這違背通常被綁定到一個Graphics字體相關的計算的目的:做一個不同FontRenderContext比繪畫的計算可能會引入計算的大小和實際的,畫的尺寸之間的偏差。

是否有人有用於計算串的邊界在Swing,無論字體大小的清潔,可靠的解決方案,並且無論被施加到圖形縮放因子的?

+1

我傾向於'String'表示形式轉換爲'Shape'然後應用所有變換(例如縮放)到「形狀」。 –

+0

@AndrewThompson這可能工作,但例如創建一個'GlyphVector'並獲取其「邏輯範圍」(如鏈接問題中所做的)是相當昂貴的 - 沒有人願意爲數百或數千個標籤做。我覺得很奇怪,像這樣一個比較簡單的任務就像獲得邊界那麼困難(並且似乎涉及很多微妙的錯誤)。 – Marco13

+0

*「沒有人願意爲數百或數千個標籤做。」*爲自己說話。我,在進入「過早優化」之前,我會嘗試一下。 *「我認爲這樣一個相對(!)簡單的任務很奇怪」*與什麼相比?創造世界和平?這比用文字描述任務複雜得多。 –

回答

2

其實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設置爲正常(例如,不轉換和不縮放)。

或者你也可以創建自己的和重複使用它無處不

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

關於GlyphVector的創作。它也可以是一個選項。

檢查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(); 

因此,大家可以看到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(); 
} 
+0

感謝這些提示。它基本歸結爲使用具有固定(身份)「AffineTransform」的'FontRenderContext',這是我已經提到的解決方法之一。然而,我沒有注意到'Font#getStringBounds'無論如何都在內部創建了一個'GlyphVector',所以我不想通過嘗試避免這種情況獲得性能提升。順便說一句,「StandardGlyphVector」並不是公開的(並且查看它的源代碼揭示了所有與字體相關的計算的怪異問題,這些問題導致了這個錯誤和另一個問題..),但是可以獲得一個。 .. – Marco13

+0

...'Font'中的'GlyphVector',其實**是** StandardGlyphVector'。順便說一句,我寫了另一個答案,但我不確定任一答案是否「可接受」(也適用於其他答案),因爲這兩種方法都可能存在警告。 – Marco13

+0

如果它適合您的需求,您的答案是好的。我只是一個想法 – StanislavL

2

可能有針對此問題的解決方案(並且,順便說一下,也爲在other question這個問題)。它着眼於乍一看有點哈克,但我認爲是替代解決方案的優點和缺點:

計算與Font#getStringBounds和邊界的Graphics2DFontRenderContext了某些縮放因子完全錯誤的結果,因爲在此描述題。

計算使用「默認」(未轉換)FontRenderContext(由StanislavL in his answer的建議)可能是一種選擇(略有調整),但是從other question描述的問題仍然遭受的邊界 - 即結果是錯誤的小字體(大小小於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的內部工作不足以測量文本大小的情況適當。但它適用於所有與我相關的案例(也可能適用於其他許多案例),而且它確實會受到上述錯誤的影響,而且它的確很快。

這裏是一個非常簡單的性能對比(不是一個真正的基準,但應該給一個粗略的指標):

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 

所以StringBoundsUtils是由5倍(甚至更長的字符串)比Font#getStringBounds方法快。

上述輸出中的result列已經表明用Font#getStringBounds計算的邊界寬度與用這些StringBoundsUtils計算的邊界寬度之間的差異可以忽略不計。

但是,我想確保這不僅適用於widhts,而且適用於整個界限。所以,我創建了一個小測試:

StringBoundsUtilsTest01

在這個例子中,我們可以看到,邊界是「幾乎等於」對於這兩種方法,無論縮放和字體大小的 - 當然,認爲即使字體大小小於0.5,StringBoundsUtils也會計算出適當的邊界。

該測試的源代碼,爲了完整性:(它使用了一個小Viewer library,所述Viewer JAR is in 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()); 
    } 

} 
+0

要添加一個註釋。如果真實圖形改變了TEXT_ANTIALIASING渲染提示ON/OFF,則測量結果可能是錯誤的。 – StanislavL