아마 walk와 제가 대충 만든 walk_wrap.go 를 이용한 윈도 gui 프로그래밍에 대한 글은 이게 마지막? 이지 않을까 싶은데요 응용법이나 그런걸 좀더 세세하게 글을 더 쓸지는 모르겠지만.
이번에 다룰 내용은 특정 GUI요소에 대한게 아니라 Go와 Walk를 가지고 윈도GUI를 함에 있어서 가장 주의해야할 사항을 말씀드리겠습니다.
Go언어의 가장큰 장점은 동시성 프로그래밍이 매우 단순하다는 것이지요..

흔히 윈도 프로그래밍을 할때 스레드(Thread)를 생성할때는 위의 CreateThread나 _beginthread 같은 함수를 사용하는데 인자도 많고 복잡해 보입니다..
Go언어의 경우 그냥 앞에 go만 붙여서 쓰면 스레드로 동작을 하는 매우 단순한 구조를 가지고 있는데다 Managed 언어이니 익명함수로 스레드 함수를 남발도 가능한 놈이지요 ..
윈도 GUI를 위한 API는 Go언어를 염두하고 만들어진게 아닙니다. 원래 C/C++ 나 C# 같은 언어들에서 사용되기 위해 개발된거지만 DLL(Dynamic Linked Library)형태로 대부분의 것들이 구성되는 윈도의 특성상 Go언어에서도 DLL을 끌어다가 Go 프로그래밍 하듯 그냥 가져다 쓰고 있지만 .. 엄격하게 따져보자면 walk 를 이용해서 Go에서 윈도 GUI 가 돌아간다는 것은 Go언어의 영역과 DLL내의 함수들에 의해 돌아가는 영역이 따로 분리되어 있다고 생각하시는게 편합니다.
/**
* WaitAndCloseWin
**/
func WaitAndCloseWin() {
mgr, window := NewWindowMgrNoResizeNoMinMax("wait and close", 150, 100, GetIcon())
label := mgr.Label("5초후 종료")
ticker := time.NewTicker(time.Second)
go func(tick *time.Ticker) {
var count int = 5
for {
select {
case <-tick.C:
count--
if count <= 0 {
tick.Stop()
mgr.HideAndClose()
return
} else {
window.Synchronize(func() {
label.SetText(fmt.Sprintf("%d초 남음", count))
})
}
default:
time.Sleep(time.Millisecond)
}
}
}(ticker)
mgr.DefClosing()
mgr.StartForeground()
}

위의 함수는 창을 하나 띄우고 창에 존재하는 Label에 1초마다 카운트를 감소 시켜서 Label 의 내용을 변경시키고 5초가 넘으면 창을 종료시키는 코드입니다.
mgr.DefClosing() 메소드는 5초가 지나기 전에 창을 닫더라도 종료되지 않도록 막아주는 역할을 합니다.
/**
* DefClosing
**/
func (m *WinResMgr) DefClosing() {
m.window.Closing().Attach(func(canceled *bool, reason walk.CloseReason) {
if m.window.Visible() {
*canceled = true
}
})
}
이 함수가 호출되면 창을 닫으려 하는 이벤트가 발생할때 창이 보여지고 있는 상태면 해당 이벤트를 취소해서 창이 닫히지 않도록 만들어 줍니다.
그런 이유로 이 DefClosing() 함수를 사용한 경우에는 .. window.Close() 함수가 아닌
/**
* HideAndClose
**/
func (m *WinResMgr) HideAndClose() {
m.window.Synchronize(func() {
m.window.SetVisible(false)
m.window.Close()
})
}
이놈을 써 줘야 합니다. 앞서 창이 표시되고 있으면 닫을수 없기 때문에 먼저 창을 보이지 않도록 SetVisible(false)를 호출한 뒤에 창을 닫습니다. 그러면 Closing 이벤트에서 이벤트를 취소하지 않게되고 창이 정상적으로 닫히게 되지요.
문제는 ..

이 go 루틴과 StartForground()로 실행될 윈도창의 프로시저와 서로 다른 스레드라는 말이지요 윈도 창이 뜨고 창을 이동하든 버튼을 누르든 뭘 하든 발생하는 윈도와 관련된 이벤트 핸들링이나 기타 등등은 walk 에서 구성한 내부 스레드 내에서 동작을 하는 중이고 .. 그와 별개로 제가 5초를 카운팅 해가며 창의 타이틀을 변경시키는 고루틴을 따로 생성 하였지요..
그러면

이 고루틴 내에서 walk 가 관리중인 윈도에 대해서 뭔가를 하면 어찌 될까요?
충돌 납니다 ㅋㅋ .. 문제는 이게 cgo 나 dll 로 끌어들인 놈이라 윈도 메시지 핸들러등은 Go의 영향권이 아니라서 패닉이 난다거나 하는게 아닌 그냥 서버립니다 프로세스가.. 멈춰버려요.. 멈춰 버리는 이유를 간단히 설명하자면, 윈도API는 대부분 동기화의 테두리 안에서 동작합니다, 윈도 메시지는 순서없이 마구 발생이 가능하지요.. 마우스를 움직여 가며 버튼을 누를수도 있고 ㅎㅎ.. 그런이유로 PeekMessage 나 GetMessage 같은류의 윈도 API함수를 호출하면 내부적으로 동기화 처리를 거쳐서 동작을 합니다. 사용자가 동시에 이짓저짓을 하더라도 윈도 메시지 핸들러 입장으론 한번에 하나의 이벤트에 대한 처리만 동기화를 거쳐서 한다는 말이지요.. 그런데 윈도와 상관없이 동작하는 고루틴내에서 특정 윈도상의 UI객체의 값을 변경한다거나 창을 닫으려 한다거나 하면? 말그대로 동기화 없이 막 들이대는 꼴이 됩니다. 보통은 패닉이 나고 뻗어야할 상황이지만 위에 말했듯이 이놈은 Go런타임 영역 밖에서 관리되는 놈이니까요..
그래서 walk 의 외부에서 윈도에 접근하여 무언가 하려거든
window.Synchronize(func() {
label.SetText(fmt.Sprintf("%d초 남음", count))
})
이 Synchronize 메소드를 사용하시면 됩니다.
이 함수의 역할은 인자로 전달받은 함수를 리스트에 넣어둔 후에 윈도API의 메시지가 발생해서 (WM_SIZE든 WM_PAINT든.. WM_… 뭐든간에.. ) 발생한 윈도 메시지를 처리하는 함수의 마지막 즈음에 함수 리스트에 들어 있는 함수들을 죄다 꺼내서 실행 시킵니다.
그러면 윈도창의 동기화를 거쳐서 안전하게 윈도를 다룰수 있는 상황에서 일을 벌리게 되기 때문에.. 윈도 동기화 프로세스의 밖에서 윈도 동기화 프로세스 내에서 실행가능한 함수들을 밀어 넣을수 있다는게 됩니다.
결론은 그냥 윈도GUI창에 뭐 할라 하면 그냥 쓰세요 이 함수 Synchronize ㅋㅋ PushButton 의 클릭 이벤트나 window.Starting 같은 walk 에서 제공하는 이벤트 콜백들은 윈도의 동기화 처리 내에 있는 경우라 그런 경우들은 굳이 안쓰셔도 됩니다.
// Synchronize enqueues func f to be called some time later by the main
// goroutine from inside a message loop.
func (wb *WindowBase) Synchronize(f func()) {
wb.group.Synchronize(f)
win.PostMessage(wb.hWnd, syncMsgId, 0, 0)
}
이렇게 생겨 먹었습니다. 인자로 전달받은 함수를 큐에 넣어두고 .. 해당 윈도에 큐에 처리할 함수가 추가됫다는 메시지를 발생시킵니다. PostMessage로 윈도 메시지를 발생 시켰으니 윈도 메시지 핸들러가 메시지를 받을꺼고 그러면 동기화 처리 거쳐서 WndProc같은 윈도 메시지를 처리해줄 함수에서 동기화 문제 없이 처리가 가능해 진다는거죠 ..
예제 코드를 간단히 설명 하자면 ..
// wait and close 라는 타이틀로 창을 생성함
mgr, window := NewWindowMgrNoResizeNoMinMax("wait and close", 150, 100, GetIcon())
label := mgr.Label("5초후 종료")
// 고루틴에서 1초마다 이벤트를 발생시킬 티커 생성
ticker := time.NewTicker(time.Second)
// 백그라운드에서 지난 시간을 체크할 고루틴(스레드)
go func(tick *time.Ticker) {
var count int = 5 // 5초 카운트
// 스레드 루프
for {
select {
case <-tick.C: // ticker 이벤트 발생(1초 간격)
count-- // 5초 카운팅
// 지정된 시간이 다 지나면
if count <= 0 {
tick.Stop() // 티커 정지
mgr.HideAndClose() // 윈도 창 닫음
return // 스레드 종료
} else {
// 동기화를 위해 Synchronize 사용
window.Synchronize(func() {
// 창이 닫히기 까지 남은 시간 라벨에 표시
// label 은 walk 내에서 관리되는 객체이기 때문에
// Synchronize 를 통해서 다루지 않으면 충돌발생
label.SetText(fmt.Sprintf("%d초 남음", count))
})
}
default:
// CPU 사용율은 소중하니까!
time.Sleep(time.Millisecond)
}
}
}(ticker)
mgr.DefClosing()
mgr.StartForeground()
이쯤 되겠네요 ..
이상입니다.