如何使用HTML更新JComponent而不闪烁?

时间:2013-04-26 02:38:31

标签: java swing

我正在使用JComponent with HTML inside it(特别是JLabel)来显示和更改可能需要换行的文本。不幸的是,当我更改HTML时会出现明显的闪烁,因为它正在做两个布局+绘制周期而不是一个。有没有办法避免用错误的布局绘制JLabel?我在jframe.revalidate();之后尝试拨打setText,但没有帮助。

通过每秒更改HTML来演示闪烁的示例代码。我在Thread.sleep期间添加了一个人工paintComponent来模拟大型布局树或昂贵的绘画,以便在这个小窗口中可以看到闪烁。在屏幕截图中,左边是正确布局的标签,右边是在闪烁中间捕获的错误截图。

Left: correctly laid out panel. Right: panel during the flicker with incorrect layout

import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.WindowConstants;

public class HtmlJLabelFlicker {
    static final String text =
            "<html>This label is being set to a new value " +
            "that needs to wrap. Unfortunately, this causes " +
            "two layout + paint cycles instead of just one, " +
            "which can cause flicker if the text doesn\u2019t " +
            "change very much between refreshes. ";
    public static void createUI() {
        final JLabel label = new JLabel(text);
        final Timer timer = new Timer(1000, new ActionListener() {
            int n;
            @Override public void actionPerformed(ActionEvent e) {
                label.setText(text + n++);
            }
        });
        timer.start();
        JFrame jframe = new JFrame();
        jframe.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        jframe.addWindowListener(new WindowAdapter() {
            @Override public void windowClosed(WindowEvent e) {
                timer.stop();
            }
        });
        jframe.setSize(300, 300);
        Container content = jframe.getContentPane();
        content.add(label);
        jframe.setVisible(true);
    }

    public static void main(String[] args) throws Exception {
        SwingUtilities.invokeAndWait(new Runnable() {
            @Override public void run() {
                createUI();
            }
        });
    }
}

2 个答案:

答案 0 :(得分:6)

当我在将JLabel插入树中后尝试scrollRectToVisible时,我遇到了与OP相同的问题。问题是在布局的getPreferredSize()期间,包含HTML的JLabel在需要换行之前不知道有多少水平空间可用。它只是返回一个宽大的,假设它根本不需要包装。然后,在绘制期间,JLabel的内部FlowView将“修复”布局并设置其实际所需高度,该高度在下一次布局迭代时被缓存并返回。

我使用的解决方案是调用JLabel.paint,因为似乎没有其他方法可以告诉它完成使用实际可用宽度进行布局。

除此之外,WPF’s MeasureOverrideJavaFX’s prefHeight with contentBiasAndroid’s measure等其他用户界面系统会让度量传递可用空间,以便它们不会出现此问题。

以下是防止闪烁的相关代码:

label.setText(text + n++);
// revalidate, but do so synchronously.
Container validateRoot = label;
while (! validateRoot.isValidateRoot()) {
    Container parent = validateRoot.getParent();
    if (parent == null)
        break;
    validateRoot = parent;
}
// This first validate() call may be excluded if the width is already correct
validateRoot.validate();
NoopGraphics g = new NoopGraphics(0, 0, label.getWidth(), label.getHeight(), label.getGraphicsConfiguration(), false, false);
label.paint(g);
validateRoot.validate();
// Now you can use the measured bounds for e.g. scrollRectToVisible

NoopGraphics.java:

import java.awt.AlphaComposite;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.RenderingHints.Key;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.RenderableImage;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.Collections;
import java.util.Map;

import javax.swing.SwingUtilities;

/**
 * A subclass of Graphics2D that returns the correct FontMetrics but does not
 * actually paint anything.
 *
 * @see <a
 *      href="http://stackoverflow.com/questions/16227877/how-to-update-a-jcomponent-with-html-without-flickering">How
 *      to update a JComponent with HTML without flickering?</a>
 */
public class NoopGraphics extends Graphics2D {
    private Font font;
    private Color color = Color.BLACK;
    private final Rectangle clip;
    private Stroke stroke;
    private Paint paint;
    private Color background;
    private AffineTransform transform = new AffineTransform();
    private final RenderingHints renderingHints = new RenderingHints(Collections.<Key,Object>emptyMap());
    private Composite composite = AlphaComposite.SrcOver;
    private boolean isAntiAliased;
    private boolean usesFractionalMetrics;
    private GraphicsConfiguration graphicsConfiguration;

    public static GraphicsConfiguration getDefaultScreenGraphicsConfiguration() {
        GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice graphicsDevice = graphicsEnvironment.getDefaultScreenDevice();
        GraphicsConfiguration graphicsConfiguration = graphicsDevice.getDefaultConfiguration();
        return graphicsConfiguration;
    }
    public NoopGraphics(int x, int y, int width, int height) {
        this(x, y, width, height, getDefaultScreenGraphicsConfiguration(), false, false);
    }
    public NoopGraphics(int x, int y, int width, int height, GraphicsConfiguration graphicsConfiguration, boolean isAntiAliased, boolean usesFractionalMetrics) {
        this.graphicsConfiguration = graphicsConfiguration;
        this.isAntiAliased = isAntiAliased;
        this.usesFractionalMetrics = usesFractionalMetrics;
        this.clip = new Rectangle(x, y, width, height);
    }
    @Override public void setXORMode(Color c1) {}
    @Override public void setPaintMode() {}
    @Override public Font getFont() {return font;}
    @Override public void setFont(Font font) {this.font=font;}
    @Override public Color getColor() {return color;}
    @Override public void setColor(Color c) {this.color=c;}
    @Override public void setClip(int x, int y, int width, int height) {
    }
    @Override public void setClip(Shape clip) {this.clip.setRect(clip.getBounds());}
    @Override public FontMetrics getFontMetrics(Font f) {
        // http://stackoverflow.com/questions/2753514/java-friendlier-way-to-get-an-instance-of-fontmetrics
        return new Canvas(graphicsConfiguration).getFontMetrics(f);
    }
    @Override public Rectangle getClipBounds() {return clip.getBounds();}
    @Override public Shape getClip() {return clip;}
    @Override public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {}
    @Override public void fillRect(int x, int y, int width, int height) {}
    @Override public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) {}
    @Override public void fillOval(int x, int y, int width, int height) {}
    @Override public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) {}
    @Override public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) {}
    @Override public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) {}
    @Override public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) {}
    @Override public void drawOval(int x, int y, int width, int height) {}
    @Override public void drawLine(int x1, int y1, int x2, int y2) {}
    @Override public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bgcolor, ImageObserver observer) {return true;}
    @Override public boolean drawImage(Image img, int dx1,
            int dy1, int dx2, int dy2, int sx1, int sy1,
            int sx2, int sy2, ImageObserver observer) {
        return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null, observer);
    }
    @Override public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, ImageObserver observer) {
        return false;
    }
    @Override public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) {
        return drawImage(img, x, y, width, height, null, observer);
    }
    @Override public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) {
        return false;
    }
    @Override public boolean drawImage(Image img, int x, int y, ImageObserver observer) {
        return drawImage(img, x, y, null, observer);
    }
    @Override public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) {}
    @Override public void dispose() {}
    @Override public Graphics create() {return this;}
    @Override public void copyArea(int x, int y, int width, int height, int dx, int dy) {}
    @Override public void clipRect(int x, int y, int width, int height) {SwingUtilities.computeIntersection(x, y, width, height, this.clip);}
    @Override public void clearRect(int x, int y, int width, int height) {}
    @Override public void translate(double tx, double ty) {getTransform().translate(tx, ty);}
    @Override public void translate(int x, int y) {translate((double)x, (double)y);}
    @Override public void transform(AffineTransform Tx) {getTransform().concatenate(Tx);}
    @Override public void shear(double shx, double shy) {getTransform().shear(shx, shy);}
    @Override public void scale(double sx, double sy) {getTransform().scale(sx, sy);}
    @Override public void setTransform(AffineTransform Tx) {this.transform = Tx;}
    @Override public void setStroke(Stroke s) {this.stroke = s;}
    @Override public void setRenderingHints(Map<?, ?> hints) {this.renderingHints.clear(); this.renderingHints.putAll(hints);}
    @Override public void setRenderingHint(Key hintKey, Object hintValue) {this.renderingHints.put(hintKey, hintValue);}
    @Override public void setPaint(Paint paint) {this.paint = paint;}
    @Override public void setComposite(Composite comp) {this.composite = comp;}
    @Override public void setBackground(Color color) {this.background = color;}
    @Override public void rotate(double theta, double x, double y) {getTransform().rotate(theta, x, y);}
    @Override public void rotate(double theta) {getTransform().rotate(theta);}
    @Override public boolean hit(Rectangle rect, Shape s, boolean onStroke) {
        return false;
    }
    @Override public AffineTransform getTransform() {return this.transform;}
    @Override public Stroke getStroke() {return this.stroke;}
    @Override public RenderingHints getRenderingHints() {return renderingHints;}
    @Override public Object getRenderingHint(Key hintKey) {return renderingHints.get(hintKey);}
    @Override public Paint getPaint() {return this.paint;}
    @Override public FontRenderContext getFontRenderContext() {return new FontRenderContext(transform, isAntiAliased, usesFractionalMetrics);}
    @Override public GraphicsConfiguration getDeviceConfiguration() {return graphicsConfiguration;}
    @Override public Composite getComposite() {return composite;}
    @Override public Color getBackground() {return background;}
    @Override public void fill(Shape s) {}
    @Override public void drawString(AttributedCharacterIterator iterator, float x, float y) {}
    @Override public void drawString(AttributedCharacterIterator iterator, int x, int y) {drawString(iterator, (float)x, (float)y);}
    @Override public void drawString(String str, float x, float y) {drawString(new AttributedString(str).getIterator(), x, y);}
    @Override public void drawString(String str, int x, int y) {drawString(str, (float)x, (float)y);}
    @Override public void drawRenderedImage(RenderedImage img, AffineTransform xform) {}
    @Override public void drawRenderableImage(RenderableImage img, AffineTransform xform) {}
    @Override public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) {}
    @Override public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) {return false;}
    @Override public void drawGlyphVector(GlyphVector g, float x, float y) {}
    @Override public void draw(Shape s) {}
    @Override public void clip(Shape s) {}
    @Override public void addRenderingHints(Map<?, ?> hints) {renderingHints.putAll(hints);}
}

答案 1 :(得分:0)

您可以尝试使用所需的HTML创建新标签并替换旧标签。

创建并显示原始标签

final JLabel label = new JLabel(originalText);
Container content = jframe.getContentPane();
content.add(label);

创建新标签并替换旧标签

final JLabel newLabel = new JLabel(newtext);
content.remove(label);
content.add(newLabel);
content.validate();    // This will cause the container to show the new label.