피로곰's 모두의 프린터

Go로 윈도 네이티브 GUI 줘 패기... 4번째 입니다.

이번엔 레이아웃에 대한 부분을 다뤄보도록 합니다.

앞서 C/C++에서 윈도 API를 가지고 GUI 어플리케이션을 만드는게 매우 비효율적인 노가다 개삽질이라는 식의 표현을 한적이 있는데요. 그 이유는 ..

API에서 모든 UI객체는 전부다 윈도입니다. 창도 윈도 버튼도 윈도 에디트 박스도 체크박스도 스크롤바도 스크롤바 안의 상하단 버튼도 죄~~다 윈도입니다. 그 말은 그 하나하나를 다 CreateWindow 를 하고 그 각 객체별로 WndProc 같은 메시지 프로시저를 한땀 한땀 다 등록을 시켜야 한단 소리죠 그래야 키보드 입력이던 마우스 클릭이던 내용을 그리고 지우고 지지고 볶고 .. 가 가능합니다.

그렇게 수 없이 많은 반복적인 코드를 생성해야 한다는 점도 문제지만 윈도 API로 GUI를 만든다는게 지랄같은 가장 큰 이유는 실제 그 프로그램이 어떤 형태로 눈에 보여질지를 직관적으로 알 수 없다는 겁니다. 각 객체를 생성하고 해당 객체들이 어느 위치에 놓일지 x, y 좌표 값을 가지고 배치를 해대야 하는데 .. 그게 어찌 보일진 실행을 해 봐야 아는것이죠.

그런이유로 과거에 API로 GUI만들때는 포토샵이든 그림판이든 뭐든간에 레이아웃 디자인을 잡고 좌표값을 뽑아 놓고 그 값을 보고 코딩을 하시던 분들도 계셨습니다. 이런식으로 SetWindowPos 같은 함수로 일일이 x,y 좌표값으로 버튼 하나 에디트박스 하나 체크박스 하나 라벨하나 하나하나 한땀 한땀 위치를 잡아 대는 짓이 매우 지랄 같고 그런 놈들의 수 많은 이벤트 처리를 비롯한 반복되는 코드들을 줄인답시고 나온게 MFC같은 놈이죠.

MFC에 들어서는 그나마 .. 윈도 API에 비해선 나아진게

이렇게 눈으로 직접 봐 가며 위치도 잡고 크기 조정도 가능한 에디터를 지원하고 이 에디터를 통해 코드 상의 변수나 함수와 직접적으로 연동시키는 등의 짓도 가능해 졌습니다. 그럼에도 불구하고 API든 MFC든 C/C++라는 언어를 기반으로 MS의 직원들이 지들 나름대론 그래도 좀 편해보고자 만든 체계여도 리눅스나 웹쪽 개발만 해오던 사람 입장으론 쉽게 개념이 이해가 되지 않기도 합죠..

여튼 사설이 너무 길어지고 있긴 합니다만 .. 여튼간에 윈도 GUI 프로그래밍은 .. 어려워요 .. 

하지만 Go라는 Managed 언어를 쓰면서 너무 어렵기만 하고 노가다만 심하다면 .. 때려쳐야지 그걸 왜 합니까..

여튼간에 Go뿐 아니라 파이썬이든 러스트든간에 MS에서 제공하는 저런 API, SDK, IDE의 혜택을 받지 못하는 경우 개발자에게 쥐어진건 오직 코드작성을 하는 방법뿐이 남지 않기 때문에 대부분의 Managed 언어에서 UI 프로그래밍을 할때 레이아웃은 툴킷이나 프레임워크 차원에서 알아서 잡아버립니다.

알아서 잡아버린다는게 뭔 소리냐면 ..

웹프론트에서 반응형 그리드를 생각 하면 이해가 빠르실텐데;; 웹프론트를 해보신 적 없는 분들은 반응형이니 그리드니 뭔 소린지 모르실테니 간단히 설명 해봅니다.

1-1. 라벨  1-2. 에디트 박스 1-3. 버튼
2-1.라벨 2-2. 드롭다운박스 2-3. 버튼

코드를 작성하는 개발자는 그저 첫째줄에는 라벨, 에디트박스, 버튼 3개를 놓고 두번째 줄에는 라벨, 드롭다운박스, 버튼을 놓겠다고 코드만 작성하면 나머지는 전체 윈도의 크기를 기준하여 한줄에 3개의 요소가 있으니 넓이 / 3, 2줄이니 높이 / 2를 하여 알.아.서 레이아웃을 잡아 배치를 해버립니다.

물론 이 배치 기준도 가로기준이니 세로기준이니 그런걸 세세하게 나누기도 합니다만.

제가 만든 Walk 랩퍼의 경우 기본적으로 윈도 창은 세로(Vertical)을 기준으로 동작합니다.

func test1() {
	mgr, _ := NewWindowMgrNoResize("레이아웃 테스트", 640, 480, GetIcon())

	mgr.Label("라벨입니다.")
	le := mgr.LineEdit(false)
	le.SetText("라인 텍스트 입력")
	mgr.CheckBox("체크박스입니다", false, func() {})
	mgr.StartForeground()
}

640*480의 윈도 창을 하나 만들고 라벨, 라인텍스트, 체크박스 3개의 UI객체를 윈도에 추가를 했습니다. 세로 기준으로 레이아웃 베치가 되기 때문에 위에서 아래로 한줄당 하나의 UI요소가 배치되고 이 놈들의 넓이는 창의 넓이와 같고 높이는 높이/3 으로 각각 배치된겁니다. 

그럼 앞서 표로 설명한거 같이 그리드 형식으로 이래저래 배치를 하려면 어찌 해야 하느냐 ..

func test1() {
	mgr, _ := NewWindowMgrNoResize("레이아웃 테스트", 640, 480, GetIcon())

	mgr.HSplit()
	mgr.Label("1-1. 라벨")
	le1 := mgr.LineEdit(false)
	le1.SetText("1-2. 에디트 박스")
	mgr.PushButton("1-3. 버튼", func() {})
	mgr.EndSplit()

	mgr.HSplit()
	mgr.Label("2-1. 라벨")
	mgr.DropDownBox([]string{"1.하나", "2.둘", "3.셋"})
	mgr.PushButton("2-3. 버튼", func() {})
	mgr.EndSplit()

	mgr.StartForeground()
}

이렇게 하시면 됩니다. 1줄에 3개의 요소가 총 2줄로 전체 창의 크기에 맞춰서 알아서 레이아웃이 잡혀서 균등배치가 되었지요. 근데 이렇게 놓고보면 보기가 그닥 좋지 않습니다. 그럼 창 크기를 적당히 조절 하시면 되겠구요.

이렇게 조절 하셔도 되구요. 중요한건 창의 높이의 경우 모든 UI요소들은 최소 높이가 존재합니다. 폰트의 크기나 체크박스 같은 이미지나 여러 이유로 최소한의 높이라는게 있는데 창의 높이가 이 UI요소들의 최소 높이를 합한것보다 적은 값으로 지정된 경우에는 창의 크기를 UI 요소들의 합에 맞춰서 늘려버립니다.

예를들어

func test1() {
	mgr, _ := NewWindowMgrNoResize("레이아웃 테스트", 640, 1, GetIcon()) // 창 높이 1

	mgr.HSplit()
	mgr.Label("1-1. 라벨")
	le1 := mgr.LineEdit(false)
	le1.SetText("1-2. 에디트 박스")
	mgr.PushButton("1-3. 버튼", func() {})
	mgr.EndSplit()

	mgr.HSplit()
	mgr.Label("2-1. 라벨")
	mgr.DropDownBox([]string{"1.하나", "2.둘", "3.셋"})
	mgr.PushButton("2-3. 버튼", func() {})
	mgr.EndSplit()

	mgr.StartForeground()
}

이렇게 창 높이를 1로 생성한다 해도

이렇게 라벨이나 에디트 박스 버튼들의 최소한의 높이와 Margin 값을 합한 최소값으로 창 크기를 늘려버립니다. 어찌됫건 막 짜도 창은 제대로 구성되서 보여집니다.

위의 코드를 보시면 HSplit 이라는 놈이 등장 하는데요 ..

창에 배치되는 요소들은 기본은 위에서 아래로 세로(Vertical)으로 쌓아가니까 쉽게보자면 위에서 아래로 쌓아가는거고 그 한줄 한줄에 여러 요소를 나눠서 넣고 싶으면 그 한줄을 나누는(Split) 것이지요. 

func (m *WinResMgr) HSplit() *walk.Splitter 
func (m *WinResMgr) VSplit() *walk.Splitter
func (m *WinResMgr) EndSplit()

그래서 이 두 함수가 존재합니다. HSplit 는 가로로 분할 해주는거고 VSplit 은 세로로 분할 해주는 겁니다.

HSplit 이던 VSplit 이던 분할을 끝마칠때는 EndSplit을 호출하시면 됩니다.

HTML이나 XML에서 <tag></tag>와 같이 HSplit 이 호출되고 EndSplit 이 호출 되기 전까진 가로로 분할하여 UI 요소를 쌓는다는 소리고 VSplit 이 호출되고 EndSplit이 호출되는 사이에 작성된 UI요소의 코드들은 세로로 분할하여 쌓아간다는 소리입니다.

이 Split 함수는 각각의 분할 상황 내에서 중복해서 사용 가능합니다. 

func test2() {
	mgr, _ := NewWindowMgrNoResize("레이아웃 테스트2", 640, 480, GetIcon())

	mgr.HSplit()

	mgr.VSplit()
	mgr.Label("HSplit 안에 Vsplit1")
	mgr.Label("HSplit 안에 Vsplit2")
	mgr.Label("HSplit 안에 Vsplit3")
	mgr.PushButton("버튼1", func() {})
	mgr.EndSplit()

	mgr.TextArea(false)

	mgr.VSplit()
	mgr.Label("HSplit 안에 Vsplit2-1")
	mgr.Label("HSplit 안에 Vsplit2-2")
	mgr.Label("HSplit 안에 Vsplit2-3")
	mgr.Label("HSplit 안에 Vsplit2-4")
	mgr.PushButton("버튼2", func() {})
	mgr.EndSplit()

	mgr.EndSplit()

	mgr.StartForeground()
}

이런식으로 말이죠 .. 

레이아웃에 대한 부분은 이정도 설명하면 된것 같으니 .. 다음 글에선 UI요소들에 대한 설명을 좀 해보도록 하겠습니다.

 

공유하기

facebook twitter kakaoTalk kakaostory naver band