アプリの画面にスプレッドシートを置くと、必ず同じ問題が来ます。ユーザーが小計の数式を消す。単価の列に「12,000円」と全角で打ち込む。去年この列が空欄のまま出荷された理由を、もう誰も覚えていない。Excel ファイルを配っていたころの問題が、そのままアプリの中へ引っ越してくるだけです。
ReoGrid V5(2026年8月19日リリース)では、この 3 つに対して Excel と同じ 3 つの道具が揃いました。データの入力規則・セルのメモ・シート保護です。どれも単体で使えますが、面白いのは 3 つの役割がきれいに分かれていることです。組み合わせて初めて「ユーザーが壊せない入力シート」になります。
この記事では、C# で見積テンプレートを 1 枚組み立てながら 3 つの使い分けを見ていきます。実装はすべてコア側にあるので、コードは WinForms / WPF / Avalonia で同一、UI を持たないヘッドレスでもそのまま動きます。
まず全体像 — 3 つの役割分担
| 道具 | 何を担当するか | いつ効くか |
|---|---|---|
| シート保護 | 触らせる場所と触らせない場所を分ける | 編集が始まる前(ユーザー操作の関門) |
| データの入力規則 | 入ってくる値が妥当かを見る | 値がセルに確定する瞬間 |
| セルのメモ | 「なぜそうなのか」を人に伝える | 常時(ホバー、または固定表示) |
順番も決まっています。保護で範囲を絞り → 規則で値を絞り → メモで理由を伝える。逆から作ると、たいてい「なぜか編集できないセル」だらけのシートが出来上がります。
1. シート保護 — 「触っていい場所」を決める
保護は Excel と同じ 2 層モデルです。ここを取り違える人がとても多いので、先に書いておきます。
セルは既定でロックされています。 そしてそのロックは、シートを保護してから初めて効きます。
つまり入力フォームを作る手順は「入力させたいセルをロックする」ではなく、逆です。
using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.Protection;
// 1. 入力させたい範囲のロックを外す(それ以外は既定でロック済み)
sheet.SetRangeLock(RangePosition.Parse("A2:C100"), LockState.Unlocked);
// 2. そのうえでシートを保護する。ここで初めてロックが効く
sheet.Protect();
ロック指定は範囲スコープの側テーブルに入ります。列を丸ごと開けてもエントリは 1 件で、セルには何も書き込まれません。104 万行の列を開けてもコストは一定です。判定も矩形の差し引きで行うので、IsRangeEditable がセルを 1 つずつ調べに行くことはありません。
保護中でも許可する操作
ws.Protection は Excel の「シートの保護」ダイアログのチェック欄にそのまま対応します。
sheet.Protection.AllowSort = true; // 並べ替えは許す
sheet.Protection.AllowAutoFilter = true; // フィルタ操作も許す
sheet.Protection.AllowFormatCells = false; // 書式は触らせない(既定)
sheet.Protect();
ここに 1 つだけ落とし穴があります。 AllowSelectLockedCells と AllowSelectUnlockedCells の 2 つだけ、既定が true(=選択できる)です。他の AllowXxx は全部 false から始まります。
理由は OOXML 側の持ち方です。Excel はこれらのフラグを「禁止」として保存していて(insertRows="1" は「挿入を禁じる」の意味)、V5 は「許可」として持ちます。選択系だけは元の既定が「禁じない」なので、反転すると true になる、というわけです。ここを取り違えると、誰も 1 セルもクリックできないシートが黙って出来上がります。
拒否をユーザーに説明する
ロックされたセルを触られたときは EditRefused が上がります。
sheet.EditRefused += (_, e) =>
{
// e.Reason は Cells / Axis / Structure の 3 種類
statusBar.Text = e.Message; // Excel と同じ文面が入っている
e.Handled = true; // コントロール既定のメッセージボックスを抑止
};
Handled を立てれば、モーダルの代わりにステータスバーやトーストで知らせられます。ただし編集の可否そのものは変えられません — ここから交渉はできない、という設計です。
保護が効かないところ
保護が止めるのはユーザー操作だけです。 モデル API は開いたままなので、ファイルローダーやホストアプリのコードは保護中のシートにも書けます。Excel の UserInterfaceOnly と同じ線引きで、これは仕様どおりの挙動です。集計結果を毎晩バッチで書き戻すような処理は、保護を解除せずにそのまま書けます。
もう 1 つ正直に書いておきます。Protect("password") のパスワードは、Excel の 16 ビットの検証値です。暗号ではありません(衝突が容易に作れる代物で、Excel 自身もそう扱っています)。V5 でも Unprotect を守る以上のことはしません。中身を見せたくない用途には使わないでください。 保持しているのは、Excel で作ったブックが作られたとおりに往復するためです。
2. データの入力規則 — 値の門番を立てる
保護で範囲を絞ったら、次はその範囲に何が入ってよいかです。規則は範囲に対して設定し、値が確定する瞬間だけ評価されます。描画のたびに走ることはないので、規則を張ったシートのスクロールが重くなることはありません。

リスト — 選択肢から選ばせる
using unvell.ReoGrid.Core.Validation;
// 選択肢を直接指定する
sheet.AddValidation(RangePosition.Parse("C2:C100"), new ListValidationRule
{
Options = ["東京", "大阪", "名古屋"],
});
// マスタシートを参照する(他シート・名前付き範囲も可)
sheet.AddValidation(RangePosition.Parse("A2:A100"), new ListValidationRule
{
Source = "=マスタ!$A$1:$A$200",
});
Source には同一シートのアドレス($E$1:$E$9)、名前付き範囲(Colors)、他シート参照、Excel が保存するリテラル形式("東京,大阪,名古屋")のいずれも書けます。参照先の空セルは候補から外れるので、マスタは多めの範囲を指定しておいて構いません。
商品マスタを別シートに持ってリスト規則から参照する形にしておくと、マスタを更新するだけで全行の候補が変わります。C# 側にハードコードした string[] は、いずれ必ずマスタとずれます。
数値・日付・時刻・文字数 — 範囲で縛る
// 数量は 1〜9999 の整数だけ
sheet.AddValidation(RangePosition.Parse("C2:C100"), new ComparisonValidationRule
{
Kind = ComparisonKind.Whole,
Operator = ValidationOperator.Between,
Value1 = ValidationValue.Num(1),
Value2 = ValidationValue.Num(9999),
});
// 納品日は今日以降(境界に数式を使える)
sheet.AddValidation(RangePosition.Parse("E2:E100"), new ComparisonValidationRule
{
Kind = ComparisonKind.Date,
Operator = ValidationOperator.GreaterThanOrEqual,
Value1 = ValidationValue.Str("=TODAY()"),
});
Kind は Whole(整数)・Decimal・Date・Time・TextLength の 5 種類、Operator は Excel と同じ 8 種類です。境界には数値のほか = で始まる数式を渡せます。数式はセルを参照できるので、「開始日より後の日付だけ」のような、シートの他の値に依存する規則も書けます。
独自数式 — 重複を弾く
// 数式は範囲の左上セルを基準に書く。相対参照は行・列のオフセット分ずれる
sheet.AddValidation(RangePosition.Parse("A2:A100"), new CustomValidationRule
{
Formula = "=COUNTIF($A$2:$A$100, A2)=1", // 同じ商品を 2 回入れさせない
});
相対参照のずれ方は条件付き書式の数式ルールと同じで、$ を付けた参照は固定されます。シートの数式エンジンで評価されるため、クロスシート参照も名前付き範囲も使えます。「発注済みの型番と重複していないか」のようなチェックも、C# を書かずにシート側で完結します。
案内と警告レベル — 弾く前に伝える
規則は「弾く」だけの道具ではありません。入力する前に何を求めているかを伝えるほうが、たいてい効きます。
sheet.AddValidation(RangePosition.Parse("B2:B100"), new ComparisonValidationRule
{
Kind = ComparisonKind.Decimal,
Operator = ValidationOperator.GreaterThanOrEqual,
Value1 = ValidationValue.Num(0),
// 選択しただけで出る案内
ShowInputMessage = true,
InputTitle = "単価",
InputMessage = "税抜の金額を半角数字で入力してください。",
// 弾いたときの警告
AlertStyle = ValidationAlertStyle.Warning,
ErrorTitle = "マイナス値",
ErrorMessage = "マイナスの単価が入力されています。値引きは「値引」行で入力してください。",
IgnoreBlank = true,
});
AlertStyle は 3 段階です。
| 値 | 動作 |
|---|---|
Stop(既定) | 入力を拒否する。エディタは開いたままなので直せる |
Warning | 「このまま入力しますか?」と尋ね、答えに従う |
Information | 知らせるだけで、入力は通る |
現場の運用では、運用ルール違反は Warning、データ破壊は Stop と分けるとうまくいきます。全部 Stop にすると、例外的な取引を入れられなくなったユーザーが結局 Excel に逃げます。
なお AnyValidationRule(何でも可)と ShowInputMessage を組み合わせると、弾かずに案内だけ出すセルが作れます。備考欄の書き方ガイドなどに便利です。
いつ評価されるか — ここが一番の落とし穴
入力規則が走るのは、セル編集の確定と数式バーからの確定の 2 つだけです。逆に言えば、次のものは検査されません。
- 数式の入力(
=で始まる入力)— 確定時点では結果が定まらないため。Excel も同じ - 貼り付け・オートフィル
- API からの
SetValue/SetText
つまり入力規則は「人が打った値に対する門番」であって、データ整合性の保証ではありません。Excel とまったく同じ割り切りです。ここを誤解したまま「規則を張ったから DB に入る値は安全」と考えると、貼り付け一発で崩れます。
貼り付け後にまとめて検算したい場合や、CSV 取り込みの前にチェックしたい場合は、同じ規則を自分で呼べます。
// コントロールが編集確定時に呼んでいるものと同じ検査
ValidationResult result = sheet.ValidateInput(row, col, input);
if (!result.IsValid && result.Blocks)
{
log.Warn($"{result.Title}: {result.Message}");
}
// リスト規則の候補を取り出す(自前の入力 UI を作るときに)
IReadOnlyList<string> options = sheet.GetValidationListOptions(row, col);
シートの規則を唯一の定義として、取り込み経路からも同じ検査を呼ぶ — これが、C# 側に検証ロジックを二重に書かずに済ませるやり方です。
3. セルのメモ — 「なぜ」を人に残す
保護と規則は機械が読むルールです。残りの半分、人にしか伝えられないことを担当するのがメモです。

using unvell.ReoGrid.Core.Comments;
// 1 セル 1 件。再度呼ぶと置き換わる
sheet.SetComment(0, 3, "小計は自動計算です。編集できません。", author: "テンプレート");
// 常時表示に固定する(既定はホバー時のみ)
sheet.SetComment(0, 4, "納品日は今日以降のみ", author: "テンプレート", visible: true);
メモの付いたセルには右上に赤い三角が描かれ、ポインタがセルに乗っている間だけ吹き出しが開きます(Excel と同じ)。visible: true にすると開きっぱなしになります。
この記事の文脈でメモが効くのは、保護と規則が「できない」と言った理由を、その場に置いておけるからです。ロックされたセルをクリックしたユーザーが読むのはダイアログではなく、セルの三角です。テンプレートを配る側の意図を、テンプレート自身に書き込んでおけます。
読み出しと削除も一通り揃っています。
CellComment? comment = sheet.GetComment(1, 1);
// シート全体を行優先で列挙する(保存順も同じなので、保存し直しても差分が出ない)
foreach (var c in sheet.Comments.Items)
Console.WriteLine($"{c.Cell.ToAddress()}: {c.Text}");
sheet.RemoveComment(1, 1);
int removed = sheet.Comments.RemoveRange(RangePosition.Parse("A1:D100"));
メモはセルに随伴します。行や列を挿入すれば一緒に動き、セルごと削除されれば消えます(undo で戻ります)。
そして 1 つ覚えておくべき挙動 — メモも入力規則の UI も、印刷と PDF には出ません。 Excel の既定と同じです。画面のための注釈であって、帳票に混ざることはありません。
4. 3 つを組み合わせる — 見積テンプレート 1 枚
ここまでを 1 つのメソッドにまとめます。順番が要点です。中身を作る → ロックを外す → 規則を張る → メモを置く → 最後に保護する。
using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.Comments;
using unvell.ReoGrid.Core.Protection;
using unvell.ReoGrid.Core.Style;
using unvell.ReoGrid.Core.Validation;
static void BuildQuotationTemplate(Worksheet sheet)
{
// --- 1. テンプレートの中身 ---------------------------------
sheet.SetText(0, 0, "商品");
sheet.SetText(0, 1, "単価");
sheet.SetText(0, 2, "数量");
sheet.SetText(0, 3, "小計");
sheet.SetText(0, 4, "納品日");
for (int row = 1; row <= 99; row++)
sheet.SetFormula(row, 3, $"B{row + 1}*C{row + 1}"); // 小計は数式
sheet.SetRowStyle(0, new StyleRecord
{
Bold = true,
BackgroundColor = 0xFFEFEFEF, // ARGB。V5 の色は uint
TextAlign = HAlign.Center,
});
sheet.SetNumberFormat(RangePosition.Parse("B2:C100"), "#,##0");
// 小計は空行だと 0 になる。ゼロのセクションを "-" にして表を静かにしておく
sheet.SetNumberFormat(RangePosition.Parse("D2:D100"), "#,##0;[Red]-#,##0;\"-\"");
// --- 2. 触らせる列だけロックを外す -------------------------
// D 列(小計)と 1 行目(見出し)は既定のロックのまま
sheet.SetRangeLock(RangePosition.Parse("A2:C100"), LockState.Unlocked);
sheet.SetRangeLock(RangePosition.Parse("E2:E100"), LockState.Unlocked);
// --- 3. 値の門番 -------------------------------------------
sheet.AddValidation(RangePosition.Parse("A2:A100"), new ListValidationRule
{
Source = "=マスタ!$A$1:$A$200",
});
sheet.AddValidation(RangePosition.Parse("B2:B100"), new ComparisonValidationRule
{
Kind = ComparisonKind.Decimal,
Operator = ValidationOperator.GreaterThanOrEqual,
Value1 = ValidationValue.Num(0),
ShowInputMessage = true,
InputTitle = "単価",
InputMessage = "税抜の金額を半角数字で入力してください。",
AlertStyle = ValidationAlertStyle.Warning,
});
sheet.AddValidation(RangePosition.Parse("C2:C100"), new ComparisonValidationRule
{
Kind = ComparisonKind.Whole,
Operator = ValidationOperator.Between,
Value1 = ValidationValue.Num(1),
Value2 = ValidationValue.Num(9999),
});
sheet.AddValidation(RangePosition.Parse("E2:E100"), new ComparisonValidationRule
{
Kind = ComparisonKind.Date,
Operator = ValidationOperator.GreaterThanOrEqual,
Value1 = ValidationValue.Str("=TODAY()"),
});
// --- 4. 理由を置く -----------------------------------------
sheet.SetComment(0, 0, "商品はマスタから選択します。マスタにない商品は管理部門へ。", author: "テンプレート");
sheet.SetComment(0, 3, "小計は単価×数量の自動計算です。編集できません。", author: "テンプレート");
// --- 5. 最後に保護する -------------------------------------
sheet.Protection.AllowSort = true;
sheet.Protection.AllowAutoFilter = true;
sheet.Protect();
}
これで、ユーザーにできることは「商品を選ぶ・単価と数量を打つ・納品日を入れる・並べ替える」だけになります。小計の数式は消せず、数量に文字は入らず、なぜ D 列が編集できないのかはセルの三角が答えます。
WinForms なら、あとはコントロールに載せるだけです。
var grid = new ReoGridControl { Dock = DockStyle.Fill };
BuildQuotationTemplate(grid.ActiveWorksheet);
WPF・Avalonia でも BuildQuotationTemplate の中身は 1 文字も変わりません。3 つの機能はすべてコア側にあり、プラットフォーム側は描画と入力の受け渡しだけを担当します。
5. Excel との往復
3 つとも XLSX と reogrid-json の両方で往復します。テンプレートを Excel で作って読み込む運用も、V5 で組んだテンプレートを Excel に渡す運用も成立します。
using unvell.ReoGrid.IO.Excel;
XlsxWriter.Write(workbook, "quotation-template.xlsx");
Workbook loaded = XlsxReader.Read("quotation-template.xlsx");
| 機能 | XLSX 上の姿 |
|---|---|
| 入力規則 | <dataValidations>。sqref の複数範囲は範囲ごとに展開される |
| メモ | xl/comments{n}.xml と、箱の位置・表示状態を持つ VML の両方 |
| シート保護 | <sheetProtection>。セル単位のロックはセル書式(<xf><protection locked="0"/>)側を通って往復する |
メモで VML まで書いているのは、Excel が片方しかないファイルを「修復が必要」と判断するからです。また Excel はメモ本文の先頭に太字で「作成者:」を埋め込む形式なので、書き出し時に付けて読み込み時に外しています。Author は Excel を経由しても失われません。
手で試すなら ReoGrid Studio が早いです。Data ▸ Data Validation…、Review ▸ Edit Note…(Shift+F2)、Review ▸ Protect Sheet… に同じ機能が載っています。Studio で作ったテンプレートをそのままアプリで読み込めます。
まとめ — 落とし穴を 1 枚で
- セルは既定でロック済み。 手順は「入力させる範囲のロックを外す →
Protect()」の順 AllowSelectLockedCells/AllowSelectUnlockedCellsの 2 つだけ既定がtrue。他のAllowXxxはfalse。取り違えるとクリックできないシートができる- 保護が止めるのはユーザー操作だけ。モデル API は開いたまま(Excel の
UserInterfaceOnlyと同じ) Protect(password)のパスワードは Excel の 16 ビット検証値。暗号ではない- 入力規則が走るのは人が打った値の確定時のみ。数式・貼り付け・オートフィル・
SetValueは検査されない。必要ならValidateInputを自分で呼ぶ IgnoreBlankは既定true— 空欄は常に合格。必須入力は規則ではなく別途チェックする- メモも入力規則の UI も印刷・PDF には出ない
- コードは WinForms / WPF / Avalonia で共通。ヘッドレスでも同じように動く
入力規則・メモ・シート保護は、どれも派手な機能ではありません。ただ、業務アプリで「表を見せる」から「表に入力させる」へ進む瞬間には、この 3 つが揃っているかどうかで運用の手戻りがまるごと変わります。V5 はここを埋めました。
ReoGrid V5 の新機能を見る / 30 日トライアルを試す
次に読むもの
- データの入力規則 — 規則の種類とメッセージの全項目
- セルのメモ — 表示・列挙・Excel 往復
- シート保護 — 許可フラグと拒否イベントの全体
- ReoGrid V5 のご案内 — コアから作り直した次世代版
- 金額・消費税・端数処理を正しく扱う C# 請求書スプレッドシート
- 全角・半角の正規化を C# で — 入力の揺れを吸収する