Files
QWERTYkez.OpenXmlProcessors/QWERTYkez.ExcelProcessor/Editors/ExcelColumn.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

624 lines
18 KiB
C#

namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Внутренняя реализация <see cref="IColumn"/>.
/// </summary>
internal sealed class ExcelColumn : IColumn
{
internal readonly ExcelWriter _writer;
internal readonly ExcelSheet _sheet;
internal uint _colIndex;
internal ExcelColumn(ExcelWriter writer, ExcelSheet sheet, uint colIndex)
{
_writer = writer;
_sheet = sheet;
_colIndex = colIndex;
}
private CellStyle? GetColumnStyle(Column columnElement)
{
if (columnElement.Style?.Value is not uint styleIndex)
return null;
return _writer.GetCellStyle(styleIndex);
}
private void ApplyStyleToColumn(CellStyle style)
{
var columnElement = GetOrCreateColumnElement();
int styleIndex = _writer.GetOrCreateStyleId(style);
columnElement.Style = (uint)styleIndex;
// Принудительно устанавливаем customStyle через атрибут
if (!columnElement.ExtendedAttributes.Any(attr => attr.LocalName == "customStyle" && attr.Value == "1"))
columnElement.SetAttribute(new OpenXmlAttribute("customStyle", "", "1"));
}
internal static Column? GetColumnElementInternal(ExcelSheet sheet, uint colIndex)
{
var cols = sheet.Worksheet.GetFirstChild<Columns>();
if (cols == null) return null;
foreach (Column col in cols.Elements<Column>())
{
if (col.Min?.Value is { } min && col.Max?.Value is { } max && min <= colIndex && max >= colIndex)
return col;
}
return null;
}
/// <inheritdoc />
public IColumn Set(NumberFormatPattern format)
{
if (format == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var columnElement = GetOrCreateColumnElement();
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
if (currentStyle.TryMerge(format, out var newStyle))
{
ApplyStyleToColumn(newStyle);
ApplyStyleToColumnCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IColumn Set(CellAlign align)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var columnElement = GetOrCreateColumnElement();
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
if (currentStyle.TryMerge(align, out var newStyle))
{
ApplyStyleToColumn(newStyle);
ApplyStyleToColumnCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IColumn Set(CellBorder border)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var columnElement = GetOrCreateColumnElement();
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
if (currentStyle.TryMerge(border, out var newStyle))
{
ApplyStyleToColumn(newStyle);
ApplyStyleToColumnCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IColumn Set(CellFill fill)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var columnElement = GetOrCreateColumnElement();
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
if (currentStyle.TryMerge(fill, out var newStyle))
{
ApplyStyleToColumn(newStyle);
ApplyStyleToColumnCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IColumn Set(CellFont font)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var columnElement = GetOrCreateColumnElement();
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
if (currentStyle.TryMerge(font, out var newStyle))
{
ApplyStyleToColumn(newStyle);
ApplyStyleToColumnCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IColumn Set(CellStyle style)
{
if (style == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var columnElement = GetOrCreateColumnElement();
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
if (currentStyle.TryMerge(style, out var newStyle))
{
ApplyStyleToColumn(newStyle);
ApplyStyleToColumnCells(newStyle);
}
return this;
}
}
private void ApplyStyleToColumnCells(CellStyle style)
{
var sheetData = _sheet.GetSheetData();
foreach (var rowElement in sheetData.Elements<Row>())
{
var cellElement = FindCellInRow(rowElement, _colIndex);
if (cellElement != null)
{
var cell = new ExcelCell(_writer, _sheet, rowElement.RowIndex!.Value, _colIndex);
cell.Set(style);
}
}
}
public uint Index => _colIndex;
public string IndexLetter => NumberToColumnLetter(_colIndex);
/// <summary>Устанавливает ширину столбца без создания объекта IColumn.</summary>
internal static void SetWidth(ExcelWriter writer, ExcelSheet sheet, uint colIndex, ColumnWidth width)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var col = GetOrCreateColumnElementInternal(sheet, colIndex);
double widthInChars;
if (width.IsCharacterUnit)
{
// Если ширина задана в символах, используем напрямую
widthInChars = width.CharacterValue;
}
else
{
double targetPoints = width.GetTargetPoints();
widthInChars = writer.TryGetCalibrateCoeff(targetPoints, out var closestCw)
? closestCw : targetPoints / ColumnWidth.DefaultPointsPerChar;
}
col.Width = widthInChars;
col.CustomWidth = true;
}
}
/// <summary>Возвращает ширину столбца.</summary>
internal static ColumnWidth GetWidth(ExcelWriter writer, ExcelSheet sheet, uint colIndex)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var col = GetColumnElementInternal(sheet, colIndex);
if (col is Column column
&& column.CustomWidth?.Value == true
&& column.Width?.Value is { } wid)
return ColumnWidth.FromCharacters(wid);
return ColumnWidth.FromCharacters(8.43); // стандартная ширина
}
}
static Column GetOrCreateColumnElementInternal(ExcelSheet sheet, uint colIndex)
{
var existing = GetColumnElementInternal(sheet, colIndex);
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 = colIndex,
Max = colIndex,
Width = 8.43,
CustomWidth = true
};
cols.Append(newCol);
return newCol;
}
public IColumn MoveTo(uint index)
{
if (index == _colIndex) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
// Копируем данные в новую позицию
CopyColumnData(_colIndex, index);
// Очищаем исходный столбец
ClearColumnData(_colIndex);
_colIndex = index;
}
return this;
}
public IColumn MoveTo(string index)
{
uint idx = CellAddressHelper.ColumnLetterToIndex(index);
return MoveTo(idx);
}
public ColumnWidth Width
{
get
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var column = GetColumnElement();
if (column is Column col && col.Width?.Value is { } val && col.CustomWidth?.Value == true)
return ColumnWidth.FromCharacters(val);
return ColumnWidth.FromCharacters(8.43); // стандартная ширина
}
}
set
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var col = GetOrCreateColumnElement();
double widthInChars;
if (value.IsCharacterUnit)
{
// Если ширина задана в символах, используем напрямую
widthInChars = value.CharacterValue;
}
else
{
double targetCm = value.GetTargetCentimeters();
widthInChars = _writer.TryGetCalibrateCoeff(targetCm, out var closestCw)
? closestCw : targetCm * (ColumnWidth.DefaultPointsPerChar / 28.3464566929);
}
col.Width = widthInChars;
col.CustomWidth = true;
}
}
}
public ICell Cell(uint row)
{
return new ExcelCell(_writer, _sheet, row, _colIndex);
}
public IColumn Cell(uint row, Action<ICell> edit)
{
var cell = Cell(row);
edit(cell);
return this;
}
public IColumn Cell(uint row, string value)
{
Cell(row).Set(value); return this;
}
public IColumn Cell(uint row, bool value)
{
Cell(row).Set(value); return this;
}
public IColumn Cell(uint row, string value, NumberFormatPattern? format = null)
{
Cell(row).SetFormula(value, format); return this;
}
public IColumn Cell(uint row, DateTime value, NumberFormatPattern? format = null)
{
Cell(row).Set(value, format); return this;
}
public IColumn Cell(uint row, decimal value, NumberFormatPattern? format = null)
{
Cell(row).Set(value, format); return this;
}
public IColumn Cell(uint row, double value, NumberFormatPattern? format = null)
{
Cell(row).Set(value, format); return this;
}
public IColumn Cell(uint row, float value, NumberFormatPattern? format = null)
{
Cell(row).Set(value, format); return this;
}
public IColumn Cell(uint row, int value, NumberFormatPattern? format = null)
{
Cell(row).Set(value, format); return this;
}
public IColumn Cell(uint row, long value, NumberFormatPattern? format = null)
{
Cell(row).Set(value, format); return this;
}
public void ClearContents()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
ClearColumnData(_colIndex);
}
}
public void ClearFormats()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var sheetData = _sheet.GetSheetData();
foreach (var row in sheetData.Elements<Row>())
{
var cell = FindCellInRow(row, _colIndex);
cell?.StyleIndex = null;
}
}
}
public void Clear()
{
ClearContents();
ClearFormats();
}
public void Remove()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
// Очищаем данные
ClearColumnData(_colIndex);
// Удаляем элемент Column, если он существует
DeleteColumnElement();
}
}
public IColumn CopyTo(uint index, out IColumn copiedColumn)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
CopyColumnData(_colIndex, index);
copiedColumn = new ExcelColumn(_writer, _sheet, index);
return this;
}
}
public IColumn CopyTo(string index, out IColumn copiedColumn)
{
uint idx = CellAddressHelper.ColumnLetterToIndex(index);
return CopyTo(idx, out copiedColumn);
}
// Вспомогательные методы
Column? GetColumnElement()
{
var cols = _sheet.Worksheet.GetFirstChild<Columns>();
if (cols == null) return null;
foreach (Column col in cols.Elements<Column>())
{
if (col?.Min?.Value is { } min
&& col?.Max?.Value is { } max
&& min <= _colIndex
&& max >= _colIndex)
return col;
}
return null;
}
Column GetOrCreateColumnElement()
{
var existing = GetColumnElement();
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 = _colIndex,
Max = _colIndex,
Width = 8.43,
CustomWidth = true
};
cols.Append(newCol);
return newCol;
}
void DeleteColumnElement()
{
var col = GetColumnElement();
col?.Remove();
}
void CopyColumnData(uint sourceCol, uint targetCol)
{
if (sourceCol == targetCol) return;
var sheetData = _sheet.GetSheetData();
// Сначала удаляем существующие данные в целевом столбце (если нужно перезаписать)
ClearColumnData(targetCol);
// Копируем значения и форматы из sourceCol в targetCol
foreach (var row in sheetData.Elements<Row>())
{
var sourceCell = FindCellInRow(row, sourceCol);
if (sourceCell != null)
{
var targetCell = FindCellInRow(row, targetCol);
if (targetCell == null)
{
targetCell = new Cell();
// Вставляем в нужное место (по порядку столбцов)
InsertCellInRow(row, targetCell, targetCol);
}
// Копируем содержимое и стиль
targetCell.DataType = sourceCell.DataType;
targetCell.CellValue = sourceCell.CellValue?.CloneNode(true) as CellValue;
targetCell.CellFormula = sourceCell.CellFormula?.CloneNode(true) as CellFormula;
targetCell.StyleIndex = sourceCell.StyleIndex;
// Если есть InlineString, клонируем
if (sourceCell.InlineString != null)
targetCell.InlineString = (InlineString)sourceCell.InlineString.CloneNode(true);
}
}
// Копируем ширину столбца
var sourceColElem = GetColumnElementForIndex(sourceCol);
if (sourceColElem?.Width is { } width)
{
var targetColElem = GetOrCreateColumnElementForIndex(targetCol);
targetColElem.Width = width;
targetColElem.CustomWidth = sourceColElem.CustomWidth;
}
}
void ClearColumnData(uint col)
{
var sheetData = _sheet.GetSheetData();
foreach (var row in sheetData.Elements<Row>().ToList())
{
var cell = FindCellInRow(row, col);
cell?.Remove();
}
}
Cell? FindCellInRow(Row row, uint colIndex)
{
foreach (var cell in row.Elements<Cell>())
{
string cellRef = cell.CellReference?.Value ?? string.Empty;
if (TryParseCellReference(cellRef, out uint _, out uint colIdx) && colIdx == colIndex)
return cell;
}
return null;
}
void InsertCellInRow(Row row, Cell cell, uint colIndex)
{
string newRef = NumberToColumnLetter(colIndex) + (row.RowIndex?.Value ?? 1).ToString();
cell.CellReference = newRef;
// Вставляем в правильном порядке (по возрастанию столбцов)
bool inserted = false;
foreach (var existing in row.Elements<Cell>().ToList())
{
if (TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) &&
existingCol > colIndex)
{
existing.InsertBeforeSelf(cell);
inserted = true;
break;
}
}
if (!inserted)
row.Append(cell);
}
Column? GetColumnElementForIndex(uint col)
{
var cols = _sheet.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;
}
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;
}
int GetOrCreateNumberFormatId(NumberFormatPattern format)
{
return _writer.GetOrCreateCellFormatId(numberFormat: format);
}
static bool TryParseCellReference(string reference, out uint row, out uint col)
{
row = 0; col = 0;
if (string.IsNullOrEmpty(reference)) return false;
int i = 0;
while (i < reference.Length && char.IsLetter(reference[i])) i++;
if (i == 0) return false;
string colPart = reference.Substring(0, i);
string rowPart = reference.Substring(i);
if (!uint.TryParse(rowPart, out row)) return false;
col = CellAddressHelper.ColumnLetterToIndex(colPart);
return true;
}
static string NumberToColumnLetter(uint col)
{
if (col == 0) throw new ArgumentException("Column number must be > 0");
string result = "";
while (col > 0)
{
col--;
result = (char)('A' + (col % 26)) + result;
col /= 26;
}
return result;
}
}