diff --git a/QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs b/QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs index 04dc58f..32d1af3 100644 --- a/QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs +++ b/QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs @@ -2,8 +2,8 @@ internal static class CellAddressHelper { - private const string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - private static readonly uint[] _powers = [1, 26, 676]; + const string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + static readonly uint[] _powers = [1, 26, 676]; public static uint ColumnLetterToIndex(string col) { diff --git a/QWERTYkez.ExcelProcessor/Editors/CellAlign.cs b/QWERTYkez.ExcelProcessor/Editors/CellAlign.cs index 48dc826..08f1243 100644 --- a/QWERTYkez.ExcelProcessor/Editors/CellAlign.cs +++ b/QWERTYkez.ExcelProcessor/Editors/CellAlign.cs @@ -18,6 +18,30 @@ public readonly struct CellAlign : IEquatable /// Уменьшать размер шрифта, чтобы текст поместился в ячейку. 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; + } + + /// Преобразует горизонтальное выравнивание в тип Open XML. public bool TryGetExcelHorizontalAlignment(out HorizontalAlignmentValues value) { @@ -104,7 +128,7 @@ public readonly struct CellAlign : IEquatable return result; } - private static CellAlignHorizontal MapHorizontalFromExcel(HorizontalAlignmentValues value) + static CellAlignHorizontal MapHorizontalFromExcel(HorizontalAlignmentValues value) { if (value == HorizontalAlignmentValues.Left) { @@ -137,7 +161,7 @@ public readonly struct CellAlign : IEquatable else throw new NotSupportedException($"Unsupported horizontal alignment: {value}"); } - private static CellAlignVertical MapVerticalFromExcel(VerticalAlignmentValues value) + static CellAlignVertical MapVerticalFromExcel(VerticalAlignmentValues value) { if (value == VerticalAlignmentValues.Top) { diff --git a/QWERTYkez.ExcelProcessor/Editors/CellBorder.cs b/QWERTYkez.ExcelProcessor/Editors/CellBorder.cs index c1139da..34db090 100644 --- a/QWERTYkez.ExcelProcessor/Editors/CellBorder.cs +++ b/QWERTYkez.ExcelProcessor/Editors/CellBorder.cs @@ -1,11 +1,29 @@ namespace QWERTYkez.ExcelProcessor; + + + /// /// Определяет границы ячейки: верхнюю, нижнюю, левую, правую и диагональные. /// Каждая граница может иметь стиль и цвет. /// public readonly struct CellBorder : IEquatable { + + public static CellBorder Bottom { get; } = new() { BottomBorder = BorderSide.BlackThin }; + public static CellBorder Top { get; } = new() { TopBorder = BorderSide.BlackThin }; + public static CellBorder Left { get; } = new() { LeftBorder = BorderSide.BlackThin }; + public static CellBorder Right { get; } = new() { RightBorder = BorderSide.BlackThin }; + public static CellBorder All { get; } = new() + { + BottomBorder = BorderSide.BlackThin, + TopBorder = BorderSide.BlackThin, + LeftBorder = BorderSide.BlackThin, + RightBorder = BorderSide.BlackThin + }; + + + /// Верхняя граница. public BorderSide? TopBorder { get; init; } @@ -24,6 +42,33 @@ public readonly struct CellBorder : IEquatable /// Диагональная граница «из левого нижнего в правый верхний» (//). 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; + } + + /// Создаёт элемент Border для Open XML. public Border? ToBorder() { @@ -112,11 +157,23 @@ public readonly struct CellBorder : IEquatable /// Стиль и цвет границы. public readonly struct BorderSide : IEquatable { + /// Тонкая черная линия + public static BorderSide BlackThin { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Thin }; + + /// Толстая черная линия + public static BorderSide BlackThick { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Thick }; + + /// Средняя черная линия + public static BorderSide BlackMedium { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Medium }; + + + + /// Стиль линии границы. public BorderStyle? Style { get; init; } /// Цвет границы. - public ExColor? Color { get; init; } + public System.Drawing.Color? Color { get; init; } internal T ToBorderElement() where T : BorderPropertiesType, new() { @@ -141,9 +198,9 @@ public readonly struct BorderSide : IEquatable _ => 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}" }; } return element; @@ -162,17 +219,18 @@ public readonly struct BorderSide : IEquatable } if (borderElement.Color?.Rgb?.Value is { } rgb && rgb.Length >= 6) { - var color = System.Drawing.Color.FromArgb( - Convert.ToByte(rgb.Substring(0, 2), 16), - Convert.ToByte(rgb.Substring(2, 2), 16), - Convert.ToByte(rgb.Substring(4, 2), 16) - ); - result = result with { Color = new ExColor(color) }; + result = result with + { + Color = System.Drawing.Color.FromArgb( + Convert.ToByte(rgb.Substring(0, 2), 16), + Convert.ToByte(rgb.Substring(2, 2), 16), + Convert.ToByte(rgb.Substring(4, 2), 16)) + }; } return result; } - private static BorderStyle MapBorderStyleFromExcel(BorderStyleValues value) + static BorderStyle MapBorderStyleFromExcel(BorderStyleValues value) { if (value == BorderStyleValues.Thin) { @@ -278,4 +336,25 @@ public enum BorderStyle MediumDashDotDot, /// Наклонная штрих-пунктирная (для диагональных) SlantDashDot, +} + +/// +/// Определяет, какие границы диапазона следует применить. +/// +public enum BorderTarget +{ + /// Все границы (внешние и внутренние) – полная сетка. + All, + /// Только внешние границы диапазона. + Outside, + /// Только внутренние границы (между ячейками). + Inside, + /// Только верхняя граница диапазона. + Top, + /// Только нижняя граница диапазона. + Bottom, + /// Только левая граница диапазона. + Left, + /// Только правая граница диапазона. + Right } \ No newline at end of file diff --git a/QWERTYkez.ExcelProcessor/Editors/CellFill.cs b/QWERTYkez.ExcelProcessor/Editors/CellFill.cs index 8978408..ae5fd89 100644 --- a/QWERTYkez.ExcelProcessor/Editors/CellFill.cs +++ b/QWERTYkez.ExcelProcessor/Editors/CellFill.cs @@ -6,15 +6,13 @@ public readonly struct CellFill : IEquatable { /// Цвет фона. - public ExColor? BackgroundColor { get; init; } + public System.Drawing.Color? BackgroundColor { get; init; } /// Создаёт элемент Fill для Open XML. public Fill? ToFill() { - if (!BackgroundColor.HasValue || !BackgroundColor.Value.Color.HasValue) - return null; + if (BackgroundColor is not { } c) return null; - var c = BackgroundColor.Value.Color.Value; var fill = new Fill { PatternFill = new PatternFill @@ -37,7 +35,7 @@ public readonly struct CellFill : IEquatable Convert.ToByte(rgb.Substring(2, 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); diff --git a/QWERTYkez.ExcelProcessor/Editors/CellFont.cs b/QWERTYkez.ExcelProcessor/Editors/CellFont.cs index 37cdddf..17ce259 100644 --- a/QWERTYkez.ExcelProcessor/Editors/CellFont.cs +++ b/QWERTYkez.ExcelProcessor/Editors/CellFont.cs @@ -13,7 +13,7 @@ public readonly struct CellFont : IEquatable public string? FontFamily { get; init; } /// Цвет текста. - public ExColor? FontColor { get; init; } + public System.Drawing.Color? FontColor { get; init; } /// Жирное начертание. public bool? IsBold { get; init; } @@ -27,6 +27,35 @@ public readonly struct CellFont : IEquatable /// Зачёркивание. 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; + } + + /// Создаёт элемент Font для Open XML. public Font? ToFont() { @@ -39,11 +68,8 @@ public readonly struct CellFont : IEquatable font.FontSize = new FontSize { Val = FontSize.Value }; if (FontFamily is not null) font.FontName = new FontName { Val = FontFamily }; - if (FontColor.HasValue && FontColor.Value.Color.HasValue) - { - var c = FontColor.Value.Color.Value; + if (FontColor is { } c) font.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" }; - } if (IsBold == true) font.Bold = new Bold(); if (IsItalic == true) font.Italic = new Italic(); if (IsUnderline == true) font.Underline = new Underline(); @@ -75,7 +101,7 @@ public readonly struct CellFont : IEquatable Convert.ToByte(rgb.Substring(2, 2), 16), Convert.ToByte(rgb.Substring(4, 2), 16) ); - result = result with { FontColor = new ExColor(color) }; + result = result with { FontColor = color }; } return result; } diff --git a/QWERTYkez.ExcelProcessor/Editors/CellStyle.cs b/QWERTYkez.ExcelProcessor/Editors/CellStyle.cs new file mode 100644 index 0000000..06bf0c3 --- /dev/null +++ b/QWERTYkez.ExcelProcessor/Editors/CellStyle.cs @@ -0,0 +1,153 @@ +namespace QWERTYkez.ExcelProcessor; + +/// +/// Неизменяемый набор всех параметров оформления ячейки. +/// +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; + } +} \ No newline at end of file diff --git a/QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs b/QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs index 9883821..63893c3 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs @@ -6,15 +6,15 @@ /// public readonly struct ColumnWidth { - private readonly double _rawValue; - private readonly UnitType _unit; + readonly double _rawValue; + readonly UnitType _unit; - private enum UnitType { Characters, Points, Centimeters, Millimeters } + enum UnitType { Characters, Points, Centimeters, Millimeters } /// Коэффициент перевода символов в пункты по умолчанию (используется, если нет калибровочной таблицы). public static double DefaultPointsPerChar { get; set; } = 5.65; - private ColumnWidth(double value, UnitType unit) + ColumnWidth(double value, UnitType unit) { _rawValue = value; _unit = unit; diff --git a/QWERTYkez.ExcelProcessor/Editors/ExColor.cs b/QWERTYkez.ExcelProcessor/Editors/ExColor.cs deleted file mode 100644 index f67e910..0000000 --- a/QWERTYkez.ExcelProcessor/Editors/ExColor.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace QWERTYkez.ExcelProcessor; - -public readonly struct ExColor(System.Drawing.Color? Color) -{ - private readonly System.Drawing.Color? color = Color; - - /// Проверяет, является ли цвет автоматическим (т.е. Color == null). - 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); - } -} \ No newline at end of file diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs index 2f709e9..ee9efb0 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs @@ -5,9 +5,77 @@ /// internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col) : ICell { + + + /// + /// Возвращает эффективный стиль ячейки с учётом наследования от строки и столбца. + /// + public CellStyle? GetCellStyle() + { + var cell = GetCellElement(); + CellStyle? cellStyle = null; + + // 1. Пытаемся получить стиль самой ячейки + if (cell?.StyleIndex?.Value is uint cellStyleIndex) + { + cellStyle = writer.GetCellStyle(cellStyleIndex); + if (cellStyle != null && !cellStyle.IsEmpty()) + return cellStyle; // если у ячейки есть свой стиль, он имеет наивысший приоритет + } + + // 2. Стиль строки + var sheetData = sheet.GetSheetData(); + var rowElement = FindRowElement(sheetData, row); + if (rowElement?.StyleIndex?.Value is uint rowStyleIndex) + { + var rowStyle = writer.GetCellStyle(rowStyleIndex); + if (rowStyle != null && !rowStyle.IsEmpty()) + { + // Если у ячейки нет стиля, возвращаем стиль строки + if (cellStyle == null) + return rowStyle; + // Иначе объединяем: стиль ячейки имеет приоритет, но некоторые свойства могут быть не заданы + // (например, если у ячейки только Border, а у строки Fill, то в результате будет и Border, и Fill) + // Объединяем: сначала берём стиль строки, затем накладываем стиль ячейки (перекрывая) + return rowStyle.Merge(cellStyle); + } + } + + // 3. Стиль столбца + if (col > 0) + { + var columnElement = ExcelColumn.GetColumnElementInternal(sheet, col); + if (columnElement?.Style?.Value is uint colStyleIndex) + { + var colStyle = writer.GetCellStyle(colStyleIndex); + if (colStyle != null && !colStyle.IsEmpty()) + { + // Если нет стиля ячейки и строки, возвращаем стиль столбца + if (cellStyle == null) + return colStyle; + // Иначе объединяем: стиль ячейки + стиль строки (если есть) + стиль столбца + // Сначала объединяем стиль строки и столбца, затем накладываем стиль ячейки + // Но проще: берём стиль ячейки (если есть) и объединяем со стилем столбца + return colStyle.Merge(cellStyle); + } + } + } + + // Если ничего нет, возвращаем null + return cellStyle; // может быть null + } + + public void ApplyStyle(CellStyle style) + { + var cell = GetOrCreateCellElement(); + int styleIndex = writer.GetOrCreateStyleId(style); + cell.StyleIndex = (uint)styleIndex; + } + + // Кэш фрагментов богатого текста (только для InlineString) - private List? _runsCache; - private bool _cacheValid; + List? _runsCache; + bool _cacheValid; // ---- Реализация ICell (новые методы) ---- @@ -96,35 +164,50 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, return TryRemoveRun(index); } - public ICell Break() - { - EnsureCacheValid(); - if (_runsCache != null && _runsCache.Count > 0) - { - var lastRun = _runsCache[_runsCache.Count - 1]; - lastRun.Text += "\n"; - } - else - { - Run("\n", null); - } - _cacheValid = true; - UpdateCellFromCache(); - return this; - } + public ICell Break() + { + EnsureCacheValid(); + if (_runsCache != null && _runsCache.Count > 0) + { + var lastRun = _runsCache[_runsCache.Count - 1]; + lastRun.Text += "\n"; + EnsureWrapTextEnabled(); + } + else + { + Run("\n", null); + } + _cacheValid = true; + UpdateCellFromCache(); + return this; + } - public ICell Run(string text, RunFormat? format = null) - { - if (string.IsNullOrEmpty(text)) return this; - EnsureCacheValid(); - _runsCache ??= []; - _runsCache.Add(new ExcelRun { Text = text, Format = format }); - _cacheValid = true; - UpdateCellFromCache(); - return this; - } + public ICell Run(string text, RunFormat? format = null) + { + if (string.IsNullOrEmpty(text)) return this; + EnsureCacheValid(); + _runsCache ??= []; + _runsCache.Add(new ExcelRun { Text = text, Format = format }); + _cacheValid = true; + UpdateCellFromCache(); - public ICell RunBreak(string text, RunFormat? format = null) + if (text.Contains('\n')) + EnsureWrapTextEnabled(); + + return this; + } + + private void EnsureWrapTextEnabled() + { + var currentStyle = GetCellStyle() ?? new CellStyle(); + if (currentStyle.Align is null || currentStyle.Align.Value.WrapText != true) + { + var align = (currentStyle.Align ?? new CellAlign()) with { WrapText = true }; + Set(align); // вызовет объединение через TryMerge + } + } + + public ICell RunBreak(string text, RunFormat? format = null) { Run(text, format); Break(); @@ -307,7 +390,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, // ---- Приватные методы для работы с кэшем ---- - private void EnsureCacheValid() + void EnsureCacheValid() { if (_cacheValid) return; 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.DataType?.Value == CellValues.SharedString && cell.CellValue != null) @@ -352,7 +435,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, return string.Empty; } - private void UpdateCellFromCache() + void UpdateCellFromCache() { lock (writer._syncLock) { @@ -375,61 +458,66 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, } } - private InlineString BuildInlineStringFromCache() - { - var inline = new InlineString(); - if (_runsCache == null) return inline; - foreach (var run in _runsCache) - { - var runElement = new Run(); - runElement.Append(new Text(run.Text)); - if (run.Format is { } fmt) - { - var rPr = new RunProperties(); - if (fmt.IsBold == true) rPr.Append(new Bold()); - if (fmt.IsItalic == true) rPr.Append(new Italic()); - if (fmt.Underline.HasValue) - { - rPr.Append(new Underline - { - Val = fmt.Underline.Value switch - { - UnderlineStyle.Single => UnderlineValues.Single, - UnderlineStyle.Double => UnderlineValues.Double, - UnderlineStyle.SingleAccounting => UnderlineValues.SingleAccounting, - UnderlineStyle.DoubleAccounting => UnderlineValues.DoubleAccounting, - _ => throw new NotImplementedException(), - } - }); - } - if (fmt.IsStrike == true) rPr.Append(new Strike()); - if (fmt.Color.HasValue) - { - var excelColor = fmt.Color.Value.ToExcel(); - rPr.Append(excelColor); - } - if (fmt.FontSize.HasValue) - rPr.Append(new FontSize { Val = fmt.FontSize.Value }); - if (!string.IsNullOrEmpty(fmt.FontFamily)) - rPr.Append(new RunFont { Val = fmt.FontFamily }); - if (fmt.Vertical.HasValue) - { - var vertAlign = new VerticalTextAlignment - { - Val = fmt.Vertical.Value == VerticalTextRunAlignment.Superscript - ? VerticalAlignmentRunValues.Superscript - : VerticalAlignmentRunValues.Subscript - }; - rPr.Append(vertAlign); - } - runElement.RunProperties = rPr; - } - inline.Append(runElement); - } - return inline; - } + InlineString BuildInlineStringFromCache() + { + var inline = new InlineString(); + if (_runsCache == null) return inline; + foreach (var run in _runsCache) + { + var runElement = new Run(); + var textElement = new Text(run.Text); + // Устанавливаем preserve, если текст содержит пробелы или переносы + if (run.Text.Any(c => char.IsWhiteSpace(c))) + textElement.Space = SpaceProcessingModeValues.Preserve; + runElement.Append(textElement); - private static RunFormat MergeRunFormat(RunFormat baseFmt, RunFormat overlay) + if (run.Format is { } fmt) + { + var rPr = new RunProperties(); + if (fmt.IsBold == true) + rPr.Append(new Bold()); + if (fmt.IsItalic == true) + rPr.Append(new Italic()); + if (fmt.Underline.HasValue) + { + rPr.Append(new Underline + { + Val = fmt.Underline.Value switch + { + UnderlineStyle.Single => UnderlineValues.Single, + UnderlineStyle.Double => UnderlineValues.Double, + UnderlineStyle.SingleAccounting => UnderlineValues.SingleAccounting, + UnderlineStyle.DoubleAccounting => UnderlineValues.DoubleAccounting, + _ => throw new NotImplementedException(), + } + }); + } + if (fmt.IsStrike == true) + rPr.Append(new Strike()); + if (fmt.TryGetExcelColor(out var c)) + rPr.Append(c); + if (fmt.FontSize.HasValue) + rPr.Append(new FontSize { Val = fmt.FontSize.Value }); + if (!string.IsNullOrEmpty(fmt.FontFamily)) + rPr.Append(new RunFont { Val = fmt.FontFamily }); + if (fmt.Vertical.HasValue) + { + var vertAlign = new VerticalTextAlignment + { + Val = fmt.Vertical.Value == VerticalTextRunAlignment.Superscript + ? VerticalAlignmentRunValues.Superscript + : VerticalAlignmentRunValues.Subscript + }; + rPr.Append(vertAlign); + } + runElement.RunProperties = rPr; + } + inline.Append(runElement); + } + return inline; + } + + static RunFormat MergeRunFormat(RunFormat baseFmt, RunFormat overlay) { return new RunFormat { @@ -709,7 +797,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, 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; writer.ThrowIfDisposed(); @@ -718,47 +806,52 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, var cell = GetOrCreateCellElement(); cell.CellFormula = new CellFormula(formula); cell.DataType = null; // формула сама определяет тип - if (format != null) - SetNumberFormatInternal(cell, format); - return true; + if (format != null) Set(cell, format); + 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"); return this; } public ICell Set(NumberFormatPattern format) { - if (format == null) return this; writer.ThrowIfDisposed(); lock (writer._syncLock) { - var cell = GetOrCreateCellElement(); - SetNumberFormatInternal(cell, format); + var currentStyle = GetCellStyle() ?? new CellStyle(); + if (currentStyle.TryMerge(format, out var newStyle)) + ApplyStyle(newStyle); + return this; } - return this; - } + } - /// - public ICell Set(CellAlign align) + public ICell Set(Cell cell, NumberFormatPattern format) + { + writer.ThrowIfDisposed(); + lock (writer._syncLock) + { + var currentStyle = GetCellStyle() ?? new CellStyle(); + if (currentStyle.TryMerge(format, out var newStyle)) + cell.StyleIndex = (uint)writer.GetOrCreateStyleId(newStyle); + return this; + } + } + + /// + public ICell Set(CellAlign align) { writer.ThrowIfDisposed(); lock (writer._syncLock) { - var cell = GetOrCreateCellElement(); - int styleIndex = writer.GetOrCreateCellFormatId( - numberFormat: null, - font: null, - fill: null, - border: null, - align: align - ); - cell.StyleIndex = (uint)styleIndex; - return this; + var currentStyle = GetCellStyle() ?? new CellStyle(); + if (currentStyle.TryMerge(align, out var newStyle)) + ApplyStyle(newStyle); + return this; } } @@ -768,15 +861,39 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, writer.ThrowIfDisposed(); lock (writer._syncLock) { - var cell = GetOrCreateCellElement(); - int styleIndex = writer.GetOrCreateCellFormatId( - numberFormat: null, - font: null, - fill: null, - border: border, - align: null - ); - cell.StyleIndex = (uint)styleIndex; + // 1. Применяем границу к текущей ячейке (объединяя с существующей) + var currentStyle = GetCellStyle() ?? new CellStyle(); + CellBorder mergedBorder; + bool changed; + if (currentStyle.Border is { } currBorder) + changed = currBorder.TryMerge(border, out mergedBorder); + else + { + mergedBorder = border; + changed = true; + } + + if (!changed) + return this; + + ApplyStyle(currentStyle with { Border = mergedBorder }); + + // 2. Каскадное обновление соседей + // Для каждой стороны, которая была установлена (не null), очищаем соответствующую сторону у соседа + if (border.TopBorder.HasValue) + ClearNeighborBorder(-1, 0, b => b with { BottomBorder = null }); + + if (border.BottomBorder.HasValue) + ClearNeighborBorder(1, 0, b => b with { TopBorder = null }); + + if (border.LeftBorder.HasValue) + ClearNeighborBorder(0, -1, b => b with { RightBorder = null }); + + if (border.RightBorder.HasValue) + ClearNeighborBorder(0, 1, b => b with { LeftBorder = null }); + + // Диагональные границы не влияют на соседей, их не очищаем + return this; } } @@ -787,16 +904,10 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, writer.ThrowIfDisposed(); lock (writer._syncLock) { - var cell = GetOrCreateCellElement(); - int styleIndex = writer.GetOrCreateCellFormatId( - numberFormat: null, - font: null, - fill: fill, - border: null, - align: null - ); - cell.StyleIndex = (uint)styleIndex; - return this; + var currentStyle = GetCellStyle() ?? new CellStyle(); + if (currentStyle.TryMerge(fill, out var newStyle)) + ApplyStyle(newStyle); + return this; } } @@ -806,19 +917,148 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, writer.ThrowIfDisposed(); lock (writer._syncLock) { - var cell = GetOrCreateCellElement(); - int styleIndex = writer.GetOrCreateCellFormatId( - numberFormat: null, - font: font, - fill: null, - border: null, - align: null - ); - cell.StyleIndex = (uint)styleIndex; + var currentStyle = GetCellStyle() ?? new CellStyle(); + if (currentStyle.TryMerge(font, out var newStyle)) + ApplyStyle(newStyle); + return this; + } + } + + /// + public ICell Set(CellStyle style) + { + if (style is null) return this; + writer.ThrowIfDisposed(); + lock (writer._syncLock) + { + var currentStyle = GetCellStyle() ?? new CellStyle(); + if (currentStyle.TryMerge(style, out style)) + ApplyStyle(style); return this; } } + + void ClearNeighborBorder(int rowOffset, int colOffset, Func clearFunc) + { + var neighbor = GetNeighbor(rowOffset, colOffset); + if (neighbor is not { } neighbr) + return; + + var neighborBorder = neighbr.GetCellBorder(); + var newBorder = clearFunc(neighborBorder); + // Если после очистки граница изменилась, применяем изолированно + if (!neighborBorder.Equals(newBorder)) + neighbr.SetBorderIsolate(newBorder); + } + internal void SetBorderIsolate(CellBorder border) + { + var currentStyle = GetCellStyle() ?? new CellStyle(); + if (currentStyle.TryMerge(border, out var newStyle)) + ApplyStyle(newStyle); + } + + + + /// + /// Возвращает соседнюю ячейку по указанному смещению. + /// + /// Исходная ячейка. + /// Смещение по строкам (положительное – вниз). + /// Смещение по столбцам (положительное – вправо). + /// Соседняя ячейка, или null, если она выходит за пределы листа. + public ExcelCell? GetNeighbor(int rowOffset, int colOffset) + { + // Проверяем, что смещение не равно нулю и координаты не выходят за допустимые пределы (хотя мы не знаем границ листа) + if (rowOffset == 0 && colOffset == 0) return null; + int newRow = (int)row + rowOffset; + int newCol = (int)col + colOffset; + if (newRow < 1 || newCol < 1) return null; + // Excel допускает до 1048576 строк и 16384 столбцов (но мы не будем жестко ограничивать) + // Просто создаём объект ячейки, даже если она не существует физически. + return new ExcelCell(writer, sheet, (uint)newRow, (uint)newCol); + } + + internal ICell SetBorderOverride(CellBorder border) + { + writer.ThrowIfDisposed(); + lock (writer._syncLock) + { + // Определяем, какие стороны заданы в border + bool hasTop = border.TopBorder.HasValue; + bool hasBottom = border.BottomBorder.HasValue; + bool hasLeft = border.LeftBorder.HasValue; + bool hasRight = border.RightBorder.HasValue; + + // Если какая-то сторона задана, то для соседней ячейки на этой стороне мы должны очистить противоположную сторону. + // Например, если мы устанавливаем верхнюю границу у текущей ячейки, то у ячейки сверху нужно очистить нижнюю границу. + // Аналогично для остальных сторон. + + if (hasTop) + { + var neighbor = GetNeighbor(-1, 0); + if (neighbor != null) + { + var neighborBorder = neighbor.GetCellBorder(); + if (neighborBorder.BottomBorder.HasValue) + { + var newNeighborBorder = neighborBorder with { BottomBorder = null }; + // Применяем только границы, не затрагивая другие аспекты стиля + ((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder); + } + } + } + + if (hasBottom) + { + var neighbor = GetNeighbor(1, 0); + if (neighbor != null) + { + var neighborBorder = neighbor.GetCellBorder(); + if (neighborBorder.TopBorder.HasValue) + { + var newNeighborBorder = neighborBorder with { TopBorder = null }; + ((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder); + } + } + } + + if (hasLeft) + { + var neighbor = GetNeighbor(0, -1); + if (neighbor != null) + { + var neighborBorder = neighbor.GetCellBorder(); + if (neighborBorder.RightBorder.HasValue) + { + var newNeighborBorder = neighborBorder with { RightBorder = null }; + ((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder); + } + } + } + + if (hasRight) + { + var neighbor = GetNeighbor(0, 1); + if (neighbor != null) + { + var neighborBorder = neighbor.GetCellBorder(); + if (neighborBorder.LeftBorder.HasValue) + { + var newNeighborBorder = neighborBorder with { LeftBorder = null }; + ((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder); + } + } + } + + // Теперь устанавливаем границу для текущей ячейки (изолированно, чтобы не было зацикливания) + ((ExcelCell)this).SetBorderIsolate(border); + return this; + } + } + + + public ICell Set(bool value) { writer.ThrowIfDisposed(); @@ -849,9 +1089,10 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, var cell = GetOrCreateCellElement(); cell.DataType = CellValues.Number; cell.CellValue = new CellValue(value.ToString(CultureInfo.InvariantCulture)); - if (format != null) - SetNumberFormatInternal(cell, format); - } + cell.InlineString = null; + cell.CellFormula = null; + if (format != null) Set(cell, format); + } return this; } public ICell Set(float value, NumberFormatPattern? format = null) => Set((double)value, format); @@ -897,9 +1138,9 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, public ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell) => CopyTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex), out copiedCell); - // Private helpers + // helpers - private Cell? GetCellElement() + Cell? GetCellElement() { var sheetData = sheet.GetSheetData(); var eRow = FindRowElement(sheetData, row); @@ -907,27 +1148,37 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, return FindCellInRow(eRow, col); } - private Cell GetOrCreateCellElement() - { - var sheetData = sheet.GetSheetData(); - var eRow = GetOrCreateRowElement(sheetData, row); - var cell = FindCellInRow(eRow, col); - if (cell != null) return cell; - cell = new Cell(); - string cellRef = CellAddressHelper.ColumnIndexToLetter(col) + row.ToString(); - cell.CellReference = cellRef; - InsertCellInRow(eRow, cell, col); - return cell; - } + private Cell GetOrCreateCellElement() + { + var sheetData = sheet.GetSheetData(); + var rowElement = GetOrCreateRowElement(sheetData, row); + var cell = FindCellInRow(rowElement, col); + if (cell != null) return cell; - private static Row? FindRowElement(SheetData sheetData, uint rowIndex) + cell = new Cell(); + string cellRef = CellAddressHelper.ColumnIndexToLetter(col) + row.ToString(); + cell.CellReference = cellRef; + InsertCellInRow(rowElement, cell, col); + + // Наследование стиля + var inheritedStyle = GetCellStyle(); // теперь этот метод учитывает строку и столбец + if (inheritedStyle != null && !inheritedStyle.IsEmpty()) + { + int styleIndex = writer.GetOrCreateStyleId(inheritedStyle); + cell.StyleIndex = (uint)styleIndex; + } + + return cell; + } + + static Row? FindRowElement(SheetData sheetData, uint rowIndex) { foreach (var eRow in sheetData.Elements()) if (eRow.RowIndex?.Value == rowIndex) return eRow; return null; } - private static Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex) + static Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex) { var existing = FindRowElement(sheetData, rowIndex); if (existing != null) return existing; @@ -936,7 +1187,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, return newRow; } - private static void InsertRowElement(SheetData sheetData, Row eRow, uint rowIndex) + static void InsertRowElement(SheetData sheetData, Row eRow, uint rowIndex) { bool inserted = false; foreach (var existing in sheetData.Elements().ToList()) @@ -951,7 +1202,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, 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()) { @@ -961,7 +1212,7 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, 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(); cell.CellReference = newRef; @@ -978,37 +1229,19 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, 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 row = GetOrCreateRowElement(sheetData, rowIndex); InsertCellInRow(row, cell, colIndex); } - private void SetNumberFormatInternal(Cell cell, NumberFormatPattern format) - { - if (format == null) return; - int styleIndex = writer.GetOrCreateCellFormatId( - numberFormat: format, - font: null, - fill: null, - border: null, - align: null - ); - cell.StyleIndex = (uint)styleIndex; - } - - private string GetSharedString(uint index) + string GetSharedString(uint index) { return writer.GetSharedString(index); } - private int GetOrAddSharedString(string value) - { - return writer.GetOrAddSharedString(value); - } - - private string ExtractTextFromInlineString(InlineString? inlineString) + string ExtractTextFromInlineString(InlineString? inlineString) { if (inlineString == null) return string.Empty; var sb = new System.Text.StringBuilder(); @@ -1020,10 +1253,10 @@ internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, return sb.ToString(); } - private MergeCells? GetMergeCells() => + MergeCells? GetMergeCells() => sheet.Worksheet.GetFirstChild(); - private bool TryParseRangeReference(string reference, out ExcelRange range) + bool TryParseRangeReference(string reference, out ExcelRange range) { range = null!; if (string.IsNullOrEmpty(reference)) return false; diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs index 03a7541..5aa4391 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs @@ -16,7 +16,161 @@ internal sealed class ExcelColumn : IColumn _colIndex = colIndex; } - public uint Index => _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(); + if (cols == null) return null; + foreach (Column col in cols.Elements()) + { + if (col.Min?.Value is { } min && col.Max?.Value is { } max && min <= colIndex && max >= colIndex) + return col; + } + return null; + } + + + /// + 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; + } + } + + /// + 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; + } + } + + /// + 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; + } + } + + /// + 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; + } + } + + /// + 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; + } + } + + /// + 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()) + { + var cellElement = FindCellInRow(rowElement, _colIndex); + if (cellElement != null) + { + var cell = new ExcelCell(_writer, _sheet, rowElement.RowIndex!.Value, _colIndex); + cell.Set(style); + } + } + } + + + + + public uint Index => _colIndex; public string IndexLetter => NumberToColumnLetter(_colIndex); @@ -64,23 +218,7 @@ internal sealed class ExcelColumn : IColumn } } - // Вспомогательные внутренние методы (перенести существующую логику) - private static Column? GetColumnElementInternal(ExcelSheet sheet, uint colIndex) - { - var cols = sheet.Worksheet.GetFirstChild(); - if (cols == null) return null; - foreach (Column col in cols.Elements()) - { - 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) + static Column GetOrCreateColumnElementInternal(ExcelSheet sheet, uint colIndex) { var existing = GetColumnElementInternal(sheet, colIndex); if (existing != null) return existing; @@ -127,62 +265,46 @@ internal sealed class ExcelColumn : IColumn } public ColumnWidth Width -{ - get { - _writer.ThrowIfDisposed(); - lock (_writer._syncLock) + get { - var column = GetColumnElement(); - if (column is Column col && col.Width?.Value is { } val && col.CustomWidth?.Value == true) - return ColumnWidth.FromCharacters(val); - return ColumnWidth.FromCharacters(8.43); // стандартная ширина - } - } - set - { - _writer.ThrowIfDisposed(); - lock (_writer._syncLock) - { - var col = GetOrCreateColumnElement(); - double widthInChars; - - if (value.IsCharacterUnit) + _writer.ThrowIfDisposed(); + lock (_writer._syncLock) { - // Если ширина задана в символах, используем напрямую - widthInChars = value.CharacterValue; - } - else - { - double targetCm = value.GetTargetCentimeters(); - widthInChars = _writer.TryGetCalibrateCoeff(targetCm, out var closestCw) - ? closestCw : targetCm * (ColumnWidth.DefaultPointsPerChar / 28.3464566929); - } - - col.Width = widthInChars; - col.CustomWidth = true; - } - } -} - - public 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()) - { - var cell = FindCellInRow(row, _colIndex); - cell?.StyleIndex = (uint)fmtId; + var column = GetColumnElement(); + if (column is Column col && col.Width?.Value is { } val && col.CustomWidth?.Value == true) + return ColumnWidth.FromCharacters(val); + return ColumnWidth.FromCharacters(8.43); // стандартная ширина + } + } + set + { + _writer.ThrowIfDisposed(); + lock (_writer._syncLock) + { + var col = GetOrCreateColumnElement(); + double widthInChars; + + if (value.IsCharacterUnit) + { + // Если ширина задана в символах, используем напрямую + widthInChars = value.CharacterValue; + } + else + { + double targetCm = value.GetTargetCentimeters(); + widthInChars = _writer.TryGetCalibrateCoeff(targetCm, out var closestCw) + ? closestCw : targetCm * (ColumnWidth.DefaultPointsPerChar / 28.3464566929); + } + + col.Width = widthInChars; + col.CustomWidth = true; } } - return this; } + + public ICell Cell(uint row) { return new ExcelCell(_writer, _sheet, row, _colIndex); @@ -207,7 +329,7 @@ internal sealed class ExcelColumn : IColumn 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) @@ -305,7 +427,7 @@ internal sealed class ExcelColumn : IColumn // Вспомогательные методы - private Column? GetColumnElement() + Column? GetColumnElement() { var cols = _sheet.Worksheet.GetFirstChild(); if (cols == null) return null; @@ -320,7 +442,7 @@ internal sealed class ExcelColumn : IColumn return null; } - private Column GetOrCreateColumnElement() + Column GetOrCreateColumnElement() { var existing = GetColumnElement(); if (existing != null) return existing; @@ -344,13 +466,13 @@ internal sealed class ExcelColumn : IColumn return newCol; } - private void DeleteColumnElement() + void DeleteColumnElement() { var col = GetColumnElement(); col?.Remove(); } - private void CopyColumnData(uint sourceCol, uint targetCol) + void CopyColumnData(uint sourceCol, uint targetCol) { if (sourceCol == targetCol) return; 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(); foreach (var row in sheetData.Elements().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()) { @@ -410,7 +532,7 @@ internal sealed class ExcelColumn : IColumn 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(); cell.CellReference = newRef; @@ -430,7 +552,7 @@ internal sealed class ExcelColumn : IColumn row.Append(cell); } - private Column? GetColumnElementForIndex(uint col) + Column? GetColumnElementForIndex(uint col) { var cols = _sheet.Worksheet.GetFirstChild(); if (cols == null) return null; @@ -445,7 +567,7 @@ internal sealed class ExcelColumn : IColumn return null; } - private Column GetOrCreateColumnElementForIndex(uint col) + Column GetOrCreateColumnElementForIndex(uint col) { var existing = GetColumnElementForIndex(col); if (existing != null) return existing; @@ -468,12 +590,12 @@ internal sealed class ExcelColumn : IColumn return newCol; } - private int GetOrCreateNumberFormatId(NumberFormatPattern format) + int GetOrCreateNumberFormatId(NumberFormatPattern 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; if (string.IsNullOrEmpty(reference)) return false; @@ -487,7 +609,7 @@ internal sealed class ExcelColumn : IColumn return true; } - private static string NumberToColumnLetter(uint col) + static string NumberToColumnLetter(uint col) { if (col == 0) throw new ArgumentException("Column number must be > 0"); string result = ""; diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs index d1c0d21..c6a8c68 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs @@ -5,10 +5,10 @@ /// internal sealed class ExcelRange : IRange { - private readonly ExcelWriter _writer; - private readonly ExcelSheet _sheet; - private uint _rowStart, _rowEnd; - private uint _colStart, _colEnd; + internal readonly ExcelWriter _writer; + internal readonly ExcelSheet _sheet; + internal uint _rowStart, _rowEnd; + internal uint _colStart, _colEnd; internal ExcelRange(ExcelWriter writer, ExcelSheet sheet, uint rowStart, uint colStart, uint rowEnd, uint colEnd) { @@ -55,17 +55,90 @@ internal sealed class ExcelRange : IRange + void ForEachCell(Action 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; + } + } - /// - /// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить"). - /// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место. - /// - /// Номер строки для нового верхнего левого угла. - /// Номер столбца для нового верхнего левого угла. - /// Тот же объект IRange с новыми координатами. - public IRange MoveTo(uint newRow, uint newCol) + + + /// + /// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить"). + /// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место. + /// + /// Номер строки для нового верхнего левого угла. + /// Номер столбца для нового верхнего левого угла. + /// Тот же объект IRange с новыми координатами. + public IRange MoveTo(uint newRow, uint newCol) { if (newRow == _rowStart && newCol == _colStart) return this; _writer.ThrowIfDisposed(); @@ -115,7 +188,7 @@ internal sealed class ExcelRange : IRange return MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex)); } - private enum CopyOrder + enum CopyOrder { Any, LeftToRight, @@ -125,7 +198,7 @@ internal sealed class ExcelRange : IRange } /// Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода. - 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) { // Определяем все строки, которые могут понадобиться (исходные и целевые) @@ -172,7 +245,7 @@ internal sealed class ExcelRange : IRange } /// Строит словарь строк для указанных индексов строк. - private Dictionary GetRowDictionary(HashSet rowIndices) + Dictionary GetRowDictionary(HashSet rowIndices) { var dict = new Dictionary(); var sheetData = _sheet.GetSheetData(); @@ -185,7 +258,7 @@ internal sealed class ExcelRange : IRange } /// Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке). - private Cell? FindCellInRowFast(Row row, uint colIndex) + Cell? FindCellInRowFast(Row row, uint colIndex) { foreach (var cell in row.Elements()) { @@ -196,14 +269,14 @@ internal sealed class ExcelRange : IRange } /// Проверяет, пересекаются ли два прямоугольных диапазона. - 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) { return !(r1e < r2s || r2e < r1s || c1e < c2s || c2e < c1s); } /// Вставляет ячейку в указанную позицию, предварительно удаляя существующую. - private void InsertCellAt(uint row, uint col, Cell cell) + void InsertCellAt(uint row, uint col, Cell cell) { DeleteCellAt(row, col); var sheetData = _sheet.GetSheetData(); @@ -212,7 +285,7 @@ internal sealed class ExcelRange : IRange } /// Удаляет ячейку, если она существует. - private void DeleteCellAt(uint row, uint col) + void DeleteCellAt(uint row, uint col) { var sheetData = _sheet.GetSheetData(); var rowElement = FindRowElement(sheetData, row); @@ -222,7 +295,7 @@ internal sealed class ExcelRange : IRange } /// Получает или создаёт строку с указанным индексом. - private Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex) + Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex) { var existing = FindRowElement(sheetData, rowIndex); if (existing != null) return existing; @@ -232,7 +305,7 @@ internal sealed class ExcelRange : IRange } /// Вставляет ячейку в строку с сохранением порядка столбцов. - 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}"; cell.CellReference = newRef; @@ -251,7 +324,7 @@ internal sealed class ExcelRange : IRange } /// Очищает данные (содержимое) в указанном диапазоне. - 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++) { @@ -334,103 +407,12 @@ internal sealed class ExcelRange : IRange 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; - } - - /// - 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; - } - } - - /// - 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; - } - } - - /// - 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; - } - } - - /// - 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; - } - } - /// /// Применяет стиль ко всем ячейкам диапазона. /// /// Индекс стиля для применения. /// Если true, создаёт недостающие ячейки (по умолчанию true). - private void ApplyStyleToRange(uint styleIndex, bool createIfMissing = true) + void ApplyStyleToRange(uint styleIndex, bool createIfMissing = true) { for (uint row = _rowStart; row <= _rowEnd; row++) { @@ -517,7 +499,7 @@ internal sealed class ExcelRange : IRange { if (GetSubCell(row, col, out var cell)) { - cell.Set(formula, format); + cell.SetFormula(formula, format); return true; } return false; @@ -619,7 +601,7 @@ internal sealed class ExcelRange : IRange { if (GetSubCell(row, col, out var cell)) { - cell.Set(formula, format); + cell.SetFormula(formula, format); return true; } 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 rowElement = FindRowElement(sheetData, row); @@ -735,7 +717,7 @@ internal sealed class ExcelRange : IRange 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)>(); @@ -784,7 +766,7 @@ internal sealed class ExcelRange : IRange } } - private Row GetOrCreateRowElement(uint rowIndex) + Row GetOrCreateRowElement(uint rowIndex) { var existing = FindRowElement(_sheet.GetSheetData(), rowIndex); if (existing != null) return existing; @@ -793,14 +775,14 @@ internal sealed class ExcelRange : IRange return newRow; } - private static Row? FindRowElement(SheetData sheetData, uint rowIndex) + static Row? FindRowElement(SheetData sheetData, uint rowIndex) { foreach (var row in sheetData.Elements()) if (row.RowIndex?.Value == rowIndex) return row; return null; } - private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex) + static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex) { bool inserted = false; foreach (var existing in sheetData.Elements().ToList()) @@ -815,7 +797,7 @@ internal sealed class ExcelRange : IRange if (!inserted) sheetData.Append(row); } - private Cell? FindCellInRow(Row row, uint colIndex) + Cell? FindCellInRow(Row row, uint colIndex) { foreach (var cell in row.Elements()) { @@ -825,7 +807,7 @@ internal sealed class ExcelRange : IRange return null; } - private Column? GetColumnElementForIndex(uint col) + Column? GetColumnElementForIndex(uint col) { var worksheet = _sheet.Worksheet; var cols = worksheet.GetFirstChild(); @@ -839,7 +821,7 @@ internal sealed class ExcelRange : IRange return null; } - private Column GetOrCreateColumnElementForIndex(uint col) + Column GetOrCreateColumnElementForIndex(uint col) { var existing = GetColumnElementForIndex(col); if (existing != null) return existing; @@ -861,10 +843,10 @@ internal sealed class ExcelRange : IRange return newCol; } - private MergeCells? GetMergeCells() => + MergeCells? GetMergeCells() => _sheet.Worksheet.GetFirstChild(); - private bool TryParseRangeReference(string reference, out ExcelRange range) + bool TryParseRangeReference(string reference, out ExcelRange range) { range = null!; if (string.IsNullOrEmpty(reference)) return false; @@ -877,10 +859,4 @@ internal sealed class ExcelRange : IRange range = new ExcelRange(_writer, _sheet, rowStart, colStart, rowEnd, colEnd); return true; } - - // создаёт стиль только с числовым форматом - private int GetOrCreateNumberFormatId(NumberFormatPattern format) - { - return _writer.GetOrCreateCellFormatId(numberFormat: format); - } } \ No newline at end of file diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs index edc54ec..06db33e 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs @@ -5,9 +5,9 @@ /// internal sealed class ExcelRow : IRow { - private readonly ExcelWriter _writer; - private readonly ExcelSheet _sheet; - private uint _rowIndex; + readonly ExcelWriter _writer; + readonly 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(); foreach (var row in sheetData.Elements()) @@ -55,7 +55,7 @@ internal sealed class ExcelRow : IRow return null; } - private static Row GetOrCreateRowElementInternal(ExcelSheet sheet, uint rowIndex) + static Row GetOrCreateRowElementInternal(ExcelSheet sheet, uint rowIndex) { var existing = GetRowElementInternal(sheet, rowIndex); if (existing != null) return existing; @@ -66,7 +66,7 @@ internal sealed class ExcelRow : IRow return newRow; } - private static void InsertRowElementInternal(SheetData sheetData, Row row, uint rowIndex) + static void InsertRowElementInternal(SheetData sheetData, Row row, uint rowIndex) { bool inserted = false; foreach (var existing in sheetData.Elements().ToList()) @@ -136,25 +136,157 @@ 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()) + { + 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); + } + /// - public IRow SetNumberFormat(NumberFormatPattern format) + public IRow Set(NumberFormatPattern format) { if (format == null) return this; _writer.ThrowIfDisposed(); lock (_writer._syncLock) { - // Находим все ячейки в этой строке и устанавливаем формат - var sheetData = _sheet.GetSheetData(); - var rowElement = FindRowElement(sheetData, _rowIndex); - if (rowElement == null) return this; - int formatIndex = GetOrCreateNumberFormatId(format); - foreach (var cell in rowElement.Elements()) - cell.StyleIndex = (uint)formatIndex; + var rowElement = GetOrCreateRowElement(); + var currentStyle = GetRowStyle(rowElement) ?? new CellStyle(); + if (currentStyle.TryMerge(format, out var newStyle)) + { + ApplyStyleToRow(newStyle); + ApplyStyleToRowCells(newStyle); + } + return this; } - return this; } + /// + 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; + } + } + + /// + 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; + } + } + + /// + 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; + } + } + + /// + 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; + } + } + + /// + 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; + } + + + /// 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) { - Cell(col).Set(value, format); return this; + Cell(col).SetFormula(value, format); return this; } 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) { - Cell(col).Set(value, format); return this; + Cell(col).SetFormula(value, format); return this; } 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 existing = FindRowElement(sheetData, _rowIndex); @@ -357,7 +489,7 @@ internal sealed class ExcelRow : IRow return newRow; } - private static Row? FindRowElement(SheetData sheetData, uint rowIndex) + static Row? FindRowElement(SheetData sheetData, uint rowIndex) { // Поиск по атрибуту RowIndex. В Open XML строки могут идти не по порядку. foreach (var row in sheetData.Elements()) @@ -368,7 +500,7 @@ internal sealed class ExcelRow : IRow return null; } - private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex) + static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex) { // Вставка с сохранением сортировки по RowIndex bool inserted = false; @@ -385,7 +517,7 @@ internal sealed class ExcelRow : IRow sheetData.Append(row); } - private int GetOrCreateNumberFormatId(NumberFormatPattern format) + int GetOrCreateNumberFormatId(NumberFormatPattern format) { // Создаём стиль, содержащий только числовой формат, и возвращаем его индекс return _writer.GetOrCreateCellFormatId(numberFormat: format); diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs index 97a60a1..c5ba288 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs @@ -6,8 +6,8 @@ /// internal sealed class ExcelRun : IRun { - private string _text = string.Empty; - private RunFormat? _format; + string _text = string.Empty; + RunFormat? _format; /// public string Text diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs index 8e4cd11..e2bb4d1 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs @@ -77,7 +77,7 @@ internal sealed class ExcelSheet : ISheet 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) @@ -129,7 +129,7 @@ internal sealed class ExcelSheet : ISheet 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) diff --git a/QWERTYkez.ExcelProcessor/Editors/Interfaces.cs b/QWERTYkez.ExcelProcessor/Editors/Interfaces.cs index 4c403cd..d3d9ca4 100644 --- a/QWERTYkez.ExcelProcessor/Editors/Interfaces.cs +++ b/QWERTYkez.ExcelProcessor/Editors/Interfaces.cs @@ -219,7 +219,22 @@ public interface IRow RowHeight Height { get; set; } /// Устанавливает числовой формат для всех ячеек строки. - IRow SetNumberFormat(NumberFormatPattern format); + IRow Set(NumberFormatPattern format); + + /// Устанавливает выравнивание для всех ячеек диапазона. + IRow Set(CellAlign format); + + /// Устанавливает границы для всех ячеек диапазона. + IRow Set(CellBorder format); + + /// Устанавливает заливку для всех ячеек диапазона. + IRow Set(CellFill format); + + /// Устанавливает шрифт для всех ячеек диапазона. + IRow Set(CellFont format); + + /// Устанавливает шрифт ячейки. + IRow Set(CellStyle font); /// Возвращает ячейку в заданном столбце (индекс с 1). ICell Cell(uint col); @@ -346,7 +361,22 @@ public interface IColumn ColumnWidth Width { get; set; } /// Устанавливает числовой формат для всех ячеек столбца. - IColumn SetNumberFormat(NumberFormatPattern format); + IColumn Set(NumberFormatPattern format); + + /// Устанавливает выравнивание для всех ячеек диапазона. + IColumn Set(CellAlign format); + + /// Устанавливает границы для всех ячеек диапазона. + IColumn Set(CellBorder format); + + /// Устанавливает заливку для всех ячеек диапазона. + IColumn Set(CellFill format); + + /// Устанавливает шрифт для всех ячеек диапазона. + IColumn Set(CellFont format); + + /// Устанавливает шрифт ячейки. + IColumn Set(CellStyle font); /// Возвращает ячейку в заданной строке (индекс с 1). ICell Cell(uint row); @@ -476,19 +506,22 @@ public interface IRange IRange MoveTo(uint rowIndex, string colIndex); /// Устанавливает числовой формат для всех ячеек диапазона. - IRange SetNumberFormat(NumberFormatPattern format); + IRange Set(NumberFormatPattern format); /// Устанавливает выравнивание для всех ячеек диапазона. - IRange SetCellAlign(CellAlign format); + IRange Set(CellAlign format); /// Устанавливает границы для всех ячеек диапазона. - IRange SetCellBorder(CellBorder format); + IRange Set(CellBorder format); /// Устанавливает заливку для всех ячеек диапазона. - IRange SetCellFill(CellFill format); + IRange Set(CellFill format); /// Устанавливает шрифт для всех ячеек диапазона. - IRange SetCellFont(CellFont format); + IRange Set(CellFont format); + + /// Устанавливает шрифт ячейки. + IRange Set(CellStyle font); /// Перечисляет все ячейки диапазона (по строкам). IEnumerable Cells { get; } @@ -733,6 +766,9 @@ public interface ICell /// Возвращает шрифт ячейки. CellFont GetCellFont(); + /// Возвращает шрифт ячейки. + CellStyle? GetCellStyle(); + /// Пытается извлечь логическое значение. bool TryGetBoolean(out bool value); @@ -754,25 +790,28 @@ public interface ICell /// Пытается установить формулу (без вычисленного значения). /// Текст формулы (например, "SUM(A1:A5)"). /// Необязательный числовой формат для результата. - bool TrySet(string formula, NumberFormatPattern? format = null); + bool TrySetFormula(string formula, NumberFormatPattern? format = null); /// Устанавливает формулу (выбрасывает исключение при ошибке). - ICell Set(string formula, NumberFormatPattern? format = null); + ICell SetFormula(string formula, NumberFormatPattern? format = null); /// Устанавливает числовой формат ячейки (не меняя значение). ICell Set(NumberFormatPattern format); /// Устанавливает выравнивание текста ячейки. - ICell Set(CellAlign format); + ICell Set(CellAlign align); /// Устанавливает границы ячейки. - ICell Set(CellBorder format); + ICell Set(CellBorder border); /// Устанавливает заливку ячейки. - ICell Set(CellFill format); + ICell Set(CellFill fill); /// Устанавливает шрифт ячейки. - ICell Set(CellFont format); + ICell Set(CellFont font); + + /// Устанавливает стиль ячейки. + ICell Set(CellStyle style); /// Устанавливает простое текстовое значение (без форматирования). ICell Set(string value); diff --git a/QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs b/QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs index a5dacb3..429507f 100644 --- a/QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs +++ b/QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs @@ -3,7 +3,7 @@ public class NumberFormatPattern { public string Format { get; } - internal int? Id { get; private set; } + internal int? Id { get; set; } public NumberFormatPattern(string format, ushort id = 0) { diff --git a/QWERTYkez.ExcelProcessor/Editors/RangeBorderExtensions.cs b/QWERTYkez.ExcelProcessor/Editors/RangeBorderExtensions.cs new file mode 100644 index 0000000..292d31b --- /dev/null +++ b/QWERTYkez.ExcelProcessor/Editors/RangeBorderExtensions.cs @@ -0,0 +1,149 @@ +namespace QWERTYkez.ExcelProcessor; + +/// +/// Методы расширения для установки границ диапазона. +/// +public static class RangeBorderExtensions +{ + /// + /// Устанавливает границы для диапазона с указанным стилем и цветом. + /// + /// Диапазон ячеек. + /// Стиль линии границы. + /// Цвет границы (необязательно). + /// Какие границы применять (по умолчанию Outside). + /// Тот же диапазон для цепочки вызовов. + 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); + + /// + /// Устанавливает границы для диапазона с указанными параметрами. + /// + /// Диапазон ячеек. + /// Стиль и цвет границы. + /// Какие границы применять (по умолчанию Outside). + /// Тот же диапазон для цепочки вызовов. + 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; + } + + /// + /// Очищает все границы в диапазоне (сбрасывает до None). + /// + 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 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); + } +} \ No newline at end of file diff --git a/QWERTYkez.ExcelProcessor/Editors/RowHeight.cs b/QWERTYkez.ExcelProcessor/Editors/RowHeight.cs index 64d77bc..fc22e47 100644 --- a/QWERTYkez.ExcelProcessor/Editors/RowHeight.cs +++ b/QWERTYkez.ExcelProcessor/Editors/RowHeight.cs @@ -5,13 +5,13 @@ /// public readonly struct RowHeight(double points) { - private const double POINTS_PER_INCH = 72.0; - private const double INCH_PER_CM = 1.0 / 2.54; - private 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_INCH = 72.0; + const double INCH_PER_CM = 1.0 / 2.54; + const double POINTS_PER_CM = POINTS_PER_INCH * INCH_PER_CM; // ≈ 28.3464566929 + const double POINTS_PER_MM = POINTS_PER_CM / 10.0; // ≈ 2.83464566929 - private const double DXA_PER_POINT = 20.0; // 1 point = 20 dxa - private const double POINTS_PER_DXA = 1.0 / DXA_PER_POINT; + const double DXA_PER_POINT = 20.0; // 1 point = 20 dxa + const double POINTS_PER_DXA = 1.0 / DXA_PER_POINT; /// Высота в пунктах (points). public double Points { get; } = points; diff --git a/QWERTYkez.ExcelProcessor/Editors/RunFormat.cs b/QWERTYkez.ExcelProcessor/Editors/RunFormat.cs index 2f09927..b13eded 100644 --- a/QWERTYkez.ExcelProcessor/Editors/RunFormat.cs +++ b/QWERTYkez.ExcelProcessor/Editors/RunFormat.cs @@ -19,7 +19,7 @@ public readonly struct RunFormat public bool? IsStrike { get; init; } /// Цвет текста фрагмента. - public ExColor? Color { get; init; } + public System.Drawing.Color? Color { get; init; } /// Размер шрифта фрагмента в пунктах. public double? FontSize { get; init; } @@ -80,9 +80,7 @@ public readonly struct RunFormat // DoubleStrike в Excel не поддерживается, опускаем Underline = underline, - Color = rPr.GetFirstChild()?.Rgb is not null - ? ExColor.FromRgb(rPr.GetFirstChild()!.Rgb!.Value!) - : null!, + Color = FromExcelColor(rPr.GetFirstChild()?.Rgb), FontSize = rPr.GetFirstChild()?.Val?.Value, FontFamily = rPr.GetFirstChild()?.Val, Vertical = vertical @@ -90,6 +88,45 @@ public readonly struct RunFormat 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 или других более удобных типов diff --git a/QWERTYkez.ExcelProcessor/ExcelWriter.cs b/QWERTYkez.ExcelProcessor/ExcelWriter.cs index 240688a..67af58e 100644 --- a/QWERTYkez.ExcelProcessor/ExcelWriter.cs +++ b/QWERTYkez.ExcelProcessor/ExcelWriter.cs @@ -8,9 +8,56 @@ namespace QWERTYkez.ExcelProcessor; /// internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter { + + readonly Dictionary _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; - private SharedStringTable? _sharedStringTable; + SharedStringTablePart? _sharedStringPart; + SharedStringTable? _sharedStringTable; internal static Dictionary? _calibrationTable; // cw -> width_pts @@ -62,7 +109,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter return true; } - private Dictionary CalibrateWidthCoeffUsingInterop() + Dictionary CalibrateWidthCoeffUsingInterop() { object? excelApp = null; object? workbooks = null; @@ -133,7 +180,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter } - private void EnsureSharedStringTable() + void EnsureSharedStringTable() { if (_sharedStringPart != null) return; _sharedStringPart = _doc.WorkbookPart?.GetPartsOfType().FirstOrDefault(); @@ -193,17 +240,17 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter } // Кэши числовых форматов - private readonly Dictionary _numberFormatCache = []; - private readonly Dictionary _numberFormatIdToPattern = []; + readonly Dictionary _numberFormatCache = []; + readonly Dictionary _numberFormatIdToPattern = []; // Кэши для компонентов стилей (чтобы не создавать дубликаты) - private readonly Dictionary _fontCache = []; - private readonly Dictionary _fillCache = []; - private readonly Dictionary _borderCache = []; - private readonly Dictionary _alignmentCache = []; + readonly Dictionary _fontCache = []; + readonly Dictionary _fillCache = []; + readonly Dictionary _borderCache = []; + readonly Dictionary _alignmentCache = []; // Кэш составных стилей (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; if (numberFormat != null) { - if (numberFormat.Id.HasValue && numberFormat.Id.Value < 164) - numFmtId = numberFormat.Id.Value; - else - numFmtId = (int)GetOrCreateNumberFormatId(numberFormat); + numFmtId = numberFormat.Id.HasValue && numberFormat.Id.Value < 164 + ? numberFormat.Id.Value : (int)GetOrCreateNumberFormatId(numberFormat); } // Получаем или создаём Font, Fill, Border, Alignment @@ -352,7 +397,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter #region Вспомогательные методы для работы со стилями - private Stylesheet EnsureStylesheet() + Stylesheet EnsureStylesheet() { var workbookPart = _doc.WorkbookPart ?? throw new InvalidOperationException("No WorkbookPart"); var stylesPart = workbookPart.WorkbookStylesPart; @@ -390,7 +435,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter return stylesheet; } - private uint GetOrCreateNumberFormatId(NumberFormatPattern pattern) + uint GetOrCreateNumberFormatId(NumberFormatPattern pattern) { if (pattern.Id.HasValue) return (uint)pattern.Id.Value; @@ -400,7 +445,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter return (uint)created.Id!.Value; } - private int GetOrCreateFontId(CellFont font) + int GetOrCreateFontId(CellFont font) { if (_fontCache.TryGetValue(font, out int id)) return id; @@ -414,7 +459,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter return _fontCache[font] = (int)newId; } - private int GetOrCreateFillId(CellFill fill) + int GetOrCreateFillId(CellFill fill) { if (_fillCache.TryGetValue(fill, out int id)) return id; @@ -428,7 +473,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter return _fillCache[fill] = (int)newId; } - private int GetOrCreateBorderId(CellBorder border) + int GetOrCreateBorderId(CellBorder border) { if (_borderCache.TryGetValue(border, out int id)) return id; @@ -443,7 +488,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter return newId; } - private int GetOrCreateAlignmentId(CellAlign align) + int GetOrCreateAlignmentId(CellAlign align) { if (_alignmentCache.TryGetValue(align, out int id)) return id; @@ -456,7 +501,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter return id; } - private Alignment GetAlignmentFromCache(int alignId) + Alignment GetAlignmentFromCache(int alignId) { foreach (var pair in _alignmentCache) if (pair.Value == alignId) @@ -464,7 +509,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter return new Alignment(); } - private CellFormat? GetCellFormatAt(uint index) + CellFormat? GetCellFormatAt(uint index) { var stylesheet = EnsureStylesheet(); 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(); stylesheet.NumberingFormats = nfs; return nfs; } - private static CellFormats CreateCellFormats(Stylesheet stylesheet) + static CellFormats CreateCellFormats(Stylesheet stylesheet) { var cfs = new CellFormats(); stylesheet.CellFormats = cfs; @@ -491,7 +536,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter - private bool _isModified = false; + bool _isModified = false; internal ExcelWriter() { } @@ -1006,7 +1051,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter #endregion - private void EnsureFullCalculationOnLoad() + void EnsureFullCalculationOnLoad() { if (_doc?.WorkbookPart?.Workbook == null) return; var workbook = _doc.WorkbookPart.Workbook; @@ -1217,7 +1262,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter // Внутренний метод, предполагает, что _syncLock уже захвачен вызывающим - private NumberFormatPattern CreateNumberFormatInternal(string formatCode) + NumberFormatPattern CreateNumberFormatInternal(string formatCode) { // Проверяем кэш по коду 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; + if (borders == null) return null; + int index = 0; + foreach (var border in borders.Elements()) { - var borders = EnsureStylesheet().Borders; - _cachedBorders = borders?.Elements().ToList() ?? []; + if (index == borderId) return border; + index++; } - return borderId < _cachedBorders.Count ? _cachedBorders[(int)borderId] : null; + return null; } - private List? _cachedBorders; - private Fill? GetFillById(uint borderId) + Fill? GetFillById(uint fillId) { - if (_cachedFills == null) + var fills = EnsureStylesheet().Fills; + if (fills == null) return null; + int index = 0; + foreach (var fill in fills.Elements()) { - var fills = EnsureStylesheet().Fills; - _cachedFills = fills?.Elements().ToList() ?? []; + if (index == fillId) return fill; + index++; } - return borderId < _cachedFills.Count ? _cachedFills[(int)borderId] : null; + return null; } - private List? _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()) { - var borders = EnsureStylesheet().Borders; - _cachedFonts = borders?.Elements().ToList() ?? []; + if (index == fontId) return font; + index++; } - return borderId < _cachedFonts.Count ? _cachedFonts[(int)borderId] : null; + return null; } - private List? _cachedFonts; } \ No newline at end of file diff --git a/QWERTYkez.ExcelProcessor/NormalizedSet.cs b/QWERTYkez.ExcelProcessor/NormalizedSet.cs index badaeb7..d10b456 100644 --- a/QWERTYkez.ExcelProcessor/NormalizedSet.cs +++ b/QWERTYkez.ExcelProcessor/NormalizedSet.cs @@ -10,7 +10,7 @@ namespace QWERTYkez.ExcelProcessor; /// internal class NormalizedSet : ISet { - private readonly HashSet _inner; + readonly HashSet _inner; /// /// Создаёт пустое нормализованное множество. @@ -32,7 +32,7 @@ internal class NormalizedSet : ISet /// /// Нормализует строку: верхний регистр и удаление диакритики. /// - private static string Normalize(string s) + static string Normalize(string s) { if (string.IsNullOrEmpty(s)) return s; diff --git a/QWERTYkez.ExcelProcessor/PlaceholderFinder.cs b/QWERTYkez.ExcelProcessor/PlaceholderFinder.cs index f03375a..fac8f19 100644 --- a/QWERTYkez.ExcelProcessor/PlaceholderFinder.cs +++ b/QWERTYkez.ExcelProcessor/PlaceholderFinder.cs @@ -22,7 +22,7 @@ internal static class PlaceholderFinder return FindInDocument(doc, [.. doc.WorkbookPart.WorksheetParts]); } - private static void ProcessWorksheet(WorksheetPart wsPart, SharedStringTable? sharedStrings, ISet result) + static void ProcessWorksheet(WorksheetPart wsPart, SharedStringTable? sharedStrings, ISet result) { var worksheet = wsPart.Worksheet; if (worksheet is null) return; @@ -31,7 +31,7 @@ internal static class PlaceholderFinder ProcessHeaderFooter(worksheet, result); } - private static void ProcessCells(Worksheet worksheet, SharedStringTable? sharedStrings, ISet result) + static void ProcessCells(Worksheet worksheet, SharedStringTable? sharedStrings, ISet result) { var sheetData = worksheet.GetFirstChild(); if (sheetData is null) return; @@ -59,7 +59,7 @@ internal static class PlaceholderFinder } } - private static void ProcessHeaderFooter(Worksheet worksheet, ISet result) + static void ProcessHeaderFooter(Worksheet worksheet, ISet result) { var hf = worksheet.Descendants().FirstOrDefault(); 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) return string.Empty; @@ -107,7 +107,7 @@ internal static class PlaceholderFinder return raw; } - private static unsafe bool FindPlaceholdersInText(string text, ref List? output) + static unsafe bool FindPlaceholdersInText(string text, ref List? output) { fixed (char* pText = text) { diff --git a/QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs b/QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs index 7f500cc..b86ad7d 100644 --- a/QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs +++ b/QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs @@ -158,7 +158,7 @@ internal static class ReplaceNumericExtensions // =========================== ОБЩАЯ ЛОГИКА =========================== - private static void ReplaceNumericCore( + static void ReplaceNumericCore( WorkbookPart workbookPart, WorksheetPart[] worksheets, IEnumerable> numericReplacements, @@ -255,7 +255,7 @@ internal static class ReplaceNumericExtensions UpdateSharedStringTable(workbookPart, allSharedStrings); } - private static void ReplaceSingleCore( + static void ReplaceSingleCore( WorkbookPart workbookPart, WorksheetPart[] worksheets, string oldValue, @@ -343,7 +343,7 @@ internal static class ReplaceNumericExtensions // =========================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ =========================== - private static string GetCellTextForNumeric(Cell cell, List allSharedStrings) + static string GetCellTextForNumeric(Cell cell, List allSharedStrings) { if (cell?.CellValue == null) return string.Empty; @@ -366,7 +366,7 @@ internal static class ReplaceNumericExtensions return val; } - private static void SetCellText(Cell cell, string newText, + static void SetCellText(Cell cell, string newText, List allSharedStrings, Dictionary sharedStringIndexMap) { if (cell.InlineString != null) @@ -388,7 +388,7 @@ internal static class ReplaceNumericExtensions cell.CellValue = new CellValue(index.ToString()); } - private static void UpdateSharedStringTable(WorkbookPart workbookPart, List allSharedStrings) + static void UpdateSharedStringTable(WorkbookPart workbookPart, List allSharedStrings) { var ssPart = workbookPart.SharedStringTablePart; ssPart ??= workbookPart.AddNewPart(); @@ -399,7 +399,7 @@ internal static class ReplaceNumericExtensions sharedStringTable.Save(); } - private static string ConcatTexts(IEnumerable texts) + static string ConcatTexts(IEnumerable texts) { var sb = new StringBuilder(); foreach (var t in texts) @@ -408,7 +408,7 @@ internal static class ReplaceNumericExtensions } // Оптимизированная замена подстроки через 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; 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; if (stylesPart == null) @@ -457,7 +457,7 @@ internal static class ReplaceNumericExtensions // =========================== КОЛОНТИТУЛЫ И КОММЕНТАРИИ =========================== - private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType) + static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType) { var worksheet = worksheetPart.Worksheet; if (worksheet is null) return; @@ -468,7 +468,7 @@ internal static class ReplaceNumericExtensions ReplaceHeaderFooter(elem, replacementDict, comparisonType); } - private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary replacementDict, StringComparison comparisonType) + static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary replacementDict, StringComparison comparisonType) { if (element?.Text is null) return; string original = element.Text; @@ -477,7 +477,7 @@ internal static class ReplaceNumericExtensions element.Text = processed; } - private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType) + static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType) { var commentsPart = worksheetPart.WorksheetCommentsPart; if (commentsPart?.Comments is null) return; @@ -493,7 +493,7 @@ internal static class ReplaceNumericExtensions } } - private static string ProcessReplacements(string input, Dictionary replacementDict, StringComparison comparisonType) + static string ProcessReplacements(string input, Dictionary replacementDict, StringComparison comparisonType) { if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input; string result = input; @@ -505,7 +505,7 @@ internal static class ReplaceNumericExtensions 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); if (idx < 0) return original; diff --git a/QWERTYkez.ExcelProcessor/ReplaceStringExtensions.cs b/QWERTYkez.ExcelProcessor/ReplaceStringExtensions.cs index b14f1e6..4280197 100644 --- a/QWERTYkez.ExcelProcessor/ReplaceStringExtensions.cs +++ b/QWERTYkez.ExcelProcessor/ReplaceStringExtensions.cs @@ -30,7 +30,7 @@ internal static class ReplaceStringExtensions } // --- Общий приватный метод, содержащий всю логику замены --- - private static void ReplaceCore(SpreadsheetDocument doc, WorksheetPart[] worksheets, Dictionary replacementDict, StringComparison comparisonType) + static void ReplaceCore(SpreadsheetDocument doc, WorksheetPart[] worksheets, Dictionary replacementDict, StringComparison comparisonType) { var workbookPart = doc.WorkbookPart!; @@ -63,7 +63,7 @@ internal static class ReplaceStringExtensions } // --- Остальные вспомогательные методы (без изменений) --- - private static IEqualityComparer GetComparerForStringComparison(StringComparison comparisonType) => + static IEqualityComparer GetComparerForStringComparison(StringComparison comparisonType) => comparisonType switch { StringComparison.Ordinal => StringComparer.Ordinal, @@ -75,7 +75,7 @@ internal static class ReplaceStringExtensions _ => StringComparer.OrdinalIgnoreCase, }; - private static void CollectCellChanges(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType, Dictionary cellChanges, List allSharedStrings) + static void CollectCellChanges(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType, Dictionary cellChanges, List allSharedStrings) { var worksheet = worksheetPart.Worksheet; if (worksheet is null) return; @@ -93,7 +93,7 @@ internal static class ReplaceStringExtensions } } - private static void ApplyCellChanges(Dictionary cellChanges, List allSharedStrings) + static void ApplyCellChanges(Dictionary cellChanges, List allSharedStrings) { foreach (var kvp in cellChanges) { @@ -114,7 +114,7 @@ internal static class ReplaceStringExtensions } } - private static void UpdateSharedStringTable(WorkbookPart workbookPart, List allSharedStrings) + static void UpdateSharedStringTable(WorkbookPart workbookPart, List allSharedStrings) { var ssPart = workbookPart.SharedStringTablePart; ssPart ??= workbookPart.AddNewPart(); @@ -125,7 +125,7 @@ internal static class ReplaceStringExtensions sharedStringTable.Save(); } - private static string ProcessReplacements(string input, Dictionary replacementDict, StringComparison comparisonType) + static string ProcessReplacements(string input, Dictionary replacementDict, StringComparison comparisonType) { if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input; string result = input; @@ -137,7 +137,7 @@ internal static class ReplaceStringExtensions 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); if (idx < 0) return original; @@ -154,7 +154,7 @@ internal static class ReplaceStringExtensions return sb.ToString(); } - private static string GetCellText(Cell cell, List allSharedStrings) + static string GetCellText(Cell cell, List allSharedStrings) { if (cell?.CellValue is null) return string.Empty; if (cell.InlineString is not null) @@ -169,7 +169,7 @@ internal static class ReplaceStringExtensions return value; } - private static int AddOrFindStringIndex(List allSharedStrings, string text) + static int AddOrFindStringIndex(List allSharedStrings, string text) { int idx = allSharedStrings.IndexOf(text); if (idx >= 0) return idx; @@ -178,7 +178,7 @@ internal static class ReplaceStringExtensions } // --- Колонтитулы (обобщённый метод) --- - private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType) + static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType) { var worksheet = worksheetPart.Worksheet; if (worksheet is null) return; @@ -189,7 +189,7 @@ internal static class ReplaceStringExtensions ReplaceHeaderFooter(elem, replacementDict, comparisonType); } - private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary replacementDict, StringComparison comparisonType) + static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary replacementDict, StringComparison comparisonType) { if (element?.Text is null) return; string original = element.Text; @@ -199,7 +199,7 @@ internal static class ReplaceStringExtensions } // --- Комментарии --- - private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType) + static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary replacementDict, StringComparison comparisonType) { var commentsPart = worksheetPart.WorksheetCommentsPart; if (commentsPart?.Comments is null) return;