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

如何在JFrame中使用多JPanel实现K-Means聚类可视化

嘿,我来帮你解决这个Swing可视化K-Means的问题!你遇到的第二个面板不响应的问题,还有想把ClusterPanel和NodePanel做成独立类、实现动态更新的需求,咱们一步步来搞定。

一、核心问题分析:重叠面板的层级与事件冲突

你当前的代码里把canvas、clusterPanel、nodePanel都放在了同一个坐标位置(10,10,480,480),且使用了null布局。Swing中后添加的组件会处于视觉层级的最上层,所以最后添加的nodePanel会完全覆盖clusterPanel,导致下层的clusterPanel无法接收鼠标事件。

同时代码里还存在变量名冲突(比如isPaintNode既是类名又是布尔变量名)、单选按钮未互斥等小问题,这些都会影响功能正常运行。

二、重构方案:独立面板类+动态更新机制

咱们把ClusterPanel和NodePanel抽成独立的JPanel子类,各自维护数据和绘制逻辑,再通过主窗口协调它们的交互与动态更新。

1. 独立的ClusterPanel类

负责聚类中心的存储、绘制与位置更新:

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

public class ClusterPanel extends JPanel {
    private List<Cluster> clusters = new ArrayList<>();

    public ClusterPanel() {
        setOpaque(false); // 设置透明,避免遮挡下层节点面板
        setLayout(null);
    }

    // 添加新聚类中心
    public void addCluster(Cluster cluster) {
        clusters.add(cluster);
        repaint();
    }

    // 更新聚类中心位置
    public void updateClusterPosition(int index, Point newPos) {
        if (index >= 0 && index < clusters.size()) {
            clusters.get(index).setPos(newPos);
            repaint();
        }
    }

    public List<Cluster> getClusters() {
        return clusters;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // 绘制聚类中心(大圆圈+编号)
        for (Cluster cluster : clusters) {
            g2d.setColor(cluster.getColor());
            g2d.fillOval(cluster.getPos().x - 15, cluster.getPos().y - 15, 30, 30);
            g2d.setColor(Color.BLACK);
            g2d.drawString(String.valueOf(cluster.getId()), cluster.getPos().x - 5, cluster.getPos().y + 5);
        }
    }

    // 聚类数据结构
    public static class Cluster {
        private int id;
        private Point pos;
        private Color color;

        public Cluster(int id, Point pos, Color color) {
            this.id = id;
            this.pos = pos;
            this.color = color;
        }

        // Getter & Setter
        public int getId() { return id; }
        public Point getPos() { return pos; }
        public void setPos(Point pos) { this.pos = pos; }
        public Color getColor() { return color; }
    }
}

2. 独立的NodePanel类

负责节点的存储、绘制与聚类颜色更新:

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

public class NodePanel extends JPanel {
    private List<Node> nodes = new ArrayList<>();

    public NodePanel() {
        setOpaque(false);
        setLayout(null);
    }

    // 添加新节点
    public void addNode(Node node) {
        nodes.add(node);
        repaint();
    }

    // 批量更新所有节点的聚类颜色
    public void updateAllNodeClusterColors(List<Integer> clusterAssignments, List<ClusterPanel.Cluster> clusters) {
        for (int i = 0; i < nodes.size(); i++) {
            int clusterId = clusterAssignments.get(i);
            if (clusterId >= 0 && clusterId < clusters.size()) {
                nodes.get(i).setClusterColor(clusters.get(clusterId).getColor());
            }
        }
        repaint();
    }

    public List<Node> getNodes() {
        return nodes;
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // 绘制节点(小圆圈+黑色边框)
        for (Node node : nodes) {
            g2d.setColor(node.getClusterColor());
            g2d.fillOval(node.getPos().x - 8, node.getPos().y - 8, 16, 16);
            g2d.setColor(Color.BLACK);
            g2d.drawOval(node.getPos().x - 8, node.getPos().y - 8, 16, 16);
        }
    }

    // 节点数据结构
    public static class Node {
        private Point pos;
        private Color clusterColor;

        public Node(Point pos) {
            this.pos = pos;
            this.clusterColor = Color.GRAY; // 默认灰色(未分配聚类)
        }

        // Getter & Setter
        public Point getPos() { return pos; }
        public Color getClusterColor() { return clusterColor; }
        public void setClusterColor(Color clusterColor) { this.clusterColor = clusterColor; }
    }
}

3. 重构后的MainUI主窗口

修复布局、事件逻辑,添加K-Means迭代功能:

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class MainUI extends JFrame {
    private JPanel drawContainer;
    private NodePanel nodePanel;
    private ClusterPanel clusterPanel;
    private boolean paintNodeMode = false;
    private boolean paintClusterMode = false;
    private int clusterIdCounter = 0;
    private Random random = new Random();

    public MainUI(String title) {
        super(title);
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        init();
        setSize(700, 540);
        centerWindow();
        setVisible(true);
    }

    private void init() {
        setLayout(null);

        // 创建绘制容器,作为节点和聚类面板的父容器
        drawContainer = new JPanel();
        drawContainer.setBackground(Color.WHITE);
        drawContainer.setBounds(10, 10, 480, 480);
        drawContainer.setLayout(null);
        add(drawContainer);

        // 初始化两个独立面板
        nodePanel = new NodePanel();
        clusterPanel = new ClusterPanel();
        nodePanel.setBounds(0, 0, 480, 480);
        clusterPanel.setBounds(0, 0, 480, 480);

        // 节点面板点击事件:添加节点
        nodePanel.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (paintNodeMode) {
                    NodePanel.Node node = new NodePanel.Node(e.getPoint());
                    nodePanel.addNode(node);
                }
            }
        });

        // 聚类面板点击事件:添加聚类中心
        clusterPanel.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (paintClusterMode) {
                    Color randomColor = new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
                    ClusterPanel.Cluster cluster = new ClusterPanel.Cluster(clusterIdCounter++, e.getPoint(), randomColor);
                    clusterPanel.addCluster(cluster);
                }
            }
        });

        // 先加节点面板,再加聚类面板,确保聚类显示在节点上层
        drawContainer.add(nodePanel);
        drawContainer.add(clusterPanel);

        // 右侧按钮面板
        JPanel buttonPanel = new JPanel();
        buttonPanel.setBounds(500, 10, 180, 480);
        buttonPanel.setLayout(null);
        add(buttonPanel);

        // 单选按钮组(互斥选择模式)
        ButtonGroup modeGroup = new ButtonGroup();
        JRadioBtn radioNode = new JRadioButton("添加节点");
        radioNode.setBounds(20, 200, 120, 20);
        radioNode.addActionListener(e -> paintNodeMode = radioNode.isSelected());
        modeGroup.add(radioNode);

        JRadioBtn radioCluster = new JRadioButton("添加聚类");
        radioCluster.setBounds(20, 230, 120, 20);
        radioCluster.addActionListener(e -> paintClusterMode = radioCluster.isSelected());
        modeGroup.add(radioCluster);

        buttonPanel.add(radioNode);
        buttonPanel.add(radioCluster);

        // K-Means迭代按钮
        JButton iterateBtn = new JButton("执行K-Means迭代");
        iterateBtn.setBounds(20, 260, 140, 30);
        iterateBtn.addActionListener(e -> runKMeansIteration());
        buttonPanel.add(iterateBtn);
    }

    // 窗口居中
    private void centerWindow() {
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension frameSize = getSize();
        int x = (screen.width - frameSize.width) / 2;
        int y = (screen.height - frameSize.height) / 2;
        setLocation(x, y);
    }

    // 模拟K-Means迭代步骤
    private void runKMeansIteration() {
        List<NodePanel.Node> nodes = nodePanel.getNodes();
        List<ClusterPanel.Cluster> clusters = clusterPanel.getClusters();

        if (nodes.isEmpty() || clusters.isEmpty()) {
            JOptionPane.showMessageDialog(this, "请先添加节点和聚类中心!");
            return;
        }

        // 1. 分配每个节点到最近的聚类
        List<Integer> assignments = new ArrayList<>();
        for (NodePanel.Node node : nodes) {
            assignments.add(findClosestCluster(node.getPos(), clusters));
        }

        // 2. 更新节点颜色
        nodePanel.updateAllNodeClusterColors(assignments, clusters);

        // 3. 更新聚类中心位置
        updateClusterCenters(assignments, nodes, clusters);
    }

    // 找到最近的聚类中心
    private int findClosestCluster(Point nodePos, List<ClusterPanel.Cluster> clusters) {
        double minDistance = Double.MAX_VALUE;
        int closestId = 0;
        for (int i = 0; i < clusters.size(); i++) {
            double distance = calculateDistance(nodePos, clusters.get(i).getPos());
            if (distance < minDistance) {
                minDistance = distance;
                closestId = i;
            }
        }
        return closestId;
    }

    // 计算两点欧氏距离
    private double calculateDistance(Point p1, Point p2) {
        int dx = p1.x - p2.x;
        int dy = p1.y - p2.y;
        return Math.sqrt(dx*dx + dy*dy);
    }

    // 更新聚类中心到所属节点的均值位置
    private void updateClusterCenters(List<Integer> assignments, List<NodePanel.Node> nodes, List<ClusterPanel.Cluster> clusters) {
        int[] count = new int[clusters.size()];
        int[] sumX = new int[clusters.size()];
        int[] sumY = new int[clusters.size()];

        for (int i = 0; i < nodes.size(); i++) {
            int clusterId = assignments.get(i);
            count[clusterId]++;
            sumX[clusterId] += nodes.get(i).getPos().x;
            sumY[clusterId] += nodes.get(i).getPos().y;
        }

        for (int i = 0; i < clusters.size(); i++) {
            if (count[i] > 0) {
                clusters.get(i).setPos(new Point(sumX[i]/count[i], sumY[i]/count[i]));
            }
        }
        clusterPanel.repaint();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new MainUI("K-Means 聚类可视化"));
    }
}

三、关键改进说明

  1. 独立职责划分:ClusterPanel和NodePanel各自管理自己的数据与绘制,代码更易维护。
  2. 透明面板叠加:设置setOpaque(false)让两个面板可以叠加显示,聚类中心自然显示在节点上层,同时不影响事件传递。
  3. 动态更新触发:通过repaint()方法在数据变化后立即重绘,实现K-Means迭代时的实时可视化。
  4. 事件逻辑优化:用MouseAdapter简化监听器代码,单选按钮用ButtonGroup实现互斥,避免同时开启两种添加模式。

现在你可以运行重构后的代码,添加节点和聚类中心,点击"执行K-Means迭代"按钮,就能看到节点颜色随聚类动态变化,聚类中心也会移动到所属节点的均值位置,两个面板都能正常响应鼠标操作啦!

内容的提问来源于stack exchange,提问作者Станислав Иванников

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:44:18