namespace QWERTYkez.ExcelProcessor; /// /// Внутренняя реализация . /// internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col) : ICell { /// /// Возвращает эффективный стиль ячейки с учётом наследования от строки и столбца. /// 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? _runsCache; bool _cacheValid; // ---- Реализация ICell (новые методы) ---- public int RunsCount { get { EnsureCacheValid(); return _runsCache?.Count ?? 0; } } public IEnumerable GetRuns() { EnsureCacheValid(); return _runsCache ?? Enumerable.Empty(); } 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()) { string text = run.GetFirstChild()?.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()) { 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; } /// public NumberFormatPattern? GetNumberFormat() { var cell = GetCellElement(); if (cell?.StyleIndex?.Value is not uint styleIndex) return null; return writer.GetNumberFormat(styleIndex); } /// public CellAlign GetCellAlign() { var cell = GetCellElement(); if (cell?.StyleIndex?.Value is not uint styleIndex) return default; return writer.GetCellAlign(styleIndex); } /// public CellBorder GetCellBorder() { var cell = GetCellElement(); if (cell?.StyleIndex?.Value is not uint styleIndex) return default; return writer.GetCellBorder(styleIndex); } /// public CellFill GetCellFill() { var cell = GetCellElement(); if (cell?.StyleIndex?.Value is not uint styleIndex) return default; return writer.GetCellFill(styleIndex); } /// 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; } } /// 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; } } /// 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; } } /// 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; } } /// 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; } } /// 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 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); } /// /// Возвращает соседнюю ячейку по указанному смещению. /// /// Исходная ячейка. /// Смещение по строкам (положительное – вниз). /// Смещение по столбцам (положительное – вправо). /// Соседняя ячейка, или null, если она выходит за пределы листа. 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()) 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().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()) { 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().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()) { var text = run.GetFirstChild()?.Text ?? string.Empty; sb.Append(text); } return sb.ToString(); } MergeCells? GetMergeCells() => sheet.Worksheet.GetFirstChild(); 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; } }