본문으로 바로가기

이 글은 supermania님이 작성하신 글로, 최대한 원본 유지를 하였습니다.

Intro

안녕하세요? supermania입니다. 인학님께서 맡겨주신 이 게시판에 처음으로 글을 올립니다. 그동안 몇 차례 강좌를 작성해 왔는데 이렇게 올리기는 처음입니다 그동안 썼던 강좌들도 빠른 시일 안에 정리해서 올릴 수 있도록 하겠습니다. 이번에 알아볼 내용을 grid를 이용한 간단한 계산기를 만드는 Tcl/Tk소스입니다. 출처는 Tcler's Wiki 입니다.

Code

wm title . Calculator
grid [entry .e -textvar e -just right] -columnspan 5
bind .e <Return> =
set n 0
foreach row {
    {7 8 9 + -}
    {4 5 6 * /}
    {1 2 3 ( )}
    {C 0 . =  }
} {
    foreach key $row {
        switch -- $key {
            =       {set cmd =}
            C       {set cmd {set clear 1; set e "";set res ""}}
            default {set cmd "hit $key"}
        }
        lappend keys [button .[incr n] -text $key -command $cmd]
    }
    eval grid $keys -sticky we ;#-padx 1 -pady 1
    set keys [list]
}
grid .$n -columnspan 2 ;# make last key (=) double wide
proc = {} {
    regsub { =.+} $::e "" ::e ;# maybe clear previous result
    if [catch {lappend ::e = [set ::res [expr 1.0*$::e]]}] {
        .e config -fg red
    }
    .e xview end
    set ::clear 1
}
proc hit {key} {
    if $::clear {
        set ::e ""
        if ![regexp {[0-9().]} $key] {set ::e $::res}
        .e config -fg black
        .e icursor end
        set ::clear 0
    }
    .e insert end $key
}
set clear 0
focus .e           ;# allow keyboard input
wm resizable . 0 0

100줄도 안 되는 짤막한 소스지만 tcl의 간결함과 tk의 강력함을 쉽게 엿볼 수 있는 소스가 될 것 같아서 선정했습니다. 간략하게 알아보겠습니다.

Comment

wm title . Calculator #1
grid [entry .e -textvar e -just right] -columnspan 5 #2
bind .e <Return> =  # 3
set n 0 #4

 상단 entry 부분을 추가, main window title을 변경, entry widget key binding, temp변수 초기화 작업을 합니다.

  • #1 : main window(tk에서 root window는 '.'입니다) title을 변경합니다.
  • #2 : widget들을 모눈종이 형태로 관리할 수 있는 grid geometry manager로 'entry' widget을 추가합니다.(-textvar option으로 entry에 입력되는 내용은 변수 'e'에 들어가고 -just option으로 entry내용은 오른쪽으로 정렬됩니다) pack으로 widget을 관리할 때는 높이나 폭을 픽셀 단위로 따로 지정해 주지만 grid로 사용할 경우 grid단위로 widget이 관리되기 때문에 특정한 크기를 가지기 위해서는 grid를 병합해야 합니다 -columnspan 5은 5칸의 grid크기를 병합시킨 것입니다.
  • #3 : entry widget (.e)에서 Return(Enter) 키를 '='이라는 proc의 호출로 bind 시킵니다 tcl에서는 함수이름에 특별한 제한도 없고 또 함수호출에 특별한 표시가 있는 것도 아니기 때문에(C언어처럼'()' 들어가는 식으로) 처음 접하시는 분들은 proc을 호출하는 부분이 조금 혼동될 수 있습니다.
  • #4 : 이후 widget을 foreach문으로 생성할 때 widget이름으로 사용할 counter변수입니다.
foreach row {  #5
    {7 8 9 + -}
    {4 5 6 * /}
    {1 2 3 ( )}
    {C 0 . =  }
} {
    foreach key $row {  #6
        switch -- $key {  #7
            =       {set cmd =}
            C       {set cmd {set clear 1; set e ""}}
            default {set cmd "hit $key"}
        }
        lappend keys [button .[incr n] -text $key -command $cmd] # 8
    }
    eval grid $keys -sticky we ;#-padx 1 -pady 1 #9
    set keys [list] #10
}
grid .$n -columnspan 2 ;# make last key (=) double wide  #11

grid를 사용할 때 일반적으로 사용하는 방법인 다중 foreach문을 사용해서 전체 grid layout key pad를 만드는 부분입니다

  • #5 : 외각 foreach문입니다. loop가 돌 때마다 row라는 변수에 list를 넣고 안쪽 내용을 처리합니다(총 4번 실행됩니다)
  • #6 : 내부 foreach문입니다 #5에서 한번 분리한 list를 다시 key라는 변수에 분리합니다, 예를 들어 #5, #6이 처음 실행될 상황이었다면 처음 #5에서 row에 {7 8 9 + -} 이 리스트가 할당되고 #6에서는 다시 이 리스트를 분리해 7,8,9,+,-순으로 key변수에 할당하면서 내부 foreach내부 코드를 수행합니다.(각각 5번 마지막은 4번 수행됩니다 / 결국 전체루프는 4 X 5 - 1 = 19번 수행됩니다 )
  • #7 : 일반적으로 많이 사용하는 switch문으로 분리한 key변수의 내용을 확인해 '='일 경우 'C'일 경우 그리고 나머지 경우에 따라 각각 다른 내용의 cmd변수를 정의합니다(이 cmd변수는 이후에 widget 생성시 event proc호출에서 그대로 사용됩니다.) '='일 경우 cmd는 '='라는 프로시져를 호출 , 'C'일경우 프로시져를 호출하지 않고 직접 명령 수행, 나머지 경우 hit이라는 프로시져를 $key parameter와 함께 호출합니다.
  • #8 : lappend는 list에 내용을 추가하는 명령으로 keys라는 list에 버튼을 추가해 나갑니다. list를 만드는 이유는 grid의 모양을 5 X 4 형태로 만들기 위한 테크닉입니다 이렇게 list로 만든 후 #9에서 gird에 한 줄로 추가시킵니다.(위에서 언급한 바대로 .incr n으로 각각의 버튼 widget의 이름은 1,2,3,4,5,6... 이렇게 붙여지고 위 switch문에서 정의한 cmd문을 -command문을 이용해 event proc으로 지정합니다. / perl처럼 초기화되지 않은 변수에 ++나 --등을 사용할 수 없습니다 #4처럼 초기화가 필요합니다.

여기까지가 각각의 내부 foreach처리 내용입니다.

  • #9 : #8에서 설명했듯이 keys라는 변수에 버튼들을 생성해 놓고 여기서 한번 grid에 등록시킵니다 '-sticky we'는 각각 grid안에 widget을 배치시키는 방법인데 n, w, e, s 문자를 조합해서 지정할 수 있습니다 (이 부분에 대해서는 조만간에 grid에 대해서 따로 정리해서 올리겠습니다) we는 widget의 크기를 유지하면서 높이와 폭을 정 가운데로 유지하는 옵션입니다.
  • #10 : #9에서 widget을 grid에 등록했으니 이제 다음 loop를 위해 keys list를 초기화합니다

여기까지가 각각의 외부 foreach처리 내용입니다

  • #11 : 마지막 생성된 .19 widget '='이 차지하는 공간을 2칸으로 만들어 보기 좋게 만들어줍니다. 당연히 .$n대신 .19를 해도 동일합니다
proc = {} {
    regsub { =.+} $::e "" ::e ;# maybe clear previous result  #12
    if [catch {lappend ::e = [set ::res [expr 1.0*$::e]]}] {  #13
        .e config -fg red
    }
    .e xview end #14
    set ::clear 1  #15
}

위에서 여러 번 언급되었던 '=' proc입니다. 이 계산기 프로그램의 핵심적인 프로시져로 사용자가 입력한 수식을 expr을 이용해서 계산하고 간단하게 예외를 처리합니다.

  • #12 : 정규식을 이용해서 '바꾸기'를 수행해 주는 regsub명령입니다 간단하게 regsub ?정규식 ?Source ?변경할값 ?Target 형태로 사용됩니다. 정규식 관련한 명령들은 추후에 더 자세하게 알아보겠습니다 / 여기서는 수식에서 '='문자가 나타난 곳 이후를 모두 지우는 작업을 합니다
  • #13 : 예외처리 구문입니다. 복잡하게 여러 명령이 중첩되어 있지만 차분하게 뜯어보면 그리 복잡하지 않습니다. if다음에 catch 명령을 수행하는 것으로 즉 catch의 {} 사이의 문장에서 예외가 발생할 경우 if문의 {} 안 내용을 수행합니다 catch에서 체크하는 부분은 $::e(global $e) 변수에 '='문자를 추가하고, $::e에 저장된 수식에 1.0을 곱한 값을 저장한 $::res의 값을 그 뒤에 추가합니다. 이 과정 전체에서 예외(error)가 없다면 그냥 다음으로 넘어가게 되고 만약 예외가 발생한다면 entry의 글자색을 red로 변경해 줍니다.
- 민인학 님 comment
'=' proc과 'hit' proc에서 변수명을 보면 $::e, $::res 같은 형태가 계속 나옵니다. 이는 일반적인 다른 프로그래밍 언어에서도 항상 발생하는 scope와 관련이 있습니다. 즉 이 예제에서 $e와 $res는 global 하게 main 루틴에서 사용되는 변수이기 때문에 proc속에서는 다른 scope를 가지게 됩니다 (invisible 합니다). 따라서 global변수에 접근하기 위해서는 위에서처럼 $::변수명 형태로 쓰거나 (::은 name space를 나타내는 표현입니다).
혹은 변수를 사용하기 전에 global e ,global res 같이 전역변수 사용을 선언해 준 뒤 $e, $res를 사용하시면 됩니다.
  • #14 : entry widget의 end index (저장된 text의 마지막 문자의 다음)을 화면의 끝에 정렬시켜 줍니다.
  • #15 : hit proc에서 flag로 쓰이는 clear라는 변수를 1로 변경해 줍니다.
proc hit {key} {  #16
    if $::clear {  #17
        set ::e ""  ①
        if ![regexp {[0-9().]} $key] {set ::e $::res} ②
        .e config -fg black  ③
        .e icursor end ④
        set ::clear 0 ⑤
    }
    .e insert end $key  #18
}
set clear 0
focus .e           ;# allow keyboard input  #19
wm resizable . 0 0 #20

 이제 거의 마지막입니다. '=', 'C'버튼을 제외한 나머지 버튼들을 처리하는 hit proc과 마무리입니다.

  • #16 : 위에 설명된 '=' proc과 흡사합니다 하지만 차이가 있다면 '=' proc은 parameter가 없지만 이 hit proc은 parameter로 key라는 변수를 받습니다.
  • #17 : 위에서 잠깐 언급했지만 clear flag(clear변수)가 켜져 있다면 먼저 entry를 지우는 작업을 해줘 야하기 때문에 필요한 if문입니다. 만약 참이면 entry의 내용을 비우고(①), 넘겨받은 key변수의 내용이 합당한 지 확인하고 (②) entry의 글자색을 black로 변경 후(④) 커서를 끝으로 이동시키고(⑤) clear flag를 0으로 세팅합니다(⑥) 방금 '='을 수행한 뒤에만 이 부분이 실행되겠죠? 그 후부터는 단순하게 #18 부분만 실행됩니다.
  • #18 : .e entry의 end index(#14에 설명)에 넘겨받은 문자를 추가시켜 줍니다.
  • #19 : entry widget에 포커스를 맞춰서 키보드로의 입력도 되도록 합니다.
  • #20 : mainwindow의 크기 재 조정을 막습니다.

Outro

서두에 말한 것처럼 짧지만 tcl/tk 특히 grid를 이용해 보는 데는 적절한 예제인 것 같습니다. 제목에는 간단한 계산기라고 말했지만 사실 이 프로그램 자체는 계산기라고 말하기는 조금 모호합니다 소스를 보신 분들은 아시겠지만 여기서 하는 일은 사용자가 작성한 수식을 expr에 넘겨주고 그 결과를 보여주는 작업을 하는 것입니다 따라서 일반 계산기보다 훨씬 유연하고 강력합니다 (키보드로 직접 pow(2,3) * 100 해보면 잘 수행된다 ) 상단 링크 wiki에 가보면 이 소스에 대한 약간의 부가적인 내용들에 대한 논의와 개선이 있으니 꼭 한번 읽어보시기 바랍니다 (또한 WindowsCE에서 실행한 화면도 있네요~ 왠지 뿌듯) 처음으로 완성해 올리는 강좌라 너무 부족한 부분이 많은 거 같습니다. 눈을 괴롭히는 수많은 오, 탈자 특히 맞춤법 error들을 너그럽게 용서해 주시길 바랍니다(혹은 email: tcltk@yongbin.com) 저 또한 tcl/tk를 하나하나 배워가는 과정이기 때문에 내용면에서도 많은 부족함을 느낍니다 여기에 추가되어야 할 내용이나 제가 잘못할고 있는 내용 역시 주저 없이 지적 바랍니다. 끝으로 강좌에 대한 좋은 제안을 해 주시고 많은 comment, 그리고 무엇보다도 이런저런 핑계로 늦어짐에도 끝까지 기다려주신 인학 님께 감사합니다~!(다시 한번 축하드려요!) 감사합니다 (_ _)