ZetCode

Java Swing 中的可调整大小的组件

最后修改于 2023 年 1 月 10 日

在 Java Swing 教程的这一部分,我们将创建一个可调整大小的组件。

可调整大小的组件

创建图表或图表时,通常使用可调整大小的组件。一个常见的可调整大小的组件是电子表格应用程序中的图表。该图表可以在应用程序的表格小部件上移动和调整大小。

为了创建一个可以在面板上自由拖动的组件,我们使用一个启用了绝对定位的面板。在我们的示例中,我们将创建一个可以在父窗口上自由移动和调整大小的组件。

当可调整大小的组件具有焦点时,会在其边框上绘制八个小矩形。这些矩形用作拖动点,我们可以在其中绘制组件并开始调整大小。

com/zetcode/ResizableComponentEx.java
package com.zetcode;

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class ResizableComponentEx extends JFrame {

    private Resizable res;

    public ResizableComponentEx() {

        initUI();
    }

    private void initUI() {

        var pnl = new JPanel(null);
        add(pnl);

        var area = new JPanel();
        area.setBackground(Color.white);

        res = new Resizable(area);
        res.setBounds(50, 50, 200, 150);
        pnl.add(res);

        addMouseListener(new MouseAdapter() {

            @Override
            public void mousePressed(MouseEvent me) {

                requestFocus();
                res.repaint();
            }
        });

        setSize(550, 400);
        setTitle("Resizable component");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var ex = new ResizableComponentEx();
            ex.setVisible(true);
        });
    }
}

ResizableComponentEx 设置面板和组件。

var pnl = new JPanel(null);

我们对可调整大小的组件使用绝对定位。通过向 JPanel 的构造函数提供 null,我们创建一个具有绝对定位的面板。

addMouseListener(new MouseAdapter() {
    
    @Override
    public void mousePressed(MouseEvent me) {

        requestFocus();
        res.repaint();
    }
});

如果我们按下了父面板(即可调整大小的组件之外),我们会获得焦点并重新绘制组件。边框上的矩形将消失。

com/zetcode/ResizableBorder.java
package com.zetcode;

import javax.swing.SwingConstants;
import javax.swing.border.Border;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;

public class ResizableBorder implements Border {

    private int dist = 8;

    int[] locations = {
        SwingConstants.NORTH, SwingConstants.SOUTH, SwingConstants.WEST,
        SwingConstants.EAST, SwingConstants.NORTH_WEST,
        SwingConstants.NORTH_EAST, SwingConstants.SOUTH_WEST,
        SwingConstants.SOUTH_EAST
    };

    int[] cursors = {
        Cursor.N_RESIZE_CURSOR, Cursor.S_RESIZE_CURSOR, Cursor.W_RESIZE_CURSOR,
        Cursor.E_RESIZE_CURSOR, Cursor.NW_RESIZE_CURSOR, Cursor.NE_RESIZE_CURSOR,
        Cursor.SW_RESIZE_CURSOR, Cursor.SE_RESIZE_CURSOR
    };

    public ResizableBorder(int dist) {
        this.dist = dist;
    }

    @Override
    public Insets getBorderInsets(Component component) {
        return new Insets(dist, dist, dist, dist);
    }

    @Override
    public boolean isBorderOpaque() {
        return false;
    }

    @Override
    public void paintBorder(Component component, Graphics g, int x, int y,
                            int w, int h) {

        g.setColor(Color.black);
        g.drawRect(x + dist / 2, y + dist / 2, w - dist, h - dist);

        if (component.hasFocus()) {

            for (int location : locations) {

                var rect = getRectangle(x, y, w, h, location);

                if (rect != null) {

                    g.setColor(Color.WHITE);
                    g.fillRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
                    g.setColor(Color.BLACK);
                    g.drawRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
                }
            }
        }
    }

    private Rectangle getRectangle(int x, int y, int w, int h, int location) {

        return switch (location) {

            case SwingConstants.NORTH -> new Rectangle(x + w / 2 - dist / 2, y, dist, dist);
            case SwingConstants.SOUTH -> new Rectangle(x + w / 2 - dist / 2, y + h - dist, dist, dist);
            case SwingConstants.WEST -> new Rectangle(x, y + h / 2 - dist / 2, dist, dist);
            case SwingConstants.EAST -> new Rectangle(x + w - dist, y + h / 2 - dist / 2, dist, dist);
            case SwingConstants.NORTH_WEST -> new Rectangle(x, y, dist, dist);
            case SwingConstants.NORTH_EAST -> new Rectangle(x + w - dist, y, dist, dist);
            case SwingConstants.SOUTH_WEST -> new Rectangle(x, y + h - dist, dist, dist);
            case SwingConstants.SOUTH_EAST -> new Rectangle(x + w - dist, y + h - dist, dist, dist);
            default -> new Rectangle();;
        };
    }

    public int getCursor(MouseEvent me) {

        var c = me.getComponent();
        int w = c.getWidth();
        int h = c.getHeight();

        for (int i = 0; i < locations.length; i++) {

            var rect = getRectangle(0, 0, w, h, locations[i]);

            if (rect != null && rect.contains(me.getPoint())) {
                return cursors[i];
            }
        }

        return Cursor.MOVE_CURSOR;
    }
}

ResizableBorder 负责绘制组件的边框并确定要使用的光标类型。

int locations[] = {

    SwingConstants.NORTH, SwingConstants.SOUTH, SwingConstants.WEST,
    SwingConstants.EAST, SwingConstants.NORTH_WEST,
    SwingConstants.NORTH_EAST, SwingConstants.SOUTH_WEST,
    SwingConstants.SOUTH_EAST
};

这些是绘制矩形的位置。这些位置也是抓取点,可以在其中抓取组件并调整其大小。

g.setColor(Color.black);
g.drawRect(x + dist / 2, y + dist / 2, w - dist, h - dist);

paintBorder() 方法中,我们绘制可调整大小的组件的边框。上面的代码绘制了组件的外边框。

if (component.hasFocus()) {

    for (int location : locations) {

        var rect = getRectangle(x, y, w, h, location);

        if (rect != null) {

            g.setColor(Color.WHITE);
            g.fillRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
            g.setColor(Color.BLACK);
            g.drawRect(rect.x, rect.y, rect.width - 1, rect.height - 1);
        }
    }
}

这八个矩形仅在可调整大小的组件当前具有焦点的情况下绘制。

private Rectangle getRectangle(int x, int y, int w, int h, int location) {

    return switch (location) {

        case SwingConstants.NORTH -> new Rectangle(x + w / 2 - dist / 2, y, dist, dist);
        case SwingConstants.SOUTH -> new Rectangle(x + w / 2 - dist / 2, y + h - dist, dist, dist);
        case SwingConstants.WEST -> new Rectangle(x, y + h / 2 - dist / 2, dist, dist);
        ...
}

getRectangle() 方法返回矩形的坐标。

public int getCursor(MouseEvent me) {

    var c = me.getComponent();
    int w = c.getWidth();
    int h = c.getHeight();

    for (int i = 0; i < locations.length; i++) {

        var rect = getRectangle(0, 0, w, h, locations[i]);

        if (rect != null && rect.contains(me.getPoint())) {
            return cursors[i];
        }
    }

    return Cursor.MOVE_CURSOR;
}

getCursor() 方法获取所讨论的抓取点的光标类型。

com/zetcode/Resizable.java
package com.zetcode;


import javax.swing.JComponent;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.event.MouseEvent;

public class Resizable extends JComponent {

    public Resizable(Component comp) {
        this(comp, new ResizableBorder(8));
    }

    public Resizable(Component comp, ResizableBorder border) {

        setLayout(new BorderLayout());
        add(comp);
        addMouseListener(resizeListener);
        addMouseMotionListener(resizeListener);
        setBorder(border);
    }

    private void resize() {

        if (getParent() != null) {
            getParent().revalidate();
        }
    }

    MouseInputListener resizeListener = new MouseInputAdapter() {

        @Override
        public void mouseMoved(MouseEvent me) {

            if (hasFocus()) {

                var resizableBorder = (ResizableBorder) getBorder();
                setCursor(Cursor.getPredefinedCursor(resizableBorder.getCursor(me)));
            }
        }

        @Override
        public void mouseExited(MouseEvent mouseEvent) {
            setCursor(Cursor.getDefaultCursor());
        }

        private int cursor;
        private Point startPos = null;

        @Override
        public void mousePressed(MouseEvent me) {

            var resizableBorder = (ResizableBorder) getBorder();
            cursor = resizableBorder.getCursor(me);
            startPos = me.getPoint();

            requestFocus();
            repaint();
        }

        @Override
        public void mouseDragged(MouseEvent me) {

            if (startPos != null) {

                int x = getX();
                int y = getY();
                int w = getWidth();
                int h = getHeight();

                int dx = me.getX() - startPos.x;
                int dy = me.getY() - startPos.y;

                switch (cursor) {

                    case Cursor.N_RESIZE_CURSOR -> {

                        if (!(h - dy < 50)) {
                            setBounds(x, y + dy, w, h - dy);
                            resize();
                        }
                    }

                    case Cursor.S_RESIZE_CURSOR -> {

                        if (!(h + dy < 50)) {
                            setBounds(x, y, w, h + dy);
                            startPos = me.getPoint();
                            resize();
                        }
                    }

                    case Cursor.W_RESIZE_CURSOR -> {

                        if (!(w - dx < 50)) {
                            setBounds(x + dx, y, w - dx, h);
                            resize();
                        }
                    }


                    case Cursor.E_RESIZE_CURSOR -> {

                        if (!(w + dx < 50)) {
                            setBounds(x, y, w + dx, h);
                            startPos = me.getPoint();
                            resize();
                        }
                    }

                    case Cursor.NW_RESIZE_CURSOR -> {

                        if (!(w - dx < 50) && !(h - dy < 50)) {
                            setBounds(x + dx, y + dy, w - dx, h - dy);
                            resize();
                        }
                    }


                    case Cursor.NE_RESIZE_CURSOR -> {

                        if (!(w + dx < 50) && !(h - dy < 50)) {
                            setBounds(x, y + dy, w + dx, h - dy);
                            startPos = new Point(me.getX(), startPos.y);
                            resize();
                        }
                    }

                    case Cursor.SW_RESIZE_CURSOR -> {

                        if (!(w - dx < 50) && !(h + dy < 50)) {
                            setBounds(x + dx, y, w - dx, h + dy);
                            startPos = new Point(startPos.x, me.getY());
                            resize();
                        }
                    }

                    case Cursor.SE_RESIZE_CURSOR -> {

                        if (!(w + dx < 50) && !(h + dy < 50)) {
                            setBounds(x, y, w + dx, h + dy);
                            startPos = me.getPoint();
                            resize();
                        }
                    }

                    case Cursor.MOVE_CURSOR -> {

                        var bounds = getBounds();
                        bounds.translate(dx, dy);
                        setBounds(bounds);
                        resize();
                    }
                }

                setCursor(Cursor.getPredefinedCursor(cursor));
            }
        }

        @Override
        public void mouseReleased(MouseEvent mouseEvent) {

            startPos = null;
        }
    };
}

Resizable 类表示在窗口上调整大小和移动的组件。

private void resize() {
    
    if (getParent() != null) {
        getParent().revalidate();
    }
}

在调整组件大小后,将调用 resize() 方法。revalidate() 方法导致重新绘制组件。

MouseInputListener resizeListener = new MouseInputAdapter() {
    
    @Override
    public void mouseMoved(MouseEvent me) {

        if (hasFocus()) {

            var border = (ResizableBorder) getBorder();
            setCursor(Cursor.getPredefinedCursor(border.getCursor(me)));
        }
    }
...
}

当我们将光标悬停在抓取点上时,我们会更改光标类型。仅当组件具有焦点时,光标类型才会更改。

@Override
public void mousePressed(MouseEvent me) {

    var resizableBorder = (ResizableBorder) getBorder();
    cursor = resizableBorder.getCursor(me);
    startPos = me.getPoint();

    requestFocus();
    repaint();
}

如果我们单击可调整大小的组件,我们会更改光标,获取拖动的起始点,将焦点赋予组件,并重新绘制它。

int x = getX();
int y = getY();
int w = getWidth();
int h = getHeight();

int dx = me.getX() - startPos.x;
int dy = me.getY() - startPos.y;

mouseDragged() 方法中,我们确定光标的 x 和 y 坐标以及组件的宽度和高度。我们计算在鼠标拖动事件期间产生的距离。

case Cursor.N_RESIZE_CURSOR -> {

    if (!(h - dy < 50)) {
        setBounds(x, y + dy, w, h - dy);
        resize();
    }
}

对于所有调整大小,我们确保组件不小于 50 像素。否则,我们可以使其变得很小,最终会隐藏组件。setBounds 方法重新定位和调整组件的大小。

Resizable component
图:可调整大小的组件

在 Java Swing 教程的这一部分,我们创建了一个可调整大小的组件。