Componentes Redimensionables en Swing

Hace unos días descubrí un post en el que se explicaba cómo crear elementos redimensionables con Swing de una manera sencilla y rápida. La entrada está explicada bastante bien y puedes consultarla aquí. Las principales características de la solución aportada por Santhosh Kumar T son:
  • Se trabaja con una clase JResizer que contiene el elemento redimensionable
  • Podemos redimensionar cualquier componente
  • Los bordes y los puntos de agarre son totalmente configurables
En esta entrada veremos cómo aplicar esta solución y un aporte realizado por mí.

En primer lugar veremos los principales detalles de la implementación.

Interfaz ResizableBorder


Esta interfaz representa el borde de un componente redimensionable.
/**
* MySwing: Advanced Swing Utilites Copyright (C) 2005 Santhosh Kumar T
* 
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
* 
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
* 
* @author Santhosh Kumar T
*/
public interface ResizableBorder extends Border{
public int getResizeCursor(MouseEvent me);
}

El único método en esta interfaz es:
  • int getResizableCursor(MouseEvent me)

Este método determina el punto de agarre en el que ha ocurrido el evento de ratón. El punto está definido por alguna de las siguientes constantes de la clase java.awt.Cursor:
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
Cursor.MOVE_CURSOR
Cursor.DEFAULT_CURSOR

Además, la interfaz javax.swing.border.Border define tres métodos que deberán implementarse en la clase que implemente ResizableBorder. Estos métodos son:
  • boolean isBorderOpaque() -> Devuelve un booleano indicando si el borde es opaco
  • Insets getBorderInsets(Component c) -> Devuelve un objeto Insets que representa los bordes de un contenedor
  • void paintBorder(Component c, Graphics g, int x, int y, int width, int height) -> Se encarga de dibujar el borde del componente especificado y con la posición y tamaño pasados como argumentos.

El autor de la entrada proporciona una implementación por defecto para esta interfaz. La clase está explicada en su post original y sólo comentaré mi aportación a la implementación original

Clase DefaultResizableBorder


/**
* MySwing: Advanced Swing Utilites Copyright (C) 2005 Santhosh Kumar T
* 
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
* 
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
* 
* @author Santhosh Kumar T
* 
* Modified by Federico Fernández
*/
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;

import javax.swing.SwingConstants;

public class DefaultResizableBorder implements ResizableBorder {
private int dist = 6;

int locations[] = { SwingConstants.NORTH, SwingConstants.SOUTH,
SwingConstants.WEST, SwingConstants.EAST,
SwingConstants.NORTH_WEST, SwingConstants.NORTH_EAST,
SwingConstants.SOUTH_WEST, SwingConstants.SOUTH_EAST, 0, // move
-1, // no location
};

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,
Cursor.MOVE_CURSOR, Cursor.DEFAULT_CURSOR, };

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

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

public boolean isBorderOpaque() {
return false;
}

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);
/*
* Federico Fernández
* Only paint rectangle if the component has the focus
*/
if(component.hasFocus())
for (int i = 0; i < locations.length - 2; i++) {
Rectangle rect = getRectangle(x, y, w, h, locations[i]);
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) {
switch (location) {
case SwingConstants.NORTH:
return new Rectangle(x + w / 2 - dist / 2, y, dist, dist);
case SwingConstants.SOUTH:
return new Rectangle(x + w / 2 - dist / 2, y + h - dist, dist, dist);
case SwingConstants.WEST:
return new Rectangle(x, y + h / 2 - dist / 2, dist, dist);
case SwingConstants.EAST:
return new Rectangle(x + w - dist, y + h / 2 - dist / 2, dist, dist);
case SwingConstants.NORTH_WEST:
return new Rectangle(x, y, dist, dist);
case SwingConstants.NORTH_EAST:
return new Rectangle(x + w - dist, y, dist, dist);
case SwingConstants.SOUTH_WEST:
return new Rectangle(x, y + h - dist, dist, dist);
case SwingConstants.SOUTH_EAST:
return new Rectangle(x + w - dist, y + h - dist, dist, dist);
}
return null;
}

public int getResizeCursor(MouseEvent me) {
Component comp = me.getComponent();
int w = comp.getWidth();
int h = comp.getHeight();

Rectangle bounds = new Rectangle(0, 0, w, h);

if (!bounds.contains(me.getPoint()))
return Cursor.DEFAULT_CURSOR;

Rectangle actualBounds = new Rectangle(dist, dist, w - 2 * dist, h - 2
* dist);
if (actualBounds.contains(me.getPoint()))
return Cursor.DEFAULT_CURSOR;

for (int i = 0; i < locations.length - 2; i++) {
Rectangle rect = getRectangle(0, 0, w, h, locations[i]);
if (rect.contains(me.getPoint()))
return cursors[i];
}

return Cursor.MOVE_CURSOR;
}
}

La idea es que el marco redimensionable sólo aparezca cuando pinchemos sobre el elemento en cuestión. De momento el único cambio que haremos en esta clase es:
if(component.hasFocus())
for (int i = 0; i < locations.length - 2; i++) {
Rectangle rect = getRectangle(x, y, w, h, locations[i]);
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);
}

Sólo pintaremos los rectángulos de agarre cuando el elemento tenga el foco. A continuación veremos la implementación de la clase JResizer
/**
* MySwing: Advanced Swing Utilites
* Copyright (C) 2005  Santhosh Kumar T
* 
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* 
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
*
* @author Santhosh Kumar T
* 
* Modified by Federico Fernández
*/
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;

import javax.swing.JComponent;
import javax.swing.border.Border;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;

public class JResizer extends JComponent {

public JResizer(Component comp) {
this(comp, new DefaultResizableBorder(6));
}

public JResizer(Component comp, ResizableBorder border) {
setLayout(new BorderLayout());
add(comp);
setBorder(border);
}

public void setBorder(Border border) {
removeMouseListener(resizeListener);
removeMouseMotionListener(resizeListener);
if (border instanceof ResizableBorder) {
addMouseListener(resizeListener);
addMouseMotionListener(resizeListener);
// Add focus listener to JResize component
addMouseListener(focusListener);      
}
super.setBorder(border);
}

private void didResized() {
if (getParent() != null) {
getParent().repaint();
invalidate();
((JComponent) getParent()).revalidate();
}
}

/*
* Added by Federico Fernández 
*/
private MouseInputListener focusListener = new MouseInputAdapter() {
public void mousePressed(MouseEvent me) {
me.getComponent().requestFocus();
repaint();
}
};

/*
* Added by Federico Fernández 
*/
public MouseInputListener getFocusListener() {
return focusListener;
}

private MouseInputListener resizeListener = new MouseInputAdapter() {
public void mouseMoved(MouseEvent me) {
ResizableBorder border = (ResizableBorder) getBorder();
setCursor(Cursor.getPredefinedCursor(border.getResizeCursor(me)));
}

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

private int cursor;
private Point startPos = null;

public void mousePressed(MouseEvent me) {      
ResizableBorder border = (ResizableBorder) getBorder();
cursor = border.getResizeCursor(me);
startPos = me.getPoint();
}

public void mouseDragged(MouseEvent me) {
if (startPos != null) {
int dx = me.getX() - startPos.x;
int dy = me.getY() - startPos.y;
switch (cursor) {
case Cursor.N_RESIZE_CURSOR:
setBounds(getX(), getY() + dy, getWidth(), getHeight() - dy);
didResized();
break;
case Cursor.S_RESIZE_CURSOR:
setBounds(getX(), getY(), getWidth(), getHeight() + dy);
startPos = me.getPoint();
didResized();
break;
case Cursor.W_RESIZE_CURSOR:
setBounds(getX() + dx, getY(), getWidth() - dx, getHeight());
didResized();
break;
case Cursor.E_RESIZE_CURSOR:
setBounds(getX(), getY(), getWidth() + dx, getHeight());
startPos = me.getPoint();
didResized();
break;
case Cursor.NW_RESIZE_CURSOR:
setBounds(getX() + dx, getY() + dy, getWidth() - dx,
getHeight() - dy);
didResized();
break;
case Cursor.NE_RESIZE_CURSOR:
setBounds(getX(), getY() + dy, getWidth() + dx, getHeight()
- dy);
startPos = new Point(me.getX(), startPos.y);
didResized();
break;
case Cursor.SW_RESIZE_CURSOR:
setBounds(getX() + dx, getY(), getWidth() - dx, getHeight()
+ dy);
startPos = new Point(startPos.x, me.getY());
didResized();
break;
case Cursor.SE_RESIZE_CURSOR:
setBounds(getX(), getY(), getWidth() + dx, getHeight() + dy);
startPos = me.getPoint();
didResized();
break;
case Cursor.MOVE_CURSOR:
Rectangle bounds = getBounds();
bounds.translate(dx, dy);
setBounds(bounds);
didResized();
}

// cursor shouldn't change while dragging
setCursor(Cursor.getPredefinedCursor(cursor));
}
}

public void mouseReleased(MouseEvent mouseEvent) {
startPos = null;
}
};
}

Al igual que antes la clase está explicada en la entrada original por lo que voy a evitar duplicar demasiado la información. A la implementación de Santhosh Kumar T se ha añadido lo siguiente:
private MouseInputListener focusListener = new MouseInputAdapter() {
public void mousePressed(MouseEvent me) {
me.getComponent().requestFocus();
repaint();
}
};

Crea un MouseInputLister que solicita el foco para el elemento en el que hemos pulsado con el botón y pinta de nuevo el marco del elemento redimensionable. Hay que tener en cuenta dos aspectos:
  1. Sólo se llamara a este método cuando se añada el listener al componente sobre el que hacemos click.
  2. El método repaint() se ejecuta siempre sobre el objeto JResizer.

public MouseInputListener getFocusListener() {
return focusListener;
}

Proporcionamos un método getter para que pueda añadirse este listener al contenedor externo del componente redimensionable. Se ha añadido para cumplir con el punto 1 anterior.
Con esto, ya tenemos todo listo para hacer que cualquier elemento sea redimensionable. A continuación se muestra un ejemplo de uso y una captura de pantalla de cómo quedaría:
import java.awt.Color;
import java.awt.Font;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class TestResizable {

public static void main(String[] args) {
JFrame frame = new JFrame("Dancing In The Moonlight %u2013 Thin Lizzy");
frame.setSize(400, 400);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// JPanel with a null layout
JPanel panel = new JPanel(null);
frame.add(panel);

// Label with the song lyric
String lyrics =  "<html>"  + 
"When I passed you in the doorway<p/>"  +  
"Well You took me with a glance<p/>"  + 
"I should have took that last bus home<p/>"  +  
"But I asked you for a dance<p/>"  +  
"...<p/>"  +  
"Dancing in the moonlight<p/>"  +  
"It's caught me in its spotlight<p/>"  +  
"It's alright, alright<p/>"  +  
"Dancing in the moonlight<p/>"  +  
"On this long hot summer night<p/>"  +  
"it's so god damn hot <p/>"  +  
"(fade out) "  +  
"</html>";        
JLabel label = new JLabel(lyrics);
label.setFont(new Font("Georgia", Font.PLAIN, 14));
label.setForeground(Color.BLUE);
label.setLocation(20, 20);

// JResizer
JResizer resizer = new JResizer(label);
resizer.setBounds(10, 10, label.getPreferredSize().width, label.getPreferredSize().height + 40);

panel.add(resizer);
// Adding the focusListener to parent panel
panel.addMouseListener(resizer.getFocusListener());
frame.setVisible(true);
}
}


0 comentarios:

Publicar un comentario