3

Adding a ToC in LO Writer is no problem, but how to insert a ToC in a ods file? For a workbook with tables longer than one page to be distributed as printout (not as file), it would be nice to have a ToC at the first sheet, listing all other sheets in the same ods file with page numbers.

I've tried to insert a Writer OLE Object, which allows for adding a ToC (inside the OLE object...), but the object seems to ignore the Headings from the other sheets. Inserting the sheet names using hyperlinks would be ok, but i found no way to also insert the page numbers.

If this requires a macro (StarBasic preferred), i'll offer a bounty.

Any ideas?

PS: I've found a Q/A in the OpenOffice.org forums dating from 2008, but i don't understand how to implement it...

4
  • The challenge here is that page numbers are not part of spreadsheets like they are in Writer. I'm working on a macro that looks for page breaks on non-empty pages, so we'll see how it goes.
    – Jim K
    Commented May 18, 2016 at 18:57
  • @JimK: thank you for your feedback - i'll add a bounty tomorrow (question isn't eligible for a bounty yet). Since the header / footer can contain a page number field, i wonder if this is available in the sheet itself, too...
    – tohuwawohu
    Commented May 18, 2016 at 19:36
  • The header / footer uses the <text:page-number> XML tag, which is also what Writer uses for Insert -> Field. I tried putting that into the body of a Calc spreadsheet (content.xml), but the tag was ignored.
    – Jim K
    Commented May 18, 2016 at 21:28
  • For the page you linked, it seems to be suggested to create blocks and to navigate with F5... I didn't understand the outline part. Nonetheless the idea can be useful. Maybe you can find some inspiration in pitonyak, 7.18, or you can do a cycle searching for heading and heading1 cell characteristic in the other sheet (maybe 1st column only) or blocks and print areas. Tricky the pagenumber, maybe computable from the print area number. Sorry just ideas, not more...
    – Hastur
    Commented May 23, 2016 at 16:51

2 Answers 2

3
+100

Ok, here is the code I came up with:

Type PageBreakLocation
    Row As Long
    Col As Long
    Sheet As Long
End Type

Function GetLocationKey(item As PageBreakLocation)
    GetLocationKey = "s" & item.Sheet & "r" & item.Row & "c" & item.Col
End Function

Type PageOfSheet
    Sheet As Long
    Page As Long
End Type

Sub CalcTableOfContents
    used_pages = FindAllUsedPages()
    page_of_each_sheet = GetPageOfEachSheet(used_pages)
    Insert_TOC(page_of_each_sheet)
    DisplayContents(page_of_each_sheet)
End Sub

Sub DisplayContents(page_of_each_sheet As Collection)
    msg = ""
    For Each value In page_of_each_sheet
        sheet_name = ThisComponent.Sheets.getByIndex(value.Sheet).getName()
        msg = msg & "Sheet(" & value.Sheet & ") """ & sheet_name & _
            """ .....Page " & value.Page & CHR(13)
    Next
    MsgBox msg
End Sub

' Insert a Table of Contents into sheet 1.
Sub Insert_TOC(page_of_each_sheet As Collection)
    oSheet = ThisComponent.Sheets.getByIndex(0)
    oCell = oSheet.getCellByPosition(1, 1)  'B2
    oCell.SetString("Table of Contents")
    row = 3   ' the fourth row
    For Each value In page_of_each_sheet
        oCell = oSheet.getCellByPosition(1, row)  ' column B
        oCell.SetString(ThisComponent.Sheets.getByIndex(value.Sheet).getName())
        oCell = oSheet.getCellByPosition(3, row)  ' column D
        oCell.SetString("Page " & value.Page)
        row = row + 1
    Next
End Sub

' Returns a collection with key as sheet number and item as page number.
Function GetPageOfEachSheet(used_pages As Collection)
    Dim page_of_each_sheet As New Collection
    page_number = 1
    For Each used_page In used_pages
        key = CStr(used_page.Sheet)
        If Not Contains(page_of_each_sheet, key) Then
            Dim value As New PageOfSheet
            value.Sheet = used_page.Sheet
            value.Page = page_number
            page_of_each_sheet.Add(value, key)
        End If
        page_number = page_number + 1
    Next
    GetPageOfEachSheet = page_of_each_sheet
End Function

' Looks through all used cells and adds those pages.
' Returns a collection of used pages.
Function FindAllUsedPages
    Dim used_pages As New Collection
    For Each addr in GetFilledRanges()
        FindPagesForRange(addr, used_pages)
    Next
    FindAllUsedPages = used_pages
End Function

' Returns an array of filled cells.
' Elements are type com.sun.star.table.CellRangeAddress.
' Note: oSheet.getPrintAreas() seemed like it might do this, but in testing,
'       it always returned empty.
Function GetFilledRanges
    allRangeResults = ThisComponent.createInstance( _
        "com.sun.star.sheet.SheetCellRanges")
    For i = 0 to ThisComponent.Sheets.getCount() - 1
        oSheet = ThisComponent.Sheets.getByIndex(i)
        With com.sun.star.sheet.CellFlags
            printable_content = .VALUE + .DATETIME + .STRING + .ANNOTATION + _
                                .FORMULA + .OBJECTS
        End With
        filled_cells = oSheet.queryContentCells(printable_content)
        allRangeResults.addRangeAddresses(filled_cells.getRangeAddresses(), False)
    Next
    ' Print allRangeResults.getRangeAddressesAsString()
    GetFilledRanges = allRangeResults.getRangeAddresses()
End Function

' Looks through the range and adds any pages to used_pages.
' Note: row.IsStartOfNewPage is only for manual breaks, so we do not use it.
Sub FindPagesForRange(range As Object, used_pages As Collection)
    oSheet = ThisComponent.Sheets.getByIndex(range.Sheet)
    aPageBreakArray = oSheet.getRowPageBreaks()
    Dim used_row_breaks() As Variant
    Dim used_col_breaks() As Variant
    prev_break_row = 0
    For nIndex = 0 To UBound(aPageBreakArray())
        break_row = aPageBreakArray(nIndex).Position
        If break_row = range.StartRow Then
            Append(used_row_breaks, break_row)
        ElseIf break_row > range.StartRow Then
            Append(used_row_breaks, prev_break_row)
        End If
        If break_row > range.EndRow Then
            Exit For
        End If
        prev_break_row = break_row
    Next
    prev_break_col = 0
    aPageBreakArray = oSheet.getColumnPageBreaks()
    For nIndex = 0 To UBound(aPageBreakArray())
        break_col = aPageBreakArray(nIndex).Position
        If break_col = range.StartColumn Then
            Append(used_col_breaks, break_col)
        ElseIf break_col > range.StartColumn Then
            Append(used_col_breaks, prev_break_col)
        End If
        If break_col > range.EndColumn Then
            Exit For
        End If
        prev_break_col = break_col
    Next
    For Each row In used_row_breaks()
        For Each col In used_col_breaks()
            Dim location As New PageBreakLocation
            location.Sheet = range.Sheet
            location.Row = row
            location.Col = col
            key = GetLocationKey(location)
            If Not Contains(used_pages, key) Then
                used_pages.Add(location, key)
            End If
        Next col
    Next row
End Sub

' Returns True if the collection contains the key, otherwise False.
Function Contains(coll As Collection, key As Variant)
    On Error Goto ErrorHandler
    coll.Item(key)
    Contains = True
    Exit Function
ErrorHandler:
    If Err <> 5 Then
         MsgBox "Error " & Err & ": " & Error$ & " (line : " & Erl & ")"
    End If
    Contains = False
End Function

' Append an element to an array, increasing the array's size by 1.
Sub Append(array() As Variant, new_elem As Variant)
    old_len = UBound(array)
    ReDim Preserve array(old_len + 1) As Variant
    array(old_len + 1) = new_elem
End Sub

It's probably a good idea to put this code in its own module since it's so large. Then to run it, go to Tools -> Macros -> Run Macro and execute the CalcTableOfContents routine.

To make it get the right page numbers, there is one important trick. The code only checks the page number of each cell. So if a cell's contents crosses into two pages, it will only count the first page.

To remedy this problem, add some content in a cell on the second page. Set it to not printable by going to Format -> Cells -> Cell Protection and checking "Hide when printing." This will force the macro to recognize the second page.

If all goes well, it should show a result like this on sheet 1:

Calc Table of Contents

Credits:

4
  • Whooaa - looks great! I'll need some time to check it thoroughly, but it seems to do what it should. There are only some minor points - in ToC, i would prefer the "real" sheet names instead of "Sheet 1", "Sheet 2" and so on. And sometimes, it seems to miss one sheet. But the answer is definitely worth the bounty, since it seems there are no other answers...
    – tohuwawohu
    Commented May 24, 2016 at 8:18
  • To get the sheet name instead of the number, you could write sheet_name = ThisComponent.Sheets.getByIndex(sheet - 1).getName() in the Insert_TOC routine. Strange that it would sometimes miss a sheet. Is there at least one cell with printable content on all of the sheets?
    – Jim K
    Commented May 24, 2016 at 18:44
  • 1
    Setting the sheet name works great - i took the liberty to edit your answer to implement this modification (hope this is ok...) Regarding the missing entries: It seems to work if every sheet spans over more than one page. With sheets fitting on a single page, some sheets seem to be ignored.
    – tohuwawohu
    Commented May 25, 2016 at 9:59
  • I could have had it working right the first time if you had let me use Python. :) Anyway, I fixed several bugs and updated the answer, so hopefully it will work now.
    – Jim K
    Commented May 25, 2016 at 22:31
0

Here's a different approach. I wondered if there's a way to determine the page breaks using IsStartOfNewPage. This works after making LO Calc calculating the page breaks by switching into the PageBreak View and back. Now, counting pages is quite easy by iterating over all used cells (using the current sheet's Cursor and GotoEndOfUsedArea).

I didn't test if cells spanning multiple pages will lead to wrong page count. Also, i assume that the resulting ToC will never take more than one page.

Option Base 0
Option Explicit

Private Type SheetInformation
    SheetIndex As Long
    SheetName As String
    PageStart as Long
    PageEnd as Long
    PageCount As Long
End Type

Public Sub Calc_ToC

    If (False = IsSpreadsheetDoc(ThisComponent)) Then
        MsgBox "Works only for spreadsheets!"
        Exit Sub
    End If
    ThisComponent.LockControllers

    Dim mySheets(ThisComponent.Sheets.getCount() - 1) As New SheetInformation
    Dim origSheet As Long
    origSheet = ThisComponent.getCurrentController.ActiveSheet.RangeAddress.Sheet

    Call collectSheetInfo(mySheets)

    dim document   as Object
    dim dispatcher as Object
    document   = ThisComponent.CurrentController.Frame
    dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")

    dim args1(0) as new com.sun.star.beans.PropertyValue
    args1(0).Name = "Nr"
    args1(0).Value = origSheet + 1
    dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args1())

    ThisComponent.unlockControllers()

    Call insertToc(mySheets)

End Sub

Private Sub collectSheetInfo(allSheetsInfo() as New SheetInformation)
    Dim i As Long
    Dim maxPage As Long
    maxPage = 0

    For i = 0 To UBound(allSheetsInfo)
        Dim sheetInfo As New SheetInformation
        sheetInfo.SheetIndex = i
        sheetInfo.SheetName = ThisComponent.Sheets.getByIndex(sheetInfo.SheetIndex).getName()
        Call getPageCount(sheetInfo)
        sheetInfo.PageStart = maxPage + 1
        sheetInfo.PageEnd = sheetInfo.PageStart + sheetInfo.PageCount - 1
        maxPage = sheetInfo.PageEnd
        allSheetsInfo(i) = sheetInfo
    Next

End Sub

Private Sub getPageCount(s As SheetInformation)
    Dim oSheet, oCell, oCursor As Object
    Dim i, j, pageCount As Long
    Dim isHorizontalPageBreak, isVerticalPageBreak As Boolean

    dim document   as Object
    dim dispatcher as Object
    document   = ThisComponent.CurrentController.Frame
    dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")

    dim args1(0) as new com.sun.star.beans.PropertyValue
    args1(0).Name = "Nr"
    args1(0).Value = s.SheetIndex + 1
    dispatcher.executeDispatch(document, ".uno:JumpToTable", "", 0, args1())

    args1(0).Name = "PagebreakMode"
    args1(0).Value = true
    dispatcher.executeDispatch(document, ".uno:PagebreakMode", "", 0, args1())
    dim args2(0) as new com.sun.star.beans.PropertyValue
    args2(0).Name = "NormalViewMode"
    args2(0).Value = true
    dispatcher.executeDispatch(document, ".uno:NormalViewMode", "", 0, args2())

    oSheet = ThisComponent.Sheets.getByIndex(s.SheetIndex)

    oCursor = oSheet.createCursor
    oCursor.GotoEndOfUsedArea(True)

    pageCount = 1

    For i=0 To oCursor.RangeAddress.EndColumn
        For j=0 To oCursor.RangeAddress.EndRow
            oCell = oSheet.GetCellByPosition(i,j)
            isHorizontalPageBreak = Abs(cINT(oCell.Rows.getByIndex(0).IsStartOfNewPage))
            isVerticalPageBreak = Abs(cINT(oCell.Columns.getByIndex(0).IsStartOfNewPage))
            If i = 0 Then
                If isHorizontalPageBreak Then
                    pageCount = pageCount + 1
                End If
            ElseIf j = 0 Then
                If isVerticalPageBreak Then
                    pageCount = pageCount + 1
                End If
            Else
                If (isHorizontalPageBreak AND isVerticalPageBreak) Then
                    pageCount = pageCount + 1
                End if
            End if
        Next j
    Next i
    s.pageCount = pageCount

End Sub

''' -------------------------------------------------------------
''' IsSpreadsheetDoc - Check if current document is a calc file
''' -------------------------------------------------------------
''' Source: "Useful Macro Information For OpenOffice.org By
''' Andrew Pitonyak", Ch. 6.1
''' -------------------------------------------------------------
Private Function IsSpreadsheetDoc(oDoc) As Boolean
  Dim s$ : s$ = "com.sun.star.sheet.SpreadsheetDocument"
  On Local Error GoTo NODOCUMENTTYPE
  IsSpreadsheetDoc = oDoc.SupportsService(s$)
  NODOCUMENTTYPE:
  If Err <> 0 Then
     IsSpreadsheetDoc = False
    Resume GOON
    GOON:
  End If
End Function

Private Sub Result(s() As SheetInformation)
    Dim msg As String
    Dim i As Integer
    Dim obj As SheetInformation
    msg = ""

    For i = 0 To UBound(s)
        obj = s(i)
        With obj
            msg = msg & .SheetName & " (Index: " & .SheetIndex & _
            ") - Pages: " & .PageCount & _
            " - from/to: " & .PageStart & "/" & .PageEnd & CHR(13)
        End With
    Next
    MsgBox(msg)
End Sub

Private Sub insertToC(s() As SheetInformation)

    Select Case MsgBox("Insert ToC on cursor position?" & CHR(10) & _
        "(Yes: Insert at cursor; No: stop macro)", 36)
        Case 6 'Yes - insert at cursor position'
            Call DoInsert(s)
        Case 7 'No - insert on new sheet'
            ThisComponent.unlockControllers()
            Exit Sub
    End Select
End Sub

Private Sub DoInsert(s() As SheetInformation)

    Dim oSheet, oCell, startCell As Object
    Dim sheet,rowStart, colStart, row, col, start As Long
    Dim sName As String
    Dim currentSheet As SheetInformation
    Dim newToc As Boolean

    oSheet = ThisComponent.getCurrentController.ActiveSheet
    startCell = ThisComponent.getCurrentSelection() 
    oCell = startCell
    rowStart = startCell.CellAddress.Row
    colStart = startCell.CellAddress.Column
    oCell.SetString("Table of Contents")
    For sheet = 1 to Ubound(s) + 1
        currentSheet = s(sheet - 1)
        row = rowStart + sheet
        oCell = oSheet.getCellByPosition(colStart, row)  ' column B
        oCell.SetString(currentSheet.SheetName)
        oCell = oSheet.getCellByPosition(colStart + 2, row)  ' column D
        start = currentSheet.PageStart

        oCell.SetString("Page " & start)
    Next
    ThisComponent.unlockControllers()
End Sub

I've used some example code by Andrew Pitonyak ("Useful Macro Information For OpenOffice.org By Andrew Pitonyak (ODT)" and "OpenOffice.org Macros Explained (PDF)") and by Villeroy's Cell introspection module, and of course some of JimK's solution.

EDIT:

The macro doesn't test every page if it contains printable content. It simply assumes that the complete "used" cell range (identified using GotoEndOfUsedArea) should be taken into account when creating the ToC. Thus, it may count empty pages as pages to print. So, it might yield bad results for sparsely filled sheets. But i hope it behaves more reliably for most cases where there are no empty pages.

So, it will expect the following sheets to be printed on six pages, even if one page (without X) may stay empty:

+-+-+     +-+-+     +-+-+
|X|X|     |X|X|     |X| |
+-+-+     +-+-+     +-+-+
|X| |     | |X|     | | |
+-+-+     +-+-+     +-+-+
|X|X|     |X|X|     | |X|
+-+-+     +-+-+     +-+-+
4
  • Interesting, I avoided IsStartOfNewPage because it says here that it is only for manual breaks. However your macro does seem to be working, at least partly. My test document has 13 pages when printed as PDF, but this macro shows the last sheet as starting on page 20. For one thing, it counts a blank sheet that did not actually get printed in the PDF, but that only accounts for one page off. I'll try to test it some more and see what else is causing problems.
    – Jim K
    Commented May 25, 2016 at 23:35
  • Ok, after looking over your code a little more, I think both of us have some good ideas. Also I found the other problem your code has with my test document. Currently getPageCount() assumes that pages with content are always in a full rectangular grid. So for example if a sheet's print area is two pages wide by two tall, it counts this as 4 pages. But what if only the first and third pages in that grid have printable content? In other words, page (0,0) and page (1,1) print, but page (0,1) and page (1,0) do not. Then it should count 2 pages, not 4.
    – Jim K
    Commented May 26, 2016 at 1:22
  • @JimK: You're right, my approach expects all four pages to print, not only the two with printable content. I didn't test this case, but i assumed the printed PDF should contain all four pages. I think there's no way to create a "correct" ToC if the user will remove some pages later from printing. And with a sparsely filled 2x2 page layout, the user may be interested in the "empty pages" to get the complete table as layed out on the screen. But since there are too many possible use cases, i think they can't be covered by one macro alone.
    – tohuwawohu
    Commented May 26, 2016 at 12:29
  • BTW: i would be glad to remove those strange dispatcher / uno calls recorded using the macro recorder, and replace them with their StarBasic equivalents. This would increase code readability...
    – tohuwawohu
    Commented May 26, 2016 at 12:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .