앱에서 Excel 파일을 다뤄야하는 경우가 있습니다. 대표적으로 아파치 소프트웨어 재단에서 지원하는 Apache Poi 라이브러리 를 사용합니다. 해당 라이브러리는 Microsoft Office 파일(워드, 엑셀, 파워포인트 등등) 읽고 쓰기 기능을 ...

[안드로이드] Apache Poi 엑셀 표만들기 예제

앱에서 Excel 파일을 다뤄야하는 경우가 있습니다.

대표적으로 아파치 소프트웨어 재단에서 지원하는 Apache Poi 라이브러리를 사용합니다.

해당 라이브러리는 Microsoft Office 파일(워드, 엑셀, 파워포인트 등등) 읽고 쓰기 기능을 제공합니다.

이번 글에서는 Apache Poi 라이브러리를 사용하는 방법과 엑셀 표를 작성해보는 예제를 다루려고 합니다.

 

 

1. Apache Poi 라이브러리 사용하기 / Excel 표 만들기 예제 (현재 글)

2. Excel 양식 복사하기 예제

3. Excel 이미지 넣기 예제

 


 

 

 

 

 

 

1. Apache Poi 라이브러리 사용

처음에는 Apache poi를 안드로이드에 비교적 쉽게 적용할 수 있는 방법을 찾게 되어서 

버전 v3.17을 사용했습니다. (링크 참조 : Poi-Android)

"근데 이왕 사용하는거면 최신 버전 사용하고 싶은데.."를 시작으로 삽질을 시작합니다.

최신 버전 jar를 다운 받아서 적용하는데 Duplicate class부터 시작해서 정신이 나갈뻔했습니다. 

결론적으로 현재 기준 Apache poi 최신 버전으로 아래와 같이 gradle 설정을 해줬습니다.

dependencies {
...

// Apache Poi v3.17
// implementation "com.github.SUPERCILEX.poi-android:poi:3.17"

// Apache Poi v5.2.3 (current latest version)
implementation group: 'org.apache.poi', name: 'poi-ooxml', version: '5.2.3'
}

 

 

 

그리고 XML Parser를 위해 Application 클래스 생성해서 AndroidManifest 등록까지 해주고,

System Property를 아래와 같이 설정합니다.

<application
android:name=".MainApplication"
...
</application>

 

 


class MainApplication : Application() {

init {
instance = this

/************************************************************************************
* XML Parser 를 위한 System properties,
* Apache Poi v3.xx 버전 사용 시 해당 properties 설정 하지 않은 경우 FactoryConfigurationError 발생
* Apache Poi v5.x.x 버전 사용 시 해당 properties 설정을 제외 해도 문제 없었음
* 정확한 이유를 찾지 못해서 유지
*************************************************************************************/

System.setProperty(
"org.apache.poi.javax.xml.stream.XMLInputFactory",
"com.fasterxml.aalto.stax.InputFactoryImpl"
)

System.setProperty(
"org.apache.poi.javax.xml.stream.XMLOutputFactory",
"com.fasterxml.aalto.stax.OutputFactoryImpl"
)

System.setProperty(
"org.apache.poi.javax.xml.stream.XMLEventFactory",
"com.fasterxml.aalto.stax.EventFactoryImpl"
)
}

companion object {
lateinit var instance: MainApplication

fun getContext(): Context {
return instance.applicationContext
}
}
}

 

 

 

 

 

 

 

2. Excel 컨트롤러 : ExcelBase

ExcelBase 클래스는 엑셀에서 sheet, row, column, cell 단위로 여러가지 조작을 위한 함수들을 가집니다.

추상 클래스로 작성되었으며 상속받는 하위클래스에서 create() 메서드를 재정의해서

실질적으로 엑셀을 작성하는데 사용됩니다.

abstract class ExcelBase {

lateinit var xssfWorkbook: XSSFWorkbook

abstract fun create(): Workbook // 상속 받고 입맛에 맞게 재정의 해줘~

fun saveExcel(workbook: Workbook, path: String) {
val excelFile = File(path)
if (excelFile.exists()) excelFile.delete()

try {
val fileOut = FileOutputStream(excelFile)
workbook.write(fileOut)
fileOut.close()
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}

fun setDefaultWidth(sheet: Sheet, width: Int) {
// initial value : 8 (2048 / 256)
sheet.defaultColumnWidth = width
}

fun setDefaultHeight(sheet: Sheet, height: Short) {
// initial value : 300
sheet.defaultRowHeight = height
}

fun setColumnWidth(sheet: Sheet, columnIndex: Int, width: Int) {
// initial value : 2048 (8 * 256)
sheet.setColumnWidth(columnIndex, width)
}

fun setRowHeight(sheet: Sheet, rowIndex: Int, height: Short) {
// initial value : 300
val row = sheet.getRow(rowIndex)
if (row != null) row.height = height
}

fun setCell(sheet: Sheet, rowIndex: Int, columnIndex: Int, value: Any) {
var row = sheet.getRow(rowIndex)
if (row == null) row = sheet.createRow(rowIndex)

var cell = row.getCell(columnIndex)
if (cell == null) cell = row.createCell(columnIndex)

when (value) {
is RichTextString -> cell.setCellValue(value)
is Int -> cell.setCellValue(value.toDouble())
is Float -> cell.setCellValue(value.toDouble())
is Double -> cell.setCellValue(value)
is Boolean -> cell.setCellValue(value)
is Date -> cell.setCellValue(value)
is Calendar -> cell.setCellValue(value)
else -> cell.setCellValue(value.toString())
}
}

fun setCell(sheet: Sheet, rowIndex: Int, columnIndex: Int, value: Any, style: XSSFCellStyle) {
var row = sheet.getRow(rowIndex)
if (row == null) row = sheet.createRow(rowIndex)

var cell = row.getCell(columnIndex)
if (cell == null) cell = row.createCell(columnIndex)

when (value) {
is RichTextString -> cell.setCellValue(value)
is Int -> cell.setCellValue(value.toDouble())
is Float -> cell.setCellValue(value.toDouble())
is Double -> cell.setCellValue(value)
is Boolean -> cell.setCellValue(value)
is Date -> cell.setCellValue(value)
is Calendar -> cell.setCellValue(value)
else -> cell.setCellValue(value.toString())
}
cell.cellStyle = style
}

fun setCellWithMerged(
sheet: Sheet,
rowStartIndex: Int,
rowEndIndex: Int,
columnStartIndex: Int,
columnEndIndex: Int,
value: Any,
style: XSSFCellStyle
) {
var row = sheet.getRow(rowStartIndex)
if (row == null) row = sheet.createRow(rowStartIndex)

var cell = row.getCell(columnStartIndex)
if (cell == null) cell = row.createCell(columnStartIndex)

when (value) {
is RichTextString -> cell.setCellValue(value)
is Double -> cell.setCellValue(value)
is Boolean -> cell.setCellValue(value)
is Date -> cell.setCellValue(value)
is Calendar -> cell.setCellValue(value)
else -> cell.setCellValue("$value")
}
cell.cellStyle = style

val cellRangeAddress = CellRangeAddress(rowStartIndex, rowEndIndex, columnStartIndex, columnEndIndex)
sheet.addMergedRegion(cellRangeAddress)

RegionUtil.setBorderTop(cell.cellStyle.borderTop, cellRangeAddress, sheet)
RegionUtil.setBorderBottom(cell.cellStyle.borderBottom, cellRangeAddress, sheet)
RegionUtil.setBorderLeft(cell.cellStyle.borderLeft, cellRangeAddress, sheet)
RegionUtil.setBorderRight(cell.cellStyle.borderRight, cellRangeAddress, sheet)
}

fun setCellWithMerged(
sheet: Sheet,
rowStartIndex: Int,
rowEndIndex: Int,
columnStartIndex: Int,
columnEndIndex: Int,
value: Any
) {
var row = sheet.getRow(rowStartIndex)
if (row == null) row = sheet.createRow(rowStartIndex)

var cell = row.getCell(columnStartIndex)
if (cell == null) cell = row.createCell(columnStartIndex)

when (value) {
is RichTextString -> cell.setCellValue(value)
is Double -> cell.setCellValue(value)
is Boolean -> cell.setCellValue(value)
is Date -> cell.setCellValue(value)
is Calendar -> cell.setCellValue(value)
else -> cell.setCellValue(value.toString())
}

val cellRangeAddress = CellRangeAddress(rowStartIndex, rowEndIndex, columnStartIndex, columnEndIndex)
sheet.addMergedRegion(cellRangeAddress)
}
}

 

 

 

 

 

 

 

3. Excel 표 만들기 : ExcelTableEx

ExcelBase를 상속받고 create 메서드에서 처음 목표했던 표를 만들어 봅니다.

class ExcelTableEx : ExcelBase() {

override fun create(): Workbook {
xssfWorkbook = XSSFWorkbook()

val sheetA = xssfWorkbook.createSheet("6 1")
setDefaultWidth(sheetA, 10)
setDefaultHeight(sheetA, 450)

setTitle(sheetA)
addContent(sheetA)

val sheetB = xssfWorkbook.createSheet("6 2")
setDefaultWidth(sheetB, 10)
setDefaultHeight(sheetB, 450)

setTitle(sheetB)
addContent(sheetB)

return xssfWorkbook
}

private fun setTitle(sheet: Sheet) {
setCellWithMerged(sheet, 1, 1, 1, 2, "구분", getTitleStyle())
setCellWithMerged(sheet, 1, 1, 3, 4, "항목", getTitleStyle())
setCell(sheet, 1, 5, "목표", getTitleStyle())
setCell(sheet, 1, 6, "결과", getTitleStyle())
}

private fun addContent(sheet: Sheet) {
setCellWithMerged(sheet, 2, 3, 1, 2, "무산소 운동", getDefaultStyle())
setCellWithMerged(sheet, 2, 2, 3, 4, "푸시업", getDefaultStyle())
setCellWithMerged(sheet, 3, 3, 3, 4, "스쿼트", getDefaultStyle())
setCell(sheet, 2, 5, "10", getDefaultStyle())
setCell(sheet, 3, 5, "20", getDefaultStyle())
setCellWithMerged(sheet, 2, 3, 6, 6, "실패", getDefaultStyle())

setCellWithMerged(sheet, 4, 4, 1, 2, "유산소 운동", getDefaultStyle())
setCellWithMerged(sheet, 4, 4, 3, 4, "달리기", getDefaultStyle())
setCell(sheet, 4, 5, "1Km", getDefaultStyle())
setCell(sheet, 4, 6, "실패", getDefaultStyle())

setCellWithMerged(sheet, 5, 6, 1, 6, "특이사항 없음", getDefaultStyle())
}

private fun getTitleStyle(): XSSFCellStyle {
val titleStyle = xssfWorkbook.createCellStyle()

// Font
val font = xssfWorkbook.createFont()
font.fontHeightInPoints = 11.toShort()
font.bold = true
titleStyle.setFont(font)

// Gravity
titleStyle.setAlignment(HorizontalAlignment.CENTER)
titleStyle.setVerticalAlignment(VerticalAlignment.CENTER)

// Foreground Color
titleStyle.fillForegroundColor = IndexedColors.LEMON_CHIFFON.index
titleStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND)

// Border
titleStyle.setBorderBottom(BorderStyle.THIN)
titleStyle.setBorderTop(BorderStyle.THIN)
titleStyle.setBorderRight(BorderStyle.THIN)
titleStyle.setBorderLeft(BorderStyle.THIN)
titleStyle.bottomBorderColor = IndexedColors.BLACK.getIndex()
titleStyle.topBorderColor = IndexedColors.BLACK.getIndex()
titleStyle.rightBorderColor = IndexedColors.BLACK.getIndex()
titleStyle.leftBorderColor = IndexedColors.BLACK.getIndex()

return titleStyle
}

private fun getDefaultStyle(): XSSFCellStyle {
val defaultStyle = xssfWorkbook.createCellStyle()

// Font
val font = xssfWorkbook.createFont()
font.fontHeightInPoints = 11.toShort()
defaultStyle.setFont(font)

// Gravity
defaultStyle.setAlignment(HorizontalAlignment.CENTER)
defaultStyle.setVerticalAlignment(VerticalAlignment.CENTER)

// Border
defaultStyle.setBorderBottom(BorderStyle.THIN)
defaultStyle.setBorderTop(BorderStyle.THIN)
defaultStyle.setBorderRight(BorderStyle.THIN)
defaultStyle.setBorderLeft(BorderStyle.THIN)

return defaultStyle
}
}

 

 

 

 




4. 실행 결과

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

binding.btnTableEx.setOnClickListener {
val tableEx = ExcelTableEx()
val workBook = tableEx.create()
tableEx.saveExcel(workBook, "/sdcard/Download/tableEx.xlsx")
}
}
}

 

 


 

 

 

 

 

 

4. 예제 다운로드

https://github.com/bictoselfdev/ExcelEx

 

 

 

 

 

 

 

[Reference]

github : poi-on-android

 

 

 

 

0 comments: