Why can't I shift focus to an unshown JPanel card in a CardLayout?
I'm implementing a CardLayout-based interface that needs to be keyboard navigation friendly. When a user finishes tabbing through all the fields on one card, the user needs to be able to tab to the next card.
I've already implemented a FocusTraversalPolicy that produces the right Component at each point in the process, and a FocusAdapter to pop up any cards newly tabbed to, but something is eating the messages and preventing focus change.
I can uncleanly pass the CardLayout to the FocusTraversalPolicy to change the card— though any of its functions are called several times in Swing's many threads and leads to strange behavior. Besides, that way's just dirty.
I do not want to use key bindings b/c that would require reimplementing all of the focus work Java already does for me, and is also really unclean.
Basically: Java dislikes shifting focus to unshown cards in CardLayouts— how can I override this?
I want to keep the program compartmentalized, as it runs in distinct steps.
This does not prevent you from creating a long scrolling form?
You can still create individual panels the way you are doing now. Then instead of adding these panels to a CardLayout where you need to swap panels, you can add the panels to a panel using a BoxLayout (or GridBagLayout).
This would even give more flexibility since each panel can be of a different size without impacting the size of every individual panel.
However, forms do not currently scroll automatically in a JScrollPane, so you may want to check out Scrolling a Form for a class this will do this for you.
It sounds like you have a wizard-like UI. If so, add a "Next" button as the last field on each card.
The action of the Next button would be to flip to the next card as set focus to the first entry field.
The last entry field on each card would transfer focus to the Next button, who could then be "pressed" with a strike of the spacebar when it receives focus (which is the default behavior of a JButton), keeping it keyboard-friendly.
This would alleviate the need for special KeyBindings or FocusTraversalPolicies.
EDIT:
Try this, using FocusListeners for the JTextFields. Tab thorugh the fields and the cards will flip to the next one automatically once you tab out of the last field. You could use ActionListeners instead if you wish:
EDIT 2: Added hack for panels that only have 1 field.
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import javax.swing.*;
public class CardLayoutDemo2 implements Runnable
{
final static String CARD1 = "One";
final static String CARD2 = "Two";
final static String CARD3 = "Three";
JPanel cards;
CardLayout cardLayout;
JTextField tf1, tf2, tf3, tf4, tf5;
JButton dummy;
public static void main(String[] args)
{
SwingUtilities.invokeLater(new CardLayoutDemo2());
}
public void run()
{
tf1 = new JTextField(10);
tf2 = new JTextField(10);
tf2.addFocusListener(new CardFlipper(CARD2));
tf3 = new JTextField(10);
tf4 = new JTextField(10);
tf4.addFocusListener(new CardFlipper(CARD3));
tf5 = new JTextField(10);
tf5.addFocusListener(new CardFlipper(CARD1));
dummy = new JButton()
{
#Override
public Dimension getPreferredSize()
{
return new Dimension(0,0);
}
};
dummy.addFocusListener(new FocusAdapter()
{
#Override
public void focusGained(FocusEvent e)
{
dummy.transferFocus();
}
});
JPanel card1 = new JPanel();
card1.add(new JLabel("One"));
card1.add(tf1);
card1.add(new JLabel("Two"));
card1.add(tf2);
JPanel card2 = new JPanel();
card2.add(new JLabel("Three"));
card2.add(tf3);
card2.add(new JLabel("Four"));
card2.add(tf4);
JPanel card3 = new JPanel();
card3.add(dummy);
card3.add(new JLabel("Five"));
card3.add(tf5);
cardLayout = new CardLayout();
cards = new JPanel(cardLayout);
cards.add(card1, CARD1);
cards.add(card2, CARD2);
cards.add(card3, CARD3);
JFrame f = new JFrame("CardLayout Demo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(cards, BorderLayout.CENTER);
f.setSize(180, 200);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private class CardFlipper extends FocusAdapter
{
private String nextCard;
CardFlipper(String cardName)
{
this.nextCard = cardName;
}
#Override
public void focusLost(FocusEvent e)
{
cardLayout.show(cards, nextCard);
}
}
}
Related
I am trying to achieve the following effect in Java:
However, I am not sure what layout to use and how. FlowLayout obviously doesn't work. GridLayout won't work either because the first 4 rows are supposed to be 1 column rows, but the 5th row needs to have 2 columns.
This is my code so far:
public class DepositPanel extends JPanel
{
private JLabel cashL, checksL;
private JTextField cashTF, checksTF;
private JButton ok, cancel;
DepositPanel()
{
JPanel depositP = new JPanel();
depositP.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 2));
depositP.setPreferredSize(new Dimension(250, 85));
JTextField cashTF = new JTextField(22);
JTextField checksTF = new JTextField(22);
JLabel cashL = new JLabel("Cash:");
JLabel checksL = new JLabel("Checks:");
ok = new JButton("OK");
cancel = new JButton("CANCEL");
depositP.add(cashL);
depositP.add(cashTF);
depositP.add(checksL);
depositP.add(checksTF);
depositP.add(ok);
depositP.add(cancel):
}
}
You could try with combinations of Layouts, 2 JPanels, 1 for buttons and 1 for fields, button panel with FlowLayout and fields panel with BoxLayout. And adding them to the frame. (I did a JFrame for testing, but you can change it to a JPanel and add that panel to your JFrame). Just be sure to have only 1 JFrame, see The use of multiple JFrames, Good / Bad Practice.
For example:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
public class DepositExample {
JFrame frame;
JPanel buttonPane, fieldsPanel;
JLabel cash, checks;
JTextField cashField, checksField;
JButton ok, cancel;
DepositExample() {
frame = new JFrame("Deposit");
buttonPane = new JPanel();
fieldsPanel = new JPanel();
cash = new JLabel("Cash");
checks = new JLabel("Checks");
cashField = new JTextField("");
checksField = new JTextField("");
ok = new JButton("OK");
cancel = new JButton("Cancel");
fieldsPanel.setLayout(new BoxLayout(fieldsPanel, BoxLayout.PAGE_AXIS));
buttonPane.setLayout(new FlowLayout());
fieldsPanel.add(cash);
fieldsPanel.add(cashField);
fieldsPanel.add(checks);
fieldsPanel.add(checksField);
buttonPane.add(ok);
buttonPane.add(cancel);
frame.add(fieldsPanel, BorderLayout.PAGE_START);
frame.add(buttonPane, BorderLayout.PAGE_END);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
public static void main(String args[]) {
new DepositExample();
}
}
To get some more spacing between components you can add EmptyBorders as recommended by #LuxxMiner in his comment below.
In this case you can use a JOptionPane to build a simple panel for you:
JTextField firstName = new JTextField(10);
JTextField lastName = new JTextField(10);
Object[] msg = {"First Name:", firstName, "Last Name:", lastName};
result = JOptionPane.showConfirmDialog(
frame,
msg,
"Use default layout",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE);
if (result == JOptionPane.YES_OPTION)
{
System.out.println(firstName.getText() + " : " + lastName.getText());
}
else
{
System.out.println("Canceled");
}
The only problem with this approach is that the focus will be on a button, not the first name text field.
So to solve this problem you can check out the RequestFocusListener found in Dialog Focus which will cause focus to be placed on the first name text field once the dialog is displayed.
JTextField firstName = new JTextField(10);
firstName.addAncestorListener( new RequestFocusListener() );
Although for more complex layouts it is better to create one or more panels each using an appropriate layout manager for the requirement.
There are many ways to achieve a layout like this. The first thing you need to get used to, is that its often simpler to split up different requirements into different containers using different layout managers.
If you separate the two buttons into their own panel and treat that panel with the buttons as "just another line" in the window, you can basically just use a GridLayout with a single column. The panel with the buttons could then use a FlowLayout to place the buttons side by side.
Try this:
public class Window extends JFrame{
....
}
JLabel example;
//Constructor
public Window(){
example = new JLabel("Sample text");
example.setBounds(x,y,width,height)
//JComponent...
setLayout(null);
setSize(width,height);
setVisible(true);
}
Without the JPanel you can specify the x and y coordinates
I have created a card game which allows the users to move the cards, which are JPanels, on top of each other. However, I noticed that if I attempt to move a card to another cards exact location (ie on top of it), that card will not always be displayed on top of that card.
For example, lets say we have 5 cards, which where built in order.
If move card1 to card2's location, then card1 will appear on top of card2. However, if I tried to move card5 to card3's location, then card5 will appear underneath card3.
How can can I make is so that the last card that I move will be the one on top?
However, I noticed that if I attempt to move a card to another cards exact location (ie on top of it), that card will not always be displayed on top of that card.
This sounds related to the Z-Ordering of components. Basically the default behaviour for Swing is that the last component added to a panel is painted first.
So you need to change the Z-Order when you add the card on the panel. You are probably using code like:
panel.add( card );
The easy solution is to use:
panel.add(0, card);
Or, when you handle the mousePressed() event when you click on the card your would use:
Component child = event.getComponent();
Component parent = child.getParent();
parent.setComponentZOrder(child, 0);
You may also want to look at the Overlap Layout which explains Z-Ordering a little more and provides a layout manager that can allow you to stack cards.
For this purpose card layout is your friend.
How to use card layout https://docs.oracle.com/javase/tutorial/uiswing/layout/card.html
Example uses:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class CardLayoutDemo implements ItemListener {
JPanel cards; //a panel that uses CardLayout
final static String BUTTONPANEL = "Card with JButtons";
final static String TEXTPANEL = "Card with JTextField";
public void addComponentToPane(Container pane) {
//Put the JComboBox in a JPanel to get a nicer look.
JPanel comboBoxPane = new JPanel(); //use FlowLayout
String comboBoxItems[] = { BUTTONPANEL, TEXTPANEL };
JComboBox cb = new JComboBox(comboBoxItems);
cb.setEditable(false);
cb.addItemListener(this);
comboBoxPane.add(cb);
//Create the "cards".
JPanel card1 = new JPanel();
card1.add(new JButton("Button 1"));
card1.add(new JButton("Button 2"));
card1.add(new JButton("Button 3"));
JPanel card2 = new JPanel();
card2.add(new JTextField("TextField", 20));
//Create the panel that contains the "cards".
cards = new JPanel(new CardLayout());
cards.add(card1, BUTTONPANEL);
cards.add(card2, TEXTPANEL);
pane.add(comboBoxPane, BorderLayout.PAGE_START);
pane.add(cards, BorderLayout.CENTER);
}
public void itemStateChanged(ItemEvent evt) {
CardLayout cl = (CardLayout)(cards.getLayout());
cl.show(cards, (String)evt.getItem());
}
/**
* Create the GUI and show it. For thread safety,
* this method should be invoked from the
* event dispatch thread.
*/
private static void createAndShowGUI() {
//Create and set up the window.
JFrame frame = new JFrame("CardLayoutDemo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Create and set up the content pane.
CardLayoutDemo demo = new CardLayoutDemo();
demo.addComponentToPane(frame.getContentPane());
//Display the window.
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
}
You can easily modify it to you fulfill your goal.
I have created a JScrollPane with a JPanel inside it and I want to add JPanel/JLabel/Other objects after pressing the button. For example after three button presses I want to get something like this:
I tried myJPane.add(testLabel) with testlabel.setBounds()but no result, I don't want to use GridLayout because of the unchangeable sizes. I would like it if the added objects had different sizes - adjusted to the text content.
What should I use for it and how?
Thanks in advance.
Best regards,
Tom.
Here is a JPanel inside a JScrollPane that adds JLabels to it when pressing the button:
public class Example extends JFrame {
public Example() {
JPanel boxPanel = new JPanel();
boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.PAGE_AXIS));
JTextField textField = new JTextField(20);
JButton sendButton = new JButton("Send");
sendButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
JLabel label = new JLabel(textField.getText());
label.setOpaque(true);
label.setBackground(Color.RED);
boxPanel.add(label);
boxPanel.add(Box.createRigidArea(new Dimension(0,5)));
textField.setText("");
boxPanel.revalidate();
// pack();
}
});
JPanel southPanel = new JPanel();
southPanel.add(textField);
southPanel.add(sendButton);
add(new JScrollPane(boxPanel));
add(southPanel, BorderLayout.PAGE_END);
setDefaultCloseOperation(EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
new Example();
}
}
The BoxLayout will stack the labels on top of each other.
Notes:
setOpaque(true) must be called on label for it to honor the background color.
Box.createRigidArea is used for creating gaps. Use it as you wish.
The call to revalidate() is imperative in order to display the new components immediately.
Calling pack() (on the JFrame) will resize it each time to fit all the new components. I just put it there for demonstration since the initial frame size is too small to display the initial components added.
I will use a BoxLayout, creating a vertical box, and after each button action, it will add a new JPanel to this box.
Example:
public class YourChat extends JPanel{
private JScrollPane sc;
private Box bv;
public YourChat(){
bv = Box.createVerticalBox();
sc = new JScrollPane(bv);
//your functions (panel creation, addition of listeners, etc)
add(sc);
}
//panel customized to have red backgroud
private class MyPanel extends JPanel(){
private JLabel label=new JLabel();
public MyPanel(String text){
setBackgroundColor(Color.red);
add(label);
}
}
//inside the action listener
public void actionPerformed(ActionEvent e) {
sc.add(new MyPanel(textField.getText()));
textField.setText("");
}
}
For extra information check on:
[https://docs.oracle.com/javase/tutorial/uiswing/layout/box.html]
See also the example
[http://www.java2s.com/Code/Java/Swing-JFC/VerticalandhorizontalBoxLayouts.htm]
Use BoxLayout if you want only add vertically, otherwise you can use FlowLayout for both directions.
Trying to change the look of a JOptionPane while its open, depending on which radiobutton the user clicks. What am I doing wrong? It works perfect if I for example add a button and move a JLabel from side to side of the window.
import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;
import static javax.swing.JOptionPane.*;
public class ChangePanel extends JFrame{
private JButton click = new JButton("CLICK ME!");
ChangePanel(){
add(click, BorderLayout.SOUTH);
click.addActionListener(new ButtonListen());
setVisible(true);
setSize(300,100);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
}
public class ButtonListen implements ActionListener{
public void actionPerformed(ActionEvent e){
PopUpPanel pop = new PopUpPanel();
showConfirmDialog(ChangePanel.this, pop, "Changeable", OK_CANCEL_OPTION);
}
}
//Send this as Parameter to the ConfirmDialog
public class PopUpPanel extends JPanel implements ActionListener{
JRadioButton jewelry = new JRadioButton("Jewelry");
JRadioButton shares = new JRadioButton("Shares");
JRadioButton machine = new JRadioButton("Machine");
PopUpPanel(){
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
ButtonGroup bg = new ButtonGroup();
JPanel north = new JPanel();
bg.add(jewelry);
jewelry.addActionListener(this);
bg.add(shares);
shares.addActionListener(this);
bg.add(machine);
machine.addActionListener(this);
north.add(jewelry);
north.add(shares);
north.add(machine);
add(north);
}
//Listener for RadioButtons
public void actionPerformed(ActionEvent e){
JTextField info1Txt = new JTextField(12);
JTextField info2Txt = new JTextField(12);
JTextField info3Txt = new JTextField(3);;
JRadioButton b = (JRadioButton)e.getSource();
if(b.getText().equals("Jewelry")){
//Dummy test text
System.out.println("Jewelry");
JPanel info1 = new JPanel();
info1.add(new JLabel("info1:"));
info1.add(info1Txt);
add(info1);
JPanel info2 = new JPanel();
info2.add(new JLabel("info2:"));
info2.add(info2Txt);
add(info2);
JPanel info3 = new JPanel();
info3.add(new JLabel("info3:"));
info3.add(info3Txt);
add(info3);
validate();
repaint();
}else if(b.getText().equals("Shares")){
//Dummy test text
System.out.println("Shares");
}else
//Dummy test text
System.out.println("Machine");
}
}
public static void main(String[] args){
new ChangePanel();
}
}
As you are working with BoxLayout, you should provide size hints to the PopUpPanel panel, which you haven't given.
When a BoxLayout lays out components from top to bottom, it tries to size each component at the component's preferred height. If the vertical space of the layout does not match the sum of the preferred heights, then BoxLayout tries to resize the components to fill the space. The components either grow or shrink to fill the space, with BoxLayout honoring the minimum and maximum sizes of each of the components.
check out the official tutorial page discussion: BoxLayout Feature
Call revalidate() and repaint() on the container after removing or adding components to it. So if you change the following lines:
validate();
repaint();
to:
revalidate();
repaint();
The content should appear. Though, it will not fit the original size of the JOptionPane. You can override PopUpPanel.getPreferredSize() to return desired size so that JOptionPane is packed properly, ie:
#Override
public Dimension getPreferredSize() {
return new Dimension(300, 300);
}
You can also use JDialog instead of JOptionPane.
Also, consider using CardLayout instead of swapping components manually. Check How to Use CardLayout for examples.
Why not just use setPreferredSize(new Dimension(300, 300)) in PopUpPanel constructor? Works fine for me. Good eye on revalidate and repaint.
I'm trying to make a little game that will first show the player a simple login screen where they can enter their name (I will need it later to store their game state info), let them pick a difficulty level etc, and will only show the main game screen once the player has clicked the play button. I'd also like to allow the player to navigate to a (hopefully for them rather large) trophy collection, likewise in what will appear to them to be a new screen.
So far I have a main game window with a grid layout and a game in it that works (Yay for me!). Now I want to add the above functionality.
How do I go about doing this? I don't think I want to go the multiple JFrame route as I only want one icon visible in the taskbar at a time (or would setting their visibility to false effect the icon too?) Do I instead make and destroy layouts or panels or something like that?
What are my options? How can I control what content is being displayed? Especially given my newbie skills?
A simple modal dialog such as a JDialog should work well here. The main GUI which will likely be a JFrame can be invisible when the dialog is called, and then set to visible (assuming that the log-on was successful) once the dialog completes. If the dialog is modal, you'll know exactly when the user has closed the dialog as the code will continue right after the line where you call setVisible(true) on the dialog. Note that the GUI held by a JDialog can be every bit as complex and rich as that held by a JFrame.
Another option is to use one GUI/JFrame but swap views (JPanels) in the main GUI via a CardLayout. This could work quite well and is easy to implement. Check out the CardLayout tutorial for more.
Oh, and welcome to stackoverflow.com!
Here is an example of a Login Dialog as #HovercraftFullOfEels suggested.
Username: stackoverflow Password: stackoverflow
import java.awt.*;
import java.awt.event.*;
import java.util.Arrays;
import javax.swing.*;
public class TestFrame extends JFrame {
private PassWordDialog passDialog;
public TestFrame() {
passDialog = new PassWordDialog(this, true);
passDialog.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new TestFrame();
frame.getContentPane().setBackground(Color.BLACK);
frame.setTitle("Logged In");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
}
});
}
}
class PassWordDialog extends JDialog {
private final JLabel jlblUsername = new JLabel("Username");
private final JLabel jlblPassword = new JLabel("Password");
private final JTextField jtfUsername = new JTextField(15);
private final JPasswordField jpfPassword = new JPasswordField();
private final JButton jbtOk = new JButton("Login");
private final JButton jbtCancel = new JButton("Cancel");
private final JLabel jlblStatus = new JLabel(" ");
public PassWordDialog() {
this(null, true);
}
public PassWordDialog(final JFrame parent, boolean modal) {
super(parent, modal);
JPanel p3 = new JPanel(new GridLayout(2, 1));
p3.add(jlblUsername);
p3.add(jlblPassword);
JPanel p4 = new JPanel(new GridLayout(2, 1));
p4.add(jtfUsername);
p4.add(jpfPassword);
JPanel p1 = new JPanel();
p1.add(p3);
p1.add(p4);
JPanel p2 = new JPanel();
p2.add(jbtOk);
p2.add(jbtCancel);
JPanel p5 = new JPanel(new BorderLayout());
p5.add(p2, BorderLayout.CENTER);
p5.add(jlblStatus, BorderLayout.NORTH);
jlblStatus.setForeground(Color.RED);
jlblStatus.setHorizontalAlignment(SwingConstants.CENTER);
setLayout(new BorderLayout());
add(p1, BorderLayout.CENTER);
add(p5, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(null);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
jbtOk.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (Arrays.equals("stackoverflow".toCharArray(), jpfPassword.getPassword())
&& "stackoverflow".equals(jtfUsername.getText())) {
parent.setVisible(true);
setVisible(false);
} else {
jlblStatus.setText("Invalid username or password");
}
}
});
jbtCancel.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
setVisible(false);
parent.dispose();
System.exit(0);
}
});
}
}
I suggest you insert the following code:
JFrame f = new JFrame();
JTextField text = new JTextField(15); //the 15 sets the size of the text field
JPanel p = new JPanel();
JButton b = new JButton("Login");
f.add(p); //so you can add more stuff to the JFrame
f.setSize(250,150);
f.setVisible(true);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Insert that when you want to add the stuff in. Next we will add all the stuff to the JPanel:
p.add(text);
p.add(b);
Now we add the ActionListeners to make the JButtons to work:
b.addActionListener(this);
public void actionPerforemed(ActionEvent e)
{
//Get the text of the JTextField
String TEXT = text.getText();
}
Don't forget to import the following if you haven't already:
import java.awt.event*;
import java.awt.*; //Just in case we need it
import java.x.swing.*;
I hope everything i said makes sense, because sometimes i don't (especially when I'm talking coding/Java) All the importing (if you didn't know) goes at the top of your code.
Instead of adding the game directly to JFrame, you can add your content to JPanel (let's call it GamePanel) and add this panel to the frame. Do the same thing for login screen: add all content to JPanel (LoginPanel) and add it to frame. When your game will start, you should do the following:
Add LoginPanel to frame
Get user input and load it's details
Add GamePanel and destroy LoginPanel (since it will be quite fast to re-create new one, so you don't need to keep it memory).