JScrollPane недостаточно широка, когда появляется вертикальная полоса прокрутки

У меня есть два JScrollPanes в одном окне. То, что слева, достаточно велико, чтобы отображать содержимое содержащейся панели. Тот, который справа, недостаточно велик, чтобы отображать его содержимое, и поэтому ему нужно создать вертикальную полосу прокрутки.

JScrollPane Issue

Но, как вы видите, проблема в том, что при появлении вертикальной полосы прокрутки полоса прокрутки появляется внутри JScrollPane. Он покрывает содержимое, содержащееся внутри, и, таким образом, горизонтальная полоса прокрутки необходима, чтобы показать все. Я хочу, чтобы это исправлено.

Я понимаю, что я могу постоянно поворачивать вертикальную полосу прокрутки, но по эстетическим соображениям я хочу, чтобы она появлялась, когда это необходимо, без необходимости создания горизонтальной полосы прокрутки.

EDIT: мой код для запуска такой же простой, как может быть:

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

Я использую MigLayout (MigLayout.com), но эта проблема появляется независимо от того, какой менеджер макетов я использую. Кроме того, если я сжимаю окно так, чтобы левая панель больше не была достаточно большой, чтобы отображать все, то такое же поведение, что и правая панель.

Ответ 1

Сначала: никогда не настраивать подсказки размеров на уровне компонента. Особенно не так, когда у вас есть мощный LayoutManager, такой как MigLayout, который поддерживает настройку на уровне менеджера.

В коде, изменяя размер pref независимо:

// 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);

Это сказало: в основном, вы попали в (спорную) ошибку в ScrollPaneLayout. Хотя это похоже на учет ширины полосы прокрутки, на самом деле это не во всех случаях. Соответствующий фрагмент из 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;
        }
    }
}

это преступник, потому что

  • он сравнивает представление pref с префиксом viewport
  • они чаще всего выполняются

Результат - это то, что вы видите: полоса прокрутки перекрывает (в смысле отсечения некоторой ширины) вид.

Взломанный пользовательский ScrollPaneLayout, который добавляет ширину полосы прокрутки, если высота представления меньше фактической высоты видового экрана, грубый пример (остерегайтесь: не качество продукции), чтобы играть с

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;
    }

}

Edit

hmm... см. прыжок (между показом или отсутствием вертикальной полосы прокрутки, как описано в комментарии): когда я заменяю текстовое поле в моем примере другой scrollPane, тогда изменение размера "рядом" с его шириной pref показывает проблема. Таким образом, взлом недостаточно хорош, возможно, что время запроса видового экрана по его масштабу неверно (в середине процесса компоновки). В настоящее время не знаю, как сделать лучше.

Изменить 2

Предварительное отслеживание: при изменении ширины по пикселям изображение кажется одноразовым. Изменение условия от < до <= похоже на исправление прыжка - по цене всегда добавляя ширину полосы прокрутки. Таким образом, в целом это приводит к шагу 1 с более широкой задней вставкой;-) Между тем, полагая, что вся логика scollLlayout нуждается в улучшении...

Подводя итоги по вашим параметрам:

  • отрегулируйте ширину префикса в компоненте компонента (MigLayout). Это самый простой недостаток - это дополнительное заднее пробел, если полоса прокрутки не отображается.
  • исправить scrollPaneLayout. Требуется некоторое усилие и тесты (см. Код ядра ScrollPaneLayout, что нужно сделать), преимущество заключается в последовательном заполнении без полосы прокрутки.
  • не опция вручную устанавливает ширину pref для компонента

Ниже приведены примеры кода для воспроизведения с помощью:

// 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);

Ответ 2

Я понял это. Проблема в том, что JScrollPane соответствует размеру содержимого внутри и перекрывает его, когда полоса прокрутки становится видимой. Поэтому я решил просто сделать контент более широким с летучей мыши, чтобы он мог разместить полосу прокрутки, если она станет видимой. Это также можно сделать, сделав вставками ширину полосы прокрутки, что, вероятно, является более ортодоксальным способом сделать это.

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");

EDIT: Ответ выше, вероятно, лучший ответ, но вот мой оригинальный ответ для любопытного, который фактически изменяет размеры панели.

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");

Итак, что же это делает контент такого же размера плюс вдвое больше ширины полосы прокрутки. Поскольку все центрировано, все выглядит одинаково, а затем некоторое дополнительное пространство на одной стороне заполняется полосой прокрутки, если она появляется.

Ответ 3

У меня была такая же проблема, и я пришел сюда после нескольких часов, пытаясь найти причину. Я закончил реализацию своего собственного ViewportLayout и хотел поделиться этим решением.

Основная проблема заключается в том, что ScrollPaneLayout не знает, что одно измерение (в вашем случае высота представления) будет ограничено максимальным значением, поэтому оно не может предварительно определить, нужны ли полосы прокрутки. Поэтому он не может настроить другой гибкий размер (в вашем случае ширина).

Пользовательский ViewportLayout может принять это во внимание, попросив его родителя разрешенной высоты и тем самым позволяя ScrollPaneLayout соответственно настроить предпочтительную ширину:

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 устанавливается следующим образом:

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

Ответ 4

Эта ошибка теперь несколько лет, но все еще нефиксирована, поэтому я добавлю свой вклад.

Вы также можете обойти ошибку, реализуя javax.swing.Scrollable в представлении compoment вашего JScrollPane. ScrollPaneLayout проверяет методы этого интерфейса, чтобы определить, нужно ли сначала отображать горизонтальную полосу прокрутки, прежде чем вернуться к расчету ошибок.

Вы можете реализовать интерфейс Scrollable так, чтобы компонент вида обрабатывался точно так же, как и до реализации Scrollable, за исключением метода определения того, отображается ли горизонтальная полоса прокрутки.

Конкретный пример, демонстрирующий это, который я не тестировал изолированно, но извлек из нашей базы кода. Изменение:

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

к

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();

Проверяя наше приложение на Java 1.8 rt.jar, модифицированное для печати большого количества отладочного вывода, мы обнаружили, что значение было выбрано только на один пиксель - так что константа, которую мы используем в вышеупомянутой папке.

Кстати, это было подано в Oracle https://bugs.openjdk.java.net/browse/JDK-8042020, но оно помечается как "Не будет исправлено".

Ответ 5

  • Этот образ говорит о GridLayout, обе части имеют одинаковый размер

  • используйте правильный LayoutManager, который принимает разные PreferredSize, BoxLayout или GridBagLayout

Ответ 6

Хорошо, у меня была такая же проблема:

  • динамически добавлять (под) панели в (основную) панель, которая помещается в jscrollpane (я хочу создать вертикальный список панелей)
  • вертикальная полоса прокрутки jscrollpane перекрывает мой контент (т.е. мои подпанели).

Я попытался выполнить множество обходных решений, найденных в Интернете (и stackoverflow) без реального решения!

Вот что я понимаю и мое рабочее решение:

  • Кажется, есть ошибка в scrollpanelayout (диспетчер компоновки, используемый jscrollpane), который не правильно вычисляет размер компонента в нем.

  • Если вы поместите mainpanel.setPreferedSize(новое измерение (1, 1)), больше не будет ошибок перекрытия, но вертикальная полоса прокрутки (событие, если поставить VERTICAL_SCROLLBAR_ALWAYS) не будет прокручиваться! Это потому, что scrollpane думает, что ваш контент имеет высоту в 1 пиксель.

  • Так просто перебирайте свою подпанель и вычисляйте соответствующую высоту:

//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);

Bonus:

Если вы хотите, чтобы содержимое в "списке" находилось в стеке:

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);

и функция 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;
}