using FontAwesome.Sharp; using Font = System.Drawing.Font; using FontStyle = System.Drawing.FontStyle; using Image = System.Drawing.Image; namespace Pf2eViewer.EntityTreeViewer; /// /// Элемент управления для отображения строки поля сущности. /// internal class EntityTreeViewerControlRow { /// /// Элемент управления, которому принадлежит эта строка. /// public readonly EntityTreeViewerControl ParentControl; /// /// Отображаемая строка поля сущности. /// public readonly StructureField Item; #region "Поля строки" /// /// Кэш изображения строки. /// public Image Cache { get { if (_cache == null) { UpdateCache(); } return _cache!; } set => _cache = value; } private Image? _cache; /// /// Кэш изображения строки при наведении. /// public Image HoveredCache { get { if (_hoveredCache == null) { UpdateCache(); } return _hoveredCache!; } set => _hoveredCache = value; } private Image? _hoveredCache; /// /// Список типов лестницы. /// public List Ladder { get => _ladder; set { _ladder = value; _cache = null; } } private List _ladder = []; /// /// Проверяет, находится ли курсор над строкой. /// public bool Hovered { get => _hovered; set { if (value != _hovered) { _hovered = value; Invalidate(); } } } private bool _hovered = false; /// /// Индекс строки в представлении. /// public int ViewIndex { get; set; } // = -1; /// /// Кнопка раскрытия/сворачивания. /// public IconButton? Button { get; set; } = null; #endregion #region "Поля для рендеринга" /// public Font Font { get => ParentControl.Font; set => ParentControl.Font = value; } /// public Color ForeColor { get => ParentControl.ForeColor; set => ParentControl.ForeColor = value; } /// public Color BackColor { get => ParentControl.BackColor; set => ParentControl.BackColor = value; } /// public Color HoverColor { get => ParentControl.HoverColor; set => ParentControl.HoverColor = value; } /// public int ItemHeight { get => ParentControl.ItemHeight; set => ParentControl.ItemHeight = value; } /// public int DepthSize { get => ParentControl.DepthSize; set => ParentControl.DepthSize = value; } /// public int ButtonSize { get => ParentControl.ButtonSize; set => ParentControl.ButtonSize = value; } /// public float TypeFontSize { get => ParentControl.TypeFontSize; set => ParentControl.TypeFontSize = value; } #endregion #region "Вычисляемые поля" /// /// Ширина строки. /// public int Width => ParentControl.ContentWidth; /// /// Высота строки. /// public int Height => ItemHeight; /// /// Флаг наличия кнопки. /// public bool HasButton => Item.FieldType == StructureFieldType.Complex; /// /// Глубина отображения строки. /// public int Depth => Item.Depth; /// /// Ширина имени в строке. /// public int NameWidth { get { Graphics g = Graphics.FromImage(Cache); return Convert.ToInt32(g.MeasureString(Item.Name, Font).Width + DepthSize); } } /// /// Строковое представление типа поля. /// public string TypeText { get { string result = Item.GetTypeString(); if (result != "") { result = $@"({result})"; } return result; } } /// /// Шрифт для отображения типа поля. /// public Font TypeFont => new(Font.FontFamily, TypeFontSize); /// /// Строковое представление значения поля. /// public string ValueText => Item.GetValueString(); /// /// Шрифт для отображения значения поля. /// public Font ValueFont => new( Font.FontFamily, Font.Size, Item.FieldType == StructureFieldType.Complex ? FontStyle.Italic : FontStyle.Regular ); /// /// Прямоугольник, занимаемый строкой в представлении. /// public RectangleF Target => new(0, ViewIndex * Height, Width, Height); /// /// Прямоугольник, занимаемый строкой в родительском представлении. /// public RectangleF RealTarget => new(0, ViewIndex * Height - ParentControl.VerticalScroll.Value, Width, Height); #endregion /// /// Список данных для рендеринга строк. /// private readonly List _renderData = []; #region "Основные методы" /// /// Создание элемента управления строки поля сущности. /// /// Родительский элемент управления. /// Строка поля структуры объекта. public EntityTreeViewerControlRow(EntityTreeViewerControl parent, StructureField item) { ParentControl = parent; Item = item; ApplyItem(); UpdateCache(); UpdateButton(); } /// /// Применение свойств к элементу управления. /// 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); } } /// /// Обновление кэша изображения строки. /// public void UpdateCache() { Cache = RenderCache(BackColor); HoveredCache = RenderCache(HoverColor); } /// /// Создание кэша изображения строки. /// /// Цвет заднего фона. /// Кеш изображения строки. 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; } /// /// Запрос на перерисовку строки. /// /// Графический объект. public void Invalidate(Graphics? g = null) { g ??= Graphics.FromImage(ParentControl.ContentImage); g.DrawImage(Hovered ? HoveredCache : Cache, Target); UpdateButton(); } #endregion #region "Кнопка" /// /// Обновление кнопки раскрытия/сворачивания. /// 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; } /// /// Событие нажатия кнопки раскрытия/сворачивания. /// /// Источник события. /// Событие нажатие кнопки private void Button_Click(object? sender, EventArgs e) { if (Item is StructureFieldComplex complex) { complex.Collapsed = !complex.Collapsed; ParentControl.UpdateView(); } } #endregion #region "Отрисовка" /// /// Отрисовка лестницы. /// /// Графический объект. 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); } } } /// /// Отрисовка содержимого строки. /// /// Графический объект. 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 Функции-помощники в отрисовке /// /// Смешивание цвета с цветом текста. /// /// Цвет оттенка. /// Степень смешения. /// Смешенный цвет. private Color BlendColor(Color color, double rate = 0.5) { return BlendColor(ForeColor, color, rate); } /// /// Смешивание цветов. /// /// Первый смешиваемый цвет. /// Второй смешиваемый цвет. /// Степень смешения. /// Смешенный цвет. 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))); } /// /// Получение цвета текста по типу значения поля. /// /// Цвет текста. 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 /// /// Класс для хранения данных рендеринга строки. /// /// Текст отображаемой строки. /// Шрифт отображаемой строки. /// Цвет отображаемой строки. private class StringRenderData(string text, Font font, Color color) { /// /// Текст отображаемой строки. /// public string Text { get; } = text; /// /// Шрифт отображаемой строки. /// public Font Font { get; } = font; /// /// Цвет отображаемой строки. /// public Color Color { get; } = color; /// /// Левый край отображаемой строки. /// public float Left { get; set; } /// /// Ширина отображаемой строки. /// public float Width { get; set; } /// /// Высота отображаемой строки. /// public float Height { get; set; } /// /// Прямоугольник, занимаемый строкой. /// public RectangleF Rect => new(Left, 0, Width, Height); /// /// Уместить строку в занимаемый прямоугольник. /// /// Элемент графики. /// Занимаемый прямоугольник /// Строка, на которую будет заканчиваться значнеие при переполнении. /// 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; } /// /// Отрисовка строки. /// /// Элемент графики. 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, } ); } } }