I need to create a multi-level bullet list via Microsoft.Office.Interop.Word and I am currently struggling with its (horrible) API (again).
I've just created the following example (not dynamic yet, just for demonstration purposes) in a VSTO document-level project for Microsoft Office Word 2010 in the programming language C#:
Word.Paragraph paragraph = null;
Word.Range range = this.Content;
paragraph = range.Paragraphs.Add();
paragraph.Range.Text = "Item 1";
paragraph.Range.ListFormat.ApplyBulletDefault(Word.WdDefaultListBehavior.wdWord10ListBehavior);
// ATTENTION: We have to outdent the paragraph AFTER its list format has been set, otherwise this has no effect.
// Without this, the the indent of "Item 2" differs from the indent of "Item 1".
paragraph.Outdent();
paragraph.Range.InsertParagraphAfter();
paragraph = range.Paragraphs.Add();
paragraph.Range.Text = "Item 1.1";
// ATTENTION: We have to indent the paragraph AFTER its text has been set, otherwise this has no effect.
paragraph.Indent();
paragraph.Range.InsertParagraphAfter();
paragraph = range.Paragraphs.Add();
paragraph.Range.Text = "Item 1.2";
paragraph.Range.InsertParagraphAfter();
paragraph = range.Paragraphs.Add();
paragraph.Range.Text = "Item 2";
paragraph.Outdent();
The code does exactly what I want (after a lot of try and error!), but it's horrible in my opinion. The format has to be applied at a VERY specific point and I have to manually indent and outdent the created paragraphs.
So my question is: Does a better approach exist to create a multi-level bullet list via Word.Interop, e.g. via shorthand methods that I haven't discovered yet?
My goal is to create a multi-level list from XML data (more specific a CustomXMLNode object)
Two other questions related to bullet lists exist on Stack Overflow, but both do not help me (the source code above is one answer to the second question):
Bullet points in Word with c# Interop
https://stackoverflow.com/questions/3768414/ms-word-list-with-sub-lists
EDIT (2013-08-08):
I've just hacked something together that outputs two arrays as a bullet list with two levels (the array with the sub-items is used for each root-item, to keep it simple). By introducing recursion, one would be able to create a bullet list with infinite levels (theoretically). But the problem remains, the code is a mess...
string[] rootItems = new string[]
{
"Root Item A", "Root Item B", "Root Item C"
};
string[] subItems = new string[]
{
"Subitem A", "Subitem B"
};
Word.Paragraph paragraph = null;
Word.Range range = this.Content;
bool appliedListFormat = false;
bool indented = false;
for (int i = 0; i < rootItems.Length; ++i)
{
paragraph = range.Paragraphs.Add();
paragraph.Range.Text = rootItems[i];
if (!appliedListFormat)
{
paragraph.Range.ListFormat.ApplyBulletDefault(Word.WdDefaultListBehavior.wdWord10ListBehavior);
appliedListFormat = true;
}
paragraph.Outdent();
paragraph.Range.InsertParagraphAfter();
for (int j = 0; j < subItems.Length; ++j)
{
paragraph = range.Paragraphs.Add();
paragraph.Range.Text = subItems[j];
if (!indented)
{
paragraph.Indent();
indented = true;
}
paragraph.Range.InsertParagraphAfter();
}
indented = false;
}
// Delete the last paragraph, since otherwise the list ends with an empty sub-item.
paragraph.Range.Delete();
EDIT (2013-08-12):
Last friday I thought I have achieved what I wanted to, but this morning I noticed, that my solution only works if the insertion point is at the end of the document. I've created the following simple example to demonstrate the (erroneous) behavior. To conclude my problem: I am able to create multi-level bullet lists at the end of the document only. As soon as I change the current selection (e.g. to the start of the document), the list is destroyed. As far as I can see, this is related to the (automatic or non-automatic) extension of the Range objects. I've tried a lot so far (I'm almost losing it), but it's all cargo-cult to me. The only thing I want to do is to insert one element after another (is it impossible to create a content control inside a paragraph, so that the text of the paragraph is followed by the content control?) and to to that in any Range of a Document. I will create a Gist on GitHub with my actual CustomXMLPart binding class this evening. Eventually someone can help me to fix that bothersome problem.
private void buttonTestStatic_Click(object sender, RibbonControlEventArgs e)
{
Word.Range range = Globals.ThisDocument.Application.Selection.Range;
Word.ListGallery listGallery = Globals.ThisDocument.Application.ListGalleries[Word.WdListGalleryType.wdBulletGallery];
Word.Paragraph paragraph = null;
Word.ListFormat listFormat = null;
// TODO At the end of the document, the ranges are automatically expanded and inbetween not?
paragraph = range.Paragraphs.Add();
listFormat = paragraph.Range.ListFormat;
paragraph.Range.Text = "Root Item A";
this.ApplyListTemplate(listGallery, listFormat, 1);
paragraph.Range.InsertParagraphAfter();
paragraph = paragraph.Range.Paragraphs.Add();
listFormat = paragraph.Range.ListFormat;
paragraph.Range.Text = "Child Item A.1";
this.ApplyListTemplate(listGallery, listFormat, 2);
paragraph.Range.InsertParagraphAfter();
paragraph = paragraph.Range.Paragraphs.Add();
listFormat = paragraph.Range.ListFormat;
paragraph.Range.Text = "Child Item A.2";
this.ApplyListTemplate(listGallery, listFormat, 2);
paragraph.Range.InsertParagraphAfter();
paragraph = paragraph.Range.Paragraphs.Add();
listFormat = paragraph.Range.ListFormat;
paragraph.Range.Text = "Root Item B";
this.ApplyListTemplate(listGallery, listFormat, 1);
paragraph.Range.InsertParagraphAfter();
}
private void ApplyListTemplate(Word.ListGallery listGallery, Word.ListFormat listFormat, int level = 1)
{
listFormat.ApplyListTemplateWithLevel(
listGallery.ListTemplates[level],
ContinuePreviousList: true,
ApplyTo: Word.WdListApplyTo.wdListApplyToSelection,
DefaultListBehavior: Word.WdDefaultListBehavior.wdWord10ListBehavior,
ApplyLevel: level);
}
EDIT (2013-08-12): I've set up a GitHub repository here which demonstrates my problem with the Word.Range objects. The OnClickButton method in the file Ribbon.cs invokes my custom mapper class. The comments there describe the problem. I know that my problems are related to the argument Word.Range object reference, but all other solutions I tried (e.g. modifying the range inside of the class) failed even harder. The best solution I've achieved so far, is to specify the Document.Content range as the argument for the MapToCustomControlsIn method. This inserts a nicely formatted multi-level bullet list (with custom XML parts bound to content controls) to the end of the document. What I want is to insert that list at a custom posiztion into the document (e.g. the current selection via Word.Selection.Range).
Florian Wolters example almost there, but the first child item numbering always not correct when I tried.
Someone gave me inspiration by suggesting using Macro and VBA script then convert to C#.
Below is the sample code tested works at my side. Hope it helps.
using Microsoft.Office.Interop.Word;
using System.Reflection;
namespace OfficeUtility
{
public class NumberListGenerate
{
public void GenerateList()
{
Application app = null;
Document doc = null;
string filePath = "c:\\output.docx";
string pdfPath = "c:\\export.pdf";
try
{
app = new Application();
app.Visible = false; // Open Microsoft Office in background
doc = app.Documents.Open(filePath, Missing.Value, false);
Range range = doc.Range();
string search = "$list";
// Find in document to generate list
while (range.Find.Execute(search))
{
ListGallery listGallery =
app.ListGalleries[WdListGalleryType.wdNumberGallery];
// Select found location
range.Select();
// Apply multi level list
app.Selection.Range.ListFormat.ApplyListTemplateWithLevel(
listGallery.ListTemplates[1],
ContinuePreviousList: false,
ApplyTo: WdListApplyTo.wdListApplyToWholeList,
DefaultListBehavior: WdDefaultListBehavior.wdWord10ListBehavior);
// First level
app.Selection.TypeText("Root Item A"); // Set text to key in
app.Selection.TypeParagraph(); // Simulate typing in MS Word
// Go to 2nd level
app.Selection.Range.ListFormat.ListIndent();
app.Selection.TypeText("Child Item A.1");
app.Selection.TypeParagraph();
app.Selection.TypeText("Child Item A.2");
app.Selection.TypeParagraph();
// Back to 1st level
app.Selection.Range.ListFormat.ListOutdent();
app.Selection.TypeText("Root Item B");
app.Selection.TypeParagraph();
// Go to 2nd level
app.Selection.Range.ListFormat.ListIndent();
app.Selection.TypeText("Child Item B.1");
app.Selection.TypeParagraph();
app.Selection.TypeText("Child Item B.2");
app.Selection.TypeParagraph();
// Delete empty item generated by app.Selection.TypeParagraph();
app.Selection.TypeBackspace();
}
// Save document
doc.Save();
// Export to pdf
doc.ExportAsFixedFormat(pdfPath, WdExportFormat.wdExportFormatPDF);
}
catch (System.Exception ex)
{
LogError(ex);
}
finally
{
if (doc != null)
{
// Need to close the document to prevent deadlock
doc.Close(false);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(doc);
}
if (app != null)
{
app.Quit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(app);
}
}
}
}
}
A summary of what needs to be done.
1.) Select the Range
2.) Configure the ListFormat Property of the Range. Must set DefaultListBehavior
to WdDefaultListBehavior.wdWord10ListBehavior
3.)Add your Level one text.
4.)Add a paragraph
5.)Add your level two text.
6.)Set ListLevelTier to level 2
public void GenerateMultiLevelList()
{
//A Range to write Text
Word.Range writRange = ActiveDoc.Range(0);
writRange.Text = "Tier 1 Bullet Text";
//Moving the Range to the End of Previously Entered Text
writRange = ActiveDoc.Range(writRange.End - 1);
//Formating the Range as a Bullet Point List
writRange.ListFormat.ApplyListTemplate(BulletListTemplate, true, Word.WdListApplyTo.wdListApplyToWholeList, Word.WdDefaultListBehavior.wdWord10ListBehavior);
//Adding a Paragraph
writRange.Paragraphs.Add();
writRange = ActiveDoc.Range(writRange.End - 1);
writRange.Text = "Tier 2 Bullet Text";
//Setting the List Level to 2
writRange.SetListLevel(2);
}
Related
I have an excel sheet (ws) that has several pictures in it, and i want to removed all of it using EPPlus.
this is what i've done, and it worked, but I don't want to remove it using the title of the picture
ws.Drawings.Remove("Picture 1");
ws.Drawings.Remove("Picture 2");
is there a way to remove them all at once?
I don't know of any method to remove them all with a single line of code, however you can do this by looping through all the drawings in a worksheet.
using (var p = new OfficeOpenXml.ExcelPackage(new FileInfo(#"c:\FooFolder\Foo.xlsx")))
{
ExcelWorkbook wb = p.Workbook;
ExcelWorksheet ew = wb.Worksheets.First();
//get the number of drawings in the worksheet to loop through.
//Subtract 1 since the drawings use a 0 base index
int drawingCount = ew.Drawings.Count -1;
//loop through the drawings starting at highest number so the collections index doesn't change as you remove them
for(int i = drawingCount; i>=0; i--)
{
//remove the drawing at current index
ew.Drawings.Remove(i);
}
p.Save();
}
Edit: After I posted this I found a much simpler method.
You can use ExcelWorksheet.Drawings.Clear() this method removes all drawings from the worksheet.
I am trying to cut specific pages of my word document(.docx), say 2, 4. I am using for loop to traverse as per the page gave splitting it based on ,.Below is the code for the same
if (startEnd.Contains(','))
{
arrSpecificPage = startEnd.Split(',');
for (int i = 0; i < arrSpecificPage.Length; i++)
{
range.Start = doc.GoTo(WdGoToItem.wdGoToPage, WdGoToDirection.wdGoToAbsolute, arrSpecificPage[i]).Start;
range.End = doc.GoTo(WdGoToItem.wdGoToPage, WdGoToDirection.wdGoToAbsolute, arrSpecificPage[i]).End;
range.Copy();
newDocument.Range().Paste();
}
newDocument.SaveAs(outputSplitDocpath);
}
but the issue with this code is that its just copying the last page only to the new document i.e 4 in this case. How to add 2 as well? What's wrong in the code?
Since you always specify the entire document "range" as the target, each time you paste the entire content of the document is replaced.
It's correct that you work with a Range object and not with a selection, but it helps if you think about a Range like a selection. If you select everything (Ctrl+A) then paste, what was selected is replaced by what is pasted. Whatever is assigned to a Range will replace the content of the Range.
The way to solve this is to "collapse" the Range - think of it like pressing the Right-arrow or left-arrow key to "collapse" a selection to its start or end point. In the object model, this is the Collapse method that takes a parameter indicating whether to collapse to the start or end point (see the code below).
Note that I've also changed the code to use document.Content instead of Document.Range. Content is a property that returns the entire body of the document; Rangeis a method that expects a start and end point defining a Range. Using the property is the preferred method for the entire document.
if (startEnd.Contains(','))
{
arrSpecificPage = startEnd.Split(',');
for (int i = 0; i < arrSpecificPage.Length; i++)
{
range.Start = doc.GoTo(WdGoToItem.wdGoToPage, WdGoToDirection.wdGoToAbsolute, arrSpecificPage[i]).Start;
range.End = doc.GoTo(WdGoToItem.wdGoToPage, WdGoToDirection.wdGoToAbsolute, arrSpecificPage[i]).End;
range.Copy();
Word.Range targetRange = newDocument.Content
targetRange.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
targetRange.Paste();
}
newDocument.SaveAs(outputSplitDocpath);
}
I have a lot of charts i am graphing and for some i need to give each X value a custom color and have that item in the legend as well. The following code achieves this but on the legend there is always the first default value which is now pointless. How would i remove/hide/clear that original default legend item? Just for reference the chartColors array is just an array of colors to cycle through.
mainChart.Legends.Add(new Legend("legend"));
mainChart.Legends["legend"].Docking = Docking.Bottom;
foreach (Series ser in mainChart.Series)
{
int i = 0;
foreach (DataPoint point in ser.Points)
{
point.Color = chartColors[i % chartColors.Count()];
mainChart.Legends["legend"].CustomItems.Add(point.Color,point.AxisLabel);
i++;
}
}
According to http://msdn.microsoft.com/en-us/library/vstudio/dd456659(v=vs.100).aspx
"Legend items in this collection are always attached to the end of other legend items in the legend. "
so to maybe clarify my question, how do i remove the "other legend items"
edit: I found this answer but it is for winforms and im not sure how to call the function they add
WinForms.Charting suppress autogenerating legend
Okay figured it out posting in case someone else needs to do it, i used the awnser from the posted question as a bases but added the points before the method was called and then only removed the non custom ones
protected void chartarea1_CustomizeLegend(object sender,System.Web.UI.DataVisualization.Charting.CustomizeLegendEventArgs e)
{
int customItems = ((Chart)sender).Legends[0].CustomItems.Count();
if (customItems>0)
{
int numberOfAutoItems = e.LegendItems.Count()-customItems;
for (int i = 0; i < numberOfAutoItems; i++)
{
e.LegendItems.RemoveAt(0);
}
}
}
for all series at your Chart do next:
series .IsVisibleInLegend = false;
mainChart.Series[0].IsVisibleInLegend = false;
I am trying to add these three types of content into a word doc. This is how I am trying to do it now. However, each item replaces the last one. Adding images always adds to the beginning of the page. I have a loop that calls a function to create the headers and tables, and then adds images after. I think the problem is ranges. I use a starting range of object start = 0;
How can I get these to add one at a time to to a new line in the document?
foreach (var category in observedColumns)
{
CreateHeadersAndTables();
createPictures();
}
Adding Headers:
object start = 0;
Word.Range rng = doc.Range(ref start , Missing.Value);
Word.Paragraph heading;
heading = doc.Content.Paragraphs.Add(Missing.Value);
heading.Range.Text = category;
heading.Range.InsertParagraphAfter();
Adding Tables:
Word.Table table;
table = doc.Content.Tables.Add(rng, 1, 5);
Adding Pictures:
doc.Application.Selection.InlineShapes.AddPicture(#path);
A simple approach will be using paragraphs to handle the Range objects and simply insert a new paragraph one by one.
Looking at the API documentation reveals that Paragraphs implements an Add method which:
Returns a Paragraph object that represents a new, blank paragraph
added to a document. (...) If Range isn't specified, the new paragraph is added after the selection or range or at the end of the document.
Source: http://msdn.microsoft.com/en-us/library/microsoft.office.interop.word.paragraphs.add(v=office.14).aspx
In that way, it gets straight forward to append new content to the document.
For completeness I have included a sample that shows how a solution might work. The sample loops through a for loop, and for each iteration it inserts:
A new line of text
A table
A picture
The sample has is implemented as a C# console application using:
.NET 4.5
Microsoft Office Object Library version 15.0, and
Microsoft Word Object Library version 15.0
... that is, the MS Word Interop API that ships with MS Office 2013.
using System;
using System.IO;
using Microsoft.Office.Interop.Word;
using Application = Microsoft.Office.Interop.Word.Application;
namespace StackOverflowWordInterop
{
class Program
{
static void Main()
{
// Open word and a docx file
var wordApplication = new Application() { Visible = true };
var document = wordApplication.Documents.Open(#"C:\Users\myUserName\Documents\document.docx", Visible: true);
// "10" is chosen by random - select a value that fits your purpose
for (var i = 0; i < 10; i++)
{
// Insert text
var pText = document.Paragraphs.Add();
pText.Format.SpaceAfter = 10f;
pText.Range.Text = String.Format("This is line #{0}", i);
pText.Range.InsertParagraphAfter();
// Insert table
var pTable = document.Paragraphs.Add();
pTable.Format.SpaceAfter = 10f;
var table = document.Tables.Add(pTable.Range, 2, 3, WdDefaultTableBehavior.wdWord9TableBehavior);
for (var r = 1; r <= table.Rows.Count; r++)
for (var c = 1; c <= table.Columns.Count; c++)
table.Cell(r, c).Range.Text = String.Format("This is cell {0} in table #{1}", String.Format("({0},{1})", r,c) , i);
// Insert picture
var pPicture = document.Paragraphs.Add();
pPicture.Format.SpaceAfter = 10f;
document.InlineShapes.AddPicture(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "img_1.png"), Range: pPicture.Range);
}
// Some console ascii-UI
Console.WriteLine("Press any key to save document and close word..");
Console.ReadLine();
// Save settings
document.Save();
// Close word
wordApplication.Quit();
}
}
}
even after reading this forum post, its still quite confusing how to create a bulletted list using migradoc / pdfsharp. I basically want to display a list of items like this:
Dodge
Nissan
Ford
Chevy
Here's a sample (a few lines added to the HelloWorld sample):
// Add some text to the paragraph
paragraph.AddFormattedText("Hello, World!", TextFormat.Italic);
// Add Bulletlist begin
Style style = document.AddStyle("MyBulletList", "Normal");
style.ParagraphFormat.LeftIndent = "0.5cm";
string[] items = "Dodge|Nissan|Ford|Chevy".Split('|');
for (int idx = 0; idx < items.Length; ++idx)
{
ListInfo listinfo = new ListInfo();
listinfo.ContinuePreviousList = idx > 0;
listinfo.ListType = ListType.BulletList1;
paragraph = section.AddParagraph(items[idx]);
paragraph.Style = "MyBulletList";
paragraph.Format.ListInfo = listinfo;
}
// Add Bulletlist end
return document;
I didn't use the AddToList method to have it all in one place. In a real application I'd use that method (it's a user-defined method, code given in this thread).
A little bit more concise than the above answer:
var document = new Document();
var style = document.AddStyle("BulletList", "Normal");
style.ParagraphFormat.LeftIndent = "0.5cm";
style.ParagraphFormat.ListInfo = new ListInfo
{
ContinuePreviousList = true,
ListType = ListType.BulletList1
};
var section = document.AddSection();
section.AddParagraph("Bullet 1", "BulletList");
section.AddParagraph("Bullet 2", "BulletList");
Style is only created once, including listinfo, and can be re-used everywhere.
With PDFsharp you must draw the bullets yourself.
With MigraDoc you add a paragraph and set paragraph.Format.ListInfo for this paragraph to create a bullet list.
The linked thread shows two helper routines:
DefineList() only sets a member variable so next time a new list will be created.
AddToList() is called for each entry.
Simply call DefineList() to start a new bullet list, then call AddToList() for every entry.
DefineList() makes a big difference for numbered lists.
Adapt the helper routines for your needs.