Does anyone know how to set the excel page break to include a certain number of columns using C# with the OpenXML SDK? What I want to do is make x columns appear on one page. I had originally thought setting the print area would do it but it doesn't. I can't find any references to do this.
This is done manually in an excel spreadsheet's "Page Break View" where you drag the vertical dotted line to include more columns.
Thanks
The OpenXML SDK distinguish between manual horizontal page breaks and manual vertical page breaks.
A manual horizontal page break allows you to specify a break above a given row Id (index).
A vertical page break allows you to specify a break to the left of the specified column Id (index).
To programmatically insert a horizontal page break use the RowBreaks and Break class.
The RowBreaks class represents a collection of all horizontal page breaks in a worksheet.
The ColumnBreaks and Break class allow you to insert a vertical page break. The
ColumnBreaks class holds all vertical page breaks for a worksheet.
The following example demonstrate the insertion of a vertical page break.
The function InsertVerticalPageBreak() takes a columnIndex (where the page break should be inserted)
and the WorksheetPart. This function first checks if the worksheet already contains a
ColumnBreaks collection. If not, one will be created. Then the function creates an instance
of the Break class and sets the Id property to the column index. I've also set the Max property
to the maximum number of rows Excel is able to handle to get a continuing vertical page break. By setting the property ManualPageBreak to true we specifing a manual page break.
I've also added a InsertHorizontalPageBreak() function to the sample to show how to
add a horizontal page break.
private void InsertPageBreaks()
{
uint columnIndex = 17U;
uint rowIndex = 51U;
using (SpreadsheetDocument sd = SpreadsheetDocument.Open("c:\\temp\\spreadsheet.xlsx", true))
{
WorkbookPart workbookPart = sd.WorkbookPart;
WorksheetPart worksheetPart = workbookPart.WorksheetParts.Last();
// Uncomment the following line to insert row page breaks.
// InsertHorizontalPageBreak(rowIndex, worksheetPart);
InsertColumnVerticalBreak(columnIndex, worksheetPart);
}
}
private void InsertHorizontalPageBreak(uint rowIndex, WorksheetPart worksheetPart)
{
Break rowBreak =
new Break() { Id = (UInt32Value)rowIndex, Max = (UInt32Value)16383U, ManualPageBreak = true };
RowBreaks rb = worksheetPart.Worksheet.GetFirstChild<RowBreaks>();
if (rb == null)
{
rb = new RowBreaks();
rb.ManualBreakCount = (UInt32Value)0;
rb.Count = (UInt32Value)0;
worksheetPart.Worksheet.Append(rb);
}
rb.Append(rowBreak);
rb.ManualBreakCount++;
rb.Count++;
}
private void InsertVerticalPageBreak(uint columnIndex, WorksheetPart worksheetPart)
{
ColumnBreaks cb = worksheetPart.Worksheet.GetFirstChild<ColumnBreaks>();
if (cb == null)
{
cb = new ColumnBreaks();
cb.ManualBreakCount = (UInt32Value)0;
cb.Count = (UInt32Value)0;
worksheetPart.Worksheet.Append(cb);
}
Break br =
new Break() { Id = (UInt32Value)columnIndex, Max = (UInt32Value)1048575U, ManualPageBreak = true };
cb.Append(br);
cb.ManualBreakCount++;
cb.Count++;
}
Related
I am not finding a way to set the ContentControl.Range.Text from where the C# is executing from (inside the content control). Perhaps I should be looking at it from a completely different perspective.
Currently I have a content control that produces a set of text with some text between [] square brackets and I want to select text and format the colour by setting the start and end of the range of characters between the []. I am stuck on trying to set the initial range to the contentcontrol I am currently using.
Most of what I have managed/found/patched together below.
object word;
Microsoft.Office.Interop.Word.Document _PWdDoc;
try
{
word = System.Runtime.InteropServices.Marshal.GetActiveObject("Word.Application");
//If there is a running Word instance, it gets saved into the word variable
}
catch (Exception ex)
{
//If there is no running instance, it creates a new one
Type type = Type.GetTypeFromProgID("Word.Application");
word = System.Activator.CreateInstance(type);
}
Microsoft.Office.Interop.Word.Application oWord = (Microsoft.Office.Interop.Word.Application) word;
_PWdDoc = oWord.ActiveDocument;
System.Collections.IEnumerator ContentX = _PWdDoc.ContentControls.GetEnumerator();
//Microsoft.Office.Interop.Word.ContentControl ContentX = Microsoft.Office.Interop.Word.ContentControls.Item[];
//Microsoft.Office.Interop.Word.Range rng = Microsoft.Office.Interop.Word.ContentControl.Range.Duplicate(ref ContentX);
//var rngX = Microsoft.Office.Interop.Word.ContentControl.Range(ContentX);
//Microsoft.Office.Interop.Word.ContentControl cc1 = ContentX;
Excuse the coding mess but it's all I can come up with with the minimal experience I have with this.
Now I have gotten the IEnumerator fo the Content Control(I think) I have no idea how to use it besides from what I have read, they say to iterate through the IEnumerables accessing each of them. That's not what I want to do. I want 1 content control. The current one that I am working in. I want to find it's range and assign it to a value. Then in that range's "text" I want to do some [fancy] highlighting.
Determining whether the current selection or a specific Range is in a content control and doing something with that content control is not a trivial matter. Most other Word objects will return something that they're "in"; content controls do not.
So the approach I use is to
create a Range that reaches from the current selection (or a specific Range) back to the beginning of the document
count the number of content controls in that range
then check whether the current selection is in the same range as the last content control of the extended range.
if it is, then I know the selection is within a content control and I can access the content control.
Here's some sample code. The snippet that calls the function I use to return the information:
Word.Range rng = null;
//Substitute a specific Range object if working with a Range, rather than a Selection
Word.ContentControl cc = IsSelectionInCC(wdApp.Selection.Range);
if ( cc != null)
{
rng = cc.Range;
rng.HighlightColorIndex = Word.WdColorIndex.wdYellow;
}
The function:
private Word.ContentControl IsSelectionInCC(Word.Range sel)
{
Word.Range rng = sel.Range;
Word.Document doc = (Word.Document) rng.Parent;
rng.Start = doc.Content.Start;
int nrCC = rng.ContentControls.Count;
Word.ContentControl cc = null;
bool InCC = false;
rng.Start = doc.Content.Start;
if (nrCC > 0)
{
if (sel.InRange(doc.ContentControls[nrCC].Range))
{
InCC = true; //Debug.Print ("Sel in cc")
cc = doc.ContentControls[nrCC];
}
else
{
sel.MoveEnd(Word.WdUnits.wdCharacter, 1);
if (sel.Text == null)
{
//Debug.Print ("Sel at end of cc")
InCC = true;
cc = doc.ContentControls[nrCC];
}
}
}
return cc;
}
Assuming you mean that the insertion point is inside a Content Control, and your Word Application object is called oWord, then you can get the range of that content control using e.g.
Microsoft.Office.Interop.Word.Range r = oWord.Selection.Range.ParentContentControl.Range
If you have nested controls You can verify that the insertion point is in a Content Control (Word 2013 and later, I think) by checking the value of inCC as follows:
Boolean inCC = (Boolean)oWord.Selection.Information[Microsoft.Office.Interop.Word.WdInformation.wdInContentControl]
However, when dealing with content controls, be aware that selecting a content control in the UI is different from selecting the "range of the content control". Programmatically, it's obvious how to select the Range - not so obvious how to select the control. If you select the Range, the ParentContentControl should be the control whose range you've selected. If you (or the user) selected the control, OTTOMH I am not so sure.
I hope this easy but I can't seem to find it. I'm working with a word document object in an Outlook VSTO project to modify values that are located between hidden key values.
IE [key_start] text [key_end]
When they make selections in the drop down from the addin I change the text in the body of the email for them.
To make this work
1) I un-hide all my keys
Word.Document doc = Inspector.WordEditor as Word.Document;
doc.Content.Font.Hidden = 0;
2) Then find my keys and generate a range between them
int start_pos = -1;
int end_pos = -1;
//SelectTextRange is a custom function to find range based on text
ValueRange = Custom.WordDocument.SelectTextRange(doc, key_value_start);
if (ValueRange != null) {
start_pos = ValueRange.End;
}
ValueRange = Custom.WordDocument.SelectTextRange(doc, key_value_end);
if (ValueRange != null) {
end_pos = ValueRange.Start;
}
3) Then update the text.
if (start_pos > -1 && end_pos > -1) {
ValueRange = doc.Range(start_pos, end_pos);
ValueRange.Text = " new text goes here ";
}
4) Then hide my keys again.
Everything works great but it looks a little tacky when the changes cascade through the doc as it updates. It looks as though the document updates on every command and doesn't wait until all my commands are finished. Is there a way to prevent the document from committing changes so I can make it do this all in one shot and not have the user see flickers of hidden text when this process occurs?
YowE3K nailed it. Thank you again.
Adding this to the beginning of my changes
doc.Application.ScreenUpdating = False
And then adding this to the end
doc.Application.ScreenUpdating = True
Did exactly what I needed. All my changes occured without the document updating and then once I set doc.Application.ScreenUpdating = True it draws the updated document with all my changes.
When programmatically generating a "normal" Excel sheet, a Range's PageBreak property can be set to xlPageBreakManual and then you can specify where the page is to break with code like so:
workbook.Worksheets[0].VPageBreaks.Add(sheet.Range["G1"]);
...but is this possible when printing a PivotTable? I would think not, since the PivotTable is fed from a datasource (a page of "raw data" in my case, which I subsequently hide), but if it is, I would like to know how.
This is what I do now to prepare the PivotTablized sheet for printing:
private void FormatPivotTable()
{
. . .
ConfigureForPrinting(_xlPivotTableSheet.UsedRange.Rows.Count);
}
private void ConfigureForPrinting(int finalRow)
{
string lastColumn = GetExcelTextColumnName(_xlPivotTableSheet.UsedRange.Columns.Count);
string printArea = String.Format("A1:{0}{1}", lastColumn, finalRow);
_xlPivotTableSheet.PageSetup.PrintArea = printArea;
_xlPivotTableSheet.PageSetup.Orientation = XlPageOrientation.xlLandscape;
_xlPivotTableSheet.PageSetup.Zoom = false;
_xlPivotTableSheet.PageSetup.FitToPagesWide = 1;
_xlPivotTableSheet.PageSetup.FitToPagesTall = 50;
_xlPivotTableSheet.PageSetup.LeftMargin = _xlApp.Application.InchesToPoints(0.5);
_xlPivotTableSheet.PageSetup.RightMargin = _xlApp.Application.InchesToPoints(0.5);
_xlPivotTableSheet.PageSetup.TopMargin = _xlApp.Application.InchesToPoints(0.5);
_xlPivotTableSheet.PageSetup.BottomMargin = _xlApp.Application.InchesToPoints(0.5);
_xlPivotTableSheet.PageSetup.HeaderMargin = _xlApp.Application.InchesToPoints(0.5);
_xlPivotTableSheet.PageSetup.FooterMargin = _xlApp.Application.InchesToPoints(0.5);
string rangeToRepeat = string.Format("$A$6:${0}$7", lastColumn);
_xlPivotTableSheet.PageSetup.PrintTitleRows = rangeToRepeat;
}
What the user wants is that each logical block of data be kept together on a sheet, so that a block (5 rows) does not span more than one sheet. IOW, if there are less than 5 rows of space on the current page when a block begins, skip to the next page with a page break.
Is this possible with PivotTables?
I understand the that when you are referring to "logical blocks of data" it means data corresponding to a PivotFieldItem. If that is the case the try this:
Set these PivotTable properties to TRUE:
ActiveSheet.PivotTables("PivotTable1").PrintTitles = True
ActiveSheet.PivotTables("PivotTable1").RepeatItemsOnEachPrintedPage = True
Also set to TRUE the LayoutPageBreak property of the PivotField for which you want to set a page break every time there is a change of item:
ActiveSheet.PivotTables("PivotTable1").PivotFields("Class").LayoutPageBreak = True
Also need to set this property to TRUE in case there are items with only one record.
ActiveSheet.PivotTables("PivotTable1").PivotFields("Class").LayoutBlankLine = True
Replace "PivotTable1" and "Class" values as required
Although the commands above are in VBA they should give you a good idea of how to set these properties using C#
I have this code that works fine, adding a FreezePane to row 7, col 3 of the first sheet (counting from the left):
private void FreezePane(int rowNum, int colNum)
{
Range cellToFreeze = (Range)_xlSheet.Cells[rowNum, colNum];
cellToFreeze.Activate();
cellToFreeze.Application.ActiveWindow.FreezePanes = true;
}
After adding another couple of sheets (the second sheet contains data as a source for a pivot table, and the third sheet contains the pivot table), I want to add a freezepane in the same place of the third/pivot Table sheet, and so I tried this:
private void FreezePanePivotTable(int rowToFreeze, int colToFreeze)
{
Range pivotTableCellToFreeze = (Range)_xlPivotTableSheet.Cells[rowToFreeze, colToFreeze];
pivotTableCellToFreeze.Activate();
pivotTableCellToFreeze.Application.ActiveWindow.FreezePanes = true;
}
That, though, crashed with "Activate method of Range class failed Exception Source: Microsoft Office Excel
Exception StackTrace: at System.RuntimeType.ForwardCallToInvokeMember(..."
So I thought, "maybe you can only have one frozen pane in a workbook" and tried calling only the new method (leaving the first sheet unfrozen), and I get this seemingly bizarre err msg: "Unable to set the Size property of the Font class"
Where this exception is occurring is in the second sheet - the source data for sheet 3/PivotTable sheet! Why does setting the font size all of a sudden cause a problem? Line 3418 is the last line below:
var itemCodeLabelCell = _xlPivotDataSheet.Cells[1, 1];
itemCodeLabelCell.Value2 = "ItemCode";
itemCodeLabelCell.Style.WrapText = false;
itemCodeLabelCell.Style.Font.Size = 12;
Do different rules apply to adding freezepanes on the main (first) sheet and others, or is it that FreezePanes and PivotTables can't coexist, or what?
SomeSheet.SomeRange.Activate() fails if SomeSheet is not already active, hence probably the "Activate method of Range class failed".
You must activate the sheet before:
_xlPivotTableSheet.Activate();
pivotTableCellToFreeze.Activate();
I want to add one picture (displaying "DRAFT") by printable Excel worksheet in C# EPPlus.
I need to know if there is a way to find the last visible row of each page of a worksheet when you are printing it. I can't pretend that it will always be a fix number of row per page because it depends on the content of the cells.
Here is my current code that use a fix number of row per page (30) to insert image. This result in approximately one image per printable page except that in each new page the image is not at the same place. (Slightly off, depending on content of cells.)
public void InsertDraftImage(ExcelWorksheet worksheet, FileInfo draft_image)
{
int maxRowNumber = worksheet.Dimension.End.Row;
int rowByPage = 30;
int numberOfPage = (maxRowNumber / rowByPage) + 1;
ExcelPicture picture = null;
for(int i = 0; i < numberOfPage; i++)
{
if(draft_image != null)
{
picture = worksheet.Drawings.AddPicture(i.ToString(), draft_image);
picture.SetSize(609, 545); //original image size
picture.SetPosition(i * rowByPage, 0, 1, 0);
picture.EditAs = eEditAs.Absolute;
}
}
After trying to implement the missing code in 'ExcelHeaderFooter.cs' from the EPPlus with a workmate without success, we finally did it by following Ernie suggestion!!
There is my final code to insert a picture into each page of a printable excel file generate with EPPlus in C#.
It is done by adding the picture in the footer and setting the Boolean ScaleWithDoc to false (default = true).
public void InsertDraftImage(ExcelWorksheet worksheet, FileInfo draft_image)
{
ExcelHeaderFooterText footer = worksheet.HeaderFooter.OddFooter; //all page have same footer
footer.InsertPicture(draft_image, PictureAlignment.Centered);
}
Added this code in my method to create the ExcelWorksheet (all the other excel style, populate, settings).
XmlAttribute temp = worksheet.WorksheetXml.CreateAttribute("scaleWithDoc");
temp.Value = "0";
worksheet.WorksheetXml.GetElementsByTagName("headerFooter")[0].Attributes.Append(temp);
package.Save();