C#:使用OpenXML读写Excel文档

对于Office文件的读写操作始终是一个比较麻烦的事情,最近做了一个Office操作的小程序,写一个文章用于备忘。
Office文件读写方法挺多,最好的方法自然是OpenXML无疑,但这库用起来还是比较麻烦。我在此做一个Excel读写小例子,其他Office类型同理。
对于OpenXML这库,首先需要知道OpenXML是什么。新建一个Excel文件,另存为a.xlsx,然后将其重命名为a.rar,解压,然后看到一大堆xml文件。不用怀疑,这就是xml标准。对于Office文件的读的访问来说,如果对OpenXML不熟,并且使用其他语言不方便调用C#的库,那么,手工解压然后找找数据的位置,然后手工解析,是最快捷的方式。当然,在可以使用OpenXML的场合,我个人还是推荐使用OpenXML。
OpenXML2.5下载地址(主体工具摘自微软官网,扩展摘自网络):https://pan.baidu.com/s/1slDa0Jr,提取码98dd
这个工具只有英语版的,但由于操作过于简单,所以对于英语小白也完全无压力。

1、首先依次安装两个库,有次序但不重要,如果你装反了那么系统会不让你继续装,并提示你先装另外一个。不论如何,两个都装上就OK。
2、安装目录下找到OpenXmlSdkTool.exe,因为常用所以可以发送到桌面快捷方式。然后打开这软件,点击Open File或者将Office文件拖到左侧的框中;然后点击Reflect Code。然后就会生成一堆代码。简单解释下,这堆代码复制到工程内,然后调用new GeneratedCode.GeneratedClass().CreatePackage("d:/a.xlsx")。根据文件类型决定生成的文件后缀。然后,就可以生成一个与导入的Office文件完全一样的文件了。
3、代码量可能稍稍有点多,不过多玩几次,看看里面的代码是怎么生成的,然后根据自己的需要,手工修改代码,比如循环生成特定字符串等等,用起来就挺方便了。
OpenXML可以写当然也可以读,但稍微麻烦点。我最近把OpenXML读Excel的功能封装成了一个类,通过这个类可以非常方便的读Excel单元格中的内容。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
////////////////////////////////////////////////////////////////////////////////
//
// Class Name:  excel_access
// Description: 用于读取Excel文档某单元格的字符串内容或者前景色、背景色等
// Class URI:   https://www.fawdlstty.com/archives/405.html
// Author:      Fawdlstty
// Author URI:  https://www.fawdlstty.com/
// Version:     0.2
// License:     MIT
// Last Update: Jul 04, 2017
//
////////////////////////////////////////////////////////////////////////////////
 
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Extensions;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace excel_access {
    public class excel_access {
        private SpreadsheetDocument m_ssDoc = null;
        private WorkbookStylesPart m_styles = null;
        private WorkbookPart m_wbPart = null;
        private List<Sheet> m_sheets = null;
        private Sheet m_sheet = null;
        private WorksheetPart m_wsPart = null;
        private String[] m_colors = null;
 
        /// <summary>
        /// 打开excel文档与工作表
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <param name="sheet">表格名称</param>
        public void open_excel(String path, String sheet = "Sheet1") {
            m_ssDoc = SpreadsheetDocument.Open (path, false);
            m_styles = SpreadsheetReader.GetWorkbookStyles (m_ssDoc);
            m_wbPart = m_ssDoc.WorkbookPart;
            m_sheets = m_wbPart.Workbook.Descendants<Sheet> ().ToList ();
            m_sheet = m_wbPart.Workbook.Descendants<Sheet> ().FirstOrDefault (c => c.Name == sheet);
            m_wsPart = (WorksheetPart) m_wbPart.GetPartById (m_sheet.Id);
 
            Colors colors = m_styles.Stylesheet.Colors;
            if (colors != null) {
                int count = colors.FirstChild.ChildElements.Count;
                m_colors = new String [count];
                for (int i = 0; i < count; ++i)
                    m_colors [i] = ((Color) colors.FirstChild.ChildElements [i]).Rgb.Value;
            }
            else {
                m_colors = new String [] {
                    "00000000", "00FFFFFF", "00FF0000", "0000FF00", "000000FF", "00FFFF00", "00FF00FF", "0000FFFF",
                    "00000000", "00FFFFFF", "00FF0000", "0000FF00", "000000FF", "00FFFF00", "00FF00FF", "0000FFFF",
                    "00800000", "00008000", "00000080", "00808000", "00800080", "00008080", "00C0C0C0", "00808080",
                    "009999FF", "00993366", "00FFFFCC", "00CCFFFF", "00660066", "00FF8080", "000066CC", "00CCCCFF",
                    "00000080", "00FF00FF", "00FFFF00", "0000FFFF", "00800080", "00800000", "00008080", "000000FF",
                    "0000CCFF", "00CCFFFF", "00CCFFCC", "00FFFF99", "0099CCFF", "00FF99CC", "00CC99FF", "00FFCC99",
                    "003366FF", "0033CCCC", "0099CC00", "00FFCC00", "00FF9900", "00FF6600", "00666699", "00969696",
                    "00003366", "00339966", "00003300", "00333300", "00993300", "00993366", "00333399", "00333333"
                };
            }
        }
 
        /// <summary>
        /// 打开工作表
        /// </summary>
        /// <param name="sheet"></param>
        public void open_sheet(String sheet = "Sheet1") {
            m_sheet = m_wbPart.Workbook.Descendants<Sheet> ().FirstOrDefault (c => c.Name == sheet);
            m_wsPart = (WorksheetPart) m_wbPart.GetPartById (m_sheet.Id);
        }
 
        /// <summary>
        /// 关闭文档
        /// </summary>
        public void close() {
            m_ssDoc.Close ();
            m_ssDoc.Dispose ();
 
            m_ssDoc = null;
            m_styles = null;
            m_wbPart = null;
            m_sheets = null;
            m_sheet = null;
            m_wsPart = null;
        }
 
        /// <summary>
        /// 获取单元格
        /// </summary>
        /// <param name="pos">位置</param>
        /// <returns>单元格</returns>
        public Cell get_item(String pos = "A1") {
            return m_wsPart.Worksheet.Descendants<Cell> ().Where (c => c.CellReference.Value == pos).FirstOrDefault ();
        }
 
        /// <summary>
        /// 获取单元格
        /// </summary>
        /// <param name="iCol">第几列,以0起始</param>
        /// <param name="iLine">第几行,以0起始</param>
        /// <returns></returns>
        public Cell get_item(int iCol, int iLine) {
            String strCol = "";
            if (iCol < 26)
                strCol = "{0}{1}".format (((char) ((int) 'a' + iCol)), iLine + 1);
            else if (iCol < 26*26+26) {
                iCol -= 26;
                strCol = "{0}{1}{2}".format(((char) ((int) 'a' + iCol / 26)), ((char) ((int) 'a' + iCol % 26)), iLine + 1);
            }
            return get_item (strCol);
        }
 
        /// <summary>
        /// 获取单元格内容
        /// </summary>
        /// <param name="cell">单元格</param>
        /// <returns>内容</returns>
        public String get_value(Cell cell) {
            string value = cell.InnerText;
            if (cell.DataType != null) {
                switch (cell.DataType.Value) {
                case CellValues.SharedString:
                    var stringTable = m_wbPart.GetPartsOfType<SharedStringTablePart>().FirstOrDefault ();
                    if (stringTable != null) {
                        value = stringTable.SharedStringTable.ElementAt (int.Parse (value)).InnerText;
                    }
                    break;
 
                case CellValues.Boolean:
                    switch (value) {
                    case "0":
                        value = "FALSE";
                        break;
                    default:
                        value = "TRUE";
                        break;
                    }
                    break;
                }
            }
            return value != null ? value : "";
        }
 
        /// <summary>
        /// 计算Tint深浅变化
        /// </summary>
        /// <param name="iColor">颜色值,取值0-255</param>
        /// <param name="tint">深浅变化参数</param>
        /// <returns>计算结果</returns>
        private int calc_single_tinted_value (int iColor, Double tint) {
            if (tint < 0)
                return (iColor * (1.0 + tint)).toInt32 ();
            else if (tint > 0)
                return (iColor * (1.0 - tint) + (255 - 255 * (1.0 - tint))).toInt32 ();
            if (iColor < 0 || iColor > 255)
                throw new Exception ("tint计算结果范围错误!");
            return iColor;
        }
 
        /// <summary>
        /// 获取标准格式颜色字符串
        /// </summary>
        /// <param name="ct">颜色类型</param>
        /// <returns>标准格式颜色字符串</returns>
        private String format_color (ColorType ct) {
            if (ct == null)
                return "";
 
            // 获取颜色数据
            String strColor = "";
            if (ct.Rgb != null) {
                strColor = ct.Rgb.Value;
            } else if (ct.Theme != null) {
                DocumentFormat.OpenXml.Drawing.Color2Type c2t = (DocumentFormat.OpenXml.Drawing.Color2Type) m_ssDoc.WorkbookPart.ThemePart.Theme.ThemeElements.ColorScheme.ChildElements [(int) ct.Theme.Value];
                //Debug.WriteLine (c2t.OuterXml);
                if (c2t.RgbColorModelHex != null)
                    strColor = c2t.RgbColorModelHex.Val;
            } else if (ct.Indexed != null) {
                int iIndexed = (int) (uint) ct.Indexed;
                if (iIndexed < m_colors.Length)
                    strColor = m_colors [iIndexed];
            }
 
            // 获取颜色字符串
            if (strColor.Length == 0) {
                return "";
            } else if (strColor.Length > 6) {
                strColor = strColor.Substring (strColor.Length - 6, 6);
            }
 
            // 计算实际颜色
            if (ct.Tint != null && ct.Tint != 0.0f) {
                int iColor = strColor.hex_to_dec();
                int iRed = (iColor >> 16) & 0xff;
                iRed = calc_single_tinted_value (iRed, ct.Tint);
                int iGreen = (iColor >> 8) & 0xff;
                iGreen = calc_single_tinted_value (iGreen, ct.Tint);
                int iBlue = (iColor) & 0xff;
                iBlue = calc_single_tinted_value (iBlue, ct.Tint);
                iColor = (iRed << 16) + (iGreen << 8) + iBlue;
                strColor = "{0:X}".format(iColor);
                while (strColor.Length < 6)
                    strColor = "0" + strColor;
            }
 
            // 生成结果颜色字符串
            if (strColor.Length > 6)
                strColor = strColor.Substring (strColor.Length - 6, 6);
            while (strColor.Length < 8)
                strColor = "F" + strColor;
            return strColor;
        }
 
        /// <summary>
        /// 获取单元格背景色
        /// </summary>
        /// <param name="cell">单元格</param>
        /// <returns>单元格背景色</returns>
        public String get_background_color(Cell cell) {
            // 获取填充类型
            int cellStyleIndex = 0;
            if (cell.StyleIndex != null)
                cellStyleIndex = (int) cell.StyleIndex.Value;
            CellFormat cf = (CellFormat) m_styles.Stylesheet.CellFormats.ChildElements [cellStyleIndex];
            Fill fill = (Fill) m_styles.Stylesheet.Fills.ChildElements [(int) cf.FillId.Value];
            return format_color (fill.PatternFill.ForegroundColor);
        }
 
        /// <summary>
        /// 获取单元格前景色
        /// </summary>
        /// <param name="cell">单元格</param>
        /// <returns>单元格前景色</returns>
        public String get_foreground_color(Cell cell) {
            // 获取填充类型
            int cellStyleIndex = 0;
            if (cell.StyleIndex != null)
                cellStyleIndex = (int) cell.StyleIndex.Value;
            CellFormat cf = (CellFormat) m_styles.Stylesheet.CellFormats.ChildElements [cellStyleIndex];
            Font font = (Font) m_styles.Stylesheet.Fonts.ChildElements [(int) cf.FontId.Value];
            return format_color (font.Color);
        }
 
        /// <summary>
        /// 获取单元格颜色类型
        /// </summary>
        /// <param name="strForeColor">前景色</param>
        /// <param name="strBackColor">背景色</param>
        /// <returns>单元格颜色类型</returns>
        private int get_cell_format_index(String strForeColor, String strBackColor, int iDefault = 0) {
            // 查找现有列表看是否存在已有类型
            OpenXmlElementList list = m_styles.Stylesheet.CellFormats.ChildElements;
            OpenXmlElementList tlist = m_styles.Stylesheet.Fonts.ChildElements;
            CellFormat cf = null;
            for (int i = 0; i < list.Count; ++i) {
                cf = (CellFormat)list[i];
                String strTmpForeColor = format_color (((Font) tlist [(int) cf.FontId.Value]).Color);
                String strTmpBackColor = format_color (((Fill) tlist [(int) cf.FillId.Value]).PatternFill.ForegroundColor);
                if (strTmpForeColor == strForeColor || strTmpBackColor == strBackColor)
                    return i;
            }
 
            return iDefault;
        }
    }
}

具体用法示例如下:

1
2
3
4
5
6
excel_access ea = new excel_access();
ea.open_excel ("D:/a.xlsx");
Cell cell = ea.get_item ("B3");
System.Diagnostics.Debug.WriteLine ("内容:{0}".format (ea.get_value (cell)));
System.Diagnostics.Debug.WriteLine ("前景色:{0}".format (get_foreground_color (cell)));
System.Diagnostics.Debug.WriteLine ("背景色:{0}".format (get_background_color (cell)));

以上代码演示了如何打开一个文档,以及获取单元格内容、前景色、背景色信息。在此重点解释下Tint这个Double类型变量。这个类型代表明暗程度深浅的变化,取值区间为[-1.0,1.0],如果为负代表颜色变暗,如果为正代表颜色变亮。如果存在这个值,那么代表变量中的颜色并非实际显示颜色,实际显示颜色还得再经过一轮深浅变换。公式摘自MSDN

说完了读,下面说说写。写Excel主要分为写单元格内容与设置单元格颜色等样式。首先,对于需要生成某个Excel文件,那么先用OpenXmlTool转成代码,然后找到GenerateWorksheetPart1Content函数,里面的Row、Cell这儿。大概代码如下:

1
2
3
4
5
6
7
8
9
Row row1 = new Row(){ RowIndex = (UInt32Value)1U, Spans = new ListValue<StringValue>() { InnerText = "1:1" }, DyDescent = 0.2D };
 
Cell cell1 = new Cell(){ CellReference = "A1", StyleIndex = (UInt32Value)1U, DataType = CellValues.SharedString, CellValue = new CellValue("0") };
row1.Append (cell1);
 
Cell cell2 = new Cell(){ CellReference = "B1", StyleIndex = (UInt32Value)2U, DataType = CellValues.SharedString, CellValue = new CellValue("1") };
row1.Append (cell2);
 
sheetData1.Append (row1);

Cell的属性CellReference代表单元格的位置。比如"A1"就是左上角第一个单元格。然后,关键位置注意(敲黑板),如果DataType类型为CellValues.SharedString,那么CellValue的内容为一个引用,否则为一个值。假如,需要在单元格存储一个数字或者一个小数,那么可以直接在CellValue里面写上相应的内容(一定不要是字符串),就行了。假如是一个字符串呢?那么稍微麻烦点,CellValue里面定义字符串类型的数字,写上索引,从0开始,然后,再找到GenerateSharedStringTablePart1Content函数,大概有以下这种形状的代码:

1
2
3
4
SharedStringItem sharedStringItem1 = new SharedStringItem();
sharedStringItem1.Append (new Text ("aaa"));
sharedStringItem1.Append (new PhoneticProperties () { FontId = (UInt32Value) 1U, Type = PhoneticValues.NoConversion });
sharedStringTable1.Append (sharedStringItem1);

注意下,写到这儿的值都是字符串。sharedStringTable1变量中第一个append的值,就是单元格定义那儿,CellValue为"0"的索引的那个值,相应的,第二个append的值,就是CellValue为"1"的索引的那个值。以此类推。相应的。如果A1和B1都是CellValues.SharedString类型,并且CellValue均为"0",那么它们的值都是sharedStringTable1变量中第一个append的值。

说完了单元格内容,下面说说写单元格样式。这个样式稍微麻烦点,需要知道三个东西:Font、Fill与CellFormat。简而言之,Font代表前景色,Fill代表背景色,CellFormat用于将其关联。
假如需要修改某个单元格的颜色,那么需要这样做:
1、找到GenerateWorkbookStylesPart1Content函数,Fonts这儿Count值+1,然后新建一个Font,Color这儿写成new Color () { Rgb = "FFDD9933" },前两个为F,后六个分别为RGB的十六进制值。
2、Fills这儿Count值+1,然后新建一个Fill,写如下代码:

1
2
3
4
5
6
7
Fill fill = new Fill();
fill.Append (new PatternFill () {
    PatternType = PatternValues.Solid,
    ForegroundColor = new ForegroundColor () { Rgb = "FF3399DD" },
    BackgroundColor = new BackgroundColor () { Indexed = (UInt32Value) 64U }
});
fills1.Append (fill);

其中Rgb这儿代表颜色,格式同上。另外别被名称忽悠了,这儿的ForegroundColor就代表背景色,BackgroundColor的Indexed值只能为64U。
3、CellsFormats这儿新建一个CellsFormat,写大概以下格式代码

1
2
3
4
5
6
7
8
9
10
cellFormats1.Append (new CellFormat () {
    NumberFormatId = (UInt32Value) 0U,
    FontId = (UInt32Value) 3U,
    FillId = (UInt32Value) 3U,
    BorderId = (UInt32Value) 0U,
    FormatId = (UInt32Value) 0U,
    ApplyFont = true,
    ApplyFill = true,
    Alignment = new Alignment () { Vertical = VerticalAlignmentValues.Center }
});

其中FontId代表之前新建的Font在Fonts的Append的位置,比如第一个append,那么就为0;FillId代表之前新建的Fill在Fills的Append的位置,比如第一个append那么就为0。这两个值稍微改一下,另外也不需要完全按照这种格式来,最好在自己的添加代码那儿复制一份,然后再改。注意下,边框也在CellsFormat这儿,自己新建一个有边框色的xlsx文档,然后生成代码,然后照着看一下,就明白了。
4、找到之前创建Cell的位置,比如A1单元格需要改为自己新建的格式那么StyleIndex的值改为CellFormat的Index(也就是CellFormats第几个append的位置,第一个append那么就为0)。如果是字符串,那么还得找到创建SharedStringItem的位置,找到创建PhoneticProperties对象时传的参数,FontId为字体的Id,必须与CellFormat里的FontId一致,否则生成的xlsx文件就可能有问题。

Excel操作就写完了。最后说下注意事项。CellStyleFormats与CellFormats非常相似,添加的都是CellFormat类型的对象,很容易搞混,这儿我们不关心CellStyleFormats类型对象,只添加CellFormats类型对象

发布者

fawdlstty

又一只萌萌哒程序猿~~

《C#:使用OpenXML读写Excel文档》上有2条评论

    1. 我封装的这个类只能用来读取excel,获取某单元格内容或颜色。你找到public的方法,看方法的描述,如果是你需要的,调用就行了。具体顺序open_excel、open_sheet、你自己的读取操作、close。

发表评论

电子邮件地址不会被公开。 必填项已用*标注