说明

本项目参考了 https://github.com/yangzhongke/PhoneAsPrompter 项目来完成实现,并对其进行了一些修改完善。

完整代码可以到 https://github.com/PuZhiweizuishuai/PPT-Remote-controlhttps://gitee.com/puzhiweizuishuai/PPT-Remote-control 查看。

软件下载地址: https://gitee.com/puzhiweizuishuai/PPT-Remote-control/releases/v1.0.0

另外,由于程序启动后会创建一个WEB服务器,用来显示PPT的操控界面,所以某些安全软件可能会报毒。但是程序本身是没有问题的。

截图

使用C#实现一个PPT遥控器-LMLPHP

具体实现

通过在Win Form项目中内嵌一个Kestrel Web服务器,我们就可以通过浏览器向web服务器发送请求来接收远程操作指令。之后通过Late Binding的方式去操作PPT。

1、在 Win Form项目中内嵌HTTP服务器

在Form窗口启动时,我们新建一个Kestrel服务器

            this.webHost = new WebHostBuilder()
                .UseKestrel()
                .Configure(ConfigureWebApp)
                .UseUrls("http://*:" + port)
                .Build();

            // 异步运行服务器
            this.webHost.RunAsync();

然后对其进行配置

private void ConfigureWebApp(IApplicationBuilder app)
        {
            app.UseDefaultFiles();
            app.UseStaticFiles();
            app.Run(async (context) =>
            {
                // 处理非静态请求
                var request = context.Request;
                var response = context.Response;
                string path = request.Path.Value;
                response.ContentType = "application/json; charset=UTF-8";
                bool hasRun = true;
                if (path == "/report")
                {
                    string value = request.Query["value"];
                    this.BeginInvoke(new Action(() => {
                        this.PageLabel.Text = value;
                    }));
                    response.StatusCode = 200;
                    await response.WriteAsync("ok");
                }
                else
                {
                    response.StatusCode = 404;
                }
            });

        }

操作PPT

首先,由于涉及到了COM编程,我们需要注意内存回收与释放,所以需要用到COMReferenceTracker类进行应用管理。

每一步用到COM的地方,都要用T方法进行资源回收。

        private dynamic T(dynamic comObj)
        {
            return this.comReference.T(comObj);
        }

以下操作使用dynamic进行操作,所有操作需要去查询VBA文档了解具体用法,以下仅演示部分操作

打开一个PPT的操作实现

        private void button1_Click(object sender, EventArgs e)
        {
            // 文件选择框
            openFileDialog.Filter = "ppt文件|*.ppt;*.pptx;*.pptm";
            if (openFileDialog.ShowDialog() != DialogResult.OK)
            {
                return;
            }

            string filename = openFileDialog.FileName;
            this.ClearComRefs();
            // 创建 PPT 对象
            dynamic pptApp = T(PowerPointHelper.CreatePowerPointApplication());
            // 显示 PPT
            pptApp.Visible = true;
            dynamic presentations = T(pptApp.Presentations);
            // 打开 PPT
            this.presentation = T(presentations.Open(filename));
            // 全屏显示
            T(this.presentation.SlideShowSettings).Run();
        }

PPT上一个动画操作实现

T(T(presentation.SlideShowWindow).View).Previous();

下一步,与上一个操作类似,只需更换Previous()方法为Next()即可。

获取注释

首先我们需要一个方法去解析注释

        private string GetInnerText(dynamic part)
        {
            StringBuilder sb = new StringBuilder();
            dynamic shapes = T(T(part).Shapes);
            int shapesCount = shapes.Count;
            for (int i = 0; i < shapesCount; i++)
            {
                dynamic shape = T(shapes[i + 1]);
                var textFrame = T(shape.TextFrame);
                // MsoTriState.msoTrue==-1
                if (textFrame.HasText == -1)
                {
                    string text = T(textFrame.TextRange).Text;
                    sb.AppendLine(text);
                }
                sb.AppendLine();
            }
            return sb.ToString();
        }

之后通过

dynamic notesPage = T(T(T(T(presentation.SlideShowWindow).View).Slide).NotesPage);
string notesText = GetInnerText(notesPage);

我们就可以获取具体每页的注释信息。

完善服务器

了解了以上的PPT操作之后,我们就需要去完善我们的Web服务器端配置。

用户访问相应的地址,然后去执行上面PPT操作部分的代码即可。

                else if (path == "/getNote")
                {
                    string notesText = null;
                    this.Invoke(new Action(() => {
                        if (this.presentation == null)
                        {
                            return;
                        }
                        try
                        {
                            dynamic notesPage = T(T(T(T(presentation.SlideShowWindow).View).Slide).NotesPage);
                            notesText = GetInnerText(notesPage);
                        }
                        catch (COMException ex)
                        {
                            notesText = "";
                        }
                    }));
                    await response.WriteAsync(notesText);
                }
                else if (path == "/next")
                {
                    response.StatusCode = 200;
                    this.Invoke(new Action(() => {
                        if (this.presentation == null)
                        {
                            return;
                        }
                        try
                        {
                            T(T(this.presentation.SlideShowWindow).View).Next();
                            hasRun = true;
                        } catch (COMException e)
                        {
                            hasRun = false;
                        }

                    }));

                    if (hasRun)
                    {
                        await response.WriteAsync("OK");
                    }
                    else
                    {
                        await response.WriteAsync("NO");
                    }
                }
                else if (path == "/previous")
                {
                    response.StatusCode = 200;
                    this.Invoke(new Action(() => {
                        if (this.presentation == null)
                        {
                            return;
                        }
                        try
                        {
                            T(T(this.presentation.SlideShowWindow).View).Previous();
                            hasRun = true;
                        }
                        catch (COMException e)
                        {
                            hasRun = false;
                        }

                    }));
                    if (hasRun)
                    {
                        await response.WriteAsync("OK");
                    }
                    else
                    {
                        await response.WriteAsync("NO");
                    }

完成前端

通过轮询的方式,不断的向服务端发送请求,获取最新的消息,这样我们就可以实现通过浏览器去操作PPT了。

<!DOCTYPE html>

<html lang="zh-cn">
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="renderer" content="webkit" />
    <title>操作你的PPT</title>
    <link rel="icon" href="/logo.ico">
    <style>
        div {
            font-size: 25px
        }
    </style>
</head>
<body>
    <div id="main" style="width:100vw;height:100vh;">
        <p id="note"></p>
    </div>
    <script src="hammer.min.js"></script>
    <script>
        function httpGet(url, cb) {
            fetch(url, {
                headers: {
                    'Content-Type': 'application/json; charset=UTF-8'
                },
                method: 'GET'
            }).then(response => response.text())
                .then(text => {
                    cb(text)
                })
                .catch(e => {
                    return null
                })
        }

        const note = document.querySelector("#note");

        let hasRun = true
        let getNotes = setInterval(() => {
            httpGet('/getNote', (text) => {
                note.innerText = text
            })
        }, 500)

        function nextPage() {
            httpGet('/next', (text) => {
                if (text == 'NO') {
                    clearInterval(getNotes)
                    note.innerText = "幻灯片播放完毕!"
                    hasRun = false
                } else {
                    if (!hasRun) {
                        getNotes = setInterval(() => {
                            httpGet('/getNote', (text) => {
                                note.innerText = text
                            })
                        }, 500)
                        hasRun = true
                    }
                }
            })
        }

        function previousPage() {
            httpGet('/previous', (text) => {
                if (text == 'NO') {
                    clearInterval(getNotes)
                    note.innerText = "幻灯片播放完毕!"
                    hasRun = false
                } else {
                    if (!hasRun) {
                        getNotes = setInterval(() => {
                            httpGet('/getNote', (text) => {
                                note.innerText = text
                            })
                        }, 500)
                        hasRun = true
                    }
                }
            })
        }

        var hammer = new Hammer(document.querySelector("#main"));
        hammer.on("swipeleft", function () {
            nextPage();
        });
        hammer.on("swiperight", function () {
            previousPage();
        });
    </script>
</body>
</html>
05-15 01:55