886 lines
29 KiB
C#
886 lines
29 KiB
C#
|
|
namespace QWERTYkez.ExcelProcessor.Editors;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Внутренняя реализация <see cref="IRange"/>.
|
|||
|
|
/// </summary>
|
|||
|
|
internal sealed class ExcelRange : IRange
|
|||
|
|
{
|
|||
|
|
private readonly ExcelWriter _writer;
|
|||
|
|
private readonly ExcelSheet _sheet;
|
|||
|
|
private uint _rowStart, _rowEnd;
|
|||
|
|
private uint _colStart, _colEnd;
|
|||
|
|
|
|||
|
|
internal ExcelRange(ExcelWriter writer, ExcelSheet sheet, uint rowStart, uint colStart, uint rowEnd, uint colEnd)
|
|||
|
|
{
|
|||
|
|
_writer = writer;
|
|||
|
|
_sheet = sheet;
|
|||
|
|
_rowStart = rowStart;
|
|||
|
|
_rowEnd = rowEnd;
|
|||
|
|
_colStart = colStart;
|
|||
|
|
_colEnd = colEnd;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public uint RowStart => _rowStart;
|
|||
|
|
public uint RowEnd => _rowEnd;
|
|||
|
|
public uint ColStart => _colStart;
|
|||
|
|
public uint ColEnd => _colEnd;
|
|||
|
|
public string ColStartLetter => CellAddressHelper.ColumnIndexToLetter(_colStart);
|
|||
|
|
public string ColEndLetter => CellAddressHelper.ColumnIndexToLetter(_colEnd);
|
|||
|
|
public uint Rows => _rowEnd - _rowStart + 1;
|
|||
|
|
public uint Cols => _colEnd - _colStart + 1;
|
|||
|
|
|
|||
|
|
public bool IsMerged
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
var mergeCells = GetMergeCells();
|
|||
|
|
if (mergeCells == null) return false;
|
|||
|
|
foreach (var mergeCell in mergeCells.Elements<MergeCell>())
|
|||
|
|
{
|
|||
|
|
if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var refRange))
|
|||
|
|
{
|
|||
|
|
if (refRange._rowStart == _rowStart && refRange._rowEnd == _rowEnd &&
|
|||
|
|
refRange._colStart == _colStart && refRange._colEnd == _colEnd)
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить").
|
|||
|
|
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="newRow">Номер строки для нового верхнего левого угла.</param>
|
|||
|
|
/// <param name="newCol">Номер столбца для нового верхнего левого угла.</param>
|
|||
|
|
/// <returns>Тот же объект IRange с новыми координатами.</returns>
|
|||
|
|
public IRange MoveTo(uint newRow, uint newCol)
|
|||
|
|
{
|
|||
|
|
if (newRow == _rowStart && newCol == _colStart) return this;
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
uint newRowEnd = newRow + (_rowEnd - _rowStart);
|
|||
|
|
uint newColEnd = newCol + (_colEnd - _colStart);
|
|||
|
|
bool overlap = RangesOverlap(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, newRowEnd, newColEnd);
|
|||
|
|
bool horizontalShift = (newRow == _rowStart);
|
|||
|
|
bool verticalShift = (newCol == _colStart);
|
|||
|
|
|
|||
|
|
if (!overlap)
|
|||
|
|
{
|
|||
|
|
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, CopyOrder.Any);
|
|||
|
|
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
|
|||
|
|
}
|
|||
|
|
else if (horizontalShift)
|
|||
|
|
{
|
|||
|
|
CopyOrder order = (newCol > _colStart) ? CopyOrder.RightToLeft : CopyOrder.LeftToRight;
|
|||
|
|
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, order);
|
|||
|
|
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
|
|||
|
|
}
|
|||
|
|
else if (verticalShift)
|
|||
|
|
{
|
|||
|
|
CopyOrder order = (newRow > _rowStart) ? CopyOrder.BottomToTop : CopyOrder.TopToBottom;
|
|||
|
|
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, order);
|
|||
|
|
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
// Диагональное перемещение – два последовательных сдвига
|
|||
|
|
MoveTo(_rowStart, newCol);
|
|||
|
|
MoveTo(newRow, _colStart);
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_rowStart = newRow;
|
|||
|
|
_rowEnd = newRowEnd;
|
|||
|
|
_colStart = newCol;
|
|||
|
|
_colEnd = newColEnd;
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IRange MoveTo(uint rowIndex, string colIndex)
|
|||
|
|
{
|
|||
|
|
return MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private enum CopyOrder
|
|||
|
|
{
|
|||
|
|
Any,
|
|||
|
|
LeftToRight,
|
|||
|
|
RightToLeft,
|
|||
|
|
TopToBottom,
|
|||
|
|
BottomToTop
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода.</summary>
|
|||
|
|
private void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd,
|
|||
|
|
uint dstRowStart, uint dstColStart, CopyOrder order)
|
|||
|
|
{
|
|||
|
|
// Определяем все строки, которые могут понадобиться (исходные и целевые)
|
|||
|
|
var rowIndices = new HashSet<uint>();
|
|||
|
|
for (uint r = srcRowStart; r <= srcRowEnd; r++) rowIndices.Add(r);
|
|||
|
|
uint dstRowEnd = dstRowStart + (srcRowEnd - srcRowStart);
|
|||
|
|
for (uint r = dstRowStart; r <= dstRowEnd; r++) rowIndices.Add(r);
|
|||
|
|
var rowsDict = GetRowDictionary(rowIndices);
|
|||
|
|
|
|||
|
|
int rowStep = 1, colStep = 1;
|
|||
|
|
int rowStart = (int)srcRowStart, rowEnd = (int)srcRowEnd;
|
|||
|
|
int colStart = (int)srcColStart, colEnd = (int)srcColEnd;
|
|||
|
|
|
|||
|
|
if (order == CopyOrder.BottomToTop)
|
|||
|
|
{
|
|||
|
|
rowStep = -1;
|
|||
|
|
rowStart = (int)srcRowEnd;
|
|||
|
|
rowEnd = (int)srcRowStart;
|
|||
|
|
}
|
|||
|
|
if (order == CopyOrder.RightToLeft)
|
|||
|
|
{
|
|||
|
|
colStep = -1;
|
|||
|
|
colStart = (int)srcColEnd;
|
|||
|
|
colEnd = (int)srcColStart;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (int r = rowStart; (rowStep > 0 ? r <= rowEnd : r >= rowEnd); r += rowStep)
|
|||
|
|
{
|
|||
|
|
if (!rowsDict.TryGetValue((uint)r, out var srcRow))
|
|||
|
|
continue;
|
|||
|
|
|
|||
|
|
for (int c = colStart; (colStep > 0 ? c <= colEnd : c >= colEnd); c += colStep)
|
|||
|
|
{
|
|||
|
|
Cell? srcCell = FindCellInRowFast(srcRow, (uint)c);
|
|||
|
|
if (srcCell == null) continue;
|
|||
|
|
|
|||
|
|
Cell cloned = (Cell)srcCell.CloneNode(true);
|
|||
|
|
uint dstRow = dstRowStart + (uint)(r - srcRowStart);
|
|||
|
|
uint dstCol = dstColStart + (uint)(c - srcColStart);
|
|||
|
|
cloned.CellReference = $"{CellAddressHelper.ColumnIndexToLetter(dstCol)}{dstRow}";
|
|||
|
|
InsertCellAt(dstRow, dstCol, cloned);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Строит словарь строк для указанных индексов строк.</summary>
|
|||
|
|
private Dictionary<uint, Row> GetRowDictionary(HashSet<uint> rowIndices)
|
|||
|
|
{
|
|||
|
|
var dict = new Dictionary<uint, Row>();
|
|||
|
|
var sheetData = _sheet.GetSheetData();
|
|||
|
|
foreach (var row in sheetData.Elements<Row>())
|
|||
|
|
{
|
|||
|
|
if (row.RowIndex?.Value is uint idx && rowIndices.Contains(idx))
|
|||
|
|
dict[idx] = row;
|
|||
|
|
}
|
|||
|
|
return dict;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке).</summary>
|
|||
|
|
private Cell? FindCellInRowFast(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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Проверяет, пересекаются ли два прямоугольных диапазона.</summary>
|
|||
|
|
private static bool RangesOverlap(uint r1s, uint c1s, uint r1e, uint c1e,
|
|||
|
|
uint r2s, uint c2s, uint r2e, uint c2e)
|
|||
|
|
{
|
|||
|
|
return !(r1e < r2s || r2e < r1s || c1e < c2s || c2e < c1s);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Вставляет ячейку в указанную позицию, предварительно удаляя существующую.</summary>
|
|||
|
|
private void InsertCellAt(uint row, uint col, Cell cell)
|
|||
|
|
{
|
|||
|
|
DeleteCellAt(row, col);
|
|||
|
|
var sheetData = _sheet.GetSheetData();
|
|||
|
|
var rowElement = GetOrCreateRowElement(sheetData, row);
|
|||
|
|
InsertCellInRow(rowElement, cell, col);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Удаляет ячейку, если она существует.</summary>
|
|||
|
|
private void DeleteCellAt(uint row, uint col)
|
|||
|
|
{
|
|||
|
|
var sheetData = _sheet.GetSheetData();
|
|||
|
|
var rowElement = FindRowElement(sheetData, row);
|
|||
|
|
if (rowElement == null) return;
|
|||
|
|
var cell = FindCellInRow(rowElement, col);
|
|||
|
|
cell?.Remove();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Получает или создаёт строку с указанным индексом.</summary>
|
|||
|
|
private 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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Вставляет ячейку в строку с сохранением порядка столбцов.</summary>
|
|||
|
|
private void InsertCellInRow(Row row, Cell cell, uint colIndex)
|
|||
|
|
{
|
|||
|
|
string newRef = $"{CellAddressHelper.ColumnIndexToLetter(colIndex)}{row.RowIndex?.Value ?? 1}";
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Очищает данные (содержимое) в указанном диапазоне.</summary>
|
|||
|
|
private void ClearRangeData(uint rowStart, uint colStart, uint rowEnd, uint colEnd)
|
|||
|
|
{
|
|||
|
|
for (uint r = rowStart; r <= rowEnd; r++)
|
|||
|
|
{
|
|||
|
|
var row = FindRowElement(_sheet.GetSheetData(), r);
|
|||
|
|
if (row == null) continue;
|
|||
|
|
for (uint c = colStart; c <= colEnd; c++)
|
|||
|
|
{
|
|||
|
|
var cell = FindCellInRow(row, c);
|
|||
|
|
cell?.Remove();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
public bool TryMerge()
|
|||
|
|
{
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
if (IsMerged) return false;
|
|||
|
|
var worksheet = _sheet.Worksheet;
|
|||
|
|
|
|||
|
|
var mergeCells = worksheet.GetFirstChild<MergeCells>();
|
|||
|
|
if (mergeCells == null)
|
|||
|
|
{
|
|||
|
|
mergeCells = new MergeCells();
|
|||
|
|
worksheet.Append(mergeCells);
|
|||
|
|
}
|
|||
|
|
string refStr = $"{ColStartLetter}{RowStart}:{ColEndLetter}{RowEnd}";
|
|||
|
|
mergeCells.Append(new MergeCell { Reference = refStr });
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Unmerge()
|
|||
|
|
{
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
var mergeCells = GetMergeCells();
|
|||
|
|
if (mergeCells == null) return;
|
|||
|
|
var toRemove = mergeCells.Elements<MergeCell>()
|
|||
|
|
.FirstOrDefault(mc => TryParseRangeReference(mc.Reference?.Value ?? string.Empty, out var rng) &&
|
|||
|
|
rng._rowStart == _rowStart && rng._rowEnd == _rowEnd &&
|
|||
|
|
rng._colStart == _colStart && rng._colEnd == _colEnd);
|
|||
|
|
toRemove?.Remove();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 refRange))
|
|||
|
|
{
|
|||
|
|
if (refRange._rowStart >= _rowStart && refRange._rowEnd <= _rowEnd &&
|
|||
|
|
refRange._colStart >= _colStart && refRange._colEnd <= _colEnd)
|
|||
|
|
return refRange;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryGetMergedRange(IRange range)
|
|||
|
|
{
|
|||
|
|
var merged = GetMergedRange();
|
|||
|
|
if (merged == null) return false;
|
|||
|
|
return merged.Equals(range);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IRange SetNumberFormat(NumberFormatPattern format)
|
|||
|
|
{
|
|||
|
|
if (format == null) return this;
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
int fmtId = GetOrCreateNumberFormatId(format);
|
|||
|
|
for (uint r = _rowStart; r <= _rowEnd; r++)
|
|||
|
|
{
|
|||
|
|
for (uint c = _colStart; c <= _colEnd; c++)
|
|||
|
|
{
|
|||
|
|
var cell = GetCellInternal(r, c);
|
|||
|
|
cell?.StyleIndex = (uint)fmtId;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
public IRange SetCellAlign(CellAlign align)
|
|||
|
|
{
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|||
|
|
numberFormat: null,
|
|||
|
|
font: null,
|
|||
|
|
fill: null,
|
|||
|
|
border: null,
|
|||
|
|
align: align
|
|||
|
|
);
|
|||
|
|
ApplyStyleToRange((uint)styleIndex);
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
public IRange SetCellBorder(CellBorder border)
|
|||
|
|
{
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|||
|
|
numberFormat: null,
|
|||
|
|
font: null,
|
|||
|
|
fill: null,
|
|||
|
|
border: border,
|
|||
|
|
align: null
|
|||
|
|
);
|
|||
|
|
ApplyStyleToRange((uint)styleIndex);
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
public IRange SetCellFill(CellFill fill)
|
|||
|
|
{
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|||
|
|
numberFormat: null,
|
|||
|
|
font: null,
|
|||
|
|
fill: fill,
|
|||
|
|
border: null,
|
|||
|
|
align: null
|
|||
|
|
);
|
|||
|
|
ApplyStyleToRange((uint)styleIndex);
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <inheritdoc />
|
|||
|
|
public IRange SetCellFont(CellFont font)
|
|||
|
|
{
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
int styleIndex = _writer.GetOrCreateCellFormatId(
|
|||
|
|
numberFormat: null,
|
|||
|
|
font: font,
|
|||
|
|
fill: null,
|
|||
|
|
border: null,
|
|||
|
|
align: null
|
|||
|
|
);
|
|||
|
|
ApplyStyleToRange((uint)styleIndex);
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Применяет стиль ко всем ячейкам диапазона.
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="styleIndex">Индекс стиля для применения.</param>
|
|||
|
|
/// <param name="createIfMissing">Если true, создаёт недостающие ячейки (по умолчанию true).</param>
|
|||
|
|
private void ApplyStyleToRange(uint styleIndex, bool createIfMissing = true)
|
|||
|
|
{
|
|||
|
|
for (uint row = _rowStart; row <= _rowEnd; row++)
|
|||
|
|
{
|
|||
|
|
for (uint col = _colStart; col <= _colEnd; col++)
|
|||
|
|
{
|
|||
|
|
var cell = GetCellInternal(row, col);
|
|||
|
|
if (cell != null)
|
|||
|
|
{
|
|||
|
|
cell.StyleIndex = styleIndex;
|
|||
|
|
}
|
|||
|
|
else if (createIfMissing)
|
|||
|
|
{
|
|||
|
|
var newCell = new Cell
|
|||
|
|
{
|
|||
|
|
CellReference = $"{CellAddressHelper.ColumnIndexToLetter(col)}{row}",
|
|||
|
|
StyleIndex = styleIndex
|
|||
|
|
};
|
|||
|
|
InsertCellAt(row, col, newCell);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IEnumerable<ICell> Cells
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
for (uint r = _rowStart; r <= _rowEnd; r++)
|
|||
|
|
for (uint c = _colStart; c <= _colEnd; c++)
|
|||
|
|
yield return new ExcelCell(_writer, _sheet, r, c);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool GetSubCell(uint row, uint col, out ICell cell)
|
|||
|
|
{
|
|||
|
|
uint globalRow = _rowStart + row - 1;
|
|||
|
|
uint globalCol = _colStart + col - 1;
|
|||
|
|
if (globalRow > _rowEnd || globalCol > _colEnd)
|
|||
|
|
{
|
|||
|
|
cell = default!;
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
cell = new ExcelCell(_writer, _sheet, globalRow, globalCol);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool GetSubCell(uint row, string col, out ICell cell)
|
|||
|
|
{
|
|||
|
|
uint colIndex = CellAddressHelper.ColumnLetterToIndex(col);
|
|||
|
|
return GetSubCell(row, colIndex, out cell);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, Action<ICell> edit)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
edit(cell);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, string value)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, bool value)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, string formula, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(formula, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, DateTime value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, decimal value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, double value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, float value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, int value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, uint col, long value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, Action<ICell> edit)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
edit(cell);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, string value)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, bool value)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, string formula, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(formula, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, DateTime value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, decimal value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, double value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, float value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, int value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public bool TryEditSubCell(uint row, string col, long value, NumberFormatPattern? format = null)
|
|||
|
|
{
|
|||
|
|
if (GetSubCell(row, col, out var cell))
|
|||
|
|
{
|
|||
|
|
cell.Set(value, format);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
public void ClearContents()
|
|||
|
|
{
|
|||
|
|
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void ClearFormats()
|
|||
|
|
{
|
|||
|
|
for (uint r = _rowStart; r <= _rowEnd; r++)
|
|||
|
|
for (uint c = _colStart; c <= _colEnd; c++)
|
|||
|
|
{
|
|||
|
|
var cell = GetCellInternal(r, c);
|
|||
|
|
cell?.StyleIndex = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Clear()
|
|||
|
|
{
|
|||
|
|
ClearContents();
|
|||
|
|
ClearFormats();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IRange CopyTo(uint rowIndex, uint colIndex, out IRange copiedRange)
|
|||
|
|
{
|
|||
|
|
_writer.ThrowIfDisposed();
|
|||
|
|
lock (_writer._syncLock)
|
|||
|
|
{
|
|||
|
|
CopyData(_rowStart, _colStart, _rowEnd, _colEnd, rowIndex, colIndex);
|
|||
|
|
copiedRange = new ExcelRange(_writer, _sheet, rowIndex, colIndex, rowIndex + Rows - 1, colIndex + Cols - 1);
|
|||
|
|
return this;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public IRange CopyTo(uint rowIndex, string colIndex, out IRange copiedRange)
|
|||
|
|
{
|
|||
|
|
uint col = CellAddressHelper.ColumnLetterToIndex(colIndex);
|
|||
|
|
return CopyTo(rowIndex, col, out copiedRange);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Вспомогательные методы
|
|||
|
|
|
|||
|
|
private Cell? GetCellInternal(uint row, uint col)
|
|||
|
|
{
|
|||
|
|
var sheetData = _sheet.GetSheetData();
|
|||
|
|
var rowElement = FindRowElement(sheetData, row);
|
|||
|
|
if (rowElement == null) return null;
|
|||
|
|
return FindCellInRow(rowElement, col);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void CopyData(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd, uint dstRowStart, uint dstColStart)
|
|||
|
|
{
|
|||
|
|
// Сохраняем все значения и форматы из исходного диапазона
|
|||
|
|
var cellsData = new List<(uint row, uint col, Cell cell)>();
|
|||
|
|
for (uint r = srcRowStart; r <= srcRowEnd; r++)
|
|||
|
|
{
|
|||
|
|
for (uint c = srcColStart; c <= srcColEnd; c++)
|
|||
|
|
{
|
|||
|
|
var cell = GetCellInternal(r, c);
|
|||
|
|
if (cell != null)
|
|||
|
|
{
|
|||
|
|
var cloned = (Cell)cell.CloneNode(true);
|
|||
|
|
cellsData.Add((r - srcRowStart + dstRowStart, c - srcColStart + dstColStart, cloned));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Вставляем в новый диапазон
|
|||
|
|
foreach (var (row, col, clonedCell) in cellsData)
|
|||
|
|
{
|
|||
|
|
InsertCellAt(row, col, clonedCell);
|
|||
|
|
}
|
|||
|
|
// Копируем ширины столбцов
|
|||
|
|
for (uint c = srcColStart; c <= srcColEnd; c++)
|
|||
|
|
{
|
|||
|
|
var srcCol = GetColumnElementForIndex(c);
|
|||
|
|
if (srcCol?.Width is { } width)
|
|||
|
|
{
|
|||
|
|
uint targetCol = c - srcColStart + dstColStart;
|
|||
|
|
var dstCol = GetOrCreateColumnElementForIndex(targetCol);
|
|||
|
|
dstCol.Width = width;
|
|||
|
|
dstCol.CustomWidth = srcCol.CustomWidth;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Копируем высоты строк
|
|||
|
|
for (uint r = srcRowStart; r <= srcRowEnd; r++)
|
|||
|
|
{
|
|||
|
|
var srcRow = FindRowElement(_sheet.GetSheetData(), r);
|
|||
|
|
if (srcRow is { } row
|
|||
|
|
&& row.Height?.Value is { } hei
|
|||
|
|
&& row.CustomHeight?.Value == true)
|
|||
|
|
{
|
|||
|
|
uint targetRow = r - srcRowStart + dstRowStart;
|
|||
|
|
var dstRow = GetOrCreateRowElement(targetRow);
|
|||
|
|
dstRow.Height = hei;
|
|||
|
|
dstRow.CustomHeight = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private Row GetOrCreateRowElement(uint rowIndex)
|
|||
|
|
{
|
|||
|
|
var existing = FindRowElement(_sheet.GetSheetData(), rowIndex);
|
|||
|
|
if (existing != null) return existing;
|
|||
|
|
var newRow = new Row { RowIndex = rowIndex };
|
|||
|
|
InsertRowElement(_sheet.GetSheetData(), newRow, rowIndex);
|
|||
|
|
return newRow;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 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 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 Column? GetColumnElementForIndex(uint col)
|
|||
|
|
{
|
|||
|
|
var worksheet = _sheet.Worksheet;
|
|||
|
|
var cols = worksheet.GetFirstChild<Columns>();
|
|||
|
|
if (cols == null) return null;
|
|||
|
|
foreach (Column c in cols.Elements<Column>())
|
|||
|
|
if (c.Min?.Value is { } min
|
|||
|
|
&& c.Max?.Value is { } max
|
|||
|
|
&& min <= col
|
|||
|
|
&& max >= col)
|
|||
|
|
return c;
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private Column GetOrCreateColumnElementForIndex(uint col)
|
|||
|
|
{
|
|||
|
|
var existing = GetColumnElementForIndex(col);
|
|||
|
|
if (existing != null) return existing;
|
|||
|
|
var worksheet = _sheet.Worksheet;
|
|||
|
|
var cols = worksheet.GetFirstChild<Columns>();
|
|||
|
|
if (cols == null)
|
|||
|
|
{
|
|||
|
|
cols = new Columns();
|
|||
|
|
worksheet.InsertAt(cols, 0);
|
|||
|
|
}
|
|||
|
|
var newCol = new Column
|
|||
|
|
{
|
|||
|
|
Min = col,
|
|||
|
|
Max = col,
|
|||
|
|
Width = 8.43,
|
|||
|
|
CustomWidth = true
|
|||
|
|
};
|
|||
|
|
cols.Append(newCol);
|
|||
|
|
return newCol;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// создаёт стиль только с числовым форматом
|
|||
|
|
private int GetOrCreateNumberFormatId(NumberFormatPattern format)
|
|||
|
|
{
|
|||
|
|
return _writer.GetOrCreateCellFormatId(numberFormat: format);
|
|||
|
|
}
|
|||
|
|
}
|