← Back to Lecture
Practical Work GUI

Library Management System - Desktop Application

Build a complete Swing GUI for the library system with forms, tables, and dialogs

Duration 4 hours
Difficulty Intermediate
Session GUI - Java Swing

Objectives

By the end of this practical work, you will be able to:

  • Create a main application window using JFrame with proper OOP design
  • Design forms using layout managers (BorderLayout, GridLayout, FlowLayout)
  • Display data in a JTable with custom TableModel
  • Implement live search filtering on table data
  • Handle user events with ActionListener and lambda expressions
  • Use JOptionPane for dialogs and user confirmations
  • Apply the MVC pattern to separate GUI from business logic

Prerequisites

  • Completed Practical Works 3-8 (Library System core)
  • MediaItem hierarchy (Book, DVD, Magazine)
  • MediaCatalog, MemberService, LoanService classes
  • Understanding of event-driven programming concepts

Project Structure

Add GUI package to your existing library system:

library-system
src
com.library
model
MediaItem.java
Book.java
Member.java
Loan.java
service
MediaCatalog.java
MemberService.java
LoanService.java
gui
LibraryFrame.java
MediaTableModel.java
MediaFormPanel.java
SearchPanel.java
LoanDialog.java
MemberDialog.java
app
LibraryGuiApp.java

Part 1: Main Application Frame

Step 1.1: Create the Main Window

Create the main application frame extending JFrame:

package com.library.gui;

import com.library.service.*;
import javax.swing.*;
import java.awt.*;

public class LibraryFrame extends JFrame {  // (#1:Extend JFrame for OOP design)
    // Services (business logic)
    private final MediaCatalog catalog;
    private final MemberService memberService;
    private final LoanService loanService;

    // GUI components
    private MediaTableModel tableModel;
    private JTable mediaTable;
    private SearchPanel searchPanel;
    private MediaFormPanel formPanel;

    public LibraryFrame(MediaCatalog catalog, MemberService memberService,
                        LoanService loanService) {
        this.catalog = catalog;
        this.memberService = memberService;
        this.loanService = loanService;

        initializeFrame();
        initializeComponents();
        layoutComponents();
    }

    private void initializeFrame() {
        setTitle("Library Management System");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  // (#2:Proper close behavior)
        setSize(1000, 700);
        setMinimumSize(new Dimension(800, 600));
        setLocationRelativeTo(null);  // (#3:Center on screen)
    }

    private void initializeComponents() {
        // Table model and table
        tableModel = new MediaTableModel(catalog);
        mediaTable = new JTable(tableModel);
        mediaTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        mediaTable.setRowHeight(25);

        // Search panel
        searchPanel = new SearchPanel(this::filterTable);

        // Form panel
        formPanel = new MediaFormPanel(this::addMedia, this::clearForm);
    }

    private void layoutComponents() {
        // Use BorderLayout for main frame  (#4:BorderLayout for main structure)
        setLayout(new BorderLayout(10, 10));

        // Create menu bar
        setJMenuBar(createMenuBar());

        // North: Search panel
        add(searchPanel, BorderLayout.NORTH);

        // Center: Table in scroll pane
        JScrollPane scrollPane = new JScrollPane(mediaTable);
        scrollPane.setBorder(BorderFactory.createTitledBorder("Media Catalog"));
        add(scrollPane, BorderLayout.CENTER);

        // East: Form panel
        add(formPanel, BorderLayout.EAST);

        // South: Action buttons
        add(createButtonPanel(), BorderLayout.SOUTH);
    }

    private JMenuBar createMenuBar() {
        JMenuBar menuBar = new JMenuBar();

        // File menu
        JMenu fileMenu = new JMenu("File");
        fileMenu.add(createMenuItem("Save", e -> saveData()));
        fileMenu.add(createMenuItem("Load", e -> loadData()));
        fileMenu.addSeparator();
        fileMenu.add(createMenuItem("Exit", e -> exitApplication()));
        menuBar.add(fileMenu);

        // Actions menu
        JMenu actionsMenu = new JMenu("Actions");
        actionsMenu.add(createMenuItem("Borrow Item", e -> showBorrowDialog()));
        actionsMenu.add(createMenuItem("Return Item", e -> showReturnDialog()));
        actionsMenu.addSeparator();
        actionsMenu.add(createMenuItem("Add Member", e -> showMemberDialog()));
        menuBar.add(actionsMenu);

        // Help menu
        JMenu helpMenu = new JMenu("Help");
        helpMenu.add(createMenuItem("About", e -> showAbout()));
        menuBar.add(helpMenu);

        return menuBar;
    }

    private JMenuItem createMenuItem(String text, java.awt.event.ActionListener listener) {
        JMenuItem item = new JMenuItem(text);
        item.addActionListener(listener);
        return item;
    }

    private JPanel createButtonPanel() {
        JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));

        JButton borrowBtn = new JButton("Borrow Selected");
        JButton returnBtn = new JButton("Return Selected");
        JButton deleteBtn = new JButton("Delete Selected");
        JButton refreshBtn = new JButton("Refresh");

        borrowBtn.addActionListener(e -> showBorrowDialog());
        returnBtn.addActionListener(e -> showReturnDialog());
        deleteBtn.addActionListener(e -> deleteSelected());
        refreshBtn.addActionListener(e -> refreshTable());

        panel.add(borrowBtn);
        panel.add(returnBtn);
        panel.add(deleteBtn);
        panel.add(refreshBtn);

        return panel;
    }

    // Event handlers - we'll implement these in the following steps
    private void filterTable(String searchText) { /* Step 3 */ }
    private void addMedia(MediaFormPanel.MediaData data) { /* Step 2 */ }
    private void clearForm() { formPanel.clearFields(); }
    private void showBorrowDialog() { /* Step 4 */ }
    private void showReturnDialog() { /* Step 4 */ }
    private void showMemberDialog() { /* Step 5 */ }
    private void deleteSelected() { /* Step 2 */ }
    private void refreshTable() { tableModel.fireTableDataChanged(); }
    private void saveData() { /* Bonus */ }
    private void loadData() { /* Bonus */ }
    private void exitApplication() {
        int confirm = JOptionPane.showConfirmDialog(this,
            "Are you sure you want to exit?", "Confirm Exit",
            JOptionPane.YES_NO_OPTION);
        if (confirm == JOptionPane.YES_OPTION) {
            System.exit(0);
        }
    }
    private void showAbout() {
        JOptionPane.showMessageDialog(this,
            "Library Management System\nVersion 1.0\nBuilt with Java Swing",
            "About", JOptionPane.INFORMATION_MESSAGE);
    }
}
Code Annotations:
  1. Extend JFrame: OOP approach for complex GUI applications
  2. EXIT_ON_CLOSE: Terminates application when window closes
  3. setLocationRelativeTo(null): Centers window on screen
  4. BorderLayout: Divides frame into 5 regions (NORTH, SOUTH, EAST, WEST, CENTER)

Part 2: Custom TableModel for JTable

Step 2.1: Create MediaTableModel

Implement AbstractTableModel to display media items in a table:

package com.library.gui;

import com.library.model.*;
import com.library.service.MediaCatalog;
import javax.swing.table.AbstractTableModel;
import java.util.ArrayList;
import java.util.List;

public class MediaTableModel extends AbstractTableModel {  // (#1:Extend AbstractTableModel)
    private static final String[] COLUMN_NAMES = {
        "ID", "Title", "Type", "Status", "Year", "Details"
    };

    private final MediaCatalog catalog;
    private List<MediaItem> filteredItems;  // (#2:Filtered view of data)

    public MediaTableModel(MediaCatalog catalog) {
        this.catalog = catalog;
        this.filteredItems = new ArrayList<>(catalog.getAllItems());
    }

    @Override
    public int getRowCount() {
        return filteredItems.size();
    }

    @Override
    public int getColumnCount() {
        return COLUMN_NAMES.length;
    }

    @Override
    public String getColumnName(int column) {  // (#3:Custom column headers)
        return COLUMN_NAMES[column];
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        MediaItem item = filteredItems.get(rowIndex);

        return switch (columnIndex) {  // (#4:Switch expression for cell values)
            case 0 -> item.getId();
            case 1 -> item.getTitle();
            case 2 -> item.getClass().getSimpleName();  // "Book", "DVD", etc.
            case 3 -> item.getStatus().name();
            case 4 -> item.getYear();
            case 5 -> getDetails(item);
            default -> "";
        };
    }

    private String getDetails(MediaItem item) {
        if (item instanceof Book book) {  // (#5:Pattern matching)
            return "Author: " + book.getAuthor();
        } else if (item instanceof DVD dvd) {
            return "Director: " + dvd.getDirector();
        } else if (item instanceof Magazine mag) {
            return "Issue: " + mag.getIssueNumber();
        }
        return "";
    }

    // Filter items based on search text
    public void filter(String searchText) {
        if (searchText == null || searchText.trim().isEmpty()) {
            filteredItems = new ArrayList<>(catalog.getAllItems());
        } else {
            String lowerSearch = searchText.toLowerCase();
            filteredItems = catalog.getAllItems().stream()
                .filter(item ->
                    item.getTitle().toLowerCase().contains(lowerSearch) ||
                    item.getId().toLowerCase().contains(lowerSearch) ||
                    getDetails(item).toLowerCase().contains(lowerSearch))
                .toList();
        }
        fireTableDataChanged();  // (#6:Notify table of data change)
    }

    // Get item at specific row
    public MediaItem getItemAt(int row) {
        if (row >= 0 && row < filteredItems.size()) {
            return filteredItems.get(row);
        }
        return null;
    }

    // Refresh data from catalog
    public void refresh() {
        filteredItems = new ArrayList<>(catalog.getAllItems());
        fireTableDataChanged();
    }
}
Code Annotations:
  1. AbstractTableModel: Base class for custom table models
  2. Filtered view: Allows search without modifying original data
  3. getColumnName: Provides header text for each column
  4. Switch expression: Clean way to return cell values by column
  5. Pattern matching: Type-safe access to subclass properties
  6. fireTableDataChanged: Tells JTable to repaint with new data

Step 2.2: Create Media Form Panel

Create a form for adding new media items:

package com.library.gui;

import com.library.model.MediaStatus;
import javax.swing.*;
import java.awt.*;
import java.util.function.Consumer;

public class MediaFormPanel extends JPanel {
    private JTextField idField;
    private JTextField titleField;
    private JComboBox<String> typeCombo;
    private JTextField yearField;
    private JTextField detailField;  // Author/Director/Issue
    private JLabel detailLabel;

    private final Consumer<MediaData> onAdd;
    private final Runnable onClear;

    // Data transfer object for form data
    public record MediaData(String id, String title, String type,
                            int year, String detail) {}  // (#1:Record for form data)

    public MediaFormPanel(Consumer<MediaData> onAdd, Runnable onClear) {
        this.onAdd = onAdd;
        this.onClear = onClear;

        setBorder(BorderFactory.createTitledBorder("Add New Media"));
        setLayout(new GridBagLayout());  // (#2:GridBagLayout for forms)
        initializeComponents();
    }

    private void initializeComponents() {
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.insets = new Insets(5, 5, 5, 5);
        gbc.anchor = GridBagConstraints.WEST;

        int row = 0;

        // ID field
        gbc.gridx = 0; gbc.gridy = row;
        add(new JLabel("ID:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
        idField = new JTextField(15);
        add(idField, gbc);

        // Title field
        row++;
        gbc.gridx = 0; gbc.gridy = row; gbc.fill = GridBagConstraints.NONE;
        add(new JLabel("Title:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
        titleField = new JTextField(15);
        add(titleField, gbc);

        // Type combo
        row++;
        gbc.gridx = 0; gbc.gridy = row; gbc.fill = GridBagConstraints.NONE;
        add(new JLabel("Type:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
        typeCombo = new JComboBox<>(new String[]{"Book", "DVD", "Magazine"});
        typeCombo.addActionListener(e -> updateDetailLabel());  // (#3:Update label on selection)
        add(typeCombo, gbc);

        // Year field
        row++;
        gbc.gridx = 0; gbc.gridy = row; gbc.fill = GridBagConstraints.NONE;
        add(new JLabel("Year:"), gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
        yearField = new JTextField(15);
        add(yearField, gbc);

        // Detail field (dynamic label)
        row++;
        gbc.gridx = 0; gbc.gridy = row; gbc.fill = GridBagConstraints.NONE;
        detailLabel = new JLabel("Author:");
        add(detailLabel, gbc);
        gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL;
        detailField = new JTextField(15);
        add(detailField, gbc);

        // Buttons
        row++;
        gbc.gridx = 0; gbc.gridy = row; gbc.gridwidth = 2;
        gbc.fill = GridBagConstraints.NONE;
        gbc.anchor = GridBagConstraints.CENTER;

        JPanel buttonPanel = new JPanel(new FlowLayout());
        JButton addButton = new JButton("Add");
        JButton clearButton = new JButton("Clear");

        addButton.addActionListener(e -> submitForm());
        clearButton.addActionListener(e -> clearFields());

        buttonPanel.add(addButton);
        buttonPanel.add(clearButton);
        add(buttonPanel, gbc);
    }

    private void updateDetailLabel() {
        String type = (String) typeCombo.getSelectedItem();
        detailLabel.setText(switch (type) {
            case "Book" -> "Author:";
            case "DVD" -> "Director:";
            case "Magazine" -> "Issue #:";
            default -> "Detail:";
        });
    }

    private void submitForm() {
        // Validate fields
        if (idField.getText().trim().isEmpty() ||
            titleField.getText().trim().isEmpty()) {
            JOptionPane.showMessageDialog(this,
                "ID and Title are required!",
                "Validation Error", JOptionPane.ERROR_MESSAGE);
            return;
        }

        int year;
        try {
            year = Integer.parseInt(yearField.getText().trim());
        } catch (NumberFormatException e) {
            JOptionPane.showMessageDialog(this,
                "Year must be a valid number!",
                "Validation Error", JOptionPane.ERROR_MESSAGE);
            return;
        }

        // Create data object and call callback
        MediaData data = new MediaData(
            idField.getText().trim(),
            titleField.getText().trim(),
            (String) typeCombo.getSelectedItem(),
            year,
            detailField.getText().trim()
        );

        onAdd.accept(data);  // (#4:Callback to parent)
    }

    public void clearFields() {
        idField.setText("");
        titleField.setText("");
        yearField.setText("");
        detailField.setText("");
        typeCombo.setSelectedIndex(0);
        onClear.run();
    }
}
Code Annotations:
  1. Record for data: Immutable transfer object for form data
  2. GridBagLayout: Most flexible layout for complex forms
  3. Dynamic label: Updates based on selected type
  4. Callback pattern: Notifies parent without tight coupling

Step 2.3: Implement Add Media in LibraryFrame

Add the handler method to LibraryFrame:

// In LibraryFrame class
private void addMedia(MediaFormPanel.MediaData data) {
    try {
        MediaItem item = switch (data.type()) {  // (#1:Create correct subtype)
            case "Book" -> new Book(data.id(), data.title(), data.year(), data.detail());
            case "DVD" -> new DVD(data.id(), data.title(), data.year(), data.detail());
            case "Magazine" -> new Magazine(data.id(), data.title(), data.year(),
                                            Integer.parseInt(data.detail()));
            default -> throw new IllegalArgumentException("Unknown type: " + data.type());
        };

        catalog.addItem(item);
        tableModel.refresh();  // (#2:Refresh table after add)

        JOptionPane.showMessageDialog(this,
            "Media item added successfully!",
            "Success", JOptionPane.INFORMATION_MESSAGE);

        formPanel.clearFields();

    } catch (IllegalArgumentException e) {
        JOptionPane.showMessageDialog(this,
            "Error adding item: " + e.getMessage(),
            "Error", JOptionPane.ERROR_MESSAGE);
    }
}

private void deleteSelected() {
    int selectedRow = mediaTable.getSelectedRow();
    if (selectedRow < 0) {
        JOptionPane.showMessageDialog(this,
            "Please select an item to delete.",
            "No Selection", JOptionPane.WARNING_MESSAGE);
        return;
    }

    MediaItem item = tableModel.getItemAt(selectedRow);
    int confirm = JOptionPane.showConfirmDialog(this,
        "Delete '" + item.getTitle() + "'?",
        "Confirm Delete", JOptionPane.YES_NO_OPTION);

    if (confirm == JOptionPane.YES_OPTION) {
        catalog.removeItem(item.getId());
        tableModel.refresh();
    }
}
Code Annotations:
  1. Switch expression: Creates correct MediaItem subtype based on form selection
  2. Refresh table: Updates display after data change

Part 3: Search Panel with Live Filtering

Step 3.1: Create SearchPanel Component

package com.library.gui;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.util.function.Consumer;

public class SearchPanel extends JPanel {
    private JTextField searchField;
    private JComboBox<String> filterType;
    private final Consumer<String> onSearch;

    public SearchPanel(Consumer<String> onSearch) {
        this.onSearch = onSearch;

        setLayout(new FlowLayout(FlowLayout.LEFT, 10, 5));
        setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
        initializeComponents();
    }

    private void initializeComponents() {
        add(new JLabel("Search:"));

        searchField = new JTextField(25);
        searchField.getDocument().addDocumentListener(new DocumentListener() {  // (#1:Live search)
            @Override
            public void insertUpdate(DocumentEvent e) { triggerSearch(); }
            @Override
            public void removeUpdate(DocumentEvent e) { triggerSearch(); }
            @Override
            public void changedUpdate(DocumentEvent e) { triggerSearch(); }
        });
        add(searchField);

        filterType = new JComboBox<>(new String[]{
            "All Fields", "Title Only", "ID Only"
        });
        filterType.addActionListener(e -> triggerSearch());
        add(filterType);

        JButton clearButton = new JButton("Clear");
        clearButton.addActionListener(e -> {
            searchField.setText("");
            filterType.setSelectedIndex(0);
        });
        add(clearButton);
    }

    private void triggerSearch() {
        String searchText = searchField.getText();
        onSearch.accept(searchText);  // (#2:Notify parent of search change)
    }

    public String getSearchText() {
        return searchField.getText();
    }

    public String getFilterType() {
        return (String) filterType.getSelectedItem();
    }
}
Code Annotations:
  1. DocumentListener: Triggers search on every keystroke for live filtering
  2. Consumer callback: Decouples search panel from table model

Step 3.2: Implement Filter in LibraryFrame

// In LibraryFrame class
private void filterTable(String searchText) {
    tableModel.filter(searchText);

    // Update status bar or label with result count
    int count = tableModel.getRowCount();
    setTitle("Library Management System - " + count + " items");  // (#1:Show count in title)
}
Tip: The search updates in real-time as you type. For better performance with large datasets, consider adding a small delay (debouncing) using javax.swing.Timer.

Part 4: Loan Dialog for Borrow/Return

Step 4.1: Create LoanDialog

package com.library.gui;

import com.library.model.*;
import com.library.service.*;
import javax.swing.*;
import java.awt.*;
import java.util.List;

public class LoanDialog extends JDialog {  // (#1:JDialog for modal windows)
    private final MediaItem mediaItem;
    private final MemberService memberService;
    private final LoanService loanService;

    private JComboBox<Member> memberCombo;
    private boolean confirmed = false;

    public LoanDialog(Frame parent, MediaItem mediaItem,
                      MemberService memberService, LoanService loanService) {
        super(parent, "Borrow Item", true);  // (#2:Modal dialog)
        this.mediaItem = mediaItem;
        this.memberService = memberService;
        this.loanService = loanService;

        initializeDialog();
    }

    private void initializeDialog() {
        setSize(400, 200);
        setLocationRelativeTo(getParent());
        setLayout(new BorderLayout(10, 10));

        // Info panel
        JPanel infoPanel = new JPanel(new GridLayout(3, 2, 5, 5));
        infoPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

        infoPanel.add(new JLabel("Item:"));
        infoPanel.add(new JLabel(mediaItem.getTitle()));

        infoPanel.add(new JLabel("Type:"));
        infoPanel.add(new JLabel(mediaItem.getClass().getSimpleName()));

        infoPanel.add(new JLabel("Member:"));
        memberCombo = new JComboBox<>();
        loadMembers();
        infoPanel.add(memberCombo);

        add(infoPanel, BorderLayout.CENTER);

        // Button panel
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        JButton borrowBtn = new JButton("Confirm Borrow");
        JButton cancelBtn = new JButton("Cancel");

        borrowBtn.addActionListener(e -> confirmBorrow());
        cancelBtn.addActionListener(e -> dispose());

        buttonPanel.add(borrowBtn);
        buttonPanel.add(cancelBtn);
        add(buttonPanel, BorderLayout.SOUTH);
    }

    private void loadMembers() {
        List<Member> members = memberService.getAllMembers();
        for (Member member : members) {
            memberCombo.addItem(member);  // (#3:Add Member objects directly)
        }
    }

    private void confirmBorrow() {
        Member selectedMember = (Member) memberCombo.getSelectedItem();
        if (selectedMember == null) {
            JOptionPane.showMessageDialog(this,
                "Please select a member.",
                "Error", JOptionPane.ERROR_MESSAGE);
            return;
        }

        try {
            loanService.borrowItem(mediaItem, selectedMember);  // (#4:Use service layer)
            confirmed = true;
            dispose();
        } catch (Exception e) {
            JOptionPane.showMessageDialog(this,
                "Error: " + e.getMessage(),
                "Borrow Failed", JOptionPane.ERROR_MESSAGE);
        }
    }

    public boolean isConfirmed() {
        return confirmed;
    }

    // Static factory method for showing dialog
    public static boolean showBorrowDialog(Frame parent, MediaItem item,
                                            MemberService memberService,
                                            LoanService loanService) {
        LoanDialog dialog = new LoanDialog(parent, item, memberService, loanService);
        dialog.setVisible(true);  // (#5:Blocks until dialog closes)
        return dialog.isConfirmed();
    }
}
Code Annotations:
  1. JDialog: Used for popup windows and modal dialogs
  2. Modal (true): Blocks input to parent until closed
  3. Add objects to JComboBox: Uses toString() for display
  4. Service layer: Business logic stays in services, not GUI
  5. setVisible blocks: Modal dialogs pause execution until closed

Step 4.2: Implement Dialog Methods in LibraryFrame

// In LibraryFrame class
private void showBorrowDialog() {
    int selectedRow = mediaTable.getSelectedRow();
    if (selectedRow < 0) {
        JOptionPane.showMessageDialog(this,
            "Please select an item to borrow.",
            "No Selection", JOptionPane.WARNING_MESSAGE);
        return;
    }

    MediaItem item = tableModel.getItemAt(selectedRow);

    // Check if available
    if (item.getStatus() != MediaStatus.AVAILABLE) {
        JOptionPane.showMessageDialog(this,
            "This item is not available for borrowing.",
            "Not Available", JOptionPane.WARNING_MESSAGE);
        return;
    }

    // Show borrow dialog
    boolean success = LoanDialog.showBorrowDialog(
        this, item, memberService, loanService);

    if (success) {
        tableModel.refresh();
        JOptionPane.showMessageDialog(this,
            "Item borrowed successfully!",
            "Success", JOptionPane.INFORMATION_MESSAGE);
    }
}

private void showReturnDialog() {
    int selectedRow = mediaTable.getSelectedRow();
    if (selectedRow < 0) {
        JOptionPane.showMessageDialog(this,
            "Please select an item to return.",
            "No Selection", JOptionPane.WARNING_MESSAGE);
        return;
    }

    MediaItem item = tableModel.getItemAt(selectedRow);

    // Check if borrowed
    if (item.getStatus() != MediaStatus.BORROWED) {
        JOptionPane.showMessageDialog(this,
            "This item is not currently borrowed.",
            "Not Borrowed", JOptionPane.WARNING_MESSAGE);
        return;
    }

    // Confirm return
    int confirm = JOptionPane.showConfirmDialog(this,
        "Return '" + item.getTitle() + "'?",
        "Confirm Return", JOptionPane.YES_NO_OPTION);

    if (confirm == JOptionPane.YES_OPTION) {
        try {
            loanService.returnItem(item);
            tableModel.refresh();
            JOptionPane.showMessageDialog(this,
                "Item returned successfully!",
                "Success", JOptionPane.INFORMATION_MESSAGE);
        } catch (Exception e) {
            JOptionPane.showMessageDialog(this,
                "Error: " + e.getMessage(),
                "Return Failed", JOptionPane.ERROR_MESSAGE);
        }
    }
}

Part 5: Member Management Dialog

Step 5.1: Create MemberDialog

package com.library.gui;

import com.library.model.Member;
import com.library.service.MemberService;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;

public class MemberDialog extends JDialog {
    private final MemberService memberService;
    private DefaultTableModel tableModel;
    private JTable memberTable;

    // Form fields
    private JTextField idField;
    private JTextField nameField;
    private JTextField emailField;

    public MemberDialog(Frame parent, MemberService memberService) {
        super(parent, "Manage Members", true);
        this.memberService = memberService;
        initializeDialog();
        loadMembers();
    }

    private void initializeDialog() {
        setSize(600, 400);
        setLocationRelativeTo(getParent());
        setLayout(new BorderLayout(10, 10));

        // Table panel
        tableModel = new DefaultTableModel(
            new String[]{"ID", "Name", "Email", "Active Loans"}, 0) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;  // (#1:Read-only table)
            }
        };
        memberTable = new JTable(tableModel);
        JScrollPane scrollPane = new JScrollPane(memberTable);
        scrollPane.setBorder(BorderFactory.createTitledBorder("Members"));
        add(scrollPane, BorderLayout.CENTER);

        // Form panel
        JPanel formPanel = new JPanel(new GridLayout(4, 2, 5, 5));
        formPanel.setBorder(BorderFactory.createTitledBorder("Add New Member"));

        formPanel.add(new JLabel("ID:"));
        idField = new JTextField();
        formPanel.add(idField);

        formPanel.add(new JLabel("Name:"));
        nameField = new JTextField();
        formPanel.add(nameField);

        formPanel.add(new JLabel("Email:"));
        emailField = new JTextField();
        formPanel.add(emailField);

        JButton addButton = new JButton("Add Member");
        addButton.addActionListener(e -> addMember());
        formPanel.add(addButton);

        JButton deleteButton = new JButton("Delete Selected");
        deleteButton.addActionListener(e -> deleteSelected());
        formPanel.add(deleteButton);

        add(formPanel, BorderLayout.SOUTH);

        // Close button
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        JButton closeButton = new JButton("Close");
        closeButton.addActionListener(e -> dispose());
        buttonPanel.add(closeButton);
        add(buttonPanel, BorderLayout.NORTH);
    }

    private void loadMembers() {
        tableModel.setRowCount(0);  // (#2:Clear existing rows)
        for (Member member : memberService.getAllMembers()) {
            tableModel.addRow(new Object[]{
                member.getId(),
                member.getName(),
                member.getEmail(),
                member.getActiveLoans().size()
            });
        }
    }

    private void addMember() {
        String id = idField.getText().trim();
        String name = nameField.getText().trim();
        String email = emailField.getText().trim();

        if (id.isEmpty() || name.isEmpty()) {
            JOptionPane.showMessageDialog(this,
                "ID and Name are required!",
                "Validation Error", JOptionPane.ERROR_MESSAGE);
            return;
        }

        try {
            Member member = new Member(id, name, email);
            memberService.addMember(member);
            loadMembers();

            // Clear form
            idField.setText("");
            nameField.setText("");
            emailField.setText("");

            JOptionPane.showMessageDialog(this,
                "Member added successfully!",
                "Success", JOptionPane.INFORMATION_MESSAGE);
        } catch (Exception e) {
            JOptionPane.showMessageDialog(this,
                "Error: " + e.getMessage(),
                "Error", JOptionPane.ERROR_MESSAGE);
        }
    }

    private void deleteSelected() {
        int selectedRow = memberTable.getSelectedRow();
        if (selectedRow < 0) {
            JOptionPane.showMessageDialog(this,
                "Please select a member to delete.",
                "No Selection", JOptionPane.WARNING_MESSAGE);
            return;
        }

        String memberId = (String) tableModel.getValueAt(selectedRow, 0);
        int confirm = JOptionPane.showConfirmDialog(this,
            "Delete this member?",
            "Confirm Delete", JOptionPane.YES_NO_OPTION);

        if (confirm == JOptionPane.YES_OPTION) {
            memberService.removeMember(memberId);
            loadMembers();
        }
    }

    // Static factory method
    public static void showMemberDialog(Frame parent, MemberService memberService) {
        MemberDialog dialog = new MemberDialog(parent, memberService);
        dialog.setVisible(true);
    }
}
Code Annotations:
  1. Read-only table: Override isCellEditable to prevent editing
  2. setRowCount(0): Clears all rows before reloading data

Step 5.2: Connect Member Dialog to LibraryFrame

// In LibraryFrame class
private void showMemberDialog() {
    MemberDialog.showMemberDialog(this, memberService);
}

Part 6: Application Entry Point

Step 6.1: Create the Main Application Class

package com.library.app;

import com.library.gui.LibraryFrame;
import com.library.model.*;
import com.library.service.*;
import javax.swing.*;

public class LibraryGuiApp {
    public static void main(String[] args) {
        // Initialize services
        MediaCatalog catalog = new MediaCatalog();
        MemberService memberService = new MemberService();
        LoanService loanService = new LoanService(catalog, memberService);

        // Add sample data
        initializeSampleData(catalog, memberService);

        // Run GUI on Event Dispatch Thread  (#1:Thread safety!)
        SwingUtilities.invokeLater(() -> {
            try {
                // Set system look and feel  (#2:Native appearance)
                UIManager.setLookAndFeel(
                    UIManager.getSystemLookAndFeelClassName());
            } catch (Exception e) {
                // Fallback to default look and feel
            }

            LibraryFrame frame = new LibraryFrame(catalog, memberService, loanService);
            frame.setVisible(true);
        });
    }

    private static void initializeSampleData(MediaCatalog catalog,
                                              MemberService memberService) {
        // Sample books
        catalog.addItem(new Book("B001", "Clean Code", 2008, "Robert C. Martin"));
        catalog.addItem(new Book("B002", "The Pragmatic Programmer", 2019, "David Thomas"));
        catalog.addItem(new Book("B003", "Design Patterns", 1994, "Gang of Four"));

        // Sample DVDs
        catalog.addItem(new DVD("D001", "The Matrix", 1999, "Wachowskis"));
        catalog.addItem(new DVD("D002", "Inception", 2010, "Christopher Nolan"));

        // Sample magazines
        catalog.addItem(new Magazine("M001", "Java Magazine", 2024, 42));
        catalog.addItem(new Magazine("M002", "IEEE Software", 2024, 128));

        // Sample members
        memberService.addMember(new Member("MEM001", "Alice Johnson", "alice@email.com"));
        memberService.addMember(new Member("MEM002", "Bob Smith", "bob@email.com"));
        memberService.addMember(new Member("MEM003", "Carol Williams", "carol@email.com"));
    }
}
Important: Always use SwingUtilities.invokeLater() to create and show GUI components. This ensures all Swing operations run on the Event Dispatch Thread (EDT), preventing threading issues.

Expected Output

Your completed application should look similar to this:

+----------------------------------------------------------+
| Library Management System - 7 items          [ - ] [ X ] |
+----------------------------------------------------------+
| File | Actions | Help                                     |
+----------------------------------------------------------+
| Search: [___________________] [All Fields v] [Clear]     |
+----------------------------------------------------------+
|                                    | Add New Media       |
| +--------------------------------+ | ID: [_________]     |
| | ID   | Title      | Type | ... | | Title: [_________]  |
| +--------------------------------+ | Type: [Book v]      |
| | B001 | Clean Code | Book | ... | | Year: [_________]   |
| | B002 | Pragmatic  | Book | ... | | Author: [________]  |
| | D001 | The Matrix | DVD  | ... | |                     |
| | D002 | Inception  | DVD  | ... | | [Add]    [Clear]    |
| | M001 | Java Mag   | Mag  | ... | +---------------------+
| +--------------------------------+ |
+----------------------------------------------------------+
| [Borrow Selected] [Return Selected] [Delete] [Refresh]   |
+----------------------------------------------------------+
                    

Deliverables

  1. LibraryFrame.java - Main application window
  2. MediaTableModel.java - Custom table model for media display
  3. MediaFormPanel.java - Form for adding new media
  4. SearchPanel.java - Search component with live filtering
  5. LoanDialog.java - Dialog for borrow operations
  6. MemberDialog.java - Dialog for member management
  7. LibraryGuiApp.java - Main entry point

Bonus Challenges

Bonus 1

Keyboard Shortcuts

Add keyboard shortcuts for common actions:

// Add to menu items
fileMenu.setMnemonic(KeyEvent.VK_F);  // Alt+F opens File menu

JMenuItem saveItem = new JMenuItem("Save");
saveItem.setAccelerator(
    KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));  // Ctrl+S

// Add to table for Delete key
mediaTable.getInputMap().put(
    KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "deleteItem");
mediaTable.getActionMap().put("deleteItem", new AbstractAction() {
    @Override
    public void actionPerformed(ActionEvent e) {
        deleteSelected();
    }
});
Bonus 2

Status Bar

Add a status bar at the bottom showing current state:

private JLabel statusLabel;

// In layoutComponents()
statusLabel = new JLabel("Ready");
statusLabel.setBorder(BorderFactory.createLoweredBevelBorder());
add(statusLabel, BorderLayout.SOUTH);  // Move buttons to a toolbar

// Update status
private void updateStatus(String message) {
    statusLabel.setText(message);
}
Bonus 3

Table Row Colors

Color rows based on item status:

mediaTable.setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column) {
        Component c = super.getTableCellRendererComponent(
            table, value, isSelected, hasFocus, row, column);

        if (!isSelected) {
            MediaItem item = tableModel.getItemAt(row);
            if (item.getStatus() == MediaStatus.BORROWED) {
                c.setBackground(new Color(255, 230, 230));  // Light red
            } else if (item.getStatus() == MediaStatus.AVAILABLE) {
                c.setBackground(new Color(230, 255, 230));  // Light green
            } else {
                c.setBackground(Color.WHITE);
            }
        }
        return c;
    }
});
Bonus 4

Save/Load Integration

Connect to your CSV persistence from Practical Work 8:

private void saveData() {
    try {
        CsvMediaRepository mediaRepo = new CsvMediaRepository("data/media.csv");
        CsvMemberRepository memberRepo = new CsvMemberRepository("data/members.csv");

        mediaRepo.saveAll(catalog.getAllItems());
        memberRepo.saveAll(memberService.getAllMembers());

        updateStatus("Data saved successfully");
        JOptionPane.showMessageDialog(this, "Data saved!", "Success",
            JOptionPane.INFORMATION_MESSAGE);
    } catch (Exception e) {
        JOptionPane.showMessageDialog(this, "Error saving: " + e.getMessage(),
            "Error", JOptionPane.ERROR_MESSAGE);
    }
}

Resources

Tips for Success

  • Start Simple: Get the basic frame working first, then add components incrementally
  • Test Often: Run your application frequently to catch layout issues early
  • Use Layout Managers: Avoid absolute positioning (setBounds) - it doesn't adapt to window resizing
  • Separate Concerns: Keep business logic in service classes, GUI code in GUI classes
  • Event Dispatch Thread: Always create and modify GUI components on the EDT using SwingUtilities.invokeLater()
  • Error Handling: Always show user-friendly error messages with JOptionPane