You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确实现CompositeView以显示Swing Markdown编辑器组件?

自定义Swing Markdown EditorKit列表视图子组件不显示问题

问题背景

基于javax.swing.text开发Markdown EditorKit,解析器工作正常,但列表对应的自定义CompositeView(MDListView)子组件完全空白。测试字符串解析后的结构:

root
| - paragraph
|   | - text
| - list
|   | - paragraph
|   |   | - text
|   |   | - code span
|   |   | - text

段落转BoxView(elem, X_AXIS)、文本转自定义View均正常,仅列表区域无内容。

调试信息

  • MDListView的getView(0)返回正确视图,getView(1)返回null
  • getView(0)的childAllocation为空矩形
  • getChildAllocation从未被调用

完整ViewFactory代码

package com.myenvironment.swing.markdown;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;

import javax.swing.text.BadLocationException;
import javax.swing.text.BoxView;
import javax.swing.text.CompositeView;
import javax.swing.text.Element;
import javax.swing.text.Position.Bias;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;

import com.myenvironment.swing.markdown.MarkdownDocument.CodeSpan;
import com.myenvironment.swing.markdown.MarkdownDocument.MDList;
import com.myenvironment.swing.markdown.MarkdownDocument.Text;

//TODO: finish the markdown view factory
public class MarkdownViewFactory implements ViewFactory{
    static abstract class MarkdownView extends View{
        public MarkdownView(Element e){
            super(e);
        }
        /**
         * A stub, added for compatibility.
         * Makes no sense for read-only documents.
         * @see View#modelToView(int,Shape,Bias)
         */
        public Shape modelToView(int pos,Shape a,Bias b)throws BadLocationException{
            return new Rectangle();
        }
        /**
         * A stub, added for compatibility.
         * Makes no sense for read-only documents.
         * @see View#viewToModel(float, float, Shape, Bias[])
         */
        public int viewToModel(float x,float y,Shape a,Bias[]biasReturn){
            return 0;
        }
    }
    static abstract class MarkdownContainerView extends CompositeView{
        public MarkdownContainerView(Element e){
            super(e);
        }
        public void paint(Graphics g,Shape allocation){
            for(int i=0;i<getViewCount();++i){
                Shape a=getChildAllocation(i,allocation);
                if(a!=null)getView(i).paint(g,a);
            }
        }
        /**
         * A stub, added for compatibility.
         * Makes no sense for read-only documents.
         * @see View#modelToView(int,Shape,Bias)
         */
        public Shape modelToView(int pos,Shape a,Bias b)throws BadLocationException{
            return new Rectangle();
        }
        /**
         * A stub, added for compatibility.
         * Makes no sense for read-only documents.
         * @see View#viewToModel(float, float, Shape, Bias[])
         */
        public int viewToModel(float x,float y,Shape a,Bias[]biasReturn){
            return 0;
        }
        protected boolean isBefore(int x,int y,Rectangle alloc){
            return (alloc.outcode(x,y)&Rectangle.OUT_LEFT)!=0;
        }
        protected boolean isAfter(int x,int y,Rectangle alloc){
            return (alloc.outcode(x,y)&Rectangle.OUT_RIGHT)!=0;
        }
        protected View getViewAtPoint(int x,int y,Rectangle alloc){
            return null;
        }
    }
    static class TextView extends MarkdownView{
        public TextView(Text e){
            super(e);
        }
        public float getPreferredSpan(int axis){
            FontMetrics fm=getContainer().getFontMetrics(getContainer().getFont());
            if(axis==X_AXIS)return fm.stringWidth(((Text)getElement()).text);
            else if(axis==Y_AXIS)return fm.getHeight();
            else throw new IllegalArgumentException();
        }
        public void paint(Graphics g,Shape allocation){
            Graphics2D g2=(Graphics2D)g;
            g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            g2.setClip(allocation);
            Rectangle r=allocation.getBounds();
            FontMetrics fm=g2.getFontMetrics();
            String text=((Text)getElement()).text;
            g2.setColor(getContainer().getForeground());
            g2.drawString(text,r.x+(r.width-fm.stringWidth(text))/2,r.y+(r.height+fm.getAscent()+fm.getLeading()-fm.getDescent())/2);
        }
    }
    static class CodeSpanView extends MarkdownView{
        public CodeSpanView(CodeSpan e){
            super(e);
        }
        public float getPreferredSpan(int axis){
            FontMetrics fm=getContainer().getFontMetrics(getContainer().getFont());
            if(axis==X_AXIS)return fm.stringWidth(((CodeSpan)getElement()).text)+fm.getHeight()/3;
            else if(axis==Y_AXIS)return fm.getHeight();
            else throw new IllegalArgumentException();
        }
        public void paint(Graphics g,Shape allocation){
            Graphics2D g2=(Graphics2D)g;
            g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            g2.setClip(allocation);
            Rectangle r=allocation.getBounds();
            g2.setColor(new Color(50,50,50,200));
            g2.fillRoundRect(r.x,r.y,r.width,r.height,r.height/5,r.height/5);
            g2.setStroke(new BasicStroke(r.height/10,1,1));
            g2.setColor(Color.BLACK);
            g2.drawRoundRect(r.x,r.y,r.width,r.height,r.height/5,r.height/5);
            FontMetrics fm=g2.getFontMetrics();
            String text=((CodeSpan)getElement()).text;
            g2.setFont(new Font(Font.MONOSPACED,Font.PLAIN,getContainer().getFont().getSize()));
            g2.setColor(Color.WHITE);
            g2.drawString(text,r.x+(r.width-fm.stringWidth(text))/2,r.y+(r.height+fm.getAscent()+fm.getLeading()-fm.getDescent())/2);
        }
    }
    static class MDListView extends MarkdownContainerView{
        private float xSpanCached=-1,ySpanCached=-1;
        public MDListView(MDList e,ViewFactory f){
            super(e);
        }
        public float getPreferredSpan(int axis){
            if(axis==Y_AXIS){
                if(ySpanCached==-1){
                    ySpanCached=0;
                    for(int i=0;i<getViewCount();++i)ySpanCached+=getView(i).getPreferredSpan(Y_AXIS);
                }
                return ySpanCached;
            }else if(axis==X_AXIS){
                if(xSpanCached==-1){
                    xSpanCached=0;
                    for(int i=0;i<getViewCount();++i)xSpanCached=Math.max(ySpanCached,getView(i).getPreferredSpan(X_AXIS));
                }
                return xSpanCached;
            }else throw new IllegalArgumentException();
        }
        public Shape getChildAllocation(int index,Shape s){
            Rectangle r=s.getBounds();
            return new Rectangle(r.x,r.y+(int)(index*getPreferredSpan(Y_AXIS)/getViewCount()),r.width,r.height/getViewCount());
        }
        public void replace(int offset,int length,View[]views){
            super.replace(offset,length,views);
            xSpanCached=-1;
            ySpanCached=-1;
        }
        protected void childAllocation(int index,Rectangle a){
            a.setBounds(getChildAllocation(index,a).getBounds());
        }
    }
    public View create(Element elem){
        View v=switch(elem){
            case MarkdownDocument.Text e->new TextView(e);
            case MarkdownDocument.CodeSpan e->new CodeSpanView(e);
            case MDList e->new MDListView(e,this);
            default->new BoxView(elem,BoxView.X_AXIS);
            // default->throw new UnsupportedOperationException("No view for "+elem.getName());
        };
        return v;
    }
}

问题根源与修复方案

核心问题

  1. 子视图未初始化:CompositeView不会自动创建子视图,必须调用build()方法,且需要实现createView()来使用ViewFactory创建子元素对应的View。
  2. getPreferredSpan计算错误:X轴宽度计算时错误引用了ySpanCached变量,导致宽度为0或异常值。
  3. 子视图位置计算错误:平均分配高度的逻辑不符合子视图实际高度,导致子视图可能被挤出显示区域或重叠。

修复后的MDListView代码

static class MDListView extends MarkdownContainerView{
    private float xSpanCached=-1,ySpanCached=-1;
    private final ViewFactory viewFactory;

    public MDListView(MDList e, ViewFactory f){
        super(e);
        this.viewFactory = f;
        // 触发子视图构建
        build();
    }

    @Override
    protected View createView(Element elem) {
        // 使用传入的ViewFactory创建子视图
        return viewFactory.create(elem);
    }

    @Override
    public float getPreferredSpan(int axis){
        if(axis==Y_AXIS){
            if(ySpanCached==-1){
                ySpanCached=0;
                for(int i=0;i<getViewCount();++i){
                    ySpanCached+=getView(i).getPreferredSpan(Y_AXIS);
                }
            }
            return ySpanCached;
        }else if(axis==X_AXIS){
            if(xSpanCached==-1){
                xSpanCached=0;
                for(int i=0;i<getViewCount();++i){
                    // 修正:比较子视图的X轴宽度
                    xSpanCached=Math.max(xSpanCached, getView(i).getPreferredSpan(X_AXIS));
                }
            }
            return xSpanCached;
        }else throw new IllegalArgumentException();
    }

    @Override
    public Shape getChildAllocation(int index, Shape s){
        if(index < 0 || index >= getViewCount()) return null;
        Rectangle r=s.getBounds();
        // 累加前面子视图的高度,计算当前子视图的Y偏移
        float yOffset = 0;
        for(int i=0; i<index; i++){
            yOffset += getView(i).getPreferredSpan(Y_AXIS);
        }
        float childHeight = getView(index).getPreferredSpan(Y_AXIS);
        return new Rectangle(r.x, r.y + (int)yOffset, r.width, (int)childHeight);
    }

    @Override
    public void replace(int offset,int length,View[]views){
        super.replace(offset,length,views);
        xSpanCached=-1;
        ySpanCached=-1;
    }

    @Override
    protected void childAllocation(int index, Rectangle a){
        Shape childAlloc = getChildAllocation(index, a);
        if(childAlloc != null){
            a.setBounds(childAlloc.getBounds());
        }
    }
}

其他注意事项

  • 确保MDList元素的子元素(如paragraph)能被ViewFactory的default分支正确处理为BoxView(X_AXIS)
  • MarkdownContainerView的paint方法逻辑可保留,但需确保子视图的allocation计算正确

内容的提问来源于stack exchange,提问作者Tech of the Absence

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 21:22:02