実装機能
TableCell.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using NewTableCell_test.MyControls.Cells;
namespace CalculationForms2.MyControls.TableCellTextPainted {
/// <summary>
/// 軽量 OnPaint テーブル(既存 TableCellText 互換APIの最小実装を網羅)
/// </summary>
[ToolboxItem(true)]
public class TableCell : Control {
#region // ===== レイアウト フィールド =====
private const string DictKeyText = "Text";
private int _rowCount = 30;
[Category("Layout")]
public int RowCount {
get => _rowCount;
set {
var v = Math.Max(1, value);
if (_rowCount == v) return;
_rowCount = v;
RecalcLayoutAndInvalidate();
}
}
private int _colCount = 10;
[Category("Layout")]
public int ColumnCount {
get => _colCount;
set {
var v = Math.Max(1, value);
if (_colCount == v) return;
_colCount = v;
RecalcLayoutAndInvalidate();
}
}
private int[] _columnWidths = Array.Empty<int>();
[Category("Layout"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public int[] ColumnWidths {
get => _columnWidths;
set { _columnWidths = value ?? Array.Empty<int>(); RecalcLayoutAndInvalidate(); }
}
private int[] _rowHeights = Array.Empty<int>();
[Category("Layout"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public int[] RowHeights {
get => _rowHeights;
set { _rowHeights = value ?? Array.Empty<int>(); RecalcLayoutAndInvalidate(); }
}
// 行の非表示管理
private readonly HashSet<int> _hiddenRows = new HashSet<int>();
// 列の非表示管理
private readonly HashSet<int> _hiddenCols = new HashSet<int>();
// 列・行のレイアウトキャッシュ
private Rectangle[] _colRects = new Rectangle[0]; // X/Width を保持(Y/Heightは行側で)
private int[] _rowTops = new int[0]; // 各行のY開始(最後に合計高さ)
private readonly Dictionary<(int r, int c), Cell> _cells = new Dictionary<(int r, int c), Cell>();
protected override void OnCreateControl() {
base.OnCreateControl();
RecalcLayoutAndInvalidate(); // ★ 初回レイアウト計算を必ず実行
}
private CellStyle DefaultStyle() {
CellStyle s = new CellStyle();
s.Align = ContentAlignment.MiddleRight;
s.BackColor = Color.White;
s.ForeColor = Color.Black;
s.Bold = false;
return s;
}
private Cell GetCell(int r, int c)
{
if (_cells.TryGetValue((r, c), out var cell)) return cell;
cell = new Cell(r, c); // 既定スタイル付きで作成
_cells[(r, c)] = cell;
return cell;
}
#endregion // ===== レイアウト =====
#region ------------------- セルに載せるコントロールの -------------------
// TableCellTextPaintedLite.cs 内にユーティリティ関数を追加
private static OverlayKind DetectOverlayKind(Control control, out string typeName)
{
typeName = null;
if (control == null) return OverlayKind.None;
// 既知の型を判定
if (control is TextBox) { typeName = typeof(TextBox).FullName; return OverlayKind.TextBox; }
if (control is ComboBox) { typeName = typeof(ComboBox).FullName; return OverlayKind.ComboBox; }
if (control is CheckBox) { typeName = typeof(CheckBox).FullName; return OverlayKind.CheckBox; }
if (control is Button) { typeName = typeof(Button).FullName; return OverlayKind.Button; }
if (control is Label) { typeName = typeof(Label).FullName; return OverlayKind.Label; }
if (control is DateTimePicker) { typeName = typeof(DateTimePicker).FullName; return OverlayKind.DateTimePicker; }
if (control is NumericUpDown) { typeName = typeof(NumericUpDown).FullName; return OverlayKind.NumericUpDown; }
if (control is TrackBar) { typeName = typeof(TrackBar).FullName; return OverlayKind.TrackBar; }
// 既知以外は Custom として実型名を入れる
typeName = control.GetType().FullName;
return OverlayKind.Custom;
}
// 既存の SetCellControl を修正
public void SetCellControl(int row, int col, Control control, Dictionary<string, string> myDictionary)
{
EnsureInside(row, col);
// 結合セルならアンカーへ
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
var cd = GetCell(row, col);
// 既存を外す
if (cd.Overlay != null)
{
Controls.Remove(cd.Overlay);
cd.Overlay.Dispose();
cd.Overlay = null;
// ★ 種別クリア
cd.OverlayType = OverlayKind.None;
cd.OverlayTypeName = null;
}
cd.Dict = myDictionary;
// 表示へ反映:dict["Text"] があればセルの Text に反映
string textFromDict;
if (cd.Dict != null && cd.Dict.TryGetValue("Text", out textFromDict))
{
cd.Text = textFromDict ?? string.Empty;
}
if (control != null)
{
cd.Overlay = control;
// ★ 種別を自動判定して保存
string tn;
cd.OverlayType = DetectOverlayKind(control, out tn);
cd.OverlayTypeName = tn;
// 位置とレイアウト
var rc = GetMergedRect(row, col);
if (rc.IsEmpty) rc = GetCellRect(row, col);
control.Bounds = Rectangle.Inflate(rc, -1, -1);
control.Margin = Padding.Empty;
control.Anchor = AnchorStyles.Left | AnchorStyles.Top;
Controls.Add(control);
control.BringToFront();
}
InvalidateCell(row, col);
}
public OverlayKind GetCellOverlayKind(int row, int col, out string typeName)
{
EnsureInside(row, col);
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cd;
if (_cells.TryGetValue((row, col), out cd) && cd != null)
{
typeName = cd.OverlayTypeName;
return cd.OverlayType;
}
typeName = null;
return OverlayKind.None;
}
#endregion
#region --------------------------- ==== コンストラクタ ===== -----------------------------------
public TableCell()
{
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.UserPaint |
ControlStyles.ResizeRedraw, true);
BackColor = Color.White;
}
#endregion --------------------------- ==== コンストラクタ ===== -----------------------------------
#region --------------------------- セルごとのイベント登録 -----------------------------------
// セル(結合アンカーに解決後)にハンドラを追加/削除
public void AddCellValueChangedHandler(int row, int col, EventHandler<CellValueChangedEventArgs> handler)
{
EnsureInside(row, col);
ResolveAnchor(ref row, ref col); // 結合セルはアンカーへ
var cell = GetCell(row, col);
cell.ValueChanged += handler;
}
public void RemoveCellValueChangedHandler(int row, int col, EventHandler<CellValueChangedEventArgs> handler)
{
EnsureInside(row, col);
ResolveAnchor(ref row, ref col);
Cell cell;
if (_cells.TryGetValue((row, col), out cell) && cell != null)
cell.ValueChanged -= handler;
}
#endregion -------------------- セルごとのイベント登録 ---------------------------
#region ------------------------- 編集可能かどうか --------------------------------
// TableCellTextPaintedLite.cs のメンバに追加
public void SetCellEditable(int row, int col, bool editable)
{
EnsureInside(row, col);
// 結合セルならアンカーに寄せる(あなたの実装に合わせて)
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cd = GetCell(row, col);
cd.IsEditable = editable;
}
public void SetRowEditable(int row, bool editable)
{
EnsureInside(row, 0);
for (int c = 0; c < ColumnCount; c++)
{
SetCellEditable(row, c, editable);
}
}
public void SetColumnEditable(int col, bool editable)
{
EnsureInside(0, col);
for (int r = 0; r < RowCount; r++)
{
SetCellEditable(r, col, editable);
}
}
#endregion
#region -------------------- セル結合 -----------------------------
// ===== 結合セル =====
public class MergedCell
{
public int Row { get; set; }
public int Column { get; set; }
public int RowSpan { get; set; } = 1;
public int ColSpan { get; set; } = 1;
public override string ToString() { return "(" + Row + "," + Column + ") " + ColSpan + "x" + RowSpan; }
}
[Category("Layout")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public List<MergedCell> MergedCells { get; private set; } = new List<MergedCell>();
public void ClearMergedCells()
{
MergedCells.Clear();
RecalcLayoutAndInvalidate();
}
// 既存 MergedCells を使って重複・交差を防ぎつつ追加
public bool MergeCells(int row, int col, int rowSpan, int colSpan)
{
EnsureInside(row, col);
rowSpan = Math.Max(1, rowSpan);
colSpan = Math.Max(1, colSpan);
// 範囲クリップ
int r2 = Math.Min(RowCount - 1, row + rowSpan - 1);
int c2 = Math.Min(ColumnCount - 1, col + colSpan - 1);
// 交差チェック(既存結合と重なるなら拒否)
for (int i = 0; i < MergedCells.Count; i++)
{
var m = MergedCells[i];
var r1a = row; var c1a = col; var r2a = r2; var c2a = c2;
var r1b = m.Row; var c1b = m.Column; var r2b = m.Row + m.RowSpan - 1; var c2b = m.Column + m.ColSpan - 1;
bool overlap = !(r2a < r1b || r2b < r1a || c2a < c1b || c2b < c1a);
if (overlap) return false;
}
MergedCells.Add(new MergedCell { Row = row, Column = col, RowSpan = r2 - row + 1, ColSpan = c2 - col + 1 });
RecalcLayoutAndInvalidate();
return true;
}
// アンカーを指定して解除
public bool UnmergeAt(int row, int col)
{
for (int i = 0; i < MergedCells.Count; i++)
{
var m = MergedCells[i];
if (m.Row == row && m.Column == col)
{
MergedCells.RemoveAt(i);
RecalcLayoutAndInvalidate();
return true;
}
}
return false;
}
// セルがどの結合に含まれるか(見つかればアンカー返却)
public bool TryGetMergeAnchor(int row, int col, out int anchorRow, out int anchorCol)
{
for (int i = 0; i < MergedCells.Count; i++)
{
var m = MergedCells[i];
int r2 = m.Row + m.RowSpan - 1, c2 = m.Column + m.ColSpan - 1;
if (row >= m.Row && row <= r2 && col >= m.Column && col <= c2)
{ anchorRow = m.Row; anchorCol = m.Column; return true; }
}
anchorRow = row; anchorCol = col;
return false;
}
private void ResolveAnchor(ref int row, ref int col)
{
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
}
public void ClearAndDemoMerges()
{
ClearMergedCells();
// 例: (1,1) から 横2×縦1
MergeCells(1, 1, 1, 2);
SetCellText(1, 1, "2x1");
// 例: (3,0) から 横3×縦2
MergeCells(3, 0, 2, 3);
SetCellText(3, 0, "3x2");
}
// 互換API:Cell_Joint(2種)
public void Cell_Joint(int column, int row, int colSpan, int rowSpan)
{
MergedCells.Add(new MergedCell { Column = column, Row = row, ColSpan = Math.Max(1, colSpan), RowSpan = Math.Max(1, rowSpan) });
RecalcLayoutAndInvalidate();
}
// 背景色を指定できる版(C# 7.3 でも OK)
public void Cell_Joint(int column, int row, int colSpan, int rowSpan,
Color? backColor = null, bool editable = false)
{
// 結合情報を追加
MergedCells.Add(new MergedCell
{
Column = column,
Row = row,
ColSpan = Math.Max(1, colSpan),
RowSpan = Math.Max(1, rowSpan)
});
// アンカーセル(左上セル)を取得
var anchor = GetCell(row, column);
// 背景色設定
if (backColor.HasValue)
{
var st = anchor.Style;
st.BackColor = backColor.Value;
anchor.Style = st;
}
// 編集可否設定
anchor.IsEditable = editable;
RecalcLayoutAndInvalidate();
}
public void Cell_Joint(Control control, int column, int row, int colSpan, int rowSpan)
{
// 互換:コントロールを載せつつ結合も記録
SetCellControl(row, column, control, null);
Cell_Joint(column, row, colSpan, rowSpan);
}
#endregion ----------------------セル結合----------------------
#region ---------------------- セルに値を格納 -----------------------
// ===== 互換API:セル操作 =====
#region ---------------------- SetCellText -----------------------
public void SetCellText(int row, int col, string text)
{
EnsureInside(row, col);
ResolveAnchor(ref row, ref col);
var cd = GetCell(row, col);
string oldText = cd.Text ?? "";
string newText = text ?? "";
if (oldText != newText) // 変更時のみ
{
cd.Text = newText;
InvalidateCell(row, col);
cd.RaiseValueChanged(oldText, newText);
}
else
{
InvalidateCell(row, col); // 再描画だけ必要なら
}
}
public void SetCellText(int row, int col, string text, ContentAlignment align)
{
EnsureInside(row, col);
Cell cd = GetCell(row, col);
cd.Text = text ?? "";
CellStyle st = cd.Style;
st.Align = align; cd.Style = st;
InvalidateCell(row, col);
}
public void SetCellText(int row, int col, string text, ContentAlignment align, Color backColor)
{
EnsureInside(row, col);
Cell cd = GetCell(row, col);
cd.Text = text ?? "";
CellStyle st = cd.Style; st.Align = align;
st.BackColor = backColor; cd.Style = st;
InvalidateCell(row, col);
}
#endregion ---------------------- setCellText-----------------------
// 新しい本体(editable 追加、既定 false)
public void SetCellDictionary(int row, int col,
Dictionary<string, string> dict, ContentAlignment align,
bool editable = false,
bool clone = true,
Color? backColor = null) // ★ 追加:引数で背景色指定可
{
EnsureInside(row, col);
ResolveAnchor(ref row, ref col);
var cd = GetCell(row, col);
// ① 編集可否
cd.IsEditable = editable;
// ② ディクショナリ
cd.Dict = (dict == null) ? null : (clone ? new Dictionary<string, string>(dict) : dict);
// ③ 背景色(引数優先 → dict["BackColor"] → 既定のまま)
var oldColor = cd.Style.BackColor; // 変更検知用
var st = cd.Style; // (CellStyle は参照型 or 値型どちらでもOKな書き方)
if (backColor.HasValue)
{
st.BackColor = backColor.Value;
}
else if (cd.Dict != null && cd.Dict.TryGetValue("BackColor", out var colStr))
{
Color parsed;
if (TryParseColor(colStr, out parsed))
st.BackColor = parsed;
}
// ★ 追加: テキスト位置を反映
st.Align = align;
cd.Style = st;
// ④ Text 反映 & 変更イベント
string oldText = cd.Text ?? "";
string newText = oldText;
string v;
if (cd.Dict != null && cd.Dict.TryGetValue(DictKeyText, out v))
newText = v ?? string.Empty;
bool textChanged = oldText != newText;
bool colorChanged = oldColor != cd.Style.BackColor;
if (textChanged)
{
cd.Text = newText;
InvalidateCell(row, col);
cd.RaiseValueChanged(oldText, newText);
}
else if (colorChanged)
{
InvalidateCell(row, col);
}
else
{
InvalidateCell(row, col);
}
}
private static bool TryParseColor(string s, out Color color)
{
color = Color.Empty;
if (string.IsNullOrWhiteSpace(s)) return false;
s = s.Trim();
// #RRGGBB or #AARRGGBB
if (s.StartsWith("#"))
{
s = s.Substring(1);
if (s.Length == 6 || s.Length == 8)
{
byte a = 255, r, g, b;
int idx = 0;
if (s.Length == 8) { a = Convert.ToByte(s.Substring(idx, 2), 16); idx += 2; }
r = Convert.ToByte(s.Substring(idx, 2), 16); idx += 2;
g = Convert.ToByte(s.Substring(idx, 2), 16); idx += 2;
b = Convert.ToByte(s.Substring(idx, 2), 16);
color = Color.FromArgb(a, r, g, b);
return true;
}
return false;
}
// R,G,B[,A]
var parts = s.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3 || parts.Length == 4)
{
int r = int.Parse(parts[0]);
int g = int.Parse(parts[1]);
int b = int.Parse(parts[2]);
int a = (parts.Length == 4) ? int.Parse(parts[3]) : 255;
color = Color.FromArgb(a, r, g, b);
return true;
}
// 名前色
var named = Color.FromName(s);
if (named.A != 0 || string.Equals(s, named.Name, StringComparison.OrdinalIgnoreCase))
{
color = named;
return true;
}
return false;
}
public void SetCellForeColor(int row, int col, Color color, bool? bold = null)
{
EnsureInside(row, col);
Cell cd = GetCell(row, col);
CellStyle st = cd.Style;
st.ForeColor = color;
if (bold.HasValue) st.Bold = bold.Value;
cd.Style = st;
InvalidateCell(row, col);
}
public void SetCellBackColor(int row, int col, Color color)
{
EnsureInside(row, col);
Cell cd = GetCell(row, col);
CellStyle st = cd.Style; st.BackColor = color; cd.Style = st;
InvalidateCell(row, col);
}
/// <summary>
/// セルのスタイルをまとめて設定する
/// </summary>
public void SetCellStyle(int row, int col,
ContentAlignment align,
bool editable = false,
bool clone = true,
Color? backColor = null,
Color? foreColor = null,
bool? bold = null,
float? fontSize = null)
{
EnsureInside(row, col);
ResolveAnchor(ref row, ref col);
var cd = GetCell(row, col);
// ① 編集可否
cd.IsEditable = editable;
// ② スタイル変更
var st = cd.Style;
st.Align = align;
if (backColor.HasValue) st.BackColor = backColor.Value;
if (foreColor.HasValue) st.ForeColor = foreColor.Value;
if (bold.HasValue) st.Bold = bold.Value;
if (fontSize.HasValue && fontSize.Value > 0f) st.FontSize = fontSize.Value;
cd.Style = st;
// ③ 再描画
InvalidateCell(row, col);
}
#endregion
#region ------------ セル情報取得 系互換API(新規追加) ===----------
#region -------------- セルのテキストと辞書配列 コントロールの取得
public string GetCellText(int row, int col) {
Cell cd;
return _cells.TryGetValue((row, col), out cd) ? (cd.Text ?? "") : "";
}
public Dictionary<string, string> GetCellDictionary(int row, int col) {
Cell cd;
return _cells.TryGetValue((row, col), out cd) ? cd.Dict : null;
}
public Control GetControlAt(int row, int col) {
Cell cd;
if (_cells.TryGetValue((row, col), out cd)) return cd.Overlay;
return null;
}
#endregion
// === 取得系互換API(新規追加) ===
public Color GetCellBackColor(int row, int col) {
EnsureInside(row, col);
// 結合セルはアンカーへ寄せる
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cd;
if (_cells.TryGetValue((row, col), out cd) && cd != null && cd.Style != null) {
// セルに個別設定があればそれを返す
return cd.Style.BackColor;
}
// 何も無ければテーブルの既定背景色
return this.BackColor;
}
// 必要に応じて前景色や太字なども取得したい場合
public Color GetCellForeColor(int row, int col) {
EnsureInside(row, col);
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cd;
if (_cells.TryGetValue((row, col), out cd) && cd != null && cd.Style != null)
return cd.Style.ForeColor;
return this.ForeColor;
}
public bool GetCellBold(int row, int col) {
EnsureInside(row, col);
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cd;
if (_cells.TryGetValue((row, col), out cd) && cd != null && cd.Style != null)
return cd.Style.Bold;
return false;
}
public ContentAlignment GetCellAlignment(int row, int col) {
EnsureInside(row, col);
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cd;
if (_cells.TryGetValue((row, col), out cd) && cd != null && cd.Style != null)
return cd.Style.Align;
return ContentAlignment.MiddleRight;
}
#endregion
#region --------------- フォントデータ取得 -----------------------
// TableCell.cs に追加
public float GetCellFontSize(int row, int col) {
EnsureInside(row, col);
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cd;
if (_cells.TryGetValue((row, col), out cd) && cd?.Style != null) {
// 個別サイズが正なら優先
if (cd.Style.FontSize > 0) return cd.Style.FontSize;
}
// 既定(コントロールの Font.Size)
return this.Font?.Size ?? 9f; // デフォルトはお好みで
}
// CellStyle に FontName がある場合はそれを使う。
// 無い場合はテーブル既定の Font.Name を返す。
public string GetCellFontName(int row, int col) {
EnsureInside(row, col);
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cd;
if (_cells.TryGetValue((row, col), out cd) && cd?.Style != null) {
// もし CellStyle に FontName プロパティを増やしたならこちらを優先
if (!string.IsNullOrEmpty(cd.Style.FontName)) return cd.Style.FontName;
}
// 既定(コントロールの Font.Name)
return this.Font?.Name ?? "Meiryo";
}
#endregion
#region -------------------- 行指定してまとめて設定 -----------------------
public void SetRowValuesAsLabels(
int rowIndex,
string[] values,
ContentAlignment align = ContentAlignment.MiddleCenter,
Color? backColor = null,
Color? foreColor = null,
float? fontSize = 14f,
bool? bold = null)
{
if (values == null) return;
for (int c = 0; c < values.Length && c < ColumnCount; c++)
{
Cell cd = GetCell(rowIndex, c);
cd.Text = values[c] ?? "";
CellStyle st = cd.Style;
st.Align = align;
if (backColor.HasValue) st.BackColor = backColor.Value;
if (foreColor.HasValue) st.ForeColor = foreColor.Value;
if (bold.HasValue) st.Bold = bold.Value;
cd.Style = st;
}
Invalidate(GetRowBounds(rowIndex));
}
/// <summary>
/// 指定行の [startCol..endCol] に values を流し込む。
/// values は startCol から順に割り当てられ、足りなければ途中で終了、余れば切り捨て。
/// editable でその範囲のセルの直接編集可否もまとめて設定できる。
/// </summary>
public void SetRowValuesAsLabels(
int rowIndex,
int startCol,
int endCol,
string[] values,
ContentAlignment align = ContentAlignment.MiddleCenter,
Color? backColor = null,
Color? foreColor = null,
float? fontSize = 14f,
bool? bold = null,
bool editable = false)
{
if (values == null) return;
// 範囲をクリップ・検証
if (rowIndex < 0 || rowIndex >= RowCount) return;
if (startCol < 0) startCol = 0;
if (endCol >= ColumnCount) endCol = ColumnCount - 1;
if (startCol > endCol) return;
// values のインデックスは startCol に合わせる
int vi = 0;
for (int c = startCol; c <= endCol; c++)
{
if (vi >= values.Length) break;
Cell cd = GetCell(rowIndex, c);
cd.Text = values[vi] ?? string.Empty;
cd.IsEditable = editable;
CellStyle st = cd.Style;
st.Align = align;
if (backColor.HasValue) st.BackColor = backColor.Value;
if (foreColor.HasValue) st.ForeColor = foreColor.Value;
if (bold.HasValue) st.Bold = bold.Value;
if (fontSize.HasValue && fontSize.Value > 0f) st.FontSize = fontSize.Value;
cd.Style = st;
vi++;
}
// 指定範囲だけ再描画
Rectangle rowBounds = GetRowBounds(rowIndex);
if (!rowBounds.IsEmpty) Invalidate(rowBounds);
}
#endregion
#region ===== レイアウト再計算と無効化 =====
private void RecalcLayoutAndInvalidate() {
int W = ClientSize.Width;
int H = ClientSize.Height;
// 列幅
_colRects = new Rectangle[Math.Max(0, ColumnCount)];
int fixedSum = 0, autoCols = 0;
// 1) 自動配分の下準備(非表示列は除外)
for (int c = 0; c < ColumnCount; c++)
{
if (_hiddenCols.Contains(c)) continue; // 非表示は計算対象外
int w = (ColumnWidths != null && c < ColumnWidths.Length) ? ColumnWidths[c] : -1;
if (w >= 0) fixedSum += w; else autoCols++;
}
int autoW = Math.Max(0, autoCols > 0 ? (ClientSize.Width - fixedSum) / autoCols : 0);
// 2) 実際の割り当て(非表示列は幅0/xは進めない)
int x = 0;
for (int c = 0; c < ColumnCount; c++)
{
int w;
if (_hiddenCols.Contains(c))
{
w = 0; // ★ 非表示:幅0
}
else
{
w = (ColumnWidths != null && c < ColumnWidths.Length && ColumnWidths[c] >= 0)
? ColumnWidths[c]
: autoW;
}
_colRects[c] = new Rectangle(x, 0, Math.Max(0, w), ClientSize.Height);
if (w > 0) x += w; // ★ 幅0の列はxを進めない(画面上で“詰める”)
}
// 行高
_rowTops = new int[Math.Max(0, RowCount + 1)];
int fixedH = 0, autoRows = 0;
for (int r = 0; r < RowCount; r++) {
if (_hiddenRows.Contains(r)) continue;
int h = (RowHeights != null && r < RowHeights.Length) ? RowHeights[r] : -1;
if (h >= 0) fixedH += h; else autoRows++;
}
int remain = Math.Max(0, H - fixedH);
int autoH = Math.Max(0, autoRows > 0 ? remain / autoRows : 0);
int y = 0;
for (int r = 0; r < RowCount; r++) {
_rowTops[r] = y;
int h = 0;
if (!_hiddenRows.Contains(r)) {
int rh = (RowHeights != null && r < RowHeights.Length) ? RowHeights[r] : -1;
h = (rh >= 0) ? rh : autoH;
}
y += h;
}
_rowTops[RowCount] = y;
// 載せているオーバレイの再配置
foreach (KeyValuePair<(int, int), Cell> kv in _cells) {
if (kv.Value.Overlay != null) {
Rectangle rc = GetMergedRect(kv.Key.Item1, kv.Key.Item2);
if (rc.IsEmpty) rc = GetCellRect(kv.Key.Item1, kv.Key.Item2);
kv.Value.Overlay.Bounds = Rectangle.Inflate(rc, -1, -1);
}
}
Invalidate();
}
#endregion
#region ===== 描画 =====
protected override void OnPaint(PaintEventArgs e) {
base.OnPaint(e);
Graphics g = e.Graphics;
using (Pen pen = new Pen(Color.Silver)) {
for (int r = 0; r < RowCount; r++) {
if (_rowTops.Length == 0) break;
int top = _rowTops[r];
int bottom = _rowTops[r + 1];
int h = Math.Max(0, bottom - top);
if (h <= 0) continue;
for (int c = 0; c < ColumnCount; c++) {
Rectangle cellRect = GetCellRect(r, c);
if (cellRect.Width <= 0 || cellRect.Height <= 0) continue;
// アンカーでなければ描かない(結合の重複描画防止)
ValueTuple<int, int> anchor = GetMergeAnchor(r, c);
if (!(anchor.Item1 == r && anchor.Item2 == c)) continue;
Rectangle mergedRect = GetMergedRect(r, c);
Cell cd = GetCell(r, c);
using (SolidBrush bg = new SolidBrush(cd.Style.BackColor))
g.FillRectangle(bg, mergedRect);
using (StringFormat sf = MakeStringFormat(cd.Style.Align))
using (SolidBrush fore = new SolidBrush(cd.Style.ForeColor)) {
// 置き換え前:
// Font f = cd.Style.Bold ? new Font(Font, FontStyle.Bold) : Font;
Font f;
float sz = (cd.Style != null && cd.Style.FontSize > 0f) ? cd.Style.FontSize : this.Font.Size;
FontStyle fs = (cd.Style != null && cd.Style.Bold) ? FontStyle.Bold : FontStyle.Regular;
if (Math.Abs(sz - this.Font.Size) > 0.01f || fs != this.Font.Style)
{
// サイズ/スタイルが既定と違う場合だけ新規フォントを作成
f = new Font(this.Font.FontFamily, sz, fs);
}
else
{
// 既定と同じなら既存を使う(Dispose不要)
f = this.Font;
}
Rectangle textRect = Rectangle.Inflate(mergedRect, -4, -2);
g.DrawString(cd.Text ?? "", f, fore, textRect, sf);
}
g.DrawRectangle(pen, Rectangle.Inflate(mergedRect, -1, -1));
}
}
}
}
public void DumpCell(object _, int r, int c) {
Cell cd;
if (_cells.TryGetValue((r, c), out cd)) {
Debug.WriteLine("[" + r + "," + c + "] text='" + (cd.Text ?? "") + "'");
} else {
Debug.WriteLine("[" + r + "," + c + "] <empty>");
}
}
#endregion
#region ===== 補助 =====
private void EnsureInside(int r, int c) {
if (r < 0 || c < 0 || r >= RowCount || c >= ColumnCount)
throw new ArgumentOutOfRangeException("セル[" + r + "," + c + "] が範囲外です。");
}
private Rectangle GetCellRect(int r, int c) {
if (_colRects.Length == 0 || _rowTops.Length == 0) return Rectangle.Empty;
if (c < 0 || c >= ColumnCount || r < 0 || r >= RowCount) return Rectangle.Empty;
Rectangle col = _colRects[c];
return Rectangle.FromLTRB(col.Left, _rowTops[r], col.Right, _rowTops[r + 1]);
}
private Rectangle GetRowBounds(int row) {
if (_rowTops.Length == 0 || row < 0 || row >= RowCount) return Rectangle.Empty;
return Rectangle.FromLTRB(0, _rowTops[row], ClientSize.Width, _rowTops[row + 1]);
}
private ValueTuple<int, int> GetMergeAnchor(int r, int c) {
foreach (MergedCell m in MergedCells) {
if (r >= m.Row && r < m.Row + m.RowSpan &&
c >= m.Column && c < m.Column + m.ColSpan) {
return new ValueTuple<int, int>(m.Row, m.Column);
}
}
return new ValueTuple<int, int>(r, c);
}
private Rectangle GetMergedRect(int r, int c) {
ValueTuple<int, int> anc = GetMergeAnchor(r, c);
int ar = anc.Item1; int ac = anc.Item2;
if (ar != r || ac != c) return Rectangle.Empty;
MergedCell m = MergedCells.FirstOrDefault(x => x.Row == ar && x.Column == ac);
int rs = Math.Max(1, m != null ? m.RowSpan : 1);
int cs = Math.Max(1, m != null ? m.ColSpan : 1);
Rectangle rect = GetCellRect(ar, ac);
for (int i = 1; i < cs; i++) rect = Rectangle.Union(rect, GetCellRect(ar, ac + i));
for (int j = 1; j < rs; j++) rect = Rectangle.Union(rect, GetCellRect(ar + j, ac));
return rect;
}
private void InvalidateCell(int r, int c) {
Rectangle rc = GetMergedRect(r, c);
if (rc.IsEmpty) rc = GetCellRect(r, c);
if (!rc.IsEmpty) Invalidate(rc);
}
private static StringFormat MakeStringFormat(ContentAlignment align) {
StringFormat sf = new StringFormat(StringFormatFlags.NoWrap);
// 水平
if (align == ContentAlignment.TopLeft || align == ContentAlignment.MiddleLeft || align == ContentAlignment.BottomLeft)
sf.Alignment = StringAlignment.Near;
else if (align == ContentAlignment.TopCenter || align == ContentAlignment.MiddleCenter || align == ContentAlignment.BottomCenter)
sf.Alignment = StringAlignment.Center;
else
sf.Alignment = StringAlignment.Far;
// 垂直
if (align == ContentAlignment.TopLeft || align == ContentAlignment.TopCenter || align == ContentAlignment.TopRight)
sf.LineAlignment = StringAlignment.Near;
else if (align == ContentAlignment.MiddleLeft || align == ContentAlignment.MiddleCenter || align == ContentAlignment.MiddleRight)
sf.LineAlignment = StringAlignment.Center;
else
sf.LineAlignment = StringAlignment.Far;
return sf;
}
#endregion
#region ===== 直接編集(インプレースエディタ) =====
private TextBox _editor;
private int _editRow = -1, _editCol = -1;
private string _editOriginalText = null;
public class CellEditEventArgs : EventArgs {
public int Row { get; }
public int Column { get; }
public string OldText { get; }
public string NewText { get; }
public CellEditEventArgs(int r, int c, string oldT, string newT) { Row = r; Column = c; OldText = oldT; NewText = newT; }
}
public event EventHandler<CellEditEventArgs> CellEdited;
public bool HitTest(Point p, out int row, out int col) {
row = col = -1;
if (_rowTops.Length < 2 || _colRects.Length == 0) return false;
if (p.X < 0 || p.Y < 0 || p.X >= ClientSize.Width || p.Y >= ClientSize.Height) return false;
// 列
for (int c = 0; c < ColumnCount; c++) {
var rc = _colRects[c];
if (p.X >= rc.Left && p.X < rc.Right) { col = c; break; }
}
if (col < 0) return false;
// 行(非表示はスキップ)
for (int r = 0; r < RowCount; r++) {
if (_hiddenRows.Contains(r)) continue;
int top = _rowTops[r], bottom = _rowTops[r + 1];
if (p.Y >= top && p.Y < bottom) { row = r; break; }
}
if (row < 0) return false;
// 結合セル内ならアンカーに寄せる
var anc = GetMergeAnchor(row, col);
row = anc.Item1; col = anc.Item2;
return true;
}
public void BeginEditAt(int row, int col, bool selectAll = true)
{
EnsureInside(row, col);
// 結合セルはアンカーへ
int ar, ac;
if (TryGetMergeAnchor(row, col, out ar, out ac)) { row = ar; col = ac; }
Cell cell = GetCell(row, col);
// ★ 編集禁止なら戻る
if (cell != null && !cell.IsEditable) return;
// 既存Overlayがあるセルはここでは編集スキップ
if (GetControlAt(row, col) != null) return;
// 既存エディタを閉じる
CancelEdit();
_editRow = row; _editCol = col;
_editOriginalText = GetCellText(row, col);
_editor = new TextBox
{
BorderStyle = BorderStyle.FixedSingle,
Font = this.Font,
Text = _editOriginalText ?? "",
Multiline = true,
};
// _editor = new TextBox { ... } の直後
float sz = (cell.Style != null && cell.Style.FontSize > 0f) ? cell.Style.FontSize : this.Font.Size;
FontStyle fs = (cell.Style != null && cell.Style.Bold) ? FontStyle.Bold : FontStyle.Regular;
_editor.Font = (Math.Abs(sz - this.Font.Size) > 0.01f || fs != this.Font.Style)
? new Font(this.Font.FontFamily, sz, fs)
: this.Font;
Rectangle rc = GetMergedRect(row, col);
if (rc.IsEmpty) rc = GetCellRect(row, col);
_editor.Bounds = Rectangle.Inflate(rc, -1, -1);
_editor.Margin = Padding.Empty;
_editor.KeyDown += Editor_KeyDown;
_editor.Leave += Editor_Leave;
Controls.Add(_editor);
_editor.BringToFront();
_editor.Focus();
if (selectAll) _editor.SelectAll();
}
public void CommitEdit() {
if (_editor == null) return;
var newText = _editor.Text;
var oldText = _editOriginalText;
int r = _editRow, c = _editCol;
// 後始末より先にテキスト反映
SetCellText(r, c, newText);
// イベント通知
CellEdited?.Invoke(this, new CellEditEventArgs(r, c, oldText, newText));
TeardownEditor();
}
public void CancelEdit() {
if (_editor == null) return;
TeardownEditor(); // テキストは反映しない
}
private void TeardownEditor() {
if (_editor != null) {
_editor.KeyDown -= Editor_KeyDown;
_editor.Leave -= Editor_Leave;
Controls.Remove(_editor);
_editor.Dispose();
_editor = null;
}
_editRow = _editCol = -1;
_editOriginalText = null;
}
private void Editor_KeyDown(object sender, KeyEventArgs e) {
if (e.KeyCode == Keys.Enter) { e.Handled = true; CommitEdit(); } else if (e.KeyCode == Keys.Escape) { e.Handled = true; CancelEdit(); }
}
private void Editor_Leave(object sender, EventArgs e) {
// フォーカスが外れたら確定(Excel風)
CommitEdit();
}
protected override void OnMouseDoubleClick(MouseEventArgs e) {
base.OnMouseDoubleClick(e);
if (HitTest(e.Location, out int r, out int c)) BeginEditAt(r, c, selectAll: true);
}
protected override void OnKeyDown(KeyEventArgs e) {
base.OnKeyDown(e);
// F2で直前にクリックしたセルを編集、という操作を入れたい場合は
// クリック済みセルを覚える必要があります。簡易版として編集中のみキー処理。
if (_editor != null) return;
if (e.KeyCode == Keys.F2) {
// 直近クリックセルの保持をしない簡易実装:マウス位置で判定(フォーカス中のみ)
var p = PointToClient(MousePosition);
if (HitTest(p, out int r, out int c)) BeginEditAt(r, c, selectAll: true);
}
}
protected override void OnResize(EventArgs e) {
base.OnResize(e);
RecalcLayoutAndInvalidate();
if (_editor != null && _editRow >= 0 && _editCol >= 0) {
var rc = GetMergedRect(_editRow, _editCol);
if (rc.IsEmpty) rc = GetCellRect(_editRow, _editCol);
_editor.Bounds = Rectangle.Inflate(rc, -1, -1);
}
}
#endregion
#region ---------------------- セルのタグ操作 -----------------------
public IReadOnlyDictionary<string, string> GetCellDictionaryReadonly(int row, int col) {
var d = GetCellDictionary(row, col);
return d == null ? null : new System.Collections.ObjectModel.ReadOnlyDictionary<string, string>(d);
}
public string GetCellTag(int row, int col, string key, string defaultValue = null) {
var d = GetCellDictionary(row, col);
if (d != null && key != null && d.TryGetValue(key, out var v)) return v;
return defaultValue;
}
public void SetCellTag(int row, int col, string key, string value) {
EnsureInside(row, col);
var cd = GetCell(row, col);
if (cd.Dict == null) cd.Dict = new Dictionary<string, string>();
if (value == null) cd.Dict.Remove(key);
else cd.Dict[key] = value;
}
public bool RemoveCellTag(int row, int col, string key) {
var d = GetCellDictionary(row, col);
return (d != null) && d.Remove(key);
}
// ====== 辞書ツールチップ ======
private ToolTip _dictTip;
private bool _dictTipInitialized;
private int _tipAutoPop = 8000, _tipInitial = 400, _tipReshow = 200;
private bool _showDictionaryToolTip = true;
private int _hoverRow = -1, _hoverCol = -1; // 直近ホバーセル(アンカー座標)
private readonly string[] _tipPreferredKeys = new[] { "Text", "Id", "ColumnName", "AnalysisRatio", "ToolTip" };
[Browsable(true), Category("Behavior"), Description("辞書の内容をツールチップで表示するかどうか")]
public bool ShowDictionaryToolTip { get => _showDictionaryToolTip; set { _showDictionaryToolTip = value; if (!value) HideDictionaryToolTip(); } }
[Browsable(true), Category("Behavior")]
public int ToolTipAutoPopDelay { get => _tipAutoPop; set { _tipAutoPop = value; ApplyDictTipDelays(); } }
[Browsable(true), Category("Behavior")]
public int ToolTipInitialDelay { get => _tipInitial; set { _tipInitial = value; ApplyDictTipDelays(); } }
[Browsable(true), Category("Behavior")]
public int ToolTipReshowDelay { get => _tipReshow; set { _tipReshow = value; ApplyDictTipDelays(); } }
private void EnsureDictTip() {
if (_dictTipInitialized || !ShowDictionaryToolTip) return;
_dictTip = new ToolTip { IsBalloon = true, UseAnimation = true, UseFading = true, ShowAlways = true };
_dictTipInitialized = true;
ApplyDictTipDelays();
}
private void ApplyDictTipDelays() {
if (_dictTip == null) return;
_dictTip.AutoPopDelay = _tipAutoPop;
_dictTip.InitialDelay = _tipInitial;
_dictTip.ReshowDelay = _tipReshow;
}
private void HideDictionaryToolTip() {
_dictTip?.Hide(this);
_hoverRow = _hoverCol = -1;
}
private static string BuildDictionaryTipText(Dictionary<string, string> dict, IEnumerable<string> preferredKeys) {
if (dict == null || dict.Count == 0) return string.Empty;
var sb = new System.Text.StringBuilder();
// 優先キー(順序固定)
if (preferredKeys != null) {
foreach (var k in preferredKeys) {
if (dict.TryGetValue(k, out var v) && !string.IsNullOrWhiteSpace(v))
sb.AppendLine($"{k} : {v}");
}
}
// 残りのキー(空でないもの、重複除外)
foreach (var kv in dict) {
if (preferredKeys != null && preferredKeys.Contains(kv.Key)) continue;
if (!string.IsNullOrWhiteSpace(kv.Value))
sb.AppendLine($"{kv.Key} : {kv.Value}");
}
return sb.ToString().TrimEnd();
}
protected override void OnMouseLeave(EventArgs e) {
base.OnMouseLeave(e);
HideDictionaryToolTip();
}
// ヒットテスト(表示ON/OFF用):既存 HitTest と同等だが内部用に簡易版
private bool HitTestForTip(Point p, out int row, out int col) {
row = col = -1;
if (p.X < 0 || p.Y < 0 || p.X >= ClientSize.Width || p.Y >= ClientSize.Height) return false;
// 列
for (int c = 0; c < ColumnCount; c++) {
var rc = _colRects[c];
if (p.X >= rc.Left && p.X < rc.Right) { col = c; break; }
}
if (col < 0) return false;
// 行(非表示スキップ)
for (int r = 0; r < RowCount; r++) {
if (_hiddenRows.Contains(r)) continue;
int top = _rowTops[r], bottom = _rowTops[r + 1];
if (p.Y >= top && p.Y < bottom) { row = r; break; }
}
if (row < 0) return false;
// 結合はアンカーへ
var anc = GetMergeAnchor(row, col);
row = anc.Item1; col = anc.Item2;
return true;
}
#endregion
#region ---------------------- 列リサイズ マウスイベント -----------------------
private const int _grip = 4; // つかみ判定のピクセル
private int _resizeCol = -1; // つかみ中の列
private int _resizeStartX; // マウス開始X
private int _resizeStartW; // 開始時の列幅
private int _minColW = 24; // 最小幅
private int HitTestColEdge(int x)
{
if (_colRects == null) return -1;
for (int c = 0; c < ColumnCount - 1; c++)
{
if (_colRects[c].Width <= 0 || _colRects[c + 1].Width <= 0) continue; // ★ 非表示の境界は無視
int edge = _colRects[c].Right;
if (Math.Abs(x - edge) <= _grip) return c;
}
return -1;
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// ===== 1) 列リサイズ中(ドラッグ中)は幅更新のみ =====
if (_resizeCol >= 0)
{
int dx = e.X - _resizeStartX;
// auto列(-1)はドラッグ開始時に固定化
if (ColumnWidths == null || ColumnWidths.Length < ColumnCount)
{
Array.Resize(ref _columnWidths, ColumnCount);
for (int i = 0; i < ColumnCount; i++)
if (_columnWidths[i] == 0) _columnWidths[i] = -1;
}
if (_columnWidths[_resizeCol] < 0)
_columnWidths[_resizeCol] = _colRects[_resizeCol].Width;
_columnWidths[_resizeCol] = Math.Max(_minColW, _resizeStartW + dx);
RecalcLayoutAndInvalidate();
// ドラッグ中はツールチップを抑止
HideDictionaryToolTip();
return;
}
// ===== 2) 列境界の上にいるか(リサイズ準備) =====
var edgeCol = HitTestColEdge(e.X);
if (edgeCol >= 0)
{
Cursor = Cursors.SizeWE;
// リサイズ意図がある時はツールチップ非表示で邪魔しない
HideDictionaryToolTip();
return;
}
Cursor = Cursors.Default;
// ===== 3) 辞書ツールチップ(通常ホバー時のみ) =====
if (!ShowDictionaryToolTip) return;
// レイアウト未計算なら何もしない
if (_rowTops == null || _rowTops.Length < 2 || _colRects == null || _colRects.Length == 0) return;
EnsureDictTip();
// ヒットテスト(結合はアンカーへ寄せる)
int r, c;
if (!HitTestForTip(e.Location, out r, out c))
{
if (_hoverRow != -1 || _hoverCol != -1) HideDictionaryToolTip();
return;
}
// セルが変わった時だけ更新
if (r != _hoverRow || c != _hoverCol)
{
_hoverRow = r; _hoverCol = c;
var cd = _cells.TryGetValue((r, c), out var d) ? d : null;
var tip = BuildDictionaryTipText(cd?.Dict, _tipPreferredKeys);
if (string.IsNullOrWhiteSpace(tip))
{
HideDictionaryToolTip();
return;
}
// 位置はカーソル近く(相対座標)
_dictTip.Show(tip, this, e.Location + new Size(16, 16), _tipAutoPop);
}
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (e.Button == MouseButtons.Left)
{
int edgeCol = HitTestColEdge(e.X);
if (edgeCol >= 0)
{
_resizeCol = edgeCol;
_resizeStartX = e.X;
_resizeStartW = _colRects[_resizeCol].Width;
Capture = true;
}
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
if (_resizeCol >= 0)
{
_resizeCol = -1;
Capture = false;
Cursor = Cursors.Default;
}
}
#endregion ---------------------- 列リサイズ -----------------------
#region ---------------------- 行と列の取得と非表示関数 -----------------------
public void SetRowVisible(int rowIndex, bool visible)
{
if (rowIndex < 0 || rowIndex >= RowCount) return;
if (!visible) _hiddenRows.Add(rowIndex); else _hiddenRows.Remove(rowIndex);
RecalcLayoutAndInvalidate(); // ★ ここを Invalidate → 再計算に
}
public bool IsRowHidden(int rowIndex) { return _hiddenRows.Contains(rowIndex); }
/// <summary>
/// 非表示にしていない行の数を返す
/// </summary>
public int GetTotalVisibleRowCount()
{
return RowCount - _hiddenRows.Count;
}
/// <summary>列の表示/非表示を切り替える</summary>
public void SetColumnVisible(int colIndex, bool visible)
{
if (colIndex < 0 || colIndex >= ColumnCount) return;
if (!visible) _hiddenCols.Add(colIndex); else _hiddenCols.Remove(colIndex);
RecalcLayoutAndInvalidate();
}
/// <summary>列が非表示かどうか</summary>
public bool IsColumnHidden(int colIndex) => _hiddenCols.Contains(colIndex);
/// <summary>非表示でない列の数</summary>
public int GetTotalVisibleColumnCount() => ColumnCount - _hiddenCols.Count;
#endregion
}
}