如何在运行时根据鼠标移动移动窗体上的所有控件

How to move all controls on a form based on mouse movement at runtime

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

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

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

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

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

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

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

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

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

  4. 每个图块 在视觉上 由 100px 的 GroupBox 控件组成 X 100px(所以 [7 X 7] 适合屏幕)

  5. 首先,我在 "Map" 中找到中心 MapTile(方块 [50, 50]),为其创建 GroupBox 并将其放置在表格中间,

  6. 然后我添加填写表格所需的其他 tiles/controls(中心 - 3 个方块,中心 + 3 个方块(上、下、左、右))。

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

  8. 当用户鼠标拖动一个图块时,显示的所有其他图块都会跟随 suit/follow 领导者更新所有 "displayed tiles" 坐标以匹配由 "dragged" 瓷砖.

管理显示的图块

  1. GroupBox 个图块处于 dragged/moved 时,我执行检查以查看位于视口外边缘的图块是否在其范围内。
  2. 例如,如果最左上角的图块的 边缘落在视口左边缘的边界之外,我将移除整个左列图块,并以编程方式添加整个右列图块。所有方向(上、下、左、右)都一样。

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

虽然我很清楚 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();
    }


}
}

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

示例

以下示例显示了如何使用 TableLayoutPanel:

  • 为单元格指定固定大小
  • 构建网格以填充表格
  • 当表单调整大小时,重建网格
  • In mouse down capture the mouse down point and current top left index of grid
  • 在鼠标移动中,根据鼠标移动计算新索引并更新索引
  • 在面板的cell paint中,绘制索引

代码如下:

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. 与其仅在视口外额外绘制 1 row/col 以节省内存,不如在边缘的各个方向(上、下、左、右)绘制整个视口的单元格价值...例如,如果您的视口可以容纳 5 个图块 (5 X 5 = 25),那么您需要在视口外的每个其他方向上绘制 5 X 5 (25 X 4 = 100)...

  2. 拖动鼠标时,只需移动form/control/"drawn"上已有的控件...这样,用户不能在拖动时,超出现有图块的边界...例如,如果它们用鼠标到达右外边缘,同时拖动最左侧的图块,则显示在左侧的图块已经存在!所以我们只是 "following the mouse",如果控件已经是 there/there 就不是问题 不是 "loss/issues" 因为此时我们没有删除或添加任何图块...

  3. 当用户停止拖动所选图块时 (onMouseUp),THEN 我们重新计算需要绘制的图块和不需要绘制的图块...所以我们仅在用户完成拖动后重绘(添加 and/or 在必要时删除控件)整组 "drawn" 图块...

使用此方法,您可以删除任何 "missplaced" 控件、控件的双重生成、控件丢失以及鼠标移动太快而无法执行 "Calculate drawn tiles" 代码时出现的任何其他问题。您还 "see" 地图在您拖动时四处移动,并且您始终在屏幕上绘制正确的图块!问题已解决!

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