Go에서 Tk로 파일 목록이 추가된 이미지 뷰어 만들기
지난 게시물에서는 CGo-Free Tcl/Tk 라이브러리에 대해 간단하게 살펴 봤습니다. 이번 시간에는 지난 번의 예제를 응용하여 이미지 뷰어를 만들어 보도록 하겠습니다.
이미지 뷰어 계획
- 지난 시간의 이미지 표시기는 이미지 삭제 기능이 없어 여러 이미지들을 불러올 수록창의 크기가 모자랐습니다. 사용하지 않는 라벨을 삭제해 줍시다.
- 이미지 표시기에 여러 이미지를 목록화할 것이라면 리스트를 만들어 줍시다.
- 한 번에 여러 이미지를 불러오는 것이 좋습니다.
- 보지 않을 이미지를 목록에서 빼는 것도 구현해 줍니다.
- 다중 이미지 중 특정 이미지를 선택해서 보는 기능을 만들어 줍시다.
수정된 Tcl/Tk 9.0 라이브러리
기존 라이브러리에는 Listbox 구현이 미흡해서 이미지 목록을 보여주기 힘듭니다. 수정된 라이브러리를 다운로드해 줍시다. Git CLI가 설치되어 있지 않다면 tarball이나 zip 파일로 다운로드받아도 좋습니다.
1git clone https://github.com/gg582/tk9.0
먼저 추가된 기능들의 몇 가지를 살펴 봅시다.
일단 새로운 함수를 살펴보기에 앞서서, 기존 함수들은 어떻게 되어 있는지 tk.go의 1017행의 Destroy 함수를 통해 구조를 간단하게 파악해보도록 하겠습니다.
1func Destroy(options ...Opt) {
2 evalErr(fmt.Sprintf("destroy %s", collect(options...)))
3}
이 함수는 evalErr라는 함수에 Tcl 스크립트 형식으로 동작을 전달해 구현되어 있습니다. 그 말은, 원하는 기능을 구현하기 위해서는 해당하는 Tcl 스크립트의 형식으로 명령을 전달하기만 된다는 뜻입니다.
예시로 리스트박스에 항목을 추가하는 메서드를 구현해 봅시다. 먼저 Tcl 스크립팅을 위해 공식 매뉴얼에서 listbox에 사용 가능한 명령어 중 insert를 살펴 봅시다.
insert 명령어의 설명 페이지를 보면, insert는 특정 인덱스에 나열된 항목들을 삽입합니다. 그렇다면 이를 구현하기 위해서
1func (l *ListboxWidget) AddItem(index int, items string) {
2 evalErr(fmt.Sprintf("%s insert %d %s", l.fpath, index, items))
3}
와 같은 코드를 작성할 수 있습니다.
이제 대략적인 구현 원리를 알았으니, Listbox를 위한 추가 기능들부터 설명하겠습니다.
리스트: 삽입/삭제
1package main
2import . "modernc.org/tk9.0"
3
4func main() {
5 length := 3
6 l := Listbox()
7 l.AddItem(0, "item1 item2 item3")
8 b1 := TButton(Txt("Delete Multiple Items, index (0-1)"), Command( func(){
9 if length >= 2 {
10 l.DeleteItems(0,1)
11 length-=2
12 }
13 }))
14 b2 := TButton(Txt("Delete One Item, index (0)"), Command( func () {
15 if length > 0 {
16 l.DeleteOne(0)
17 length-=1
18 }
19 }))
20 Pack(TExit(),l,b1,b2)
21 App.Wait()
22}
23
위 프로그램에서 AddItem은 스페이스바로 구분된 서로 다른 아이템들을 인덱스 0부터 차례대로 넣습니다. item1-item3는 차례대로 0, 1, 2 인덱스를 갖게 됩니다. 항목 삭제가 어떻게 동작하는지 예제를 실행시켜 알아봅니다.
리스트: 선택된 항목 가져오기
이제 Listbox에서 선택된 항목들을 가지고 온 후 확인해 보겠습니다.
1package main
2
3import . "modernc.org/tk9.0"
4
5func main() {
6 l := Listbox()
7 l.SelectMode("multiple")
8 l.AddItem(0, "item1 item2 item3")
9 btn := TButton(Txt("Print Selected"), Command( func() {
10 sel := l.Selected()
11 for _, i := range sel {
12 println(l.GetOne(i))
13 }
14 }))
15
16 Pack(TExit(), l, btn)
17 App.Wait()
18}
Selected 메서드는 현재 Listbox에서 선택된 모든 항목들의 인덱스를 가져옵니다. GetOne 메서드는 해당 인덱스에 해당하는 항목의 값을 가져옵니다. 콘솔에 출력되는 결과로 알 수 있습니다. 참고로 유사 메서드인 Get 메서드는 시작과 끝 인덱스를 받아 범위 내 항목의 값을 모두 가져옵니다.
다음은 리스트박스의 색상을 바꿔 보도록 하겠습니다.
먼저 아래의 예제를 살펴봅시다.
1package main
2
3import . "modernc.org/tk9.0"
4
5func main() {
6 l := Listbox()
7 l.Background("blue")
8 l.Foreground("yellow")
9 l.SelectBackground("black")
10 l.SelectForeground("white")
11 l.Height(20)
12 l.Width(6)
13 l.AddItem(0, "item1 item2 item3")
14 l.ItemForeground(0,"red")
15 l.ItemBackground(0,"green")
16 l.ItemSelectBackground(0,"white")
17 l.ItemSelectForeground(0,"black")
18 l.Relief("ridged")
19 Pack(TExit(),l)
20 App.Wait()
21}
위의 코드에서 작성한 대로, 높이가 늘어났습니다. 또한, 색상이 잘 적용된 것을 볼 수 있습니다. 여기서 특정 항목에만 색깔을 다르게 하는 옵션이 지정되어 있어첫째 줄만 색상이 다르게 적용된 것을 알 수 있습니다.
또한, 큰 차이는 없지만 Relief 메서드를 이용하면 flat, groove, raise, ridge, solid, sunken 중에위젯 테두리의 스타일을 변경할 수 있습니다.
이미지 뷰어 예제
그럼 앞서 배운 위젯을 이용해서 이미지 뷰어를 만들어 보도록 하겠습니다. 예제 프로그램은 다음과 같습니다.
1package main
2
3import (
4 "fmt"
5 "log"
6 "os"
7 "runtime"
8 "strings"
9 "path"
10
11 . "modernc.org/tk9.0"
12)
13
14var pbuttons []*TButtonWidget
15var extensions []FileType
16var pbutton *TButtonWidget = nil
17var listbox, listbox2 *ListboxWidget
18var cur *LabelWidget = nil
19var imagesLoaded []*LabelWidget
20func PhotoName(fileName string) string {
21 fileName = path.Base(fileName)
22 return fileName[:len(fileName)-len(path.Ext(fileName))]
23}
24
25func handleFileOpen() {
26 res := GetOpenFile(Multiple(true),Filetypes(extensions)) //다중 선택을 활성화하고 필터를 켭니다.
27 s := make([]string,0,1000)
28 for _, itm := range res {
29 if itm != "" {
30 tmp := strings.Split(itm," ")
31 s = append(s,tmp...)
32 }
33 }
34
35 for _, photo := range s {
36 formatCheck := strings.Split(photo, ".")
37 format := formatCheck[len(formatCheck)-1]
38
39 if (strings.Compare(format, "png") == 0) || (strings.Compare(format, "ico") == 0) {
40 picFile, err := os.Open(photo)
41 if err != nil {
42 log.Println("Error while opening photo, error is: ", err)
43 }
44
45 pictureRawData := make([]byte, 10000*10000)
46 picFile.Read(pictureRawData)
47
48 imageLabel := Label(Image(NewPhoto(Data(pictureRawData))))
49 imagesLoaded = append(imagesLoaded,imageLabel)
50 var deleteTestButton *TButtonWidget
51 deleteTestButton = TButton(
52 Txt("Unshow Image"),
53 Command(func() {
54 GridForget(imageLabel.Window)
55 GridForget(deleteTestButton.Window)
56 }))
57
58 pbuttons = append(pbuttons,deleteTestButton)
59
60 listbox.AddItem(len(imagesLoaded)-1,PhotoName(photo))
61 listbox2.AddItem(len(imagesLoaded)-1,PhotoName(photo))
62 picFile.Close()
63 }
64 }
65}
66
67func DeleteSelected () {
68 s:=listbox.Selected()
69 if len(s) == 0 {
70 return
71 }
72 for _, i := range s {
73 listbox.DeleteOne(i)
74 listbox2.DeleteOne(i)
75 if len(imagesLoaded)-1>i {
76 continue
77 }
78 if cur == imagesLoaded[i] {
79 pbutton = nil
80 cur = nil
81 }
82 Destroy(imagesLoaded[i])
83 Destroy(pbuttons[i])
84 imagesLoaded = append(imagesLoaded[:i],imagesLoaded[i+1:]...)
85 pbuttons = append(pbuttons[:i], pbuttons[i+1:]...)
86 }
87}
88
89func SelectImage() {
90 s:=listbox2.Selected()
91 if len(s) == 0 {
92 return
93 }
94
95 if len(imagesLoaded) -1 < s[0] {
96 return
97 }
98 if imagesLoaded[s[0]] == nil {
99 return
100 }
101 if cur != nil {
102 GridForget(cur.Window)
103 }
104 if pbutton != nil {
105 GridForget(pbutton.Window)
106 }
107
108 Grid(imagesLoaded[s[0]], Row(0), Column(2))
109 Grid(pbuttons[s[0]], Row(0), Column(3))
110 cur = imagesLoaded[s[0]]
111 pbutton = pbuttons[s[0]]
112}
113
114func SelectIndex(index int) {
115
116 if len(imagesLoaded) -1 <index {
117 return
118 }
119 if imagesLoaded[index] == nil {
120 return
121 }
122 if cur != nil {
123 GridForget(cur.Window)
124 }
125 if pbutton != nil {
126 GridForget(pbutton.Window)
127 }
128
129 Grid(imagesLoaded[index], Row(0), Column(2))
130 Grid(pbuttons[index], Row(0), Column(3))
131 cur = imagesLoaded[index]
132 pbutton = pbuttons[index]
133}
134
135func main() {
136 menubar := Menu()
137 //DefaultTheme("awdark","themes/awthemes-10.4.0")
138 //테마를 사용하고 싶을 때에는 테마 명과 경로를 지정해 줍니다.
139 fileMenu := menubar.Menu()
140 extensions = make([]FileType,0,1)
141 extensions = append(extensions, FileType{ "Supported Images", []string {".png",".ico"}, "" } )
142 //필터에 png와 ico를 넣어 줍니다.
143 fileMenu.AddCommand(Lbl("Open..."), Underline(0), Accelerator("Ctrl+O"), Command(func () {
144 handleFileOpen()
145 SelectIndex(len(imagesLoaded)-1)
146 } ))
147 Bind(App, "<Control-o>", Command(func() { fileMenu.Invoke(0) }))
148 fileMenu.AddCommand(Lbl("Exit"), Underline(1), Accelerator("Ctrl+Q"), ExitHandler())
149 Bind(App, "<Control-q>", Command(func() { fileMenu.Invoke(1) }))
150 menubar.AddCascade(Lbl("File"), Underline(0), Mnu(fileMenu))
151 imagesLoaded = make([]*LabelWidget, 0, 10000)
152 pbuttons = make([]*TButtonWidget,0,10000)
153 var scrollx, scroll, scroll2, scrollx2 *TScrollbarWidget
154 listbox = Listbox(Yscrollcommand(func(e *Event) { e.ScrollSet(scroll)}) , Xscrollcommand( func(e *Event) { e.ScrollSet(scrollx)}))
155 listbox2 = Listbox(Yscrollcommand(func(e *Event) { e.ScrollSet(scroll2)}), Xscrollcommand(func(e *Event) { e.ScrollSet(scrollx2)}))
156 listbox.SelectMode("multiple")
157 listbox2 = Listbox()
158 listbox.Background("white")
159 listbox.SelectBackground("blue")
160 listbox.SelectForeground("yellow")
161 listbox2.Background("grey")
162 listbox2.SelectBackground("green")
163 listbox2.SelectForeground("blue")
164 listbox2.SelectBackground("brown")
165 listbox.Height(5)
166 listbox.Width(4)
167 listbox2.Height(5)
168 listbox2.Width(4)
169 delBtn := Button(Txt("Delete"), Command(func () { DeleteSelected() }))
170 selBtn := Button(Txt("Select"), Command(func () { SelectImage() }))
171 scroll = TScrollbar(Command(func(e *Event) { e.Yview(listbox) }))
172 scrollx = TScrollbar(Orient("horizontal"),Command(func(e *Event) { e.Xview(listbox) }))
173 scroll2 = TScrollbar(Command(func(e *Event) { e.Yview(listbox2) }))
174 scrollx2 = TScrollbar(Orient("horizontal"),Command(func(e *Event) { e.Xview(listbox2) }))
175 Grid(listbox,Row(1),Column(0), Sticky("nes"))
176 Grid(scroll,Row(1),Column(1), Sticky("nes"))
177 Grid(scrollx,Row(2),Column(0), Sticky("nes"))
178 Grid(delBtn,Row(3),Column(0), Sticky("nes"))
179 Grid(listbox2,Row(1),Column(2), Sticky("nes"))
180 Grid(scroll2,Row(1),Column(3), Sticky("nes"))
181 Grid(scrollx2,Row(2),Column(2), Sticky("nes"))
182 Grid(selBtn,Row(3),Column(2), Sticky("nes"))
183 App.WmTitle(fmt.Sprintf("%s on %s", App.WmTitle(""), runtime.GOOS))
184 App.Configure(Mnu(menubar), Width("80c"), Height("60c")).Wait()
185}
186
이 예제에서는 구현을 간단하게 하기 위해 모든 이미지 위젯을 불러올 때 미리 만들어 두며, 중복 파일을 확인하지 않습니다. 앞서 말씀드린 문제점을 개선할 수도 있고, 주석 처리된 부분인 DefaultTheme 메서드를 이용하여 테마를 변경해 볼 수도 있습니다. 이러한 부분을 개선한 프로그램을 새로 만들어 보면서 연습해 보시기 바랍니다.
정리
이번 글에서는 Go 언어의 Tcl/Tk 라이브러리의 명령 호출이 어떤 식으로 동작하는지 알아보고, 리스트박스가 추가된 이미지 뷰어를 만들어 보았습니다.
- Tcl/Tk 라이브러리의 명령 호출 방식
- 리스트박스 사용 방법
- 리스트박스 위젯의 속성 변경
- 이미지 뷰어 작성
이와 같은 방식으로 다른 라이브러리들의 수정에도 도전해 보고, 추가한 기능으로 완성된 프로그램을 작성해 보시기 바랍니다.