「税込価格を出す」「自社ルールで端数を丸める」「商品コードから単価を引く」— こうした業務ロジックを、アプリのコード側と、ユーザーが触るスプレッドシートの数式側の両方に書いてしまっていないでしょうか。
これは静かに広がる二重管理です。コード側では CalcTaxIncluded() という C# のメソッドがあり、シート側では利用者が =B2*1.1 と手で書いている。消費税率が変わったとき、あるいは端数処理のルールが変わったとき、片方だけ直して、もう片方を直し忘れる。「アプリの計算と帳票の数字が合わない」という事故の温床です。
ReoGrid は WinForms / WPF 向けの .NET スプレッドシートコンポーネントで、SUM や VLOOKUP をはじめ 200 以上の Excel 互換関数を標準搭載しています。そのうえで、自社の C# ロジックを「カスタム関数」として登録できます。一度登録すれば、利用者はセルに =TAXIN(B2) と書くだけで自社の計算ロジックを呼び出せます。ロジックの実体は C# 側の 1 か所だけ。シートに散らばった数式を直して回る必要はなくなります。
この記事では、カスタム関数の登録方法を最小例から実務パターンまで、C# のコードとともに解説します。WinForms / WPF どちらでも同じ API で、Excel のインストールは不要です。
カスタム関数とは — 「数式の一級市民」を自分で増やす
ReoGrid の数式エンジンは、=SUM(A1:A10) のような数式を解析して評価します。このとき、SUM や IF といった組み込み関数を順に照合し、どれにも当てはまらない関数名が来たら、最後にカスタム関数の辞書を参照します。
つまりカスタム関数は、組み込み関数を邪魔せずに、自社の関数だけをエンジンに追加する仕組みです。利用者から見れば、SUM と TAXIN(自社関数)の区別はありません。どちらも同じように = から書けて、セル参照を渡せて、他の数式にネストできる「数式の一級市民」になります。
登録は実質1行
カスタム関数は、unvell.ReoGrid.Formula.FormulaExtension.CustomFunctions という辞書に、関数名をキーにしてデリゲートを登録するだけです。
using unvell.ReoGrid;
using unvell.ReoGrid.Formula;
// 文字列を大文字にする独自関数 UPPER2 を登録
FormulaExtension.CustomFunctions["UPPER2"] = (cell, args) =>
{
if (args.Length == 0) return null;
return Convert.ToString(args[0]).ToUpper();
};
これで、どのワークシートでも使えます。
worksheet["A1"] = "hello";
worksheet["B1"] = "=UPPER2(A1)"; // → "HELLO"
登録するデリゲートの型は Func<Cell, object[], object> です。3 つの要素の意味は次のとおりです。
| 要素 | 型 | 内容 |
|---|---|---|
第1引数 cell | Cell | 数式が入力されているセル。位置や所属ワークシートを参照できる |
第2引数 args | object[] | 評価済みの引数の値。=UPPER2(A1) なら args[0] に A1 の値が入る |
| 戻り値 | object | セルに表示・格納される計算結果 |
ここで最も重要なのは、args に渡ってくるのが「A1 というセル参照」ではなく、A1 を評価し終えた後の値そのものだということです。参照先が数値なら double、文字列なら string が入ってきます。セル参照の解決・他の数式の事前評価はすべてエンジンが済ませてくれるので、開発者は自社ロジックの中身だけに集中できます。
引数の型・未入力・エラーの扱い
args の要素は object 型なので、実務では「型変換」と「未入力チェック」を関数の入口で行うのが定石です。Excel 互換のエラー文字列をそのまま返せば、セルにも #VALUE! のように表示されます。
FormulaExtension.CustomFunctions["TAXIN"] = (cell, args) =>
{
// 引数なし・空セル参照は 0 として扱う
if (args.Length == 0 || args[0] == null) return 0d;
// 文字列・数値どちらで渡されても double に寄せる
if (!double.TryParse(Convert.ToString(args[0]), out double price))
{
return "#VALUE!"; // 数値化できなければ Excel と同じエラー表示
}
return Math.Floor(price * 1.10); // 10% 税込・端数切り捨て
};
args[0] == null… 参照先が空セルのケースを先に潰すdouble.TryParse(Convert.ToString(...))… セルに全角数字や文字列が紛れても落ちない"#VALUE!"を返す … 例外を投げずに、利用者が見慣れたエラー表示で返す
戻り値に double を返せば数値セルとして、string を返せば文字列セルとして扱われます。数値で返しておけば、その結果をさらに =SUM(...) などで集計できます。
消費税の軽減税率(8% / 10%)の税率別集計や、インボイス制度に沿った端数処理そのものについては、書式と数式での扱いを別記事【「請求書の合計が1円ずれる」を防ぐ — C# で通貨書式・消費税・端数処理を正しく扱う】で詳しく解説しています。本記事は「自社ロジックを関数化する仕組み」に焦点を当てます。
実務パターン1:自社マスタから単価を引き当てる
カスタム関数が最も効くのは、シートの外にある状態(マスタデータや業務ルール)を数式から呼び出したいときです。たとえば、商品コードを渡すと単価を返す関数を考えます。単価マスタは C# 側の Dictionary(実際には DB やマスタサービス)にあります。
// アプリ起動時に一度だけ用意するマスタ(実際は DB から読み込む)
var priceTable = new Dictionary<string, double>
{
["A-100"] = 1200,
["A-200"] = 3800,
["B-050"] = 540,
};
// 商品コード → 単価 を引く自社関数 UNITPRICE
FormulaExtension.CustomFunctions["UNITPRICE"] = (cell, args) =>
{
if (args.Length == 0 || args[0] == null) return 0d;
string code = Convert.ToString(args[0]);
return priceTable.TryGetValue(code, out double price) ? price : "#N/A";
};
利用者はマスタの存在を意識せず、VLOOKUP のように使えます。
worksheet["A2"] = "A-100";
worksheet["B2"] = "=UNITPRICE(A2)"; // → 1200
worksheet["C2"] = 3; // 数量
worksheet["D2"] = "=UNITPRICE(A2) * C2"; // → 3600(他の数式にネストできる)
VLOOKUP であれば単価マスタをシート上のどこかに貼り付けておく必要がありますが、カスタム関数ならマスタはアプリ側に隠したまま、関数として開放できます。単価が変わってもシートには手を入れません。マスタを更新したら(必要に応じて)worksheet.Recalculate() を呼べば、関連セルが一斉に再計算されます。
実務パターン2:自社ルールの集計を関数化する
引数は可変長で渡せるので、標準関数では表現しづらい「自社ルールの集計」も関数にできます。たとえば重み付き平均です。
// =WAVG(値1, 重み1, 値2, 重み2, ...)
FormulaExtension.CustomFunctions["WAVG"] = (cell, args) =>
{
double sum = 0, weight = 0;
for (int i = 0; i + 1 < args.Length; i += 2)
{
double v = Convert.ToDouble(args[i]);
double w = Convert.ToDouble(args[i + 1]);
sum += v * w;
weight += w;
}
return weight == 0 ? 0d : sum / weight;
};
worksheet["A1"] = "=WAVG(80, 3, 60, 1)"; // → 75((80*3 + 60*1) / 4)
評価ロジックが既存の C# 資産にあるなら、その内側からドメインサービスを呼ぶだけで済みます。「スコアリング」「与信判定」「料金プランの計算」など、すでにテスト済みのビジネスロジックを、シートの数式として再利用できます。
実務パターン3:セルの位置に応じて振る舞う
第1引数の cell を使うと、計算元セルの位置や所属シートに応じて挙動を変えられます。たとえば、自分がどこにいるかを返す関数です。
FormulaExtension.CustomFunctions["WHEREAMI"] = (cell, args) =>
{
// 例: "Sheet1!C3" のように自分の位置を返す
return $"{cell.Worksheet.Name}!{cell.Address}";
};
cell.Worksheet から所属するワークシートに、cell.Address から "C3" のようなセルアドレスに、それぞれアクセスできます。行番号で料率を変える、シートごとに参照するマスタを切り替える、といった「文脈依存の計算」もこれで書けます。
運用上の注意点
カスタム関数を実務に組み込むときに、押さえておきたいポイントです。
標準関数と衝突しない名前にする
CustomFunctions の辞書は、組み込みの 200+ 関数をすべて照合した後に参照されます。そのため SUM や IF のような既存名で登録しても、組み込み側が優先されて呼ばれません。自社プレフィックスを付けた名前(ACME_TAXIN など)にしておくと、衝突も誤解も避けられます。
関数名は大文字で登録・大文字で記述する
Excel の慣習に合わせ、関数名は TAXIN のように大文字で登録し、数式中でも大文字で記述するのが確実です。登録したキーと、数式中の関数名が一致したときに呼び出されます。
登録はアプリ起動時に一度だけ
CustomFunctions は静的(アプリケーション全体で共有)です。Program.Main やフォームの初期化処理で一度登録すれば、すべてのワークブック・ワークシートで有効になります。各画面で登録し直す必要はありません。
外部状態に依存するなら再計算を明示する
参照先セルの値が変われば、それに依存する数式は自動で再計算されます。一方、マスタや DB のようなシート外の状態に依存する関数は、その状態が変わったことをエンジンは知りません。マスタを更新したら worksheet.Recalculate() を呼んで、明示的に再評価させてください。範囲を絞って Recalculate(range) と呼ぶこともできます。
まとめ
ReoGrid のカスタム関数は、たった 1 行で登録できます。
FormulaExtension.CustomFunctions["関数名"] = (cell, args) => { /* 自社ロジック */ };
argsには評価済みの値が渡るので、セル参照の解決を気にせずロジックに集中できる- 税込計算・自社マスタの引き当て・独自集計などを、利用者には「Excel 数式」として開放できる
- ロジックの実体は C# 側の 1 か所に集約され、シートに散らばらない
- すでにテスト済みのドメインロジックを、数式の中から再利用できる
「業務アプリの計算ルール」をスプレッドシート側に寄せると、コードと利用者の両方がシンプルになります。コードとシートで同じ計算を二重に持つのをやめ、自社ロジックを単一の真実の源(single source of truth)にしましょう。
ReoGrid は WinForms / WPF どちらでも同じ API で動作し、評価用に無料でお試しいただけます。導入のご相談はお問い合わせからどうぞ。
