【问题标题】:How to move all controls on a form based on mouse movement at runtime如何在运行时根据鼠标移动移动表单上的所有控件
【发布时间】:2020-01-29 20:40:09
【问题描述】:

我在我的 winforms 应用程序中执行某个任务时遇到了一点小问题。

我基本上是在尝试在 winform 上重新创建“Top-View RTS Map”。为了节省内存,并非​​“地图”的所有图块都显示在屏幕上。只有那些适合视口的。因此,我试图允许用户在显示的图块上执行 pan/scroll 以导航整个地图!

现在,我通过在运行时动态创建和显示GroupBox 控件来做到这一点。这些代表瓷砖......

我创建了自己的对象来支持所有这些(包含屏幕坐标、行和列信息等)

这是我目前用伪代码完成所有这些的方式:

一般地创建表单、图块和地图

  1. 我创建了一个 600px X 600px 的 winforms 表单。

  2. 我创建了一个新的“地图”(使用 List<MapTile>),在表单加载时它是 100 瓦乘 100 瓦(用于测试),并将其保存到一个变量中。

  3. 我通过另一个列表(或从主列表 bool MapTile.isDrawn 派生的属性)跟踪显示的图块

  4. 每个图块在视觉上由一个 100 像素的 GroupBox 控件组成 X 100 像素(因此 [7 X 7] 适合屏幕)

  5. 首先,我在“地图”中找到中心 MapTile(平铺 [50, 50]),为其创建 GroupBox 并将其放在表单的中间,

  6. 然后我添加填写表单所需的其他磁贴/控件(中心 - 3 个磁贴,中心 + 3 个磁贴(上、下、左和右))。

  7. 当然,每个图块都会订阅适当的鼠标事件来执行拖动操作

  8. 当用户鼠标拖动一个图块时,通过更新所有“显示的图块”坐标以匹配“拖动”图块所做的移动,所有其他显示的图块都跟随/跟随领导者。

管理显示的图块

  1. 在拖动/移动 GroupBox 磁贴时,我会检查视口外边缘的磁贴是否在其范围内。
  2. 例如,如果最左上角图块的 边缘超出视口左边缘的边界,我会删除整个左列图块,并添加整个右侧以编程方式显示列图块。所有方向(上、下、左和右)都是如此。

到目前为止,只要我不走得太快,它就可以正常工作......但是,当我拖动瓷砖“太快”通过外边缘时(例如:第 2 点 ci-dessus 将适用的地方),似乎应用程序无法跟上,因为它没有在表单上添加它们应该在的列或行,而其他时候,它没有时间删除行或列的所有控件,我最终当它们不应该出现在屏幕上时,它们仍然在屏幕上。那时,整个网格/地图失去平衡并停止按预期工作,因为应该在一个边缘触发的事件没有(图块不存在)和/或现在有多个具有相同名称的控件表单和删除或引用失败...

虽然我很清楚 winforms 并非旨在执行密集的 GPU/GDI 操作,但您会认为这种简单的事情在 winforms 中仍然可以轻松实现吗?

我将如何在运行时使其更具响应性?这是我的整套代码:

表单代码

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();
    }


}
}

【问题讨论】:

  • 我会创建网格(使用 DataGridView、TableLayoutPanel、GDI+ 或其他),然后在拖放中,只计算新索引并更新索引,而不移动网格。跨度>

标签: c# .net winforms gdi+


【解决方案1】:

我会使用(DataGridViewTableLayoutPanelGDI+ 或其他)创建网格,然后在拖放中,只计算新索引并更新索引,而不移动网格。

示例

以下示例显示了如何使用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;
    }
}

【讨论】:

  • 这很好,并且“sorta”做了我想要它做的事情......但是虽然它是我试图实现的功能概念的解决方案,但它仍然超出了问题,即:如何使用提供的格式正确地做到这一点?例如,您现在的解决方案的一个缺点是它超出了地图/不会停在 0(-1 或更多),而且,除了数字变化之外,它不会直观地显示地图移动...另一种方式/原始方式,显示运动......
  • 它超出地图/不在0处停止 → 然后通过检查索引值轻松停止。
  • 它不会直观地显示地图正在移动 如果您更喜欢它移动,只需移动 TableLayoutPanel 并摆脱 GroupBox 组。
  • 我个人会使用基于单个自定义绘制控件的解决方案。所有渲染都将基于模型在我的自定义控件的OnPain 方法中完成。不过帖子够长了。我试图通过一个简单的代码来解释这个想法,我相信你明白了:) 你可能可以在 GDI+ 呈现的自定义控件中通过更高的性能和更好的视觉支持来做同样的事情。
【解决方案2】:

因此,对于任何尝试这样做的人,作为一个概念,以下是解决此问题的方法:

  1. 不要只在视口外额外绘制 1 行/列以节省内存,而是在边缘的每个方向(上、下、左和右)上绘制整个视口的单元格...例如,如果您的视口可以容纳 5 个图块 (5 X 5 = 25),那么您需要在视口外沿其他方向绘制 5 X 5 (25 X 4 = 100)...

  2. 当鼠标被拖动时,只需移动已经在表单/控件/“绘制”上的控件...这样,用户在拖动时不能超出现有图块的范围...例如,如果他们用鼠标到达右外边缘,同时拖动最左边的瓷砖,左侧显示的瓷砖已经存在!所以我们只是“跟随鼠标”,如果控件已经存在/没有“丢失/问题”,这不是问题,因为我们此时没有删除或添加任何图块......

  3. 当用户停止拖动选定的图块 (onMouseUp) 时,我们会重新计算需要绘制的图块和不需要绘制的图块......所以我们只重绘(添加和/或在必要时删除控件)用户完成拖动后的整个“绘制”图块集......

使用此方法,您可以删除任何“错位”控件、控件的双重生成、控件丢失以及当鼠标移动得太快而无法执行“计算绘制的图块”代码时出现的任何其他问题。您还可以在拖动时“看到”地图四处移动,并且您始终在屏幕上绘制正确的图块!问题解决了!

但是,我确实发现,当我使用 UserControl 而不是表单本身时,控件的绘制和更新速度比将它们添加到表单本身要快得多,而且效果更好......因此,我接受了将这方面概述为实际答案的答案,并将其放在此处以供将来可能想知道如何将其作为概念执行的任何其他人使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-11
    • 2020-03-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多