adding a non-scrolling component to a JScrollPane

73 Views Asked by At

I'd like to add a non-scrolling element, always visible element (a "pinned" element) in a JScrollPane of a Swing GUI. Taking this example code:

public class Main {
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        JEditorPane editorPane = new JEditorPane();
        editorPane.setText("""
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                """);
        JScrollPane scroll = new JScrollPane(editorPane);
        editorPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
        editorPane.add(new JLabel("this shouldn't scroll!"));

        frame.getContentPane().add(scroll);
        frame.setSize(600, 400);
        frame.setVisible(true);
    }
}

Which produces this GUI:
the swing panel created by the code

I need a way to:

  1. Make sure that the "this shouldn't scroll!" JLabel doesn't move when the scroll pane scrolls.
  2. Pin the "this shouldn't scroll!" JLabel to the right side of the panel (currently, it becomes hidden with a resize of the panel. I'd like it to move left when the right side of the panel is pushed smaller, to make sure it's always on screen).

I've been able to make an awful implementation of this using swing's glass pane, but it means a lot more manual updating for me, and position always being the same, no matter how the swing panel is resized.

Thank you for any help!

3

There are 3 best solutions below

0
MadProgrammer On BEST ANSWER

As a personal preference, I'd probably use a compound layout, simular to this example as it gives you more control over the the component been rendered (ie much easier to deal with live components like buttons) and it's position.

You "could" make use of the JLayer API, for example...

enter image description here

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLayer;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.plaf.LayerUI;

public class Main {
    public static void main(String[] args) {
        new Main();
    }

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                } catch (IOException ex) {
                    Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() throws IOException {
            FixedTextLayerUI layerUI = new FixedTextLayerUI();
            layerUI.setText("this shouldn't scroll!");

            JTextArea ta = new JTextArea(20, 40);
            StringBuilder sb = new StringBuilder(1024);
            // Bring your own content ;)
            try (BufferedReader br = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/resources/StarWarsNewHope.txt")))) {
                String text = null;
                while ((text = br.readLine()) != null) {
                    sb.append(text);
                    sb.append(System.lineSeparator());
                }
                ta.setText(sb.toString());
            }
            JLayer<JComponent> layer = new JLayer<JComponent>(new JScrollPane(ta), layerUI);
            setLayout(new BorderLayout());
            add(layer);
        }

    }

    public class FixedTextLayerUI extends LayerUI<JComponent> {

        private String text;

        public void setText(String text) {
            this.text = text;
        }

        public String getText() {
            return text;
        }

        @Override
        public void paint(Graphics g, JComponent c) {
            super.paint(g, c);

            Graphics2D g2 = (Graphics2D) g.create();

            FontMetrics fm = g2.getFontMetrics();
            int stringWidth = fm.stringWidth(getText());
            int x = c.getWidth() - stringWidth - 8;
            int y = fm.getHeight() + fm.getAscent() + 8;

            if (c instanceof JLayer) {
                JLayer layer = (JLayer) c;
                if (layer.getView() instanceof JScrollPane) {
                    JScrollPane scrollPane = (JScrollPane) layer.getView();
                    JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
                    if (scrollBar.isVisible()) {
                        x -= scrollBar.getWidth();
                    }
                }
            }

            g2.drawString(text, x, y);

            g2.dispose();
        }

    }
}

See How to Decorate Components with the JLayer Class for more details.

2
Abra On

I suggest using the "column header" (of the JScrollPane). In the below code, the "column header" is a JPanel with BoxLayout.

import java.awt.BorderLayout;
import java.awt.EventQueue;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class Main implements Runnable {

    @Override
    public void run() {
        JFrame frame = new JFrame("Pinned");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JTextArea txtAr = new JTextArea("""
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                d
                """);
        txtAr.setColumns(40);
        JScrollPane scrollPane = new JScrollPane(txtAr);
        JPanel hdr = new JPanel();
        hdr.setBackground(txtAr.getBackground());
        BoxLayout layout = new BoxLayout(hdr, BoxLayout.LINE_AXIS);
        hdr.setLayout(layout);
        JLabel first = new JLabel("d");
        first.setFont(txtAr.getFont());
        hdr.add(first);
        hdr.add(Box.createHorizontalGlue());
        JLabel last = new JLabel("this shouldn't scroll");
        last.setFont(txtAr.getFont());
        hdr.add(last);
        scrollPane.setColumnHeaderView(hdr);
        frame.add(scrollPane, BorderLayout.CENTER);
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
    }

    public static void main(String[] args) throws Exception {
        EventQueue.invokeLater(new GUI());
    }
}

Here is a screen capture:

screen capture

0
camickr On

Java provides an OverlayLayout layout manager, which should be able to handle this requirement but which I find almost impossible to use.

Instead you could use the custom OverlayPanel class which will allow you to overlay two components.

There are two keys to the code:

  1. you must specify the proper ZOrder of each component to make sure the background component is painted before the foreground component
  2. you need to override the isOptimizedDrawaingEnabled() to force both components to be repainted all the time.

The foreground component can be positioned in one of the 9 anchor locations supported by the GridBagLayout "anchor" constraint.

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;

public class OverlayPanel extends JPanel
{
    private GridBagLayout layout;
    private GridBagConstraints gbc;
    private Component background;
    private Component foreground;

    public OverlayPanel(Component background, Component foreground)
    {
        this(background, foreground, GridBagConstraints.CENTER);
    }

    public OverlayPanel(Component background, Component foreground, int foregroundAnchor)
    {
        layout = new GridBagLayout();
        setLayout( layout );

        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;

        setForegroundComponent(foreground, foregroundAnchor );
        setBackgroundComponent( background );
    }

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

    public void setBackgroundComponent(Component background)
    {
        this.background = background;

        gbc.weightx = 1.0f;
        gbc.weighty = 1.0f;
        gbc.fill = GridBagConstraints.BOTH;
        gbc.anchor = GridBagConstraints.CENTER;

        super.add(background, gbc);
        setComponentZOrder(background, 1);
    }

    public void setForegroundComponent(Component foreground, int foregroundAnchor)
    {
        this.foreground = foreground;

        gbc.weightx = 0.0f;
        gbc.weighty = 0.0f;
        gbc.fill = GridBagConstraints.NONE;
        gbc.anchor = foregroundAnchor;

        super.add(foreground, gbc);
        setComponentZOrder(foreground, 0);
    }

    public void setForegroundPadding(int padX, int padY)
    {
        GridBagConstraints gbc = layout.getConstraints( foreground );

        gbc.ipadx = padX;
        gbc.ipady = padY;

        layout.setConstraints(foreground, gbc);
        revalidate();
        repaint();
    }

    private static void createAndShowGUI()
    {
        JTextArea textArea = new JTextArea(10, 20);
        JScrollPane scrollPane = new JScrollPane( textArea );

        JLabel label  = new JLabel("Testing");

        OverlayPanel overlay = new OverlayPanel(scrollPane, label, GridBagConstraints.FIRST_LINE_END);
//      OverlayPanel overlay = new OverlayPanel(scrollPane, label);
        overlay.setForegroundPadding(8 + UIManager.getInt("ScrollBar.width"), 8);

        JFrame frame = new JFrame("OverlayPanel");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(overlay);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationByPlatform( true );
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        java.awt.EventQueue.invokeLater( () -> createAndShowGUI() );
    }
}