[윈폼] Gdi+를 사용해 보자
![[윈폼] Gdi+를 사용해 보자](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1644885011527%2FQLE0WxoWk.png&w=3840&q=75)
Search for a command to run...
![[윈폼] Gdi+를 사용해 보자](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1644885011527%2FQLE0WxoWk.png&w=3840&q=75)
No comments yet. Be the first to comment.
핵심 원칙: "한 번 빌드하고, 어디서든 실행한다 (Build once, run anywhere)" 도커의 가장 큰 장점은 환경 일관성입니다. 동일한 도커 이미지를 사용하여 개발, 테스트, 운영 환경을 구성함으로써 "제 PC에서는 됐는데..." 하는 문제를 최소화할 수 있습니다. 1. 개발 단계 (Development) 목표: 빠른 코드 변경 반영, 쉬운 디버깅, 실제 운영 환경과 유사한 환경 구성. Docker 사용 방안: Dockerf...
목록을 조회할 때 기본적으로 페이지, 정렬, 검색에 대한 처리를 해야 합니다. 이를 쉽게 하는 방법에 대해 소개 합니다. 페이지, 정렬, 검색을 쿼리 인자로 받게 하기 위해 SearchOptions을 만든 후, | SearchOptions.cs public class SearchOptions { public class SearchOptions { [FromQuery(Name = "pn")] [DefaultValue(0)] ...
EF Core는 제한적인 JSON 속성을 지원합니다. EF Core 8에 추가된 기본 형식 컬렉션으로 JSON 열에 매핑할 수 있습니다. public class PrimitiveCollections { public IEnumerable<int> Ints { get; set; } public ICollection<string> Strings { get; set; } public IList<DateOnly> Dates { ge...
DB에서 데이터를 삭제하면 일반적으로 복구할 수 없습니다. 또한 관계에 따라 영구 삭제 자체가 어려울 수도 있습니다. 그래서 데이터를 영구 삭제하는 대신 IsDeleted 속성을 true로 주고 IsDeleted 속성을 필터링해서 조회하는 방법을 사용하기도 합니다. 이를 소프트 삭제라고 합니다. 그런데 EF에서 알아서 데이터 삭제 시 소프트 삭제를 하고 쿼리시 IsDeleted 속성을 체크해서 삭제한 데이터를 제외한 데이터만 쿼리하게 하는 ...
EF Core를 사용하면서 문자열 길이 등의 특성을 일일이 지정하는 것은 번거롭습니다. ... [MaxLength(32)] public string? 제목 { get; set; } 엔터티가 한 개일 때는 상관이 없으나 제목 유형이 여러 엔터티에 사용될 경우 유형을 지정하기 번거롭습니다. 속성 유형을 도메인으로 관리하면 참 편할텐데요, ValueConverter를 이용할 수 있습니다. 그런데 이것을 인터페이스 정적 추상를 사용해서 다음처럼 ...
이번 글에서는 Windows Forms에서 GDI+를 자유롭게 이용할 수 있도록 사용법을 살펴 보도록 하겠습니다.
Windows 초창기 부터 그래픽 장치 인터페이스(GDI) API를 제공 했었는데요, Windows XP 운영체제부터 Windows GDI+를 사용할 수 있게 되었습니다. GDI+는 GDI에 비해 그라데이션 브러쉬 및 알파 블렌딩과 같은 새로운 기능이 추가되었고, 친숙한 개체지향 스타일로 각종 기능을 사용할 수 있게 되었습니다.
두가지 방식이 있습니다. Windows Forms의 화면에 보여지는 모든 컨트롤들은 OnPaintBackground()및 OnPaint() 메소드를 재정의 할 수 있습니다. 이때, e 인자의 Graphics 개체를 통해 GDI+ 기능을 이용할 수 있습니다.
두번째 방법은 CreateGraphics() 메소드를 통해 Graphics 개체를 생성한 후 사용하는 방법입니다.
아, 한가지 방법이 더 있군요. Bitmap 개체를 Graphics.FromImage() 메소드를 통해 Graphics를 획득해서 사용할 수 있습니다.
Graphics는 화면 출력 용으로만 사용하나요?아닙니다. GDI+는 장치 컨텍스트에 독립적입니다. 그렇기 때문에 프린트 출력을 할때에도 Graphics를 사용하게 됩니다.
먼저 .NET 5로 Windows Forms 응용 어플리케이션 템플릿으로 프로젝트를 생성한 후, 아무 컨트롤이나 폼에 배치해봅니다.

예시로 CheckBox를 생성해봤는데요, Windows Forms에서는 화면에 보이는 모든 컨트롤은 Control에서 상속받도록 되어 있습니다. 한번 추적해보죠.
public class CheckBox : ButtonBase {}
public abstract class ButtonBase : Control {}
OnPaintBackground()및 OnPaint() 메소드는 Control의 메소드 이므로, Windows Forms의 보여지는 모든 컨트롤은 그리기 기능이 존재하는 샘입니다.
그렇다면 Control에서 상속받아 구현을 해볼까요? 이름을 CanvasControl이라고 하겠습니다.
public class CanvasControl : Control
{
protected override void OnPaintBackground(PaintEventArgs pevent)
{
var g = pevent.Graphics;
g.Clear(Color.Black);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
}
}
폼 디자이너에서 컨트롤이 구분되도록 배경색을 검정색으로 칠하는 코드만 넣었습니다. 그런 다음 폼 디자이너에서 CanvasControl을 추가하면 다음처럼 보이게 됩니다.
실행화면1

컨트롤이 화면에 보여져야 할 때 먼저 OnPaintBackground()가 호출되어 배경을 그릴 수 있도록 하고, 그다음 OnPaint()가 호출되어 내용을 그릴 수 있게 되어 있습니다. 이런식으로 Windows Forms 컨트롤이 화면에 보여지게 되는거죠.
그런데 검정 배경화면만 있으니 BackColor에 검정 지정한 것 마냥 차이가 없습니다. 검정색 배경에 도형을 그려보도록 합시다.
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
var rect = ClientRectangle;
var clipRect = e.ClipRectangle;
// 선
g.DrawLine(Pens.Red, 50, 50, 100, 100);
// 면
g.DrawRectangle(Pens.Green, 10, 10, 30, 30);
// 원
g.DrawEllipse(Pens.Blue, 100, 100, 50, 50);
}
실행화면2

원하는대로 잘 그려지는군요. 그런데 한가지 궁금한 점이 생깁니다.
OnPaint()는 정확히 언제 호출될까요?Windows Forms는 나름 효과적으로 컨트롤을 그리도록 최적화 되어 있는데요, 화면에 새로 보여질 때만 호출된다는 점입니다. 다음의 코드로 이를 확인해봅시다.
private bool firstDraw = true;
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
var rect = ClientRectangle;
// 선
g.DrawLine(Pens.Red, 50, 50, 100, 100);
// 면
g.DrawRectangle(Pens.Green, 10, 10, 30, 30);
// 원
g.DrawEllipse(Pens.Blue, 100, 100, 50, 50);
if (firstDraw == false)
g.FillRectangle(Brushes.LightGray, rect);
firstDraw = false;
}
firstDraw에 의해 두번째 그리기부터는 전체 영역을 회색으로 그리도록 했는데요,
최초화면

폼의 사이즈를 작게 조절해서 다음과 같이 줄인 후,


신기합니다. 분명히 컨트롤 전체 영역에 갈색으로 칠해져야 할 텐데요, 다시 그려져야 할 영역만 칠하고 있습니다. OnPaint() 메소드가 호출될 때의 Graphics는 갱신되어야 할 영역만 그려지도록 클리핑(Clipping) 됨을 알 수 있습니다.
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
var rect = ClientRectangle;
g.FillEllipse(Brushes.Green, rect);
}
위와 같이 수정한 후 CanvasControl을 폼에 Dock.Fill 해 봅시다.
실행화면

ClientRectangle은 컨트롤의 그릴 수 있는 영역입니다. 예상했던 대로 녹색으로 가득 찬 원이 그려졌는데요, 사이즈를 한번 늘려봅시다.

원이 크기에 맞게 늘어나거나 줄어드는 예상과 달리 Windows Forms의 효율적 그리기로 인해서 이상하게 보입니다.
컨트롤의 사이즈가 변경되었을 때 전체를 다시 그려야 할 경우,
public CanvasControl()
{
ResizeRedraw = true;
}
이렇게 ResizedRedraw 속성에 true를 적용하면,

컨트롤의 사이즈가 변경됐을 때 전체 영역을 다시 그리게 됩니다.
Windows Forms에서 제공하는 GDI+ 기능은 대부분 Windows GDI+의 레퍼 기능이며, 컨트롤에서 생성하거나, 이미지에서 생성하거나, OnPaint() 인자로 받는 Graphics를 통해 GDI+ 기능을 사용할 수 있게 됩니다.
GDI+ 에서 제공하는 개체는 다음과 같습니다.
IDisposable 인터페이스가 구현된 개체는 반드시 사용이 완료되면 Dispose해야 하며 그 이유는 내부적으로 비관리 헨들을 사용해 운영체제의 자원이 사용되기 때문입니다.
GDI+는 그릴 항목을 회전, 크기 조정, 변환등을 할 수 있도록 좌표 변환 체계를 가지고 있습니다.
예제의 Draw()메소드에서 world 좌표 공간을 사용합니다. TranslateTransform()등의 Matrix 기능을 이용하면 이것을 Page 좌표로 변환할 수 있습니다. 즉, 좌표 (0,0)을 기준 (또는 사용자의 임의)에 의해 구성된 항목을 회전, 크기 조정, 위치 조정을 할 수 있게 됩니다.
protected override void OnPaint(PaintEventArgs e)
{
Sample3(e);
}
private void Sample3(PaintEventArgs e)
{
var g = e.Graphics;
var rect = ClientRectangle;
g.TranslateTransform(100, 50);
Draw(g, Font);
g.TranslateTransform(130, 130);
Draw(g, Font);
g.TranslateTransform(130, 130);
g.ScaleTransform(1.5f, 1.5f);
g.RotateTransform(45);
Draw(g, Font);
////
static void Draw(Graphics g, Font font)
{
using var glPen = new Pen(Color.White)
{
};
using var sPen = new Pen(Color.White);
g.DrawLine(glPen, 0, 0, 150, 0);
g.DrawLine(sPen, 50, -5, 50, 5);
g.DrawLine(sPen, 100, -5, 100, 5);
g.DrawString("50", font, Brushes.White, 41, -24);
g.DrawString("100", font, Brushes.White, 88, -24);
g.DrawString("x", font, Brushes.White, 160, -8);
g.DrawLine(glPen, 0, 0, 0, 100);
g.DrawLine(sPen, -5, 50, 5, 50);
g.DrawString("50", font, Brushes.White, -26, 42);
g.DrawString("y", font, Brushes.White, -6, 110);
g.DrawLine(Pens.Red, 0, 0, 160, 80);
}
}

위의 예제에서 소개되지 않은 기능을 포함한 관련 GDI+ 기능은 다음과 같습니다. 간단히 예제를 만들어 결과를 확인하면 어떻게 동작하는지를 알 수 있습니다.
화면 Graphics를 이용하면 즉각적으로 화면에 항목이 그려집니다. 그런데 항목을 이동해야 하는 등 전체 영역을 다시 그려야 할 경우 지우는 작업에 의해 화면이 깜빡이게 되는데요, 여기서는 Bitmap을 통해서 깜빡임을 없애는 방법에 대해서 설명하겠습니다.
private Bitmap buffer;
private Graphics bg;
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
if (buffer != null)
buffer.Dispose();
buffer = new Bitmap(ClientSize.Width, ClientSize.Height);
bg = Graphics.FromImage(buffer);
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{
}
protected override void OnPaint(PaintEventArgs e)
{
Sample4(e);
Invalidate();
}
private int x, y;
private void Sample4(PaintEventArgs e)
{
var g = e.Graphics;
x++;
if (x > ClientSize.Width)
{
x = 0;
y += 20;
}
bg.TranslateTransform(x, y);
bg.ScaleTransform(0.3f, 0.3f);
Draw(bg);
bg.ResetTransform();
g.DrawImage(buffer, 0, 0);
// ----
void Draw(Graphics g)
{
g.Clear(Color.Black);
g.FillEllipse(Brushes.Green, ClientRectangle);
}
}
컨트롤의 ClientSize만큼의 Bitmap을 생성한 후, 영역을 OnPaintBackground()에서 지우는 대신 Bitmap에서 획득한 Graphics를 지워줍니다. 그리기 작업 후, OnPaint()에서는 Bitmap을 DrawImage하는 것으로 메모리에서 작업한 항목을 그려서 깜빡임을 없앨 수 있게 되었습니다.