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.