TeeChart: Expand the "clickable" width of a Line/FastLine - c#

I have a WinForms application where a number of lines are drawn in a TeeChart component. It is requested that it shall be possible to delete a line by right-clicking it.
Everything works fine, the clickseries event is captured and so on, but the user finds it difficult to hit the line on right click. The question is, is it possible to increase the region where the Line/FastLine object is sensible for clicking? That is, make the line wider without drawing the line any wider on the screen.
Tnx in advance

Yes, this is possible. The key to achieve that is PointInLineTolerance method. To achieve what you request you can combine it with NearestPoint's tool GetNearestPoint method as shown in this example:
public Form1()
{
InitializeComponent();
InitializeChart();
}
private void InitializeChart()
{
tChart1.Aspect.View3D = false;
tChart1.Series.Add(new Steema.TeeChart.Styles.Line()).FillSampleValues();
tChart1.MouseMove += TChart1_MouseMove;
}
private void TChart1_MouseMove(object sender, MouseEventArgs e)
{
var nearestPoint = new Steema.TeeChart.Tools.NearestPoint(tChart1[0]);
nearestPoint.Active = false;
var p = new Point(e.X, e.Y);
var index = nearestPoint.GetNearestPoint(p);
if (index != -1)
{
const int tolerance = 10;
var px = tChart1[0].CalcXPos(index);
var py = tChart1[0].CalcYPos(index);
var index2 = (index == tChart1[0].Count - 1) ? index - 1 : index + 1;
var qx = tChart1[0].CalcXPos(index2);
var qy = tChart1[0].CalcYPos(index2);
if (Steema.TeeChart.Drawing.Graphics3D.PointInLineTolerance(p, px, py, qx, qy, tolerance))
{
tChart1.Header.Text = "point " + index.ToString() + " clicked";
}
else
{
tChart1.Header.Text = "No point";
}
}
An alternative could be using an invisible fake series with same data as the original series.

Related

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.

C# - show child control (Listbox) on top of parent control(textbox) in winform

Created a custom intellisense textbox (textbox with listbox a child).
As shown in below image, the listbox pops up when i enter a char which all works fine and good but when i am at the end of textbox the listbox is partially visible, is there anyway i can show the whole listbox content?
Tried this "Show control inside user control outside the boundaries of its parent
But when the popup window opens the text box looses focus and i cannot type anything further, my intellisense textbox keeps giving better results based on what they type but in this situation i am not able to type anymore.
FYI tried to add pParentControl.Focus() into show method defined in other article as shown below, missing something?
public void Show(Control pParentControl)
{
if (pParentControl == null) return;
// position the popup window
var loc = pParentControl.PointToScreen(new Point(0, pParentControl.Height));
pParentControl.Focus();
m_tsdd.Show(loc);
}
Here is the complete code
class TextBox_AutoComplete : TextBox
{
#region Class Members
List<string> dictionary;
ListBox listbox = new ListBox();
#endregion
private PopupHelper m_popup;
#region Extern functions
[DllImport("user32")]
private extern static int GetCaretPos(out Point p);
#endregion
#region Constructors
public TextBox_AutoComplete() : base()
{
this.Margin = new Padding(0, 0, 0, 0);
this.Multiline = true;
this.Dock = DockStyle.Fill;
this.KeyDown += Textbox_KeyDown;
this.KeyUp += Textbox_KeyUp;
listbox.Parent = this;
listbox.KeyUp += List_OnKeyUp;
listbox.Visible = false;
this.dictionary = new List<string>();
}
#endregion
#region Properties
public List<string> Dictionary
{
get { return this.dictionary; }
set { this.dictionary = value; }
}
#endregion
#region Methods
private static string GetLastString(string s)
{
Regex rgx = new Regex("[^a-zA-Z0-9_.\\[\\]]");
s = rgx.Replace(s, " ");
string[] strArray = s.Split(' ');
return strArray[strArray.Length - 1];
}
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
Point cp;
GetCaretPos(out cp);
List<string> lstTemp = new List<string>();
List<string> TempFilteredList = new List<string>();
string LastString = GetLastString(this.Text.Substring(0, SelectionStart));
//MessageBox.Show(LastString);
/*seperated them so that column name matches are found first*/
TempFilteredList.AddRange(dictionary.Where(n => n.Replace("[", "").ToUpper().Substring(n.IndexOf(".") > 0 ? n.IndexOf(".") : 0).StartsWith(LastString.ToUpper())
).Select(r => r)
.ToList());
TempFilteredList.AddRange(dictionary.Where(n => n.Replace("[", "").ToUpper().StartsWith(LastString.ToUpper())
|| n.ToUpper().StartsWith(LastString.ToUpper()))
.Select(r => r)
.ToList());
lstTemp = TempFilteredList.Distinct().Select(r => r).ToList();
/*Getting max width*/
int maxWidth = 0, temp = 0;
foreach (var obj in lstTemp)
{
temp = TextRenderer.MeasureText(obj.ToString(), new Font("Arial", 10, FontStyle.Regular)).Width;
if (temp > maxWidth)
{
maxWidth = temp;
}
}
listbox.SetBounds(cp.X + 20, cp.Y + 20, maxWidth, 60);
if (lstTemp.Count != 0 && LastString != "")
{
listbox.DataSource = lstTemp;
// listbox.Show();
if (m_popup == null)
m_popup = new PopupHelper(listbox);
m_popup.Show(this);
}
else if (m_popup != null)
{
//listbox.Hide();
m_popup.Hide();
}
}
protected void Textbox_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Down)
{
if (listbox.Visible == true)
{
listbox.Focus();
}
e.Handled = true;
}
else if (e.KeyCode == Keys.Escape)
{
listbox.Visible = false;
e.Handled = true;
}
}
protected void Textbox_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space && listbox.Visible == true)
{
listbox.Focus();
List_OnKeyUp(listbox, new KeyEventArgs(Keys.Space));
e.Handled = true;
}
if (e.KeyCode == Keys.Down && listbox.Visible == true)
{
listbox.Focus();
e.Handled = true;
}
}
private void List_OnKeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter)
{
int Selection_Start = this.SelectionStart;
string StrLS = GetLastString(this.Text.Substring(0, Selection_Start));
this.Select(Selection_Start - StrLS.Length, StrLS.Length);
// MessageBox.Show(this.Selection_Start.ToString() + " Last string" + StrLS);
this.SelectedText=((ListBox)sender).SelectedItem.ToString();
listbox.Hide();
this.Focus();
}
}
#endregion
}
public sealed class PopupHelper : IDisposable
{
private readonly Control m_control;
private readonly ToolStripDropDown m_tsdd;
private readonly Panel m_hostPanel; // workarround - some controls don't display correctly if they are hosted directly in ToolStripControlHost
public PopupHelper(Control pControl)
{
m_hostPanel = new Panel();
m_hostPanel.Padding = Padding.Empty;
m_hostPanel.Margin = Padding.Empty;
m_hostPanel.TabStop = false;
m_hostPanel.BorderStyle = BorderStyle.None;
m_hostPanel.BackColor = Color.Transparent;
m_tsdd = new ToolStripDropDown();
m_tsdd.CausesValidation = false;
m_tsdd.Padding = Padding.Empty;
m_tsdd.Margin = Padding.Empty;
m_tsdd.Opacity = 0.9;
m_control = pControl;
m_control.CausesValidation = false;
m_control.Resize += MControlResize;
//m_hostPanel.Controls.Add(m_control);
m_tsdd.Padding = Padding.Empty;
m_tsdd.Margin = Padding.Empty;
m_tsdd.MinimumSize = m_tsdd.MaximumSize = m_tsdd.Size = pControl.Size;
m_tsdd.Items.Add(new ToolStripControlHost(m_control));
}
private void ResizeWindow()
{
m_tsdd.MinimumSize = m_tsdd.MaximumSize = m_tsdd.Size = m_control.Size;
m_hostPanel.MinimumSize = m_hostPanel.MaximumSize = m_hostPanel.Size = m_control.Size;
}
private void MControlResize(object sender, EventArgs e)
{
ResizeWindow();
}
/// <summary>
/// Display the popup and keep the focus
/// </summary>
/// <param name="pParentControl"></param>
public void Show(Control pParentControl)
{
if (pParentControl == null) return;
// position the popup window
var loc = pParentControl.PointToScreen(new Point(0, pParentControl.Height));
pParentControl.Focus();
m_tsdd.Show(loc);
}
public void Hide()
{
m_tsdd.Hide();
}
public void Close()
{
m_tsdd.Close();
}
public void Dispose()
{
m_control.Resize -= MControlResize;
m_tsdd.Dispose();
m_hostPanel.Dispose();
}
}
Firstly, I personally don't see any benefit in having a control inside another. Yes, the child control is locked inside its parent's boundaries automatically for you, but this benefit is negated by the issue that you're facing, and solving that issue requires the same work as when the two controls have no relation. In both cases, you'll have to do the calculations manually to keep the child visible inside its parent. In the second case the parent is the app's window.
Secondly, I don't recommend using hacks like the one mentioned in the comments to show the child outside its parent's boundaries. The hack creates more issues than it solves, as you found out. And what's the point of that hack anyway? If you want to show the child outside the parent, then don't make it a child control in the first place, and you don't need any hack.
The best solution is the one that you find in any well designed app, and in Windows itself. Open any app, let's say Notepad, and right-click near the upper-left corner. You'll see the context menu pulling to lower-right direction. Now right-click near the other three corners and you'll see the context menu pulling in different direction each time, so it will always be visible inside the app. Now if you resize the app window too small and right-click, the context menu will choose the best direction but some of it will be outside the app because the window is too small. That's why you need your list not to be a child, but it's up to you, and it's only about these edge cases. The solution will be similar in both cases.
You're displaying the list in this line:
listbox.SetBounds(cp.X + 20, cp.Y + 20, maxWidth, 60);
The key is cp.X and cp.Y. This is what decides where the list will appear. You need to make this point dynamic and responsive to the boundaries of the parent. You fixed the width to maxWidth and height to 60, so I will use those values in the calculation.
To make sure the list will not go beyond the bottom:
var y = this.Height < cp.Y + 60 ? this.Height - 60 : cp.Y;
To make sure the list will not go beyond the right:
var x = this.Width < cp.X + maxWidth ? this.Width - maxWidth : cp.X;
Now you can show your list at the calculated point:
listbox.SetBounds(x, y, maxWidth, 60);
Notes:
I didn't include the 20 gap that you used. I think it looks better without the gap and I haven't seen any app that has a gap. If you prefer the gap, add it to the calculation of x and y. Don't add it in the SetBounds() or that will screw up the calculation.
The calculation above doesn't take into account when the parent size is too small to show the child inside. If you want to support that edge case, you need to make the child a separate control and add some checks to the calculation.

Connect 4 in C# windows form application

I've been trying to create connect 4 (the game) in windows forms applications in C# but I'm kinda struggling to create a method so the coins actually drop and not stay were the user clicks. Also I don't know where should I place the win condition method. Here is my code so far:
namespace ConnectFour
{
public partial class Form1 : Form
{
Button[] gameButtons = new Button[42]; //array of buttons for markers(red and blue)
bool blue = true; //blue is set to true if the next marker is to be a blue
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
this.Text = "Connect 4";
this.BackColor = Color.BlanchedAlmond;
this.Width = 500;
this.Height = 500;
for (int i = 0; i < gameButtons.Length; i++)
{
int index = i;
this.gameButtons[i] = new Button();
int x = 50 + (i % 7) * 50;
int y = 50 + (i / 7) * 50;
this.gameButtons[i].Location = new System.Drawing.Point(x, y);
this.gameButtons[i].Name = "btn" + (index + 1);
this.gameButtons[i].Size = new System.Drawing.Size(50, 50);
this.gameButtons[i].TabIndex = i;
//this.gameButtons[i].Text = Convert.ToString(index);
this.gameButtons[i].UseVisualStyleBackColor = true;
this.gameButtons[i].Visible = true;
gameButtons[i].Click += (sender1, ex) => this.buttonHasBeenPressed(sender1, index);
this.Controls.Add(gameButtons[i]);
}
}
private void buttonHasBeenPressed(object sender, int i)
{
if (((Button)sender).BackColor == Color.BlanchedAlmond)
{
if (blue == true)
{
((Button)sender).BackColor = Color.Red;
}
else
{
((Button)sender).BackColor = Color.Blue;
}
blue = !blue;
}
}
private void fourInARow(int a, int b, int c,int d)
{
if (gameButtons[a].BackColor == gameButtons[b].BackColor && gameButtons[a].BackColor == gameButtons[c].BackColor && gameButtons[a].BackColor==gameButtons[d].BackColor)
{
if (gameButtons[a].BackColor == Color.Blue)
{
MessageBox.Show("the winner is player 1");
}
else
{
MessageBox.Show("the winner is player 2");
}
}
}
}
}
You've probably figured it out by now, but what I would do is take a look at the BackColor of each Button below the one the user pressed until we either hit the bottom row or we find a Button that doesn't have a "BlanchedAlmond" BackColor, and then that's the one we change.
To find a Button directly below another one, we just look at the Button with an Index of 7 more than the current Index. And since we pass the Button object's index in the index argument to our function, we can use that.
Here's a commented code example:
private void ButtonHasBeenPressed(object sender, int index)
{
// Get the button that the user pressed
var pressedButton = (Button)sender;
// Only do something if they clicked a "neutral" button
if (pressedButton.BackColor == Color.BlanchedAlmond)
{
// The backcolor will be set based on whether or not blue is true or false
var newBackColor = blue ? Color.Red : Color.Blue;
// Get the index of the button that the user clicked
var buttonToChangeIndex = index;
// From where the user clicked, look down at each button below (index + 7)
// until we find the last button that has a BlanchedAlmond backcolor
while (buttonToChangeIndex + 7 < gameButtons.Count() &&
gameButtons[buttonToChangeIndex + 7].BackColor == Color.BlanchedAlmond)
{
buttonToChangeIndex += 7; // Set to the index to point to this button
}
// Now we set that button's backcolor
gameButtons[buttonToChangeIndex].BackColor = newBackColor;
// Flip our blue flag
blue = !blue;
}
}
The fact that you use a one-dimensional array to hold the buttons makes things a little more difficult. Since the layout of this “connect four” game is two-dimensional, it would seem logical to make a two-dimensional array. This will help facilitate the “Drop” in a column. Your current code will need to translate which cells in the one-dimensional array make up a column, namely the one the user clicked.
The code below solves this “Dropping” issue by using a two-dimensional array with a method that gets the ROW index of the next available space in a given column. This method returns the next available open slot index in the given column starting at the bottom and going up or returns -1 if the column is full.
You should seriously consider making a class of tokens/markers/buttons, then another Class that is a game board for these tokens. The logic to see if there are 4 consecutive colors horizontally, vertically and diagonally could possibly be complex. However, this logic would fit perfectly towards a GameBoard Object. In addition, the “GameBoard” object could keep track of all the moves etc…
This will greatly free up your code to focus on the visual aspects of the form and not worry about the logic of the game. I hope this makes sense.
Button[,] gameButtons = new Button[7,6]; //array of buttons for markers(red and blue)
bool blue = true; //blue is set to true if the next marker is to be a blue
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
this.Text = "Connect 4";
this.BackColor = Color.BlanchedAlmond;
this.Width = 500;
this.Height = 500;
int x;
int y;
for (int row = 0; row < gameButtons.GetLength(0); row++) {
x = 50 + (row % 7) * 50;
for (int col = 0; col < gameButtons.GetLength(1); col++) {
y = 50*col + 50;
Button newButton = new Button();
newButton.Location = new System.Drawing.Point(x, y);
newButton.Name = "btn" + (row + col + 1);
newButton.Size = new System.Drawing.Size(50, 50);
newButton.TabIndex = row + col;
newButton.UseVisualStyleBackColor = true;
newButton.Visible = true;
newButton.Click += (sender1, ex) => this.buttonHasBeenPressed(sender1);
gameButtons[row, col] = newButton;
Controls.Add(gameButtons[row,col]);
}
}
}
private void buttonHasBeenPressed(object sender) {
Button buttonClicked = (Button)sender;
int col = buttonClicked.Location.X / 50 - 1;
int targetRow = GetButtonRow(col);
if (targetRow >= 0) {
if (blue == true) {
gameButtons[col, targetRow].BackColor = Color.Red;
} else {
gameButtons[col, targetRow].BackColor = Color.Blue;
}
blue = !blue;
}
}
public int GetButtonRow(int colIndex) {
Button curButton;
for (int row = gameButtons.GetLength(1) - 1; row >= 0; row--) {
curButton = gameButtons[arrayColIndex, row];
if (curButton.BackColor != Color.Red && curButton.BackColor != Color.Blue) {
return row;
}
}
return -1;
}

How to move label's text right to left,when a character hides in left it shows on right?

Here is my key code:
using System;
using System.Windows.Forms;
namespace Scroller
{
public partial class Form1 : Form
{
int i, j;
bool k = false;
public Form1()
{
InitializeComponent();
}
private void timer1_Tick(object sender, EventArgs e)
{
label1.Text = "Time:"+ System.DateTime.Now.ToString();
i--;
j = i + this.Width;
if (i < this.Width && i > 0)
{
label1.Left = i;
}
else
if (i < 0 && k == false)
{
label1.Left = i;
k = true;
}
else
if (i < 0 && k == true)
{
label1.Left = j;
k = false;
}
if (i < 0 - label1.Width)
{
i = this.Width - label1.Width;
}
}
private void Form1_Load(object sender, EventArgs e)
{
label1.Text = "Time:"+ System.DateTime.Now.ToString();
i = this.Width - label1.Width;
label1.Left = i;
}
}
}
The effect that I want to make is the whole time string move right to left. When a pixel of the text disappear on the left side (because it is out of the form's left border),the pixel will shows on the right side.
In other words, the effect can't be make by delete the first character of string and append it to the last.
I knew that it will be easier to use two label to do it. Set one's location in the form and hide the other right by the form. Move them in the same time.
When the first label hit the left border of the form, the second hit the right border of the form. And the first one move out, the second move in. Until the second totally move in, reset their x location.
But I just want to use one label. So I chose to quickly switch the label's location, and try to "cheat" user's eye. The problem is when the label switch between left and right, it flash very obviously. Even though I set timer's interval below 20,the problem still exist.
Could you help me dissolve the flash problem or enlighten me other ways which can just use one label and one timer to make the effect I need?
Thanks. If I didn't describer my problem clear enough or need more code, please let me know.
I don't think you can work out the flashing problem changing the label's location in a windows form.
Another solution would be to set the label width the same size as the form width, make the label text fill all the width using spaces and make the timer always get the last character and put it on the beginning of the string.
Sample code below.
private void timer1_Tick(object sender, EventArgs e)
{
label1.Text = label1.Text.Substring(label1.Text.Length - 1) + label1.Text.Remove(label1.Text.Length - 1);
}
private void Form1_Load(object sender, EventArgs e)
{
// The total spaces required to fill the form vary from form.width and the initial label.text.width
// Width | Spaces
// 177 | 13
// 228 | 30
// 297 | 53
// 318 | 60
// The spacesEnd = 60 work for a form with width 319
int spacesBegin = 0, spacesEnd = 60;
label1.Text = "Time:" + System.DateTime.Now.ToString();
label1.AutoSize = false;
label1.Left = -3;
label1.Width = this.Width - 1;
label1.Height = 15;
label1.BorderStyle = BorderStyle.FixedSingle;
for (int i = 0; i < spacesBegin; i++)
label1.Text = " " + label1.Text;
for (int i = 0; i < spacesEnd; i++)
label1.Text += " ";
Timer timer = new Timer();
timer.Tick += timer1_Tick;
timer.Interval = 50;
timer.Start();
}

see values of chart points when the mouse is on points

I have a chart and I want the user to see the values when the pointer is on the points.
By using digEmAll's help in the page finding the value of the points in a chart ,I could write the following code:
Point? prevPosition = null;
ToolTip tooltip = new ToolTip();
void chart1_MouseMove(object sender, MouseEventArgs e)
{
var pos = e.Location;
if (prevPosition.HasValue && pos == prevPosition.Value)
return;
tooltip.RemoveAll();
prevPosition = pos;
var results = chart1.HitTest(pos.X, pos.Y, false, ChartElementType.PlottingArea);
foreach (var result in results)
{
if (result.ChartElementType == ChartElementType.PlottingArea)
{
chart1.Series[0].ToolTip = "X=#VALX, Y=#VALY";
}
}
}
by the above code,the user can see the values when the pointer is near to a series.But now How can I let the user to see the values only when the pointer is on the points?
I replaced
int k = result.PointIndex;
if (k >= 0)
{
chart1.Series[0].Points[k].ToolTip = "X=#VALX, Y=#VALY";
}
instead of
chart1.Series[0].ToolTip = "X=#VALX, Y=#VALY";
to solve my problem.But It wasn't usefull.
You should modify the code in this way:
Point? prevPosition = null;
ToolTip tooltip = new ToolTip();
void chart1_MouseMove(object sender, MouseEventArgs e)
{
var pos = e.Location;
if (prevPosition.HasValue && pos == prevPosition.Value)
return;
tooltip.RemoveAll();
prevPosition = pos;
var results = chart1.HitTest(pos.X, pos.Y, false,
ChartElementType.DataPoint);
foreach (var result in results)
{
if (result.ChartElementType == ChartElementType.DataPoint)
{
var prop = result.Object as DataPoint;
if (prop != null)
{
var pointXPixel = result.ChartArea.AxisX.ValueToPixelPosition(prop.XValue);
var pointYPixel = result.ChartArea.AxisY.ValueToPixelPosition(prop.YValues[0]);
// check if the cursor is really close to the point (2 pixels around the point)
if (Math.Abs(pos.X - pointXPixel) < 2 &&
Math.Abs(pos.Y - pointYPixel) < 2)
{
tooltip.Show("X=" + prop.XValue + ", Y=" + prop.YValues[0], this.chart1,
pos.X, pos.Y - 15);
}
}
}
}
}
The idea is to check if the mouse is very close to the point e.g. 2 pixels around it (because is really unlikely to be exactly on the point) and show the tooltip in that case.
Here's a complete working example.
I would take this solution:
Add custom tooltip event handler:
this.chart1.GetToolTipText += this.chart1_GetToolTipText;
Implement event handler:
private void chart1_GetToolTipText(object sender, ToolTipEventArgs e)
{
// Check selected chart element and set tooltip text for it
switch (e.HitTestResult.ChartElementType)
{
case ChartElementType.DataPoint:
var dataPoint = e.HitTestResult.Series.Points[e.HitTestResult.PointIndex];
e.Text = string.Format("X:\t{0}\nY:\t{1}", dataPoint.XValue, dataPoint.YValues[0]);
break;
}
}
Consider the following as a possible better option than tooltips...use the label feature of the chart control.
DataPoint _prevPoint;
void chart1_MouseMove(object sender, MouseEventArgs e)
{
// this if statement clears the values from the previously activated point.
if (_prevPoint) {
_prevPoint.MarkerStyle = MarkerStyle.None;
_prevPoint.IsValueShownAsLabel = false;
}
var result = chart1.HitTest(e.X, e.Y, ChartElementType.DataPoint);
if (result.ChartElementType == ChartElementType.DataPoint)
{
var prop = result.Object as DataPoint;
if (prop != null)
{
prop.IsValueShownAsLabel = true;
prop.MarkerStyle = MarkerStyle.Star4;
}
}
}
I've tested this and i'm using it currently. It's very nice on charts with a lot of points since it shows the marker on the chart as well.

Categories

Resources