Windows Forms에서 캔들 차트를 만들어보자 - slog

.NET 5의 Windows Forms은 .NET Framework 때와 많이 보완 된 것으로 보입니다. 닷넷데브에서도 한번 소개된 적이 있는데요,

Windows Forms has always been known as a managed wrapper around the Win32 API set. As such, Windows Forms has always depended heavily on an interop layer to communicate with the unmanaged Windows components. The top priority from the early days of .NET Core has been the optimising of our interop layer, making structs blittable, explicitly opting for more efficient “W”-functions, and using “unsafe” code where possible. All these changes are what we call “peanut butter changes”, in a sense that each of these are tiny and hardly observable, but over a lifetime of an application these changes add up to a substantial performance gains.

Windows Forms 개발을 주로 하시는 분들도 .NET 5로 넘어오시는게 이롭다는 것을 이 문서만 봐도 대략 느끼실 수 있을 것 같습니다.

이 문서의 내용 중 흥미로운 점은 GDI+ 를 GDI로 부분 교체해서 성능 향상을 얻었다는 내용입니다. 그래서 성능 또는 동작성에 대한 어떤 차이가 생겼는지 차트를 만들면서 비교해 볼 생각입니다.

좋아요 2

Windows Forms는 컨트롤들이 어느정도 많아지면 느려지는 단점이 있는데요, 이게 먼저 궁금해서 테스트한 결과 약간 빨라졌다 정도의 차이로 큰 속도향상은 없어 아쉬웠습니다.

어쨌든 약간 빨라졌다는데 위안을 삼아 계속 진행해보겠습니다.

더불어서 최종적으로 d2dlib도 써볼 생각입니다

좋아요 1

먼저 캔들스틱차트가 어떻게 구성되어 있는지를 분석해야 합니다. 캔들차트도 다양한 종류가 있지만 우리나라에서 사용하는 대표적인 모양을 구현하는것으로 합니다.

image

X-Y 축의 Y축은 가격, X축은 시간으로 이루어진 차트입니다. 그리고 강 봉의 의미는 다음과 같습니다.

image

하나의 봉은 시(Open),고(High),저(Low),종(Close)가로 이루어져 있습니다.

자, 이제 대략적인 형태는 파악되었고 봉의 굵기라던가 색이라던가 그런 세세한 속성들은 차치하고, 값과 표현에만 집중해서 구현해봅시다.

좋아요 2

먼저 값입니다. 값은 바로 앞전에서 분석한대로 시고저종 입니다. 4가지의 값이 있으면 하나의 봉을 표현할 수 있는데요, 가운데 봉의 색은 Close > Open일 때 녹색, Close < Open일 때 빨강색으로 표현하면 됩니다.
값의 다양한 형식을 수용하고 싶은데요, 하나의 봉의 구조는 그래서 다음과 같이 정의 합니다.

public interface IQuote<T>
{
    T Open { get; }
    T High { get; }
    T Low { get; }
    T Close { get; }
}

자 그럼 표현은 이렇게 정의했으니, 위의 하나의 봉을 어떻게 표현할까를 생각해 봅시다.
우리가 바라보고있는 모니터는 픽셀로 이루어진 해상도를 지니고 있습니다. 즉, Y의 단위는 픽셀, X의 단위도 픽셀입니다. 요즘 노트북을 사용하고 있는 대부분의 해상도는 1920x1080일 텐데요, Y축이 1080개의 픽셀로, X축이 1920개의 픽셀로 이루어졌다는 의미입니다.

그런데, 캔들스틱차트의 좌표 단위는 어떨까요? Y축은 가격, X축은 시간입니다. 즉,
(시간,가격) 좌표계를 (픽셀, 픽셀)로 변환해야 합니다.

시간과 가격은 픽셀로 변환할 수 있는 근거가 없습니다. 약속이 필요하는데요, 이런 식입니다.

  • 값의 (X, Y)축의 최대, 최소 값을 구한 후 변환해야 할 화면 (X, Y) 최대, 최소(0)에 맞게 비율로 조정한다.
  • 화면 (X, Y) 좌표계의 최대, 최소(0)를 구한 후 변환비를 통해 값를 조정한다.

이때, (X, Y)를 같은 1:1비율로 조정할 것이냐, 화면에 맞게 비율비를 다르게 할것이냐를 결정하면 원하는 최종 픽셀로 이루어진 좌표 단위로 변환할 수 있게 됩니다.

좋아요 2

값에 관련해서, 구조체를 써야만 Span의 대상이 됩니다. 시간이 된다면, MemoryMapedFile를 이용해서 차트로 값을 표현하는 것도 해볼 텐데요, 구조체를 사용할 때 주의할 점이 있습니다.

인터페이스를 가진 C# 구조체 설계시 주의할 점 - C# 프로그래밍 배우기 (Learn C# Programming) (csharpstudy.com)

좋아요 2

사용자 컨트롤을 만들 때 PictureBox 또는 UserControl을 쓰는데요, 이벤트 방식으로 처리하거나 컨트롤을 조합하는게 아니라면 그냥 Control을 상속받아 구현하는게 좋습니다.

    public partial class CanclestickChartControl : Control
    {
        /// <summary>
        /// 배경을 OnPaintBackground에서 그리지 않습니다.
        /// </summary>
        /// <param name="pevent"></param>
        protected override void OnPaintBackground(PaintEventArgs pevent)
        {
        }

        protected override void OnPaint(PaintEventArgs e)
        {
        }
    }
좋아요 1

배경을 OnPaint에서 그렸습니다.

        protected override void OnPaint(PaintEventArgs e)
        {
            var g = e.Graphics;
            var clip = e.ClipRectangle;
            var rec = ClientRectangle;

            // 검정색 배경을 그립니다.
            g.FillRectangle(Brushes.Black, rec);
        }

컴파일을 하면 MainForm에 이미 적용된 CandlestickChartControl에도 반영이 됩니다.

image

실행하면 다음과 같이 보입니다.

image

좋아요 1

GDI+를 사용할 때는 주의점이 있습니다. Brush 등 GDI+ 개체를 생성하게 되면 핸들 등 운영체제 자원을 사용하게 되는데, 필요 없을 때는 반드시 명시적 해제를 해줘야만 합니다. 그렇지 않으면 해제 시점이 GC 시점으로 미뤄지게 됩니다.

좋아요 1

캔들스틱차트의 경우 컨트롤의 사이즈에 따라 바가 작아지거나 커지거나 하는것 보다는 스크롤바가 생겨서 이동하는게 좀 더 자연스러울 것 같습니다.

그렇게 하려면 Control 대신 ScrollableControl을 상속받가 구현하면 편한데요, ScrollableControl가 스크롤에 관련된 처리를 이미 하기 때문입니다.
스크롤에 관련된 처리는

AutoScroll = true;
AutoScrollMinSize = new Size(640, 480);

여기서 AutoScrollMinSize가 논리 화면의 크기가 됩니다.

        protected override void OnPaint(PaintEventArgs e)
        {
            var rect = ClientRectangle;

            //if (this.DesignMode == true)
            //    return;
            if (mustRedraw == true)
            {
                Redraw();
                mustRedraw = false;
            }

            e.Graphics.DrawImage(c.Buffer, DisplayRectangle.X, DisplayRectangle.Y);
        }

차트를 다시 그리는 것을 억제하기 위해 Bitmap애 캐시를 했는데요, DisplayRectangle.X, Y좌표는 ScrollableControl에서 제공하는 스크롤의 위치를 변경했을 떄 같이 변경하게 됩니다. 그러면 다음처럼 자연스럽게 스크롤 처리가 되게 됩니다.

image
image
image

좋아요 1