c# winform gdi+ 더블버퍼링 라이브러리

May 24, 2023 | 0 comments

개요

더블 버퍼링은 화면의 깜빡임과 잔상을 줄이기 위해 사용되는 기술입니다.
기본적으로 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 를 참조하시면 됩니다.

Learn more on this topic

Related Blog Posts

Join in the conversation

Leave a Comment

0 Comments

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

무료 온라인 전광판

전광판

텍스트를 입력하고 텍스트 효과 및 배경효과 를 변경해서 전체화면으로 표시할 수 있는 전광판 용도로 사용하실 수 있습니다. 각종 스포츠 및 공연 관람시 응원 용도로 사용이 가능합니다.

Carousel

여러개의 슬라이드를 추가하여 프레젠테이션 및 이미지 슬라이드 용도로 사용하실 수 있습니다. 브라우저가 포함된 IT 기기로 큰 모니터에 연결하여 매장 내 공지사항 및 메뉴소개를 이미지로 표시할 수 있습니다.

Pin It on Pinterest

Shares
Share This

Share This

Share this post with your friends!

Shares