Automating OpenOffice.org - Part 2
From LXF Wiki
AUTOMATING OPENOFFICE.ORG BASIC SCRIPTING SERIES
- Part 1: OOo Basic: Use macros in Writer
- Part 2: OOo Basic: Use macros in Calc
- Part 3: OOo Basic: Query databases
(Original version written by Mark Bain for LXF issue 81.)
PART 2 Keep spreadsheets at arm’s length and work with data from the console
| Table of contents |
OOo Basic Use macros in Calc
From Charles Babbage’s Difference Engine to OOo’s spreadsheet Calc, number crunching was always meant to be automated. Taking some of the pain and monotony out working with columns of data is one of the great reasons for using a spreadsheet in the first place. Thanks to a combination of OOo Basic and Calc, it’s possible not only to automate of the most arduous tasks but, as I’ll show you, to manipulate interesting data directly from the command line.
As with last month, the first step to macro nirvana is creating a document. The code to open a new, blank Writer document is:
sub main loadNewFile end sub sub loadNewFile dim doc as object, desk as object, myFile as string, Dummy() myFile = “private:factory/swriter” desk = CreateUnoService(“com.sun.star.frame.Desktop”) doc = desk.loadComponentFromUrl(myFile,”_blank”,0,Dummy()) end sub
If you look through the code you will see that the type of file to be opened is defined by the line
myFile = “private:factory/swriter”
It’s then just a matter of knowing what to put in instead of swriter. To open a spreadsheet we need to change it to scalc:
myFile = “private:factory/scalc”
Remember to be lazy
Now, I know what you’re thinking – you don’t want a separate subroutine for each file type; you just want a single subroutine to do all the work. Well that’s exactly how a good programmer should think, and here’s how you’d do it:
sub main loadNewFile(“scalc”) end sub sub loadNewFile (filetype as string) dim doc as object, desk as object, myFile as string, Dummy() myFile = “private:factory/” & filetype desk = CreateUnoService(“com.sun.star.frame.Desktop”) doc = desk.loadComponentfromurl(myFile,”_blank”,0,Dummy()) end sub
Notice how we now send the file type to the subroutine, giving us much more flexibility. Most important of all, we only need one subroutine regardless of whether we want to open a Writer document or a Calc spreadsheet. If you want, you can even give it a default file type, by making use of the Optional parameter and the isMissing method:
sub loadNewFile (optional filetype as string) if isMissing(filetype) then filetype = “scalc” end if
OK – now we can open up a blank spreadsheet – but what about writing to one of the cells? This next subroutine will do just that:
sub writeToCell dim sheet as object, cell as object sheet=thisComponent.sheets(0) cell=sheet.getCellByPosition(0,0) cell.string=”Hello World” end sub
You’ll have to remember to run this from the Main subroutine.
It may be worth looking through the writeToCell subroutine just to understand the basics fully. We’ve seen thisComponent before (when we looked at OOo Basic and the Writer document) and it simply refers to the current document (in this case, the spreadsheet) that we’re in. Next we select the sheet that we’re dealing with, which is sheet(0) – the first sheet (or Sheet1) in Calc. Sheet(1) would refer to the second sheet, and so on. Finally we select the cell that we want by using the getCellByPosition method, which requires the column number and row number to be input. Position (0,0) refers to A1, (1,0) to B1, (0,1) to A2 etc. This is great, but the order of your sheets may change; what if you want to refer to them by name?. No problem – instead of the sheets statement use the getByName method:
sheet=thisComponent.sheets.getByName(“Sheet1”)
We’ve seen how easy it is to write text to the document (even easier than it is in Writer), so let’s start using the spreadsheet to do something useful:
sub simple_maths dim sheet as object, cell as object sheet=thisComponent.sheets.getByName(“Sheet1”) cell=sheet.getCellByPosition(0,0) cell.value=10 cell=sheet.getCellByPosition(0,1) cell.value=10 cell=sheet.getCellByPosition(0,2) cell.formula=”=A1+A2” end sub
Admittedly that’s not particularly useful, but it does show you how simple it is to load the spreadsheet with data, and then to manipulate that data. It can be made more useful by allowing the numbers to be input to the subroutine:
sub simple_maths(numbA as double, numbB as double) dim sheet as object, cell as object sheet=thisComponent.sheets.getByName(“Sheet1”) cell=sheet.getCellByPosition(0,0) cell.value=numbA cell=sheet.getCellByPosition(0,1) cell.value=numbB cell=sheet.getCellByPosition(0,2) cell.formula=”=A1+A2” end sub
Now you just need to amend the Main subroutine:
simple_maths(12.5,35.7)
This is a very simple example, and it would be quicker to type the details directly into the spreadsheet. However, it’s only meant to be a starting point, and you can start making the operation as complicated as you require. You may also think that only passing two numbers into the subroutine is just too limiting – you may want to pass 10 figures, or 100 or 1,000. Fortunately, it’s very easy to pass an array to a subroutine:
sub main loadNewFile simple_maths_array(array(45,67,89,34)) end sub sub simple_maths_array(numbers) dim sheet as object, cell as object, r as integer, sum as double sheet = thisComponent.sheets.getByName(“Sheet1”) sum = 0 for r = 0 to ubound(numbers) sum = sum + numbers(r) cell = sheet.getCellByPosition(0,r) cell.value = numbers(r) next cell = sheet.getCellByPosition(0,r+1) cell.value = sum end sub
The simple_maths_array subroutine populates the first column of Sheet1 with the contents of an array of numbers, then inserts the sum of all of the numbers at the bottom. Having written information to a spreadsheet, you may well be asking if it is possible to use data in an existing one. Of course it is – I wouldn’t have mentioned it if it wasn’t. This next subroutine opens an existing spreadsheet (~/test.ods) and displays the contents of cell A1 of Sheet1:
sub dataFromExistingFile dim doc as object, desk as object, sheet as object, cell as object dim url as string, contents as double, Dummy() desk = CreateUnoService(“com.sun.star.frame.Desktop”) url=”file://~/test.ods” doc=desk.loadComponentfromurl(url,”_blank”,0,Dummy()) sheet = thisComponent.sheets.getByName(“Sheet1”) cell = sheet.getCellByPosition(0,0) contents = cell.value msgbox(contents) end sub
A thought may occur to you at this point – what happens if the cell contains text instead of a number? Surely the command contents = cell.value will cause the subroutine to crash. Actually it doesn’t: if the cell contains text, the value parameter is set to zero, thus preventing any such problems.
Adding maths to the equation
Everything we’ve looked at so far is very simple – just reading and writing to cells. How about doing something a little more interesting? How about using the mathematical formulae that are built into OpenOffice.org Calc? Let’s say that instead of just writing an array of numbers to the spreadsheet, we want the total, or the average, or even the standard deviation. We can do this by using the FunctionAccess service:
sub usingOOoFunctions(iArray) dim service as object, sheet as object, cell as object service = createUnoService( “com.sun.star.sheet. FunctionAccess” ) sheet = thisComponent.sheets.getByName(“Sheet1”) cell = sheet.getCellByPosition(0,0) cell.value = service.callFunction( “STDEV”, iArray ) end sub
As always remember to change Main so that the new subroutine can be run:
usingOOoFunctions(array(45,67,89,34))
I’m sure that you can immediately see a couple of disadvantages with usingOOoFunctions – at the moment it would only be able to calculate standard deviation, it will only use Sheet1, and it will only write to cell A1. However, by the suitable use of input parameters we can make this a very adaptable subroutine:
sub usingOOoFunctions( fType as string, sName as string, _c as integer, r as integer, iArray ) dim service as object, sheet as object, cell as object service = createUnoService( “com.sun.star.sheet.FunctionAccess” ) sheet = thisComponent.sheets.getByName(sName) cell = sheet.getCellByPosition(c,r) cell.value = service.callFunction( fType, iArray ) end sub
Modify Main so that it contains:
usingOOoFunctions(“STDEV”,”Sheet1”, 1, 1, array(45,67,89,34))
This raises an important question – how can you handle results that might cause the program to crash? For instance, you could try
usingOOoFunctions(“SQRT”,”Sheet1”, 1, 1, array(-1))
It’s probably obvious to you that this won’t work, because it calls for the square root of –1, always a no-no. You can try capturing all error-creating situations by writing code such as
if (fType <> “SQRT” and iArray(0) <> -1 ) then
but this means you’ll have to know every possible combination of function and number that could cause you a problem. The most efficient solution would be to write an error handler. Let’s look at an example (that will crash):
function dummy as double dim service as object service = createUnoService( “com.sun.star.sheet.FunctionAccess” ) dummy = service.callFunction( “SQRT”, array(-1) ) end function
Run it with
msgbox (dummy)
This will complain as soon as it gets to the return line, but we can stop that happening by introducing an ‘on error resume next’ statement at the start of the function. If an error occurs this time, the function will just move straight on to the next line of code. However you may (quite rightly) say that you don’t want the code to continue – you want it just to exit neatly. If so, you need to add some code to handle the error appropriately:
function dummy as double dim service as object on error goto errorFound service = createUnoService( “com.sun.star.sheet.FunctionAccess” ) dummy = service.callFunction( “SQRT”, array(-1) ) exit function errorFound: msgbox(“Invalid input. Result set to -1”) dummy=-1 end function
Rather than just continuing, the function will jump to the point in the code marked errorFound: – the colon (:) defines it as being a jump destination. Notice that the code contains a line stating exit function just before the error-handling portion. Without this, the error-handling code will always be run even if there is no error – we, of course, only want the error handling to operate if there actually been an error.
Functions are not subroutines
In the examples above we’ve used functions and subroutines. You may be wondering what the difference is between the two. A function and a subroutine are basically the same, except that the function will return a result. This means that when you define a function you must state which data type it is going to return. Here’s a simple example to give you the idea. First we’ll set a variable by using a subroutine:
dim sheet as object, cell as object sub main loadNewFile sheet=thisComponent.sheets(0) cell=sheet.getCellByPosition(0,0) simple_sub end sub sub simple_sub cell.value = 1 end sub
Next we’ll do the same again, but this time by using a function:
dim sheet as object, cell as object sub main loadNewFile sheet=thisComponent.sheets(0) cell=sheet.getCellByPosition(0,0) cell.value = simple_function end sub function simple_function as integer simple_function = 1 end function
Notice that the subroutine writes to the cell directly, whereas the function supplies an output that is then used to write to the cell. A second thing to take note of is that some of the variables (sheet and cell) have been made global. This means that they are made available to all of the functions and subroutines. If a variable is defined within a procedure, it only exists for the time that the subroutine or function is running (this is often referred to as the scope of the variable). This is very useful, but it does mean that you have to be very careful when it comes to the naming of variables:
dim sheet_number as integer
dim sheet as object, cell as object
sub main
loadNewFile
set_sheetnumber
sheet= _
thisComponent.sheets(sheet_number)
cell=sheet.getCellByPosition(0,0)
cell.value = sheet_number
end sub
sub set_sheetnumber
sheet_number = 1
end sub
The number 1 is written to A1 in Sheet2. If we were to insert dim sheet_number as integer into the subroutine set_sheetnumber in the example above, a new variable called sheet_number would be created. This new variable would only be accessible within the set_sheetnumber subroutine. Despite having the same name as the variable in the main subroutine, both variables are different, and can hold different values. Now we can happily read and write to any cell that we want, in any of the sheets within the spreadsheet. This means that we can have a look at the sheet names next. They’re a bit boring as they stand – Sheet1, Sheet2, Sheet3 – and not very informative. And there are only three of them anyway.
sub changeSheetNames dim sheet as object sheet = thisComponent.createInstance(“com.sun.star.sheet.Spreadsheet”) thisComponent.Sheets.insertByName(“MySheet”, Sheet) thisComponent.sheets.removebyname(“Sheet1”) thisComponent.sheets.removebyname(“Sheet2”) thisComponent.sheets.removebyname(“Sheet3”) end sub
Nice and easy – but still a bit limiting. It can be made really useful by passing in a array containing the sheet names to be created – watch:
dim i as integer for i = 0 to ubound(sheetNames) sheet = thisComponent.createInstance(“com.sun.star.sheet.Spreadsheet”) thisComponent.Sheets.insertByName(sheetNames(i), Sheet) next
On my command…
Finally, we can bring together everything that we’ve looked at in this tutorial (plus some bits from last month). The following code will run shell commands (in this case df and du), save the results to file and load the data into a spreadsheet. Here goes:
const tmpFile as string = “/tmp/myfile.tmp” const bshFile as string = “/tmp/runme.bsh” sub main theFullWorks end sub function buildCommand (ipCommand as string) as string buildCommand = “rm -f “ & tmpFile & “;” _ & ipCommand & “ | sed s/’\t’/’ ‘/g >” & tmpFile & “;” _ & “while [ “”$(grep ‘ ‘ “ & tmpFile & “)”” != “””” ];” _ & “do cat “ & tmpFile & “ | sed s/’ ‘/’ ‘/g > “ & tmpFile & “1;” _ & “mv “ & tmpFile & “1 “ & tmpFile & “;” & “done” end function sub theFullWorks dim command as string loadNewFile changeSheetNames (array(“Disk Space Usage”,”File Usage”)) command = buildCommand(“df|grep -v Filesystem”) reportSheet(command,”Disk Space Usage”) command = buildCommand(“du /| sort -nr”) reportSheet(command,”File Usage”) end sub sub reportSheet (command as string, sheetName as string) dim sheet as object, cell as object dim iNumber As Integer, oNumber As Integer, iLine As String dim i as integer, c as integer iNumber = Freefile oNumber = Freefile Open bshFile For output As #oNumber print #oNumber,command close #oNumber shell(“bash -c “”” & bshFile & “”””,,,true) i = 1 sheet=thisComponent.sheets.getByName(sheetName) Open tmpFile For Input As #iNumber While not EOF(iNumber) dim cArray Line Input #iNumber, iLine cArray = split(iLine) for c=0 to ubound(cArray) cell=sheet.getCellByPosition(c,i) cell.string=cArray(c) next i = i + 1 wend Close #iNumber end sub
Most of the code here is quite straightforward, but there are a few places that may look a bit intimidating. For instance, that bit with all the &s. What’s that all about? This is just building up the command that will be sent to the Linux shell. If you want to see what is actually going to be sent, just add a msgbox thus:
Sub main
dim command as string
command = buildCommand(“df|grep -v Filesystem”)
msgbox(command)
end sub
(see the example at the top of the page). The code that we’ve looked at is all fairly simple, but I’m sure that you’ll agree that we can start to do some very powerful things with it.

