Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README_V2.md
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,28 @@ exporter.Export("Book1.xlsx", dt, configuration: config);

![image](docs/images/freeze-pane-1.png)

#### 14. Per-cell formulas and formatting

Wrapping a value in `OpenXmlCellValue` sets a formula, number format or style for that one cell, overriding the column's settings:

```csharp
var rows = new[]
{
new Dictionary<string, object?>
{
["Input"] = 3,
["Share"] = new OpenXmlCellValue(0.25) { NumberFormat = "0.0%", Style = { FontColor = Color.Red } },
["Double"] = new OpenXmlCellValue(6) { Formula = "A2*2" }
}
};

var exporter = MiniExcelV2.Exporters.GetOpenXmlExporter();
exporter.Export("Book1.xlsx", rows);
```

The value given with a formula is written as its cached result, which Excel shows until it recalculates. Pass `null` to leave the result for Excel to calculate.
A cell with only a style keeps the number format it would otherwise have had.


### Fill Data To Excel Template <a name="docs-template" />

Expand Down
10 changes: 10 additions & 0 deletions src/MiniExcel.OpenXml/Constants/WorksheetXml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ internal static string Cell(string cellReference, string? cellType, string style
};
}

/// <summary>
/// A formula cell, with its cached result when <paramref name="cachedValue"/> is not null.
/// </summary>
internal static string FormulaCell(string cellReference, string? cellType, string styleIndex, string formula, string? cachedValue)
{
var type = cachedValue is null ? "" : $" t=\"{cellType}\"";
var value = cachedValue is null ? "" : $"<x:v>{XmlHelper.EncodeXml(cachedValue)}</x:v>";
return $"""<x:c r="{cellReference}"{type} s="{styleIndex}"><x:f>{XmlHelper.EncodeXml(formula)}</x:f>{value}</x:c>""";
}

internal static string Autofilter(string dimensionRef) => $"<x:autoFilter ref=\"{dimensionRef}\" />";
internal static string Drawing(int sheetIndex) => $"<x:drawing r:id=\"rDrawing{sheetIndex}\" />";
}
30 changes: 30 additions & 0 deletions src/MiniExcel.OpenXml/Models/OpenXmlCellValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using MiniExcelLib.OpenXml.Styles;

namespace MiniExcelLib.OpenXml.Models;

/// <summary>
/// A value to export together with a formula or style of its own, overriding the column's settings for that cell.
/// </summary>
/// <param name="value">The value written to the cell. When <see cref="Formula"/> is set, it is the cached result of the formula.</param>
public sealed class OpenXmlCellValue(object? value)
{
/// <summary>
/// The value written to the cell, or the cached result when <see cref="Formula"/> is set.
/// </summary>
public object? Value { get; } = value;

/// <summary>
/// A formula for the cell, without the leading equals sign.
/// </summary>
public string? Formula { get; set; }

/// <summary>
/// A number format for the cell, replacing the column's format.
/// </summary>
public string? NumberFormat { get; set; }

/// <summary>
/// The style of the cell, such as its font color.
/// </summary>
public OpenXmlCellStyle Style { get; } = new();
}
155 changes: 91 additions & 64 deletions src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ internal partial class DefaultSheetStyleBuilder(SheetStyleBuilderContext context

private readonly OpenXmlStyleOptions _styleOptions = styleOptions;

private string CellHorizontalAlignment => _styleOptions.HorizontalAlignment switch
{
HorizontalCellAlignment.Center => "center",
HorizontalCellAlignment.Right => "right",
_ => "general"
};

private string CellVerticalAlignment => _styleOptions.VerticalAlignment switch
{
VerticalCellAlignment.Top => "top",
VerticalCellAlignment.Center => "center",
_ => "bottom"
};

protected internal override SheetStyleElementInfos GetGeneratedElementInfos()
{
return GeneratedElementInfos;
Expand All @@ -28,20 +42,7 @@ protected internal override SheetStyleElementInfos GetGeneratedElementInfos()
[CreateSyncVersion]
protected override async Task GenerateNumFmtAsync()
{
const int numFmtIndex = 166;
var index = 0;
foreach (var map in Context.SheetStyleFormatsCache.FormatMappings)
{
index++;

/*
* <x:numFmt numFmtId="{numFmtIndex + i}" formatCode="{x.Format}"
*/
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "numFmt", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index + Context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "formatCode", null, map.Format).ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
}
await GenerateCustomNumFmtsAsync().ConfigureAwait(false);
}

[CreateSyncVersion]
Expand Down Expand Up @@ -100,6 +101,8 @@ protected override async Task GenerateFontAsync()
await NewWriter.WriteAttributeStringAsync(null, "val", null, "2").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);

await GenerateCustomFontsAsync().ConfigureAwait(false);
}

[CreateSyncVersion]
Expand Down Expand Up @@ -344,20 +347,9 @@ protected override async Task GenerateCellXfAsync()
_ => "bottom"
};

var cellHorizontalAlignment = _styleOptions.HorizontalAlignment switch
{
HorizontalCellAlignment.Center => "center",
HorizontalCellAlignment.Right => "right",
_ => "general"
};
var cellHorizontalAlignment = CellHorizontalAlignment;
var cellVerticalAlignment = CellVerticalAlignment;

var cellVerticalAlignment = _styleOptions.VerticalAlignment switch
{
VerticalCellAlignment.Top => "top",
VerticalCellAlignment.Center => "center",
_ => "bottom"
};

/* Empty style is required because Excel always considers the first one to be the default and ignores all its properties
* <x:xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" />
* */
Expand Down Expand Up @@ -529,42 +521,77 @@ protected override async Task GenerateCellXfAsync()
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);

const int numFmtIndex = 166;
for (var i = 1; i <= Context.CustomFormatCount; i++)
{
/*
* <x:xf numFmtId=""{numFmtIndex + i}"" fontId=""0"" fillId=""0"" borderId=""1"" xfId=""0"" applyNumberFormat=""1"" applyFill=""1"" applyBorder=""1"" applyAlignment=""1"" applyProtection=""1"">
* <x:alignment horizontal=""general"" vertical=""bottom"" textRotation=""0"" wrapText=""0"" indent=""0"" relativeIndent=""0"" justifyLastLine=""0"" shrinkToFit=""0"" readingOrder=""0"" />
* <x:protection locked=""1"" hidden=""0"" />
* </x:xf>
*/
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + i + Context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{Context.OldElementInfos.FontCount}").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{Context.OldElementInfos.FillCount}").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{Context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "relativeIndent", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "justifyLastLine", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "shrinkToFit", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "readingOrder", null, "0").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "protection", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "locked", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "hidden", null, "0").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
}
await GenerateCustomCellXfsAsync().ConfigureAwait(false);
}

[CreateSyncVersion]
protected override async Task GenerateCustomFontAsync(string argb)
{
/*
* <x:font>
* <x:vertAlign val="baseline" />
* <x:sz val="11" />
* <x:color rgb="{argb}" />
* <x:name val="Calibri" />
* <x:family val="2" />
* </x:font>
*/
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "font", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "vertAlign", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "val", null, "baseline").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "sz", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "val", null, "11").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "color", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "rgb", null, argb).ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "name", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "val", null, "Calibri").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "family", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "val", null, "2").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
}

[CreateSyncVersion]
protected override async Task GenerateCustomCellXfAsync(string numFmtId, string? fontId)
{
/*
* <x:xf numFmtId="{numFmtId}" fontId="{fontId}" fillId="0" borderId="1" xfId="0" applyNumberFormat="1" applyFill="1" applyBorder="1" applyAlignment="1" applyProtection="1">
* <x:alignment horizontal="general" vertical="bottom" textRotation="0" wrapText="0" indent="0" relativeIndent="0" justifyLastLine="0" shrinkToFit="0" readingOrder="0" />
* <x:protection locked="1" hidden="0" />
* </x:xf>
*/
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, numFmtId).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "fontId", null, fontId ?? $"{Context.OldElementInfos.FontCount}").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{Context.OldElementInfos.FillCount}").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{Context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false);
if (fontId is not null)
await NewWriter.WriteAttributeStringAsync(null, "applyFont", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, CellHorizontalAlignment).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "vertical", null, CellVerticalAlignment).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "relativeIndent", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "justifyLastLine", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "shrinkToFit", null, "0").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "readingOrder", null, "0").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteStartElementAsync(OldReader.Prefix, "protection", OldReader.NamespaceURI).ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "locked", null, "1").ConfigureAwait(false);
await NewWriter.WriteAttributeStringAsync(null, "hidden", null, "0").ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
await NewWriter.WriteEndElementAsync().ConfigureAwait(false);
}
}
Loading
Loading