# [C# 开发技巧系列]使用C#操作Word和Excel程序
**一、引言**
在我们日常办公中,我们经常可能遇到一些重复性的工作的,比如,我们在写毕业设计的时候,有时候我们写的过程中不注意,当整篇毕业论文写完之后,发现在毕业论文中存在很多空白的段落,这是我们就需要人工重新审阅一遍论文,再手动删除一些空白行,由于毕业论文也不是一篇,有开题报告啊,文献翻译等等,这样就可能需要我们人工都去审阅一篇把一些空白行删除,这样既花时间,我们也看的累。然后还有一个例子就是——我们人事部门的MM们,一到月末的时候就需要给本月的寿星员工发送邮件来通知参加生日会,如果员工信息是在Excel中的话,这时候人事的MM就要手动地从中查找本月寿星的邮箱,然后用Outlook一个一个添加邮件地址来给本月寿星发送邮件的,为了让人事MM们不再那么累,所以就想能不能用程序自动化地完成这一系列的过程呢?答案是肯定的,下面就让我来实现上面的两个需求的,使我们(尤其是人事MM们)的办公更加Easy。
**二、自动删除Word中的空白行和页**
在引言部分,我们已经提出了这个需求的。记得当时在写毕业论文的时候,我也做过这些重复的事情,经常写完之后会再去审阅一遍毕业论文中的所有文档,然后手动把一些空白行删除掉,由于当时并不知道可以对Word来进行自动化编程,所以只能傻傻地做这样一些重复的事情。但是现在不一样了,自从接触了VSTO之后,才知道Office一系列产品都是提供了一些公开的API的,我们可以利用这些对象使我们自定义Office程序和使Office自动化地工作,下面就具体讲讲如何实现这个小的工具的。
首先,我们先明确下这个工具需要实现的功能——自动移除Word文档中的空白行。然后向大家解释下实现该工具的思路:
* 我们打开一个Word文档,该Word文档就是一个**Word.Document**对象
* Word文档中内容都是段落组成的,然而段落在Word对象模型中是**Word.Paragraph**对象
* **空白行或空白页也就是段落的内容为空**,明白了这点,我们就可以在程序中对段落对象的文本进行判断,如果段落内容为空,我们就删除该段落,这样也就实现了移除空白行的功能了。
有了上面的思路之后,然后大家只需要了解Word中对象模型,然后通过对象模型找到段落对象,然后再判断它的文本是否为空,为空就删除段落,不为空就什么都不做。所以思路有了之后,就是要了解Word对象模型了,对于这部分内容,大家可以参考我博客的中的——[创建Word解决方案](http://www.cnblogs.com/zhili/archive/2013/03/09/VSTO_Word.html)。由于代码中都有注释,这里就直接看实现该工具的核心代码:
```
string[] wordPatharray = null;
// 打开需要操作的Word文档
private void btnOpen_Click(object sender, EventArgs e)
{
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
openFileDialog.Filter = "Word document(*.doc;*.docx)|*.dox;**.docx|All Files(*.*)|*.*";
// 设置允许选择多个文件,该属性默认为false的,即只允许选择一个文件
openFileDialog.Multiselect = true;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
txtWordPath.Text = openFileDialog.FileName;
// 获得所有选定文件的文件名
wordPatharray = openFileDialog.FileNames;
}
}
}
// 移除Word中的所有空页
private void btnRemove_Click(object sender, EventArgs e)
{
Word.Application wordapp = null;
Word.Document doc = null;
try
{
// 启动Word应用程序并设置不可见
wordapp = new Word.Application();
// 如果不设置该属性,就可以看到Word程序的启动过程,这个和我们手动启动Word是一样的
wordapp.Visible = false;
// 遍历每个文件名
foreach (var wordpath in wordPatharray)
{
doc = wordapp.Documents.Open(wordpath);
// 删除所有空白页面
Word.Paragraph paragraph;
Word.Paragraphs paragraphs = doc.Paragraphs;
for (int i = paragraphs.Count; i > 0; i--)
{
paragraph = paragraphs[i];
// 如果段落的文本为空的话,首先选择该段落,然后再调用Word中Selection对象的Delete方法来删除
// 不为空什么都不做
if (paragraph.Range.Text.Trim() == string.Empty)
{
paragraph.Range.Select();
wordapp.Selection.Delete();
}
}
if (doc != null)
{
// 先保存所有修改再关闭Word文档
doc.Save();
((Word._Document)doc).Close();
}
}
MessageBox.Show("删除空白行成功");
}
catch (Exception ex)
{
MessageBox.Show("异常发生,异常信息为:" + ex.Message);
}
finally
{
// 释放资源
// 退出Word程序
if (wordapp != null)
{
((Word._Application)wordapp).Quit();
}
doc = null;
wordapp = null;
}
}
```
为了测试该程序的正确性,这里我建立了两个测试文档,为了测试,我故意在文档中删除了空白行和空白页面,下面是两个测试文档的截图:
![](https://box.kancloud.cn/2016-01-23_56a2eb2ff1d7d.jpg)
下面就看看该工具的运行效果(效果图是一段动画,认为这样可以更加说明运行效果)
![](https://box.kancloud.cn/2016-01-23_56a2eb301fa62.gif):
**三、人事部门的福音——自动给本月寿星员工发送邮件提醒**
为了帮助大家更好地理解该程序,还是像之前一样,首先说说实现该程序的思路:
* 我们首先需要打开员工信息表,此时我们可以利用Excel对象模型中的**Excel.Application.Workbooks.Open**方法来获得一个工作簿对象,关于更多Excel对象模型的内容可以转向——[创建Excel解决方案](http://www.cnblogs.com/zhili/archive/2013/02/24/ExcelAddin.html)。
* 通过第一步我们已经获得了工作簿对象了,然后通过遍历工作簿中的激活表,即表格一(Sheet1),我们可以通过**workbook.ActiveSheet**来获得表格一对象。
* 遍历表格一中的所有行来找到生日信息中的月份,如果月份等于当前月份,就给该员工的邮箱进行发邮件。
* 对于自动发送邮件的实现,该实现和我们手动操作Outlook过程是一样,手动操作时,我们需要手动打开Outlook(**在程序中就是创建Outlook应用程序对象**),然后点击新建邮件(在程序中就是通过**Applicatin**对象的**CreateItem(Outlook.OlItemType.olMailItem)**方法来创建一个邮件项目),在新建邮件窗口中指定收件人,主题,邮件内容之后,点击Outlook中的发送邮件按钮(在程序中就是通过指定 **Outlook.MailItem对象**(即代表一个邮件窗体)的**To(收件人)、Subject(主题)、Body(邮件内容**)属性,然后再调用Send方法来发送邮件)
明白了思路之后,我们理解代码会更加容易了,具体实现代码为:
```
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;
// 引用Excel和Outlook的命名空间
using Excel = Microsoft.Office.Interop.Excel;
using Outlook = Microsoft.Office.Interop.Outlook;
string excelpath = string.Empty;
// 打开员工表格
private void btnOpen_Click(object sender, EventArgs e)
{
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
openFileDialog.Filter = "Excel File(*.xls;*.xlsx)|*.xls;**.xlsx|All Files(*.*)|*.*";
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
txtExcelPath.Text = openFileDialog.FileName;
excelpath = openFileDialog.FileName;
}
}
}
// 自动给本月寿星发邮件通知
private void btnSendEmail_Click(object sender, EventArgs e)
{
if (!File.Exists(txtExcelPath.Text))
{
MessageBox.Show("员工表路径不存在,请确保输入正确的文件路径");
return;
}
if (txbBirthday.Text.Trim() == string.Empty || txbEmail.Text.Trim() == string.Empty)
{
MessageBox.Show("请先输入员工表中生日信息所在的列和邮箱信息所在的列!");
return;
}
// 输入信息都正确时开始发送邮件
SendEmail(int.Parse(txbBirthday.Text.Trim()), int.Parse(txbEmail.Text.Trim()));
}
// 发送邮件方法
private void SendEmail(int birthDayColumn,int emailColumn)
{
// 获得当前月份
int nowmonth = DateTime.Now.Month;
// 发送邮件地址字符串
string toEmailString = string.Empty;
string emailBody="请收到邮件的员工,请本月28号到休闲室来参加生日Party";
Excel.Application excelApp = null;
Excel.Workbook workbook =null;
Excel.Worksheet worksheet = null;
Excel.Range range = null;
try
{
// 新建Excel应用程序被设置它不可见
excelApp = new Excel.Application();
excelApp.Visible = false;
workbook = excelApp.Workbooks.Open(excelpath);
// 获得打开文件的激活表格
worksheet= workbook.ActiveSheet;
// 遍历表格中的所有行
for (int row = 2; row < worksheet.UsedRange.Rows.Count + 1; row++)
{
// 因为我的测试表格中第四列是生日信息,在Excel中第一行的下标是从1开始的
// 这里本来需要在页面设置一个文本框让用户填写生日信息是在那一列的
// 这里为了测试就直接在程序中指定
// 下面的Range就代表生日列中每一个单元格
range = worksheet.Cells[row, birthDayColumn];
// 我们可以通过Range.Value来获得单元格中的生日信息
// 因为我生日单元格中为日期格式,所以获得的是日期类型,所以直接通过Month属性来获得月份
int month = range.Value.Month;
// 如果我们的Excel文档中生日时间设置为文本格式的话,这时候就需要通过分割字符串的方式来获得月份
// 通过Split函数来把生日信息以'/'符号分隔,分隔的数组的第二个就是月份
//int month = Int32.Parse(birthday.Split('/')[1]);
// 如果月份等于当前月的话,就给这个人发邮件
if (month == nowmonth)
{
// 获得本月生日员工的邮件地址
toEmailString += ";" + ((Excel.Range)worksheet.Cells[row, emailColumn]).Value;
}
}
}
catch (Exception ex)
{
MessageBox.Show("读取员工表格时出错,异常信息为:" + ex.Message);
return;
}
finally
{
workbook.Close(Excel.XlSaveAction.xlDoNotSaveChanges);
excelApp.Quit();
if (workbook != null)
{
Marshal.FinalReleaseComObject(workbook);
workbook = null;
}
if (excelApp != null)
{
Marshal.FinalReleaseComObject(excelApp);
excelApp = null;
}
}
if (CreateEmailItem("生日提醒", toEmailString, emailBody))
{
MessageBox.Show("成功给本月寿星发送邮件提醒");
}
}
// 创建邮件项
private bool CreateEmailItem(string subjectEmail,string toEmail,string bodyEmail)
{
Outlook.Application outlookapp = null;
Outlook.MailItem email =null;
try
{
// 创建邮件项,就如你手动点新建邮件一样
outlookapp = new Outlook.Application();
email = outlookapp.CreateItem(Outlook.OlItemType.olMailItem);
// 指定邮件的主题,收件人和内容,就如你在新建邮件窗体中输入收件人,主题和内容一样
email.Subject = subjectEmail;
email.To = toEmail;
email.Body = bodyEmail;
email.Importance = Outlook.OlImportance.olImportanceHigh;
// 发送邮件,就如你点界面上的发送邮件操作一样
((Outlook._MailItem)email).Send();
}
catch(Exception ex)
{
MessageBox.Show("发送邮件的时候失败,异常信息为:" + ex.Message);
return false;
}
finally
{
// 释放资源
((Outlook._Application)outlookapp).Quit();
if (email != null)
{
Marshal.FinalReleaseComObject(email);
email = null;
}
if (outlookapp != null)
{
Marshal.FinalReleaseComObject(outlookapp);
outlookapp = null;
}
}
return true;
}
```
为了测试程序,我新建了一个员工信息表,表格的格式如下(你当然可以根据自己的需要更改格式):
![](https://box.kancloud.cn/2016-01-23_56a2eb30489b5.jpg)
现在就让我们看看该程序的运行效果:
![](https://box.kancloud.cn/2016-01-23_56a2eb30603a3.gif)
**四、 小结**
到这里,本专题的内容就和大家介绍完了,在下一个专题中将向大家介绍下如何通过Office提供的API的来遥控幻灯片。如果大家对本专题中两个工具的实现源码有任何的疑问,都可以在下面留言给我。觉得不错的话,帮忙推荐下,感谢大家的支持
- C# 基础知识系列
- C# 基础知识系列 专题一:深入解析委托——C#中为什么要引入委托
- C# 基础知识系列 专题二:委托的本质论
- C# 基础知识系列 专题三:如何用委托包装多个方法——委托链
- C# 基础知识系列 专题四:事件揭秘
- C# 基础知识系列 专题五:当点击按钮时触发Click事件背后发生的事情
- C# 基础知识系列 专题六:泛型基础篇——为什么引入泛型
- C# 基础知识系列 专题七: 泛型深入理解(一)
- C# 基础知识系列 专题八: 深入理解泛型(二)
- C# 基础知识系列 专题九: 深入理解泛型可变性
- C#基础知识系列 专题十:全面解析可空类型
- C# 基础知识系列 专题十一:匿名方法解析
- C#基础知识系列 专题十二:迭代器
- C#基础知识 专题十三:全面解析对象集合初始化器、匿名类型和隐式类型
- C# 基础知识系列 专题十四:深入理解Lambda表达式
- C# 基础知识系列 专题十五:全面解析扩展方法
- C# 基础知识系列 专题十六:Linq介绍
- C#基础知识系列 专题十七:深入理解动态类型
- 你必须知道的异步编程 C# 5.0 新特性——Async和Await使异步编程更简单
- 全面解析C#中参数传递
- C#基础知识系列 全面解析C#中静态与非静态
- C# 基础知识系列 C#中易混淆的知识点
- C#进阶系列
- C#进阶系列 专题一:深入解析深拷贝和浅拷贝
- C#进阶系列 专题二:你知道Dictionary查找速度为什么快吗?
- C# 开发技巧系列
- C# 开发技巧系列 使用C#操作Word和Excel程序
- C# 开发技巧系列 使用C#操作幻灯片
- C# 开发技巧系列 如何动态设置屏幕分辨率
- C# 开发技巧系列 C#如何实现图片查看器
- C# 开发技巧 如何防止程序多次运行
- C# 开发技巧 实现属于自己的截图工具
- C# 开发技巧 如何使不符合要求的元素等于离它最近的一个元素
- C# 线程处理系列
- C# 线程处理系列 专题一:线程基础
- C# 线程处理系列 专题二:线程池中的工作者线程
- C# 线程处理系列 专题三:线程池中的I/O线程
- C# 线程处理系列 专题四:线程同步
- C# 线程处理系列 专题五:线程同步——事件构造
- C# 线程处理系列 专题六:线程同步——信号量和互斥体
- C# 多线程处理系列专题七——对多线程的补充
- C#网络编程系列
- C# 网络编程系列 专题一:网络协议简介
- C# 网络编程系列 专题二:HTTP协议详解
- C# 网络编程系列 专题三:自定义Web服务器
- C# 网络编程系列 专题四:自定义Web浏览器
- C# 网络编程系列 专题五:TCP编程
- C# 网络编程系列 专题六:UDP编程
- C# 网络编程系列 专题七:UDP编程补充——UDP广播程序的实现
- C# 网络编程系列 专题八:P2P编程
- C# 网络编程系列 专题九:实现类似QQ的即时通信程序
- C# 网络编程系列 专题十:实现简单的邮件收发器
- C# 网络编程系列 专题十一:实现一个基于FTP协议的程序——文件上传下载器
- C# 网络编程系列 专题十二:实现一个简单的FTP服务器
- C# 互操作性入门系列
- C# 互操作性入门系列(一):C#中互操作性介绍
- C# 互操作性入门系列(二):使用平台调用调用Win32 函数
- C# 互操作性入门系列(三):平台调用中的数据封送处理
- C# 互操作性入门系列(四):在C# 中调用COM组件
- CLR
- 谈谈: String 和StringBuilder区别和选择
- 谈谈:程序集加载和反射
- 利用反射获得委托和事件以及创建委托实例和添加事件处理程序
- 谈谈:.Net中的序列化和反序列化
- C#设计模式
- UML类图符号 各种关系说明以及举例
- C#设计模式(1)——单例模式
- C#设计模式(2)——简单工厂模式
- C#设计模式(3)——工厂方法模式
- C#设计模式(4)——抽象工厂模式
- C#设计模式(5)——建造者模式(Builder Pattern)
- C#设计模式(6)——原型模式(Prototype Pattern)
- C#设计模式(7)——适配器模式(Adapter Pattern)
- C#设计模式(8)——桥接模式(Bridge Pattern)
- C#设计模式(9)——装饰者模式(Decorator Pattern)
- C#设计模式(10)——组合模式(Composite Pattern)
- C#设计模式(11)——外观模式(Facade Pattern)
- C#设计模式(12)——享元模式(Flyweight Pattern)
- C#设计模式(13)——代理模式(Proxy Pattern)
- C#设计模式(14)——模板方法模式(Template Method)
- C#设计模式(15)——命令模式(Command Pattern)
- C#设计模式(16)——迭代器模式(Iterator Pattern)
- C#设计模式(17)——观察者模式(Observer Pattern)
- C#设计模式(18)——中介者模式(Mediator Pattern)
- C#设计模式(19)——状态者模式(State Pattern)
- C#设计模式(20)——策略者模式(Stragety Pattern)
- C#设计模式(21)——责任链模式
- C#设计模式(22)——访问者模式(Vistor Pattern)
- C#设计模式(23)——备忘录模式(Memento Pattern)
- C#设计模式总结
- WPF快速入门系列
- WPF快速入门系列(1)——WPF布局概览
- WPF快速入门系列(2)——深入解析依赖属性
- WPF快速入门系列(3)——深入解析WPF事件机制
- WPF快速入门系列(4)——深入解析WPF绑定
- WPF快速入门系列(5)——深入解析WPF命令
- WPF快速入门系列(6)——WPF资源和样式
- WPF快速入门系列(7)——深入解析WPF模板
- WPF快速入门系列(8)——MVVM快速入门
- WPF快速入门系列(9)——WPF任务管理工具实现
- ASP.NET 开发
- ASP.NET 开发必备知识点(1):如何让Asp.net网站运行在自定义的Web服务器上
- ASP.NET 开发必备知识点(2):那些年追过的ASP.NET权限管理
- ASP.NET中实现回调
- 跟我一起学WCF
- 跟我一起学WCF(1)——MSMQ消息队列
- 跟我一起学WCF(2)——利用.NET Remoting技术开发分布式应用
- 跟我一起学WCF(3)——利用Web Services开发分布式应用
- 跟我一起学WCF(3)——利用Web Services开发分布式应用
- 跟我一起学WCF(4)——第一个WCF程序
- 跟我一起学WCF(5)——深入解析服务契约 上篇
- 跟我一起学WCF(6)——深入解析服务契约 下篇
- 跟我一起学WCF(7)——WCF数据契约与序列化详解
- 跟我一起学WCF(8)——WCF中Session、实例管理详解
- 跟我一起学WCF(9)——WCF回调操作的实现
- 跟我一起学WCF(10)——WCF中事务处理
- 跟我一起学WCF(11)——WCF中队列服务详解
- 跟我一起学WCF(12)——WCF中Rest服务入门
- 跟我一起学WCF(13)——WCF系列总结
- .NET领域驱动设计实战系列
- .NET领域驱动设计实战系列 专题一:前期准备之EF CodeFirst
- .NET领域驱动设计实战系列 专题二:结合领域驱动设计的面向服务架构来搭建网上书店
- .NET领域驱动设计实战系列 专题三:前期准备之规约模式(Specification Pattern)
- .NET领域驱动设计实战系列 专题四:前期准备之工作单元模式(Unit Of Work)
- .NET领域驱动设计实战系列 专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
- .NET领域驱动设计实战系列 专题六:DDD实践案例:网上书店订单功能的实现
- .NET领域驱动设计实战系列 专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能
- .NET领域驱动设计实战系列 专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现
- .NET领域驱动设计实战系列 专题九:DDD案例:网上书店AOP和站点地图的实现
- .NET领域驱动设计实战系列 专题十:DDD扩展内容:全面剖析CQRS模式实现
- .NET领域驱动设计实战系列 专题十一:.NET 领域驱动设计实战系列总结