How to keep table formatting when sorting table generated by PHPSpreadsheet? - excel

I have generated an Excel table using PHPSpreadsheet including the style and the autofilter:
The problem is when I sort the data by the second and third columns, the table formatting is gone. This is how it looks like compared if I use Table Style directly from Excel (using Home-> Format as Table):
Is there any way to keep the formatting when I sort the table generated from PHPSpreadsheet?
Relevant PHP Code:
for ($rowNumber = 0, $rowNumberMax = sizeof($rows); $rowNumber < $rowNumberMax; $rowNumber++) //rows (all data)
{
$columnNumber = 0; //1 = A
for ($i = 0, $j = sizeof($tableColumns); $i < $j; $i++) //loop through table header label
{
foreach ($rows[$rowNumber] as $rowKey => $rowValue) //loop through single row data
{
if($tableColumns[$i] == $rowKey)
{
$sheet->setCellValueByColumnAndRow($columnNumber + 1, ($rowNumber + 5), $rowValue);
$currentCell = Utilities::num2alpha($columnNumber) .''. ($rowNumber + 5);
$sheet->getStyle($currentCell)->getNumberFormat()->setFormatCode('#');
$sheet->getStyle($currentCell)->getAlignment()->setVertical(\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT);
if(($rowNumber+5) % 2 == 0)
{
//even row
$sheet->getStyle($currentCell)->getFill()->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)->getStartColor()->setARGB('ffd9e1f2');
}
else
{
//odd row
}
$columnNumber++;
break;
}
}
}
}
//set autofilter
$headerFirstCellPosition = 'A4';
$tableLastCellPosition = Utilities::num2alpha(sizeof($tableColumns) - 1) . '' . (sizeof($rows) + 4);
$sheet->setAutoFilter($headerFirstCellPosition . ':' . $tableLastCellPosition);

The problem is you were just applying formatting to the cells based on if the row was even or odd, but it wasn't actually replicating a table in Excel. You would find the same result in Excel if you just formatted every other row like you did with your PHP code, where the "table" format would get lost.
Somebody just recently implemented a first pass of the actual table feature in Excel: https://github.com/PHPOffice/PhpSpreadsheet/pull/2671
You need to be on PHPSpreadSheet version 1.23.0 in order to be able to use this.
Using that, you would have to modify your code but you can go to the Samples section in the code area and view how to implement it: https://github.com/PHPOffice/PhpSpreadsheet/tree/master/samples/Table
https://github.com/PHPOffice/PhpSpreadsheet/blob/master/samples/Table/01_Table.php
Here is the relevant code (I removed some of the lines and added additional comments from the 01_Table.php sample at the link provided).
Table styles can be found here: https://github.com/PHPOffice/PhpSpreadsheet/blob/master/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php
// Create Table
$table = new Table('A1:D17', 'Sales_Data');
// Create Table Style
$tableStyle = new TableStyle();
// this line is the style type you want, you can verify this in Excel by clicking the "Format as Table" button and then hovering over the style you like to get the name
$tableStyle->setTheme(TableStyle::TABLE_STYLE_MEDIUM2);
// this gives you the alternate row color; I suggest to use either this or columnStripes as both together do not look good
$tableStyle->setShowRowStripes(true);
// similar to the alternate row color but does it for columns; I suggest to use either this or rowStripes as both together do not look good; I personally set to false and only used the rowStripes
$tableStyle->setShowColumnStripes(true);
// this will bold everything in the first column; I personally set to false
$tableStyle->setShowFirstColumn(true);
// this will bold everything in the last column; I personally set to false
$tableStyle->setShowLastColumn(true);
$table->setStyle($tableStyle);
Also make sure that you include the following to be able to use these:
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
Implementing that into your code will then allow you to sort using the auto filters and keep the formatting like you are expecting.
There are a few caveats such as:
Note that PreCalculateFormulas needs to be disabled when saving spreadsheets containing tables with formulae (totals or column formulae).
Also, as I am actually currently working on doing this, it doesn't look like you can apply an autofilter and have a table at the same time at this point.
That does appear to be on the todo list though, as the first link I provided the contributor has "Filter expressions similar to AutoFilter."
Otherwise, that should get you what you want and aside from being able to auto filter prior to creating the Excel file, it has worked well in my small testing.
Edit to add:
I think you can actually simplify your code a bit by using the functionality of PHPSpreadsheet to create a a spreadsheet from an array.
Documentation from PHPSpreadsheet can be found here: https://phpspreadsheet.readthedocs.io/en/latest/topics/accessing-cells/#setting-a-range-of-cells-from-an-array
You'll need to change it so that the array that is holding the info starts with your headers, so I believe that would look similar to this for your code:
$rows = [
['header1', 'header2', 'header3', 'header4']
];
Then you can populate the $rows array with your data from the rows either with a loop or just a single declaration depending on what you are putting in there, but basically using the below to populate the array.
$rows[] = [
$field1Data,
$field2Data,
$field3Data,
$field4Data
];
After you do that, you can then generate the spreadsheet using the following:
$sheet->getActiveSheet()
->fromArray(
$rows, // the data to set
NULL, // array values with this value will not be set
'A1', // top left coordinate of the worksheet range where we want to set these values (default is A1)
true // adds 0 to cell instead of blank if a 0 is the value
);
After doing the above, you can then add the code to create the table I posted and then save the file and you should be good.
Also, if you are in a situation where you still need to use the autofilter (for instance if you want to pre-filter the file on one or more columns which at this point you can't use a table when doing), you can make the autofilter call a bit easier.
// determine the the number of rows in the active sheet
$highestRow = $spreadsheet->getActiveSheet()->getHighestRow();
// get the highest column letter
$highestColumn = $spreadsheet->getActiveSheet()->getHighestColumn();
// set autofilter range
$spreadsheet->getActiveSheet()->setAutoFilter('A1:'.$highestColumn.$highestRow);
I realize the additional edit goes beyond the question, but figured I'd point it out since there are some built-in methods that you could use to reduce some of your code.
-Matt

Related

How can I resize an Excel Table using Gembox.Spreadsheet?

I'm replacing Excel Table contents in an existing workbook with new contents from C# code using Gembox.Spreadsheet. Sometimes the data has more rows than the existing table, sometimes it has fewer. To resize the table my first attempt has been to incrementally add or remove rows. However, this can be slow if the difference in the number of rows is quite large. Here's the code:
var workbook = ExcelFile.Load("workbook.xlsx");
var table = workbook.Sheets["Sheet1"].Tables["Table1"];
var lastWrittenRowIndex = 0;
for(var rowIndex = 0; rowIndex < data.Count; rowIndex++)
{
// If the table isn't big enough for this new row, add it
if (rowIndex == table.Rows.Count) table.Rows.Add();
// … Snipped code to add information in 'data' into 'table' …
lastWrittenRowIndex = rowIndex;
}
// All data written, now wipe out any unused rows
while (lastWrittenRowIndex + 1 < table.Rows.Count)
{
table.Rows.RemoveAt(table.Rows.Count - 1);
}
Adding a profiler shows that by far the slowest operation is table.Rows.Add(). I haven't yet profiled a situation where I need to remove the rows, but I anticipate the same.
I know how large my data is before writing, so how can I prepare the table to be of the correct size in a smaller operation? There are formulae and pivot tables referencing the table and I don't want to break them.
Try again with this latest version that was just released (Full version: 45.0.35.1010):
https://www.gemboxsoftware.com/spreadsheet/downloads/BugFixes.htm
It has a Table.Rows.Add overload method that takes count.
There are also similar ones for Insert and RemoveAt as well, see the following help page:
https://www.gemboxsoftware.com/spreadsheet/help/html/Methods_T_GemBox_Spreadsheet_Tables_TableRowCollection.htm
Last just as an FYI, you can additionally also set the following:
workbook.AutomaticFormulaUpdate = false;
This should improve the performances as well.
Note, setting this property to false also improves the performances of all ExcelWorksheet.Rows and ExcelWorksheet.Columns insert and remove methods.

How can we include the cell formula while export to excel from .rdlc

In my rdlc report have following columns
SlNo, Item, Uom, Qty, Rate, Amount
Here the Amount field is a formula (Rate*Qty)
The report is working fine, and when i export to excel also displaying the values are correctly.
But my problem is, after export to excel, when i change the Qty or Rate columns in excel file the Amount is not get changed automatically, because the formula is missing in the excel cell.
How can we include the formula in Amount column while export to excel from .rdlc?
I'm afraid that this required behaviour isn't really possible by just using the rdlc rendering.
In my search I stumbled upon this same link that QHarr posted: https://social.msdn.microsoft.com/Forums/en-US/3ddf11bf-e10f-4a3e-bd6a-d666eacb5ce4/report-viewer-export-ms-report-data-to-excel-with-formula?forum=vsreportcontrols
I haven't tried the project that they're suggesting but this might possibly be your best solution if it works. Unfortunately I do not have the time to test it myself, so if you test this please share your results.
I thought of the following workaround that seems to work most of the times, but isn't really that reliable because the formula sometimes gets displayed as full-text instead of being calculated. But I guess this could be solved by editing the excel file just after being exported, and changing the cell properties of this column containing the formula or just triggering the calculate.
Using the built-in-field Globals!RenderFormat.Name you can determine the render mode, this way you can display the result correctly when the report is being rendered to something different than Excel. When you export to Excel, you could change the value of the cell to the actual formula.
To form the formula it's self you'll need to figure this out on your own, but the RowNumber(Scope as String) function can be of use here to determine the row number of your cells.
Here is a possible example for the expression value of your amount column
=IIF(Globals!RenderFormat.Name LIKE "EXCEL*", "=E" & Cstr(RowNumber("DataSet1")+2) & "*F" & Cstr(RowNumber("DataSet1")+2) ,Fields!Rate.Value * Fields!Qty.Value )
Now considering that this formula sometimes gets displayed as full-text, and you'll probably have to edit the file post-rendering. If it's too complicated to determine which row/column the cell is on, you could also do this post-rendering. But I believe that the above expression should be easy enough to use to get your desired result without having to do much after rendering.
Update: The following code could be used to force the calculation of the formula (post rendering)
var fpath = #"C:\MyReport.xlsx";
using (var fs = File.Create(fpath))
{
var lr = new LocalReport();
//Initializing your reporter
lr.ReportEmbeddedResource = "MyReport.rdlc";
//Rendering to excel
var fbytes = lr.Render("Excel");
fs.Write(fbytes, 0, fbytes.Length);
}
var xlApp = new Microsoft.Office.Interop.Excel.Application() { Visible = false };
var wb = xlApp.Workbooks.Open(fpath);
var ws = wb.Worksheets[1];
var range = ws.UsedRange;
foreach (var cell in range.Cells)
{
var cellv = cell.Text as string;
if (!string.IsNullOrWhiteSpace(cellv) && cellv.StartsWith("="))
{
cell.Formula = cellv;
}
}
wb.Save();
wb.Close(0);
xlApp.Quit();

Set $objValidation/Dropdown range from variable

I am attempting to have phpexcel set the range for $objValidation based off a variable so not to have null values in my dropdown. This was my code
$objValidation->setFormula1('Index!$A$5:$A'.'count(Index!$A$5:$A$200');
which resulted in additional blank/null values in my dropbox making it bigger than need be. what I would like to do is something like this
$sql_temp = "SELECT `tempID`,`serialNUM` FROM `temp_sensor_specs` WHERE `statusTYPE`='SPARE'";
$result_temp = mysqli_query($link, $sql_temp);
$row_temp = mysqli_fetch_all($result_temp,MYSQLI_NUM);
$objPHPExcel->getActiveSheet()->fromArray($row_temp,null,'A5');
$count_temp = count($row_temp) + 4;
$objValidation = $objPHPExcel->getActiveSheet()->getCell('B4')->getDataValidation();
$objValidation->setType(PHPExcel_Cell_DataValidation::TYPE_LIST);
$objValidation->setErrorStyle(PHPExcel_Cell_DataValidation::STYLE_INFORMATION);
$objValidation->setAllowBlank(true);
$objValidation->setShowDropDown(true);
$objValidation->setErrorTitle('Input error');
$objValidation->setError('Value is not in list');
$objValidation->setFormula1('Index!$A$5:$A$count_temp');
So that didn't work I've also tried it in several variations as such
$objValidation->setFormula1('Index!$A$5:$A'.'$count_temp');
$objValidation->setFormula1('Index!$A$5:$A'.count($row_temp) + 4);
$objValidation->setFormula1('Index!$A$5:$A'$count_temp);
I really feel I've used syntax incorrectly, but can't figure out how. I've done similar range setting in loops for( $i=4; $i<=15; $i++ ){
$objValidation = $objPHPExcel->getActiveSheet()->getCell('B'.$i)->getDataValidation(); but also don't think this needs to be looped it should be a simple count and set that value as the count return +4 (as my dropdown starts on cell row 5)
thanks in advance
So the proper syntax ended up being `$objValidation->setFormula1('Index!$A$5:$A'."$count_temp");

Disable Excel Error Checking through Coldfusion

I enforced the number having leading zero to the text .
It is working well but it is showing green icon on the corner on clicking it displays the error .
My question is that how i can stop or disable this button not to show on the excel sheet programmatically.
In excel sheet you can easily disable error checking but i want this through code
Any Help will be appreciated
Thanks!
Short answer:
The ability to suppress Excel's error checking on a cell level is supported in POI 3.14+. See below details. The other option is to leave the cells as numeric, but apply a mask with leading zeroes, ie: SpreadSheetFormatCell(sheet, {dataFormat="00000"}, row, column).
Longer answer:
After a bit of reading, it turns out the ability to suppress this error was added in POI 3.14. Unfortunately, CF11 is bundled with an older version, 3.9. However, you can access the newer functionality by loading POI 3.14 (or later) using this.javaSettings in your Application.cfc, along with a bit of java code.
Start by creating the spreadsheet as usual:
// format a few cells as text, then add values with leading zero
cfSheet = SpreadSheetNew("Test", true);
SpreadSheetFormatCell(cfSheet, {dataFormat="#"}, 1, 1);
SpreadSheetSetCellValue(cfSheet, "01234", 1, 1);
SpreadSheetFormatCell(cfSheet, {dataFormat="#"}, 1, 2);
SpreadSheetSetCellValue(cfSheet, "05678", 1, 2);
In order to manipulate the spreadsheet with the newer classes, it must be read into a new Workbook object using the POI WorkbookFactory. The factory supports loading from both a physical file OR a stream of bytes. (For brevity, I will use bytes).
// load workbook from byte array
bytes = SpreadsheetReadBinary( cfSheet );
stream = createObject("java", "java.io.ByteArrayInputStream").init(bytes);
factory = createObject("java", "org.apache.poi.ss.usermodel.WorkbookFactory");
wb = factory.create(stream);
// grab reference to our worksheet
jSheet = wb.getSheet("Test");
Next create a range object representing the cells where you want to suppress the "number as text" error. Then add the range to the list of ignored errors in the sheet.
// Create range of cells containing numbers as text
CellRange = createObject("java", "org.apache.poi.ss.util.CellRangeAddress");
rangeToModify = CellRange.valueOf("A1:A2");
// Flag the range to ignore "number as text" error
IgnoreTypes = createObject("java", "org.apache.poi.ss.usermodel.IgnoredErrorType");
jSheet.addIgnoredErrors(rangeToModify, [ IgnoreTypes.NUMBER_STORED_AS_TEXT] );
Finally, save the updated workbook to a file:
// Save to disk
fos = createObject("java", "java.io.FileOutputStream").init("c:/path/uniqueName.xlsx");
wb.write(fos);
fos.close();

OLE2 command for returning the number of columns in an Excel file

Do we have an OLE2 command that can be used in Oracle Forms to return the number of columns in my excel file?!
I want to open an excel file from Oracle Forms and go through all columns and then just some the colomns.
Thanks!
To accomplish this I use a trivial solution, I iterate through all the col until the first empty one in the first line. Of course my file have always have the first row's col filled.
When I have a non filled row I use a constant, manually defined...
I iterate through like this:
Variant ws = /*(set your worksheet here)*/;
int col = 1;
for (int col = 1; toString(ws.olePropertyGet("Cell", row, col).olePropertyGet("value")) != ""); ++col)
//do stuff ++count;
It is dirty, but I never found a better way to do this, and I will follow this question to find a new one.

Resources