壊れない入力シートを作る — ReoGrid V5 の入力規則・セルのメモ・シート保護

· unvell team
壊れない入力シートを作る — ReoGrid V5 の入力規則・セルのメモ・シート保護

アプリの画面にスプレッドシートを置くと、必ず同じ問題が来ます。ユーザーが小計の数式を消す単価の列に「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 つだけ落とし穴があります。 AllowSelectLockedCellsAllowSelectUnlockedCells の 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()"),
});

KindWhole(整数)・DecimalDateTimeTextLength の 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 / AllowSelectUnlockedCells2 つだけ既定が true。他の AllowXxxfalse。取り違えるとクリックできないシートができる
  • 保護が止めるのはユーザー操作だけ。モデル API は開いたまま(Excel の UserInterfaceOnly と同じ)
  • Protect(password) のパスワードは Excel の 16 ビット検証値。暗号ではない
  • 入力規則が走るのは人が打った値の確定時のみ。数式・貼り付け・オートフィル・SetValue は検査されない。必要なら ValidateInput を自分で呼ぶ
  • IgnoreBlank は既定 true空欄は常に合格。必須入力は規則ではなく別途チェックする
  • メモも入力規則の UI も印刷・PDF には出ない
  • コードは WinForms / WPF / Avalonia で共通。ヘッドレスでも同じように動く

入力規則・メモ・シート保護は、どれも派手な機能ではありません。ただ、業務アプリで「表を見せる」から「表に入力させる」へ進む瞬間には、この 3 つが揃っているかどうかで運用の手戻りがまるごと変わります。V5 はここを埋めました。

ReoGrid V5 の新機能を見る / 30 日トライアルを試す


次に読むもの

ご自身のプロジェクトで ReoGrid を試す

.NET WinForms / WPF 向けの Excel 互換スプレッドシートコンポーネント。30 日間の無償トライアルをご利用いただけます。

ニュースレター

最新リリースをメールでお届け

ReoGrid の新バージョン・新機能・技術記事のお知らせをお送りします。配信停止はいつでも可能です。

関連する記事

IF のネストが 3 段を超えたら IFS — IF / AND / OR / IFERROR で条件分岐を整理する

合否判定、ランク分け、達成率の評価——業務の表は条件分岐だらけ。定番の IF はネストが深くなると括弧の対応が追えなくなる。ネストを平らにする IFS、条件を束ねる AND / OR、エラーを既定値に丸める IFERROR の使い分けを例で整理し、ReoGrid(IFS / IFERROR は V4.5 で対応)で WinForms / WPF アプリに同じ数式を載せる方法まで解説する。

「氏名を姓と名に分ける」「住所から都道府県を取り出す」— LEFT / MID / FIND / SUBSTITUTE で文字列を数式で加工する

名簿の氏名を姓と名に分割する、住所から都道府県だけ取り出す、商品コードをハイフンで分解する——業務データの文字列加工は C# なら Split 一発だが、「ユーザーが画面上で確認・修正できる形」にしたいならスプレッドシートの数式が向いている。LEFT・MID・FIND・SUBSTITUTE・TEXTJOIN の定番レシピと落とし穴(全角スペース、神奈川県・和歌山県・鹿児島県の 4 文字問題)を整理し、ReoGrid(V4.5 で対応)で WinForms / WPF アプリに同じ数式を載せる方法まで解説する。

EDATE / EOMONTH / WORKDAY / NETWORKDAYS — 支払期日・営業日を数式で計算する

「月末締め翌月末払い」「5 営業日以内に発送」「期日の 3 営業日前にリマインド」——業務アプリの日付ルールは、DateTime で自前実装すると意外に面倒。Excel には EDATE・EOMONTH・WORKDAY・NETWORKDAYS という専用関数がある。4 つの役割と定番の落とし穴(起点日を数える・数えない問題、月末への丸め)を整理し、ReoGrid(V4.5 で対応)で WinForms / WPF アプリ内に同じ日付計算を載せる方法まで解説する。