Sunday, June 28, 2009

Yet more Word automation

Today I was checking out possibilities of optimising my Word automation code. I know that theoretically it's possible to send to Word a list of strings and then get Word to turn those strings into a table, but I tried this once and didn't succeed. The first stage is to build a long string, whilst separating the various values with a separator character, which is obviously one which won't appear in the text itself. I used the caret (^) as a separator, and one tells Word to use this character with the DefaultTableSeparator method. The code for the first step is thus:
wrdApp.defaulttableseparator:= '^';
tmp:= '';
with qJobs do // get text from this database table
 begin
  close;
  parambyname ('a').AsInteger:= a;
  parambyname ('b').AsInteger:= b;
  open;
  while not eof do
   begin
    tmp:= tmp + fieldbyname ('name').asstring + '^';
    next
  end
end;

tmp[length(tmp)]:= ' '; // remove the final caret
wrdSel.typetext (tmp);
The exported text will be in the form "a^b^c^d^e^f^g".

The key to turning this text into a table is to selected the exported text first and then turn it into a table. My normal method of exporting text into Word doesn't leave the text selected at the end, so I had to figure out how to select it. The exported text is always going to be a paragraph by itself, and it's always going to be the current paragraph (but not necessarily the final paragraph at the time of export). To quote an article which I found today, "if you want to know [the current paragraph number] in order to operate on the currently selected paragraph, table or other object, you can simply use: selection.collectionname (1)."
which in Delphi translates to "wrdSel.paragraphs.item(1).range.select". The Delphi "gotcha" is that Word collections have to be referred to as "collections.item(x)", not "collections(x)".

The next stage is turning the selected text into a table; this is done via the 'ConvertToTable' method, which takes a large number of parameters. Fortunately, we only need the first three; the first parameter says how to separate the table items, which is via the default separator character. The Word constant for this is "wdSeparateByDefaultListSeparator", whose value is 3. The second parameter is the number of rows in the table; although this parameter might seem to be important, it is irrelevant as the table adds rows according to the need, and so I pass the number 1. The third parameter, which is much more important, is the number of columns. Here is the code for this part:
wrdSel.paragraphs.item(1).range.select;
wrdSel.converttotable (3, 1, 2);

Normally I add a table to a Word document using this code:
wrdTable:= WrdDoc.Tables.Add (wrdSel.Range, 1, 12, 1, 2);
In the case of ConvertToTable, however, the table has already been created, and so the above line is useless. The solution is to access the current table which has been added to the document, not assuming that this is the first or last table. The way to do this is the same way in which I accessed the current paragraph, viz
wrdTab:= wrdSel.tables.item(1); // the new table

Once I have a pointer to the current table, I can do anything I want with it, such as removing the borders, shading one column, setting text alignment, etc.

Here is the final and complete code:
wrdApp.defaulttableseparator:= '^';
wrdSel:= wrdApp.selection;
tmp:= '';

with qJobs do
 begin
  close;
  parambyname ('a').AsInteger:= a;
  parambyname ('b').AsInteger:= b;
  open;
  while not eof do
   begin
    tmp:= tmp + fieldbyname ('name').asstring + '^';
    next
  end
end;

tmp[length(tmp)]:= ' ';
wrdSel.ParagraphFormat.Alignment:= 2;
wrdSel.typetext (tmp);
// the below line always selects the current paragraph
wrdSel.paragraphs.item(1).range.select;
wrdSel.converttotable (3, 1, 2);
wrdTab:= wrdSel.tables.item(1); // the new table
wrdTab.borders.insidelinestyle:= false; // no borders
wrdTab.borders.outsidelinestyle:= false;

wrdSel.homekey (wdStory); // jump to beginning of document
I used this code in a different place in the program to create a table with 75 rows of 5 columns each. The code executed almost instantaneously, as opposed to the length of time it took before. This technique cannot be slower than building the table manually, but the advantage becomes clearer the more rows and columns that have to be added.

Incidentally, I have one table with twelve columns and an changeable number of rows, but there is text only in the first and last columns. Of the other ten columns, only one will contain an 'X', which represents the numerical value for the first column in a range between one and ten. I did not use the 'ConvertToTable' technique to build this table as it is sparse, and the CTT technique should only be used for dense tables.

Next up is a similar conceptual technique for exporting data to Excel.

No comments: