Setiap akhir semester, meja guru penuh dengan daftar nilai tugas, UTS, dan UAS. Semua angka itu kemudian harus berubah menjadi nilai akhir, predikat, status tuntas, dan ranking kelas. Kalau masih memakai kalkulator dan buku leger, satu kelas saja bisa menyita satu malam. Karena itu, tutorial ini mengajak Anda membangun aplikasi nilai siswa VB.NET yang mengerjakan seluruh hitungan tersebut secara otomatis, mulai dari bobot nilai dan KKM sampai cetak rekap A4.
Aplikasi ini bernama SC Nilai Siswa. Project-nya memakai Windows Forms .NET 8 tanpa database dan tanpa paket NuGet, karena seluruh data tersimpan sebagai file JSON di samping file .exe. Source code lengkapnya gratis dan berlisensi MIT, jadi Anda bisa langsung download SC Nilai Siswa VB.NET di sharecode.id lalu mencocokkannya dengan setiap langkah di bawah.

Apa yang Akan Kita Bangun
Sebelum menulis kode, mari lihat dulu hasil akhirnya. Dengan begitu, Anda tahu arah setiap class dan form yang akan kita buat nanti.
- Input nilai Tugas, UTS, dan UAS (0–100, satu desimal) per siswa dengan NIS unik.
- Nilai akhir otomatis berdasarkan bobot, lengkap dengan pratinjau langsung saat guru mengetik angka.
- Predikat A/B/C/D serta keterangan Tuntas atau Belum Tuntas berwarna sesuai KKM.
- Ranking kelas otomatis, sehingga nilai yang sama mendapat peringkat yang sama (1, 2, 2, 4).
- Kartu statistik berisi jumlah siswa, rata-rata kelas, nilai tertinggi, nilai terendah, dan persentase tuntas.
- Pencarian berdasarkan NIS atau nama, ekspor CSV untuk Excel, serta cetak rekap A4 lewat Print Preview.
Persiapan: Visual Studio 2022 dan .NET 8
Pertama, pasang Visual Studio 2022 versi 17.8 atau lebih baru (edisi Community gratis) dengan workload .NET desktop development. Versi 17.8 menjadi batas minimum karena dukungan .NET 8 baru masuk pada rilis tersebut. Sebaliknya, Visual Studio 2019 sama sekali tidak bisa membuka project .NET 8. Jika Anda lebih suka terminal, cukup pasang .NET 8 SDK lalu jalankan perintah dotnet.
Membuat Project WinForms VB dari Nol
- Buka Visual Studio 2022, lalu pilih Create a new project.
- Ketik “Windows Forms App” pada kotak pencarian, kemudian pilih template berbahasa Visual Basic (bukan C#, juga bukan versi .NET Framework).
- Isi nama project
SCNilaiSiswa, tentukan folder, lalu klik Next. - Pilih framework .NET 8.0 (Long Term Support), setelah itu klik Create.
- Hapus
Form1.vbbawaan, lalu buat folderFormsuntuk menampung FormUtama dan FormPengaturan.
Lewat terminal, perintah setaranya adalah dotnet new winforms -lang VB -n SCNilaiSiswa. Setelah itu, buka file .vbproj dan sesuaikan isinya seperti bagian berikut.
Isi File SCNilaiSiswa.vbproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>SCNilaiSiswa</RootNamespace>
<AssemblyName>SCNilaiSiswa</AssemblyName>
<StartupObject>SCNilaiSiswa.Program</StartupObject>
<MyType>WindowsFormsWithCustomSubMain</MyType>
<OptionExplicit>On</OptionExplicit>
<OptionStrict>On</OptionStrict>
<OptionInfer>On</OptionInfer>
<Product>SC Nilai Siswa VB.NET</Product>
<Version>1.0.0</Version>
</PropertyGroup>
<ItemGroup>
<Import Include="System.Drawing" />
<Import Include="System.Windows.Forms" />
</ItemGroup>
</Project>
Ada tiga bagian penting di file ini. Pertama, net8.0-windows bersama UseWindowsForms mengaktifkan Windows Forms. Kedua, MyType bernilai WindowsFormsWithCustomSubMain karena kita memakai Sub Main sendiri di Program.vb, bukan Application Framework bawaan VB. Ketiga, OptionStrict On memaksa setiap konversi tipe tertulis jelas, sehingga kesalahan seperti menjumlahkan String dengan Decimal langsung tertangkap saat build.
Struktur Folder dan File
sc-nilai-siswa-vbnet/
├── SCNilaiSiswa.sln
└── SCNilaiSiswa/
├── SCNilaiSiswa.vbproj
├── Program.vb Model.vb Penyimpanan.vb
├── Repo.vb Rekap.vb Util.vb
└── Forms/
├── FormUtama.vb / .Designer.vb
└── FormPengaturan.vb / .Designer.vb
| File | Fungsi |
|---|---|
| Program.vb | Titik masuk, budaya id-ID, memuat data, membuka FormUtama |
| Model.vb | Class Siswa dan Pengaturan |
| Penyimpanan.vb | Muat dan simpan JSON di folder data |
| Repo.vb | Data di memori, rumus nilai akhir, predikat, tuntas, ranking, data contoh |
| Rekap.vb | Cetak rekap A4 multi halaman lewat PrintDocument |
| Util.vb | Warna tema, gaya DataGridView, kotak pesan, pembungkus CSV |
| Forms\FormUtama | Layar utama: input, tabel, statistik, ekspor, cetak |
| Forms\FormPengaturan | Identitas kelas, bobot penilaian, KKM |
Pembagian ini sengaja ketat: form tidak pernah menulis file, sedangkan Repo tidak pernah menyentuh kontrol. Akibatnya, rumus penilaian mudah Anda ganti tanpa membongkar tampilan.
Arsitektur Aplikasi Nilai Siswa VB.NET

Diagram di atas memperlihatkan empat lapisan. Lapisan teratas berisi FormUtama, FormPengaturan, dan class Rekap untuk mencetak. Ketiganya hanya membaca DaftarSiswa serta Kelas, lalu memanggil fungsi milik module Repo.
Selanjutnya, Repo menampung data di memori sekaligus memegang semua rumus. Repo kemudian meminta module Penyimpanan membaca atau menulis JSON lewat System.Text.Json. Terakhir, file fisik berada di folder data\ di samping SCNilaiSiswa.exe.
Keputusan desain terpenting ada di sini: nilai akhir, predikat, dan ranking tidak ikut tersimpan. Ketiganya bergantung pada bobot dan KKM, jadi Repo selalu menghitungnya ulang. Dengan demikian, saat guru mengganti bobot, seluruh tabel langsung berubah tanpa perlu memperbarui data lama.
Langkah 1: Program.vb dan Budaya Angka Indonesia
Friend Module Program
<STAThread>
Friend Sub Main()
' Format angka & tanggal Indonesia (85,50 - 07 November 2026)
Dim budaya As New CultureInfo("id-ID")
CultureInfo.DefaultThreadCurrentCulture = budaya
CultureInfo.DefaultThreadCurrentUICulture = budaya
Thread.CurrentThread.CurrentCulture = budaya
Thread.CurrentThread.CurrentUICulture = budaya
Application.SetHighDpiMode(HighDpiMode.SystemAware)
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
AddHandler Application.ThreadException,
Sub(s, e) Galat("Terjadi kesalahan: " & e.Exception.Message)
Repo.MuatSemua()
Application.Run(New FormUtama())
End Sub
End Module
Empat baris pertama mengatur budaya id-ID untuk semua thread. Hasilnya, angka tampil sebagai 85,50 dengan koma desimal, sedangkan tanggal tampil sebagai 08 November 2026. Selain itu, Application.ThreadException menangkap error yang lolos lalu menampilkannya lewat kotak pesan Galat, sehingga aplikasi tidak langsung tertutup. Terakhir, Repo.MuatSemua() membaca data sebelum FormUtama terbuka.
Langkah 2: Model Data Siswa dan Pengaturan
Public Class Siswa
Public Property Nis As String = ""
Public Property Nama As String = ""
Public Property NilaiTugas As Decimal
Public Property NilaiUts As Decimal
Public Property NilaiUas As Decimal
End Class
' Identitas kelas, bobot, dan KKM
Public Class Pengaturan
Public Property NamaSekolah As String = "SMA Sharecode Mandiri"
Public Property Kelas As String = "XI IPA 1"
Public Property MataPelajaran As String = "Matematika"
Public Property TahunAjaran As String = "2026/2027"
Public Property Semester As String = "Ganjil"
Public Property Guru As String = "Nur Sharecode, S.Pd."
' ... NipGuru dan Kota
' Bobot dalam persen, totalnya wajib 100
Public Property BobotTugas As Integer = 30
Public Property BobotUts As Integer = 30
Public Property BobotUas As Integer = 40
Public Property Kkm As Decimal = 75
End Class
Class Siswa hanya memuat lima property, yaitu NIS, nama, dan tiga nilai mentah. Tipe Decimal sengaja kita pilih ketimbang Double karena nilai rapor butuh pembulatan presisi, misalnya 82,35 tidak boleh berubah menjadi 82,34999.
Sementara itu, class Pengaturan menampung identitas kelas, bobot dalam persen, dan KKM. Nilai default-nya langsung terisi lewat inisialisasi property, sehingga pengaturan.json yang belum ada tetap menghasilkan objek lengkap. Gambar berikut memperlihatkan bentuk JSON dari kedua class tersebut.

Langkah 3: Penyimpanan JSON dengan System.Text.Json
Module Penyimpanan adalah satu-satunya bagian yang menyentuh disk. Isinya generik, jadi satu pasang method cukup untuk dua file yang berbeda bentuk.
Folder data dan Method Muat(Of T)
Private ReadOnly Opsi As New JsonSerializerOptions With {.WriteIndented = True}
Public ReadOnly Property FolderData As String
Get
Dim folder = Path.Combine(AppContext.BaseDirectory, "data")
Directory.CreateDirectory(folder)
Return folder
End Get
End Property
' Baca file JSON menjadi objek bertipe T
Public Function Muat(Of T As {Class, New})(namaFile As String) As T
Dim lokasi = Path.Combine(FolderData, namaFile)
If Not File.Exists(lokasi) Then Return New T()
Try
Dim hasil = JsonSerializer.Deserialize(Of T)(File.ReadAllText(lokasi), Opsi)
Return If(hasil, New T())
Catch ex As JsonException
' File rusak: simpan salinannya agar data lama tidak hilang begitu saja
File.Copy(lokasi, lokasi & ".rusak-" & DateTime.Now.ToString("yyyyMMddHHmmss"), True)
Galat("File " & namaFile & " rusak dan tidak bisa dibaca." & vbCrLf &
"Salinannya sudah disimpan di folder data, aplikasi memakai data kosong.")
Return New T()
End Try
End Function
Property FolderData menggabungkan AppContext.BaseDirectory dengan nama folder data, lalu membuat folder itu bila belum ada. Kemudian, Muat(Of T) mengembalikan objek baru jika file belum ada. Constraint {Class, New} memungkinkan pemanggilan New T() di dalam method generik.
Blok Catch menangani JSON yang rusak, misalnya akibat guru mengedit file lewat Notepad. Alih-alih membuang data, method ini menyalin file rusak ke nama berakhiran .rusak-waktu, memberi peringatan, lalu memakai data kosong.
Menyimpan Aman Lewat File .tmp
Public Sub Simpan(Of T)(namaFile As String, data As T)
Dim lokasi = Path.Combine(FolderData, namaFile)
Dim sementara = lokasi & ".tmp"
' Tulis ke file sementara dulu, baru ganti file asli (aman bila listrik mati)
File.WriteAllText(sementara, JsonSerializer.Serialize(data, Opsi))
File.Move(sementara, lokasi, True)
End Sub
Method Simpan(Of T) tidak langsung menimpa siswa.json. Sebaliknya, method ini menulis ke file .tmp terlebih dahulu, lalu File.Move dengan parameter True mengganti file asli. Jika listrik padam saat proses menulis, file lama tetap utuh karena hanya file sementara yang rusak.
Langkah 4: Repo, Rumus Nilai Akhir, Predikat, dan Ranking
Module Repo adalah jantung aplikasi. Selain menampung data di memori, module ini memegang seluruh rumus penilaian, sehingga Anda cukup mengubah satu file bila kurikulum sekolah berbeda.
Memuat Data dan Mengisi Data Contoh
Private Const FILE_SISWA As String = "siswa.json"
Private Const FILE_PENGATURAN As String = "pengaturan.json"
' Data kelas di memori
Public DaftarSiswa As New List(Of Siswa)()
Public Kelas As New Pengaturan()
Public Sub MuatSemua()
Dim pertamaKali = Not AdaFile(FILE_SISWA)
DaftarSiswa = Muat(Of List(Of Siswa))(FILE_SISWA)
Kelas = Muat(Of Pengaturan)(FILE_PENGATURAN)
If pertamaKali Then
IsiDataContoh()
SimpanSiswa()
SimpanPengaturan()
End If
End Sub
' Cari siswa berdasarkan NIS (huruf besar/kecil tidak berpengaruh)
Public Function CariSiswa(nis As String) As Siswa
Return DaftarSiswa.FirstOrDefault(
Function(s) String.Equals(s.Nis, nis.Trim(), StringComparison.OrdinalIgnoreCase))
End Function
Variabel pertamaKali mencatat apakah siswa.json sudah ada sebelum proses muat. Jika belum, IsiDataContoh menambahkan 12 siswa contoh lalu langsung menyimpannya, jadi aplikasi tidak tampil kosong saat pertama kali jalan. Fungsi CariSiswa memakai OrdinalIgnoreCase dan Trim(), sehingga NIS dengan spasi di ujung tetap cocok.
Rumus Nilai Akhir Berbobot
''' <summary>
''' Nilai akhir = (Tugas x bobot + UTS x bobot + UAS x bobot) / 100,
''' dibulatkan 2 desimal.
''' </summary>
Public Function NilaiAkhir(s As Siswa) As Decimal
Dim total = s.NilaiTugas * Kelas.BobotTugas +
s.NilaiUts * Kelas.BobotUts +
s.NilaiUas * Kelas.BobotUas
Return Math.Round(total / 100D, 2, MidpointRounding.AwayFromZero)
End Function
Rumus ini mengalikan setiap nilai dengan bobotnya, menjumlahkan hasilnya, lalu membaginya dengan 100. Misalnya Budi mendapat Tugas 80, UTS 85, dan UAS 82 dengan bobot 30/30/40. Hasilnya adalah (2400 + 2550 + 3280) / 100 = 82,30.
Perhatikan juga MidpointRounding.AwayFromZero. Secara default .NET memakai pembulatan bankir, sehingga 0,125 menjadi 0,12. Padahal guru terbiasa membulatkan setengah ke atas, jadi opsi ini membuat 0,125 menjadi 0,13 seperti hitungan manual.
Predikat dan Status Tuntas KKM
''' <summary>A: 90-100, B: 80-89,99, C: 70-79,99, D: di bawah 70.</summary>
Public Function Predikat(nilai As Decimal) As String
If nilai >= 90 Then Return "A"
If nilai >= 80 Then Return "B"
If nilai >= 70 Then Return "C"
Return "D"
End Function
' Tuntas bila nilai akhir mencapai KKM kelas
Public Function Tuntas(nilai As Decimal) As Boolean
Return nilai >= Kelas.Kkm
End Function
Fungsi Predikat memeriksa ambang mulai dari yang tertinggi. Karena setiap If langsung Return, urutan pemeriksaan menentukan hasil: 92 berhenti di A, sedangkan 78,7 lolos sampai C. Sementara itu, Tuntas membandingkan nilai dengan KKM milik pengaturan kelas, jadi KKM 75 atau 70 cukup Anda ubah dari FormPengaturan.
Ranking Kompetisi 1-2-2-4
''' <summary>
''' Ranking kompetisi (1-2-2-4): nilai akhir sama mendapat peringkat sama,
''' peringkat berikutnya melompati jumlah siswa yang seri. Kunci = NIS.
''' </summary>
Public Function HitungRanking() As Dictionary(Of String, Integer)
Dim hasil As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
Dim nilai = DaftarSiswa.Select(Function(s) New With {s.Nis, .Akhir = NilaiAkhir(s)}).ToList()
For Each n In nilai
Dim akhir = n.Akhir
hasil(n.Nis) = 1 + nilai.Where(Function(x) x.Akhir > akhir).Count()
Next
Return hasil
End Function
Ranking memakai pola kompetisi, sama seperti klasemen olahraga. Untuk setiap siswa, peringkatnya sama dengan 1 plus jumlah siswa yang nilainya lebih tinggi. Akibatnya, Budi dan Fajar yang sama-sama 82,30 berbagi peringkat 5, sedangkan Ahmad dengan 81,70 langsung mendapat peringkat 7.
Hasilnya berupa Dictionary berkunci NIS, sehingga tabel, CSV, dan rekap cetak cukup memanggil rank(s.Nis). Kompleksitasnya memang O(n²), namun untuk satu kelas berisi 30–50 siswa prosesnya tetap instan.

Langkah 5: Mendesain FormUtama
FormUtama berukuran 1100×660 piksel dengan tiga area, yaitu header gelap di atas, panel input di kiri, dan area isi di kanan. Semua kontrol berasal dari Form Designer, sehingga Anda bisa menggeser posisinya tanpa menyentuh kode.
Daftar Kontrol FormUtama
| Nama kontrol | Tipe | Fungsi |
|---|---|---|
| pnlHeader, lblIdentitas | Panel (Dock Top), Label | Header dan identitas kelas |
| btnCetak, btnEkspor, btnPengaturan | Button | Cetak rekap, ekspor CSV, buka pengaturan |
| txtNis, txtNama | TextBox | NIS dan nama siswa |
| numTugas, numUts, numUas | NumericUpDown (DecimalPlaces 1) | Nilai 0–100 |
| lblPratinjau | Label | Pratinjau nilai akhir dan predikat |
| btnSimpan, btnBaru, btnHapus | Button | Simpan, kosongkan form, hapus siswa |
| lblJumlah, lblRata, lblTertinggi, lblTerendah, lblTuntas | Label | Isi lima kartu statistik |
| txtCari, lblBobot | TextBox, Label | Pencarian dan ringkasan bobot |
| dgvSiswa | DataGridView | Sepuluh kolom, colNo sampai colRanking |
Aktifkan juga property KeyPreview = True pada form agar pintasan Ctrl+S bekerja dari kontrol mana pun.
Event Load dan Pratinjau Nilai Akhir
Private Sub FormUtama_Load(sender As Object, e As EventArgs) Handles MyBase.Load
GayaGrid(dgvSiswa)
RataKanan(colTugas, colUts, colUas, colAkhir)
RataTengah(colNo, colPredikat, colKet, colRanking)
colAkhir.DefaultCellStyle.Format = "N2"
' ... format 0.# untuk kolom Tugas, UTS, UAS
For Each n In {numTugas, numUts, numUas}
n.Maximum = 100
AddHandler n.ValueChanged, AddressOf PerbaruiPratinjau
Next
TampilkanIdentitas()
TampilkanData()
SiswaBaru()
End Sub
' Hitung nilai akhir sementara setiap kali angka berubah
Private Sub PerbaruiPratinjau(sender As Object, e As EventArgs)
Dim s As New Siswa With {.NilaiTugas = numTugas.Value, .NilaiUts = numUts.Value, .NilaiUas = numUas.Value}
Dim akhir = NilaiAkhir(s)
lblPratinjau.Text = $"Nilai akhir: {akhir:N2} ({Predikat(akhir)})"
lblPratinjau.ForeColor = If(Tuntas(akhir), Warna.Sukses, Warna.Bahaya)
End Sub
Saat form terbuka, GayaGrid dari Util.vb memberi gaya seragam pada tabel, lalu RataKanan dan RataTengah mengatur perataan kolom. Loop For Each kemudian memasang satu handler ValueChanged untuk ketiga NumericUpDown. Akibatnya, setiap kali guru mengetik angka, PerbaruiPratinjau menghitung nilai akhir dan mewarnai label hijau atau merah.
Menampilkan Tabel dan Warna Ketuntasan
Private Sub TampilkanData()
Dim rank = HitungRanking()
Dim kata = txtCari.Text.Trim()
Dim data = DaftarSiswa.AsEnumerable()
If kata <> "" Then
data = data.Where(Function(s) s.Nis.Contains(kata, StringComparison.OrdinalIgnoreCase) OrElse
s.Nama.Contains(kata, StringComparison.OrdinalIgnoreCase))
End If
' Isi ulang tabel, urut nama
dgvSiswa.Rows.Clear()
Dim no = 0
For Each s In data.OrderBy(Function(x) x.Nama)
no += 1
Dim akhir = NilaiAkhir(s)
Dim lulus = Tuntas(akhir)
Dim i = dgvSiswa.Rows.Add(no, s.Nis, s.Nama, s.NilaiTugas, s.NilaiUts, s.NilaiUas, akhir,
Predikat(akhir), If(lulus, "Tuntas", "Belum Tuntas"), rank(s.Nis))
' Hijau untuk Tuntas, merah untuk Belum Tuntas
Dim sel = dgvSiswa.Rows(i).Cells(colKet.Index)
sel.Style.ForeColor = If(lulus, Warna.Sukses, Warna.Bahaya)
sel.Style.SelectionForeColor = sel.Style.ForeColor
Next
dgvSiswa.ClearSelection()
TampilkanStatistik()
End Sub
Pertama, method ini menghitung ranking untuk seluruh kelas, bukan hanya hasil pencarian. Dengan begitu, peringkat tetap benar walaupun tabel sedang tersaring. Selanjutnya, sel keterangan mendapat warna hijau atau merah. Baris SelectionForeColor menyamakan warna saat baris terpilih, sehingga teks merah tidak berubah hitam ketika guru mengklik baris.
Kartu Statistik Kelas
Private Sub TampilkanStatistik()
Dim nilai = DaftarSiswa.Select(Function(s) NilaiAkhir(s)).ToList()
lblJumlah.Text = nilai.Count.ToString()
If nilai.Count = 0 Then
lblRata.Text = "-" : lblTertinggi.Text = "-" : lblTerendah.Text = "-" : lblTuntas.Text = "-"
Return
End If
' Rata-rata, tertinggi, terendah, dan jumlah tuntas
Dim jumlahTuntas = nilai.Where(Function(n) Tuntas(n)).Count()
lblRata.Text = nilai.Average().ToString("N2")
lblTertinggi.Text = nilai.Max().ToString("N2")
lblTerendah.Text = nilai.Min().ToString("N2")
lblTuntas.Text = $"{jumlahTuntas} ({jumlahTuntas * 100.0 / nilai.Count:0}%)"
End Sub
Kartu statistik memakai LINQ, yaitu Average, Max, dan Min atas daftar nilai akhir. Namun, ketiga fungsi itu melempar exception jika daftar kosong, karena itu ada pemeriksaan nilai.Count = 0 di awal. Perkalian dengan 100.0 mengubah hasil menjadi Double, lalu format :0 membulatkan persentase tanpa desimal.
Menyimpan Siswa, NIS Unik, dan Mode Ubah
Private Sub Simpan()
Dim nis = txtNis.Text.Trim()
Dim nama = txtNama.Text.Trim()
If nis = "" Then Peringatan("NIS wajib diisi.") : txtNis.Focus() : Return
If nama = "" Then Peringatan("Nama siswa wajib diisi.") : txtNama.Focus() : Return
' Siswa baru atau ubah nilai?
Dim s = CariSiswa(nis)
If s Is Nothing Then
s = New Siswa With {.Nis = nis}
DaftarSiswa.Add(s)
ElseIf Not _modeUbah Then
Peringatan($"NIS {nis} sudah dipakai oleh ""{s.Nama}"". Klik barisnya di tabel bila ingin mengubah nilai.")
txtNis.Focus()
Return
End If
s.Nama = nama
s.NilaiTugas = numTugas.Value
s.NilaiUts = numUts.Value
s.NilaiUas = numUas.Value
SimpanSiswa()
TampilkanData()
SiswaBaru()
lblInfo.Text = $"✔ Tersimpan: {nis} - {nama} (nilai akhir {NilaiAkhir(s):N2})"
End Sub
Validasi awal memastikan NIS dan nama terisi. Setelah itu, CariSiswa menentukan alur. Jika NIS belum ada, method membuat Siswa baru. Namun, jika NIS sudah ada sementara form tidak dalam mode ubah, method menolak karena NIS harus unik. Terakhir, SimpanSiswa menulis JSON, tabel menyegarkan diri, dan form kembali kosong.
Klik Baris, Hapus, dan Pintasan Ctrl+S
Private Sub dgvSiswa_CellClick(sender As Object, e As DataGridViewCellEventArgs) Handles dgvSiswa.CellClick
If e.RowIndex < 0 Then Return
Dim nis = Convert.ToString(dgvSiswa.Rows(e.RowIndex).Cells(colNis.Index).Value)
Dim s = CariSiswa(nis)
If s IsNot Nothing Then IsiForm(s)
End Sub
' Hapus hanya berlaku dalam mode ubah
Private Sub Hapus()
If Not _modeUbah Then Return
Dim s = CariSiswa(txtNis.Text)
If s Is Nothing Then Return
If Not Tanya($"Hapus data nilai ""{s.Nama}"" (NIS {s.Nis})?") Then Return
DaftarSiswa.Remove(s)
SimpanSiswa()
TampilkanData()
SiswaBaru()
End Sub
' Ctrl+S dari kontrol mana pun (KeyPreview = True)
Private Sub FormUtama_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
If e.Control AndAlso e.KeyCode = Keys.S Then
e.SuppressKeyPress = True
Simpan()
End If
End Sub
Event CellClick mengambil NIS dari baris yang terklik lalu memanggil IsiForm. Method tersebut mengunci txtNis agar NIS tidak berubah, mengaktifkan tombol Hapus, dan menyalakan mode ubah. Sebelum menghapus, Tanya menampilkan konfirmasi Ya/Tidak. Sementara itu, SuppressKeyPress mencegah bunyi “ding” bawaan Windows.
Langkah 6: FormPengaturan dan Bobot Wajib 100%

FormPengaturan terbuka sebagai dialog modal dari tombol Pengaturan. Bagian atas berisi identitas kelas untuk header dan kop rekap, sedangkan bagian bawah berisi bobot serta KKM.
| Nama kontrol | Tipe | Fungsi |
|---|---|---|
| txtSekolah, txtKelas, txtMapel, txtTahun | TextBox | Identitas kelas |
| cboSemester | ComboBox | Ganjil atau Genap |
| txtGuru, txtNip, txtKota | TextBox | Data tanda tangan rekap |
| numBTugas, numBUts, numBUas, numKkm | NumericUpDown | Bobot persen dan KKM |
| lblTotal | Label | Total bobot berwarna |
| btnSimpan, btnBatal | Button | AcceptButton dan CancelButton |
Validasi Total Bobot
Private Function TotalBobot() As Integer
Return CInt(numBTugas.Value + numBUts.Value + numBUas.Value)
End Function
' Satu handler untuk tiga NumericUpDown
Private Sub PerbaruiTotal(sender As Object, e As EventArgs) Handles numBTugas.ValueChanged, numBUts.ValueChanged, numBUas.ValueChanged
Dim total = TotalBobot()
If total = 100 Then
lblTotal.Text = "Total bobot: 100% ✔"
lblTotal.ForeColor = Warna.Sukses
Else
lblTotal.Text = $"Total bobot: {total}% (harus tepat 100%)"
lblTotal.ForeColor = Warna.Bahaya
End If
End Sub
' Tombol Simpan (dipangkas)
Private Sub btnSimpan_Click(sender As Object, e As EventArgs) Handles btnSimpan.Click
' ... validasi sekolah, kelas, mapel
If TotalBobot() <> 100 Then
Peringatan($"Total bobot Tugas + UTS + UAS harus 100%, sekarang {TotalBobot()}%.")
Return
End If
' ... salin semua isian ke Kelas
Kelas.Kkm = numKkm.Value
SimpanPengaturan()
DialogResult = DialogResult.OK
Close()
End Sub
Klausa Handles berisi tiga kontrol sekaligus, jadi satu method cukup menangani ketiganya. Label total berubah hijau saat jumlahnya tepat 100, sebaliknya merah bila belum. Setelah dialog tertutup dengan DialogResult.OK, FormUtama memanggil TampilkanData lagi, sehingga seluruh nilai akhir dan ranking langsung mengikuti bobot baru.
Langkah 7: Ekspor CSV yang Rapi di Excel
Banyak sekolah tetap mengolah leger di Excel. Oleh karena itu, tombol Ekspor CSV menghasilkan file yang langsung rapi saat Anda membukanya di Excel versi Indonesia.
Membungkus Nilai dengan Fungsi Csv
''' <summary>Membungkus nilai CSV (pemisah titik koma, cocok untuk Excel Indonesia).</summary>
Public Function Csv(nilai As Object) As String
Dim teks = Convert.ToString(nilai, Indo)
If teks.IndexOfAny({";"c, """"c, ControlChars.Cr, ControlChars.Lf}) >= 0 Then
teks = """" & teks.Replace("""", """""") & """"
End If
Return teks
End Function
Excel berbahasa Indonesia memakai titik koma sebagai pemisah daftar, karena koma sudah menjadi pemisah desimal. Fungsi Csv mengubah nilai memakai budaya id-ID, lalu membungkusnya dengan tanda kutip bila teks mengandung titik koma, kutip, atau baris baru. Selain itu, kutip di dalam teks berubah menjadi dua kutip sesuai standar CSV.
Method EksporCsv
Using dlg As New SaveFileDialog()
dlg.Filter = "File CSV (*.csv)|*.csv"
dlg.FileName = $"nilai_{Kelas.Kelas}_{Kelas.MataPelajaran}.csv".Replace(" ", "_")
If dlg.ShowDialog(Me) <> DialogResult.OK Then Return
Dim rank = HitungRanking()
Dim baris As New List(Of String) From {
$"Rekap Nilai;{Csv(Kelas.NamaSekolah)}",
' ... baris identitas kelas, bobot, dan KKM
"",
"No;NIS;Nama;Tugas;UTS;UAS;Nilai Akhir;Predikat;Keterangan;Ranking"
}
' Satu baris per siswa, urut nama
Dim no = 0
For Each s In DaftarSiswa.OrderBy(Function(x) x.Nama)
no += 1
Dim akhir = NilaiAkhir(s)
baris.Add(String.Join(";", no, Csv(s.Nis), Csv(s.Nama), Csv(s.NilaiTugas), Csv(s.NilaiUts), Csv(s.NilaiUas),
Csv(akhir), Predikat(akhir), If(Tuntas(akhir), "Tuntas", "Belum Tuntas"), rank(s.Nis)))
Next
' UTF-8 dengan BOM agar terbaca benar saat dibuka di Excel
File.WriteAllLines(dlg.FileName, baris, New UTF8Encoding(True))
End Using
Beberapa baris pertama berisi kop, yaitu nama sekolah, identitas kelas, bobot, KKM, lalu judul kolom. Setelah itu, setiap siswa menghasilkan satu baris berurutan nama. Bagian terpenting ada di New UTF8Encoding(True), yaitu UTF-8 dengan BOM. Tanpa BOM, Excel sering salah membaca nama yang memuat huruf khusus.
Langkah 8: Cetak Rekap A4 dengan PrintDocument
Class Rekap menggambar laporan langsung dengan GDI+ di atas PrintDocument. Cara ini memang lebih panjang ketimbang report designer, namun hasilnya tidak butuh paket tambahan dan tetap jalan di semua edisi Visual Studio.
Membuat Dokumen dan Print Preview
Public Shared Function BuatDokumen() As PrintDocument
Dim r As New Rekap()
Dim doc As New PrintDocument()
doc.DocumentName = $"Rekap Nilai {Kelas.Kelas} - {Kelas.MataPelajaran}"
doc.DefaultPageSettings.PaperSize = New PaperSize("A4", 827, 1169)
doc.DefaultPageSettings.Margins = New Margins(50, 50, 50, 40)
AddHandler doc.BeginPrint, AddressOf r.Mulai
AddHandler doc.PrintPage, AddressOf r.CetakHalaman
AddHandler doc.Disposed, Sub(s, e) r.Bersihkan()
Return doc
End Function
' Tampilkan jendela pratinjau
Public Shared Sub Tampilkan(pemilik As IWin32Window)
Using doc = BuatDokumen(), dlg As New PrintPreviewDialog()
dlg.Document = doc
dlg.UseAntiAlias = True
' ... judul, ukuran, posisi jendela
Try
dlg.ShowDialog(pemilik)
Catch ex As InvalidPrinterException
Peringatan("Tidak ada printer yang terpasang. Pasang printer (atau Microsoft Print to PDF) untuk mencetak rekap.")
End Try
End Using
End Sub
Ukuran kertas memakai satuan seperseratus inci, jadi 827×1169 setara dengan A4. Handler BeginPrint mereset indeks siswa setiap kali proses cetak mulai, karena dokumen yang sama bisa melewati pratinjau lalu printer. Selain itu, blok Catch InvalidPrinterException memberi pesan ramah bila komputer belum punya printer sama sekali.
Halaman Bersambung Otomatis
Private Sub CetakHalaman(sender As Object, e As PrintPageEventArgs)
Dim g = e.Graphics
Dim area = e.MarginBounds
Dim y As Single = area.Top
Dim batasBawah As Single = area.Bottom - TINGGI_KAKI
_halaman += 1
' ... kop di halaman 1, judul "(lanjutan)" di halaman berikutnya
' Tabel siswa
If _indeks < _data.Count Then
y = BarisTabel(g, area, y, Nothing)
While _indeks < _data.Count AndAlso y + TINGGI_BARIS <= batasBawah
y = BarisTabel(g, area, y, _data(_indeks))
_indeks += 1
End While
End If
' Ringkasan + tanda tangan bila semua siswa sudah tercetak dan masih muat
If _indeks >= _data.Count AndAlso y + 16 + TINGGI_PENUTUP <= batasBawah Then
Penutup(g, area, y + 16)
_penutupSelesai = True
End If
' ... kaki halaman: waktu cetak dan nomor halaman
e.HasMorePages = Not _penutupSelesai
End Sub
Variabel batasBawah menyisakan ruang untuk kaki halaman. Loop While menggambar baris siswa sampai ruang habis, lalu _indeks mengingat posisi terakhir. Jika masih ada sisa, HasMorePages = True membuat .NET memanggil event ini lagi untuk halaman berikutnya. Dalam uji coba dengan 50 siswa, rekap memanjang menjadi dua halaman dengan rapi.
Ringkasan dan Tanda Tangan Guru
Private Sub Penutup(g As Graphics, area As Rectangle, y As Single)
Dim nilai = DaftarSiswa.Select(Function(s) NilaiAkhir(s)).ToList()
Dim jumlahTuntas = nilai.Where(Function(n) Tuntas(n)).Count()
' ... gambar enam baris ringkasan di kiri
' Tanda tangan di kanan
Dim xT = area.Right - 230
Dim tempat = If(Kelas.Kota.Trim() = "", "", Kelas.Kota.Trim() & ", ")
g.DrawString(tempat & DateTime.Now.ToString("dd MMMM yyyy"), _huruf, Brushes.Black, xT, y)
g.DrawString("Guru Mata Pelajaran,", _huruf, Brushes.Black, xT, y + 18)
Using garisBawah As New Font(_hurufTebal, FontStyle.Bold Or FontStyle.Underline)
g.DrawString(Kelas.Guru, garisBawah, Brushes.Black, xT, y + 100)
End Using
If Kelas.NipGuru.Trim() <> "" Then
g.DrawString("NIP. " & Kelas.NipGuru, _huruf, Brushes.Black, xT, y + 118)
End If
End Sub
Bagian penutup mengulang statistik yang sama dengan kartu di layar. Kemudian, sisi kanan memuat kota, tanggal berformat Indonesia, nama guru bergaris bawah, dan NIP. Jika NIP kosong, barisnya tidak muncul sama sekali.

Contoh Kasus 1: Wali Kelas Menyusun Rapor dan Ranking Semester
Bu Nur Sharecode adalah guru Matematika sekaligus wali kelas XI IPA 1 di SMA Sharecode Mandiri. Menjelang pembagian rapor semester ganjil, beliau harus menyerahkan nilai akhir dan ranking kelas ke bagian kurikulum dalam dua hari.
- Buka Pengaturan, isi tahun ajaran 2026/2027 semester Ganjil, bobot 30/30/40, dan KKM 75.
- Masukkan nilai 13 siswa, termasuk siswa pindahan Zahra Kurnia. Pratinjau langsung memperlihatkan nilai akhir 81,70 (B) sebelum guru menekan tombol Simpan.
- Klik judul kolom Rank agar tabel urut dari peringkat 1.
- Tekan Cetak Rekap, lalu pilih Microsoft Print to PDF untuk lampiran leger.

Hasilnya, Ayu Wulandari menempati peringkat 1 dengan 92,00. Berikutnya ada Siti Rahmawati 91,70 dan Indah Permatasari 89,80. Menariknya, Budi dan Fajar berbagi peringkat 5, begitu juga Ahmad dan Zahra di peringkat 7, sehingga peringkat 6 dan 8 tidak muncul. Kartu statistik juga langsung memberi bahan laporan, yaitu rata-rata kelas 80,85 dan 10 dari 13 siswa tuntas (77%). Untuk mata pelajaran lain, Bu Nur cukup menyalin folder aplikasi ke folder baru, karena setiap folder punya data sendiri.
Contoh Kasus 2: Guru Mapel Mencari Siswa yang Perlu Remedial
Setelah UAS, guru mata pelajaran wajib memberi remedial kepada siswa di bawah KKM. Daripada memeriksa baris satu per satu, guru cukup mengklik judul kolom Nilai Akhir sehingga nilai terendah naik ke atas.

Tiga baris merah langsung tampak, yaitu Muhammad Iqbal 66,30, Rizky Pratama 69,20, dan Dewi Anggraini 70,70. Guru lalu melihat komponen nilainya. Misalnya, Iqbal lemah di UTS (60), sedangkan Dewi paling rendah di UAS (68). Dengan demikian, materi remedial bisa menyasar komponen yang tepat.
Setelah remedial, guru mengklik baris Dewi dan mengganti UAS menjadi 80. Pratinjau segera menghitung (75×30 + 70×30 + 80×40) / 100 = 75,50, predikat C, dan warnanya berubah hijau karena sudah melewati KKM. Setelah guru menekan Ctrl+S, kartu Tuntas ikut naik. Terakhir, tombol Ekspor CSV menghasilkan file yang siap guru kirim ke wali kelas.
Menjalankan dan Membagikan Aplikasi
dotnet run --project SCNilaiSiswa dotnet publish SCNilaiSiswa -c Release -r win-x64 --self-contained false -o publish
Perintah pertama menjalankan aplikasi tanpa Visual Studio. Sementara itu, perintah kedua menghasilkan folder publish berisi SCNilaiSiswa.exe yang siap Anda salin ke laptop guru. Komputer tujuan perlu .NET 8 Desktop Runtime. Jika tidak ingin memasang runtime, ganti opsi menjadi --self-contained true dengan konsekuensi ukuran file lebih besar.
Ide Pengembangan Lanjutan
- Pindahkan data ke SQLite agar satu file database menampung banyak kelas dan mata pelajaran.
- Komponen nilai yang fleksibel, misalnya beberapa nilai tugas harian dalam
List(Of Decimal). - Impor daftar siswa dari file CSV hasil ekspor aplikasi dapodik atau Excel.
- Ambang predikat yang bisa guru atur sendiri dari FormPengaturan.
- Grafik distribusi nilai per predikat agar sebaran kemampuan kelas lebih mudah terbaca.
- Backup otomatis folder data ke flashdisk atau cloud setiap kali aplikasi tertutup.
Kesalahan Umum
- Membuka folder, bukan file .sln. Fitur Open Folder membuat project tidak bisa di-debug, jadi selalu buka SCNilaiSiswa.sln.
- Memakai Visual Studio 2019. Versi ini tidak mengenal .NET 8, sehingga project gagal termuat. Perbarui ke VS 2022 17.8 atau lebih baru.
- Lupa Option Strict. Tanpa
OptionStrict On, konversi diam-diam seperti String ke Decimal baru ketahuan saat aplikasi jalan. - Budaya angka tidak cocok. CSV dengan pemisah titik koma akan bertumpuk dalam satu kolom bila Excel memakai format regional English.
- Total bobot bukan 100. FormPengaturan memang menolak, tetapi pastikan juga rumus tambahan buatan Anda tetap membagi dengan 100.
- File data terkunci. Menjalankan aplikasi dari dalam zip atau dari C:\Program Files membuat folder data gagal tertulis, jadi ekstrak dulu ke folder biasa.
- Mengira peringkat hilang. Lompatan dari 5 ke 7 bukan bug, melainkan hasil ranking kompetisi untuk nilai yang seri.
Kesimpulan
Lewat tutorial ini, Anda sudah membangun aplikasi rekap nilai lengkap dengan Windows Forms .NET 8. Model yang ringkas, penyimpanan JSON yang aman, serta module Repo yang memegang semua rumus membuat bobot, predikat, KKM, dan ranking mudah Anda sesuaikan dengan kebijakan sekolah. Selain itu, ekspor CSV dan cetak rekap A4 membuat hasilnya langsung siap pakai untuk leger maupun rapor.
Jika ingin langsung mencoba atau mengembangkannya, download source code aplikasi nilai siswa VB.NET di sharecode.id secara gratis. Buka file .sln-nya, tekan F5, lalu mulai ubah rumus sesuai kurikulum sekolah Anda.
Baca Juga
- Tips dan Trik VB.NET
- Menampilkan informasi CPU dan RAM menggunakan VB.NET
- Aplikasi Penjualan Dengan Visual Basic 6 – Part 1 – Desain Database
Referensi: Desktop Guide (Windows Forms .NET), How to serialize and deserialize JSON in .NET (System.Text.Json), PrintDocument Class, DataGridView Control (Windows Forms).

