2026-06-08 14:31:31 +07:00
|
|
|
|
namespace QWERTYkez.ExcelProcessor;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-05 15:58:03 +07:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Определяет границы ячейки: верхнюю, нижнюю, левую, правую и диагональные.
|
|
|
|
|
|
/// Каждая граница может иметь стиль и цвет.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public readonly struct CellBorder : IEquatable<CellBorder>
|
|
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-05 15:58:03 +07:00
|
|
|
|
/// <summary>Верхняя граница.</summary>
|
|
|
|
|
|
public BorderSide? TopBorder { get; init; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Нижняя граница.</summary>
|
|
|
|
|
|
public BorderSide? BottomBorder { get; init; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Левая граница.</summary>
|
|
|
|
|
|
public BorderSide? LeftBorder { get; init; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Правая граница.</summary>
|
|
|
|
|
|
public BorderSide? RightBorder { get; init; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Диагональная граница «из левого верхнего в правый нижний» (\\).</summary>
|
|
|
|
|
|
public BorderSide? DiagonalLeft { get; init; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Диагональная граница «из левого нижнего в правый верхний» (//).</summary>
|
|
|
|
|
|
public BorderSide? DiagonalRight { get; init; }
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-05 15:58:03 +07:00
|
|
|
|
/// <summary>Создаёт элемент Border для Open XML.</summary>
|
|
|
|
|
|
public Border? ToBorder()
|
|
|
|
|
|
{
|
|
|
|
|
|
bool hasAny = TopBorder.HasValue || BottomBorder.HasValue || LeftBorder.HasValue ||
|
|
|
|
|
|
RightBorder.HasValue || DiagonalLeft.HasValue || DiagonalRight.HasValue;
|
|
|
|
|
|
if (!hasAny) return null;
|
|
|
|
|
|
|
|
|
|
|
|
var border = new Border();
|
|
|
|
|
|
if (TopBorder.HasValue)
|
|
|
|
|
|
border.TopBorder = TopBorder.Value.ToBorderElement<TopBorder>();
|
|
|
|
|
|
if (BottomBorder.HasValue)
|
|
|
|
|
|
border.BottomBorder = BottomBorder.Value.ToBorderElement<BottomBorder>();
|
|
|
|
|
|
if (LeftBorder.HasValue)
|
|
|
|
|
|
border.LeftBorder = LeftBorder.Value.ToBorderElement<LeftBorder>();
|
|
|
|
|
|
if (RightBorder.HasValue)
|
|
|
|
|
|
border.RightBorder = RightBorder.Value.ToBorderElement<RightBorder>();
|
|
|
|
|
|
|
|
|
|
|
|
// Обработка диагональных границ
|
|
|
|
|
|
if (DiagonalLeft.HasValue || DiagonalRight.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Диагональ \\ (из левого верхнего в правый нижний) = DiagonalDown
|
|
|
|
|
|
border.DiagonalDown = DiagonalLeft.HasValue;
|
|
|
|
|
|
// Диагональ // (из левого нижнего в правый верхний) = DiagonalUp
|
|
|
|
|
|
border.DiagonalUp = DiagonalRight.HasValue;
|
|
|
|
|
|
|
|
|
|
|
|
// Стиль и цвет берём из первой заданной диагонали (например, DiagonalLeft)
|
|
|
|
|
|
var diagSide = (DiagonalLeft ?? DiagonalRight)!.Value;
|
|
|
|
|
|
border.DiagonalBorder = diagSide.ToBorderElement<DiagonalBorder>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return border;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Создаёт CellBorder из элемента Border Open XML.</summary>
|
|
|
|
|
|
public static CellBorder FromBorder(Border? border)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (border == null)
|
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
|
|
var result = new CellBorder
|
|
|
|
|
|
{
|
|
|
|
|
|
TopBorder = BorderSide.FromBorderProperties(border.TopBorder),
|
|
|
|
|
|
BottomBorder = BorderSide.FromBorderProperties(border.BottomBorder),
|
|
|
|
|
|
LeftBorder = BorderSide.FromBorderProperties(border.LeftBorder),
|
|
|
|
|
|
RightBorder = BorderSide.FromBorderProperties(border.RightBorder)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Диагональные границы
|
|
|
|
|
|
if (border.DiagonalDown?.Value == true)
|
|
|
|
|
|
result = result with { DiagonalLeft = BorderSide.FromBorderProperties(border.DiagonalBorder) };
|
|
|
|
|
|
if (border.DiagonalUp?.Value == true)
|
|
|
|
|
|
result = result with { DiagonalRight = BorderSide.FromBorderProperties(border.DiagonalBorder) };
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override bool Equals(object? obj) => obj is CellBorder other && Equals(other);
|
|
|
|
|
|
public bool Equals(CellBorder other) => this == other;
|
|
|
|
|
|
|
|
|
|
|
|
public static bool operator ==(CellBorder left, CellBorder right) =>
|
|
|
|
|
|
Equals(left.TopBorder, right.TopBorder) &&
|
|
|
|
|
|
Equals(left.BottomBorder, right.BottomBorder) &&
|
|
|
|
|
|
Equals(left.LeftBorder, right.LeftBorder) &&
|
|
|
|
|
|
Equals(left.RightBorder, right.RightBorder) &&
|
|
|
|
|
|
Equals(left.DiagonalLeft, right.DiagonalLeft) &&
|
|
|
|
|
|
Equals(left.DiagonalRight, right.DiagonalRight);
|
|
|
|
|
|
|
|
|
|
|
|
public static bool operator !=(CellBorder left, CellBorder right) => !(left == right);
|
|
|
|
|
|
|
|
|
|
|
|
public override int GetHashCode()
|
|
|
|
|
|
{
|
|
|
|
|
|
unchecked
|
|
|
|
|
|
{
|
|
|
|
|
|
int hash = 17;
|
|
|
|
|
|
hash = hash * 31 + (TopBorder?.GetHashCode() ?? 0);
|
|
|
|
|
|
hash = hash * 31 + (BottomBorder?.GetHashCode() ?? 0);
|
|
|
|
|
|
hash = hash * 31 + (LeftBorder?.GetHashCode() ?? 0);
|
|
|
|
|
|
hash = hash * 31 + (RightBorder?.GetHashCode() ?? 0);
|
|
|
|
|
|
hash = hash * 31 + (DiagonalLeft?.GetHashCode() ?? 0);
|
|
|
|
|
|
hash = hash * 31 + (DiagonalRight?.GetHashCode() ?? 0);
|
|
|
|
|
|
return hash;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Стиль и цвет границы.</summary>
|
|
|
|
|
|
public readonly struct BorderSide : IEquatable<BorderSide>
|
|
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
/// <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 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-05 15:58:03 +07:00
|
|
|
|
/// <summary>Стиль линии границы.</summary>
|
|
|
|
|
|
public BorderStyle? Style { get; init; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Цвет границы.</summary>
|
2026-06-19 15:06:40 +07:00
|
|
|
|
public System.Drawing.Color? Color { get; init; }
|
2026-06-05 15:58:03 +07:00
|
|
|
|
|
|
|
|
|
|
internal T ToBorderElement<T>() where T : BorderPropertiesType, new()
|
|
|
|
|
|
{
|
|
|
|
|
|
var element = new T();
|
|
|
|
|
|
if (Style.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
element.Style = Style.Value switch
|
|
|
|
|
|
{
|
|
|
|
|
|
BorderStyle.Thin => BorderStyleValues.Thin,
|
|
|
|
|
|
BorderStyle.Medium => BorderStyleValues.Medium,
|
|
|
|
|
|
BorderStyle.Dashed => BorderStyleValues.Dashed,
|
|
|
|
|
|
BorderStyle.Dotted => BorderStyleValues.Dotted,
|
|
|
|
|
|
BorderStyle.Thick => BorderStyleValues.Thick,
|
|
|
|
|
|
BorderStyle.Double => BorderStyleValues.Double,
|
|
|
|
|
|
BorderStyle.Hair => BorderStyleValues.Hair,
|
|
|
|
|
|
BorderStyle.MediumDashed => BorderStyleValues.MediumDashed,
|
|
|
|
|
|
BorderStyle.DashDot => BorderStyleValues.DashDot,
|
|
|
|
|
|
BorderStyle.MediumDashDot => BorderStyleValues.MediumDashDot,
|
|
|
|
|
|
BorderStyle.DashDotDot => BorderStyleValues.DashDotDot,
|
|
|
|
|
|
BorderStyle.MediumDashDotDot => BorderStyleValues.MediumDashDotDot,
|
|
|
|
|
|
BorderStyle.SlantDashDot => BorderStyleValues.SlantDashDot,
|
|
|
|
|
|
_ => throw new NotImplementedException(),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-06-19 15:06:40 +07:00
|
|
|
|
if (Color.HasValue)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
var c = Color.Value;
|
2026-06-05 15:58:03 +07:00
|
|
|
|
element.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" };
|
|
|
|
|
|
}
|
|
|
|
|
|
return element;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Создаёт BorderSide из элемента границы Open XML (TopBorder, BottomBorder, LeftBorder, RightBorder, DiagonalBorder).</summary>
|
|
|
|
|
|
public static BorderSide FromBorderProperties(BorderPropertiesType? borderElement)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (borderElement is null)
|
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
|
|
var result = new BorderSide();
|
|
|
|
|
|
if (borderElement.Style is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
result = result with { Style = MapBorderStyleFromExcel(borderElement.Style.Value) };
|
|
|
|
|
|
}
|
|
|
|
|
|
if (borderElement.Color?.Rgb?.Value is { } rgb && rgb.Length >= 6)
|
|
|
|
|
|
{
|
2026-06-19 15:06:40 +07:00
|
|
|
|
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))
|
|
|
|
|
|
};
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 15:06:40 +07:00
|
|
|
|
static BorderStyle MapBorderStyleFromExcel(BorderStyleValues value)
|
2026-06-05 15:58:03 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (value == BorderStyleValues.Thin)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.Thin;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.Medium)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.Medium;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.Dashed)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.Dashed;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.Dotted)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.Dotted;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.Thick)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.Thick;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.Double)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.Double;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.Hair)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.Hair;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.MediumDashed)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.MediumDashed;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.DashDot)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.DashDot;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.MediumDashDot)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.MediumDashDot;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.DashDotDot)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.DashDotDot;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.MediumDashDotDot)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.MediumDashDotDot;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (value == BorderStyleValues.SlantDashDot)
|
|
|
|
|
|
{
|
|
|
|
|
|
return BorderStyle.SlantDashDot;
|
|
|
|
|
|
}
|
|
|
|
|
|
else throw new NotSupportedException($"Unsupported border style: {value}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override bool Equals(object? obj) => obj is BorderSide other && Equals(other);
|
|
|
|
|
|
public bool Equals(BorderSide other) => this == other;
|
|
|
|
|
|
|
|
|
|
|
|
public static bool operator ==(BorderSide left, BorderSide right) =>
|
|
|
|
|
|
left.Style == right.Style && Equals(left.Color, right.Color);
|
|
|
|
|
|
|
|
|
|
|
|
public static bool operator !=(BorderSide left, BorderSide right) => !(left == right);
|
|
|
|
|
|
|
|
|
|
|
|
public override int GetHashCode()
|
|
|
|
|
|
{
|
|
|
|
|
|
unchecked
|
|
|
|
|
|
{
|
|
|
|
|
|
int hash = 17;
|
|
|
|
|
|
hash = hash * 31 + Style.GetHashCode();
|
|
|
|
|
|
hash = hash * 31 + (Color?.GetHashCode() ?? 0);
|
|
|
|
|
|
return hash;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Тип линии границы</summary>
|
|
|
|
|
|
public enum BorderStyle
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary> Тонкая линия </summary>
|
|
|
|
|
|
Thin,
|
|
|
|
|
|
/// <summary> Средняя линия </summary>
|
|
|
|
|
|
Medium,
|
|
|
|
|
|
/// <summary> Штриховая </summary>
|
|
|
|
|
|
Dashed,
|
|
|
|
|
|
/// <summary> Точечная </summary>
|
|
|
|
|
|
Dotted,
|
|
|
|
|
|
/// <summary> Толстая линия </summary>
|
|
|
|
|
|
Thick,
|
|
|
|
|
|
/// <summary> Двойная линия </summary>
|
|
|
|
|
|
Double,
|
|
|
|
|
|
/// <summary> Волосяная (очень тонкая) </summary>
|
|
|
|
|
|
Hair,
|
|
|
|
|
|
/// <summary> Средняя штриховая </summary>
|
|
|
|
|
|
MediumDashed,
|
|
|
|
|
|
/// <summary> Штрих-пунктирная </summary>
|
|
|
|
|
|
DashDot,
|
|
|
|
|
|
/// <summary> Средняя штрих-пунктирная </summary>
|
|
|
|
|
|
MediumDashDot,
|
|
|
|
|
|
/// <summary> Штрих-пунктир-пунктир </summary>
|
|
|
|
|
|
DashDotDot,
|
|
|
|
|
|
/// <summary> Средняя штрих-пунктир-пунктир </summary>
|
|
|
|
|
|
MediumDashDotDot,
|
|
|
|
|
|
/// <summary> Наклонная штрих-пунктирная (для диагональных) </summary>
|
|
|
|
|
|
SlantDashDot,
|
2026-06-19 15:06:40 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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
|
2026-06-05 15:58:03 +07:00
|
|
|
|
}
|