You can use GridBagLayout - this is a grid with a "rubber" size of rows and columns, in which a component can occupy several cells. The advantage of this solution will be the alignment of lines in height in different branches of calculations. Disadvantages: you need to recalculate the size and position of the components on the grid when changing the tree, and you can not just implement the folding part of the calculations.
Example of implementation:
import java.awt.*; import java.awt.event.*; import java.awt.geom.Line2D; import java.util.*; import java.util.List; import javax.swing.*; import javax.swing.border.StrokeBorder; public class GridBagTreeDisplay { static class TopLineBorder extends StrokeBorder { private static final long serialVersionUID = 819507837355280534L; public TopLineBorder(BasicStroke stroke) { super(stroke); } @Override public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { float size = this.getStroke().getLineWidth(); Graphics2D g2d = (Graphics2D)g.create(); g2d.setStroke(this.getStroke()); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.draw( new Line2D.Float( x + size / 2, y + size / 2, width - size, y + size / 2 ) ); g2d.dispose(); } } /** * Класс, поддерживающий древовидное отображение с помощью GridBagLayout * * Манипуляции с узлами дерева осуществляются непосредственно с экземплярами * класса TreeView.TreeNode. * * Для отображения изменений нужно вызвать метод update() */ static class TreeView { private Container container; private TreeNode root; public TreeView( Container container ) { this.container = container; this.container.setLayout( new GridBagLayout() ); } public void update() { container.removeAll(); if ( root != null ) { root.computeAll( 0 ); root.layoutComponentsOn( container ); } container.revalidate(); container.repaint(); } public void setRootComponent( JComponent rootComponent ) { Objects.requireNonNull( rootComponent ); this.root = new TreeNode( null, rootComponent ); } public TreeNode root() { return root; } public Optional<TreeNode> findNodeByComponent( JComponent componentToFind ) { return root.findByComponent( componentToFind ); } class TreeNode { private JComponent component; private int position; private int depth; private int width; private int height; private TreeNode parent; private List<TreeNode> children = new ArrayList<>(); TreeNode( TreeNode parent, JComponent component ) { this.parent = parent; this.component = component; this.depth = parent != null ? parent.depth + 1 : 0; } /** * Обновляет высоту и ширину поддерева с корнем в этом узле, * рекурсивно обновляя высоту и ширину для поддеревьев */ private void computeSize() { if ( children.isEmpty() ) { width = 1; height = 1; } else { int sumChildWidth = 0; int maxChildHeight = 0; for ( TreeNode child : children ) { child.computeSize(); sumChildWidth += child.width; maxChildHeight = Math.max( maxChildHeight, child.height ); } width = sumChildWidth; height = maxChildHeight + 1; } } /** * Устанавливает горизонтальную позицию этого узла в сетке * и обновляет позиции в поддеревьях * * @param newPosition - новая позиция */ private void computePosition( int newPosition ) { this.position = newPosition; int childLeft = position; for ( TreeNode child : children ) { child.computePosition( childLeft ); childLeft += child.width; } } /** * обновляет размеры и горизонтальную позицию узла и * его поддеревьев * * @param newPosition */ private void computeAll( int newPosition ) { computeSize(); computePosition( newPosition ); } public void updateTreeView() { TreeView.this.update(); } /** * Заменяет текущих детей этого узла на новые, * содержащие переданные компоненты * * @param components */ public void setChildren( JComponent... components ) { children.clear(); for ( JComponent component : components ) { children.add( new TreeNode( this, component ) ); } } public TreeNode parent() { return parent; } public List<TreeNode> children() { return Collections.unmodifiableList( children ); } public JComponent component() { return component; } public TreeView treeView() { return TreeView.this; } public Optional<TreeNode> findByComponent( JComponent componentToFind ) { if ( this.component == componentToFind ) { return Optional.of( this ); } for ( TreeNode child : children ) { Optional<TreeNode> found = child.findByComponent( componentToFind ); if ( found.isPresent() ) return found; } return Optional.empty(); } /** * Размещает компоненты текущего узла и дочерних узлов в * переданном контейнере * * @param container */ private void layoutComponentsOn( Container container ) { if ( !(container.getLayout() instanceof GridBagLayout ) ) { throw new IllegalArgumentException( "container must use GridBagLayout" ); } layoutComponentsOnRecursive( container, this.height ); } private void layoutComponentsOnRecursive( Container container, int treeHeight ) { // параметры размещения компонента: GridBagConstraints gbc = new GridBagConstraints( position, // позиция по горизонтали treeHeight - depth, // позиция по вертикали width, // число ячеек по ширине 1, // число ячеек по высоте 1.0, // вес компонента по ширине, при ненулевом весе GridBagLayout // постарается занять всю ширину контейнера 0, // вес по высоте GridBagConstraints.CENTER, // компонент располагается в центре своей области GridBagConstraints.HORIZONTAL, // и растягивается по горизонтали new Insets( 2, 5, 2, 5 ), // отступы от краев области 0, 0 // минимальные ширина и высота ); // компонентам из узлов с детьми устанавливаем границу с линией component.setBorder( !children.isEmpty() ? new TopLineBorder( new BasicStroke( 1.0f ) ) : null ); container.add( component, gbc ); for ( TreeNode child : children ) { child.layoutComponentsOnRecursive( container, treeHeight ); } } } } static JLabel[] makeClickableLabels( TreeView display, String... captions ) { JLabel[] labels = Arrays.stream( captions ) .map( caption -> new JLabel( caption, SwingConstants.CENTER ) ) .toArray( JLabel[]::new ); for ( JLabel label : labels ) { label.addMouseListener( new MouseAdapter() { @Override public void mouseClicked( MouseEvent event ) { display.findNodeByComponent( (JComponent)event.getComponent() ) .ifPresent( node -> { if ( event.isShiftDown() ) { node.setChildren(); } else { node.setChildren( makeClickableLabels( display, "left", "center", "right" ) ); } display.update(); }); } }); } return labels; } static void initUi() { JFrame frame = new JFrame( "" ); frame.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE ); JPanel content = new JPanel(); TreeView treeView = new TreeView( content ); treeView.setRootComponent( makeClickableLabels( treeView, "root" )[0] ); TreeView.TreeNode root = treeView.root(); root.setChildren( makeClickableLabels( treeView, "1-left", "1-right" ) ); root.children().get( 0 ).setChildren( makeClickableLabels( treeView, "2-left-left", "2-left-center", "2-left-right" ) ); root.children().get( 1 ).setChildren( makeClickableLabels( treeView, "2-right-center" ) ); root.children().get( 1 ).children().get( 0 ).setChildren( makeClickableLabels( treeView, "3-right-center-center" ) ); root.updateTreeView(); frame.add( content, BorderLayout.CENTER ); frame.setSize( 800, 600 ); frame.setVisible( true ); } public static void main(String[] args) { EventQueue.invokeLater( GridBagTreeDisplay::initUi ); } }
Result:
