65.9K
CodeProject 正在变化。 阅读更多。
Home

免费创建 PDF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (41投票s)

2011年1月3日

CPOL

14分钟阅读

viewsIcon

132326

downloadIcon

1996

了解如何使用 C# 自动化 Microsoft Word,根据可在运行时修改以反映动态信息的模板文档来创建 PDF 文件。

引言

在我作为开发者的旅程中,我遇到了将文档输出为 PDF 的需求。我的第一反应是使用一个 PDF 编写工具。但问题是,我不想花费所有时间来开发和更新一个我可以用代码修改的 PDF 模板。我希望最终用户能够根据需要修改模板。我当然可以创建一个某种复杂的系统来实现这一点,或者给他们一个编辑器,但这两种选择都不吸引我。定制应用程序难道不应该是让生活更轻松吗?

最终,我的解决方案似乎很简单:使用 Microsoft Word 文档作为模板,用 C# 写入,并使用 Word 内置的工具以编程方式将其保存为 PDF。由于 Microsoft 同时控制着 Word 和 .NET 环境,我理所当然地认为一切都会顺利。但事实并非完全如此。即使在使用 .NET 4.0 和 Microsoft Word 2010 时,也有一些需要注意的陷阱。但最终,我得到了我想要的:一个对最终用户来说简单的解决方案,以及一个强大的、可扩展的解决方案,我可以在后端用于多个不同的应用程序。

解决方案概览

对于那些喜欢快速了解我计划在代码中做什么的人来说,以下是我们的步骤:

  1. 通过 Microsoft.Office.Interop.Word .NET 组件连接到 Microsoft Word 2010。
  2. 循环遍历文档,查找用户提供的关键词进行替换——将每个关键词替换为其对应的项。
  3. 将文档另存为 PDF。

很简单,对吧?细节才是真正的挑战。

遇到的问题

当您在系统之间建立桥梁时,至少会遇到一个问题。您可能会认为,在谈论两个 Microsoft 系统时,这不会是个大问题,但不幸的是,事实并非如此。

最大的问题在于您使用哪个版本的 Interop 库。提供了两个版本——一个是 .NET 组件,另一个是 COM 组件。我首先发现的是,虽然我认为两者都使用了 COM 包装器,但 COM 组件似乎比 .NET 组件更容易出错。不过,两者都有问题。由于此系统使用 COM 包装器,Word 进程有时不会收到系统已完成的消息。最糟糕的情况下,即使您正确关闭并销毁了变量,也可能会出现多个 winword.exe 实例同时运行。我遇到了一些不同的“解决方案”来解决这个问题。

第一个解决方案认为,系统已关闭对象,但垃圾回收器尚未运行,因此对象仍然存在于内存中。因此,想法是您应该手动调用垃圾回收器。出于某种原因,由于第一次调用不起作用,因此建议实际调用两次。以下是建议的代码:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();

我在一个线程上尝试了这一点。不起作用。相反,我的系统会锁定,直到所有 winword.exe 进程都释放为止。进程释放的速度与我什么都不做时一样慢。唯一的变化是我的应用程序锁定了。更糟糕的是,我们手动调用了垃圾回收器。这会丢弃垃圾回收器所做的所有优化。有关垃圾回收器的更多想法以及为什么不应该这样做,Jim Lyon 在这里写了一篇相当不错的文章:这里

还有一些“传闻”中的解决方案在网上流传。我认为最有趣的是一位开发人员发现如何进入进程列表并终止所有当前正在运行的 winword.exe 进程。我给他一些创意分,但那个试图在运行此程序时编写 Word 文档的人可能会对此 hack “跳出框框”解决方案(我不会在此处发布代码)有所评论。在这种情况下,结束可能是我们唯一的选择,但我们真的希望尽可能避免这种情况。

所以,我们面临一个问题。我们如何终止 winword.exe,或者更好的是,如何阻止它挂起。经过艰苦的努力(好吧,也许只是一些随机的猜测),我制定了一份关于处理 Microsoft Office 工具的 Interop 的“最佳实践”列表(是的,这包括 Excel 和 PowerPoint)。

最佳实践

首先需要注意的是,系统是有效的。好吧,也许它不像我们希望的那样工作,但那是因为我们是控制狂。我们想优化系统。每一字节内存都需要从头到尾得到控制。放下吧。让系统做它需要做的事情,而不要试图控制它。疯狂地调用垃圾回收器、像数字精神病患者一样终止进程,或采取其他控制方式只会让系统发狂。

接下来要做的是控制您能控制的事情(这开始听起来像 宁静祷文)。在尝试打开文档之前,请确保它存在。如果可能,请尽量确保文档类型正确(手动,尽管也有编程方法)。这包括扩展名和 Office 版本(如果您使用的是 Office 2007 组件,却遇到了 2010 文件,例如)。确认所有这些细节后,确保您的代码已优化,以免因大量调用而让组件发疯。最后,不要不必要地打开或关闭应用程序。如果您认为在应用程序的整个生命周期中需要多次使用它,请保持应用程序对象打开。也许可以将其设为一个(天哪)全局变量。

最后我想强调的最佳实践是了解正在发生的事情。这听起来很明显,但有时事情确实会发生。请务必了解组件何时被初始调用,以及何时被关闭。检查以确保组件的析构函数语句在任何 try/catchfinally 块中都已正确设置。逐步调试代码,以确保事情按预期进行。我见过很多人责怪容易攻击的目标,却发现问题只是一个简单的编码错误。并非说我们曾经做过这样的事情,但其他人可能需要知道这一点。

代码

因此,我们知道可以使用 Microsoft Word .NET 组件,并且知道如何安全地使用它。现在您心中一定有一个问题:我们可以用这种新发现的力量做什么很棒的事情?在这篇文章中,我想展示如何使用 Microsoft Word 和“另存为 PDF”功能创建令人惊叹的 Word 模板,而无需使用书签或其他高级 Microsoft Word 项目。Word 自动化还有许多其他用途,但这个实际示例将让您体验到可用的强大功能,并为我们最初的问题提供答案。

我决定不提供多个代码片段,而是将我的代码记录得很好并在此处呈现。这样您就可以将代码和文档复制并粘贴到您自己的应用程序中。这是完成所有繁重工作的类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Word = Microsoft.Office.Interop.Word;
using System.Reflection;
 
namespace AutoWord
{
    public static class Document
    { 
        public static bool Process(string strWordDoc, string strPDFDoc,
            Dictionary<string,string> dReplacements)
        {
            //A set of objects needed to pass into the calls
            object oMissing = System.Reflection.Missing.Value;
            object oFalse = false;
            object oTrue = true;
 
            //The variable that will store the return value
            bool bolOutput = true;
 
            //Creates the needed objects (the application and the document)
            Word._Application oWord;
            Word._Document oDoc;
 
            //Checks to see if the file does not exist (which would throw an error)
            if (!System.IO.File.Exists(strWordDoc))
            {
                //Since the file does not exist, write out the
                //error to the console and exit
                Console.WriteLine("The file does not exist on the path specified.");
                return false;
            }
 
            try
            { 
                //Start up Microsoft Word
                oWord = new Word.Application();
 
                //If set to false, all work will be done in the background
                //Set this to true if you want to see what is going on in
                //the system - great for debugging.
                oWord.Visible = true;
 
                //Opens the Word Document
                //Parameters:
                //  strWordDoc = Document Name
                //  oFalse = Don't convert conversions
                //  oTrue = Open in Read-only mode
                oDoc = oWord.Documents.Open(strWordDoc, oFalse, oTrue);
 
                //Loop through each range of the document (header, body, footer, etc.)
                foreach (Word.Range oRange in oDoc.StoryRanges)
                {
                    //Loops through our Dictionary looking for the keys to replace
                    foreach (KeyValuePair<string,string> dEntry in dReplacements)
                    {
                        //This is what we are looking for (the Key)
                        oRange.Find.Text = dEntry.Key.ToString();
 
                        //This is what we will replace it with
                        oRange.Find.Replacement.Text = dEntry.Value.ToString();
 
                        //Find the item even if it wraps the text
                        oRange.Find.Wrap = Word.WdFindWrap.wdFindContinue;
 
                        //Replace every instance of that item (this is key)
                        oRange.Find.Execute(Replace: Word.WdReplace.wdReplaceAll);
                    }
                }
 
                //Export the document to a PDF file
                oDoc.ExportAsFixedFormat(strPDFDoc,
                    Word.WdExportFormat.wdExportFormatPDF);
 
                //Close the document without saving anything
                oDoc.Close(oFalse, oMissing, oMissing);
 
                //Close Word without saving anything
                oWord.Quit(oFalse, oMissing, oMissing);
 
                //Set the return value to true, indicating the process
                //completed successfully
                bolOutput = true;
 
            }
            catch (Exception ex)
            {
                //Here is where you put your logging code
                Console.WriteLine(ex.ToString());
                bolOutput = false;
            }
            finally
            { 
                //Releases the objects
                oDoc = null;
                oWord = null; 
            }
 
            //Actually output the return value
            return bolOutput; 
        }
    }
}

示例代码

这段代码非常简单易用。基本上,您只需要进行一次方法调用即可完成。对于那些可能之前没有使用过 dictionary 对象或不理解在这种情况下如何使用它的人,我将包括 dictionary 对象的创建和使用。

Dictionary<string,string> dKeywords = new Dictionary<string,string>();
 
//Load the dictionary object up with tags and their replacement
//strings.
dKeywords.Add("<<Title>>", "PDF Creation Tool");
dKeywords.Add("<<Name>>","Timothy Corey");
dKeywords.Add("<<Email>>", "me@timothycorey.com");
dKeywords.Add("<<Website>>", "www.timothycorey.com");
 
//Use the verbatim character to eliminate the need for double backslashes (@)
AutoWord.Document.Process(@"C:\Temp\MyDocument.docx",@"C:\Temp\Portfolio.pdf",dKeywords);

虽然我为我的标签选择了一个特定的命名约定,但该系统将查找您指定的任何 string 并将其替换为 dictionary 对象的值成员。请注意,实际调用 AutoWord.Document.Process 方法时,它会请求 Word 模板、要保存的 PDF 文件以及要替换的项(dictionary 对象)。我使用了 verbatim string 文字(string 前面的 @ 符号),这样就不需要转义斜杠,因为它们通常会被解释为转义字符本身。因此,而不是写“C:\\Temp\\MyDocument.docs”,我能够写 @”C:\Temp\MyDocument.docx”。两者意思相同。

用途

当看到很酷的东西时,我总是会问自己:“我为什么要用它?”。现在,对于免费代码,答案可以是“就是因为”,但我认为使用这段代码有一些非常棒的理由。我认为它非常强大的第一个领域是账户创建、账户维护或其他用户特定操作。您可以创建预先格式化的模板,然后用用户的特定信息填写。从那里,您可以将其通过电子邮件发送给用户,或将其存放在他们的共享驱动器中。

另一个使用它的好方法是记录存储。您可以拥有一个系统,自动填写使用报告(或其他类型的报告)并为您存储。这样,您可以拥有一个完全自动化的系统,该系统可以完成工作并报告自身。

快速笔记

此解决方案是使用 .NET 4.0 和 Microsoft Word 2010 设计的。但是,在 .NET 3.5 和 Microsoft Word 2007 中使用此解决方案无需修改。我相信 Microsoft Word 2003 大部分功能相同,但我尚未全部测试,以确定其接近程度。另请注意,虽然本文讨论的是如何使用 Microsoft Word 文档作为模板来创建 PDF,但您可以使用与 Microsoft Excel 和 Microsoft PowerPoint 相同的一些技术。可用的功能是惊人的。

常见问题解答

  1. 您为什么不使用 Microsoft Word 书签?
    好问题。很高兴您问了。基本上,我们可以使用书签做非常类似的事情。在 C# 中,我们可以用值填充每个书签,然后就可以开始工作了。我决定不使用它们,因为我没有选择不使用它们,而且我认为跳过它们会更简单。在 Word 中进行合并时,您别无选择,只能使用书签。但是,它们对最终用户来说可能有点复杂。更糟糕的是(在我看来),您不能在两个地方使用相同的书签名。问题在于,当您想在文档的一个位置放置相同的信息时。您最终会得到诸如“name1”、“name2”等命名的书签。由于我已经能够操作文档,因此我决定跳过这个系统并自己实现。我的系统只是找到一个 string 匹配并替换它。这意味着我可以在五个地方放置一个标签,如“<>”,它会将所有五个都替换为相同的值。标签格式也由我决定。我可以决定将其改为“**name**”。另一个好处是它为我们的类打开了其他用途。例如,假设您的公司想发布一份敏感文档的 PDF。他们想删除所有出现的公司名称,并将其替换为星号。没问题。只需将公司名称作为标签名称输入,并将值设置为星号。该系统将在几秒钟内处理整个文档并输出,而不会更改原始文档。
  2. 如果 winword.exe 进程未关闭怎么办?
    我首先建议您等待几分钟。有时它需要几分钟才能关闭(不酷,我知道,但现在我无能为力)。如果进程仍未自行关闭,请检查代码中的错误。确保文档版本正确,具有正确的标签,并且您有权将 PDF 文件保存到指定位置。Word 进程中的错误不会被传递回来(我们只是盲目调用 Word),这意味着错误会导致进程挂起。作为此诊断过程的一部分,请以可见模式运行 Word 应用程序(参见代码中的布尔变量以进行设置)。这可能会显示您忽略的错误。如果您实在找不到挂起的原因,并且确定不是错误,您可能需要想办法终止该进程。我讨厌这么说,因为它意味着放弃。我认为在您用尽所有其他选项之前,不应考虑这一点。但是,有时您可能需要这样做。这真的不难。基本上,您需要遍历进程并终止所有名为 winword.exe 的进程。您可以通过捕获代码创建的特定进程来获得更精确的结果,然后只终止该进程。这是我推荐的。
  3. 为什么我应该使用这个而不是(插入工具名称)?
    我的理念是,在寻找新工具来完成任务之前,先使用我已有的工具。如果您已经有了 Word,为什么不使用它呢?另外,我真的很不喜欢把事情搞复杂。如果我必须学习新东西才能做像创建 PDF 这样简单的事情,我认为有问题。大多数应用程序都带有模板编辑器或某种创建报表模板的方法。它们吹嘘其易用性。太棒了,但我的用户已经知道如何使用 Microsoft Word,所以我认为此方法拥有最好的模板编辑器。
  4. 如果服务器上没有 Microsoft Word 怎么办?
    首先,此解决方案更适合桌面应用程序,而不是基于服务器的解决方案。但是,如果您真的想在未安装 Microsoft Word 的地方使用它,那么您将需要研究使用 System.IO.Package.IO 命名空间来操作文档,而无需使用 Microsoft Word 本身。不幸的是,这将使您得到一个 Microsoft Word 文档而不是 PDF(糟糕)。然后,将其转换为 PDF 需要一个转换工具。这样我们就回到了原点。如果您根本无法在服务器上安装 Microsoft Word(我理解如果您不能),我建议在另一台安装了 Word 的服务器上创建一个 Web 服务。这将允许您在服务器环境中安全地创建 PDF 文档。
  5. 关于 C# 中的 Word 自动化不是有很多文章吗?为什么还要写一篇新的?
    在写这篇文章之前我考虑过这个问题。有很多人在 Word 自动化方面做了出色的工作。但是,我不断遇到不完整的解决方案。能够从 C# 操作 Microsoft Word 文档固然很好,但有什么用呢?我不想做一个理论练习。我想要一个可行、有价值的解决方案。我认为即时从模板创建 PDF 文档符合这一标准。我还想分享我在 Word 自动化陷阱方面的经验,因为似乎有很多关于如何最好地处理这些问题的困惑。

结论

因此,在这篇文章中,我们讨论了如何在不花费额外金钱且不使用任何特殊报表设计器的情况下,在 C# 中创建 PDF。我希望您喜欢这段代码,就像我喜欢编写它一样。我附上了一个完全可用的解决方案,允许您测试此功能。请在下方告诉我您的想法。

历史

  • 2011 年 1 月 1 日 – 初始版本
© . All rights reserved.