Uploaded by hehehaha

swingtutorial

advertisement
Creating a Swing-Based GUI for the Kakuro Game in
Java
This SWING programming tutorial will guide you through creating a Swing-based GUI program for the
demo game Kakuro.
Kakuro, often referred to as "Cross Sums" or "Kakro," is a logic-based puzzle game that combines
elements of crossword puzzles and Sudoku. Similar to Sudoku, the goal of Kakuro is to fill a grid with
numbers.
In a Kakuro puzzle, you are presented with a grid of blank cells, and your objective is to fill each cell with a
digit from 1 to 9. The puzzle also includes "clues" or "sums" provided as hints throughout the grid. These
sums represent the total value of the digits that must be placed within certain groups of adjacent cells, both
horizontally and vertically.
The challenge lies in finding the correct combination of numbers that meets the sum requirements without
repeating any digit within the group of cells and without violating the basic rules of Sudoku, which include
no repeating numbers in rows or columns.
You've been provided with a KakuroGame class that has all the required methods for making the game,
but many of them are just stubs with hard coded values. The KakuroGame class is not intended to be an
example of a complete game.
By the end of this tutorial you will have used all of the SWING elements needed for the final assignment in
this course. We'll start with a mosty empty UI class file file.
Step 1: Set up
Download the provided zip file and unzip so that you can work in the gradle project.
The class you will be editing is the KakuroUI.java class. The file should look like the listing below when
you open it.
package ui;
import javax.swing.JFrame;
import javax.swing.JButton;
import
import
import
import
import
javax.swing.JPanel;
javax.swing.JLabel;
javax.swing.JOptionPane;
javax.swing.BoxLayout;
javax.swing.JMenuBar;
import
import
import
import
import
javax.swing.JMenu;
javax.swing.JMenuItem;
java.awt.BorderLayout;
java.awt.GridLayout;
java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.GridLayout;
import kakuro.KakuroGame;
public class KakuroUI extends JFrame {
}
Step 2: Instance Variables
Instance variables in a Java class are used to store and manage data that needs to persist across multiple
methods or instances of the class. In the context of a graphical user interface (GUI) application, instance
variables are used to maintain the state and behavior of GUI components.
1. Storing Component References: Instance variables are often used to store references to GUI
components, such as buttons, labels, text fields, panels, and more. By storing these references in
instance variables, you can access and manipulate these components from multiple methods within
your class.
private JButton submitButton;
private JTextField usernameField;
private JLabel statusLabel;
In the example above, submitButton , usernameField , and statusLabel are instance variables
that store references to GUI components. Storing these references allows you to update their
properties or respond to user interactions in any instance method of the class.
2. Maintaining Component State: GUI components often have associated states that need to be
tracked. For instance, you might need to keep track of whether a button is enabled or disabled, the
text entered in a text field, or the selected item in a combo box. Instance variables are useful for
storing and managing such states.
private boolean isButtonEnabled;
private String userInputText;
private int selectedComboBoxIndex;
These instance variables can help you keep track of the state of various GUI components, making it
easier to implement specific behaviors or validations.
3. Custom Components: If you have custom GUI components or classes that extend Swing
components, you can use instance variables to store instances of these custom components. This
enables you to work with them throughout your GUI class.
private CustomPanel customPanel;
private MyCustomButton customButton;
Storing custom components as instance variables provides easy access to their methods and
properties.
Instance variables store references to components, track component states, manage event handling, and
allow you to work with custom components effectively. This organization and encapsulation of GUI-related
data and functionality contribute to cleaner and more maintainable GUI code.
Because this is your first GUI program, you may not easily be able to identify which components should be
local variables and which should be instance variables. For now, add the instance variables shown below.
They are sufficient for completing the class for this tutorial.
private JPanel gameContainer;
private JLabel messageLabel;
private JMenuBar menuBar;
private PositionAwareButton[][] buttons;
private KakuroGame game;
Step 3: Create the Constructor
Write a constructor that calls the superclass constructor and then a method used by the UI to initialize the
frame and set the basic properties, basicSetUp() . Then write that method.
Be sure to set the default close operation in your setup method. In Java Swing, setting the default close
operation for a JFrame using the setDefaultCloseOperation method allows you to specify the behavior of
the window when the user clicks the close button (typically the "X" button in the window's title bar).
Common options include JFrame.EXITONCLOSE, which terminates the application, and
JFrame.DISPOSEONCLOSE, which disposes of the frame without terminating the entire application.
Setting the default close operation for a JFrame or any top-level window in a Swing GUI application is
essential because it defines the behavior of the window when the user attempts to close it, typically by
clicking the "X" button in the window's title bar. This setting is crucial because it determines how the
application responds to user actions and ensures a graceful and expected termination or disposal of
resources.
User Expectations: Users expect that clicking the close button on a window will result in a specific
behavior. By setting the default close operation, you ensure that your application behaves in a manner
consistent with user expectations. Failing to do so might confuse or frustrate users.
Resource Management: Properly setting the default close operation allows your application to
manage system resources efficiently. For example, if you use JFrame.EXITONCLOSE, it ensures that
the application terminates gracefully, releasing any resources it acquired during its execution.
Custom Behavior: Depending on your application's requirements, you might want to implement
custom behavior when the user closes the window. Setting the default close operation allows you to
define and control this behavior. For example, you could prompt the user to confirm the closing action
or save unsaved data before exiting.
Consistency: Setting the default close operation provides consistency in how your application
responds to user actions across different platforms and operating systems. It ensures that your
application behaves the same way, regardless of the underlying platform.
Preventing Unintended Closures: In some cases, you might want to prevent the user from accidentally
closing the main window. By setting the default close operation to JFrame.DONOTHINGON_CLOSE,
you can intercept the closing action and implement custom logic, such as showing a confirmation
dialog.
Proper Cleanup: Depending on your application's logic, there may be cleanup tasks that need to be
performed before the application exits. Setting the default close operation allows you to hook into the
window-closing event and execute cleanup code, such as saving settings or closing open files.
public KakuroUI(String title) {
super();
basicSetUp(title);
}
private void basicSetUp(String title){
this.setTitle(title);
gameContainer = new JPanel();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout());
}
We also set the layout manager for the main frame of the application. Layout managers in Java Swing
control the arrangement and sizing of components within a container. The BorderLayout manager is one of
the most commonly used layout managers. BorderLayout divides the container into five regions: NORTH,
SOUTH, EAST, WEST, and CENTER. Components added to a container with BorderLayout are placed in
one of these regions, and they can expand or shrink based on the available space. This layout manager is
particularly useful for creating simple user interfaces with distinct regions for different components, such as
a menu bar at the top (NORTH), buttons on the right (EAST), and a main content area (CENTER).
Step 4: Compile and run
Add a main method to create an instance of KakuroUI and make it visible.
public static void main(String[] args) {
KakuroUI example = new KakuroUI("NxM Games");
example.setVisible(true);
}
You can use the supplied gradle file to compile the program. It will create a jar file in build/libs called
kakuro.jar . Run that jar file on a computer with a display (i.e. not your docker container) using java jar path/to/jar/kakuro.jar . We haven't added any components or information yet so the application
will likely show up as a tiny window with nothing in it.
Step 5: Initial GUI Components
Each component is created in a method that returns the finished component. The components are then
added to the layout manager of the main frame. This approach to GUI programming results in an easy to
read program that is modular and easy to maintain.
startupMessage
Create a method startupMessage to create and configure a JPanel for displaying a startup message.
You should use a JLabel to display the message.
Then write a second method for setting up the main game container. gameContainer is a member
variable that can be accessed from any instance method in this class. We'll add the panel for the startup
message to the game container.
private JPanel startupMessage() {
JPanel temp = new JPanel();
// Customize the message as desired
temp.add(new JLabel("Time to play some board games!"));
return temp;
}
public void setupGameContainer(){
gameContainer.add(startupMessage());
}
The add() method is a fundamental method provided by container classes, such as JFrame, JPanel,
JScrollPane, and others, to add graphical components, like buttons, labels, text fields, or custom widgets,
to the container's user interface.
Whenever we make a new component we must run the method to create the component and then the
component needs to be added to the panel we are building. Add these three lines of code to the
constructor. Then compile and run your application again.
setupGameContainer();
add(gameContainer, BorderLayout.CENTER);
pack().
The pack() method is used to automatically size a window (typically a JFrame) to fit its contained
components. It calculates the minimum size required to display all components properly and sets the
window's size accordingly. This method ensures that the window is neither too large nor too small to
accommodate its content.
makeButtonPanel:
Create a method makeButtonPanel to create and configure a JPanel to hold buttons. This panel will go in
the EAST section of the main panel's borderlayout. For this button panel, we'll use BoxLayout to stack the
buttons vertically.
private JPanel makeButtonPanel() {
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));
buttonPanel.add(makeKakuroButton());
buttonPanel.add(makeNewGameButton());
return buttonPanel;
}
Here is a breakdown of the steps in the method above:
JPanel buttonPanel = new JPanel(); : We create a new JPanel called buttonPanel to hold our
buttons.
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS)); : We set the
layout of buttonPanel to use BoxLayout with a vertical orientation (BoxLayout.Y_AXIS). This means
that buttons added to this panel will be stacked vertically.
buttonPanel.add(makeKakuroButton()) ; and buttonPanel.add(makeNewGameButton()); :
We add the "Play Kakuro" button and "New Puzzle" button (created by calling respective methods) to
the buttonPanel.
You will also need to write the methods to make the buttons. We'll be adding more code to these methods
later in the tutorial.
private JButton makeNewGameButton() {
JButton button = new JButton("New Puzzle");
return button;
}
private JButton makeKakuroButton() {
JButton button = new JButton("Kakuro");
return button;
}
Don't forget to call your makeButtonPanel() method and add it to the layout manager for the main
application frame. You can do that in a single line of code. add(makeButtonPanel(),
BorderLayout.EAST); Make sure the call to pack() is the last line of the constructor.
Compile and run your application again. It should look something like this:
Step 6: Make the Grid of Game Buttons
The PositionAwareButton is a custom Swing component that is a subclass of
javax.swing.JButton . It has been created specifically for the Kakuro game to represent individual cells
within the game's grid. This class is called "PositionAware" because it is designed to be aware of its
position within the grid, meaning it knows its coordinates (across and down) within the grid. Here's an
explanation of its key features:
1. Coordinates Storage: Each PositionAwareButton instance has two instance variables, xPos
and yPos , which store the button's position in the grid. These coordinates help identify which cell the
button represents, allowing for easy access to the game's data associated with that cell.
2. Getter and Setter Methods: The class provides getter and setter methods ( getAcross() ,
getDown() , setAcross() , and setDown() ) to access and modify these coordinates. These
methods allow you to retrieve the button's position or update it as needed.
3. Grid Representation: The PositionAwareButton class is particularly useful when you want to
create a grid of buttons, where each button corresponds to a specific cell in the game. By storing the
position information directly in the button, you can easily map user interactions to specific game logic
and data, such as placing numbers in the correct cell.
4. Custom Behavior: While it extends JButton and inherits its basic button functionality, you can
customize the behavior of PositionAwareButton further to suit the requirements of the game. For
example, you can add action listeners to handle user input and game logic based on the button's
position.
makeKakuroGrid is the method that creates a grid of buttons and on a JPanel. .
private JPanel makeKakuroGrid(int wide, int tall) {
JPanel panel = new JPanel();
buttons = new PositionAwareButton[tall][wide];
panel.setLayout(new GridLayout(wide, tall));
for (int y = 0; y < wide; y++) {
for (int x = 0; x < tall; x++) {
buttons[y][x] = new PositionAwareButton();
buttons[y][x].setAcross(x + 1);
buttons[y][x].setDown(y + 1);
panel.add(buttons[y][x]);
}
}
return panel;
}
The makeKakuroGrid method is responsible for creating and configuring the grid of
PositionAwareButton instances within the Kakuro game's user interface.
1. Grid Creation: Inside the makeKakuroGrid method, a JPanel named panel is created to serve
as a container for the grid of buttons. The buttons array is also initialized to hold
PositionAwareButton instances.
2. Grid Layout: The layout manager of the panel is set to GridLayout , which is configured to create
a grid with a specified number of rows and columns. This layout ensures that the buttons are evenly
distributed in rows and columns, resembling the game board.
3. Button Initialization: Within nested loops, PositionAwareButton instances are created for each
cell in the grid. These buttons are assigned their coordinates ( xPos and yPos ) based on their
positions in the loop. The buttons are then added to the panel .
4. Significance: The makeKakuroGrid method is crucial for the game because it sets up the visual
representation of the game board. By creating a grid of PositionAwareButton instances, each
button knows its position within the grid. This is essential for various game interactions, such as:
User Input: When a player clicks on a button, the button's position can be used to identify the
corresponding cell in the game board, allowing the player to input numbers or perform actions for
that cell.
Game Logic: The button grid serves as the user interface for interacting with the game's logic.
Actions like entering numbers, checking for valid moves, and updating the view are all linked to
these buttons.
Game State: The state of each cell in the game is represented by these buttons. When the game
state changes, the buttons' text can be updated to reflect the current state.
You'll need to edit the setupGameContainer method to add the KakuroGrid Jpanel to it.
gameContainer.add(makeKakuroGrid(3, 3)); Compile and run again and you should see a grid of
buttons in your application along with the other things.
Add a Menu Bar
A JMenuBar is a Swing component in Java that represents a menu bar typically found at the top of a
graphical user interface (GUI). It serves as a container for organizing and displaying various menu items,
such as File, Edit, View, and more, which users can interact with to perform actions or access submenus.
JMenuBar is an essential part of building intuitive and user-friendly GUIs in Java Swing applications,
allowing developers to create menus and submenus to organize and provide easy access to application
functionality.
Create a method makeMenu to create and configure a menu bar. You can add a submenu and menu item
as an example.
private void makeMenu() {
menuBar = new JMenuBar();
JMenu menu = new JMenu("A submenu");
// Customize the menu item label
JMenuItem item = new JMenuItem("An item (e.g., save)");
menu.add(item);
menuBar.add(menu);
}
This method is responsible for creating and configuring a menu bar with a submenu and menu item. Here
is a description of what is happening in each of the lines of code above.
menuBar = new JMenuBar(); : We create a JMenuBar component to hold our menu.
JMenu menu = new JMenu("A submenu"); : We create a submenu with the label "A submenu."
JMenuItem item = new JMenuItem("An item (e.g., save)"); : We create a menu item with
the label "An item (e.g., save)."
menu.add(item); : We add the menu item to the submenu.
menuBar.add(menu); : We add the submenu to the menu bar.
The last step is to make sure that the menubar is added to the application. Add these two lines to your
constructor before the call to pack();
makeMenu();
setJMenuBar(menuBar);
At this point your GUI application looks fairly complete.
However, it is missing any ability to handle user input or to interact with the game. We next need to add
event listeners and event handlers.
Step 7: Define Event Handler Methods
The instance variable game is used in event handler methods to interact with the game logic and update
the user interface based on the game's state. This variable is an instance of the KakuroGame class, which
represents the game's underlying logic and data.
Make sure that the game instance variable is initialized in the constructor for the UI.
game = new KakuroGame(3, 3);
At this point your constructor should look like this:
public KakuroUI(String title) {
super();
basicSetUp(title);
setupGameContainer();
add(gameContainer, BorderLayout.CENTER);
add(makeButtonPanel(), BorderLayout.EAST);
makeMenu();
setJMenuBar(menuBar);
game = new KakuroGame(3, 3);
pack();
}
Starting a Game
First configure the event listener for the newGame button.
Find the method that created the new game button and add an action listener to that method.
button.addActionListener(e -> newGame());
To add a lambda expression listener for a button, you should locate the code where the button is created
and add the addActionListener method. Remember that lambda expressions are a concise way to
define listeners and actions for GUI components in Java, making your code more readable and
maintainable.
This lambda listener is calling a method called newGame() so we must now write that method.
protected void newGame() {
game.newGame(); // Start a new game
updateView(); // Update the view to reflect the new game state
}
In the newGame method, the game instance variable is accessed to start a new game by calling the
newGame method on the KakuroGame object. This method initializes the game state, including the
game board, numbers, and any other necessary data.
After starting a new game, the updateView method is called. The updateView method iterates
over the game board and uses the game instance variable to retrieve the current state of each cell. It
then updates the text displayed on each button (representing cells) to reflect the current state of the
game. Add the updateView method to your code.
protected void updateView() {
for (int y = 0; y < game.getHeight(); y++) {
for (int x = 0; x < game.getWidth(); x++) {
// Updates the text on the buttons
buttons[y][x].setText(game.getCell(x + 1, y + 1));
}
}
}
The Kakuro button also needs a listener. Add the following line of code to the makeKakuroButton
method button.addActionListener(e -> kakuro());
Then you must write the kakuro method to be called. We will make it a simple pop up with some
information about the game.
private void kakuro(){
String message = "Kakuro is a logic-based puzzle game that combines
elements of crossword puzzles and Sudoku, requiring players to fill
a grid with numbers while adhering to specific rules, including no
repeating numbers in the same word and summing to a given total in
each clue.";
JOptionPane.showMessageDialog(null,message);
}
Getting input from the player
There are many different ways to get input from users from a GUI. For this demo example we've chosen to
use a JOptionPane . JOptionPane is a Swing class in Java that provides a simple way to display
standard dialog boxes and pop-up messages in graphical user interfaces. It allows developers to create
dialog boxes for tasks like displaying messages, prompting for user input, and confirming actions.
JOptionPane provides a user-friendly and consistent way to interact with users in Java applications,
making it a valuable component for handling dialogs and user feedback.
An option pane makes sense for the Kakuro game because the player must enter a number. If the game
could be played with only mouseclicks, the option pane would be unecessary.
The first step is to write the method that raises the option pane. This method is responsible for handling
user input for entering numbers.
The enterNumber method is called when a button representing a game cell is clicked. It should prompt
the user to input a value and then call the takeTurn method from the KakuroGame class and then and
the user interface with the entered value.
private void enterNumber(ActionEvent e) {
String num = JOptionPane.showInputDialog("Please input a value");
PositionAwareButton clicked = (PositionAwareButton) (e.getSource());
if (game.takeTurn(clicked.getAcross(), clicked.getDown(), num)) {
// Update the button text
clicked.setText(game.getCell(clicked.getAcross(), clicked.getDown()));
}
}
In the enterNumber method, the game instance variable is used to interact with the game logic
when the user enters a number.
When a user clicks on a button representing a game cell, the enterNumber method is called. It
prompts the user to input a value.
The game instance variable is used to call the takeTurn method on the KakuroGame object. This
method takes the user's input, the coordinates of the clicked cell ( clicked.getAcross() and
clicked.getDown() ), and attempts to update the game state with the entered value.
If the move is valid and the game state is updated, the game instance variable is again used to
retrieve the updated state of the clicked cell. The text displayed on the button representing that cell is
updated to show the new value.
The next step is to add the event listeners that will call the enterNumber method. To add a lambda
expression listener for the "Enter Number" button, edit the makeButtonGrid method.
private JPanel makeButtonGrid(int tall, int wide) {
JPanel panel = new JPanel();
buttons = new PositionAwareButton[tall][wide];
panel.setLayout(new GridLayout(wide, tall));
for (int y = 0; y < wide; y++) {
for (int x = 0; x < tall; x++) {
buttons[y][x] = new PositionAwareButton();
buttons[y][x].setAcross(x + 1);
buttons[y][x].setDown(y + 1);
// Add a lambda expression listener to call enterNumber method
buttons[y][x].addActionListener(e -> {
enterNumber(e);
checkGameState();
});
panel.add(buttons[y][x]);
}
}
return panel;
}
In the code above, we use a lambda expression as an action listener for each button created in the grid.
When a button is clicked, it will call the enterNumber method, passing the ActionEvent as an
argument.
This code shows how you can assign two actions to a single event. After the user has entered a number, it
is important to check the state of the game to see if the game is over. Thus, the checkGameState()
method is called after a turn is taken.
The checkGameState method checks if the game is done. If it is, it shows a confirmation dialog asking if
the player wants to play again or start a new game.
private void checkGameState() {
int selection = 0;
JOptionPane gameOver = new JOptionPane();
String congrats = "Well done! Do you want another puzzle?";
String button = "Play Again?";
if (game.isDone()) {
/*the next line isn't rendering in the pdf. Here it is in parts.
It should be one line of code.
selection = gameOver.showConfirmDialog(null,
congrats,
button,
JOptionPane.YES_NO_OPTION);
*/
selection = gameOver.showConfirmDialog(null, congrats,button, JOptionPane.YES_NO_OPTIO
if (selection == JOptionPane.NO_OPTION) {
System.exit(0);
} else {
newGame();
}
}
}
In the checkGameState method, the game instance variable is used to check the current state of the
game, specifically whether the game is completed.
The game instance variable's isDone method is called to determine if the game has been
completed. If the game is completed, a confirmation dialog is displayed to the user.
Using the Memu
The only menu item we made was for saving things so we need to create the listener and event handler for
saving. In this example, the saving functionality is not implemented, but we can still attach a listener to the
menu item and write an event handler as a place holder.
Add this line of code to the makeMenu() method.
item.addActionListener(e -> saveSomething());
Now we must write the saveSomething() method. The saveSomething method displays a dialog that
informs the user that this should prompt for save files. You can implement the actual save functionality
here if needed.
protected void saveSomething() {
JOptionPane.showMessageDialog(null, "This should prompt for save files");
}
In the saveSomething method, the game instance variable is not directly used for game state
manipulation. Instead, this method displays a dialog to inform the user about saving files. If saving
functionality were to be implemented, the game instance variable could potentially be used to save the
game state or other related data.
Conclusion
In this tutorial you have explored many aspects of Swing GUI programming.
1. Creating a Swing-Based GUI: The tutorial guides you through creating a Swing-based graphical
user interface (GUI) for the Kakuro game using Java.
2. Instance Variables: Instance variables are used to store GUI components that will be needed by
more than one method. Examples include game containers, buttons, labels, and the Kakuro game
instance.
3. Setting Default Close Operation: Setting the default close operation in the setup method is essential
to specify the behavior of the window when the user clicks the close button, ensuring a clean
application termination.
4. Layout Management: The tutorial explains how to set up layout managers, like BorderLayout and
BoxLayout, to control the arrangement and sizing of components within the GUI.
5. Button Grid: A custom PositionAwareButton class is introduced to represent buttons within a
grid. The makeKakuroGrid method is used to create and configure this button grid, ensuring each
button is aware of its position in the grid.
6. Event Handling: Event listeners and handlers are implemented to capture user interactions. Actions
like starting a new game, displaying information, and entering numbers into cells are covered.
7. Menu Bar: A JMenuBar is added to the GUI, demonstrating how to create and configure menu bars,
submenus, and menu items. It also shows how to attach action listeners to menu items.
8. Checking Game State: The checkGameState method checks if the game is completed. It displays a
confirmation dialog, allowing the player to play again or exit the game.
9. Modularity and Separation of Concerns: The tutorial emphasizes the importance of clean code
architecture, modularity, and separation of concerns, making the codebase more maintainable.
10. User Experience: The GUI provides user-friendly feedback, enhancing the player's experience with
informative messages and options.
By following the tutorial, you've learned how to create a Java Swing-based GUI for the Kakuro game. You
are now equipped with the knowledge to build more complex and interactive Swing applications while
maintaining clean code architecture and modularity.
It may be worthwhile to spend some time inspecting the architecture of the KakuroGame class as well. It
has many similarities to the MancalaGame structure you have been asked to create for Assignment Three.
Download