PowerPoint Programming: Issues while trying to access ruler margins - c#

I am trying to access the indentation levels of various bulleted list items. So I created a simple function:
private float[] findIndentSpacing(TextRange t, int level) {
if(level == 1) {
RulerLevel rl = t.Parent.Ruler.Levels(2);
//bullet must start at 0 on the first level for now
return new float[2] { 0, rl.LeftMargin * Settings.Scaler() };
} else {
RulerLevel rl = t.Parent.Ruler.Levels[level];
return new float[2] { rl.FirstMargin * Settings.Scaler(), rl.LeftMargin * Settings.Scaler() };
}
}
So that first if statement is a work around. The first level LeftMargin always returns: -2.14748365E+9 for some reason. I've tried to just grab levels after the first and they return actual values. That being said, after one level has been accessed all other levels change and become equal. For example if I try to access: t.Parent.Ruler.Levels[2].FirstMargin, then for some reason t.Parent.Ruler.Levels[3].FirstMargin become the same, and so forth. LeftMargin changes also.
I've tried accessing the ruler object in different ways: by selection, by shape, by text and every way I've thought to try the result is the same.
Ideas?
More info:
I read the following threads, but they are more about writing than reading, but I feel like the problem is similar: PowerPoint Programming: Indentation with Ruler margin levels not working?
http://answers.microsoft.com/en-us/office/forum/office_2007-customize/why-shapetextframerulerlevelsi-cant-set-the-bullet/9eac3e46-b13b-433e-b588-216ead1d9c1a?tab=AllReplies#tabs
I made this one: http://answers.microsoft.com/en-us/office/forum/office_2010-customize/find-bullet-spacing-information-in-an-automated/4525b6b8-6331-4f33-8127-789ea3641589?page=1&tm=1336535132591

In 2007 and 2010 I think you'll need to work with TextRange2 and TextFrame2 objects.
In PPT 2003 and previous, the TextFrame could have 5 indent levels, and all paragraphs at a given indent level shared the same LeftMargin and FirstMargin.
From 2007 on, TextFrames can have up to 9 indent levels, and each paragraph can have its own Left/First margins, independent of the margins set on other paragraphs at the same indent level.
Try this in PPT's VBA IDE. Select the text you're looking at then:
Sub Levels()
Dim oSh as Shape
Dim x As Long
Set oSh = ActiveWindow.Selection.ShapeRange(1)
With oSh.TextFrame2.Ruler
For x = 1 to .Count
Debug.Print .Levels(x).FirstMargin
Debug.Print .Levels(x).LeftMargin
Next
End With
End Sub

Related

C# WindowsForm Add DatagridView Content into PowerPoint's slide

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);
}
}

How to fill remaining space of a line in iText7

I'm trying to fill the remaining space of the last line of a paragraph using iText7 with C#:
var par = new Paragraph(text);
par.Add(c);
document.Add(par);
How can i add - char to fill the space left by the line? Something like LineSeparator(new DashedLine() but from the beginning on the last character of my paragraph instead of new line.
You can use the concept of tabs and tab stops for it. This concept is not iText-specific.
Roughly speaking you can define points (tab stops) and adding a tab would "jump" to the next point. In your case the tab stop is the end of the line and you only need one tab.
Here is a complete example that uses small dashes on the baseline as the filling. You can implement ILineDrawer yourself to customize the behavior or subclass/configure an existing implementation. The code is in Java, but to convert it to C# you basically need to do some capitalization and that's it.
Document doc = ....;
Paragraph p = new Paragraph("Hello world").add(new Tab());
ILineDrawer filling = new DashedLine();
PageSize pageSize = doc.getPdfDocument().getDefaultPageSize();
Rectangle effectivePageSize = doc.getPageEffectiveArea(pageSize);
float rightTabStopPoint = effectivePageSize.getWidth();
TabStop tabStop = new TabStop(rightTabStopPoint, TabAlignment.LEFT, filling);
p.addTabStops(tabStop);
doc.add(p);
Result looks as follows:

Office.Interop.Word and Lists with SubLevels

I've been scouring the interwebs for documentation leading me to be able to create lists. In doing so, I've not really been able to find any documentation that will allow me to create `lists within lists.
I've tried using the built-in macro recorder, but for whatever reason, it behaves differently when recording vs. when not recording (e.g. when I create a list item, and hit enter + tab, it doesn't create a sub list).
I've found "The Wordmeister's" MSDN post which helped me get to making a list, but lists within lists doesn't work so well for me.
Word.Paragraph p2 = doc.Paragraphs.Add();
Word.Range p2rng = p2.Range;
object oTrue = true;
object oFalse = false;
object oListName = "TreeList";
Word.ListTemplate lstTemp = doc.ListTemplates.Add(ref oTrue, ref oListName);
int l;
p2rng.ParagraphFormat.TabIndent(1);
p2rng.Text = "Rates:\r\nLevel 1\rLevel 1.1\rLevel 1.2\rLevel 2\rLevel 2.1\rLevel 2.1.1";
l = 1;
lstTemp.ListLevels[l].NumberFormat = "%" + l.ToString() + ".";
lstTemp.ListLevels[l].NumberStyle = Word.WdListNumberStyle.wdListNumberStyleArabic;
lstTemp.ListLevels[l].NumberPosition = wordApp.CentimetersToPoints(0.5f * (l - 1));
lstTemp.ListLevels[l].TextPosition = wordApp.CentimetersToPoints(0.5f * l);
l = 2;
lstTemp.ListLevels[l].NumberFormat = "%" + (l - 1).ToString() + ".%" + l.ToString() + ".";
lstTemp.ListLevels[l].NumberStyle = Word.WdListNumberStyle.wdListNumberStyleArabic;
lstTemp.ListLevels[l].NumberPosition = wordApp.CentimetersToPoints(0.5f * (l - 1));
lstTemp.ListLevels[l].TextPosition = wordApp.CentimetersToPoints(0.5f * l);
l = 3;
lstTemp.ListLevels[l].NumberFormat = "%" + (l - 2).ToString() + "%" + (l - 1).ToString() + ".%" + l.ToString() + ".";
lstTemp.ListLevels[l].NumberStyle = Word.WdListNumberStyle.wdListNumberStyleArabic;
lstTemp.ListLevels[l].NumberPosition = wordApp.CentimetersToPoints(0.5f * (l - 1));
lstTemp.ListLevels[l].TextPosition = wordApp.CentimetersToPoints(0.5f * l);
object oListApplyTo = Word.WdListApplyTo.wdListApplyToWholeList;
object oListBehavior = Word.WdDefaultListBehavior.wdWord10ListBehavior;
p2rng.ListFormat.ApplyListTemplate(lstTemp, ref oFalse, ref oListApplyTo, ref oListBehavior);
All credit to Cindy Meister for this code, it is only slightly modified to work for my use case.
The above results in the following:
Basically, how do you create multi level lists (like the following image) with lists within lists?
It's not really possible to create a "list within a list" in Word.
What you can do is put static text in the list level, in front of the dynamic number. This is the same idea as using "Chapter" or "Section" in front of the level's number, except in this case it should be a bullet symbol.
The place to define this, based on the code sample in the question, is:
lstTemp.ListLevels[l].NumberFormat = "%" + l.ToString() + ".";
As part of the NumberFormat string, in other words. For symobls this will require a conversion from unicode hex or decimal to the String data type. For example, for a solid, round bullet and an outline, round bullet for levels 1 and 2, respectively (hard-coding the list level for purposes of clarity):
char Symbol1 = (char)9679;
char Symbol2 = (char)9675;
lstTemp.ListLevels[1].NumberFormat = Symbol1.ToString() + "\t%" + l.ToString() + ".";
lstTemp.ListLevels[2].NumberFormat = Symbol2.ToString() + "\t%" + 2.ToString() + ".";
Lets try providing an answer that doesn't get deleted.
The updated list within lists example provided by #Jaberwocky can be achieved using the technique I explained in a previous post.
MS-Word: Cross reference to custom reference type
To apply the above to the specific instance required by #Jaberwock we need to amend the number formats of the list templates to which styles are linked. I'll use Word to set up the styles and required multilevel list and then include a short VBA macro which shows how to amend the list number format.
In line with the link above we first need to create our styles. The emulate the list within list example above we need to define two styles. I've defined 'ListWithinList 1' and 'ListWithinList 2'.
The key settings for these two styles are to set the outline level as 1 and 2 respectively, and to set appropriate tab stops. I've used tabs at 1,2,3 and 4 cm. Add some text to a word document and apply the styles. I've included the navigation pane in the picture below so that we can see the indentation due to the outline level of the styles
The next step is to define the multilevel list and link each level to the relevant style
Settings for outline level 1
settings for outline level 2
Our text now looks like this
I've used Word up to this point to avoid the tediousness of setting up styles and list templates programatically.
Let's now modify the format of the list numbering using a snippet of VBA.
Option Explicit
Public Sub AddTextToListNumber()
Dim my_prefix(1 To 2) As String
Dim my_index As Long
my_prefix(1) = ChrW(&H25AA) & vbTab ' small black square
my_prefix(2) = ChrW(&H25AB) & vbTab ' small white square
For my_index = 1 To 2
With ActiveDocument.Styles("ListWithinList " & CStr(my_index)).ListTemplate.ListLevels(my_index)
.numberformat = my_prefix(my_index) & .numberformat
End With
Next
End Sub
If we run the code above then the text in our document becomes
Which looks a bit ugly because of the 1cm tab stops.
If there is anything that isn't clear above please add a comment and if possible I'll update the answer.
NOTE: We didn't need the VBA code to complete setting up the list formats as we could have used appropriate Alt+XXXX keyboard sequences to insert the characters in the number format box of the multilevel list dialog box.

Finding cycles Depth-first-search

I am trying to repair graphs by deleting one edges. The only problem I am running in to is, when there are multiple cycles in the graph for example: 0 3, 2 3, 0 2, 1 2, 3 1. This can be fixed by extracting 3 1, but how do I let the program no that 3 1 is the edge that has to be removed?
Any suggestions? :)
Formatted code from comment...
...
else if (backedges.Count > 1)
{
foreach (Side side in backedges)
{
Node end = Side.node2;
Node begin = Side.node1;
List<Side> allsidesycle = new List<Side>();
while (begin != Side.node2)
{
end = begin;
begin = begin.pi;
Side be = new Side(begin, end);
allsidescycle.Add(be);
}
To find cycles you may want to use breadth first search (bfs).
Using bfs on unweighted (or equally weighted) graph grantees that then the first time a node is visited is the shortest path to that node.
If you visit it for the second time - you have a cycle. To remove it modify the graph by deleting that 2nd edge.

Merging PDFs programatically while maintaining the "Combine files..." bookmark structure?

I originally asked this on Adobe's forums but yet to receive any reponses.
I have to merge a set of many (100+) PDF files into a single report on a weekly basis, and so far, I have been doing the process by hand by selecting the files, right clicking, and selecting "Combine supported files in Acrobat". What I would like to do is replicate this exact same process programmatically (preferrably in Excel/VBA, but C# or Batch commands are acceptable alternatives). I currently have code that will combine pdf files, but it it does not keep the bookmark structure the same way that "Combine supported files in Acrobat" does.
In other words, say I have three files called "A.pdf", "B.pdf", and "C.pdf", and each file contains two bookmarks called "Bkmrk 1" and "Bkmrk 2". I want to programatically combine these three files into a single file that has 9 bookmarks that look like the structure below:
A
Bkmrk 1
Bkmrk 2
B
Bkmrk 1
Bkmrk 2
C
Bkmrk 1
Bkmrk 2
I at first tried automating the process via the Acrobat SDK, but from what I understand the Acrobat SDK does not allow programs to interact with the dialog box that appears when you execute the "Combine Files" menu option, so that did not work. I also tried the option to programatically insert pages from one pdf file into another, but that does not produce the bookmark structure that I am looking for, nor does it let me manipulate the bookmark heirarchy to create the bookmark structure I am looking for.
Does anyone have an idea of how to do this? Any help would be greatly appreciated!
This was pure hell to get working, so I'm happy to share what I've got. This was adapted from code I found here, and will merge files, and put bookmarks at each merge point:
Private mlngBkmkCounter As Long
Public Sub updfConcatenate(pvarFromPaths As Variant, _
pstrToPath As String)
Dim origPdfDoc As Acrobat.CAcroPDDoc
Dim newPdfDoc As Acrobat.CAcroPDDoc
Dim lngNewPageCount As Long
Dim lngInsertPage As Long
Dim i As Long
Set origPdfDoc = CreateObject("AcroExch.PDDoc")
Set newPdfDoc = CreateObject("AcroExch.PDDoc")
mlngBkmkCounter = 0
'set the first file in the array as the "new"'
If newPdfDoc.Open(pvarFromPaths(LBound(pvarFromPaths))) = True Then
updfInsertBookmark "Test Start", lngInsertPage, , newPdfDoc
mlngBkmkCounter = 1
For i = LBound(pvarFromPaths) + 1 To UBound(pvarFromPaths)
Application.StatusBar = "Merging " & pvarFromPaths(i) & "..."
If origPdfDoc.Open(pvarFromPaths(i)) = True Then
lngInsertPage = newPdfDoc.GetNumPages
newPdfDoc.InsertPages lngInsertPage - 1, origPdfDoc, 0, origPdfDoc.GetNumPages, False
updfInsertBookmark "Test " & i, lngInsertPage, , newPdfDoc
origPdfDoc.Close
mlngBkmkCounter = mlngBkmkCounter + 1
End If
Next i
newPdfDoc.Save PDSaveFull, pstrToPath
End If
ExitHere:
Set origPdfDoc = Nothing
Set newPdfDoc = Nothing
Application.StatusBar = False
Exit Sub
End Sub
The insert-bookmark code... You would need to array your bookmarks from each document, and then set them
Public Sub updfInsertBookmark(pstrCaption As String, _
plngPage As Long, _
Optional pstrPath As String, _
Optional pMyPDDoc As Acrobat.CAcroPDDoc, _
Optional plngIndex As Long = -1, _
Optional plngParentIndex As Long = -1)
Dim MyPDDoc As Acrobat.CAcroPDDoc
Dim jso As Object
Dim BMR As Object
Dim arrParents As Variant
Dim bkmChildsParent As Object
Dim bleContinue As Boolean
Dim bleSave As Boolean
Dim lngIndex As Long
If pMyPDDoc Is Nothing Then
Set MyPDDoc = CreateObject("AcroExch.PDDoc")
bleContinue = MyPDDoc.Open(pstrPath)
bleSave = True
Else
Set MyPDDoc = pMyPDDoc
bleContinue = True
End If
If plngIndex > -1 Then
lngIndex = plngIndex
Else
lngIndex = mlngBkmkCounter
End If
If bleContinue = True Then
Set jso = MyPDDoc.GetJSObject
Set BMR = jso.bookmarkRoot
If plngParentIndex > -1 Then
arrParents = jso.bookmarkRoot.Children
Set bkmChildsParent = arrParents(plngParentIndex)
bkmChildsParent.createchild pstrCaption, "this.pageNum= " & plngPage, lngIndex
Else
BMR.createchild pstrCaption, "this.pageNum= " & plngPage, lngIndex
End If
MyPDDoc.SetPageMode 3 '3 — display using bookmarks'
If bleSave = True Then
MyPDDoc.Save PDSaveIncremental, pstrPath
MyPDDoc.Close
End If
End If
ExitHere:
Set jso = Nothing
Set BMR = Nothing
Set arrParents = Nothing
Set bkmChildsParent = Nothing
Set MyPDDoc = Nothing
End Sub
To use:
Public Sub uTest_pdfConcatenate()
Const cPath As String = "C:\MyPath\"
updfConcatenate Array(cPath & "Test1.pdf", _
cPath & "Test2.pdf", _
cPath & "Test3.pdf"), "C:\Temp\TestOut.pdf"
End Sub
You might need to consider a commercial tool such as Aspose.Pdf.Kit to get the level of flexibility you're after. It does support file concatenation and bookmark manipulation.
There's a 30 day unlimited trial so you can't really lose out other than time if it doesn't work for you.
Use iText# (http://www.itextpdf.com/). imho it is one of the best PDF-tools around. A code to do (approximately) what you want can be found here http://java-x.blogspot.com/2006/11/merge-pdf-files-with-itext.html
Do not worry that all examples talk about Java, the classes and functions are the same in .NET
hth
Mario
Docotic.Pdf library can merge PDF files while maintaining outline (bookmarks) structure.
There is nothing special should be done. You just append all documents one after another and that's all.
using (PdfDocument pdf = new PdfDocument())
{
string[] filesToMerge = ...
foreach (string file in filesToMerge)
pdf.Append(file);
pdf.Save("merged.pdf");
}
Disclaimer: I work for Bit Miracle, vendor of the library.
The Acrobat SDK does let you create and read bookmarks. Check your SDK API Reference for:
PDDocGetBookmarkRoot()
PDBookmark* (AddChild, AddNewChild, GetNext, GetPrev... lots of functions in there)
If the "combine files" dialog doesn't give you the control you need, make your own dialog.

Categories