Repositoryの例
#Repository
#interface
#依存性の注入
#DI
#initializer
更新: 2026-02-14
Repositoryファイルと
その元となるInterface
を提示する
--- IBookmarkRepository.cs ---
using System;
using System.Collections.Generic;
using System.Text;
namespace Bookmarks
{
public interface IBookmarkRepository
{
Task<List<Bookmark>> GetAllAsync();
Task<long> AddAsync(string name, string url);
Task UpdateAsync(long id, string name, string url);
Task DeleteAsync(long id);
}
}
--- Bookmark.cs ---
using System;
using System.Collections.Generic;
using System.Text;
namespace Bookmarks
{
public class Bookmark
{
public long Id { get; set; }
public string Name { get; set; } = "";
public string Url { get; set; } = "";
}
}
--- IDatabaseInitializer.cs ---
using System;
using System.Collections.Generic;
using System.Text;
namespace Bookmarks
{
internal interface IDatabaseInitializer
{
Task InitializeAsync();
}
}
--- SqliteDatabaseInitializer.cs ---
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.Text;
namespace Bookmarks
{
internal class SqliteDatabaseInitializer : IDatabaseInitializer
{
// exeと同じフォルダに置くDBファイル名
private const string DbFileName = "bookmarks.db";
public async Task InitializeAsync()
{
// 1) exeと同じフォルダのDBパス
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string dbPath = Path.Combine(baseDir, DbFileName);
// 2) 接続(SQLiteはファイルが無ければ自動作成されます)
string cs = $"Data Source={dbPath}";
await using var conn = new SqliteConnection(cs);
await conn.OpenAsync();
// 3) 任意:安定性向上(好みで)
await ExecuteNonQueryAsync(conn, "PRAGMA journal_mode = WAL;");
await ExecuteNonQueryAsync(conn, "PRAGMA synchronous = NORMAL;");
// 4) DDL(テーブルが無ければ作成)
string ddl = @"
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL
);";
await ExecuteNonQueryAsync(conn, ddl);
}
private static async Task ExecuteNonQueryAsync(SqliteConnection conn, string sql)
{
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
}
}
}
--- SqliteBookmarkRepository.cs ---
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.Text;
namespace Bookmarks
{
public class SqliteBookmarkRepository : IBookmarkRepository
{
private const string DbFileName = "bookmarks.db";
private static string ConnectionString
{
get
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string dbPath = Path.Combine(baseDir, DbFileName);
return $"Data Source={dbPath}";
}
}
public async Task<List<Bookmark>> GetAllAsync()
{
var list = new List<Bookmark>();
await using var conn = new SqliteConnection(ConnectionString);
await conn.OpenAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT id, name, url FROM bookmarks ORDER BY id DESC;";
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync())
{
list.Add(new Bookmark
{
Id = r.GetInt64(0),
Name = r.GetString(1),
Url = r.GetString(2)
});
}
return list;
}
public async Task<long> AddAsync(string name, string url)
{
await using var conn = new SqliteConnection(ConnectionString);
await conn.OpenAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = @"
INSERT INTO bookmarks(name, url) VALUES ($name, $url);
SELECT last_insert_rowid();";
cmd.Parameters.AddWithValue("$name", name);
cmd.Parameters.AddWithValue("$url", url);
object? idObj = await cmd.ExecuteScalarAsync();
return Convert.ToInt64(idObj);
}
public async Task UpdateAsync(long id, string name, string url)
{
await using var conn = new SqliteConnection(ConnectionString);
await conn.OpenAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "UPDATE bookmarks SET name=$name, url=$url WHERE id=$id;";
cmd.Parameters.AddWithValue("$id", id);
cmd.Parameters.AddWithValue("$name", name);
cmd.Parameters.AddWithValue("$url", url);
await cmd.ExecuteNonQueryAsync();
}
public async Task DeleteAsync(long id)
{
await using var conn = new SqliteConnection(ConnectionString);
await conn.OpenAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM bookmarks WHERE id=$id;";
cmd.Parameters.AddWithValue("$id", id);
await cmd.ExecuteNonQueryAsync();
}
}
}
--- Program.cs ---
using Microsoft.Extensions.DependencyInjection;
namespace Bookmarks
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static async Task Main()
{
ApplicationConfiguration.Initialize();
// DI コンテナ構築(Hostは使わない)
var services = new ServiceCollection();
services.AddSingleton<IDatabaseInitializer, SqliteDatabaseInitializer>();
services.AddSingleton<IBookmarkRepository, SqliteBookmarkRepository>();
services.AddTransient<Form1>();
using var sp = services.BuildServiceProvider();
// ★ アプリ起動前にDB初期化
await sp.GetRequiredService<IDatabaseInitializer>()
.InitializeAsync();
Application.Run(sp.GetRequiredService<Form1>());
}
}
}
ポイント
ポップアップメニューの表示方法
#DataGridView
#popup menu
#contextMenuStrip
#async
更新: 2026-02-14
DataGridViewのデータが存在する部分を右クリックされたら、ポップアップメニューを表示する
コントロール名はcontextMenuStrip
Itemはデザイナーから設定する。
Itemをクリックしたら動作するイベントハンドラーはアイテムを表示して、それをダブルクリックする。
(toolStripMenuDelete_Click()が実処理の部分)
private void dataGridView1_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
// 右クリック、かつデータ行(ヘッダー以外)の上である場合
if (e.Button == MouseButtons.Right && e.RowIndex >= 0)
{
// 1. 右クリックした行を選択状態にする(これまでの処理)
dataGridView1.ClearSelection();
dataGridView1.Rows[e.RowIndex].Selected = true;
dataGridView1.CurrentCell = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex >= 0 ? e.ColumnIndex : 0];
// 2. 【ここを追加!】マウスの右クリックした位置にメニューを強制表示
// Cursor.Position はスクリーン上のマウス位置、dataGridView1 は表示の基準となるコントロールです
contextMenuStrip1.Show(Cursor.Position);
}
}
---
private async void toolStripMenuDelete_Click(object sender, EventArgs e)
{
if (dataGridView1.CurrentRow == null) return;
// 2. 選択されているデータオブジェクト(Bookmark)を取得
var selectedBookmark = dataGridView1.CurrentRow?.DataBoundItem as Bookmark;
if (selectedBookmark == null) return;
// 3. 確認ダイアログを表示
// ※フォントサイズを大きくしたい場合は、前回提案したカスタムフォーム(MyMessageBox)を使用してください
DialogResult result = MessageBox.Show(
$"{selectedBookmark.Name} を削除してもよろしいですか?",
"削除の確認",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
// 4. 「はい」が押された場合のみ処理を続行
if (result == DialogResult.Yes)
{
try
{
// 5. 非同期でリポジトリの削除メソッドを呼び出す
await _repo.DeleteAsync(selectedBookmark.Id);
// 6. メモリ上のリストから削除(これでUIからも自動的に消えます)
_bookmarkList.Remove(selectedBookmark);
}
catch (Exception ex)
{
// エラーハンドリング(Addの時と同様の形式)
MessageBox.Show($"削除に失敗しました:\n{ex.Message}", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
ポイント
contextMenuStrip1.Show(Cursor.Position);
これがないと変な場所に出る
DBなどのAsync処理がある場合、関数自体もasyncにする
Windowsはシングルクリックとダブルクリックを分けられない
#クリック
#シングルクリック
#ダブルクリック
更新: 2026-02-11
結論から申し上げますと、DataGridView はシングルクリック(CellClick)とダブルクリック(CellDoubleClick)をイベントとして明確に使い分けることができます。
ただし、Windows の仕様上、ダブルクリックをした際には**「シングルクリックのイベントが走った直後にダブルクリックのイベントが走る」**という挙動になります。そのため、シングルクリック側に「ブラウザを開く」という重い処理を入れる場合は、少し注意が必要です。
イベントの競合:
前述の通り、ダブルクリックして「編集」しようとした際にも、1回目のクリックで「ブラウザが開く」動作が走ります。
対策1: 「URL列」など特定の列をクリックした時だけブラウザを開くように、e.ColumnIndex で判定を入れる。
対策2: DataGridViewLinkColumn を使用し、CellContentClick(文字部分のクリック)で反応させる。
UseShellExecute:
現在 .NET (Core/5/6以降) を使用されている場合、単に Process.Start(url) と書くと例外が発生します。ProcessStartInfo を使って UseShellExecute = true を指定するのがモダンな書き方です。
もし、シングルクリックで「選択」、ダブルクリックで「編集」という標準的な挙動を維持しつつ、リンクも開きたい場合は、「Ctrlキーを押しながらクリックした時だけブラウザを開く」 という仕様にするのも非常に使いやすい(エンジニア好み)ですよ。
/// <summary>
/// セルクリック時に、Ctrlキーが押されていれば適切なアプリケーションで開きます。
/// Webサイトならブラウザ、それ以外(ファイルパス等)なら VS Code で開きます。
/// </summary>
private void DataGridView1_CellClick(object? sender, DataGridViewCellEventArgs e)
{
// ヘッダー行や無効なインデックスは無視
if (e.RowIndex < 0) return;
// 行データ(Bookmarkオブジェクト)の取得
var row = dataGridView1.Rows[e.RowIndex];
if (row.DataBoundItem is not Bookmark bookmark) return;
// ★ Ctrlキーが押されている場合のみ実行
if (Control.ModifierKeys == Keys.Control)
{
string pathOrUrl = bookmark.Url.Trim();
if (string.IsNullOrEmpty(pathOrUrl)) return;
try
{
if (pathOrUrl.StartsWith("http://") || pathOrUrl.StartsWith("https://"))
{
// 1. Webサイトの場合:既定のブラウザで開く
Process.Start(new ProcessStartInfo
{
FileName = pathOrUrl,
UseShellExecute = true
});
}
else
{
// 2. それ以外(ファイルパス、フォルダ、ドメインなど):VS Codeで開く
// "code" コマンドにパスを引数として渡します
Process.Start(new ProcessStartInfo
{
FileName = "code",
// パスにスペースが含まれている場合に備え、ダブルクォーテーションで囲む
Arguments = $"\"{pathOrUrl}\"",
UseShellExecute = true,
CreateNoWindow = true
});
}
}
catch (Exception ex)
{
// VS Codeがインストールされていない、またはパスが不正な場合のエラー処理
MessageBox.Show($"起動に失敗しました:\n{ex.Message}", "実行エラー",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
ポイント
Windowsの標準的な挙動では、ダブルクリック(DoubleClick)は以下の順序でイベントが発生します:
MouseDown
Click
MouseUp
(ここでダブルクリック判定の時間を待つ)
MouseDown
DoubleClick
MouseUp
通常の CellClick に「URLを開く」処理を入れてしまうと、手順2の時点でブラウザが起動してしまい、手順6の編集画面へ辿り着く前にフォーカスがブラウザに奪われてしまいます。そのため、**「特殊な入力(Ctrlキーや右クリック)」**をトリガーにするのが、シングルカラム(1列)UIにおける最適解です。
List<T>はデータの追加を DataGridView に通知する機能を持っていない
#list<T>
#BindingList<T>
#DataGridView追加
更新: 2026-02-11
C# の List<T> に要素を追加して Refresh() を呼んでも、DataGridView は「リストの中身(個数)が増えたこと」を検知できず、表示が更新されません。今回「最後に追加した行しか出ない」ように見えるのは、グリッドが内部的に持っている行数管理と実際のデータ数が食い違っているためです。
これを解決するには、WinForms で標準的に使われる BindingList<T> を使用するのが最も確実でプロフェッショナルな方法です。
修正案:BindingList を使用する
BindingList を使うと、リストに Add や Insert をした瞬間に、DataGridView が自動的にそれを検知して行を増やしてくれます。
public partial class Form1 : Form
{
private readonly IBookmarkRepository _repo;
// ★ リストを保持するフィールドを追加
private BindingList<Bookmark> _bookmarkList = new();
public Form1(IBookmarkRepository repo)
{
InitializeComponent();
_repo = repo;
// データソースをあらかじめ紐付けておく
dataGridView1.DataSource = _bookmarkList;
this.Load += Form1_Load;
// ... 他の初期化設定 ...
}
}
---
private async void Form1_Load(object? sender, EventArgs e)
{
dataGridView1.AutoGenerateColumns = false;
// DBから全件取得
var data = await _repo.GetAllAsync();
// BindingList を作り直して DataSource にセット
_bookmarkList = new BindingList<Bookmark>(data);
dataGridView1.DataSource = _bookmarkList;
}
---
if (result == DialogResult.OK)
{
// null 合体演算子を使用して値を安全に取得
string name = form.EditedName ?? string.Empty;
string url = form.EditedUrl ?? string.Empty;
try
{
// 1. データベースに非同期で追加し、新規IDを取得
long newId = await _repo.AddAsync(name, url);
// 2. メモリ保持用のオブジェクトを生成
var newBookmark = new Bookmark { Id = newId, Name = name, Url = url };
// 3. BindingList の先頭に挿入(この時点で画面に自動反映されます)
_bookmarkList.Insert(0, newBookmark);
// ★ BindingList を使用している場合、Refresh() は基本不要ですが、
// 描画の乱れを防ぐおまじないとして残しておいても問題ありません。
// dataGridView1.Refresh();
}
catch (Exception ex)
{
// テスト・運用時に役立つ詳細なエラー表示
MessageBox.Show($"データベースへの追加に失敗しました:\n{ex.Message}",
"エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
ポイント
なぜこれで解決するのか?
通知メカニズム: BindingList は IBindingList インターフェースを実装しており、「項目が増えた」「減った」というイベントを DataGridView に飛ばします。
Refresh() の限界: dataGridView1.Refresh() は「見た目を再描画する」だけで、「データの行数を確認し直す」機能はありません。
別の方法(手っ取り早く直す場合)
もし BindingList を使わずに今のコードのまま直したい場合は、以下のように DataSource を一度 null にして再設定 する必要がありますが、スクロール位置がリセットされるなどの弊害があるため、上記 BindingList の方が推奨されます。
// 非推奨だが動く方法
dataGridView1.DataSource = null;
dataGridView1.DataSource = list;
確認ポイント
Refresh() について:
_bookmarkList が BindingList であれば、Insert した瞬間にグリッドに新しい行が表示されます。そのため、Refresh() は書かなくても動作します。
例外処理の範囲:
AddAsync(DB操作)だけでなく、その後の Insert(UI操作)も try ブロックに入っているため、万が一のメモリ不足などのトラブルもキャッチできる安全な構成になっています。
文字列がURI形式かあるいはWindowsのパス形式かチェックする
#url
#path
#windows path
更新: 2026-02-11
http(s):// ... OK
C:\xxx ... OK
それ以外はエラー
最も正確で推奨される方法は Path.IsPathFullyQualified メソッドを使用することです。
C# で http:// または https:// から始まる有効な URL かどうかを判定するには、Uri.TryCreate メソッドを使用するのが最も堅牢で「プロ」に近いやり方です。
単純な文字列比較(StartsWith)でも判定は可能ですが、Uri クラスを使うことで「URL としての構造が正しいか」まで同時にチェックできます。
1. Uri.TryCreate を使った推奨コード
この方法は、URL の形式(スキーム)だけでなく、不正な文字が含まれていないかも検証できるため、テストの観点からも非常に信頼性が高いです。
// URLの形式が正しいか簡易チェック
// 1. URLとして解析可能かチェック
// 2. 絶対参照(Absolute)であることを確認
bool isUrl = false;
if (Uri.TryCreate(EditedUrl, UriKind.Absolute, out Uri? resultUri) &&
(resultUri.Scheme == Uri.UriSchemeHttp || resultUri.Scheme == Uri.UriSchemeHttps))
{
// 有効な http:// または https:// の形式
isUrl = true;
}
// URLがWindowsのファイルパス形式の場合も許可する(絶対パスのみ)
var isFilePath = Path.IsPathFullyQualified(EditedUrl);
if (!isUrl && !isFilePath)
{
MessageBox.Show("URLの形式が正しくありません。", "入力エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
ポイント
.NET Framework 環境(旧来のWinFormsなど)の場合
IsPathFullyQualified が使えない古いバージョンでは、Path.IsPathRooted と Path.GetPathRoot を組み合わせて判定します。
C#
using System.IO;
string EditedUrl = txtUrl.Text.Trim();
// 1. ルートが含まれているか(\ や C: で始まっているか)を確認
// 2. そのルートが単なる「\」ではなく、ドライブレター等を含んでいるか確認
bool isAbsolute = Path.IsPathRooted(EditedUrl) &&
!EditedUrl.StartsWith("\\") ||
(EditedUrl.StartsWith("\\\\")); // UNCパス対応
// より厳密に「C:\」などの形式を確認する場合:
string? root = Path.GetPathRoot(EditedUrl);
if (root != null && root.Length >= 2 && (root.Contains(":") || root.StartsWith("\\\\")))
{
// 絶対パスとみなす
}
コードのポイント解説
UriKind.Absolute:
相対パス(例: /index.html)を「不正」として弾き、必ずプロトコル(スキーム)から始まる絶対パスであることを保証します。
Uri.UriSchemeHttp(s):
文字列で "http" と書く代わりに標準定数を使うことで、タイポを防ぎ、コードの意図を明確にします。
例外が発生しない:
new Uri(input) と書くと不正な文字列でクラッシュしますが、TryCreate は false を返すだけなので安全です。
標準的な「キャンセル(ESC)」や「確定(Enter)」以外の**特殊なキー(F12など)**に独自の動作を割り当てたい
#ショートカット
#short cut
#ショートカットキー
更新: 2026-02-11
標準的な「キャンセル(ESC)」や「確定(Enter)」以外の**特殊なキー(F12など)**に独自の動作を割り当てたい場合は、フォームの KeyDown イベント を使用します。
実装には2つのステップが必要です。
実装の手順
1. フォームの KeyPreview を有効にする
デフォルトでは、フォーカスがあるコントロール(テキストボックスなど)がキー入力を先に受け取ってしまいます。これを「フォームが先に受け取る」設定に変更します。
2. KeyDown イベントでキーを判定する
押されたキーが F12 かどうかをチェックし、目的のボタンの PerformClick() メソッドを呼び出します。
public partial class BookmarkEditForm : Form
{
// プロパティ等は省略...
/// <summary>
/// フォームの初期設定を行います。
/// </summary>
private void SetupForm()
{
InitializeComponent();
// --- ステップ1: フォームが先にキー入力を受け取るようにする ---
// これを忘れると、テキストボックスにフォーカスがある時にキーを拾えません
this.KeyPreview = true;
// --- イベントの登録 ---
this.KeyDown += BookmarkEditForm_KeyDown;
buttonUpdate.Click += buttonUpdate_Click;
buttonCancel.Click += buttonCancel_Click;
// 標準的なボタン対応(前回の内容)
this.AcceptButton = buttonUpdate; // Enter
this.CancelButton = buttonCancel; // ESC
}
/// <summary>
/// キーボードの入力イベントを処理します。
/// </summary>
private void BookmarkEditForm_KeyDown(object? sender, KeyEventArgs e)
{
// ケース1: F12キーで特定のボタン(buttonAなど)を発火させる
if (e.KeyCode == Keys.F12)
{
// マウスでクリックした時と同じ動作を実行
buttonUpdate.PerformClick();
// OSや他のコントロールへこのイベントをこれ以上渡さない
e.Handled = true;
}
// ケース2: Ctrl + S で保存(更新)ボタンを発火させる
// Modifiers を使うことで「Ctrlが押されているか」を判定できます
if (e.Control && e.KeyCode == Keys.S)
{
buttonUpdate.PerformClick();
e.Handled = true;
// ビープ音などを防ぐ(必要に応じて)
e.SuppressKeyPress = true;
}
}
private void buttonUpdate_Click(object? sender, EventArgs e)
{
// 更新処理...
this.DialogResult = DialogResult.OK;
this.Close();
}
private void buttonCancel_Click(object? sender, EventArgs e)
{
// キャンセル処理...
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
ポイント
KeyPreview = true: これが最も重要です。フォームレベルでキーを横取りするために必須の設定です。
PerformClick(): ボタンの Click イベント内に書かれたロジックを再利用できるため、コードの重複を防げます。
e.SuppressKeyPress = true: Ctrl + S などを押したときに Windows 標準の「ポン」という警告音が鳴るのを防ぎたい場合に使用します。
複数キーの組み合わせ: e.Control (Ctrl), e.Shift (Shift), e.Alt (Alt) プロパティを使って、より複雑なショートカットも作成可能です。
ESC, ENTERキーをボタンに割り当てる
#esc
#enter
#button
更新: 2026-02-11
ESCキーを押したら、キャンセルボタンが発火する
ENTERキーを押したら、確定ボタンが発火する
public partial class BookmarkEditForm : Form
{
// 呼び出し元に返す値
public string? EditedName { get; private set; }
public string? EditedUrl { get; private set; }
/// <summary>
/// コンストラクタ共通の初期化処理。
/// コントロールの生成とイベントの紐付けを担当します。
/// </summary>
private void SetupForm()
{
InitializeComponent();
// イベントハンドラーをコードで登録
btnUpdate.Click += BtnUpdate_Click;
btnCancel.Click += BtnCancel_Click;
btnUpdate.TabIndex = 0;
btnCancel.TabIndex = 1;
btnCancel.TabIndex = 2;
btnUpdate.TabIndex = 3;
// キャンセルボタンをESCに対応させる
this.CancelButton = btnCancel;
// Enterキーで更新ボタンを押せるようにする
this.AcceptButton = btnUpdate;
}
// 新規追加用
public BookmarkEditForm()
{
SetupForm();
}
// 編集用(★これが必要)
public BookmarkEditForm(string name, string url)
{
SetupForm();
txtName.Text = name;
txtUrl.Text = url;
}
// 固定値の初期化ならここでOK
private void BookmarkEditForm_Load(object sender, EventArgs e)
{
}
private void BtnUpdate_Click(object? sender, EventArgs e)
{
EditedName = txtName.Text.Trim();
EditedUrl = txtUrl.Text.Trim();
DialogResult = DialogResult.OK;
Close();
}
private void BtnCancel_Click(object? sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
ポイント
小画面の表示位置を親画面の中央にする
#小画面表示
更新: 2026-02-11
1. 小画面を親画面の中心に表示する
WinFormsでは、フォームの StartPosition プロパティを設定することで表示位置を制御できます。親画面の中央に出すには CenterParent を指定します。
private async void AddButton_Click(object sender, EventArgs e)
{
// 新規なので初期値は空
using var form = new BookmarkEditForm("", "");
// ★ 表示位置を親画面(this)の中央にする
form.StartPosition = FormStartPosition.CenterParent;
// ★ タイトルを追加用に設定
form.Text = "ブックマーク追加";
if (form.ShowDialog(this) == DialogResult.OK)
{
// ... _repo.AddAsync(form.EditedName, form.EditedUrl) などの追加処理 ...
}
}
ポイント
CenterParent の注意点: ShowDialog(this) のように、引数に親フォーム(this)を渡して呼び出さないと、正しく中央に配置されないことがあります。
DB更新とDataGridViewの更新
#update
#event handler
#callback
更新: 2026-02-11
すでに Form1 のコンストラクタで _repo が注入(依存性注入)されているため、そのフィールドをそのまま使用して非同期更新を行います。
イベントハンドラー(コールバック)の登録方法も示します
注意点
デザイナー(雷マークのアイコン)からも登録し、コードでも += で登録すると、1回のダブルクリックでイベントが2回走ることになります。どちらか片方に統一するのが鉄則です。
**「どこで何が起きているか(デバッグのしやすさ)」**を優先するなら、コードで明示的に登録する今のスタイルを継続するのが正解と言えます。
public Form1(IBookmarkRepository repo)
{
InitializeComponent();
_repo = repo;
// --- イベントの登録 ---
this.Load += Form1_Load;
// DataGridViewのダブルクリックイベントをコードで紐付け
dataGridView1.CellDoubleClick += dataGridView1_CellDoubleClick;
// --- 外観の設定(既にあるコード) ---
this.Text = "ブックマーク一覧";
// ... 以下略 ...
}
---
/// <summary>
/// DataGridViewのセルをダブルクリックした際に、該当する行のブックマーク情報を編集フォームで開き、
/// データベースおよび画面表示を更新します。
/// </summary>
/// <param name="sender">イベントの発生元(DataGridView)</param>
/// <param name="e">セルの位置情報を含むイベント引数</param>
/// <remarks>
/// コンストラクタで受け取ったリポジトリ(_repo)を使用して非同期に更新を永続化します。
/// </remarks>
private async void dataGridView1_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
// 1. ヘッダー行や無効な領域をクリックした場合は処理を中断
if (e.RowIndex < 0) return;
// 2. クリックされた行に紐付いているデータオブジェクトを取得
var row = dataGridView1.Rows[e.RowIndex];
// 3. DataBoundItem が Bookmark 型であることを判定し、変数「bookmark」に割り当てる
if (row.DataBoundItem is not Bookmark bookmark) return;
// 4. 編集用フォームを生成(現在の値を初期値としてコンストラクタに渡す)
using var form = new BookmarkEditForm(bookmark.Name, bookmark.Url);
// 5. フォームをモーダルダイアログとして表示し、ユーザーの操作結果を取得
var result = form.ShowDialog(this);
// 6. 「OK」ボタンで終了した場合のみ、データの更新処理を開始
if (result == DialogResult.OK)
{
// フォームで入力された新しい値を、メモリ上のオブジェクトに反映
if (form.EditedName is not null) bookmark.Name = form.EditedName;
if (form.EditedUrl is not null) bookmark.Url = form.EditedUrl;
try
{
// 7. フィールド _repo を使用して、データベースの値を非同期で更新(待機する)
await _repo.UpdateAsync(bookmark.Id, bookmark.Name, bookmark.Url);
// 8. オブジェクトの変更を DataGridView の画面表示に反映させる
dataGridView1.Refresh();
}
catch (Exception ex)
{
// データベースの接続エラーやSQLエラーが発生した場合の例外処理
MessageBox.Show($"データベースの更新に失敗しました:\n{ex.Message}", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
ポイント
イベントの紐付け:
Form1 のコンストラクタ内で、イベントハンドラーを登録するのを忘れないようにしてください。
データ同期の仕組み:
dataGridView1.DataSource にセットされた List 内のオブジェクトと、ここで編集している bookmark 変数は、メモリ上の同じ参照を指しています。そのため、UpdateAsync で DB を更新し、Refresh() を呼ぶだけで画面表示が最新状態に同期されます。
なぜ今のコードでは「コード登録」がおすすめなのか
現在の Form1 は、以下の理由から**コードでの登録(Code-First 的なアプローチ)**と非常に相性が良いです。
依存性注入(DI)を使っている:
コンストラクタで _repo を受け取っていますね。このように「ロジックを外部から持ち込む」スタイルは、UIの設定も明示的にコードで書くプロフェッショナルなスタイルと親和性が高いです。
プロパティ設定をコードにまとめている:
フォント設定や SelectionMode などをコードで細かく制御されています。これらと一緒にイベント登録も並べておけば、「このフォームの初期化ロジック」が1箇所に集約されます。
Designer.cs の「ブラックボックス化」を防ぐ:
デザイナーで設定すると、Visual Studio が自動生成する Form1.Designer.cs の中に登録コードが書き込まれます。ここを直接編集することは推奨されないため、Git などの差分管理で「いつの間にかイベントが外れていた」というトラブルが起きやすくなります。
DataGridView のダブルクリックによる編集処理
#DataGridView
#DoubleClick
#using宣言
更新: 2026-02-11
DataGridView のダブルクリックによる編集処理例
using System;
using System.Collections.Generic;
using System.Text;
namespace Bookmarks
{
public class Bookmark
{
public long Id { get; set; }
public string Name { get; set; } = "";
public string Url { get; set; } = "";
}
}
---
/// <summary>
/// DataGridViewのセルをダブルクリックした際に、該当する行のブックマーク情報を編集するフォームを表示します。
/// </summary>
/// <param name="sender">イベントの発生元(DataGridView)</param>
/// <param name="e">セルの位置情報(行・列インデックス)を含むイベント引数</param>
/// <remarks>
/// 行ヘッダーのクリックや、バインドされているデータが Bookmark 型でない場合は処理を中断します。
/// 編集が完了(DialogResult.OK)すると、元のデータソースに値を反映し表示を更新します。
/// </remarks>
private void dataGridView1_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
// ヘッダー部分をクリックした場合は RowIndex が -1 になるため、ガード節で弾く
if (e.RowIndex < 0)
return;
// クリックされた行の行オブジェクトを取得
var row = dataGridView1.Rows[e.RowIndex];
// DataBoundItem を Bookmark 型にキャスト。型が不一致なら即 return (パターンマッチング)
if (row.DataBoundItem is not Bookmark bookmark)
return;
// 編集用フォームの生成。using 宣言によりスコープを抜ける際に自動で破棄される
using var form = new BookmarkEditForm(bookmark.Name, bookmark.Url);
// フォームをモーダルとして表示し、ユーザーが「保存」などを押したかを確認
var result = form.ShowDialog(this);
// 編集が正常に完了した場合のみ処理を継続
if (result == DialogResult.OK)
{
// フォーム側のプロパティから値を取得し、null でなければ元のオブジェクトに反映
if (form.EditedName is not null)
bookmark.Name = form.EditedName;
if (form.EditedUrl is not null)
bookmark.Url = form.EditedUrl;
// DataGridView の再描画を行い、変更を画面に反映させる
dataGridView1.Refresh();
}
// Cancel の場合は、元のデータ(bookmark)を書き換えない
}
ポイント
ガード節の活用: if (e.RowIndex < 0) return; のように、無効なデータの場合に早めに return させることで、メインの処理がインデント(深さ)に埋もれず読みやすくなります。
パターンマッチング (is not): 型の判定と変数の割り当てを一行で行う、近年の C# らしい簡潔な書き方です。
Using 宣言: using (var form = ...) と括弧を使わずに書くことで、コードのネストを浅く保てます。
using 宣言(Using Declarations)
#using
#C#8.0
更新: 2026-02-11
using var 変数名 = ...; と書くことで、そのメソッドが終了するタイミング(スコープを抜ける時)で自動的に Dispose() が呼ばれるようになります。
private void dataGridView1_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0) return;
var row = dataGridView1.Rows[e.RowIndex];
if (row.DataBoundItem is not Bookmark bookmark) return;
// AIの提案を採用したスッキリした書き方
using var form = new BookmarkEditForm(bookmark.Name, bookmark.Url);
var result = form.ShowDialog(this);
if (result == DialogResult.OK)
{
bookmark.Name = form.EditedName;
bookmark.Url = form.EditedUrl;
dataGridView1.Refresh();
}
}
ポイント
C# のパターンマッチング(is not Bookmark bookmark)を使うと、その時点で既に bookmark という変数が宣言された状態になります。そのため、その直後の var bookmark = ... という行は「同じ名前の変数をもう一度作ろうとしている」と見なされ、コンパイルエラーになります。
var row = dataGridView1.Rows[e.RowIndex];
// 1. パターンマッチングで型チェックと変数割り当てを同時に行う
if (row.DataBoundItem is not Bookmark bookmark)
return;
// この時点で bookmark は既に Bookmark 型として割り当てられているので、
// キャストを書き直す必要はありません。
using (var form = new BookmarkEditForm(bookmark.Name, bookmark.Url))
{
// フォームの表示処理など
form.ShowDialog();
}
ポイント
二重定義の解消: is not Bookmark bookmark を使った時点で、もし条件が false(つまり Bookmark 型だった場合)なら、その後のコードで bookmark 変数がそのまま使えます。
スコープの活用: is not パターンで return させる書き方は「ガード節」として非常にクリーンで、それ以降の処理でキャストなしに目的の型を扱えるのが C# の便利な仕様です。
DataGridView の行をダブルクリックして別フォームを開く(WinForms)
#新しいForm
#別のForm
#開く
#Dialog
更新: 2026-02-11
WinForms アプリケーションで、
DataGridView の データ行をダブルクリックしたときに、別の Form(編集画面など)を開く
最小構成のコードサンプルです。
この例では、
DataGridView に表示されている列は Name のみ
実データは DataBoundItem から取得
画面を開くことだけに責務を限定
しています。
using System;
using System.Windows.Forms;
namespace Bookmarks
{
public partial class BookmarkEditForm : Form
{
// 呼び出し元に返す値
public string EditedName { get; private set; }
public string EditedUrl { get; private set; }
public BookmarkEditForm(string name, string url)
{
InitializeComponent();
textBoxName.Text = name;
textBoxUrl.Text = url;
}
private void buttonUpdate_Click(object sender, EventArgs e)
{
// ★ 入力値を結果として詰める
EditedName = textBoxName.Text.Trim();
EditedUrl = textBoxUrl.Text.Trim();
DialogResult = DialogResult.OK;
Close();
}
private void buttonCancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
}
---
private void dataGridView1_CellDoubleClick(
object sender,
DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0)
return;
var row = dataGridView1.Rows[e.RowIndex];
var bookmark = row.DataBoundItem as Bookmark;
if (bookmark == null)
return;
using (var form = new BookmarkEditForm(bookmark.Name, bookmark.Url))
{
var result = form.ShowDialog(this);
if (result == DialogResult.OK)
{
// ★ 編集結果を反映する(具体処理)
bookmark.Name = form.EditedName;
bookmark.Url = form.EditedUrl;
// Name 列だけ表示している想定
dataGridView1.Refresh();
}
else
{
// Cancel 時は何もしない
}
}
}
ポイント
CellDoubleClick を使うことで
どの行がダブルクリックされたかを安全に取得できます
e.RowIndex < 0 のチェックは
ヘッダー行を誤って処理しないために必須です
DataGridView に存在しない列(例:Url)は
Cells["Url"] では参照せず、DataBoundItem を使用します
DataGridView の行をダブルクリックして別フォームを開く(WinForms)
#新しいForm
#別のForm
#開く
#Dialog
更新: 2026-02-08
WinForms アプリケーションで、
DataGridView の データ行をダブルクリックしたときに、別の Form(編集画面など)を開く
最小構成のコードサンプルです。
この例では、
DataGridView に表示されている列は Name のみ
実データは DataBoundItem から取得
画面を開くことだけに責務を限定
しています。
using System;
using System.Windows.Forms;
namespace Bookmarks
{
public partial class BookmarkEditForm : Form
{
// 呼び出し元に返す値
public string EditedName { get; private set; }
public string EditedUrl { get; private set; }
public BookmarkEditForm(string name, string url)
{
InitializeComponent();
textBoxName.Text = name;
textBoxUrl.Text = url;
}
private void buttonUpdate_Click(object sender, EventArgs e)
{
// ★ 入力値を結果として詰める
EditedName = textBoxName.Text.Trim();
EditedUrl = textBoxUrl.Text.Trim();
DialogResult = DialogResult.OK;
Close();
}
private void buttonCancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
}
---
private void dataGridView1_CellDoubleClick(
object sender,
DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0)
return;
var row = dataGridView1.Rows[e.RowIndex];
var bookmark = row.DataBoundItem as Bookmark;
if (bookmark == null)
return;
using (var form = new BookmarkEditForm(bookmark.Name, bookmark.Url))
{
var result = form.ShowDialog(this);
if (result == DialogResult.OK)
{
// ★ 編集結果を反映する(具体処理)
bookmark.Name = form.EditedName;
bookmark.Url = form.EditedUrl;
// Name 列だけ表示している想定
dataGridView1.Refresh();
}
else
{
// Cancel 時は何もしない
}
}
}
ポイント
CellDoubleClick を使うことで
どの行がダブルクリックされたかを安全に取得できます
e.RowIndex < 0 のチェックは
ヘッダー行を誤って処理しないために必須です
DataGridView に存在しない列(例:Url)は
Cells["Url"] では参照せず、DataBoundItem を使用します
Visual Studio で Windows Forms の「Form」を追加する手順を、いちばんよく使うやり方から整理します。
#WinForm
#Form
#新しいForm
#新しいフォーム
更新: 2026-02-08
ソリューションエクスプローラーを開く
プロジェクト名を右クリック
[追加] → [Windows フォーム]
名前を入力(例:Form2.cs)
**[追加]**をクリック
👉 これで
Form2.cs
Form2.Designer.cs
Form2.resx
が自動でセット作成されます。
方法②:[追加] → [新しい項目] から選ぶ(種類を細かく指定したい時)
プロジェクトを右クリック
[追加] → [新しい項目]
左で [Windows フォーム] を選択
[Windows フォーム] をクリック
名前を入力して [追加]
👉
UserControl
ダイアログ
クラス
などを同時に選びたいときに便利。
追加した Form を表示するには?
例:Form1 から Form2 を開く
private void button1_Click(object sender, EventArgs e)
{
var form2 = new Form2();
form2.Show(); // モードレス
// form2.ShowDialog(); // モーダル
}
よくある勘違いポイント ⚠️
Form を追加しただけでは表示されない
→ Show() / ShowDialog() が必要
AutoGenerateColumns や DataGridView とは無関係
→ ただの画面(ウィンドウ)追加
デザイナーが開かない場合
→ Form2.cs を右クリック → [デザイナーの表示]
業務系っぽい命名ルール(おすすめ)
用途 名前例
一覧画面 BookListForm
編集画面 BookEditForm
設定画面 SettingsForm
ダイアログ ConfirmDialog
ツールボックス(コントロール一覧)が非表示になっているだけです。WinFormsあるあるです。
方法①:メニューからツールボックスを表示(最優先)
上部メニューの
[表示] → [ツールボックス]
もしくは ショートカットキー
Ctrl + Alt + X
👉 画面の 左側 に
「Button / Label / TextBox / DataGridView …」
が出てくるはずです。
方法②:フォームをアクティブにする(重要)
ツールボックスは Form デザイナーがアクティブな時しか中身が出ません。
✅ 確認ポイント
BookmarkEditForm.cs [デザイン] が開いている
コード画面(.cs)ではない
手順
BookmarkEditForm.cs を右クリック
[デザイナーの表示]
👉 これをやらないと
ツールボックスが空
そもそも表示されない
ことがあります。
方法③:ツールボックスが「自動的に隠れている」だけの場合
Visual Studio はたまに勝手に折りたたみます 😇
画面左端に 細いタブ(📌ツールボックス) がないか確認
あったらクリック
📌(ピン)を押して固定
方法④:レイアウトが壊れている場合(最終手段)
どうしても出ない場合👇
[ウィンドウ] → [ウィンドウ レイアウトのリセット]
Visual Studio を再起動
※ 作業中のレイアウトは初期化されます
それでも出ない場合(レアケース)
フォームが WPF ではないか
クラスファイルを Form として作ってしまった
public class BookmarkEditForm // ← Form 継承してない
正しくは:
public partial class BookmarkEditForm : Form
あなたのスクショを見る限り
👉 System.Windows.Forms.Form を継承しているので問題なしです。
var form = new BookmarkEditForm();
form.Show(); // モードレス
// form.ShowDialog(); // モーダル
ポイント
ポイント説明:新しい Form を追加する際の注意点(WinForms)
① 「Windows フォーム」として追加する
新しい画面は、必ず Windows フォームとして追加する。
❌ クラスファイルを追加してはいけない
⭕ [追加] → [Windows フォーム]
これにより、以下が自動生成される。
FormName.cs
FormName.Designer.cs
FormName.resx
② Form 名は役割が分かる名前にする
Form 名は「画面の用途」が分かる名前にする。
例:
BookmarkListForm(一覧)
BookmarkEditForm(編集)
BookmarkAddForm(新規)
SettingsForm(設定)
※ Form2 のまま使わない
③ Form は必ず Form を継承していることを確認
作成後、クラス宣言を確認する。
public partial class BookmarkEditForm : Form
これが崩れていると、
デザイナーが開かない
ツールボックスが使えない
④ デザイナー画面で UI を作成する
UI(ボタン・テキストボックス等)は
コードではなくデザイナーで配置する。
FormName.cs を右クリック
[デザイナーの表示]
業務アプリでは
「配置はデザイナー/処理はコード」
と分けるのが基本。
⑤ Form を作っただけでは表示されない
Form は作成しただけでは画面に出ない。
表示するにはコードが必要。
var form = new BookmarkEditForm();
form.Show(); // モードレス
// form.ShowDialog(); // モーダル
⑥ Form は「1画面=1責務」を守る
1つの Form に役割を詰め込みすぎない。
一覧表示 → 一覧用 Form
編集 → 編集用 Form
確認 → ダイアログ Form
👉 業務系では保守性が大きく変わる。
⑦ データ処理は Form に書かない
Form は 画面とイベント処理だけにする。
DB処理:Repository
ロジック:Service / Manager
Form:表示・入力・ボタン処理のみ
👉 今回の BookmarkRepository 構成は正解。
まとめ(要点)
Form は Windows フォームとして追加
名前は役割ベースで付ける
Form 継承を確認
UI はデザイナーで作る
表示は Show() / ShowDialog()
1 Form = 1 画面
WinForms + DataGridView:表示列を固定して必要な項目だけを一覧表示する(DI + SQLite)
更新: 2026-02-08
説明文(サンプルの狙い)
このサンプルは、WinForms の DataGridView に対して 「表示したい列だけ」をデザイナーで固定し、データ側は全項目を持ったまま一覧表示する方法を示します。
業務アプリでは、DBには id や url、作成日・更新日などを保持しつつ、一覧画面では name のみ表示することがよくあります。本サンプルではその定番パターンを、DataPropertyNameによる列バインドで実現します。
コード(表示まで)
1) Model(表示データの型)
namespace Bookmarks
{
internal class Bookmark
{
public long Id { get; set; } // DB上は保持(画面には出さない)
public string Name { get; set; } = ""; // 画面に出す
public string Url { get; set; } = ""; // DB上は保持(画面には出さない)
}
}
---
2) Repository(一覧取得だけ)
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Bookmarks
{
internal interface IBookmarkRepository
{
Task<List<Bookmark>> GetAllAsync();
}
internal class SqliteBookmarkRepository : IBookmarkRepository
{
private const string DbFileName = "bookmarks.db";
private static string ConnectionString
{
get
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string dbPath = Path.Combine(baseDir, DbFileName);
return $"Data Source={dbPath}";
}
}
public async Task<List<Bookmark>> GetAllAsync()
{
var list = new List<Bookmark>();
await using var conn = new SqliteConnection(ConnectionString);
await conn.OpenAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT id, name, url FROM bookmarks ORDER BY id DESC;";
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync())
{
list.Add(new Bookmark
{
Id = r.GetInt64(0),
Name = r.GetString(1),
Url = r.GetString(2)
});
}
return list;
}
}
}
---
3) Form(DataGridViewに表示)
※「Loadイベントを確実に呼ぶ」ために、コンストラクタで this.Load += ... を付けています(デザイナーで紐づいていれば不要)
namespace Bookmarks
{
public partial class Form1 : Form
{
private readonly IBookmarkRepository _repo;
public Form1(IBookmarkRepository repo)
{
InitializeComponent();
_repo = repo;
this.Load += Form1_Load; // デザイナーで紐づけ済みなら省略OK
}
private async void Form1_Load(object? sender, EventArgs e)
{
// 重要:デザイナーで列を作っているので、自動生成はOFFにする
dataGridView1.AutoGenerateColumns = false;
// DataSourceには「全部のプロパティ」を持つListを渡してOK
// 表示されるのは、列のDataPropertyNameで紐づけたものだけ
dataGridView1.DataSource = await _repo.GetAllAsync();
}
}
}
---
4) Program.cs(DIでFormを作る)
using Microsoft.Extensions.DependencyInjection;
namespace Bookmarks
{
internal static class Program
{
[STAThread]
static async Task Main()
{
ApplicationConfiguration.Initialize();
var services = new ServiceCollection();
// Repository登録(SQLite実装)
services.AddSingleton<IBookmarkRepository, SqliteBookmarkRepository>();
// Form登録
services.AddTransient<Form1>();
using var sp = services.BuildServiceProvider();
Application.Run(sp.GetRequiredService<Form1>());
}
}
}
ポイント
デザイナーでのやり方(列を1本だけ作る)
フォームに DataGridView を配置(名前は dataGridView1 のままでOK)
DataGridView をクリックして、右下のプロパティ欄の
「列の編集…」(または Columns (Collection))を開く
「追加」 で列を1本追加
ColumnType:DataGridViewTextBoxColumn
追加した列を選択して右側のプロパティを設定
(Name):colName(任意)
HeaderText:名前
DataPropertyName:Name ← ここが重要(Bookmarkクラスのプロパティ名)
AutoSizeMode:Fill(任意:横いっぱいにする)
余計な列があるなら削除(「名前」列だけ残す)
ポイント:列を“手で作る”ので、URLやIDは画面に出ません(業務の「非表示項目」パターン)。
補足(ハマりどころ・業務っぽい使い方)
**DataPropertyName は「列名」ではなく「オブジェクトのプロパティ名」**です
例:Bookmark.Name に合わせて DataPropertyName = "Name" にする
DataGridView.AutoGenerateColumns = false は コード側で必ず明示しておくと安全
(デザイナーで列を作ったのに、勝手に全列が増える事故を防げます)
URLを表示しなくてもロジックでは使えます
行選択時に DataBoundItem から取り出せばOK(右クリック編集やダブルクリックで開く等)
var b = (Bookmark)dataGridView1.CurrentRow.DataBoundItem;
// b.Url を使える(画面には出てない)
WinForms + SQLite + DI による デスクトップアプリ初期化サンプル(.dbファイル作成)
#winforms
#sqlite
#DI
#dependency-injection
#database-initialization
#db初期化
#dbファイル作成
更新: 2026-02-07
このサンプルは、C# WinForms アプリケーションにおける SQLite データベース初期化の基本構成を示したものです。
データベースファイルを exe と同じフォルダに自動生成し、テーブル作成などの初期化処理を UI(Form)から分離して実装しています。
初期化処理は IDatabaseInitializer インターフェースとして抽象化し、
Program.cs から DI(Dependency Injection)を使って起動前に実行する構成としています。
これにより、Form 側はデータベースの存在を意識せず、表示とユーザー操作に専念できます。
SQLite には Microsoft.Data.Sqlite を使用し、
CREATE TABLE IF NOT EXISTS による安全な初期化、
async / await による非同期処理を採用しています。
Bookmarks/
├ Program.cs
├ IDatabaseInitializer.cs
├ SqliteDatabaseInitializer.cs
└ Form1.cs (今は空でもOK)
---- IDatabaseInitializer.cs ----
// Task / async を使うための名前空間
using System.Threading.Tasks;
namespace Bookmarks
{
/// <summary>
/// データベース初期化を行うためのインターフェース。
///
/// ・DBファイルの作成
/// ・テーブル(スキーマ)の作成
/// ・マイグレーション処理
///
/// など「アプリ起動前に1回だけ行う処理」を抽象化するために定義する。
/// </summary>
public interface IDatabaseInitializer
{
/// <summary>
/// データベースの初期化処理を非同期で実行する。
///
/// 実装クラスでは以下のような処理を行うことを想定している。
/// ・SQLite ファイルが存在しなければ作成
/// ・必要なテーブルが存在しなければ CREATE TABLE
/// ・将来的にはスキーマバージョン管理(マイグレーション)もここに含められる
///
/// Program.cs からアプリ起動前に呼び出される。
/// </summary>
Task InitializeAsync();
}
}
---- SqliteDatabaseInitializer.cs ----
// Microsoft.Data.Sqlite 名前空間
// SQLite を ADO.NET で操作するためのクラス(SqliteConnection 等)を提供
// NuGet パッケージ Microsoft.Data.Sqlite が必要
using Microsoft.Data.Sqlite;
// AppDomain や Path を使うため
using System;
// ファイルパス操作用(Path.Combine など)
using System.IO;
// async / Task を使うため
using System.Threading.Tasks;
namespace Bookmarks
{
/// <summary>
/// SQLite を使用したデータベース初期化クラス。
///
/// IDatabaseInitializer を実装し、
/// ・DBファイルの作成
/// ・テーブル作成(DDL)
/// ・SQLite の初期設定
/// を担当する。
///
/// Program.cs からアプリ起動前に1回だけ呼び出される想定。
/// </summary>
internal class SqliteDatabaseInitializer : IDatabaseInitializer
{
// 作成する SQLite データベースファイル名
// exe と同じフォルダに配置する
private const string DbFileName = "bookmarks.db";
/// <summary>
/// SQLite データベースの初期化処理を非同期で実行する。
///
/// ・DBファイルが存在しなければ自動作成
/// ・必要なテーブルを CREATE TABLE IF NOT EXISTS で作成
/// ・SQLite の動作設定(PRAGMA)を適用
/// </summary>
public async Task InitializeAsync()
{
// 実行ファイル(exe)が置かれているフォルダのパスを取得
// アプリをフォルダごとコピーするだけで移行できる構成にするため
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
// DBファイルのフルパスを生成
string dbPath = Path.Combine(baseDir, DbFileName);
// SQLite の接続文字列
// ファイルが存在しなければ自動的に作成される
string cs = $"Data Source={dbPath}";
// SQLite に接続(await using により非同期 Dispose)
await using var conn = new SqliteConnection(cs);
// 実際に DB ファイルを開く(ここでファイルが作成される)
await conn.OpenAsync();
// ---- SQLite の動作設定(任意) ----
// WAL モード:
// ・同時アクセスに強い
// ・突然終了時の耐性が向上
await ExecuteNonQueryAsync(conn, "PRAGMA journal_mode = WAL;");
// synchronous = NORMAL:
// ・安全性と速度のバランスを取る設定
await ExecuteNonQueryAsync(conn, "PRAGMA synchronous = NORMAL;");
// ---- テーブル作成 ----
// bookmarks テーブルを作成
// IF NOT EXISTS により、既に存在する場合は何もしない
string ddl = @"
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL
);";
// DDL を実行
await ExecuteNonQueryAsync(conn, ddl);
}
/// <summary>
/// SQL を実行するための共通ヘルパーメソッド。
///
/// INSERT / UPDATE / DELETE / CREATE TABLE など、
/// 結果セットを返さない SQL を実行する用途で使用する。
/// </summary>
private static async Task ExecuteNonQueryAsync(
SqliteConnection conn,
string sql)
{
// コマンドを生成
await using var cmd = conn.CreateCommand();
// 実行する SQL を設定
cmd.CommandText = sql;
// SQL を実行
await cmd.ExecuteNonQueryAsync();
}
}
}
---- Form1.cs ----
namespace Bookmarks
{
/// <summary>
/// ブックマーク管理アプリのメイン画面。
///
/// 現時点では UI のみを担当し、
/// ・データベース初期化
/// ・SQLite への直接アクセス
/// は一切行わない。
///
/// データ操作は Repository(例:IBookmarkRepository)経由で行い、
/// この Form は「表示とユーザー操作」だけを担当する想定。
/// </summary>
public partial class Form1 : Form
{
/// <summary>
/// Form1 のコンストラクタ。
///
/// InitializeComponent() は
/// ・デザイナで配置したコントロールの生成
/// ・イベントハンドラの関連付け
/// などを自動生成コードで行う。
///
/// 現時点では依存関係の注入や
/// 初期データの読み込みは行っていない。
/// </summary>
public Form1()
{
// デザイナで作成した UI を初期化
InitializeComponent();
}
}
}
---- Program.cs ----
// Microsoft.Extensions.DependencyInjection 名前空間を使用する
// ServiceCollection / GetRequiredService など DI 関連の型を使うため
// NuGet パッケージ Microsoft.Extensions.DependencyInjection が必要
using Microsoft.Extensions.DependencyInjection;
// async / Task を使うための名前空間
using System.Threading.Tasks;
namespace Bookmarks
{
// アプリケーション全体のエントリーポイントを持つクラス
// static クラスにするのが WinForms の定石
internal static class Program
{
// STAThread:
// WinForms / WPF では必須(COM・UIスレッド関連)
[STAThread]
// C# のエントリーポイント
// async Task Main にすることで await が使える
static async Task Main()
{
// .NET 6+ の WinForms 初期化処理
// 高DPI、既定フォントなどの設定を行う
ApplicationConfiguration.Initialize();
// --- DI コンテナの構築開始 ---
// サービス(依存関係)を登録するためのコレクションを作成
var services = new ServiceCollection();
// IDatabaseInitializer を要求されたら
// SqliteDatabaseInitializer を 1 インスタンスだけ生成して使い回す
// → DB 初期化はアプリ全体で1回で十分なので Singleton
services.AddSingleton<IDatabaseInitializer, SqliteDatabaseInitializer>();
// Form1 は必要になるたびに新しく生成
// → 通常の WinForms の使い方なので Transient
services.AddTransient<Form1>();
// 登録したサービス情報を元に
// 実際にオブジェクトを生成できる ServiceProvider を作成
using var sp = services.BuildServiceProvider();
// --- アプリ起動前の初期化処理 ---
// IDatabaseInitializer を取得し
// SQLite の DB ファイル作成・テーブル作成を実行
// await しているので、完了するまで次へ進まない
await sp.GetRequiredService<IDatabaseInitializer>()
.InitializeAsync();
// --- WinForms アプリ起動 ---
// DI コンテナから Form1 を取得して起動
// Form1 のコンストラクタ引数も自動で解決される
Application.Run(sp.GetRequiredService<Form1>());
}
}
}
ポイント
exe と同じフォルダに SQLite DB を自動生成し、
初期化処理を Program.cs から分離する設計例
本サンプルでは、データベース初期化処理を Program.cs から実行しています。
これは、Form の生成前に DB の存在とスキーマを保証し、
UI 側が初期化失敗を意識せずに動作できるようにするためです。
SQLite のデータベースファイルは 実行ファイル(exe)と同じフォルダに作成されます。
これにより、アプリケーションフォルダをそのままコピーするだけで
環境移行が可能な構成としています。
初期化処理は IDatabaseInitializer インターフェースとして定義し、
実装クラス(SqliteDatabaseInitializer)に依存しない形で呼び出しています。
将来的に SQLite 以外の DB(例:MySQL)へ切り替える場合も、
実装クラスを差し替えることで対応可能です。
本サンプルでは Entity Framework などの ORM は使用せず、
Microsoft.Data.Sqlite を用いた ADO.NET 直接操作を採用しています。
これにより、SQL の挙動やトランザクションの流れを理解しやすくしています。
初期化処理は async / await を用いて非同期で実行していますが、
アプリ起動前に完了することを保証するため、
Program.cs 側では初期化完了を待ってから Form を起動しています。
本サンプルは 小規模・個人向けデスクトップアプリを想定しています。
大規模アプリやチーム開発では、
マイグレーション管理やエラーハンドリングの設計を
別途検討する必要があります。
WinForms DataGridView:一覧表示向けの基本UI設定(フォント/ヘッダー色/閲覧専用/行ヘッダー非表示)
#WinForms
#DataGridView
#UI
#一覧画面
#ReadOnly
#見た目調整
#ヘッダー色
#フォント
#選択色
#テンプレート
更新: 2026-02-06
WinForms の DataGridView を「一覧画面として見やすく・誤操作しにくく」するための基本テンプレートです。
具体的には、以下をまとめて設定します。
フォントサイズを大きくして視認性を確保(セル+ヘッダー)
行とヘッダーの高さを調整し、文字が潰れないようにする
**閲覧専用(編集不可)**にして誤操作を防止
末尾の「新規行(*)」や左の行ヘッダーなど、不要なUIを非表示
ヘッダーの色や選択色を整えて、“それっぽい”見た目にする
(特に EnableHeadersVisualStyles=false が重要)
public Form1()
{
InitializeComponent(); // フォーム上のコントロール(DataGridViewなど)を生成・配置する
this.Text = "ブックマーク一覧"; // タイトルバーの文字(日本語OK)
// 画面外に出てしまう事故対策:起動位置と状態を安定させる
this.StartPosition = FormStartPosition.CenterScreen; // 画面中央に表示
this.WindowState = FormWindowState.Normal; // 常に通常表示で開始
this.ShowInTaskbar = true; // タスクバーに表示
// 確認用(常に最前面):邪魔になるので通常は使わない
// this.TopMost = true;
// 画面の視認性向上:フォントを大きくする
var f = new Font("Yu Gothic UI", 20f); // DataGridView用フォント(必要に応じてサイズ調整)
// --- DataGridView:見た目(フォント・高さ) ---
dataGridView1.DefaultCellStyle.Font = f; // セル(本文)のフォント
dataGridView1.ColumnHeadersDefaultCellStyle.Font = f; // 列ヘッダーのフォント
dataGridView1.RowTemplate.Height = 44; // 行の高さ(フォントに合わせて増やす)
dataGridView1.ColumnHeadersHeight = 44; // ヘッダー行の高さ
// 動作確認用:データが無いとフォント変更が分かりにくいので1行追加(確認後は消してOK)
dataGridView1.Rows.Add("サンプル");
// --- DataGridView:操作(編集禁止・選択モード) ---
dataGridView1.ReadOnly = true; // セル編集禁止(閲覧専用)
dataGridView1.AllowUserToAddRows = false; // 末尾の「新規行(*)」を消す
dataGridView1.AllowUserToDeleteRows = false; // 行削除禁止
dataGridView1.EditMode = DataGridViewEditMode.EditProgrammatically; // ユーザー操作で編集開始しない
dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect; // 行単位で選択
dataGridView1.MultiSelect = false; // 複数選択を禁止(扱いやすい)
dataGridView1.RowHeadersVisible = false; // 左端の行ヘッダー(無意味な列)を非表示
// --- DataGridView:色(ヘッダーと選択色) ---
// 重要:EnableHeadersVisualStyles が true のままだと、ヘッダー色指定がOSテーマに負ける
dataGridView1.EnableHeadersVisualStyles = false; // ヘッダーの描画をOSテーマから切り離す(必須)
// ヘッダーの配色:落ち着いたグレー + 黒文字
dataGridView1.ColumnHeadersDefaultCellStyle.BackColor = Color.LightGray; // ヘッダー背景
dataGridView1.ColumnHeadersDefaultCellStyle.ForeColor = Color.Black; // ヘッダー文字色
dataGridView1.ColumnHeadersDefaultCellStyle.SelectionBackColor = Color.LightGray; // ヘッダー選択時背景(変えない)
dataGridView1.ColumnHeadersDefaultCellStyle.SelectionForeColor = Color.Black; // ヘッダー選択時文字色(変えない)
// 本文(行)の配色:通常は白、選択は薄い青(強すぎない)
dataGridView1.DefaultCellStyle.BackColor = Color.White; // 通常背景
dataGridView1.DefaultCellStyle.ForeColor = Color.Black; // 通常文字色
dataGridView1.DefaultCellStyle.SelectionBackColor = Color.LightSteelBlue; // 選択背景
dataGridView1.DefaultCellStyle.SelectionForeColor = Color.Black; // 選択文字色
}
ポイント
EnableHeadersVisualStyles = false はヘッダー色変更の必須条件です。
true のままだと OS テーマに負けて色が変わらないことがあります。
Rows.Add("サンプル") は 確認用です。実データを表示するようになったら削除してください。
RowTemplate.Height は 既に表示済みの行には効かないケースがあります。
データバインド後に行高さを揃えたい場合は、別途 AutoSizeRowsMode や Rows[i].Height を使います。
20pt はかなり大きめです。一般的な一覧だと 12〜16ptが多いです(環境に合わせて調整)。
列幅を「1列で横いっぱい」にしたい場合は、列側で AutoSizeMode = Fill を設定します(別サンプル推奨)。
本サンプルは DataGridView を「一覧表示専用UI」として使うことを前提にしています。
編集・追加・削除を行う用途には向いていません。
EnableHeadersVisualStyles = false を設定しない場合、
ヘッダー色の指定は Windows テーマに上書きされます。
ヘッダー配色を制御したい場合は必須です。
Rows.Add("サンプル") は 動作確認用のダミーデータです。
実際の運用では、データバインド(DataSource)に置き換えてください。
RowTemplate.Height は、
既に生成された行には反映されない場合があります。
データバインド後に高さを揃える場合は、
AutoSizeRowsMode や個別に Rows[i].Height を設定してください。
フォントサイズ 20pt は視認性重視の設定です。
実アプリでは 12〜16pt 程度が一般的です。
列を「1列で横幅いっぱい」にしたい場合は、
列側で AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill を指定してください。
本コードは WinForms (.NET / .NET Framework) 共通で動作します。
switch文のwhen
#switch
#when
#case
更新: 2026-02-06
意味と仕組み
1. キーワードは when: C# 7.0 以降で導入されたパターンマッチングの一環として、case ラベルの後ろに when を記述することで、値が一致し、かつ特定の条件を満たす場合のみ 処理を実行させることができます。
2. 実行順序: 通常の switch 文では値の一致のみを見ますが、when を使うと「型や値のチェック」→「when の条件評価」の順で判定されます。条件が false の場合は、その case は無視され、次の case の判定に移ります。
3. 変数の利用: 上記の例の case int i when ... のように、マッチした値を一時変数(ここでは i)に格納して、その変数を when 句の中で使うことができます。これにより、「数字である」かつ「偶数である」といった複合的な条件分岐をすっきりと記述できます。
Android開発などのJava経験者にとっては、if-else で記述していた複雑な条件分岐を switch 文だけで表現できるようになる便利な機能です。
var n = 15;
switch (n)
{
// n が 0 より大きく、かつ 2 で割り切れる場合
case int i when i > 0 && i % 2 == 0:
Console.WriteLine("正の偶数です");
break;
// n が 0 より大きく、かつ 2 で割り切れない(奇数)場合
case int i when i > 0 && i % 2 != 0:
Console.WriteLine("正の奇数です");
break;
// n が 0 の場合
case 0:
Console.WriteLine("ゼロです");
break;
// それ以外(負の数など)
default:
Console.WriteLine("負の数です");
break;
}
ポイント
配列 + LINQ(Where / Select / FirstOrDefault)
#LINQ
#配列
#Where
#Select
#FirstOrDefault
更新: 2026-02-06
配列は IEnumerable<T> を実装しているため、List<T> と同様に LINQ を使えます。
(注意:フィールドに var は使えないので、プロパティや明示型にします。)
using System;
using System.Linq; // LINQ を使うために必要
public class Sample {
// var name = ""; // エラー: var はフィールドには使えません
public string Name { get; set; } = ""; // string 型に変更
}
class Program {
static void Main() {
// 1. 配列の準備
Sample[] hairetsu = new Sample[3];
for (int i = 0; i < hairetsu.Length; i++) {
hairetsu[i] = new Sample { Name = "User_" + i };
}
// ------------------------------
// LINQ の使用例
// ------------------------------
// 例1: フィルタリング (Where)
// Name に "50" を含む要素だけを抽出する
var filteredItems = hairetsu.Where(item => item.Name.Contains("50"));
Console.WriteLine("--- フィルタリング結果 ---");
foreach (var item in filteredItems) {
Console.WriteLine(item.Name);
}
// 例2: データの変換/射影 (Select)
// Sample オブジェクトの配列から、Name(文字列)だけの配列を作る
string[] onlyNames = hairetsu
.Select(item => item.Name)
.ToArray(); // 結果を配列として確定させる
// 例3: 条件を満たす最初の要素を取得 (FirstOrDefault)
// Name が "User_99" の要素を探す
Sample? targetItem = hairetsu.FirstOrDefault(s => s.Name == "User_99");
if (targetItem != null) {
Console.WriteLine($"\n見つかったデータ: {targetItem.Name}");
}
}
}
ポイント
1) using System.Linq; が必要
2) Where/Select は遅延実行なので、foreach や ToArray() で確定する
3) 配列に戻すなら ToArray()