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.
/**
* 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:
- Baris
k.anchor = GridBagConstraints.LINE_END: label didorong ke ujung kanan selnya sehingga rapat dengan isian di sebelahnya. - Baris
k.weightx = 1.0(laluk.fill = HORIZONTAL): hanya kolom isian yang menyerap sisa lebar jendela; kolom label tetap sempit. - Method
hasilKontak(int)dankosongkan(): 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:
- Baris
new DefaultTableModel(new Object[]{...}, 0): konstruktor menerima judul kolom dan jumlah baris awal nol; baris datang belakangan lewataddRow. - Baris
isCellEditableyang ditimpa: sel tabel dikunci — perubahan data hanya lewat formulir dan tombol, satu pintu masuk. - 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:
== 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 = 3Keluaran 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 #
- Oracle: Kelas JTable (JDK 25)
- Oracle: Kelas DefaultTableModel (JDK 25)
- Oracle: Kelas GridBagLayout (JDK 25)
- Oracle: A Visual Guide to Layout Managers
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
DemoAntarmuka.java
Unduh berkasimport 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 berkasimport 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/**
* 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 berkasimport 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 berkasimport 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;
}
}