Add Columns to Existing Excel 2007 workbook using Open Xml - c#

I have a predefined Excel workbook with all sheets in place and I need to write content to it. I succesfully write to cells.
The problem is in a particular worksheet that i need to add three columns to it. In the code bellow, first i'm grabbing the Worksheet and then i proceed to add columns. This code runs fine, i mean, no exception is thrown, but then I get an error when I try to open the Excel file, stating that there are some content that cannot be read and all the content of this particular worksheet is cleared.
I know that the problem is with this operation because if I comment out those lines that add columns, the workbook opens just fine with all the cells values I write from code in place.
This is the relevant code, for testing purpose I'm trying to add 3 columns:
using (SpreadsheetDocument document = SpreadsheetDocument.Open(outputPath, true)){
Sheet sheet2 = document.WorkbookPart.Workbook.Descendants<Sheet>().Single( s => s.Name == "Miscellaneous Credit" );
Worksheet workSheet2 = ( (WorksheetPart)document.WorkbookPart.GetPartById( sheet2.Id ) ).Worksheet;
Columns cs = new Columns();
for ( var y = 1; y <= 3; y++ ) {
Column c = new Column()
{
Min = (UInt32Value)1U,
Max = (UInt32Value)1U,
Width = 44.33203125D,
CustomWidth = true
};
cs.Append( c );
}
workSheet2.Append( cs );
}
EDIT : As per Chris's explanation about columns's concept
using (SpreadsheetDocument document = SpreadsheetDocument.Open(outputPath, true)){
Sheet sheet2 = document.WorkbookPart.Workbook.Descendants<Sheet>().Single( s => s.Name == "Miscellaneous Credit" );
Worksheet workSheet2 = ( (WorksheetPart)document.WorkbookPart.GetPartById( sheet2.Id ) ).Worksheet;
// Check if the column collection exists
Columns cs = workSheet2.Elements<Columns>().FirstOrDefault();
if ( ( cs == null ) ) {
// If Columns appended to worksheet after sheetdata Excel will throw an error.
SheetData sd = workSheet2.Elements<SheetData>().FirstOrDefault();
if ( ( sd != null ) ) {
cs = workSheet2.InsertBefore( new Columns(), sd );
}
else {
cs = new Columns();
workSheet2.Append( cs );
}
}
//create a column object to define the width of columns 1 to 3
Column c = new Column
{
Min = (UInt32Value)1U,
Max = (UInt32Value)3U,
Width = 44.33203125,
CustomWidth = true
};
cs.Append( c );
}

This first part of answer deals about how to set columns width (based on the initial sample code, I was thinking that you wanted only define the width of the columns).
First, it seems you misunderstood what are Min and Max properties of the Column object. They represent respectively First and Last column affected by this 'column info' record. So if you have a set of contiguous columns with the same width, you can set that width using one Column class. In your snippet you define 3 times the width of the same column (Index 1).
Then, you presume Columns collection doesn't exist yet...
And finally, the main point is that if the Columns collection is appended after SheetData, Excel will throw error.
Final code that work for me (Open XML SDK 2.0)
using (SpreadsheetDocument document = SpreadsheetDocument.Open(outputPath, true)) {
Sheet sheet2 = document.WorkbookPart.Workbook.Descendants<Sheet>().Single(s => s.Name == "Your sheet name");
Worksheet workSheet2 = ((WorksheetPart)document.WorkbookPart.GetPartById(sheet2.Id)).Worksheet;
// Check if the column collection exists
Columns cs = workSheet2.Elements<Columns>().FirstOrDefault();
if ((cs == null)) {
// If Columns appended to worksheet after sheetdata Excel will throw an error.
SheetData sd = workSheet2.Elements<SheetData>().FirstOrDefault();
if ((sd != null)) {
cs = workSheet2.InsertBefore(new Columns(), sd);
} else {
cs = new Columns();
workSheet2.Append(cs);
}
}
//create a column object to define the width of columns 1 to 3
Column c = new Column {
Min = (UInt32Value)1U,
Max = (UInt32Value)3U,
Width = 44.33203125,
CustomWidth = true
};
cs.Append(c);
}
I'm still confused on how to perform column insert. Says I have
columns A, B and C, I want to insert three columns between B and C,
ending up with columns A,B,C,D,E,F. How can i achieve it?
The Columns object in OpenXml SDK is here to store styles and width informations for the columns. Inserting a Column in the collection won't "insert" a column in the sheet.
"Inserting" a column like you mean is a very large and complex task with OpenXmlSDK.
From my understanding of the problem, it means you will have to find all cells and shift them by changing their reference (ex. a cell with ref "B1" would become "F1" after inserting 3 columns, etc ...). And it means you will have to change a lot of other things (reference of cell in formulas for example).
This kind of task could be easily done with Office.Interop or probably with libraries like EEPlus or ClosedXml.

Related

Get a cell location from a given value and find the value on the right from cell location from Excel using EPPlus and Linq

This is what I want to have
Find the location of the value "arriere" in an Excel sheet and get the value next to it.
column I row 35 = "arriere"
column J row 35 = 1456.00
Right now I'm using the following code :
using (var package = new ExcelPackage(f.FullName))
{
var worksheet = package.Workbook.Worksheets[0];
var montantArriere = from cell in worksheet.Cells["G:L"]
where cell.Value?.ToString() == "Total Arriéré"
select worksheet.Cells[cell.Start.Row, 10].Value;
}
The code works but if the value "arriere" change of column I won't be able to find the value next to it since the cell.Start.Row is set by 10 from the start.
Is there any way to have the value next to "arriere" more dynamically?
You can use Offset on cell to get a cell based on the offset you choose. (row, column)
using (var package = new ExcelPackage(f.FullName))
{
var worksheet = package.Workbook.Worksheets[0];
var montantArriere = from cell in worksheet.Cells["G:L"]
where cell.Value?.ToString() == "Total Arriéré"
select cell.Offset(0,1).Value;
}

Copying cell range with images in Excel files

I'm copying cells from one Excel sheet into another with GemBox.Spreadsheet. The cells are coming from a specific named range and I'm using CellRange.CopyTo method like this:
ExcelFile book = ExcelFile.Load("sv-data.xlsx");
ExcelWorksheet sheet1 = book.Worksheets[0];
CellRange range1 = sheet1.NamedRanges["SV"].Range;
ExcelWorksheet sheet2 = book.Worksheets.Add("Sheet2");
range1.CopyTo(sheet2, 14, 3);
This works great for all the cells' value and formatting, but it doesn't copy over the images.
Is this the intended behavior? How can I copy both data and images?
EDIT (2022-10-28):
In the current latest version of GemBox.Spreadsheet the CellRange.CopyTo() method is copying pictures, shapes, and charts.
Also, there is another set of CellRange.CopyTo() overload methods that accept the CopyOptions parameter with which you can specify what you want to copy.
For example:
range1.CopyTo(sheet2, row2, column2,
new CopyOptions() { CopyTypes = CopyTypes.Values | CopyTypes.Styles | CopyTypes.Drawings });
Also, see the second example on this page (it shows various options for copying and deleting cell ranges):
https://www.gemboxsoftware.com/spreadsheet/examples/excel-sheet-copy-delete/111
ORIGINAL:
Yes, it seems to be intended because images are not stored inside the cells, but rather inside a sheet. They are part of a separate collection, the ExcelWorksheet.Pictures.
So, perhaps you could iterate through that collection and copy the required elements.
For example, something like the following:
ExcelFile book = ExcelFile.Load("sv-data.xlsx");
ExcelWorksheet sheet1 = book.Worksheets[0];
CellRange range1 = sheet1.NamedRanges["SV"].Range;
ExcelWorksheet sheet2 = book.Worksheets.Add("Sheet2");
int row2 = 14;
int column2 = 3;
range1.CopyTo(sheet2, row2, column2);
int rowOffset = row2 - range1.FirstRowIndex;
int columnOffset = column2 - range1.FirstColumnIndex;
foreach (ExcelPicture picture1 in sheet1.Pictures)
{
ExcelDrawingPosition position1 = picture1.Position;
CellRange pictureRange1 = sheet1.Cells.GetSubrangeAbsolute(position1.From.Row.Index, position1.From.Column.Index, position1.To.Row.Index, position1.To.Column.Index);
if (range1.Overlaps(pictureRange1))
{
ExcelPicture picture2 = sheet2.Pictures.AddCopy(picture1);
ExcelDrawingPosition position2 = picture2.Position;
position2.From.Row = sheet2.Rows[position2.From.Row.Index + rowOffset];
position2.To.Row = sheet2.Rows[position2.To.Row.Index + rowOffset];
position2.From.Column = sheet2.Columns[position2.From.Column.Index + columnOffset];
position2.To.Column = sheet2.Columns[position2.To.Column.Index + columnOffset];
}
}
book.Save("output.xlsx");

How can I get column width via openxml and c#

My task is parse an excel file and converted it to web table.
To achieve that objective, I need the column numbers, width of each column, row numbers, and each cell and cell property within the row.
So far, I can get the rows, the cells, the cell property such as border,font, and so on. But I can't get the column width.
When I open the excel file and get columns by following code
Columns columns = sheet.Descendants<Columns>().FirstOrDefault()
But, sometimes I can get it, sometimes the value is null.
I read the excel file by openxml tools. The following code is not always there.
Columns columns1 = new Columns();
Column column1 = new Column(){ Min = (UInt32Value)7U, Max = (UInt32Value)7U, Width = 39.6328125D, CustomWidth = true };
columns1.Append(column1);
If you open an empty excel file and do not change column width, then you save it. The code is not there.
So my question is how can I get the column width?
A column width can have either the default width or custom width. As you state, the custom width can be read from Column.Width property. If the default column width is set, it can be read from SheetFormatProperties Class. However, if DefaultColumnWidth property is set to null, The default column width is 8.43 characters.
To get the DefaultColumnWidth :
using (SpreadsheetDocument spreadSheetDocument = SpreadsheetDocument.Open(filePath, true))
{
WorkbookPart workbookPart = spreadSheetDocument.WorkbookPart;
IEnumerable<Sheet> sheets = spreadSheetDocument.WorkbookPart.Workbook.GetFirstChild<Sheets>().Elements<Sheet>();
string relationshipId = sheets.First().Id.Value;
WorksheetPart worksheetPart = (WorksheetPart)spreadSheetDocument.WorkbookPart.GetPartById(relationshipId);
Worksheet workSheet = worksheetPart.Worksheet;
var sheetFormatProps = workSheet.SheetFormatProperties;
var defaultColWidth = sheetFormatProps.DefaultColumnWidth;
if (defaultColWidth == null)
{
defaultColWidth = 8.43;
}
}

Excel-Protection from one sheet is set in another

I created an excel file dynamicly using openXML. Inside this sheet there are multiple sheets. Inside each sheet there can be rows that are write-protected.
I use an excel file as template. In this template there are "normal" rows which allow editing and a row that does not. I grab the row and copy it to the places where I do not want the user to be able to edit the contents:
private Row CloneRow(Row sourceRow, uint index, bool? hidden = null)
{
var targetRow = (Row) sourceRow.CloneNode(true);
if (hidden.HasValue)
{
targetRow.Hidden = hidden;
}
foreach (Cell cell in targetRow.Elements<Cell>())
{
// Update the references for reserved cells.
string cellReference = cell.CellReference.Value;
cell.CellReference = new StringValue(cellReference.Replace(targetRow.RowIndex.Value.ToString(), index.ToString()));
cell.CellFormula = null;
}
// Update the row index.
targetRow.RowIndex = new UInt32Value(index);
return targetRow;
}
the parameter sourceRow is read from the template:
List<Row> rows = sheet.ChildElements.OfType<Row>().ToList();
rowChangeAllowed=rows.FirstOrDefault(rw=>rw.RowIndex==3);
rowNotChangeAllowed=rows.FirstOrDefault(rw=>rw.RowIndex==4);
Everything works as expected. But when I open the file in Excel, rows that should be proteced on ANY sheet are protected on ALL sheets.
Example:
Sheet 1: Row 4+5 should be protected
Sheet 2: Row 7 should be protected.
Now on sheet 1 rows 4,5 and 7 are protected
When I switch to the second sheet, suddenly everything works as needed: On Sheet 1, row 4+5 are still protected, but row 7 is not.
Because the behaviour is only wrong directly after opening the file, but is correct when I switch between the sheets: Is there an additional command I have to call to "refresh" the file after creating?
Additional Issue:
When I change a cell in sheet 1, it also is automaticly changed in sheet 2 (again: until I swap the sheets once manually)
The problem was having to many views in the sheet. The following code solved the issue:
//There can only be one sheet that has focus
SheetViews views = worksheetPart.Worksheet.GetFirstChild<SheetViews>();
if (views != null)
{
views.Remove();
worksheetPart.Worksheet.Save();
}
(got it from http://blogs.msdn.com/b/brian_jones/archive/2009/02/19/how-to-copy-a-worksheet-within-a-workbook.aspx)

Select range in aspose

Do you know an equivalent to VBA code:
Range(Selection, Selection.End(xlToRight)).Select
In Aspose.Cells. It seems that its only possible to select the last cell in the entire row:
public Aspose.Cells.Cell EndCellInRow ( Int32 rowIndex )
Or the last cell on the right within a range:
public Aspose.Cells.Cell EndCellInRow ( Int32 startRow, Int32 endRow, Int32 startColumn, Int32 endColumn )
but then you must know more or less how big your table is going to be.
I found this from 2009: http://www.aspose.com/community/forums/permalink/196519/196405/showthread.aspx but that will not resolve my problem as I may have many tables in a sheet both horizontally and vertiacally. And I can't predict where they are going to be.
Edit1:
Sorry if this is dumb question, but ctrl+shift+arrow is such a common operation that I can't believe it would be not implemented so I'm making sure I really have to re-invent the wheel.
Aspose.Cells provides the list of tables in a worksheet using property named 'Worksheet.ListObjects'. 'ListObjects' is a colloection of 'ListObject' type which represents a Table in an excel sheet. That means if one has more than one Tables in a worksheet, the ListObjects collection will give access to every table in the worksheet very conveniently. Each 'ListObject' in turn contains a property named 'DataRange' which specifies all the cells inside a Table. For the sake of convenience DataRange can be used for following operations on a Table:
To apply styles/formatting on the cells in Table
To get the data values
Merge or move the cells in Range
Export contents
To get enumerator to traverse through Table cells
To make selection of cells from DataRange, you can traverse using DataRange to get all the cells in a Row (This could also be done for a column)
Applying any operation on Table cells like after selecting cells using Ctrl+Shift+Arrow, could be performed using a workbook object as follows:
Workbook workbook = new Workbook(new FileStream("book1.xls", FileMode.Open));
if (workbook.Worksheets[0].ListObjects.Count > 0)
{
foreach (ListObject table in workbook.Worksheets[0].ListObjects)
{
Style st = new Style();
st.BackgroundColor = System.Drawing.Color.Aqua;
st.ForegroundColor = System.Drawing.Color.Black;
st.Font.Name = "Agency FB";
st.Font.Size = 16;
st.Font.Color = System.Drawing.Color.DarkRed;
StyleFlag stFlag = new StyleFlag();
stFlag.All = true;
table.DataRange.ApplyStyle(st, stFlag);
}
}
workbook.Save("output.xls");
There is also some worthy information available in Aspose docs about Table styles and applying formatting on a ListObject. For getting last Table cell in a certain row or column, I am sure this will help:
int iFirstRowIndex = table.DataRange.FirstRow;
int iFirstColumnIndex = table.DataRange.FirstColumn;
int iLastRowIndex = table.DataRange.RowCount + iFirstRowIndex;
int iLastColumnIndex = table.DataRange.ColumnCount + iFirstColumnIndex;
for (int rowIndex = 0; rowIndex < table.DataRange.RowCount; rowIndex++)
{
//Get last cell in every row of table
Cell cell = worksheet.Cells.EndCellInColumn(rowIndex + iFirstRowIndex, rowIndex + iFirstRowIndex, (short)iFirstColumnIndex, (short)(iLastColumnIndex - 1));
//display cell value
System.Console.WriteLine(cell.Value);
}

Categories

Resources