一. 應(yīng)用場(chǎng)景
開發(fā)一個(gè)課件在線學(xué)習(xí)功能,要求將WORD, EXCEL, PPT類型課件可在線打開學(xué)習(xí);最初設(shè)想使用第三方office插件,無奈價(jià)格太高放棄使用;
我們最終的方案是:利用office自身的另存為功能,在服務(wù)器將上傳的office文件轉(zhuǎn)化為pdf格式,然后網(wǎng)頁打開pdf文件實(shí)現(xiàn)在線學(xué)習(xí)功能;
項(xiàng)目實(shí)現(xiàn)方案:新建一個(gè)windows自啟的service安裝在服務(wù)器,這個(gè)服務(wù)里面會(huì)建立一個(gè)HttpListener,用于監(jiān)聽文件轉(zhuǎn)換的請(qǐng)求,請(qǐng)求來了后交給ServiceHandle處理,ServiceHandle會(huì)調(diào)用ConvertHelper把指定目錄下的office文件轉(zhuǎn)化為pdf,并放在相同目錄下,以供在線學(xué)習(xí)使用;
二. 工具及環(huán)境
- VS2019
- Office:本地開發(fā)2016版;服務(wù)器2010版
- 開發(fā)環(huán)境 win10
- 服務(wù)器環(huán)境 windows server 2012 R2
- VS開發(fā)使用到的nuget包:NetOffice
三. 創(chuàng)建windows server項(xiàng)目
VS2012下開發(fā)Windows服務(wù), 參考地址:
https://www.cnblogs.com/zhy-1992/p/6515850.html
基本按照這個(gè)思路開發(fā)出一個(gè)基礎(chǔ)service完全沒問題;這里需要提醒下的是:項(xiàng)目的文件路徑不要帶有空格,否則在執(zhí)行bat批處理操作時(shí)會(huì)出現(xiàn)問題。
四. 使用NetOffice實(shí)現(xiàn)文件轉(zhuǎn)換
上面只是創(chuàng)建一個(gè)服務(wù), 真正轉(zhuǎn)換的核心功能還沒開始,這時(shí)我們需要利用NetOffice插件;
項(xiàng)目目錄:

利用nuget包管理器安裝所需包,這是項(xiàng)目的packages.config文件:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NetOffice.Core" version="1.7.4.4" targetFramework="net452" />
<package id="NetOffice.Excel" version="1.7.4.4" targetFramework="net452" />
<package id="NetOffice.PowerPoint" version="1.7.4.4" targetFramework="net452" />
<package id="NetOffice.Word" version="1.7.4.4" targetFramework="net452" />
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net45" />
</packages>
轉(zhuǎn)換核心代碼:ConvertHelper.cs
public class ConvertHelper
{
public static void ConvetToPdf(string sourcePath, string targetPath)
{
if (!File.Exists(sourcePath))
{
throw new Exception(string.Format("文件{0}不存在", sourcePath));
}
if (File.Exists(targetPath))
{
throw new Exception(string.Format("目標(biāo)文件{0}已存在", targetPath));
}
LogHelper.Info<Program>("開始轉(zhuǎn)換:" + Path.GetFileName(sourcePath));
var targetDirectory = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
}
var ext = Path.GetExtension(sourcePath).ToLower();
if (ext == ".ppt" || ext == ".pptx")
{
ConvertPptToPdf(sourcePath, targetPath);
}
else if (ext == ".doc" || ext == ".docx")
{
ConvertDocumentToPdf(sourcePath, targetPath);
}
else if (ext == ".xls" || ext == ".xlsx")
{
ConvertExcelToPdf(sourcePath, targetPath);
}
else
{
LogHelper.Info<Program>("{0}不支持轉(zhuǎn)換pdf", sourcePath);
return;
}
if (File.Exists(targetPath))
{
LogHelper.Info<Program>("轉(zhuǎn)換成功:" + Path.GetFileName(sourcePath));
}
else
{
LogHelper.Info<Program>("轉(zhuǎn)換失敗:" + Path.GetFileName(sourcePath));
}
}
/// <summary>
/// 轉(zhuǎn)換ppt文件到pdf文件(不支持超時(shí)終止)
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="targetPath"></param>
private static void ConvertPptToPdf(string sourcePath, string targetPath)
{
NetOffice.PowerPointApi.Application application = null;
NetOffice.PowerPointApi.Presentation presentation = null;
var cts = new CancellationTokenSource();
var thread = new Thread(() =>
{
try
{
application = new NetOffice.PowerPointApi.Application();
presentation = application.Presentations.Open(sourcePath, true, NetOffice.OfficeApi.Enums.MsoTriState.msoTrue, false);
presentation.SaveCopyAs(targetPath, NetOffice.PowerPointApi.Enums.PpSaveAsFileType.ppSaveAsPDF);
}
catch (Exception ex)
{
if (ex.InnerException != null && ex.InnerException is ThreadAbortException)
{
LogHelper.Info<Program>("ConvertPptToPdf: {0}操作超時(shí)", Path.GetFileName(sourcePath));
}
else
{
LogHelper.Error<Program>(ex, "ConvertPptToPdf: {0}出現(xiàn)異常", Path.GetFileName(sourcePath));
}
}
finally
{
if (presentation != null)
{
presentation.Close();
presentation = null;
}
if (application != null)
{
application.Quit();
application.Dispose();
application = null;
}
//killProccess("POWERPNT");
}
});
cts.Token.Register(() =>
{
thread.Abort();
});
cts.CancelAfter(Program.MaxThreads * 1000);
thread.Start();
thread.Join();
GC.Collect();
GC.WaitForPendingFinalizers();
}
/// <summary>
/// 轉(zhuǎn)換word文件到pdf文件(支持超時(shí)終止)
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="targetPath"></param>
private static void ConvertDocumentToPdf(string sourcePath, string targetPath)
{
NetOffice.WordApi.Application application = null;
NetOffice.WordApi.Document document = null;
var cts = new CancellationTokenSource();
var thread = new Thread(() =>
{
try
{
application = new NetOffice.WordApi.Application();
document = application.Documents.Open(sourcePath);
document.ExportAsFixedFormat(targetPath, NetOffice.WordApi.Enums.WdExportFormat.wdExportFormatPDF);
}
catch (Exception ex)
{
if (ex.InnerException != null && ex.InnerException is ThreadAbortException)
{
LogHelper.Info<Program>("ConvertDocumentToPdf: {0}操作超時(shí)", Path.GetFileName(sourcePath));
}
else
{
LogHelper.Error<Program>(ex, "ConvertDocumentToPdf: {0}出現(xiàn)異常", Path.GetFileName(sourcePath));
}
}
finally
{
if (document != null)
{
document.Close();
document = null;
}
if (application != null)
{
application.Quit();
application.Dispose();
application = null;
}
//killProccess("WINWORD");
}
});
cts.Token.Register(() =>
{
thread.Abort();
});
cts.CancelAfter(Program.MaxThreads * 1000);
thread.Start();
thread.Join();
GC.Collect();
GC.WaitForPendingFinalizers();
}
private static void ConvertExcelToPdf(string sourcePath, string targetPath)
{
NetOffice.ExcelApi.Application application = null;
NetOffice.ExcelApi.Workbook wookbook = null;
var cts = new CancellationTokenSource();
var thread = new Thread(() =>
{
try
{
application = new NetOffice.ExcelApi.Application();
wookbook = application.Workbooks.Open(sourcePath);
wookbook.ExportAsFixedFormat(NetOffice.ExcelApi.Enums.XlFixedFormatType.xlTypePDF, targetPath);
}
catch (Exception ex)
{
if (ex.InnerException != null && ex.InnerException is ThreadAbortException)
{
LogHelper.Info<Program>("ConvertExcelToPdf: {0}操作超時(shí)", Path.GetFileName(sourcePath));
}
else
{
LogHelper.Error<Program>(ex, "ConvertExcelToPdf: {0}出現(xiàn)異常", Path.GetFileName(sourcePath));
}
}
finally
{
if (wookbook != null)
{
wookbook.Close();
wookbook = null;
}
if (application != null)
{
application.Quit();
application.Dispose();
application = null;
}
//killProccess("EXCEL");
}
});
cts.Token.Register(() =>
{
thread.Abort();
});
cts.CancelAfter(Program.MaxThreads * 1000);
thread.Start();
thread.Join();
GC.Collect();
GC.WaitForPendingFinalizers();
}
public static void CleanProccess()
{
LogHelper.Info<Program>("CleanProccess Start");
killProccess("WINWORD");
killProccess("EXCEL");
LogHelper.Info<Program>("CleanProccess Finish");
}
private static void killProccess(string appName)
{
// Store all running process in the system
Process[] runingProcess = Process.GetProcesses();
for (int i = 0; i < runingProcess.Length; i++)
{
// compare equivalent process by their name
if (string.Equals(runingProcess[i].ProcessName, appName, StringComparison.OrdinalIgnoreCase))
{
try
{
// kill running process
runingProcess[i].Kill();
LogHelper.Info<Program>("Kill {0} [{1}]", appName, runingProcess[i].Id);
}
catch (Exception ex)
{
if (ex is System.InvalidOperationException)
{
//進(jìn)程已經(jīng)關(guān)閉
}
else
{
LogHelper.Error<Program>(ex, "Kill {0} [{1}] Error", appName, runingProcess[i].Id);
}
}
}
}
}
}
ServiceListener監(jiān)聽http請(qǐng)求,端口號(hào)默認(rèn)8091;
public ServiceListener()
{
_stop = new ManualResetEvent(false);
_idle = new ManualResetEvent(false);
_busy = new Semaphore(Program.MaxThreads, Program.MaxThreads);
_listener = new HttpListener();
_listenerThread = new Thread(HandleRequests);
}
public void Start()
{
var url = String.Format(@"http://localhost:{0}/", Program.ListenerPort); // Port=8091
LogHelper.Info<ServiceListener>("Listenning Start:" + url);
_listener.Prefixes.Add(url);
_listener.Start();
_listenerThread.Start();
}
五. 系統(tǒng)調(diào)用http服務(wù),轉(zhuǎn)換文件
public void ConvertToPdf(string serviceUrl, string sourcePath, string pdfPath)
{
var url = $"{serviceUrl}?srcPath={sourcePath}&tarPath={pdfPath}";
using (var webClient = new WebClient())
{
var result = webClient.DownloadString(url);
}
}
至此,開發(fā)完畢,本地開發(fā)完成;
六. 本地測(cè)試功能
- 注冊(cè)服務(wù),執(zhí)行注冊(cè)服務(wù).bat
- 啟動(dòng)服務(wù),執(zhí)行啟動(dòng)服務(wù).bat
-
查看服務(wù)運(yùn)行情況
image.png
但是測(cè)試轉(zhuǎn)換時(shí)遇到了問題,日志錯(cuò)誤為 "Office 檢測(cè)到該文件有問題。為幫助保護(hù)您的計(jì)算機(jī),此文件無法打開。":
2020-05-02 09:04:22:248 Info [Thread9] 開始轉(zhuǎn)換:a10a54166224453abdd8d4b809f15449.ppt
2020-05-02 09:04:25:379 Exception [Thread10] ConvertPptToPdf: a10a54166224453abdd8d4b809f15449.ppt出現(xiàn)異常 Exception:
System.Runtime.InteropServices.COMException (0x80004005): See inner exception(s) for details. ---> System.Reflection.TargetInvocationException: 調(diào)用的目標(biāo)發(fā)生了異常。 ---> System.Runtime.InteropServices.COMException: Presentations.Open : Office 檢測(cè)到該文件有問題。為幫助保護(hù)您的計(jì)算機(jī),此文件無法打開。
--- 內(nèi)部異常堆棧跟蹤的結(jié)尾 ---
在 System.RuntimeType.InvokeDispMethod(String name, BindingFlags invokeAttr, Object target, Object[] args, Boolean[] byrefModifiers, Int32 culture, String[] namedParameters)
在 System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams)
在 NetOffice.Invoker.MethodReturn(COMObject comObject, String name, Object[] paramsArray)
在 NetOffice.Invoker.MethodReturn(COMObject comObject, String name, Object[] paramsArray)
在 NetOffice.PowerPointApi.Presentations.Open(String fileName, Object readOnly, Object untitled, Object withWindow)
在 LMS.DocumentConvertService.ConvertHelper.LMS.DocumentConvertService\Service\ConvertHelper.cs:行號(hào) 75
2020-05-02 09:04:33:477 Info [Thread9] 轉(zhuǎn)換失敗:a10a54166224453abdd8d4b809f15449.ppt
問題原因:service不能沒有權(quán)限進(jìn)行桌面交互;因此需要開啟此權(quán)限;
相關(guān)資料:
https://www.cnblogs.com/94cool/archive/2010/04/12/1710261.html
https://www.cnblogs.com/ymworkroom/articles/6673989.html
https://blog.csdn.net/jiangxinyu/article/details/5397060
我才用了其中2種較簡(jiǎn)單解決方案:
- 修改代碼,在ProjectInstaller.cs中ProjectInstaller類中重載OnAfterInstall
[RunInstaller(true)]
public partial class ProjectInstaller : System.Configuration.Install.Installer
{
public ProjectInstaller()
{
InitializeComponent();
}
protected override void OnAfterInstall(IDictionary savedState)
{
try
{
base.OnAfterInstall(savedState);
System.Management.ManagementObject myService = new System.Management.ManagementObject(
string.Format("Win32_Service.Name='{0}'", this.serviceInstaller1.ServiceName));
System.Management.ManagementBaseObject changeMethod = myService.GetMethodParameters("Change");
changeMethod["DesktopInteract"] = true;
System.Management.ManagementBaseObject OutParam = myService.InvokeMethod("Change", changeMethod, null);
}
catch (Exception)
{
}
}
}
- SC程序修改, 允許與桌面進(jìn)行交互
用批處理的方式實(shí)現(xiàn),加入如下命令到>啟動(dòng)服務(wù).bat文件中
sc config MonitorService type=interact type=own
至此,本地開發(fā)環(huán)境轉(zhuǎn)換服務(wù)成功運(yùn)行。
接下來準(zhǔn)備上服務(wù)器?。?/p>
