簡體   English   中英

JLabels 在復雜的繪制過程中繪制不正確

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

我正在從頭開始編寫自定義 HTML 渲染器。 它呈現塊、內聯塊、內聯元素、浮動等。

我制作了一個擴展 JPanel 並覆蓋paintComponent()方法的Drawable接口。 它有兩個子類:塊和字符。

我的代碼是這樣工作的:我有一個名為panel的根JPanel ,它包含在WebDocument Swing 元素中,它也擴展了JPanel 我所有的塊,包括根 HTML 元素,都被添加到該面板上並具有坐標 (0,0) 並跨越整個panel的區域。 這樣我就可以正確渲染溢出的內容。

然后,每個元素內部都有一個名為text_layerJPanel ,它也有坐標 (0,0) 並跨越整個區域。 我不記得我為什么這樣做了,也許是因為在選擇時更容易實現文本突出顯示,或者我在 ClearType 抗鋸齒方面遇到了一些問題。

我還有兩種 Drawables:Blocks 和 Characters,它們都擴展了JPanel 每個Block都有一個type字段,它只有 2 個值 - 用於 HTML 元素和文本節點。 在布局過程中, Layouter實體為每個塊創建線,每個塊都有自己的Line實體數組。 Line 可以包含任意數量的任何Drawable - 一個 Block 或一個 Character,但通常它們不能混合,因為只有type值為 'text' 的葉節點由Characters組成,並且它們不能包含Blocks在他們里面。

最后,每個Character元素都擴展了JPanel類,並將自己定位在某個任意位置,而不是 (0,0),這與Blocks不同。 它的尺寸也非常受限。 它內部有一個glyph JLabel 字段,代表一個字母, glyph被添加到Character (即JPanel )中並位於 (0,0),因此它與它完美對齊。

它在很多情況下都工作得很好(例如,我可以有一個類型為“元素”的Block ,里面有 3 個類型為“文本”的Blocks ,它們包含Characters ,而字符又包含glyph JLabel,並且一切正常,包括我的自定義文本選擇)。 但是,當我有一個“元素” Block和另一個“元素” Block時,它會停止工作——無論是“塊”還是“內聯”,它都是在 HTML 布局方面。

有時最里面的塊中的文本不會顯示,有時文本會呈現,但選擇根本不起作用(選擇是通過改變glyph JLabels 的背景來執行的)。 我的意思是,我看到調試消息,正在選擇正確的字母,但視覺上沒有任何變化。

有時文本在重繪后會變成半透明或非常非常黑(例如,重繪是由鼠標單擊和拖動觸發的)。 如果我將每個text_layer JPanel 直接添加到 WebDocument 中的根 JPanel ,則可以看到帶有非常黑色和粗體文本的第二種情況(如舊glyph WebDocument未被刪除,即使我明確地將它們從text_layers中刪除)。 如果我將text_layers添加到它們的“所有者” Blocks中,可以看到在幾個第一個Blocks中具有半透明文本的第一種情況。

此外,我還必須注意paintComponent()方法,該方法又調用我的自定義draw()方法,僅針對Blocks而不是針對Characters被覆蓋。 所以Characters不會自己繪制,它們只是充當JLabels的容器。

我不知道如何調試它以及如何在這種情況下制作一個最小的工作示例。 你能幫忙或給出建議在哪里看嗎?

更新:我非常接近解決方案,但仍需要修復方面的幫助。

當我刪除布局並用我自己的方法重新繪制時,JLabel(字母)的文本半透明和自定義文本選擇的問題完全消失了。 你看,我的WebDocument上有一個調整大小處理程序,它在調整組件大小時觸發。 絕對需要將內容調整為用戶調整的窗口大小。

但是當啟動測試程序時,我同時進行了兩個“布局”和“重繪”。 這就是為什么我看到這樣的事情:

渲染結果

我已經嘗試使用易失性標志isPainting並使我自己的繪畫/重繪方法同步。 但由於某種原因,它沒有幫助。

因此,當我僅在componentResized()組件偵聽器中保留布局和重繪調用時,它工作正常。 但這不是正確的解決方案,因為我的測試方法是獨立的。 我的意思是,我需要能夠隨時清除畫布並調用另一個方法,因此布局和重繪調用也應該保留在那里。

布局方法已經同步。 所以,我真的不明白發生了什么,為什么會重疊。

以下是代碼的關鍵部分:

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

那是我的構造函數。

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

這就是調整組件事件的回調。 請注意,如果我刪除invokeLater()包裝,錯誤仍然存在,但直到達到超時為止根面板為空。 使用invokeLater()我可以立即看到初始文檔版本,所以我想最好保留它。

這是一般的布局代碼()請注意,該方法在不同的 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;
}

最后是自定義繪圖/繪畫代碼:

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

我嘗試了更多的實驗,這是我發現的:

  1. 如果我完全注釋掉componentResized()方法中的代碼,則根本不會打印任何內容 - 我想那是因為我沒有為我的 Swing 容器硬編碼設置正確的尺寸,所以其中一個尺寸可以設置為零我的代碼。
  2. 如果我將basicTest()調用移至main()方法,則沒有任何改變。
  3. 如果我在basicTest()調用后清除面板內容和document的邏輯模型然后再次調用它,並將componentResized()中的代碼留在原地,我沒有工作文本選擇,但第一個的半透明文本元素不見了。 同樣在這種情況下,第二個紫色輪廓塊中的文本似乎比應有的更粗。
  4. 如果我保留 (3) 中的“重新初始化”代碼,但再次注釋掉componentResized() ,則不會繪制任何內容。

正如我現在看到的,如果我刪除我的componentResized()方法或調整大小事件偵聽器分配,我根本沒有輸出。

這很奇怪,因為它在 20 分鍾前就可以很好地繪制內容,即使沒有調整大小處理程序——也許是因為我在布局/繪制方法開始時在sleep后使用了invokeLater()調用,我不知道。

因此:

當我在不同線程上有兩個布局/重繪調用“對”時,我看到文本選擇和文本透明度(它不應該是透明的)被破壞:在構造函數(主)線程或來自LayoutTest類的invokeLater()的匿名線程中,以及來自document.resize()方法中invokeLater()的 AWT-EventQueue 線程/匿名線程。

無論如何,這兩個線程是不同的,所以我應該擺脫其中一個。 但是,如果我擺脫第二個,則不會顯示任何內容,即使視口的所有尺寸都是硬編碼的。 所以我必須保留第二個調用,並在我的類中重新實現邏輯。

也許在某處保留一個Map ,它會告訴我們要顯示哪個測試用例。 然后調用將呈現當前測試的單個test()方法,而不是在構造函數中調用test() ,讓它由onComponentResized()代碼調用。 並保持調整大小處理程序到位。

聽起來您在呈現自定義 HTML 渲染器時遇到了一些問題。 在沒有看到代碼並且無法重現問題的情況下,很難提供特定的解決方案。

您可以嘗試調試並可能解決問題的一些事情:

確保在需要更新時正確地使組件失效並重新繪制它們。 您可以使用 JPanel 類的 invalidate 和 repaint 方法來執行此操作。

檢查您是否正確處理了嵌套塊的布局和渲染。 確保您相對於它們的父塊正確定位了內部塊,並且當父塊的大小發生變化時您正確地更新了布局。

檢查可能導致渲染問題的任何潛在線程問題。 確保您沒有同時從多個線程修改組件,因為這會導致渲染問題。

考慮使用布局管理器來處理組件的定位和大小。 這可以更輕松地管理組件的布局,尤其是當您有嵌套塊時。

確保您正確處理了文本的渲染,包括字體渲染和文本抗鋸齒。 這可能是一個復雜的問題,有許多因素會影響文本的呈現方式,例如字體、文本大小以及 Graphics 對象使用的呈現提示。

我在我的代碼中發現了一個錯誤,它與 Swing 無關。 問題出在parts向量上,我在其中存儲了內聯塊的片段(以在行邊界上拆分單個文本跨度)。 因為我忘記清除向量並從我的 Swing 層次結構中刪除片段(Block/JPanel 實例),所以在第二次布局調用之后一切都被破壞了。

謝謝你們的幫助,伙計們。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM