Pernah merasa gaji baru masuk, tapi pertengahan bulan dompet sudah tipis tanpa tahu uangnya pergi ke mana? Masalah yang sama juga sering menimpa pemilik warung atau kedai kecil, karena uang usaha dan uang pribadi bercampur di satu laci. Solusinya sebenarnya sederhana, yaitu mencatat setiap uang masuk dan keluar, lalu melihat polanya setiap bulan. Pada tutorial ini kita membangun aplikasi keuangan VB.NET bernama SC Keuangan dari nol dengan Windows Forms dan .NET 8, tanpa database dan tanpa paket NuGet.
Hasil akhirnya berupa satu layar utama yang berisi form input catatan, kartu ringkasan pemasukan, pengeluaran, dan selisih, tabel catatan per bulan, serta grafik batang pengeluaran per kategori. Selain itu, aplikasi ini bisa mengekspor data bulanan ke CSV agar Anda bisa membukanya di Excel. Source code lengkapnya gratis, jadi Anda bisa langsung download SC Keuangan VB.NET di sharecode.id lalu mengikuti tutorial ini sambil membuka project aslinya.

Fitur yang Akan Kita Bangun
Sebelum menulis kode, mari kita sepakati dulu daftar fiturnya. Dengan begitu, setiap langkah nanti punya tujuan yang jelas.
- Pencatatan pemasukan dan pengeluaran lengkap dengan tanggal, kategori, keterangan, dan jumlah.
- Kategori bawaan per jenis, plus kategori baru yang cukup Anda ketik langsung di combo box.
- Input nominal yang otomatis memberi titik ribuan, misalnya
65000menjadi65.000. - Filter bulan memakai tombol
<,>, dan Bulan Ini. - Tiga kartu ringkasan bulanan, plus saldo seluruhnya di header.
- Grafik batang pengeluaran per kategori buatan sendiri dengan GDI+, tanpa library chart.
- Klik baris tabel untuk mengubah atau menghapus catatan, selalu dengan konfirmasi.
- Ekspor CSV per bulan dengan pemisah titik koma yang cocok untuk Excel versi Indonesia.
- Semua data tersimpan sebagai file JSON di folder
datasamping file .exe.
Persiapan: Visual Studio 2022 dan .NET 8
Pertama, pastikan Anda memakai Visual Studio 2022 versi 17.8 atau lebih baru dengan workload .NET desktop development. Versi ini penting karena Visual Studio 2019 tidak mengenal .NET 8. Anda bisa mengecek versinya lewat menu Help → About Microsoft Visual Studio. Sebagai alternatif, cukup pasang .NET 8 SDK jika Anda lebih suka build lewat command line.
Membuat Project Windows Forms VB
- Buka Visual Studio 2022, lalu pilih Create a new project.
- Ketik “Windows Forms” di kotak pencarian, kemudian pilih template Windows Forms App yang berlabel Visual Basic (bukan C#, dan bukan versi .NET Framework).
- Isi nama project
SCKeuanganbeserta nama solution yang sama, setelah itu klik Next. - Pilih framework .NET 8.0 (Long Term Support), lalu klik Create.
- Hapus
Form1.vbbawaan, kemudian buat folderFormsdan tambahkan form baru bernamaFormUtama.
Setelah itu, tambahkan file class kosong Program.vb, Model.vb, Penyimpanan.vb, Repo.vb, Util.vb, dan PanelGrafik.vb. Isinya akan kita lengkapi satu per satu pada langkah berikutnya.
Mengatur File Project (.vbproj)
Selanjutnya, klik dua kali nama project di Solution Explorer untuk membuka file .vbproj. Samakan isinya dengan potongan berikut.
<PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net8.0-windows</TargetFramework> <UseWindowsForms>true</UseWindowsForms> <RootNamespace>SCKeuangan</RootNamespace> <AssemblyName>SCKeuangan</AssemblyName> <StartupObject>SCKeuangan.Program</StartupObject> <MyType>WindowsFormsWithCustomSubMain</MyType> <OptionExplicit>On</OptionExplicit> <OptionStrict>On</OptionStrict> <OptionInfer>On</OptionInfer> </PropertyGroup>
Dua baris paling penting adalah StartupObject dan MyType. Keduanya membuat aplikasi mulai dari Sub Main milik kita sendiri, bukan dari Application Framework bawaan VB. Selain itu, OptionStrict On memaksa konversi tipe yang eksplisit, sehingga kesalahan seperti menjumlahkan teks dengan angka langsung tertangkap saat build.
Struktur Folder dan File
Struktur akhir project terlihat seperti ini.
SCKeuangan.sln
SCKeuangan/
├── SCKeuangan.vbproj
├── Program.vb
├── Model.vb
├── Repo.vb
├── Penyimpanan.vb
├── Util.vb
├── PanelGrafik.vb
└── Forms/
├── FormUtama.vb
└── FormUtama.Designer.vb
| File | Fungsi |
|---|---|
Program.vb | Titik masuk aplikasi, mengatur budaya id-ID dan penanganan error global. |
Model.vb | Class Catatan untuk satu transaksi. |
Repo.vb | Menampung data di memori, kategori bawaan, filter bulan, saldo, dan data contoh. |
Penyimpanan.vb | Membaca dan menulis file JSON. |
Util.vb | Warna tema, format Rupiah, gaya DataGridView, input nominal, dan helper CSV. |
PanelGrafik.vb | Kontrol grafik batang pengeluaran per kategori. |
FormUtama.vb | Logika layar utama, mulai dari simpan, ubah, hapus, sampai ekspor. |
FormUtama.Designer.vb | Tata letak kontrol hasil Form Designer. |
Arsitektur Aplikasi Keuangan VB.NET
Aplikasi ini sengaja memakai arsitektur berlapis yang ringan. Form tidak pernah menyentuh file secara langsung, melainkan meminta data ke modul Repo. Kemudian Repo meneruskan urusan baca tulis ke modul Penyimpanan, dan modul itulah yang berbicara dengan file catatan.json.

Keuntungan pola ini cukup nyata. Misalnya, kelak Anda ingin pindah ke SQLite, maka cukup modul Penyimpanan dan sedikit bagian Repo yang berubah, sementara form tetap sama. Selain itu, logika seperti filter bulan dan saldo berada di satu tempat, sehingga mudah Anda uji dan baca ulang.
Langkah 1: Titik Masuk Program dan Budaya id-ID
Kita mulai dari Program.vb. Module ini mengatur format angka dan tanggal Indonesia sebelum form pertama muncul.
Friend Module Program
<STAThread>
Friend Sub Main()
' Format angka & tanggal Indonesia (15.000, Senin 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)
' Kesalahan tak terduga ditampilkan sebagai pesan, aplikasi tidak langsung tertutup
AddHandler Application.ThreadException,
Sub(s, e) Galat("Terjadi kesalahan: " & e.Exception.Message)
Repo.MuatSemua()
Application.Run(New FormUtama())
End Sub
End Module
Budaya id-ID membuat titik menjadi pemisah ribuan dan nama bulan tampil dalam Bahasa Indonesia, misalnya “Oktober 2026”. Kemudian handler ThreadException menangkap error tak terduga lalu menampilkannya lewat Galat(), sehingga aplikasi tidak langsung tertutup. Terakhir, Repo.MuatSemua() membaca data dari file sebelum FormUtama tampil.
Langkah 2: Membuat Model Catatan
Satu transaksi keuangan cukup kita wakili dengan satu class. Buka Model.vb, lalu tulis class Catatan berikut.
Public Class Catatan
Public Const MASUK As String = "Masuk"
Public Const KELUAR As String = "Keluar"
Public Property Id As Guid = Guid.NewGuid()
Public Property Tanggal As Date = Date.Today
Public Property Jenis As String = KELUAR
Public Property Kategori As String = ""
Public Property Keterangan As String = ""
Public Property Jumlah As Decimal
' ...
End Class
Setiap catatan mendapat Id bertipe Guid yang unik, jadi kita tidak perlu memikirkan nomor urut. Selain itu, Jenis hanya berisi dua nilai konstanta, yaitu "Masuk" atau "Keluar". Perhatikan juga tipe Jumlah, yaitu Decimal. Tipe ini wajib untuk uang karena Double bisa menghasilkan selisih pembulatan kecil yang menyebalkan.
Properti Hitung IsMasuk dan Nilai
Berikutnya, tambahkan dua properti baca saja di dalam class yang sama.
<JsonIgnore>
Public ReadOnly Property IsMasuk As Boolean
Get
Return Jenis = MASUK
End Get
End Property
''' <summary>Jumlah bertanda: positif untuk pemasukan, negatif untuk pengeluaran.</summary>
<JsonIgnore>
Public ReadOnly Property Nilai As Decimal
Get
Return If(IsMasuk, Jumlah, -Jumlah)
End Get
End Property
Properti Nilai adalah trik kecil yang sangat membantu. Karena pengeluaran bernilai negatif, saldo cukup kita dapat dengan menjumlahkan semua Nilai. Atribut <JsonIgnore> membuat kedua properti ini tidak ikut masuk ke file JSON, sebab nilainya selalu bisa kita hitung ulang dari Jenis dan Jumlah. Agar atribut itu terbaca, jangan lupa menambahkan Imports System.Text.Json.Serialization di bagian atas file.

Langkah 3: Penyimpanan JSON dengan System.Text.Json
Modul Penyimpanan bertugas membaca dan menulis file JSON di folder data. Karena memakai System.Text.Json bawaan .NET 8, kita tidak perlu memasang paket apa pun.
Public Module Penyimpanan
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
Public Function AdaFile(namaFile As String) As Boolean
Return File.Exists(Path.Combine(FolderData, namaFile))
End Function
' ...
Opsi WriteIndented membuat file JSON rapi dan mudah Anda baca dengan Notepad. Kemudian properti FolderData menunjuk ke folder data di samping file .exe, sekaligus membuat folder tersebut bila belum ada. Fungsi AdaFile nanti berguna untuk mendeteksi apakah aplikasi baru pertama kali berjalan.
Membaca File dengan Muat(Of 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
Fungsi ini generik, sehingga bisa memuat tipe apa saja selama tipe itu punya konstruktor kosong. Jika file belum ada, fungsi langsung mengembalikan objek baru yang kosong. Sebaliknya, jika isi file rusak, misalnya karena Anda mengeditnya manual dan lupa satu koma, fungsi ini menyalin file tersebut dengan akhiran .rusak- plus stempel waktu. Dengan demikian, data lama tidak hilang dan Anda masih bisa memperbaikinya nanti.
Menulis File dengan Aman
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 tidak menimpa file asli secara langsung. Pertama, method ini menulis JSON ke catatan.json.tmp. Setelah penulisan selesai, barulah File.Move dengan parameter overwrite = True mengganti file asli. Akibatnya, bila listrik padam di tengah proses, file catatan.json yang lama tetap utuh.
Langkah 4: Repo sebagai Pusat Logika Data
Modul Repo menyimpan seluruh catatan di memori selama aplikasi berjalan. Selain itu, modul ini menyediakan daftar kategori bawaan.
Public Module Repo
Private Const FILE_CATATAN As String = "catatan.json"
Public ReadOnly KategoriMasuk As String() = {"Gaji", "Penjualan", "Bonus", "Lainnya"}
Public ReadOnly KategoriKeluar As String() = {"Makan & Minum", "Transportasi", "Belanja", "Tagihan", "Pulsa & Internet", "Lainnya"}
' Semua catatan semua bulan, terisi sekali saat aplikasi mulai
Public DaftarCatatan As New List(Of Catatan)()
Public Sub MuatSemua()
Dim pertamaKali = Not AdaFile(FILE_CATATAN)
DaftarCatatan = Muat(Of List(Of Catatan))(FILE_CATATAN)
If pertamaKali Then
IsiDataContoh()
SimpanCatatan()
End If
End Sub
' Tulis seluruh list ke catatan.json
Public Sub SimpanCatatan()
Simpan(FILE_CATATAN, DaftarCatatan)
End Sub
' ...
Semua catatan dari semua bulan berada dalam satu List(Of Catatan) dan satu file. Untuk catatan pribadi atau UMKM kecil, pendekatan ini sudah lebih dari cukup, karena ribuan baris JSON tetap terbaca dalam sekejap. Kemudian MuatSemua mengecek apakah file belum ada. Jika benar begitu, Repo mengisi data contoh lalu langsung menyimpannya, sehingga pengguna baru tidak melihat layar kosong.
Filter Bulan dan Saldo Seluruhnya
Public Function CariCatatan(id As Guid) As Catatan
Return DaftarCatatan.FirstOrDefault(Function(c) c.Id = id)
End Function
''' <summary>Catatan pada bulan tertentu (tanggal apa pun di bulan itu).</summary>
Public Function CatatanBulan(bulan As Date) As List(Of Catatan)
Return DaftarCatatan.Where(Function(c) c.Tanggal.Year = bulan.Year AndAlso c.Tanggal.Month = bulan.Month) _
.OrderByDescending(Function(c) c.Tanggal).ToList()
End Function
Public Function SaldoSeluruhnya() As Decimal
Return DaftarCatatan.Sum(Function(c) c.Nilai)
End Function
Ketiga fungsi ini memakai LINQ. Pertama, CariCatatan mencari satu catatan berdasarkan Id, dan fungsi ini penting saat proses ubah atau hapus. Kedua, CatatanBulan menyaring catatan yang tahun dan bulannya sama, lalu mengurutkannya dari tanggal terbaru. Terakhir, SaldoSeluruhnya cukup menjumlahkan properti Nilai yang sudah bertanda tadi.
Data Contoh untuk Pertama Kali
Private Sub IsiDataContoh()
' Tanggal contoh di bulan berjalan, tidak melewati hari ini
Dim hari = Function(d As Integer) New Date(Date.Today.Year, Date.Today.Month, Math.Min(d, Date.Today.Day))
DaftarCatatan.AddRange({
New Catatan With {.Tanggal = hari(1), .Jenis = Catatan.MASUK, .Kategori = "Gaji", .Keterangan = "Gaji dari CV Sharecode Mandiri", .Jumlah = 5000000},
New Catatan With {.Tanggal = hari(5), .Jenis = Catatan.MASUK, .Kategori = "Penjualan", .Keterangan = "Jual source code di sharecode.id", .Jumlah = 750000},
New Catatan With {.Tanggal = hari(2), .Jenis = Catatan.KELUAR, .Kategori = "Tagihan", .Keterangan = "Listrik & air", .Jumlah = 350000},
' ... lima catatan pengeluaran lain
})
End Sub
Lambda hari memastikan tanggal contoh tidak melewati hari ini. Misalnya aplikasi pertama kali berjalan tanggal 3, maka catatan “tanggal 7” otomatis bergeser ke tanggal 3. Akibatnya, data contoh selalu tampil di bulan berjalan tanpa tanggal di masa depan.
Langkah 5: Util untuk Format Rupiah dan Input Nominal
Modul Util berisi fungsi kecil yang form pakai berulang kali. Dua fungsi pertama mengurus format uang.
Public ReadOnly Indo As New CultureInfo("id-ID")
Public Function Rupiah(nilai As Decimal) As String
Return "Rp " & nilai.ToString("N0", Indo)
End Function
''' <summary>"Rp 15.000" / "15.000" / "15000" menjadi 15000.</summary>
Public Function ParseAngka(teks As String) As Decimal
Dim bersih = teks.Replace("Rp", "").Replace(" ", "").Trim()
Dim hasil As Decimal
If Decimal.TryParse(bersih, NumberStyles.Number, Indo, hasil) Then Return hasil
Return 0D
End Function
Format N0 dengan budaya Indonesia menghasilkan angka bertitik ribuan tanpa desimal, misalnya Rp 1.250.000. Sebaliknya, ParseAngka membuang teks “Rp” dan spasi, lalu mengubah teks kembali menjadi Decimal. Karena memakai TryParse, input yang aneh tidak memicu error, melainkan menghasilkan nol, lalu validasi menolaknya.
Modul yang sama juga punya Info, Peringatan, Galat, dan Tanya sebagai pembungkus MessageBox, serta GayaGrid yang menyeragamkan tampilan DataGridView, seperti header gelap, baris selang-seling, dan seleksi satu baris penuh.
TextBox Nominal dengan Titik Ribuan Otomatis
Public Sub PasangInputRupiah(tb As TextBox)
tb.MaxLength = 15
AddHandler tb.KeyPress,
Sub(s As Object, e As KeyPressEventArgs)
If Not Char.IsControl(e.KeyChar) AndAlso Not Char.IsDigit(e.KeyChar) Then e.Handled = True
End Sub
' Setiap teks berubah, susun ulang digit menjadi format ribuan
AddHandler tb.TextChanged,
Sub(s As Object, e As EventArgs)
Dim digit = New String(tb.Text.Where(Function(c) Char.IsDigit(c)).ToArray())
If digit.Length > 15 Then digit = digit.Substring(0, 15)
Dim rapi = If(digit = "", "", Decimal.Parse(digit, Indo).ToString("N0", Indo))
If tb.Text <> rapi Then
tb.Text = rapi
tb.SelectionStart = tb.Text.Length
End If
End Sub
End Sub
Handler KeyPress menolak semua tombol selain angka dan tombol kontrol seperti Backspace. Kemudian handler TextChanged mengambil digitnya saja, memformat ulang dengan titik ribuan, lalu memindahkan kursor ke ujung teks. Pengecekan If tb.Text <> rapi penting agar event tidak memicu dirinya sendiri tanpa henti. Hasilnya, pengguna mengetik 65000 dan langsung melihat 65.000.
Langkah 6: Mendesain FormUtama
Buka FormUtama di Form Designer, lalu atur ukuran form sekitar 1100 × 680 dengan MinimumSize 1060 × 640. Aktifkan juga KeyPreview = True agar form bisa menangkap pintasan Ctrl+S. Tata letaknya terdiri dari tiga panel utama, yaitu header di atas, panel input di kiri, dan panel isi yang mengisi sisa ruang.
| Nama kontrol | Tipe | Fungsi |
|---|---|---|
pnlHeader | Panel (Dock Top) | Judul aplikasi dan lblSaldoTotal di kanan. |
pnlInput | Panel (Dock Left) | Wadah seluruh kontrol input catatan. |
lblForm | Label | Judul mode, berganti antara “Catatan Baru” dan “Ubah Catatan”. |
dtpTanggal | DateTimePicker | Tanggal transaksi. |
rdoMasuk, rdoKeluar | RadioButton | Pilihan jenis pemasukan atau pengeluaran. |
cboKategori | ComboBox (DropDown) | Pilih kategori atau ketik kategori baru. |
txtKeterangan | TextBox | Keterangan bebas. |
txtJumlah | TextBox (rata kanan) | Nominal dengan titik ribuan otomatis. |
btnSimpan, btnBaru, btnHapus | Button | Simpan, kosongkan form, dan hapus catatan terpilih. |
lblInfo | Label | Pesan status setelah simpan atau hapus. |
Kontrol Ringkasan, Tabel, dan Grafik
| Nama kontrol | Tipe | Fungsi |
|---|---|---|
btnSebelum, btnBerikut, btnBulanIni | Button | Navigasi bulan. |
dtpBulan | DateTimePicker | Format custom MMMM yyyy dengan ShowUpDown = True. |
btnEkspor | Button | Ekspor CSV bulan terpilih. |
lblPemasukan, lblPengeluaran, lblSaldo | Label di tiga panel kartu | Angka ringkasan bulanan. |
lblKartu3 | Label | Judul kartu ketiga, misalnya “Selisih Oktober 2026”. |
dgvCatatan | DataGridView | Kolom colTanggal, colKategori, colKeterangan, colJumlah. |
pnlGrafik | PanelGrafik (kontrol buatan) | Grafik pengeluaran per kategori. |
Kontrol PanelGrafik akan muncul di Toolbox setelah Anda melakukan build sekali. Karena itu, kerjakan dulu Langkah 8 bila Anda ingin menaruhnya lewat Designer.
Langkah 7: Logika FormUtama
Sekarang tekan F7 untuk pindah ke kode FormUtama.vb. Form ini menyimpan dua state, yaitu _idUbah untuk catatan yang sedang Anda ubah dan _tampil untuk catatan yang sedang tampil di tabel.
''' <summary>Id catatan yang sedang diubah. Guid.Empty = catatan baru.</summary>
Private _idUbah As Guid = Guid.Empty
''' <summary>Catatan yang sedang tampil di tabel (urutan sama dengan baris).</summary>
Private _tampil As New List(Of Catatan)()
Private Sub FormUtama_Load(sender As Object, e As EventArgs) Handles MyBase.Load
GayaGrid(dgvCatatan)
RataKanan(colJumlah)
PasangInputRupiah(txtJumlah)
dtpBulan.Value = AwalBulan(Date.Today)
IsiKategori()
Tampilkan()
CatatanBaru()
End Sub
' Helper: tanggal berapa pun menjadi tanggal 1 di bulan itu
Private Shared Function AwalBulan(tgl As Date) As Date
Return New Date(tgl.Year, tgl.Month, 1)
End Function
Nilai Guid.Empty pada _idUbah berarti form sedang dalam mode catatan baru. Kemudian event Load memasang gaya grid, membuat kolom jumlah rata kanan, dan mengaktifkan input Rupiah. Setelah itu, form memilih bulan ini dan mengisi semua tampilan. Fungsi AwalBulan membantu membandingkan bulan tanpa repot dengan tanggal.
Kategori Dinamis dengan IsiKategori
Private Sub IsiKategori()
Dim bawaan = If(rdoMasuk.Checked, KategoriMasuk, KategoriKeluar)
Dim buatan = DaftarCatatan.Where(Function(c) c.Jenis = JenisTerpilih) _
.Select(Function(c) c.Kategori)
Dim semua = bawaan.Concat(buatan.Where(Function(k) Not bawaan.Contains(k))).Distinct().ToArray()
' Pertahankan teks lama bila masih ada di daftar baru
Dim teksLama = cboKategori.Text
cboKategori.Items.Clear()
cboKategori.Items.AddRange(semua)
cboKategori.Text = If(semua.Contains(teksLama), teksLama, semua(0))
End Sub
Inilah alasan kategori baru tidak perlu menu khusus. Prosedur ini menggabungkan kategori bawaan dengan semua kategori yang pernah Anda pakai pada jenis yang sama. Jadi, setelah Anda sekali mengetik “Bahan Baku”, kategori itu langsung muncul di pilihan pengeluaran berikutnya. Properti JenisTerpilih sendiri hanya mengembalikan Catatan.MASUK bila rdoMasuk tercentang, selain itu Catatan.KELUAR.
Menampilkan Tabel dan Kartu Ringkasan
Private Sub Tampilkan()
_tampil = CatatanBulan(BulanTerpilih)
dgvCatatan.Rows.Clear()
For Each c In _tampil
Dim jumlah = If(c.IsMasuk, "+ ", "- ") & Rupiah(c.Jumlah)
Dim i = dgvCatatan.Rows.Add(c.Tanggal.ToString("dd/MM/yyyy"), c.Kategori, c.Keterangan, jumlah)
dgvCatatan.Rows(i).Cells(colJumlah.Index).Style.ForeColor = If(c.IsMasuk, Warna.Sukses, Warna.Bahaya)
Next
dgvCatatan.ClearSelection()
' Ringkasan bulan terpilih
Dim masuk = _tampil.Where(Function(c) c.IsMasuk).Sum(Function(c) c.Jumlah)
Dim keluar = _tampil.Where(Function(c) Not c.IsMasuk).Sum(Function(c) c.Jumlah)
Dim selisih = masuk - keluar
lblPemasukan.Text = Rupiah(masuk)
lblPengeluaran.Text = Rupiah(keluar)
lblSaldo.Text = If(selisih < 0, "- ", "") & Rupiah(Math.Abs(selisih))
lblSaldo.ForeColor = If(selisih < 0, Warna.Bahaya, Warna.Primer)
lblKartu3.Text = $"Selisih {BulanTerpilih:MMMM yyyy}"
' ...
Prosedur Tampilkan menjadi jantung layar utama. Pertama, prosedur ini mengambil catatan bulan terpilih ke _tampil. Urutan list ini sama persis dengan urutan baris tabel, sehingga indeks baris bisa langsung menunjuk ke objek Catatan. Kemudian setiap baris mendapat tanda plus hijau atau minus merah. Setelah itu, prosedur menghitung total pemasukan, pengeluaran, dan selisih. Jika selisih negatif, kartu ketiga berubah merah sebagai peringatan bahwa bulan ini besar pasak daripada tiang.
Menyiapkan Data Grafik dengan GroupBy
Dim saldo = SaldoSeluruhnya()
lblSaldoTotal.Text = "Saldo seluruhnya: " & If(saldo < 0, "- ", "") & Rupiah(Math.Abs(saldo))
pnlGrafik.AturData(
_tampil.Where(Function(c) Not c.IsMasuk) _
.GroupBy(Function(c) c.Kategori) _
.Select(Function(g) New KeyValuePair(Of String, Decimal)(g.Key, g.Sum(Function(c) c.Jumlah))))
End Sub
Bagian akhir Tampilkan mengisi saldo seluruhnya di header, yaitu saldo dari semua bulan dan bukan hanya bulan terpilih. Selanjutnya, query LINQ mengambil pengeluaran saja, mengelompokkannya per kategori, lalu menjumlahkan tiap kelompok. Hasilnya berupa pasangan nama kategori dan total yang kita kirim ke pnlGrafik.AturData.
Menyimpan Catatan Baru atau Hasil Ubah

Private Sub Simpan()
Dim kategori = cboKategori.Text.Trim()
Dim jumlah = ParseAngka(txtJumlah.Text)
If kategori = "" Then Peringatan("Kategori wajib diisi.") : cboKategori.Focus() : Return
If jumlah <= 0 Then Peringatan("Jumlah harus lebih dari 0.") : txtJumlah.Focus() : Return
' Cari catatan lama, atau buat baru bila tidak ada
Dim c As Catatan = Nothing
If _idUbah <> Guid.Empty Then c = CariCatatan(_idUbah)
If c Is Nothing Then
c = New Catatan()
DaftarCatatan.Add(c)
End If
c.Tanggal = dtpTanggal.Value.Date
c.Jenis = JenisTerpilih
c.Kategori = kategori
c.Keterangan = txtKeterangan.Text.Trim()
c.Jumlah = jumlah
SimpanCatatan()
' ...
Prosedur ini melayani dua mode sekaligus. Jika _idUbah berisi Id, prosedur mengambil catatan lama lewat CariCatatan. Sebaliknya, jika kosong, prosedur membuat Catatan baru dan menambahkannya ke list. Karena Catatan adalah class (tipe referensi), mengubah properti c berarti langsung mengubah objek di dalam DaftarCatatan. Terakhir, SimpanCatatan() menulis seluruh list ke JSON.
Pindah Otomatis ke Bulan Catatan
lblInfo.Text = $"✔ Tersimpan: {c.Kategori} {Rupiah(c.Jumlah)}"
' Pindah ke bulan catatan tersebut agar langsung terlihat
If AwalBulan(c.Tanggal) <> BulanTerpilih Then
dtpBulan.Value = AwalBulan(c.Tanggal) ' memicu Tampilkan()
Else
Tampilkan()
End If
IsiKategori()
CatatanBaru()
End Sub
Detail kecil ini membuat aplikasi terasa ramah. Misalnya Anda sedang melihat November tetapi mencatat pengeluaran tanggal 3 Oktober, maka tampilan otomatis pindah ke Oktober. Dengan begitu, Anda langsung melihat catatan yang baru tersimpan. Mengubah dtpBulan.Value memicu event ValueChanged, dan event itu menjalankan Tampilkan(). Setelah itu, IsiKategori menyegarkan daftar kategori, lalu CatatanBaru mengosongkan form.
Mengubah dan Menghapus Catatan
Private Sub dgvCatatan_CellClick(sender As Object, e As DataGridViewCellEventArgs) Handles dgvCatatan.CellClick
If e.RowIndex < 0 OrElse e.RowIndex >= _tampil.Count Then Return
IsiForm(_tampil(e.RowIndex))
End Sub
' Salin isi catatan ke form dan masuk mode ubah
Private Sub IsiForm(c As Catatan)
_idUbah = c.Id
lblForm.Text = "Ubah Catatan"
dtpTanggal.Value = c.Tanggal
rdoMasuk.Checked = c.IsMasuk
rdoKeluar.Checked = Not c.IsMasuk
IsiKategori()
cboKategori.Text = c.Kategori
txtKeterangan.Text = c.Keterangan
txtJumlah.Text = c.Jumlah.ToString("N0")
btnHapus.Enabled = True
End Sub
Saat Anda mengklik baris, event CellClick mengambil objek dari _tampil berdasarkan indeks baris. Pengecekan e.RowIndex < 0 mencegah error ketika yang Anda klik justru header kolom. Kemudian IsiForm menyalin data ke kontrol input, menyimpan Id ke _idUbah, dan mengaktifkan tombol Hapus.

Hapus dengan Konfirmasi
Private Sub Hapus()
Dim c = CariCatatan(_idUbah)
If c Is Nothing Then Return
If Not Tanya($"Hapus catatan {c.Kategori} {Rupiah(c.Jumlah)} tanggal {c.Tanggal:dd/MM/yyyy}?") Then Return
DaftarCatatan.Remove(c)
SimpanCatatan()
Tampilkan()
CatatanBaru()
lblInfo.Text = "Catatan dihapus."
End Sub
Penghapusan selalu melewati Tanya() yang menampilkan kategori, jumlah, dan tanggal catatan. Pesan yang spesifik seperti ini jauh lebih aman ketimbang sekadar “Yakin hapus?”, karena pengguna bisa memastikan baris yang benar sebelum menekan Yes. Setelah itu, prosedur menghapus objek dari list, menulis ulang file, lalu menyegarkan tampilan.
Navigasi Bulan dan Pintasan Keyboard
Private Sub dtpBulan_ValueChanged(sender As Object, e As EventArgs) Handles dtpBulan.ValueChanged
Tampilkan()
If _idUbah = Guid.Empty Then CatatanBaru()
End Sub
Private Sub btnSebelum_Click(sender As Object, e As EventArgs) Handles btnSebelum.Click
dtpBulan.Value = BulanTerpilih.AddMonths(-1)
End Sub
' Ctrl+S dari mana saja (butuh 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
Semua tombol navigasi cukup mengubah dtpBulan.Value, lalu event ValueChanged mengurus sisanya. Tombol berikutnya memakai AddMonths(1), sedangkan Bulan Ini memakai AwalBulan(Date.Today). Selain Ctrl+S, kode asli juga menangani tombol Enter di txtJumlah dengan pola yang sama. Akibatnya, pencatatan cepat cukup dengan mengetik nominal lalu menekan Enter.
Langkah 8: Grafik Pengeluaran per Kategori dengan GDI+
Alih-alih memasang library chart, kita membuat kontrol sendiri yang mewarisi Panel. Cara ini membuat aplikasi tetap ringan dan sekaligus mengajarkan dasar menggambar di Windows Forms.
Public Class PanelGrafik
Inherits Panel
Private _data As New List(Of KeyValuePair(Of String, Decimal))()
Public Property Judul As String = "Pengeluaran per kategori"
Public Sub New()
DoubleBuffered = True ' cegah kedip saat digambar ulang
ResizeRedraw = True ' gambar ulang saat ukuran berubah
BackColor = Color.White
End Sub
' Terima data baru, urutkan dari terbesar, lalu minta gambar ulang
Public Sub AturData(data As IEnumerable(Of KeyValuePair(Of String, Decimal)))
_data = data.OrderByDescending(Function(x) x.Value).ToList()
Invalidate()
End Sub
' ...
Properti DoubleBuffered mencegah grafik berkedip, sedangkan ResizeRedraw membuat kontrol menggambar ulang setiap kali ukurannya berubah. Kemudian AturData mengurutkan kategori dari pengeluaran terbesar dan memanggil Invalidate(). Pemanggilan itu meminta Windows menjalankan OnPaint pada kesempatan berikutnya.
Menggambar Batang di OnPaint
Protected Overrides Sub OnPaint(e As PaintEventArgs)
MyBase.OnPaint(e)
Dim g = e.Graphics
' ... siapkan font & kuas, gambar judul, cek data kosong
Dim total = _data.Sum(Function(x) x.Value)
Dim terbesar = _data.Max(Function(x) x.Value)
Dim kanan = New StringFormat With {.Alignment = StringAlignment.Far}
For Each item In _data
Dim persen = If(total = 0, 0D, item.Value / total * 100D)
g.DrawString(item.Key, fTeks, Brushes.Black, kiri, y)
g.DrawString($"{Rupiah(item.Value)} ({persen:0}%)", fAngka, kuasAbu,
New RectangleF(kiri, y, lebar, 20), kanan)
y += 25
' Panjang batang relatif terhadap kategori terbesar
Dim panjang = CSng(If(terbesar = 0, 0D, item.Value / terbesar) * lebar)
g.FillRectangle(kuasJalur, kiri, y, lebar, 10)
g.FillRectangle(kuasBatang, kiri, y, Math.Max(panjang, 2.0F), 10)
y += 24
If y > ClientSize.Height - 20 Then Exit For
Next
Ada dua angka berbeda di sini. Persentase memakai total sebagai pembagi, sehingga semua persen berjumlah sekitar 100. Sebaliknya, panjang batang memakai terbesar sebagai pembagi, jadi kategori terbesar selalu memenuhi lebar panel dan perbandingan antarbatang tetap jelas. Batang abu-abu muda (kuasJalur) menjadi latar, lalu batang merah menimpanya. Kode asli membungkus font dan kuas di dalam blok Using agar resource GDI+ langsung terlepas.

Langkah 9: Ekspor CSV untuk Excel
Fitur ekspor membutuhkan helper kecil di Util untuk membungkus nilai yang mengandung karakter khusus.
''' <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 versi Indonesia memakai titik koma sebagai pemisah daftar, karena koma sudah berperan sebagai tanda desimal. Oleh karena itu, file CSV kita memakai titik koma. Jika sebuah keterangan kebetulan berisi titik koma, tanda kutip, atau baris baru, fungsi ini membungkusnya dengan tanda kutip ganda sesuai aturan CSV.
Prosedur EksporCsv
Private Sub EksporCsv()
If _tampil.Count = 0 Then
Peringatan("Tidak ada catatan di bulan ini.")
Return
End If
Using dlg As New SaveFileDialog()
dlg.Filter = "File CSV (*.csv)|*.csv"
dlg.FileName = $"keuangan_{BulanTerpilih:yyyy-MM}.csv"
If dlg.ShowDialog(Me) <> DialogResult.OK Then Return
Dim baris As New List(Of String) From {"Tanggal;Jenis;Kategori;Keterangan;Jumlah"}
For Each c In _tampil.OrderBy(Function(x) x.Tanggal)
baris.Add(String.Join(";", Csv(c.Tanggal.ToString("yyyy-MM-dd")), Csv(c.Jenis),
Csv(c.Kategori), Csv(c.Keterangan), Csv(c.Nilai)))
Next
baris.Add("")
baris.Add(";;;Total pemasukan;" & Csv(_tampil.Where(Function(c) c.IsMasuk).Sum(Function(c) c.Jumlah)))
baris.Add(";;;Total pengeluaran;" & Csv(_tampil.Where(Function(c) Not c.IsMasuk).Sum(Function(c) c.Jumlah)))
baris.Add(";;;Selisih;" & Csv(_tampil.Sum(Function(c) c.Nilai)))
File.WriteAllLines(dlg.FileName, baris, New UTF8Encoding(True))
Info("Data berhasil diekspor ke:" & vbCrLf & dlg.FileName)
End Using
End Sub
Nama file bawaan mengikuti bulan terpilih, misalnya keuangan_2026-10.csv. Kemudian prosedur mengurutkan catatan dari tanggal terlama agar enak Anda baca seperti buku kas. Kolom Jumlah memakai Nilai yang bertanda, sehingga Anda bisa langsung memakai rumus SUM di Excel. Terakhir, New UTF8Encoding(True) menulis BOM agar Excel tidak salah membaca karakter seperti “&”.
Contoh Isi File CSV
Berikut isi file hasil ekspor untuk data Oktober pada contoh kasus UMKM di bawah.
Tanggal;Jenis;Kategori;Keterangan;Jumlah 2026-10-01;Keluar;Sewa Tempat;Sewa kios Oktober (usaha);-1000000 2026-10-03;Keluar;Bahan Baku;Biji kopi & susu (usaha);-3200000 2026-10-05;Masuk;Penjualan;Omzet minggu 1 Kedai Kopi Sharecode;2100000 2026-10-10;Keluar;Tagihan;Listrik kios (usaha);-450000 2026-10-12;Masuk;Penjualan;Omzet minggu 2 Kedai Kopi Sharecode;2350000 2026-10-15;Keluar;Ambil Pribadi;Prive: uang belanja rumah;-1500000
Lima baris berikutnya beserta blok total di bagian bawah file:
2026-10-19;Masuk;Penjualan;Omzet minggu 3 Kedai Kopi Sharecode;1950000 2026-10-21;Keluar;Transportasi;Antar pesanan (usaha);-250000 2026-10-26;Masuk;Penjualan;Omzet minggu 4 Kedai Kopi Sharecode;2600000 2026-10-28;Keluar;Gaji Karyawan;Upah barista paruh waktu;-1800000 ;;;Total pemasukan;9000000 ;;;Total pengeluaran;8200000 ;;;Selisih;800000
Baris kosong memisahkan data dari blok total. Selain itu, blok total menempati kolom keempat dan kelima, sehingga labelnya sejajar dengan kolom Keterangan saat Anda membukanya di Excel.
Contoh Kasus 1: UMKM Memisahkan Uang Usaha dan Pribadi
Bayangkan Bu Rina, pemilik Kedai Kopi Sharecode. Selama ini omzet, belanja bahan, dan uang belanja rumah keluar masuk dari satu laci. Akibatnya, setiap akhir bulan ia tidak tahu kedainya untung atau hanya terlihat ramai. Dengan SC Keuangan, Bu Rina membuat beberapa kategori baru cukup dengan mengetiknya, yaitu “Bahan Baku”, “Sewa Tempat”, “Gaji Karyawan”, dan satu kategori khusus “Ambil Pribadi” untuk uang yang ia bawa pulang.
Kuncinya ada di kategori “Ambil Pribadi” atau prive. Setiap kali mengambil uang kedai untuk kebutuhan rumah, ia mencatatnya sebagai pengeluaran dengan kategori ini. Dengan demikian, uang pribadi tetap tercatat tetapi terpisah jelas dari biaya usaha. Kolom keterangan juga ia beri penanda “(usaha)” agar mudah ia saring di Excel nanti.

Membaca Hasil Bulan Oktober
Pada akhir Oktober, Bu Rina menekan tombol < sampai bulan Oktober. Kartu ringkasan menunjukkan pemasukan Rp 9.000.000, pengeluaran Rp 8.200.000, dan selisih Rp 800.000. Namun, angka itu belum menjawab apakah kedai untung. Karena pengeluaran juga memuat prive Rp 1.500.000, laba usaha sebenarnya adalah selisih plus prive, yaitu Rp 2.300.000.
Selanjutnya, grafik memperlihatkan bahwa Bahan Baku menjadi pengeluaran terbesar dengan porsi 39%, kemudian Gaji Karyawan 22% dan Ambil Pribadi 18%. Dari sini Bu Rina bisa mengambil keputusan konkret, misalnya mencari pemasok susu yang lebih murah atau membeli biji kopi dalam jumlah lebih besar. Terakhir, ia menekan Ekspor CSV dan mengirim file itu ke akuntan atau menyimpannya sebagai arsip.
Contoh Kasus 2: Mahasiswa dan Karyawan Mengatur Anggaran Bulanan
Kasus kedua datang dari Raka, karyawan baru di CV Sharecode Mandiri dengan gaji Rp 5.000.000. Ia ingin memakai aturan sederhana 50/30/20, yaitu 50% untuk kebutuhan, 30% untuk keinginan, dan 20% untuk tabungan. SC Keuangan memang belum punya fitur anggaran khusus. Meski begitu, fitur yang ada sudah cukup untuk menjalankan aturan tersebut secara konsisten.
Pertama, Raka mencatat gaji sebagai pemasukan kategori “Gaji” setiap tanggal gajian. Kemudian ia mencatat setiap pengeluaran harian, seperti makan siang, bensin, dan paket internet, segera setelah transaksi terjadi. Pintasan Enter di kolom jumlah membuat pencatatan cukup beberapa detik saja. Untuk tabungan, ia membuat kategori pengeluaran “Tabungan” setiap kali menyisihkan uang ke rekening lain.
Mengevaluasi Anggaran Setiap Akhir Bulan
Pada akhir bulan, Raka cukup melihat persentase di samping setiap batang grafik. Misalnya Makan & Minum ternyata 25% dari pengeluaran dan Belanja 37%, maka ia tahu pos mana yang perlu ia kurangi bulan depan. Selain itu, kartu selisih yang berubah merah menjadi alarm bahwa ia sudah memakai lebih dari gajinya.
Hal yang sama berlaku untuk mahasiswa yang menerima uang saku bulanan. Cukup ganti kategori “Gaji” dengan “Uang Saku” lalu catat pengeluaran kos, fotokopi, dan jajan. Karena kategori baru langsung tersimpan sebagai pilihan, aplikasi akan menyesuaikan diri dengan kebiasaan masing-masing pengguna tanpa perlu mengubah kode.
Menjalankan dan Membuat File Siap Pakai
Untuk menjalankan aplikasi, buka SCKeuangan.sln lalu tekan F5. Jika Anda tidak memakai Visual Studio, jalankan perintah berikut dari folder solution.
dotnet run --project SCKeuangan dotnet publish SCKeuangan -c Release -r win-x64 --self-contained false -o publish
Perintah pertama langsung menjalankan aplikasi. Sementara itu, perintah kedua menghasilkan folder publish yang berisi SCKeuangan.exe. Komputer tujuan perlu .NET 8 Desktop Runtime. Jika Anda ingin aplikasi berjalan tanpa runtime terpasang, ganti --self-contained false menjadi true, meski ukuran folder hasilnya jadi jauh lebih besar.
Data tersimpan di data\catatan.json di samping file .exe. Untuk backup, cukup salin folder data ke flashdisk atau cloud. Sebaliknya, hapus file catatan.json bila Anda ingin mulai dari kosong, karena aplikasi akan membuat data contoh baru saat berjalan lagi.
Ide Pengembangan Lanjutan
- Anggaran per kategori: tambahkan batas bulanan dan warnai batang grafik saat pengeluaran melewati batas.
- Beberapa dompet: tambahkan properti
Dompet(Kas Usaha, Rekening Pribadi, e-wallet) agar pemisahan uang lebih tegas. - SQLite: ganti modul Penyimpanan bila data sudah puluhan ribu baris atau perlu laporan tahunan yang kompleks.
- Grafik tren: buat kontrol GDI+ kedua untuk membandingkan pemasukan dan pengeluaran 12 bulan terakhir.
- Kata sandi: tambahkan form login sederhana dan enkripsi file JSON memakai
System.Security.Cryptography. - Impor CSV: baca mutasi rekening bank agar pencatatan tidak perlu manual.
Kesalahan Umum
- Membuka folder, bukan file .sln. Menu Open Folder membuat project tidak bisa Anda debug, jadi selalu buka
SCKeuangan.sln. - Visual Studio 2019 tidak bisa membuka .NET 8. Perbarui ke Visual Studio 2022 versi 17.8 ke atas lewat Visual Studio Installer.
- Lupa Option Strict. Tanpa
OptionStrict On, konversi tipe implisit bisa lolos build lalu error saat aplikasi berjalan. - Memakai Double untuk uang. Gunakan
Decimalagar tidak muncul selisih pembulatan seperti 0,1 + 0,2. - Budaya angka tertukar. Tanpa budaya
id-ID, teks “15.000” bisa terbaca sebagai lima belas koma nol. Karena itu, kode ini selalu mengirimIndokeTryParsedanToString. - File data terkunci. Jangan membuka
catatan.jsondi editor yang mengunci file saat aplikasi berjalan, dan hindari menaruh aplikasi diC:\Program Filesyang butuh izin admin untuk menulis. - Kontrol PanelGrafik tidak muncul di Toolbox. Lakukan Build Solution (Ctrl+Shift+B) dulu, setelah itu tutup dan buka lagi tab form.
- Kategori beda tulisan. “Bahan baku” dan “Bahan Baku” terhitung sebagai dua kategori, jadi pilih dari daftar ketimbang mengetik ulang.
Kesimpulan
Kita sudah membangun aplikasi keuangan VB.NET yang lengkap dari nol, mulai dari model Catatan dengan properti bertanda, penyimpanan JSON yang aman, Repo dengan filter bulan, sampai form utama dengan kartu ringkasan, grafik GDI+, dan ekspor CSV. Semua itu berjalan di atas .NET 8 tanpa database dan tanpa paket tambahan, sehingga mudah Anda pelajari maupun kembangkan.
Selain cocok sebagai bahan belajar, aplikasi ini juga langsung berguna untuk mencatat keuangan pribadi atau memisahkan uang usaha seperti pada contoh Kedai Kopi Sharecode. Jadi, silakan download source code SC Keuangan VB.NET gratis di sharecode.id, buka di Visual Studio 2022, lalu coba tambahkan salah satu ide pengembangan di atas. Lisensinya MIT, jadi Anda bebas memakainya untuk tugas kuliah, skripsi, maupun usaha.
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: Windows Forms untuk .NET (Microsoft Learn), Serialisasi dan deserialisasi JSON dengan System.Text.Json, Kontrol DataGridView (Windows Forms), Menggunakan objek Graphics dan GDI+ di Windows Forms.

