Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
F
Ftt_Common
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Ftt_Common
Commits
71e27d87
Commit
71e27d87
authored
Sep 04, 2020
by
tr
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
1、Excel导出封装
parent
e0ad8dea
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
653 additions
and
125 deletions
+653
-125
ExcelExportHelper.cs
FunnyPick/FunnyPick/ExcelExportHelper.cs
+613
-125
FunnyPick.csproj
FunnyPick/FunnyPick/FunnyPick.csproj
+6
-0
Utils.cs
FunnyPick/FunnyPick/Utils.cs
+34
-0
FunnyPick.csprojAssemblyReference.cache
...bug/netcoreapp3.0/FunnyPick.csprojAssemblyReference.cache
+0
-0
No files found.
FunnyPick/FunnyPick/ExcelExportHelper.cs
View file @
71e27d87
...
...
@@ -13,6 +13,8 @@ using NPOI.SS.Util;
using
RtfPipe
;
//
using
System.Data.SqlClient
;
using
System.Windows.Forms
;
using
System.Diagnostics
;
namespace
FunnyPick
{
...
...
@@ -22,10 +24,7 @@ namespace FunnyPick
public
class
ExcelExportHelper
{
#
region
私有变量
/// <summary>
/// 模板路径
/// </summary>
string
_mouldFilePath
=
""
;
/// <summary>
/// Excel文件对象
/// </summary>
...
...
@@ -34,6 +33,10 @@ namespace FunnyPick
/// 全局数据集
/// </summary>
DataSet
_ds
=
null
;
/// <summary>
/// 模板路径
/// </summary>
string
_mouldFilePath
=
""
;
#
endregion
...
...
@@ -41,31 +44,24 @@ namespace FunnyPick
/// <summary>
/// 导出文件(绝对)路径
/// </summary>
public
string
ExportFileName
{
get
;
}
=
string
.
Empty
;
public
object
FileService
{
get
;
private
set
;
}
public
string
ExportFileName
{
get
;
set
;
}
=
string
.
Empty
;
/// <summary>
/// 自定义表名列表
/// <para>如果设置该列表,则只取列表中的表数据,否则取所有数据</para>
/// </summary>
public
List
<
string
>
CustomTableList
=
null
;
/// <summary>
/// 构造函数
/// <para>配合UnifyExport()使用</para>
/// </summary>
/// <param name="excelMouldName">模板名称</param>
/// <param name="xmlName">(数据源)xml配置文件</param>
/// <param name="keyID">单据主键</param>
public
ExcelExportHelper
(
string
excelMouldName
,
string
xmlName
,
int
keyID
)
{
//赋值全局数据源
this
.
_ds
=
this
.
GetExportExcelDataSource
(
xmlName
,
keyID
);
//模板临时文件及Excel对象初始化
this
.
_mouldFilePath
=
Path
.
Combine
(
Utils
.
StartupPath
,
"Template"
,
excelMouldName
);
this
.
ExportFileName
=
Path
.
Combine
(
Utils
.
StartupPath
,
"temp"
,
$"Excel导出临时文件-
{
DateTime
.
Now
.
ToString
(
"yyyMMdd-hhmmss"
)}
.xls"
);
this
.
_Workbook
=
this
.
GetWorkbook
(
this
.
_mouldFilePath
);
}
public
ExcelExportHelper
()
{
}
/// <summary>
/// 导出(单sheet页)
/// </summary>
/// <returns></returns>
p
ublic
bool
Export
()
p
rivate
bool
Export
()
{
try
{
...
...
@@ -91,7 +87,7 @@ namespace FunnyPick
/// 导出(多sheet页)
/// </summary>
/// <returns></returns>
p
ublic
bool
ExportSheets
(
string
tableName
,
string
groupCol
)
p
rivate
bool
ExportSheets
(
string
tableName
,
string
groupCol
)
{
try
{
...
...
@@ -134,7 +130,7 @@ namespace FunnyPick
/// <param name="groupCol"></param>
/// <param name="Reclassify"></param>
/// <returns></returns>
p
ublic
bool
ExportSOP
(
string
tableName
,
string
groupCol
,
bool
landscape
=
false
)
p
rivate
bool
ExportSOP
(
string
tableName
,
string
groupCol
)
{
try
{
...
...
@@ -162,14 +158,66 @@ namespace FunnyPick
}
}
//删除模板文件
this
.
_Workbook
.
RemoveSheetAt
(
1
);
this
.
_Workbook
.
RemoveSheetAt
(
1
);
//横向打印
for
(
int
i
=
0
;
i
<
this
.
_Workbook
.
NumberOfSheets
;
i
++)
this
.
_Workbook
.
RemoveSheetAt
(
1
);
//删除:工艺单汇总-模板
this
.
_Workbook
.
RemoveSheetAt
(
1
);
//删除:工艺明细-模板
//保存Excel文件
this
.
SaveExcel
(
this
.
ExportFileName
);
}
catch
(
Exception
ex
)
{
var
sheet_i
=
this
.
_Workbook
.
GetSheetAt
(
i
)
as
HSSFSheet
;
sheet_i
.
PrintSetup
.
Landscape
=
landscap
e
;
Utils
.
ShowMessage
(
$"导出失败:
{
ex
.
Message
}
"
)
;
return
fals
e
;
}
finally
{
GC
.
Collect
();
}
return
true
;
}
/// <summary>
/// 导出(款式SOP维护-工艺单)
/// </summary>
/// <param name="tableName"></param>
/// <param name="groupCol"></param>
/// <param name="Reclassify"></param>
/// <returns></returns>
private
bool
ExportSOP1
(
string
tableName
,
string
groupCol
,
string
numCol
)
{
try
{
//获取分组
var
groupList
=
Utils
.
GetColumnList
(
this
.
_ds
.
Tables
[
tableName
],
groupCol
);
//按照分组创建新的sheet页, 并复制sheet0的模板格式
var
sheet0
=
this
.
_Workbook
?.
GetSheetAt
(
0
)
as
HSSFSheet
;
//封页
var
sheet1
=
this
.
_Workbook
?.
GetSheetAt
(
1
)
as
HSSFSheet
;
//工艺单汇总-模板
var
sheet2
=
this
.
_Workbook
?.
GetSheetAt
(
2
)
as
HSSFSheet
;
//工艺明细-模板
DataTable
dt
=
this
.
_ds
.
Tables
[
tableName
];
this
.
FillSheet
(
sheet0
);
for
(
int
i
=
0
;
i
<
groupList
.
Count
;
i
++)
{
var
sheetNew
=
this
.
_Workbook
.
CreateSheet
(
$"
{
groupList
[
i
]}
流程"
)
as
HSSFSheet
;
this
.
CopySheet
(
sheet1
,
sheetNew
);
//填充汇总数据
dt
.
DefaultView
.
RowFilter
=
$"
{
groupCol
}
= '
{
groupList
[
i
]}
'"
;
this
.
FillSheet
(
sheetNew
);
//根据页码渲染明细
var
SheetNoList
=
GetSheetNoFromDefaultView
(
dt
.
DefaultView
,
numCol
);
SheetNoList
.
Sort
();
//排序
foreach
(
var
n
in
SheetNoList
)
{
dt
.
DefaultView
.
RowFilter
=
$"
{
groupCol
}
= '
{
groupList
[
i
]}
' and ISNULL(
{
numCol
}
,'')='
{
n
}
'"
;
if
(
dt
.
DefaultView
.
Count
==
0
)
continue
;
var
IEName
=
dt
.
DefaultView
[
0
][
"IEName"
].
ToString
();
var
sheetNewIE
=
this
.
_Workbook
.
CreateSheet
(
$"
{
groupList
[
i
]}{
n
}
-
{
IEName
.
Replace
(
@"/"
,
"_"
)}
"
)
as
HSSFSheet
;
this
.
CopySheet
(
sheet2
,
sheetNewIE
);
this
.
FillSheet
(
sheetNewIE
);
}
}
//删除模板文件
this
.
_Workbook
.
RemoveSheetAt
(
1
);
//删除:工艺单汇总-模板
this
.
_Workbook
.
RemoveSheetAt
(
1
);
//删除:工艺明细-模板
//保存Excel文件
this
.
SaveExcel
(
this
.
ExportFileName
);
}
...
...
@@ -185,6 +233,28 @@ namespace FunnyPick
return
true
;
}
/// <summary>
/// 获取页号集合
/// </summary>
/// <param name="defaultView"></param>
/// <returns></returns>
private
List
<
string
>
GetSheetNoFromDefaultView
(
DataView
defaultView
,
string
numCol
)
{
var
result
=
new
List
<
string
>();
if
(
Utils
.
IsContainsColumn
(
defaultView
,
numCol
))
{
foreach
(
DataRowView
r
in
defaultView
)
{
var
n
=
r
[
numCol
].
ToString
();
if
(
result
.
Contains
(
n
))
continue
;
//
result
.
Add
(
n
);
}
}
return
result
;
}
/// <summary>
/// 复制Sheet页
/// </summary>
...
...
@@ -214,6 +284,8 @@ namespace FunnyPick
{
to
.
AddMergedRegion
(
from
.
GetMergedRegion
(
i
));
}
//复制打印设置
this
.
CopyPrintSetup
(
from
,
to
);
}
/// <summary>
/// 复制行(内容、列宽、类型与格式)
...
...
@@ -239,17 +311,54 @@ namespace FunnyPick
targetCell
.
SetCellType
(
sourceCell
.
CellType
);
//复制单元格类型
}
}
/// <summary>
/// 复制打印设置
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
public
void
CopyPrintSetup
(
HSSFSheet
from
,
HSSFSheet
to
)
{
//打印设置复制
IPrintSetup
s_printSetup
=
from
.
PrintSetup
;
IPrintSetup
t_printSetup
=
to
.
PrintSetup
;
t_printSetup
.
Copies
=
s_printSetup
.
Copies
;
t_printSetup
.
Draft
=
s_printSetup
.
Draft
;
t_printSetup
.
FitHeight
=
s_printSetup
.
FitHeight
;
t_printSetup
.
FitWidth
=
s_printSetup
.
FitWidth
;
t_printSetup
.
FooterMargin
=
s_printSetup
.
FooterMargin
;
t_printSetup
.
HeaderMargin
=
s_printSetup
.
HeaderMargin
;
t_printSetup
.
HResolution
=
s_printSetup
.
HResolution
;
t_printSetup
.
Landscape
=
s_printSetup
.
Landscape
;
t_printSetup
.
LeftToRight
=
s_printSetup
.
LeftToRight
;
t_printSetup
.
NoColor
=
s_printSetup
.
NoColor
;
t_printSetup
.
NoOrientation
=
s_printSetup
.
NoOrientation
;
t_printSetup
.
Notes
=
s_printSetup
.
Notes
;
t_printSetup
.
PageStart
=
s_printSetup
.
PageStart
;
t_printSetup
.
PaperSize
=
s_printSetup
.
PaperSize
;
t_printSetup
.
Scale
=
s_printSetup
.
Scale
;
t_printSetup
.
UsePage
=
s_printSetup
.
UsePage
;
t_printSetup
.
ValidSettings
=
s_printSetup
.
ValidSettings
;
t_printSetup
.
VResolution
=
s_printSetup
.
VResolution
;
//边距复制
to
.
SetMargin
(
MarginType
.
TopMargin
,
from
.
GetMargin
(
MarginType
.
TopMargin
));
to
.
SetMargin
(
MarginType
.
RightMargin
,
from
.
GetMargin
(
MarginType
.
RightMargin
));
to
.
SetMargin
(
MarginType
.
BottomMargin
,
from
.
GetMargin
(
MarginType
.
BottomMargin
));
to
.
SetMargin
(
MarginType
.
LeftMargin
,
from
.
GetMargin
(
MarginType
.
LeftMargin
));
}
/// <summary>
/// 填充Sheet模板
/// </summary>
/// <param name="sheet"></param>
/// <param name="rowIndex"></param>
private
void
FillSheet
(
HSSFSheet
sheet
)
{
/*模板导出规则:
* 1、每行只能是一种类型,要么全部是基本信息,要么全是明细(如果混合,则按第一个绑定信息类型界定此行类型)
* 基本信息绑定格式:[表名.字段名]
* 明细信息绑定格式:(表名.字段名)
* 合并信息绑定格式:{表名.字段名}
* 二维码绑定格式:<表名.字段名>
* 2、图片必须以Image或Picture结尾,区分大小写
*/
...
...
@@ -270,8 +379,10 @@ namespace FunnyPick
string
bind
=
cell
.
StringCellValue
;
bool
IsBaseInfo
=
bind
.
StartsWith
(
"["
)
&&
bind
.
EndsWith
(
"]"
);
bool
IsDetailInfo
=
bind
.
StartsWith
(
"("
)
&&
bind
.
EndsWith
(
")"
);
string
[]
strArray
=
bind
.
Trim
(
new
char
[]
{
'['
,
']'
,
'('
,
')'
}).
Split
(
'.'
);
//去除括号,并分割表名与列名
if
((
IsBaseInfo
||
IsDetailInfo
)
&&
strArray
.
Length
==
2
)
bool
IsMerge
=
bind
.
StartsWith
(
"{"
)
&&
bind
.
EndsWith
(
"}"
);
bool
IsQRCode
=
bind
.
StartsWith
(
"<"
)
&&
bind
.
EndsWith
(
">"
);
string
[]
strArray
=
bind
.
Trim
(
new
char
[]
{
'['
,
']'
,
'('
,
')'
,
'{'
,
'}'
,
'<'
,
'>'
}).
Split
(
'.'
);
//去除括号,并分割表名与列名
if
((
IsBaseInfo
||
IsDetailInfo
||
IsMerge
||
IsQRCode
)
&&
strArray
.
Length
==
2
)
{
bindList
.
Add
(
new
BindInfo
{
...
...
@@ -279,16 +390,20 @@ namespace FunnyPick
ColumnName
=
strArray
[
1
],
CellColumn
=
col
,
IsDetailInfo
=
IsDetailInfo
,
IsMerge
=
IsMerge
,
IsQRCode
=
IsQRCode
,
});
}
}
//填充数据
int
scale
=
1
;
//不管是否绑定,行都要加1,所以这里默认为1
if
(
bindList
.
Count
>
0
)
if
(
bindList
.
Count
>
0
&&
this
.
_ds
.
Tables
[
bindList
[
0
].
TableName
]
!=
null
)
{
EmptyCounts
=
0
;
//命中数据行时,清空计数
int
ViewCount
=
this
.
_ds
.
Tables
[
bindList
[
0
].
TableName
].
DefaultView
.
Count
;
//数据总行数
if
(
bindList
[
0
].
IsDetailInfo
&&
ViewCount
>
1
)
//明细,并且多余1行时,采用插行渲染
bool
IsDetailInfo
=
bindList
[
0
].
IsDetailInfo
;
//同行的第1个绑定,决定该行类型
string
tableName
=
bindList
[
0
].
TableName
;
int
ViewCount
=
this
.
_ds
.
Tables
.
Contains
(
tableName
)
?
this
.
_ds
.
Tables
[
tableName
].
DefaultView
.
Count
:
0
;
//数据总行数(表不存在数据源中,当0处理)
if
(
IsDetailInfo
&&
ViewCount
>
1
)
//明细,并且多余1行时,采用插行渲染
{
int
InsertRowCount
=
ViewCount
-
1
;
//本来就有一行,所以:插入行数 = 数据行数 - 1
this
.
MyInsertRow
(
sheet
,
currentRow
,
InsertRowCount
,
(
HSSFRow
)
sheet
.
GetRow
(
currentRow
-
1
));
//从当前行开始,复制当前行上一行的行格式
...
...
@@ -296,7 +411,6 @@ namespace FunnyPick
{
foreach
(
BindInfo
bind
in
bindList
)
{
//这里减1,是为了消除scale默认值
ICell
cell
=
sheet
?.
GetRow
(
currentRow
+
i
).
GetCell
(
bind
.
CellColumn
);
this
.
LoadData
(
cell
,
bind
,
RowIndex
:
i
);
}
...
...
@@ -304,13 +418,15 @@ namespace FunnyPick
//赋值标量:插入行数大于1时,标量 = 插入行数
if
(
InsertRowCount
>
scale
)
scale
=
InsertRowCount
;
}
else
{
foreach
(
BindInfo
bind
in
bindList
)
{
ICell
cell
=
sheet
?.
GetRow
(
currentRow
).
GetCell
(
bind
.
CellColumn
);
if
(
bind
.
IsMerge
)
this
.
LoadDataMerge
(
cell
,
bind
);
else
this
.
LoadData
(
cell
,
bind
);
}
}
...
...
@@ -327,12 +443,19 @@ namespace FunnyPick
}
/// <summary>
/// 填充Sheet模板:单行渲染
/// </summary>
/// <param name="sheet"></param>
/// <param name="rowIndex"></param>
private
void
FillSheet
(
HSSFSheet
sheet
,
int
rowIndex
)
{
/*模板导出规则:
* 1、每行只能是一种类型,要么全部是基本信息,要么全是明细(如果混合,则按第一个绑定信息类型界定此行类型)
* 基本信息绑定格式:[表名.字段名]
* 明细信息绑定格式:(表名.字段名)
* 合并信息绑定格式:{表名.字段名}
* 二维码绑定格式:<表名.字段名>
* 2、图片必须以Image或Picture结尾,区分大小写
*/
...
...
@@ -353,8 +476,10 @@ namespace FunnyPick
string
bind
=
cell
.
StringCellValue
;
bool
IsBaseInfo
=
bind
.
StartsWith
(
"["
)
&&
bind
.
EndsWith
(
"]"
);
bool
IsDetailInfo
=
bind
.
StartsWith
(
"("
)
&&
bind
.
EndsWith
(
")"
);
string
[]
strArray
=
bind
.
Trim
(
new
char
[]
{
'['
,
']'
,
'('
,
')'
}).
Split
(
'.'
);
//去除括号,并分割表名与列名
if
((
IsBaseInfo
||
IsDetailInfo
)
&&
strArray
.
Length
==
2
)
bool
IsMerge
=
bind
.
StartsWith
(
"{"
)
&&
bind
.
EndsWith
(
"}"
);
bool
IsQRCode
=
bind
.
StartsWith
(
"<"
)
&&
bind
.
EndsWith
(
">"
);
string
[]
strArray
=
bind
.
Trim
(
new
char
[]
{
'['
,
']'
,
'('
,
')'
,
'{'
,
'}'
,
'<'
,
'>'
}).
Split
(
'.'
);
//去除括号,并分割表名与列名
if
((
IsBaseInfo
||
IsDetailInfo
||
IsMerge
||
IsQRCode
)
&&
strArray
.
Length
==
2
)
{
bindList
.
Add
(
new
BindInfo
{
...
...
@@ -362,6 +487,8 @@ namespace FunnyPick
ColumnName
=
strArray
[
1
],
CellColumn
=
col
,
IsDetailInfo
=
IsDetailInfo
,
IsMerge
=
IsMerge
,
IsQRCode
=
IsQRCode
,
});
}
}
...
...
@@ -404,11 +531,11 @@ namespace FunnyPick
bool
IsImage
=
ColumnName
.
EndsWith
(
"Image"
)
||
ColumnName
.
EndsWith
(
"Picture"
);
//图片列规范:必须以Image或Picture结尾,区分大小写
if
(
this
.
_ds
.
Tables
.
Contains
(
TableName
)
&&
this
.
_ds
.
Tables
[
TableName
].
DefaultView
.
Count
>
0
&&
this
.
_ds
.
Tables
[
TableName
].
Columns
.
Contains
(
ColumnName
))
{
cell
.
SetCellValue
(
""
);
//先清除单元格信息
var
dr
=
this
.
_ds
.
Tables
[
TableName
].
DefaultView
[
RowIndex
];
if
(
IsImage
)
{
cell
.
SetCellValue
(
""
);
//先清除单元格信息
#
region
加载图片
try
...
...
@@ -417,20 +544,35 @@ namespace FunnyPick
if
(
dr
[
ColumnName
]
!=
DBNull
.
Value
&&
dr
[
ColumnName
].
GetType
().
Name
.
ToLower
().
Equals
(
"byte[]"
))
{
byte
[]
bytes
=
dr
[
ColumnName
]
as
byte
[];
if
(
bytes
.
Length
==
0
)
return
;
bool
IsSegment
=
bytes
.
Length
.
Equals
(
32
);
//是否进行图片分离
var
img
=
IsSegment
?
Utils
.
FetchImageSingle
(
bytes
)
:
bytes
;
int
CellRow
=
cell
.
RowIndex
;
int
CellCol
=
cell
.
ColumnIndex
;
var
range
=
cell
.
IsMergedCell
?
this
.
GetMergedRange
(
cell
)
:
new
CellRangeAddress
(
CellRow
,
CellRow
,
CellCol
,
CellCol
);
var
range
=
this
.
GetMergedRange
(
cell
);
this
.
LoadImage
(
cell
,
img
,
range
);
}
}
catch
(
Exception
ex
)
{
throw
new
Exception
(
$"加载图片出错:
{
ex
.
Message
}
"
);
//Utils.Write
($"加载图片出错:{ex.Message}");
}
#
endregion
}
else
if
(
bind
.
IsQRCode
)
{
#
region
二维码处理
var
str
=
dr
[
ColumnName
].
ToString
();
if
(
string
.
IsNullOrEmpty
(
str
))
return
;
var
bytes
=
Utils
.
GenQRCode
(
dr
[
ColumnName
].
ToString
());
if
(
bytes
==
null
||
bytes
.
Length
==
0
)
return
;
var
range
=
this
.
GetMergedRange
(
cell
);
this
.
LoadImage
(
cell
,
bytes
,
range
);
#
endregion
}
else
{
cell
.
SetCellValue
(
dr
[
ColumnName
].
ToString
());
...
...
@@ -457,7 +599,7 @@ namespace FunnyPick
//取出图片宽高
var
ms
=
new
MemoryStream
(
bytes
);
Bitmap
bmp
=
bmp
=
new
Bitmap
(
ms
);
Bitmap
bmp
=
new
Bitmap
(
ms
);
var
height
=
bmp
.
Height
;
var
width
=
bmp
.
Width
;
// 图片位置空间的高度
...
...
@@ -465,7 +607,7 @@ namespace FunnyPick
for
(
int
i
=
1
;
i
<=
range
.
LastRow
-
range
.
FirstRow
+
1
;
i
++)
AreaHeight
+=
sheet
.
GetRow
(
i
).
Height
;
//根据宽高比计算出宽度
int
AreaWidth
=
width
*
AreaHeight
/
height
;
int
AreaWidth
=
AreaHeight
*
width
/
height
;
//获取图片索引
int
picIndex
=
sheet
.
Workbook
.
AddPicture
(
bytes
,
PictureType
.
JPEG
);
//创建图片锚点
...
...
@@ -483,22 +625,155 @@ namespace FunnyPick
};
//重新绘制图片
IDrawing
patriarch
=
null
;
IDrawing
patriarch
=
this
.
GetSheetDrawingObject
(
sheet
);
var
pict
=
(
HSSFPicture
)
patriarch
.
CreatePicture
(
anchor
,
picIndex
);
bmp
.
Dispose
();
ms
.
Close
();
}
#
region
多行合并渲染
/// <summary>
/// 加载单元格数据
/// </summary>
/// <param name="cell"></param>
/// <param name="bind"></param>
/// <param name="RowIndex"></param>
private
void
LoadDataMerge
(
ICell
cell
,
BindInfo
bind
)
{
if
(
cell
==
null
)
return
;
string
TableName
=
bind
.
TableName
;
//表名
string
ColumnName
=
bind
.
ColumnName
;
//列名
bool
IsImage
=
ColumnName
.
EndsWith
(
"Image"
)
||
ColumnName
.
EndsWith
(
"Picture"
);
//图片列规范:必须以Image或Picture结尾,区分大小写
if
(
this
.
_ds
.
Tables
.
Contains
(
TableName
)
&&
this
.
_ds
.
Tables
[
TableName
].
DefaultView
.
Count
>
0
&&
this
.
_ds
.
Tables
[
TableName
].
Columns
.
Contains
(
ColumnName
))
{
if
(
IsImage
)
{
cell
.
SetCellValue
(
""
);
//先清除单元格信息
#
region
加载图片
try
{
// 第一次取不到居然会有异常抛出...
patriarch
=
sheet
.
DrawingPatriarch
;
//获取单元格范围
int
CellRow
=
cell
.
RowIndex
;
int
CellCol
=
cell
.
ColumnIndex
;
var
range
=
cell
.
IsMergedCell
?
this
.
GetMergedRange
(
cell
)
:
new
CellRangeAddress
(
CellRow
,
CellRow
,
CellCol
,
CellCol
);
//循环当前视图,填充图片
int
index
=
0
;
foreach
(
DataRowView
r
in
this
.
_ds
.
Tables
[
TableName
].
DefaultView
)
{
//图片列非空,并且是byte[]类型才处理
if
(
r
[
ColumnName
]
!=
DBNull
.
Value
&&
r
[
ColumnName
].
GetType
().
Name
.
ToLower
().
Equals
(
"byte[]"
))
{
byte
[]
bytes
=
r
[
ColumnName
]
as
byte
[];
if
(
bytes
.
Length
==
0
)
continue
;
bool
IsSegment
=
bytes
.
Length
.
Equals
(
32
);
//是否进行图片分离
var
img
=
IsSegment
?
Utils
.
FetchImageSingle
(
bytes
)
:
bytes
;
//加载图片
this
.
LoadImageMerge
(
cell
,
img
,
range
,
index
);
index
++;
}
catch
}
}
catch
(
Exception
ex
)
{
//Utils.Write($"加载图片出错:{ex.Message}");
}
#
endregion
}
else
{
var
mergeStrList
=
new
List
<
string
>();
foreach
(
DataRowView
r
in
this
.
_ds
.
Tables
[
TableName
].
DefaultView
)
{
patriarch
=
sheet
.
CreateDrawingPatriarch
();
mergeStrList
.
Add
(
r
[
ColumnName
].
ToString
().
Trim
());
}
cell
.
SetCellValue
(
string
.
Join
(
"\r\n"
,
mergeStrList
));
}
}
else
{
//不存在的列名或表名,直接清空
cell
.
SetCellValue
(
""
);
}
}
/// <summary>
/// 绘制图片
/// </summary>
/// <param name="cell"></param>
/// <param name="bytes"></param>
/// <param name="range"></param>
private
void
LoadImageMerge
(
ICell
cell
,
byte
[]
bytes
,
CellRangeAddress
range
,
int
index
)
{
if
(
bytes
.
Length
==
0
)
return
;
//获取单元格对应的Sheet页
HSSFSheet
sheet
=
cell
.
Sheet
as
HSSFSheet
;
int
Columnlndex
=
range
.
FirstColumn
+
index
;
//取出图片宽高
var
ms
=
new
MemoryStream
(
bytes
);
Bitmap
bmp
=
bmp
=
new
Bitmap
(
ms
);
var
height
=
bmp
.
Height
;
var
width
=
bmp
.
Width
;
// 图片位置空间的高度
short
AreaHeight
=
0
;
for
(
int
i
=
0
;
i
<=
range
.
LastRow
-
range
.
FirstRow
;
i
++)
AreaHeight
+=
sheet
.
GetRow
(
i
).
Height
;
//根据列宽填充(在一个合并区域里,每个图片占一列)
int
AreaWidth
=
sheet
.
GetColumnWidth
(
Columnlndex
);
//获取图片索引
int
picIndex
=
sheet
.
Workbook
.
AddPicture
(
bytes
,
PictureType
.
JPEG
);
//创建图片锚点
var
anchor
=
new
HSSFClientAnchor
()
{
Col1
=
Columnlndex
,
//起始列
Col2
=
Columnlndex
,
//结束列
Row1
=
range
.
FirstRow
,
//起始行
Row2
=
range
.
LastRow
,
//结束行
//说明:此处的x和y可看做相对于左上边框的偏移位置;左上(dx1,dy1)和右下(dx2,dy2)分别可看做矩形框的对角顶点,图片在此矩形内绘制
Dx1
=
0
,
Dy1
=
0
,
//起始单元格中的x,y坐标
Dx2
=
AreaWidth
,
Dy2
=
AreaHeight
//结束单元格中的x, y坐标
};
//重新绘制图片
IDrawing
patriarch
=
this
.
GetSheetDrawingObject
(
sheet
);
var
pict
=
(
HSSFPicture
)
patriarch
.
CreatePicture
(
anchor
,
picIndex
);
bmp
.
Dispose
();
ms
.
Close
();
}
#
endregion
/// <summary>
/// 获取绘图对象
/// </summary>
/// <param name="sheet"></param>
/// <returns></returns>
private
IDrawing
GetSheetDrawingObject
(
HSSFSheet
sheet
)
{
IDrawing
result
=
null
;
try
{
// 第一次取不到居然会有异常抛出...
result
=
sheet
.
DrawingPatriarch
;
}
catch
{
result
=
sheet
.
CreateDrawingPatriarch
();
}
return
result
;
}
/// <summary>
/// 获取合并列区域
/// </summary>
...
...
@@ -509,7 +784,8 @@ namespace FunnyPick
{
CellRangeAddress
result
=
null
;
//获取单元格对应的Sheet页
if
(
cell
.
IsMergedCell
)
{
HSSFSheet
sheet
=
cell
.
Sheet
as
HSSFSheet
;
for
(
int
i
=
0
;
i
<
sheet
.
NumMergedRegions
;
i
++)
{
...
...
@@ -521,9 +797,70 @@ namespace FunnyPick
break
;
}
}
}
else
{
int
row
=
cell
.
RowIndex
;
int
col
=
cell
.
ColumnIndex
;
result
=
new
CellRangeAddress
(
row
,
row
,
col
,
col
);
}
return
result
;
}
/// <summary>
/// 批量插入复制行
/// </summary>
/// <param name="sheet">Excel页</param>
/// <param name="RowStart">起始行</param>
/// <param name="RowCount">(插入)总行数</param>
/// <param name="sourceRow">(复制)源</param>
private
void
MyInsertRow
(
HSSFSheet
sheet
,
int
RowStart
,
int
RowCount
,
HSSFRow
sourceRow
)
{
//插入行数为0时,直接返回
if
(
RowCount
<=
0
)
return
;
#
region
批量移动行
sheet
.
ShiftRows
(
RowStart
,
//--开始行
sheet
.
LastRowNum
,
//--结束行
RowCount
,
//--插入行数
true
,
//是否复制行高
false
,
//是否重置行高
true
//是否移动批注
);
#
endregion
#
region
创建行,并复制
rowSource
行的格式
//首行特殊处理,并且后续各行行高要与首行一致
HSSFRow
firstTargetRow
=
sheet
.
GetRow
(
RowStart
)
as
HSSFRow
;
//复制单元格样式(除了第一行,第一行的行已经存在,这里处理未创建的行)
for
(
int
i
=
RowStart
;
i
<
RowStart
+
RowCount
;
i
++)
{
HSSFRow
targetRow
=
null
;
HSSFCell
sourceCell
=
null
;
HSSFCell
targetCell
=
null
;
targetRow
=
i
==
RowStart
?
firstTargetRow
:
sheet
.
CreateRow
(
i
)
as
HSSFRow
;
targetRow
.
Height
=
firstTargetRow
.
Height
;
//复制与首行保持一致
for
(
int
m
=
sourceRow
.
FirstCellNum
;
m
<
sourceRow
.
LastCellNum
;
m
++)
{
sourceCell
=
sourceRow
.
GetCell
(
m
)
as
HSSFCell
;
if
(
sourceCell
==
null
)
continue
;
targetCell
=
targetRow
.
CreateCell
(
m
)
as
HSSFCell
;
targetCell
.
CellStyle
=
sourceCell
.
CellStyle
;
targetCell
.
SetCellType
(
sourceCell
.
CellType
);
//合并列处理
if
(
sourceCell
.
IsMergedCell
)
{
var
range
=
this
.
GetMergedRange
(
sourceCell
);
//这里的行跨度都是1,所以主要处理列
var
newRange
=
new
CellRangeAddress
(
i
,
i
,
range
.
FirstColumn
,
range
.
LastColumn
);
sheet
.
AddMergedRegion
(
newRange
);
}
}
}
#
endregion
}
/// <summary>
/// 获取Excel文件对象
...
...
@@ -559,6 +896,7 @@ namespace FunnyPick
fs
.
Close
();
}
}
/// <summary>
/// 设置单元格的值
/// </summary>
...
...
@@ -605,65 +943,12 @@ namespace FunnyPick
/// <param name="rowend"></param>
/// <param name="colstart"></param>
/// <param name="colend"></param>
p
ublic
void
SetMergedCell
(
HSSFSheet
sheet
,
int
rowstart
,
int
rowend
,
int
colstart
,
int
colend
)
p
rivate
void
SetMergedCell
(
HSSFSheet
sheet
,
int
rowstart
,
int
rowend
,
int
colstart
,
int
colend
)
{
CellRangeAddress
range
=
new
CellRangeAddress
(
rowstart
,
rowend
,
colstart
,
colend
);
sheet
.
AddMergedRegion
(
range
);
}
/// <summary>
/// 批量插入复制行
/// </summary>
/// <param name="sheet">Excel页</param>
/// <param name="RowStart">起始行</param>
/// <param name="RowCount">(插入)总行数</param>
/// <param name="sourceRow">(复制)源</param>
private
void
MyInsertRow
(
HSSFSheet
sheet
,
int
RowStart
,
int
RowCount
,
HSSFRow
sourceRow
)
{
//插入行数为0时,直接返回
if
(
RowCount
<=
0
)
return
;
#
region
批量移动行
sheet
.
ShiftRows
(
RowStart
,
//--开始行
sheet
.
LastRowNum
,
//--结束行
RowCount
,
//--插入行数
true
,
//是否复制行高
false
,
//是否重置行高
true
//是否移动批注
);
#
endregion
#
region
创建行,并复制
rowSource
行的格式
//首行特殊处理,并且后续各行行高要与首行一致
HSSFRow
firstTargetRow
=
sheet
.
GetRow
(
RowStart
)
as
HSSFRow
;
//复制单元格样式(除了第一行,第一行的行已经存在,这里处理未创建的行)
for
(
int
i
=
RowStart
;
i
<
RowStart
+
RowCount
;
i
++)
{
HSSFRow
targetRow
=
null
;
HSSFCell
sourceCell
=
null
;
HSSFCell
targetCell
=
null
;
targetRow
=
i
==
RowStart
?
firstTargetRow
:
sheet
.
CreateRow
(
i
)
as
HSSFRow
;
targetRow
.
Height
=
firstTargetRow
.
Height
;
//复制与首行保持一致
for
(
int
m
=
sourceRow
.
FirstCellNum
;
m
<
sourceRow
.
LastCellNum
;
m
++)
{
sourceCell
=
sourceRow
.
GetCell
(
m
)
as
HSSFCell
;
if
(
sourceCell
==
null
)
continue
;
targetCell
=
targetRow
.
CreateCell
(
m
)
as
HSSFCell
;
targetCell
.
CellStyle
=
sourceCell
.
CellStyle
;
targetCell
.
SetCellType
(
sourceCell
.
CellType
);
//合并列处理
if
(
sourceCell
.
IsMergedCell
)
{
var
range
=
this
.
GetMergedRange
(
sourceCell
);
//这里的行跨度都是1,所以主要处理列
var
newRange
=
new
CellRangeAddress
(
i
,
i
,
range
.
FirstColumn
,
range
.
LastColumn
);
sheet
.
AddMergedRegion
(
newRange
);
}
}
}
#
endregion
}
#
region
数据源处理
...
...
@@ -672,7 +957,7 @@ namespace FunnyPick
/// </summary>
/// <param name="xmlName"></param>
/// <returns></returns>
public
DataSet
Get
ExportExcelDataSource
(
string
xmlName
,
int
keyID
)
public
DataSet
Get
DataFromXml
(
string
xmlName
,
int
keyID
)
{
DataSet
result
=
new
DataSet
();
...
...
@@ -682,30 +967,34 @@ namespace FunnyPick
{
xmlName
+=
".xml"
;
}
var
xmlPath
=
$@"
{
Utils
.
Xml
Path
}
\
{
xmlName
}
"
;
var
xmlPath
=
$@"
{
Utils
.
ConfigFile
Path
}
\
{
xmlName
}
"
;
if
(!
File
.
Exists
(
xmlPath
))
return
result
;
XDocument
xml
=
XDocument
.
Load
(
xmlPath
);
var
tables
=
xml
.
Root
.
Element
(
"ExcelExport"
).
Elements
(
"Table"
);
using
(
MySqlCommand
cmd
=
new
My
SqlCommand
())
using
(
SqlCommand
cmd
=
new
SqlCommand
())
{
foreach
(
XElement
e
in
tables
)
{
//Name为必须字段,其他都为非必须字段
string
Name
=
e
.
Attribute
(
"Name"
).
Value
;
//做过滤,部分多数据源,可提高查询速度
if
(
CustomTableList
!=
null
&&
CustomTableList
.
Count
>
0
&&
!
CustomTableList
.
Contains
(
Name
))
continue
;
//
string
IsConvert
=
e
.
Attribute
(
"IsConvert"
)?.
Value
;
string
NameCol
=
e
.
Attribute
(
"NameCol"
)?.
Value
;
string
ValueCol
=
e
.
Attribute
(
"ValueCol"
)?.
Value
;
string
SumValueCol
=
e
.
Attribute
(
"SumValueCol"
)?.
Value
;
string
IsRtf
=
e
.
Attribute
(
"IsRtf"
)?.
Value
;
string
sql
=
e
.
Value
;
DataTable
dt
=
cmd
.
Execute
(
sql
,
CommandType
.
Text
,
new
System
.
Data
.
SqlClient
.
SqlParameter
(
"ID"
,
keyID
))
;
dt
=
this
.
ConvertTable
(
dt
,
Name
,
IsConvert
,
NameCol
,
ValueCol
,
SumValueCol
,
IsRtf
);
if
(!
result
.
Tables
.
Contains
(
Name
))
{
result
.
Tables
.
Add
(
dt
);
}
//DataTable dt = cmd.e(sql, CommandType.Text, new System.Data.SqlClient.SqlParameter("ID", keyID)).Tables[0]
;
//
dt = this.ConvertTable(dt, Name, IsConvert, NameCol, ValueCol, SumValueCol, IsRtf);
//
if (!result.Tables.Contains(Name))
//
{
//
result.Tables.Add(dt);
//
}
}
}
...
...
@@ -755,7 +1044,7 @@ namespace FunnyPick
drNew
[
col
]
=
val
;
if
(
IsNeedSum
)
{
sum
+=
Utils
.
UConvert
.
ToDecimal
(
val
,
2
);
sum
+=
decimal
.
Round
(
Convert
.
ToDecimal
(
val
)
,
2
);
}
}
if
(
IsNeedSum
)
...
...
@@ -800,17 +1089,216 @@ namespace FunnyPick
}
#
endregion
#
region
统一导出接口
/// <summary>
/// 统一导出接口(SOP定制)
/// </summary>
/// <param name="lnk">标签控件</param>
/// <param name="xml">xml配置文件</param>
/// <param name="keyID">主键ID</param>
/// <param name="m">导出模型</param>
/// <param name="exportPath">导出路径</param>
public
void
UnifyExportSOP
(
string
xml
,
int
keyID
,
ExportModel
m
,
string
mName
)
{
if
(
m
==
null
)
{
Utils
.
ThrowErrorMessage
(
"导出Excel必须设置导出模型!"
);
return
;
}
//全局变量初始化
this
.
_ds
=
this
.
GetDataFromXml
(
xml
,
keyID
);
this
.
_mouldFilePath
=
$@"
{
Utils
.
ExcelTemplateFilePath
}
\
{
m
.
UniquePrefix
}{
mName
}
"
;
this
.
_Workbook
=
this
.
GetWorkbook
(
this
.
_mouldFilePath
);
this
.
ExportFileName
=
this
.
GetExportFileFullName
();
//如未显式指定导出模型,则从配置文件读取
bool
IsSuccess
=
this
.
ExportSOP1
(
m
.
GroupTableName
,
m
.
GroupColumnName
,
m
.
ReclassifyColumnName
);
if
(
IsSuccess
)
{
Process
.
Start
(
this
.
ExportFileName
);
}
}
/// <summary>
/// 统一导出接口
/// </summary>
/// <param name="lnk">标签控件</param>
/// <param name="xml">xml配置文件</param>
/// <param name="keyID">主键ID</param>
/// <param name="m">导出模型</param>
/// <param name="exportPath">导出路径</param>
public
void
UnifyExport
(
LinkLabel
lnk
,
string
xml
,
int
keyID
,
ExportModel
m
=
null
,
string
exportPath
=
""
)
{
//赋值全局数据源
this
.
_ds
=
this
.
GetDataFromXml
(
xml
,
keyID
);
//模板导出路径
this
.
ExportFileName
=
this
.
GetExportFileFullName
(
exportPath
);
//如未显式指定导出模型,则从配置文件读取
if
(
m
==
null
)
m
=
Utils
.
GetExportModel
(
xml
);
//显示上下文菜单
this
.
ShowContextMenu
(
lnk
,
m
);
}
/// <summary>
/// 统一导出接口
/// </summary>
/// <param name="lnk">标签控件</param>
/// <param name="ds">数据源</param>
/// <param name="m">导出模型</param>
public
void
UnifyExport
(
LinkLabel
lnk
,
DataSet
ds
,
ExportModel
m
)
{
if
(
ds
==
null
||
m
==
null
)
{
Utils
.
ThrowErrorMessage
(
"导出Excel必须赋值数据源和导出模型!"
);
return
;
}
//赋值全局数据源
this
.
_ds
=
ds
;
//模板导出路径
this
.
ExportFileName
=
this
.
GetExportFileFullName
();
//显示上下文菜单
this
.
ShowContextMenu
(
lnk
,
m
);
}
/// <summary>
/// 获取导出文件全名
/// </summary>
/// <param name="exportPath"></param>
/// <returns></returns>
private
string
GetExportFileFullName
(
string
exportPath
=
""
)
{
string
path
=
""
;
string
tmpName
=
$"Excel导出临时文件-
{
DateTime
.
Now
.
ToString
(
"yyyMMdd-hhmmss"
)}
.xls"
;
if
(
string
.
IsNullOrEmpty
(
exportPath
)
||
!
Directory
.
Exists
(
exportPath
))
{
path
=
Path
.
Combine
(
Utils
.
StartupPath
,
"temp"
,
tmpName
);
}
else
{
path
=
Path
.
Combine
(
exportPath
,
tmpName
);
}
return
path
;
}
/// <summary>
/// 显示上下文菜单
/// </summary>
/// <param name="lnk"></param>
/// <param name="UniquePrefix"></param>
private
void
ShowContextMenu
(
LinkLabel
lnk
,
ExportModel
m
)
{
//根据模板目录创建下拉菜单项(显示时去掉统一前缀,简洁显示)
DirectoryInfo
directory
=
new
DirectoryInfo
(
Utils
.
ExcelTemplateFilePath
);
var
items
=
directory
.
GetFiles
().
Where
(
f
=>
f
.
Name
.
IndexOf
(
m
.
UniquePrefix
)
==
0
)
.
Select
(
p
=>
new
MenuItem
(
p
.
Name
.
Replace
(
m
.
UniquePrefix
,
""
),
new
EventHandler
(
MenuItem_Click
))
{
Tag
=
m
})
.
ToArray
();
if
(
items
.
Length
==
0
)
{
Utils
.
ShowMessage
(
"请先配置模板信息!"
);
return
;
}
//创建菜单并显示
ContextMenu
cm
=
new
ContextMenu
();
cm
.
MenuItems
.
AddRange
(
items
);
cm
.
Show
(
lnk
,
new
Point
(
0
,
lnk
.
Height
));
}
/// <summary>
/// 模板项点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private
void
MenuItem_Click
(
object
sender
,
EventArgs
e
)
{
var
item
=
sender
as
MenuItem
;
if
(
string
.
IsNullOrEmpty
(
item
.
Text
))
return
;
try
{
Utils
.
ShowWaitDialog
(
"正在导出Excel文件, 请稍候!"
,
"Exporting excel file, please wait!"
);
//打开模板
var
m
=
item
.
Tag
as
ExportModel
;
this
.
_mouldFilePath
=
$@"
{
Utils
.
ExcelTemplateFilePath
}
\
{
m
.
UniquePrefix
}{
item
.
Text
}
"
;
this
.
_Workbook
=
this
.
GetWorkbook
(
this
.
_mouldFilePath
);
//根据设置导出
bool
IsSuccess
=
false
;
if
(!
string
.
IsNullOrEmpty
(
m
.
GroupTableName
.
Trim
())
&&
!
string
.
IsNullOrEmpty
(
m
.
GroupColumnName
.
Trim
()))
{
IsSuccess
=
this
.
ExportSheets
(
m
.
GroupTableName
,
m
.
GroupColumnName
);
}
else
{
IsSuccess
=
this
.
Export
();
}
//成功时,打开文件
if
(
IsSuccess
)
{
Process
.
Start
(
this
.
ExportFileName
);
}
}
finally
{
Utils
.
CloseWaitDialog
();
}
}
#
endregion
}
/// <summary>
/// 绑定信息
/// </summary>
class
BindInfo
{
/// <summary>
/// 表名
/// </summary>
public
string
TableName
{
get
;
set
;
}
/// <summary>
/// 列名
/// </summary>
public
string
ColumnName
{
get
;
set
;
}
/// <summary>
/// 单元格的列序号
/// </summary>
public
int
CellColumn
{
get
;
set
;
}
/// <summary>
/// 是否明细信息
/// </summary>
public
bool
IsDetailInfo
{
get
;
set
;
}
/// <summary>
/// 是否合并
/// </summary>
public
bool
IsMerge
{
get
;
set
;
}
/// <summary>
/// 是否二维码
/// </summary>
public
bool
IsQRCode
{
get
;
set
;
}
}
/// <summary>
/// Excel导出模型
/// </summary>
public
class
ExportModel
{
/// <summary>
/// 模板唯一前缀(通过该前缀,初始化上下文菜单选项)
/// </summary>
public
string
UniquePrefix
{
get
;
set
;
}
/// <summary>
/// 分组的表名(不需要分组是给空)
/// </summary>
public
string
GroupTableName
{
get
;
set
;
}
/// <summary>
/// 分组的列名(不需要分组是给空)
/// </summary>
public
string
GroupColumnName
{
get
;
set
;
}
/// <summary>
/// 二级分类列名(用于多行合并到一个Sheet页)
/// </summary>
public
string
ReclassifyColumnName
{
get
;
set
;
}
}
}
FunnyPick/FunnyPick/FunnyPick.csproj
View file @
71e27d87
...
...
@@ -11,4 +11,10 @@
<PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Windows.Forms">
<HintPath>C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\System.Windows.Forms.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
FunnyPick/FunnyPick/Utils.cs
View file @
71e27d87
...
...
@@ -15,6 +15,10 @@ namespace FunnyPick
public
static
string
XmlPath
=>
$@"
{
AppContext
.
BaseDirectory
}
\xml"
;
public
static
string
ExcelTemplateFilePath
=>
AppContext
.
BaseDirectory
;
public
static
object
ConfigFilePath
{
get
;
internal
set
;
}
public
static
class
UConvert
{
public
static
bool
ToBool
(
object
o
)
...
...
@@ -131,5 +135,35 @@ namespace FunnyPick
{
throw
new
NotImplementedException
();
}
internal
static
byte
[]
GenQRCode
(
string
v
)
{
throw
new
NotImplementedException
();
}
internal
static
void
CloseWaitDialog
()
{
throw
new
NotImplementedException
();
}
internal
static
ExportModel
GetExportModel
(
string
xml
)
{
throw
new
NotImplementedException
();
}
internal
static
bool
IsContainsColumn
(
DataView
defaultView
,
string
numCol
)
{
throw
new
NotImplementedException
();
}
internal
static
void
ShowWaitDialog
(
string
v1
,
string
v2
)
{
throw
new
NotImplementedException
();
}
internal
static
void
ThrowErrorMessage
(
string
v
)
{
throw
new
NotImplementedException
();
}
}
}
FunnyPick/FunnyPick/obj/Debug/netcoreapp3.0/FunnyPick.csprojAssemblyReference.cache
View file @
71e27d87
No preview for this file type
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment