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 на контейнера.   -  person Guillaume Polet    schedule 21.07.2012
comment
Мисля, че бих предпочел контейнерът да расте.   -  person Thunderforge    schedule 21.07.2012


Отговори (6)


Първо: никога -винаги променяйте съветите за оразмеряване на ниво компонент. Особено не е така, когато имате мощен LayoutManager като MigLayout, който поддържа настройване на ниво мениджър.

В код, коригиране на предпочитания размер на каквото и да е:

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

това е виновникът, защото

  • сравнява предпочитанията за изглед с предпочитанията за прозорец за изглед
  • те са едни и същи през повечето време

Резултатът е това, което виждате: лентата за превъртане припокрива (в смисъл на отрязване на някаква ширина) изгледа.

Хак наоколо е персонализиран 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;
    }

}

Редактиране

хм ... вижте прескачането (между показване и непоказване на вертикалната лента за превъртане, както е описано в коментара): когато заменя текстовото поле в моя пример с друг scrollPane, тогава преоразмеряването "близо" до неговата предпочитана ширина показва проблема. Така че хакът не е достатъчно добър, възможно е времето на запитване на прозореца за изглед за неговия обхват да е неправилно (в средата на процеса на оформление). В момента няма идея как да се направи по-добре.

Редактиране 2

пробно проследяване: когато правите промяна на ширината пиксел по пиксел, усещането е като еднократна грешка. Промяната на условието от < на <= изглежда коригира прескачането - на цената на винаги добавяне на ширината на лентата за превъртане. Така че като цяло това води до първа стъпка с по-широка завършваща вмъкване ;-) Междувременно вярваме, че цялата логика на scollLlayout трябва да бъде подобрена ...

За да обобщим вашите възможности:

  • коригира ширината на pref в (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
Благодаря ви за много подробното решение. Освен че трябва да деактивирам хоризонталната лента за превъртане, не виждам никаква разлика между използването на вмъквания или предпочитания за създаване на достатъчно място за припокриване на лента за превъртане, така че може би това е просто въпрос на лични предпочитания. Харесва ми идеята да поправя този бъг в 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
и не - visibleAmount не е свойството, което търсите за регулиране на ширината .. и не: няма нужда да деактивирате показването на хоризонталната лента за превъртане, ако предпочитаната ширина е коригирана от някоя от опциите, които предложих - 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, което приема различен предпочитан размер, 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