如何正确实现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; } }
问题根源与修复方案
核心问题
- 子视图未初始化:
CompositeView不会自动创建子视图,必须调用build()方法,且需要实现createView()来使用ViewFactory创建子元素对应的View。 - getPreferredSpan计算错误:X轴宽度计算时错误引用了ySpanCached变量,导致宽度为0或异常值。
- 子视图位置计算错误:平均分配高度的逻辑不符合子视图实际高度,导致子视图可能被挤出显示区域或重叠。
修复后的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
相关产品推荐
相关产品推荐

