Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add freeze panes #626

Merged
merged 11 commits into from
Jul 11, 2024
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
Loading