Как переместить все элементы управления в форме на основе движения мыши во время выполнения - PullRequest
1 голос
/ 29 января 2020

У меня небольшая проблема с выполнением определенной задачи в моем приложении winforms.

Я в основном пытаюсь воссоздать "Карту RTS с топ-обзором" на winform. В целях экономии памяти на экране отображаются не все плитки «Карты». Только те, которые вписываются в область просмотра. Поэтому я пытаюсь разрешить пользователю выполнять панорамирование / прокрутка для отображаемых плиток, чтобы перемещаться по всей карте!

Сейчас я делаю это, создавая и отображая GroupBox элементы управления динамически во время выполнения. Они представляют плитки ...

Я создал свои собственные объекты для поддержки всего этого (содержит координаты экрана, информацию о строках и столбцах и т. Д. c.)

Вот как я В настоящее время я выполняю все это в псевдокоде:

Создание формы, плиток и карты в целом

  1. Я создаю winforms форма размером 600px X 600px.

  2. Я создаю новую «Карту» (используя List<MapTile>) размером 100 на 100 плиток (для тестирования) при загрузке формы и сохраняю ее в переменную.

  3. Я отслеживаю отображаемые плитки через другой список (или свойство, которое получается из основного списка bool MapTile.isDrawn)

  4. Каждая плитка визуально сделана из GroupBox элемента управления размером 100px X 100px (поэтому [7 X 7] из них помещается на экране)

  5. Для начала Я нахожу центр MapTile (плитка [50, 50]) на «Карте», создаю для него GroupBox и помещаю его в середине формы,

  6. Затем я добавляю ее плитки / элементы управления, необходимые для заполнения формы (в центре - 3 плитки, в центре + 3 плитки (вверх, вниз, влево и вправо)).

  7. Каждая плитка, конечно, подписывается на соответствующие события мыши для выполнения перетаскивания

  8. Когда пользовательская мышь перетаскивает плитку, все остальные отображаемые плитки следуют примеру или следуют за лидером, обновляя все координаты «отображаемых плиток» до соответствует движению, которое было сделано «перетаскиваемой» плиткой.

how the map looks when the form loads for the first time

Управление отображаемыми плитками

  1. Пока перетаскиваются / перемещаются плитки GroupBox, я проверяю, находятся ли плитки, которые находятся на внешнем крае области просмотра, в пределах его границ.
  2. Если Например, правый край верхнего левого тайла выходит за границы левого края окна просмотра, я удаляю все левые плитки столбцов и программно добавляю все правые плитки столбцов. То же самое распространяется во всех направлениях (вверх, вниз, влево и вправо).

Пока это работает нормально, пока я не go слишком быстро ... однако, когда я перетаскиваю плитки "слишком быстро" перешли внешний край (например, там, где будет применяться пункт 2 ci-dessus), кажется, что приложение не может идти в ногу, потому что оно не добавляет столбец или строку, где они должны быть в форме и в других случаях у него нет времени, чтобы удалить все элементы управления строки или столбца, и я получаю элементы управления, которые все еще находятся на экране, когда их там быть не должно. В этот момент вся сетка / карта вышли из равновесия и перестали работать, как предполагалось, потому что либо события, которые должны возникать на одном ребре, не работают (плитки не присутствуют), и / или теперь есть несколько элементов управления с одинаковым именем на форма и удаление или ссылки не удается ...

what the map looks like when it fails

Хотя я хорошо знаю, что winforms не предназначен для выполнения интенсивных операций GPU / GDI, вы мог бы подумать, что что-то такое простое все еще будет легко выполнимо в winforms?

Как мне go сделать это более отзывчивым во время выполнения? Вот весь мой набор кода:

Код формы

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace RTSAttempt
{
public enum DrawChange
{
    None,
    Rem_First_Draw_Last,
    Rem_Last_Draw_First
};

public partial class Form1 : Form
{
    public string selected { get; set; }
    private int _xPos { get; set; }
    private int _yPos { get; set; }
    private bool _dragging { get; set; }
    public List<MapTile> mapTiles { get; set; }
    public List<MapTile> drawnTiles { get { return this.mapTiles.Where(a => a.Drawn == true).ToList(); } }

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //init globals
        this.selected = "";
        this._dragging = false;
        this.mapTiles = new List<MapTile>();

        //for testing, let's do 100 x 100 map
        for (int i = 0; i < 100; i++)
        {
            for (int x = 0; x < 100; x++)
            {
                MapTile tile = new MapTile(x, i, false, -1, -1, false);
                this.mapTiles.Add(tile);
            }
        }

        GenerateStartupTiles();
    }

    /// <summary>
    /// Used to generate the first set of map tiles on screen and dispaly them.
    /// </summary>
    private void GenerateStartupTiles()
    {

        //find center tile based on list size
        double center = Math.Sqrt(this.mapTiles.Count);

        //if not an even number of map tiles, we take the next one after the root.
        if (this.mapTiles.Count % 2 != 0)
            center += 1;

        //now that we have the root, we divide by 2 to get the true center tile.
        center = center / 2;

        //get range of tiles to display...
        int startat = (int)center - 3;
        int endat = (int)center + 3;

        //because the screen is roughly 600 by 600, we can display 7 X 7 tiles...
        for (int row = 0; row < 7; row++)
        {
            for (int col = 0; col < 7; col++)
            {
                //get the current tile we are trying to display.
                MapTile tile = mapTiles.First(a => a.Row == (startat + row) && a.Col == (startat + col));

                //create and define the GroupBox control we use to display the tile on screen.
                GroupBox pct = new GroupBox();
                pct.Width = 100;
                pct.Height = 100;

                //find start position on screen
                if (row == 0)
                    pct.Top = -50;
                else
                    pct.Top = -50 + (row * 100);

                if (col == 0)
                    pct.Left = -50;
                else
                    pct.Left = -50 + (col * 100);

                tile.X = pct.Left;
                tile.Y = pct.Top;

                pct.Name = tile.ID;
                pct.Tag = Color.LightGray;

                //subscribe to necessary events.
                pct.MouseEnter += Pct_MouseEnter;
                pct.MouseLeave += Pct_MouseLeave;
                pct.Click += Pct_Click;
                pct.Paint += Pct_Paint;
                pct.MouseDown += Pct_MouseDown;
                pct.MouseMove += Pct_MouseMove;
                pct.MouseUp += Pct_MouseUp;
                pct.Text = tile.DisplayID;
                //add the tile to the screen
                this.Controls.Add(pct);
                //set the tile to Drawn mode...
                tile.Drawn = true;
            }
        }
    }

    private void Pct_MouseUp(object sender, MouseEventArgs e)
    {
        //self explanatory
        if (this._dragging)
        {
            Cursor.Current = Cursors.Default;
            this._dragging = false;
        }
    }

    private void Pct_MouseMove(object sender, MouseEventArgs e)
    {
        var c = sender as GroupBox;
        if (!_dragging || null == c) return;

        //get original position, and movement step/distance for calcs.
        int newTop = e.Y + c.Top - _yPos;
        int newLeft = e.X + c.Left - _xPos;
        int movedByX = this.drawnTiles.First(a => a.ID.ToString() == c.Name).X;
        int movedByY = this.drawnTiles.First(a => a.ID.ToString() == c.Name).Y;
        movedByY = newTop - movedByY;
        movedByX = newLeft - movedByX;
        //perform all tile movements here
        MoveAllTiles(movedByX, movedByY);
    }
    /// <summary>
    /// This method performs all tile movements on screen, and updates the listing properly.
    /// </summary>
    /// <param name="X">int - the amount fo pixels that the dragged tile has moved horizontally</param>
    /// <param name="Y">int - the amount fo pixels that the dragged tile has moved vertically</param>
    private void MoveAllTiles(int X, int Y)
    {
        //used to single out the operation, if any, that we need to do after this move (remove row or col, from edges)
        DrawChange colAction = DrawChange.None;
        DrawChange rowAction = DrawChange.None;

        //move all tiles currently being displayed first... 
        for (int i = 0; i < this.drawnTiles.Count; i++)
        {
            //first, determine new coordinates of tile.
            drawnTiles[i].Y = drawnTiles[i].Y + Y;
            drawnTiles[i].X = drawnTiles[i].X + X;

            //find the control
            GroupBox tmp = this.Controls.Find(drawnTiles[i].ID, true)[0] as GroupBox;

            //perform screen move
            tmp.Top = drawnTiles[i].Y;
            tmp.Left = drawnTiles[i].X;
            tmp.Refresh();
        }

        //dtermine which action to perform, if any...
        if (drawnTiles.Last().Y > this.Height)
            rowAction = DrawChange.Rem_Last_Draw_First;
        else if ((drawnTiles.First().Y + 100) < 0)
            rowAction = DrawChange.Rem_First_Draw_Last;
        else
            rowAction = DrawChange.None;

        if ((drawnTiles.First().X + 100) < 0)
            colAction = DrawChange.Rem_First_Draw_Last;
        else if (drawnTiles.Last().X > this.Width)
            colAction = DrawChange.Rem_Last_Draw_First;
        else
            colAction = DrawChange.None;

        //get currently dispalyed tile range.
        int startRow = this.drawnTiles.First().Row;
        int startCol = this.drawnTiles.First().Col;
        int endRow = this.drawnTiles.Last().Row;
        int endCol = this.drawnTiles.Last().Col;

        //perform the correct action(s), if necessary.

        if (rowAction == DrawChange.Rem_First_Draw_Last)
        {
            //remove the first row of tiles from the screen
            this.drawnTiles.Where(a => a.Row == startRow).ToList().ForEach(a => { a.Drawn = false; this.Controls.RemoveByKey(a.ID); this.Refresh(); });

            //add the last row of tiles on screen... 
            List<MapTile> TilesToAdd = this.mapTiles.Where(a => a.Row == endRow + 1 && a.Col >= startCol && a.Col <= endCol).ToList();
            int newTop = this.drawnTiles.Last().Y + 100;
            for (int i = 0; i < TilesToAdd.Count; i++)
            {
                int newLeft = (i == 0 ? drawnTiles.First().X : drawnTiles.First().X + (i * 100));
                //create and add the new tile, and set it to Drawn = true.
                GroupBox pct = new GroupBox();
                pct.Name = TilesToAdd[i].ID.ToString();
                pct.Width = 100;
                pct.Height = 100;
                pct.Top = newTop;
                TilesToAdd[i].Y = newTop;
                pct.Left = newLeft;
                TilesToAdd[i].X = newLeft;
                pct.Tag = Color.LightGray;
                pct.MouseEnter += Pct_MouseEnter;
                pct.MouseLeave += Pct_MouseLeave;
                pct.Click += Pct_Click;
                pct.Paint += Pct_Paint;
                pct.MouseDown += Pct_MouseDown;
                pct.MouseMove += Pct_MouseMove;
                pct.MouseUp += Pct_MouseUp;
                pct.Text = TilesToAdd[i].DisplayID;
                this.Controls.Add(pct);
                TilesToAdd[i].Drawn = true;
            }
        }
        else if (rowAction == DrawChange.Rem_Last_Draw_First)
        {
            //remove last row of tiles
            this.drawnTiles.Where(a => a.Row == endRow).ToList().ForEach(a => { a.Drawn = false; this.Controls.RemoveByKey(a.ID); this.Refresh(); });

            //add first row of tiles
            List<MapTile> TilesToAdd = this.mapTiles.Where(a => a.Row == startRow - 1 && a.Col >= startCol && a.Col <= endCol).ToList();
            int newTop = this.drawnTiles.First().Y - 100;
            for (int i = 0; i < TilesToAdd.Count; i++)
            {
                int newLeft = (i == 0 ? drawnTiles.First().X : drawnTiles.First().X + (i * 100));
                //create and add the new tile, and set it to Drawn = true.
                GroupBox pct = new GroupBox();
                pct.Name = TilesToAdd[i].ID.ToString();
                pct.Width = 100;
                pct.Height = 100;
                pct.Top = newTop;
                TilesToAdd[i].Y = newTop;
                pct.Left = newLeft;
                TilesToAdd[i].X = newLeft;
                pct.Tag = Color.LightGray;
                pct.MouseEnter += Pct_MouseEnter;
                pct.MouseLeave += Pct_MouseLeave;
                pct.Click += Pct_Click;
                pct.Paint += Pct_Paint;
                pct.MouseDown += Pct_MouseDown;
                pct.MouseMove += Pct_MouseMove;
                pct.MouseUp += Pct_MouseUp;
                pct.Text = TilesToAdd[i].DisplayID;
                this.Controls.Add(pct);
                TilesToAdd[i].Drawn = true;
            }
        }

        if (colAction == DrawChange.Rem_First_Draw_Last)
        {
            //remove the first column of tiles
            this.drawnTiles.Where(a => a.Col == startCol).ToList().ForEach(a => { a.Drawn = false; this.Controls.RemoveByKey(a.ID); this.Refresh(); });


            //add the last column of tiles
            List<MapTile> TilesToAdd = this.mapTiles.Where(a => a.Col == endCol + 1 && a.Row >= startRow && a.Row <= endRow).ToList();
            int newLeft = this.drawnTiles.Last().X + 100;
            for (int i = 0; i < TilesToAdd.Count; i++)
            {
                int newTop = (i == 0 ? drawnTiles.First().Y : drawnTiles.First().Y + (i * 100));
                //create and add the new tile, and set it to Drawn = true.
                GroupBox pct = new GroupBox();
                pct.Name = TilesToAdd[i].ID.ToString();
                pct.Width = 100;
                pct.Height = 100;
                pct.Top = newTop;
                TilesToAdd[i].Y = newTop;
                pct.Left = newLeft;
                TilesToAdd[i].X = newLeft;
                pct.Tag = Color.LightGray;
                pct.MouseEnter += Pct_MouseEnter;
                pct.MouseLeave += Pct_MouseLeave;
                pct.Click += Pct_Click;
                pct.Paint += Pct_Paint;
                pct.MouseDown += Pct_MouseDown;
                pct.MouseMove += Pct_MouseMove;
                pct.MouseUp += Pct_MouseUp;
                pct.Text = TilesToAdd[i].DisplayID;
                this.Controls.Add(pct);
                TilesToAdd[i].Drawn = true;
            }
        }
        else if (colAction == DrawChange.Rem_Last_Draw_First)
        {
            //remove last column of tiles
            this.drawnTiles.Where(a => a.Col == endCol).ToList().ForEach(a => { a.Drawn = false; this.Controls.RemoveByKey(a.ID); this.Refresh(); });

            //add first column of tiles
            List<MapTile> TilesToAdd = this.mapTiles.Where(a => a.Col == startCol - 1 && a.Row >= startRow && a.Row <= endRow).ToList();
            int newLeft = this.drawnTiles.First().X - 100;
            for (int i = 0; i < TilesToAdd.Count; i++)
            {
                int newTop = (i == 0 ? drawnTiles.First().Y : drawnTiles.First().Y + (i * 100));
                //create and add the new tile, and set it to Drawn = true.
                GroupBox pct = new GroupBox();
                pct.Name = TilesToAdd[i].ID.ToString();
                pct.Width = 100;
                pct.Height = 100;
                pct.Top = newTop;
                TilesToAdd[i].Y = newTop;
                pct.Left = newLeft;
                TilesToAdd[i].X = newLeft;
                pct.Tag = Color.LightGray;
                pct.MouseEnter += Pct_MouseEnter;
                pct.MouseLeave += Pct_MouseLeave;
                pct.Click += Pct_Click;
                pct.Paint += Pct_Paint;
                pct.MouseDown += Pct_MouseDown;
                pct.MouseMove += Pct_MouseMove;
                pct.MouseUp += Pct_MouseUp;
                ToolTip tt = new ToolTip();
                tt.SetToolTip(pct, pct.Name);
                pct.Text = TilesToAdd[i].DisplayID;
                this.Controls.Add(pct);
                TilesToAdd[i].Drawn = true;
            }
        }
    }

    private void Pct_MouseDown(object sender, MouseEventArgs e)
    {
        //self explanatory
        if (e.Button != MouseButtons.Left) return;
        _dragging = true;
        _xPos = e.X;
        _yPos = e.Y;
    }

    private void Pct_Click(object sender, EventArgs e)
    {
        //changes the border color to reflect the selected tile... 
        if (!String.IsNullOrWhiteSpace(selected))
        {
            if (this.Controls.Find(selected, true).Length > 0)
            {
                GroupBox tmp = this.Controls.Find(selected, true)[0] as GroupBox;
                ControlPaint.DrawBorder(tmp.CreateGraphics(), tmp.ClientRectangle, Color.LightGray, ButtonBorderStyle.Solid);
            }
        }

        GroupBox pct = sender as GroupBox;
        ControlPaint.DrawBorder(pct.CreateGraphics(), pct.ClientRectangle, Color.Red, ButtonBorderStyle.Solid);
        this.selected = pct.Name;
    }

    private void Pct_Paint(object sender, PaintEventArgs e)
    {
        //draws the border based on the correct tag.
        GroupBox pct = sender as GroupBox;
        Color clr = (Color)pct.Tag;
        ControlPaint.DrawBorder(e.Graphics, pct.ClientRectangle, clr, ButtonBorderStyle.Solid);
    }

    private void Pct_MouseLeave(object sender, EventArgs e)
    {
        //draws the border back to gray, only if this is not the selected tile...
        GroupBox pct = sender as GroupBox;
        if (this.selected != pct.Name)
        {
            pct.Tag = Color.LightGray;
            pct.Refresh();
        }
    }

    private void Pct_MouseEnter(object sender, EventArgs e)
    {
        //draws a red border around the tile to show which tile the mouse is currently hovering on...
        GroupBox pct = sender as GroupBox;
        pct.Tag = Color.Red;
        pct.Refresh();
    }
}
}

MapTile объект

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RTSAttempt
{

public class MapTile
{
    /// <summary>
    /// Represents the row of the tile on the map
    /// </summary>
    public int Row { get; set; }
    /// <summary>
    /// Represents the column of the tile on the map
    /// </summary>
    public int Col { get; set; }
    /// <summary>
    /// Represents the ID of this tile ([-1,-1], [0,0], [1,1], etc
    /// </summary>
    public string ID { get { return "Tile_" + this.Row + "_" + this.Col; } }

    public string DisplayID { get { return this.Row + ", " + this.Col; } }
    /// <summary>
    /// If this tile is currently selected or clicked.
    /// </summary>
    public bool Selected { get; set; }
    /// <summary>
    /// Represents the X screen coordinates of the tile
    /// </summary>
    public int X { get; set; }
    /// <summary>
    /// Represents the Y screen coordinates of the tile
    /// </summary>
    public int Y { get; set; }
    /// <summary>
    /// Represents whether this tile is currently being drawn on the screen. 
    /// </summary>
    public bool Drawn { get; set; }


    public MapTile(int idCol = -1, int idRow = -1, bool selected = false, int screenX = -1, int screenY = -1, bool drawn = false)
    {
        this.Col = idCol;
        this.Row = idRow;
        this.Selected = selected;
        this.X = screenX;
        this.Y = screenY;
        this.Drawn = drawn;
    }

    public override bool Equals(object obj)
    {
        MapTile tmp = obj as MapTile;
        if (tmp == null)
            return false;

        return this.ID == tmp.ID;
    }

    public override int GetHashCode()
    {
        return this.ID.GetHashCode();
    }


}
}

Ответы [ 2 ]

2 голосов
/ 30 января 2020

Я бы создал сетку, используя (DataGridView, TableLayoutPanel, GDI+ или что-то еще), а затем перетаскивая, просто вычислял новые индексы и обновлял индексы, не перемещая сетку.

Пример

В следующем примере показано, как это сделать, используя TableLayoutPanel:

  • Назначить фиксированный размер ячейкам
  • Построение сетки для заполнения формы
  • Когда размер формы изменится, перестройте сетку
  • При наведении курсора мыши наведите курсор мыши вниз и текущий верхний левый индекс сетки
  • При перемещении мыши вычислите новый индекс на основе движения мыши и обновите индекс
  • . В области рисования ячеек на панели нарисуйте индексы

Вот код:

int topIndex = 0, leftIndex = 0;
int originalLeftIndex = 0, originalTopIndex = 0;
int cellSize = 100;
Point p1;
TableLayoutPanel panel;
void LayoutGrid()
{
    panel.SuspendLayout();
    var columns = (ClientSize.Width / cellSize) + 1;
    var rows = (ClientSize.Height / cellSize) + 1;
    panel.RowCount = rows;
    panel.ColumnCount = columns;
    panel.ColumnStyles.Clear();
    panel.RowStyles.Clear();
    for (int i = 0; i < columns; i++)
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, cellSize));
    for (int i = 0; i < rows; i++)
        panel.RowStyles.Add(new RowStyle(SizeType.Absolute, cellSize));
    panel.Width = columns * cellSize;
    panel.Height = rows * cellSize;
    panel.CellBorderStyle = TableLayoutPanelCellBorderStyle.Single;
    panel.ResumeLayout();
}
protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);
    panel = new MyGrid();
    this.Controls.Add(panel);
    LayoutGrid();
    panel.MouseDown += Panel_MouseDown;
    panel.MouseMove += Panel_MouseMove;
    panel.CellPaint += Panel_CellPaint;
}
protected override void OnSizeChanged(EventArgs e)
{
    base.OnSizeChanged(e);
    if (panel != null)
        LayoutGrid();
}
private void Panel_CellPaint(object sender, TableLayoutCellPaintEventArgs e)
{
    var g = e.Graphics;
    TextRenderer.DrawText(g, $"({e.Column + leftIndex}, {e.Row + topIndex})",
        panel.Font, e.CellBounds, panel.ForeColor);
}
private void Panel_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        var dx = (e.Location.X - p1.X) / cellSize;
        var dy = (e.Location.Y - p1.Y) / cellSize;
        leftIndex = originalLeftIndex - dx;
        topIndex = originalTopIndex - dy;
        panel.Invalidate();
    }
}
private void Panel_MouseDown(object sender, MouseEventArgs e)
{
    p1 = e.Location;
    originalLeftIndex = leftIndex;
    originalTopIndex = topIndex;
}

Для предотвращения мерцания:

public class MyGrid : TableLayoutPanel
{
    public MyGrid()
    {
        DoubleBuffered = true;
    }
}
1 голос
/ 30 января 2020

Итак, для тех, кто пытается сделать это, в качестве концепции, вот как исправить эту проблему:

  1. Вместо того, чтобы рисовать дополнительно 1 строку / столбец вне области просмотра для сохранения памяти, нарисуйте все ячейки видового экрана в каждом направлении по краям (вверх, вниз, влево и вправо) ... например, если ваш видовой экран может содержать 5 плиток (5 X 5 = 25), то вам нужно нарисовать 5 X 5 вне области просмотра в любом другом направлении (25 X 4 = 100) ...

  2. Когда мышь перетаскивают, просто переместите элементы управления, которые уже находятся в форме / control / "draw" ... таким образом, пользователь не может при перетаскивании go выйти за границы существующих плиток ... например, если они достигают внешнего правого края с помощью мыши, перетаскивая крайний левый плитка, плитки для показа слева уже существуют! Таким образом, мы просто «следуем за мышью», что не является проблемой, если элементы управления уже есть / нет «потерь / проблем», потому что мы не удаляем и не добавляем никакие плитки в этой точке ...

  3. Когда пользователь прекращает перетаскивать выбранную плитку вокруг (onMouseUp), ТОГДА мы пересчитываем плитки, которые должны быть нарисованы, и плитки, которые не ... поэтому мы только перерисовываем (добавляем и / или при необходимости удалите элементы управления) весь набор «нарисованных» плиток после перетаскивания пользователем ...

С помощью этого метода вы удаляете все «неправильно расположенные» элементы управления, двойное поколение элементов управления, отсутствующих элементов управления и любых других проблем, возникающих, когда мышь перемещается слишком быстро для выполнения кода «Рассчитать нарисованные плитки». Вы также «видите» карту, перемещающуюся при перетаскивании, и у вас всегда отображаются правильные плитки на экране! Проблема решена!

Однако я обнаружил, что когда вместо формы используется UserControl, элементы управления рисуются и обновляются намного быстрее и лучше, чем если бы я просто добавил их в саму форму ... поэтому я принял ответ, который обрисовывает этот аспект в качестве фактического ответа, и поместил его здесь для любого другого в будущем, который может задаться вопросом, как сделать это как концепцию.

...