viernes, 2 de septiembre de 2011

HOW TO: resize jcombobox popup // redimensionar popup de un JCombobox

Hola a todos. si sueles programar en java, o al menos lo intentas (como es mi caso xD),  mas de alguna ocasión has creado una aplicación de escritorio usando las JFC para crear tu GUI. si es así, quizá es bastante posible que hallas usado un JCombobox que se llena desde la base de datos (con información la cual no sabemos la longitud que posee).

cuando este caso se presenta pueden ocurrir los siguientes problemas o complicaciones del diseño:
  • El objecto no muestra su texto completo, dejando incompleta la información que necesitamos mostrar en el JCombobox
  • El componente (JCombobox) se auto-re dimensiona al ancho del objeto con el texto mas extenso... cuando esto sucede, es bastante común que el componente desordene otros componentes que están ubicados cerca de el, obligándonos a re diseñar o re ubicar algunos componentes en dicha pantalla; lo cual implica tiempo, tiempo que lo podríamos estar usando para avanzar en otras pantallas del sistema o... simplemente para navegar por Internet ^^
un ejemplo ilustrado del terrible problema:

EDITO: debido a un problema con algunas versiones de jdk lanza un Null Pointer; al final de la entrada agrego un metodo alternativo para lograr el mismo objetivo.

Bueno para solucionar dichos problemas vamos a crear una clase que implemente la interfaz PopupMenuListener la cual va a cambiar los límites (ancho) del menú emergente para proveer al JComboBox algunas funcionalidades diferentes. entre las cuales:
  • la ventana emergente puede ser más amplio que el JCombobox
  • la ventana emergente se muestra por encima del JCombobox (útil cuando el componente esta ubicado al fondo de la pantalla y las opciones que desplegara son bastantes, evitando así que las opciones ultimas de la lista no sean visibles)

La clase la llamaremos CustomPopupMenuListener.java, así que empecemos :D
primero lo primero: crear la clase, implementar la interfaz y sus métodos (comentamos el codigo de los metodos - throw new UnsupportedOperationException("Not supported yet.");-
.
package resources.test;

import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

/**
 *
 * @author McCubo
 */
public class CustomPopupMenuListener implements PopupMenuListener{

    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
        //throw new UnsupportedOperationException("Not supported yet.");
    }

    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        //throw new UnsupportedOperationException("Not supported yet.");
    }

    public void popupMenuCanceled(PopupMenuEvent e) {
        //throw new UnsupportedOperationException("Not supported yet.");
    }

} 
Luego creamos las propiedades de la clase: dos tipo boolean , un constructor el cual reciba como parámetros esas propiedades y los respectivos getters y setters de las dos propiedades:


package resources.test;

import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

/**
 *
 * @author McCubo
 */
public class CustomPopupMenuListener implements PopupMenuListener{

    private boolean popupWider;
    private boolean popupAbove;

    public CustomPopupMenuListener(boolean popupWider, boolean popupAbove) {
        this.popupWider = popupWider;
        this.popupAbove = popupAbove;
    }

    public boolean isPopupAbove() {
        return popupAbove;
    }

    public void setPopupAbove(boolean popupAbove) {
        this.popupAbove = popupAbove;
    }

    public boolean isPopupWider() {
        return popupWider;
    }

    public void setPopupWider(boolean popupWider) {
        this.popupWider = popupWider;
    }
    
    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
        //throw new UnsupportedOperationException("Not supported yet.");
    }

    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        //throw new UnsupportedOperationException("Not supported yet.");
    }

    public void popupMenuCanceled(PopupMenuEvent e) {
       //throw new UnsupportedOperationException("Not supported yet.");
    }

}
Ahora la parte que nos interesa mas, dando funcionalidad al método popupMenuWillBecomeVisible, método el cual es invocado justo en el momento antes que se va a desplegar la lista de elementos del JCombobox, quedando el codigo:



package resources.test;

import java.awt.Dimension;
import java.awt.Point;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.basic.BasicComboPopup;

/**
 *
 * @author McCubo
 */
public class CustomPopupMenuListener implements PopupMenuListener {

    private boolean popupWider;
    private boolean popupAbove;

    public CustomPopupMenuListener(boolean popupWider, boolean popupAbove) {
        this.popupWider = popupWider;
        this.popupAbove = popupAbove;
    }

    public boolean isPopupAbove() {
        return popupAbove;
    }

    public void setPopupAbove(boolean popupAbove) {
        this.popupAbove = popupAbove;
    }

    public boolean isPopupWider() {
        return popupWider;
    }

    public void setPopupWider(boolean popupWider) {
        this.popupWider = popupWider;
    }

    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
        JComboBox comboBox = (JComboBox) e.getSource();
        if (comboBox.getItemCount() == 0) {
            return;
        }
        Object child = comboBox.getAccessibleContext().getAccessibleChild(0);
        if (child instanceof BasicComboPopup) {
            if (popupAbove) {
                popupAbove((BasicComboPopup) child);
            }
            if (popupWider) {
                popupWider((BasicComboPopup) child);
            }
        }
    }

    private void popupAbove(BasicComboPopup popup) {
        Point parent = popup.getInvoker().getLocationOnScreen();
        int height = popup.getPreferredSize().height;
        popup.setLocation(parent.x, parent.y - height);
    }

    private void popupWider(BasicComboPopup popup) {
        JList list = popup.getList();
        JScrollPane scrollPane = (JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, list);
        // se suma el valor de la barra de desplazamiento vertical al ancho que tomara el popup, de lo contrario, la barra vertical oculta
        // una porcion del item mas largo
        int popupWidth = list.getPreferredSize().width + 5 + getScrollBarWidth(popup, scrollPane);
        Dimension scrollPaneSize = scrollPane.getPreferredSize();
        popupWidth = Math.max(popupWidth, scrollPaneSize.width);
        scrollPaneSize.width = popupWidth;
        scrollPane.setPreferredSize(scrollPaneSize);
        scrollPane.setMaximumSize(scrollPaneSize);
    }

    /**
     * @return el valor de la barra de desplazamiento vertical (si es que esta es visible)
     */
    private int getScrollBarWidth(BasicComboPopup popup, JScrollPane scrollPane) {
        int scrollBarWidth = 0;
        JComboBox comboBox = (JComboBox) popup.getInvoker();
        if (comboBox.getItemCount() > comboBox.getMaximumRowCount()) {
            JScrollBar vertical = scrollPane.getVerticalScrollBar();
            scrollBarWidth = vertical.getPreferredSize().width;
        }
        return scrollBarWidth;
    }

    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        //throw new UnsupportedOperationException("Not supported yet.");
    }

    public void popupMenuCanceled(PopupMenuEvent e) {
        //throw new UnsupportedOperationException("Not supported yet.");
    }
}

El código es un poco sencillo, pero el resultado que obtenemos ayuda bastante. para aplicarlo a un combobox en nuestra aplicacion, seria de la siguente manera:

       jComboBox1.addPopupMenuListener(new CustomPopupMenuListener(true, true));

Opcion #2. (si el método anterior arroja un Null Pointer Exception)
Creamos una clase que descienda de JCombobox y sobreescribimos 2 metodos. el codigo deberia quedar asi:
import javax.swing.*; 
import java.awt.*; 
import java.util.Vector; 
 
public class CustomJCombobox extends JComboBox{ 
 
    public CustomJCombobox() { 
    } 
 
    private boolean layingOut = false; 
    // Sobreescribimos este Metodo :)
    public void doLayout(){ 
        try{ 
            layingOut = true; 
            super.doLayout(); 
        }finally{ 
            layingOut = false; 
        } 
    } 
    // Tambien sobreescribimos este otro metodo :D
    public Dimension getSize(){ 
        Dimension dim = super.getSize(); 
        if(!layingOut) 
            dim.width = Math.max(dim.width, getPreferredSize().width); 
        return dim; 
    } 
}

y el resultado seria algo asi usando cualquiera de las dos opciones:
el popup se mustra sobre el combo por el segundo boolean que se le pasa al constructor, de lo contrario se mostraria bajo el combo ;)
Espero este pequeño aporte les pueda servir :D
Saludos!

7 comentarios:

  1. Saludos,
    Pruebo el codigo que pones pero cuando doy clic para desplegar la lista de elementos contenidos en el combo, me da un error de java.lang.NullPointerException
    at componente.CustomPopupMenuListener.popupAbove(CustomPopupMenuListener.java:69)

    EL problema es que el metodo popup.getInvoker() retorna null.

    ResponderEliminar
  2. No hay manera que logre hacer funcionar correctamente el PopupMenu del componente amigo, continua dando el nullpointer del mensaje de arriba, te agradecría que me ayudaras con esto.

    ResponderEliminar
  3. @konaksisk Hey hola y antes que nada que bien que te das un tiempo para comentar. bueno :$ la verdad esperaba que nadie comentara de ese problema (el cual un compañero de trabajo me lo hizo saber xD ) pero ese problema es debido a la version de JDK que estes utilizando. debido a que los elementos del jcombobox no son añadidos hasta al final de la creacion del componente. pero bueno, ahi agregue un pequeño codigo,el cual es totalmente diferente al anterior.
    Me gustaria saber si este metodo te funciona :). espero que si :)
    Saludos y gracias por comentar!!!

    ResponderEliminar
  4. @konaksisk
    por cierto perdon lo tarde de la respuesta pero estaba de vacaciones y no habia visto el blog. Saludos!!!!

    ResponderEliminar
  5. Saludos Miguel, espero que hayas pasado unas vacaciones de navidad agradables.
    hermano un millón de gracias por responder a mis dudas sobre este elemento tan buscado por mi.
    efectivamente estoy usando la versión 1.6.0.26 de JVM. probé esta segunda opción que posteas y trabaja a las mil maravillas, además de ser bien sencilla, y encaja efectivamente con mi necesidad, pues debido a las necesidades de mi proyecto tenía desarrollado un componente JObjetoComoboBox que hereda de JCombobox y el cual recibe una lista de objetos no importa de que clase y muestra el campo que querramos de los objetos en la lista a traves de la API reflection de java. Por lo que solo tuve que incorporarle a dicha clase el override de los dos metodos que posteaste hoy. La verdad es que me has dado una ayuda grande grande.
    LLevo bastante tiempo programando en java pero el Swing no ha sido mi fuertes hasta ahora que tuve que usarlo para un software solicitado

    ResponderEliminar
  6. @konaksisk hey hola. pues antes que nada me alegra que te halla servido el segundo codigo :D. y lo de la API de reflection se escucha interesante :D Espero en tu proyecto todo te salga lo planeado :D
    Saludos!!!

    ResponderEliminar
    Respuestas
    1. Muchisimas gracias Miguel Cubias Caceres... el código funciona de mil maravillas...

      Eliminar