项目说明
我使用电脑录制视频教程的时候,会展示PPT给观众,同时也有一些提示性的文字给我自己看。这就类似于很多电视节目录制现场的“提词器”。
节目录制现场的提词器
在PC环境下,PowerPoint也具有提词器功能,在编辑PPT的时候,把每一页的备注中写上提示词即可。投影到屏幕上的给观众看的画面没有提示词,而演讲者的电脑屏幕的画面中有提示词。但是这要求使用投影仪或者使用双屏幕。而我的视频录制环境是我和观众是看的同一块屏幕,因此无法使用PowerPoint的提词功能。所以我只能自己开发一个应用。
既然我和观众是看的同一块屏幕,如果想达到“观众看不到提示词,而我能看到”的效果,就只能把提示词展示到额外的显示设备上。我们每个人都有智能手机,因此我就想到了把智能手机做为显示提示词的设备。基于这个想法,我开发出了一个桌面应用,这个应用提供了一个内嵌的Web服务器,提供了“获取当前PPT页面备注文字”以及“翻页”等功能的接口,并且提供了一个调用这些接口的网页;这样,只要在手机上访问这个网页,就可以通过手机来获取提示词,也可以通过手机来切换PPT的翻页。下图是我使用这个提词应用实际工作的场景:
我的提词器实际工作场景
这个应用使用.NET 5/.NET Core开发,但是思路是不局限于语言的,其他编程语言的开发者也可以使用你习惯的语言来开发。
我的应用主要使用了两个技术,一个是在WinForm程序中内嵌Web服务器,另一个就是通过代码控制PowerPoint文档。我下面将对它们分别做讲解。
.NET 内嵌Web服务器技术
.NET中可以使用Kestrel实现内嵌Web服务器,而Kestrel就是ASP.NET Core项目默认的Web服务器。由于Kestrel只是一个NuGet包而已,因此可以把它装到任何.NET项目上,比如控制台、WinForm、WPF、Xamarin等。其实所谓的ASP.NETCore项目本质上也只是一个装了Kestrel等相关包的控制台程序而已。
这里演示在WinForm项目中的用法,其他类型项目操作步骤都差不多:
1、首先创建一个WinForm项目,然后在项目根目录下创建名字为wwwroot的文件夹,这个文件夹用来放html、js、css等静态文件。
2、在项目的csproj文件中增加如下配置:
<None Update="wwwroot\**"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
这段配置的作用是:项目构建的时候,会把wwwroot目录下所有的内容都复制到输出目录下。
3、在wwwroot目录下创建index.html文件。
4、通过Nuget安装:Microsoft.AspNetCore.Owin
Install-Package Microsoft.AspNetCore.Owin
5、在主窗口中声明
private IWebHost host;
在窗口的构造函数中添加如下内容:
host = new WebHostBuilder().UseKestrel().UseUrls("http://*:80").Configure(Configure).Build();
host.RunAsync();
其中"http://*:80" 表示网站监听所有网卡(这样就可以通过其他设备访问内嵌的网站了),并且通过80端口提供服务。要确保计算机中的防火墙中开启了对应端口的访问权限。
在需要关闭服务器或者窗口关闭的时候执行如下代码,否则程序不会正常退出:
host.StopAsync();
host.WaitForShutdown();
然后声明如下方法:
public void Configure(IApplicationBuilderapp)
{app.UseDefaultFiles();app.UseStaticFiles();app.Run(async(context) =>{varrequest = context.Request;varresponse = context.Response;stringpath = request.Path.Value;if(path == "/report"){response.StatusCode= 200;awaitresponse.WriteAsync("OK");}else{response.StatusCode= 404;}});
}
其中app.UseDefaultFiles()表示启用对于index.html等默认文档的支持;app.UseStaticFiles()表示把wwwroot提供为静态文件夹。app.Run()中的代码意思为:如果用户访问了/report这个路径,则输出OK,否则就响应码为404。
代码控制PowerPoint文档
代码需要实现读取PowerPoint页面的备注文字以及翻页等功能,这需要使用Office Automation技术,也就是通过代码调用Office的COM接口,当前前提条件就是计算机上必须安装PowerPoint软件。
微软官方推荐的在.NET中访问Office的方法就是在Visual Studio中通过COM引用生成Office的Interop程序集,也就是所谓的“Early Binding”。这种方式的优点就是一切对象都是强类型的,所以代码编写比较方便。而缺点就是和特定Office版本绑定,必须注意开发的时候的Office绑定,必须用尽可能低的版本的Office进行开发。不知道是我本地环境的原因还是.NET 5对于这种方式支持不成熟,我在.NET 5项目通过COM引用方式使用的时候,一直出现“MsoTriState在未被引用的程序集中定义。必须添加对程序集office的引用”的编译错误,如下图:
Figure3编译错误
也可以使用Late Binding方式操作,也就是通过dynamic这种方式进行COM接口的访问。这种方式的优点是不依赖于特性Office版本,缺点就是全都是弱类型调用,因此需要查询文档,开发效率比较低。
我发现一个开源项目NetOffice(https://netoffice.io/),它仍然是强类型的,但是不依赖于特定的Office版本。最大的遗憾就是它目前的.NET Standard版本的开发正在进行,所以目前的版本仍然不支持.NET Core。
经过比较,我只能选择Late Binding这种方式来进行了。
由于Com的复杂性,特别是“引用计数”这种比较古老的资源管理技术的复杂性,导致晚绑定的对象回收要十分注意,否则会导致Office无法退出。我封装了一个简单的库Zack.ComObjectHelpers,可以简化Com对象资源的回收。
这个库的Nuget安装命令是:
Install-PackageZack.ComObjectHelpers
然后使用其中的COMReferenceTracker类进行COM引用的管理:打开文档创建一个COMReferenceTracker对象,在每一步可能返回Com对象的地方,都用T方法进行资源回收,操作完成后调用Dispose。
如下的代码就是打开一个PPT文档,然后进入演示模式的代码:
private dynamic presentation;
private COMReferenceTracker comRefTracker =new COMReferenceTracker();private void Form1_Closed(object sender,EventArgs e)
{this.comRefTracker.Dispose();
}private dynamic T(dynamic comObj)
{returnthis.comRefTracker.T(comObj);
}private void MiOpen_Click(object sender,System.EventArgs e)
{stringfilename = "d:/1.pptx";dynamicpptApp = T(PowerPointHelper.CreatePowerPointApplication());pptApp.Visible= true;dynamicpresentations = T(pptApp.Presentations);this.presentation= T(presentations.Open(filename));T(this.presentation.SlideShowSettings).Run();
}
C#操作Office Automation的文档、资料比较少,不过由于COM对象本身是跨语言的,而VBA操作Office Automation的资料非常多,因此完全参考VBA操作的资料即可。比如下面的代码就是我仿照网上搜索“VBA 读取PowerPoint备注”的代码改造成C#语法而成的“读取当前PowerPoint页面的备注”代码:
dynamic notesPage =T(T(T(T(presentation.SlideShowWindow).View).Slide).NotesPage);
notesText = GetInnerText(notesPage);private string GetInnerText(dynamic part)
{StringBuildersb = new StringBuilder();dynamicshapes = T(T(part).Shapes);intshapesCount = shapes.Count;for(int i = 0; i < shapesCount; i++){dynamicshape = T(shapes[i + 1]);vartextFrame = T(shape.TextFrame);if(textFrame.HasText == -1)//MsoTriState.msoTrue==-1{stringtext = T(textFrame.TextRange).Text;sb.AppendLine(text);}sb.AppendLine();}returnsb.ToString();
}
手机提词器应用代码
上面已经把整个应用的最核心代码介绍了,想了解整个项目的代码请访问项目的github页面https://github.com/yangzhongke/PhoneAsPrompter
手机控制电脑中视频播放
用“应用中内嵌Web服务器”技术,我还实现了一个“手机控制电脑中视频播放”的应用,可以通过手机控制电脑中的视频播放器进行暂停、播放、调节音量、快进快退等功能,甚至可以进一步开发完成切换直播电视频道等功能。代码也放到了上面github页面的VideoRemoteController项目中。
视频教程
除了这篇文章,我还录制了大约2个小时的视频教程来更详细的讲解代码,视频都是免费的,可以访问我推送的这条公众号消息中的其他视频。也可以访问我的哔哩哔哩、头条、微博等平台中同步发布的账号中的视频,账号名都是“杨中科”。
点击阅读原文访问开源项目