607 lines
18 KiB
C#
607 lines
18 KiB
C#
using FontAwesome.Sharp;
|
|
using Font = System.Drawing.Font;
|
|
using FontStyle = System.Drawing.FontStyle;
|
|
using Image = System.Drawing.Image;
|
|
|
|
namespace Pf2eViewer.EntityTreeViewer;
|
|
|
|
/// <summary>
|
|
/// Элемент управления для отображения строки поля сущности.
|
|
/// </summary>
|
|
internal class EntityTreeViewerControlRow
|
|
{
|
|
/// <summary>
|
|
/// Элемент управления, которому принадлежит эта строка.
|
|
/// </summary>
|
|
public readonly EntityTreeViewerControl ParentControl;
|
|
|
|
/// <summary>
|
|
/// Отображаемая строка поля сущности.
|
|
/// </summary>
|
|
public readonly StructureField Item;
|
|
|
|
#region "Поля строки"
|
|
|
|
/// <summary>
|
|
/// Кэш изображения строки.
|
|
/// </summary>
|
|
public Image Cache
|
|
{
|
|
get
|
|
{
|
|
if (_cache == null)
|
|
{
|
|
UpdateCache();
|
|
}
|
|
return _cache!;
|
|
}
|
|
set => _cache = value;
|
|
}
|
|
private Image? _cache;
|
|
|
|
/// <summary>
|
|
/// Кэш изображения строки при наведении.
|
|
/// </summary>
|
|
public Image HoveredCache
|
|
{
|
|
get
|
|
{
|
|
if (_hoveredCache == null)
|
|
{
|
|
UpdateCache();
|
|
}
|
|
return _hoveredCache!;
|
|
}
|
|
set => _hoveredCache = value;
|
|
}
|
|
private Image? _hoveredCache;
|
|
|
|
/// <summary>
|
|
/// Список типов лестницы.
|
|
/// </summary>
|
|
public List<LadderType> Ladder
|
|
{
|
|
get => _ladder;
|
|
set
|
|
{
|
|
_ladder = value;
|
|
_cache = null;
|
|
}
|
|
}
|
|
private List<LadderType> _ladder = [];
|
|
|
|
/// <summary>
|
|
/// Проверяет, находится ли курсор над строкой.
|
|
/// </summary>
|
|
public bool Hovered
|
|
{
|
|
get => _hovered;
|
|
set
|
|
{
|
|
if (value != _hovered)
|
|
{
|
|
_hovered = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
}
|
|
private bool _hovered = false;
|
|
|
|
/// <summary>
|
|
/// Индекс строки в представлении.
|
|
/// </summary>
|
|
public int ViewIndex { get; set; } // = -1;
|
|
|
|
/// <summary>
|
|
/// Кнопка раскрытия/сворачивания.
|
|
/// </summary>
|
|
public IconButton? Button { get; set; } = null;
|
|
|
|
#endregion
|
|
|
|
#region "Поля для рендеринга"
|
|
|
|
/// <inheritdoc cref="EntityTreeViewerControl.Font"/>
|
|
public Font Font
|
|
{
|
|
get => ParentControl.Font;
|
|
set => ParentControl.Font = value;
|
|
}
|
|
|
|
/// <inheritdoc cref="EntityTreeViewerControl.ForeColor"/>
|
|
public Color ForeColor
|
|
{
|
|
get => ParentControl.ForeColor;
|
|
set => ParentControl.ForeColor = value;
|
|
}
|
|
|
|
/// <inheritdoc cref="EntityTreeViewerControl.BackColor"/>
|
|
public Color BackColor
|
|
{
|
|
get => ParentControl.BackColor;
|
|
set => ParentControl.BackColor = value;
|
|
}
|
|
|
|
/// <inheritdoc cref="EntityTreeViewerControl.HoverColor"/>
|
|
public Color HoverColor
|
|
{
|
|
get => ParentControl.HoverColor;
|
|
set => ParentControl.HoverColor = value;
|
|
}
|
|
|
|
/// <inheritdoc cref="EntityTreeViewerControl.ItemHeight"/>
|
|
public int ItemHeight
|
|
{
|
|
get => ParentControl.ItemHeight;
|
|
set => ParentControl.ItemHeight = value;
|
|
}
|
|
|
|
/// <inheritdoc cref="EntityTreeViewerControl.DepthSize"/>
|
|
public int DepthSize
|
|
{
|
|
get => ParentControl.DepthSize;
|
|
set => ParentControl.DepthSize = value;
|
|
}
|
|
|
|
/// <inheritdoc cref="EntityTreeViewerControl.ButtonSize"/>
|
|
public int ButtonSize
|
|
{
|
|
get => ParentControl.ButtonSize;
|
|
set => ParentControl.ButtonSize = value;
|
|
}
|
|
|
|
/// <inheritdoc cref="EntityTreeViewerControl.TypeFontSize"/>
|
|
public float TypeFontSize
|
|
{
|
|
get => ParentControl.TypeFontSize;
|
|
set => ParentControl.TypeFontSize = value;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region "Вычисляемые поля"
|
|
|
|
/// <summary>
|
|
/// Ширина строки.
|
|
/// </summary>
|
|
public int Width => ParentControl.ContentWidth;
|
|
|
|
/// <summary>
|
|
/// Высота строки.
|
|
/// </summary>
|
|
public int Height => ItemHeight;
|
|
|
|
/// <summary>
|
|
/// Флаг наличия кнопки.
|
|
/// </summary>
|
|
public bool HasButton => Item.FieldType == StructureFieldType.Complex;
|
|
|
|
/// <summary>
|
|
/// Глубина отображения строки.
|
|
/// </summary>
|
|
public int Depth => Item.Depth;
|
|
|
|
/// <summary>
|
|
/// Ширина имени в строке.
|
|
/// </summary>
|
|
public int NameWidth
|
|
{
|
|
get
|
|
{
|
|
Graphics g = Graphics.FromImage(Cache);
|
|
return Convert.ToInt32(g.MeasureString(Item.Name, Font).Width + DepthSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Строковое представление типа поля.
|
|
/// </summary>
|
|
public string TypeText
|
|
{
|
|
get
|
|
{
|
|
string result = Item.GetTypeString();
|
|
if (result != "")
|
|
{
|
|
result = $@"({result})";
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Шрифт для отображения типа поля.
|
|
/// </summary>
|
|
public Font TypeFont => new(Font.FontFamily, TypeFontSize);
|
|
|
|
/// <summary>
|
|
/// Строковое представление значения поля.
|
|
/// </summary>
|
|
public string ValueText => Item.GetValueString();
|
|
|
|
/// <summary>
|
|
/// Шрифт для отображения значения поля.
|
|
/// </summary>
|
|
public Font ValueFont =>
|
|
new(
|
|
Font.FontFamily,
|
|
Font.Size,
|
|
Item.FieldType == StructureFieldType.Complex ? FontStyle.Italic : FontStyle.Regular
|
|
);
|
|
|
|
/// <summary>
|
|
/// Прямоугольник, занимаемый строкой в представлении.
|
|
/// </summary>
|
|
public RectangleF Target => new(0, ViewIndex * Height, Width, Height);
|
|
|
|
/// <summary>
|
|
/// Прямоугольник, занимаемый строкой в родительском представлении.
|
|
/// </summary>
|
|
public RectangleF RealTarget =>
|
|
new(0, ViewIndex * Height - ParentControl.VerticalScroll.Value, Width, Height);
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Список данных для рендеринга строк.
|
|
/// </summary>
|
|
private readonly List<StringRenderData> _renderData = [];
|
|
|
|
#region "Основные методы"
|
|
|
|
/// <summary>
|
|
/// Создание элемента управления строки поля сущности.
|
|
/// </summary>
|
|
/// <param name="parent">Родительский элемент управления.</param>
|
|
/// <param name="item">Строка поля структуры объекта.</param>
|
|
public EntityTreeViewerControlRow(EntityTreeViewerControl parent, StructureField item)
|
|
{
|
|
ParentControl = parent;
|
|
Item = item;
|
|
ApplyItem();
|
|
UpdateCache();
|
|
|
|
UpdateButton();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Применение свойств к элементу управления.
|
|
/// </summary>
|
|
private void ApplyItem()
|
|
{
|
|
_renderData.Clear();
|
|
|
|
_renderData.Add(new(Item.Name, Font, ForeColor));
|
|
_renderData.Add(new(TypeText, TypeFont, GetValueColor()));
|
|
_renderData.Add(
|
|
new(ValueText.Replace("\r", "").Replace('\n', ' '), ValueFont, GetValueColor())
|
|
);
|
|
|
|
if (HasButton)
|
|
{
|
|
Button = new()
|
|
{
|
|
Width = ButtonSize,
|
|
Height = ButtonSize,
|
|
IconSize = ButtonSize,
|
|
IconChar = IconChar.MinusSquare,
|
|
ImageAlign = ContentAlignment.MiddleCenter,
|
|
TextAlign = ContentAlignment.MiddleCenter,
|
|
Visible = false,
|
|
};
|
|
|
|
Button.Click += Button_Click;
|
|
|
|
ParentControl.ContentPanel.Controls.Add(Button);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Обновление кэша изображения строки.
|
|
/// </summary>
|
|
public void UpdateCache()
|
|
{
|
|
Cache = RenderCache(BackColor);
|
|
HoveredCache = RenderCache(HoverColor);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Создание кэша изображения строки.
|
|
/// </summary>
|
|
/// <param name="backColor">Цвет заднего фона.</param>
|
|
/// <returns>Кеш изображения строки.</returns>
|
|
private Image RenderCache(Color backColor)
|
|
{
|
|
Image cache = new Bitmap(Width, Height);
|
|
Graphics g = Graphics.FromImage(cache);
|
|
|
|
g.FillRectangle(new SolidBrush(backColor), 0, 0, Width, Height);
|
|
|
|
RenderLadder(g);
|
|
RenderContent(g);
|
|
|
|
g.Save();
|
|
return cache;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Запрос на перерисовку строки.
|
|
/// </summary>
|
|
/// <param name="g">Графический объект.</param>
|
|
public void Invalidate(Graphics? g = null)
|
|
{
|
|
g ??= Graphics.FromImage(ParentControl.ContentImage);
|
|
|
|
g.DrawImage(Hovered ? HoveredCache : Cache, Target);
|
|
|
|
UpdateButton();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region "Кнопка"
|
|
|
|
/// <summary>
|
|
/// Обновление кнопки раскрытия/сворачивания.
|
|
/// </summary>
|
|
public void UpdateButton()
|
|
{
|
|
if (Button is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int cx = Convert.ToInt32((0.5 + Depth) * DepthSize - 0.5 * ButtonSize);
|
|
int cy = Convert.ToInt32((0.5 + ViewIndex) * ItemHeight - 0.5 * ButtonSize);
|
|
|
|
Button.Left = cx;
|
|
Button.Top = cy;
|
|
|
|
Button!.IconChar = ((StructureFieldComplex)Item).Collapsed
|
|
? IconChar.PlusSquare
|
|
: IconChar.MinusSquare;
|
|
Button.Visible = Item.Visible;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Событие нажатия кнопки раскрытия/сворачивания.
|
|
/// </summary>
|
|
/// <param name="sender">Источник события.</param>
|
|
/// <param name="e">Событие нажатие кнопки</param>
|
|
private void Button_Click(object? sender, EventArgs e)
|
|
{
|
|
if (Item is StructureFieldComplex complex)
|
|
{
|
|
complex.Collapsed = !complex.Collapsed;
|
|
ParentControl.UpdateView();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region "Отрисовка"
|
|
|
|
/// <summary>
|
|
/// Отрисовка лестницы.
|
|
/// </summary>
|
|
/// <param name="g">Графический объект.</param>
|
|
private void RenderLadder(Graphics g)
|
|
{
|
|
int n = Depth;
|
|
int h = ItemHeight;
|
|
int w = DepthSize;
|
|
|
|
if (_ladder.Count < Depth)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Pen pen = new(Color.FromArgb(127, ForeColor.R, ForeColor.G, ForeColor.B), 1);
|
|
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
|
|
|
|
float cy = 0.5f * h;
|
|
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
float cx = (i + 1.5f) * w;
|
|
|
|
switch (_ladder[i])
|
|
{
|
|
case LadderType.Last:
|
|
g.DrawLine(pen, cx, 0, cx, cy);
|
|
break;
|
|
case LadderType.Through:
|
|
g.DrawLine(pen, cx, 0, cx, h);
|
|
break;
|
|
case LadderType.Empty:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (i == n - 1)
|
|
{
|
|
g.DrawLine(pen, cx, cy, (i + 2) * w, cy);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Отрисовка содержимого строки.
|
|
/// </summary>
|
|
/// <param name="g">Графический объект.</param>
|
|
private void RenderContent(Graphics g)
|
|
{
|
|
float[] xs =
|
|
[
|
|
DepthSize * (Item.Depth + 1),
|
|
ParentControl.NameWidth,
|
|
ParentControl.NameWidth + g.MeasureString(TypeText, TypeFont).Width + 3,
|
|
Width,
|
|
];
|
|
|
|
Pen p = new(Color.Aqua, 1);
|
|
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
StringRenderData item = _renderData[i];
|
|
|
|
item.Left = xs[i];
|
|
item.Width = xs[i + 1] - xs[i];
|
|
item.Height = ItemHeight;
|
|
|
|
// g.DrawRectangle(p, item.Rect);
|
|
item.Draw(g);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Функции-помощники в отрисовке
|
|
|
|
/// <summary>
|
|
/// Смешивание цвета с цветом текста.
|
|
/// </summary>
|
|
/// <param name="color">Цвет оттенка.</param>
|
|
/// <param name="rate">Степень смешения.</param>
|
|
/// <returns>Смешенный цвет.</returns>
|
|
private Color BlendColor(Color color, double rate = 0.5)
|
|
{
|
|
return BlendColor(ForeColor, color, rate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Смешивание цветов.
|
|
/// </summary>
|
|
/// <param name="color1">Первый смешиваемый цвет.</param>
|
|
/// <param name="color2">Второй смешиваемый цвет.</param>
|
|
/// <param name="rate">Степень смешения.</param>
|
|
/// <returns>Смешенный цвет.</returns>
|
|
private Color BlendColor(Color color1, Color color2, double rate)
|
|
{
|
|
return Color.FromArgb(
|
|
Mix(color1.A, color2.A),
|
|
Mix(color1.R, color2.R),
|
|
Mix(color1.G, color2.G),
|
|
Mix(color1.B, color2.B)
|
|
);
|
|
|
|
int Mix(int a, int b) => Convert.ToInt32(Math.Round(a * rate + b * (1 - rate)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Получение цвета текста по типу значения поля.
|
|
/// </summary>
|
|
/// <returns>Цвет текста.</returns>
|
|
public Color GetValueColor()
|
|
{
|
|
return Item switch
|
|
{
|
|
StructureFieldPrimitive primitive => Type.GetTypeCode(primitive.Type) switch
|
|
{
|
|
TypeCode.Boolean => BlendColor(Color.DarkGoldenrod),
|
|
TypeCode.Int32 => BlendColor(Color.Blue),
|
|
TypeCode.Double => BlendColor(Color.Aqua),
|
|
TypeCode.String => BlendColor(Color.Green),
|
|
_ => ForeColor,
|
|
},
|
|
StructureFieldEnum => BlendColor(Color.Red),
|
|
StructureFieldNull => BlendColor(BackColor),
|
|
_ => ForeColor,
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Класс для хранения данных рендеринга строки.
|
|
/// </summary>
|
|
/// <param name="text">Текст отображаемой строки.</param>
|
|
/// <param name="font">Шрифт отображаемой строки.</param>
|
|
/// <param name="color">Цвет отображаемой строки.</param>
|
|
private class StringRenderData(string text, Font font, Color color)
|
|
{
|
|
/// <summary>
|
|
/// Текст отображаемой строки.
|
|
/// </summary>
|
|
public string Text { get; } = text;
|
|
|
|
/// <summary>
|
|
/// Шрифт отображаемой строки.
|
|
/// </summary>
|
|
public Font Font { get; } = font;
|
|
|
|
/// <summary>
|
|
/// Цвет отображаемой строки.
|
|
/// </summary>
|
|
public Color Color { get; } = color;
|
|
|
|
/// <summary>
|
|
/// Левый край отображаемой строки.
|
|
/// </summary>
|
|
public float Left { get; set; }
|
|
|
|
/// <summary>
|
|
/// Ширина отображаемой строки.
|
|
/// </summary>
|
|
public float Width { get; set; }
|
|
|
|
/// <summary>
|
|
/// Высота отображаемой строки.
|
|
/// </summary>
|
|
public float Height { get; set; }
|
|
|
|
/// <summary>
|
|
/// Прямоугольник, занимаемый строкой.
|
|
/// </summary>
|
|
public RectangleF Rect => new(Left, 0, Width, Height);
|
|
|
|
/// <summary>
|
|
/// Уместить строку в занимаемый прямоугольник.
|
|
/// </summary>
|
|
/// <param name="g">Элемент графики.</param>
|
|
/// <param name="rect">Занимаемый прямоугольник</param>
|
|
/// <param name="filler">Строка, на которую будет заканчиваться значнеие при переполнении.</param>
|
|
/// <returns></returns>
|
|
private string Fit(Graphics g, RectangleF rect, string filler = "…")
|
|
{
|
|
if (Text.Length == 0 || g.MeasureString(Text, Font).Width <= rect.Width)
|
|
{
|
|
return Text;
|
|
}
|
|
|
|
string txt = Text.Length > 100 ? Text[..100] : Text;
|
|
|
|
int i = txt.Length;
|
|
|
|
while (g.MeasureString(txt + filler, Font).Width > rect.Width)
|
|
{
|
|
txt = txt[..--i];
|
|
if (i == 0)
|
|
break;
|
|
}
|
|
|
|
return txt + filler;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Отрисовка строки.
|
|
/// </summary>
|
|
/// <param name="g">Элемент графики.</param>
|
|
public void Draw(Graphics g)
|
|
{
|
|
g.DrawString(
|
|
Fit(g, Rect),
|
|
Font,
|
|
new SolidBrush(Color),
|
|
Rect,
|
|
new StringFormat
|
|
{
|
|
Alignment = StringAlignment.Near,
|
|
LineAlignment = StringAlignment.Center,
|
|
FormatFlags = StringFormatFlags.NoWrap,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|