I'm using Epplus for generate a xlsx file, all working well before that I added this code:
var address = new ExcelAddress("G2:G5");
var condition = ws.ConditionalFormatting.AddExpression(address);
condition.Style.Font.Color.Color = Color.Red;
condition.Formula = string.Format("IF(G{0} < 25, 1, 0", 1);
essentially I'm trying to apply a different color for each cell, based on the value contained in the cell.
The file is generated correctly, but, when I open it Excel say that the file is corrupted.
As you can see I used as address G2:G5, but I also need to know how can I add a range of column between G to Y, I've a number of rows variable so I don't know the exact number to specify.
Someone knows what's the problem? Thanks.
For starters, you need proper syntax. You are missing a closing brace at the end of your formula.
|
V
condition.Formula = string.Format("IF(G{0} < 25, 1, 0)", 1);
Related
I'm working wth .NET 4.7.2, Windowsform.
I have a datagridview and I manage to generate a powerpoint file pptx.
I made a first ppt slide and I'd like to add the datagridview content into the second ppt slide given that I need to have the option to change the data within the PPt slide.
Microsoft.Office.Interop.PowerPoint.Application pptApp = new Microsoft.Office.Interop.PowerPoint.Application();
pptApp.Visible = Microsoft.Office.Core.MsoTriState.msoTrue;
Microsoft.Office.Interop.PowerPoint.Slides slides;
Microsoft.Office.Interop.PowerPoint._Slide slide;
Microsoft.Office.Interop.PowerPoint._Slide slide2;
Microsoft.Office.Interop.PowerPoint.TextRange objText;
// Create File
Presentation pptPresentation = pptApp.Presentations.Add(Microsoft.Office.Core.MsoTriState.msoTrue);
CustomLayout customLayout = pptPresentation.SlideMaster.CustomLayouts[PpSlideLayout.ppLayoutText];
// new Slide
slides = pptPresentation.Slides;
slide = slides.AddSlide(1, customLayout);
slide2 = slides.AddSlide(1, customLayout);
// title
objText = slide.Shapes[1].TextFrame.TextRange;
objText.Text = "Bonds Screner Report";
objText.Font.Name = "Haboro Contrast Ext Light";
objText.Font.Size = 32;
Shape shape1 = slide.Shapes[2];
slide.Shapes.AddPicture("C:\\mylogo.png", Microsoft.Office.Core.MsoTriState.msoFalse, Microsoft.Office.Core.MsoTriState.msoTrue, shape1.Left, shape1.Top, shape1.Width, shape1.Height);
slide.NotesPage.Shapes[2].TextFrame.TextRange.Text = "Disclaimer";
dataGridViewBonds.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableAlwaysIncludeHeaderText;
dataGridViewBonds.SelectAll();
DataObject obj = dataGridViewBonds.GetClipboardContent();
Clipboard.SetDataObject(obj, true);
Shape shapegrid = slide2.Shapes[2];
I know I'm not so far by now but I miss smething. Any help would be appreciated !
I am familiar with Excel interop and have used it many times and most likely have become numb to the awkward ways in which interop works. Using PowerPoint interop can be very frustrating for numerous reasons, however, the biggest I feel is the lack of documentation and the differences between the different MS versions.
In addition, I looked for a third-party PowerPoint library and “Aspose” looked like the only option, unfortunately it is not a “free” option. I will assume there is a free third-party option and I just did not look in the right place… Or there may be a totally different way to do this possibly with XML. I am confident I am preaching to the choir.
Therefore, what I have been able to put together may work for you. For starters, looking at your current posted code, there is one part missing that you need to get the “copied” grid cells into the slide…
slide.Shapes.Paste();
This will paste the “copied” cells from the grid into an “unformatted” table into the slide. This will copy the “row header” if it is displayed in the grid in addition to the “new row” if the grids AllowUserToAddRows is set to true. If this “unformatted paste” works for you, then you are good to go.
If you prefer to have at least a minimally formatted table and ignore the row headers and last empty row… It may be easier to simply “create” a new Table in the slide with the size we want along with the correct number of rows and columns. Granted, this may be more work, however, using the paste is going require this anyway “IF” you want the table formatted.
The method (below) takes a power point _Slide and a DataGridView. The code “creates” a new Table in the slide based on the number of rows and columns in the given grid. With this approach, the table will be “formatted” using the default “Table Style” in the presentation. So, this may give you the formatting you want by simply “creating” the table as opposed to “pasting” the table.
I have tried to “apply” one of the existing “Table Styles” in power point, however, the examples I saw used something like…
table.ApplyStyle("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
Which uses a GUID id to identify “which” style to use. I am not sure why MS decided on this GUID approach… this is beyond me, and it worked for “some” styles but not all.
Also, more common-sense solutions that showed something like…
table.StylePreset = TableStylePreset.MediumStyle2Accent2;
Unfortunately using my 2019 version of Office PowerPoint, this property does not exist. I have abandoned further research on this as it appears to be version dependent. Very annoying!
Given this, it may be easier if we format the cells individually as we want. We will need to add the cells text from the grid into the individual cells anyway, so we could also format the individual cells at the same time. Again, I am confident there is a better way, however, I could not find one.
Below the InsertTableIntoSlide(_Slide slide, DataGridView dgv) method takes a slide and a grid as parameters. It will add a table to the slide with data from the given grid. A brief code trace is below.
First a check is made to get the number of total rows in the grid (not including the headers) totRows. If the grids AllowUsersToAddRows is true, then the total rows variable is decremented by 1 to ignore this new row. Next the number of columns in the grid is set to the variable totCols. The top left X and Y point is defined topLeftX and topLeftY to position the table in the slide along with the tables width and height.
ADDED NOTE: Using the AllowUserToAddRows property to determine the number of rows … may NOT work as described above and will “miss” the last row… “IF” AllowUserToAddRows is true (default) AND the grid is data bound to a data source that does NOT allow new rows to be added. In that case you do NOT want to decrement the totRows variable.
Next a “Table” “Shape” is added to the slide using the previous variables to define the base table dimensions. Next are two loops. The first loop adds the header cells to the first row in the table. Then a second loop to add the data from the cells in the grid… to the table cells in the slide.
The commented-out code is left as an example such that you want to do some specific formatting for the individual cells. This was not need in my case since the “default” table style was close to the formatting I wanted.
Also, a note that “ForeColor” is the “Back ground” color of the cell/shape. Strange!
I hope this helps and again, sympathize more about having to use PowerPoint interop… I could not.
private void InsertTableIntoSlide(_Slide slide, DataGridView dgv) {
try {
int totRows;
if (dgv.AllowUserToAddRows) {
totRows = dgv.Rows.Count - 1;
}
else {
totRows = dgv.Rows.Count;
}
int totCols = dgv.Columns.Count;
int topLeftX = 10;
int topLeftY = 10;
int width = 400;
int height = 100;
// add extra row for header row
Shape shape = slide.Shapes.AddTable(totRows + 1, totCols, topLeftX, topLeftY, width, height);
Table table = shape.Table;
for (int i = 0; i < dgv.Columns.Count; i++) {
table.Cell(1, i+1).Shape.TextFrame.TextRange.Text = dgv.Columns[i].HeaderText;
//table.Cell(1, i+1).Shape.Fill.ForeColor.RGB = ColorTranslator.ToOle(Color.Blue);
//table.Cell(1, i+1).Shape.TextFrame.TextRange.Font.Bold = Microsoft.Office.Core.MsoTriState.msoTrue;
//table.Cell(1, i+1).Shape.TextFrame.TextRange.Font.Color.RGB = ColorTranslator.ToOle(Color.White);
}
int curRow = 2;
for (int i = 0; i < totRows; i++) {
for (int j = 0; j < totCols; j++) {
if (dgv.Rows[i].Cells[j].Value != null) {
table.Cell(curRow, j + 1).Shape.TextFrame.TextRange.Text = dgv.Rows[i].Cells[j].Value.ToString();
//table.Cell(curRow, j + 1).Shape.Fill.ForeColor.RGB = ColorTranslator.ToOle(Color.LightGreen);
//table.Cell(curRow, j + 1).Shape.TextFrame.TextRange.Font.Bold = Microsoft.Office.Core.MsoTriState.msoTrue;
//table.Cell(curRow, j + 1).Shape.TextFrame.TextRange.Font.Color.RGB = ColorTranslator.ToOle(Color.Black);
}
}
curRow++;
}
}
catch (Exception ex) {
MessageBox.Show("Error: " + ex.Message);
}
}
I'm trying to create a spreadsheet where the first sheet ("Catalog") contains some pre-filled and some empty values in a column. I want the values to be in a drop down list that are restricted to values found in the second sheet ("Products").
I would expect that if I set the the Excel validation formula for cells "A1:A1048576" in the "Catalog" sheet to be a list validation of "Products!A1:A100" that every cell would only allow values from "Products!A1:A100". However, I'm finding that my formula gets incremented for every row in the "Catalog" sheet (i.e. In row 2 the formula becomes "Products!A2:A101", in row 3 the formula becomes "Products!A3:A102").
If version matters I'm using EPPlus.Core v1.5.4 from NuGet.
I'm not sure if this is a bug or if I'm going about applying my formula wrong?
I've already tried directly applying the validation to every cell in the column one cell at a time. I found that not only does it moderately increase the size of the resulting Excel file but more importantly it also exponentially increases the time taken to generate the Excel file. Even applying the validation one cell at a time on the first 2000 rows more than doubles the generation time.
ExcelPackage package = new ExcelPackage();
int catalogProductCount = 10;
int productCount = 100;
var catalogWorksheet = package.Workbook.Worksheets.Add($"Catalog");
for (int i = 1; i <= catalogProductCount; i++)
{
catalogWorksheet.Cells[i, 1].Value = $"Product {i}";
}
var productsWorksheet = package.Workbook.Worksheets.Add($"Products");
for (int i = 1; i <= productCount; i++)
{
productsWorksheet.Cells[i, 1].Value = $"Product {i}";
}
var productValidation = catalogWorksheet.DataValidations.AddListValidation($"A1:A1048576");
productValidation.ErrorStyle = ExcelDataValidationWarningStyle.stop;
productValidation.ErrorTitle = "An invalid product was entered";
productValidation.Error = "Select a product from the list";
productValidation.ShowErrorMessage = true;
productValidation.Formula.ExcelFormula = $"Products!A1:A{productCount}";
I guess I'm not that adept at Excel formulas.
Changing this line:
productValidation.Formula.ExcelFormula = $"Products!A1:A{productCount}";
to this:
productValidation.Formula.ExcelFormula = $"Products!$A$1:$A${productCount}";
stopped the auto increment issue. Hopefully this answer will save someone else some sanity as I wasted half a day on this issue myself.
What I do: populate & format an Excel file using a mix of Interop and ClosedXML.
First, the file is populated via Interop, then saved, closed, then I format the cells' RichText using ClosedXML.
Unfortunately, this formatting causes Excel to view my file as "corrupt" and needs to repair it.
This is the relevant part:
var workbook = new XLWorkbook(xlsPath);
var sheet = workbook.Worksheet("Error Log");
for (var rownum = 2; rownum <= 10000; rownum++)
{
var oldcell = sheet.Cell("C" + rownum);
var newcell = sheet.Cell("D" + rownum);
var oldtext = oldcell.GetFormattedString();
if(string.IsNullOrEmpty(oldtext.Trim()))
break;
XlHelper.ColorCellText(oldcell, "del", System.Drawing.Color.Red);
XlHelper.ColorCellText(newcell, "add", System.Drawing.Color.Green);
}
workbook.Save();
And the colouring method:
public static void ColorCellText(IXLCell cel, string tagName, System.Drawing.Color col)
{
var rex = new Regex("\\<g\\sid\\=[\\sa-z0-9\\.\\:\\=\\\"]+?\\>");
var txt = cel.GetFormattedString();
var mc = rex.Matches(txt);
var xlcol = XLColor.FromColor(col);
foreach (Match m in mc)
{
txt = txt.Replace(m.Value, "");
txt = txt.Replace("</g>", "");
}
var startTag = string.Format("[{0}]", tagName);
var endTag = string.Format("[/{0}]", tagName);
var crt = cel.RichText;
crt.ClearText();
while (txt.Contains(startTag) || txt.Contains(endTag))
{
var pos1 = txt.IndexOf(startTag);
if (pos1 == -1)
pos1 = 0;
var pos2 = txt.IndexOf(endTag);
if (pos2 == -1)
pos2 = txt.Length - 1;
var txtLen = pos2 - pos1 - 5;
crt.AddText(txt.Substring(0, pos1));
crt.AddText(txt.Substring(pos1 + 5, txtLen)).SetFontColor(xlcol);
txt = txt.Substring(pos2 + 6);
}
if (!string.IsNullOrEmpty(txt))
crt.AddText(txt);
}
Error in file myfile.xlsx
The following repairs were performed: _x000d__x000a__x000d__x000a_
Repaired records:
string properties of /xl/sharedStrings.xml-Part (strings)
I've been through all the xmls looking for clues. In the affected sheet, in comparison view of Productivity Tool, some blocks appear as inserted in the repaired file and deleted in the corrupt one, although nothing significant seemed changed - except for one thing: the style attribute of that cell. Here an example:
<x:c r="AA2" s="59">
<x:f>
(IFERROR(VLOOKUP(G2,Legende!$A$42:$B$45,2,FALSE),0))
</x:f>
</x:c>
I have checked the styles.xml for style 59, but there is none. In the repaired file, this style has been changed to 14, which in my styles.xml is listed as a number format.
Unfortunately, a global search/replace of these invalid style indexes did not resolve the issue.
Seeing the things going on here with corrupt indexes, renamed xmls, invalid named ranges etc., I took a different route: not to use interop at all, maybe the corruption was caused by Excel in the first place and the coloring was only the last straw.
Using ClosedXml only:
Wow. Just wow. This makes it even worse. I commented out the colouring part since without that, Interop produced a readable file without errors, so that's what I expect of ClosedXml too.
This is how I open the file and address the worksheet with ClosedXml:
var wb= new XLWorkbook(xlsPath);
var errors = wb.Worksheet("Error Log");
This is how I write the values into the file:
errors.Cell(zeile, 1).SetValue(fname);
With zeile being a simple int counter.
I then dare to set a column width:
errors.Column(2).Width = 50;
errors.Column(3).Width = 50;
errors.Column(4).Width = 50;
As well as setting some values in another sheet in exactly the same fashion before saving with validation.
wb.Save(true);
wb.Dispose();
Lo and behold: The validation throws errors:
Attribute 'name' should have unique value. Its current value 'Legende duplicates with others.
Attribute 'sheetId' should have unique value. Its current value '4' duplicates with others.
A couple more errors like attribute 'top' having invalid value '11.425781'.
Excel cannot open the file directly, must repair it. My Sheet "Legende" is now empty and the first sheet instead of third, and I get an additional fourth sheet "Restored_Table1" which contains my original "Legende" contents.
What the hell is going on with this file??
New attempt: re-create the Excel template from scratch - in LibreOffice.
I now think that the issue is entirely misleading. If I use the newly created file from LibreOffice, the validation causes a System.OutOfMemory exception due to too many validation errors. Opening in Excel requires repair, gives additional sheet and so forth.
Creating in LibreOffice, then opening in Excel, saving, then using that file as template produces a much better result albeit not perfect yet.
Since I copied parts over from the old Excel file into LO while creating the new file, I assume some corrupt remnant got copied over.
I cannot shake the feeling that this is the file itself after all and has nothing to do with how I edit it!
Will post updaate tomorrow.
OK. Stuff this.
I created a completely fresh file with LibreOffice, making sure not to copy over anything at all from the original file, and I ditched Interop in favour of ClosedXml.
=> This produced a corrupt file in which my first sheet was cleared and its contents move to a "Restored_Table1".
After I opened my fresh new template with Excel via Open/Repair and saved it, the resulting, uncoloured file was NOT corrupt.
=> Colouring it produces the "original" corruption, all sheets intact.
ClosedXml seems to be marginally slower than Interop but at this point I couldn't care less. I guess we will have to live with the "corrupt" message and just get on with it.
I hate xlsx.
Using Excel Interop, I can configure a sheet for printing with code like this:
_xlSheetPlatypus.PageSetup.PrintArea = "A1:" +
GetExcelTextColumnName(
_xlSheetPlatypus.UsedRange.Columns.Count) +
_xlSheetPlatypus.UsedRange.Rows.Count;
_xlSheetPlatypus.PageSetup.Orientation = Excel.XlPageOrientation.xlLandscape;
_xlSheetPlatypus.PageSetup.Zoom = false;
_xlSheetPlatypus.PageSetup.FitToPagesWide = 1;
_xlSheetPlatypus.PageSetup.FitToPagesTall = 100;
_xlSheetPlatypus.PageSetup.LeftMargin = _xlApp.Application.InchesToPoints(0.5);
_xlSheetPlatypus.PageSetup.RightMargin = _xlApp.Application.InchesToPoints(0.5);
_xlSheetPlatypus.PageSetup.TopMargin = _xlApp.Application.InchesToPoints(0.5);
_xlSheetPlatypus.PageSetup.BottomMargin = _xlApp.Application.InchesToPoints(0.5);
_xlSheetPlatypus.PageSetup.HeaderMargin = _xlApp.Application.InchesToPoints(0.5);
_xlSheetPlatypus.PageSetup.FooterMargin = _xlApp.Application.InchesToPoints(0.5);
_xlSheetPlatypus.PageSetup.PrintTitleRows = String.Format("${0}:${0}", CUSTOMER_HEADING_ROW);
I think I can pretty much emulate that with Spreadsheet Light with this code:
SLPageSettings ps = new SLPageSettings();
// PrintArea
// ???
// PrintTitleRows
ps.PrintHeadings = true;
ps.SetCenterHeaderText(String.Format("${0}:${0}", CUSTOMER_HEADING_ROW);
// Margins
ps.SetNarrowMargins();
ps.TopMargin = 0.5;
ps.BottomMargin = 0.5;
ps.LeftMargin = 0.5;
ps.RightMargin = 0.5;
ps.HeaderMargin = 0.5;
ps.FooterMargin = 0.5;
// Orientation
ps.Orientation = OrientationValues.Landscape;
// Zoom
//psByCust.ZoomScale = what should this be? Is not a boolean...
// FitToPagesWide
//psByCust.FitToWidth = ; "cannot be assigned to" so how can I set this?
// FitToPagesTall
//psByCust.FitToHeight = 100; "cannot be assigned to" so how can I set this?
I'm not sure about many of these, though, especially the replacement code for "PrintTitleRows" ("PrintHeadings" and "SetCenterHeaderText"), but one thing seems to be totally missing from Spreadsheet Light, namely "PrintArea".
Also, what should the "Zoom" value be? And what corresponds to "FitToPagesWide" and "FitToPagesTall"?
What is the analagous way to accomplish the same thing with Spreadsheet Light? Or does Spreadsheet Light just automatically determine the range to print based on non-empty cells?
I can help with some of these. First up, Print Area.
As Vincent says: Rule 1: Everything begins and ends with SLDocument
SLDocument myWorkbook = new SLDocument();
myWorkbook.SetPrintArea("A1", "E10");
// or
myWorkbook.SetPrintArea(1, 1, 10, 5);
Next: Fit to Pages:
SLPageSettings settings = new SLPageSettings();
settings.ScalePage(2, 3) // print 2 pages wide and 3 long
// There is no info on how to just scale in one dimension, I would use
// a definitely too big™ number in the field you don't care about
// Eg for fit all columns on one page:
settings.ScalePage(1, 99999);
Zoom is a view (not print) thing AFAIK and can be changed between 10 and 400 to set the Zoom percentage - equivalent to Ctrl-scrolling when viewing a spreadsheet.
PrintHeadings displays the row (1, 2, 3, ...) and column headings (A, B, C, ...) when you print, so you can see the value in say, D25 easier.
For printing titles, you use the Set<position>HeaderText in SLPageSettings:
settings.SetCenterHeaderText("My Workbook Title");
Similarly SetLeftHeaderText and SetRightHeaderText
I don't know how to set 'RowsToRepeatAtTop' and 'ColumnsToRepeatAtLeft' which would match Interop's PrintTitleRows.
ProTip: The chm help file that comes with SpreadsheetLight is very useful.
Hope someone can crack this issues with EPPLUS and Formulas.
I'm getting an invalid #REF! when I try to assign a formula to a cell, yet the last row seems to accept the formula without a problem and does the calculations right.
Here is what the logic looks like at the time of assignment of the Formula. I am referencing data from another sheet.
string formula1 = "";
string formula2 = "";
int uniqueTimeRow = 14;
if (uniqueTimes.Rows.Count != 0)
{
foreach (DataRow row in uniqueTimes.Rows)
{
if (row["ExecutionTime"].ToString() != "")
{
wsSummary.InsertRow(uniqueTimeRow, 1, uniqueTimeRow);
wsSummary.SetValue(uniqueTimeRow, 2, row["ExecutionTime"].ToString());
formula1 = "SUMIF(DataSummary[Strategy],$B" + uniqueTimeRow.ToString() + ",DataSummary[ExecQty])";
formula2 = "SUMIF(DataSummary[Strategy],$B" + uniqueTimeRow.ToString() + ",DataSummary[PrincipalAmount])";
wsSummary.Cells[uniqueTimeRow, 3].Formula = formula1;
wsSummary.Cells[uniqueTimeRow, 4].Formula = formula2;
uniqueTimeRow++;
}
}
}
This is what the result excel file looks like.
Table Produced in Excel with the invalid #REF!
This is the Formula Produced on the Last Cell:
=SUMIF(DataSummary[Strategy],$B28,DataSummary[ExecQty])
=SUMIF(DataSummary[Strategy],$B28,DataSummary[PrincipalAmount])
If I copy these two formulas upwards this is what is produced as expected:
=SUMIF(DataSummary[Strategy],$B27,DataSummary[ExecQty])
=SUMIF(DataSummary[Strategy],$B27,DataSummary[PrincipalAmount])
When it has the invalid #REF! this is what shows up in the formula:
=SUMIF(#REF!,$B27,#REF!)
Just hit the very same problem. I had it working and then it broke after refactoring. Before refactoring (by pure chance) I was copying rows upwards one at a time and this appears to be the only way it works properly.
To clarify you have to copy a row then paste it onto the row above. Now copy that row, and paste that onto the row above:
copy row 10, paste row 9, copy row 9, paste row 8... and so on...