.NET アプリケーションで Excel 風の条件付き書式を実装する

· unvell team
.NET アプリケーションで Excel 風の条件付き書式を実装する

条件付き書式は、なくなって初めてありがたみがわかる Excel 機能の代表格です。マイナスの値を赤で表示する、ステータスが「期限超過」の行をハイライトする、進捗率の列に緑のデータバーを出す。これを .NET アプリに組み込むだけで、画面が「どこを見ればよいか」をユーザーに語りかけてくれるようになります。

本記事では、条件付き書式が裏側でどう動いているのか、スケールするパターンとそうでないパターンは何か、を順を追って解説します。


メンタルモデル

条件付き書式ルールは、3 つの要素から成り立っています。

  1. 適用範囲A1:A1000、シート全体、など。
  2. 条件 — セルごとに真偽を返す数式または比較。
  3. スタイル — 条件が真のときに適用するもの(文字色、塗りつぶし、罫線、データバー、アイコン)。

Excel および .xlsx でラウンドトリップ可能なライブラリでは、ルールはシートに 1 度だけ保存される のがポイントです。セルごとにコピーされるわけではありません。レンダラはセルを描画するたびにオンザフライで評価します。これがパフォーマンスに大きく影響します(後述)。


最初のルール

定番のケース: しきい値を超えた値をハイライトします。

using unvell.ReoGrid.ConditionalStyle;   // Rule

sheet.ConditionalStyles.Add(new Rule(
    "THIS > 100",                       // 条件 (THIS = セル値)
    "A1:A1000",                          // 範囲
    new WorksheetRangeStyle
    {
        Flag      = PlainStyleFlag.TextColor | PlainStyleFlag.BackColor,
        TextColor = SolidColor.White,
        BackColor = SolidColor.Red,
    }));
100 を超える値だけが赤地に白文字で描かれ、それ以外はそのままの数値列
実行結果 — 上のルールを数値の列に適用したもの。手で色を付けたセルはひとつもなく、250・101・780・133・415 が THIS > 100 を満たして白抜き赤地になり、100 はなりません(B 列は画像用のラベル)。

これで A1:A1000 の範囲にあるセルのうち、値が 100 を超えるものは白文字 + 赤背景で表示されるようになります。A37 = 250 を代入すれば自動で再描画され、50 に戻せばハイライトが消えます。イベント配線は不要です。


ルールから何が見えるか — 行全体のハイライトができない理由

ルールの数式は、適用範囲の各セルについて 1 回ずつ評価され、THIS がそのセルの値になります。V4 のセルごとの文脈はこれだけです。つまり、数式に書いたセル番地は、評価対象のセルに合わせてズレません

これが効いてくるのが、いちばんよく求められる「F 列が "期限超過" の行を丸ごと色付けしたい」というケースです。Excel なら列を絶対参照・行を相対参照にして書きます。

// Excel の定石。ただし ReoGrid V4 では 1 行も色が付かない。
sheet.ConditionalStyles.Add(new Rule(
    "$F1 = \"期限超過\"",
    "A1:Z1000",
    new WorksheetRangeStyle { Flag = PlainStyleFlag.BackColor, BackColor = SolidColor.LightCoral }));
2 行が Overdue になっているのに、どの行にもハイライトが付いていない請求一覧のシート
実行結果 — 上の Excel の定石をそのまま実行したもの。1 行も色が付かず、エラーも出ません(3 行目と 5 行目が Overdue)。

$F1 は範囲内のどのセルを評価するときも $F1 のままなので、ルールの成否は F1 という 1 つのセルだけで決まり、範囲全体が色付くか、まったく色付かないかのどちらかになります(reogrid-v4#15)。V4 では、ルールは「そのセル自身についての条件」に限り、行の色付けは普通にスタイルを設定して行います。

// V4 での行ハイライト: ステータスが変わったときに行にスタイルを設定する
var overdue = new WorksheetRangeStyle
{
    Flag      = PlainStyleFlag.BackColor,
    BackColor = SolidColor.LightCoral,
};

for (int r = 1; r <= lastRow; r++)
{
    if ((sheet[r, 5] as string) == "期限超過")
        sheet.SetRangeStyles(new RangePosition(r, 0, 1, 26), overdue);
}

ReoGrid V5 は OOXML のルールセット(expression ルールを含む)を実装しており、そちらでは参照がセルごとにズレるため、上の Excel の定石がそのまま動きます。


データバーとアイコンセット

ReoGrid V4 のルールは 1 種類だけです — 数式ひとつと WorksheetRangeStyle ひとつ。DataBarRule もカラースケールもアイコンセットもありません。これらは ReoGrid V5 で OOXML のルールセットとして実装され、.xlsx とのラウンドトリップにも対応しました。

V4 では、データバーはルールではなく描画の仕事になります。カスタムセルボディならセルの文字の背面にバーを描けます。十数行で書けて、色も形も思いどおりです。

public class ProgressBarCell : CellBody
{
    public override void OnPaint(CellDrawingContext dc)
    {
        double value = Math.Clamp(Cell.Worksheet.GetCellData<double>(Cell.Position), 0, 1);
        var bounds   = GetBodyBounds();
        int barWidth = (int)(value * bounds.Width);

        if (barWidth > 0)
        {
            var rect = new Rectangle(bounds.Left, bounds.Top + 1, barWidth, bounds.Height - 2);
            dc.Graphics.FillRectangleLinear(SolidColor.SteelBlue, SolidColor.SkyBlue, 90f, rect);
        }

        dc.DrawCellText();   // 書式適用後の値をバーの上に描く
    }
}

.xlsx を使う場合の違いは 1 点です。ルールは Excel にラウンドトリップしますが、CellBody はしません。描いているのはあなたのコードなので、保存されたファイルにあるのはただのセルです。


パフォーマンス: ハマりやすい落とし穴

「すべてのルールを、すべてのセルに対して、すべての描画タイミングで評価する」という素朴な実装は、小さな範囲なら十分速いですが、大きな範囲では破綻します。経験則を 4 つ挙げます。

  1. ルールは、列全体ではなく実際のデータ範囲に適用するA:A をスコープにすると、Excel が持ちうる全 1,048,576 セルに対して評価が走ります。A1:A1000 のように絞り込めば、データのある場所だけを見るようになります。
  2. 条件自体に高コストな数式を使わないVLOOKUP(...) をセル単位 × 再描画ごとに評価するルールは、もはやルールではなくベンチマークです。
  3. 条件付き書式が掛かっている範囲への一括投入に注意。1 件挿入するたびに重なるルールが再評価される場合、5,000 セルを 1 つずつ入れるのは最悪ケースです。ReoGrid 4.4 ではこの経路を専用に最適化し、1 つの条件付き書式ルール下での一括ロードが 4.3.13 比で約 11,700 倍速くなりました。詳しくは v4.4 リリースノート の実測値をご覧ください。
  4. ルール変更はバッチ化する。10 個のルールをループで追加するなら、再描画を停止し、追加し終えてから再開する。1 ルールごとに再描画してはいけません。

スケールするパターンは、「範囲を絞り、条件を軽くし、既にルールが適用されている範囲には一括 API(SetRangeData)を使う」というものです。


.xlsx へのラウンドトリップ

ユーザーが Excel に書き出す場合、書式は 無事に往復してほしい ところです。互換性で押さえておくべき点が 2 つあります。

  • 標準ルールタイプを使う。「セル値が X と比較」「数式が真」「上位/下位 N」「重複」「データバー」「カラースケール」「アイコンセット」は OpenXML 仕様に含まれており、綺麗にラウンドトリップします。
  • 個別の独自スタイル(特定ストップを持つグラデーション、プログラム指定したフォント)はラウンドトリップしますが、ルールのセマンティクスが OpenXML で表現可能 であることが前提です。.NET のユーザー定義関数を呼ぶ条件は Excel に対応するものが無いため、書き出しでは生き残りません。

安全な目安: 「Excel の『新しいルール』ダイアログだけで(VBA を書かずに)記述できるか?」です。これに該当するルールはラウンドトリップします。


ルールの削除

実装に必要となる操作は 2 つあります。

// シート上のすべてのルールを削除
sheet.ConditionalStyles.Clear();

// あるセルが現在いずれかのルールで書式設定されているかを確認
bool isFormatted = sheet.HasConditionalStyle(new CellPosition("A37"));

HasConditionalStyle は、「このセルは本当に赤いのか、それともルールのせいで赤く見えているのか?」を判別するときに便利です。コピー&ペーストの挙動を実装するときや、ユーザー向けにスタイルインスペクタを作るときによく使います。


どんなときに使うべきか

条件付き書式が向いているのは、概ね次のようなケースです。

  • ユーザーがデータを スキャン して把握したい(分析ではなく一目で確認したい)
  • 視覚的なヒントが データそのもの に依存する(UI 状態ではない)
  • Excel と同じ振る舞い を自社アプリでも維持したい

逆に、ハイライトが 選択・ホバー・アプリのレベルの状態 に依存する場合は、条件付き書式の出番ではありません。それは UI の関心事であり、ワークブックではなくコントロールの描画ロジックに置くべきです。


さらに読む

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

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

ニュースレター

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

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

関連する記事