This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -18,6 +18,30 @@ public readonly struct CellAlign : IEquatable<CellAlign>
|
||||
/// <summary>Уменьшать размер шрифта, чтобы текст поместился в ячейку.</summary>
|
||||
public bool? ShrinkToFit { get; init; }
|
||||
|
||||
|
||||
internal bool TryMerge(CellAlign other, out CellAlign result)
|
||||
{
|
||||
// Если other не содержит новых значений, возвращаем this
|
||||
if (other.Horizontal == Horizontal &&
|
||||
other.Vertical == Vertical &&
|
||||
other.WrapText == WrapText &&
|
||||
other.ShrinkToFit == ShrinkToFit)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new CellAlign
|
||||
{
|
||||
Horizontal = other.Horizontal ?? Horizontal,
|
||||
Vertical = other.Vertical ?? Vertical,
|
||||
WrapText = other.WrapText ?? WrapText,
|
||||
ShrinkToFit = other.ShrinkToFit ?? ShrinkToFit
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Преобразует горизонтальное выравнивание в тип Open XML.</summary>
|
||||
public bool TryGetExcelHorizontalAlignment(out HorizontalAlignmentValues value)
|
||||
{
|
||||
@@ -104,7 +128,7 @@ public readonly struct CellAlign : IEquatable<CellAlign>
|
||||
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<CellAlign>
|
||||
else throw new NotSupportedException($"Unsupported horizontal alignment: {value}");
|
||||
}
|
||||
|
||||
private static CellAlignVertical MapVerticalFromExcel(VerticalAlignmentValues value)
|
||||
static CellAlignVertical MapVerticalFromExcel(VerticalAlignmentValues value)
|
||||
{
|
||||
if (value == VerticalAlignmentValues.Top)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Определяет границы ячейки: верхнюю, нижнюю, левую, правую и диагональные.
|
||||
/// Каждая граница может иметь стиль и цвет.
|
||||
/// </summary>
|
||||
public readonly struct CellBorder : IEquatable<CellBorder>
|
||||
{
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
|
||||
/// <summary>Верхняя граница.</summary>
|
||||
public BorderSide? TopBorder { get; init; }
|
||||
|
||||
@@ -24,6 +42,33 @@ public readonly struct CellBorder : IEquatable<CellBorder>
|
||||
/// <summary>Диагональная граница «из левого нижнего в правый верхний» (//).</summary>
|
||||
public BorderSide? DiagonalRight { get; init; }
|
||||
|
||||
|
||||
public bool TryMerge(CellBorder other, out CellBorder result)
|
||||
{
|
||||
if (other.TopBorder == TopBorder &&
|
||||
other.BottomBorder == BottomBorder &&
|
||||
other.LeftBorder == LeftBorder &&
|
||||
other.RightBorder == RightBorder &&
|
||||
other.DiagonalLeft == DiagonalLeft &&
|
||||
other.DiagonalRight == DiagonalRight)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new CellBorder
|
||||
{
|
||||
TopBorder = other.TopBorder ?? TopBorder,
|
||||
BottomBorder = other.BottomBorder ?? BottomBorder,
|
||||
LeftBorder = other.LeftBorder ?? LeftBorder,
|
||||
RightBorder = other.RightBorder ?? RightBorder,
|
||||
DiagonalLeft = other.DiagonalLeft ?? DiagonalLeft,
|
||||
DiagonalRight = other.DiagonalRight ?? DiagonalRight
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Создаёт элемент Border для Open XML.</summary>
|
||||
public Border? ToBorder()
|
||||
{
|
||||
@@ -112,11 +157,23 @@ public readonly struct CellBorder : IEquatable<CellBorder>
|
||||
/// <summary>Стиль и цвет границы.</summary>
|
||||
public readonly struct BorderSide : IEquatable<BorderSide>
|
||||
{
|
||||
/// <summary>Тонкая черная линия</summary>
|
||||
public static BorderSide BlackThin { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Thin };
|
||||
|
||||
/// <summary>Толстая черная линия</summary>
|
||||
public static BorderSide BlackThick { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Thick };
|
||||
|
||||
/// <summary>Средняя черная линия</summary>
|
||||
public static BorderSide BlackMedium { get; } = new() { Color = System.Drawing.Color.Black, Style = BorderStyle.Medium };
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>Стиль линии границы.</summary>
|
||||
public BorderStyle? Style { get; init; }
|
||||
|
||||
/// <summary>Цвет границы.</summary>
|
||||
public ExColor? Color { get; init; }
|
||||
public System.Drawing.Color? Color { get; init; }
|
||||
|
||||
internal T ToBorderElement<T>() where T : BorderPropertiesType, new()
|
||||
{
|
||||
@@ -141,9 +198,9 @@ public readonly struct BorderSide : IEquatable<BorderSide>
|
||||
_ => 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<BorderSide>
|
||||
}
|
||||
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,
|
||||
/// <summary> Наклонная штрих-пунктирная (для диагональных) </summary>
|
||||
SlantDashDot,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Определяет, какие границы диапазона следует применить.
|
||||
/// </summary>
|
||||
public enum BorderTarget
|
||||
{
|
||||
/// <summary>Все границы (внешние и внутренние) – полная сетка.</summary>
|
||||
All,
|
||||
/// <summary>Только внешние границы диапазона.</summary>
|
||||
Outside,
|
||||
/// <summary>Только внутренние границы (между ячейками).</summary>
|
||||
Inside,
|
||||
/// <summary>Только верхняя граница диапазона.</summary>
|
||||
Top,
|
||||
/// <summary>Только нижняя граница диапазона.</summary>
|
||||
Bottom,
|
||||
/// <summary>Только левая граница диапазона.</summary>
|
||||
Left,
|
||||
/// <summary>Только правая граница диапазона.</summary>
|
||||
Right
|
||||
}
|
||||
@@ -6,15 +6,13 @@
|
||||
public readonly struct CellFill : IEquatable<CellFill>
|
||||
{
|
||||
/// <summary>Цвет фона.</summary>
|
||||
public ExColor? BackgroundColor { get; init; }
|
||||
public System.Drawing.Color? BackgroundColor { get; init; }
|
||||
|
||||
/// <summary>Создаёт элемент Fill для Open XML.</summary>
|
||||
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<CellFill>
|
||||
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);
|
||||
|
||||
@@ -13,7 +13,7 @@ public readonly struct CellFont : IEquatable<CellFont>
|
||||
public string? FontFamily { get; init; }
|
||||
|
||||
/// <summary>Цвет текста.</summary>
|
||||
public ExColor? FontColor { get; init; }
|
||||
public System.Drawing.Color? FontColor { get; init; }
|
||||
|
||||
/// <summary>Жирное начертание.</summary>
|
||||
public bool? IsBold { get; init; }
|
||||
@@ -27,6 +27,35 @@ public readonly struct CellFont : IEquatable<CellFont>
|
||||
/// <summary>Зачёркивание.</summary>
|
||||
public bool? IsStrike { get; init; }
|
||||
|
||||
|
||||
internal bool TryMerge(CellFont other, out CellFont result)
|
||||
{
|
||||
if (other.FontSize == FontSize &&
|
||||
other.FontFamily == FontFamily &&
|
||||
other.FontColor == FontColor &&
|
||||
other.IsBold == IsBold &&
|
||||
other.IsItalic == IsItalic &&
|
||||
other.IsUnderline == IsUnderline &&
|
||||
other.IsStrike == IsStrike)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new CellFont
|
||||
{
|
||||
FontSize = other.FontSize ?? FontSize,
|
||||
FontFamily = other.FontFamily ?? FontFamily,
|
||||
FontColor = other.FontColor ?? FontColor,
|
||||
IsBold = other.IsBold ?? IsBold,
|
||||
IsItalic = other.IsItalic ?? IsItalic,
|
||||
IsUnderline = other.IsUnderline ?? IsUnderline,
|
||||
IsStrike = other.IsStrike ?? IsStrike
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Создаёт элемент Font для Open XML.</summary>
|
||||
public Font? ToFont()
|
||||
{
|
||||
@@ -39,11 +68,8 @@ public readonly struct CellFont : IEquatable<CellFont>
|
||||
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<CellFont>
|
||||
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;
|
||||
}
|
||||
|
||||
153
QWERTYkez.ExcelProcessor/Editors/CellStyle.cs
Normal file
153
QWERTYkez.ExcelProcessor/Editors/CellStyle.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Неизменяемый набор всех параметров оформления ячейки.
|
||||
/// </summary>
|
||||
public sealed record CellStyle
|
||||
{
|
||||
public CellAlign? Align { get; init; }
|
||||
public CellFont? Font { get; init; }
|
||||
public CellFill? Fill { get; init; }
|
||||
public CellBorder? Border { get; init; }
|
||||
public NumberFormatPattern? NumberFormat { get; init; }
|
||||
|
||||
public bool IsEmpty() =>
|
||||
Align == null && Font == null && Fill == null && Border == null && NumberFormat == null;
|
||||
|
||||
internal bool TryMerge(NumberFormatPattern format, out CellStyle result)
|
||||
{
|
||||
result = this;
|
||||
if (format is not null)
|
||||
{
|
||||
if (!Equals(NumberFormat, format))
|
||||
{
|
||||
result = result with { NumberFormat = format };
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool TryMerge(CellAlign align, out CellStyle result)
|
||||
{
|
||||
result = this;
|
||||
var current = Align ?? new CellAlign();
|
||||
if (current.TryMerge(align, out align))
|
||||
{
|
||||
result = result with { Align = align };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool TryMerge(CellBorder border, out CellStyle result)
|
||||
{
|
||||
result = this;
|
||||
var current = Border ?? new CellBorder();
|
||||
if (current.TryMerge(border, out border))
|
||||
{
|
||||
result = result with { Border = border };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool TryMerge(CellFill fill, out CellStyle result)
|
||||
{
|
||||
result = this;
|
||||
var current = Fill ?? new CellFill();
|
||||
if (!current.Equals(fill))
|
||||
{
|
||||
result = result with { Fill = fill };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool TryMerge(CellFont font, out CellStyle result)
|
||||
{
|
||||
result = this;
|
||||
var current = Font ?? new CellFont();
|
||||
if (!current.Equals(font))
|
||||
{
|
||||
result = result with { Font = font };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
internal CellStyle Merge(CellStyle other)
|
||||
{
|
||||
if (other == null) return this;
|
||||
return new CellStyle
|
||||
{
|
||||
Align = other.Align ?? Align,
|
||||
Font = other.Font ?? Font,
|
||||
Fill = other.Fill ?? Fill,
|
||||
Border = other.Border ?? Border,
|
||||
NumberFormat = other.NumberFormat ?? NumberFormat
|
||||
};
|
||||
}
|
||||
|
||||
internal bool TryMerge(CellStyle other, out CellStyle result)
|
||||
{
|
||||
result = this;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
// Объединяем Align, если есть
|
||||
if (other.Align is not null)
|
||||
{
|
||||
var currentAlign = Align ?? new CellAlign();
|
||||
if (currentAlign.TryMerge(other.Align.Value, out var newAlign))
|
||||
{
|
||||
result = result with { Align = newAlign };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Аналогично для Font
|
||||
if (other.Font is not null)
|
||||
{
|
||||
var currentFont = Font ?? new CellFont();
|
||||
if (currentFont.TryMerge(other.Font.Value, out var newFont))
|
||||
{
|
||||
result = result with { Font = newFont };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Для Fill просто заменяем, если он задан (нет nullable-полей)
|
||||
if (other.Fill is not null)
|
||||
{
|
||||
if (!Fill.Equals(other.Fill))
|
||||
{
|
||||
result = result with { Fill = other.Fill };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Border
|
||||
if (other.Border is not null)
|
||||
{
|
||||
var currentBorder = Border ?? new CellBorder();
|
||||
if (currentBorder.TryMerge(other.Border.Value, out var newBorder))
|
||||
{
|
||||
result = result with { Border = newBorder };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// NumberFormat
|
||||
if (other.NumberFormat is not null)
|
||||
{
|
||||
if (!Equals(NumberFormat, other.NumberFormat))
|
||||
{
|
||||
result = result with { NumberFormat = other.NumberFormat };
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,15 @@
|
||||
/// </summary>
|
||||
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 }
|
||||
|
||||
/// <summary>Коэффициент перевода символов в пункты по умолчанию (используется, если нет калибровочной таблицы).</summary>
|
||||
public static double DefaultPointsPerChar { get; set; } = 5.65;
|
||||
|
||||
private ColumnWidth(double value, UnitType unit)
|
||||
ColumnWidth(double value, UnitType unit)
|
||||
{
|
||||
_rawValue = value;
|
||||
_unit = unit;
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
public readonly struct ExColor(System.Drawing.Color? Color)
|
||||
{
|
||||
private readonly System.Drawing.Color? color = Color;
|
||||
|
||||
/// <summary>Проверяет, является ли цвет автоматическим (т.е. Color == null).</summary>
|
||||
public bool IsAuto => color is null;
|
||||
|
||||
public System.Drawing.Color? Color => color;
|
||||
|
||||
public static ExColor FromArgb(byte r, byte g, byte b) => new(System.Drawing.Color.FromArgb(r, g, b));
|
||||
public static ExColor FromName(string knownColor) => new(System.Drawing.Color.FromName(knownColor));
|
||||
|
||||
|
||||
|
||||
public static implicit operator ExColor(Color exColor) => FromExcel(exColor);
|
||||
public static implicit operator Color(ExColor color) => color.ToExcel();
|
||||
public static ExColor FromExcel(Color excelColor)
|
||||
{
|
||||
if (excelColor == null)
|
||||
return new ExColor(null);
|
||||
|
||||
// Если цвет автоматический
|
||||
if (excelColor.Auto != null && excelColor.Auto.Value)
|
||||
return new ExColor(null);
|
||||
|
||||
// Если задан RGB
|
||||
if (excelColor.Rgb?.Value is { } rgb && rgb.Length > 0)
|
||||
{
|
||||
if (rgb.Length == 6) // RRGGBB
|
||||
{
|
||||
byte r = Convert.ToByte(rgb.Substring(0, 2), 16);
|
||||
byte g = Convert.ToByte(rgb.Substring(2, 2), 16);
|
||||
byte b = Convert.ToByte(rgb.Substring(4, 2), 16);
|
||||
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
|
||||
}
|
||||
else if (rgb.Length == 8) // AARRGGBB (альфа игнорируется)
|
||||
{
|
||||
byte r = Convert.ToByte(rgb.Substring(2, 2), 16);
|
||||
byte g = Convert.ToByte(rgb.Substring(4, 2), 16);
|
||||
byte b = Convert.ToByte(rgb.Substring(6, 2), 16);
|
||||
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
// По умолчанию — автоматический цвет
|
||||
return new ExColor(null);
|
||||
}
|
||||
public Color ToExcel()
|
||||
{
|
||||
var excelColor = new Color();
|
||||
{
|
||||
if (color.HasValue)
|
||||
{
|
||||
excelColor.Rgb = $"{color.Value.R:X2}{color.Value.G:X2}{color.Value.B:X2}";
|
||||
}
|
||||
else excelColor.Auto = true;
|
||||
}
|
||||
return excelColor;
|
||||
}
|
||||
|
||||
public static ExColor FromRgb(string rgb)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rgb)) return new ExColor(null);
|
||||
if (rgb.Length == 6)
|
||||
{
|
||||
byte r = Convert.ToByte(rgb.Substring(0, 2), 16);
|
||||
byte g = Convert.ToByte(rgb.Substring(2, 2), 16);
|
||||
byte b = Convert.ToByte(rgb.Substring(4, 2), 16);
|
||||
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
|
||||
}
|
||||
else if (rgb.Length == 8)
|
||||
{
|
||||
byte r = Convert.ToByte(rgb.Substring(2, 2), 16);
|
||||
byte g = Convert.ToByte(rgb.Substring(4, 2), 16);
|
||||
byte b = Convert.ToByte(rgb.Substring(6, 2), 16);
|
||||
return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
|
||||
}
|
||||
return new ExColor(null);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,77 @@
|
||||
/// </summary>
|
||||
internal sealed class ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col) : ICell
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает эффективный стиль ячейки с учётом наследования от строки и столбца.
|
||||
/// </summary>
|
||||
public CellStyle? GetCellStyle()
|
||||
{
|
||||
var cell = GetCellElement();
|
||||
CellStyle? cellStyle = null;
|
||||
|
||||
// 1. Пытаемся получить стиль самой ячейки
|
||||
if (cell?.StyleIndex?.Value is uint cellStyleIndex)
|
||||
{
|
||||
cellStyle = writer.GetCellStyle(cellStyleIndex);
|
||||
if (cellStyle != null && !cellStyle.IsEmpty())
|
||||
return cellStyle; // если у ячейки есть свой стиль, он имеет наивысший приоритет
|
||||
}
|
||||
|
||||
// 2. Стиль строки
|
||||
var sheetData = sheet.GetSheetData();
|
||||
var rowElement = FindRowElement(sheetData, row);
|
||||
if (rowElement?.StyleIndex?.Value is uint rowStyleIndex)
|
||||
{
|
||||
var rowStyle = writer.GetCellStyle(rowStyleIndex);
|
||||
if (rowStyle != null && !rowStyle.IsEmpty())
|
||||
{
|
||||
// Если у ячейки нет стиля, возвращаем стиль строки
|
||||
if (cellStyle == null)
|
||||
return rowStyle;
|
||||
// Иначе объединяем: стиль ячейки имеет приоритет, но некоторые свойства могут быть не заданы
|
||||
// (например, если у ячейки только Border, а у строки Fill, то в результате будет и Border, и Fill)
|
||||
// Объединяем: сначала берём стиль строки, затем накладываем стиль ячейки (перекрывая)
|
||||
return rowStyle.Merge(cellStyle);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Стиль столбца
|
||||
if (col > 0)
|
||||
{
|
||||
var columnElement = ExcelColumn.GetColumnElementInternal(sheet, col);
|
||||
if (columnElement?.Style?.Value is uint colStyleIndex)
|
||||
{
|
||||
var colStyle = writer.GetCellStyle(colStyleIndex);
|
||||
if (colStyle != null && !colStyle.IsEmpty())
|
||||
{
|
||||
// Если нет стиля ячейки и строки, возвращаем стиль столбца
|
||||
if (cellStyle == null)
|
||||
return colStyle;
|
||||
// Иначе объединяем: стиль ячейки + стиль строки (если есть) + стиль столбца
|
||||
// Сначала объединяем стиль строки и столбца, затем накладываем стиль ячейки
|
||||
// Но проще: берём стиль ячейки (если есть) и объединяем со стилем столбца
|
||||
return colStyle.Merge(cellStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если ничего нет, возвращаем null
|
||||
return cellStyle; // может быть null
|
||||
}
|
||||
|
||||
public void ApplyStyle(CellStyle style)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
int styleIndex = writer.GetOrCreateStyleId(style);
|
||||
cell.StyleIndex = (uint)styleIndex;
|
||||
}
|
||||
|
||||
|
||||
// Кэш фрагментов богатого текста (только для InlineString)
|
||||
private List<IRun>? _runsCache;
|
||||
private bool _cacheValid;
|
||||
List<IRun>? _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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICell Set(CellAlign align)
|
||||
{
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
var cell = GetOrCreateCellElement();
|
||||
int styleIndex = writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: null,
|
||||
border: null,
|
||||
align: align
|
||||
);
|
||||
cell.StyleIndex = (uint)styleIndex;
|
||||
return this;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICell Set(CellStyle style)
|
||||
{
|
||||
if (style is null) return this;
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
var currentStyle = GetCellStyle() ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(style, out style))
|
||||
ApplyStyle(style);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void ClearNeighborBorder(int rowOffset, int colOffset, Func<CellBorder, CellBorder> clearFunc)
|
||||
{
|
||||
var neighbor = GetNeighbor(rowOffset, colOffset);
|
||||
if (neighbor is not { } neighbr)
|
||||
return;
|
||||
|
||||
var neighborBorder = neighbr.GetCellBorder();
|
||||
var newBorder = clearFunc(neighborBorder);
|
||||
// Если после очистки граница изменилась, применяем изолированно
|
||||
if (!neighborBorder.Equals(newBorder))
|
||||
neighbr.SetBorderIsolate(newBorder);
|
||||
}
|
||||
internal void SetBorderIsolate(CellBorder border)
|
||||
{
|
||||
var currentStyle = GetCellStyle() ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(border, out var newStyle))
|
||||
ApplyStyle(newStyle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает соседнюю ячейку по указанному смещению.
|
||||
/// </summary>
|
||||
/// <param name="cell">Исходная ячейка.</param>
|
||||
/// <param name="rowOffset">Смещение по строкам (положительное – вниз).</param>
|
||||
/// <param name="colOffset">Смещение по столбцам (положительное – вправо).</param>
|
||||
/// <returns>Соседняя ячейка, или null, если она выходит за пределы листа.</returns>
|
||||
public ExcelCell? GetNeighbor(int rowOffset, int colOffset)
|
||||
{
|
||||
// Проверяем, что смещение не равно нулю и координаты не выходят за допустимые пределы (хотя мы не знаем границ листа)
|
||||
if (rowOffset == 0 && colOffset == 0) return null;
|
||||
int newRow = (int)row + rowOffset;
|
||||
int newCol = (int)col + colOffset;
|
||||
if (newRow < 1 || newCol < 1) return null;
|
||||
// Excel допускает до 1048576 строк и 16384 столбцов (но мы не будем жестко ограничивать)
|
||||
// Просто создаём объект ячейки, даже если она не существует физически.
|
||||
return new ExcelCell(writer, sheet, (uint)newRow, (uint)newCol);
|
||||
}
|
||||
|
||||
internal ICell SetBorderOverride(CellBorder border)
|
||||
{
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
// Определяем, какие стороны заданы в border
|
||||
bool hasTop = border.TopBorder.HasValue;
|
||||
bool hasBottom = border.BottomBorder.HasValue;
|
||||
bool hasLeft = border.LeftBorder.HasValue;
|
||||
bool hasRight = border.RightBorder.HasValue;
|
||||
|
||||
// Если какая-то сторона задана, то для соседней ячейки на этой стороне мы должны очистить противоположную сторону.
|
||||
// Например, если мы устанавливаем верхнюю границу у текущей ячейки, то у ячейки сверху нужно очистить нижнюю границу.
|
||||
// Аналогично для остальных сторон.
|
||||
|
||||
if (hasTop)
|
||||
{
|
||||
var neighbor = GetNeighbor(-1, 0);
|
||||
if (neighbor != null)
|
||||
{
|
||||
var neighborBorder = neighbor.GetCellBorder();
|
||||
if (neighborBorder.BottomBorder.HasValue)
|
||||
{
|
||||
var newNeighborBorder = neighborBorder with { BottomBorder = null };
|
||||
// Применяем только границы, не затрагивая другие аспекты стиля
|
||||
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBottom)
|
||||
{
|
||||
var neighbor = GetNeighbor(1, 0);
|
||||
if (neighbor != null)
|
||||
{
|
||||
var neighborBorder = neighbor.GetCellBorder();
|
||||
if (neighborBorder.TopBorder.HasValue)
|
||||
{
|
||||
var newNeighborBorder = neighborBorder with { TopBorder = null };
|
||||
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasLeft)
|
||||
{
|
||||
var neighbor = GetNeighbor(0, -1);
|
||||
if (neighbor != null)
|
||||
{
|
||||
var neighborBorder = neighbor.GetCellBorder();
|
||||
if (neighborBorder.RightBorder.HasValue)
|
||||
{
|
||||
var newNeighborBorder = neighborBorder with { RightBorder = null };
|
||||
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRight)
|
||||
{
|
||||
var neighbor = GetNeighbor(0, 1);
|
||||
if (neighbor != null)
|
||||
{
|
||||
var neighborBorder = neighbor.GetCellBorder();
|
||||
if (neighborBorder.LeftBorder.HasValue)
|
||||
{
|
||||
var newNeighborBorder = neighborBorder with { LeftBorder = null };
|
||||
((ExcelCell)neighbor).SetBorderIsolate(newNeighborBorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Теперь устанавливаем границу для текущей ячейки (изолированно, чтобы не было зацикливания)
|
||||
((ExcelCell)this).SetBorderIsolate(border);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public ICell Set(bool value)
|
||||
{
|
||||
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<Row>())
|
||||
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<Row>().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<Cell>())
|
||||
{
|
||||
@@ -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<MergeCells>();
|
||||
|
||||
private bool TryParseRangeReference(string reference, out ExcelRange range)
|
||||
bool TryParseRangeReference(string reference, out ExcelRange range)
|
||||
{
|
||||
range = null!;
|
||||
if (string.IsNullOrEmpty(reference)) return false;
|
||||
|
||||
@@ -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<Columns>();
|
||||
if (cols == null) return null;
|
||||
foreach (Column col in cols.Elements<Column>())
|
||||
{
|
||||
if (col.Min?.Value is { } min && col.Max?.Value is { } max && min <= colIndex && max >= colIndex)
|
||||
return col;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IColumn Set(NumberFormatPattern format)
|
||||
{
|
||||
if (format == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var columnElement = GetOrCreateColumnElement();
|
||||
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(format, out var newStyle))
|
||||
{
|
||||
ApplyStyleToColumn(newStyle);
|
||||
ApplyStyleToColumnCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IColumn Set(CellAlign align)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var columnElement = GetOrCreateColumnElement();
|
||||
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(align, out var newStyle))
|
||||
{
|
||||
ApplyStyleToColumn(newStyle);
|
||||
ApplyStyleToColumnCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IColumn Set(CellBorder border)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var columnElement = GetOrCreateColumnElement();
|
||||
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(border, out var newStyle))
|
||||
{
|
||||
ApplyStyleToColumn(newStyle);
|
||||
ApplyStyleToColumnCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IColumn Set(CellFill fill)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var columnElement = GetOrCreateColumnElement();
|
||||
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(fill, out var newStyle))
|
||||
{
|
||||
ApplyStyleToColumn(newStyle);
|
||||
ApplyStyleToColumnCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IColumn Set(CellFont font)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var columnElement = GetOrCreateColumnElement();
|
||||
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(font, out var newStyle))
|
||||
{
|
||||
ApplyStyleToColumn(newStyle);
|
||||
ApplyStyleToColumnCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IColumn Set(CellStyle style)
|
||||
{
|
||||
if (style == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var columnElement = GetOrCreateColumnElement();
|
||||
var currentStyle = GetColumnStyle(columnElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(style, out var newStyle))
|
||||
{
|
||||
ApplyStyleToColumn(newStyle);
|
||||
ApplyStyleToColumnCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void ApplyStyleToColumnCells(CellStyle style)
|
||||
{
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
foreach (var rowElement in sheetData.Elements<Row>())
|
||||
{
|
||||
var cellElement = FindCellInRow(rowElement, _colIndex);
|
||||
if (cellElement != null)
|
||||
{
|
||||
var cell = new ExcelCell(_writer, _sheet, rowElement.RowIndex!.Value, _colIndex);
|
||||
cell.Set(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public uint Index => _colIndex;
|
||||
public string IndexLetter => NumberToColumnLetter(_colIndex);
|
||||
|
||||
|
||||
@@ -64,23 +218,7 @@ internal sealed class ExcelColumn : IColumn
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательные внутренние методы (перенести существующую логику)
|
||||
private static Column? GetColumnElementInternal(ExcelSheet sheet, uint colIndex)
|
||||
{
|
||||
var cols = sheet.Worksheet.GetFirstChild<Columns>();
|
||||
if (cols == null) return null;
|
||||
foreach (Column col in cols.Elements<Column>())
|
||||
{
|
||||
if (col?.Min?.Value is { } min
|
||||
&& col?.Max?.Value is { } max
|
||||
&& min <= colIndex
|
||||
&& max >= colIndex)
|
||||
return col;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Column GetOrCreateColumnElementInternal(ExcelSheet sheet, uint colIndex)
|
||||
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<Row>())
|
||||
{
|
||||
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<Columns>();
|
||||
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<Row>().ToList())
|
||||
@@ -399,7 +521,7 @@ internal sealed class ExcelColumn : IColumn
|
||||
}
|
||||
}
|
||||
|
||||
private Cell? FindCellInRow(Row row, uint colIndex)
|
||||
Cell? FindCellInRow(Row row, uint colIndex)
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
@@ -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<Columns>();
|
||||
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 = "";
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
/// </summary>
|
||||
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<ICell> action)
|
||||
{
|
||||
for (uint r = _rowStart; r <= _rowEnd; r++)
|
||||
for (uint c = _colStart; c <= _colEnd; c++)
|
||||
{
|
||||
var cell = new ExcelCell(_writer, _sheet, r, c);
|
||||
action(cell);
|
||||
}
|
||||
}
|
||||
|
||||
public IRange Set(NumberFormatPattern format)
|
||||
{
|
||||
if (format == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
ForEachCell(cell => cell.Set(format));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public IRange Set(CellAlign align)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
ForEachCell(cell => cell.Set(align));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public IRange Set(CellBorder border)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
ForEachCell(cell => cell.Set(border));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public IRange Set(CellFill fill)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
ForEachCell(cell => cell.Set(fill));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public IRange Set(CellFont font)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
ForEachCell(cell => cell.Set(font));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public IRange Set(CellStyle style)
|
||||
{
|
||||
if (style == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
ForEachCell(cell => cell.Set(style));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить").
|
||||
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
|
||||
/// </summary>
|
||||
/// <param name="newRow">Номер строки для нового верхнего левого угла.</param>
|
||||
/// <param name="newCol">Номер столбца для нового верхнего левого угла.</param>
|
||||
/// <returns>Тот же объект IRange с новыми координатами.</returns>
|
||||
public IRange MoveTo(uint newRow, uint newCol)
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить").
|
||||
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
|
||||
/// </summary>
|
||||
/// <param name="newRow">Номер строки для нового верхнего левого угла.</param>
|
||||
/// <param name="newCol">Номер столбца для нового верхнего левого угла.</param>
|
||||
/// <returns>Тот же объект IRange с новыми координатами.</returns>
|
||||
public IRange MoveTo(uint newRow, uint newCol)
|
||||
{
|
||||
if (newRow == _rowStart && newCol == _colStart) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода.</summary>
|
||||
private void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd,
|
||||
void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd,
|
||||
uint dstRowStart, uint dstColStart, CopyOrder order)
|
||||
{
|
||||
// Определяем все строки, которые могут понадобиться (исходные и целевые)
|
||||
@@ -172,7 +245,7 @@ internal sealed class ExcelRange : IRange
|
||||
}
|
||||
|
||||
/// <summary>Строит словарь строк для указанных индексов строк.</summary>
|
||||
private Dictionary<uint, Row> GetRowDictionary(HashSet<uint> rowIndices)
|
||||
Dictionary<uint, Row> GetRowDictionary(HashSet<uint> rowIndices)
|
||||
{
|
||||
var dict = new Dictionary<uint, Row>();
|
||||
var sheetData = _sheet.GetSheetData();
|
||||
@@ -185,7 +258,7 @@ internal sealed class ExcelRange : IRange
|
||||
}
|
||||
|
||||
/// <summary>Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке).</summary>
|
||||
private Cell? FindCellInRowFast(Row row, uint colIndex)
|
||||
Cell? FindCellInRowFast(Row row, uint colIndex)
|
||||
{
|
||||
foreach (var cell in row.Elements<Cell>())
|
||||
{
|
||||
@@ -196,14 +269,14 @@ internal sealed class ExcelRange : IRange
|
||||
}
|
||||
|
||||
/// <summary>Проверяет, пересекаются ли два прямоугольных диапазона.</summary>
|
||||
private static bool RangesOverlap(uint r1s, uint c1s, uint r1e, uint c1e,
|
||||
static bool RangesOverlap(uint r1s, uint c1s, uint r1e, uint c1e,
|
||||
uint r2s, uint c2s, uint r2e, uint c2e)
|
||||
{
|
||||
return !(r1e < r2s || r2e < r1s || c1e < c2s || c2e < c1s);
|
||||
}
|
||||
|
||||
/// <summary>Вставляет ячейку в указанную позицию, предварительно удаляя существующую.</summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>Удаляет ячейку, если она существует.</summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>Получает или создаёт строку с указанным индексом.</summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>Вставляет ячейку в строку с сохранением порядка столбцов.</summary>
|
||||
private void InsertCellInRow(Row row, Cell cell, uint colIndex)
|
||||
void InsertCellInRow(Row row, Cell cell, uint colIndex)
|
||||
{
|
||||
string newRef = $"{CellAddressHelper.ColumnIndexToLetter(colIndex)}{row.RowIndex?.Value ?? 1}";
|
||||
cell.CellReference = newRef;
|
||||
@@ -251,7 +324,7 @@ internal sealed class ExcelRange : IRange
|
||||
}
|
||||
|
||||
/// <summary>Очищает данные (содержимое) в указанном диапазоне.</summary>
|
||||
private void ClearRangeData(uint rowStart, uint colStart, uint rowEnd, uint colEnd)
|
||||
void ClearRangeData(uint rowStart, uint colStart, uint rowEnd, uint colEnd)
|
||||
{
|
||||
for (uint r = rowStart; r <= rowEnd; r++)
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange SetCellAlign(CellAlign align)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: null,
|
||||
border: null,
|
||||
align: align
|
||||
);
|
||||
ApplyStyleToRange((uint)styleIndex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange SetCellBorder(CellBorder border)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: null,
|
||||
border: border,
|
||||
align: null
|
||||
);
|
||||
ApplyStyleToRange((uint)styleIndex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange SetCellFill(CellFill fill)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: null,
|
||||
fill: fill,
|
||||
border: null,
|
||||
align: null
|
||||
);
|
||||
ApplyStyleToRange((uint)styleIndex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRange SetCellFont(CellFont font)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
int styleIndex = _writer.GetOrCreateCellFormatId(
|
||||
numberFormat: null,
|
||||
font: font,
|
||||
fill: null,
|
||||
border: null,
|
||||
align: null
|
||||
);
|
||||
ApplyStyleToRange((uint)styleIndex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Применяет стиль ко всем ячейкам диапазона.
|
||||
/// </summary>
|
||||
/// <param name="styleIndex">Индекс стиля для применения.</param>
|
||||
/// <param name="createIfMissing">Если true, создаёт недостающие ячейки (по умолчанию true).</param>
|
||||
private void ApplyStyleToRange(uint styleIndex, bool createIfMissing = true)
|
||||
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<Row>())
|
||||
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<Row>().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<Cell>())
|
||||
{
|
||||
@@ -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<Columns>();
|
||||
@@ -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<MergeCells>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
/// </summary>
|
||||
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<Row>())
|
||||
@@ -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<Row>().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<Cell>())
|
||||
{
|
||||
var colIndex = GetColumnIndex(cellElement.CellReference?.Value);
|
||||
if (colIndex > 0)
|
||||
{
|
||||
var cell = new ExcelCell(_writer, _sheet, _rowIndex, colIndex);
|
||||
// Применяем стиль через Set, чтобы объединить с существующим
|
||||
cell.Set(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
static uint GetColumnIndex(string? cellReference)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cellReference)) return 0;
|
||||
int i = 0;
|
||||
while (i < cellReference!.Length && char.IsLetter(cellReference[i])) i++;
|
||||
if (i == 0) return 0;
|
||||
string colPart = cellReference.Substring(0, i);
|
||||
return CellAddressHelper.ColumnLetterToIndex(colPart);
|
||||
}
|
||||
|
||||
CellStyle? GetRowStyle(Row rowElement)
|
||||
{
|
||||
if (rowElement.StyleIndex?.Value is not uint styleIndex)
|
||||
return null;
|
||||
return _writer.GetCellStyle(styleIndex);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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>())
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow Set(CellAlign align)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = GetOrCreateRowElement();
|
||||
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(align, out var newStyle))
|
||||
{
|
||||
ApplyStyleToRow(newStyle);
|
||||
ApplyStyleToRowCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow Set(CellBorder border)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = GetOrCreateRowElement();
|
||||
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(border, out var newStyle))
|
||||
{
|
||||
ApplyStyleToRow(newStyle);
|
||||
ApplyStyleToRowCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow Set(CellFill fill)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = GetOrCreateRowElement();
|
||||
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(fill, out var newStyle))
|
||||
{
|
||||
ApplyStyleToRow(newStyle);
|
||||
ApplyStyleToRowCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow Set(CellFont font)
|
||||
{
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = GetOrCreateRowElement();
|
||||
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(font, out var newStyle))
|
||||
{
|
||||
ApplyStyleToRow(newStyle);
|
||||
ApplyStyleToRowCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRow Set(CellStyle style)
|
||||
{
|
||||
if (style == null) return this;
|
||||
_writer.ThrowIfDisposed();
|
||||
lock (_writer._syncLock)
|
||||
{
|
||||
var rowElement = GetOrCreateRowElement();
|
||||
var currentStyle = GetRowStyle(rowElement) ?? new CellStyle();
|
||||
if (currentStyle.TryMerge(style, out var newStyle))
|
||||
{
|
||||
ApplyStyleToRow(newStyle);
|
||||
ApplyStyleToRowCells(newStyle);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void ApplyStyleToRow(CellStyle style)
|
||||
{
|
||||
var rowElement = GetOrCreateRowElement();
|
||||
int styleIndex = _writer.GetOrCreateStyleId(style);
|
||||
rowElement.StyleIndex = (uint)styleIndex;
|
||||
rowElement.CustomFormat = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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<Row>())
|
||||
@@ -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);
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
/// </summary>
|
||||
internal sealed class ExcelRun : IRun
|
||||
{
|
||||
private string _text = string.Empty;
|
||||
private RunFormat? _format;
|
||||
string _text = string.Empty;
|
||||
RunFormat? _format;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Text
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -219,7 +219,22 @@ public interface IRow
|
||||
RowHeight Height { get; set; }
|
||||
|
||||
/// <summary>Устанавливает числовой формат для всех ячеек строки.</summary>
|
||||
IRow SetNumberFormat(NumberFormatPattern format);
|
||||
IRow Set(NumberFormatPattern format);
|
||||
|
||||
/// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary>
|
||||
IRow Set(CellAlign format);
|
||||
|
||||
/// <summary>Устанавливает границы для всех ячеек диапазона.</summary>
|
||||
IRow Set(CellBorder format);
|
||||
|
||||
/// <summary>Устанавливает заливку для всех ячеек диапазона.</summary>
|
||||
IRow Set(CellFill format);
|
||||
|
||||
/// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary>
|
||||
IRow Set(CellFont format);
|
||||
|
||||
/// <summary>Устанавливает шрифт ячейки.</summary>
|
||||
IRow Set(CellStyle font);
|
||||
|
||||
/// <summary>Возвращает ячейку в заданном столбце (индекс с 1).</summary>
|
||||
ICell Cell(uint col);
|
||||
@@ -346,7 +361,22 @@ public interface IColumn
|
||||
ColumnWidth Width { get; set; }
|
||||
|
||||
/// <summary>Устанавливает числовой формат для всех ячеек столбца.</summary>
|
||||
IColumn SetNumberFormat(NumberFormatPattern format);
|
||||
IColumn Set(NumberFormatPattern format);
|
||||
|
||||
/// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary>
|
||||
IColumn Set(CellAlign format);
|
||||
|
||||
/// <summary>Устанавливает границы для всех ячеек диапазона.</summary>
|
||||
IColumn Set(CellBorder format);
|
||||
|
||||
/// <summary>Устанавливает заливку для всех ячеек диапазона.</summary>
|
||||
IColumn Set(CellFill format);
|
||||
|
||||
/// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary>
|
||||
IColumn Set(CellFont format);
|
||||
|
||||
/// <summary>Устанавливает шрифт ячейки.</summary>
|
||||
IColumn Set(CellStyle font);
|
||||
|
||||
/// <summary>Возвращает ячейку в заданной строке (индекс с 1).</summary>
|
||||
ICell Cell(uint row);
|
||||
@@ -476,19 +506,22 @@ public interface IRange
|
||||
IRange MoveTo(uint rowIndex, string colIndex);
|
||||
|
||||
/// <summary>Устанавливает числовой формат для всех ячеек диапазона.</summary>
|
||||
IRange SetNumberFormat(NumberFormatPattern format);
|
||||
IRange Set(NumberFormatPattern format);
|
||||
|
||||
/// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary>
|
||||
IRange SetCellAlign(CellAlign format);
|
||||
IRange Set(CellAlign format);
|
||||
|
||||
/// <summary>Устанавливает границы для всех ячеек диапазона.</summary>
|
||||
IRange SetCellBorder(CellBorder format);
|
||||
IRange Set(CellBorder format);
|
||||
|
||||
/// <summary>Устанавливает заливку для всех ячеек диапазона.</summary>
|
||||
IRange SetCellFill(CellFill format);
|
||||
IRange Set(CellFill format);
|
||||
|
||||
/// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary>
|
||||
IRange SetCellFont(CellFont format);
|
||||
IRange Set(CellFont format);
|
||||
|
||||
/// <summary>Устанавливает шрифт ячейки.</summary>
|
||||
IRange Set(CellStyle font);
|
||||
|
||||
/// <summary>Перечисляет все ячейки диапазона (по строкам).</summary>
|
||||
IEnumerable<ICell> Cells { get; }
|
||||
@@ -733,6 +766,9 @@ public interface ICell
|
||||
/// <summary>Возвращает шрифт ячейки.</summary>
|
||||
CellFont GetCellFont();
|
||||
|
||||
/// <summary>Возвращает шрифт ячейки.</summary>
|
||||
CellStyle? GetCellStyle();
|
||||
|
||||
/// <summary>Пытается извлечь логическое значение.</summary>
|
||||
bool TryGetBoolean(out bool value);
|
||||
|
||||
@@ -754,25 +790,28 @@ public interface ICell
|
||||
/// <summary>Пытается установить формулу (без вычисленного значения).</summary>
|
||||
/// <param name="formula">Текст формулы (например, "SUM(A1:A5)").</param>
|
||||
/// <param name="format">Необязательный числовой формат для результата.</param>
|
||||
bool TrySet(string formula, NumberFormatPattern? format = null);
|
||||
bool TrySetFormula(string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает формулу (выбрасывает исключение при ошибке).</summary>
|
||||
ICell Set(string formula, NumberFormatPattern? format = null);
|
||||
ICell SetFormula(string formula, NumberFormatPattern? format = null);
|
||||
|
||||
/// <summary>Устанавливает числовой формат ячейки (не меняя значение).</summary>
|
||||
ICell Set(NumberFormatPattern format);
|
||||
|
||||
/// <summary>Устанавливает выравнивание текста ячейки.</summary>
|
||||
ICell Set(CellAlign format);
|
||||
ICell Set(CellAlign align);
|
||||
|
||||
/// <summary>Устанавливает границы ячейки.</summary>
|
||||
ICell Set(CellBorder format);
|
||||
ICell Set(CellBorder border);
|
||||
|
||||
/// <summary>Устанавливает заливку ячейки.</summary>
|
||||
ICell Set(CellFill format);
|
||||
ICell Set(CellFill fill);
|
||||
|
||||
/// <summary>Устанавливает шрифт ячейки.</summary>
|
||||
ICell Set(CellFont format);
|
||||
ICell Set(CellFont font);
|
||||
|
||||
/// <summary>Устанавливает стиль ячейки.</summary>
|
||||
ICell Set(CellStyle style);
|
||||
|
||||
/// <summary>Устанавливает простое текстовое значение (без форматирования).</summary>
|
||||
ICell Set(string value);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
149
QWERTYkez.ExcelProcessor/Editors/RangeBorderExtensions.cs
Normal file
149
QWERTYkez.ExcelProcessor/Editors/RangeBorderExtensions.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
namespace QWERTYkez.ExcelProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Методы расширения для установки границ диапазона.
|
||||
/// </summary>
|
||||
public static class RangeBorderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Устанавливает границы для диапазона с указанным стилем и цветом.
|
||||
/// </summary>
|
||||
/// <param name="range">Диапазон ячеек.</param>
|
||||
/// <param name="style">Стиль линии границы.</param>
|
||||
/// <param name="color">Цвет границы (необязательно).</param>
|
||||
/// <param name="target">Какие границы применять (по умолчанию Outside).</param>
|
||||
/// <returns>Тот же диапазон для цепочки вызовов.</returns>
|
||||
public static IRange Set(this IRange range, BorderStyle style, BorderTarget target = BorderTarget.Outside,
|
||||
System.Drawing.Color? color = null) => range.Set(new() { Style = style, Color = color ?? System.Drawing.Color.Black }, target);
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает границы для диапазона с указанными параметрами.
|
||||
/// </summary>
|
||||
/// <param name="range">Диапазон ячеек.</param>
|
||||
/// <param name="borderSide">Стиль и цвет границы.</param>
|
||||
/// <param name="target">Какие границы применять (по умолчанию Outside).</param>
|
||||
/// <returns>Тот же диапазон для цепочки вызовов.</returns>
|
||||
public static IRange Set(this IRange range, BorderSide borderSide, BorderTarget target = BorderTarget.Outside)
|
||||
{
|
||||
if (range is null) throw new ArgumentNullException(nameof(range));
|
||||
if (range is not ExcelRange excelRange)
|
||||
throw new ArgumentException("Range must be of type ExcelRange", nameof(range));
|
||||
|
||||
var writer = excelRange._writer;
|
||||
var sheet = excelRange._sheet;
|
||||
uint rowStart = range.RowStart;
|
||||
uint rowEnd = range.RowEnd;
|
||||
uint colStart = range.ColStart;
|
||||
uint colEnd = range.ColEnd;
|
||||
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
// Определяем, какие стороны нужны для каждой ячейки
|
||||
for (uint r = rowStart; r <= rowEnd; r++)
|
||||
{
|
||||
for (uint c = colStart; c <= colEnd; c++)
|
||||
{
|
||||
var cell = new ExcelCell(writer, sheet, r, c);
|
||||
var newBorder = new CellBorder();
|
||||
|
||||
bool isTopRow = (r == rowStart);
|
||||
bool isBottomRow = (r == rowEnd);
|
||||
bool isLeftCol = (c == colStart);
|
||||
bool isRightCol = (c == colEnd);
|
||||
|
||||
// В зависимости от target, определяем, какие стороны устанавливать
|
||||
if (target == BorderTarget.All || target == BorderTarget.Outside)
|
||||
{
|
||||
if (isTopRow) newBorder = newBorder with { TopBorder = borderSide };
|
||||
if (isBottomRow) newBorder = newBorder with { BottomBorder = borderSide };
|
||||
if (isLeftCol) newBorder = newBorder with { LeftBorder = borderSide };
|
||||
if (isRightCol) newBorder = newBorder with { RightBorder = borderSide };
|
||||
}
|
||||
|
||||
if (target == BorderTarget.All || target == BorderTarget.Inside)
|
||||
{
|
||||
// Внутренние границы: для каждой ячейки устанавливаем правую и нижнюю,
|
||||
// если есть сосед справа/снизу внутри диапазона
|
||||
if (c < colEnd) newBorder = newBorder with { RightBorder = borderSide };
|
||||
if (r < rowEnd) newBorder = newBorder with { BottomBorder = borderSide };
|
||||
}
|
||||
|
||||
// Отдельные стороны
|
||||
if (target == BorderTarget.Top && isTopRow)
|
||||
newBorder = newBorder with { TopBorder = borderSide };
|
||||
if (target == BorderTarget.Bottom && isBottomRow)
|
||||
newBorder = newBorder with { BottomBorder = borderSide };
|
||||
if (target == BorderTarget.Left && isLeftCol)
|
||||
newBorder = newBorder with { LeftBorder = borderSide };
|
||||
if (target == BorderTarget.Right && isRightCol)
|
||||
newBorder = newBorder with { RightBorder = borderSide };
|
||||
|
||||
// Применяем границу к ячейке (изолированно, без каскада)
|
||||
cell.SetBorderIsolate(newBorder);
|
||||
|
||||
// Если ячейка находится на внешней границе диапазона, очищаем соответствующую сторону у соседа вне диапазона
|
||||
if (isTopRow && (target == BorderTarget.Outside || target == BorderTarget.All || target == BorderTarget.Top))
|
||||
ClearNeighborBorder(cell, -1, 0, b => b with { BottomBorder = null });
|
||||
|
||||
if (isBottomRow && (target == BorderTarget.Outside || target == BorderTarget.All || target == BorderTarget.Bottom))
|
||||
ClearNeighborBorder(cell, 1, 0, b => b with { TopBorder = null });
|
||||
|
||||
if (isLeftCol && (target == BorderTarget.Outside || target == BorderTarget.All || target == BorderTarget.Left))
|
||||
ClearNeighborBorder(cell, 0, -1, b => b with { RightBorder = null });
|
||||
|
||||
if (isRightCol && (target == BorderTarget.Outside || target == BorderTarget.All || target == BorderTarget.Right))
|
||||
ClearNeighborBorder(cell, 0, 1, b => b with { LeftBorder = null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Очищает все границы в диапазоне (сбрасывает до None).
|
||||
/// </summary>
|
||||
public static IRange ClearBorders(this IRange range)
|
||||
{
|
||||
if (range is null) throw new ArgumentNullException(nameof(range));
|
||||
if (range is not ExcelRange excelRange)
|
||||
throw new ArgumentException("Range must be of type ExcelRange", nameof(range));
|
||||
|
||||
var writer = excelRange._writer;
|
||||
var sheet = excelRange._sheet;
|
||||
uint rowStart = range.RowStart;
|
||||
uint rowEnd = range.RowEnd;
|
||||
uint colStart = range.ColStart;
|
||||
uint colEnd = range.ColEnd;
|
||||
|
||||
writer.ThrowIfDisposed();
|
||||
lock (writer._syncLock)
|
||||
{
|
||||
for (uint r = rowStart; r <= rowEnd; r++)
|
||||
{
|
||||
for (uint c = colStart; c <= colEnd; c++)
|
||||
{
|
||||
var cell = new ExcelCell(writer, sheet, r, c);
|
||||
// Удаляем все границы
|
||||
cell.SetBorderIsolate(new CellBorder()); // пустая граница
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
// Вспомогательный метод для очистки конкретной стороны у соседа
|
||||
static void ClearNeighborBorder(ExcelCell cell, int rowOffset, int colOffset, Func<CellBorder, CellBorder> clearFunc)
|
||||
{
|
||||
var neighbor = cell.GetNeighbor(rowOffset, colOffset);
|
||||
if (neighbor is null)
|
||||
return;
|
||||
|
||||
var neighborBorder = neighbor.GetCellBorder();
|
||||
var newBorder = clearFunc(neighborBorder);
|
||||
if (!neighborBorder.Equals(newBorder))
|
||||
neighbor.SetBorderIsolate(newBorder);
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,13 @@
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>Высота в пунктах (points).</summary>
|
||||
public double Points { get; } = points;
|
||||
|
||||
@@ -19,7 +19,7 @@ public readonly struct RunFormat
|
||||
public bool? IsStrike { get; init; }
|
||||
|
||||
/// <summary>Цвет текста фрагмента.</summary>
|
||||
public ExColor? Color { get; init; }
|
||||
public System.Drawing.Color? Color { get; init; }
|
||||
|
||||
/// <summary>Размер шрифта фрагмента в пунктах.</summary>
|
||||
public double? FontSize { get; init; }
|
||||
@@ -80,9 +80,7 @@ public readonly struct RunFormat
|
||||
// DoubleStrike в Excel не поддерживается, опускаем
|
||||
|
||||
Underline = underline,
|
||||
Color = rPr.GetFirstChild<Color>()?.Rgb is not null
|
||||
? ExColor.FromRgb(rPr.GetFirstChild<Color>()!.Rgb!.Value!)
|
||||
: null!,
|
||||
Color = FromExcelColor(rPr.GetFirstChild<Color>()?.Rgb),
|
||||
FontSize = rPr.GetFirstChild<FontSize>()?.Val?.Value,
|
||||
FontFamily = rPr.GetFirstChild<RunFont>()?.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 или других более удобных типов
|
||||
|
||||
Reference in New Issue
Block a user