개요
더블 버퍼링은 화면의 깜빡임과 잔상을 줄이기 위해 사용되는 기술입니다.
기본적으로 WinForm은 한 번에 한 개의 화면을 그립니다.
따라서 화면을 다시 그리는 동안에는 이전에 그려진 내용이 보이는 문제가 발생할 수 있습니다.
이를 해결하기 위해 더블 버퍼링을 사용하여 백 버퍼에 그린 다음
그림이 완전히 그려진 후에 한 번에 화면에 표시하는 방식을 채택합니다.
WinForms 컨트롤은 기본적으로 더블 버퍼링을 지원하지 않습니다.
그래서 다음 MSDN 링크에서 설명하는 것처럼 별도 추가 작업이 필요합니다.
Using Double Buffering – Windows Forms .NET Framework | Microsoft Learn
기존 컨트롤의 조합 만으로 구현이 가능한 경우는 How to: Reduce Graphics Flicker with Double Buffering for Forms and Controls – Windows Forms .NET Framework | Microsoft Learn 에서 설명하는 것처럼 비교적 간단하게 처리가 가능합니다.
하지만, 직접 그리기 작업을 구현하는 경우는 How to: Manually Render Buffered Graphics – Windows Forms .NET Framework | Microsoft Learn 에서 설명하는 것처럼 조금 더 추가적인 작업이 필요합니다.
이번 포스팅에서는 직접 그리기 작업을 구현하는 경우 쉽게 재사용이 가능하게 하기 위해 Library Class 를 별도로 구현해서 적용해 보겠습니다.
Library Class(BufferedRender.cs)
소스 코드는 다음과 같습니다.
생성자(BufferedRender(Control control))에서 해당 유저 컨트롤의 인스턴스를 파라메터로 지정하며
컨트롤의 사이즈가 변경되는 경우 해당 사이즈에 맞게 버퍼를 초기화 하기 위한 메서드(InitDefault())가 있습니다.
그리고, 백 버퍼에 먼저 드로잉을 한 다음 컨트롤에 한번에 렌더링하는 메서드(Render(Action actDraw, Graphics g))가 있습니다.
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace WinformApp48
{
public class BufferedRender : IDisposable
{
BufferedGraphicsContext GraphicManager;
BufferedGraphics ManagedBackBuffer;
public Control Current { get;private set; }
BufferedRender() {
GraphicManager = BufferedGraphicsManager.Current;
}
public BufferedRender(Control control) : this()
{
Current = control;
}
public void InitDefault()
{
GraphicManager.MaximumBuffer = new Size(Current.Width + 1, Current.Height + 1);
ManagedBackBuffer?.Dispose();
if (Current.ClientSize == Size.Empty)
{
ManagedBackBuffer = GraphicManager.Allocate(Current.CreateGraphics(), new Rectangle(Point.Empty, new Size(1, 1)));
}
else
{
ManagedBackBuffer = GraphicManager.Allocate(Current.CreateGraphics(), Current.ClientRectangle);
}
}
public void Render(Action<Graphics> actDraw, Graphics g)
{
actDraw?.Invoke(ManagedBackBuffer.Graphics);
ManagedBackBuffer?.Render(g);
}
public void Dispose()
{
ManagedBackBuffer?.Dispose();
}
}
}
직접 그리기 작업을 구현한 컨트롤(DateDisplayBuffered.cs)
최상단의 전처리문에서 조건부 컴파일 상수를 지정합니다.
해당 상수로 지정된 경우 더블 버퍼링이 적용이 되며
해당 상수의 이름이 변경되면 더블 버퍼링이 적용되지 않습니다.
(예 : #define UseBufferedRender_ )
DateDisplay.cs 파일은 동일한 소스코드 이지만 컴파일 상수만 다르게 지정된 파일입니다.
1개의 폼에서 런타임에 동시에 비교하기 위해 단순히 별도 컨트롤로 구분하였습니다.
코드 차이점을 쉽게 설명하기 위해 컴파일 상수를 사용하였으며, 컴파일 상수가 사용된 부분을 보면 Libary 를 어떻게 적용할 수 있는지 확인이 가능합니다.
#define UseBufferedRender
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace WinformApp48
{
public class DateDisplayBuffered : Control
{
Timer timer;
#if UseBufferedRender
BufferedRender bufferedRender = null;
#endif
Font font = SystemFonts.DefaultFont;
public DateDisplayBuffered()
{
timer = new Timer();
timer.Interval = 300;
timer.Enabled = true;
timer.Tick += (s, e) =>
{
this.Invalidate();
};
#if UseBufferedRender
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
bufferedRender = new BufferedRender(this);
bufferedRender?.InitDefault();
#endif
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
#if UseBufferedRender
bufferedRender?.Dispose();
#endif
font?.Dispose();
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
#if UseBufferedRender
bufferedRender?.InitDefault();
#endif
this.Refresh();
}
protected override void OnPaint(PaintEventArgs e)
{
try
{
#if UseBufferedRender
bufferedRender?.Render(DrawReal, e.Graphics);
#else
DrawReal(e.Graphics);
#endif
}
catch (Exception Exp)
{
Debug.WriteLine(Exp.Message);
}
}
Color GetColorByDay(DateTime dt)
{
return dt.DayOfWeek == DayOfWeek.Saturday ? Color.Blue : (dt.DayOfWeek == DayOfWeek.Sunday ? Color.Red : Color.Black);
}
private void DrawReal(Graphics g)
{
g.SetClip(new Region(this.GetVisibleRect()), CombineMode.Replace);
g.Clear(Parent.BackColor);
DateTime dtStart = DateTime.Now;
DateTime dtEnd = DateTime.Now.AddYears(1);
int iPadding = 4;
int iDateGap = 5;
int iTotDays = (dtEnd - dtStart).Days;
DateTime dtCurrent = DateTime.Now;
Color dayColor = Color.Black;
Point p1 = new Point(0, 0);
Point p2 = new Point(0, 0);
int top1 = 50;
int top2 = 53;
int bottom = 60;
int left = 0;
g.DrawString($"Now : {DateTime.Now.ToString("mm:ss.ffff")}", font, Brushes.Black, new PointF(1, 1));
for (int k = 0; k <= iTotDays; k++)
{
dtCurrent = dtStart.AddDays(k);
dayColor = GetColorByDay(dtCurrent);
left = iPadding + k * iDateGap;
if (dtCurrent.Day % 5 == 0)
{
p1 = new Point(left, top1);
p2 = new Point(left, bottom);
g.DrawLine(new Pen(dayColor), p1, p2);
g.DrawString(dtCurrent.Day.ToString(), font, new SolidBrush(dayColor), new PointF(left, top1 - 20));
}
else
{
p1 = new Point(left, top2);
p2 = new Point(left, bottom);
g.DrawLine(new Pen(dayColor), p1, p2);
}
if (dtCurrent.Day == 1)
{
g.DrawLine(Pens.DarkGreen, new PointF(left, 15), new PointF(left, bottom));
g.DrawString($"{dtCurrent.Year}-{dtCurrent.Month:00}", font, Brushes.Black, new PointF(left, 15));
}
}
}
}
}
Youtube
영상으로도 확인하실 수 있습니다.
해당 영상은 아래 Github 에서 다운로드한 소스코드를 실행한 영상입니다.
Github
해당 예제의 소스 코드는 다음 Github repository 를 참조하시면 됩니다.
0 Comments