简体   繁体   English

JLabels 在复杂的绘制过程中绘制不正确

[英]JLabels are not drawing correctly in complicated drawing process

I'm writing a custom HTML renderer from scratch.我正在从头开始编写自定义 HTML 渲染器。 It renders block, inline-block, inline elements, floats etc.它呈现块、内联块、内联元素、浮动等。

I made a Drawable interface that extends JPanel and overrides paintComponent() method.我制作了一个扩展 JPanel 并覆盖paintComponent()方法的Drawable接口。 It has two subclasses: Block and Character.它有两个子类:块和字符。

My code is working this way: I have a root JPanel called panel , which is contained inside the WebDocument Swing element, which extends JPanel as well.我的代码是这样工作的:我有一个名为panel的根JPanel ,它包含在WebDocument Swing 元素中,它也扩展了JPanel All my blocks, including root HTML element, are added onto that panel and have coordinates (0,0) and span the full panel 's area.我所有的块,包括根 HTML 元素,都被添加到该面板上并具有坐标 (0,0) 并跨越整个panel的区域。 This way I can render overflowing content correctly.这样我就可以正确渲染溢出的内容。

Then, each element has a JPanel called text_layer inside it, that also has coordinates (0,0) and spans the full area.然后,每个元素内部都有一个名为text_layerJPanel ,它也有坐标 (0,0) 并跨越整个区域。 I don't remember why I did it this way, maybe because it was easier to implement text highlight on selection, or maybe I had some problems with ClearType antialiasing.我不记得我为什么这样做了,也许是因为在选择时更容易实现文本突出显示,或者我在 ClearType 抗锯齿方面遇到了一些问题。

Also I have 2 kinds of Drawables: Blocks and Characters, both of them extend JPanel .我还有两种 Drawables:Blocks 和 Characters,它们都扩展了JPanel Every Block has a type field, that has only 2 values - for HTML elements and for text nodes.每个Block都有一个type字段,它只有 2 个值 - 用于 HTML 元素和文本节点。 During layout process the Layouter entity creates lines for each block, and every block has its own array of Line entities.在布局过程中, Layouter实体为每个块创建线,每个块都有自己的Line实体数组。 Line can contain any Drawable in it in any quantity - a Block or a Character, but generally they can't be mixed, because only leaf nodes, that have a type value of 'text', consist of Characters , and they cannot contain Blocks inside them. Line 可以包含任意数量的任何Drawable - 一个 Block 或一个 Character,但通常它们不能混合,因为只有type值为 'text' 的叶节点由Characters组成,并且它们不能包含Blocks在他们里面。

Finally, each Character element extends JPanel class, and positions itself in some arbitrary spot, and not (0,0), unlike the Blocks .最后,每个Character元素都扩展了JPanel类,并将自己定位在某个任意位置,而不是 (0,0),这与Blocks不同。 It also has a very constrained size.它的尺寸也非常受限。 It has a glyph JLabel field inside it, which represents a single letter, and the glyph is added into the Character (that is a JPanel ) and is positioned at (0,0), so it is aligned with it perfectly.它内部有一个glyph JLabel 字段,代表一个字母, glyph被添加到Character (即JPanel )中并位于 (0,0),因此它与它完美对齐。

It works just well in many cases (for example, I can have a Block with type 'element', and 3 Blocks with type 'text' inside it, they contain Characters , which in turn contain glyph JLabels, and everything works fine, including my custom text selection).它在很多情况下都工作得很好(例如,我可以有一个类型为“元素”的Block ,里面有 3 个类型为“文本”的Blocks ,它们包含Characters ,而字符又包含glyph JLabel,并且一切正常,包括我的自定义文本选择)。 But it stops working when I have an 'element' Block with another 'element' Block inside it - no matter, 'block' or 'inline' it is in terms of HTML layout.但是,当我有一个“元素” Block和另一个“元素” Block时,它会停止工作——无论是“块”还是“内联”,它都是在 HTML 布局方面。

Sometimes the text in innermost blocks just won't show up, and sometimes the text does render, but the selection does not work at all (the selection is performed by changing background of glyph JLabels).有时最里面的块中的文本不会显示,有时文本会呈现,但选择根本不起作用(选择是通过改变glyph JLabels 的背景来执行的)。 I mean, I see debug messages, that correct letters are being selected, but nothing does visually change.我的意思是,我看到调试消息,正在选择正确的字母,但视觉上没有任何变化。

And sometimes the text becomes either semi-transparent or very, very black after repaint (the repaint is triggered by mouse clicks and drags, for example).有时文本在重绘后会变成半透明或非常非常黑(例如,重绘是由鼠标单击和拖动触发的)。 The second situation with very black and bold text (like the old glyph JLabels are not removed, even though I explicitly remove them from text_layers ) can be seen if I add every text_layer JPanel directly to the root JPanel in WebDocument .如果我将每个text_layer JPanel 直接添加到 WebDocument 中的根 JPanel ,则可以看到带有非常黑色和粗体文本的第二种情况(如旧glyph WebDocument未被删除,即使我明确地将它们从text_layers中删除)。 If I add text_layers to their 'owner' Blocks , the first situation with semi-transparent text in several first Blocks can be seen.如果我将text_layers添加到它们的“所有者” Blocks中,可以看到在几个第一个Blocks中具有半透明文本的第一种情况。

Also I have to note that the paintComponent() method, that in turn calls my custom draw() method, is overridden only for Blocks , and not for Characters .此外,我还必须注意paintComponent()方法,该方法又调用我的自定义draw()方法,仅针对Blocks而不是针对Characters被覆盖。 So Characters do not draw themselves, they just act as the containers for JLabels .所以Characters不会自己绘制,它们只是充当JLabels的容器。

I don't know how to debug it and how to make a minimal working example in this situation.我不知道如何调试它以及如何在这种情况下制作一个最小的工作示例。 Can you please help or give an advice where to look at?你能帮忙或给出建议在哪里看吗?

UPDATE: I'm very close to the solution, but still need help with the fix.更新:我非常接近解决方案,但仍需要修复方面的帮助。

The problem with text semi-transparency of JLabels (letters) and with custom text selection is completely gone when I remove layout and repaint in my own method.当我删除布局并用我自己的方法重新绘制时,JLabel(字母)的文本半透明和自定义文本选择的问题完全消失了。 You see, I have a resize handler on my WebDocument , which is triggered when the component is resized.你看,我的WebDocument上有一个调整大小处理程序,它在调整组件大小时触发。 It is absolutely needed to adjust the content to the window resizing by user.绝对需要将内容调整为用户调整的窗口大小。

But when the test program is launched, I have two "layouts" and "repaints" going on at the same time.但是当启动测试程序时,我同时进行了两个“布局”和“重绘”。 That's why I see such things:这就是为什么我看到这样的事情:

渲染结果

I have already tried using volatile flag isPainting and making my own paint/repaint methods synchronized.我已经尝试使用易失性标志isPainting并使我自己的绘画/重绘方法同步。 But for some reason it did not help.但由于某种原因,它没有帮助。

So, when I leave the layout and repaint calls only in componentResized() component listener, it works fine.因此,当我仅在componentResized()组件侦听器中保留布局和重绘调用时,它工作正常。 But this is not the proper solution, because my test methods are self-contained.但这不是正确的解决方案,因为我的测试方法是独立的。 I mean, I need to be able to clear the canvas and call another method at any time, so the layout and repaint calls should stay there, too.我的意思是,我需要能够随时清除画布并调用另一个方法,因此布局和重绘调用也应该保留在那里。

The layout methods are already synchronized.布局方法已经同步。 So, I can't really understand what is going on and why it is overlapping.所以,我真的不明白发生了什么,为什么会重叠。

Here are critical parts of the code:以下是代码的关键部分:

public LayoutTests() {
    super("Simple component test");
    document = new WebDocument();
    JPanel cp = new JPanel();
    cp.setLayout(new BoxLayout(cp, BoxLayout.PAGE_AXIS));
    bp = new JPanel();
    bp.setLayout(new BoxLayout(bp, BoxLayout.LINE_AXIS));

    document.setBorder(BorderFactory.createLineBorder(Color.black, 1));

    document.panel.setBackground(Color.WHITE);
    setContentPane(cp);

    cp.add(document);
    cp.add(bp);

    document.width = 478;
    document.height = 300;
    //document.panel.setBounds(0, 0, cp.width, cp.height);
    //document.root.setBounds(1, 1, cp.width-2, cp.height-2);
    document.root.addMouseListeners();
    
    //bp.setBounds(9, 283, 474, 86);

    Block root = document.root;

    root.setBounds(1, 1, document.width, document.height);

    root.setWidth(-1);
    root.height = document.height-2;
    root.viewport_height = root.height;
    root.orig_height = root.height;
    root.max_height = root.height;
    //root.setBounds(root.getX(), root.getY(), root.width, root.height);


    document.debug = true;

    // Here the web document is getting laid out and painted
    basicTest();

    document.getParent().invalidate();
    document.repaint();
    btn = new JButton("Close");
    btn.addActionListener(new ActionListener(){
        @Override
        public void actionPerformed(ActionEvent e) {
            System.exit(0);
        }
    });
    //btn.setBounds((bp.getWidth() - btn.getPreferredSize().width) / 2, (bp.getHeight() - btn.getPreferredSize().height)-10, btn.getPreferredSize().width, btn.getPreferredSize().height);
    bp.add(Box.createHorizontalGlue());
    bp.add(btn);
    bp.add(Box.createHorizontalGlue());
    cp.setBorder(BorderFactory.createEmptyBorder(9, 10, 9, 10));
    cp.setPreferredSize(new Dimension(494, 370));
    pack();
    setLocationRelativeTo(null);
    setDefaultCloseOperation(EXIT_ON_CLOSE);

    addComponentListener(new java.awt.event.ComponentAdapter() {
        @Override
        public void componentMoved(java.awt.event.ComponentEvent evt) {}

        @Override
        public void componentResized(java.awt.event.ComponentEvent evt) {
            int w = getWidth();
            int h = getHeight();
            Insets insets = getInsets();
            if (insets != null) {
                w -= insets.left + insets.right;
                h -= insets.top + insets.bottom;
            }
            document.setBounds(pad[0], pad[1], w - pad[0] * 2, h - 93);
            bp.setBounds(pad[0], h - 38, w - pad[0] * 2, 30);
            document.resized();
        }
    });

That's my constructor.那是我的构造函数。

public void resized() {
    //panel.repaint();
    width = getWidth();
    height = getHeight();
    panel.setBounds(borderSize, borderSize, getWidth() - borderSize * 2, getHeight() - borderSize * 2);
    if (getWidth() != last_width || getHeight() != last_height) {
        root.setBounds(borderSize, borderSize, width - borderSize * 2 - root.getScrollbarYSize(), height - borderSize * 2 - root.getScrollbarXSize());
        root.document.ready = false;
        root.width = width - borderSize * 2 - 2;
        root.height = height - borderSize * 2 - 2;
        root.viewport_width = !root.hasVerticalScrollbar() ? root.width : root.width - root.getScrollbarYSize();
        root.viewport_height = !root.hasHorizontalScrollbar() ? root.height : root.height - root.getScrollbarXSize();
        root.orig_height = (int) (root.height / root.ratio);
        root.max_height = root.height;
        root.document.ready = true;
        //System.err.println("Root width: " + root.width);
        //System.err.println("Viewport width: " + root.viewport_width);
        final Block instance = root;
        if (root.document.ready && !root.document.isPainting && !resizing) {
            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    try {
                        // Delay here is just for testing purposes
                        Thread.sleep(1500);
                        resizing = true;
                        root.performLayout();
                        root.forceRepaintAll();
                        resizing = false;
                        JFrame frame = (JFrame) SwingUtilities.getWindowAncestor(instance);
                        frame.getContentPane().repaint();
                    } catch (InterruptedException ex) {
                        Logger.getLogger(WebDocument.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }

            });
            
        }
    }
    last_width = getWidth();
    last_height = getHeight();
}

And that's the callback for the resize component event.这就是调整组件事件的回调。 Note that if I remove the invokeLater() wrap, the bug is still present, but until the root panel is empty until the timeout is reached.请注意,如果我删除invokeLater()包装,错误仍然存在,但直到达到超时为止根面板为空。 With invokeLater() I can see the initial document version instantly, so I guess it is better to keep it.使用invokeLater()我可以立即看到初始文档版本,所以我想最好保留它。

Here is the layout code in general ()note that the method calls itself recursively on different Block nodes and even on the same one sometimes to update its scrollbar presence/absence state):这是一般的布局代码()请注意,该方法在不同的 Block 节点上递归调用自身,有时甚至在同一个节点上更新其滚动条存在/不存在状态):

public synchronized void performLayout() {
    if (document.inLayout && this == document.root) {
        try {
            // Another layout is in progress, try to wait a little bit
            Thread.sleep(50);
            System.out.println("Retrying layout...");
        } catch (InterruptedException ex) {
            Logger.getLogger(Block.class.getName()).log(Level.SEVERE, null, ex);
        }
        performLayout();
        return;
    }
    document.inLayout = true;
    // The actual element layout is performed in here
    performLayout(false, false);
    document.inLayout = false;
}

And finally the custom drawing/painting code:最后是自定义绘图/绘画代码:

public synchronized void forceRepaintAll() {
    if (document.isPainting) {
        try {
            // Another repaint is in progress, try to wait a little bit
            Thread.sleep(50);
            System.out.println("Retrying repaint...");
        } catch (InterruptedException ex) {
            Logger.getLogger(Block.class.getName()).log(Level.SEVERE, null, ex);
        }
        forceRepaintAll();
        return;
    }
    document.isPainting = true;
    if (document.debug) {
        System.out.println("Complete repaint started");
    }
    // Searching for root; we could just use document.root field,
    // but this way we can process detached elemkent subtrees, so why not
    Block b = this;
    while (b.parent != null) {
        b = b.parent;
    }
    // Flush temporary buffers
    b.flushBuffersRecursively();
    // Actual painting goes here
    b.draw();
    if (document.debug) {
        System.out.println("Complete repaint finished");
    }
    document.isPainting = false;
}

I tried experimenting a bit more, and here is what I found:我尝试了更多的实验,这是我发现的:

  1. If I completely comment out the code in componentResized() method, nothing is printed at all - I guess that's because I don't have the right dimensions set for my Swing containers hardcoded, so one of the dimensions can be just set to zero in my code.如果我完全注释掉componentResized()方法中的代码,则根本不会打印任何内容 - 我想那是因为我没有为我的 Swing 容器硬编码设置正确的尺寸,所以其中一个尺寸可以设置为零我的代码。
  2. If I move the basicTest() call to main() method, nothing is changed.如果我将basicTest()调用移至main()方法,则没有任何改变。
  3. If I clear the panel contents and the document 's logical model after basicTest() call and then call it again, and leave the code in componentResized() in place, I have NOT working text selection, but the semi-transparency of the first text element is gone.如果我在basicTest()调用后清除面板内容和document的逻辑模型然后再次调用它,并将componentResized()中的代码留在原地,我没有工作文本选择,但第一个的半透明文本元素不见了。 Also in this case the text in the second purple outlined block seems bolder than it should be.同样在这种情况下,第二个紫色轮廓块中的文本似乎比应有的更粗。
  4. If I leave the "re-init" code from (3), but comment out the componentResized() again, nothing is painted.如果我保留 (3) 中的“重新初始化”代码,但再次注释掉componentResized() ,则不会绘制任何内容。

As I can see now, I have no output at all if I remove my componentResized() method or the resize event listener assignment.正如我现在看到的,如果我删除我的componentResized()方法或调整大小事件侦听器分配,我根本没有输出。

That's strange, because it was painting the content just fine 20 minutes ago even without the resize handler - maybe because I was using invokeLater() calls after sleep in the beginning of layout/paint methods, I don't know.这很奇怪,因为它在 20 分钟前就可以很好地绘制内容,即使没有调整大小处理程序——也许是因为我在布局/绘制方法开始时在sleep后使用了invokeLater()调用,我不知道。

As a result:因此:

I see that the text selection and the text transparency (it should NOT be transparent) are broken when I have two layout/repaint call "pairs" on different threads: in constructor (Main) thread or anonymous thread from invokeLater() from LayoutTest class, and from the AWT-EventQueue Thread / anonymous thread from invokeLater() in the document.resize() method.当我在不同线程上有两个布局/重绘调用“对”时,我看到文本选择和文本透明度(它不应该是透明的)被破坏:在构造函数(主)线程或来自LayoutTest类的invokeLater()的匿名线程中,以及来自document.resize()方法中invokeLater()的 AWT-EventQueue 线程/匿名线程。

These two threads are different anyway, so I should get rid from one of them.无论如何,这两个线程是不同的,所以我应该摆脱其中一个。 But if I get rid from the second one, nothing is displayed, even though all the dimensions of the viewport are hardcoded.但是,如果我摆脱第二个,则不会显示任何内容,即使视口的所有尺寸都是硬编码的。 So I have to keep the second call, and re-implement the logic in my class.所以我必须保留第二个调用,并在我的类中重新实现逻辑。

Maybe keep a Map somewhere, which will tell, which test case we are wanting to display.也许在某处保留一个Map ,它会告诉我们要显示哪个测试用例。 And then call the single test() method which will render the current test, and not call test() in constructor, leaving it to be called by onComponentResized() code.然后调用将呈现当前测试的单个test()方法,而不是在构造函数中调用test() ,让它由onComponentResized()代码调用。 And keep the resize handler in place.并保持调整大小处理程序到位。

It sounds like you are experiencing some issues with the rendering of your custom HTML renderer.听起来您在呈现自定义 HTML 渲染器时遇到了一些问题。 It's difficult to provide a specific solution without seeing the code and being able to reproduce the issue.在没有看到代码并且无法重现问题的情况下,很难提供特定的解决方案。

There are a few things you could try to debug and potentially fix the issue:您可以尝试调试并可能解决问题的一些事情:

Make sure you are properly invalidating and repainting the components when they need to be updated.确保在需要更新时正确地使组件失效并重新绘制它们。 You can use the invalidate and repaint methods of the JPanel class to do this.您可以使用 JPanel 类的 invalidate 和 repaint 方法来执行此操作。

Check that you are properly handling the layout and rendering of nested blocks.检查您是否正确处理了嵌套块的布局和渲染。 Make sure that you are positioning the inner blocks correctly relative to their parent blocks, and that you are properly updating the layout when the size of the parent block changes.确保您相对于它们的父块正确定位了内部块,并且当父块的大小发生变化时您正确地更新了布局。

Check for any potential threading issues that may be causing problems with the rendering.检查可能导致渲染问题的任何潜在线程问题。 Make sure that you are not modifying the components from multiple threads concurrently, as this can cause rendering issues.确保您没有同时从多个线程修改组件,因为这会导致渲染问题。

Consider using a layout manager to handle the positioning and sizing of your components.考虑使用布局管理器来处理组件的定位和大小。 This can make it easier to manage the layout of your components, especially when you have nested blocks.这可以更轻松地管理组件的布局,尤其是当您有嵌套块时。

Make sure you are properly handling the rendering of text, including font rendering and text antialiasing.确保您正确处理了文本的渲染,包括字体渲染和文本抗锯齿。 This can be a complex issue, and there are a number of factors that can affect the way text is rendered, such as the font, the size of the text, and the rendering hints used by the Graphics object.这可能是一个复杂的问题,有许多因素会影响文本的呈现方式,例如字体、文本大小以及 Graphics 对象使用的呈现提示。

I found a bug in my code, and it was not Swing related.我在我的代码中发现了一个错误,它与 Swing 无关。 The problem was with the parts vector where I stored fragments of inline blocks (to split single text spans on line boundaries).问题出在parts向量上,我在其中存储了内联块的片段(以在行边界上拆分单个文本跨度)。 Because I forgot to clear the vector and to remove the fragments (Block/JPanel instances) from my Swing hierarchy, after the second layout call everything got broken.因为我忘记清除向量并从我的 Swing 层次结构中删除片段(Block/JPanel 实例),所以在第二次布局调用之后一切都被破坏了。

Thanks for trying to help, guys.谢谢你们的帮助,伙计们。

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

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