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

JTree组件渲染异常及行高、图标统一优化问题求助

JTree界面优化问题

我尝试使用标准JTree实现树形展示,编写了如下测试代码,期望实现类似NetBeans IDE的美观树形界面,优化后仍存在两个问题:

  • Windows7 150%缩放环境下,节点文字底部被截断1-2像素,需调整字体大小或行高;
  • 已通过else { node.setAllowsChildren(false); }解决叶子节点初始显示+号的问题,现需为所有节点设置统一的自定义图标。

测试代码

package inspector;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;


/**
 *
 * @author Alex
 */
public class TreeTest {

    private static ValueMap prepareTree() {
        ValueMap obj = new ValueMap();
        obj.set("x", new Value(1));
        obj.set("y", new Value(1));

        ValueArray users = new ValueArray();
        obj.set("users", users);

        ValueMap user = new ValueMap();
        user.set("login", new Value("Alex654"));
        user.set("password", new Value("123456"));

        ValueMap info = new ValueMap();
        info.set("city", new Value("Moscow"));
        info.set("job", new Value("Developer"));
        info.set("firstName", new Value("Alex"));
        info.set("lastName", new Value("Popov"));
        user.set("bio", info);

        users.push(user);

        ValueMap user2 = new ValueMap();
        user2.set("login", new Value("admin"));
        user2.set("password", new Value("test"));
        user2.set("bio", new Value(null));

        users.push(user2);

        return obj;
    }

    static class Value {

        Value() {
            value = null;
        }

        Value(Object val) {
            this.value = val;
        }

        @Override
        public String toString() {
            if (value instanceof Integer || value instanceof Double) {
                return value + "";
            } else {
                return "\"" + value.toString() + "\"";
            }
        }

        Object value;
    }

    static class ValueArray extends Value {

        ValueArray() {
            items = new ArrayList<Value>();
        }

        ValueArray(Value val) {
            items = new ArrayList<Value>();
            items.add(val);
        }

        void push(Value val) {
            items.add(val);
        }

        ArrayList<Value> items;
    }

    static class ValueMap extends Value {

        ValueMap() {
            items = new HashMap<String, Value>();
        }

        Value get(String key) {
            return items.get(key);
        }

        void put(String key, Value val) {
            items.put(key, val);
        }

        void set(String key, Value val) {
            items.put(key, val);
        }

        HashMap<String, Value> items;
    }

    public static class ValueWrapper {

        ValueWrapper(String label, Value val) {
            this.label = label;
            this.val = val;
        }

        String getLabel() {
            return label;
        }

        Value getValue() {
            return val;
        }

        @Override
        public String toString() {
            String result = label;
            if (val instanceof ValueArray) {
                result += ": Array";
            }
            else if (val instanceof ValueMap) {
                result += ": { Object }";
            }
            else result += ": " + val.toString();

            return result;
        }

        private String label;
        private Value val;
    }

    public static void processChildren(DefaultMutableTreeNode node) {
        Value val = ((ValueWrapper)node.getUserObject()).getValue();
        if (val instanceof ValueArray) {
            ArrayList<Value> items = ((ValueArray)val).items;
            for (int i = 0; i < items.size(); i++) {
                DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(new ValueWrapper(i + "", items.get(i)));
                node.add(newNode);
                processChildren(newNode);
            }
        } else if (val instanceof ValueMap) {
            HashMap<String, Value> props = ((ValueMap)val).items;
            Set<String> keys = props.keySet();
            for (String key: keys) {
                DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(new ValueWrapper(key, props.get(key)));
                node.add(newNode);
                if (!key.equals("__proto__")) {
                    processChildren(newNode);
                }
            }
        } else {
            node.setAllowsChildren(false);
        }
    }


    public static void main(String[] args) {

        try {
            UIManager.setLookAndFeel(
                UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {}

        final ValueMap root = prepareTree();
        if (root == null) return;

        final JFrame frame = new JFrame("Object Inspector");
        JPanel cp = new JPanel();
        cp.setBorder(BorderFactory.createEmptyBorder(9, 10, 9, 10));
        frame.setContentPane(cp);
        cp.setLayout(new BoxLayout(cp, BoxLayout.PAGE_AXIS));

        DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(new ValueWrapper("obj", root));
        processChildren(rootNode);

        DefaultTreeModel treeModel = new DefaultTreeModel(rootNode, true);
        final JTree tree = new JTree(treeModel);

        final JPanel contentpane = new JPanel();
        contentpane.setBackground(Color.WHITE);
        contentpane.setOpaque(true);
        contentpane.setLayout(new BorderLayout());
        contentpane.add(tree);

        //contentpane.setBounds(0, 0, 490, 380);
        final int width = 490, height = 418;
        cp.setPreferredSize(new Dimension(width, height));

        final JScrollPane scrollpane = new JScrollPane(contentpane);
        scrollpane.setOpaque(false);
        scrollpane.getInsets();

        cp.add(scrollpane);
        scrollpane.setBackground(Color.WHITE);
        scrollpane.setOpaque(true);
        //setSize(518, 420);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

解决方案

1. 解决高缩放比例下文字截断问题

有两种常用处理方式:

  • 直接调整行高:给JTree设置足够容纳文字的行高,数值可根据实际字体大小调整:
    tree.setRowHeight(22);
    
  • 适配缩放的字体与行高:根据系统缩放比例调整字体,再匹配行高避免截断:
    Font originalFont = tree.getFont();
    // 150%缩放环境下,将字体放大1.5倍
    Font scaledFont = originalFont.deriveFont(originalFont.getSize() * 1.5f);
    tree.setFont(scaledFont);
    // 行高比字体高度大6像素,预留足够显示空间
    tree.setRowHeight(scaledFont.getSize() + 6);
    

也可以通过UIManager全局设置JTree行高,对所有JTree实例生效:

UIManager.put("Tree.rowHeight", 22);

2. 设置统一自定义图标

通过自定义TreeCellRenderer替换默认图标,步骤如下:

  1. 创建自定义渲染器类,继承DefaultTreeCellRenderer,为所有节点设置统一图标;
  2. 将该渲染器绑定到目标JTree。

示例代码:

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeCellRenderer;
import java.awt.Component;

class CustomTreeIconRenderer extends DefaultTreeCellRenderer {
    private final Icon customIcon;

    public CustomTreeIconRenderer(Icon icon) {
        this.customIcon = icon;
    }

    @Override
    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
        // 为所有节点设置统一图标
        setIcon(customIcon);
        return this;
    }
}

在创建JTree后,设置渲染器:

// 替换为你的图标路径,比如本地图标文件
Icon customIcon = new ImageIcon("icons/custom-node-icon.png");
tree.setCellRenderer(new CustomTreeIconRenderer(customIcon));

如果没有本地图标,也可以使用系统内置图标或自定义绘制图标。

内容的提问来源于stack exchange,提问作者Alex654

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 04:45:05