Java Swing trying to add a stationary JButton over a JScrollPane

135 views Asked by At

I am trying to make a text editor with a button that appears at the bottom right of the editor regardless if you scroll up or down and appears over the text area

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

public class Problem{

    public static void main(String[] args){
        //Setting up the frame
        JFrame window = new JFrame();
        window.setSize(600, 400);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //Making the LayeredPane
        JLayeredPane LP = new JLayeredPane();
        LP.setLayout(new BorderLayout());

        //Making the ScrollPane and JTextArea
        JTextArea textArea = new JTextArea(100,50);
        textArea.setText("Test Text");
        JScrollPane back = new JScrollPane();
        back.setViewportView(textArea);

        //Making the panel that appears in the front of the text
        JPanel front = new JPanel();
        front.setLayout(null);
        front.setBackground(new Color(0,0,0,0));
        front.setOpaque(false);
        JButton button = new JButton("test");
        button.setBounds(200,200,50,20);
        front.add(button);

        LP.add(back,BorderLayout.CENTER);
        LP.setLayer(back,0,0);

        LP.add(front,BorderLayout.CENTER);
        LP.setLayer(front,1,0);

        window.add(LP);
        window.setVisible(true);
    }
}

I am seeing just the JButton with a white background, if I don't add the second layer "front" I see my back JScrollPane with the JTextArea

2

There are 2 answers

6
camickr On

Swing is designed/optimized to display/paint components in 2 dimensions. The vast majority of layout managers will make sure that the components don't overlap.

This means that you can't use a layout manager on your layered pane (if you want the components to overlap). Instead, you must manually set the size/location of components on each layer.

When you use a JLayeredPane the painting of components on each layer is managed so that the higher layer is painted last.

So your code might be changed to something like:

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

public class Main
{
    public static void main(String[] args)
    {
        JFrame window = new JFrame();
        window.setSize(600, 400);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //Making the LayeredPane
        JLayeredPane LP = new JLayeredPane();

        //Making the ScrollPane and JTextArea
        JTextArea textArea = new JTextArea(20,40);
        textArea.setText("Test Text");
        textArea.setSize( textArea.getPreferredSize() );
        JScrollPane back = new JScrollPane( textArea);
        back.setSize( textArea.getSize() );

        JButton button = new JButton("test");
        button.setBounds(200,200,50,20);

        LP.add(back, new Integer(0));
        LP.add(button, new Integer(1));

        window.add(LP);
        window.setVisible(true);
    }
}

There is one layout manager in the JDK, the OverlayLayout, which is designed to stack components on top of one another. However, even this layout manager does not paint components properly when the components overlap. The trick when using this layout manager is to override the isOptimizedDrawing() method of the panel using the layout manager to make sure all components are repainted all the time. In this case, make sure the bottom panel is always painted before the top panel.

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

public class Main2
{
    public static void main(String[] args)
    {
        JFrame window = new JFrame();
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel contentPane = new JPanel()
        {
            @Override
            public boolean isOptimizedDrawingEnabled()
            {
                return false;
            }
        };
        contentPane.setLayout( new OverlayLayout(contentPane) );

        JPanel top = new JPanel( new GridBagLayout() );
        top.setBorder( new EmptyBorder(0, 0, 16, 16) );
        top.setOpaque(false);
        top.setAlignmentX(1.0f);
        top.setAlignmentY(1.0f);

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        gbc.anchor = GridBagConstraints.LAST_LINE_END;

        JButton button = new JButton("test");
        top.add(button, gbc);
        contentPane.add(top);

        //Making the ScrollPane and JTextArea
        JTextArea textArea = new JTextArea(10,25);
        textArea.setText("Test Text");
        JScrollPane back = new JScrollPane( textArea);
        back.setAlignmentX(1.0f);
        back.setAlignmentY(1.0f);
        contentPane.add(back);

        window.add(contentPane);
        window.pack();
        window.setVisible(true);
    }
}

The benefit of this approach is that the button will move as the frame is resized.

However, as a user I would still get annoyed with a button appearing over top of the text in my text area.

Edit:

If you really need components to overlap then I would suggest you could:

  1. look at MadProgrammers solution to use a GridBagLayout. This approach gives far more control over the alignment of the components
  2. check out the Overlap Layout which also provides more flexibility when aligning overlapping components

It should be noted that both above approaches may still require you to override the isOptimizedDrawEnabled(...) method to make sure components are painted properly. I am not aware of any layout manager the allows you to overlap components and works without this override.

1
MadProgrammer On

Caveat

I'm not a fan of this is idea. It's not a "common" UX concept that many desktop users would be presented with and there are a number of, arguably, better solutions which leverage the pre-existing experience of users.

This requires some "hacking" to get to work, so, there's no guarantee that it will work on all platforms or continue to work into the future.

Why doesn't it work?

This is a rather technical question which delves deep into the core of how Swing, and in particular, the JScrollPane work. Let's just say, I don't have the time or desire to dig into, but I know the JScrollPane is heavy optimised, which may be affecting the way in which anything which overlays it gets updated - or it could just be the way that the painting system works.

Runnable example...

This takes the idea by camickr (all credit to him), but instead of using a OverlayLayout, makes use of a GridBagLayout to position the button. Why? Because the GridBagLayout gives me more control over the position of the button - it's a personal thing.

enter image description here

import java.awt.*;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.swing.*;

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

    public Main() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new BorderLayout());

            JTextArea textArea = new JTextArea(40, 40);
            try (Reader reader = new InputStreamReader(getClass().getResourceAsStream("/resources/StarWarsNewHope.txt"))) {
                textArea.read(reader, "A long list");
            } catch (IOException exp) {
                exp.printStackTrace();
            }
            JButton button = new JButton("Am I in your way yet");

            JPanel contentPane = new JPanel() {
                @Override
                public boolean isOptimizedDrawingEnabled() {
                    return false;
                }
            };
            contentPane.setLayout(new GridBagLayout());

            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridx = 0;
            gbc.gridy = 0;
            gbc.weightx = 1.0;
            gbc.weighty = 1.0;
            // Change this to reposition the button some where else
            gbc.anchor = GridBagConstraints.FIRST_LINE_END;
            gbc.insets = new Insets(32, 32, 32, 32);
            gbc.ipadx = 16;
            gbc.ipady = 16;
            contentPane.add(button, gbc);

            gbc = new GridBagConstraints();
            gbc.gridx = 0;
            gbc.gridy = 0;
            gbc.weightx = 1.0;
            gbc.weighty = 1.0;
            gbc.fill = GridBagConstraints.BOTH;

            JScrollPane scrollPane = new JScrollPane(textArea);
            contentPane.add(scrollPane, gbc);

            add(contentPane);
        }

    }
}

You should probably also look at How to Use Scroll Panes and the section on Providing Custom Decorations for some alternatives