3 Commits

Author SHA1 Message Date
melekhin
5302edfb8f borders 2026-06-19 16:35:17 +07:00
melekhin
e373d4108a many debugs
All checks were successful
Publish NuGet packages / publish (push) Successful in 28s
2026-06-19 15:06:40 +07:00
melekhin
08b39b7bfe Sheet.TryMergeBy...()
All checks were successful
Publish NuGet packages / publish (push) Successful in 23s
2026-06-17 10:39:00 +07:00
24 changed files with 1676 additions and 651 deletions

View File

@@ -2,8 +2,8 @@
internal static class CellAddressHelper internal static class CellAddressHelper
{ {
private const string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; const string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static readonly uint[] _powers = [1, 26, 676]; static readonly uint[] _powers = [1, 26, 676];
public static uint ColumnLetterToIndex(string col) public static uint ColumnLetterToIndex(string col)
{ {

View File

@@ -18,6 +18,30 @@ public readonly struct CellAlign : IEquatable<CellAlign>
/// <summary>Уменьшать размер шрифта, чтобы текст поместился в ячейку.</summary> /// <summary>Уменьшать размер шрифта, чтобы текст поместился в ячейку.</summary>
public bool? ShrinkToFit { get; init; } public bool? ShrinkToFit { get; init; }
internal bool TryMerge(CellAlign other, out CellAlign result)
{
// Если other не содержит новых значений, возвращаем this
if (other.Horizontal == Horizontal &&
other.Vertical == Vertical &&
other.WrapText == WrapText &&
other.ShrinkToFit == ShrinkToFit)
{
result = default;
return false;
}
result = new CellAlign
{
Horizontal = other.Horizontal ?? Horizontal,
Vertical = other.Vertical ?? Vertical,
WrapText = other.WrapText ?? WrapText,
ShrinkToFit = other.ShrinkToFit ?? ShrinkToFit
};
return true;
}
/// <summary>Преобразует горизонтальное выравнивание в тип Open XML.</summary> /// <summary>Преобразует горизонтальное выравнивание в тип Open XML.</summary>
public bool TryGetExcelHorizontalAlignment(out HorizontalAlignmentValues value) public bool TryGetExcelHorizontalAlignment(out HorizontalAlignmentValues value)
{ {
@@ -104,7 +128,7 @@ public readonly struct CellAlign : IEquatable<CellAlign>
return result; return result;
} }
private static CellAlignHorizontal MapHorizontalFromExcel(HorizontalAlignmentValues value) static CellAlignHorizontal MapHorizontalFromExcel(HorizontalAlignmentValues value)
{ {
if (value == HorizontalAlignmentValues.Left) if (value == HorizontalAlignmentValues.Left)
{ {
@@ -137,7 +161,7 @@ public readonly struct CellAlign : IEquatable<CellAlign>
else throw new NotSupportedException($"Unsupported horizontal alignment: {value}"); else throw new NotSupportedException($"Unsupported horizontal alignment: {value}");
} }
private static CellAlignVertical MapVerticalFromExcel(VerticalAlignmentValues value) static CellAlignVertical MapVerticalFromExcel(VerticalAlignmentValues value)
{ {
if (value == VerticalAlignmentValues.Top) if (value == VerticalAlignmentValues.Top)
{ {

View File

@@ -1,11 +1,53 @@
namespace QWERTYkez.ExcelProcessor; namespace QWERTYkez.ExcelProcessor;
/// <summary> /// <summary>
/// Определяет границы ячейки: верхнюю, нижнюю, левую, правую и диагональные. /// Определяет границы ячейки: верхнюю, нижнюю, левую, правую и диагональные.
/// Каждая граница может иметь стиль и цвет. /// Каждая граница может иметь стиль и цвет.
/// </summary> /// </summary>
public readonly struct CellBorder : IEquatable<CellBorder> public readonly struct CellBorder : IEquatable<CellBorder>
{ {
public static CellBorder BottomThin { get; } = new() { BottomBorder = BorderSide.BlackThin };
public static CellBorder TopThin { get; } = new() { TopBorder = BorderSide.BlackThin };
public static CellBorder LeftThin { get; } = new() { LeftBorder = BorderSide.BlackThin };
public static CellBorder RightThin { get; } = new() { RightBorder = BorderSide.BlackThin };
public static CellBorder AllThin { get; } = new()
{
BottomBorder = BorderSide.BlackThin,
TopBorder = BorderSide.BlackThin,
LeftBorder = BorderSide.BlackThin,
RightBorder = BorderSide.BlackThin
};
public static CellBorder BottomMedium { get; } = new() { BottomBorder = BorderSide.BlackMedium };
public static CellBorder TopMedium { get; } = new() { TopBorder = BorderSide.BlackMedium };
public static CellBorder LeftMedium { get; } = new() { LeftBorder = BorderSide.BlackMedium };
public static CellBorder RightMedium { get; } = new() { RightBorder = BorderSide.BlackMedium };
public static CellBorder AllMedium { get; } = new()
{
BottomBorder = BorderSide.BlackMedium,
TopBorder = BorderSide.BlackMedium,
LeftBorder = BorderSide.BlackMedium,
RightBorder = BorderSide.BlackMedium
};
public static CellBorder BottomThick { get; } = new() { BottomBorder = BorderSide.BlackThick };
public static CellBorder TopThick { get; } = new() { TopBorder = BorderSide.BlackThick };
public static CellBorder LeftThick { get; } = new() { LeftBorder = BorderSide.BlackThick };
public static CellBorder RightThick { get; } = new() { RightBorder = BorderSide.BlackThick };
public static CellBorder AllThick { get; } = new()
{
BottomBorder = BorderSide.BlackThick,
TopBorder = BorderSide.BlackThick,
LeftBorder = BorderSide.BlackThick,
RightBorder = BorderSide.BlackThick
};
/// <summary>Верхняя граница.</summary> /// <summary>Верхняя граница.</summary>
public BorderSide? TopBorder { get; init; } public BorderSide? TopBorder { get; init; }
@@ -24,6 +66,33 @@ public readonly struct CellBorder : IEquatable<CellBorder>
/// <summary>Диагональная граница «из левого нижнего в правый верхний» (//).</summary> /// <summary>Диагональная граница «из левого нижнего в правый верхний» (//).</summary>
public BorderSide? DiagonalRight { get; init; } public BorderSide? DiagonalRight { get; init; }
public bool TryMerge(CellBorder other, out CellBorder result)
{
if (other.TopBorder == TopBorder &&
other.BottomBorder == BottomBorder &&
other.LeftBorder == LeftBorder &&
other.RightBorder == RightBorder &&
other.DiagonalLeft == DiagonalLeft &&
other.DiagonalRight == DiagonalRight)
{
result = default;
return false;
}
result = new CellBorder
{
TopBorder = other.TopBorder ?? TopBorder,
BottomBorder = other.BottomBorder ?? BottomBorder,
LeftBorder = other.LeftBorder ?? LeftBorder,
RightBorder = other.RightBorder ?? RightBorder,
DiagonalLeft = other.DiagonalLeft ?? DiagonalLeft,
DiagonalRight = other.DiagonalRight ?? DiagonalRight
};
return true;
}
/// <summary>Создаёт элемент Border для Open XML.</summary> /// <summary>Создаёт элемент Border для Open XML.</summary>
public Border? ToBorder() public Border? ToBorder()
{ {
@@ -112,11 +181,23 @@ public readonly struct CellBorder : IEquatable<CellBorder>
/// <summary>Стиль и цвет границы.</summary> /// <summary>Стиль и цвет границы.</summary>
public readonly struct BorderSide : IEquatable<BorderSide> public readonly struct BorderSide : IEquatable<BorderSide>
{ {
/// <summary>Тонкая черная линия</summary>
public static BorderSide BlackThin { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Thin };
/// <summary>Толстая черная линия</summary>
public static BorderSide BlackThick { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Thick };
/// <summary>Средняя черная линия</summary>
public static BorderSide BlackMedium { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Medium };
/// <summary>Стиль линии границы.</summary> /// <summary>Стиль линии границы.</summary>
public BorderStyle? Style { get; init; } public BorderStyle? Style { get; init; }
/// <summary>Цвет границы.</summary> /// <summary>Цвет границы.</summary>
public ExColor? Color { get; init; } public System.Drawing.Color? Color { get; init; }
internal T ToBorderElement<T>() where T : BorderPropertiesType, new() internal T ToBorderElement<T>() where T : BorderPropertiesType, new()
{ {
@@ -141,9 +222,9 @@ public readonly struct BorderSide : IEquatable<BorderSide>
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
} }
if (Color.HasValue && Color.Value.Color.HasValue) if (Color.HasValue)
{ {
var c = Color.Value.Color.Value; var c = Color.Value;
element.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" }; element.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" };
} }
return element; return element;
@@ -162,17 +243,18 @@ public readonly struct BorderSide : IEquatable<BorderSide>
} }
if (borderElement.Color?.Rgb?.Value is { } rgb && rgb.Length >= 6) if (borderElement.Color?.Rgb?.Value is { } rgb && rgb.Length >= 6)
{ {
var color = System.Drawing.Color.FromArgb( result = result with
{
Color = System.Drawing.Color.FromArgb(
Convert.ToByte(rgb.Substring(0, 2), 16), Convert.ToByte(rgb.Substring(0, 2), 16),
Convert.ToByte(rgb.Substring(2, 2), 16), Convert.ToByte(rgb.Substring(2, 2), 16),
Convert.ToByte(rgb.Substring(4, 2), 16) Convert.ToByte(rgb.Substring(4, 2), 16))
); };
result = result with { Color = new ExColor(color) };
} }
return result; return result;
} }
private static BorderStyle MapBorderStyleFromExcel(BorderStyleValues value) static BorderStyle MapBorderStyleFromExcel(BorderStyleValues value)
{ {
if (value == BorderStyleValues.Thin) if (value == BorderStyleValues.Thin)
{ {
@@ -279,3 +361,24 @@ public enum BorderStyle
/// <summary> Наклонная штрих-пунктирная (для диагональных) </summary> /// <summary> Наклонная штрих-пунктирная (для диагональных) </summary>
SlantDashDot, SlantDashDot,
} }
/// <summary>
/// Определяет, какие границы диапазона следует применить.
/// </summary>
public enum BorderTarget
{
/// <summary>Все границы (внешние и внутренние) полная сетка.</summary>
All,
/// <summary>Только внешние границы диапазона.</summary>
Outside,
/// <summary>Только внутренние границы (между ячейками).</summary>
Inside,
/// <summary>Только верхняя граница диапазона.</summary>
Top,
/// <summary>Только нижняя граница диапазона.</summary>
Bottom,
/// <summary>Только левая граница диапазона.</summary>
Left,
/// <summary>Только правая граница диапазона.</summary>
Right
}

View File

@@ -6,15 +6,13 @@
public readonly struct CellFill : IEquatable<CellFill> public readonly struct CellFill : IEquatable<CellFill>
{ {
/// <summary>Цвет фона.</summary> /// <summary>Цвет фона.</summary>
public ExColor? BackgroundColor { get; init; } public System.Drawing.Color? BackgroundColor { get; init; }
/// <summary>Создаёт элемент Fill для Open XML.</summary> /// <summary>Создаёт элемент Fill для Open XML.</summary>
public Fill? ToFill() public Fill? ToFill()
{ {
if (!BackgroundColor.HasValue || !BackgroundColor.Value.Color.HasValue) if (BackgroundColor is not { } c) return null;
return null;
var c = BackgroundColor.Value.Color.Value;
var fill = new Fill var fill = new Fill
{ {
PatternFill = new PatternFill PatternFill = new PatternFill
@@ -37,7 +35,7 @@ public readonly struct CellFill : IEquatable<CellFill>
Convert.ToByte(rgb.Substring(2, 2), 16), Convert.ToByte(rgb.Substring(2, 2), 16),
Convert.ToByte(rgb.Substring(4, 2), 16) Convert.ToByte(rgb.Substring(4, 2), 16)
); );
return new CellFill { BackgroundColor = new ExColor(color) }; return new CellFill { BackgroundColor = color };
} }
public override bool Equals(object? obj) => obj is CellFill other && Equals(other); public override bool Equals(object? obj) => obj is CellFill other && Equals(other);

View File

@@ -13,7 +13,7 @@ public readonly struct CellFont : IEquatable<CellFont>
public string? FontFamily { get; init; } public string? FontFamily { get; init; }
/// <summary>Цвет текста.</summary> /// <summary>Цвет текста.</summary>
public ExColor? FontColor { get; init; } public System.Drawing.Color? FontColor { get; init; }
/// <summary>Жирное начертание.</summary> /// <summary>Жирное начертание.</summary>
public bool? IsBold { get; init; } public bool? IsBold { get; init; }
@@ -27,6 +27,35 @@ public readonly struct CellFont : IEquatable<CellFont>
/// <summary>Зачёркивание.</summary> /// <summary>Зачёркивание.</summary>
public bool? IsStrike { get; init; } public bool? IsStrike { get; init; }
internal bool TryMerge(CellFont other, out CellFont result)
{
if (other.FontSize == FontSize &&
other.FontFamily == FontFamily &&
other.FontColor == FontColor &&
other.IsBold == IsBold &&
other.IsItalic == IsItalic &&
other.IsUnderline == IsUnderline &&
other.IsStrike == IsStrike)
{
result = default;
return false;
}
result = new CellFont
{
FontSize = other.FontSize ?? FontSize,
FontFamily = other.FontFamily ?? FontFamily,
FontColor = other.FontColor ?? FontColor,
IsBold = other.IsBold ?? IsBold,
IsItalic = other.IsItalic ?? IsItalic,
IsUnderline = other.IsUnderline ?? IsUnderline,
IsStrike = other.IsStrike ?? IsStrike
};
return true;
}
/// <summary>Создаёт элемент Font для Open XML.</summary> /// <summary>Создаёт элемент Font для Open XML.</summary>
public Font? ToFont() public Font? ToFont()
{ {
@@ -39,11 +68,8 @@ public readonly struct CellFont : IEquatable<CellFont>
font.FontSize = new FontSize { Val = FontSize.Value }; font.FontSize = new FontSize { Val = FontSize.Value };
if (FontFamily is not null) if (FontFamily is not null)
font.FontName = new FontName { Val = FontFamily }; font.FontName = new FontName { Val = FontFamily };
if (FontColor.HasValue && FontColor.Value.Color.HasValue) if (FontColor is { } c)
{
var c = FontColor.Value.Color.Value;
font.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" }; font.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" };
}
if (IsBold == true) font.Bold = new Bold(); if (IsBold == true) font.Bold = new Bold();
if (IsItalic == true) font.Italic = new Italic(); if (IsItalic == true) font.Italic = new Italic();
if (IsUnderline == true) font.Underline = new Underline(); if (IsUnderline == true) font.Underline = new Underline();
@@ -75,7 +101,7 @@ public readonly struct CellFont : IEquatable<CellFont>
Convert.ToByte(rgb.Substring(2, 2), 16), Convert.ToByte(rgb.Substring(2, 2), 16),
Convert.ToByte(rgb.Substring(4, 2), 16) Convert.ToByte(rgb.Substring(4, 2), 16)
); );
result = result with { FontColor = new ExColor(color) }; result = result with { FontColor = color };
} }
return result; return result;
} }

View File

@@ -0,0 +1,153 @@
namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Неизменяемый набор всех параметров оформления ячейки.
/// </summary>
public sealed record CellStyle
{
public CellAlign? Align { get; init; }
public CellFont? Font { get; init; }
public CellFill? Fill { get; init; }
public CellBorder? Border { get; init; }
public NumberFormatPattern? NumberFormat { get; init; }
public bool IsEmpty() =>
Align == null && Font == null && Fill == null && Border == null && NumberFormat == null;
internal bool TryMerge(NumberFormatPattern format, out CellStyle result)
{
result = this;
if (format is not null)
{
if (!Equals(NumberFormat, format))
{
result = result with { NumberFormat = format };
return true;
}
}
return false;
}
internal bool TryMerge(CellAlign align, out CellStyle result)
{
result = this;
var current = Align ?? new CellAlign();
if (current.TryMerge(align, out align))
{
result = result with { Align = align };
return true;
}
return false;
}
internal bool TryMerge(CellBorder border, out CellStyle result)
{
result = this;
var current = Border ?? new CellBorder();
if (current.TryMerge(border, out border))
{
result = result with { Border = border };
return true;
}
return false;
}
internal bool TryMerge(CellFill fill, out CellStyle result)
{
result = this;
var current = Fill ?? new CellFill();
if (!current.Equals(fill))
{
result = result with { Fill = fill };
return true;
}
return false;
}
internal bool TryMerge(CellFont font, out CellStyle result)
{
result = this;
var current = Font ?? new CellFont();
if (!current.Equals(font))
{
result = result with { Font = font };
return true;
}
return false;
}
internal CellStyle Merge(CellStyle other)
{
if (other == null) return this;
return new CellStyle
{
Align = other.Align ?? Align,
Font = other.Font ?? Font,
Fill = other.Fill ?? Fill,
Border = other.Border ?? Border,
NumberFormat = other.NumberFormat ?? NumberFormat
};
}
internal bool TryMerge(CellStyle other, out CellStyle result)
{
result = this;
bool changed = false;
// Объединяем Align, если есть
if (other.Align is not null)
{
var currentAlign = Align ?? new CellAlign();
if (currentAlign.TryMerge(other.Align.Value, out var newAlign))
{
result = result with { Align = newAlign };
changed = true;
}
}
// Аналогично для Font
if (other.Font is not null)
{
var currentFont = Font ?? new CellFont();
if (currentFont.TryMerge(other.Font.Value, out var newFont))
{
result = result with { Font = newFont };
changed = true;
}
}
// Для Fill просто заменяем, если он задан (нет nullable-полей)
if (other.Fill is not null)
{
if (!Fill.Equals(other.Fill))
{
result = result with { Fill = other.Fill };
changed = true;
}
}
// Border
if (other.Border is not null)
{
var currentBorder = Border ?? new CellBorder();
if (currentBorder.TryMerge(other.Border.Value, out var newBorder))
{
result = result with { Border = newBorder };
changed = true;
}
}
// NumberFormat
if (other.NumberFormat is not null)
{
if (!Equals(NumberFormat, other.NumberFormat))
{
result = result with { NumberFormat = other.NumberFormat };
changed = true;
}
}
return changed;
}
}

View File

@@ -6,15 +6,15 @@
/// </summary> /// </summary>
public readonly struct ColumnWidth public readonly struct ColumnWidth
{ {
private readonly double _rawValue; readonly double _rawValue;
private readonly UnitType _unit; readonly UnitType _unit;
private enum UnitType { Characters, Points, Centimeters, Millimeters } enum UnitType { Characters, Points, Centimeters, Millimeters }
/// <summary>Коэффициент перевода символов в пункты по умолчанию (используется, если нет калибровочной таблицы).</summary> /// <summary>Коэффициент перевода символов в пункты по умолчанию (используется, если нет калибровочной таблицы).</summary>
public static double DefaultPointsPerChar { get; set; } = 5.65; public static double DefaultPointsPerChar { get; set; } = 5.65;
private ColumnWidth(double value, UnitType unit) ColumnWidth(double value, UnitType unit)
{ {
_rawValue = value; _rawValue = value;
_unit = unit; _unit = unit;

View File

@@ -1,82 +0,0 @@
namespace QWERTYkez.ExcelProcessor;
public readonly struct ExColor(System.Drawing.Color? Color)
{
private readonly System.Drawing.Color? color = Color;
/// <summary>Проверяет, является ли цвет автоматическим (т.е. Color == null).</summary>
public bool IsAuto => color is null;
public System.Drawing.Color? Color => color;
public static ExColor FromArgb(byte r, byte g, byte b) => new(System.Drawing.Color.FromArgb(r, g, b));
public static ExColor FromName(string knownColor) => new(System.Drawing.Color.FromName(knownColor));
public static implicit operator ExColor(Color exColor) => FromExcel(exColor);
public static implicit operator Color(ExColor color) => color.ToExcel();
public static ExColor FromExcel(Color excelColor)
{
if (excelColor == null)
return new ExColor(null);
// Если цвет автоматический
if (excelColor.Auto != null && excelColor.Auto.Value)
return new ExColor(null);
// Если задан RGB
if (excelColor.Rgb?.Value is { } rgb && rgb.Length > 0)
{
if (rgb.Length == 6) // RRGGBB
{
byte r = Convert.ToByte(rgb.Substring(0, 2), 16);
byte g = Convert.ToByte(rgb.Substring(2, 2), 16);
byte b = Convert.ToByte(rgb.Substring(4, 2), 16);
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
}
else if (rgb.Length == 8) // AARRGGBB (альфа игнорируется)
{
byte r = Convert.ToByte(rgb.Substring(2, 2), 16);
byte g = Convert.ToByte(rgb.Substring(4, 2), 16);
byte b = Convert.ToByte(rgb.Substring(6, 2), 16);
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
}
}
// По умолчанию — автоматический цвет
return new ExColor(null);
}
public Color ToExcel()
{
var excelColor = new Color();
{
if (color.HasValue)
{
excelColor.Rgb = $"{color.Value.R:X2}{color.Value.G:X2}{color.Value.B:X2}";
}
else excelColor.Auto = true;
}
return excelColor;
}
public static ExColor FromRgb(string rgb)
{
if (string.IsNullOrEmpty(rgb)) return new ExColor(null);
if (rgb.Length == 6)
{
byte r = Convert.ToByte(rgb.Substring(0, 2), 16);
byte g = Convert.ToByte(rgb.Substring(2, 2), 16);
byte b = Convert.ToByte(rgb.Substring(4, 2), 16);
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
}
else if (rgb.Length == 8)
{
byte r = Convert.ToByte(rgb.Substring(2, 2), 16);
byte g = Convert.ToByte(rgb.Substring(4, 2), 16);
byte b = Convert.ToByte(rgb.Substring(6, 2), 16);
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
}
return new ExColor(null);
}
}

View File

@@ -5,9 +5,77 @@
/// </summary> /// </summary>
internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col) : ICell internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col) : ICell
{ {
/// <summary>
/// Возвращает эффективный стиль ячейки с учётом наследования от строки и столбца.
/// </summary>
public CellStyle? GetCellStyle()
{
var cell = GetCellElement();
CellStyle? cellStyle = null;
// 1. Пытаемся получить стиль самой ячейки
if (cell?.StyleIndex?.Value is uint cellStyleIndex)
{
cellStyle = writer.GetCellStyle(cellStyleIndex);
if (cellStyle != null && !cellStyle.IsEmpty())
return cellStyle; // если у ячейки есть свой стиль, он имеет наивысший приоритет
}
// 2. Стиль строки
var sheetData = sheet.GetSheetData();
var rowElement = FindRowElement(sheetData, row);
if (rowElement?.StyleIndex?.Value is uint rowStyleIndex)
{
var rowStyle = writer.GetCellStyle(rowStyleIndex);
if (rowStyle != null && !rowStyle.IsEmpty())
{
// Если у ячейки нет стиля, возвращаем стиль строки
if (cellStyle == null)
return rowStyle;
// Иначе объединяем: стиль ячейки имеет приоритет, но некоторые свойства могут быть не заданы
// (например, если у ячейки только Border, а у строки Fill, то в результате будет и Border, и Fill)
// Объединяем: сначала берём стиль строки, затем накладываем стиль ячейки (перекрывая)
return rowStyle.Merge(cellStyle);
}
}
// 3. Стиль столбца
if (col > 0)
{
var columnElement = ExcelColumn.GetColumnElementInternal(sheet, col);
if (columnElement?.Style?.Value is uint colStyleIndex)
{
var colStyle = writer.GetCellStyle(colStyleIndex);
if (colStyle != null && !colStyle.IsEmpty())
{
// Если нет стиля ячейки и строки, возвращаем стиль столбца
if (cellStyle == null)
return colStyle;
// Иначе объединяем: стиль ячейки + стиль строки (если есть) + стиль столбца
// Сначала объединяем стиль строки и столбца, затем накладываем стиль ячейки
// Но проще: берём стиль ячейки (если есть) и объединяем со стилем столбца
return colStyle.Merge(cellStyle);
}
}
}
// Если ничего нет, возвращаем null
return cellStyle; // может быть null
}
public void ApplyStyle(CellStyle style)
{
var cell = GetOrCreateCellElement();
int styleIndex = writer.GetOrCreateStyleId(style);
cell.StyleIndex = (uint)styleIndex;
}
// Кэш фрагментов богатого текста (только для InlineString) // Кэш фрагментов богатого текста (только для InlineString)
private List<IRun>? _runsCache; List<IRun>? _runsCache;
private bool _cacheValid; bool _cacheValid;
// ---- Реализация ICell (новые методы) ---- // ---- Реализация ICell (новые методы) ----
@@ -103,6 +171,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
{ {
var lastRun = _runsCache[_runsCache.Count - 1]; var lastRun = _runsCache[_runsCache.Count - 1];
lastRun.Text += "\n"; lastRun.Text += "\n";
EnsureWrapTextEnabled();
} }
else else
{ {
@@ -121,9 +190,23 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
_runsCache.Add(new ExcelRun { Text = text, Format = format }); _runsCache.Add(new ExcelRun { Text = text, Format = format });
_cacheValid = true; _cacheValid = true;
UpdateCellFromCache(); UpdateCellFromCache();
if (text.Contains('\n'))
EnsureWrapTextEnabled();
return this; 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) public ICell RunBreak(string text, RunFormat? format = null)
{ {
Run(text, format); Run(text, format);
@@ -307,7 +390,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
// ---- Приватные методы для работы с кэшем ---- // ---- Приватные методы для работы с кэшем ----
private void EnsureCacheValid() void EnsureCacheValid()
{ {
if (_cacheValid) return; if (_cacheValid) return;
lock (writer._syncLock) lock (writer._syncLock)
@@ -338,7 +421,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
} }
} }
private string GetStringFromCell(Cell cell) string GetStringFromCell(Cell cell)
{ {
if (cell == null) return string.Empty; if (cell == null) return string.Empty;
if (cell.DataType?.Value == CellValues.SharedString && cell.CellValue != null) if (cell.DataType?.Value == CellValues.SharedString && cell.CellValue != null)
@@ -352,7 +435,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
return string.Empty; return string.Empty;
} }
private void UpdateCellFromCache() void UpdateCellFromCache()
{ {
lock (writer._syncLock) lock (writer._syncLock)
{ {
@@ -375,19 +458,26 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
} }
} }
private InlineString BuildInlineStringFromCache() InlineString BuildInlineStringFromCache()
{ {
var inline = new InlineString(); var inline = new InlineString();
if (_runsCache == null) return inline; if (_runsCache == null) return inline;
foreach (var run in _runsCache) foreach (var run in _runsCache)
{ {
var runElement = new Run(); var runElement = new Run();
runElement.Append(new Text(run.Text)); 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) if (run.Format is { } fmt)
{ {
var rPr = new RunProperties(); var rPr = new RunProperties();
if (fmt.IsBold == true) rPr.Append(new Bold()); if (fmt.IsBold == true)
if (fmt.IsItalic == true) rPr.Append(new Italic()); rPr.Append(new Bold());
if (fmt.IsItalic == true)
rPr.Append(new Italic());
if (fmt.Underline.HasValue) if (fmt.Underline.HasValue)
{ {
rPr.Append(new Underline rPr.Append(new Underline
@@ -402,12 +492,10 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
} }
}); });
} }
if (fmt.IsStrike == true) rPr.Append(new Strike()); if (fmt.IsStrike == true)
if (fmt.Color.HasValue) rPr.Append(new Strike());
{ if (fmt.TryGetExcelColor(out var c))
var excelColor = fmt.Color.Value.ToExcel(); rPr.Append(c);
rPr.Append(excelColor);
}
if (fmt.FontSize.HasValue) if (fmt.FontSize.HasValue)
rPr.Append(new FontSize { Val = fmt.FontSize.Value }); rPr.Append(new FontSize { Val = fmt.FontSize.Value });
if (!string.IsNullOrEmpty(fmt.FontFamily)) if (!string.IsNullOrEmpty(fmt.FontFamily))
@@ -429,7 +517,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
return inline; return inline;
} }
private static RunFormat MergeRunFormat(RunFormat baseFmt, RunFormat overlay) static RunFormat MergeRunFormat(RunFormat baseFmt, RunFormat overlay)
{ {
return new RunFormat return new RunFormat
{ {
@@ -709,7 +797,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
public double? TryGetNumber() => TryGetNumber(out double v) ? v : null; public double? TryGetNumber() => TryGetNumber(out double v) ? v : null;
public bool TrySet(string formula, NumberFormatPattern? format = null) public bool TrySetFormula(string formula, NumberFormatPattern? format = null)
{ {
if (string.IsNullOrEmpty(formula)) return false; if (string.IsNullOrEmpty(formula)) return false;
writer.ThrowIfDisposed(); writer.ThrowIfDisposed();
@@ -718,30 +806,41 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
var cell = GetOrCreateCellElement(); var cell = GetOrCreateCellElement();
cell.CellFormula = new CellFormula(formula); cell.CellFormula = new CellFormula(formula);
cell.DataType = null; // формула сама определяет тип cell.DataType = null; // формула сама определяет тип
if (format != null) if (format != null) Set(cell, format);
SetNumberFormatInternal(cell, format);
return true; return true;
} }
} }
public ICell Set(string formula, NumberFormatPattern? format = null) public ICell SetFormula(string formula, NumberFormatPattern? format = null)
{ {
if (!TrySet(formula, format)) if (!TrySetFormula(formula, format))
throw new InvalidOperationException("Failed to set formula"); throw new InvalidOperationException("Failed to set formula");
return this; return this;
} }
public ICell Set(NumberFormatPattern format) public ICell Set(NumberFormatPattern format)
{ {
if (format == null) return this;
writer.ThrowIfDisposed(); writer.ThrowIfDisposed();
lock (writer._syncLock) lock (writer._syncLock)
{ {
var cell = GetOrCreateCellElement(); var currentStyle = GetCellStyle() ?? new CellStyle();
SetNumberFormatInternal(cell, format); if (currentStyle.TryMerge(format, out var newStyle))
} ApplyStyle(newStyle);
return this; return this;
} }
}
public ICell Set(Cell cell, NumberFormatPattern format)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var currentStyle = GetCellStyle() ?? new CellStyle();
if (currentStyle.TryMerge(format, out var newStyle))
cell.StyleIndex = (uint)writer.GetOrCreateStyleId(newStyle);
return this;
}
}
/// <inheritdoc /> /// <inheritdoc />
public ICell Set(CellAlign align) public ICell Set(CellAlign align)
@@ -749,15 +848,9 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
writer.ThrowIfDisposed(); writer.ThrowIfDisposed();
lock (writer._syncLock) lock (writer._syncLock)
{ {
var cell = GetOrCreateCellElement(); var currentStyle = GetCellStyle() ?? new CellStyle();
int styleIndex = writer.GetOrCreateCellFormatId( if (currentStyle.TryMerge(align, out var newStyle))
numberFormat: null, ApplyStyle(newStyle);
font: null,
fill: null,
border: null,
align: align
);
cell.StyleIndex = (uint)styleIndex;
return this; return this;
} }
} }
@@ -768,15 +861,39 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
writer.ThrowIfDisposed(); writer.ThrowIfDisposed();
lock (writer._syncLock) lock (writer._syncLock)
{ {
var cell = GetOrCreateCellElement(); // 1. Применяем границу к текущей ячейке (объединяя с существующей)
int styleIndex = writer.GetOrCreateCellFormatId( var currentStyle = GetCellStyle() ?? new CellStyle();
numberFormat: null, CellBorder mergedBorder;
font: null, bool changed;
fill: null, if (currentStyle.Border is { } currBorder)
border: border, changed = currBorder.TryMerge(border, out mergedBorder);
align: null else
); {
cell.StyleIndex = (uint)styleIndex; 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; return this;
} }
} }
@@ -787,15 +904,9 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
writer.ThrowIfDisposed(); writer.ThrowIfDisposed();
lock (writer._syncLock) lock (writer._syncLock)
{ {
var cell = GetOrCreateCellElement(); var currentStyle = GetCellStyle() ?? new CellStyle();
int styleIndex = writer.GetOrCreateCellFormatId( if (currentStyle.TryMerge(fill, out var newStyle))
numberFormat: null, ApplyStyle(newStyle);
font: null,
fill: fill,
border: null,
align: null
);
cell.StyleIndex = (uint)styleIndex;
return this; return this;
} }
} }
@@ -806,19 +917,148 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
writer.ThrowIfDisposed(); writer.ThrowIfDisposed();
lock (writer._syncLock) lock (writer._syncLock)
{ {
var cell = GetOrCreateCellElement(); var currentStyle = GetCellStyle() ?? new CellStyle();
int styleIndex = writer.GetOrCreateCellFormatId( if (currentStyle.TryMerge(font, out var newStyle))
numberFormat: null, ApplyStyle(newStyle);
font: font,
fill: null,
border: null,
align: null
);
cell.StyleIndex = (uint)styleIndex;
return this; return this;
} }
} }
/// <inheritdoc />
public ICell Set(CellStyle style)
{
if (style is null) return this;
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
var currentStyle = GetCellStyle() ?? new CellStyle();
if (currentStyle.TryMerge(style, out style))
ApplyStyle(style);
return this;
}
}
void ClearNeighborBorder(int rowOffset, int colOffset, Func<CellBorder, CellBorder> clearFunc)
{
var neighbor = GetNeighbor(rowOffset, colOffset);
if (neighbor is not { } neighbr)
return;
var neighborBorder = neighbr.GetCellBorder();
var newBorder = clearFunc(neighborBorder);
// Если после очистки граница изменилась, применяем изолированно
if (!neighborBorder.Equals(newBorder))
neighbr.SetBorderIsolate(newBorder);
}
internal void SetBorderIsolate(CellBorder border)
{
var currentStyle = GetCellStyle() ?? new CellStyle();
if (currentStyle.TryMerge(border, out var newStyle))
ApplyStyle(newStyle);
}
/// <summary>
/// Возвращает соседнюю ячейку по указанному смещению.
/// </summary>
/// <param name="cell">Исходная ячейка.</param>
/// <param name="rowOffset">Смещение по строкам (положительное вниз).</param>
/// <param name="colOffset">Смещение по столбцам (положительное вправо).</param>
/// <returns>Соседняя ячейка, или null, если она выходит за пределы листа.</returns>
public ExcelCell? GetNeighbor(int rowOffset, int colOffset)
{
// Проверяем, что смещение не равно нулю и координаты не выходят за допустимые пределы (хотя мы не знаем границ листа)
if (rowOffset == 0 && colOffset == 0) return null;
int newRow = (int)row + rowOffset;
int newCol = (int)col + colOffset;
if (newRow < 1 || newCol < 1) return null;
// Excel допускает до 1048576 строк и 16384 столбцов (но мы не будем жестко ограничивать)
// Просто создаём объект ячейки, даже если она не существует физически.
return new ExcelCell(writer, sheet, (uint)newRow, (uint)newCol);
}
internal ICell SetBorderOverride(CellBorder border)
{
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
// Определяем, какие стороны заданы в border
bool hasTop = border.TopBorder.HasValue;
bool hasBottom = border.BottomBorder.HasValue;
bool hasLeft = border.LeftBorder.HasValue;
bool hasRight = border.RightBorder.HasValue;
// Если какая-то сторона задана, то для соседней ячейки на этой стороне мы должны очистить противоположную сторону.
// Например, если мы устанавливаем верхнюю границу у текущей ячейки, то у ячейки сверху нужно очистить нижнюю границу.
// Аналогично для остальных сторон.
if (hasTop)
{
var neighbor = GetNeighbor(-1, 0);
if (neighbor != null)
{
var neighborBorder = neighbor.GetCellBorder();
if (neighborBorder.BottomBorder.HasValue)
{
var newNeighborBorder = neighborBorder with { BottomBorder = null };
// Применяем только границы, не затрагивая другие аспекты стиля
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
}
}
}
if (hasBottom)
{
var neighbor = GetNeighbor(1, 0);
if (neighbor != null)
{
var neighborBorder = neighbor.GetCellBorder();
if (neighborBorder.TopBorder.HasValue)
{
var newNeighborBorder = neighborBorder with { TopBorder = null };
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
}
}
}
if (hasLeft)
{
var neighbor = GetNeighbor(0, -1);
if (neighbor != null)
{
var neighborBorder = neighbor.GetCellBorder();
if (neighborBorder.RightBorder.HasValue)
{
var newNeighborBorder = neighborBorder with { RightBorder = null };
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
}
}
}
if (hasRight)
{
var neighbor = GetNeighbor(0, 1);
if (neighbor != null)
{
var neighborBorder = neighbor.GetCellBorder();
if (neighborBorder.LeftBorder.HasValue)
{
var newNeighborBorder = neighborBorder with { LeftBorder = null };
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
}
}
}
// Теперь устанавливаем границу для текущей ячейки (изолированно, чтобы не было зацикливания)
((ExcelCell)this).SetBorderIsolate(border);
return this;
}
}
public ICell Set(bool value) public ICell Set(bool value)
{ {
writer.ThrowIfDisposed(); writer.ThrowIfDisposed();
@@ -849,8 +1089,9 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
var cell = GetOrCreateCellElement(); var cell = GetOrCreateCellElement();
cell.DataType = CellValues.Number; cell.DataType = CellValues.Number;
cell.CellValue = new CellValue(value.ToString(CultureInfo.InvariantCulture)); cell.CellValue = new CellValue(value.ToString(CultureInfo.InvariantCulture));
if (format != null) cell.InlineString = null;
SetNumberFormatInternal(cell, format); cell.CellFormula = null;
if (format != null) Set(cell, format);
} }
return this; return this;
} }
@@ -897,9 +1138,9 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
public ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell) => public ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell) =>
CopyTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex), out copiedCell); CopyTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex), out copiedCell);
// Private helpers // helpers
private Cell? GetCellElement() Cell? GetCellElement()
{ {
var sheetData = sheet.GetSheetData(); var sheetData = sheet.GetSheetData();
var eRow = FindRowElement(sheetData, row); var eRow = FindRowElement(sheetData, row);
@@ -910,24 +1151,34 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
private Cell GetOrCreateCellElement() private Cell GetOrCreateCellElement()
{ {
var sheetData = sheet.GetSheetData(); var sheetData = sheet.GetSheetData();
var eRow = GetOrCreateRowElement(sheetData, row); var rowElement = GetOrCreateRowElement(sheetData, row);
var cell = FindCellInRow(eRow, col); var cell = FindCellInRow(rowElement, col);
if (cell != null) return cell; if (cell != null) return cell;
cell = new Cell(); cell = new Cell();
string cellRef = CellAddressHelper.ColumnIndexToLetter(col) + row.ToString(); string cellRef = CellAddressHelper.ColumnIndexToLetter(col) + row.ToString();
cell.CellReference = cellRef; cell.CellReference = cellRef;
InsertCellInRow(eRow, cell, col); InsertCellInRow(rowElement, cell, col);
// Наследование стиля
var inheritedStyle = GetCellStyle(); // теперь этот метод учитывает строку и столбец
if (inheritedStyle != null && !inheritedStyle.IsEmpty())
{
int styleIndex = writer.GetOrCreateStyleId(inheritedStyle);
cell.StyleIndex = (uint)styleIndex;
}
return cell; return cell;
} }
private static Row? FindRowElement(SheetData sheetData, uint rowIndex) static Row? FindRowElement(SheetData sheetData, uint rowIndex)
{ {
foreach (var eRow in sheetData.Elements<Row>()) foreach (var eRow in sheetData.Elements<Row>())
if (eRow.RowIndex?.Value == rowIndex) return eRow; if (eRow.RowIndex?.Value == rowIndex) return eRow;
return null; return null;
} }
private static Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex) static Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex)
{ {
var existing = FindRowElement(sheetData, rowIndex); var existing = FindRowElement(sheetData, rowIndex);
if (existing != null) return existing; if (existing != null) return existing;
@@ -936,7 +1187,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
return newRow; return newRow;
} }
private static void InsertRowElement(SheetData sheetData, Row eRow, uint rowIndex) static void InsertRowElement(SheetData sheetData, Row eRow, uint rowIndex)
{ {
bool inserted = false; bool inserted = false;
foreach (var existing in sheetData.Elements<Row>().ToList()) foreach (var existing in sheetData.Elements<Row>().ToList())
@@ -951,7 +1202,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
if (!inserted) sheetData.Append(eRow); if (!inserted) sheetData.Append(eRow);
} }
private static Cell? FindCellInRow(Row eRow, uint colIndex) static Cell? FindCellInRow(Row eRow, uint colIndex)
{ {
foreach (var cell in eRow.Elements<Cell>()) foreach (var cell in eRow.Elements<Cell>())
{ {
@@ -961,7 +1212,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
return null; return null;
} }
private static void InsertCellInRow(Row eRow, Cell cell, uint colIndex) static void InsertCellInRow(Row eRow, Cell cell, uint colIndex)
{ {
string newRef = CellAddressHelper.ColumnIndexToLetter(colIndex) + (eRow.RowIndex?.Value ?? 1).ToString(); string newRef = CellAddressHelper.ColumnIndexToLetter(colIndex) + (eRow.RowIndex?.Value ?? 1).ToString();
cell.CellReference = newRef; cell.CellReference = newRef;
@@ -978,37 +1229,19 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
if (!inserted) eRow.Append(cell); if (!inserted) eRow.Append(cell);
} }
private void InsertCellAt(uint rowIndex, uint colIndex, Cell cell) void InsertCellAt(uint rowIndex, uint colIndex, Cell cell)
{ {
var sheetData = sheet.GetSheetData(); var sheetData = sheet.GetSheetData();
var row = GetOrCreateRowElement(sheetData, rowIndex); var row = GetOrCreateRowElement(sheetData, rowIndex);
InsertCellInRow(row, cell, colIndex); InsertCellInRow(row, cell, colIndex);
} }
private void SetNumberFormatInternal(Cell cell, NumberFormatPattern format) string GetSharedString(uint index)
{
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); return writer.GetSharedString(index);
} }
private int GetOrAddSharedString(string value) string ExtractTextFromInlineString(InlineString? inlineString)
{
return writer.GetOrAddSharedString(value);
}
private string ExtractTextFromInlineString(InlineString? inlineString)
{ {
if (inlineString == null) return string.Empty; if (inlineString == null) return string.Empty;
var sb = new System.Text.StringBuilder(); var sb = new System.Text.StringBuilder();
@@ -1020,10 +1253,10 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row,
return sb.ToString(); return sb.ToString();
} }
private MergeCells? GetMergeCells() => MergeCells? GetMergeCells() =>
sheet.Worksheet.GetFirstChild<MergeCells>(); sheet.Worksheet.GetFirstChild<MergeCells>();
private bool TryParseRangeReference(string reference, out ExcelRange range) bool TryParseRangeReference(string reference, out ExcelRange range)
{ {
range = null!; range = null!;
if (string.IsNullOrEmpty(reference)) return false; if (string.IsNullOrEmpty(reference)) return false;

View File

@@ -16,6 +16,160 @@ internal sealed class ExcelColumn : IColumn
_colIndex = colIndex; _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 uint Index => _colIndex;
public string IndexLetter => NumberToColumnLetter(_colIndex); public string IndexLetter => NumberToColumnLetter(_colIndex);
@@ -64,23 +218,7 @@ internal sealed class ExcelColumn : IColumn
} }
} }
// Вспомогательные внутренние методы (перенести существующую логику) static Column GetOrCreateColumnElementInternal(ExcelSheet sheet, uint colIndex)
private 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;
}
private static Column GetOrCreateColumnElementInternal(ExcelSheet sheet, uint colIndex)
{ {
var existing = GetColumnElementInternal(sheet, colIndex); var existing = GetColumnElementInternal(sheet, colIndex);
if (existing != null) return existing; if (existing != null) return existing;
@@ -165,23 +303,7 @@ internal sealed class ExcelColumn : IColumn
} }
} }
public IColumn SetNumberFormat(NumberFormatPattern format)
{
if (format == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
int fmtId = GetOrCreateNumberFormatId(format);
// Применяем ко всем ячейкам столбца
var sheetData = _sheet.GetSheetData();
foreach (var row in sheetData.Elements<Row>())
{
var cell = FindCellInRow(row, _colIndex);
cell?.StyleIndex = (uint)fmtId;
}
}
return this;
}
public ICell Cell(uint row) public ICell Cell(uint row)
{ {
@@ -207,7 +329,7 @@ internal sealed class ExcelColumn : IColumn
public IColumn Cell(uint row, string value, NumberFormatPattern? format = null) public IColumn Cell(uint row, string value, NumberFormatPattern? format = null)
{ {
Cell(row).Set(value, format); return this; Cell(row).SetFormula(value, format); return this;
} }
public IColumn Cell(uint row, DateTime value, NumberFormatPattern? format = null) public IColumn Cell(uint row, DateTime value, NumberFormatPattern? format = null)
@@ -305,7 +427,7 @@ internal sealed class ExcelColumn : IColumn
// Вспомогательные методы // Вспомогательные методы
private Column? GetColumnElement() Column? GetColumnElement()
{ {
var cols = _sheet.Worksheet.GetFirstChild<Columns>(); var cols = _sheet.Worksheet.GetFirstChild<Columns>();
if (cols == null) return null; if (cols == null) return null;
@@ -320,7 +442,7 @@ internal sealed class ExcelColumn : IColumn
return null; return null;
} }
private Column GetOrCreateColumnElement() Column GetOrCreateColumnElement()
{ {
var existing = GetColumnElement(); var existing = GetColumnElement();
if (existing != null) return existing; if (existing != null) return existing;
@@ -344,13 +466,13 @@ internal sealed class ExcelColumn : IColumn
return newCol; return newCol;
} }
private void DeleteColumnElement() void DeleteColumnElement()
{ {
var col = GetColumnElement(); var col = GetColumnElement();
col?.Remove(); col?.Remove();
} }
private void CopyColumnData(uint sourceCol, uint targetCol) void CopyColumnData(uint sourceCol, uint targetCol)
{ {
if (sourceCol == targetCol) return; if (sourceCol == targetCol) return;
var sheetData = _sheet.GetSheetData(); var sheetData = _sheet.GetSheetData();
@@ -389,7 +511,7 @@ internal sealed class ExcelColumn : IColumn
} }
} }
private void ClearColumnData(uint col) void ClearColumnData(uint col)
{ {
var sheetData = _sheet.GetSheetData(); var sheetData = _sheet.GetSheetData();
foreach (var row in sheetData.Elements<Row>().ToList()) foreach (var row in sheetData.Elements<Row>().ToList())
@@ -399,7 +521,7 @@ internal sealed class ExcelColumn : IColumn
} }
} }
private Cell? FindCellInRow(Row row, uint colIndex) Cell? FindCellInRow(Row row, uint colIndex)
{ {
foreach (var cell in row.Elements<Cell>()) foreach (var cell in row.Elements<Cell>())
{ {
@@ -410,7 +532,7 @@ internal sealed class ExcelColumn : IColumn
return null; return null;
} }
private void InsertCellInRow(Row row, Cell cell, uint colIndex) void InsertCellInRow(Row row, Cell cell, uint colIndex)
{ {
string newRef = NumberToColumnLetter(colIndex) + (row.RowIndex?.Value ?? 1).ToString(); string newRef = NumberToColumnLetter(colIndex) + (row.RowIndex?.Value ?? 1).ToString();
cell.CellReference = newRef; cell.CellReference = newRef;
@@ -430,7 +552,7 @@ internal sealed class ExcelColumn : IColumn
row.Append(cell); row.Append(cell);
} }
private Column? GetColumnElementForIndex(uint col) Column? GetColumnElementForIndex(uint col)
{ {
var cols = _sheet.Worksheet.GetFirstChild<Columns>(); var cols = _sheet.Worksheet.GetFirstChild<Columns>();
if (cols == null) return null; if (cols == null) return null;
@@ -445,7 +567,7 @@ internal sealed class ExcelColumn : IColumn
return null; return null;
} }
private Column GetOrCreateColumnElementForIndex(uint col) Column GetOrCreateColumnElementForIndex(uint col)
{ {
var existing = GetColumnElementForIndex(col); var existing = GetColumnElementForIndex(col);
if (existing != null) return existing; if (existing != null) return existing;
@@ -468,12 +590,12 @@ internal sealed class ExcelColumn : IColumn
return newCol; return newCol;
} }
private int GetOrCreateNumberFormatId(NumberFormatPattern format) int GetOrCreateNumberFormatId(NumberFormatPattern format)
{ {
return _writer.GetOrCreateCellFormatId(numberFormat: format); return _writer.GetOrCreateCellFormatId(numberFormat: format);
} }
private static bool TryParseCellReference(string reference, out uint row, out uint col) static bool TryParseCellReference(string reference, out uint row, out uint col)
{ {
row = 0; col = 0; row = 0; col = 0;
if (string.IsNullOrEmpty(reference)) return false; if (string.IsNullOrEmpty(reference)) return false;
@@ -487,7 +609,7 @@ internal sealed class ExcelColumn : IColumn
return true; return true;
} }
private static string NumberToColumnLetter(uint col) static string NumberToColumnLetter(uint col)
{ {
if (col == 0) throw new ArgumentException("Column number must be > 0"); if (col == 0) throw new ArgumentException("Column number must be > 0");
string result = ""; string result = "";

View File

@@ -5,10 +5,10 @@
/// </summary> /// </summary>
internal sealed class ExcelRange : IRange internal sealed class ExcelRange : IRange
{ {
private readonly ExcelWriter _writer; internal readonly ExcelWriter _writer;
private readonly ExcelSheet _sheet; internal readonly ExcelSheet _sheet;
private uint _rowStart, _rowEnd; internal uint _rowStart, _rowEnd;
private uint _colStart, _colEnd; internal uint _colStart, _colEnd;
internal ExcelRange(ExcelWriter writer, ExcelSheet sheet, uint rowStart, uint colStart, uint rowEnd, uint colEnd) internal ExcelRange(ExcelWriter writer, ExcelSheet sheet, uint rowStart, uint colStart, uint rowEnd, uint colEnd)
{ {
@@ -55,6 +55,79 @@ internal sealed class ExcelRange : IRange
void ForEachCell(Action<ICell> action)
{
for (uint r = _rowStart; r <= _rowEnd; r++)
for (uint c = _colStart; c <= _colEnd; c++)
{
var cell = new ExcelCell(_writer, _sheet, r, c);
action(cell);
}
}
public IRange Set(NumberFormatPattern format)
{
if (format == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
ForEachCell(cell => cell.Set(format));
return this;
}
}
public IRange Set(CellAlign align)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
ForEachCell(cell => cell.Set(align));
return this;
}
}
public IRange Set(CellBorder border)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
ForEachCell(cell => cell.Set(border));
return this;
}
}
public IRange Set(CellFill fill)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
ForEachCell(cell => cell.Set(fill));
return this;
}
}
public IRange Set(CellFont font)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
ForEachCell(cell => cell.Set(font));
return this;
}
}
public IRange Set(CellStyle style)
{
if (style == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
ForEachCell(cell => cell.Set(style));
return this;
}
}
@@ -115,7 +188,7 @@ internal sealed class ExcelRange : IRange
return MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex)); return MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex));
} }
private enum CopyOrder enum CopyOrder
{ {
Any, Any,
LeftToRight, LeftToRight,
@@ -125,7 +198,7 @@ internal sealed class ExcelRange : IRange
} }
/// <summary>Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода.</summary> /// <summary>Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода.</summary>
private void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd, void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd,
uint dstRowStart, uint dstColStart, CopyOrder order) uint dstRowStart, uint dstColStart, CopyOrder order)
{ {
// Определяем все строки, которые могут понадобиться (исходные и целевые) // Определяем все строки, которые могут понадобиться (исходные и целевые)
@@ -172,7 +245,7 @@ internal sealed class ExcelRange : IRange
} }
/// <summary>Строит словарь строк для указанных индексов строк.</summary> /// <summary>Строит словарь строк для указанных индексов строк.</summary>
private Dictionary<uint, Row> GetRowDictionary(HashSet<uint> rowIndices) Dictionary<uint, Row> GetRowDictionary(HashSet<uint> rowIndices)
{ {
var dict = new Dictionary<uint, Row>(); var dict = new Dictionary<uint, Row>();
var sheetData = _sheet.GetSheetData(); var sheetData = _sheet.GetSheetData();
@@ -185,7 +258,7 @@ internal sealed class ExcelRange : IRange
} }
/// <summary>Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке).</summary> /// <summary>Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке).</summary>
private Cell? FindCellInRowFast(Row row, uint colIndex) Cell? FindCellInRowFast(Row row, uint colIndex)
{ {
foreach (var cell in row.Elements<Cell>()) foreach (var cell in row.Elements<Cell>())
{ {
@@ -196,14 +269,14 @@ internal sealed class ExcelRange : IRange
} }
/// <summary>Проверяет, пересекаются ли два прямоугольных диапазона.</summary> /// <summary>Проверяет, пересекаются ли два прямоугольных диапазона.</summary>
private static bool RangesOverlap(uint r1s, uint c1s, uint r1e, uint c1e, static bool RangesOverlap(uint r1s, uint c1s, uint r1e, uint c1e,
uint r2s, uint c2s, uint r2e, uint c2e) uint r2s, uint c2s, uint r2e, uint c2e)
{ {
return !(r1e < r2s || r2e < r1s || c1e < c2s || c2e < c1s); return !(r1e < r2s || r2e < r1s || c1e < c2s || c2e < c1s);
} }
/// <summary>Вставляет ячейку в указанную позицию, предварительно удаляя существующую.</summary> /// <summary>Вставляет ячейку в указанную позицию, предварительно удаляя существующую.</summary>
private void InsertCellAt(uint row, uint col, Cell cell) void InsertCellAt(uint row, uint col, Cell cell)
{ {
DeleteCellAt(row, col); DeleteCellAt(row, col);
var sheetData = _sheet.GetSheetData(); var sheetData = _sheet.GetSheetData();
@@ -212,7 +285,7 @@ internal sealed class ExcelRange : IRange
} }
/// <summary>Удаляет ячейку, если она существует.</summary> /// <summary>Удаляет ячейку, если она существует.</summary>
private void DeleteCellAt(uint row, uint col) void DeleteCellAt(uint row, uint col)
{ {
var sheetData = _sheet.GetSheetData(); var sheetData = _sheet.GetSheetData();
var rowElement = FindRowElement(sheetData, row); var rowElement = FindRowElement(sheetData, row);
@@ -222,7 +295,7 @@ internal sealed class ExcelRange : IRange
} }
/// <summary>Получает или создаёт строку с указанным индексом.</summary> /// <summary>Получает или создаёт строку с указанным индексом.</summary>
private Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex) Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex)
{ {
var existing = FindRowElement(sheetData, rowIndex); var existing = FindRowElement(sheetData, rowIndex);
if (existing != null) return existing; if (existing != null) return existing;
@@ -232,7 +305,7 @@ internal sealed class ExcelRange : IRange
} }
/// <summary>Вставляет ячейку в строку с сохранением порядка столбцов.</summary> /// <summary>Вставляет ячейку в строку с сохранением порядка столбцов.</summary>
private void InsertCellInRow(Row row, Cell cell, uint colIndex) void InsertCellInRow(Row row, Cell cell, uint colIndex)
{ {
string newRef = $"{CellAddressHelper.ColumnIndexToLetter(colIndex)}{row.RowIndex?.Value ?? 1}"; string newRef = $"{CellAddressHelper.ColumnIndexToLetter(colIndex)}{row.RowIndex?.Value ?? 1}";
cell.CellReference = newRef; cell.CellReference = newRef;
@@ -251,7 +324,7 @@ internal sealed class ExcelRange : IRange
} }
/// <summary>Очищает данные (содержимое) в указанном диапазоне.</summary> /// <summary>Очищает данные (содержимое) в указанном диапазоне.</summary>
private void ClearRangeData(uint rowStart, uint colStart, uint rowEnd, uint colEnd) void ClearRangeData(uint rowStart, uint colStart, uint rowEnd, uint colEnd)
{ {
for (uint r = rowStart; r <= rowEnd; r++) for (uint r = rowStart; r <= rowEnd; r++)
{ {
@@ -334,103 +407,12 @@ internal sealed class ExcelRange : IRange
return merged.Equals(range); 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>
/// Применяет стиль ко всем ячейкам диапазона. /// Применяет стиль ко всем ячейкам диапазона.
/// </summary> /// </summary>
/// <param name="styleIndex">Индекс стиля для применения.</param> /// <param name="styleIndex">Индекс стиля для применения.</param>
/// <param name="createIfMissing">Если true, создаёт недостающие ячейки (по умолчанию true).</param> /// <param name="createIfMissing">Если true, создаёт недостающие ячейки (по умолчанию true).</param>
private void ApplyStyleToRange(uint styleIndex, bool createIfMissing = true) void ApplyStyleToRange(uint styleIndex, bool createIfMissing = true)
{ {
for (uint row = _rowStart; row <= _rowEnd; row++) for (uint row = _rowStart; row <= _rowEnd; row++)
{ {
@@ -517,7 +499,7 @@ internal sealed class ExcelRange : IRange
{ {
if (GetSubCell(row, col, out var cell)) if (GetSubCell(row, col, out var cell))
{ {
cell.Set(formula, format); cell.SetFormula(formula, format);
return true; return true;
} }
return false; return false;
@@ -619,7 +601,7 @@ internal sealed class ExcelRange : IRange
{ {
if (GetSubCell(row, col, out var cell)) if (GetSubCell(row, col, out var cell))
{ {
cell.Set(formula, format); cell.SetFormula(formula, format);
return true; return true;
} }
return false; return false;
@@ -727,7 +709,7 @@ internal sealed class ExcelRange : IRange
// Вспомогательные методы // Вспомогательные методы
private Cell? GetCellInternal(uint row, uint col) Cell? GetCellInternal(uint row, uint col)
{ {
var sheetData = _sheet.GetSheetData(); var sheetData = _sheet.GetSheetData();
var rowElement = FindRowElement(sheetData, row); var rowElement = FindRowElement(sheetData, row);
@@ -735,7 +717,7 @@ internal sealed class ExcelRange : IRange
return FindCellInRow(rowElement, col); return FindCellInRow(rowElement, col);
} }
private void CopyData(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd, uint dstRowStart, uint dstColStart) void CopyData(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd, uint dstRowStart, uint dstColStart)
{ {
// Сохраняем все значения и форматы из исходного диапазона // Сохраняем все значения и форматы из исходного диапазона
var cellsData = new List<(uint row, uint col, Cell cell)>(); var cellsData = new List<(uint row, uint col, Cell cell)>();
@@ -784,7 +766,7 @@ internal sealed class ExcelRange : IRange
} }
} }
private Row GetOrCreateRowElement(uint rowIndex) Row GetOrCreateRowElement(uint rowIndex)
{ {
var existing = FindRowElement(_sheet.GetSheetData(), rowIndex); var existing = FindRowElement(_sheet.GetSheetData(), rowIndex);
if (existing != null) return existing; if (existing != null) return existing;
@@ -793,14 +775,14 @@ internal sealed class ExcelRange : IRange
return newRow; return newRow;
} }
private static Row? FindRowElement(SheetData sheetData, uint rowIndex) static Row? FindRowElement(SheetData sheetData, uint rowIndex)
{ {
foreach (var row in sheetData.Elements<Row>()) foreach (var row in sheetData.Elements<Row>())
if (row.RowIndex?.Value == rowIndex) return row; if (row.RowIndex?.Value == rowIndex) return row;
return null; return null;
} }
private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex) static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex)
{ {
bool inserted = false; bool inserted = false;
foreach (var existing in sheetData.Elements<Row>().ToList()) foreach (var existing in sheetData.Elements<Row>().ToList())
@@ -815,7 +797,7 @@ internal sealed class ExcelRange : IRange
if (!inserted) sheetData.Append(row); if (!inserted) sheetData.Append(row);
} }
private Cell? FindCellInRow(Row row, uint colIndex) Cell? FindCellInRow(Row row, uint colIndex)
{ {
foreach (var cell in row.Elements<Cell>()) foreach (var cell in row.Elements<Cell>())
{ {
@@ -825,7 +807,7 @@ internal sealed class ExcelRange : IRange
return null; return null;
} }
private Column? GetColumnElementForIndex(uint col) Column? GetColumnElementForIndex(uint col)
{ {
var worksheet = _sheet.Worksheet; var worksheet = _sheet.Worksheet;
var cols = worksheet.GetFirstChild<Columns>(); var cols = worksheet.GetFirstChild<Columns>();
@@ -839,7 +821,7 @@ internal sealed class ExcelRange : IRange
return null; return null;
} }
private Column GetOrCreateColumnElementForIndex(uint col) Column GetOrCreateColumnElementForIndex(uint col)
{ {
var existing = GetColumnElementForIndex(col); var existing = GetColumnElementForIndex(col);
if (existing != null) return existing; if (existing != null) return existing;
@@ -861,10 +843,10 @@ internal sealed class ExcelRange : IRange
return newCol; return newCol;
} }
private MergeCells? GetMergeCells() => MergeCells? GetMergeCells() =>
_sheet.Worksheet.GetFirstChild<MergeCells>(); _sheet.Worksheet.GetFirstChild<MergeCells>();
private bool TryParseRangeReference(string reference, out ExcelRange range) bool TryParseRangeReference(string reference, out ExcelRange range)
{ {
range = null!; range = null!;
if (string.IsNullOrEmpty(reference)) return false; if (string.IsNullOrEmpty(reference)) return false;
@@ -877,10 +859,4 @@ internal sealed class ExcelRange : IRange
range = new ExcelRange(_writer, _sheet, rowStart, colStart, rowEnd, colEnd); range = new ExcelRange(_writer, _sheet, rowStart, colStart, rowEnd, colEnd);
return true; return true;
} }
// создаёт стиль только с числовым форматом
private int GetOrCreateNumberFormatId(NumberFormatPattern format)
{
return _writer.GetOrCreateCellFormatId(numberFormat: format);
}
} }

View File

@@ -5,9 +5,9 @@
/// </summary> /// </summary>
internal sealed class ExcelRow : IRow internal sealed class ExcelRow : IRow
{ {
private readonly ExcelWriter _writer; readonly ExcelWriter _writer;
private readonly ExcelSheet _sheet; readonly ExcelSheet _sheet;
private uint _rowIndex; uint _rowIndex;
internal ExcelRow(ExcelWriter writer, ExcelSheet sheet, uint rowIndex) internal ExcelRow(ExcelWriter writer, ExcelSheet sheet, uint rowIndex)
{ {
@@ -47,7 +47,7 @@ internal sealed class ExcelRow : IRow
} }
// Вспомогательные методы // Вспомогательные методы
private static Row? GetRowElementInternal(ExcelSheet sheet, uint rowIndex) static Row? GetRowElementInternal(ExcelSheet sheet, uint rowIndex)
{ {
var sheetData = sheet.GetSheetData(); var sheetData = sheet.GetSheetData();
foreach (var row in sheetData.Elements<Row>()) foreach (var row in sheetData.Elements<Row>())
@@ -55,7 +55,7 @@ internal sealed class ExcelRow : IRow
return null; return null;
} }
private static Row GetOrCreateRowElementInternal(ExcelSheet sheet, uint rowIndex) static Row GetOrCreateRowElementInternal(ExcelSheet sheet, uint rowIndex)
{ {
var existing = GetRowElementInternal(sheet, rowIndex); var existing = GetRowElementInternal(sheet, rowIndex);
if (existing != null) return existing; if (existing != null) return existing;
@@ -66,7 +66,7 @@ internal sealed class ExcelRow : IRow
return newRow; return newRow;
} }
private static void InsertRowElementInternal(SheetData sheetData, Row row, uint rowIndex) static void InsertRowElementInternal(SheetData sheetData, Row row, uint rowIndex)
{ {
bool inserted = false; bool inserted = false;
foreach (var existing in sheetData.Elements<Row>().ToList()) foreach (var existing in sheetData.Elements<Row>().ToList())
@@ -136,24 +136,156 @@ internal sealed class ExcelRow : IRow
} }
} }
} }
private void ApplyStyleToRowCells(CellStyle style)
{
var sheetData = _sheet.GetSheetData();
var rowElement = FindRowElement(sheetData, _rowIndex);
if (rowElement == null) return;
// Применяем стиль ко всем существующим ячейкам
foreach (var cellElement in rowElement.Elements<Cell>())
{
var colIndex = GetColumnIndex(cellElement.CellReference?.Value);
if (colIndex > 0)
{
var cell = new ExcelCell(_writer, _sheet, _rowIndex, colIndex);
// Применяем стиль через Set, чтобы объединить с существующим
cell.Set(style);
}
}
}
static uint GetColumnIndex(string? cellReference)
{
if (string.IsNullOrEmpty(cellReference)) return 0;
int i = 0;
while (i < cellReference!.Length && char.IsLetter(cellReference[i])) i++;
if (i == 0) return 0;
string colPart = cellReference.Substring(0, i);
return CellAddressHelper.ColumnLetterToIndex(colPart);
}
CellStyle? GetRowStyle(Row rowElement)
{
if (rowElement.StyleIndex?.Value is not uint styleIndex)
return null;
return _writer.GetCellStyle(styleIndex);
}
/// <inheritdoc /> /// <inheritdoc />
public IRow SetNumberFormat(NumberFormatPattern format) public IRow Set(NumberFormatPattern format)
{ {
if (format == null) return this; if (format == null) return this;
_writer.ThrowIfDisposed(); _writer.ThrowIfDisposed();
lock (_writer._syncLock) lock (_writer._syncLock)
{ {
// Находим все ячейки в этой строке и устанавливаем формат var rowElement = GetOrCreateRowElement();
var sheetData = _sheet.GetSheetData(); var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
var rowElement = FindRowElement(sheetData, _rowIndex); if (currentStyle.TryMerge(format, out var newStyle))
if (rowElement == null) return this; {
int formatIndex = GetOrCreateNumberFormatId(format); ApplyStyleToRow(newStyle);
foreach (var cell in rowElement.Elements<Cell>()) ApplyStyleToRowCells(newStyle);
cell.StyleIndex = (uint)formatIndex;
} }
return this; return this;
} }
}
/// <inheritdoc />
public IRow Set(CellAlign align)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var rowElement = GetOrCreateRowElement();
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
if (currentStyle.TryMerge(align, out var newStyle))
{
ApplyStyleToRow(newStyle);
ApplyStyleToRowCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IRow Set(CellBorder border)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var rowElement = GetOrCreateRowElement();
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
if (currentStyle.TryMerge(border, out var newStyle))
{
ApplyStyleToRow(newStyle);
ApplyStyleToRowCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IRow Set(CellFill fill)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var rowElement = GetOrCreateRowElement();
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
if (currentStyle.TryMerge(fill, out var newStyle))
{
ApplyStyleToRow(newStyle);
ApplyStyleToRowCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IRow Set(CellFont font)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var rowElement = GetOrCreateRowElement();
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
if (currentStyle.TryMerge(font, out var newStyle))
{
ApplyStyleToRow(newStyle);
ApplyStyleToRowCells(newStyle);
}
return this;
}
}
/// <inheritdoc />
public IRow Set(CellStyle style)
{
if (style == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var rowElement = GetOrCreateRowElement();
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
if (currentStyle.TryMerge(style, out var newStyle))
{
ApplyStyleToRow(newStyle);
ApplyStyleToRowCells(newStyle);
}
return this;
}
}
void ApplyStyleToRow(CellStyle style)
{
var rowElement = GetOrCreateRowElement();
int styleIndex = _writer.GetOrCreateStyleId(style);
rowElement.StyleIndex = (uint)styleIndex;
rowElement.CustomFormat = true;
}
/// <inheritdoc /> /// <inheritdoc />
public ICell Cell(uint col) => new ExcelCell(_writer, _sheet, _rowIndex, col); public ICell Cell(uint col) => new ExcelCell(_writer, _sheet, _rowIndex, col);
@@ -176,7 +308,7 @@ internal sealed class ExcelRow : IRow
public IRow Cell(uint col, string value, NumberFormatPattern? format = null) public IRow Cell(uint col, string value, NumberFormatPattern? format = null)
{ {
Cell(col).Set(value, format); return this; Cell(col).SetFormula(value, format); return this;
} }
public IRow Cell(uint col, DateTime value, NumberFormatPattern? format = null) public IRow Cell(uint col, DateTime value, NumberFormatPattern? format = null)
@@ -226,7 +358,7 @@ internal sealed class ExcelRow : IRow
public IRow Cell(string col, string value, NumberFormatPattern? format = null) public IRow Cell(string col, string value, NumberFormatPattern? format = null)
{ {
Cell(col).Set(value, format); return this; Cell(col).SetFormula(value, format); return this;
} }
public IRow Cell(string col, DateTime value, NumberFormatPattern? format = null) public IRow Cell(string col, DateTime value, NumberFormatPattern? format = null)
@@ -347,7 +479,7 @@ internal sealed class ExcelRow : IRow
// Вспомогательные методы // Вспомогательные методы
private Row GetOrCreateRowElement() Row GetOrCreateRowElement()
{ {
var sheetData = _sheet.GetSheetData(); var sheetData = _sheet.GetSheetData();
var existing = FindRowElement(sheetData, _rowIndex); var existing = FindRowElement(sheetData, _rowIndex);
@@ -357,7 +489,7 @@ internal sealed class ExcelRow : IRow
return newRow; return newRow;
} }
private static Row? FindRowElement(SheetData sheetData, uint rowIndex) static Row? FindRowElement(SheetData sheetData, uint rowIndex)
{ {
// Поиск по атрибуту RowIndex. В Open XML строки могут идти не по порядку. // Поиск по атрибуту RowIndex. В Open XML строки могут идти не по порядку.
foreach (var row in sheetData.Elements<Row>()) foreach (var row in sheetData.Elements<Row>())
@@ -368,7 +500,7 @@ internal sealed class ExcelRow : IRow
return null; return null;
} }
private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex) static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex)
{ {
// Вставка с сохранением сортировки по RowIndex // Вставка с сохранением сортировки по RowIndex
bool inserted = false; bool inserted = false;
@@ -385,7 +517,7 @@ internal sealed class ExcelRow : IRow
sheetData.Append(row); sheetData.Append(row);
} }
private int GetOrCreateNumberFormatId(NumberFormatPattern format) int GetOrCreateNumberFormatId(NumberFormatPattern format)
{ {
// Создаём стиль, содержащий только числовой формат, и возвращаем его индекс // Создаём стиль, содержащий только числовой формат, и возвращаем его индекс
return _writer.GetOrCreateCellFormatId(numberFormat: format); return _writer.GetOrCreateCellFormatId(numberFormat: format);

View File

@@ -6,8 +6,8 @@
/// </summary> /// </summary>
internal sealed class ExcelRun : IRun internal sealed class ExcelRun : IRun
{ {
private string _text = string.Empty; string _text = string.Empty;
private RunFormat? _format; RunFormat? _format;
/// <inheritdoc /> /// <inheritdoc />
public string Text public string Text

View File

@@ -77,7 +77,7 @@ internal sealed class ExcelSheet : ISheet
public ISheet Cell(uint row, uint col, string formula, NumberFormatPattern? format = null) public ISheet Cell(uint row, uint col, string formula, NumberFormatPattern? format = null)
{ {
Cell(row, col).Set(formula, format); return this; Cell(row, col).SetFormula(formula, format); return this;
} }
public ISheet Cell(uint row, uint col, DateTime value, NumberFormatPattern? format = null) public ISheet Cell(uint row, uint col, DateTime value, NumberFormatPattern? format = null)
@@ -129,7 +129,7 @@ internal sealed class ExcelSheet : ISheet
public ISheet Cell(uint row, string col, string formula, NumberFormatPattern? format = null) public ISheet Cell(uint row, string col, string formula, NumberFormatPattern? format = null)
{ {
Cell(row, col).Set(formula, format); return this; Cell(row, col).SetFormula(formula, format); return this;
} }
public ISheet Cell(uint row, string col, DateTime value, NumberFormatPattern? format = null) public ISheet Cell(uint row, string col, DateTime value, NumberFormatPattern? format = null)
@@ -212,20 +212,63 @@ internal sealed class ExcelSheet : ISheet
/// <inheritdoc /> /// <inheritdoc />
public IRange RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol) public IRange RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol)
{ {
return new ExcelRange(Book, this, startRow, startCol, endRow, endCol); if (startRow == 0) throw new ArgumentException("startRow must be >= 1", nameof(startRow));
if (startCol == 0) throw new ArgumentException("startCol must be >= 1", nameof(startCol));
if (endRow == 0) throw new ArgumentException("endRow must be >= 1", nameof(endRow));
if (endCol == 0) throw new ArgumentException("endCol must be >= 1", nameof(endCol));
// Приводим к корректному порядку (пользователь мог передать start > end)
uint rowStart = Math.Min(startRow, endRow);
uint rowEnd = Math.Max(startRow, endRow);
uint colStart = Math.Min(startCol, endCol);
uint colEnd = Math.Max(startCol, endCol);
return new ExcelRange(Book, this, rowStart, colStart, rowEnd, colEnd);
} }
/// <inheritdoc /> /// <inheritdoc />
public IRange RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol) public IRange RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol)
{ {
if (string.IsNullOrEmpty(startCol)) throw new ArgumentException("startCol cannot be null or empty", nameof(startCol));
if (string.IsNullOrEmpty(endCol)) throw new ArgumentException("endCol cannot be null or empty", nameof(endCol));
uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol); uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol);
uint endColIdx = CellAddressHelper.ColumnLetterToIndex(endCol); uint endColIdx = CellAddressHelper.ColumnLetterToIndex(endCol);
return new ExcelRange(Book, this, startRow, startColIdx, endRow, endColIdx); if (startColIdx == 0) throw new ArgumentException($"Invalid column letter: '{startCol}'", nameof(startCol));
if (endColIdx == 0) throw new ArgumentException($"Invalid column letter: '{endCol}'", nameof(endCol));
return RangeByIndexes(startRow, startColIdx, endRow, endColIdx);
}
/// <inheritdoc />
public IRange RangeByLength(uint startRow, uint startCol, uint rows, uint cols)
{
if (startRow == 0) throw new ArgumentException("startRow must be >= 1", nameof(startRow));
if (startCol == 0) throw new ArgumentException("startCol must be >= 1", nameof(startCol));
if (rows == 0) throw new ArgumentException("rows must be > 0", nameof(rows));
if (cols == 0) throw new ArgumentException("cols must be > 0", nameof(cols));
checked
{
uint endRow = startRow + rows - 1;
uint endCol = startCol + cols - 1;
return new ExcelRange(Book, this, startRow, startCol, endRow, endCol);
}
}
/// <inheritdoc />
public IRange RangeByLength(uint startRow, string startCol, uint rows, uint cols)
{
if (string.IsNullOrEmpty(startCol)) throw new ArgumentException("startCol cannot be null or empty", nameof(startCol));
uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol);
if (startColIdx == 0) throw new ArgumentException($"Invalid column letter: '{startCol}'", nameof(startCol));
return RangeByLength(startRow, startColIdx, rows, cols);
} }
/// <inheritdoc /> /// <inheritdoc />
public ISheet RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol, Action<IRange> edit) public ISheet RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol, Action<IRange> edit)
{ {
if (edit is null) throw new ArgumentNullException(nameof(edit));
var range = RangeByIndexes(startRow, startCol, endRow, endCol); var range = RangeByIndexes(startRow, startCol, endRow, endCol);
edit(range); edit(range);
return this; return this;
@@ -234,31 +277,16 @@ internal sealed class ExcelSheet : ISheet
/// <inheritdoc /> /// <inheritdoc />
public ISheet RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol, Action<IRange> edit) public ISheet RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol, Action<IRange> edit)
{ {
if (edit is null) throw new ArgumentNullException(nameof(edit));
var range = RangeByIndexes(startRow, startCol, endRow, endCol); var range = RangeByIndexes(startRow, startCol, endRow, endCol);
edit(range); edit(range);
return this; return this;
} }
/// <inheritdoc />
public IRange RangeByLength(uint startRow, uint startCol, uint rows, uint cols)
{
if (rows == 0 || cols == 0)
throw new ArgumentException("Rows and columns must be greater than 0");
uint endRow = startRow + rows - 1;
uint endCol = startCol + cols - 1;
return new ExcelRange(Book, this, startRow, startCol, endRow, endCol);
}
/// <inheritdoc />
public IRange RangeByLength(uint startRow, string startCol, uint rows, uint cols)
{
uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol);
return RangeByLength(startRow, startColIdx, rows, cols);
}
/// <inheritdoc /> /// <inheritdoc />
public ISheet RangeByLength(uint startRow, uint startCol, uint rows, uint cols, Action<IRange> edit) public ISheet RangeByLength(uint startRow, uint startCol, uint rows, uint cols, Action<IRange> edit)
{ {
if (edit is null) throw new ArgumentNullException(nameof(edit));
var range = RangeByLength(startRow, startCol, rows, cols); var range = RangeByLength(startRow, startCol, rows, cols);
edit(range); edit(range);
return this; return this;
@@ -267,6 +295,7 @@ internal sealed class ExcelSheet : ISheet
/// <inheritdoc /> /// <inheritdoc />
public ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action<IRange> edit) public ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action<IRange> edit)
{ {
if (edit is null) throw new ArgumentNullException(nameof(edit));
var range = RangeByLength(startRow, startCol, rows, cols); var range = RangeByLength(startRow, startCol, rows, cols);
edit(range); edit(range);
return this; return this;
@@ -274,4 +303,27 @@ internal sealed class ExcelSheet : ISheet
#endregion #endregion
#region Merge Operations
/// <inheritdoc />
public bool TryMergeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol) =>
RangeByIndexes(startRow, startCol, endRow, endCol).TryMerge();
/// <inheritdoc />
public bool TryMergeByIndexes(uint startRow, string startCol, uint endRow, string endCol) =>
RangeByIndexes(startRow, startCol, endRow, endCol).TryMerge();
/// <inheritdoc />
public bool TryMergeByLength(uint startRow, uint startCol, uint rows, uint cols) =>
RangeByLength(startRow, startCol, rows, cols).TryMerge();
/// <inheritdoc />
public bool TryMergeByLength(uint startRow, string startCol, uint rows, uint cols) =>
RangeByLength(startRow, startCol, rows, cols).TryMerge();
#endregion
} }

View File

@@ -99,6 +99,18 @@ public interface ISheet
/// <summary>Редактирует диапазон, заданный начальной ячейкой и размером (буква столбца).</summary> /// <summary>Редактирует диапазон, заданный начальной ячейкой и размером (буква столбца).</summary>
ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action<IRange> edit); ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action<IRange> edit);
/// <summary>Возвращает диапазон ячеек по начальным и конечным индексам строк и столбцов.</summary>
bool TryMergeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol);
/// <summary>Возвращает диапазон ячеек по начальным и конечным координатам с буквенным обозначением столбцов.</summary>
bool TryMergeByIndexes(uint startRow, string startCol, uint endRow, string endCol);
/// <summary>Возвращает диапазон, начиная с указанной ячейки, заданной размером (строки x столбцы).</summary>
bool TryMergeByLength(uint startRow, uint startCol, uint rows, uint cols);
/// <summary>Возвращает диапазон по начальной ячейке и размеру с буквенным обозначением столбца.</summary>
bool TryMergeByLength(uint startRow, string startCol, uint rows, uint cols);
/// <summary>Возвращает ячейку по номеру строки и столбца (оба начиная с 1).</summary> /// <summary>Возвращает ячейку по номеру строки и столбца (оба начиная с 1).</summary>
ICell Cell(uint row, uint col); ICell Cell(uint row, uint col);
@@ -207,7 +219,22 @@ public interface IRow
RowHeight Height { get; set; } RowHeight Height { get; set; }
/// <summary>Устанавливает числовой формат для всех ячеек строки.</summary> /// <summary>Устанавливает числовой формат для всех ячеек строки.</summary>
IRow SetNumberFormat(NumberFormatPattern format); IRow Set(NumberFormatPattern format);
/// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary>
IRow Set(CellAlign format);
/// <summary>Устанавливает границы для всех ячеек диапазона.</summary>
IRow Set(CellBorder format);
/// <summary>Устанавливает заливку для всех ячеек диапазона.</summary>
IRow Set(CellFill format);
/// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary>
IRow Set(CellFont format);
/// <summary>Устанавливает шрифт ячейки.</summary>
IRow Set(CellStyle font);
/// <summary>Возвращает ячейку в заданном столбце (индекс с 1).</summary> /// <summary>Возвращает ячейку в заданном столбце (индекс с 1).</summary>
ICell Cell(uint col); ICell Cell(uint col);
@@ -334,7 +361,22 @@ public interface IColumn
ColumnWidth Width { get; set; } ColumnWidth Width { get; set; }
/// <summary>Устанавливает числовой формат для всех ячеек столбца.</summary> /// <summary>Устанавливает числовой формат для всех ячеек столбца.</summary>
IColumn SetNumberFormat(NumberFormatPattern format); IColumn Set(NumberFormatPattern format);
/// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary>
IColumn Set(CellAlign format);
/// <summary>Устанавливает границы для всех ячеек диапазона.</summary>
IColumn Set(CellBorder format);
/// <summary>Устанавливает заливку для всех ячеек диапазона.</summary>
IColumn Set(CellFill format);
/// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary>
IColumn Set(CellFont format);
/// <summary>Устанавливает шрифт ячейки.</summary>
IColumn Set(CellStyle font);
/// <summary>Возвращает ячейку в заданной строке (индекс с 1).</summary> /// <summary>Возвращает ячейку в заданной строке (индекс с 1).</summary>
ICell Cell(uint row); ICell Cell(uint row);
@@ -464,19 +506,22 @@ public interface IRange
IRange MoveTo(uint rowIndex, string colIndex); IRange MoveTo(uint rowIndex, string colIndex);
/// <summary>Устанавливает числовой формат для всех ячеек диапазона.</summary> /// <summary>Устанавливает числовой формат для всех ячеек диапазона.</summary>
IRange SetNumberFormat(NumberFormatPattern format); IRange Set(NumberFormatPattern format);
/// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary> /// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary>
IRange SetCellAlign(CellAlign format); IRange Set(CellAlign format);
/// <summary>Устанавливает границы для всех ячеек диапазона.</summary> /// <summary>Устанавливает границы для всех ячеек диапазона.</summary>
IRange SetCellBorder(CellBorder format); IRange Set(CellBorder format);
/// <summary>Устанавливает заливку для всех ячеек диапазона.</summary> /// <summary>Устанавливает заливку для всех ячеек диапазона.</summary>
IRange SetCellFill(CellFill format); IRange Set(CellFill format);
/// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary> /// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary>
IRange SetCellFont(CellFont format); IRange Set(CellFont format);
/// <summary>Устанавливает шрифт ячейки.</summary>
IRange Set(CellStyle font);
/// <summary>Перечисляет все ячейки диапазона (по строкам).</summary> /// <summary>Перечисляет все ячейки диапазона (по строкам).</summary>
IEnumerable<ICell> Cells { get; } IEnumerable<ICell> Cells { get; }
@@ -721,6 +766,9 @@ public interface ICell
/// <summary>Возвращает шрифт ячейки.</summary> /// <summary>Возвращает шрифт ячейки.</summary>
CellFont GetCellFont(); CellFont GetCellFont();
/// <summary>Возвращает шрифт ячейки.</summary>
CellStyle? GetCellStyle();
/// <summary>Пытается извлечь логическое значение.</summary> /// <summary>Пытается извлечь логическое значение.</summary>
bool TryGetBoolean(out bool value); bool TryGetBoolean(out bool value);
@@ -742,25 +790,28 @@ public interface ICell
/// <summary>Пытается установить формулу (без вычисленного значения).</summary> /// <summary>Пытается установить формулу (без вычисленного значения).</summary>
/// <param name="formula">Текст формулы (например, "SUM(A1:A5)").</param> /// <param name="formula">Текст формулы (например, "SUM(A1:A5)").</param>
/// <param name="format">Необязательный числовой формат для результата.</param> /// <param name="format">Необязательный числовой формат для результата.</param>
bool TrySet(string formula, NumberFormatPattern? format = null); bool TrySetFormula(string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает формулу (выбрасывает исключение при ошибке).</summary> /// <summary>Устанавливает формулу (выбрасывает исключение при ошибке).</summary>
ICell Set(string formula, NumberFormatPattern? format = null); ICell SetFormula(string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает числовой формат ячейки (не меняя значение).</summary> /// <summary>Устанавливает числовой формат ячейки (не меняя значение).</summary>
ICell Set(NumberFormatPattern format); ICell Set(NumberFormatPattern format);
/// <summary>Устанавливает выравнивание текста ячейки.</summary> /// <summary>Устанавливает выравнивание текста ячейки.</summary>
ICell Set(CellAlign format); ICell Set(CellAlign align);
/// <summary>Устанавливает границы ячейки.</summary> /// <summary>Устанавливает границы ячейки.</summary>
ICell Set(CellBorder format); ICell Set(CellBorder border);
/// <summary>Устанавливает заливку ячейки.</summary> /// <summary>Устанавливает заливку ячейки.</summary>
ICell Set(CellFill format); ICell Set(CellFill fill);
/// <summary>Устанавливает шрифт ячейки.</summary> /// <summary>Устанавливает шрифт ячейки.</summary>
ICell Set(CellFont format); ICell Set(CellFont font);
/// <summary>Устанавливает стиль ячейки.</summary>
ICell Set(CellStyle style);
/// <summary>Устанавливает простое текстовое значение (без форматирования).</summary> /// <summary>Устанавливает простое текстовое значение (без форматирования).</summary>
ICell Set(string value); ICell Set(string value);

View File

@@ -3,7 +3,7 @@
public class NumberFormatPattern public class NumberFormatPattern
{ {
public string Format { get; } public string Format { get; }
internal int? Id { get; private set; } internal int? Id { get; set; }
public NumberFormatPattern(string format, ushort id = 0) public NumberFormatPattern(string format, ushort id = 0)
{ {

View File

@@ -0,0 +1,149 @@
namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Методы расширения для установки границ диапазона.
/// </summary>
public static class RangeBorderExtensions
{
/// <summary>
/// Устанавливает границы для диапазона с указанным стилем и цветом.
/// </summary>
/// <param name="range">Диапазон ячеек.</param>
/// <param name="style">Стиль линии границы.</param>
/// <param name="color">Цвет границы (необязательно).</param>
/// <param name="target">Какие границы применять (по умолчанию Outside).</param>
/// <returns>Тот же диапазон для цепочки вызовов.</returns>
public static IRange Set(this IRange range, BorderStyle style, BorderTarget target = BorderTarget.Outside,
System.Drawing.Color? color = null) => range.Set(new() { Style = style, Color = color ?? System.Drawing.Color.Black }, target);
/// <summary>
/// Устанавливает границы для диапазона с указанными параметрами.
/// </summary>
/// <param name="range">Диапазон ячеек.</param>
/// <param name="borderSide">Стиль и цвет границы.</param>
/// <param name="target">Какие границы применять (по умолчанию Outside).</param>
/// <returns>Тот же диапазон для цепочки вызовов.</returns>
public static IRange Set(this IRange range, BorderSide borderSide, BorderTarget target = BorderTarget.Outside)
{
if (range is null) throw new ArgumentNullException(nameof(range));
if (range is not ExcelRange excelRange)
throw new ArgumentException("Range must be of type ExcelRange", nameof(range));
var writer = excelRange._writer;
var sheet = excelRange._sheet;
uint rowStart = range.RowStart;
uint rowEnd = range.RowEnd;
uint colStart = range.ColStart;
uint colEnd = range.ColEnd;
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
// Определяем, какие стороны нужны для каждой ячейки
for (uint r = rowStart; r <= rowEnd; r++)
{
for (uint c = colStart; c <= colEnd; c++)
{
var cell = new ExcelCell(writer, sheet, r, c);
var newBorder = new CellBorder();
bool isTopRow = (r == rowStart);
bool isBottomRow = (r == rowEnd);
bool isLeftCol = (c == colStart);
bool isRightCol = (c == colEnd);
// В зависимости от target, определяем, какие стороны устанавливать
if (target == BorderTarget.All || target == BorderTarget.Outside)
{
if (isTopRow) newBorder = newBorder with { TopBorder = borderSide };
if (isBottomRow) newBorder = newBorder with { BottomBorder = borderSide };
if (isLeftCol) newBorder = newBorder with { LeftBorder = borderSide };
if (isRightCol) newBorder = newBorder with { RightBorder = borderSide };
}
if (target == BorderTarget.All || target == BorderTarget.Inside)
{
// Внутренние границы: для каждой ячейки устанавливаем правую и нижнюю,
// если есть сосед справа/снизу внутри диапазона
if (c < colEnd) newBorder = newBorder with { RightBorder = borderSide };
if (r < rowEnd) newBorder = newBorder with { BottomBorder = borderSide };
}
// Отдельные стороны
if (target == BorderTarget.Top && isTopRow)
newBorder = newBorder with { TopBorder = borderSide };
if (target == BorderTarget.Bottom && isBottomRow)
newBorder = newBorder with { BottomBorder = borderSide };
if (target == BorderTarget.Left && isLeftCol)
newBorder = newBorder with { LeftBorder = borderSide };
if (target == BorderTarget.Right && isRightCol)
newBorder = newBorder with { RightBorder = borderSide };
// Применяем границу к ячейке (изолированно, без каскада)
cell.SetBorderIsolate(newBorder);
// Если ячейка находится на внешней границе диапазона, очищаем соответствующую сторону у соседа вне диапазона
if (isTopRow && (target == BorderTarget.Outside || target == BorderTarget.All || target == BorderTarget.Top))
ClearNeighborBorder(cell, -1, 0, b => b with { BottomBorder = null });
if (isBottomRow && (target == BorderTarget.Outside || target == BorderTarget.All || target == BorderTarget.Bottom))
ClearNeighborBorder(cell, 1, 0, b => b with { TopBorder = null });
if (isLeftCol && (target == BorderTarget.Outside || target == BorderTarget.All || target == BorderTarget.Left))
ClearNeighborBorder(cell, 0, -1, b => b with { RightBorder = null });
if (isRightCol && (target == BorderTarget.Outside || target == BorderTarget.All || target == BorderTarget.Right))
ClearNeighborBorder(cell, 0, 1, b => b with { LeftBorder = null });
}
}
}
return range;
}
/// <summary>
/// Очищает все границы в диапазоне (сбрасывает до None).
/// </summary>
public static IRange ClearBorders(this IRange range)
{
if (range is null) throw new ArgumentNullException(nameof(range));
if (range is not ExcelRange excelRange)
throw new ArgumentException("Range must be of type ExcelRange", nameof(range));
var writer = excelRange._writer;
var sheet = excelRange._sheet;
uint rowStart = range.RowStart;
uint rowEnd = range.RowEnd;
uint colStart = range.ColStart;
uint colEnd = range.ColEnd;
writer.ThrowIfDisposed();
lock (writer._syncLock)
{
for (uint r = rowStart; r <= rowEnd; r++)
{
for (uint c = colStart; c <= colEnd; c++)
{
var cell = new ExcelCell(writer, sheet, r, c);
// Удаляем все границы
cell.SetBorderIsolate(new CellBorder()); // пустая граница
}
}
}
return range;
}
// Вспомогательный метод для очистки конкретной стороны у соседа
static void ClearNeighborBorder(ExcelCell cell, int rowOffset, int colOffset, Func<CellBorder, CellBorder> clearFunc)
{
var neighbor = cell.GetNeighbor(rowOffset, colOffset);
if (neighbor is null)
return;
var neighborBorder = neighbor.GetCellBorder();
var newBorder = clearFunc(neighborBorder);
if (!neighborBorder.Equals(newBorder))
neighbor.SetBorderIsolate(newBorder);
}
}

View File

@@ -5,13 +5,13 @@
/// </summary> /// </summary>
public readonly struct RowHeight(double points) public readonly struct RowHeight(double points)
{ {
private const double POINTS_PER_INCH = 72.0; const double POINTS_PER_INCH = 72.0;
private const double INCH_PER_CM = 1.0 / 2.54; const double INCH_PER_CM = 1.0 / 2.54;
private const double POINTS_PER_CM = POINTS_PER_INCH * INCH_PER_CM; // ≈ 28.3464566929 const double POINTS_PER_CM = POINTS_PER_INCH * INCH_PER_CM; // ≈ 28.3464566929
private const double POINTS_PER_MM = POINTS_PER_CM / 10.0; // ≈ 2.83464566929 const double POINTS_PER_MM = POINTS_PER_CM / 10.0; // ≈ 2.83464566929
private const double DXA_PER_POINT = 20.0; // 1 point = 20 dxa const double DXA_PER_POINT = 20.0; // 1 point = 20 dxa
private const double POINTS_PER_DXA = 1.0 / DXA_PER_POINT; const double POINTS_PER_DXA = 1.0 / DXA_PER_POINT;
/// <summary>Высота в пунктах (points).</summary> /// <summary>Высота в пунктах (points).</summary>
public double Points { get; } = points; public double Points { get; } = points;

View File

@@ -19,7 +19,7 @@ public readonly struct RunFormat
public bool? IsStrike { get; init; } public bool? IsStrike { get; init; }
/// <summary>Цвет текста фрагмента.</summary> /// <summary>Цвет текста фрагмента.</summary>
public ExColor? Color { get; init; } public System.Drawing.Color? Color { get; init; }
/// <summary>Размер шрифта фрагмента в пунктах.</summary> /// <summary>Размер шрифта фрагмента в пунктах.</summary>
public double? FontSize { get; init; } public double? FontSize { get; init; }
@@ -80,9 +80,7 @@ public readonly struct RunFormat
// DoubleStrike в Excel не поддерживается, опускаем // DoubleStrike в Excel не поддерживается, опускаем
Underline = underline, Underline = underline,
Color = rPr.GetFirstChild<Color>()?.Rgb is not null Color = FromExcelColor(rPr.GetFirstChild<Color>()?.Rgb),
? ExColor.FromRgb(rPr.GetFirstChild<Color>()!.Rgb!.Value!)
: null!,
FontSize = rPr.GetFirstChild<FontSize>()?.Val?.Value, FontSize = rPr.GetFirstChild<FontSize>()?.Val?.Value,
FontFamily = rPr.GetFirstChild<RunFont>()?.Val, FontFamily = rPr.GetFirstChild<RunFont>()?.Val,
Vertical = vertical Vertical = vertical
@@ -90,6 +88,45 @@ public readonly struct RunFormat
return fmt; return fmt;
} }
static System.Drawing.Color? FromExcelColor(string? rgb)
{
if (string.IsNullOrWhiteSpace(rgb)) return null;
if (rgb!.Length == 6)
{
byte r = Convert.ToByte(rgb.Substring(0, 2), 16);
byte g = Convert.ToByte(rgb.Substring(2, 2), 16);
byte b = Convert.ToByte(rgb.Substring(4, 2), 16);
return System.Drawing.Color.FromArgb(r, g, b);
}
else if (rgb.Length == 8)
{
byte a = Convert.ToByte(rgb.Substring(0, 2), 16);
byte r = Convert.ToByte(rgb.Substring(2, 2), 16);
byte g = Convert.ToByte(rgb.Substring(4, 2), 16);
byte b = Convert.ToByte(rgb.Substring(6, 2), 16);
return System.Drawing.Color.FromArgb(a, r, g, b);
}
else return null;
}
public bool TryGetExcelColor(out Color excelColor)
{
if (Color is { } c)
{
excelColor = new Color() { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" };
return true;
}
else
{
excelColor = null!;
return false;
}
}
/* /*
методы для извлечения OpenXmlElement или других более удобных типов методы для извлечения OpenXmlElement или других более удобных типов

View File

@@ -8,9 +8,56 @@ namespace QWERTYkez.ExcelProcessor;
/// </summary> /// </summary>
internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
{ {
readonly Dictionary<CellStyle, int> _styleCache = [];
internal int GetOrCreateStyleId(CellStyle style)
{
lock (_syncLock)
{
if (_styleCache.TryGetValue(style, out int id))
return id;
// Создаём CellFormat через существующий метод GetOrCreateCellFormatId
int newId = GetOrCreateCellFormatId(
style.NumberFormat,
style.Font,
style.Fill,
style.Border,
style.Align
);
_styleCache[style] = newId;
return newId;
}
}
internal CellStyle? GetCellStyle(uint styleIndex)
{
if (styleIndex == 0) return null;
var align = GetCellAlign(styleIndex);
var font = GetCellFont(styleIndex);
var fill = GetCellFill(styleIndex);
var border = GetCellBorder(styleIndex);
var numberFormat = GetNumberFormat(styleIndex);
bool hasAny = !align.Equals(default) || !font.Equals(default) || !fill.Equals(default) ||
!border.Equals(default) || numberFormat != null;
if (!hasAny) return null;
return new CellStyle
{
Align = align.Equals(default) ? null : align,
Font = font.Equals(default) ? null : font,
Fill = fill.Equals(default) ? null : fill,
Border = border.Equals(default) ? null : border,
NumberFormat = numberFormat
};
}
// Работа с общей таблицей строк // Работа с общей таблицей строк
private SharedStringTablePart? _sharedStringPart; SharedStringTablePart? _sharedStringPart;
private SharedStringTable? _sharedStringTable; SharedStringTable? _sharedStringTable;
internal static Dictionary<int, double>? _calibrationTable; // cw -> width_pts internal static Dictionary<int, double>? _calibrationTable; // cw -> width_pts
@@ -62,7 +109,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
return true; return true;
} }
private Dictionary<int, double> CalibrateWidthCoeffUsingInterop() Dictionary<int, double> CalibrateWidthCoeffUsingInterop()
{ {
object? excelApp = null; object? excelApp = null;
object? workbooks = null; object? workbooks = null;
@@ -133,7 +180,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
} }
private void EnsureSharedStringTable() void EnsureSharedStringTable()
{ {
if (_sharedStringPart != null) return; if (_sharedStringPart != null) return;
_sharedStringPart = _doc.WorkbookPart?.GetPartsOfType<SharedStringTablePart>().FirstOrDefault(); _sharedStringPart = _doc.WorkbookPart?.GetPartsOfType<SharedStringTablePart>().FirstOrDefault();
@@ -193,17 +240,17 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
} }
// Кэши числовых форматов // Кэши числовых форматов
private readonly Dictionary<string, NumberFormatPattern> _numberFormatCache = []; readonly Dictionary<string, NumberFormatPattern> _numberFormatCache = [];
private readonly Dictionary<uint, NumberFormatPattern> _numberFormatIdToPattern = []; readonly Dictionary<uint, NumberFormatPattern> _numberFormatIdToPattern = [];
// Кэши для компонентов стилей (чтобы не создавать дубликаты) // Кэши для компонентов стилей (чтобы не создавать дубликаты)
private readonly Dictionary<CellFont, int> _fontCache = []; readonly Dictionary<CellFont, int> _fontCache = [];
private readonly Dictionary<CellFill, int> _fillCache = []; readonly Dictionary<CellFill, int> _fillCache = [];
private readonly Dictionary<CellBorder, int> _borderCache = []; readonly Dictionary<CellBorder, int> _borderCache = [];
private readonly Dictionary<CellAlign, int> _alignmentCache = []; readonly Dictionary<CellAlign, int> _alignmentCache = [];
// Кэш составных стилей (CellFormat) // Кэш составных стилей (CellFormat)
private readonly Dictionary<(int fontId, int fillId, int borderId, int alignId, int numFmtId), int> _cellFormatCache = []; readonly Dictionary<(int fontId, int fillId, int borderId, int alignId, int numFmtId), int> _cellFormatCache = [];
// Конструктор, фабричные методы без изменений (опущены) // Конструктор, фабричные методы без изменений (опущены)
@@ -294,10 +341,8 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
int numFmtId = -1; int numFmtId = -1;
if (numberFormat != null) if (numberFormat != null)
{ {
if (numberFormat.Id.HasValue && numberFormat.Id.Value < 164) numFmtId = numberFormat.Id.HasValue && numberFormat.Id.Value < 164
numFmtId = numberFormat.Id.Value; ? numberFormat.Id.Value : (int)GetOrCreateNumberFormatId(numberFormat);
else
numFmtId = (int)GetOrCreateNumberFormatId(numberFormat);
} }
// Получаем или создаём Font, Fill, Border, Alignment // Получаем или создаём Font, Fill, Border, Alignment
@@ -352,7 +397,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
#region Вспомогательные методы для работы со стилями #region Вспомогательные методы для работы со стилями
private Stylesheet EnsureStylesheet() Stylesheet EnsureStylesheet()
{ {
var workbookPart = _doc.WorkbookPart ?? throw new InvalidOperationException("No WorkbookPart"); var workbookPart = _doc.WorkbookPart ?? throw new InvalidOperationException("No WorkbookPart");
var stylesPart = workbookPart.WorkbookStylesPart; var stylesPart = workbookPart.WorkbookStylesPart;
@@ -390,7 +435,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
return stylesheet; return stylesheet;
} }
private uint GetOrCreateNumberFormatId(NumberFormatPattern pattern) uint GetOrCreateNumberFormatId(NumberFormatPattern pattern)
{ {
if (pattern.Id.HasValue) if (pattern.Id.HasValue)
return (uint)pattern.Id.Value; return (uint)pattern.Id.Value;
@@ -400,7 +445,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
return (uint)created.Id!.Value; return (uint)created.Id!.Value;
} }
private int GetOrCreateFontId(CellFont font) int GetOrCreateFontId(CellFont font)
{ {
if (_fontCache.TryGetValue(font, out int id)) if (_fontCache.TryGetValue(font, out int id))
return id; return id;
@@ -414,7 +459,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
return _fontCache[font] = (int)newId; return _fontCache[font] = (int)newId;
} }
private int GetOrCreateFillId(CellFill fill) int GetOrCreateFillId(CellFill fill)
{ {
if (_fillCache.TryGetValue(fill, out int id)) if (_fillCache.TryGetValue(fill, out int id))
return id; return id;
@@ -428,7 +473,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
return _fillCache[fill] = (int)newId; return _fillCache[fill] = (int)newId;
} }
private int GetOrCreateBorderId(CellBorder border) int GetOrCreateBorderId(CellBorder border)
{ {
if (_borderCache.TryGetValue(border, out int id)) if (_borderCache.TryGetValue(border, out int id))
return id; return id;
@@ -443,7 +488,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
return newId; return newId;
} }
private int GetOrCreateAlignmentId(CellAlign align) int GetOrCreateAlignmentId(CellAlign align)
{ {
if (_alignmentCache.TryGetValue(align, out int id)) if (_alignmentCache.TryGetValue(align, out int id))
return id; return id;
@@ -456,7 +501,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
return id; return id;
} }
private Alignment GetAlignmentFromCache(int alignId) Alignment GetAlignmentFromCache(int alignId)
{ {
foreach (var pair in _alignmentCache) foreach (var pair in _alignmentCache)
if (pair.Value == alignId) if (pair.Value == alignId)
@@ -464,7 +509,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
return new Alignment(); return new Alignment();
} }
private CellFormat? GetCellFormatAt(uint index) CellFormat? GetCellFormatAt(uint index)
{ {
var stylesheet = EnsureStylesheet(); var stylesheet = EnsureStylesheet();
if (stylesheet.CellFormats == null || index >= stylesheet.CellFormats.Count!.Value) if (stylesheet.CellFormats == null || index >= stylesheet.CellFormats.Count!.Value)
@@ -473,14 +518,14 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
} }
// Вспомогательные создания коллекций // Вспомогательные создания коллекций
private static NumberingFormats CreateNumberingFormats(Stylesheet stylesheet) static NumberingFormats CreateNumberingFormats(Stylesheet stylesheet)
{ {
var nfs = new NumberingFormats(); var nfs = new NumberingFormats();
stylesheet.NumberingFormats = nfs; stylesheet.NumberingFormats = nfs;
return nfs; return nfs;
} }
private static CellFormats CreateCellFormats(Stylesheet stylesheet) static CellFormats CreateCellFormats(Stylesheet stylesheet)
{ {
var cfs = new CellFormats(); var cfs = new CellFormats();
stylesheet.CellFormats = cfs; stylesheet.CellFormats = cfs;
@@ -491,7 +536,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
private bool _isModified = false; bool _isModified = false;
internal ExcelWriter() { } internal ExcelWriter() { }
@@ -1006,7 +1051,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
#endregion #endregion
private void EnsureFullCalculationOnLoad() void EnsureFullCalculationOnLoad()
{ {
if (_doc?.WorkbookPart?.Workbook == null) return; if (_doc?.WorkbookPart?.Workbook == null) return;
var workbook = _doc.WorkbookPart.Workbook; var workbook = _doc.WorkbookPart.Workbook;
@@ -1217,7 +1262,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
// Внутренний метод, предполагает, что _syncLock уже захвачен вызывающим // Внутренний метод, предполагает, что _syncLock уже захвачен вызывающим
private NumberFormatPattern CreateNumberFormatInternal(string formatCode) NumberFormatPattern CreateNumberFormatInternal(string formatCode)
{ {
// Проверяем кэш по коду // Проверяем кэш по коду
if (_numberFormatCache.TryGetValue(formatCode, out var existing)) if (_numberFormatCache.TryGetValue(formatCode, out var existing))
@@ -1257,36 +1302,42 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
// Вспомогательные методы для извлечения элементов из стилей (нужно закешировать или обращаться напрямую) // Вспомогательные методы для извлечения элементов из стилей (нужно закешировать или обращаться напрямую)
private Border? GetBorderById(uint borderId) Border? GetBorderById(uint borderId)
{
if (_cachedBorders == null)
{ {
var borders = EnsureStylesheet().Borders; var borders = EnsureStylesheet().Borders;
_cachedBorders = borders?.Elements<Border>().ToList() ?? []; if (borders == null) return null;
} int index = 0;
return borderId < _cachedBorders.Count ? _cachedBorders[(int)borderId] : null; foreach (var border in borders.Elements<Border>())
}
private List<Border>? _cachedBorders;
private Fill? GetFillById(uint borderId)
{ {
if (_cachedFills == null) if (index == borderId) return border;
index++;
}
return null;
}
Fill? GetFillById(uint fillId)
{ {
var fills = EnsureStylesheet().Fills; var fills = EnsureStylesheet().Fills;
_cachedFills = fills?.Elements<Fill>().ToList() ?? []; if (fills == null) return null;
int index = 0;
foreach (var fill in fills.Elements<Fill>())
{
if (index == fillId) return fill;
index++;
} }
return borderId < _cachedFills.Count ? _cachedFills[(int)borderId] : null; return null;
} }
private List<Fill>? _cachedFills;
private Font? GetFontById(uint borderId) Font? GetFontById(uint fontId)
{ {
if (_cachedFonts == null) var fonts = EnsureStylesheet().Fonts;
if (fonts == null) return null;
int index = 0;
foreach (var font in fonts.Elements<Font>())
{ {
var borders = EnsureStylesheet().Borders; if (index == fontId) return font;
_cachedFonts = borders?.Elements<Font>().ToList() ?? []; index++;
} }
return borderId < _cachedFonts.Count ? _cachedFonts[(int)borderId] : null; return null;
} }
private List<Font>? _cachedFonts;
} }

View File

@@ -10,7 +10,7 @@ namespace QWERTYkez.ExcelProcessor;
/// </summary> /// </summary>
internal class NormalizedSet : ISet<string> internal class NormalizedSet : ISet<string>
{ {
private readonly HashSet<string> _inner; readonly HashSet<string> _inner;
/// <summary> /// <summary>
/// Создаёт пустое нормализованное множество. /// Создаёт пустое нормализованное множество.
@@ -32,7 +32,7 @@ internal class NormalizedSet : ISet<string>
/// <summary> /// <summary>
/// Нормализует строку: верхний регистр и удаление диакритики. /// Нормализует строку: верхний регистр и удаление диакритики.
/// </summary> /// </summary>
private static string Normalize(string s) static string Normalize(string s)
{ {
if (string.IsNullOrEmpty(s)) if (string.IsNullOrEmpty(s))
return s; return s;

View File

@@ -22,7 +22,7 @@ internal static class PlaceholderFinder
return FindInDocument(doc, [.. doc.WorkbookPart.WorksheetParts]); return FindInDocument(doc, [.. doc.WorkbookPart.WorksheetParts]);
} }
private static void ProcessWorksheet(WorksheetPart wsPart, SharedStringTable? sharedStrings, ISet<string> result) static void ProcessWorksheet(WorksheetPart wsPart, SharedStringTable? sharedStrings, ISet<string> result)
{ {
var worksheet = wsPart.Worksheet; var worksheet = wsPart.Worksheet;
if (worksheet is null) return; if (worksheet is null) return;
@@ -31,7 +31,7 @@ internal static class PlaceholderFinder
ProcessHeaderFooter(worksheet, result); ProcessHeaderFooter(worksheet, result);
} }
private static void ProcessCells(Worksheet worksheet, SharedStringTable? sharedStrings, ISet<string> result) static void ProcessCells(Worksheet worksheet, SharedStringTable? sharedStrings, ISet<string> result)
{ {
var sheetData = worksheet.GetFirstChild<SheetData>(); var sheetData = worksheet.GetFirstChild<SheetData>();
if (sheetData is null) return; if (sheetData is null) return;
@@ -59,7 +59,7 @@ internal static class PlaceholderFinder
} }
} }
private static void ProcessHeaderFooter(Worksheet worksheet, ISet<string> result) static void ProcessHeaderFooter(Worksheet worksheet, ISet<string> result)
{ {
var hf = worksheet.Descendants<HeaderFooter>().FirstOrDefault(); var hf = worksheet.Descendants<HeaderFooter>().FirstOrDefault();
if (hf is null) return; if (hf is null) return;
@@ -84,7 +84,7 @@ internal static class PlaceholderFinder
} }
} }
private static string GetCellValue(Cell cell, SharedStringTable? sharedStrings) static string GetCellValue(Cell cell, SharedStringTable? sharedStrings)
{ {
if (cell?.CellValue is null && cell?.InlineString is null) if (cell?.CellValue is null && cell?.InlineString is null)
return string.Empty; return string.Empty;
@@ -107,7 +107,7 @@ internal static class PlaceholderFinder
return raw; return raw;
} }
private static unsafe bool FindPlaceholdersInText(string text, ref List<string>? output) static unsafe bool FindPlaceholdersInText(string text, ref List<string>? output)
{ {
fixed (char* pText = text) fixed (char* pText = text)
{ {

View File

@@ -158,7 +158,7 @@ internal static class ReplaceNumericExtensions
// =========================== ОБЩАЯ ЛОГИКА =========================== // =========================== ОБЩАЯ ЛОГИКА ===========================
private static void ReplaceNumericCore<T>( static void ReplaceNumericCore<T>(
WorkbookPart workbookPart, WorkbookPart workbookPart,
WorksheetPart[] worksheets, WorksheetPart[] worksheets,
IEnumerable<KeyValuePair<string, T>> numericReplacements, IEnumerable<KeyValuePair<string, T>> numericReplacements,
@@ -255,7 +255,7 @@ internal static class ReplaceNumericExtensions
UpdateSharedStringTable(workbookPart, allSharedStrings); UpdateSharedStringTable(workbookPart, allSharedStrings);
} }
private static void ReplaceSingleCore<T>( static void ReplaceSingleCore<T>(
WorkbookPart workbookPart, WorkbookPart workbookPart,
WorksheetPart[] worksheets, WorksheetPart[] worksheets,
string oldValue, string oldValue,
@@ -343,7 +343,7 @@ internal static class ReplaceNumericExtensions
// =========================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ =========================== // =========================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ===========================
private static string GetCellTextForNumeric(Cell cell, List<string> allSharedStrings) static string GetCellTextForNumeric(Cell cell, List<string> allSharedStrings)
{ {
if (cell?.CellValue == null) return string.Empty; if (cell?.CellValue == null) return string.Empty;
@@ -366,7 +366,7 @@ internal static class ReplaceNumericExtensions
return val; return val;
} }
private static void SetCellText(Cell cell, string newText, static void SetCellText(Cell cell, string newText,
List<string> allSharedStrings, Dictionary<string, int> sharedStringIndexMap) List<string> allSharedStrings, Dictionary<string, int> sharedStringIndexMap)
{ {
if (cell.InlineString != null) if (cell.InlineString != null)
@@ -388,7 +388,7 @@ internal static class ReplaceNumericExtensions
cell.CellValue = new CellValue(index.ToString()); cell.CellValue = new CellValue(index.ToString());
} }
private static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings) static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings)
{ {
var ssPart = workbookPart.SharedStringTablePart; var ssPart = workbookPart.SharedStringTablePart;
ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>(); ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>();
@@ -399,7 +399,7 @@ internal static class ReplaceNumericExtensions
sharedStringTable.Save(); sharedStringTable.Save();
} }
private static string ConcatTexts(IEnumerable<Text> texts) static string ConcatTexts(IEnumerable<Text> texts)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var t in texts) foreach (var t in texts)
@@ -408,7 +408,7 @@ internal static class ReplaceNumericExtensions
} }
// Оптимизированная замена подстроки через string.Create (без unsafe) // Оптимизированная замена подстроки через string.Create (без unsafe)
private static unsafe string ReplaceSubstring(string original, int start, int length, string replacement) static unsafe string ReplaceSubstring(string original, int start, int length, string replacement)
{ {
if (length == 0) return original; if (length == 0) return original;
int newLen = original.Length - length + replacement.Length; int newLen = original.Length - length + replacement.Length;
@@ -428,7 +428,7 @@ internal static class ReplaceNumericExtensions
} }
} }
private static uint CreateNumberFormat(WorkbookPart workbookPart, string format) static uint CreateNumberFormat(WorkbookPart workbookPart, string format)
{ {
var stylesPart = workbookPart.WorkbookStylesPart; var stylesPart = workbookPart.WorkbookStylesPart;
if (stylesPart == null) if (stylesPart == null)
@@ -457,7 +457,7 @@ internal static class ReplaceNumericExtensions
// =========================== КОЛОНТИТУЛЫ И КОММЕНТАРИИ =========================== // =========================== КОЛОНТИТУЛЫ И КОММЕНТАРИИ ===========================
private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType) static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
var worksheet = worksheetPart.Worksheet; var worksheet = worksheetPart.Worksheet;
if (worksheet is null) return; if (worksheet is null) return;
@@ -468,7 +468,7 @@ internal static class ReplaceNumericExtensions
ReplaceHeaderFooter(elem, replacementDict, comparisonType); ReplaceHeaderFooter(elem, replacementDict, comparisonType);
} }
private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType) static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
if (element?.Text is null) return; if (element?.Text is null) return;
string original = element.Text; string original = element.Text;
@@ -477,7 +477,7 @@ internal static class ReplaceNumericExtensions
element.Text = processed; element.Text = processed;
} }
private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType) static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
var commentsPart = worksheetPart.WorksheetCommentsPart; var commentsPart = worksheetPart.WorksheetCommentsPart;
if (commentsPart?.Comments is null) return; if (commentsPart?.Comments is null) return;
@@ -493,7 +493,7 @@ internal static class ReplaceNumericExtensions
} }
} }
private static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType) static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input; if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input;
string result = input; string result = input;
@@ -505,7 +505,7 @@ internal static class ReplaceNumericExtensions
return result; return result;
} }
private static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType) static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType)
{ {
int idx = original.IndexOf(oldValue, comparisonType); int idx = original.IndexOf(oldValue, comparisonType);
if (idx < 0) return original; if (idx < 0) return original;

View File

@@ -30,7 +30,7 @@ internal static class ReplaceStringExtensions
} }
// --- Общий приватный метод, содержащий всю логику замены --- // --- Общий приватный метод, содержащий всю логику замены ---
private static void ReplaceCore(SpreadsheetDocument doc, WorksheetPart[] worksheets, Dictionary<string, string> replacementDict, StringComparison comparisonType) static void ReplaceCore(SpreadsheetDocument doc, WorksheetPart[] worksheets, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
var workbookPart = doc.WorkbookPart!; var workbookPart = doc.WorkbookPart!;
@@ -63,7 +63,7 @@ internal static class ReplaceStringExtensions
} }
// --- Остальные вспомогательные методы (без изменений) --- // --- Остальные вспомогательные методы (без изменений) ---
private static IEqualityComparer<string> GetComparerForStringComparison(StringComparison comparisonType) => static IEqualityComparer<string> GetComparerForStringComparison(StringComparison comparisonType) =>
comparisonType switch comparisonType switch
{ {
StringComparison.Ordinal => StringComparer.Ordinal, StringComparison.Ordinal => StringComparer.Ordinal,
@@ -75,7 +75,7 @@ internal static class ReplaceStringExtensions
_ => StringComparer.OrdinalIgnoreCase, _ => StringComparer.OrdinalIgnoreCase,
}; };
private static void CollectCellChanges(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType, Dictionary<Cell, string> cellChanges, List<string> allSharedStrings) static void CollectCellChanges(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType, Dictionary<Cell, string> cellChanges, List<string> allSharedStrings)
{ {
var worksheet = worksheetPart.Worksheet; var worksheet = worksheetPart.Worksheet;
if (worksheet is null) return; if (worksheet is null) return;
@@ -93,7 +93,7 @@ internal static class ReplaceStringExtensions
} }
} }
private static void ApplyCellChanges(Dictionary<Cell, string> cellChanges, List<string> allSharedStrings) static void ApplyCellChanges(Dictionary<Cell, string> cellChanges, List<string> allSharedStrings)
{ {
foreach (var kvp in cellChanges) foreach (var kvp in cellChanges)
{ {
@@ -114,7 +114,7 @@ internal static class ReplaceStringExtensions
} }
} }
private static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings) static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings)
{ {
var ssPart = workbookPart.SharedStringTablePart; var ssPart = workbookPart.SharedStringTablePart;
ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>(); ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>();
@@ -125,7 +125,7 @@ internal static class ReplaceStringExtensions
sharedStringTable.Save(); sharedStringTable.Save();
} }
private static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType) static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input; if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input;
string result = input; string result = input;
@@ -137,7 +137,7 @@ internal static class ReplaceStringExtensions
return result; return result;
} }
private static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType) static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType)
{ {
int idx = original.IndexOf(oldValue, comparisonType); int idx = original.IndexOf(oldValue, comparisonType);
if (idx < 0) return original; if (idx < 0) return original;
@@ -154,7 +154,7 @@ internal static class ReplaceStringExtensions
return sb.ToString(); return sb.ToString();
} }
private static string GetCellText(Cell cell, List<string> allSharedStrings) static string GetCellText(Cell cell, List<string> allSharedStrings)
{ {
if (cell?.CellValue is null) return string.Empty; if (cell?.CellValue is null) return string.Empty;
if (cell.InlineString is not null) if (cell.InlineString is not null)
@@ -169,7 +169,7 @@ internal static class ReplaceStringExtensions
return value; return value;
} }
private static int AddOrFindStringIndex(List<string> allSharedStrings, string text) static int AddOrFindStringIndex(List<string> allSharedStrings, string text)
{ {
int idx = allSharedStrings.IndexOf(text); int idx = allSharedStrings.IndexOf(text);
if (idx >= 0) return idx; if (idx >= 0) return idx;
@@ -178,7 +178,7 @@ internal static class ReplaceStringExtensions
} }
// --- Колонтитулы (обобщённый метод) --- // --- Колонтитулы (обобщённый метод) ---
private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType) static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
var worksheet = worksheetPart.Worksheet; var worksheet = worksheetPart.Worksheet;
if (worksheet is null) return; if (worksheet is null) return;
@@ -189,7 +189,7 @@ internal static class ReplaceStringExtensions
ReplaceHeaderFooter(elem, replacementDict, comparisonType); ReplaceHeaderFooter(elem, replacementDict, comparisonType);
} }
private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType) static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
if (element?.Text is null) return; if (element?.Text is null) return;
string original = element.Text; string original = element.Text;
@@ -199,7 +199,7 @@ internal static class ReplaceStringExtensions
} }
// --- Комментарии --- // --- Комментарии ---
private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType) static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{ {
var commentsPart = worksheetPart.WorksheetCommentsPart; var commentsPart = worksheetPart.WorksheetCommentsPart;
if (commentsPart?.Comments is null) return; if (commentsPart?.Comments is null) return;