이번엔 가장 활용도가 높은 편인 테이블뷰에 대해서 알려드리겠습니다.
우선;; interface{}를 사용해서 조금 고급지게 리스트박스를 위한 모델구조를 잡고 하면 보긴 좋긴 하겠으나 애초에 그냥 막 짜서 적용해서 쓰기 바쁜 와중에 만들어 쓴 코드고..
lxn/walk 의 샘플 코드에 존재하는 것들을 크게 건드리지 않고 사용하다보니 그닥 맘에 드는 구조는 아니라는 점은 미리 말씀드립니다.
우선 테이블을 구성하기 위해서는 2개의 구조체와 몇가지 메소드를 미리 작성하셔야 합니다.
/**
* TestListItem
**/
type TestListItem struct {
Name string
Level int
Sex int
Class string
checked bool
}
/**
* TestListModel
**/
type TestListModel struct {
walk.TableModelBase
items []TestListItem
}
/**
* RowCount
**/
func (m *TestListModel) RowCount() int {
return len(m.items)
}
/**
* Value
**/
func (m *TestListModel) Value(row, col int) interface{} {
item := m.items[row]
switch col {
case 0:
return item.Name
case 1:
return item.Level
case 2:
if item.Sex == 0 {
return "중성"
} else if item.Sex == 1 {
return "남성"
} else if item.Sex == 2 {
return "여성"
} else {
return "알수없음"
}
case 3:
return item.Class
}
panic("unexpected col")
}
/**
* Checked
**/
func (m *TestListModel) Checked(row int) bool {
return m.items[row].checked
}
/**
*
**/
func (m *TestListModel) CheckedCount() int {
var cnt int
for _, item := range m.items {
if item.checked {
cnt++
}
}
return cnt
}
/**
* SetChecked
**/
func (m *TestListModel) SetChecked(row int, checked bool) error {
m.items[row].checked = checked
return nil
}
/**
* ResetRows
**/
func (m *TestListModel) ResetRows() {
m.items = nil
m.PublishRowsReset()
}
TestListItem 은 테이블 데이터 리스트의 한줄 한줄의.. 그러니까 sql db 다룰때 기준으로 치면 row의 정보를 담은 구조체 입니다. 이 row 데이터를 담은 구조체는 테이블뷰 모델에 해당하는 TestListModel 구조체에서 items 배열로 선언되어 사용됩니다.
이 모델 구조는 lxn/walk 의 샘플코드에서 사용되는 구조를 그대로 차용하고 있는 관계로 .. 원하시면 좀더 좋은 방향으로 맘껏 고쳐 쓰시기 바랍니다.
만들고자 하는 테이블뷰가 있으면 row 정보를 담을 구조체를 만들고 실제 테이블뷰에서 사용될 walk.TableModelBase를 포함한 모델 구조체 2개를 만드시면 됩니다. 위 코드는 예제삼아 TestListItem, TestListModel 이라 했는데요 ..
개발하는 프로그램에 추가되는 목적에 따라서 PlayerListItem, PlayerListModel 이라던가 UserListItem, UserListModel 등으로 이름은 변경하여 사용하시면 되겠습니다.
row 정보를 담는 item 구조체에는 테이블에서 다뤄질 데이터들을 원하는대로 작성하시면 되는데요 주의하실점은 맨 마지막에 존재하는 checked bool 맴버 변수는 그냥 늘 맨 마지막에 추가해 두신다 생각 하십시요. 체크박스를 포함하는 테이블을 다룰때에 사용되는 변수인데 체크박스를 쓰던 안쓰던 저놈은 그냥 냅두시던지 체크박스를 전혀 사용 하지 않으실거면 이 변수와 아래 설명하는 체크박스 관련된 메소드들을 다 제거하고 쓰시던지 하시면 됩니다.
테이블뷰 모델에 대한 메소드가 몇가지 존재하는데요 ..
RowCount 는 테이블에 존재하는 row 데이터 갯수입니다. 몇줄이 존재하냐?를 얻고 싶을때 쓰시면 됩니다.
Checked, CheckedCount, SetChecked 함수는 테이블이 체크박스를 포함해서 만들어지는 경우에는 row 의 맨 좌측에 체크박스가 활성화되서 데이터가 표시되는데 특정 row의 체크박스의 체크여부를 확인하는게 Checked 함수이고 CheckedCount 는 체크된 row 의 갯수를, SetChecked 함수는 특정 row 를 체크하거나 풀거나 하는 역할을 해주는 함수입니다. 이것들은 뒤에 나올 예제들을 보시면 아실꺼구요, 코드 자체도 워낙 단순하다보니 뭐라 설명할 이유도 ;;; ㅋㅋ
테이블뷰의 메소드중 가장 중요한건
/**
* Value
**/
func (m *TestListModel) Value(row, col int) interface{} {
item := m.items[row]
switch col {
case 0:
return item.Name
case 1:
return item.Level
case 2:
if item.Sex == 0 {
return "중성"
} else if item.Sex == 1 {
return "남성"
} else if item.Sex == 2 {
return "여성"
} else {
return "알수없음"
}
case 3:
return item.Class
}
panic("unexpected col")
}
이놈 입니다..
특정 행과 열의 값을 얻을때 사용되고 실제 테이블에 데이터가 표시될때 호출이 되는 메소드 입니다. 실제 데이터는 앞서 설명한 item 에 대한 구조체에 담기지만 실제 테이블은 대체로 사람이 보는 용도로 사용되기 떄문에 데이터를 원하는 형태로 가공하여 표시하기 마련입니다. 그런 이유로 이 함수에서 원하는 형태로 return 된 형태로 테이블에 표시되게 됩니다.
예제를 봐 가며 설명하는게 더 빠를것 같네요..
/**
* listTest1
**/
func listTest1() {
mgr, window := NewWindowMgrNoResize("리스트1", 640, 500, GetIcon())
cbModel := new(TestListModel)
testTv := mgr.TableView(cbModel, []tableViewHeader{
{Title: "이름", Width: 100},
{Title: "레벨", Width: 100},
{Title: "성별", Width: 100},
{Title: "직업", Width: 100},
}, false, false)
testTv.ItemActivated().Attach(func() {
currIdx := testTv.CurrentIndex()
if currIdx < 0 {
return
}
fmt.Println("= 더블클릭 ========================")
fmt.Println("선택된 아이템:", currIdx)
fmt.Println("이름:", cbModel.items[currIdx].Name)
fmt.Println("레벨:", cbModel.items[currIdx].Level)
fmt.Println("성별:", cbModel.items[currIdx].Sex)
fmt.Println("직업:", cbModel.items[currIdx].Class)
})
window.Starting().Attach(func() {
for i := 0; i < 10; i++ {
od := TestListItem{}
od.Name = fmt.Sprintf("사용자%02d", i)
od.Level = i + 1
od.Sex = (i % 2)
od.Class = fmt.Sprintf("직업%d", i+1)
cbModel.items = append(cbModel.items, od)
}
cbModel.PublishRowsReset()
})
mgr.StartForeground()
}

아주 기본적인 형태의 테이블을 하나 생성 하였습니다.
cbModel := new(TestListModel)
우선 앞서 설명한 item 데이터를 포함한 테이블 모델을 하나 생성하구요 이놈은 테이블뷰를 생성할때 인자로 전달되고 이런저런 짓을 하는데 사용되게 됩니다.
testTv := mgr.TableView(cbModel, []tableViewHeader{
{Title: "이름", Width: 100},
{Title: "레벨", Width: 100},
{Title: "성별", Width: 100},
{Title: "직업", Width: 100},
}, false, false)
이렇게 테이블뷰을 생성하게 되는데요 첫번째 인자는 앞서 만든 테이블 모델 구조체를 넣으시면 됩니다.
두번째 인자는 테이블의 헤더를 설정하기 위한 구조체 배열을 전달하는데요. 앞서 제공한 walk_wrap.go 파일내에 보면
/**
* tableViewHeader
**/
type tableViewHeader struct {
Title string
Width int
Align string
}
이렇게 tableViewHeader 라는 구조체가 정의되어 있습니다.

첫번째 Title 은 테이블뷰의 헤더에 각 컬럼의 타이틀입니다.

Width 는 각 컬럼의 넓이를 입력하시면 됩니다. 값은 Pixel 값입니다.
마지막 Align 은 각 컬럼의 정렬 방식인데요 left, right, center 로 지정가능합니다. 단 맨 좌측 첫 컬럼의 경우 정렬 기준을 변경하려면 좀더 많은짓이 가능한데 현재는 지원하고 있지 않습니다. ( 윈도 핸들 가지고 윈도 API로 사부작 사부작 하면 방법이 있긴 합니다만 .. ) Align 값은 기본적으로 좌측정렬입니다.
세번째 인자는 테이블뷰의 각 row 를 한번에 단 하나만 선택 가능한지 아니면 여러개를 선택 가능한지를 true, false 로 전달하면 됩니다.
마지막 인자는 체크박스를 사용할지 말지를 true, false 로 전달하면 됩니다.
기본 테이블 뷰에 대한 예제를 먼저 설명하고 각 케이스를 다시 설명하겠습니다.
우선 위에 날린 첫번째 예제는 다중 선택이 안되고 체크박스도 없는 기본적인 테이블을 생성하였습니다.

이놈은 이렇게 한번에 하나씩만 선택이 가능합니다.
데이터의 입력은 윈도가 생성이 완료되고 화면에 보여지기 시작하는 Stating() 이벤트에 Attach 하여
window.Starting().Attach(func() {
for i := 0; i < 10; i++ {
od := TestListItem{}
od.Name = fmt.Sprintf("사용자%02d", i)
od.Level = i + 1
od.Sex = (i % 2)
od.Class = fmt.Sprintf("직업%d", i+1)
cbModel.items = append(cbModel.items, od)
}
cbModel.PublishRowsReset()
})
이렇게 10개의 데이터를 추가 했습니다. 앞서 테이블뷰에는 item 구조체와 모델 구조체를 만들어 쓰면 된다고 했는데요.
예로 작성한 TestListeItem 데이터를 생성하고 멤버들에 값을 정한다음 모델 구조체인 cbModel.items 배열에 추가해 넣었습니다.
이후 cbModel상의 데이터를 실질 화면에 보여지는 테이블에 내보내면(Publish) 실제 테이블뷰에 적용이됩니다. 이때 쓰는게 cbModel.PublishRowsReset() 메소드 입니다. 뒤에 비스무리한 메소드명이 몇몇 나오는데 이놈들은 walk.TableModelBase 라는 walk 에 내장된 놈입니다.
예제를 만드느라 창이 뜰때를 기준해서 아무데이터나 막 만들어 넣었지만 .. 필요에 따라 파일을 읽어서 라던지.. 특정 버튼을 눌렀을때 라던지.. 어떤 데이터를 수신 했을때 라던지.. 그런식으로 활용할수 있겠지요..
앞서 모델 구조체의 Value 라는 메소드에 대해서 설명 했는데요.
성별값은 (i % 2) 의 int 값이 들어가지만 리스트에는 중성이니 남성이니 표시가 되고 있습니다.
case 2:
if item.Sex == 0 {
return "중성"
} else if item.Sex == 1 {
return "남성"
} else if item.Sex == 2 {
return "여성"
} else {
return "알수없음"
}
그 이유가 Value 메소드 내부에서 컬럼 인덱스가 2인경우에 .. 그러니까 이름(0), 레벨(1), 성별(2) 순서니까 컬럼인덱스가 2이면 성별에 해당하는 컬럼인 것이지요 성별에 해당하는 컬럼은 item 구조체의 값을 그대로 리턴한게 아닌 값에 따라서 중성, 남성, 여성 등으로 return 하고 있습니다. 이 Value 메소드에서 지정한대로 표시가 된다는 거죠.
이 Value 메소드가 중요한건.. 테이블뷰를 생성할때
testTv := mgr.TableView(cbModel, []tableViewHeader{
{Title: "레벨", Width: 100},
{Title: "이름", Width: 100},
{Title: "성별", Width: 100},
{Title: "직업", Width: 100},
}, false, false)
이렇게 생성 해놓고는
/**
* Value
**/
func (m *TestListModel) Value(row, col int) interface{} {
item := m.items[row]
switch col {
case 0:
return item.Name
case 1:
return item.Level
case 2:
if item.Sex == 0 {
return "중성"
} else if item.Sex == 1 {
return "남성"
} else if item.Sex == 2 {
return "여성"
} else {
return "알수없음"
}
case 3:
return item.Class
}
panic("unexpected col")
}
Value 메소드를 이렇게 만들어두면 0번 인덱스의 컬럼은 ‘레벨’ 값이 출력 되어야 함에도 Value 메소드에서 0번 컬럼에 대해서 이름에 해당하는 값을 리턴하고 있음으로 테이블뷰의 헤더에 표시된 타이틀과 실제 표시되는 데이터가 다르게 출력될수 있습니다. 한마디로 원하는 형태에 맞춰서 잘~~ 작성 하시라 이말입니다.
테이블 뷰에서 가장 빈번히 사용하는 기능은 특정 row 를 더블클릭 했을때.. 그 선택한 row 의 값을 가지고 무언가를 하는 경우 인데요.
testTv.ItemActivated().Attach(func() {
currIdx := testTv.CurrentIndex()
if currIdx < 0 {
return
}
fmt.Println("= 더블클릭 ========================")
fmt.Println("선택된 아이템:", currIdx)
fmt.Println("이름:", cbModel.items[currIdx].Name)
fmt.Println("레벨:", cbModel.items[currIdx].Level)
fmt.Println("성별:", cbModel.items[currIdx].Sex)
fmt.Println("직업:", cbModel.items[currIdx].Class)
})
그런때 사용 가능한게 ItemActivated() 이벤트 입니다. 이 이벤트에 Attach 를 하고 테이블뷰 생성시 리턴받은 walk.TableView 변수의 CurrentIndex() 메소드를 통해 현재 선택된 row 의 인덱스 값을 얻을 수 있습니다. 이 값이 -1 인 경우에는 현재 선택된 row 가 없다는 소리니 예외처리 빼먹지 마시구요.
예제를 만들기 위해 선택된 row 의 실질 값을 출력하도록 만들어 봤습니다.

두번째 예제를 가봅시다.. 두번째는 체크박스를 포함한 경우입니다.
/**
* listTest2
**/
func listTest2() {
mgr, window := NewWindowMgrNoResize("리스트2(CheckBox)", 640, 500, GetIcon())
cbModel := new(TestListModel)
testTv := mgr.TableView(cbModel, []tableViewHeader{
{Title: "이름", Width: 100},
{Title: "레벨", Width: 100},
{Title: "성별", Width: 100},
{Title: "직업", Width: 100},
}, true, false)
mgr.PushButton("체크된 아이템", func() {
for i := 0; i < len(cbModel.items); i++ {
if cbModel.Checked(i) {
fmt.Println(i, "체크됨", cbModel.items[i].Name)
}
}
})
mgr.PushButton("체크된 아이템 이름변경", func() {
for i := 0; i < len(cbModel.items); i++ {
if cbModel.Checked(i) {
cbModel.items[i].Name = fmt.Sprintf("변경된이름%d", i)
cbModel.PublishRowChanged(i)
}
}
})
testTv.ItemActivated().Attach(func() {
currIdx := testTv.CurrentIndex()
if currIdx < 0 {
return
}
if cbModel.Checked(currIdx) {
cbModel.SetChecked(currIdx, false)
} else {
cbModel.SetChecked(currIdx, true)
}
cbModel.PublishRowChanged(currIdx)
})
window.Starting().Attach(func() {
for i := 0; i < 10; i++ {
od := TestListItem{}
od.Name = fmt.Sprintf("사용자%02d", i)
od.Level = i + 1
od.Sex = (i % 2)
od.Class = fmt.Sprintf("직업%d", i+1)
cbModel.items = append(cbModel.items, od)
}
cbModel.PublishRowsReset()
})
mgr.StartForeground()
}
기본적으로 달라진것은 TableView 로 테이블을 생성할때 3번째 인자를 True 로 주었습니다.

그러면 이와 같이 리스트의 각 줄 앞에 체크박스가 생깁니다.
체크박스가 유용한 경우는 특정 데이터를 선택해서 삭제하거나 변경을 하거나 하는등을 기능을 여러 줄에 동시에 적용을 하려고 할때라던지 .. 여러 용도로 테이블뷰에서 체크박스는 유용하게 사용 가능합니다.
단 기본 상태의 체크박스는 정확히 체크박스 사각형을 클릭해야만 체크상태 변경이 가능합니다만 .. 이런 식이면 제 경우에는 좀 불편하다고 느껴저서 앞서 설명한 특정 row 를 더블클릭 했을 경우에 발생하는 ItemActivated 이벤트에 체크박스를 제어하는 코드를 추가해서 쓰는 편입니다.
testTv.ItemActivated().Attach(func() {
currIdx := testTv.CurrentIndex()
if currIdx < 0 {
return
}
if cbModel.Checked(currIdx) {
cbModel.SetChecked(currIdx, false)
} else {
cbModel.SetChecked(currIdx, true)
}
cbModel.PublishRowChanged(currIdx)
})
이렇게 말이죠.. 테이블뷰의 데이터 영역에서 더블클릭이 발생됫고 선택된 row 가 존재하면 해당 인덱스의 체크 상태에 따라서 체크를 하거나 풀거나 합니다. SetChecked 함수가 하는 짓이라 해봐야 cbModel.items[인덱스].checked 의 값을 true, false 로 변경해주는 것이지만 해당 코드 작성시엔 무슨 바람이 불어서인지 Set으로 시작하는 메소드들을 만들어 뒀네요 ㅎㅎ
데이터를 추가할때는 cbModel.PublishRowsReset()으로 전체 데이터를 리스트에 적용하도록 하였는데요 위 코드는 선택된 특정 row 의 체크 상태만 변경이 되기 때문에 cbModel의 PublishRowChanged 메소드로 선택된 특정 줄만을 다시 그리도록 호출하였습니다. 이 함수를 출력 안하면 제때 보여지는 정보가 업데이트 되지 않아서 체크가 됫는데 안되있거나 실제로 체크가 풀렸는데 여전히 체크가 된체로 보일수 있으니 데이터 변동이 올경우 이 Publish 로 시작하는 놈들은 잊지말고 호출해주셔야 합니다.
위 코드를 적용하면 원래는

이 체크박스를 직접 클릭 해야지만 체크 상태를 변경 가능하지만..

이 영역 어디를 더블클릭 해도 체크상태를 변경 가능하게 됩니다.

하단에 2가지 버튼이 존재하는데요.
“체크된 아이템” 버튼은 체크되어 있는 row 의 인덱스와 Name 값을 출력해 줍니다.
mgr.PushButton("체크된 아이템", func() {
for i := 0; i < len(cbModel.items); i++ {
if cbModel.Checked(i) {
fmt.Println(i, "체크됨", cbModel.items[i].Name)
}
}
})

0,2,4,6,8 번째 인덱스에 해당하는 놈들을 체크한 상태로 버튼을 누른 결과구요
“체크된 아이템 이름변경” 은 체크된 아이템의 이름 값을 “변경된이름[인덱스값]” 으로 변경해줍니다.
mgr.PushButton("체크된 아이템 이름변경", func() {
for i := 0; i < len(cbModel.items); i++ {
if cbModel.Checked(i) {
cbModel.items[i].Name = fmt.Sprintf("변경된이름%d", i)
cbModel.PublishRowChanged(i)
}
}
})

앞서 선택해둔 상태에서 이 번튼을 누르니 이름 값이 전부 변경되었습니다.
마지막으로 다중선택에 대한 예를 다뤄 보겠습니다.
/**
* listTest3
**/
func listTest3() {
mgr, window := NewWindowMgrNoResize("리스트2(다중선택)", 640, 500, GetIcon())
cbModel := new(TestListModel)
testTv := mgr.TableView(cbModel, []tableViewHeader{
{Title: "이름", Width: 100},
{Title: "레벨", Width: 100},
{Title: "성별", Width: 100},
{Title: "직업", Width: 100},
}, false, true)
mgr.PushButton("선택된 아이템", func() {
idxs := testTv.SelectedIndexes()
if len(idxs) == 0 {
return
}
for _, i := range idxs {
fmt.Println(i, "선택된", cbModel.items[i].Name)
}
})
mgr.PushButton("선택된 아이템 이름변경", func() {
idxs := testTv.SelectedIndexes()
if len(idxs) == 0 {
return
}
for _, i := range idxs {
cbModel.items[i].Name = fmt.Sprintf("변경된이름%d", i)
cbModel.PublishRowChanged(i)
}
})
window.Starting().Attach(func() {
for i := 0; i < 10; i++ {
od := TestListItem{}
od.Name = fmt.Sprintf("사용자%02d", i)
od.Level = i + 1
od.Sex = (i % 2)
od.Class = fmt.Sprintf("직업%d", i+1)
cbModel.items = append(cbModel.items, od)
}
cbModel.PublishRowsReset()
})
mgr.StartForeground()
}
기본적인 코드의 구조는 체크박스와 비슷합니다만 mgr.TableView 의 3번째 인자는 체크박스와 관련이니 false 로 마지막 다중선태에 대한 값을 true 로 변경하였습니다.
이렇게 하면

드래그해서 전체를 다 선택하거나

일부만 선택 하거나

Ctrl 키를 누른채로 특정한 놈들만 골라서 선택하거나

이렇게 하나를 선택해둔 후

쉬프트를 누른채로 선택한 놈까지 여러놈을 선택이 가능합니다. 일반적인 리스트컨트롤이나 탐색기등에서 파일 선택하는 방식과 흡사하다 보시면 되겠습니다.
이놈도 체크박스 예제와 같이 선택된 아이템의 정보를 출력하거나 선택된 아이템의 이름을 변경하는 버튼이 존재하는데요.
mgr.PushButton("선택된 아이템", func() {
idxs := testTv.SelectedIndexes()
if len(idxs) == 0 {
return
}
for _, i := range idxs {
fmt.Println(i, "선택된", cbModel.items[i].Name)
}
})
mgr.PushButton("선택된 아이템 이름변경", func() {
idxs := testTv.SelectedIndexes()
if len(idxs) == 0 {
return
}
for _, i := range idxs {
cbModel.items[i].Name = fmt.Sprintf("변경된이름%d", i)
cbModel.PublishRowChanged(i)
}
})
핵심은 walk.TableView 의 SelectedIndexes() 메소드입니다. 이놈은 int 형 배열을 리턴하고 이 배열에는 선택된 item의 인덱스가 들어가 있습니다.
그리하야 이 SelectedIndexes() 로 얻은 배열에 저장된 값을 가지고 선택된 놈들의 특정 값을 출력하거나


선택된 놈들의 이름을 변경 할수도 있습니다.

테이블 뷰에 대한건 좀;; 복잡하고 맘에 안드는 구조긴 한데 -_-;; …
언젠가 좀 다듬어서 다시 walk 관련 무언가를 할일 있으면 그때 손대보도록 하겠구요.
나머지는 lxn/walk 의 TableView 예제를 참고하시면 더 다양한 활용 방법을 얻을 수 있습니다.