지금까지 walkmgr 에 추가된 기능들로도 어지간한 윈도 네이티브 GUI 프로그래밍은 가능 했습니다만. 보편적인 GUI 프로그래밍에서 가장 크게 부족한 기능중 하나가 모달 다이얼로그(Modal Dialog) 기능이었습니다.
흔히 다이얼로그(Dialog)라고 하자면 대화창 또는 대화상자라고 칭하곤 하지요..
일반적으로는 사용자의 의사를 묻거나 사용자의 어떤 선택이 필요한 경우 사용자 선택에 따른 로직의 분기가 필요하다던가 하는 경우에 사용됩니다.
가장 쉽게 접하는것은 MessageBox 같은 함수겠지요. 자바스크립트를 기준하자면 alert 이나 confirm 같은 함수로 생성되는 대화창을 들 수 있겠습니다.
기존에 제공되던 walkmgr.NewWin 같은 메소드를 통해 생성하는 일반적인 윈도와 모달 다이얼로그가 가지는 가장큰 차이점은 해당 창을 생성한 부모창과 별개로 동작하느냐 아니냐의 차이를 들 수 있습니다.
사실 win32 API나 MFC를 다뤄보신 분들은 다이얼로그를 말할때 모달(Modal), 모달리스(Modaless) 2가지 종류가 있는걸 아시는 분들도 있으실텐데요..
모달 다이얼로그는 호출한 부모창을 점유(?) 한채로 실행되기 때문에 모달 다이럴로그를 호출한 부모창은 이 다이럴로그창이 닫기기 전까지는 제어권을 상실합니다.
모달리스 다이얼로그는 말그대로 창을 생성한 부모와 상관없이 독립적으로 동작을하고 창이 떠있더라도 부모창도 따로 버튼을 누르거나 창을 옮기거나 하는짓이 가능합니다.
모달 다이럴로그는 제어권을 점유한 상태이기 때문에 창을 닫기 전까진 부모창을 이동하거나 부모창의 버튼, 메뉴등을 클릭하거나 에디트박스의 내용을 변경한다던지.. 하물며 창의 선택조차 되지 않습니다.
이러한 모달 다이얼로그가 왜 필요한가?
한마디로 사용자가 어떤 선택을 하기 전까지는 아무것도 하지 않아야 하는 상황을 들 수 있겠습니다.
사용자로 하여금 선택지를 주고 그 선택이 있기 전까지 아무런 연산을 하고 싶지 않을경우 라던지 ..를 들 수 있겠지요.
자바스크립트에서의 경우도 alert 나 confirm 을 호출하면 이 대화창이 떠있는 동안은 다음 스코프로 진행되지 않고 대기하는것과 같은 이유입니다.
다이얼로그와 관련해서 추가된 것들이 좀 있어서 ..
기존에 walkmgr 을 사용하시던 분들의 경우에
go get -u github.com/pirogom/walkmgr로 패키지 업데이트를 하신 뒤에 사용하시기 바랍니다.
모듈을 사용하시는 경우에는 go get -u 만 하셔도 됩니다만.. GOPROXY 관련해서 패키지 업데이트 인식이 안되거나 캐시 문제로 변경사항이 인지되지 않는 환경이시면 ..
go env -w GOPRIVATE=github.com/pirogom
go get -u다이얼로그와 관련된 메소드들은 따로 dialog.go 파일로 분리해 두었으니 참고 하시구요.
언제나 그렇듯이 예제는 walkmgr_example 저장소에 올려두었습니다.
func TestDialog1(t *testing.T) {
wm := walkmgr.NewWin("다이얼로그 예제1", 640, 480)
// 레이아웃 지정 없음 ( 기본 LAYOUT_VERT )
wm.PushButton("기본", func() {
dlg := walkmgr.NewDialog(wm.Window(), "기본", 300, 300, nil)
dlg.Label("라벨1")
dlg.Label("라벨2")
dlg.Label("라벨3")
dlg.StartDLG()
})
// 세로 배치
wm.PushButton("LAYOUT_VERT", func() {
dlg := walkmgr.NewDialog(wm.Window(), "LAYOUT_VERT", 300, 300, nil, walkmgr.LAYOUT_VERT)
dlg.Label("라벨1")
dlg.Label("라벨2")
dlg.Label("라벨3")
dlg.StartDLG()
})
// 가로 배치
wm.PushButton("LAYOUT_HORI", func() {
dlg := walkmgr.NewDialog(wm.Window(), "LAYOUT_VERT", 300, 300, nil, walkmgr.LAYOUT_HORI)
dlg.Label("라벨1")
dlg.Label("라벨2")
dlg.Label("라벨3")
dlg.StartDLG()
})
wm.Start()
}기본적으로 모달 다이얼로그는 부모창이 무조건 존재해야 하기 때문에 .. 기본적으로 창 하나를 만들었습니다.
일반 윈도를 만들고 창의 요소를 배치하고 하는 모든 과정은 NewWin을 통해 창을 생성하고 PushButton 이나 Label 등을 다루는것과 동일한 방법으로 다루시면 됩니다만.. 몇가지 경우에 사용되는 메소드의 명칭이 다릅니다.
우선 다이얼로그를 생성하는 메소드는 NewDialog 인데요
func NewDialog(owner *walk.MainWindow, title string, width, height int, margin *walk.Margins, lt ...LayoutType) *WalkUI {첫번째 인자는 다이얼로그를 생성하는 부모창의 윈도 핸들을 전달하시면 됩니다. WalkUI 의 Window() 메소드의 리턴값을 전달하시면 됩니다.
두번째 인자는 창 타이틀 명칭을 전달하시면 되구요, width, height는 창의 넓이와 높이를 지정하시면 됩니다 margin 인자는 레이아웃의 가장자리의 마진 값을 지정하는 옵션인데 일반적으론 사용하실일이 없을겁니다. 여백없이 빡빡하게 레이아웃 구성을 하고 싶으시다면 walk.Margins 구조체에 값을 담아서 전달하시면 되겠습니다.
마지막 lt 인자는 NewWin 등에서도 설명했던 레이아웃의 방향? 형태? 를 지정하시면 됩니다만. 이 인자는 사용않하더라도 기본 값인 walkmgr.LAYOUT_VERT가 기본입니다.




결과를 보면 lt 인자를 사용 하지 않은 창과 LAYOUT_VERT를 적용한 창의 레이아웃 구성이 같은걸 보실 수 있습니다.
func TestDialog2(t *testing.T) {
wm := walkmgr.NewWin("다이얼로그 결과", 640, 480)
wm.PushButton("팝업", func() {
dlg := walkmgr.NewDialog(wm.Window(), "다이얼로그 결과", 300, 300, nil)
dlg.Label("다이얼로그 결과")
dlg.PushButton("확인", func() {
dlg.Dlg().Accept()
})
dlg.PushButton("취소", func() {
dlg.Dlg().Cancel()
})
dlg.PushButton("100", func() {
dlg.CloseDLG(100)
})
result := dlg.StartDLG()
switch result {
case 0:
walkmgr.MsgBox("Not choice")
case win.IDOK:
walkmgr.MsgBox("IDOK")
case win.IDCANCEL:
walkmgr.MsgBox("IDCANCEL")
default:
walkmgr.MsgBox(fmt.Sprintf("%d", result))
}
})
wm.Start()
}두번째 예제를 보자면 ..
다이얼로그의 사용 이유가 사용자의 선택에 따라 후속 처리를 분기할 수 있기 때문이라고 말씀 드렸는데요 그러려면 사용자가 무엇을 선택 했는지를 알 수 있어야 합니다.
그런 이유로 다이얼로그의 경우 Start(), StartForground(), Close() 같은 메소드가 아닌 StartDLG(), StartForgroundDLG(), CloseDLG() 같은 다이얼로그 전용 메소드들을 사용합니다.
그 가장큰 이유가 일반창은 윈도 프로세스가 종료된 뒤의 실행 결과를 얻는 구조가 아니지만 다이얼로그의 경우 해당 창이 종료하며 리턴되는 값을 지정할 수 있는 구조로 되어 있습니다.
위의 예제에서 사용된
dlg.Dlg().Accept(), Cancel() 메소드는 MS에서 다이얼로그를 위해 미리 지정해둔 리턴값들이 몇가지 됩니다. IDOK, IDCANCEL 같은 것들을 들 수 있겠지요. walk 패키지에서도 이러한 것들을 미리 지정을 해 두었고 보편적으로 가장 많이 사용하는 IDOK, IDCANCEL 은 따로 메소드로 구성을 해 두었습니다.
물론 IDOK 의 경우 dlg.CloseDLG(walk.IDOK) 와 동일한 동작을 보여주긴 합니다만..
개인적으로 권장하는 방향은 IDOK, IDCANCEL 같은 것들에 연연할 필요는 없이 사용자가 어떤 액션도 없이 그냥 창을 닫아버린 에는 StartDLG() 의 리턴값은 0이라는 것만 기억하시면 됩니다. 나머지는 사용자가 편한대로 구성하시면 된다는 말이죠.
“확인” 버튼은 100, “취소” 버트는 1000을 한들 어느 값이 어떤 선택을 한것인지만 따로 처리하시면 될거라 봅니다.

이 예제의 경우 확인, 취소, 100 중 아무것도 선택 없이 창을 그냥 닫아버리면.

확인 버튼을 클릭하면

100 버튼을 클릭하면.

이렇게 다이얼로그의 결과를 따로 알려주도록 구성된 예제입니다.
dlg.PushButton("100", func() {
dlg.CloseDLG(100)
})100버튼을 보면 CloseDLG(100) 이라고 작성되어 있는게 보이실겁니다. 이 CloseDLG에 인자로 전달된 int 값이 StartDLG나 StartForegroundDLG() 같은 메소드의 리턴값으로 반환되게 됩니다.
한마디로 CloseDLG 에 어떤 값을 전달하느냐가 생성한 대화창의 결과 값이 된다는 말이 되겠습니다.
기존에 다이얼로그가 아닌 창을 다루셨던 분들은 .. Start 계열의 메소드나 Close 대신 DLG를 붙인 StartDLG, CloseDLG 같은 메소드들을 사용해야 한다는 점과 WalkUI의 Window() 메소드를 호출해서 walk.MainWindow 객체를 가지고 뭔가를 하던 짓들을 다이얼로그의 경우 Window() 가 아닌 Dlg() 메소드를 통해 접근하셔야 한다는 점 정도가 다르다고 보시면됩니다.
func TestDialog3(t *testing.T) {
wm := walkmgr.NewWin("DialogTest", 640, 480)
wm.PushButton("다이얼로그", func() {
var canClose = true
dlg := walkmgr.NewDialog(wm.Window(), "TestDLG", 300, 300, nil)
dlg.PushButton("창닫기 가능", func() {
canClose = true
})
dlg.PushButton("창닫기 불가", func() {
canClose = false
})
dlg.Starting(func() {
walkmgr.MsgBox("창 시작 이벤트")
})
dlg.Closing(func() bool {
if canClose {
walkmgr.MsgBox("창닫기 가능한 상태입니다. 창을 닫습니다.")
} else {
walkmgr.MsgBox("창닫기 불가한 상태입니다. 창을 닫을 수 없습니다.")
}
return canClose
})
dlg.StartDLG()
})
wm.Start()
}그 외에 창 프로세스 시작시에 무언가를 하는 Starting 콜백이나 창이 닫힐때 무언갈 하는 Closing 콜백, IgnoreClosing 같은 창 닫힘 버튼등을 통해 창을 닫지 못하게 하는등의 일반 윈도에서 사용되던 대부분의 구조는 그대로 사용 가능하시니 기존 글들을 참고하시면 되겠습니다.