2026-06-08 14:31:31 +07:00
|
|
|
|
namespace QWERTYkez.ExcelProcessor;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Внутренняя реализация <see cref="ICell"/>.
|
|
|
|
|
|
/// </summary>
|
2026-06-17 09:33:21 +07:00
|
|
|
|
internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col) : ICell
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Возвращает эффективный стиль ячейки с учётом наследования от строки и столбца.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public CellStyle? GetCellStyle()
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
CellStyle? cellStyle = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Пытаемся получить стиль самой ячейки
|
|
|
|
|
|
if (cell?.StyleIndex?.Value is uint cellStyleIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
cellStyle = writer.GetCellStyle(cellStyleIndex);
|
|
|
|
|
|
if (cellStyle != null && !cellStyle.IsEmpty())
|
|
|
|
|
|
return cellStyle; // если у ячейки есть свой стиль, он имеет наивысший приоритет
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Стиль строки
|
|
|
|
|
|
var sheetData = sheet.GetSheetData();
|
|
|
|
|
|
var rowElement = FindRowElement(sheetData, row);
|
|
|
|
|
|
if (rowElement?.StyleIndex?.Value is uint rowStyleIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rowStyle = writer.GetCellStyle(rowStyleIndex);
|
|
|
|
|
|
if (rowStyle != null && !rowStyle.IsEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
// Если у ячейки нет стиля, возвращаем стиль строки
|
|
|
|
|
|
if (cellStyle == null)
|
|
|
|
|
|
return rowStyle;
|
|
|
|
|
|
// Иначе объединяем: стиль ячейки имеет приоритет, но некоторые свойства могут быть не заданы
|
|
|
|
|
|
// (например, если у ячейки только Border, а у строки Fill, то в результате будет и Border, и Fill)
|
|
|
|
|
|
// Объединяем: сначала берём стиль строки, затем накладываем стиль ячейки (перекрывая)
|
|
|
|
|
|
return rowStyle.Merge(cellStyle);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Стиль столбца
|
|
|
|
|
|
if (col > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var columnElement = ExcelColumn.GetColumnElementInternal(sheet, col);
|
|
|
|
|
|
if (columnElement?.Style?.Value is uint colStyleIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
var colStyle = writer.GetCellStyle(colStyleIndex);
|
|
|
|
|
|
if (colStyle != null && !colStyle.IsEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
// Если нет стиля ячейки и строки, возвращаем стиль столбца
|
|
|
|
|
|
if (cellStyle == null)
|
|
|
|
|
|
return colStyle;
|
|
|
|
|
|
// Иначе объединяем: стиль ячейки + стиль строки (если есть) + стиль столбца
|
|
|
|
|
|
// Сначала объединяем стиль строки и столбца, затем накладываем стиль ячейки
|
|
|
|
|
|
// Но проще: берём стиль ячейки (если есть) и объединяем со стилем столбца
|
|
|
|
|
|
return colStyle.Merge(cellStyle);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Если ничего нет, возвращаем null
|
|
|
|
|
|
return cellStyle; // может быть null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void ApplyStyle(CellStyle style)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
int styleIndex = writer.GetOrCreateStyleId(style);
|
|
|
|
|
|
cell.StyleIndex = (uint)styleIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-17 09:33:21 +07:00
|
|
|
|
// Кэш фрагментов богатого текста (только для InlineString)
|
2026-06-19 15:06:40 +07:00
|
|
|
|
List<IRun>? _runsCache;
|
|
|
|
|
|
bool _cacheValid;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
2026-06-17 09:33:21 +07:00
|
|
|
|
// ---- Реализация ICell (новые методы) ----
|
|
|
|
|
|
|
|
|
|
|
|
public int RunsCount
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
return _runsCache?.Count ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IEnumerable<IRun> GetRuns()
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
return _runsCache ?? Enumerable.Empty<IRun>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IRun? GetRunAt(int index)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
if (_runsCache is null || index < 0 || index >= _runsCache.Count)
|
|
|
|
|
|
return null;
|
|
|
|
|
|
return _runsCache[index];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryGetRunAt(int index, out IRun run)
|
|
|
|
|
|
{
|
|
|
|
|
|
run = GetRunAt(index)!;
|
|
|
|
|
|
return run != null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IRun? First()
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
return _runsCache?.FirstOrDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryGetFirst(out IRun run)
|
|
|
|
|
|
{
|
|
|
|
|
|
run = First()!;
|
|
|
|
|
|
return run != null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IRun? Last()
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
return _runsCache?.LastOrDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryGetLast(out IRun run)
|
|
|
|
|
|
{
|
|
|
|
|
|
run = Last()!;
|
|
|
|
|
|
return run != null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryRemoveRun(IRun run)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (run is null) return false;
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
if (_runsCache is null) return false;
|
|
|
|
|
|
bool removed = _runsCache.Remove(run);
|
|
|
|
|
|
if (removed)
|
|
|
|
|
|
{
|
|
|
|
|
|
_cacheValid = true;
|
|
|
|
|
|
UpdateCellFromCache();
|
|
|
|
|
|
}
|
|
|
|
|
|
return removed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryRemoveRun(int index)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
if (_runsCache is null || index < 0 || index >= _runsCache.Count)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
_runsCache.RemoveAt(index);
|
|
|
|
|
|
_cacheValid = true;
|
|
|
|
|
|
UpdateCellFromCache();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryRemoveRun(int index, out IRun? removed)
|
|
|
|
|
|
{
|
|
|
|
|
|
removed = GetRunAt(index);
|
|
|
|
|
|
if (removed is null) return false;
|
|
|
|
|
|
return TryRemoveRun(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
public ICell Break()
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
if (_runsCache != null && _runsCache.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var lastRun = _runsCache[_runsCache.Count - 1];
|
|
|
|
|
|
lastRun.Text += "\n";
|
|
|
|
|
|
EnsureWrapTextEnabled();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Run("\n", null);
|
|
|
|
|
|
}
|
|
|
|
|
|
_cacheValid = true;
|
|
|
|
|
|
UpdateCellFromCache();
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell Run(string text, RunFormat? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(text)) return this;
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
_runsCache ??= [];
|
|
|
|
|
|
_runsCache.Add(new ExcelRun { Text = text, Format = format });
|
|
|
|
|
|
_cacheValid = true;
|
|
|
|
|
|
UpdateCellFromCache();
|
|
|
|
|
|
|
|
|
|
|
|
if (text.Contains('\n'))
|
|
|
|
|
|
EnsureWrapTextEnabled();
|
|
|
|
|
|
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void EnsureWrapTextEnabled()
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
if (currentStyle.Align is null || currentStyle.Align.Value.WrapText != true)
|
|
|
|
|
|
{
|
|
|
|
|
|
var align = (currentStyle.Align ?? new CellAlign()) with { WrapText = true };
|
|
|
|
|
|
Set(align); // вызовет объединение через TryMerge
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell RunBreak(string text, RunFormat? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
Run(text, format);
|
|
|
|
|
|
Break();
|
|
|
|
|
|
return this;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 09:33:21 +07:00
|
|
|
|
public ICell Sub(string text, RunFormat? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var subFormat = format is { } fmt
|
|
|
|
|
|
? new RunFormat
|
|
|
|
|
|
{
|
|
|
|
|
|
IsBold = fmt.IsBold,
|
|
|
|
|
|
IsItalic = fmt.IsItalic,
|
|
|
|
|
|
Underline = fmt.Underline,
|
|
|
|
|
|
IsStrike = fmt.IsStrike,
|
|
|
|
|
|
Color = fmt.Color,
|
|
|
|
|
|
FontSize = fmt.FontSize,
|
|
|
|
|
|
FontFamily = fmt.FontFamily,
|
|
|
|
|
|
Vertical = VerticalTextRunAlignment.Subscript
|
|
|
|
|
|
}
|
|
|
|
|
|
: new RunFormat { Vertical = VerticalTextRunAlignment.Subscript };
|
|
|
|
|
|
return Run(text, subFormat);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell Sup(string text, RunFormat? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var supFormat = format is { } fmt
|
|
|
|
|
|
? new RunFormat
|
|
|
|
|
|
{
|
|
|
|
|
|
IsBold = fmt.IsBold,
|
|
|
|
|
|
IsItalic = fmt.IsItalic,
|
|
|
|
|
|
Underline = fmt.Underline,
|
|
|
|
|
|
IsStrike = fmt.IsStrike,
|
|
|
|
|
|
Color = fmt.Color,
|
|
|
|
|
|
FontSize = fmt.FontSize,
|
|
|
|
|
|
FontFamily = fmt.FontFamily,
|
|
|
|
|
|
Vertical = VerticalTextRunAlignment.Superscript
|
|
|
|
|
|
}
|
|
|
|
|
|
: new RunFormat { Vertical = VerticalTextRunAlignment.Superscript };
|
|
|
|
|
|
return Run(text, supFormat);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryInsertRun(int index, string text, RunFormat? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (index < 0 || string.IsNullOrEmpty(text)) return false;
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
_runsCache ??= [];
|
|
|
|
|
|
if (index > _runsCache.Count) return false;
|
|
|
|
|
|
_runsCache.Insert(index, new ExcelRun { Text = text, Format = format });
|
|
|
|
|
|
_cacheValid = true;
|
|
|
|
|
|
UpdateCellFromCache();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryInsertRunBreak(int index, string text, RunFormat? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!TryInsertRun(index, text, format)) return false;
|
|
|
|
|
|
return TryInsertRun(index + 1, "\n", null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryInsertSub(int index, string text, RunFormat? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var subFormat = format is { } fmt
|
|
|
|
|
|
? new RunFormat
|
|
|
|
|
|
{
|
|
|
|
|
|
IsBold = fmt.IsBold,
|
|
|
|
|
|
IsItalic = fmt.IsItalic,
|
|
|
|
|
|
Underline = fmt.Underline,
|
|
|
|
|
|
IsStrike = fmt.IsStrike,
|
|
|
|
|
|
Color = fmt.Color,
|
|
|
|
|
|
FontSize = fmt.FontSize,
|
|
|
|
|
|
FontFamily = fmt.FontFamily,
|
|
|
|
|
|
Vertical = VerticalTextRunAlignment.Subscript
|
|
|
|
|
|
}
|
|
|
|
|
|
: new RunFormat { Vertical = VerticalTextRunAlignment.Subscript };
|
|
|
|
|
|
return TryInsertRun(index, text, subFormat);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryInsertSup(int index, string text, RunFormat? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var supFormat = format is { } fmt
|
|
|
|
|
|
? new RunFormat
|
|
|
|
|
|
{
|
|
|
|
|
|
IsBold = fmt.IsBold,
|
|
|
|
|
|
IsItalic = fmt.IsItalic,
|
|
|
|
|
|
Underline = fmt.Underline,
|
|
|
|
|
|
IsStrike = fmt.IsStrike,
|
|
|
|
|
|
Color = fmt.Color,
|
|
|
|
|
|
FontSize = fmt.FontSize,
|
|
|
|
|
|
FontFamily = fmt.FontFamily,
|
|
|
|
|
|
Vertical = VerticalTextRunAlignment.Superscript
|
|
|
|
|
|
}
|
|
|
|
|
|
: new RunFormat { Vertical = VerticalTextRunAlignment.Superscript };
|
|
|
|
|
|
return TryInsertRun(index, text, supFormat);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void ApplyFormatToAllRuns(RunFormat format)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureCacheValid();
|
|
|
|
|
|
if (_runsCache is null || _runsCache.Count == 0) return;
|
|
|
|
|
|
foreach (var run in _runsCache)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (run is ExcelRun xRun)
|
|
|
|
|
|
{
|
|
|
|
|
|
var baseFmt = xRun.Format ?? new RunFormat();
|
|
|
|
|
|
xRun.Format = MergeRunFormat(baseFmt, format);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_cacheValid = true;
|
|
|
|
|
|
UpdateCellFromCache();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell ClearRuns()
|
|
|
|
|
|
{
|
|
|
|
|
|
_runsCache = null;
|
|
|
|
|
|
_cacheValid = true;
|
|
|
|
|
|
UpdateCellFromCache(); // устанавливаем пустой InlineString
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Существующие методы (частично изменены) ----
|
|
|
|
|
|
|
|
|
|
|
|
// (здесь остаются все старые методы: IsMerged, GetMergedRange, MoveTo, Height, Width, IsNumber, и т.д.)
|
|
|
|
|
|
// Они не меняются, за исключением методов Set и ClearContent, которые должны сбрасывать кэш.
|
|
|
|
|
|
|
|
|
|
|
|
// Пример: метод Set(string) - сбрасываем кэш и устанавливаем обычный текст
|
|
|
|
|
|
public ICell Set(string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (value == null) return this;
|
|
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
// Сбрасываем кэш
|
|
|
|
|
|
_runsCache = null;
|
|
|
|
|
|
_cacheValid = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (double.TryParse(value, out double num))
|
|
|
|
|
|
{
|
|
|
|
|
|
cell.DataType = CellValues.Number;
|
|
|
|
|
|
cell.CellValue = new CellValue(num.ToString(CultureInfo.InvariantCulture));
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
int idx = writer.GetOrAddSharedString(value);
|
|
|
|
|
|
cell.DataType = CellValues.SharedString;
|
|
|
|
|
|
cell.CellValue = new CellValue(idx.ToString());
|
|
|
|
|
|
}
|
|
|
|
|
|
// Удаляем InlineString, если был
|
|
|
|
|
|
cell.InlineString = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Аналогично для Set(bool), Set(числа), Set(DateTime) - нужно сбрасывать кэш и удалять InlineString.
|
|
|
|
|
|
// Для краткости приведу только один метод, остальные аналогично.
|
|
|
|
|
|
|
|
|
|
|
|
// Метод ClearContent - сбрасываем кэш
|
|
|
|
|
|
public void ClearContent()
|
|
|
|
|
|
{
|
|
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
cell.CellValue = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
cell.InlineString = null;
|
|
|
|
|
|
cell.DataType = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
_runsCache = null;
|
|
|
|
|
|
_cacheValid = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Метод ClearFormat не трогает текст, поэтому кэш остаётся валидным (если был).
|
|
|
|
|
|
// Но если стиль меняется, кэш не сбрасываем.
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Приватные методы для работы с кэшем ----
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
void EnsureCacheValid()
|
2026-06-17 09:33:21 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (_cacheValid) return;
|
|
|
|
|
|
lock (writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_cacheValid) return;
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
_runsCache = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (cell != null && cell.DataType?.Value == CellValues.InlineString && cell.InlineString != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Читаем InlineString
|
|
|
|
|
|
foreach (var run in cell.InlineString.Elements<Run>())
|
|
|
|
|
|
{
|
|
|
|
|
|
string text = run.GetFirstChild<Text>()?.Text ?? string.Empty;
|
|
|
|
|
|
RunFormat? format = RunFormat.FromRunProperties(run.RunProperties!);
|
|
|
|
|
|
_runsCache.Add(new ExcelRun { Text = text, Format = format });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (cell != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Преобразуем содержимое в один Run (без форматирования)
|
|
|
|
|
|
string text = GetStringFromCell(cell);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(text))
|
|
|
|
|
|
_runsCache.Add(new ExcelRun { Text = text, Format = null });
|
|
|
|
|
|
}
|
|
|
|
|
|
// Если ячейка пуста, кэш остаётся пустым
|
|
|
|
|
|
_cacheValid = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
string GetStringFromCell(Cell cell)
|
2026-06-17 09:33:21 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (cell == null) return string.Empty;
|
|
|
|
|
|
if (cell.DataType?.Value == CellValues.SharedString && cell.CellValue != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return writer.GetSharedString(uint.Parse(cell.CellValue.Text));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cell.CellValue != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return cell.CellValue.Text;
|
|
|
|
|
|
}
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
void UpdateCellFromCache()
|
2026-06-17 09:33:21 +07:00
|
|
|
|
{
|
|
|
|
|
|
lock (writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
if (_runsCache == null || _runsCache.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Устанавливаем пустой InlineString
|
|
|
|
|
|
cell.InlineString = new InlineString();
|
|
|
|
|
|
cell.DataType = CellValues.InlineString;
|
|
|
|
|
|
cell.CellValue = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
cell.InlineString = BuildInlineStringFromCache();
|
|
|
|
|
|
cell.DataType = CellValues.InlineString;
|
|
|
|
|
|
cell.CellValue = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
InlineString BuildInlineStringFromCache()
|
|
|
|
|
|
{
|
|
|
|
|
|
var inline = new InlineString();
|
|
|
|
|
|
if (_runsCache == null) return inline;
|
|
|
|
|
|
foreach (var run in _runsCache)
|
|
|
|
|
|
{
|
|
|
|
|
|
var runElement = new Run();
|
|
|
|
|
|
var textElement = new Text(run.Text);
|
|
|
|
|
|
// Устанавливаем preserve, если текст содержит пробелы или переносы
|
|
|
|
|
|
if (run.Text.Any(c => char.IsWhiteSpace(c)))
|
|
|
|
|
|
textElement.Space = SpaceProcessingModeValues.Preserve;
|
|
|
|
|
|
runElement.Append(textElement);
|
|
|
|
|
|
|
|
|
|
|
|
if (run.Format is { } fmt)
|
|
|
|
|
|
{
|
|
|
|
|
|
var rPr = new RunProperties();
|
|
|
|
|
|
if (fmt.IsBold == true)
|
|
|
|
|
|
rPr.Append(new Bold());
|
|
|
|
|
|
if (fmt.IsItalic == true)
|
|
|
|
|
|
rPr.Append(new Italic());
|
|
|
|
|
|
if (fmt.Underline.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
rPr.Append(new Underline
|
|
|
|
|
|
{
|
|
|
|
|
|
Val = fmt.Underline.Value switch
|
|
|
|
|
|
{
|
|
|
|
|
|
UnderlineStyle.Single => UnderlineValues.Single,
|
|
|
|
|
|
UnderlineStyle.Double => UnderlineValues.Double,
|
|
|
|
|
|
UnderlineStyle.SingleAccounting => UnderlineValues.SingleAccounting,
|
|
|
|
|
|
UnderlineStyle.DoubleAccounting => UnderlineValues.DoubleAccounting,
|
|
|
|
|
|
_ => throw new NotImplementedException(),
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fmt.IsStrike == true)
|
|
|
|
|
|
rPr.Append(new Strike());
|
|
|
|
|
|
if (fmt.TryGetExcelColor(out var c))
|
|
|
|
|
|
rPr.Append(c);
|
|
|
|
|
|
if (fmt.FontSize.HasValue)
|
|
|
|
|
|
rPr.Append(new FontSize { Val = fmt.FontSize.Value });
|
|
|
|
|
|
if (!string.IsNullOrEmpty(fmt.FontFamily))
|
|
|
|
|
|
rPr.Append(new RunFont { Val = fmt.FontFamily });
|
|
|
|
|
|
if (fmt.Vertical.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var vertAlign = new VerticalTextAlignment
|
|
|
|
|
|
{
|
|
|
|
|
|
Val = fmt.Vertical.Value == VerticalTextRunAlignment.Superscript
|
|
|
|
|
|
? VerticalAlignmentRunValues.Superscript
|
|
|
|
|
|
: VerticalAlignmentRunValues.Subscript
|
|
|
|
|
|
};
|
|
|
|
|
|
rPr.Append(vertAlign);
|
|
|
|
|
|
}
|
|
|
|
|
|
runElement.RunProperties = rPr;
|
|
|
|
|
|
}
|
|
|
|
|
|
inline.Append(runElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
return inline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static RunFormat MergeRunFormat(RunFormat baseFmt, RunFormat overlay)
|
2026-06-17 09:33:21 +07:00
|
|
|
|
{
|
|
|
|
|
|
return new RunFormat
|
|
|
|
|
|
{
|
|
|
|
|
|
IsBold = overlay.IsBold ?? baseFmt.IsBold,
|
|
|
|
|
|
IsItalic = overlay.IsItalic ?? baseFmt.IsItalic,
|
|
|
|
|
|
Underline = overlay.Underline ?? baseFmt.Underline,
|
|
|
|
|
|
IsStrike = overlay.IsStrike ?? baseFmt.IsStrike,
|
|
|
|
|
|
Color = overlay.Color ?? baseFmt.Color,
|
|
|
|
|
|
FontSize = overlay.FontSize ?? baseFmt.FontSize,
|
|
|
|
|
|
FontFamily = overlay.FontFamily ?? baseFmt.FontFamily,
|
|
|
|
|
|
Vertical = overlay.Vertical ?? baseFmt.Vertical
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-05 15:58:03 +07:00
|
|
|
|
public bool IsMerged
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
var range = GetMergedRange();
|
|
|
|
|
|
return range != null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IRange? GetMergedRange()
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
var mergeCells = GetMergeCells();
|
|
|
|
|
|
if (mergeCells == null) return null;
|
|
|
|
|
|
foreach (var mergeCell in mergeCells.Elements<MergeCell>())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var range))
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
if (row >= range.RowStart && row <= range.RowEnd &&
|
|
|
|
|
|
col >= range.ColStart && col <= range.ColEnd)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
return range;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryGetMergedRange(IRange range)
|
|
|
|
|
|
{
|
|
|
|
|
|
var merged = GetMergedRange();
|
|
|
|
|
|
if (merged == null) return false;
|
|
|
|
|
|
return merged.Equals(range);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 09:33:21 +07:00
|
|
|
|
public uint Row => row;
|
|
|
|
|
|
public uint Col => col;
|
|
|
|
|
|
public string ColLetter => CellAddressHelper.ColumnIndexToLetter(col);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
|
|
|
|
|
public ICell MoveTo(uint rowIndex, uint colIndex)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
if (rowIndex == row && colIndex == col) return this;
|
|
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
// Вырезать-вставить: копируем данные в новую позицию, очищаем старую
|
|
|
|
|
|
var srcCell = GetCellElement();
|
|
|
|
|
|
if (srcCell != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cloned = (Cell)srcCell.CloneNode(true);
|
|
|
|
|
|
InsertCellAt(rowIndex, colIndex, cloned);
|
|
|
|
|
|
srcCell.Remove();
|
|
|
|
|
|
}
|
2026-06-17 09:33:21 +07:00
|
|
|
|
row = rowIndex;
|
|
|
|
|
|
col = colIndex;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell MoveTo(uint rowIndex, string colIndex) => MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex));
|
|
|
|
|
|
|
|
|
|
|
|
public RowHeight Height
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
get => ExcelRow.GetHeight(writer, sheet, row);
|
|
|
|
|
|
set => ExcelRow.SetHeight(writer, sheet, row, value);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ColumnWidth Width
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
get => ExcelColumn.GetWidth(writer, sheet, col);
|
|
|
|
|
|
set => ExcelColumn.SetWidth(writer, sheet, col, value);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsNumber
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell == null) return false;
|
|
|
|
|
|
if (cell.DataType != null && cell.DataType.Value == CellValues.Number) return true;
|
|
|
|
|
|
// Если тип не указан, но значение число – Excel считает числом
|
|
|
|
|
|
if (cell.DataType == null && cell.CellValue != null && double.TryParse(cell.CellValue.Text, out _)) return true;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsBoolean
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
return cell != null && cell.DataType != null && cell.DataType.Value == CellValues.Boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsError
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
return cell != null && cell.DataType != null && cell.DataType.Value == CellValues.Error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsDate
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!IsNumber) return false;
|
|
|
|
|
|
var format = GetNumberFormat();
|
|
|
|
|
|
if (format == null) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Встроенный формат: проверяем по ID
|
|
|
|
|
|
if (format.Id.HasValue && format.Id.Value < 164)
|
|
|
|
|
|
{
|
|
|
|
|
|
int id = format.Id.Value;
|
|
|
|
|
|
return (id >= 14 && id <= 22) || // даты
|
|
|
|
|
|
(id >= 27 && id <= 36) || // время и дата-время
|
|
|
|
|
|
(id >= 45 && id <= 47); // другие форматы времени
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Пользовательский формат: ищем символы даты/времени
|
|
|
|
|
|
string code = format.Format?.ToLowerInvariant() ?? string.Empty;
|
|
|
|
|
|
bool isDate = code.Contains('d') && code.Contains('m') && code.Contains('y');
|
|
|
|
|
|
bool isTime = code.Contains('h') && code.Contains('m') && code.Contains('s');
|
|
|
|
|
|
return isDate || isTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsLocked
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
|
|
|
|
|
return false;
|
2026-06-17 09:33:21 +07:00
|
|
|
|
return writer.IsCellLocked(styleIndex);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
public bool HasFormula => GetCellElement()?.CellFormula != null;
|
|
|
|
|
|
|
|
|
|
|
|
public string GetString()
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell == null) return string.Empty;
|
|
|
|
|
|
if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Общая таблица строк
|
|
|
|
|
|
if (uint.TryParse(cell.CellValue?.Text, out uint idx))
|
|
|
|
|
|
return GetSharedString(idx);
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cell.DataType != null && cell.DataType.Value == CellValues.InlineString)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Rich text – извлекаем весь текст
|
|
|
|
|
|
return ExtractTextFromInlineString(cell.InlineString);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cell.DataType != null && cell.DataType.Value == CellValues.Boolean)
|
|
|
|
|
|
return cell.CellValue?.Text ?? "FALSE";
|
|
|
|
|
|
if (cell.DataType != null && cell.DataType.Value == CellValues.Error)
|
|
|
|
|
|
return cell.CellValue?.Text ?? "#NULL!";
|
|
|
|
|
|
// Число или общий тип
|
|
|
|
|
|
return cell.CellValue?.Text ?? string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public NumberFormatPattern? GetNumberFormat()
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
|
|
|
|
|
return null;
|
2026-06-17 09:33:21 +07:00
|
|
|
|
return writer.GetNumberFormat(styleIndex);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public CellAlign GetCellAlign()
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
|
|
|
|
|
return default;
|
2026-06-17 09:33:21 +07:00
|
|
|
|
return writer.GetCellAlign(styleIndex);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public CellBorder GetCellBorder()
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
|
|
|
|
|
return default;
|
2026-06-17 09:33:21 +07:00
|
|
|
|
return writer.GetCellBorder(styleIndex);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public CellFill GetCellFill()
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
|
|
|
|
|
return default;
|
2026-06-17 09:33:21 +07:00
|
|
|
|
return writer.GetCellFill(styleIndex);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public CellFont GetCellFont()
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell?.StyleIndex?.Value is not uint styleIndex)
|
|
|
|
|
|
return default;
|
2026-06-17 09:33:21 +07:00
|
|
|
|
return writer.GetCellFont(styleIndex);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryGetBoolean(out bool value)
|
|
|
|
|
|
{
|
|
|
|
|
|
value = false;
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell == null) return false;
|
|
|
|
|
|
if (cell.DataType != null && cell.DataType.Value == CellValues.Boolean)
|
|
|
|
|
|
{
|
|
|
|
|
|
string txt = cell.CellValue?.Text ?? "0";
|
|
|
|
|
|
value = txt == "1" || txt.Equals("true", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public bool? GetBoolean() => TryGetBoolean(out bool v) ? v : null;
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryGetDate(out DateTime value)
|
|
|
|
|
|
{
|
|
|
|
|
|
value = default;
|
|
|
|
|
|
if (!IsNumber) return false;
|
|
|
|
|
|
if (TryGetNumber(out double num))
|
|
|
|
|
|
{
|
|
|
|
|
|
// Excel даты: 1 января 1900 = 1, 1 января 1904 = 0 в 1904 системе
|
|
|
|
|
|
// Предполагаем 1900 систему
|
|
|
|
|
|
value = DateTime.FromOADate(num);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public DateTime? TryGetDate() => TryGetDate(out DateTime d) ? d : null;
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryGetNumber(out double value)
|
|
|
|
|
|
{
|
|
|
|
|
|
value = 0;
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell == null) return false;
|
|
|
|
|
|
if (cell.DataType != null && cell.DataType.Value == CellValues.Number)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (cell.CellValue != null && double.TryParse(cell.CellValue.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (cell.DataType == null && cell.CellValue != null && double.TryParse(cell.CellValue.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
|
|
|
|
|
|
return true;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public double? TryGetNumber() => TryGetNumber(out double v) ? v : null;
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
public bool TrySetFormula(string formula, NumberFormatPattern? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrEmpty(formula)) return false;
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
cell.CellFormula = new CellFormula(formula);
|
|
|
|
|
|
cell.DataType = null; // формула сама определяет тип
|
2026-06-19 15:06:40 +07:00
|
|
|
|
if (format != null) Set(cell, format);
|
|
|
|
|
|
return true;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
public ICell SetFormula(string formula, NumberFormatPattern? format = null)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
if (!TrySetFormula(formula, format))
|
2026-06-05 15:58:03 +07:00
|
|
|
|
throw new InvalidOperationException("Failed to set formula");
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell Set(NumberFormatPattern format)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
if (currentStyle.TryMerge(format, out var newStyle))
|
|
|
|
|
|
ApplyStyle(newStyle);
|
|
|
|
|
|
return this;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
2026-06-19 15:06:40 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell Set(Cell cell, NumberFormatPattern format)
|
|
|
|
|
|
{
|
|
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
if (currentStyle.TryMerge(format, out var newStyle))
|
|
|
|
|
|
cell.StyleIndex = (uint)writer.GetOrCreateStyleId(newStyle);
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellAlign align)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
if (currentStyle.TryMerge(align, out var newStyle))
|
|
|
|
|
|
ApplyStyle(newStyle);
|
|
|
|
|
|
return this;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellBorder border)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
// 1. Применяем границу к текущей ячейке (объединяя с существующей)
|
|
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
CellBorder mergedBorder;
|
|
|
|
|
|
bool changed;
|
|
|
|
|
|
if (currentStyle.Border is { } currBorder)
|
|
|
|
|
|
changed = currBorder.TryMerge(border, out mergedBorder);
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
mergedBorder = border;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!changed)
|
|
|
|
|
|
return this;
|
|
|
|
|
|
|
|
|
|
|
|
ApplyStyle(currentStyle with { Border = mergedBorder });
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Каскадное обновление соседей
|
|
|
|
|
|
// Для каждой стороны, которая была установлена (не null), очищаем соответствующую сторону у соседа
|
|
|
|
|
|
if (border.TopBorder.HasValue)
|
|
|
|
|
|
ClearNeighborBorder(-1, 0, b => b with { BottomBorder = null });
|
|
|
|
|
|
|
|
|
|
|
|
if (border.BottomBorder.HasValue)
|
|
|
|
|
|
ClearNeighborBorder(1, 0, b => b with { TopBorder = null });
|
|
|
|
|
|
|
|
|
|
|
|
if (border.LeftBorder.HasValue)
|
|
|
|
|
|
ClearNeighborBorder(0, -1, b => b with { RightBorder = null });
|
|
|
|
|
|
|
|
|
|
|
|
if (border.RightBorder.HasValue)
|
|
|
|
|
|
ClearNeighborBorder(0, 1, b => b with { LeftBorder = null });
|
|
|
|
|
|
|
|
|
|
|
|
// Диагональные границы не влияют на соседей, их не очищаем
|
|
|
|
|
|
|
2026-06-05 15:58:03 +07:00
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellFill fill)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
if (currentStyle.TryMerge(fill, out var newStyle))
|
|
|
|
|
|
ApplyStyle(newStyle);
|
|
|
|
|
|
return this;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellFont font)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
if (currentStyle.TryMerge(font, out var newStyle))
|
|
|
|
|
|
ApplyStyle(newStyle);
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellStyle style)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (style is null) return this;
|
|
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
if (currentStyle.TryMerge(style, out style))
|
|
|
|
|
|
ApplyStyle(style);
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void ClearNeighborBorder(int rowOffset, int colOffset, Func<CellBorder, CellBorder> clearFunc)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighbor = GetNeighbor(rowOffset, colOffset);
|
|
|
|
|
|
if (neighbor is not { } neighbr)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
var neighborBorder = neighbr.GetCellBorder();
|
|
|
|
|
|
var newBorder = clearFunc(neighborBorder);
|
|
|
|
|
|
// Если после очистки граница изменилась, применяем изолированно
|
|
|
|
|
|
if (!neighborBorder.Equals(newBorder))
|
|
|
|
|
|
neighbr.SetBorderIsolate(newBorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
internal void SetBorderIsolate(CellBorder border)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentStyle = GetCellStyle() ?? new CellStyle();
|
|
|
|
|
|
if (currentStyle.TryMerge(border, out var newStyle))
|
|
|
|
|
|
ApplyStyle(newStyle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Возвращает соседнюю ячейку по указанному смещению.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="cell">Исходная ячейка.</param>
|
|
|
|
|
|
/// <param name="rowOffset">Смещение по строкам (положительное – вниз).</param>
|
|
|
|
|
|
/// <param name="colOffset">Смещение по столбцам (положительное – вправо).</param>
|
|
|
|
|
|
/// <returns>Соседняя ячейка, или null, если она выходит за пределы листа.</returns>
|
|
|
|
|
|
public ExcelCell? GetNeighbor(int rowOffset, int colOffset)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Проверяем, что смещение не равно нулю и координаты не выходят за допустимые пределы (хотя мы не знаем границ листа)
|
|
|
|
|
|
if (rowOffset == 0 && colOffset == 0) return null;
|
|
|
|
|
|
int newRow = (int)row + rowOffset;
|
|
|
|
|
|
int newCol = (int)col + colOffset;
|
|
|
|
|
|
if (newRow < 1 || newCol < 1) return null;
|
|
|
|
|
|
// Excel допускает до 1048576 строк и 16384 столбцов (но мы не будем жестко ограничивать)
|
|
|
|
|
|
// Просто создаём объект ячейки, даже если она не существует физически.
|
|
|
|
|
|
return new ExcelCell(writer, sheet, (uint)newRow, (uint)newCol);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal ICell SetBorderOverride(CellBorder border)
|
|
|
|
|
|
{
|
|
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Определяем, какие стороны заданы в border
|
|
|
|
|
|
bool hasTop = border.TopBorder.HasValue;
|
|
|
|
|
|
bool hasBottom = border.BottomBorder.HasValue;
|
|
|
|
|
|
bool hasLeft = border.LeftBorder.HasValue;
|
|
|
|
|
|
bool hasRight = border.RightBorder.HasValue;
|
|
|
|
|
|
|
|
|
|
|
|
// Если какая-то сторона задана, то для соседней ячейки на этой стороне мы должны очистить противоположную сторону.
|
|
|
|
|
|
// Например, если мы устанавливаем верхнюю границу у текущей ячейки, то у ячейки сверху нужно очистить нижнюю границу.
|
|
|
|
|
|
// Аналогично для остальных сторон.
|
|
|
|
|
|
|
|
|
|
|
|
if (hasTop)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighbor = GetNeighbor(-1, 0);
|
|
|
|
|
|
if (neighbor != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighborBorder = neighbor.GetCellBorder();
|
|
|
|
|
|
if (neighborBorder.BottomBorder.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var newNeighborBorder = neighborBorder with { BottomBorder = null };
|
|
|
|
|
|
// Применяем только границы, не затрагивая другие аспекты стиля
|
|
|
|
|
|
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hasBottom)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighbor = GetNeighbor(1, 0);
|
|
|
|
|
|
if (neighbor != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighborBorder = neighbor.GetCellBorder();
|
|
|
|
|
|
if (neighborBorder.TopBorder.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var newNeighborBorder = neighborBorder with { TopBorder = null };
|
|
|
|
|
|
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hasLeft)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighbor = GetNeighbor(0, -1);
|
|
|
|
|
|
if (neighbor != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighborBorder = neighbor.GetCellBorder();
|
|
|
|
|
|
if (neighborBorder.RightBorder.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var newNeighborBorder = neighborBorder with { RightBorder = null };
|
|
|
|
|
|
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hasRight)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighbor = GetNeighbor(0, 1);
|
|
|
|
|
|
if (neighbor != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var neighborBorder = neighbor.GetCellBorder();
|
|
|
|
|
|
if (neighborBorder.LeftBorder.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var newNeighborBorder = neighborBorder with { LeftBorder = null };
|
|
|
|
|
|
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Теперь устанавливаем границу для текущей ячейки (изолированно, чтобы не было зацикливания)
|
|
|
|
|
|
((ExcelCell)this).SetBorderIsolate(border);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-05 15:58:03 +07:00
|
|
|
|
public ICell Set(bool value)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
cell.DataType = CellValues.Boolean;
|
|
|
|
|
|
cell.CellValue = new CellValue(value ? "1" : "0");
|
|
|
|
|
|
cell.InlineString = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell Set(DateTime value, NumberFormatPattern? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
double oa = value.ToOADate();
|
|
|
|
|
|
Set(oa, format);
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell Set(decimal value, NumberFormatPattern? format = null) => Set((double)value, format);
|
|
|
|
|
|
public ICell Set(double value, NumberFormatPattern? format = null)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
cell.DataType = CellValues.Number;
|
|
|
|
|
|
cell.CellValue = new CellValue(value.ToString(CultureInfo.InvariantCulture));
|
2026-06-19 15:06:40 +07:00
|
|
|
|
cell.InlineString = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
if (format != null) Set(cell, format);
|
|
|
|
|
|
}
|
2026-06-05 15:58:03 +07:00
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
public ICell Set(float value, NumberFormatPattern? format = null) => Set((double)value, format);
|
|
|
|
|
|
public ICell Set(int value, NumberFormatPattern? format = null) => Set((double)value, format);
|
|
|
|
|
|
public ICell Set(long value, NumberFormatPattern? format = null) => Set((double)value, format);
|
|
|
|
|
|
|
|
|
|
|
|
public void ClearFormat()
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
cell?.StyleIndex = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Clear()
|
|
|
|
|
|
{
|
|
|
|
|
|
ClearContent();
|
|
|
|
|
|
ClearFormat();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell CopyTo(uint rowIndex, uint colIndex, out ICell copiedCell)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (writer._syncLock)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
var srcCell = GetCellElement();
|
|
|
|
|
|
if (srcCell != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cloned = (Cell)srcCell.CloneNode(true);
|
|
|
|
|
|
InsertCellAt(rowIndex, colIndex, cloned);
|
2026-06-17 09:33:21 +07:00
|
|
|
|
copiedCell = new ExcelCell(writer, sheet, rowIndex, colIndex);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
copiedCell = new ExcelCell(writer, sheet, rowIndex, colIndex);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell) =>
|
|
|
|
|
|
CopyTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex), out copiedCell);
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
// helpers
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
Cell? GetCellElement()
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
var sheetData = sheet.GetSheetData();
|
|
|
|
|
|
var eRow = FindRowElement(sheetData, row);
|
|
|
|
|
|
if (eRow == null) return null;
|
|
|
|
|
|
return FindCellInRow(eRow, col);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
private Cell GetOrCreateCellElement()
|
|
|
|
|
|
{
|
|
|
|
|
|
var sheetData = sheet.GetSheetData();
|
|
|
|
|
|
var rowElement = GetOrCreateRowElement(sheetData, row);
|
|
|
|
|
|
var cell = FindCellInRow(rowElement, col);
|
|
|
|
|
|
if (cell != null) return cell;
|
|
|
|
|
|
|
|
|
|
|
|
cell = new Cell();
|
|
|
|
|
|
string cellRef = CellAddressHelper.ColumnIndexToLetter(col) + row.ToString();
|
|
|
|
|
|
cell.CellReference = cellRef;
|
|
|
|
|
|
InsertCellInRow(rowElement, cell, col);
|
|
|
|
|
|
|
|
|
|
|
|
// Наследование стиля
|
|
|
|
|
|
var inheritedStyle = GetCellStyle(); // теперь этот метод учитывает строку и столбец
|
|
|
|
|
|
if (inheritedStyle != null && !inheritedStyle.IsEmpty())
|
|
|
|
|
|
{
|
|
|
|
|
|
int styleIndex = writer.GetOrCreateStyleId(inheritedStyle);
|
|
|
|
|
|
cell.StyleIndex = (uint)styleIndex;
|
|
|
|
|
|
}
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static Row? FindRowElement(SheetData sheetData, uint rowIndex)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
foreach (var eRow in sheetData.Elements<Row>())
|
|
|
|
|
|
if (eRow.RowIndex?.Value == rowIndex) return eRow;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
static Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
var existing = FindRowElement(sheetData, rowIndex);
|
|
|
|
|
|
if (existing != null) return existing;
|
|
|
|
|
|
var newRow = new Row { RowIndex = rowIndex };
|
|
|
|
|
|
InsertRowElement(sheetData, newRow, rowIndex);
|
|
|
|
|
|
return newRow;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
static void InsertRowElement(SheetData sheetData, Row eRow, uint rowIndex)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
bool inserted = false;
|
|
|
|
|
|
foreach (var existing in sheetData.Elements<Row>().ToList())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (existing.RowIndex?.Value > rowIndex)
|
|
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
existing.InsertBeforeSelf(eRow);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
inserted = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-17 09:33:21 +07:00
|
|
|
|
if (!inserted) sheetData.Append(eRow);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
static Cell? FindCellInRow(Row eRow, uint colIndex)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
foreach (var cell in eRow.Elements<Cell>())
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex)
|
|
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
static void InsertCellInRow(Row eRow, Cell cell, uint colIndex)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
string newRef = CellAddressHelper.ColumnIndexToLetter(colIndex) + (eRow.RowIndex?.Value ?? 1).ToString();
|
2026-06-05 15:58:03 +07:00
|
|
|
|
cell.CellReference = newRef;
|
|
|
|
|
|
bool inserted = false;
|
2026-06-17 09:33:21 +07:00
|
|
|
|
foreach (var existing in eRow.Elements<Cell>().ToList())
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (CellAddressHelper.TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) && existingCol > colIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
existing.InsertBeforeSelf(cell);
|
|
|
|
|
|
inserted = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-17 09:33:21 +07:00
|
|
|
|
if (!inserted) eRow.Append(cell);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
void InsertCellAt(uint rowIndex, uint colIndex, Cell cell)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
var sheetData = sheet.GetSheetData();
|
2026-06-05 15:58:03 +07:00
|
|
|
|
var row = GetOrCreateRowElement(sheetData, rowIndex);
|
|
|
|
|
|
InsertCellInRow(row, cell, colIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
string GetSharedString(uint index)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-17 09:33:21 +07:00
|
|
|
|
return writer.GetSharedString(index);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
string ExtractTextFromInlineString(InlineString? inlineString)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (inlineString == null) return string.Empty;
|
|
|
|
|
|
var sb = new System.Text.StringBuilder();
|
|
|
|
|
|
foreach (var run in inlineString.Elements<Run>())
|
|
|
|
|
|
{
|
|
|
|
|
|
var text = run.GetFirstChild<Text>()?.Text ?? string.Empty;
|
|
|
|
|
|
sb.Append(text);
|
|
|
|
|
|
}
|
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
MergeCells? GetMergeCells() =>
|
2026-06-17 09:33:21 +07:00
|
|
|
|
sheet.Worksheet.GetFirstChild<MergeCells>();
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
bool TryParseRangeReference(string reference, out ExcelRange range)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
range = null!;
|
|
|
|
|
|
if (string.IsNullOrEmpty(reference)) return false;
|
|
|
|
|
|
string[] parts = reference.Split(':');
|
|
|
|
|
|
if (parts.Length != 2) return false;
|
|
|
|
|
|
if (!CellAddressHelper.TryParseCellReference(parts[0] + "1", out uint row1, out uint col1)) return false;
|
|
|
|
|
|
if (!CellAddressHelper.TryParseCellReference(parts[1] + "1", out uint row2, out uint col2)) return false;
|
|
|
|
|
|
uint rowStart = Math.Min(row1, row2), rowEnd = Math.Max(row1, row2);
|
|
|
|
|
|
uint colStart = Math.Min(col1, col2), colEnd = Math.Max(col1, col2);
|
2026-06-17 09:33:21 +07:00
|
|
|
|
range = new ExcelRange(writer, sheet, rowStart, colStart, rowEnd, colEnd);
|
2026-06-05 15:58:03 +07:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|