2010년 1월 6일 수요일

[.NET Compact Framework-C#] 화면캡쳐-CopyFromScreen

.NET Compact Framework 에서 생략된 유용한 그래픽스 함수 중 하나인 CopyFromScreen...
기본 컨트롤들은 투명처리가 되지 않기 때문에 완벽하게 커버할 수 있는건 아니지만 CopyFromScreen이 있다면 팝업되는 컨트롤들은 어느정도 보이기에는 커버할 수 있다. 하지만 아쉽게도 컴팩트 버전에서는 생략되었다.
직접 개발한것은 아니지만 아래 소스는 Win32 API를 이용하여 같은 작동을 하도록 구현하였다.
출처는 기억이 잘 나지 않는다.(-_-)

작동 원리는 Device Context를 얻어오는 Win32 API인 GetDC 함수에 윈도우 핸들을 NULL을 넣으면 전체 화면의 Device Context를 얻게 되는데 같은 Win32 API인 BitBlt로 사용자 비트맵의 DC로 카피를 하는 방법으로 구현하였다.

자세한 사항은 소스를 참고하기 바란다.


응용을 하자면 BitBlt의 좌표를 조절하여 부분 화면을 캡처할 수도 있으며 Win32 API중의 하나인 AlphaBlend 함수를 이용하여 배경의 명도를 조절하거나 반투명 처리가 된 이미지 추출도 가능하니 관심있는 개발자들은 시도해보길 바란다.

[.NET Compact Framework-C#] Control Double Buffering

컨트롤 커스터마이징을 하다 보면 요구사항에 따라 Flicker와 같이 부드럽게 동적으로 움직이는 컨트롤이 필요할 때가 있다. 컨트롤 클래스를 상속받아 열심히 오너 드로잉을 해 보면 알겠지만 Invalidate를 이용하여 다시 그리기를 시도하면 컨트롤이 아주 작지 않는 이상 깜박 거림을 볼 수 있을 것이다. 이를 방지하기 위해서 타이머에서 그래픽스 객체를 직접 얻어다가 그려주는 방식을 사용할 수도 있겠지만 보편적인 Invalidate를 사용하는 방법을 소개해보고자 한다.
결론 부터 말하자면 수동적인(?) 더블 버퍼링을 사용하는 방법인데 다음과 같이 정리할 수 있다.

1. 컨트롤 크기와 같은 이미지를 생성한다.
2. 이미지의 그래픽스 객체를 얻어 OnPaint 나 OnPaintBackground 내의 그리기 루틴을 이미지에 그린다.
3. 컨트롤 자체의 그래픽스 객체에 이미지를 뿌려준다.

자세한 내용은 아래 소스를 참고하면 되지만 포인트 별 부연설명을 하자면 다음과 같다.


1. 컨트롤 사이즈 변경(OnResize)에서 컨트롤 크기와 같은 이미지를 생성한다. 이 때 screenBuffer, graphics, paintEvent는 클래스 멤버이다.

private Bitmap screenBuffer;
private Graphics graphics;
private PaintEventArgs paintEvent;

protected override void OnResize(EventArgs e)
{
base.OnResize(e);

if (graphics != null)
{
graphics.Dispose();
}

if (screenBuffer != null)
{
screenBuffer.Dispose();
}

screenBuffer = new Bitmap(Width, Height);
graphics = Graphics.FromImage(screenBuffer);
paintEvent = new PaintEventArgs(graphics, ClientRectangle);
}


2. OnPaint, OnPaintBackground 를 오버라이딩 하여 그리기 중복 방지를 위하여 하위클래스에서 상속 받을 수 없게 한다.

protected override sealed void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
}

protected override sealed void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground(e);
}


3. OnDraw 메소드를 가상함수로 정의하고 컨트롤 그리기의 공통행위(?)를 구현한다. 예제 소스에서는 BackColor로 화면을 클리어 하는 루틴을 구현해 넣었다.

protected virtual void OnDraw(PaintEventArgs e)
{
e.Graphics.Clear(BackColor);
}


4. OnPaint, OnPaintBackground 에서 base 호출을 주석처리하고 둘 중 한군데에 screenBuffer를 뿌리게 한다. 예제 소스에서는 OnPaintBackground에서 뿌리게 하였다.

protected override sealed void OnPaint(PaintEventArgs e)
{
//base.OnPaint(e);
}

protected override sealed void OnPaintBackground(PaintEventArgs e)
{
//base.OnPaintBackground(e);
if (this.graphics != null && screenBuffer != null)
{
OnDraw(paintEvent);
e.Graphics.DrawImage(screenBuffer, 0, 0);
}
}


5. 컨트롤을 더 이상 사용하지 않을 경우를 생각해서 다음과 같이 Dispose 함수를 오버라이딩 하여 구현한다.

protected override void Dispose(bool disposing)
{
if (disposing)
{
if (graphics != null)
{
graphics.Dispose();
}

if (screenBuffer != null)
{
screenBuffer.Dispose();
}
}
base.Dispose(disposing);
}

이 방법을 사용하면 디자이너에서도 사용할 수 있으므로 디자이너에서 컨트롤 배치를 하는 개발자라면 유용하게 쓰일 듯 하다.

2010년 1월 5일 화요일

[.NET Compact Framework-C#] 리소스 이미지 속도개선

프로젝트를 진행하다 보면 .NET Compact Framework 에서 기본적으로 제공되는 컨트롤들 만으로는 고객의 입맛을 맞출 수 없다. 그러다 보면 이미지를 처 바르는(-_-) 행태를 보이게 되는데 사용자에 따라 파일로 된 이미지를 동적으로 로딩하던지 아니면 리소스에 추가하여 사용하던지 할 것이다.

이번 프로젝트에서는 후자를 사용했는데(까놓고 얘기하자면 사용하기 편하다는 이유 밖에 없지만) 이미지가 조금만 많아져도 로딩이 너무 느려지는 현상이 있다. 게다가 리소스 디자이너에서 자동 생성하는 코드가 static 으로 되어 있어 한번 로딩하면 메모리상에서 사라지지도 않는다.

메모리에서 사라지지도 않는데 느리다니... 이는 정말 어처구니 없는 리소스 관리자인듯... 리소스 디자이너를 손 대봤자 Visual Studio가 원상복귀 할것이 뻔하고... 메모리에서 사라지지 못하게 하는건 어쩔 수 없다 치더라도 로딩 속도를 줄여야겠다고 생각했다.

방법은 간단하다. 리소스를 사용하는 클래스에서 이미지 이름으로 생성되는 함수를 static image 멤버에 미리 할당하고 사용하면 static 로딩시에는 같은 속도를 내겠지만 리소스의 데이터를 해석해서 Image 로 만드는 과정을 반복하지 않아 재 사용 시 속도를 줄일 수 있다. 아래는 기존의 느린 방법과 새로운 개선된 방법에 대한 예시이다.

1. 기존 방법

public class Test
{
private Image image;
public Test()
{
image = Test.Properties.Resources.test_image;
}
}


2. 개선된 방법

public class Test
{
private static Image text_image = Test.Properties.Resources.test_image;
private Image image;
public Test()
{
image = text_image;
}
}


리소스 이미지를 사용하고 반복 로딩을 많이 할 경우 위와 같은 방법을 사용하면 확실히 속도 개선을 볼 것이다.

덧붙여 말하자면 조금 어처구니 없는것은... 디자이너에서 바로 이미지를 삽입하면 리소스에서 바로 읽어오는 형태(1번과 같은 방법으로)로 코드가 자동완성되는데... MS는 이런걸 알고도 리소스 매니저를 만든것인지...

[.NET Compact Framework-C#] Native DLL(Marshaling) 사용 시 주의할 점

.NET Compact Framework 을 사용하여 프로젝트를 진행하는 사람이라면 느끼겠지만... 너무 느리고... 너무 부족하다... 필자도 이번 프로젝트를 진행하면서 JAVA와의 코드 전환을 용이하게 하려고 .NET Compact Framework 을 이용했지만 몇달전 쯤 부터 "C/C++ 로 개발할 것을..." 하는 후회를 밥먹듯이 하고 있다.

여튼... 앞에 너무 느리고 너무 부족하다는 말을 했는데... 이번에는 너무 부족한 부분에 대해 얘기해 볼까 한다.  그냥 PC에서 돌아가는 .NET Framework은 그래도 어느 정도 API가 있어서 쓸만할지 모르지만 .NET Compact Framework 에서 먼가를 하려고 하면 VS의 자동완성 UI에서 함수를 찾을 수 없는 경우가 부지기수다. MSDN을 찾아보면 결국 돌아오는 답은...

".NET Framework에서 모든 플랫폼의 모든 버전을 지원하지는 않습니다. 지원되는 버전의 목록은 시스템 요구 사항을 참조하십시오."

결국 대부분의 C# 개발자들은 이 부족함을 극복하기 위하여 Native DLL을 사용하게 된다.  이 때 능숙하지 않는 개발자라면 데이터 타입을 어떻게 맞춰야 하는지... 즉 마샬링을 어떻게 해야 하는지에 대한 문제에 고민을 하게 된다. (여기서 능숙하다는 표현은 프로그래밍 언어에 대한 전반적인 이해가 아니라 마샬링에 대해 능숙함을 나타낸다.) 이에 대해 구글링을 해보고... 책도 찾아보고... 난이도가 높은 구조체 마샬링도 해보고... 하지만... 불규칙적으로 Native Exception(0x80000002)을 토해내며 죽는 어플리케이션...

마샬링에 대해 정통한 사람이 이 글을 읽으면 비웃겠지만... 필자는 아래와 같은 방법을 사용하여 오류를 피해갔다.

예를 들어 다음과 같은 C 코드로 abc.dll 을 만들었다고 가정한다.

typedef struct ABC
{
int a;
int b;
int c;
} ABC;

ABC* g_abc = NULL;

void SetABC(ABC* abc)
{
g_abc = abc;
}

void SetA(int a)
{
if (!g_abc)
{
return;
}
g_abc->a = a;
}

void SetB(int b)
{
if (!g_abc)
{
return;
}
g_abc->b = b;
}

void SetC(int c)
{
if (!g_abc)
{
return;
}
g_abc->c = c;
}

int GetA()
{
if (!g_abc)
{
return 0;
}
return g_abc->a;
}

int GetB()
{
if (!g_abc)
{
return 0;
}
return g_abc->b;
}

int GetC()
{
if (!g_abc)
{
return 0;
}
return g_abc->c;
}


abc.dll 을 다음과 같이 C#에서 사용한다.

[DllImport("abc.dll")]
private static extern void SetABC(IntPtr pABC);
[DllImport("abc.dll")]
private static extern void SetA(int a);
[DllImport("abc.dll")]
private static extern int GetA();
...
IntPtr pAbc = Marshal.AllocHGlobal(12);
// 관리되지 않는 메모리 사용구간: 시작
SetABC(pABC);
SetA(10);
int a = GetA();
...
// 관리되지 않는 메모리 사용구간: 끝
Marshal
.FreeHGlobal(pABC);

위 코드에서 일반적인 사용방법과 다른 부분이 있다면 구조체나 버퍼를 할당할 필요가 있을 때 Marshal.AllocHGlobal(int)를 사용하고 해제할 때 Marshal.FreeHGlobal(IntPtr)을 사용했다는 점이다. 메모리 할당 시에 12를 사용한것은 C에서 sizeof(ABC) 하면 나오는 값이다.

원리를 살펴보자면... C#에서 사용하는 관리되지 않는 메모리와 관리되는 메모리의 차이점을 이용한건데...
관리되지 않는 Native DLL내의 함수에 관리되는 메모리 영역의 버퍼를 넣었을 경우 소스에서 명시된 "관리되지 않는 메모리 사용구간"이 수행되는 동안 다른 사용자 쓰레드에 의해 가비지 컬렉션등의 메모리 관리가 이루어졌을 경우 문제가 생길 여지가 있다는 것이다.

위 소스로 해당 문제가 발생하는지는 사실 확인을 하지 못했다. 하지만 공개는 할 수 없고... 유사한 구조를 가진 소스에서 문제가 발생하는 것은 확인하였고 위 방법으로 해결하였다.

1차적으로 Native DLL을 위와 같이 만들면 안되겠지만... 다른 사람이 만들어 배포된것을 사용할 수 밖에 없을 때 마샬링이 찜찜할 경우 위와 같이 사용하면 대부분의 문제는 피해갈 수 있을 것으로 생각된다.

먼가 설명이 조금 허접한 것 같은데 글이 잘못되었거나 이해가 가지 않으면 태클 환영함...-_-