피로곰's 모두의 프린터

반응형

오랜만입니다.. 이래저래 벌린일이 많다보니 ..

이번에는 PushButton 과 ImageView 를 다뤄 보겠습니다.. 푸쉬 버튼의 경우에는 워낙 오만 예제에서 이미 다 등장을 햇던 놈이라 굳이 설명을 하지 않더라도 대충;; 뭔지 아실테지만 .. 간단하게나마 설명하고 넘어가겠습니다.

/**
*	PushButton
**/
func (m *WinResMgr) PushButton(text string, clickFunc func()) *walk.PushButton {
	btn, _ := walk.NewPushButton(m.GetParent())
	btn.SetText(text)
	btn.Clicked().Attach(clickFunc)

	m.addObj(btn)
	return btn
}

이렇게 생겨 먹은 놈이구요 ..

	mgr.PushButton("LineEdit 변경", func() {
		le.SetText(fmt.Sprintf("현재 유닉스 타임스탬프: %d", time.Now().Unix()))
	})
	mgr.PushButton("LineEdit 값", func() {
		MsgBox(le.Text())
	})

이런식으로 씁니다..

첫번째 파라메터인 text string은 버튼의 텍스트를 지정합니다. 버튼의 텍스트랄게 뭐 있나요..

이렇게 버튼에 쓰여진 문자.. 어떤 버튼이 무슨짓을 하는 버튼인진 알아야 누를테니까 .. 

두번째 파라메터인 clickFunc 은 버튼이 눌렸을때 호출될 함수를 지정하시면 됩니다. 익명함수로 바로 때려 넣으셔도 되고 미리 만들어둔 함수를 넘기셔도 되구요..

라벨에서도 그렇고 EditText 에서도 그랬지만 .. 이 PushButton 함수는 walk.PushButton 이라는 놈을 리턴합니다 이 리턴된 놈을 가지고 Text(), SetText() 같은 함수로 현재 버튼의 텍스트를 가져오거나 바꾸거나 할 수 있습니다.

쉽게 말해서 토글식으로 버튼이 눌릴때마다 다른 상태값에 따라서 버튼의 텍스트를 변경하는 등의 짓도 가능하다는 말이겠지요.

단지 주의 하질점은 ..

	mgr.PushButton("LineEdit 값", func() {
		MsgBox(le.Text())
	})

이런식으로 MsgBox 함수 같이 사용자가 대화창을 닫기전까지 블럭(Block)되는 형태의 함수를 clickFunc 내에서 사용하는 경우에 버튼의 클릭 이벤트는 각각 개별적으로 발생 함으로 만약 사용자가 버튼을 계속 누르게 되면 clickFunc이 계속 쌓입니다? ㅋㅋ 

이건 콜백형태로 동작하는 무언가들에서 흔히 일어나는 일이니 알아서들 주의 하셔야 합니다.


ImaveView 를 가봅시다.

ImageView는 말그대로 윈도 창에 png나 jpg같은 이미지 파일을 읽어서 표시 해주는 겁니다.

이런 짓이나 ..

이런짓에 저는 사용하고 있습니다.

/**
*	ImageViewWin
**/
func ImageViewWin() {
	var currImage *walk.Image

	defer func() {
		// 새 이미지 로드전에 기존 이미지 해제 않하면
		// 메모리릭 발생 !! 주의
		if currImage != nil {
			(*currImage).Dispose()
		}
	}()

	mgr, window := NewWindowMgrNoResizeNoMinMax("TableView 예제", 1920, 1040, GetIcon())

	mgr.HSplit()

	idealIV := mgr.ImageView()
	idealIV.SetMode(walk.ImageViewModeIdeal)

	cornerIV := mgr.ImageView()
	cornerIV.SetMode(walk.ImageViewModeCorner)

	centerIV := mgr.ImageView()
	centerIV.SetMode(walk.ImageViewModeCenter)

	mgr.EndSplit()

	mgr.HSplit()
	shrinkIV := mgr.ImageView()
	shrinkIV.SetMode(walk.ImageViewModeShrink)

	zoomIV := mgr.ImageView()
	zoomIV.SetMode(walk.ImageViewModeZoom)

	stretchIV := mgr.ImageView()
	stretchIV.SetMode(walk.ImageViewModeStretch)
	mgr.EndSplit()

	updateImg := func(fname string) {
		// 새 이미지 로드전에 기존 이미지 해제 않하면
		// 메모리릭 발생 !! 주의
		if currImage != nil {
			(*currImage).Dispose()
		}
		currImage = LoadImage(fname)
		if currImage != nil {
			idealIV.SetImage(*currImage)
			cornerIV.SetImage(*currImage)
			centerIV.SetImage(*currImage)
			shrinkIV.SetImage(*currImage)
			zoomIV.SetImage(*currImage)
			stretchIV.SetImage(*currImage)
		}
	}

	mgr.PushButton("피로곰", func() {
		updateImg("img0.png")
	})
	mgr.PushButton("환자1", func() {
		updateImg("img2.png")
	})
	mgr.PushButton("환자2", func() {
		updateImg("img1.png")
	})

	//
	window.Starting().Attach(func() {
	})

	mgr.StartForeground()
}

하단의 피로곰, 환자1, 환자2 버튼에 따라서 이미지를 읽어서 지원하는 정렬형태에 맞춰서 출력하는 예제입니다.

/**
*	ImageViewFromFile
**/
func (m *WinResMgr) ImageViewFromFile(imgFile string) *walk.ImageView {
	iv, _ := walk.NewImageView(m.GetParent())
	img, imgErr := walk.NewImageFromFile(imgFile)

	if imgErr == nil {
		iv.SetImage(img)
	}
	m.addObj(iv)
	return iv
}

/**
*	ImageViewFromFile2
**/
func (m *WinResMgr) ImageViewFromFile2(imgFile string, im walk.ImageViewMode) *walk.ImageView {
	iv, _ := walk.NewImageView(m.GetParent())
	img, imgErr := walk.NewImageFromFile(imgFile)

	if imgErr == nil {
		iv.SetMode(im)
		iv.SetImage(img)
	}
	m.addObj(iv)
	return iv
}

/**
*	ImageView
**/
func (m *WinResMgr) ImageView() *walk.ImageView {
	iv, _ := walk.NewImageView(m.GetParent())
	m.addObj(iv)
	return iv
}

/**
*	LoadImage
**/
func LoadImage(fileName string) *walk.Image {
	retImage, retImageErr := walk.NewImageFromFile(fileName)

	if retImageErr != nil {
		return nil
	}
	return &retImage
}

우선 ImageViewFromFile 함수는 ImageView를 생성함과 동시에 바로 이미지 파일을 읽어서 출력해주는 용도의 함수입니다. 인자는 파일경로만 입력하시면 됩니다.

ImageViewFromFile2의 경우 ImageView를 생성함과 동시에 바로 파일을 읽는데 이미지를 어떤식으로 표시할지 확대를 할지 ImageView의 크기에 맞출지 원본크기 그대로 보여줄지 같은 옵션을 walk.ImageViewMode로 지정가능합니다.

ImageViewFromFile이나 ImageViewFromFile2 함수는 ImageVIew를 생성과 동시에 이미지 파일을 지정하고 그 뒤엔 프로그램이나 윈도창이 소멸할때까지 이미지를 변경할일 없는 경우에 사용하시면 됩니다. 만약 이미지를 상황에따라 변경해야 하는 경우에는 이 두 함수 말고 LoadImage 함수와 SetImage를 통해 제어하시면 됩니다.( 뒤에 설명합니다.. 어짜피 이 함수들은 image 객체를 리턴하지 않기 때문에;; 따로 변경이 불가하긴 합니다만.. )

 ImageView 함수는 walk.ImageView 객체를 생성해 줍니다. 파라메터는 없으니 그냥 사용만 하면되고 walk.ImageView객체를 이용해서 이미지를 설정하는 등의 짓을 해야하니 리턴되는 객체는 꼭 변수로 받아두소서 ..

LoadImage 함수는 ImageView 에서 이미지를 출력하는데 사용되는 walk.Image 객체를 이미지 파일을 읽어서 만들어주는 함수입니다. 이 함수로 만든 walk.Image 로 ImageView에 이미지를 출력시키고, 변경시키는 등의 짓이 가능합니다.

우선 ImageViewFromFile 함수는 생성하면서 이미지 파일까지 읽어다 지정을 해버리니 호출한 순간 필요한 처리는 끝나는 샘이 되겠구요.

그런이유로 예제는 ImageView->LoadImage->SetImage 과정을 통해 동적으로 파일을 읽어서 이미지를 설정, 변경하는 과정에 대한 예를 다뤘습니다.

	mgr.HSplit()

	idealIV := mgr.ImageView()
	idealIV.SetMode(walk.ImageViewModeIdeal)

	cornerIV := mgr.ImageView()
	cornerIV.SetMode(walk.ImageViewModeCorner)

	centerIV := mgr.ImageView()
	centerIV.SetMode(walk.ImageViewModeCenter)

	mgr.EndSplit()

	mgr.HSplit()
	shrinkIV := mgr.ImageView()
	shrinkIV.SetMode(walk.ImageViewModeShrink)

	zoomIV := mgr.ImageView()
	zoomIV.SetMode(walk.ImageViewModeZoom)

	stretchIV := mgr.ImageView()
	stretchIV.SetMode(walk.ImageViewModeStretch)
	mgr.EndSplit()

우선 ImageView() 함수로 각 위치에 ImageView를 생성해두고 각 이미지뷰의 출력 모드를 다르게 하여 생성하였습니다.

ImageView를 생성 뒤에 SetMode 라는 메소드를 사용하는데요.. 이게 이미지의 출력모드를 지정하는 겁니다.

const (
	ImageViewModeIdeal ImageViewMode = iota 
	ImageViewModeCorner						
	ImageViewModeCenter						
	ImageViewModeShrink						
	ImageViewModeZoom						
	ImageViewModeStretch					
)

지정가능한 값은 walk코드와 예제의 이미지 출력을 보시면 되겠습니다. 직접 원하는 이미지로 모드를 바꿔가며 출력해보시면 이해가 빠릅니다. ImageView보다 이미지가 크거나 이미지보다 ImageView가 큰 경우에 어떤식으로 이미지를 표시 할지를 모드변경을 통해 처리하시면 됩니다.

	updateImg := func(fname string) {
		// 새 이미지 로드전에 기존 이미지 해제 않하면
		// 메모리릭 발생 !! 주의
		if currImage != nil {
			(*currImage).Dispose()
		}
		currImage = LoadImage(fname)
		if currImage != nil {
			idealIV.SetImage(*currImage)
			cornerIV.SetImage(*currImage)
			centerIV.SetImage(*currImage)
			shrinkIV.SetImage(*currImage)
			zoomIV.SetImage(*currImage)
			stretchIV.SetImage(*currImage)
		}
	}
    
   	mgr.PushButton("피로곰", func() {
		updateImg("img0.png")
	})
	mgr.PushButton("환자1", func() {
		updateImg("img2.png")
	})
	mgr.PushButton("환자2", func() {
		updateImg("img1.png")
	})

전체 ImageView의 이미지를 변경 해주는 함수를 하나 작성 하고 이 함수를 각 버튼에 따라 다른 이미지를 읽어서 전체 ImageView의 이미지를 교체해 줍니다.

주석에도 써져 있듯이 .. 이 시점에서 walk.Image를 다루는데 가장 중요한 부분이 나옵니다.

동적으로 프로그램이 동작하는 중간에 ImaveView의 이미지를 교체하는 경우에 기존 이미지의 메모리 해제가 제대로 되지 않아서 메모리 릭이 발생합니다.

Go언어는 Managed언어이고 GC도 있고 메모리 관리는 알아서 해줄텐데 왜 메모리 릭이 발생하느냐? 라고 물으신다면 이 walk 라는 놈은 윈도 API의 랩퍼(Wrapper)입니다. 윈도API는 원래 Go언어를 위해 만들어진 API가 아닙니다. 그럼으로 윈도 API의 DLL들을 Go에서 사용가능하도록 동적으로 링크하여 사용하는 lxn/win 이라는 패키지를 walk 가 쓰고 있는거구요 그 walk를 제가 또 쓰고 있으니 ㅎㅎ 

결국 이 윈도API가 다뤄지는 내부에서는 흔히 윈도프로그래밍에서 말하는 DC니 HANDLE이니 HBITMAP이니 이런 것들을 walk 내부에서 다루고 있는겁니다. 한마디로 Go언어 단에서 다루는 walk.ImageView나 walk.Image객체들이야 Go에서 메모리 관리를 하면서 알아서 해제도 하고 GC에 넣네 마네 해주겠지만 이놈들이 내부에서 C로 작성된 DLL들의 것들을 가져와서 한 어떤 행위들은 알아서 정리되지 못합니다. 이는 Go의 영역을 벗어난 곳에서 일어난 일이기 때문이고 ..

일단 동적으로 이미지를 변경해가며 쓰는 경우엔 그런 이유로 .. 직접 객체를 정리를 해줘야 합니다.

그래서 

	var currImage *walk.Image

	defer func() {
		// 새 이미지 로드전에 기존 이미지 해제 않하면
		// 메모리릭 발생 !! 주의
		if currImage != nil {
			(*currImage).Dispose()
		}
	}()

상단에 currImage라고 변수하나를 선언 해두고 이놈으로 돌려 쓰고 해당 창이 종료될때에 Dispose()를 호출하여 이미지 객체가 내부에서 사용중인 윈도 리소스를 반환을 합니다.

	updateImg := func(fname string) {
		// 새 이미지 로드전에 기존 이미지 해제 않하면
		// 메모리릭 발생 !! 주의
		if currImage != nil {
			(*currImage).Dispose()
		}

updateImg 함수에서도 같은 짓을 하고 있습니다. 새로운 이미지를 로드하기 전에 기존에 읽었던 이미지는 Dispose()를 호출해서 리소스를 반환해 줘야 메모리릭이 발생하지 않습니다.

윈도API에서 이미지를 처리하는 방식은 일반적으로 순수비트맵으로 다루기 때문에 실제 이미지 파일의 크기에 비해서 메모리를 더 크게 차지할겁니다. 화면에 표시되는 픽셀을 압축해가면서 돌아갈 정도로 윈도API의 이미지 처리는 고도화 되지 못했거든요 ㅋㅋ..

메모리릭이 어떤식으로 발생하는지 체험하고 싶으시면 Dispose부분을 빼고 테스트 해보시는것도 ㅎㅎ 

ImageView 는 사용이 복잡한건 아닌데 .. 메모리릭 가능성만 잘 체크해 쓰시면 됩니다.

ImageView도 이벤트 처리도 가능합니다.

	imgView.MouseDown().Attach(func(x, y int, mouse walk.MouseButton) {
		if mouse == walk.LeftButton {

		} else if mouse == walk.RightButton {
        
        }
	})
	imgView.MouseMove().Attach(func(x, y int, button walk.MouseButton) {
		win.SetCursor(win.LoadCursor(0, win.MAKEINTRESOURCE(win.IDC_HAND)))
	})

이런식으로 MouseDown 이벤트에서 마우스 좌클릭이냐 우클릭이냐를 따지실 수 있고 MouseMove 이벤트에서 이미지 위에 마우스가 올라가는 경우에 커서를 손모양으로 변경하는 등의 짓도 가능합니다. 다른 아이콘으로도 가능하구요.. 단지 예제에서 쓰는 win.IDC_HAND의 경우 윈도 공용 리소스라 상관 없지만 직접 커스텀 아이콘 같은걸 사용하기 위해 icon 파일을 읽어서 뭔가 처리를 하는건 과정이 복잡하기도 하고 walk.Image 객체와 같이 Dispose 이슈도 생각 해야 해서 권장하진 않습니다.

이렇게 ImageView에 대한 간단한 예도 마무리 하겠습니다.

이상입니다.

p.s ImageView 부터는 GDI+를 사용하기 때문에 윈도에 VC++ 재배포가능 패키지가 단 1개라도 설치되서 존재해야 합니다. 

https://docs.microsoft.com/ko-KR/cpp/windows/latest-supported-vc-redist?view=msvc-170 

 

지원되는 최신 Visual C++ 재배포 가능 패키지 다운로드

이 문서에서는 최신 버전의 Visual C++ 재배포 가능 패키지 패키지에 대한 다운로드 링크를 나열합니다.

docs.microsoft.com

 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band