Files
QWERTYkez.OpenXmlProcessors/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs
melekhin e373d4108a
All checks were successful
Publish NuGet packages / publish (push) Successful in 28s
many debugs
2026-06-19 15:06:40 +07:00

1272 lines
43 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Внутренняя реализация <see cref="ICell"/>.
/// </summary>
internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col) : ICell
{
/// <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;
}
// Кэш фрагментов богатого текста (только для InlineString)
List<IRun>? _runsCache;
bool _cacheValid;
// ---- Реализация 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);
}
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)
{
Run(text, format);
Break();
return this;
}
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 не трогает текст, поэтому кэш остаётся валидным (если был).
// Но если стиль меняется, кэш не сбрасываем.
// ---- Приватные методы для работы с кэшем ----
void EnsureCacheValid()
{
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;
}
}
string GetStringFromCell(Cell cell)
{
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;
}
void UpdateCellFromCache()
{
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;
}
}
}
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)
{
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
};
}
public bool IsMerged
{
get
{
var range = GetMergedRange();
return range != null;
}
}
public IRange? GetMergedRange()
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
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))
{
if (row >= range.RowStart && row <= range.RowEnd &&
col >= range.ColStart && col <= range.ColEnd)
return range;
}
}
return null;
}
}
public bool TryGetMergedRange(IRange range)
{
var merged = GetMergedRange();
if (merged == null) return false;
return merged.Equals(range);
}
public uint Row => row;
public uint Col => col;
public string ColLetter => CellAddressHelper.ColumnIndexToLetter(col);
public ICell MoveTo(uint rowIndex, uint colIndex)
{
if (rowIndex == row && colIndex == col) return this;
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
// Вырезать-вставить: копируем данные в новую позицию, очищаем старую
var srcCell = GetCellElement();
if (srcCell != null)
{
var cloned = (Cell)srcCell.CloneNode(true);
InsertCellAt(rowIndex, colIndex, cloned);
srcCell.Remove();
}
row = rowIndex;
col = colIndex;
}
return this;
}
public ICell MoveTo(uint rowIndex, string colIndex) => MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex));
public RowHeight Height
{
get => ExcelRow.GetHeight(writer, sheet, row);
set => ExcelRow.SetHeight(writer, sheet, row, value);
}
public ColumnWidth Width
{
get => ExcelColumn.GetWidth(writer, sheet, col);
set => ExcelColumn.SetWidth(writer, sheet, col, value);
}
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;
return writer.IsCellLocked(styleIndex);
}
}
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;
return writer.GetNumberFormat(styleIndex);
}
/// <inheritdoc />
public CellAlign GetCellAlign()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return default;
return writer.GetCellAlign(styleIndex);
}
/// <inheritdoc />
public CellBorder GetCellBorder()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return default;
return writer.GetCellBorder(styleIndex);
}
/// <inheritdoc />
public CellFill GetCellFill()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return default;
return writer.GetCellFill(styleIndex);
}
/// <inheritdoc />
public CellFont GetCellFont()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return default;
return writer.GetCellFont(styleIndex);
}
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;
public bool TrySetFormula(string formula, NumberFormatPattern? format = null)
{
if (string.IsNullOrEmpty(formula)) return false;
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var cell = GetOrCreateCellElement();
cell.CellFormula = new CellFormula(formula);
cell.DataType = null; // формула сама определяет тип
if (format != null) Set(cell, format);
return true;
}
}
public ICell SetFormula(string formula, NumberFormatPattern? format = null)
{
if (!TrySetFormula(formula, format))
throw new InvalidOperationException("Failed to set formula");
return this;
}
public ICell Set(NumberFormatPattern format)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var currentStyle = GetCellStyle() ?? new CellStyle();
if (currentStyle.TryMerge(format, out var newStyle))
ApplyStyle(newStyle);
return this;
}
}
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)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var currentStyle = GetCellStyle() ?? new CellStyle();
if (currentStyle.TryMerge(align, out var newStyle))
ApplyStyle(newStyle);
return this;
}
}
/// <inheritdoc />
public ICell Set(CellBorder border)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
// 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 });
// Диагональные границы не влияют на соседей, их не очищаем
return this;
}
}
/// <inheritdoc />
public ICell Set(CellFill fill)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var currentStyle = GetCellStyle() ?? new CellStyle();
if (currentStyle.TryMerge(fill, out var newStyle))
ApplyStyle(newStyle);
return this;
}
}
/// <inheritdoc />
public ICell Set(CellFont font)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
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);
return this;
}
}
public ICell Set(bool value)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
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)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var cell = GetOrCreateCellElement();
cell.DataType = CellValues.Number;
cell.CellValue = new CellValue(value.ToString(CultureInfo.InvariantCulture));
cell.InlineString = null;
cell.CellFormula = null;
if (format != null) Set(cell, format);
}
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()
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var cell = GetCellElement();
cell?.StyleIndex = null;
}
}
public void Clear()
{
ClearContent();
ClearFormat();
}
public ICell CopyTo(uint rowIndex, uint colIndex, out ICell copiedCell)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var srcCell = GetCellElement();
if (srcCell != null)
{
var cloned = (Cell)srcCell.CloneNode(true);
InsertCellAt(rowIndex, colIndex, cloned);
copiedCell = new ExcelCell(writer, sheet, rowIndex, colIndex);
}
else
{
copiedCell = new ExcelCell(writer, sheet, rowIndex, colIndex);
}
return this;
}
}
public ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell) =>
CopyTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex), out copiedCell);
// helpers
Cell? GetCellElement()
{
var sheetData = sheet.GetSheetData();
var eRow = FindRowElement(sheetData, row);
if (eRow == null) return null;
return FindCellInRow(eRow, col);
}
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;
}
return cell;
}
static Row? FindRowElement(SheetData sheetData, uint rowIndex)
{
foreach (var eRow in sheetData.Elements<Row>())
if (eRow.RowIndex?.Value == rowIndex) return eRow;
return null;
}
static Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex)
{
var existing = FindRowElement(sheetData, rowIndex);
if (existing != null) return existing;
var newRow = new Row { RowIndex = rowIndex };
InsertRowElement(sheetData, newRow, rowIndex);
return newRow;
}
static void InsertRowElement(SheetData sheetData, Row eRow, uint rowIndex)
{
bool inserted = false;
foreach (var existing in sheetData.Elements<Row>().ToList())
{
if (existing.RowIndex?.Value > rowIndex)
{
existing.InsertBeforeSelf(eRow);
inserted = true;
break;
}
}
if (!inserted) sheetData.Append(eRow);
}
static Cell? FindCellInRow(Row eRow, uint colIndex)
{
foreach (var cell in eRow.Elements<Cell>())
{
if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex)
return cell;
}
return null;
}
static void InsertCellInRow(Row eRow, Cell cell, uint colIndex)
{
string newRef = CellAddressHelper.ColumnIndexToLetter(colIndex) + (eRow.RowIndex?.Value ?? 1).ToString();
cell.CellReference = newRef;
bool inserted = false;
foreach (var existing in eRow.Elements<Cell>().ToList())
{
if (CellAddressHelper.TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) && existingCol > colIndex)
{
existing.InsertBeforeSelf(cell);
inserted = true;
break;
}
}
if (!inserted) eRow.Append(cell);
}
void InsertCellAt(uint rowIndex, uint colIndex, Cell cell)
{
var sheetData = sheet.GetSheetData();
var row = GetOrCreateRowElement(sheetData, rowIndex);
InsertCellInRow(row, cell, colIndex);
}
string GetSharedString(uint index)
{
return writer.GetSharedString(index);
}
string ExtractTextFromInlineString(InlineString? inlineString)
{
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();
}
MergeCells? GetMergeCells() =>
sheet.Worksheet.GetFirstChild<MergeCells>();
bool TryParseRangeReference(string reference, out ExcelRange range)
{
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);
range = new ExcelRange(writer, sheet, rowStart, colStart, rowEnd, colEnd);
return true;
}
}