sobota, 28 sierpnia 2010

Word i C# - cz. I

Dzisiaj trochę na temat automatyzacji Worda za użyciem platformy .NET. Powiedzmy, że nasz program ma automatycznie generować dyplomy uznania dla uczniów. Mają one powstawać na bazie dokumentu-szablonu w formacie Worda 2003. W szablonie miejsca, które mają zostać automatycznie zamienione na dane uczniów są zaznaczone specjalnymi znacznikami, takimi jak np. %UCZEN%. Dzisiaj będzie o "brzydkim" rozwiązaniu takiego zadania ;)

Brzydkie jest korzystanie z obiektów COM samo w sobie. Platforma .NET dostarcza ułatwienie do korzystania z takich obiektów - Interop (od interoperability). Nasz program dzięki korzystaniu z obiektów COM nie będzie działał na plikach "doc" - on będzie działał na samym Wordzie - uruchomi go, skopiuje szablon odpowiednią liczbę razy za każdym razem odpowiednio podmieniając znaczniki, dodając podziały stron i wypluwając wszystkie dyplomy jako jeden dokument. OK, to zaczynamy.

Najpierw trzeba dodać referencję do odpowiedniego komponentu COM - w "Add reference" i zakładce "COM" wybieramy "Microsoft Word Object Library". U mnie jest to wersja 12 (Word 2007), ale z Wordem 2003 też nie będzie problemów ;)

Dodany komponent siedzi w przestrzeni nazw Microsoft.Office.Interop.Word. Aby nie trzeba było pisać zawsze tak długiego kwalifikatora warto wykorzystać using - tylko że ta przestrzeń zawiera mnóstwo klas, których nazwy mogą kolidować np. z Interopem dla Excela (jeżeli go używamy). Dlatego najlepiej skrócić po prostu kwalifikator do samego Word:

using Word = Microsoft.Office.Interop.Word;


Brzydota zaczyna się na samym początku. Najczęściej przez nas używanym obiektem w kodzie będzie:

object oMissing = System.Reflection.Missing.Value;

Czyli specjalna "wartość brakująca", którą to będziemy przekazywali jako ref object. W Interopie wszystkie argumenty są przekazywane przez ref object - a jest ich zazwyczaj kilkanaście. Przypominają się (słusznie!) czasy WinAPI.


Zaczynamy uruchamiając Worda:

Word.Application oWord;
oWord = new Word.Application();
oWord.Visible = false;


Dzięki trzeciej linijce nasz Word nie wyskoczy w postaci okna, tylko będzie niewidoczny. Tak, my naprawdę go uruchamiamy.

Przygotowujemy sobie dwa dokumenty: wejściowy szablon i wynik:

Word.Document oDocOriginal;
Word.Document oDocNew;
object oFileName = Path.GetFullPath(@"..\..\Dyplom.doc");
oDocOriginal = oWord.Documents.Add(ref oFileName, ref oMissing, ref oMissing, ref oMissing); // open existing file
oDocNew = oWord.Documents.Add(ref oMissing, ref oMissing, ref oMissing, ref oMissing); // create new document


Lista naszych wyróżnionych uczniów:

string[] students = new string[] { "Jan Kowalski", "Paweł Nowak", "Anna Nowakowska" };

Teraz przeglądając tablicę studentów wykonamy następujące kroki:

1. Skopiujemy na koniec naszego nowego dokumentu treść całego szablonu:

object oEndOfDoc = @"\endofdoc";
oDocNew.Bookmarks.get_Item(ref oEndOfDoc).Range.FormattedText = oDocOriginal.Content.FormattedText;


"\endofdoc" to specjalna etykieta występująca tylko na końcu dokumentu. Druga linia wstawia w miejsce treści związanej z tą etykietą (która jest pusta) całą treść szablonu.

2. Podmienimy tag "%UCZEN%" na dane odpowiedniego ucznia:

foreach (Word.Range tmpRange in oDocNew.StoryRanges)
{
  tmpRange.Find.Text = "%UCZEN%";
  tmpRange.Find.Replacement.Text = students[i];
  tmpRange.Find.Wrap = Word.WdFindWrap.wdFindContinue;
  object replaceAll = Word.WdReplace.wdReplaceAll;
  tmpRange.Find.Execute(ref oMissing, ref oMissing, ref oMissing,
    ref oMissing, ref oMissing, ref oMissing, ref oMissing,
    ref oMissing, ref oMissing, ref oMissing, ref replaceAll,
    ref oMissing, ref oMissing, ref oMissing, ref oMissing);
}


Jak widać polecenie "Find & Replace" do przyjaznych nie należy.

3. I na koniec, jeżeli nie jest to ostatni uczeń, to dodajemy podział strony:

object oPageBreak = Word.WdBreakType.wdPageBreak;
oDocNew.Bookmarks.get_Item(ref oEndOfDoc).Range.InsertBreak(ref oPageBreak);



Po przejściu całej listy uczniów zapisujemy nasz nowy dokument kolejną funkcją-potworem:

object FileName = Path.GetFullPath(@"..\..\Wynik.doc");
oDocNew.SaveAs(ref FileName, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing);


i zamykamy Worda prosząc go, aby nas nie pytał o zapisanie ewentualnych zmian:

object doNotSaveChanges = Word.WdSaveOptions.wdDoNotSaveChanges;
oWord.Quit(ref doNotSaveChanges, ref oMissing, ref oMissing);


Cały kod przykładu:
http://pastebin.com/CsaPJP19

Aha - jeżeli planujecie właśnie tak postąpić, kiedy macie zrobić automatyzację Worda w aplikacji Webowej to cóż, nie jest to dobry pomysł. Dlaczego?

1. Jak widać kod nie jest czytelny żadną miarą - taka już specyfika COM.

2. Tak naprawdę uruchamiacie Worda - a tutaj wiele może pójść źle (np. informacja, że jeszcze nie podaliśmy inicjałów po instalacji i oczekiwanie na ich podanie).

3. Aby uruchomić Worda na serwerze trzeba dodać możliwość uruchamiania danego komponentu COM, a użytkownik uruchamiający Worda i tak musi być Administratorem.

Jakie jest lepsze rozwiązanie? O tym będzie w części drugiej - jest to rozwiązanie, które można z równie dużym powodzeniem zaimplementować także w Javie, nie wykorzystując żadnych dobrodziejstw platformy .NET.

Więcej:
http://www.codeproject.com/KB/aspnet/wordapplication.aspx
http://weblogs.asp.net/guystarbuck/archive/2008/05/13/automated-search-and-replace-in-multiple-word-2007-documents-with-c.aspx

3 komentarze:

  1. a może jakiś kurs dla Open Office Writera :D W końcu z Worda już mało kto korzysta :P

    OdpowiedzUsuń
  2. W Word'zie jest taka opcja jak Korespondencja seryjna. Można sobie w Excelu przygotować tabelkę z uczniami których chcemy wyróżnić, a potem wygenerować odpowiedni dokument już wypełniony.
    Niestety my programiści mamy tendencję do tworzenia własnego rozwiązania w ulubionym języku programowania, gdy nie możemy czegoś znaleźć w czasie krótszym niż 5 minut ;)

    OdpowiedzUsuń