EPPLUS AutoFit cells - c#

how can i set autosize on my cells, based on the max length of one input.
using (rng = workSheet.Cells["A1:G1"])
{
rng.Style.Font.Bold = true;
rng.Style.Fill.PatternType = ExcelFillStyle.Solid;
rng.Style.Fill.BackgroundColor.SetColor(Color.DarkBlue);
rng.Style.Font.Color.SetColor(Color.White);
}
using (ExcelRange col = workSheet.Cells[2, 6, 7, 7])
{
col.Style.Numberformat.Format = "yyyy-mm-dd HH:mm";
col.Style.HorizontalAlignment = ExcelHorizontalAlignment.Right;
}
for (int i = 1; i <= a; i++)
{
workSheet.Cells["A1"].Value = "RU_ID";
workSheet.Cells["B1"].Value = "COR_REQ_ID";
workSheet.Cells["C1"].Value = "RU_NAME";
workSheet.Cells["D1"].Value = "PARENT_RU_NAME";
workSheet.Cells["E1"].Value = "ADJUSTMENT_STATE";
workSheet.Cells["F1"].Value = "COR_START";
workSheet.Cells["G1"].Value = "COR_END";
}
...
rng.AutoFitColumns();
string path = #"D:\excel\test.xlsx";
Stream stream = File.Create(path);
excel.SaveAs(stream);
stream.Close();
byte[] data = File.ReadAllBytes(path);
}
The only thing that AutoFitColumn is doing is to bring the cell to the size of the header, as if i have the header as "STH" and the inputs as "Something good", "something to increase cell size" than AutoFitColumn will set the size based on "STH" not "something to increase cell size".
Thanks in advance for the help.

Look at your lines:
using (rng = workSheet.Cells["A1:G1"])
...
rng.AutoFitColumns();
Notice you are call AutoFitColumns on the range of of your headers A1:G1 so EPPlus is using only those cells to determine the width of the columns.
Just do this instead:
workSheet.Cells.AutoFitColumns();
since Cells in Epplus only contain cells with actual values so there is no real concern over efficiency.

Related

OfficeOpenXML excel Resize rows with cells merged across them

Text in a merged cell across rows is cut off because Excel ignores merged cells when automatically resizing a row.
How can I resize cell height to its contents for rows that resize doesn't work for?
I am posting this out there to help others that had this problem. I posted a solution as an answer.
For this solution, I am only showing code related to resizing rows not displaying data
Solution:
///
/// Merged Row Model Psuedo Code
///
MergedRow {
// Fields merged across rows
...
// list of rows with non merged data
list<indvidulerow> data
}
///
/// Code to find rows with merged cells.
///
... // worksheet prep
var mergedRows = new Dictionary<int, MergedRow>();
... // begin generating rows
...
// code generating the first merged cell of a row
using (var r = sheet.Cells[rowIndex, 1, rowIndex+ mergedRow.Data.Count - 1, 1]){
if(mergedRow.Data.count>1){
r.Merge=true;
mergedRows.Add(rowIndex, mergedRow);
}
... // code for remainder of of cell
///
/// Code to Resize Rows. Making last row expand enough to see
/// all content in merged cells
///
foreach (var firstRowIndex in mergedRows.Keys){
var numRowsMerged = mergedRows[firstRowIndex].Data.Count;
var columnHeights = new List<ColumnHeightCalcModel>();
for (var colIndex = 0; colIndex < totalColumns; colIndex++){
var calcModel = new ColumnHeightCalcModel();
var combinedHeight = 0.0;
var lastRowHeight = 0.0;
for (rowIndex = 0; rowIndex < numRowsMerged; rowIndex++){
var cell = sheet.Cells[firstRowIndex + rowIndex, colIndex + 1];
var cellText = cell.Text;
var cellmerged = cell.Merge;
var cellWidth = sheet.Column(colIndex+1).Width;
var font = cell.Style.Font;
if (string.IsNullOrEmpty(cellText)) combinedHeight += 0.0;
var bitmap =new Bitmap(1,1);
var graphics = Graphics.FromImage(bitmap);
var pixelWidth = Convert.ToInt32(cellWidth * 6.0); //6.0 pixels per excel column width
var drawingFont = new Font(font.Name, font.Size);
var size = graphics.MeasureString(cellText, drawingFont, pixelWidth);
//72 DPI and 96 points per inch. Excel height in points with max of 409 per Excel requirements.
combinedHeight += Math.Min(Convert.ToDouble(size.Height) * 72 / 96, 409);
if (cellmerged) break;
lastRowHeight = Math.Min(Convert.ToDouble(size.Height) * 72 / 96, 409);
}
calcModel.TotalCombinedHeight = combinedHeight;
calcModel.LastRowHeight = lastRowHeight;
columnHeights.Add(calcModel);
}
var row = sheet.Row(firstRowIndex + numRowsMerged - 1);
row.CustomHeight = true;
var maxCombo = columnHeights.Max(col => col.TotalCombinedHeight);
var maxLast = columnHeights.Max(col => col.LastRowHeight);
row.Height = maxCombo - maxLast;
}
End Solution
Links that helped me find this solution
The following link was for Merged columns but it helped with merged rows.
Autofit Row Height of Merged Cell in EPPlus

Apply format to many excel cells at once

I would like to format all added values in my excel file and i have a "small" and "fast" solution like this:
Item2 is a List<string>, Item3 is a List<List<string>>
if (chkWithValues.Checked && results.Item3.Any())
{
var rows = results.Item3.Count;
var cols = results.Item3.Max(x => x.Count);
object[,] values = new object[rows, cols];
object[,] format = new object[rows, cols];
//All returned items are inserted into the Excel file
//Item2 contains the database types, Item3 the Values
// pgMain shows the progress for the selected tables
for (int j = 0; j < results.Item3.Count(); j++)
{
int tmpNbr = 1;
foreach (string value in results.Item3[j])
{
values[j, tmpNbr - 1] = Converter.Convert(results.Item2[tmpNbr - 1], value).ToString().Replace("'", "");
format[j, tmpNbr - 1] = ExcelColumnTypes.ConvertToExcelTypes(results.Item2[tmpNbr - 1]);
tmpNbr++;
}
pgMain.Maximum = results.Item3.Count();
pgMain.PerformStep();
}
Excel.Range range = xlWorksheet.Range["A3", GetExcelColumnName(cols) + (rows + 2)];
range.Value = values;
range.NumberFormat = format;
}
To add the numberformat efficiently with a single assignment, I've found a solution with a 2d array, which contains all number formats that should be set.
The problem is that I get the following error message "Unable to set the NumberFormat property of the Range class" when I have more than (i think) 50.000 cells to format.
Does someone know a solution that is fast and can handle a large amount of cells without error?
update:
ExcelColumnTypes.ConvertToExcelTypes
public static string ConvertToExcelTypes(string databaseType)
{
if (DatabaseColumnTypes.DOUBLE.Contains(databaseType))
return DOUBLEPO1;
if (DatabaseColumnTypes.DATE.Contains(databaseType))
return DATE2;
if (DatabaseColumnTypes.INTEGER.Contains(databaseType))
return INT;
return TEXT;
}
The DatabaseColumnTypes are List with const or direct const.
Sample:
public const string VARBINARY = "varbinary";
public static List<string> STRING = new List<string>()
{
CHAR,
VARCHAR,
TEXT,
NTEXT,
NCHAR,
NVARCHAR,
BINARY,
VARBINARY
};
Please change the
range.NumberFormat = format;
range.NumberFormatLocal = format;
Then it should work

Get Formatted Cell Values efficiently

I would like to be able to efficiently retrieve a multi-dimensional array of formatted cell values from Excel. When I say formatted values, I mean I would like to get them exactly as they appear in Excel with all the cell NumberFormat applied.
The Range.Value and Range.Value2 properties work great for retrieving the cell values of a large number of cells into a multi-dimensional array. But those are the actual cell values (well at least with Range.Value2 is, I'm not quite sure what Range.Value is doing with respect to some of the values).
If I want to retrieve the actual text that is displayed in the cells, I can use the Range.Text property. This has some caveats. First, you need to AutoFit the cells or else you may get something like #### if not all the text is visible with the current cell width. Secondly, Range.Text does not work for more than one cell at a time so you would have to loop through all of the cells in the range and this can be extremely slow for large data sets.
The other method that I tried is to copy the range into the clipboard and then parse the clipboard text as a tab-separated data stream and transfer it into a multi-dimensional array. This seems to work great, although it is slower than getting Range.Value2, it is much faster for large datasets than getting Range.Text. However, I don't like the idea of using the system clipboard. If this was a really long operation that takes 60 seconds and while that operation is running, the user may decide to switch to another application and would be very unhappy to find that their clipboard either doesn't work or has mysterious data in it.
Is there a way that I can retrieve the formatted cell values to a multi-dimensional array efficiently?
I have added some sample code that is run from a couple ribbon buttons in a VSTO app. The first set some good test values and number formats and the second button will display what they look like when retrieved using one of these methods in a MessageBox.
The sample output on my system is(It could be different on yours due to Regional Settings):
Output using Range.Value
1/25/2008 3:19:32 PM 5.12345
2008-01-25 15:19:32 0.456
Output using Range.Value2
39472.6385648148 5.12345
2008-01-25 15:19:32 0.456
Output using Clipboard Copy
1/25/2008 15:19 5.12
2008-01-25 15:19:32 45.60%
Output using Range.Text and Autofit
1/25/2008 15:19 5.12
2008-01-25 15:19:32 45.60%
The Range.Text and Clipboard methods produce the correct output, but as explained above they both have problems: Range.Text is slow and Clipboard is bad practice.
private void SetSampleValues()
{
var sheet = (Microsoft.Office.Interop.Excel.Worksheet) Globals.ThisAddIn.Application.ActiveSheet;
sheet.Cells.ClearContents();
sheet.Cells.ClearFormats();
var range = sheet.Range["A1"];
range.NumberFormat = "General";
range.Value2 = "2008-01-25 15:19:32";
range = sheet.Range["A2"];
range.NumberFormat = "#";
range.Value2 = "2008-01-25 15:19:32";
range = sheet.Range["B1"];
range.NumberFormat = "0.00";
range.Value2 = "5.12345";
range = sheet.Range["B2"];
range.NumberFormat = "0.00%";
range.Value2 = ".456";
}
private string ArrayToString(ref object[,] vals)
{
int dim1Start = vals.GetLowerBound(0); //Excel Interop will return index-1 based arrays instead of index-0 based
int dim1End = vals.GetUpperBound(0);
int dim2Start = vals.GetLowerBound(1);
int dim2End = vals.GetUpperBound(1);
var sb = new StringBuilder();
for (int i = dim1Start; i <= dim1End; i++)
{
for (int j = dim2Start; j <= dim2End; j++)
{
sb.Append(vals[i, j]);
if (j != dim2End)
sb.Append("\t");
}
sb.Append("\n");
}
return sb.ToString();
}
private void GetCellValues()
{
var sheet = (Microsoft.Office.Interop.Excel.Worksheet)Globals.ThisAddIn.Application.ActiveSheet;
var usedRange = sheet.UsedRange;
var sb = new StringBuilder();
sb.Append("Output using Range.Value\n");
var vals = (object [,]) usedRange.Value; //1-based array
sb.Append(ArrayToString(ref vals));
sb.Append("\nOutput using Range.Value2\n");
vals = (object[,])usedRange.Value2; //1-based array
sb.Append(ArrayToString(ref vals));
sb.Append("\nOutput using Clipboard Copy\n");
string previousClipboardText = Clipboard.GetText();
usedRange.Copy();
string clipboardText = Clipboard.GetText();
Clipboard.SetText(previousClipboardText);
vals = new object[usedRange.Rows.Count, usedRange.Columns.Count]; //0-based array
ParseClipboard(clipboardText,ref vals);
sb.Append(ArrayToString(ref vals));
sb.Append("\nOutput using Range.Text and Autofit\n");
//if you dont autofit, Range.Text may give you something like #####
usedRange.Columns.AutoFit();
usedRange.Rows.AutoFit();
vals = new object[usedRange.Rows.Count, usedRange.Columns.Count];
int startRow = usedRange.Row;
int endRow = usedRange.Row + usedRange.Rows.Count - 1;
int startCol = usedRange.Column;
int endCol = usedRange.Column + usedRange.Columns.Count - 1;
for (int r = startRow; r <= endRow; r++)
{
for (int c = startCol; c <= endCol; c++)
{
vals[r - startRow, c - startCol] = sheet.Cells[r, c].Text;
}
}
sb.Append(ArrayToString(ref vals));
MessageBox.Show(sb.ToString());
}
//requires reference to Microsoft.VisualBasic to get TextFieldParser
private void ParseClipboard(string text, ref object[,] vals)
{
using (var tabReader = new TextFieldParser(new StringReader(text)))
{
tabReader.SetDelimiters("\t");
tabReader.HasFieldsEnclosedInQuotes = true;
int row = 0;
while (!tabReader.EndOfData)
{
var fields = tabReader.ReadFields();
for (int i = 0; i < fields.Length; i++)
vals[row, i] = fields[i];
row++;
}
}
}
private void button1_Click(object sender, RibbonControlEventArgs e)
{
SetSampleValues();
}
private void button2_Click(object sender, RibbonControlEventArgs e)
{
GetCellValues();
}
I've found a partial solution. Apply the NumberFormat value to the parsed double of Value2. This only works for single cells as returning an array for NumberFormat with different formats in the array returns System.DBNull.
double.Parse(o.Value2.ToString()).ToString(o.NumberFormat.ToString())
The dates don't work with this though. If you know which columns contains certain things, like a formatted date, you can use DateTime.FromOADate on the double and then value.ToString(format) with the NumberFormat. The code below gets close but is not complete.
<snip>
sb.Append("\nOutput using Range.Value2\n");
vals = (object[,])usedRange.Value2; //1-based array
var format = GetFormat(usedRange);
sb.Append(ArrayToString(ref vals, format));
</snip>
private static object[,] GetFormat(Microsoft.Office.Interop.Excel.Range range)
{
var rows = range.Rows.Count;
var cols = range.Columns.Count;
object[,] vals = new object[rows, cols];
for (int r = 1; r <= rows; ++r)
{
for (int c = 1; c <= cols; ++c)
{
vals[r-1, c-1] = range[r, c].NumberFormat;
}
}
return vals;
}
private static string ArrayToString(ref object[,] vals, object[,] numberformat = null)
{
int dim1Start = vals.GetLowerBound(0); //Excel Interop will return index-1 based arrays instead of index-0 based
int dim1End = vals.GetUpperBound(0);
int dim2Start = vals.GetLowerBound(1);
int dim2End = vals.GetUpperBound(1);
var sb = new StringBuilder();
for (int i = dim1Start; i <= dim1End; i++)
{
for (int j = dim2Start; j <= dim2End; j++)
{
if (numberformat != null)
{
var format = numberformat[i-1, j-1].ToString();
double v;
if (double.TryParse(vals[i, j].ToString(), out v))
{
if (format.Contains(#"/") || format.Contains(":"))
{// parse a date
var date = DateTime.FromOADate(v);
sb.Append(date.ToString(format));
}
else
{
sb.Append(v.ToString(format));
}
}
else
{
sb.Append(vals[i, j].ToString());
}
}
else
{
sb.Append(vals[i, j]);
}
if (j != dim2End)
sb.Append("\t");
}
sb.Append("\n");
}
return sb.ToString();
}
One solution to your problem is to use:
Range(XYZ).Value(11) = Range(ABC).Value(11)
As described here, this will:
Returns the recordset representation of the specified Range object in an XML format.
Assuming that your excel is configured in OpenXML format, this will copy the value/formula AND the formatting of range ABC and inject it into range XYZ.
Additionally, this answer explains the difference between Value and Value2.
.Value2 gives you the underlying value of the cell (could be empty, string, error, number (double) or boolean)
.Value gives you the same as .Value2 except if the cell was formatted as currency or date it gives you a VBA currency (which may truncate decimal places) or VBA date.

ABCPDF Calculation of header size and position.

The problem is a header file, which I have to include on each page of the pdf file generated by abcpdf.
The header file contains more than one image file and several lines of text, which varies from case to case.
The problem is that I do not know how to calculate the size of the header. I need to have its size to allocate the rectangle positions to put the rest of html file on each page together with header. I am using C#.
First off you need to create your document with enough space at the top to allow a header to be added. The settings below are for a normal A4 document with a header of about 1/5 of the page. Remember the coordinates on a PDF are from the bottom right not the top left..
//Setting to create the document using ABCPdf 8
var theDoc = new Doc();
theDoc.MediaBox.String = "A4";
theDoc.HtmlOptions.PageCacheEnabled = false;
theDoc.HtmlOptions.ImageQuality = 101;
theDoc.Rect.Width = 719;
theDoc.Rect.Height = 590;
theDoc.Rect.Position(2, 70);
theDoc.HtmlOptions.Engine = EngineType.Gecko;
The code below out puts a header across each page on the document, with a header image then a colored box under the image with some custom text in.
The header image in this case is 1710 x 381 to keep the resolution of the image as high as possible to stop it looking fuzzy when printed.
private static Doc AddHeader(Doc theDoc)
{
int theCount = theDoc.PageCount;
int i = 0;
//Image header
for (i = 1; i <= theCount; i++)
{
theDoc.Rect.Width = 590;
theDoc.Rect.Height = 140;
theDoc.Rect.Position(0, 706);
theDoc.PageNumber = i;
string imagefilePath = HttpContext.Current.Server.MapPath("/images/pdf/pdf-header.png");
Bitmap myBmp = (Bitmap)Bitmap.FromFile(imagefilePath);
theDoc.AddImage(myBmp);
}
//Blue header box
for (i = 2; i <= theCount; i++)
{
theDoc.Rect.String = "20 15 590 50";
theDoc.Rect.Position(13, 672);
System.Drawing.Color c = System.Drawing.ColorTranslator.FromHtml("#468DCB");
theDoc.Color.Color = c;
theDoc.PageNumber = i;
theDoc.FillRect();
}
//Blue header text
for (i = 2; i <= theCount; i++)
{
theDoc.Rect.String = "20 15 586 50";
theDoc.Rect.Position(25, 660);
System.Drawing.Color cText = System.Drawing.ColorTranslator.FromHtml("#ffffff");
theDoc.Color.Color = cText;
string theFont = "Century Gothic";
theDoc.Font = theDoc.AddFont(theFont);
theDoc.FontSize = 14;
theDoc.PageNumber = i;
theDoc.AddText("Your Text Here");
}
return theDoc;
}

How do I autofit a column that has merged cells?

In C#, for an excel spreadsheet, how do I autofit a column that has merged cells? I tried doing this with the code below, but it does not autofit the column.
Here is my code so far:
Worksheet xlSheet
xlSheet.Cells[2, 1] = "Autobiographies and Titles, Autobiographies and Titles, Autobiographies
and Titles, Autobiographies and Titles, Autobiographies and Titles, Autobiographies and Titles,
Autobiographies and Titles, Autobiographies and Titles";
Range hRangeSubsystemName = xlSheet.get_Range("A2", "G2");
hRangeSubsystemName.MergeCells = true;
hRangeSubsystemName.EntireColumn.AutoFit();
You can't use autofit on columns with merged cells in Excel.
See MS support article:
http://support.microsoft.com/kb/212010
I wrote a function to do this with Gembox which you might find helpful
private int AutoSizeMergedCells(CellRange myMergedCells, string text)
{
var file = new ExcelFile();
file.Worksheets.Add("AutoSize");
var ws = file.Worksheets[0];
ws.Cells[0, 0].Column.Width = myMergedCells.Sum(x => x.Column.Width);
ws.Cells[0, 0].Value = text;
ws.Cells[0, 0].Style.WrapText = true;
ws.Cells[0, 0].Row.AutoFit();
var result = ws.Cells[0, 0].Row.Height;
file = null;
return result;
}
Perhaps you can convert this into C#, but I found a VB macro here that will simulate the autofit of any merged cells on the active sheet. Source credits parry from MrExcel.com
Sub AutoFitMergedCellRowHeight()
Dim CurrentRowHeight As Single, MergedCellRgWidth As Single
Dim CurrCell As Range
Dim ActiveCellWidth As Single, PossNewRowHeight As Single
Dim StartCell As Range, c As Range, MergeRng As Range, Cell As Range
Dim a() As String, isect As Range, i
'Take a note of current active cell
Set StartCell = ActiveCell
'Create an array of merged cell addresses that have wrapped text
For Each c In ActiveSheet.UsedRange
If c.MergeCells Then
With c.MergeArea
If .Rows.Count = 1 And .WrapText = True Then
If MergeRng Is Nothing Then
Set MergeRng = c.MergeArea
ReDim a(0)
a(0) = c.MergeArea.Address
Else
Set isect = Intersect(c, MergeRng)
If isect Is Nothing Then
Set MergeRng = Union(MergeRng, c.MergeArea)
ReDim Preserve a(UBound(a) + 1)
a(UBound(a)) = c.MergeArea.Address
End If
End If
End If
End With
End If
Next c
Application.ScreenUpdating = False
'Loop thru merged cells
For i = 0 To UBound(a)
Range(a(i)).Select
With ActiveCell.MergeArea
If .Rows.Count = 1 And .WrapText = True Then
'Application.ScreenUpdating = False
CurrentRowHeight = .RowHeight
ActiveCellWidth = ActiveCell.ColumnWidth
For Each CurrCell In Selection
MergedCellRgWidth = CurrCell.ColumnWidth + MergedCellRgWidth
Next
.MergeCells = False
.Cells(1).ColumnWidth = MergedCellRgWidth
.EntireRow.AutoFit
PossNewRowHeight = .RowHeight
.Cells(1).ColumnWidth = ActiveCellWidth
.MergeCells = True
.RowHeight = IIf(CurrentRowHeight > PossNewRowHeight, _
CurrentRowHeight, PossNewRowHeight)
End If
End With
MergedCellRgWidth = 0
Next i
StartCell.Select
Application.ScreenUpdating = True
'Clean up
Set CurrCell = Nothing
Set StartCell = Nothing
Set c = Nothing
Set MergeRng = Nothing
Set Cell = Nothing
End Sub
please try this
private double AutoSizeMergedCells( string text)
{
Excel.Worksheet ws = xlWorkBook.Sheets[1];
`enter code here`ws.Cells[14, 10].ColumnWidth = 9.29+7.43+10.71+11.29;(size width range)
ws.Cells[14, 10].Value = text;
ws.Cells[14, 10].Style.WrapText = true;
ws.Cells[14, 10].Rows.AutoFit();
var result = ws.Cells[14, 10].RowHeight;
ws.Cells[14, 10].Value = "";
return result;
}
I had the same issue. I've made workaround. After filling Worksheet with data, I use my ExtensionMethod mySheet.AutoFitRowsWithMergedCells();
I'm iterating all data in worksheet. When I find merged cell I copy it's sizes and value into not merged temporary cell before last used column in same row, executes AutoFit(), witch now works correctly and removes temp cell.
public static void AutoFitRowsWithMergedCells(this Worksheet worksheet)
{
var range = worksheet.UsedRange;
int rowsCount = range.Rows.Count;
int columnsCount = range.Columns.Count;
for (int rowIndex = 1; rowIndex <= rowsCount; rowIndex++)
{
for (int columnIndex = 1; columnIndex <= columnsCount; columnIndex++)
{
var subRange = range.GetSubRange(rowIndex, columnIndex);
if ((bool)subRange.MergeCells && subRange.Value != null)
{
var mergedArea = subRange.MergeArea;
double mergedColumnsWidth = 0;
foreach (dynamic column in mergedArea.Columns)
{
mergedColumnsWidth += column.ColumnWidth;
}
var tempRange = worksheet.Cells[rowIndex, columnsCount + 1];
tempRange.Value = subRange.Value;
tempRange.Style.WrapText = true;
double originalTempColumnWidth = tempRange.Columns[1].ColumnWidth;
tempRange.Columns[1].ColumnWidth = mergedColumnsWidth;
subRange.EntireRow.AutoFit();
double correctHeight = subRange.EntireRow.RowHeight;
tempRange.Value = string.Empty;
tempRange.Columns[1].ColumnWidth = originalTempColumnWidth;
subRange.EntireRow.RowHeight = correctHeight;
}
}
}
}

Categories