ClosedXML read styles when column and row is overlapping - c#

I want read the styles of this worksheet from Excel via ClosedXML. So on column "E" there is a Fill -> BackgroundColor (blue) and on row "15" there is a Fill -> BackgroundColor (purple). How can I read via ClosedXML which property is now the overlapping one (or which property was set last) so I can set the styles for "E15" correctly?
I was looking for some kind of attribute which indicates that the row "15" was set last and is now the "top layer" but I had no luck so far.
Has someone experienced a similar problem?

string fileName = "d:\\test.xlsx";
using FileStream fs = File.OpenRead(fileName);
using XLWorkbook workbook = new XLWorkbook(fs);
IXLWorksheet worksheet = workbook.Worksheets.Worksheet("Test");
IXLCell cell = worksheet.Cell(5, "E");
Console.WriteLine($"5E value:{cell.Value} color:{cell.Style.Fill.BackgroundColor}");
cell = worksheet.Cell(6, "E");
Console.WriteLine($"6E value:{cell.Value} color:{cell.Style.Fill.BackgroundColor}");
Poperty BackgroundColor contains actual color
screenshot of my test excel
Also you can try to get all styles of worksheet.
((ClosedXML.Excel.XLWorksheet)worksheet).Styles
But XLWorksheet internal class and you get access to it only using reflection and this is not good practice.

Related

How to read the state of a checkbox in an excel file with EPPlus ( C# )

The title already explains my problem pretty well. I have an excel file which contains checkboxes and I would like to read their state (checked or not) using the EPPlus library.
I am not sure if this is even supported. So far I have found no documentation or examples for that specific problem using EPPlus.
If you add a Cell link then pulling the value is straight forward. I don't believe that the Drawing Object contains the value.
using System.Linq;
using OfficeOpenXml;
using OfficeOpenXml.Drawing;
namespace EPPlus {
public void Run() {
var excelFile = new System.IO.FileInfo(System.IO.Path.Combine(BaseDirectory, "Excel", "Checkbox.xlsx"));
using (ExcelPackage excel = new ExcelPackage(excelFile))
{
ExcelWorksheet sheet = excel.Workbook.Worksheets.SingleOrDefault(a => a.Name == "Sheet1");
ExcelDrawing checkbox2 = sheet.Drawings.SingleOrDefault(a => a.Name == "Check Box 2");
var value = sheet.Cells["G5"].Value.ToString();
}
}
}
}
For existing excel, just designate a cell somewhere and link it to the checkbox. Insert the true/false value directly to that cell (NOT TO THE CHECKBOX). The checkbox will automatically reflect the value of the cell in the checkbox.
You can put all designated cells in a certain column, then hide that column. :)

How do I insert Excel cells without creating a corrupt file?

I'm using the OpenXML SDK to update the contents of an Excel spreadsheet. When inserting cells into an Excel row they must be inserted in the correct order or the file will not open properly in Excel. I'm using the following code to find the first cell that will be after the cell I am inserting. This code comes almost directly from the OpenXML SDK documentation
public static Cell GetFirstFollowingCell(Row row, string newCellReference)
{
Cell refCell = null;
foreach (Cell cell in row.Elements<Cell>())
{
if (string.Compare(cell.CellReference.Value, newCellReference, true) > 0)
{
refCell = cell;
break;
}
}
return refCell;
}
When I edit files with this code and then open them in Excel, Excel reports that the file is corrupted. Excel is able to repair the file, but most of the data is removed from the workbook. Why does this result in file corruption?
Side note: I tried two different .NET Excel libraries before turning to the painfully low-level OpenXML SDK. NPOI created spreadsheets with corruption and EPPlus threw an exception whenever I tried to save. I was using the most recent version of each.
The code you are using is seriously flawed. This is very unfortunate, seeing as it comes from the documentation. It may work acceptably for spreadsheets that only use the first 26 columns but will fail miserably when confronted with "wider" spreadsheets. The first 26 columns are named alphabetically, A-Z. Columns 27-52 are named AA-AZ. Column 53-78 are named BA-BZ. (You should notice the pattern.)
Cell "AA1" should come after all cells with a single character column name (i.e. "A1" - "Z1"). Let's examine the current code comparing cell "AA1" with cell "B1".
string.Compare("B1", "AA1", true) returns the value 1
The code interprets this to mean that "AA1" should be placed before cell "B1".
The calling code will insert "AA1" before "B1" in the XML.
At this point the cells will be out of order and the Excel file is corrupted. Clearly, string.Compare by itself is not a sufficient test to determine the proper order of cells in a row. A more sophisticated comparison is required.
public static bool IsNewCellAfterCurrentCell(string currentCellReference, string newCellReference)
{
var columnNameRegex = new Regex("[A-Za-z]+");
var currentCellColumn = columnNameRegex.Match(currentCellReference).Value;
var newCellColumn = columnNameRegex.Match(newCellReference).Value;
var currentCellColumnLength = currentCellColumn.Length;
var newCellColumnLength = newCellColumn.Length;
if (currentCellColumnLength == newCellColumnLength)
{
var comparisonValue = string.Compare(currentCellColumn, newCellColumn, StringComparison.OrdinalIgnoreCase);
return comparisonValue > 0;
}
return currentCellColumnLength < newCellColumnLength;
}
If you wanted to place a new cell in column "BC" and you were comparing to cell "D5" you would use IsCellAfterColumn("D5", "BC5"). Substituting the new comparison function into the original code and simplifying with LINQ:
public static Cell GetFirstFollowingCell(Row row, string newCellReference)
{
var rowCells = row.Elements<Cell>();
return rowCells.FirstOrDefault(c => IsNewCellAfterCurrentCell(c.CellReference.Value, newCellReference));
}

Read Excel Cell Format

I'm working on this program that will read the data in excel file and put it into our database. The program is written in Visual Studio 2010 using C#, and I'm using the NPOI library.
In the past, I was able to read the spreadsheet row by row and cell by cell to get the data, but the new format of the excel file will not allow me to do this easily. (The excel is given by another user, so I can't really make big changes to it).
There are several "tables" in one sheet (using borders and headers for each column name), and I will need to get data mainly from the tables but sometimes outside the tables too.
I was wondering if I were to read the spreadsheet row by row (which is what I'm a bit for familiar with), is there a way I can tell that I have reached a table? Is there a way I can read the "format" of the cell?
What I mean is, for example, "this cell has borders around it so starting this row is a table." or "the text in this cell is bold, so this row is the header row for this new table."
In the past I was only able to read the "text" for the spreadsheet and not the format/style. I've been searching on the internet and I can only find how to set the style for output excel but not how to read the format from input.
Any help is appreciated, thanks!
It would be better to have the various tables in your source workbook defined as named ranges with known names. Then you can get the associated area like this -
using System.IO;
using System.Windows;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
// ...
using (var file = new FileStream(workbookLocation, FileMode.Open, FileAccess.Read))
{
var workbook = new XSSFWorkbook(file);
var nameInfo = workbook.GetName("TheTable");
var tableRange = nameInfo.RefersToFormula;
// Do stuff with the table
}
If you have no control over the source spreadsheet and cannot define the tables as named ranges, you can read the cell formats as you suggest. Here is an example of reading the TopBorder style -
using (var file = new FileStream(workbookLocation, FileMode.Open, FileAccess.Read))
{
var workbook = new XSSFWorkbook(file);
var sheet = workbook.GetSheetAt(0);
for (int rowNo = 0; rowNo <= sheet.LastRowNum; rowNo++)
{
var row = sheet.GetRow(rowNo);
if (row == null) // null is when the row only contains empty cells
continue;
for (int cellNo = 0; cellNo <= row.LastCellNum; cellNo++)
{
var cell = row.GetCell(cellNo);
if (cell == null) // null is when the cell is empty
continue;
var topBorderStyle = cell.CellStyle.BorderTop;
if (topBorderStyle != BorderStyle.None)
{
MessageBox.Show(string.Format("Cell row: {0} column: {1} has TopBorder: {2}", cell.Row.RowNum, cell.ColumnIndex, topBorderStyle));
}
}
}
}

How to set cells' background?

How to set the background of several cells within a row (or of a whole row) in OpenXml?
Having read several articles:
Coloring cells in excel sheet using openXML in C#
Advanced styling in Excel Open XML
I still cannot make it work.
My task is actually at first glance seems to be somewhat easier and a little bit different from what is written in those articles. The mentioned tutorials predominantly show how to create a new document and style it. While I need to change the styling of the existing one.
That is, I have an existing xlsx document (a report template). I populate the report with the necessary values (managed to do it thanks to SO open xml excel read cell value and MSDN Working with sheets (Open XML SDK)). But next I need to mark several rows with, say, red background.
I am neither sure whether to use CellStyle nor if I should use CellFormat or something else...This is what I have got up to now:
SpreadsheetDocument doc = SpreadsheetDocument.Open("ole.xlsx", true);
Sheet sheet = (Sheet)doc.WorkbookPart
.Workbook
.Sheets
.FirstOrDefault();
WorksheetPart worksheetPart = (WorksheetPart)doc.WorkbookPart
.GetPartById(sheet.Id);
Worksheet worksheet = worksheetPart.Worksheet;
CellStyle cs = new CellStyle();
cs.Name = StringValue.FromString("Normal");
cs.FormatId = 0;
cs.BuiltinId = 0;
//where are the style values?
WorkbookStylesPart wbsp = doc.WorkbookPart
.GetPartsOfType<WorkbookStylesPart>()
.FirstOrDefault();
wbsp.Stylesheet.CellStyles.Append(cs);
wbsp.Stylesheet.Save();
Cell cell = GetCell(worksheet, "A", 20);
cell.StyleIndex = 1U;//get the new cellstyle index somehow
doc.Close();
Actually I would greatly appreciate a more light-weight and easy example of how to style, say, cell A20 or range from A20 to J20. Or probably a link to some more consecutive tutorial.
In the end I changed my mind to use cell background and used fonts. Thanks to answer by foson in SO Creating Excel document with OpenXml sdk 2.0 I managed to add a new Font and a new CellFormat, having preserved the original cell's formatting (i.e. having changed the font color only):
SpreadsheetDocument doc = SpreadsheetDocument.Open("1.xlsx", true);
Sheet sheet = (Sheet)doc.WorkbookPart.Workbook.Sheets.FirstOrDefault();
WorksheetPart worksheetPart = (WorksheetPart)doc.WorkbookPart
.GetPartById(sheet.Id);
Worksheet worksheet = worksheetPart.Worksheet;
WorkbookStylesPart styles = doc.WorkbookPart.WorkbookStylesPart;
Stylesheet stylesheet = styles.Stylesheet;
CellFormats cellformats = stylesheet.CellFormats;
Fonts fonts = stylesheet.Fonts;
UInt32 fontIndex = fonts.Count;
UInt32 formatIndex = cellformats.Count;
Cell cell = GetCell(worksheet, "A", 19);
cell.CellValue = new CellValue(DateTime.Now.ToLongTimeString());
cell.DataType = new EnumValue<CellValues>(CellValues.String);
CellFormat f = (CellFormat)cellformats.ElementAt((int)cell.StyleIndex.Value);
var font = (Font)fonts.ElementAt((int)f.FontId.Value);
var newfont = (Font)font.Clone();
newfont.Color = new Color() { Rgb = new HexBinaryValue("ff0000") };
fonts.Append(newfont);
CellFormat newformat = (CellFormat)f.Clone();
newformat.FontId = fontIndex;
cellformats.Append(newformat);
stylesheet.Save();
cell.StyleIndex = formatIndex;
doc.Close();
You have 3 options:
Use MS lib ExcelDataReader which requires your server installing Office and usually does not work if your program is running in IIS.
Use closed source libs.
Use OpenXML.
Try my code using pure OpenXML:
https://stackoverflow.com/a/59806422/6782249
cSimple solution:
Try this: (it works with the nuget package ClosedXML v 0.95.4)
using ClosedXML.Excel;
XLWorkbook wb = new XLWorkbook();
IXLWorksheet ws = wb.Worksheets.Add("Test Background Color");
ws.Cell("A1").Style.Fill.BackgroundColor = XLColor.LightBlue;
ws.Cell("A1").Value = "This cell should have light blue background";
wb.SaveAs(#"c:\Test\test.xlsx");

How can I get back a previously created Excel ListObject?

I am creating a ListObject in Excel using VSTO as follows:
ListObject lo = ws_vsto.Controls.AddListObject(range, "MyList");
(The range variable is a previously defined range.)
If I then loop through the worksheet Controls collection I can find that ListObject.
However, if I save the workbook and reopen it, the Controls collection is empty. How can I get this ListObject back after re-opening so that I can continue working with it?
EDIT
I've got a bit further:
var wb = Globals.ThisAddIn.Application.ActiveWorkbook;
var wb_vsto = wb.GetVstoObject();
foreach (Excel.Worksheet ws in wb.Worksheets)
{
var wsv = ws.GetVstoObject();
foreach (Excel.ListObject l in ws.ListObjects)
{
MessageBox.Show(l.Name);
var lo = wsv.Controls.AddListObject(l);
Excel.Range range = lo.Range;
range.Activate();
}
}
When I get to the var lo = line, I have a ListObject added to the Controls collection and available for use. However, it's DataSource property holds null. Is there an easy way to get the original data source back?
I then thought about rebuilding the data source from the information in the range. The range.Activate() line selects the list in Excel (so I know it has the right thing). However, I can't for the life of me work out how to get the data out of that range and get the address of the range. The MSDN documentation talks about the Address property but this doesn't appear to actually exist. (MSDN documentation for VSTO seems ropey at best).
I made the following changes to the initial code. You need to get a VSTO Listobject back from the Factory.
var wb = Globals.ThisAddIn.Application.ActiveWorkbook;
var wb_vsto = wb.GetVstoObject();
foreach (Excel.Worksheet ws in wb.Worksheets)
{
var wsv = ws.GetVstoObject();
foreach (Excel.ListObject l in ws.ListObjects)
{
MessageBox.Show(l.Name);
//var lo = wsv.Controls.AddListObject(l);
Microsoft.Office.Tools.Excel.ListObject lo =
Globals.Factory.GetVstoObject(l);
// I can now get at the datasource if neede
var ds = lo.DataSource;
// In my case the datasource was DataTable.
DataTable t = (DataTable)d;
if (t.Rows.Count > 0)
{
foreach (DataRow r in t.Rows)
{
// Access row data.
}
}
//Excel.Range range = lo.Range;
//range.Activate();
}
}
Have you tried?
Microsoft.Office.Tools.Excel.ListObject lo= Microsoft.Office.Tools.Excel.ListObject.GetVstoObject(l) (it's C# I'm not sure in VB)
From MSDN GetVstoObject, be sure to read the remarks.
About your first question,
you created the listOject with the code
ListObject lo = ws_vsto.Controls.AddListObject(range, "MyList");
To recover the object after save/reope the workbook, try this line of code:
ListObject lo = Globals.Factory.GetVstoObject(Worksheet.ListObjects["MyList"]);
You simply can't get back a ListObject control that you created dynmically during run-time. this is from the MSDN documentation.
By default, dynamically created list objects are not persisted in the
worksheet as host controls when the worksheet is closed.
The only way I can get back my ListObject is to create them directly in the sheets of my template during design-time.

Categories

Resources