| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
- c# 업비트 api키 목록
- 업비트 API
- 북마크
- Prism
- maui
- c# 차트
- WPF
- c# restapi
- C#
- c# restapi 호출
- 업비트
- Upbit API
- c# 업비트
- upbit
- c# api호출
- 나만의 사이트모음집
- Chart
- c# 라이브 차트
- XAML
- 차트
- 라이브 차트
- c# maui
- 즐겨찾기
- 업비트 c#
- 업비트 차트
- c# websocket
- Today
- Total
하아찡
C# MAUI 차트 - 완 - 본문
이전글
C# MAUI 차트 - 휠
이전글https://thesh.tistory.com/131 C# 업비트 차트 라이브 처리API 호출 목록 북마크사이트https://mysitecollection.com/guest/1/19/coinapi 내사이트모음자신만의 사이트를 저장해두고 사용합니다.mysitecollection.com
thesh.tistory.com
추가된 기능
Y축 시간
클릭시 Y축에따른 가격
아마 이제 더이상 차트에 관한 코드는 작업해서 올리지 않을거같습니다.
(중간중간 작업한 코드는 올라가겠지만 따로 글을 작성하지는 않을듯합니다.)
해당 코드에서 수정하시고 추가하고싶은 기능을 추가하시면 될듯합니다.
이벤트는 ICommand를 통해 값을 전달받아서 사용합니다
OnPanUpdated -> 드래그 기능(모바일 및 PC 다 사용가능)
OnMouseClick -> 마우스 클릭 기능
OnMouseWheel -> 마우스 휠 확대 축소기능(윈도우에서만 가능)
XAML에서 적용방법은 아래 코드에서 XAML코드를 보시면 됩니다.
실행화면


코드
적용코드 XAML
<ChartCandle:CandleChartView x:Name="MyChartViewRef" ItemsSource="{Binding ChartView}" OnPanUpdated="{Binding PanCommand}" OnMouseClick="{Binding MouseClickCommand}" OnMouseWheel="{Binding MouseWheelCommand }" />
혹시나 Binding관련해서 잘못하시는 분들을 위해서 ViewModel쪽 이벤트 코드를 조금 보여드리겠습니다.
// ICommand 변수 생성
public ICommand MouseWheelCommand { get; }
// 생성자에다가 추가
MouseWheelCommand = new Command<MouseWheelEventArgs>(HandleMouseWheel);
// Command에 연결될 함수 생성
private void HandleMouseWheel(MouseWheelEventArgs args)
{
// TODO 휠 사용시 필요했던 이벤트 실행
}
위 코드를 이제 View쪽에서 Binding쪽에서 바인딩을 하시면 HandleMouseWheel 함수가 호출이 됩니다.
대신 연결해줄때는 HandleMouseWheel함수가 아닌 ICommand 변수를 바인딩해줍니다. 그래서 ICommand 변수는 Public으로 하셔야지 사용이 가능합니다.
CandleChartDrawable.cs
using CoinManager.Upbit;
using CoinManager.Utils;
using Microsoft.UI.Xaml.Controls;
using Newtonsoft.Json.Linq;
using System;
namespace CoinManager.ChartControl
{
public class CandleChartDrawable : IDrawable
{
public IList<Candle> Candles { get; set; } = new List<Candle>();
public int DefaultCandleCnt { get; set; } = 250;
public int ShowCandleCnt { get; set; } = 250;
public Color colorUp { get; set; } = Color.FromArgb("#CD0000");
public Color colorDown { get; set; } = Color.FromArgb("#315F97");
public Color colorGrid { get; set; } = Color.FromArgb("#80B8B7C1");
public ChartXaxisScaler xScaler { get; private set; }
public ChartYaxisScaler Yscaler { get; private set; }
public float PriceArea { get; set; } = 200f; // 금액표시 영역
public float TimestempArea { get; set; } = 50f;
private float TopGap = 10.0f;
public void Draw(ICanvas canvas, RectF dirtyRect)
{
if (Candles == null || Candles.Count == 0)
return;
float width = (dirtyRect.Width - PriceArea) > 0 ? dirtyRect.Width - PriceArea : dirtyRect.Width;
float height = ((dirtyRect.Height - TimestempArea) > 0 ? dirtyRect.Height - TimestempArea : dirtyRect.Height) - TopGap;
float spacing = 2f / Math.Max(ShowCandleCnt - DefaultCandleCnt, 1);
float candleWidth = width / ShowCandleCnt - spacing;
// 캔들 범위 계산
double max = Candles.Max(c => c.Hp);
double min = Candles.Min(c => c.Lp);
float chartHeight = height - TopGap;
Yscaler = new ChartYaxisScaler(min, max, chartHeight, topGap: 10);
double niceGap = Yscaler.Gap;
double niceMin = Math.Floor(min / niceGap) * niceGap;
double niceMax = Math.Ceiling(max / niceGap) * niceGap;
double range = niceMax - niceMin;
DateTime startTime = Convert.ToDateTime(Candles[0].Utc);
DateTime endTime = Convert.ToDateTime(Candles[Candles.Count - 1].Utc);
xScaler = new ChartXaxisScaler(startTime, endTime, (candleWidth + spacing) * Candles.Count);
string timeFormat = "HH:mm";
if(xScaler.Gap.Ticks >= TimeSpan.FromDays(3).Ticks)
{
timeFormat = "yyyy-MM-dd";
}
else if (xScaler.Gap.Ticks >= TimeSpan.FromDays(1).Ticks)
{
timeFormat = "MM-dd";
}
int xIndex = 0;
// 그리드도 같은 기준으로
DrawHorizGridLine(canvas, width, height, niceMin, niceMax, niceGap);
int testCnt = 0;
for (int i = 0; i < Candles.Count; i++, testCnt++, xIndex++)
{
var c = Candles[i];
float x = i * (candleWidth + spacing);
if (x > dirtyRect.Width) break;
float yHigh = Yscaler.ToY(c.Hp);
float yLow = Yscaler.ToY(c.Lp);
float yOpen = Yscaler.ToY(c.Op);
float yClose = Yscaler.ToY(c.Tp);
DateTime time = Convert.ToDateTime(c.Utc);
if (((time.Ticks % xScaler.Gap.Ticks == 0)) || (Candles.Count - 1) == i)
{
canvas.StrokeColor = colorGrid;
canvas.StrokeSize = 1;
canvas.FontColor = Colors.White;
canvas.FontSize = 14;
DateTime thisCandleTime = Convert.ToDateTime(Candles[i].Utc);
float xAxis = xScaler.ToX(thisCandleTime);
canvas.DrawLine(xAxis + 0.5f, 0, xAxis + 0.5f, height);
canvas.DrawString(thisCandleTime.ToString(timeFormat), xAxis - 20, height + 5, 100, 20, HorizontalAlignment.Center, VerticalAlignment.Top);
}
bool isUp = c.Tp >= c.Op;
canvas.StrokeColor = isUp ? colorUp : colorDown;
canvas.StrokeSize = 1;
canvas.DrawLine(x + candleWidth / 2, yHigh, x + candleWidth / 2, yLow);
canvas.DrawLine(x, yOpen, x + candleWidth, yOpen);
float top = Math.Min(yOpen, yClose);
float bottom = Math.Max(yOpen, yClose);
canvas.FillColor = isUp ? colorUp : colorDown;
canvas.FillRectangle(x, top, candleWidth, bottom - top);
}
/*
canvas.FontColor = Colors.White;
canvas.FontSize = 14;
canvas.DrawString(
$"캔들 수: {testCnt}",
100, 10,
dirtyRect.Width,
20,
HorizontalAlignment.Left,
VerticalAlignment.Top);
*/
}
private void DrawHorizGridLine(ICanvas canvas, float width, float height, double niceMin, double niceMax, double niceGap)
{
float chartHeight = height - TopGap;
int lineCount = (int)((niceMax - niceMin) / niceGap);
canvas.StrokeColor = colorGrid;
canvas.StrokeSize = 1;
canvas.FontColor = Colors.White;
canvas.FontSize = 14;
for (int i = 0; i <= lineCount; i++)
{
double price = niceMin + i * niceGap;
float y = chartHeight * (float)((niceMax - price) / (niceMax - niceMin)) + TopGap;
canvas.DrawLine(0, y + 0.5f, width, y + 0.5f);
canvas.DrawString(
PriceFormat(price),
width + 10, y - 10,
PriceArea,
20,
HorizontalAlignment.Left,
VerticalAlignment.Top);
}
}
private string PriceFormat(double price)
{
if (price >= 1000)
return price.ToString("N0");
else if (price >= 100)
return price.ToString("N1");
else if (price >= 1)
return price.ToString("N2");
else
return price.ToString("0.000"); // 예: 0.001, 0.005 등
}
}
}
CandleChartView.cs
using CoinManager.Upbit;
using CoinManager.Utils;
using Microsoft.Maui.Graphics;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Input;
#if WINDOWS
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Input;
#endif
namespace CoinManager.ChartControl
{
public class CandleChartView : GraphicsView
{
private readonly CandleChartDrawable _drawable = new();
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create(
nameof(ItemsSource),
typeof(IList<Candle>),
typeof(CandleChartView),
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is CandleChartView view)
{
view.DetachCandleListeners(oldValue as IEnumerable<Candle>);
view.AttachCandleListeners(newValue as IEnumerable<Candle>);
view._drawable.Candles = (IList<Candle>)newValue ?? new List<Candle>();
view.Invalidate(); // 최초 그리기
}
});
public static readonly BindableProperty OnPanUpdatedProperty =
BindableProperty.Create(
nameof(OnPanUpdated),
typeof(ICommand),
typeof(CandleChartView));
public ICommand OnPanUpdated
{
get => (ICommand)GetValue(OnPanUpdatedProperty);
set => SetValue(OnPanUpdatedProperty, value);
}
public static readonly BindableProperty OnMouseWheelProperty =
BindableProperty.Create(
nameof(OnMouseWheel),
typeof(ICommand),
typeof(CandleChartView));
public ICommand OnMouseWheel
{
get => (ICommand)GetValue(OnMouseWheelProperty);
set => SetValue(OnMouseWheelProperty, value);
}
public static readonly BindableProperty OnMouseClickProperty =
BindableProperty.Create(
nameof(OnMouseClick),
typeof(ICommand),
typeof(CandleChartView));
public ICommand OnMouseClick
{
get => (ICommand)GetValue(OnMouseClickProperty);
set => SetValue(OnMouseClickProperty, value);
}
public IList<Candle> ItemsSource
{
get => (IList<Candle>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public CandleChartView()
{
_drawable.ShowCandleCnt = 250;
Drawable = _drawable;
var pan = new PanGestureRecognizer();
pan.PanUpdated += HandlePanGesture;
GestureRecognizers.Add(pan);
}
private void AttachCandleListeners(IEnumerable<Candle> candles)
{
if (candles == null) return;
foreach (var c in candles)
{
c.PropertyChanged += OnCandleChanged;
}
// ObservableCollection이라면 CollectionChanged도 구독해서 나중에 새 Candle 추가되면 거기에도 구독해줘야 함
if (candles is INotifyCollectionChanged obs)
{
obs.CollectionChanged += OnCollectionChanged;
}
}
private void DetachCandleListeners(IEnumerable<Candle> candles)
{
if (candles == null) return;
foreach (var c in candles)
{
c.PropertyChanged -= OnCandleChanged;
}
if (candles is INotifyCollectionChanged obs)
{
obs.CollectionChanged -= OnCollectionChanged;
}
}
private void OnCandleChanged(object sender, PropertyChangedEventArgs e)
{
Invalidate(); // 속성 변경되면 다시 그리기
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Candle c in e.NewItems)
{
c.PropertyChanged += OnCandleChanged;
}
}
if (e.OldItems != null)
{
foreach (Candle c in e.OldItems)
{
c.PropertyChanged -= OnCandleChanged;
}
}
Invalidate();
}
private void HandlePanGesture(object sender, PanUpdatedEventArgs e)
{
if (e.StatusType == GestureStatus.Running)
{
OnPanUpdated?.Execute(
new PanArgs
{
X = e.TotalX,
Y = e.TotalY
}
); // ViewModel에서 처리됨
Invalidate(); // 필요 시 View도 다시 그림
}
}
#if WINDOWS
protected override void OnHandlerChanged()
{
base.OnHandlerChanged();
if (Handler?.PlatformView is FrameworkElement platformView)
{
platformView.PointerWheelChanged += OnPointerWheelChanged;
platformView.PointerPressed += OnPointerPressed;
}
}
private void OnPointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
//var delta // 휠 위: +120, 아래: -120
var ptrPt = e.GetCurrentPoint(sender as UIElement);
int delta = ptrPt.Properties.MouseWheelDelta;
var position = ptrPt.Position;
bool isUp = delta > 0;
if(!isUp) _drawable.ShowCandleCnt = Math.Min(_drawable.ShowCandleCnt + 10, 1440);
else _drawable.ShowCandleCnt = Math.Max(_drawable.ShowCandleCnt - 10, 1);
OnMouseWheel?.Execute(new MouseWheelEventArgs
{
IsUp = isUp,
X = position.X,
Y = position.Y
});
Invalidate();
}
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (_drawable.Yscaler == null) return;
var ptrPt = e.GetCurrentPoint(sender as UIElement);
var position = ptrPt.Position;
double price = _drawable.Yscaler.ToPrice((float)position.Y);
// 원하면 커맨드도 호출 가능
OnMouseClick?.Execute(new MouseClickEventArgs
{
Price = price
});
}
#endif
}
}
CandleEvent.cs
namespace CoinManager.ChartControl
{
public class MouseWheelEventArgs
{
public bool IsUp { get; set; }
public double X { get; set; }
public double Y { get; set; }
}
public class MouseClickEventArgs
{
public double Price { get; set; }
}
public class PanArgs
{
public double X { get; set; }
public double Y { get; set; }
}
}
ChartXaxisScaler.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CoinManager.ChartControl
{
public class ChartXaxisScaler
{
private readonly DateTime _start;
private readonly DateTime _end;
private readonly double _totalMinutes;
private readonly float _width;
private readonly TimeSpan _gap;
public ChartXaxisScaler(DateTime start, DateTime end, float width)
{
_start = start;
_end = end;
_width = width;
_totalMinutes = (_end - _start).TotalMinutes;
_gap = GetNiceTimeGap(_end - _start);
}
public float ToX(DateTime time)
{
double minutes = (time - _start).TotalMinutes;
return (float)(minutes / _totalMinutes) * _width;
}
public IEnumerable<DateTime> GetLabelTimes()
{
DateTime alignedStart = new DateTime((_start.Ticks / _gap.Ticks) * _gap.Ticks).Add(_gap);
int count = 0;
const int maxCount = 10;
bool yielded = false;
for (DateTime t = alignedStart; t <= _end && count < maxCount; t = t.Add(_gap))
{
yield return t;
yielded = true;
count++;
}
if (!yielded)
yield return _start; // 최소 1개 보장
}
public TimeSpan Gap => _gap;
private static TimeSpan GetNiceTimeGap(TimeSpan duration)
{
double m = duration.TotalMinutes;
if (m <= 60) return TimeSpan.FromMinutes(5);
if (m <= 180) return TimeSpan.FromMinutes(15);
if (m <= 360) return TimeSpan.FromMinutes(30);
if (m <= 1440) return TimeSpan.FromHours(3);
if (m <= 4320) return TimeSpan.FromHours(6);
if (m <= 8640) return TimeSpan.FromHours(12);
if (m <= 17280) return TimeSpan.FromDays(1);
if (m <= 43200) return TimeSpan.FromDays(3);
if (duration.Ticks <= TimeSpan.FromDays(7).Ticks) return TimeSpan.FromDays(7);
if (duration.Ticks <= TimeSpan.FromDays(30).Ticks) return TimeSpan.FromDays(10);
if (duration.Ticks <= TimeSpan.FromDays(60).Ticks) return TimeSpan.FromDays(15);
if (duration.Ticks <= TimeSpan.FromDays(90).Ticks) return TimeSpan.FromDays(30);
if (duration.Ticks <= TimeSpan.FromDays(180).Ticks) return TimeSpan.FromDays(45);
if (duration.Ticks <= TimeSpan.FromDays(210).Ticks) return TimeSpan.FromDays(60);
if (duration.Ticks <= TimeSpan.FromDays(240).Ticks) return TimeSpan.FromDays(90);
if (duration.Ticks <= TimeSpan.FromDays(545).Ticks) return TimeSpan.FromDays(180);
return TimeSpan.FromDays(365);
}
}
}
ChartYaxisScaler.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CoinManager.ChartControl
{
public class ChartYaxisScaler
{
private readonly double _min;
private readonly double _max;
private readonly double _gap;
private readonly double _niceMin;
private readonly double _niceMax;
private readonly float _chartHeight;
private readonly float _topGap;
public ChartYaxisScaler(double min, double max, float chartHeight, float topGap = 0, int targetLineCount = 10)
{
_gap = CalculateNiceGap(min, max, targetLineCount);
_niceMin = Math.Floor(min / _gap) * _gap;
_niceMax = Math.Ceiling(max / _gap) * _gap;
_min = _niceMin;
_max = _niceMax;
_chartHeight = chartHeight;
_topGap = topGap;
}
public float ToY(double price)
{
double ratio = (_max - price) / (_max - _min);
return (float)(ratio * _chartHeight) + _topGap;
}
public double ToPrice(float y)
{
float adjustedY = y - _topGap;
double ratio = adjustedY / _chartHeight;
return _max - ratio * (_max - _min);
}
public IEnumerable<double> GetGridPrices()
{
for (double p = _niceMin; p <= _niceMax + _gap / 2; p += _gap)
{
yield return Math.Round(p, GetPrecision(_gap));
}
}
public double NiceMin => _niceMin;
public double NiceMax => _niceMax;
public double Gap => _gap;
private double CalculateNiceGap(double min, double max, int targetLineCount)
{
double range = max - min;
if (range == 0) return 1;
double roughGap = range / targetLineCount;
double exponent = Math.Floor(Math.Log10(roughGap));
double baseGap = Math.Pow(10, exponent);
double[] multipliers = { 1, 2, 2.5, 5, 10 };
foreach (var m in multipliers)
{
double candidate = baseGap * m;
if (candidate >= roughGap)
return candidate;
}
return baseGap * 10;
}
private int GetPrecision(double value)
{
int precision = 0;
while (value < 1)
{
value *= 10;
precision++;
}
return precision;
}
}
}
'C# > 업비트' 카테고리의 다른 글
| C# MAUI 차트 - 휠 (0) | 2025.05.27 |
|---|---|
| C# 업비트 차트 라이브 처리 (0) | 2025.05.26 |
| C# 업비트 캔들 작업 이슈 (1) | 2025.05.24 |
| C# 업비트 API 캔들데이터처리 (0) | 2025.05.22 |
| C# 업비트API 요청수 제한 (1) | 2025.05.22 |