Хочу реализовать отображение графиков используя wpf.
Приблизительно как на картинке. Интересует только View(разметка xaml).
Массив на вход: list
Как это можно отобразить во View? Не используя сторонние библиотеки?
P. S. Смотрел на stackoverflow — данная тема поднималась уже много раз и много лет. Но нигде не было ни одного рабочего примера. Только ответы в духе: «это сложно», «за вас никто не сделает».
Поэтому критерием закрытия темы объявляю реальный рабочий пример! Давайте добьем эту задачу! Кто то ленится, а кто-то просто не может сделать — отнеситесь с пониманием.
Ответ
Не понимаю, что развели за разборки: сложно, невозможно, много кода... Фыр. Это же WPF, тут всё просто.
Для начала сгенерируем случайные данные:
using System;
using System.Collections.Generic;
using System.Linq;
using static System.Math;
namespace CandlestickChartApp
{
public partial class MainWindow
{
public MainWindow()
{
DataContext = new Candlestick();
InitializeComponent();
}
}
public class Candlestick
{
private const int PriceCount = 500;
private const int PricesPerCandle = 10;
public List
public Candlestick()
{
var rnd = new Random(1);
var today = DateTime.Today;
var date = DateTime.Today;
var value = 300;
for (var i = 0; i < Prices.Capacity; i++)
Prices.Add(new Price { Date = date = date.AddMinutes(5), Value = value += rnd.Next(-9, 10) });
for (var i = 0; i < Candles.Capacity; i++) {
var prices = Prices.Select(p => p.Value).Skip(i * PricesPerCandle).Take(PricesPerCandle + 1);
Candles.Add(new Candle {
Date = (Prices[i * PricesPerCandle].Date - today).TotalMinutes / 5,
Min = prices.Min(), Max = prices.Max(), Height = prices.Max() - prices.Min(),
DeltaMin = prices.First(), DeltaMax = prices.Last(), DeltaHeight = Abs(prices.Last() - prices.First()),
IsPositive = prices.First() < prices.Last(),
});
}
Candles.ForEach(c => c.Fix());
PriceCurrent = Prices.Last().Value;
PriceMin = Prices.Min(p => p.Value) - 20;
PriceMax = Prices.Max(p => p.Value) + 20;
PriceHeight = PriceMax - PriceMin - 40;
for (double price = Round(PriceMin / 10) * 10; price < PriceMax; price += 50)
Labels.Add(price);
}
}
public class Price
{
public DateTime Date { get; set; }
public double Value { get; set; }
}
public class Candle
{
public double Date { get; set; }
public double Min { get; set; }
public double Max { get; set; }
public double Height { get; set; }
public double DeltaMin { get; set; }
public double DeltaMax { get; set; }
public double DeltaHeight { get; set; }
public bool IsPositive { get; set; }
public void Fix()
{
if (!IsPositive) {
var min = DeltaMin;
DeltaMin = DeltaMax;
DeltaMax = min;
}
}
}
}
А потом остаётся только описать несколько шаблонов и стилей:
И пожалуйста:
Даты преобразуются к double, чтобы использовать как координату X. Цены генерируются в виде double и используются как Y (в доменной области они должны быть decimal).
Списки свечек и надписей служат источником элементов для ItemsControl. В качестве панели для размещения элементов используется Canvas вместо стандартной VirtualizingStackPanel, что позволяет размещать элементы не стопкой, а по координатам. Координаты прикрепляются через стилизацию ContentPresenter, которые оборачивают каждый элемент.
Возникает загвоздка с координатой Y, так как в WPF она считается от левого верхнего угла, а в графике — от левого нижнего. Это решается при помощи преобразования вертикального отражения относительно центра: сначала отражается всё содержимое контейнера, потом текстовые надписи отражаются ещё раз, чтобы их можно было прочитать.
Маргины вроде 0 8 0 -8 — это сдвиги элементов относительно исходной позиции без изменения "внешнего" размера (в данном случае сдвиг вниз на 8 пунктов). Этот приём используется для центрирования надписей по вертикали и сдвига самого графика.
Так как WPF не умеет рисовать прямоугольники с отрицательной высотой, то надо удостовериться, что "минимальное" значение DeltaMin меньше "максимального" DeltaMax
Запросы LINQ в коде не очень оптимальные, совершается много избыточных проходов по коллекции. Это сделано исключительно для простоты кода, в реальном приложении придётся написать менее кратко.
Также к имени класса Candle можно дописать суффикс ViewModel, так как этот класс — не часть домена, а используется исключительно как удобный источник данных для XAML. По вкусу можно сделать свойства свечки более доменными, но тогда понадобится писать конвертеры IValueConverter для преобразования DateTime и decimal к double. Это уже вопрос вкуса.
Всего-то 80 строк XAML.
Разумеется, это пример, и многое зашито в коде. Доведение до ума, в том числе дорисовывание вертикальных линий сетки — домашнее задание.
Реальный контрол должен учитывать, что диапазоны значений бывают разные по масштабу, давать возможность перематывать и масштабировать график и т.п. Но вот отобразить свечки — ну вообще никаких проблем. Развели разборки на пустом месте.
Человеку хочется потратить 500 репы на непонятно зачем нужную задачку — его воля. Может, он изучает WPF и хочет красивый простой пример. Может, у него неадекватный заказчик, который запрещает пользоваться библиотеками. Разные бывают ситуации. Если человек готов пожертвовать потом и кровью добытую репу, то, наверное, очень надо.
Комментариев нет:
Отправить комментарий