WinForms: ToolTips will not (re)appear after the initial mouse hover - c#

I am working on a form that uses a SharpMap MapBox to display objects as points on a world map. Currently, if I enter the MapBox (mapBox1) with the cursor and stop on the point, it displays the tooltip as I want it to. However, once I've stopped the mouse within the MapBox (not necessarily on the point) and move the mouse within the MapBox, moving to the point will not (re)display the tooltip. However, if I leave the MapBox (say, moving the cursor out of the window or onto one of the menu strips, or onto a button overlaid on the map) I can then get the tooltip to appear, but only once before I have to move the cursor as before.
What is causing this behaviour, and is there any easy way to fix it?
I've tried using ToolTip.Hide(), ToolTip.Active = false (and then setting it to true again when I want it displayed) and refreshing the MapBox at various points.
Relevant code:
The ToolTip is global, and the constructor defines it as follows:
toolTip.InitialDelay = 1000;
toolTip.ReshowDelay = 750;
toolTip.ShowAlways = true;
I then have two event handlers for the mouse, both tied to the MapBox. "obj" is a global object of a custom class containing the latitude and longitude points.
private void mapBox1_MouseHover(object sender, EventArgs e)
{
PointF pos = mapBox1.PointToClient(Cursor.Position);
int screenToleranceX = 20, screenToleranceY = 20;
PointF posLow = new PointF(pos.X - screenToleranceX, pos.Y - screenToleranceY);
PointF posHigh = new PointF(pos.X + screenToleranceX, pos.Y + screenToleranceY);
GeoAPI.Geometries.Coordinate objLoc = new GeoAPI.Geometries.Coordinate(obj.longitude, obj.latitude);
PointF objPoint = mapBox1.Map.WorldToImage(objLoc);
if (posLow.X <= objPoint.X && objPoint.X <= posHigh.X && posLow.Y <= objPoint.Y && objPoint.Y <= posHigh.Y)
{
toolTip.Active = true;
toolTip.Show(obj.Name, mapBox1, mapBox1.PointToClient(Cursor.Position));
}
}
private void mapBox1_MouseMove(GeoAPI.Geometries.Coordinate worldPos, MouseEventArgs imagePos)
{
PointF pos = mapBox1.PointToClient(Cursor.Position);
int screenToleranceX = 20, screenToleranceY = 20;
PointF posLow = new PointF(pos.X - screenToleranceX, pos.Y - screenToleranceY);
PointF posHigh = new PointF(pos.X + screenToleranceX, pos.Y + screenToleranceY);
GeoAPI.Geometries.Coordinate objLoc = new GeoAPI.Geometries.Coordinate(obj.longitude, obj.latitude);
PointF objPoint = mapBox1.Map.WorldToImage(objLoc);
if (toolTip.Active && (posLow.X > objPoint.X || objPoint.X > posHigh.X || posLow.Y > objPoint.Y || objPoint.Y > posHigh.Y))
{
toolTip.Active = false;
}
}
** EDIT **
As per the accepted answer, I have the following code as a solution, with hopes to refine it further as needed. This works for now, however (using an externally declared bool, toolTipDisp, defaulted to false):
private void mapBox1_MouseMove(GeoAPI.Geometries.Coordinate worldPos, MouseEventArgs imagePos)
{
PointF pos = mapBox1.PointToClient(Cursor.Position);
int screenToleranceX = 20, screenToleranceY = 20;
PointF posLow = new PointF(pos.X - screenToleranceX, pos.Y - screenToleranceY);
PointF posHigh = new PointF(pos.X + screenToleranceX, pos.Y + screenToleranceY);
GeoAPI.Geometries.Coordinate objLoc = new GeoAPI.Geometries.Coordinate(obj.longitude, obj.latitude);
PointF objPoint = mapBox1.Map.WorldToImage(objLoc);
if (posLow.X <= objPoint.X && objPoint.X <= posHigh.X && posLow.Y <= objPoint.Y && objPoint.Y <= posHigh.Y)
{
if (!toolTipDisp)
{
toolTip.Show(obj.Name, mapBox1, mapBox1.PointToClient(Cursor.Position));
toolTipDisp = true;
}
}
else
{
toolTip.Hide(mapBox1);
toolTipDisp = false;
}
}

Try this (pseudo-code):
private string _previous;
private void mapBox1_MouseMove(GeoAPI.Geometries.Coordinate worldPos, MouseEventArgs imagePos)
{
var text = ...; // generate tooltip text based on the new position
if(text != _previous)
{
_previous = text;
tooltip.Show(text, mapBox1, mapBox1.PointToClient(imagePos.Location));
}
}
private void mapBox1_MouseLeave(object sender, EventArgs e)
{
toolTip.Hide(mapBox1);
}

Related

How to detect if user is shaking form c# (detect shake),

I've made a paint clone and want to clear the bord when you shake the form, (so by holding mouse1 down on the top bar and moving it right and left fast).
I tried to compare two points on the x-axis, the points are set based on the mouse position and tested against a treshold. And I added a combo so you need to overcome the treshold multiple times to avoid accidental activation, to help with this there is also a timer that resets the combo.
The problem is that it sees fast movement to one side as a shake I still want the user to be able to move the form without clearing the canvas.
int firstpos = 0;
int secondpos = 0;
bool isfirst = true;
int combo = 0;
int threshold = 50;
Point lastPoint;
private void TopBar_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)//to move the form
{
this.Left += e.X - lastPoint.X;
this.Top += e.Y - lastPoint.Y;
}
if (isfirst)
{
firstpos = e.X;
isfirst = false;
}
else
{
secondpos = e.X;
int diff = secondpos - firstpos;
if (diff < 0) diff *= -1;//make positive
if (diff >= threshold)
{
combo++;
Thread t = new Thread(startdecay);
t.Start();
}
if (combo == 4)
{
canvas.Invalidate();//clear the canvas
combo = 0;
}
isfirst = true;
}
}
void startdecay()
{
Thread.Sleep(1000);
combo = 0;
}
private void TopBar_MouseDown(object sender, MouseEventArgs e)
{
lastPoint = new Point(e.X, e.Y);
}
the form
isfirst == true: Save firstpos.
Set isfirst = false.
isfirst == false: Get secondpos.
Get diff.
if Math.Sign(diff) != saveddiff {
increment shakecounter.
Set saveddiff = Math.Sign(diff).
}
Set firstpos = secondpos.
Accept when shakecounter over some value desired.
Don't forget to reset everything when Mouse Button goes up.

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

I am having a bit of a small issue performing a certain task in my winforms application.
I am basically attempting to recreate a "Top-View RTS Map", on a winform. In order to save memory, not all tiles of the "Map" are displayed on the screen. Only the ones that fit within the viewport. Therefore, I am trying to allow the user to perform a pan/scroll on the displayed tiles in order to navigate the entire map!
Right now, I am doing this by creating and displaying GroupBox controls dynamically at runtime. These represent the tiles...
I have created my own objects to support all of this (contains screen coordinates, Row and Col info, etc.)
Here is how I am currently accomplishing all of this, in pseudo-code:
Creating the form, tiles and the map in general
I create a winforms form that is 600px X 600px.
I create a new "Map" (using a List<MapTile>) that is 100 tiles by 100 tiles (for testing) on form load and save that into a variable.
I keep track of the displayed tiles via another list (or property that derives from the main list bool MapTile.isDrawn)
Each tile is visually made of a GroupBox control that is 100px
X 100px (so [7 X 7] of them fit on screen)
To start, I find the center MapTile (tile [50, 50]) in the "Map", create the GroupBox for it and place that in the middle of the form,
I then add the other tiles/controls necessary to fill in the form (center - 3 tiles, center + 3 tiles (up, down, left, and right)).
Each tile, of course, subscribes to the proper mouse events to perform a drag
When the user mouse drags a tile, all other tiles being displayed follow suit/follow the leader by updating all "displayed tiles" coordinates to match the movement that was made by the "dragged" tile.
Managing Displayed Tiles
While the GroupBox tiles are being dragged/moved, I perform a check to see if the tiles that are on the outer edge of the viewport are within its bounds.
If, as an example, the top-left-most tile's right edge falls outside the bounds of the left edge of the viewport, I remove the entire left column tiles, and add the entire right column tiles programmatically. The same goes all directions (up, down, left and right).
So far, this works fine as long as I don't go too fast... however, when I drag the tiles "too fast" passed an outer edge (e.g.: where point 2 ci-dessus would apply), it seems that the application is unable to keep up because it doesn't add the column or row where they should be on the form, and other times, it does not have time to remove all controls of a row or column and I end up with controls that are still on the screen when they shouldn't be there. At that point the entire grid/map is off balance and stops working as intended because either the events that should fire on one edge don't (the tiles are not present) and/or there are now multiple controls with the same name on the form and the removal or referencing fails...
While I am well aware that winforms is not designed to perform intensive GPU/GDI operations, you would think that something this simple would still be easily do-able in winforms?
How would I go about making this more responsive at runtime? Here's my entire set of code:
Form code
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 object
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();
}
}
}
I'd create the grid using (DataGridView, TableLayoutPanel, GDI+, or whatever) and then in the drag and drop, just calculate the new indexes and update the indexes, without moving the grid.
Example
The following example shows how to do it using a TableLayoutPanel:
Assign a fixed size to cells
Build the grid to fill the form
When form resizes, rebuild the grid
In mouse down capture the mouse down point and current top left index of grid
In mouse move, calculate the new index based on mouse movement and update index
In cell paint of the panel, draw the indexes
Here is the code:
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;
}
To prevent flicker:
public class MyGrid : TableLayoutPanel
{
public MyGrid()
{
DoubleBuffered = true;
}
}
So, for anyone trying to do this, as a concept, here is how to fix this issue:
Instead of only drawing 1 row/col extra outside the viewport to save memory, draw the entire viewport's worth of cells in every direction of the edges (up, down, left and right)... for example, if your viewport can hold 5 tiles (5 X 5 = 25), then you need to draw 5 X 5 outside the viewport in every other direction (25 X 4 = 100)...
When the mouse is being dragged, just move the controls that are already on the form/control/"drawn"... this way, the user cannot, while dragging, go outside the bounds of the existing tiles... example, if they reach the outer right edge with the mouse, while dragging the left-most tile, the tiles to show on the left already exist! So we are just "following the mouse", which is not an issue if the controls are already there/there is no "loss/issues" because we are not removing or adding any tiles at this point...
When the user stops dragging the selected tile around (onMouseUp), THEN we re-calculate the tiles that need to be drawn and those that don't... so we only redraw (add and/or remove controls where necessary) the entire set of "drawn" tiles after the user is done dragging...
With this method, you remove any "missplaced" controls, double generation of controls, controls missing and any other issue that arises when the mouse moves too fast for the "Calculate drawn tiles" code to execute. You also "see" the map moving around as you drag, and you always have the correct tiles drawn on the screen! Problem solved!
However, I did find that when I used a UserControl instead of the form itself, the controls draw and update much faster and better than if I just add them to the form itself... therefore, I have accepted the answer that outlines that aspect as the actual answer, and placed this here for anyone else in future that might wonder how to do this as a concept.

am trying to make c# event puzzle game on for an assignment

i got the picture box to move. But when i click down on the picture box it jumps all over the place. Can anyone help me with this at all?
namespace Move_Shapes
{
public partial class Form1 : Form
{
int X = 0;
int Y = 0;
int mX = 0;
int mY = 0;
bool Move = false;
public Form1()
{
InitializeComponent();
}
private void pic_TL_Click(object sender, EventArgs e)
{
//X = MousePosition.X - this.Location.X - 8 - pic_TL.Location.X; // loc of cursor in picture
//Y = MousePosition.Y - this.Location.Y - 30 - pic_TL.Location.Y;
//label1.Text = X.ToString();
//label2.Text = Y.ToString();
//X = MousePosition.X - this.Location.X - 8;
//Y = MousePosition.Y - this.Location.Y - 30;
//label3.Text = X.ToString();
//label4.Text = Y.ToString();
}
private void pic_TL_MouseDown(object sender, MouseEventArgs e)
{
X = MousePosition.X - this.Location.X - 8 - pic_TL.Location.X; // loc of cursor in picture
Y = MousePosition.Y - this.Location.Y - 30 - pic_TL.Location.Y;
label1.Text = X.ToString();
label2.Text = Y.ToString();
Move = true;
}
private void pic_TL_MouseMove(object sender, MouseEventArgs e)
{
mX = MousePosition.X - this.Location.X - 8 - pic_TL.Location.X; // loc of cursor in picture
mY = MousePosition.Y - this.Location.Y - 30 - pic_TL.Location.Y;
if (Move)
{
pic_TL.Location = new Point(mX - X, mY - Y);
}
}
private void pic_TL_MouseUp(object sender, MouseEventArgs e)
{
Move = false;
}
}
}
On MoseMove you set location to difference of current mouse position and initial mouse position.
pic_TL.Location = new Point(mX - X, mY - Y);
I case mouse moved over one pixel, picture will move to left-top corner.

Keep an image on foreground and manage collision

I'm working on a game app for windows phone.
I'm moving an image over the screen (which is a Map). The user can place a ball where he wants. My question is how can I not allow the ball to be placed on the image?
I think I could check for collision an move the ball out of the rectangle when if necessary.
I have 3 methods for moving my object (ManipulationStarted, OnManipulationDelta, and ManipulationOver).
The one I have a problem with is:
private void OnManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
Point A = new Point();
Point B = new Point();
e.Handled = true;
var transform = (sender as UIElement).RenderTransform as TranslateTransform;
A.X = transform.X += e.DeltaManipulation.Translation.X;
A.Y = transform.Y += e.DeltaManipulation.Translation.Y;
B.X = savx = transform.X;
B.Y = savy = transform.Y;
for (int i = 0; i < cur.lobst.Count(); i++)
{
if ((A.X > cur.lobst[i].px
&& A.X < (cur.lobst[i].px + cur.lobst[i].pw))
&& (A.Y > cur.lobst[i].py
&& A.Y < (cur.lobst[i].py + cur.lobst[i].ph)))
{
MessageBox.Show(
"Point inside the rectangle, so inside (under) my object ");
}
}
}
The message box is never shown. Why is that?
you can try this ...
public bool Intersects(Rect r1,Rect r2)
{
r1.Intersect(r2);
if(r1.IsEmpty)
{
return false;
}
else
{
return true;
}
}
then while checking ...
ismoveallowed = true;
for (int i = 0; i < cur.lobst.Count(); i++)
{
if(Intersects(r1,r2))
{
ismovallowed = false;
break;
}
}
if(ismovealoowed)
{
// move the object.
}
i hope this will help you ..

Unable to translate click coordinates to picturebox

I have a PictureBox with some graphics drawn, able to zoom by mousewheel. To keep the graphics at the (approximately) same position, not to have to move each time after zooming, I translate the graphics after each zoom. Here is my zooming code:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(pictureBox1.BackColor);
float _step = 1.0f;
if (todo == "zoom out")
{
float step = 0;
if (CurrentRate >= 0.60f) step = 0.05f;
else if (CurrentRate >= 0.40f && CurrentRate < 0.60f) step = 0.025f;
else if (CurrentRate >= 0.05f && CurrentRate < 0.40f) step = 0.0125f;
CurrentRate -= step; // current rate is 1.0 on startup
_step = step;
//pictureBox1.Location = new Point((int)(pictureBox1.Location.X + step * 1500), (int)(pictureBox1.Location.Y + step * 1500));
translateX += step * 10500; //achieved these numbers after few dozens of tries, it actually keeps the graphics at the same position..
translateY += step * 8500;
todo = null;
}
else if (todo == "zoom in")
{
float step = 0;
if (CurrentRate >= 1.80f && CurrentRate <= 1.95f) step = 0.0125f;
else if (CurrentRate >= 0.80f && CurrentRate < 1.80f) step = 0.025f;
else if (CurrentRate >= 0.03f && CurrentRate < 0.80f) step = 0.05f;
CurrentRate += step;
_step = step;
translateX -= step * 10500;
translateY -= step * 8500;
//pictureBox1.Location = new Point((int)(pictureBox1.Location.X - step * 1500), (int)(pictureBox1.Location.Y - step * 1500));
todo = null;
}
e.Graphics.TranslateTransform(translateX, translateY); //move it to keep same position
e.Graphics.ScaleTransform(CurrentRate, CurrentRate); //rescale according to the zoom
//the drawing itself (of everything, also the things mentioned below)
Now, what I am trying to do. The user clicks the picturebox, a small rectangle should be drawn at the click position. When he clicks again, another rectangle is drawn, and the rectangles are connected by a line. And on and on to lets say 50 connected rectangles.
Now, the rectangles connect correctly, but everything is drawn with a horrible offset. I believe this is caused by the translation. So I tried to translate the click coordinates as well:
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
//MessageBox.Show("e.Location: " + e.Location.ToString() + "to client e.location: " + PointToClient(e.Location).ToString() + "cursor position: " + Cursor.Position.ToString() + "to client cursor position:" + PointToClient(Cursor.Position).ToString() + "/nto screen cursor position: " + PointToScreen(Cursor.Position).ToString());
if (trackDrawing)
{
Point[] rectanglePos = new Point[1];
rectanglePos[0] = new Point(e.Location.X + (int)(translateX), e.Location.Y + (int)translateY);
drawBuffer.Add(rectanglePos);
drawBuffertype.Add("DRAWTRACKRECTANGLE");
if (trackDrawingBuffer.Count > 0)
{
Point[] linePos = new Point[2];
linePos[0] = trackDrawingBuffer[trackDrawingBuffer.Count - 1];
linePos[1] = new Point(e.Location.X + (int)translateX, e.Location.Y + (int)translateY); ;
drawBuffer.Add(linePos);
drawBuffertype.Add("DRAWTRACKLINE");
}
trackDrawingBuffer.Add(new Point(e.Location.X + (int)translateX, e.Location.Y + (int)translateY));
pictureBox1.Invalidate();
}
//some more unrelated code
But that doesn't work. I have tried also without the translates here at the MouseDown event, but still it draws with offset. I am not quite sure how to describe the behavior properly, so I have done a short vid (about 30s) to explain the offset..
The video
Any ideas? Thank you in advance
**
EDIT
**
Now, after edits done according to the answers, my code looks this:
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
if (trackDrawing)
{
Matrix m = transform.Clone();
m.Invert();
Point[] rectanglePos = new Point[1];
rectanglePos[0] = new Point(e.Location.X - 3, e.Location.Y - 3);
m.TransformPoints(rectanglePos);
drawBuffer.Add(rectanglePos);
drawBuffertype.Add("DRAWTRACKRECTANGLE");
if (trackDrawingBuffer.Count > 0)
{
Point[] linePos = new Point[2];
linePos[0] = trackDrawingBuffer[trackDrawingBuffer.Count - 1];
linePos[1] = new Point(e.Location.X, e.Location.Y );
m.TransformPoints(linePos);
drawBuffer.Add(linePos);
drawBuffertype.Add("DRAWTRACKLINE");
}
trackDrawingBuffer.Add(rectanglePos[0]);
pictureBox1.Invalidate();
}
Now, here the translating part, including the code where I get the matrix offset
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(pictureBox1.BackColor);
transform.Translate(-translateX, -translateY);
float _step = 1.0f;
if (todo == "zoom out")
{
float step = 0;
if (CurrentRate >= 0.60f) step = 0.05f;
else if (CurrentRate >= 0.40f && CurrentRate < 0.60f) step = 0.025f;
else if (CurrentRate >= 0.05f && CurrentRate < 0.40f) step = 0.0125f;
CurrentRate -= step;
_step = step;
translateX += step * 10500;
translateY += step * 8500;
todo = null;
}
else if (todo == "zoom in")
{
float step = 0;
if (CurrentRate >= 1.80f && CurrentRate <= 1.95f) step = 0.0125f;
else if (CurrentRate >= 0.80f && CurrentRate < 1.80f) step = 0.025f;
else if (CurrentRate >= 0.03f && CurrentRate < 0.80f) step = 0.05f;
CurrentRate += step;
_step = step;
//pictureBox1.Scale((1f + step), (1f + step));
translateX -= step * 10500;
translateY -= step * 8500;
todo = null;
}
transform.Translate(translateX, translateY); // transform is the Matrix
e.Graphics.Transform = transform;
e.Graphics.ScaleTransform(CurrentRate, CurrentRate);
and here the drawing itself:
for (int i = 0; i < drawBuffer.Count; i++)
{
//...
else if (drawBuffertype[i].ToUpper().Contains("DRAWTRACKRECTANGLE"))
{
e.Graphics.FillRectangle(new SolidBrush(Color.Red), drawBuffer[i][0].X, drawBuffer[i][0].Y, 6, 6);
}
else if (drawBuffertype[i].ToUpper().Contains("DRAWTRACKLINE"))
{
e.Graphics.DrawLine(new Pen(Color.OrangeRed, 2), drawBuffer[i][0], drawBuffer[i][1]);
}
And still drawing like in the first part of video. I just have to be missing something really basic here...
Not my area of expertise...
...but you can keep a class level Matrix to represent the current state of the "world". You can translate, scale, and/or rotate that Matrix to manipulate the world. Just assign that Matrix to e.Graphics.Transform before drawing everything.
Now, when the user clicks, you can clone that Matrix and Invert() it, allowing you to use its TransformPoints() method. This will convert from the screen coords to the equivalent world coords. Store the converted world coords in a List so you can reuse them in the Paint() event.
Play with this example. Add two buttons to a blank form and wire up their click events to the respective typical method names I've got below. Run it and click a few points on the screen. Now hit the first button to rotate, and/or the second button to zoom in. Now try adding a few more points by clicking some more on the form. Hit the buttons and see what happens. Everything should stay relative to each other (I hope):
public partial class Form1 : Form
{
private Matrix MyMatrix = new Matrix();
private List<Point> Points = new List<Point>();
public Form1()
{
InitializeComponent();
this.WindowState = FormWindowState.Maximized;
this.Shown += new EventHandler(Form1_Shown);
}
void Form1_Shown(object sender, EventArgs e)
{
Point Center = new Point(this.ClientRectangle.Width / 2, this.ClientRectangle.Height / 2);
MyMatrix.Translate(Center.X, Center.Y);
this.MouseDown += new MouseEventHandler(Form1_MouseDown);
this.Paint += new PaintEventHandler(Form1_Paint);
}
void Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Transform = MyMatrix;
// draw the origin in the center of the form:
e.Graphics.DrawLine(Pens.Red, new Point(-10, 0), new Point(10, 0));
e.Graphics.DrawLine(Pens.Red, new Point(0, -10), new Point(0, 10));
// draw our stored points (that have already been converted to world coords)
foreach (Point pt in Points)
{
Rectangle rc = new Rectangle(pt, new Size(1, 1));
rc.Inflate(10, 10);
e.Graphics.DrawRectangle(Pens.Black, rc);
}
}
void Form1_MouseDown(object sender, MouseEventArgs e)
{
Matrix m = MyMatrix.Clone();
m.Invert();
Point[] pts = new Point[] {new Point(e.X, e.Y)};
m.TransformPoints(pts);
Points.Add(pts[0]);
this.Refresh();
}
private void button1_Click(object sender, EventArgs e)
{
MyMatrix.Rotate(10);
this.Refresh();
}
private void button2_Click(object sender, EventArgs e)
{
MyMatrix.Scale(1.1f, 1.1f);
this.Refresh();
}
}
Maybe this simple code can help you. This is all done without translation or scaling of any kind. But if you apply scaling to your graphics, it will work the same.
this.pictureBox1.Scale(new SizeF(2.5f, 2.5f));
So...
Actually I defined the side of the rectangle to 15 unit wide.
private int RectSideLen = 15;
So every time I click in the picture box I assume that I click the center of my rectangle to be drawn. This mean that our rectangle will start at the click location minus half rectangle side.
int cornerOffset = RectSideLen / 2;
Point newUpLeftCorner = e.Location;
newUpLeftCorner.Offset(-cornerOffset, -cornerOffset);
Then I add it to a list of rectangle and refresh the picture box to redraw it with the new rectangle added.
pictureBox1.Refresh();
and inside the paint event of the picture box I simply draw the precalculated rectangle.
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
using (Pen pen = new Pen(Color.Red, 1))
{
foreach (Rectangle r in DrawBuffer)
{
e.Graphics.DrawRectangle(pen, r);
}
}
}
So here is the complete sample.
public partial class Form1 : Form
{
private int RectSideLen = 15;
private IList<Rectangle> DrawBuffer = new List<Rectangle>();
public Form1()
{
InitializeComponent();
}
private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
int cornerOffset = RectSideLen / 2;
Point newUpLeftCorner = e.Location;
newUpLeftCorner.Offset(-cornerOffset, -cornerOffset);
DrawBuffer.Add(new Rectangle(newUpLeftCorner, new Size(RectSideLen, RectSideLen)));
pictureBox1.Refresh();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
using (Pen pen = new Pen(Color.Red, 1))
{
foreach (Rectangle r in DrawBuffer)
{
e.Graphics.DrawRectangle(pen, r);
}
}
}
}

Categories

Resources