Bagian pertama proyek ini membangun kerangka tampilan aplikasi Buku Kontak: satu jendela utama dengan formulir pengisian di atas, tabel data di tengah, dan deretan tombol aksi di bawah. Kita memakai tiga layout manager sesuai karakternya: GridBagLayout untuk formulir dua kolom, BorderLayout untuk kerangka jendela, FlowLayout untuk deretan tombol. Keputusan desain sejak awal: kode tampilan harus bisa diperiksa tanpa layar, karena komponen ringan Swing hanyalah objek Java biasa yang propertinya bisa dibaca.

Data Satu Baris Kontak #

Sebelum menulis tampilan, kita tetapkan bentuk datanya: satu kontak adalah satu nilai utuh berisi id penanda unik, nama, email, dan telepon. Record menjadi wadah yang tepat karena kontak tidak pernah diubah sebagian — setiap penyuntingan menghasilkan objek baru yang menggantikan yang lama.

java Kontak.java
/**
 * Satu baris data kontak pada aplikasi Buku Kontak.
 * Record hanya menyimpan nilai; aturan bisnis hidup di DaftarKontak.
 */
public record Kontak(int id, String nama, String email, String telepon) {
}

Record menyediakan method pengakses beserta equals, hashCode, dan toString otomatis; bentuk cetak Kontak[id=1, nama=Budi, ...] akan sering muncul pada keluaran pengujian. Validasi sengaja tidak ada di sini — pada Bagian 2, aturan bisnis berkumpul di DaftarKontak agar mudah dites.

Formulir Kontak dengan GridBagLayout #

Formulir menempatkan label dan isian dalam pola dua kolom: label rata kanan, isian meregang ke kiri. Pola seperti ini adalah wilayah kerja GridBagLayout, layout yang mengatur letak lewat objek kendali GridBagConstraints:

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

/**
 * Panel formulir kontak: tiga isian berjajar dengan label rata kanan.
 * Panel hanya membaca dan menulis komponen Swing.
 */
public class PanelFormKontak extends JPanel {

    private final JTextField isianNama = new JTextField(16);
    private final JTextField isianEmail = new JTextField(16);
    private final JTextField isianTelepon = new JTextField(16);

    public PanelFormKontak() {
        super(new GridBagLayout());
        GridBagConstraints k = new GridBagConstraints();
        k.insets = new Insets(4, 4, 4, 4);
        k.anchor = GridBagConstraints.LINE_END;

        String[] label = { "Nama:", "Email:", "Telepon:" };
        JTextField[] isian = { isianNama, isianEmail, isianTelepon };
        for (int i = 0; i < label.length; i++) {
            k.gridx = 0;
            k.gridy = i;
            add(new JLabel(label[i]), k);
            k.gridx = 1;
            k.fill = GridBagConstraints.HORIZONTAL;
            k.weightx = 1.0;
            add(isian[i], k);
            k.fill = GridBagConstraints.NONE;
            k.weightx = 0.0;
        }
    }

    /** Membaca isi formulir sebagai satu kontak; id ditentukan pemanggil. */
    public Kontak hasilKontak(int id) {
        return new Kontak(id,
                isianNama.getText().strip(),
                isianEmail.getText().strip(),
                isianTelepon.getText().strip());
    }

    /** Mengisi formulir dari satu kontak. */
    public void isi(Kontak kontak) {
        isianNama.setText(kontak.nama());
        isianEmail.setText(kontak.email());
        isianTelepon.setText(kontak.telepon());
    }

    /** Mengosongkan seluruh isian setelah kontak selesai disimpan. */
    public void kosongkan() {
        isianNama.setText("");
        isianEmail.setText("");
        isianTelepon.setText("");
    }
}

Mari kita bedah bagian pentingnya:

  1. Baris k.anchor = GridBagConstraints.LINE_END: label didorong ke ujung kanan selnya sehingga rapat dengan isian di sebelahnya.
  2. Baris k.weightx = 1.0 (lalu k.fill = HORIZONTAL): hanya kolom isian yang menyerap sisa lebar jendela; kolom label tetap sempit.
  3. Method hasilKontak(int) dan kosongkan(): panel hanya membaca dan mengosongkan isian, tanpa mengambil keputusan apa pun tentang data.

Tabel di dalam JScrollPane #

Data kontak berbentuk baris-kolom, maka komponen tepat adalah JTable. Swing menyertakan DefaultTableModel, model serbaguna yang menyimpan data di dalamnya sendiri, sehingga tabel tampil cepat tanpa kelas model khusus. Ingat satu kebiasaan JTable: tabel tidak menggulir sendiri dan judul kolom hanya tampil bila tabel berada di dalam JScrollPane:

import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

/**
 * Panel tabel kontak: JTable di dalam JScrollPane.
 * Data disimpan pada DefaultTableModel — praktis untuk permulaan.
 */
public class PanelTabelKontak extends JScrollPane {

    private final JTable tabel;
    private final DefaultTableModel model;

    public PanelTabelKontak() {
        super();
        model = new DefaultTableModel(
                new Object[] { "Id", "Nama", "Email", "Telepon" }, 0) {
            // Sel tabel tidak bisa disunting langsung; perubahan lewat formulir.
            @Override
            public boolean isCellEditable(int baris, int kolom) {
                return false;
            }
        };
        tabel = new JTable(model);
        setViewportView(tabel);
    }

    /** Tabel yang dipakai panel — pembacaan nilai lewat view ini. */
    public JTable tabel() {
        return tabel;
    }

    /** Menambah satu kontak sebagai baris baru. */
    public void tambahBaris(Kontak kontak) {
        model.addRow(new Object[] {
                kontak.id(), kontak.nama(), kontak.email(), kontak.telepon() });
    }

    /** Jumlah baris data saat ini. */
    public int jumlahBaris() {
        return model.getRowCount();
    }

    /**
     * Id untuk baris berikutnya: jumlah baris + 1.
     * Cepat, tetapi rapuh — id akan berulang setelah baris dihapus.
     */
    public int idBerikutnya() {
        return jumlahBaris() + 1;
    }
}

Tiga keputusan pada berkas tersebut patut dicermati:

  1. Baris new DefaultTableModel(new Object[]{...}, 0): konstruktor menerima judul kolom dan jumlah baris awal nol; baris datang belakangan lewat addRow.
  2. Baris isCellEditable yang ditimpa: sel tabel dikunci — perubahan data hanya lewat formulir dan tombol, satu pintu masuk.
  3. Method idBerikutnya(): id baru dihitung dari jumlah baris — cepat tetapi rapuh: setelah satu baris dihapus, angka yang sama bisa terpakai ulang. Dibiarkan sebagai bahan perbaikan Bagian 2.

Kerangka Jendela Utama #

Kerangka jendela memakai BorderLayout: formulir di NORTH, tabel di CENTER, tombol di SOUTH. Satu kendala lingkungan perlu dijelaskan: JFrame adalah komponen heavyweight yang pembuatannya menuntut layar sungguhan, sehingga kelas ini tidak diinstansiasi oleh kode pengujian. Pada komputer berkantor grafis, new JendelaUtama() membuka jendela nyata:

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * Jendela utama aplikasi Buku Kontak.
 * JFrame tidak bisa dibuat pada lingkungan tanpa layar, sehingga
 * perakitan panel tombol dipisahkan ke method static yang bisa
 * dipanggil tanpa membuka jendela nyata.
 */
public class JendelaUtama extends JFrame {

    public JendelaUtama() {
        super("Buku Kontak");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        PanelFormKontak form = new PanelFormKontak();
        PanelTabelKontak tabel = new PanelTabelKontak();
        JPanel tombol = bangunPanelTombol(form, tabel);

        JPanel isi = new JPanel(new BorderLayout(8, 8));
        isi.add(form, BorderLayout.NORTH);
        isi.add(tabel, BorderLayout.CENTER);
        isi.add(tombol, BorderLayout.SOUTH);
        setContentPane(isi);

        pack();
        setLocationRelativeTo(null);
    }

    /**
     * Membangun panel tombol beserta aksinya.
     * Pada tahap ini tombol Tambah menulis langsung ke tabel:
     * bekerja, tetapi aturan data terkunci di dalam listener.
     */
    static JPanel bangunPanelTombol(PanelFormKontak form, PanelTabelKontak tabel) {
        JPanel tombol = new JPanel(new FlowLayout(FlowLayout.LEADING));
        JButton tambah = new JButton("Tambah");

        tambah.addActionListener(e -> {
            Kontak baru = form.hasilKontak(tabel.idBerikutnya());
            tabel.tambahBaris(baru);
            form.kosongkan();
        });

        tombol.add(tambah);
        return tombol;
    }
}

Perhatikan dua hal pada berkas tersebut. Method bangunPanelTombol(...) dinyatakan static dan tidak menyentuh JFrame — hanya menerima panel ringan — sehingga kode pemasangan tombol yang sama dapat diperiksa tanpa layar. Selain itu, listener Tambah pada tahap ini menulis langsung ke tabel: membaca formulir, membuat kontak, menambah baris. Aturan penomoran id dan saringan masukan kini terkunci di dalam listener — tidak bisa dipanggil dari pengujian, apalagi penyimpanan berkas.

Menyimulasikan Antarmuka Tanpa Layar #

Komponen ringan Swing hidup sebagai objek Java biasa sebelum digambar ke layar, jadi kita merakit seluruh isi jendela lalu membaca strukturnya. Kunci simulasinya adalah doClick(): method ini menjalankan actionPerformed listener secara sinkron di thread yang sama — seperti klik pengguna, tanpa jendela:

import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JTable;

/**
 * Menguji perakitan antarmuka tanpa membuka jendela nyata:
 * susun isi jendela dari komponen ringan, isi formulir,
 * lalu picu tombol Tambah dengan doClick().
 */
public class DemoAntarmuka {

    public static void main(String[] args) {
        PanelFormKontak form = new PanelFormKontak();
        PanelTabelKontak panelTabel = new PanelTabelKontak();
        JPanel tombol = JendelaUtama.bangunPanelTombol(form, panelTabel);

        JPanel isi = new JPanel(new BorderLayout(8, 8));
        isi.add(form, BorderLayout.NORTH);
        isi.add(panelTabel, BorderLayout.CENTER);
        isi.add(tombol, BorderLayout.SOUTH);

        System.out.println("== struktur antarmuka ==");
        System.out.println("anak panel isi  = " + isi.getComponentCount());
        System.out.println("anak form       = " + form.getComponentCount());
        System.out.println("anak tombol     = " + tombol.getComponentCount());
        JTable tabel = panelTabel.tabel();
        System.out.println("kolom tabel     = " + tabel.getColumnCount());
        System.out.println("judul kolom     = " + tabel.getColumnName(0) + ", "
                + tabel.getColumnName(1) + ", " + tabel.getColumnName(2) + ", "
                + tabel.getColumnName(3));

        System.out.println();
        System.out.println("== simulasi klik Tambah ==");
        form.isi(new Kontak(0, "Budi", "[email protected]", "0812-1000-1001"));
        ((JButton) tombol.getComponent(0)).doClick();
        cetakBaris(tabel);

        form.isi(new Kontak(0, "Siti", "[email protected]", "0812-1000-1002"));
        ((JButton) tombol.getComponent(0)).doClick();
        cetakBaris(tabel);

        System.out.println("form kosong     = " + form.hasilKontak(0).nama().isEmpty());
        System.out.println("id berikutnya   = " + panelTabel.idBerikutnya());
    }

    /** Mencetak seluruh baris tabel lewat pembacaan view. */
    private static void cetakBaris(JTable tabel) {
        System.out.println("jumlah baris    = " + tabel.getRowCount());
        for (int r = 0; r < tabel.getRowCount(); r++) {
            System.out.println("  baris " + r + ": " + tabel.getValueAt(r, 0)
                    + " | " + tabel.getValueAt(r, 1)
                    + " | " + tabel.getValueAt(r, 2)
                    + " | " + tabel.getValueAt(r, 3));
        }
    }
}

Output yang Dihasilkan:

text
== struktur antarmuka ==
anak panel isi  = 3
anak form       = 6
anak tombol     = 1
kolom tabel     = 4
judul kolom     = Id, Nama, Email, Telepon

== simulasi klik Tambah ==
jumlah baris    = 1
  baris 0: 1 | Budi | [email protected] | 0812-1000-1001
jumlah baris    = 2
  baris 0: 1 | Budi | [email protected] | 0812-1000-1001
  baris 1: 2 | Siti | [email protected] | 0812-1000-1002
form kosong     = true
id berikutnya   = 3

Keluaran tersebut membuktikan tiga hal. Struktur tersusun benar: panel isi memuat tiga anak sesuai tiga wilayah BorderLayout, formulir memuat enam komponen, dan tabel mengumumkan empat kolomnya. Dua klik menambah baris 1 | Budi | ... lalu 2 | Siti | ... — baris dibaca lewat getValueAt milik view, bukti JTable sungguh membaca model. Terakhir, formulir kosong setelah simpan dan idBerikutnya() bernilai 3 — perilaku yang berganti begitu aturan id pindah ke logika inti.

Antarmuka kini berdiri, tetapi data masih hidup di dalam DefaultTableModel: tanpa validasi, tanpa pencarian, dan penomoran id rapuh terhadap penghapusan. Bagian 2 memindahkan seluruh aturan ke kelas DaftarKontak yang terpisah dari Swing, agar logika bisa diuji dan disimpan.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

DemoAntarmuka.java

Unduh berkas
java
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JTable;

/**
 * Menguji perakitan antarmuka tanpa membuka jendela nyata:
 * susun isi jendela dari komponen ringan, isi formulir,
 * lalu picu tombol Tambah dengan doClick().
 */
public class DemoAntarmuka {

    public static void main(String[] args) {
        PanelFormKontak form = new PanelFormKontak();
        PanelTabelKontak panelTabel = new PanelTabelKontak();
        JPanel tombol = JendelaUtama.bangunPanelTombol(form, panelTabel);

        JPanel isi = new JPanel(new BorderLayout(8, 8));
        isi.add(form, BorderLayout.NORTH);
        isi.add(panelTabel, BorderLayout.CENTER);
        isi.add(tombol, BorderLayout.SOUTH);

        System.out.println("== struktur antarmuka ==");
        System.out.println("anak panel isi  = " + isi.getComponentCount());
        System.out.println("anak form       = " + form.getComponentCount());
        System.out.println("anak tombol     = " + tombol.getComponentCount());
        JTable tabel = panelTabel.tabel();
        System.out.println("kolom tabel     = " + tabel.getColumnCount());
        System.out.println("judul kolom     = " + tabel.getColumnName(0) + ", "
                + tabel.getColumnName(1) + ", " + tabel.getColumnName(2) + ", "
                + tabel.getColumnName(3));

        System.out.println();
        System.out.println("== simulasi klik Tambah ==");
        form.isi(new Kontak(0, "Budi", "[email protected]", "0812-1000-1001"));
        ((JButton) tombol.getComponent(0)).doClick();
        cetakBaris(tabel);

        form.isi(new Kontak(0, "Siti", "[email protected]", "0812-1000-1002"));
        ((JButton) tombol.getComponent(0)).doClick();
        cetakBaris(tabel);

        System.out.println("form kosong     = " + form.hasilKontak(0).nama().isEmpty());
        System.out.println("id berikutnya   = " + panelTabel.idBerikutnya());
    }

    /** Mencetak seluruh baris tabel lewat pembacaan view. */
    private static void cetakBaris(JTable tabel) {
        System.out.println("jumlah baris    = " + tabel.getRowCount());
        for (int r = 0; r < tabel.getRowCount(); r++) {
            System.out.println("  baris " + r + ": " + tabel.getValueAt(r, 0)
                    + " | " + tabel.getValueAt(r, 1)
                    + " | " + tabel.getValueAt(r, 2)
                    + " | " + tabel.getValueAt(r, 3));
        }
    }
}

JendelaUtama.java

Unduh berkas
java
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * Jendela utama aplikasi Buku Kontak.
 * JFrame tidak bisa dibuat pada lingkungan tanpa layar, sehingga
 * perakitan panel tombol dipisahkan ke method static yang bisa
 * dipanggil tanpa membuka jendela nyata.
 */
public class JendelaUtama extends JFrame {

    public JendelaUtama() {
        super("Buku Kontak");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        PanelFormKontak form = new PanelFormKontak();
        PanelTabelKontak tabel = new PanelTabelKontak();
        JPanel tombol = bangunPanelTombol(form, tabel);

        JPanel isi = new JPanel(new BorderLayout(8, 8));
        isi.add(form, BorderLayout.NORTH);
        isi.add(tabel, BorderLayout.CENTER);
        isi.add(tombol, BorderLayout.SOUTH);
        setContentPane(isi);

        pack();
        setLocationRelativeTo(null);
    }

    /**
     * Membangun panel tombol beserta aksinya.
     * Pada tahap ini tombol Tambah menulis langsung ke tabel:
     * bekerja, tetapi aturan data terkunci di dalam listener.
     */
    static JPanel bangunPanelTombol(PanelFormKontak form, PanelTabelKontak tabel) {
        JPanel tombol = new JPanel(new FlowLayout(FlowLayout.LEADING));
        JButton tambah = new JButton("Tambah");

        tambah.addActionListener(e -> {
            Kontak baru = form.hasilKontak(tabel.idBerikutnya());
            tabel.tambahBaris(baru);
            form.kosongkan();
        });

        tombol.add(tambah);
        return tombol;
    }
}

Kontak.java

Unduh berkas
java
/**
 * Satu baris data kontak pada aplikasi Buku Kontak.
 * Record hanya menyimpan nilai; aturan bisnis hidup di DaftarKontak.
 */
public record Kontak(int id, String nama, String email, String telepon) {
}

PanelFormKontak.java

Unduh berkas
java
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

/**
 * Panel formulir kontak: tiga isian berjajar dengan label rata kanan.
 * Panel hanya membaca dan menulis komponen Swing.
 */
public class PanelFormKontak extends JPanel {

    private final JTextField isianNama = new JTextField(16);
    private final JTextField isianEmail = new JTextField(16);
    private final JTextField isianTelepon = new JTextField(16);

    public PanelFormKontak() {
        super(new GridBagLayout());
        GridBagConstraints k = new GridBagConstraints();
        k.insets = new Insets(4, 4, 4, 4);
        k.anchor = GridBagConstraints.LINE_END;

        String[] label = { "Nama:", "Email:", "Telepon:" };
        JTextField[] isian = { isianNama, isianEmail, isianTelepon };
        for (int i = 0; i < label.length; i++) {
            k.gridx = 0;
            k.gridy = i;
            add(new JLabel(label[i]), k);
            k.gridx = 1;
            k.fill = GridBagConstraints.HORIZONTAL;
            k.weightx = 1.0;
            add(isian[i], k);
            k.fill = GridBagConstraints.NONE;
            k.weightx = 0.0;
        }
    }

    /** Membaca isi formulir sebagai satu kontak; id ditentukan pemanggil. */
    public Kontak hasilKontak(int id) {
        return new Kontak(id,
                isianNama.getText().strip(),
                isianEmail.getText().strip(),
                isianTelepon.getText().strip());
    }

    /** Mengisi formulir dari satu kontak. */
    public void isi(Kontak kontak) {
        isianNama.setText(kontak.nama());
        isianEmail.setText(kontak.email());
        isianTelepon.setText(kontak.telepon());
    }

    /** Mengosongkan seluruh isian setelah kontak selesai disimpan. */
    public void kosongkan() {
        isianNama.setText("");
        isianEmail.setText("");
        isianTelepon.setText("");
    }
}

PanelTabelKontak.java

Unduh berkas
java
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;

/**
 * Panel tabel kontak: JTable di dalam JScrollPane.
 * Data disimpan pada DefaultTableModel — praktis untuk permulaan.
 */
public class PanelTabelKontak extends JScrollPane {

    private final JTable tabel;
    private final DefaultTableModel model;

    public PanelTabelKontak() {
        super();
        model = new DefaultTableModel(
                new Object[] { "Id", "Nama", "Email", "Telepon" }, 0) {
            // Sel tabel tidak bisa disunting langsung; perubahan lewat formulir.
            @Override
            public boolean isCellEditable(int baris, int kolom) {
                return false;
            }
        };
        tabel = new JTable(model);
        setViewportView(tabel);
    }

    /** Tabel yang dipakai panel — pembacaan nilai lewat view ini. */
    public JTable tabel() {
        return tabel;
    }

    /** Menambah satu kontak sebagai baris baru. */
    public void tambahBaris(Kontak kontak) {
        model.addRow(new Object[] {
                kontak.id(), kontak.nama(), kontak.email(), kontak.telepon() });
    }

    /** Jumlah baris data saat ini. */
    public int jumlahBaris() {
        return model.getRowCount();
    }

    /**
     * Id untuk baris berikutnya: jumlah baris + 1.
     * Cepat, tetapi rapuh — id akan berulang setelah baris dihapus.
     */
    public int idBerikutnya() {
        return jumlahBaris() + 1;
    }
}