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

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

Проблема JScrollPane

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

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

РЕДАКТИРОВАТЬ: Мой код для запуска настолько прост, насколько это возможно:

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

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


person Thunderforge    schedule 20.07.2012    source источник
comment
Я попытался вызвать setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)), но он просто создал очень большую рамку вокруг содержимого, которая ничего не делала для предотвращения появления горизонтальной полосы прокрутки.   -  person Thunderforge    schedule 21.07.2012
comment
Одна вещь, которую вам нужно выяснить, это то, как вы хотите, чтобы она вел себя, когда появляется вертикальная полоса прокрутки. Либо содержимое должно сжиматься (затем работать с LayoutManager содержимого), либо контейнер должен увеличиваться (и затем работать с LayoutManager Container.   -  person Guillaume Polet    schedule 21.07.2012
comment
Я думаю, что я бы предпочел, чтобы контейнер рос.   -  person Thunderforge    schedule 21.07.2012


Ответы (6)


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

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

это виновник, потому что

  • он сравнивает преф представления с префиксом вьюпорта
  • они одинаковые большую часть времени

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

Обходной путь — это пользовательский 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;
    }

}

Изменить

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

Изменить 2

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

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

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

Ниже приведены примеры кода, с которыми можно поиграть:

// 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);
person kleopatra    schedule 21.07.2012
comment
Спасибо за очень подробное решение. Помимо отключения горизонтальной полосы прокрутки, я не вижу никакой разницы между использованием insets или prefs для создания достаточного пространства для размещения перекрывающейся полосы прокрутки, так что, возможно, это просто вопрос личных предпочтений. Мне нравится идея исправления этой ошибки в JScrollPane, так что я обязательно поиграю с этим. Еще раз спасибо за ваш подробный ответ! - person Thunderforge; 22.07.2012
comment
Только что попытался создать свой собственный JScrollPane с переопределенным методом ScrollPaneLayout PreferredLayoutSize, используя ваш код. Это работает некоторое время. Когда я пытаюсь изменить размер окна, любые JScrollPanes, которые не имеют видимой панели VerticalScrollBar, быстро переключаются между правильным размером, который мы ищем, и неправильным размером. Я могу поиграть с этим еще немного, но если у вас есть какие-либо идеи, я хотел бы их услышать. - person Thunderforge; 22.07.2012
comment
После некоторой отладки я обнаружил, что результаты pane.getViewport().getExtentSize() меняются довольно часто, что, по-видимому, приводит к описанному мной поведению. Ищем, как это исправить. - person Thunderforge; 22.07.2012

Я понял. Проблема в том, что 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");

РЕДАКТИРОВАТЬ: ответ выше, вероятно, лучший ответ, но вот мой оригинальный ответ для любопытных, который фактически изменяет размер панели.

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

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

person Thunderforge    schedule 21.07.2012
comment
+1 хорошая идея отправить сюда свое решение, пожалуйста, вы можете принять свой пост (2 дня ???) - person mKorbel; 21.07.2012
comment
и no - visibleAmount не является тем свойством, которое вы ищете для настройки ширины.. и нет: нет необходимости отключать отображение горизонтальной полосы прокрутки, если ширина pref регулируется любым из предложенных мной вариантов - person kleopatra; 22.07.2012
comment
А, я вижу, что getPreferredSize() на вертикальной полосе прокрутки, вероятно, лучший способ получить ширину, чем getVisibleAmount(). Я изменил это. Вы правы, этот метод приводит к отключению горизонтальной полосы прокрутки, что может подойти не всем. - person Thunderforge; 22.07.2012

У меня была та же проблема, и я прибыл сюда после нескольких часов попыток найти причину. В итоге я реализовал свой собственный 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());
person meyertee    schedule 29.03.2013

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

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

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

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

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

to

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, но он помечен как "Не будет исправлено".

person Mike B    schedule 19.09.2017

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

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

person mKorbel    schedule 20.07.2012
comment
На самом деле я использую MigLayout (miglayout.com). Извините, надо было упомянуть об этом первым. Однако, похоже, это происходит независимо от того, какой макет я использую. - person Thunderforge; 21.07.2012
comment
Извините, я не совсем понимаю, о чем вы спрашиваете. Вы спрашиваете, отправил ли я этот вопрос в SSCCE? Это что? - person Thunderforge; 21.07.2012
comment
Ах хорошо. Я отредактировал исходный пост некоторое время назад, чтобы включить код. Опять же, такое поведение происходит независимо от того, использую ли я MigLayout или нет. И если я изменяю размер окна, чтобы оно было слишком маленьким, чтобы отображать содержимое на левой панели, то происходит то же поведение, что и на правой панели. - person Thunderforge; 21.07.2012
comment
-1 за предложение менее мощных - по сравнению с Mig - LayoutManagers, которые не решают проблему. На самом деле, это не менеджер кода приложения, который плохо себя ведет, это ScrollPaneLayout ;-) - person kleopatra; 21.07.2012

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

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

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

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

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

  • Если вы поместите mainpanel.setPreferedSize(new Dimension(1, 1)), больше не будет ошибки перекрытия, но вертикальная полоса прокрутки (событие, если указано как VERTICAL_SCROLLBAR_ALWAYS) не будет прокручиваться! Это потому, что панель прокрутки считает, что ваш контент имеет высоту 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);

Бонус:

Если вы хотите, чтобы содержимое в «списке» располагалось вверху:

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;
} 
person Alexxx    schedule 23.01.2017