점 찍기

18장에서 GDI+ 도형을 출력하는 방법에 대해 알아 보았는데 GDI+ 다양한 도형을 여러 가지 방법으로 그릴 있는 최신의 고성능 그래픽 엔진임이 분명하다. 그런데 도무지 이해가 가지 않는 면이 있는데 가장 기본적인 그래픽 요소라고 있는 점을 찍는 메서드가 없다. 하나를 찍어야 일이 흔하지는 않지만 그래도 명실공히 점은 그래픽을 구성하는 최소 단위가 아닌가?

점을 찍는 메소드가 제외된 원인에 대해서 여러 모로 조사해 보았으나 공식적인 문서에서는 해답을 구하지 못했다. 그래서 나름대로 추측해 보았는데 아마 GDI+ 그래픽 공간이 실수 좌표 체계를 쓰기 때문이 아닌가 하고 생각된다. 좌표 변환에 의해 확대, 축소가 가능하다 보니 1픽셀짜리 고정된 크기의 점은 문제가 수도 있다. 점이라는 위치 정보만 있을 뿐이니 확대해도 점일 뿐이고 축소한다고 해서 사라지는 것도 이상하니까 말이다. 그러나 이런 식이라면 선분도 마찬가지 문제가 있어 일관성이 없어 보인다. 이것은 하나의 추측일 뿐이며 정확한 이유는 아직 모르는 상태이다.

그러나 제공되지 않는다 하여 점을 찍으라는 법은 없다. 비트맵에 점을 찍는 메서드가 제공되므로 비트맵에 일단 찍은 화면으로 비트맵을 전송하는 방법을 생각해 있는데 이건 아무래도 너무 느릴 같다. 그래서 API 함수로도 호출해 보고 궁리 끝에 점을 찍는 메서드를 직접 만들어 보기도 했다. 좌표와 색상을 인수로 받아 아주 짧은 직선을 하나 그리면 점을 찍을 있을 것이다. 다음 예제는 점으로 사인 곡선을 그린다.

 

: SetPixel

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Windows.Forms;

using System.Runtime.InteropServices;

 

namespace SetPixel

{

      public partial class Form1 : Form

      {

             [DllImport("Gdi32.dll")]

             public static extern bool SetPixelV(IntPtr hdc, int x, int y, uint color);

             [DllImport("Gdi32.dll")]

             public static extern uint GetPixel(IntPtr hdc, int x, int y);

 

             public Form1()

             {

                   InitializeComponent();

             }

 

             public void SetPixel(Graphics g, int x, int y, Color c)

             {

                   /* API 함수 호출

                   IntPtr hdc;

                   hdc = g.GetHdc();

                   SetPixelV(hdc, x, y, (uint)(c.R | c.G << 8 | c.B << 16));

                   g.ReleaseHdc(hdc);

                   //*/

 

                   //* 짧은 선으로 긋기

                   Pen P = new Pen(c);

                   g.DrawLine(P, x, y, x + 0.1f, y + 0.1f);

                   //*/

             }

 

             public Color GetPixel(Graphics g, int x, int y)

             {

                   int c;

                   IntPtr hdc;

                   hdc = g.GetHdc();

                   c = (int)GetPixel(hdc, x, y);

                   g.ReleaseHdc(hdc);

                   return Color.FromArgb(255, c & 0xff, c >> 8 & 0xff, c >> 16 & 0xff);

             }

 

             private void Form1_Paint(object sender, PaintEventArgs e)

             {

                   int x, y;

                   for (x = 0; x < 1000; x += 3)

                   {

                         y = (int)(Math.Sin(x * Math.PI / 180) * 50) + 100;

                         SetPixel(e.Graphics, x / 3, y, Color.Blue);

                   }

                   e.Graphics.FillRectangle(Brushes.Red, 10, 170, 80, 80);

                   e.Graphics.FillRectangle(Brushes.Green, 100, 170, 80, 80);

             }

 

             private void Form1_MouseDown(object sender, MouseEventArgs e)

             {

                   Color c;

                   Graphics g = CreateGraphics();

                   c = GetPixel(g, e.X, e.Y);

                   g.FillRectangle(new SolidBrush(c), 200, 170, 80, 80);

                   g.Dispose();

             }

      }

}

 

예제처럼 복잡한 수식으로 도형을 그린다거나 물체의 이동 궤적을 표현하고 싶을 때는 점찍기 메서드가 필요하다. 실행해 보면 사인 곡선이 그려진다. 아래쪽의 사각형은 색상 검출을 위해 출력해 것이다.

점을 출력하는 번째 방법은 Win32 SetPixel 함수를 호출하는 것이다. 운영체제가 제공하는 함수이니 정확하게 동작한다. 그러나 관리 코드에서 API 함수를 호출하는 것은 여러 가지 문제가 있어 결코 쉬운 일이 아니다. 먼저 SetPixel 요구하는 DC 핸들을 구해야 하는데 다행히 Graphics 핸들 조사 메서드가 제공된다. , 이렇게 구한 핸들은 관리 대상이 아니므로 직접 해제해야 하는 번거로운 면이 있다.

번째 문제는 닷넷의 Color 타입과 Win32 COLORREF 타입이 호환되지 않는다는 점이다. 크기는 32비트지만 하나는 부호가 있고 하나는 부호가 없으며 알파의 표현 여부가 다르다. 게다가 RGB 순서까지도 반대로 되어 있어 단순한 캐스팅으로는 변환할 없고 비트 연산으로 밀고 합치고 해야 한다. 결과 코드를 보면 그다지 복잡하지는 않지만 그렇다고 해서 그리 간단한 것도 아니다.

예제의 SetPixel 메소드는 모든 과정을 내부에서 알아서 수행하므로 필요할 호출하기만 하면 된다. 메소드는 내부적으로 SetPixelV 함수를 호출하는데 SetPixelV SetPixel 비해 이전 색상값을 리턴하지 않으므로 속도가 조금 빠른 차이가 있을 점을 찍는 기능은 동일하다. API 함수를 호출하면 이식성에도 불리하고 아무래도 성능상의 불이익이 있으므로 자체적으로 해결하는 다른 방법도 시도해 보았다.

메서드의 모양이 참으로 기괴하다고 느낄텐데 이는 GDI+ 특이한 좌표 해석 방법에 기인한다. GDI+ 출력 메서드는 Win32 그래픽 함수들과는 달리 좌표를 포함하는 특성을 보인다. 그래서 (x,y)에서 (x+1,y) 선을 그으면 개가 찍힌다. 그렇다고 해서 (x,y)에서 (x,y) 선을 그으면 시작과 끝이 같아 아무 것도 그려지지 않는데 좌표를 포함하는 것도 이상하고 좌표 해석의 일관성도 없다. 사각형이나 타원을 그려 봐도 마찬가지로 4픽셀이거나 아니면 아예 그려지지 않았다. 그래서 부득이하게 0.1 더하는 꽁수를 사용했는데 이렇게 하면 시작 좌표와 좌표가 일치하지는 않으므로 픽셀짜리 점이 제대로 찍힌다. 좌표 공간이 실수이므로 이런 꽁수가 통하되 다만 이상 확대해 버리면 점이 선으로 나타날 수도 있다.

다음은 점을 조사하는 방법에 대해 생각해 보자. Graphics 점을 찍는 메소드가 없으니 점을 조사하는 함수도 당연히 없다. 닷넷이 제공하는 조사 메소드는 비트맵 클래스에 있으므로 점을 조사하려면 현재 화면을 비트맵으로 떠서 조사해야 한다. 그러나 하나의 색상을 얻기 위해 비트맵을 뜬다는 것은 말도 안되는 소리이므로 경우는 GetPixel API 함수를 호출하는 것이 가장 효율적이다. GetPixel 함수로 색상을 얻은 Color 구조체 타입으로 바꾸어 리턴하도록 만들었다. 예제를 실행해 놓고 마우스로 클릭하면 클릭한 지점의 색상을 조사하여 오른쪽에 사각형을 하나 그린다.

클래스 라이브러리가 기능을 제공하지 않으므로 잔머리를 굴려서 어떻게 하든 점을 찍고 조사하는데 성공했지만 방법보다는 나은 방법이 존재할 것으로 생각된다. 점을 찍는 메서드가 없는지, 아니면 다른 좋은 방법이 있는지 솔직히 모르겠다. 아뭏든 머리로는 방법 이상은 생각나지 않는다. 혹시 좋은 방법이나 공식적이고도 효율적인 방법을 발견했다면 가르쳐 주기 바란다.