Pregunta JScrollPane no es suficientemente ancho cuando aparece la barra de desplazamiento vertical


Tengo dos JScrollPanes en la misma ventana. El de la izquierda es lo suficientemente grande como para mostrar el contenido del panel contenido. El de la derecha no es lo suficientemente grande como para mostrar sus contenidos y, por lo tanto, necesita crear una barra de desplazamiento vertical.

JScrollPane Issue

Pero como puede ver, el problema es que cuando aparece la barra de desplazamiento vertical, la barra de desplazamiento aparece en el dentro del JScrollPane. Cubre el contenido contenido en el interior y, por lo tanto, se necesita una barra de desplazamiento horizontal para mostrar todo. Quiero eso arreglado.

Me doy cuenta de que puedo activar la barra de desplazamiento vertical todo el tiempo, pero por razones estéticas, solo deseo que aparezca cuando sea necesario, sin que sea necesario que aparezca un panel de desplazamiento horizontal.

EDITAR: Mi código para comenzar esto es tan simple como puede ser:

JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
this.add(groupPanelScroller, "align center");

Estoy usando MigLayout (MigLayout.com), pero este problema parece aparecer sin importar qué administrador de diseño estoy usando. Además, si reduzco la ventana para que el panel izquierdo ya no sea lo suficientemente grande como para mostrar todo, se produce el mismo comportamiento que el panel derecho.


10
2017-07-20 21:41


origen


Respuestas:


Primero: nunca jamás ajustar las sugerencias de tamaño en el nivel de componente. Especialmente no tan cuando tienes un poderoso LayoutManager como MigLayout que admite ajustes en el nivel de administrador.

En código, ajustando el tamaño pref de lo que sea:

// calculate some width to add to pref, f.i. to take the scrollbar width into account
final JScrollPane pane = new JScrollPane(comp);
int prefBarWidth = pane.getVerticalScrollBar().getPreferredSize().width;
// **do not**  
comp.setPreferredSize(new Dimension(comp.getPreferredSize().width + prefBarWidth, ...);
// **do**
String pref = "(pref+" + prefBarWidth + "px)"; 
content.add(pane, "width " + pref);

Dicho eso: básicamente, tocas un error (discutible) en ScrollPaneLayout. Si bien parece tener en cuenta el ancho de la barra de desplazamiento, en realidad no lo hace en todos los casos. El fragmento relevante de preferredLayoutSize

// filling the sizes used for calculating the pref
Dimension extentSize = null;
Dimension viewSize = null;
Component view = null;

if (viewport != null) {
    extentSize = viewport.getPreferredSize();
    view = viewport.getView();
    if (view != null) {
        viewSize = view.getPreferredSize();
    } else {
        viewSize = new Dimension(0, 0);
    }
}

....

// the part trying to take the scrollbar width into account

if ((vsb != null) && (vsbPolicy != VERTICAL_SCROLLBAR_NEVER)) {
    if (vsbPolicy == VERTICAL_SCROLLBAR_ALWAYS) {
        prefWidth += vsb.getPreferredSize().width;
    }
    else if ((viewSize != null) && (extentSize != null)) {
        boolean canScroll = true;
        if (view instanceof Scrollable) {
            canScroll = !((Scrollable)view).getScrollableTracksViewportHeight();
        }
        if (canScroll && 
            // following condition is the **culprit** 
            (viewSize.height > extentSize.height)) {
            prefWidth += vsb.getPreferredSize().width;
        }
    }
}

es el culpable, porque

  • está comparando la vista pref contra la ventana de visualización pref
  • son lo mismo la mayor parte del tiempo

El resultado es lo que está viendo: la barra de desplazamiento se superpone (en el sentido de cortar algo de ancho) a la vista.

Un truco es un ScrollPaneLayout personalizado que agrega el ancho de la barra de desplazamiento si la altura de la vista es menor que la real altura de la vista, un ejemplo crudo (cuidado: no calidad de producción) para jugar con

public static class MyScrollPaneLayout extends ScrollPaneLayout {

    @Override
    public Dimension preferredLayoutSize(Container parent) {
        Dimension dim =  super.preferredLayoutSize(parent);
        JScrollPane pane = (JScrollPane) parent;
        Component comp = pane.getViewport().getView();
        Dimension viewPref = comp.getPreferredSize();
        Dimension port = pane.getViewport().getExtentSize();
        // **Edit 2** changed condition to <= to prevent jumping
        if (port.height < viewPref.height) {
            dim.width += pane.getVerticalScrollBar().getPreferredSize().width;
        }
        return dim;
    }

}

Editar

hmm ... ver el salto (entre mostrar vs. no mostrar la barra de desplazamiento vertical, como se describe en el comentario): cuando remplazo el campo de texto en mi ejemplo con otro scrollPane, entonces el problema se vuelve a cambiar al tamaño "cerca" de su ancho pref. Entonces, el truco no es lo suficientemente bueno, podría ser que el momento de preguntarle a la ventana gráfica por su extensión sea incorrecto (en el medio del proceso de disposición). Actualmente no tengo idea, cómo hacerlo mejor.

Editar 2

seguimiento tentativo: al hacer un cambio de ancho de píxel por píxel, se siente como un error único. Cambiar la condición de < a <= parece arreglar el salto - al precio de siempre agregar el ancho de la barra de desplazamiento. Entonces, en general, esto lleva al primer paso con una inserción más larga ;-) Mientras tanto, creemos que toda la lógica de scollLlayout necesita mejorarse ...

Para resumir tus opciones:

  • ajuste el ancho de pref en un (MigLayout) componentConstraint. Es el inconveniente más simple, es un espacio en blanco posterior adicional en caso de que la barra de desplazamiento no se muestre
  • arregla el scrollPaneLayout. Requiere un poco de esfuerzo y pruebas (ver el código del núcleo ScrollPaneLayout que se debe hacer), la ventaja es un relleno uniforme sin la barra de desplazamiento
  • no es una opción establecer manualmente el ancho de pref en el componente

A continuación hay ejemplos de código para jugar:

// adjust the pref width in the component constraint
MigLayout layout = new MigLayout("wrap 2", "[][]");
final JComponent comp = new JPanel(layout);
for (int i = 0; i < 10; i++) {
    comp.add(new JLabel("some item: "));
    comp.add(new JTextField(i + 5));
}

MigLayout outer = new MigLayout("wrap 2", 
        "[][grow, fill]");
JComponent content = new JPanel(outer);
final JScrollPane pane = new JScrollPane(comp);
int prefBarWidth = pane.getVerticalScrollBar().getPreferredSize().width;
String pref = "(pref+" + prefBarWidth + "px)";
content.add(pane, "width " + pref);
content.add(new JTextField("some dummy") );
Action action = new AbstractAction("add row") {

    @Override
    public void actionPerformed(ActionEvent e) {
        int count = (comp.getComponentCount() +1)/ 2;
        comp.add(new JLabel("some Item: "));
        comp.add(new JTextField(count + 5));
        pane.getParent().revalidate();
    }
};
frame.add(new JButton(action), BorderLayout.SOUTH);
frame.add(content);
frame.pack();
frame.setSize(frame.getWidth()*2, frame.getHeight());
frame.setVisible(true);

// use a custom ScrollPaneLayout
MigLayout layout = new MigLayout("wrap 2", "[][]");
final JComponent comp = new JPanel(layout);
for (int i = 0; i < 10; i++) {
    comp.add(new JLabel("some item: "));
    comp.add(new JTextField(i + 5));
}

MigLayout outer = new MigLayout("wrap 2", 
        "[][grow, fill]");
JComponent content = new JPanel(outer);
final JScrollPane pane = new JScrollPane(comp);
pane.setLayout(new MyScrollPaneLayout());
content.add(pane);
content.add(new JTextField("some dummy") );
Action action = new AbstractAction("add row") {

    @Override
    public void actionPerformed(ActionEvent e) {
        int count = (comp.getComponentCount() +1)/ 2;
        comp.add(new JLabel("some Item: "));
        comp.add(new JTextField(count + 5));
        pane.getParent().revalidate();
    }
};
frame.add(new JButton(action), BorderLayout.SOUTH);
frame.add(content);
frame.pack();
frame.setSize(frame.getWidth()*2, frame.getHeight());
frame.setVisible(true);

10
2017-07-21 16:23



Me lo imaginé. El problema es que JScrollPane se ajusta al tamaño del contenido y se superpone cuando la barra de desplazamiento se hace visible. Así que decidí simplemente hacer el contenido más amplio desde el principio para que se acomode a la barra de desplazamiento si se vuelve visible. Esto también se puede hacer haciendo que las inserciones tengan el ancho de la barra de desplazamiento, que es probablemente la manera más ortodoxa de hacerlo.

int scrollBarWidth = new JScrollPane().getVerticalScrollBar().getPreferredSize();
groupPanel.setLayout(new MigLayout("insets 1 " + scrollBarWidth + " 1 " + scrollBarWidth*1.5); //Adjust multipliers and widths to suit
//...
//Add everything in
//...
JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
groupPanelScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
this.add(groupPanelScroller, "align center");

EDITAR: La respuesta anterior es probablemente la mejor respuesta, pero esta es mi respuesta original para los curiosos, que en realidad cambia el tamaño del panel.

JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
groupPanel.setPreferredSize(new Dimension(groupPanel.getPreferredSize().width + groupPanelScroller.getVerticalScrollBar().getVisibleAmount()*2, 
        groupPanel.getPreferredSize().height));
groupPanelScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
this.add(groupPanelScroller, "align center");

Entonces, ¿qué significa que el contenido tiene el mismo tamaño más el doble del ancho de la barra de desplazamiento? Dado que todo está centrado, todo se ve igual y, si aparece, parte del espacio extra de un lado se rellena con la barra de desplazamiento.


3
2017-07-21 02:09



Tuve el mismo problema y llegué aquí después de horas tratando de encontrar la causa. Terminé implementando mi propio ViewportLayout y también quería compartir esa solución.

El problema subyacente es que ScrollPaneLayout no sabe que una dimensión (en su caso, el alto de la vista) estará restringida a un valor máximo, por lo que no puede predeterminar si se necesitarán barras de desplazamiento. Por lo tanto, no puede ajustar la otra dimensión flexible (en su caso, el ancho).

Un ViewportLayout personalizado puede tener esto en cuenta, preguntando a su padre la altura permitida y, por lo tanto, permite que ScrollPaneLayout ajuste el ancho preferido en consecuencia:

public class ConstrainedViewPortLayout extends ViewportLayout {

    @Override
    public Dimension preferredLayoutSize(Container parent) {

        Dimension preferredViewSize = super.preferredLayoutSize(parent);

        Container viewportContainer = parent.getParent();
        if (viewportContainer != null) {
            Dimension parentSize = viewportContainer.getSize();
            preferredViewSize.height = parentSize.height;
        }

        return preferredViewSize;
    }
}

ViewportLayout se establece así:

scrollPane.getViewport().setLayout(new ConstrainedViewPortLayout());

1
2018-03-29 22:34



Este error tiene varios años pero todavía no se ha corregido, así que agregaré mi contribución.

También puedes evitar el error implementando javax.swing.Scrollable en la vista compoment de su JScrollPane. los ScrollPaneLayout comprueba los métodos de esta interfaz para determinar si primero se muestra una barra de desplazamiento horizontal, antes de volver a su cálculo de errores.

Puede implementar la interfaz desplazable para que el componente de vista se trate exactamente como antes de implementar Scrollable, excepto para el método que determina si se visualiza una barra de desplazamiento horizontal.

Un ejemplo concreto para demostrar esto que no he probado de forma aislada, sino que lo he extraído de nuestra base de código. Cambio:

JPanel panel = new JPanel();
JScrollPane pane = new JScrollPane(panel);

a

class ScrollableJPanel extends JPanel implements Scrollable {
    @Override public Dimension getPreferredScrollableViewportSize() {
        return getPreferredSize();
    }

    @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        JScrollPane scrollPane = (JScrollPane)getParent();
        return orientation == SwingConstants.VERTICAL ? scrollPane.getVerticalScrollBar().getUnitIncrement() : scrollPane.getHorizontalScrollBar().getUnitIncrement();
    }

    @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        JScrollPane scrollPane = (JScrollPane)getParent();
        return orientation == SwingConstants.VERTICAL ? scrollPane.getVerticalScrollBar().getBlockIncrement() : scrollPane.getHorizontalScrollBar().getBlockIncrement();
    }

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

    @Override public boolean getScrollableTracksViewportWidth() {
        Dimension sz = getSize();
        Dimension vsz = getViewportSize();
        return sz.width - 1 <= vsz.width; // This is the bodge
    }
}

JPanel panel = new ScrollableJPanel();
JScrollPane pane = new JScrollPane();

Después de probar nuestra aplicación con Java 1.8 rt.jar modificado para imprimir muchos resultados de depuración, encontramos que el valor solo estaba en un píxel, por lo que esa es la constante que estamos usando en el grupo anterior.

Por cierto, esto ha sido archivado con Oracle https://bugs.openjdk.java.net/browse/JDK-8042020, pero está marcado como "No se arregla".


1
2017-09-19 17:18



  • esta imagen hablando GridLayout, ambas partes tienen el mismo tamaño

  • uso apropiado LayoutManager que aceptando diferentes PreferredSize, BoxLayout o GridBagLayout 


0
2017-07-20 22:00



Ok, tuve exactamente el mismo problema:

  • añadir dinámicamente (sub) paneles a un panel (principal), que se pone en un jscrollpane (quiero crear una lista vertical de paneles)
  • la barra de desplazamiento vertical del jscrollpane se superpone a mi contenido (es decir, mis subpaneles).

¡He intentado seguir muchas de las soluciones encontradas en Internet (y stackoverflow) sin una solución real!

Esto es lo que entiendo, y mi solución de trabajo:

  • Parece que hay un error en scrollpanelayout (el administrador de diseño utilizado por el jscrollpane) que no calcula correctamente el tamaño del componente en él.

  • Si coloca mainpanel.setPreferedSize (nueva dimensión (1, 1)), no hay más errores de superposición, pero la barra de desplazamiento vertical (evento si se pone como VERTICAL_SCROLLBAR_ALWAYS) no se desplazará. Es porque el panel de desplazamiento cree que su contenido tiene una altura de 1 píxel.

  • Así que solo itere sobre su subpanel y calcule la altura adecuada:

//into your constructor or somewhere else
JPanel mainpanel = new JPanel();
mainpanel.setLayout(new BoxLayout(mainpanel, BoxLayout.Y_AXIS));
JScrollPane scrollPane = new JScrollPane(mainpanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);


//into another function to create dynamic content
int height = 0;
for (int i = 0; i < nb_subpanel_to_create; ++i) {
    JPanel subpanel = create_your_sub_panel();
    height += subpanel.getPreferedSize().height;
    mainpanel.add(subpanel);
}
mainpanel.setPreferedSize(new Dimension(1, height);

Prima:

Si desea que el contenido en la "lista" se acumule en la parte superior:

List<JComponent> cmps = new LinkedList<JComponent>();
int height = 0;
for (int i = 0; i < nb_subpanel_to_create; ++i) {
    JPanel subpanel = create_your_sub_panel();
    height += subpanel.getPreferedSize().height;
    cmps.add(subpanel);
}
mainpanel.add(stackNorth(subpanel));
mainpanel.setPreferedSize(new Dimension(1, height);

y la función stackNorth:

public static JPanel stackNorth(List<JComponent> components) {
     JPanel last = new JPanel(new BorderLayout());
     JPanel first = last;
     for (JComponent jComponent : components) {
          last.add(jComponent, BorderLayout.NORTH);
          JPanel tmp = new JPanel(new BorderLayout());
          last.add(tmp, BorderLayout.CENTER);
          last = tmp;
     }
     return first;
} 

0
2018-01-23 22:17