Skip to content

Commit

Permalink
Add freeze panes (#626)
Browse files Browse the repository at this point in the history
* add template formula example images

* add template formula example to readme

* add template formula example to readme

* get basics working

* add config option

* add WritePanes method, remove FreezeTopRow, add FreezeRowCount and FreezeColumnCount options, add unit test

* add xml methods to WorksheetXml

* remove unused namespaces

* revert disable file delete

* add freeze panes feature to table and idatareader
  • Loading branch information
meld-cp authored Jul 11, 2024
1 parent 273fe97 commit 1cd491c
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 7 deletions.
25 changes: 25 additions & 0 deletions src/MiniExcel/OpenXml/Constants/WorksheetXml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,34 @@ internal class WorksheetXml
internal static string Dimension(string dimensionRef)
=> $"{StartDimension}{dimensionRef}\"/>";

internal const string StartSheetViews = "<x:sheetViews>";
internal const string EndSheetViews = "</x:sheetViews>";

internal static string StartSheetView( int tabSelected=1, int workbookViewId=0 )
=> $"<x:sheetView tabSelected=\"{tabSelected}\" workbookViewId=\"{workbookViewId}\">";
internal const string EndSheetView = "</x:sheetView>";

internal const string StartSheetData = "<x:sheetData>";
internal const string EndSheetData = "</x:sheetData>";

internal static string StartPane( int? xSplit, int? ySplit, string topLeftCell, string activePane, string state )
=> string.Concat(
"<x:pane",
xSplit.HasValue ? $" xSplit=\"{xSplit.Value}\"" : string.Empty,
ySplit.HasValue ? $" ySplit=\"{ySplit.Value}\"" : string.Empty,
$" topLeftCell=\"{topLeftCell}\"",
$" activePane=\"{activePane}\"",
$" state=\"{state}\"",
"/>");

internal static string PaneSelection( string pane, string activeCell, string sqref)
=> string.Concat(
$"<x:selection",
$" pane=\"{pane}\"",
string.IsNullOrWhiteSpace(activeCell) ? string.Empty : $" activeCell=\"{activeCell}\"",
string.IsNullOrWhiteSpace(sqref) ? string.Empty : $" sqref=\"{sqref}\"",
"/>");

internal static string StartRow(int rowIndex)
=> $"<x:row r=\"{rowIndex}\">";
internal const string EndRow = "</x:row>";
Expand Down
99 changes: 95 additions & 4 deletions src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
using MiniExcelLibs.Attributes;
using MiniExcelLibs.OpenXml.Constants;
using MiniExcelLibs.OpenXml.Constants;
using MiniExcelLibs.OpenXml.Models;
using MiniExcelLibs.Utils;
using MiniExcelLibs.Zip;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using static MiniExcelLibs.Utils.ImageHelper;

namespace MiniExcelLibs.OpenXml
{
Expand Down Expand Up @@ -136,6 +133,9 @@ private void GenerateSheetByIDataReader(MiniExcelStreamWriter writer, IDataReade
}
maxColumnIndex = props.Count;

//sheet view
WriteSheetViews(writer);

WriteColumnsWidths(writer, props);

writer.Write(WorksheetXml.StartSheetData);
Expand Down Expand Up @@ -260,6 +260,9 @@ private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable
writer.Write(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, maxColumnIndex)));
}

//sheet view
WriteSheetViews(writer);

//cols:width
WriteColumnsWidths(writer, props);

Expand Down Expand Up @@ -331,6 +334,9 @@ private void GenerateSheetByDataTable(MiniExcelStreamWriter writer, DataTable va
var prop = GetColumnInfosFromDynamicConfiguration(columnName);
props.Add(prop);
}

//sheet view
WriteSheetViews(writer);

WriteColumnsWidths(writer, props);

Expand Down Expand Up @@ -389,6 +395,91 @@ private static void WriteColumnsWidths(MiniExcelStreamWriter writer, IEnumerable
writer.Write(WorksheetXml.EndCols);
}

private void WriteSheetViews(MiniExcelStreamWriter writer) {
// exit early if no style to write
if (_configuration.FreezeRowCount <= 0 && _configuration.FreezeColumnCount <= 0)
{
return;
}

// start sheetViews
writer.Write(WorksheetXml.StartSheetViews);
writer.Write(WorksheetXml.StartSheetView());

// Write panes
WritePanes(writer);

// end sheetViews
writer.Write(WorksheetXml.EndSheetView);
writer.Write(WorksheetXml.EndSheetViews);
}

private void WritePanes(MiniExcelStreamWriter writer) {

string activePane;
if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0)
{
activePane = "bottomRight";
}
else if (_configuration.FreezeColumnCount > 0)
{
activePane = "topRight";
}
else
{
activePane = "bottomLeft";
}
writer.Write( WorksheetXml.StartPane(
xSplit: _configuration.FreezeColumnCount > 0 ? _configuration.FreezeColumnCount : (int?)null,
ySplit: _configuration.FreezeRowCount > 0 ? _configuration.FreezeRowCount : (int?)null,
topLeftCell: ExcelOpenXmlUtils.ConvertXyToCell(
_configuration.FreezeColumnCount + 1,
_configuration.FreezeRowCount + 1
),
activePane: activePane,
state: "frozen"
) );

// write pane selections
if (_configuration.FreezeColumnCount > 0 && _configuration.FreezeRowCount > 0)
{
// freeze row and column
/*
<selection pane="topRight" activeCell="B1" sqref="B1"/>
<selection pane="bottomLeft" activeCell="A3" sqref="A3"/>
<selection pane="bottomRight" activeCell="B3" sqref="B3"/>
*/
var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount+1, 1);
writer.Write(WorksheetXml.PaneSelection("topRight", cellTR, cellTR));

var cellBL = ExcelOpenXmlUtils.ConvertXyToCell(1, _configuration.FreezeRowCount+1);
writer.Write(WorksheetXml.PaneSelection("bottomLeft", cellBL, cellBL));

var cellBR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount+1, _configuration.FreezeRowCount+1);
writer.Write(WorksheetXml.PaneSelection("bottomRight", cellBR, cellBR));
}
else if ( _configuration.FreezeColumnCount > 0 )
{
// freeze column
/*
<selection pane="topRight" activeCell="A1" sqref="A1"/>
*/
var cellTR = ExcelOpenXmlUtils.ConvertXyToCell(_configuration.FreezeColumnCount, 1);
writer.Write(WorksheetXml.PaneSelection("topRight", cellTR, cellTR));

}
else
{
// freeze row
/*
<selection pane="bottomLeft"/>
*/
writer.Write(WorksheetXml.PaneSelection("bottomLeft", null, null));

}

}

private static void PrintHeader(MiniExcelStreamWriter writer, List<ExcelColumnInfo> props)
{
var xIndex = 1;
Expand Down
2 changes: 2 additions & 0 deletions src/MiniExcel/OpenXml/OpenXmlConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class OpenXmlConfiguration : Configuration
public bool FillMergedCells { get; set; }
public TableStyles TableStyles { get; set; } = TableStyles.Default;
public bool AutoFilter { get; set; } = true;
public int FreezeRowCount { get; set; } = 1;
public int FreezeColumnCount { get; set; } = 0;
public bool EnableConvertByteArray { get; set; } = true;
public bool IgnoreTemplateParameterMissing { get; set; } = true;
public bool EnableWriteNullValueCell { get; set; } = true;
Expand Down
67 changes: 64 additions & 3 deletions tests/MiniExcelTests/MiniExcelOpenXmlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ public void SaveAsControlChracter()
string path = GetTempXlsxPath();
char[] chars = new char[] {'\u0000','\u0001','\u0002','\u0003','\u0004','\u0005','\u0006','\u0007','\u0008',
'\u0009', //<HT>
'\u000A', //<LF>
'\u000B','\u000C',
'\u000A', //<LF>
'\u000B','\u000C',
'\u000D', //<CR>
'\u000E','\u000F','\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016',
'\u000E','\u000F','\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016',
'\u0017','\u0018','\u0019','\u001A','\u001B','\u001C','\u001D','\u001E','\u001F','\u007F'
};
var input = chars.Select(s => new { Test = s.ToString() });
Expand Down Expand Up @@ -823,6 +823,67 @@ public void SaveAsByIEnumerableIDictionary()
}
}

[Fact()]
public void SaveAsFrozenRowsAndColumnsTest() {

var config = new OpenXmlConfiguration
{
FreezeRowCount = 1,
FreezeColumnCount = 2
};

{
// Test enumerable
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
MiniExcel.SaveAs(
path,
new[] {
new { Column1 = "MiniExcel", Column2 = 1 },
new { Column1 = "Github", Column2 = 2}
},
configuration: config
);

using (var stream = File.OpenRead(path)) {
var rows = stream.Query(useHeaderRow: true).ToList();

Assert.Equal("MiniExcel", rows[0].Column1);
Assert.Equal(1, rows[0].Column2);
Assert.Equal("Github", rows[1].Column1);
Assert.Equal(2, rows[1].Column2);
}

Assert.Equal("A1:B3", Helpers.GetFirstSheetDimensionRefValue(path));
//File.Delete(path);
}

{
// test table
var table = new DataTable();
{
table.Columns.Add("a", typeof(string));
table.Columns.Add("b", typeof(decimal));
table.Columns.Add("c", typeof(bool));
table.Columns.Add("d", typeof(DateTime));
table.Rows.Add("some text", 1234567890, true, DateTime.Now);
table.Rows.Add(@"<test>Hello World</test>", -1234567890, false, DateTime.Now.Date);
}
var pathTable = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");
MiniExcel.SaveAs(pathTable, table, configuration: config );

Assert.Equal("A1:D3", Helpers.GetFirstSheetDimensionRefValue(pathTable));


// data reader
var reader = table.CreateDataReader();
var pathReader = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx");

MiniExcel.SaveAs(pathReader, reader, configuration: config);
Assert.Equal("A1:D3", Helpers.GetFirstSheetDimensionRefValue(pathTable));
}

}

[Fact()]
public void SaveAsByDapperRows()
{
Expand Down

0 comments on commit 1cd491c

Please sign in to comment.