WinForms 超入門:カレンダーで選んだ日に「1行メモ」を追加し、JSONに保存する(単機能ミニアプリ)

余計な機能は入れません。

できることは「日付を選ぶ → メモを入力 → 追加 → 自動保存/起動時に復元」だけ。

画面構成(Designerで配置)

  • MonthCalendar … monthCalendar1
  • TextBox … txtMemo(1行。Multiline=false)
  • Button … btnAdd(テキスト「追加」)
  • ListBox … lstMemos(選択日のメモ一覧を表示)

モデル(保存する形)

public class DayMemo
{
    public DateTime Date { get; set; }      // 日付のみ使う(時刻は 00:00)
    public string Text { get; set; } = "";
}

JSONストレージ(最小・同期版)

using System.Text;
using System.Text.Json;

static class JsonStorage
{
    private static readonly JsonSerializerOptions Opt = new() { WriteIndented = true };

    private static string GetPath()
    {
        var dir = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "MyCompany", "MyMiniMemo");
        Directory.CreateDirectory(dir);
        return Path.Combine(dir, "memos.json");
    }

    public static void Save(List<DayMemo> memos)
    {
        var json = JsonSerializer.Serialize(memos, Opt);
        File.WriteAllText(GetPath(), json, new UTF8Encoding(false)); // UTF-8(BOMなし)
    }

    public static List<DayMemo> Load()
    {
        var path = GetPath();
        if (!File.Exists(path)) return new();
        var json = File.ReadAllText(path, Encoding.UTF8);
        return JsonSerializer.Deserialize<List<DayMemo>>(json) ?? new();
    }
}

フォームコード(コピペで動く最小版)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private List<DayMemo> _all = new(); // ぜんぶのメモ

    public Form1()
    {
        InitializeComponent();

        // 起動時に読み込み&表示
        Load += (_, __) =>
        {
            _all = JsonStorage.Load();
            UpdateListForSelectedDate();
        };

        // 「追加」ボタン
        btnAdd.Click += (_, __) =>
        {
            var text = txtMemo.Text.Trim();
            if (text.Length == 0) return;

            var d = monthCalendar1.SelectionStart.Date;
            _all.Add(new DayMemo { Date = d, Text = text });
            JsonStorage.Save(_all);       // その場で保存
            txtMemo.Clear();
            UpdateListForSelectedDate();  // 画面を更新
        };

        // カレンダーの日付を変えたら、その日のメモだけ表示
        monthCalendar1.DateChanged += (_, __) => UpdateListForSelectedDate();

        // 終了時の保険(万一未保存があっても保存)
        FormClosing += (_, __) => JsonStorage.Save(_all);
    }

    private void UpdateListForSelectedDate()
    {
        var d = monthCalendar1.SelectionStart.Date;
        var lines = _all
            .Where(m => m.Date.Date == d)
            .Select(m => m.Text)
            .ToList();

        lstMemos.DataSource = null;   // 再バインドのため一旦外す
        lstMemos.DataSource = lines;  // テキストだけを表示
    }
}

以下の3行は、「フォームが最初に表示される直前に、保存済みデータを読み込み、画面(その日のリスト)を初期表示する」ためのコードです。

// 起動時に読み込み&表示
Load += (_, __) =>
{
    _all = JsonStorage.Load();
    UpdateListForSelectedDate();
};

何をやっているか(行ごと解説)

  1. Load += …
  • Form.Load イベントにハンドラを登録しています。
  • Load は フォームが最初に可視化される直前に1回 呼ばれるイベントです(コンストラクタの後、Shownの前)。
  • ここでデータ読み込みやデータバインディングを行うのが定番です。
  1. (_, __) => { … }
  • 無名関数(ラムダ式)でハンドラを定義。
  • 本来は (object? sender, EventArgs e) という2引数が来ますが、使わないので慣例的に _ や __ という名前にして「未使用」を示しています(同じ意味で (_, _) => と書くこともあります)。
  1. _all = JsonStorage.Load();
  • 保存ファイル(例:%LOCALAPPDATA%\MyCompany\MyMiniMemo\memos.json)から メモ全件を読み込んで _all にセット
  • ファイルがなければ 空のリストを返す実装になっているはずなので、初回起動でも安全です。
  1. UpdateListForSelectedDate();
  • 直前行で読み込んだ _all から、カレンダーの選択日(monthCalendar1.SelectionStart.Date)のメモだけを抽出し、ListBox に表示し直します。
  • これで起動直後から「きょう(既定は今日が選択)」のメモ一覧が出ます。

似た書き方(等価な長い書式)

this.Load += new EventHandler(Form1_Load);

private void Form1_Load(object? sender, EventArgs e)
{
    _all = JsonStorage.Load();
    UpdateListForSelectedDate();
}
  • どちらでも同じ動作。ラムダ版はその場で短く書けるのが利点です。

なぜコンストラクタではなく Load で?

  • コンストラクタは InitializeComponent() の直後で、UIはまだ描画前
  • Load はフォームのハンドルが作られ、UIの準備が整ったタイミングなので、データバインディングやサイズ依存の初期化などを置くのに都合が良いからです(実務でも「重い処理はLoad、初回表示後に見せたいものはShown」という使い分けが多い)。

補足(実運用の小ワザ)

  • 読み込みが重い場合は Load で時間がかかると初回表示が遅れます。サイズが大きくなったら 非同期読み込み+読み込み後にUI反映にすると体験が良くなります。
  • BoldedDates を使っている版なら、UpdateListForSelectedDate() に加えて UpdateCalendarMarks() もここで呼ぶと、起動直後から太字マークが付きます。

これまでの「コードで += 登録する方法」に加えて、Visual Studioの[プロパティ]ウィンドウ(稲妻アイコン=イベント)から登録する方法を追記します。単機能メモアプリの例にそのまま当てはめています。

  1. 対象を選ぶ
    • フォーム本体のイベント(Load, FormClosing など)→ フォームの背景(空白)をクリックして Form1 を選択
    • コントロールのイベント(btnAdd.Click, monthCalendar1.DateChanged など)→ その コントロールをクリック
  2. [プロパティ]ウィンドウを開く(F4)→ 上部の 稲妻アイコン をクリックして「イベント」一覧に切り替え。
  3. 欄にハンドラ名を入力して Enter例:
    • Form(Form1)の Load → Form1_Load
    • Form(Form1)の FormClosing → Form1_FormClosing
    • btnAdd の Click → btnAdd_Click
    • btnDelete の Click → btnDelete_Click
    • monthCalendar1 の DateChanged → monthCalendar1_DateChanged
    → Enterを押すと コードビハインドにメソッドの雛形が自動生成 され、同時に Designer.cs に購読コードが追加 されます。
  4. 既存メソッドを割り当てたいときは、欄の右端の (ドロップダウン)から既存ハンドラを選択。
  5. 解除したいときは、その欄に入っているハンドラ名を選択して Delete を押す。

注意:Designer登録と手書きの += を併用すると二重登録になります。どちらか一方に統一してください。

直接 Form1.Designer.cs を手で編集しないのが安全です(Designerが上書きします)。


生成される“購読コード”の例(Designer.cs)

  • .NET 6/7/8 のWinForms(SDKスタイル)では概ね次のように生成されます:
this.Load += Form1_Load;
this.FormClosing += Form1_FormClosing;
btnAdd.Click += btnAdd_Click;
btnDelete.Click += btnDelete_Click;
monthCalendar1.DateChanged += monthCalendar1_DateChanged;
  • .NET Frameworkでは古い書式になることがあります:
this.Load += new System.EventHandler(this.Form1_Load);
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
this.btnAdd.Click += new System.EventHandler(this.btnAdd_Click);
this.monthCalendar1.DateChanged += new System.Windows.Forms.DateRangeEventHandler(this.monthCalendar1_DateChanged);

どちらも意味は同じです(言語仕様の簡略記法かどうかの違い)。


ハンドラ本体の中身(例:単機能メモアプリ)

すでにラムダで書いていた処理をそのままメソッドに移すだけです。

// フィールド
private List<DayMemo> _all = new();

// 起動時:保存済みデータを読み込み、当日分を表示
private void Form1_Load(object sender, EventArgs e)
{
    _all = JsonStorage.Load();
    UpdateListForSelectedDate();
    // BoldedDates も使っている場合は↓を追加
    // UpdateCalendarMarks();
}

// 追加ボタン:1行メモを追加して保存→再描画
private void btnAdd_Click(object sender, EventArgs e)
{
    var text = txtMemo.Text.Trim();
    if (text.Length == 0) return;

    var d = monthCalendar1.SelectionStart.Date;
    _all.Add(new DayMemo { Date = d, Text = text });
    JsonStorage.Save(_all);

    txtMemo.Clear();
    UpdateListForSelectedDate();
    // UpdateCalendarMarks();
}

// 削除ボタン:選択中の1件を削除して保存→再描画(ListBox版)
private void btnDelete_Click(object sender, EventArgs e)
{
    if (lstMemos.SelectedItem is DayMemo target)
    {
        _all.Remove(target);
        JsonStorage.Save(_all);
        UpdateListForSelectedDate();
        // UpdateCalendarMarks();
    }
}

// カレンダーの日付変更:当日分だけに表示を切り替え
private void monthCalendar1_DateChanged(object sender, DateRangeEventArgs e)
{
    UpdateListForSelectedDate();
}

// 終了時:保険として保存
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    JsonStorage.Save(_all);
}

UpdateListForSelectedDate() や(使っていれば)UpdateCalendarMarks() は、これまでの実装をそのまま利用します。


どっちを使うべき?

  • 初学者やUI中心の開発なら、プロパティウィンドウで登録が直感的で分かりやすい。
  • コードで一括管理したい/条件で付け外ししたい場合は、コンストラクタ内で += に統一。

チームや教材ではどちらかに統一して、二重登録を避ける運用にしてください。


動かし方

  1. 上の4つのコントロールをフォームに置いて、名前を合わせる。
  2. DayMemo、JsonStorage、Form1 のコードを貼る。
  3. 実行 → 日付を選ぶ → メモを入力 → 追加。次回起動時も残っています。

なぜこれが「単機能」か

  • 複雑な編集や削除、並び替え、太字表示(BoldedDates)はあえて外しました。
  • まず「入力→保存→復元→表示」の最小サイクルを体で覚えるのが目的です。

発展課題(必要になったら)

  • 削除:lstMemos.SelectedIndex を使って対象の DayMemo を _all から削除→保存→再描画。
  • 日付に印(BoldedDates):その日にメモが1つでもあれば太字で強調。
  • 原子的保存:memos.json.tmp に書いてから Move で置換(停電・異常終了に強い)。
  • DataGridView化:複数列(例:カテゴリ)を持たせたい場合は BindingList<DayMemo> + DataGridViewに変更。

発展課題4点をそれぞれ反映した“そのまま動く”サンプルを2種類用意しました。

  • A) ListBox版:削除/BoldedDates/原子的保存
  • B) DataGridView版:BindingList<DayMemo>+カテゴリ列/BoldedDates/原子的保存

A) ListBox版(削除+BoldedDates+原子的保存)

画面(Designer で配置するコントロール名)

  • MonthCalendar … monthCalendar1
  • TextBox … txtMemo(1行)
  • Button … btnAdd(追加)
  • ListBox … lstMemos
  • Button … btnDelete(削除)

モデル & ストレージ

public class DayMemo
{
    public DateTime Date { get; set; }      // 日付のみ使用(時刻は 00:00 運用)
    public string Text { get; set; } = "";
}
using System.Text;
using System.Text.Json;

static class JsonStorage
{
    private static readonly JsonSerializerOptions Opt = new() { WriteIndented = true };

    private static string GetPath()
    {
        var dir = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "MyCompany", "MyMiniMemo");
        Directory.CreateDirectory(dir);
        return Path.Combine(dir, "memos.json");
    }

    public static void Save(List<DayMemo> memos)
    {
        var path = GetPath();
        var tmp  = path + ".tmp";                         // 原子的保存:一時ファイルへ
        var json = JsonSerializer.Serialize(memos, Opt);

        File.WriteAllText(tmp, json, new UTF8Encoding(false));
        File.Move(tmp, path, overwrite: true);            // 置換(.NET 6+)
    }

    public static List<DayMemo> Load()
    {
        var path = GetPath();
        if (!File.Exists(path)) return new();
        var json = File.ReadAllText(path, Encoding.UTF8);
        return JsonSerializer.Deserialize<List<DayMemo>>(json) ?? new();
    }
}

フォームコード(そのまま貼り付け)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private List<DayMemo> _all = new(); // 全メモ

    public Form1()
    {
        InitializeComponent();

        Load += (_, __) =>
        {
            _all = JsonStorage.Load();
            UpdateListForSelectedDate();
            UpdateCalendarMarks();
        };

        // 追加
        btnAdd.Click += (_, __) =>
        {
            var text = txtMemo.Text.Trim();
            if (text.Length == 0) return;

            var d = monthCalendar1.SelectionStart.Date;
            _all.Add(new DayMemo { Date = d, Text = text });
            JsonStorage.Save(_all);

            txtMemo.Clear();
            UpdateListForSelectedDate();
            UpdateCalendarMarks();
        };

        // 削除(選択中の1件を削除)
        btnDelete.Click += (_, __) =>
        {
            if (lstMemos.SelectedItem is DayMemo target)
            {
                _all.Remove(target);            // 同一参照なのでこれでOK
                JsonStorage.Save(_all);
                UpdateListForSelectedDate();
                UpdateCalendarMarks();
            }
        };

        // 日付が変わったらその日のメモだけ表示
        monthCalendar1.DateChanged += (_, __) => UpdateListForSelectedDate();

        // 念のため終了時にも保存
        FormClosing += (_, __) => JsonStorage.Save(_all);
    }

    private void UpdateListForSelectedDate()
    {
        var d = monthCalendar1.SelectionStart.Date;
        // 元リストの同一参照を持つフィルタビュー
        var filtered = _all.Where(m => m.Date.Date == d).ToList();

        lstMemos.DataSource = null;
        lstMemos.DisplayMember = nameof(DayMemo.Text);
        lstMemos.ValueMember   = null;
        lstMemos.DataSource    = filtered;
    }

    private void UpdateCalendarMarks()
    {
        var dates = _all
            .Select(m => m.Date.Date)
            .Distinct()
            .ToArray();

        monthCalendar1.BoldedDates = dates;   // 1つでもメモがあれば太字
        monthCalendar1.UpdateBoldedDates();
    }
}

B) DataGridView版(BindingList<DayMemo>+カテゴリ列)

複数列を扱うテーブル運用に切替。BindingList<T> を使うので、編集が即反映されます。

画面(Designer の名前)

  • MonthCalendar … monthCalendar1(BoldedDates用)
  • DataGridView … dataGridView1(AutoGenerateColumns = true 推奨)
  • TextBox … txtTitle(件名)
  • TextBox … txtCategory(カテゴリ)
  • DateTimePicker … dtpDate(追加時の期日)
  • Button … btnAddRow(追加)
  • Button … btnDeleteRow(選択行削除)

モデル(カテゴリ列を追加)

public class DayMemo
{
    public int      Id       { get; set; }
    public DateTime Date     { get; set; }    // 期日
    public string   Title    { get; set; } = "";
    public string   Category { get; set; } = "";
}

※ 既存ファイルが {Date,Text} だけでも、読み込み時に足りないプロパティは既定値になります。

ストレージ(Aと同じ実装でOK)

上記 JsonStorage をそのまま使えます(ファイル名やフォルダ名を変えたい場合は適宜)。

フォームコード

using System;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Collections.Generic;

public partial class FormGrid : Form
{
    private BindingList<DayMemo> _items = new();

    public FormGrid()
    {
        InitializeComponent();

        Load += (_, __) =>
        {
            var loaded = JsonStorage.Load();

            // 既存の {Date, Text} 形式から来たときの互換処理
            int nextId = 1;
            foreach (var x in loaded)
            {
                if (x.Id == 0) x.Id = nextId++;
                if (x.Title is null || x.Title.Length == 0)
                {
                    // 旧 Text を Title として扱う(旧モデルからの移行用)
                    // Text プロパティがない場合は無視(新モデルのみ)
                    try
                    {
                        var textProp = x.GetType().GetProperty("Text");
                        if (textProp != null)
                        {
                            var oldText = textProp.GetValue(x) as string;
                            if (!string.IsNullOrEmpty(oldText)) x.Title = oldText;
                        }
                    }
                    catch { /* 何もしない */ }
                }
            }

            _items = new BindingList<DayMemo>(loaded);
            BindGrid();
            UpdateCalendarMarks();
        };

        FormClosing += (_, __) => JsonStorage.Save(_items.ToList());

        btnAddRow.Click += (_, __) =>
        {
            var item = new DayMemo
            {
                Id       = _items.Count == 0 ? 1 : _items.Max(i => i.Id) + 1,
                Date     = dtpDate.Value.Date,
                Title    = txtTitle.Text.Trim(),
                Category = txtCategory.Text.Trim()
            };
            if (item.Title.Length == 0) return;

            _items.Add(item);
            JsonStorage.Save(_items.ToList());
            UpdateCalendarMarks();

            txtTitle.Clear();
            // txtCategory は残す運用でもOK
        };

        btnDeleteRow.Click += (_, __) =>
        {
            foreach (DataGridViewRow row in dataGridView1.SelectedRows)
            {
                if (row.DataBoundItem is DayMemo x)
                {
                    _items.Remove(x);
                }
            }
            JsonStorage.Save(_items.ToList());
            UpdateCalendarMarks();
        };

        // グリッド上で直接編集された場合にも太字を更新
        _items.ListChanged += (_, __) =>
        {
            UpdateCalendarMarks();
        };
    }

    private void BindGrid()
    {
        dataGridView1.AutoGenerateColumns = true;
        dataGridView1.DataSource = _items;

        // もし列を明示したい場合は AutoGenerateColumns=false にして
        // DataGridViewTextBoxColumn 等を手動追加してください。
    }

    private void UpdateCalendarMarks()
    {
        var dates = _items
            .Select(x => x.Date.Date)
            .Distinct()
            .ToArray();

        monthCalendar1.BoldedDates = dates;
        monthCalendar1.UpdateBoldedDates();
    }
}

メモ

  • 原子的保存:.tmp → Move(overwrite:true) で既存ファイルの破損を防止(.NET 6+)。
  • BoldedDates は“日単位”の強調です。数が多い場合は必要に応じて最適化してください。
  • DataGridView 版は行を直接編集できます。保存タイミングはここでは「行追加・削除時+終了時」にしています。頻繁に編集するなら、数秒デバウンス自動保存にするのが快適です。

参考)

訪問数 2 回, 今日の訪問数 3回