Membuat dan Mengatur JTable + GlazedLists Dengan Mudah Di simple-jpa 0.5


Salah satu perubahan yang cukup besar yang baru saja saya selesaikan di simple-jpa 0.5 adalah merombak node yang menampilkan tabel di view Griffon.   Walaupun plugin simple-jpa pada dasarnya berkaitan dengan persistence layer atau database, saya sengaja menambahkan beberapa node Swing builder yang dipakai di view.   Hal ini karena hampir semua aplikasi bisnis memiliki view yang menyajikan data dalam bentuk tabel.   Dengan menyediakan node tersebut, saya berusaha agar proses pembuatan dan perubahan tabel bisa semudah dan senyaman mungkin.   Btw, Griffon telah memiliki plugin khusus untuk GlazedLists yang juga menyediakan node baru.   Plugin tersebut sebenarnya lebih lengkap tetapi ditujukan untuk keperluan umum.   Sementara itu, yang saya kembangkan di simple-jpa lebih merupakan cara singkat untuk menyelesaikan kebutuhan sehari-hari yang sering saya jumpai saat bekerja dengan tabel.

Node baru yang saya tambahkan akan diawali dengan glazedXXX(), misalnya glazedTable() dan glazedColumn().   Ada juga beberapa yang sifatnya tidak terikat pada GlazedLists seperti templateRenderer() dan condition().   Node lama yang dipakai di versi sebelum 0.5 seperti eventTableModel() dan tableColumnConfig() masih tersedia tapi tidak disarankan untuk dipakai lagi.

Sebagai percobaan, saya akan membuat sebuah proyek baru yang memakai Griffon 1.4 dan plugin simple-jpa 0.5.   Saya kemudian menambahkan sebuah domain class baru dengan nama Mahasiswa yang isinya seperti berikut ini:

package domain

import ...

@DomainModel @Entity @Canonical
class Mahasiswa {

    @Size(min=5, max=5)
    String nim

    @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    LocalDate tanggalLahir

    @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    LocalDate tanggalDaftar

    @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    LocalDate tanggalLulus

    @NotBlank @Size(max=50)
    String nama

    @Size(min=3, max=3)
    String kelas

    @ElementCollection
    Set daftarNilai = new HashSet<>()

    public Float nilaiRataRata() {
        daftarNilai.sum() / daftarNilai.size()
    }

}

Setelah itu, saya memakai fasilitas scaffolding dari simple-jpa untuk menghasilkan sebuah MVC baru.   Saya kemudian mengubah isi dari file MahasiswaModel.groovy menjadi seperti berikut ini:

package project

import ...

class MahasiswaModel {

    BasicEventList mahasiswaList = new BasicEventList<>()

}

Pada kode program di atas, saya mendeklarasikan sebuah BasicEventList (dari GlazedLists) yang akan saya pakai untuk menampung seluruh instance dari Mahasiswa yang akan ditampilkan di tabel nantinya.   Kode program yang memakainya terlihat di MahasiswaController.groovy seperti berikut ini:

package project

import ...

@SimpleJpaTransaction
class MahasiswaController {

    def model
    def view

    void mvcGroupInit(Map args) {
        execInsideUISync {
            DateTimeFormatter df = DateTimeFormat.forPattern("dd-MM-yyyy")
            model.mahasiswaList << new Mahasiswa("99001", df.parseLocalDate("01-03-1985"),
                df.parseLocalDate("09-09-2003"), df.parseLocalDate("10-10-2010"),
                "Jocki Hendry", "T01",  [90, 95, 80, 70, 90, 93, 87, 77, 88, 90].toSet())
            model.mahasiswaList << new Mahasiswa("99002", df.parseLocalDate("14-02-1987"),
                df.parseLocalDate("01-09-2005"), df.parseLocalDate("10-11-2012"),
                "Lena", "T01",  [80, 70, 60, 80, 75, 85, 50, 70, 90, 90].toSet())
            model.mahasiswaList << new Mahasiswa("99003", df.parseLocalDate("01-03-1986"),
                df.parseLocalDate("09-09-2006"), df.parseLocalDate("10-10-2012"),
                "Berry", "T02",  [90, 90, 88, 88, 99, 87, 60, 89].toSet())
            model.mahasiswaList << new Mahasiswa("99004", df.parseLocalDate("01-03-1983"),
                df.parseLocalDate("09-09-2003"), null,
                "Sandy", "T02",  [50, 60, 40, 30, 80, 40, 30, 90, 55, 70, 60].toSet())
            model.mahasiswaList << new Mahasiswa("99005", df.parseLocalDate("01-03-1984"),
                df.parseLocalDate("09-09-2004"), null,
                "Gerry", "T03",  [40, 30, 50, 30, 80, 40, 30, 55, 65, 70, 66, 50].toSet())
        }
    }

    void mvcGroupDestroy() {
        destroyEntityManager()
    }

}

Kode program pada mvcGroupInit() akan dikerjakan pada saat MVCGroup dibuat.   Kode program tersebut akan membuat lima instance dari class Mahasiswa dan meletakkannya pada model.mahasiswaList (sebuah BasicEventList yang saya definisikan sebelumnya).

Setelah BasicEventList terisi, langkah selanjutnya adalah menampilkannya.   Bagaimana cara menampilkan BasicEventList yang berisi kumpulan Mahasiswa tersebut? Saya cukup memberikan kode program berikut ini di MahasiswaView.groov:

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim')
                glazedColumn(name: 'Nama', property: 'nama')
                glazedColumn(name: 'Kelas', property: 'kelas')
            }
        }
    }
}

Pada kode program di atas, node glazedTable() mewakili sebuah JTable secara keseluruhan.   Node ini akan selalu mengembalikan sebuah instance dari GlazedTable yang merupakan sebuah JTable.   Di dalam node glazedTable(), saya menambahkan masing-masing node glazedColumn() untuk mewakili sebuah kolom di tabel.   Atribut name akan menjadi judul untuk kolom tersebut, sementara itu atribut property adalah nama property dari object Mahasiswa yang harus ditampilkan untuk tabel tersebut.   Hasilnya akan terlihat seperti pada gambar berikut ini:

Tampilan Tabel Biasa

Tampilan Tabel Biasa

Dengan kode program yang sederhana yang mudah dipahami, saya sudah menampilkan isi dari sebuah BasicEventList melalui sebuah JTable.

Tapi seringkali saya tidak hanya perlu menampilkan isi property, tapi juga perlu menampilkan hasil kalkulasi dari method.   Sebagai contoh, pada class Mahasiswa terdapat method hitungRataRata() yang akan mengembalikan nilai rata-rata dari sebuah object mahasiswa.   Untuk mengerjakan sebuah method, saya dapat menggunakan atribut expression dari node GlazedColumn() seperti yang ditunjukkan oleh kode program berikut ini:

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim')
                glazedColumn(name: 'Nama', property: 'nama')
                glazedColumn(name: 'Kelas', property: 'kelas')
                glazedColumn(name: 'Nilai Rata-Rata', expression: { it.nilaiRataRata() })
            }
        }
    }
}

Pada closure yang diberikan untuk atribut expression, nilai it akan merujuk pada object yang sedang ditampilkan (ingat bahwa setiap baris adalah sebuah object!).   Tampilan kode program di atas akan terlihat seperti:

Tabel dengan kolom berisi hasil perhitungan

Tabel dengan kolom berisi hasil perhitungan

Dengan menggunakan atribut expression, saya dapat melewatkan kode program apa saja untuk menghitung nilai sebuah kolom.   Sebagai contoh, saya menambahkan beberapa kolom yang merupakan hasil perhitungan sehingga kode program view menjadi seperti berikut ini:

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim')
                glazedColumn(name: 'Nama', property: 'nama')
                glazedColumn(name: 'Kelas', property: 'kelas')
                glazedColumn(name: 'Usia', expression: { Years.yearsBetween(it.tanggalLahir, LocalDate.now()).years })
                glazedColumn(name: 'Angkatan', expression: { it.tanggalDaftar.getYear() })
                glazedColumn(name: 'Tahun Lulus', expression: { it.tanggalLulus?.getYear() })
                glazedColumn(name: 'Nilai Rata-Rata', expression: { it.nilaiRataRata() })
                glazedColumn(name: 'Nilai OK?', expression: { it.nilaiRataRata() > 70 })

            }
        }
    }
}

Kode program di atas memiliki 5 kolom yang nilainya merupakan hasil kalkulasi dari closure, seperti yang terlihat pada gambar berikut ini:

Tabel dengan kolom hasil kode program

Tabel dengan kolom hasil kode program

Node glazedColumn memiliki atribut columnClass yang dapat dipakai sebagai informasi jenis tipe data dari sebuah kolom. Tampilan dari tabel akan disesuaikan berdasarkan tipe data tersebut. Sebagai contoh, bila kolom memiliki columnClass dengan tipe data angka, maka nilai dari kolom tersebut akan rata kanan. Saya dapat menambahkan informasi columnClass seperti pada contoh berikut ini:

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim')
                glazedColumn(name: 'Nama', property: 'nama')
                glazedColumn(name: 'Kelas', property: 'kelas')
                glazedColumn(name: 'Usia', expression: { Years.yearsBetween(it.tanggalLahir, LocalDate.now()).years },
                    columnClass: Integer)
                glazedColumn(name: 'Angkatan', expression: { it.tanggalDaftar.getYear() },
                    columnClass: Integer)
                glazedColumn(name: 'Tahun Lulus', expression: { it.tanggalLulus?.getYear() },
                    columnClass: Integer)
                glazedColumn(name: 'Nilai Rata-Rata', expression: { it.nilaiRataRata() },
                    columnClass: Float)
                glazedColumn(name: 'Nilai OK?', expression: { it.nilaiRataRata() > 70 },
                    columnClass: Boolean)
            }
        }
    }
}

Tampilan program saat dijalankan kini akan menjadi seperti yang terlihat pada gambar berikut ini:

Tampilan tabel setelah definisi tipe data kolom

Tampilan tabel setelah definisi tipe data kolom

Tidak seperti perancangan view di simple-jpa versi sebelumnya, node glazedColumn() memiliki pemisahan antara nilai dan renderer-nya.   Sebuah nilai dapat ditampilkan melalui renderer yang men-format nilai tersebut.   Operasi seperti pengurutan (sorting) kolom nantinya akan berdasarkan nilai asli, bukan berdasarkan nilai yang sudah di-format.   Salah satu renderer bawaan yang sangat berguna disini adalah templateRenderer().   Sebagai contoh, saya menambahkan renderer pada view saya sehingga menjadi seperti berikut ini:

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim')
                glazedColumn(name: 'Nama', property: 'nama')
                glazedColumn(name: 'Kelas', property: 'kelas')
                glazedColumn(name: 'Usia', expression: { Years.yearsBetween(it.tanggalLahir, LocalDate.now()).years },
                    columnClass: Integer)
                glazedColumn(name: 'Angkatan', expression: { it.tanggalDaftar.getYear() },
                        columnClass: Integer) {
                    templateRenderer(templateString: '${it}/${it+1}')
                }
                glazedColumn(name: 'Tahun Lulus', expression: { it.tanggalLulus?.getYear() },
                        columnClass: Integer) {
                    templateRenderer(templateString: "\${it?:'Belum Lulus'}")
                }
                glazedColumn(name: 'Nilai Rata-Rata', expression: { it.nilaiRataRata() },
                        columnClass: Float) {
                    templateRenderer(templateString: "\${floatFormat(it,2)}")
                }
                glazedColumn(name: 'Nilai OK?', expression: { it.nilaiRataRata() > 70 },
                    columnClass: Boolean)

            }
        }
    }
}

Tampilan view kini akan terlihat seperti pada gambar berikut ini:

Memakai template renderer

Memakai template renderer

Pada atribut templateString di node templateRenderer(), saya dapat memberikan sebuah ekspresi Groovy.   Nilai dari variabel it dan value akan merujuk pada nilai yang hendak ditampilkan.   Walaupun templateString dapat berisi kode program apa saja, sebaiknya atribut tersebut tidak melakukan proses yang rumit (karena proses yang rumit sebaiknya diolah di controller).   Agar ekspresi di templateString tidak terlalu panjang, saya menyediakan beberapa fungsi untuk men-format data, yang berupa:  numberFormat(), floatFormat(), percentFormat(), currencyFormat(), dan titleCase().

Node templateRenderer() pada dasarnya akan menghasilkan sebuah JLabel.   Hal ini menyebabkan saya dapat mengkonfigurasi property JLabel yang dihasilkan dengan melewatkannya sebagai atribut di templateRenderer().   Salah satu contoh yang sering dipakai adalah mengubah sebuah kolom menjadi rata kiri, rata tengah, atau rata kanan, seperti yang terlihat pada kode program berikut ini:

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim')
                glazedColumn(name: 'Nama', property: 'nama')
                glazedColumn(name: 'Kelas', property: 'kelas')
                glazedColumn(name: 'Usia', expression: { Years.yearsBetween(it.tanggalLahir, LocalDate.now()).years },
                    columnClass: Integer)
                glazedColumn(name: 'Angkatan', expression: { it.tanggalDaftar.getYear() },
                        columnClass: Integer) {
                    templateRenderer(templateString: '${it}/${it+1}', horizontalAlignment: SwingConstants.CENTER)
                }
                glazedColumn(name: 'Tahun Lulus', expression: { it.tanggalLulus?.getYear() },
                        columnClass: Integer) {
                    templateRenderer(templateString: "\${it?:'Belum Lulus'}", horizontalAlignment: SwingConstants.CENTER)
                }
                glazedColumn(name: 'Nilai Rata-Rata', expression: { it.nilaiRataRata() },
                        columnClass: Float) {
                    templateRenderer(templateString: "\${floatFormat(it,2)}", horizontalAlignment: SwingConstants.RIGHT)
                }
                glazedColumn(name: 'Nilai OK?', expression: { it.nilaiRataRata() > 70 },
                    columnClass: Boolean)

            }
        }
    }
}

Sebuah node templateRenderer() dapat menerima satu atau lebih node condition().   Fungsi node condition() adalah melakukan evaluasi pada saat tabel sudah terisi dengan data dan melakukan perubahan property dari renderer berdasarkan kondisi tersebut.   Sebagai contoh, saya dapat membuat tulisan ‘Belum Lulus’ pada kolom ‘Tahun Lulus’ ditampilkan dalam tulisan berwarna merah dengan menggubah kode program view menjadi:

...
glazedColumn(name: 'Tahun Lulus', expression: { it.tanggalLulus?.getYear() },
     columnClass: Integer) {
  templateRenderer(templateString: "\${it?:'Belum Lulus'}", horizontalAlignment: SwingConstants.CENTER) {
    condition(if_: {value}, then_property_: 'foreground', is_: Color.BLACK, else_is_: Color.RED)
  }
}
...

Tampilan program setelah perubahan di atas akan terlihat seperti pada gambar berikut ini:

Memakai condition() di renderer

Memakai condition() di renderer

Beberapa variasi yang mungkin untuk node condition() adalah:

condition(if_: {...}, then_property_: '...', is_: ...)
condition(if_: {...}, then_property_: '...', is_: ..., else_is_: ...)
condition(if_: {...}, then_property_: '...', is_: ..., else_property_: '...', else_is_: ...)
condition(if_: {...}, then_: {...})
condition(if_: {...}, then_: {...}, else_: {...})

Pada closure yang dilewatkan sebagai atribut untuk if_, then_ dan else_, saya dapat menggunakan salah satu variabel yang nilainya adalah seperti berikut ini:

  • table – berisi JTable yang sedang ditampilkan.
  • value – berisi nilai yang hendak ditampilkan oleh renderer ini.
  • isSelected – menunjukkan apakah renderer ini berada dalam baris yang sedang terpilih.
  • hasFocus – menunjukkan apakah renderer ini sedang mendapat fokus.
  • row – berisi nilai yang menunjukkan nomor baris.
  • column – berisi nilai yang menunjukkan nomor kolom.
  • component – adalah sebuah JComponent yang mewakili renderer ini sendiri.

Sebuah renderer yang mendukung node condition() akan membolehkan terdapat lebih dari satu node tersebut, dimana mereka akan di-eksekusi secara berurutan.   Sebagai contoh, berikut ini adalah contoh view yang menunjukkan variasi penggunaan condition():

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim')
                glazedColumn(name: 'Nama', property: 'nama')
                glazedColumn(name: 'Kelas', property: 'kelas')
                glazedColumn(name: 'Usia', expression: { Years.yearsBetween(it.tanggalLahir, LocalDate.now()).years },
                    columnClass: Integer)
                glazedColumn(name: 'Angkatan', expression: { it.tanggalDaftar.getYear() },
                        columnClass: Integer) {
                    templateRenderer(templateString: '${it}/${it+1}', horizontalAlignment: SwingConstants.CENTER)
                }
                glazedColumn(name: 'Tahun Lulus', expression: { it.tanggalLulus?.getYear() },
                        columnClass: Integer) {
                    templateRenderer(templateString: "\${it?:'Belum Lulus'}", horizontalAlignment: SwingConstants.CENTER) {
                        condition(if_: {value}, then_property_: 'foreground', is_: Color.BLACK, else_is_: Color.RED)
                        condition(if_: {isSelected}, then_: {component.foreground = Color.WHITE})
                    }
                }
                glazedColumn(name: 'Nilai Rata-Rata', expression: { it.nilaiRataRata() },
                        columnClass: Float) {
                    templateRenderer(templateString: "\${floatFormat(it,2)}", horizontalAlignment: SwingConstants.RIGHT){
                        condition(if_: {value < 60}, then_property_: 'background', is_: Color.RED)
                        condition(if_: {value >= 60 && value < 80}, then_property_: 'background', is_: Color.YELLOW)
                        condition(if_: {value >= 80}, then_property_: 'background', is_: Color.GREEN)
                        condition(if_: {isSelected}, then_property_: 'foreground', is_: Color.BLUE, else_is_: Color.BLACK)
                    }
                }
                glazedColumn(name: 'Nilai OK?', expression: { it.nilaiRataRata() > 70 },
                    columnClass: Boolean)

            }
        }
    }
}

Hasil dari kode program di atas akan terlihat seperti pada gambar berikut ini:

Lebih dari satu condition() pada renderer

Lebih dari satu condition() pada renderer

Selain memakai renderer bawaan simple-jpa, saya juga dapat memakai custom renderer buatan sendiri.   Sebagai contoh, Swing memiliki renderer untuk kolom bertipe boolean yang menampilkannya dalam bentuk checkbox.   Tapi seandainya saya menginginkan hal yang berbeda, saya dapat membuat sebuah renderer baru seperti berikut ini:

package swing

import ...

class MyBooleanRenderer extends DefaultTableCellRenderer {

    public static final Icon OK_ICON =
	new ImageIcon(MyBooleanRenderer.getResource("/ok.png"))
    public static final Icon NOT_OK_ICON =
	new ImageIcon(MyBooleanRenderer.getResource("/not_ok.png"))

    public MyBooleanRenderer() {
        super()
    }

    @Override
    Component getTableCellRendererComponent(JTable table, Object value,
		boolean isSelected, boolean hasFocus, int row, int column) {
        JLabel c = super.getTableCellRendererComponent(table, value,
		isSelected, hasFocus, row, column)
        c.setText(null)
        c.setIcon(value? OK_ICON: NOT_OK_ICON)
        c
    }
}

Untuk memakai renderer di atas, saya mengubah kode program view menjadi seperti berikut ini:

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim')
                glazedColumn(name: 'Nama', property: 'nama')
                glazedColumn(name: 'Kelas', property: 'kelas')
                glazedColumn(name: 'Usia', expression: { Years.yearsBetween(it.tanggalLahir, LocalDate.now()).years },
                    columnClass: Integer)
                glazedColumn(name: 'Angkatan', expression: { it.tanggalDaftar.getYear() },
                        columnClass: Integer) {
                    templateRenderer(templateString: '${it}/${it+1}', horizontalAlignment: SwingConstants.CENTER)
                }
                glazedColumn(name: 'Tahun Lulus', expression: { it.tanggalLulus?.getYear() },
                        columnClass: Integer) {
                    templateRenderer(templateString: "\${it?:'Belum Lulus'}", horizontalAlignment: SwingConstants.CENTER) {
                        condition(if_: {value}, then_property_: 'foreground', is_: Color.BLACK, else_is_: Color.RED)
                        condition(if_: {isSelected}, then_: {component.foreground = Color.WHITE})
                    }
                }
                glazedColumn(name: 'Nilai Rata-Rata', expression: { it.nilaiRataRata() },
                        columnClass: Float) {
                    templateRenderer(templateString: "\${floatFormat(it,2)}", horizontalAlignment: SwingConstants.RIGHT){
                        condition(if_: {value < 60}, then_property_: 'background', is_: Color.RED)
                        condition(if_: {value >= 60 && value < 80}, then_property_: 'background', is_: Color.YELLOW)
                        condition(if_: {value >= 80}, then_property_: 'background', is_: Color.GREEN)
                        condition(if_: {isSelected}, then_property_: 'foreground', is_: Color.BLUE, else_is_: Color.BLACK)
                    }
                }
                glazedColumn(name: 'Nilai OK?', expression: { it.nilaiRataRata() > 70 },columnClass: Boolean) {
                    widget(new MyBooleanRenderer(), horizontalAlignment: SwingConstants.CENTER)
                }

            }
        }
    }
}

Tampilan kode program di atas akan terlihat seperti pada gambar berikut ini:

Memakai renderer buatan sendiri

Memakai renderer buatan sendiri

Renderer buatan sendiri juga dapat mendukung node condition() bila mereka diberi annotation @ConditionSupport seperti berikut ini:

@ConditionSupport
class MyBooleanRenderer extends DefaultTableCellRenderer {
  ...
}

Untuk mendukung node condition() di custom renderer di view, saya dapat menggunakan kode program seperti berikut ini:

...
glazedColumn(name: 'Nilai OK?', expression: { it.nilaiRataRata() > 70 },columnClass: Boolean) {
  customConditionalRenderer(new MyBooleanRenderer()) {
      condition(if_: {!value}, then_property_: 'text', is_: 'Warning!', else_is_: null)
  }
}
...

Jangan lupa bahwa ClosureRenderer bawaan dari Groovy juga masih dapat dipakai, misalnya dengan kode program seperti berikut ini:

...
glazedColumn() {
  cellRenderer {
    label()
    onRender {
       cell = 'text'
    }
  }
}
...

Node glazedColumn() selain menerima node yang berkaitan dengan cell renderer, juga menerima node yang mewakili header renderer seperti defaultHeaderRenderer. Sebagai contoh, berikut adalah kode program yang menambahkan icon pada judul di kolom ‘Angkatan’:

...
glazedColumn(name: 'Angkatan', expression: { it.tanggalDaftar.getYear() }, columnClass: Integer, width: 70) {
  templateRenderer(templateString: '${it}/${it+1}',
     horizontalAlignment: SwingConstants.CENTER)
  defaultHeaderRenderer(icon: imageIcon('/griffon-icon-16x16.png'),
     horizontalTextPosition: SwingConstants.RIGHT)
}
...

Hasilnya akan terlihat seperti pada gambar berikut ini:

Mengubah header renderer

Mengubah header renderer

Hal berikutnya yang sering saya lakukan saat bekerja dengan tabel adalah mengatur ukuran kolom.   Nodes glazedColumn mendukung atribut width yang sama seperti pada Groovy, yaitu dalam format [minWidth, preferredWidth, maxWidth].   Selain itu, sebuah nilai tunggal pada atribut width akan mengisi minWidth, preferredWidth, dan maxWidth dengan nilai yang sama.   Sebagai contoh, saya akan mengatur ukuran kolom dengan mengubah kode program view menjadi seperti berikut ini:

package project

import ...

application(title: 'Mahasiswa',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true) {

    panel(id: 'mainPanel') {
        borderLayout()

        scrollPane(constraints: CENTER) {
            glazedTable(list: model.mahasiswaList) {
                glazedColumn(name: 'Nim', property: 'nim', width: 50)
                glazedColumn(name: 'Nama', property: 'nama', width: [80,80])
                glazedColumn(name: 'Kelas', property: 'kelas', width: 50)
                glazedColumn(name: 'Usia', expression: { Years.yearsBetween(it.tanggalLahir, LocalDate.now()).years },
                    columnClass: Integer, width: 30)
                glazedColumn(name: 'Angkatan', expression: { it.tanggalDaftar.getYear() },
                        columnClass: Integer, width: 70) {
                    templateRenderer(templateString: '${it}/${it+1}', horizontalAlignment: SwingConstants.CENTER)
                }
                glazedColumn(name: 'Tahun Lulus', expression: { it.tanggalLulus?.getYear() },
                        columnClass: Integer, width: 100) {
                    templateRenderer(templateString: "\${it?:'Belum Lulus'}", horizontalAlignment: SwingConstants.CENTER) {
                        condition(if_: {value}, then_property_: 'foreground', is_: Color.BLACK, else_is_: Color.RED)
                        condition(if_: {isSelected}, then_: {component.foreground = Color.WHITE})
                    }
                }
                glazedColumn(name: 'Nilai Rata-Rata', expression: { it.nilaiRataRata() },
                        columnClass: Float) {
                    templateRenderer(templateString: "\${floatFormat(it,2)}", horizontalAlignment: SwingConstants.RIGHT){
                        condition(if_: {value < 60}, then_property_: 'background', is_: Color.RED)
                        condition(if_: {value >= 60 && value < 80}, then_property_: 'background', is_: Color.YELLOW)
                        condition(if_: {value >= 80}, then_property_: 'background', is_: Color.GREEN)
                        condition(if_: {isSelected}, then_property_: 'foreground', is_: Color.BLUE, else_is_: Color.BLACK)
                    }
                }
                glazedColumn(name: 'Nilai OK?', expression: { it.nilaiRataRata() > 70 }, columnClass: Boolean) {
                    customConditionalRenderer(new MyBooleanRenderer(), horizontalAlignment: SwingConstants.CENTER)
                }
            }
        }
    }
}

Tampilan view di atas akan terlihat seperti pada gambar berikut ini:

Mengatur ukuran kolom

Mengatur ukuran kolom

Berikutnya, saya akan menambahkan fasilitas pengurutan (sorting) untuk setiap kolom yang ada.   Saya dapat melakukannya hanya dengan mengubah satu baris kode program di view menjadi seperti berikut ini:

package project

import ...

application(...) {

   glazedTable(list: model.mahasiswaList, sortingStrategy: TableComparatorChooser.SINGLE_COLUMN) {
     ...
   }
}

Hasilnya akan terlihat seperti pada gambar berikut ini:

Menambahkan fasilitas sorting pada tabel

Menambahkan fasilitas sorting pada tabel

Walaupun yang saya definisikan di model adalah sebuah BasicEventList, node glazedTable() akan secara otomatis membuat SortedList dari BasicEventList tersebut bila atribut sortingStrategy diberikan.

Atribut lain dari node GlazedTable() adalah atribut onValueChanged.   Atribut ini akan menerima sebuah closure yang akan dikerjakan bila ada baris yang dipilih di tabel.   Closure ini nantinya akan menjadi implementasi dari ListSelectionListener yang diberikan pada JTable.selectionModel.addListSelectionListener().   Pada closure ini, saya dapat mengakses seluruh atribut dan method dari JTable (nilai dari variabel delegate merujuk pada JTable tersebut).   Sebagai contoh, kode program berikut ini akan menampilkan sebuah kotak dialog berisi nama mahasiswa yang dipilih setiap kali baris di tabel dipilih:

package project

import ...

application(...) {
   glazedTable(list: model.mahasiswaList, onValueChanged: {
      JOptionPane.showMessageDialog(delegate, "Anda memilih ${model.getValueAt(selectedRow,1)}")
   }) {
      ...
   }
}

Cara yang lebih MVC adalah memisahkan kode program yang melakukan aksi ke dalam controller, seperti pada contoh berikut ini:

// Isi pada MahasiswaView.groovy
package project

import ...

application(...) {
   glazedTable(list: model.mahasiswaList, onValueChanged: controller.tableValueChanged) {
      ...
   }
}

// Isi pada MahasiswaController.groovy
package project

import ...

@SimpleJpaTransaction
class MahasiswaController {

    def model
    def view

    ...

    def tableValueChanged = {
        JOptionPane.showMessageDialog(view.mainPanel, "Anda memilih ${model.getValueAt(selectedRow,1)}")
    }

}

Perihal Solid Snake
I'm nothing...

5 Responses to Membuat dan Mengatur JTable + GlazedLists Dengan Mudah Di simple-jpa 0.5

  1. Komang Hendra Santosa mengatakan:

    sudah terbitkah versi yang 0.5 mas?

  2. Tolhah Hamzah mengatakan:

    Permisi mas, saya mau tanya…

    saya menggunakan simple-jpa 0.6. saya sedang melakukan pemanggilan pada sebuah stored procedure yang saya buat, dan ingin menampilkan menggunakan glazed ini

    pada Controller ParamBukuTabunganController, saya memanggil stored procedure-nya menggunakan executeNativeQuery dalam method listAll

    void mvcGroupInit(Map args) {
    ……………..
    listAll()
    }

    void mvcGroupDestroy() {
        destroyEntityManager()
    }
    
    @Transaction(newSession = true)
    def listAll = {
        execInsideUISync {
            model.transaksiList.clear()
        }
    
        List transaksiResult = executeNativeQuery("CALL rpt_bukuTabungan(2031616)", [flushMode: FlushModeType.COMMIT])
    
        execInsideUISync {
            model.transaksiList.addAll(transaksiResult)
        }
    }
    

    lalu, pada model ParamBukuTabunganModel :

    package gfsimlkm6

    import domain.*
    import ca.odell.glazedlists.*
    import ca.odell.glazedlists.swing.*
    import groovy.beans.Bindable
    import org.joda.time.*
    import javax.swing.event.*
    import simplejpa.swing.*
    import org.jdesktop.swingx.combobox.EnumComboBoxModel
    //import project.MembershipSearch

    class ParamBukuTabunganModel {

    ......................
    
    ....................
    
    BasicEventList transaksiList = new BasicEventList()
    

    }

    kemudian, di view ParamBukuTabunganView nya:

    import javax.swing.JComponent
    import javax.swing.KeyStroke
    import javax.swing.SwingConstants
    import javax.swing.SwingUtilities
    import java.awt.event.KeyEvent

    import static ca.odell.glazedlists.gui.AbstractTableComparatorChooser.*
    import static javax.swing.SwingConstants.*
    import net.miginfocom.swing.MigLayout
    import org.joda.time.*
    import java.awt.*
    import org.jdesktop.swingx.prompt.PromptSupport

    application(title: ‘Parameter Cetak Buku Tabungan’,
    preferredSize: [320, 240],
    pack: true,
    //location: [50,50],
    locationByPlatform: true,
    iconImage: imageIcon(‘/griffon-icon-48×48.png’).image,
    iconImages: [imageIcon(‘/griffon-icon-48×48.png’).image,
    imageIcon(‘/griffon-icon-32×32.png’).image,
    imageIcon(‘/griffon-icon-16×16.png’).image]) {

    panel(id: ‘mainPanel’, layout: new MigLayout(‘hidemode 2’, ‘[right][left,grow]’, ”)) {

            scrollPane(constraints: 'wrap') {
                glazedTable(list: model.transaksiList) {
                    glazedColumn(name: 'Debet', property: 'debet')
                    glazedColumn(name: 'Kredit', property: 'kredit')
                    glazedColumn(name: 'Saldo', property: 'saldo')
                    glazedColumn(name: 'ID', property: 'id')
                }
            }
    

    }
    }

    Saya ingin menampilkan hasil query dari stored procedure yang saya buat. seharusnya akan muncul data-data transaksi yang ada…
    tetapi sepertinya glazed table tidak dapat membaca property yang dihasilkan oleh query (seperti: debet, kredit, saldo, id….)

    dan muncul dialog error (berulang2) yang berisi tulisan seperti ini:

    Exception evaluating property ‘debet’ for java.util.Arays:$ArrayList, Reason: groovy.lang.MissingPropertyException: no such property debet for class : java.sql.Timestamp
    Possible solution: date

    itu kenapa ya mas?
    apakah executeNativeQuery tidak dapat dipadukan dengan glazedTable ?
    mohon bantuannya mas, terima kasih banyak : )

    • Solid Snake mengatakan:

      executeNativeQuery() akan mengembalikan List yang berisi array, bukan domain object. Hal ini karena pengguna dapat melakukan query SQL secara bebas.

      Sementara itu, glazedTable() hanya mendukung List yang berisi domain object. Misalnya, glazedColumn(name: 'debet') akan memanggil getDebet() dari masing-masing object di dalam List.

      Solusinya, bila memungkinkan, konversi hasil native query dari array menjadi domain object. Atau, bila ingin tetap memakai array, coba pakai table() bawaan Groovy yang membuat JTable biasa.

  3. River Rita Ferdian mengatakan:

    mas,,tutorialnya sangat membantu,,tapi bisa gx ya kalau listing ini di pakai di netbeans,,?? Kalau bisa,,gimana ya mas caranya..?? Saya butuh sekali tutorial yg seperti ini untuk netbeans.. trims

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: