2026-06-08 14:31:31 +07:00
|
|
|
|
namespace QWERTYkez.ExcelProcessor;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Внутренняя реализация <see cref="ICell"/>.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
internal sealed class ExcelCell : ICell
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly ExcelWriter _writer;
|
|
|
|
|
|
private readonly ExcelSheet _sheet;
|
|
|
|
|
|
private uint _row;
|
|
|
|
|
|
private uint _col;
|
|
|
|
|
|
|
|
|
|
|
|
internal ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col)
|
|
|
|
|
|
{
|
|
|
|
|
|
_writer = writer;
|
|
|
|
|
|
_sheet = sheet;
|
|
|
|
|
|
_row = row;
|
|
|
|
|
|
_col = col;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 TrySet(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)
|
|
|
|
|
|
SetNumberFormatInternal(cell, format);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell Set(string formula, NumberFormatPattern? format = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!TrySet(formula, format))
|
|
|
|
|
|
throw new InvalidOperationException("Failed to set formula");
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ICell Set(NumberFormatPattern format)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (format == null) return this;
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
SetNumberFormatInternal(cell, format);
|
|
|
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellAlign align)
|
|
|
|
|
|
{
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|
|
|
|
|
numberFormat: null,
|
|
|
|
|
|
font: null,
|
|
|
|
|
|
fill: null,
|
|
|
|
|
|
border: null,
|
|
|
|
|
|
align: align
|
|
|
|
|
|
);
|
|
|
|
|
|
cell.StyleIndex = (uint)styleIndex;
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellBorder border)
|
|
|
|
|
|
{
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|
|
|
|
|
numberFormat: null,
|
|
|
|
|
|
font: null,
|
|
|
|
|
|
fill: null,
|
|
|
|
|
|
border: border,
|
|
|
|
|
|
align: null
|
|
|
|
|
|
);
|
|
|
|
|
|
cell.StyleIndex = (uint)styleIndex;
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellFill fill)
|
|
|
|
|
|
{
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|
|
|
|
|
numberFormat: null,
|
|
|
|
|
|
font: null,
|
|
|
|
|
|
fill: fill,
|
|
|
|
|
|
border: null,
|
|
|
|
|
|
align: null
|
|
|
|
|
|
);
|
|
|
|
|
|
cell.StyleIndex = (uint)styleIndex;
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public ICell Set(CellFont font)
|
|
|
|
|
|
{
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|
|
|
|
|
numberFormat: null,
|
|
|
|
|
|
font: font,
|
|
|
|
|
|
fill: null,
|
|
|
|
|
|
border: null,
|
|
|
|
|
|
align: null
|
|
|
|
|
|
);
|
|
|
|
|
|
cell.StyleIndex = (uint)styleIndex;
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 14:31:31 +07:00
|
|
|
|
public ICell Text(Action<ICellText> value)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (value == null) return this;
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
var textObj = new ExcelCellText();
|
|
|
|
|
|
value(textObj);
|
|
|
|
|
|
cell.InlineString = BuildInlineString(textObj);
|
|
|
|
|
|
cell.DataType = CellValues.InlineString;
|
|
|
|
|
|
cell.CellValue = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
|
|
|
|
|
|
// Если есть разрыв строки (символ \n), включаем перенос текста для ячейки
|
|
|
|
|
|
bool hasNewline = textObj.GetRuns().Any(r => r.Text != null && r.Text.Contains('\n'));
|
|
|
|
|
|
if (hasNewline)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentAlign = GetCellAlign();
|
|
|
|
|
|
if (currentAlign.WrapText != true)
|
|
|
|
|
|
{
|
|
|
|
|
|
var newAlign = new CellAlign
|
|
|
|
|
|
{
|
|
|
|
|
|
Horizontal = currentAlign.Horizontal,
|
|
|
|
|
|
Vertical = currentAlign.Vertical,
|
|
|
|
|
|
WrapText = true,
|
|
|
|
|
|
ShrinkToFit = currentAlign.ShrinkToFit
|
|
|
|
|
|
};
|
|
|
|
|
|
Set(newAlign);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return this;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 14:31:31 +07:00
|
|
|
|
/// <inheritdoc />
|
2026-06-16 15:52:08 +07:00
|
|
|
|
public ICellText Text()
|
2026-06-08 14:31:31 +07:00
|
|
|
|
{
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
2026-06-16 15:52:08 +07:00
|
|
|
|
// 1. Получаем или создаём элемент ячейки
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Убедимся, что у ячейки правильный тип и есть InlineString
|
|
|
|
|
|
if (cell.DataType == null || cell.DataType.Value != CellValues.InlineString)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Если был другой тип (например, число) — меняем на InlineString
|
|
|
|
|
|
cell.DataType = CellValues.InlineString;
|
|
|
|
|
|
// Удаляем старый InlineString, если был
|
|
|
|
|
|
cell.InlineString?.Remove();
|
|
|
|
|
|
// Создаём новый пустой InlineString
|
|
|
|
|
|
cell.InlineString = new InlineString();
|
|
|
|
|
|
// Добавляем пустой Text (обязательно для Open XML)
|
|
|
|
|
|
cell.InlineString.AppendChild(new Text());
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (cell.InlineString == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Если тип InlineString, но сам элемент отсутствует
|
|
|
|
|
|
cell.InlineString = new InlineString();
|
|
|
|
|
|
cell.InlineString.AppendChild(new Text());
|
|
|
|
|
|
}
|
2026-06-08 14:31:31 +07:00
|
|
|
|
|
2026-06-16 15:52:08 +07:00
|
|
|
|
// 3. Строим объект ICellText на основе содержимого InlineString
|
2026-06-08 14:31:31 +07:00
|
|
|
|
var textObj = new ExcelCellText();
|
|
|
|
|
|
foreach (var run in cell.InlineString.Elements<Run>())
|
|
|
|
|
|
{
|
|
|
|
|
|
string text = run.GetFirstChild<Text>()?.Text ?? string.Empty;
|
|
|
|
|
|
RunFormat? format = null;
|
|
|
|
|
|
if (run.RunProperties != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
format = RunFormat.FromRunProperties(run.RunProperties);
|
|
|
|
|
|
}
|
2026-06-16 12:03:56 +07:00
|
|
|
|
textObj.Run(text, format);
|
2026-06-08 14:31:31 +07:00
|
|
|
|
}
|
2026-06-16 15:52:08 +07:00
|
|
|
|
|
|
|
|
|
|
// Если не было ни одного Run, текст всё равно должен быть доступен (пустой)
|
|
|
|
|
|
// Можно оставить textObj пустым, а можно сразу добавить пустой Run
|
|
|
|
|
|
// (но это не обязательно – пользователь может добавить run позже)
|
2026-06-08 14:31:31 +07:00
|
|
|
|
return textObj;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public bool TryText(out ICellText cellText)
|
|
|
|
|
|
{
|
|
|
|
|
|
cellText = Text()!;
|
|
|
|
|
|
return cellText is not null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-05 15:58:03 +07:00
|
|
|
|
public ICell Set(string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (value == null) return this;
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetOrCreateCellElement();
|
|
|
|
|
|
// Проверяем, можно ли сохранить как число?
|
|
|
|
|
|
if (double.TryParse(value, out double num))
|
|
|
|
|
|
{
|
|
|
|
|
|
cell.DataType = CellValues.Number;
|
|
|
|
|
|
cell.CellValue = new CellValue(num.ToString());
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Используем общую таблицу строк
|
|
|
|
|
|
int idx = GetOrAddSharedString(value);
|
|
|
|
|
|
cell.DataType = CellValues.SharedString;
|
|
|
|
|
|
cell.CellValue = new CellValue(idx.ToString());
|
|
|
|
|
|
}
|
|
|
|
|
|
cell.InlineString = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
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));
|
|
|
|
|
|
if (format != null)
|
|
|
|
|
|
SetNumberFormatInternal(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 ClearContent()
|
|
|
|
|
|
{
|
|
|
|
|
|
_writer.ThrowIfDisposed();
|
|
|
|
|
|
lock (_writer._syncLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cell = GetCellElement();
|
|
|
|
|
|
if (cell != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
cell.CellValue = null;
|
|
|
|
|
|
cell.CellFormula = null;
|
|
|
|
|
|
cell.InlineString = null;
|
|
|
|
|
|
cell.DataType = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// Private helpers
|
|
|
|
|
|
|
|
|
|
|
|
private Cell? GetCellElement()
|
|
|
|
|
|
{
|
|
|
|
|
|
var sheetData = _sheet.GetSheetData();
|
|
|
|
|
|
var row = FindRowElement(sheetData, _row);
|
|
|
|
|
|
if (row == null) return null;
|
|
|
|
|
|
return FindCellInRow(row, _col);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Cell GetOrCreateCellElement()
|
|
|
|
|
|
{
|
|
|
|
|
|
var sheetData = _sheet.GetSheetData();
|
|
|
|
|
|
var row = GetOrCreateRowElement(sheetData, _row);
|
|
|
|
|
|
var cell = FindCellInRow(row, _col);
|
|
|
|
|
|
if (cell != null) return cell;
|
|
|
|
|
|
cell = new Cell();
|
|
|
|
|
|
string cellRef = CellAddressHelper.ColumnIndexToLetter(_col) + _row.ToString();
|
|
|
|
|
|
cell.CellReference = cellRef;
|
|
|
|
|
|
InsertCellInRow(row, cell, _col);
|
|
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static Row? FindRowElement(SheetData sheetData, uint rowIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var row in sheetData.Elements<Row>())
|
|
|
|
|
|
if (row.RowIndex?.Value == rowIndex) return row;
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
bool inserted = false;
|
|
|
|
|
|
foreach (var existing in sheetData.Elements<Row>().ToList())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (existing.RowIndex?.Value > rowIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
existing.InsertBeforeSelf(row);
|
|
|
|
|
|
inserted = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!inserted) sheetData.Append(row);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static Cell? FindCellInRow(Row row, uint colIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var cell in row.Elements<Cell>())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex)
|
|
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void InsertCellInRow(Row row, Cell cell, uint colIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
string newRef = CellAddressHelper.ColumnIndexToLetter(colIndex) + (row.RowIndex?.Value ?? 1).ToString();
|
|
|
|
|
|
cell.CellReference = newRef;
|
|
|
|
|
|
bool inserted = false;
|
|
|
|
|
|
foreach (var existing in row.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) row.Append(cell);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void InsertCellAt(uint rowIndex, uint colIndex, Cell cell)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sheetData = _sheet.GetSheetData();
|
|
|
|
|
|
var row = GetOrCreateRowElement(sheetData, rowIndex);
|
|
|
|
|
|
InsertCellInRow(row, cell, colIndex);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SetNumberFormatInternal(Cell cell, NumberFormatPattern format)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (format == null) return;
|
|
|
|
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|
|
|
|
|
numberFormat: format,
|
|
|
|
|
|
font: null,
|
|
|
|
|
|
fill: null,
|
|
|
|
|
|
border: null,
|
|
|
|
|
|
align: null
|
|
|
|
|
|
);
|
|
|
|
|
|
cell.StyleIndex = (uint)styleIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string GetSharedString(uint index)
|
|
|
|
|
|
{
|
|
|
|
|
|
return _writer.GetSharedString(index);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private int GetOrAddSharedString(string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
return _writer.GetOrAddSharedString(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private InlineString BuildInlineString(ExcelCellText textObj)
|
|
|
|
|
|
{
|
|
|
|
|
|
var inline = new InlineString();
|
|
|
|
|
|
foreach (var run in textObj.GetRuns())
|
|
|
|
|
|
{
|
|
|
|
|
|
var runElement = new Run();
|
|
|
|
|
|
// Всегда создаём элемент Text, даже если строка состоит из "\n"
|
|
|
|
|
|
runElement.Append(new Text(run.Text));
|
|
|
|
|
|
|
|
|
|
|
|
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.Color.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var excelColor = fmt.Color.Value.ToExcel();
|
|
|
|
|
|
rPr.Append(excelColor);
|
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private MergeCells? GetMergeCells() =>
|
|
|
|
|
|
_sheet.Worksheet.GetFirstChild<MergeCells>();
|
|
|
|
|
|
|
|
|
|
|
|
private 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|