"Real-world GUI applications go beyond buttons and text fields. Swing's advanced controls — radio buttons, checkboxes, and menus — give users richer ways to interact with your program."- Claude 2026
This page introduces more advanced Swing controls that let users make selections and navigate an application:
We'll build these up step by step into a complete Character Builder application where the player chooses a character class, selects abilities, and views a summary through the menu.
The class extends JFrame and the constructor handles all setup. The main method is the launcher — it hands off to the Event Dispatch Thread using SwingUtilities.invokeLater().
JFrame
main
SwingUtilities.invokeLater()
We'll use setLocationRelativeTo(null) to center the window on screen, and setLayout(null) to position components manually using exact pixel coordinates — giving us precise control over the layout.
setLocationRelativeTo(null)
setLayout(null)
import javax.swing.*; import javax.swing.border.*; import java.awt.*; import java.awt.event.*; public class CharacterBuilder extends JFrame { public CharacterBuilder() { setTitle("Character Builder"); setSize(500, 350); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); setLocationRelativeTo(null); // center on screen setLayout(null); // manual positioning // Components will be added here... setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new CharacterBuilder()); } }
setBounds(x, y, width, height)
Radio buttons are grouped using a ButtonGroup so only one can be selected at a time. The buttons themselves are added to a visible JPanel, while the ButtonGroup works invisibly behind the scenes to enforce mutual exclusivity.
ButtonGroup
JPanel
Checkboxes are independent — each has its own state and can be selected or deselected without affecting the others.
// Declare as instance variables private JRadioButton warriorBtn, mageBtn, archerBtn; private ButtonGroup classGroup; private JPanel classPanel; // Inside the constructor: // Panel with a titled border to hold the radio buttons classPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); classPanel.setBorder(BorderFactory.createTitledBorder("Choose Your Class")); classPanel.setBounds(20, 30, 440, 65); // Create radio buttons warriorBtn = new JRadioButton("Warrior"); mageBtn = new JRadioButton("Mage"); archerBtn = new JRadioButton("Archer"); warriorBtn.setSelected(true); // default selection // Group them so only one can be selected at a time classGroup = new ButtonGroup(); classGroup.add(warriorBtn); classGroup.add(mageBtn); classGroup.add(archerBtn); classPanel.add(warriorBtn); classPanel.add(mageBtn); classPanel.add(archerBtn); add(classPanel);
// Declare as instance variables private JCheckBox stealthBox, fireballBox, healBox, shieldBox; private JPanel abilityPanel; // Inside the constructor: // GridLayout arranges checkboxes in a 2x2 grid abilityPanel = new JPanel(new GridLayout(2, 2, 10, 5)); abilityPanel.setBorder(BorderFactory.createTitledBorder("Select Abilities")); abilityPanel.setBounds(20, 110, 440, 90); stealthBox = new JCheckBox("Stealth"); fireballBox = new JCheckBox("Fireball"); healBox = new JCheckBox("Heal"); shieldBox = new JCheckBox("Shield Block"); abilityPanel.add(stealthBox); abilityPanel.add(fireballBox); abilityPanel.add(healBox); abilityPanel.add(shieldBox); add(abilityPanel);
Before writing event handlers, it helps to plan what each control needs to do. In this application:
A shared updateStatus() method keeps things organized — rather than duplicating logic in every listener, each listener just calls this one method.
updateStatus()
// Declare as instance variables private JLabel statusLabel; private JButton buildButton; // Inside the constructor, after adding panels: statusLabel = new JLabel("Class: Warrior | Abilities: none"); statusLabel.setBounds(20, 215, 440, 25); add(statusLabel); buildButton = new JButton("Build Character"); buildButton.setBounds(170, 248, 160, 30); add(buildButton); // Attach the same listener to all radio buttons ActionListener classListener = e -> updateStatus(); warriorBtn.addActionListener(classListener); mageBtn.addActionListener(classListener); archerBtn.addActionListener(classListener); // Attach the same listener to all checkboxes ActionListener abilityListener = e -> updateStatus(); stealthBox.addActionListener(abilityListener); fireballBox.addActionListener(abilityListener); healBox.addActionListener(abilityListener); shieldBox.addActionListener(abilityListener); // Build button shows a summary dialog buildButton.addActionListener(e -> showSummary());
private void updateStatus() { String charClass = warriorBtn.isSelected() ? "Warrior" : mageBtn.isSelected() ? "Mage" : "Archer"; StringBuilder abilities = new StringBuilder(); if (stealthBox.isSelected()) abilities.append("Stealth "); if (fireballBox.isSelected()) abilities.append("Fireball "); if (healBox.isSelected()) abilities.append("Heal "); if (shieldBox.isSelected()) abilities.append("Shield Block "); String abilityStr = abilities.length() > 0 ? abilities.toString().trim() : "none"; statusLabel.setText("Class: " + charClass + " | Abilities: " + abilityStr); } private void showSummary() { String charClass = warriorBtn.isSelected() ? "Warrior" : mageBtn.isSelected() ? "Mage" : "Archer"; StringBuilder abilities = new StringBuilder(); if (stealthBox.isSelected()) abilities.append(" • Stealth\n"); if (fireballBox.isSelected()) abilities.append(" • Fireball\n"); if (healBox.isSelected()) abilities.append(" • Heal\n"); if (shieldBox.isSelected()) abilities.append(" • Shield Block\n"); String abilityStr = abilities.length() > 0 ? abilities.toString() : " • None selected\n"; JOptionPane.showMessageDialog(this, "=== Character Summary ===\n\n" + "Class: " + charClass + "\n\n" + "Abilities:\n" + abilityStr, "Your Character", JOptionPane.INFORMATION_MESSAGE ); }
A Swing menu bar is built from three components working together:
Once built, the menu bar is attached to the JFrame using setJMenuBar(). Each JMenuItem gets its own ActionListener just like a button.
setJMenuBar()
JMenuItem
ActionListener
// Declare as instance variables private JMenuBar menuBar; private JMenu fileMenu, helpMenu; private JMenuItem resetItem, exitItem, aboutItem; // Inside the constructor: menuBar = new JMenuBar(); // File menu fileMenu = new JMenu("File"); resetItem = new JMenuItem("Reset"); exitItem = new JMenuItem("Exit"); resetItem.addActionListener(e -> { classGroup.clearSelection(); warriorBtn.setSelected(true); stealthBox.setSelected(false); fireballBox.setSelected(false); healBox.setSelected(false); shieldBox.setSelected(false); updateStatus(); }); exitItem.addActionListener(e -> System.exit(0)); fileMenu.add(resetItem); fileMenu.addSeparator(); fileMenu.add(exitItem); // Help menu helpMenu = new JMenu("Help"); aboutItem = new JMenuItem("About"); aboutItem.addActionListener(e -> JOptionPane.showMessageDialog(this, "Character Builder v1.0", "About", JOptionPane.INFORMATION_MESSAGE) ); helpMenu.add(aboutItem); menuBar.add(fileMenu); menuBar.add(helpMenu); setJMenuBar(menuBar);
setVisible(true) should always be the last line of the constructor. This ensures every component is fully added and configured before the window appears on screen. Calling it too early can result in components not showing up or the window rendering before it's fully built.
setVisible(true)
setVisible(true); add(buildButton); // may not appear! add(statusLabel); // may not appear!
add(buildButton); add(statusLabel); setVisible(true); // everything is ready
CharacterBuilder
import javax.swing.*; import javax.swing.border.*; import java.awt.*; import java.awt.event.*; public class CharacterBuilder extends JFrame { private JRadioButton warriorBtn, mageBtn, archerBtn; private ButtonGroup classGroup; private JPanel classPanel; private JCheckBox stealthBox, fireballBox, healBox, shieldBox; private JPanel abilityPanel; private JLabel statusLabel; private JButton buildButton; private JMenuBar menuBar; private JMenu fileMenu, helpMenu; private JMenuItem resetItem, exitItem, aboutItem; public CharacterBuilder() { setTitle("Character Builder"); setSize(500, 310); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); setLocationRelativeTo(null); setLayout(null); // --- Class Panel (Radio Buttons) --- classPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 5)); classPanel.setBorder(BorderFactory.createTitledBorder("Choose Your Class")); classPanel.setBounds(20, 30, 440, 65); warriorBtn = new JRadioButton("Warrior"); mageBtn = new JRadioButton("Mage"); archerBtn = new JRadioButton("Archer"); warriorBtn.setSelected(true); classGroup = new ButtonGroup(); classGroup.add(warriorBtn); classGroup.add(mageBtn); classGroup.add(archerBtn); classPanel.add(warriorBtn); classPanel.add(mageBtn); classPanel.add(archerBtn); add(classPanel); // --- Ability Panel (CheckBoxes) --- abilityPanel = new JPanel(new GridLayout(2, 2, 10, 5)); abilityPanel.setBorder(BorderFactory.createTitledBorder("Select Abilities")); abilityPanel.setBounds(20, 110, 440, 90); stealthBox = new JCheckBox("Stealth"); fireballBox = new JCheckBox("Fireball"); healBox = new JCheckBox("Heal"); shieldBox = new JCheckBox("Shield Block"); abilityPanel.add(stealthBox); abilityPanel.add(fireballBox); abilityPanel.add(healBox); abilityPanel.add(shieldBox); add(abilityPanel); // --- Status Label --- statusLabel = new JLabel("Class: Warrior | Abilities: none"); statusLabel.setBounds(20, 215, 440, 25); add(statusLabel); // --- Build Button --- buildButton = new JButton("Build Character"); buildButton.setBounds(170, 248, 160, 30); add(buildButton); // --- Event Handlers --- ActionListener classListener = e -> updateStatus(); warriorBtn.addActionListener(classListener); mageBtn.addActionListener(classListener); archerBtn.addActionListener(classListener); ActionListener abilityListener = e -> updateStatus(); stealthBox.addActionListener(abilityListener); fireballBox.addActionListener(abilityListener); healBox.addActionListener(abilityListener); shieldBox.addActionListener(abilityListener); buildButton.addActionListener(e -> showSummary()); // --- Menu Bar --- menuBar = new JMenuBar(); fileMenu = new JMenu("File"); resetItem = new JMenuItem("Reset"); exitItem = new JMenuItem("Exit"); resetItem.addActionListener(e -> { classGroup.clearSelection(); warriorBtn.setSelected(true); stealthBox.setSelected(false); fireballBox.setSelected(false); healBox.setSelected(false); shieldBox.setSelected(false); updateStatus(); }); exitItem.addActionListener(e -> System.exit(0)); fileMenu.add(resetItem); fileMenu.addSeparator(); fileMenu.add(exitItem); helpMenu = new JMenu("Help"); aboutItem = new JMenuItem("About"); aboutItem.addActionListener(e -> JOptionPane.showMessageDialog(this, "Character Builder v1.0", "About", JOptionPane.INFORMATION_MESSAGE)); helpMenu.add(aboutItem); menuBar.add(fileMenu); menuBar.add(helpMenu); setJMenuBar(menuBar); setVisible(true); } private void updateStatus() { String charClass = warriorBtn.isSelected() ? "Warrior" : mageBtn.isSelected() ? "Mage" : "Archer"; StringBuilder abilities = new StringBuilder(); if (stealthBox.isSelected()) abilities.append("Stealth "); if (fireballBox.isSelected()) abilities.append("Fireball "); if (healBox.isSelected()) abilities.append("Heal "); if (shieldBox.isSelected()) abilities.append("Shield Block "); String abilityStr = abilities.length() > 0 ? abilities.toString().trim() : "none"; statusLabel.setText("Class: " + charClass + " | Abilities: " + abilityStr); } private void showSummary() { String charClass = warriorBtn.isSelected() ? "Warrior" : mageBtn.isSelected() ? "Mage" : "Archer"; StringBuilder abilities = new StringBuilder(); if (stealthBox.isSelected()) abilities.append(" • Stealth\n"); if (fireballBox.isSelected()) abilities.append(" • Fireball\n"); if (healBox.isSelected()) abilities.append(" • Heal\n"); if (shieldBox.isSelected()) abilities.append(" • Shield Block\n"); String abilityStr = abilities.length() > 0 ? abilities.toString() : " • None selected\n"; JOptionPane.showMessageDialog(this, "=== Character Summary ===\n\n" + "Class: " + charClass + "\n\n" + "Abilities:\n" + abilityStr, "Your Character", JOptionPane.INFORMATION_MESSAGE); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new CharacterBuilder()); } }
Save the file as CharacterBuilder.java, then compile and run from the terminal:
CharacterBuilder.java
javac CharacterBuilder.java
java CharacterBuilder