Есть два разных видения архитектуры GUI и плагинного движка для рисования сложных графических объектов.
Необходимо выбрать один. Мне очень надоело спорить с коллегой в процессе выбора. Поэтому выношу
вопрос на обсуждение программерской общественностью. Тексты приводятся как написали сами авторы вариантов архитектуры.
Итак.
1 вариант.
а) Есть набор графических примитивов:
Line
Rectangle
Ellipse
TrendLine
Triangle
Channel
и т.д.
а) Есть плагинный движок — запускающий на выполнение плагины. Плагины имеют возможность рисовать
графические примитивы в произвольном количестве и произвольных же сочетаниях. Существует набор так называемых стандартных графических индикаторов — которые присутствуют в любом торговом терминале — тренды, каналы, горизонтальные и вертикальные линии,"вилы Эндрюса", "уровни Фибоначчи" и т.д.
В самом простом случае графический индикатор может состоять из 1-2 линий. В сложном случае графический индикатор может состоять из множества графических примитивов в самых разных сочетаниях со сложной логикой размещения относительно друг друга.
К примеру — пусть этот индикатор рисует две линии — обозначающие минимальный и максимальный уровень цены из двух массивов данных:
class DoubleMA : IIndicator
{
[Parameter]
private int periodOne = 7;
[Parameter]
private int periodTwo = 14;
[Parameter]
private PriceType priceType = PriceType.Close;
[Visible]
private IBuffer bufferOne = null;
[Visible]
private IBuffer bufferTwo = null;
private Indicator ma1;
private Indicator ma2;
private readonly Color bufferOneCloudColor = Color.FromArgb(128, Color.Red);
private readonly Color bufferTwoCloudColor = Color.FromArgb(128, Color.Green);
double currentMin = double.MaxValue;
double currentMax = double.MinValue;
IGraphicObject maxLine;
IGraphicObject minLine;
public void Init(IIndicatorInit init)
{
ma1 = init.CreateIndicator("MA", periodOne, priceType);
ma2 = init.CreateIndicator("MA", periodTwo, priceType);
}
public void Run(IIndicatorRun run)
{
double currentMA1 = ma1(0, 0);
bufferOne[0] = currentMA1;
double currentMA2 = ma2(0, 0);
bufferTwo[0] = currentMA2;
if (currentMA1 > currentMA2)
bufferOne.SetCloudColor(0, bufferOneCloudColor);
else
bufferTwo.SetCloudColor(0, bufferTwoCloudColor);
// СОЗДАЕМ ГРАФИЧЕСКИЕ ОБЪЕКТЫ !!!
if (currentMax < buffer.Max)
{
currentMax = buffer.Max;
if (maxLine != null)
run.Graphics.DeleteObject(maxLine);
maxLine = run.Graphics.CreateLine("Maxlevel", bufferOne.Max);
}
if (currentMin > buffer.Min)
{
currentMin = buffer.Min;
if (minLine != null)
run.Graphics.DeleteObject(minLine);
minLine = run.Graphics.CreateLine("Minlevel", bufferOne.Min);
}
// СОЗДАЕМ ГРАФИЧЕСКИЕ ОБЪЕКТЫ !!!
}
public void Deinit(IIndicatorDeinit deinit)
{
}
}
Результатом работы этого индикатора, помимо вывода данных из двух массивов (bufferOne, bufferTwo) — будет также вывод двух горизонтальных линий — обозначающих минимум/максимум из массивов.
Данные для рисования линий берутся GUIём из созданных в индикаторе линий и рисуются с учетом текущих масштабов
окна и т.д.
Плюсы этого варианта архитектуры:
1) Органичная интеграция возможности рисования графических объектов в существующий API плагинов(все операции с графическими объектами происходит через интерфейс IGraphics).
2) Работа с графическими объектами в терминах предметной области(уровни цен и т.д.) — без необходимости производить дополнительные расчеты в самом индикаторе при изменении размеров окна
3) Возможность редактирования графических объектов пользователем после их создания индикатором.
4) Абстрагирование от физической природы графического устройства.
==============================================================================================================================
Вариант 2.
Имеется контрол чарта, работающий с плагинами, которые представляют из себя на выходе последовательные массивы данных для построения данных.
Встала задача — сделать возможность чтобы плагины могли рисовать линии тренда и прочие примитивы, в общем графические объекты, причем с обратной связью на клики юзера.
Возможные варианты:
1. Функциональность существующего плагинного движка наращивается.
2. Создается отдельный плагинный движок.
3. Хардкод набора примитивов пряс в контроле в соотвествии с конкретным имеющимся пожеланием заказчика, аналогично как это сделано в TeeChart и
аналогичных контролах.
Следует учитывать следующие тезисы:
1. Почти нулевая вероятность того что третья сторона вообще когда-либо воспользуется возможностями рисования графических объектов, поскольку итак уже существует возможность выводить массив, в котором в качестве значений выступает произвольная графика.
2. Функциональность плагинов типа-1 и типа-2 принципильно отличаются. Первый работает с массивами данных и выводит массивы-индикаторы/графики. Второй — это составной, но еденичный объект, не связаный со значениями плагинов типа-1. А плагины типа-1 не связан с графическим объектом типа-2, они никак не пересекаются ни по входящим ни по исходящим данным.
3. Конечный вывод тоже кардинально различается, чтобы не смешивать плагины с графическим объектом и плагины с индикатором. Они должны находиться в разных списках, по разному присваиваться конечному контролу, один с обратной связью, другой нет, в общем с точки зрения GUI для юзера — разные вещи.
4. Юзеру далекому от программирования схема "все-в-одном" покажется сложной.
Плагин типа-2 на порядок проще, чем тип-1, если он реализуется в виде отдельного движка — это просто набор небольшой набор полей для обратной связи. И единственный метод, в который передается Graphics на котором необходимо нарисовать то что надо нарисовать. Как вариант — метод ничего не принимает, но возвращает Image, но в таком случае ожидается падение производительности, в связи с необходимостью наложения растра на контрол через альфа-канал.
Пример сорца плагина типа-2., если он будет реализован в виде отдельного движка. Реализация данного примера в рамках плагинного движка заточеного под плагины типа-1 будет выглядеть сложнее на порядок.
class PainterTextLabel: PainterProto
{
public PainterTextLabel()
{
RequiredPointsCount = 1;
}
internal override void DrawPaint(Graphics GFX)
{
if (HPoint[0].Initialized)
{
GFX.DrawString(Text, new Font("Arial", TextFontSize), new SolidBrush(LineColor), HPoint[0].X, HPoint[0].Y);
if (AfterEditMode)
{
GFX.DrawRectangle(new Pen(Color.Red, 3), HPoint[0].X - 2, HPoint[0].Y -2, 4, 4);
}
}
}
}
Так же имеются причины почему в чем сложность делать отдельный движок под тип-2: архитектура проекта стала уже сложной и добавление плагинного движка будет трудозатратно.