Related
Intro:
This pseudocode is based on Multiple chart areas in one column, however, when I enable many chart areas and all of them are aligned one above the other, the graphic height is getting lower while the axis X labels are getting away from the chart.
Let's see this in a deep way:
I have a panel which contains inside itself the chart.
Every chart area makes reference to a selected resource, so in this case, we have 5 chart areas as 5 resources selected on the checked list box.
From the third resource, I add 300 as the minimum scroll value of the panel, so the size of the chart increases, and the chart areas do not look excessively different in height every time we add a chart area.
Chart view for 13 Resources:
Chart view for 18 Resources:
Chart view for 21 resources:
And finally, chart view for 26 or more resoures:
Code:
Auxiliar method to clone a chart area
private ChartArea CloneChartArea(ChartArea chartArea, string resultName)
{
var result = new ChartArea();
result.Name = resultName;
result.AxisX.MajorTickMark.LineColor = chartArea.AxisX.MajorTickMark.LineColor;
result.AxisY.MajorTickMark.LineColor = chartArea.AxisY.MajorTickMark.LineColor;
result.AxisX.LabelStyle.Font = chartArea.AxisX.LabelStyle.Font;
result.AxisY.LabelStyle.Font = chartArea.AxisY.LabelStyle.Font;
result.AxisX.LabelStyle.ForeColor = chartArea.AxisX.LabelStyle.ForeColor;
result.AxisY.LabelStyle.ForeColor = chartArea.AxisY.LabelStyle.ForeColor;
/// The following lines allow us to paint multiple chart areas in one column
result.AlignWithChartArea = chartArea.Name;
result.AlignmentStyle = AreaAlignmentStyles.Position;
result.AlignmentOrientation = AreaAlignmentOrientations.Vertical;
return result;
}
Auxiliar method to set axis y label
private ChartArea SetAxisLabelY(ChartArea chartArea, string ylabel, Color foreColor, float fontSize)
{
chartArea.AxisY.Title = ylabel;
chartArea.AxisY.TitleAlignment = StringAlignment.Center;
chartArea.AxisY.TitleFont = new Font("Century Gothic", fontSize, FontStyle.Regular, GraphicsUnit.Point, ((byte)(0)));
chartArea.AxisY.TitleForeColor = foreColor;
return chartArea;
}
Main method to set chart values:
private void SetChartValues(IEnumerable<IGrouping<int, ResourceDailyCalendar>> data)
{
while (chart1.Series.Any())
{
chart1.Series.RemoveAt(0);
if (chart1.ChartAreas.Count > 1)
{
chart1.ChartAreas.RemoveAt(1);
}
}
panel5.AutoScrollMinSize = new Size(0, 0);
if (data != null && data.Count() > 0)
{
var resourcesNames = data.SelectMany(group => group).Select(element => element.Resource).Distinct().ToList();
var chartAreaIndex = 1;
foreach (var resourceName in resourcesNames)
{
var chartAreaName = string.Format("ChartArea{0}", chartAreaIndex);
ChartArea chartArea = chart1.ChartAreas.Last();
if (!chart1.ChartAreas.Any(ca => ca.Name.Equals(chartAreaName)))
{
chartArea = CloneChartArea(chartArea, resultName: chartAreaName);
chart1.ChartAreas.Add(chartArea);
if (chartAreaIndex > 3)
{
var minScroll = panel5.AutoScrollMinSize;
if (minScroll.IsEmpty)
{
minScroll.Height = panel5.Size.Height;
}
minScroll.Height += 300;
panel5.AutoScrollMinSize = minScroll;
}
}
chartArea = SetAxisLabelY(chartArea, resourceName, Color.Yellow, 10F);
var totalSerie = new Series()
{
Name = $"{resourceName} - Total",
ChartArea = chartArea.Name,
ChartType = SeriesChartType.StackedColumn,
Color = Color.Black,
BorderWidth = 1,
BorderDashStyle = ChartDashStyle.Solid,
IsVisibleInLegend = false
};
var allowedSerie = new Series()
{
Name = $"{resourceName} - Allowed",
ChartArea = chartArea.Name,
ChartType = SeriesChartType.StackedColumn,
Color = Color.Gray,
BorderWidth = 1,
BorderDashStyle = ChartDashStyle.Solid,
IsVisibleInLegend = false
};
var currentSerie = new Series()
{
Name = $"{resourceName} - Current",
ChartArea = chartArea.Name,
ChartType = SeriesChartType.StackedColumn,
BorderWidth = 1,
BorderDashStyle = ChartDashStyle.Solid,
IsVisibleInLegend = false
};
var x = 0;
var maxValue = 0.0;
foreach (var group in data)
{
string axisLabel;
if (group.Count() == 1) /// Agrupados por día
{
if (group.First().Resource != resourceName) continue;
axisLabel = group.First().Date.Value.ToShortDateString();
}
else
{
if (group.Count() > 1 && group.Count() < 8) /// Agrupados por semanas
{
axisLabel = group.First().YYYYWW.ToString();
}
else /// Agrupados por meses
{
axisLabel = group.First().YYYYMM.ToString();
}
}
var effectiveWorkingHours = group.Where(i => i.Resource == resourceName).Sum(i => i.EffectiveWorkingHours);
currentSerie.Points.AddXY(x, effectiveWorkingHours);
currentSerie.Points[x].AxisLabel = axisLabel;
maxValue = effectiveWorkingHours > maxValue ? effectiveWorkingHours : maxValue;
var workingHours = group.Where(i => i.Resource == resourceName).Sum(i => i.WorkingHours);
maxValue = workingHours > maxValue ? workingHours : maxValue;
workingHours -= effectiveWorkingHours;
allowedSerie.Points.AddXY(x, workingHours);
allowedSerie.Points[x].AxisLabel = axisLabel;
var periodTotalHours = group.Where(i => i.Resource == resourceName).Sum(i => i.PeriodTotalHours);
maxValue = periodTotalHours > maxValue ? periodTotalHours : maxValue;
periodTotalHours -= (workingHours + effectiveWorkingHours);
totalSerie.Points.AddXY(x, periodTotalHours);
totalSerie.Points[x].AxisLabel = axisLabel;
x++;
}
chart1.Series.Add(currentSerie);
chart1.Series.Add(allowedSerie);
chart1.Series.Add(totalSerie);
chart1.ChartAreas.First(ca => ca.Name == chartArea.Name).AxisY.Maximum = maxValue * 1.1;
chartAreaIndex++;
}
/// Resize de chart
var currentHeight = 0;
var chartAreasCounter = chart1.ChartAreas.Count();
foreach (ChartArea ca in chart1.ChartAreas)
{
ca.AxisY.Minimum = 0;
ca.Position.Height = 100 / chartAreasCounter;
ca.Position.Y = currentHeight;
ca.Position.X = 0;
ca.Position.Width = 100;
ca.AxisX.MaximumAutoSize = 5;
currentHeight += 100 / chartAreasCounter;
}
chart1.ApplyPaletteColors();
chart1.Update();
System.Windows.Forms.Application.DoEvents();
}
}
Could anyone help me how to prevent this please?
I have a list of signals in a listview. When the user checks one, the values of the signals are being plotted on the chart. Moreover there is a vertical annotation which the user can drag across the graph and see the values for every x value. Each signal has one rectangle annotation that shows the Y value.
My problem is that when the user checks a new signal then the old rectangle annotations do not disappear.
Here is what I mean :
enter image description here
Here is my code so far :
List<RectangleAnnotation> anno = new List<RectangleAnnotation>();
List<Series> checkedItems = new List<Series>();
private void listView1_ItemCheck(object sender, ItemCheckEventArgs e)
{
if (listView1.FocusedItem != null)
{
double xFactor = 0.03;
double yFactor = 0.02;
CA = chart1.ChartAreas[0];
if (e.NewValue == CheckState.Checked)
{
anno.Clear();
Series s12 = new Series();
s12 = chart1.Series.Add((listView1.Items[e.Index].Text).ToString());
s12.ChartType = SeriesChartType.Line;
s12.MarkerStyle = MarkerStyle.Circle; // make the points stand out
s12.MarkerSize = 3;
checkedItems.Add(s12);
for (int i = 0; i < chart1.Series.Count - 1; i++)
{
anno.Add(new RectangleAnnotation());
anno[i].AxisX = CA.AxisX;
anno[i].IsSizeAlwaysRelative = false;
anno[i].Width = 20 * xFactor;
anno[i].Height = 8 * yFactor;
// VA.Name = "myRect";
anno[i].LineColor = Color.Black;
anno[i].BackColor = Color.Black;
anno[i].AxisY = CA.AxisY;
anno[i].Y = -anno[i].Height;
// RA[i].X = VA.X - RA[i].Width / 2;
anno[i].Text = "Hello";
anno[i].ForeColor = Color.Black;
anno[i].Font = new System.Drawing.Font("Arial", 8f);
anno[i].Text = String.Format("{0:0.00}", 0);
chart1.Annotations.Add(anno[i]);
}
for (int r = 0; r < num_rows; r++)
{
DataPoint dp = new DataPoint();
dp.SetValueXY(r, values[r, listView1.Items.IndexOf(listView1.Items[e.Index])]);
// chart1.Series[checkedListBox1.Items[e.Index].ToString()].Points.Add(dp);
s12.Points.AddXY(r, values[r, listView1.Items.IndexOf(listView1.Items[e.Index])]);
}
}
}
}
private void chart1_AnnotationPositionChanging(object sender, AnnotationPositionChangingEventArgs e)
{
int pt1 = (int)e.NewLocationX;
int i = 0;
foreach (var signal in checkedItems) {
double val = signal.Points[pt1].YValues[0];
foreach (var sim in signal.Points[pt1].YValues)
{
anno[i].Y = sim + 0.5;
}
if (sender == VA) anno[i].X = VA.X - anno[i].Width / 2;
anno[i].Text = String.Format("{0:0.00}", val);
i++;
Console.WriteLine(anno.Count);
}
I have thought of adding
chart1.Annotations.clear();
But it deletes all Annotations including the vertical. I only want to delete the rectangle annotations.
I had Gantt chart like this. I'd like to show AxisX2 with value is percentage that I prepared formula for it. I had challenges to show AxisX2 and set series for it.
Here is the Gantt chart I captured click here.
I expect to one more axis like this.
Please help, Thank you .
Here are some basic function to render that chart
public void setUpGantt(Chart chart)
{
chart.Series.Clear();
Series s = chart.Series.Add("sszs");
s.ChartType = SeriesChartType.RangeBar;
s.YValueType = ChartValueType.DateTime;
s.ResetIsVisibleInLegend () ;
s.IsVisibleInLegend = true;
Axis ax = chart.ChartAreas[0].AxisX;
Axis ay = chart.ChartAreas[0].AxisY;
ax.MajorGrid.Enabled = false;
ay.IntervalType = DateTimeIntervalType.Minutes;
ay.Interval = 60;
ay.LabelStyle.Format = "HH:mm";
ay.Minimum = 0;
ay.Maximum = 0.2;
limitGantt(chart, "0:00", "24:00");
s.ToolTip = "#VALY1{HH:mm}~#VALY2{HH:mm}";
}
public void limitGantt(Chart chart, string start, string end)
{
Axis ax = chart.ChartAreas[0].AxisX;
Axis ay = chart.ChartAreas[0].AxisY;
ay.Minimum = fromTimeString(start).ToOADate();
ay.Maximum = fromTimeString(end).ToOADate();
}
DateTime fromTimeString(string time)
{
var p = time.Split(':');
int sec = p.Length == 3 ? Convert.ToInt16(p[2]) : 0;
TimeSpan t = new TimeSpan(Convert.ToInt16(p[0]), Convert.ToInt16(p[1]), sec);
return DateTime.Today.Add(t);
}
public void addGanttTask(Series s, string start, string end, Color c, int slot, string [] array)
{
DateTime start_ = fromTimeString(start);
DateTime end_ = fromTimeString(end);
int pt = s.Points.AddXY(slot, start_, end_);
s.Points[pt].Color = c;
s.IsVisibleInLegend = true;
if (array != null)
{
for (int i = 0; i < array.Length; i++)
{
if (slot == i + 1)
{ s.Points[pt].AxisLabel = array[i];
}
}
}
}
Taw's comment
public void setUpGantt(Chart chart)
{
chart.Series.Clear();
Series s = chart.Series.Add("sszs");
s.ChartType = SeriesChartType.RangeBar;
s.YValueType = ChartValueType.DateTime;
s.ResetIsVisibleInLegend () ;
s.IsVisibleInLegend = true;
Axis ax = chart.ChartAreas[0].AxisX;
Axis ay = chart.ChartAreas[0].AxisY;
ax.MajorGrid.Enabled = false;
Axis ax2 = chart.ChartAreas[0].AxisX2;
ax2.Enabled = AxisEnabled.True;
ax2.Maximum = 100;
ax2.MajorGrid.Enabled = false;
ay.IntervalType = DateTimeIntervalType.Minutes;
ay.Interval = 60;
ay.LabelStyle.Format = "HH:mm";
ay.Minimum = 0;
ay.Maximum = 0.2;
limitGantt(chart, "0:00", "24:00");
s.ToolTip = "#VALY1{HH:mm}~#VALY2{HH:mm}";
}
I'd like to have percentage = Green Time/Total time (from 00:
00 to Current time) example
Thank you for TaW's suggestion. It worked for me.
I post here to share who want to know
public void setUpGantt(Chart chart)
{
chart.Series.Clear();
Series s = chart.Series.Add("sszs");
s.ChartType = SeriesChartType.RangeBar;
s.YValueType = ChartValueType.DateTime;
s.ResetIsVisibleInLegend () ;
s.IsVisibleInLegend = true;
Axis ax = chart.ChartAreas[0].AxisX;
Axis ay = chart.ChartAreas[0].AxisY;
ax.MajorGrid.Enabled = false;
Axis ax2 = chart.ChartAreas[0].AxisX2;
ax2.Enabled = AxisEnabled.True;
ax2.MajorGrid.Enabled = true;
ax2.CustomLabels.Clear();// clear previous value when switch another data
for (int i = 0; i < 12; i++)
{
CustomLabel cl = new CustomLabel();
cl.FromPosition = i+0.5;
cl.ToPosition = i+1.5;
cl.Text = i+" %"; // example value to show on CustomLabel
ax2.CustomLabels.Add(cl);
}
ay.IntervalType = DateTimeIntervalType.Minutes;
ay.Interval = 60;
ay.LabelStyle.Format = "HH:mm";
ay.Minimum = 0;
ay.Maximum = 0.2;
limitGantt(chart, "0:00", "24:00");
s.ToolTip = "#VALY1{HH:mm}~#VALY2{HH:mm}";
}
The main script to show one more Axis with CustomLabel
Axis ax2 = chart.ChartAreas[0].AxisX2;
ax2.Enabled = AxisEnabled.True;
ax2.MajorGrid.Enabled = true;
ax2.CustomLabels.Clear();// clear previous value when switch another data
for (int i = 0; i < 12; i++)
{
CustomLabel cl = new CustomLabel();
cl.FromPosition = i+0.5;
cl.ToPosition = i+1.5;
cl.Text = i+" %"; // example value to show on CustomLabel
ax2.CustomLabels.Add(cl);
}
My program is pretty straight forward in concept - it allows a user to take scores during bowling tournaments, as well as showing the players the scores through a scoreboard.
There's a score sheet form in which they enter scores, and a scoreboard form that shows the scores to the players, by division. The scoreboard is run in a different thread than the main program.
The scoreboard is comprised of a TableLayoutPanel which I manipulate programmatically to represent a table of the scores to show. My issue with this is that it takes a long time (especially for a long list of players) for the table to render. The user experience of watching the table render itself leaves to be desired as well.
I tested the speed of rendering textboxes, labels, and picture boxes to lessen the load; textboxes won, so I changed the score labels to textboxes... but it's still not enough.
It's a lot to look through, but if anyone has any idea how I could further speed up the rendering of the scoreboard, I'd be all ears/eyes.
Here's my process broken down.
The calling method (called every 15 seconds by a timer):
private void switchBoard()
{
night = nights.GetNight(nightID);
NightDay = night.Night_Date.ToString("ddd");
//set the id of the division to show
dtDivisions = scoreBoard.roll_display(dtDivisions);
//get the row that is set to show
DataRow[] drs = dtDivisions.Select("showing = 1");
DataRow dr = drs[0];
//update the title
lbl_title.Top = 30;
lbl_title.Text = (string)dr["title"] + "'s Qualifying - " + NightDay;
lbl_title.Width = this.Width;
lbl_title.TextAlign = ContentAlignment.MiddleCenter;
//SET UP THE TABLE
//get number of columns (games) for selected night
int Cols = games.GetCountGamesForTourNightByDivision(tourID, nightID, scoreBoard.ShowDivision) + 3; //ACCOUNT FOR HEADER COLS, RANK AND TOTALS
//get number of rows (players) for selected night
int Rows = players.GetCountPlayersForTourNightByDivision(TourID, nightID, scoreBoard.ShowDivision) + 1; //ACCOUNT FOR HEADER ROWS
//generate the table
GenerateTable(Cols, Rows);
//generate the headers
GenerateHeaders(tourID, nightID);
//fill in the scores
GenerateScoreLabels(tourID, nightID, scoreBoard.ShowDivision);
}
Generating the table:
private void GenerateTable(int columnCount, int rowCount)
{
//Clear out the existing controls, we are generating a new table layout
this.tblPnlScoreboard.Controls.Clear();
//Clear out the existing row and column styles
this.tblPnlScoreboard.ColumnStyles.Clear();
this.tblPnlScoreboard.RowStyles.Clear();
//setting up the row and column counts
this.tblPnlScoreboard.ColumnCount = columnCount;
this.tblPnlScoreboard.RowCount = rowCount;
for (int x = 0; x < columnCount; x++)
{
//add a column
if(x==0) //ranking column
{
this.tblPnlScoreboard.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute,30));
}
else if(x==1) //names
{
this.tblPnlScoreboard.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
}
else if(x==columnCount-1) //totals
{
this.tblPnlScoreboard.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
}
else //games
{
this.tblPnlScoreboard.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, (this.tblPnlScoreboard.Width - 130) / columnCount));
}
for (int y = 0; y < rowCount; y++)
{
//add rows. Only do this once, when creating the first column
if (x == 0)
{
if(y==0)
{
this.tblPnlScoreboard.RowStyles.Add(new RowStyle(SizeType.Absolute, 50));
}
else
{
this.tblPnlScoreboard.RowStyles.Add(new RowStyle(SizeType.AutoSize));
}
}
}
}
}
Generating the headers:
private void GenerateHeaders(int TourID, int NightID)
{
//get the players to display
DataTable dtPlayers = players.GetPlayersForTourNightByDivision(tourID, nightID, scoreBoard.ShowDivision);
int Row = 1; //0 is the header row for Games and so on
foreach (DataRow dr in dtPlayers.Rows)
{
//create the label
Label lblPlayer = new Label();
lblPlayer.Name = dr["ID"].ToString(); //name is the ID of the player
lblPlayer.Text = dr["player_name"].ToString(); //the text is the name of the player
lblPlayer.BackColor = Color.Transparent;
lblPlayer.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
lblPlayer.TextAlign = ContentAlignment.MiddleLeft;
lblPlayer.AutoSize = true;
lblPlayer.Height = tblPnlScoreboard.GetRowHeights()[Row];
//add the label to the table
this.tblPnlScoreboard.Controls.Add(lblPlayer, 1, Row);
//create the Total label
Label lblTotal = new Label();
lblTotal.Name = "lbl_total"; //name is arbitrary in this context
lblTotal.Text = dr["Total"].ToString(); //the text is the total
lblTotal.BackColor = Color.Transparent;
lblTotal.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
lblTotal.TextAlign = ContentAlignment.MiddleLeft;
lblTotal.AutoSize = true;
lblTotal.Height = tblPnlScoreboard.GetRowHeights()[Row];
//add the label to the table
this.tblPnlScoreboard.Controls.Add(lblTotal, tblPnlScoreboard.ColumnCount, Row);
//increment the row index
Row++;
}
//totals column
Label lblTotals = new Label();
//lblTotals.Width = this.tblPnlScoreboard.GetColumnWidths()[this.tblPnlScoreboard.ColumnCount - 1];
lblTotals.Height = tblPnlScoreboard.GetRowHeights()[0];
lblTotals.Name = "lbl_total"; //name is the ID of the Game
lblTotals.Text = "Totals"; //text is the display name of the Game
lblTotals.BackColor = Color.Transparent;
lblTotals.ForeColor = Color.White;
lblTotals.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
lblTotals.TextAlign = ContentAlignment.MiddleCenter;
lblTotals.AutoSize = true;
lblTotals.Anchor = (AnchorStyles.None);
//add the label to the table
this.tblPnlScoreboard.Controls.Add(lblTotals, this.tblPnlScoreboard.ColumnCount-1, 0);
//get the games to display
DataTable dtGames = games.GetGamesForTourNightByDivision(tourID, nightID, scoreBoard.ShowDivision);
int Col = 2; //0 is the header column for rank, 1 is the header col for Players
foreach (DataRow dr in dtGames.Rows)
{
//create the label
Label lblGame = new Label();
lblGame.Width = this.tblPnlScoreboard.GetColumnWidths()[Col];
lblGame.Height = tblPnlScoreboard.GetRowHeights()[0];
lblGame.Name = dr["ID"].ToString(); //name is the ID of the Game
lblGame.Text = dr["disp_name"].ToString().Replace("Game ", ""); //text is the display name of the Game
lblGame.BackColor = Color.Transparent;
lblGame.ForeColor = Color.White;
lblGame.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
lblGame.TextAlign = ContentAlignment.MiddleCenter;
lblGame.Anchor = (AnchorStyles.None);
//add the label to the table
this.tblPnlScoreboard.Controls.Add(lblGame, Col, 0);
//increment the column index
Col++;
}
}
Finally, generating the scores:
private void GenerateScoreLabels(int TourID, int NightID, int DivID)
{
//get the id of the playergames record
//expl: each player/game pair has a unique ID - these IDs will be used to update the scores
Players players = new Players();
DataTable dtScores = players.GetPlayerGamesIDsForTourNightByDivision(TourID, NightID, scoreBoard.ShowDivision);
Divisions Divs = new Divisions();
DataTable dtColors = Divs.GetDivisionScoreboardColors(DivID);
foreach (DataRow dr in dtScores.Rows)
{
//find the coordinates in the table, where the textbox should be added
int col = FindX((int)dr["fk_game_id"]);
int row = FindY((int)dr["fk_player_id"]);
if (col > 0 && row > 0)
{
TextBox txt_score = new TextBox();
txt_score.Name = dr["ID"].ToString(); //name of the control is the player/game ID
txt_score.Text = dr["score"].ToString(); //the text in the control is the score
txt_score.ForeColor = Color.Black;
txt_score.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
txt_score.Width = this.tblPnlScoreboard.GetColumnWidths()[col];
txt_score.Height = tblPnlScoreboard.GetRowHeights()[0];
if(row % 2 == 0)
{
txt_score.BackColor = Color.FromArgb((int)dtColors.Rows[0]["sb_even_row_color"]);
}
else
{
txt_score.BackColor = Color.FromArgb((int)dtColors.Rows[0]["sb_odd_row_color"]);
}
txt_score.BorderStyle = BorderStyle.None;
txt_score.TextAlign = HorizontalAlignment.Center;
txt_score.Anchor = (AnchorStyles.Top);
this.tblPnlScoreboard.Controls.Add(txt_score, col, row);
//start the switchboard timer
ttmr_switch.Enabled = true;
}
}
}
On the CellPaint event of the TableLayoutPanel, I have these processes:
private void tblPnlScoreboard_CellPaint(object sender, TableLayoutCellPaintEventArgs e)
{
Graphics g = e.Graphics;
Rectangle r = e.CellBounds;
SolidBrush sb = GetBrushFor(e.Row, e.Column, scoreBoard.ShowDivision);
g.FillRectangle(sb, r);
sb.Dispose();
}
Selection of Colors:
private SolidBrush GetBrushFor(int row, int column, int DivID)
{
DataTable dt_colors = divisions.GetDivisionScoreboardColors(DivID);
if (row == 0)
{ //column headers
SolidBrush brush = new SolidBrush(Color.FromArgb((int)dt_colors.Rows[0]["sb_column_header_color"]));
return brush;
}
else
{
if(row % 2 == 0) //even row
{
SolidBrush brush = new SolidBrush(Color.FromArgb((int)dt_colors.Rows[0]["sb_even_row_color"]));
return brush;
}
else //odd row
{
SolidBrush brush = new SolidBrush(Color.FromArgb((int)dt_colors.Rows[0]["sb_odd_row_color"]));
return brush;
}
}
}
Some people suggested you to use a "proper technology". I would rather say "use properly the technology". Even this weird (sorry) design/implementation choice can be made to work much faster, as shown in the code below, which as you may see is handling rebuilding a table containing 100 rows x 10 columns 10 times per sec - not a big deal compared to professional grids, but far from the original implementation.
Key points:
1. Enclose table rebuild with Suspend/ResumeLayout to avoid intensive recalculations during the process.
2. Use custom double buffered TableLayoutPanel to avoid flickering.
3. Custom paint the data cells to avoid allocating a lot of controls.
Since essential data related parts are missing in the code you gave us, I can't provide you exactly the same working code. Hope you can recognize and map it to your stuff.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace Tests
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ScoreBoardForm { WindowState = FormWindowState.Maximized });
}
}
class ScoreBoardForm : Form
{
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
players = new List<Player>();
for (int i = 0; i < 100; i++)
players.Add(new Player { ID = i + 1, Name = "P" + (i + 1), Total = random.Next(1000) });
games = new List<Game>();
for (int i = 0; i < 10; i++)
games.Add(new Game { ID = i + 1, Name = "G" + (i + 1) });
scoreBoardTable = new ScoreBoardTable { Dock = DockStyle.Fill, Parent = this };
scoreBoardTable.Bounds = this.DisplayRectangle;
UpdateScoreBoard();
scoreBoardTable.CellPaint += OnScoreBoardTableCellPaint;
updateTimer = new Timer { Interval = 100 };
updateTimer.Tick += (_sender, _e) => UpdateScoreBoard();
updateTimer.Start();
}
private void OnScoreBoardTableCellPaint(object sender, TableLayoutCellPaintEventArgs e)
{
int playerIndex = e.Row - 1, gameIndex = e.Column - 2;
if (playerIndex >= 0 && playerIndex < players.Count && gameIndex >= 0 && gameIndex < games.Count)
{
using (var br = new SolidBrush(GetBackColor(e.Row)))
e.Graphics.FillRectangle(br, e.CellBounds);
var score = GetScore(players[playerIndex], games[gameIndex]);
var sf = new StringFormat { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center };
e.Graphics.DrawString(score.ToString(), defaultCellFont, Brushes.Black, e.CellBounds, sf);
}
}
private int GetScore(Player player, Game game)
{
return random.Next(10000);
}
class ScoreBoardTable : TableLayoutPanel
{
public ScoreBoardTable() { DoubleBuffered = AutoScroll = true; }
}
TableLayoutPanel scoreBoardTable;
Timer updateTimer;
List<Player> players;
List<Game> games;
Random random = new Random();
class Player
{
public int ID;
public string Name;
public int Total;
}
class Game
{
public int ID;
public string Name;
}
private void UpdateScoreBoard()
{
scoreBoardTable.SuspendLayout();
GenerateTable(games.Count + 3, players.Count + 1);
GenerateHeaderCells();
// Custom cell paint is much faster, but requires a good data model.
// If you uncomment the following line, make sure to get rid of CellPaint.
//GenerateScoreCells();
scoreBoardTable.ResumeLayout(true);
}
private void GenerateTable(int columnCount, int rowCount)
{
scoreBoardTable.Controls.Clear();
scoreBoardTable.ColumnStyles.Clear();
scoreBoardTable.RowStyles.Clear();
scoreBoardTable.ColumnCount = columnCount;
scoreBoardTable.RowCount = rowCount;
// Columns
// Ranking
scoreBoardTable.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 30));
// Name
scoreBoardTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
// Games
var percent = (columnCount - 3) / (float)columnCount;
for (int col = 2; col < columnCount - 1; col++)
scoreBoardTable.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, percent));
// Totals
scoreBoardTable.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
// Rows
// Header
scoreBoardTable.RowStyles.Add(new RowStyle(SizeType.Absolute, 50));
// Players
for (int row = 1; row < rowCount; row++)
scoreBoardTable.RowStyles.Add(new RowStyle(SizeType.AutoSize));
}
private void GenerateHeaderCells()
{
Color backColor = Color.DarkGray, foreColor;
int row, col;
// Header
row = 0;
foreColor = Color.White;
col = 0;
AddCell(row, col++, "rank", "", foreColor, backColor);
AddCell(row, col++, "playerName", "Player", foreColor, backColor);
foreach (var game in games)
AddCell(row, col++, "gameName" + game.ID, game.Name, foreColor, backColor);
AddCell(row, col, "totalColumn", "Totals", foreColor, backColor);
// Rows
foreColor = Color.Black;
row++;
foreach (var player in players)
{
backColor = GetBackColor(row);
AddCell(row, 0, "playerRank_" + player.ID, "", foreColor, backColor, ContentAlignment.MiddleLeft);
AddCell(row, 1, "playerName_" + player.ID, player.Name, foreColor, backColor, ContentAlignment.MiddleLeft);
AddCell(row, scoreBoardTable.ColumnCount, "playerTotal_" + player.ID, player.Total.ToString(), foreColor, backColor, ContentAlignment.MiddleRight);
row++;
}
}
private void GenerateScoreCells()
{
var foreColor = Color.Black;
int row = 1;
foreach (var player in players)
{
var backColor = GetBackColor(row);
int col = 2;
foreach (var game in games)
{
var score = GetScore(player, game);
AddCell(row, col, "score_" + player.ID + "_" + game.ID, score.ToString(), foreColor, backColor, ContentAlignment.MiddleRight, false);
col++;
}
row++;
}
}
static readonly Font defaultCellFont = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
private Label AddCell(int row, int col, string name, string text, Color foreColor, Color backColor, ContentAlignment textAlign = ContentAlignment.MiddleCenter, bool autoSize = true)
{
var label = new Label();
label.Name = name;
label.Text = text;
label.BackColor = backColor;
label.ForeColor = foreColor;
label.Font = defaultCellFont;
label.TextAlign = textAlign;
label.AutoSize = autoSize;
label.Margin = new Padding(0);
label.Dock = DockStyle.Fill;
scoreBoardTable.Controls.Add(label, col, row);
return label;
}
static Color GetBackColor(int row)
{
if (row % 2 == 0) //even row
return Color.Yellow;
else //odd row
return Color.LightGreen;
}
}
}
EDIT And here is equivalent implementation using DataGridView (note that now the number of rows (players) are ten times more at the same refresh rate):
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace Tests
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ScoreBoardForm { WindowState = FormWindowState.Maximized });
}
}
class ScoreBoardForm : Form
{
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
players = new List<Player>();
for (int i = 0; i < 1000; i++)
players.Add(new Player { ID = i + 1, Name = "P" + (i + 1), Total = random.Next(1000) });
games = new List<Game>();
for (int i = 0; i < 10; i++)
games.Add(new Game { ID = i + 1, Name = "G" + (i + 1) });
InitScoreBoard();
UpdateScoreBoard();
updateTimer = new Timer { Interval = 100 };
updateTimer.Tick += (_sender, _e) => UpdateScoreBoard();
updateTimer.Start();
}
DataGridView scoreBoardTable;
Timer updateTimer;
List<Player> players;
List<Game> games;
Random random = new Random();
class Player
{
public int ID;
public string Name;
public int Total;
}
class Game
{
public int ID;
public string Name;
}
private int GetScore(Player player, Game game)
{
return random.Next(10000);
}
void InitScoreBoard()
{
scoreBoardTable = new DataGridView { Dock = DockStyle.Fill, Parent = this };
scoreBoardTable.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
scoreBoardTable.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
scoreBoardTable.MultiSelect = false;
scoreBoardTable.CellBorderStyle = DataGridViewCellBorderStyle.None;
scoreBoardTable.BackgroundColor = Color.Honeydew;
scoreBoardTable.ForeColor = Color.Black;
scoreBoardTable.AllowUserToAddRows = scoreBoardTable.AllowUserToDeleteRows = scoreBoardTable.AllowUserToOrderColumns = scoreBoardTable.AllowUserToResizeRows = false;
scoreBoardTable.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
scoreBoardTable.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells;
scoreBoardTable.RowHeadersVisible = false;
scoreBoardTable.EnableHeadersVisualStyles = false;
var style = scoreBoardTable.DefaultCellStyle;
style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
style = scoreBoardTable.ColumnHeadersDefaultCellStyle;
style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
style.BackColor = Color.Navy;
style.ForeColor = Color.White;
style = scoreBoardTable.RowHeadersDefaultCellStyle;
style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
style = scoreBoardTable.RowsDefaultCellStyle;
style.SelectionForeColor = style.ForeColor = Color.Black;
style.SelectionBackColor = style.BackColor = Color.Yellow;
style = scoreBoardTable.AlternatingRowsDefaultCellStyle;
style.SelectionForeColor = style.ForeColor = Color.Black;
style.SelectionBackColor = style.BackColor = Color.LightGreen;
scoreBoardTable.CellFormatting += OnScoreBoardCellFormatting;
}
private void UpdateScoreBoard()
{
scoreBoardTable.ColumnCount = 3 + games.Count;
for (int c = 0; c < scoreBoardTable.ColumnCount; c++)
{
var col = scoreBoardTable.Columns[c];
if (c == 0)
{
col.Name = "Rank";
col.HeaderText = "";
col.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
col.Width = 48;
}
else if (c == 1)
{
col.Name = "Player";
col.HeaderText = "Player";
col.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
}
else if (c == scoreBoardTable.ColumnCount - 1)
{
col.Name = "Totals";
col.HeaderText = "Totals";
col.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
//col.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
}
else
{
var game = games[c - 2];
col.Name = "Game_" + game.ID;
col.HeaderText = game.Name;
col.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
//col.AutoSizeMode = DataGridViewAutoSizeColumnMode.DisplayedCells;
}
}
scoreBoardTable.RowCount = players.Count;
scoreBoardTable.AutoResizeColumnHeadersHeight();
scoreBoardTable.Invalidate();
}
private void OnScoreBoardCellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
var player = players[e.RowIndex];
int col = e.ColumnIndex;
if (col == 0)
e.Value = "";
else if (col == 1)
e.Value = player.Name;
else if (col == scoreBoardTable.ColumnCount - 1)
e.Value = player.Total.ToString();
else
{
var game = games[col - 2];
e.Value = GetScore(player, game).ToString();
}
e.FormattingApplied = true;
}
}
}
Well, the CellPaint event was the culprit. I was able to achieve what I wanted by negating the CellPaint event, and instead manipulating the controls in the panel so as to have the right background colors as well as size so they fill the grid.
Thank you for all your comments - they were very helpful. As per HighCore's comment, I will now be looking into WPF and I might be able to get something even slicker.
I need help with the Check button. After a user adds all the 42 numbers in the textbox and enter a number from 0-9 in the "Enter number" area and clicks on the start button, next what he should do is run through the array of labels with the red label or lblCovece and he should collect the same values like the number enters before. And after he clicks on the Check button, the programme should first validate if the value that is selected with the red label is the same as the number entered. If is valid the label should turn green and than the result should appear in the label lblResultE(the result for example should be like this: if the number entered is 2, the result it is 2+2+2...)and if is not valid in the lblResultE we take out 10 points. That's what i did by now with some help.:) thank you.
namespace Seminarska
{
public partial class Form1 : Form
{
private Label l,l2,lblCovece,l4,lblResultE;
private Button bUp, bRight, bLeft, bDown, bCheck,bStart, bReset;
private TextBox txtVnes, txtGoal;
private Label[] pole;
public Form1()
{
InitializeComponent();
l2 = new Label();
l2.Text = " Enter one number";
l2.Location = new Point(230, 200);
l2.AutoSize = true;
l4 = new Label();
l4.Text = "Score";
l4.Location = new Point(240, 260);
l4.AutoSize = true;
lblResultE = new Label();
lblResultE.Location = new Point(350, 260);
lblResultE.AutoSize = true;
bLeft = new Button();
bLeft.Location = new Point(0, 250);
bLeft.Width=75;
bLeft.Height = 25;
bLeft.Text = "LEFT";
bCheck = new Button();
bCheck.Location = new Point(75, 250);
bCheck.Width = 75;
bCheck.Height = 25;
bCheck.Text = "Check";
bRight = new Button();
bRight.Location = new Point(150, 250);
bRight.Width = 75;
bRight.Height = 25;
bRight.Text = "RIGHT";
bUp = new Button();
bUp.Location = new Point(75, 220);
bUp.Width = 75;
bUp.Height = 25;
bUp.Text = "UP";
bDown = new Button();
bDown.Location = new Point(75, 280);
bDown.Width = 75;
bDown.Height = 25;
bDown.Text = "DOWN";
bStart = new Button();
bStart.Location = new Point(240, 165);
bStart.Width = 75;
bStart.Height = 25;
bStart.Text = "START";
bReset = new Button();
bReset.Location = new Point(320, 165);
bReset.Width = 75;
bReset.Height = 25;
bReset.Text = "RESET";
txtVnes = new TextBox();
txtVnes.Location = new Point(240, 10);
txtVnes.Width = 160;
txtVnes.Height = 130;
txtVnes.Multiline = true;
txtGoal = new TextBox();
txtGoal.Width = 75;
txtGoal.Height = 25;
txtGoal.Location = new Point(330, 200);
lblCovece = new Label();
lblCovece.Location = new Point(160,165);
lblCovece.Width = 20;
lblCovece.Height = 20;
lblCovece.TextAlign = ContentAlignment.MiddleCenter;
lblCovece.Text = "O";
lblCovece.BackColor = Color.FromArgb(255, 0, 0);
int a = 0;
pole = new Label[42];
this.Controls.Add(lblCovece);
for (int i = 1; i <= 6; i++)
{
for (int j = 1; j <= 7; j++)
{
l = new Label();
l.Name = "label" + i.ToString() + j.ToString();
l.Text = "Z";
l.Width = 20;
l.Height = 20;
l.TextAlign = ContentAlignment.MiddleCenter;
l.Parent = this;
l.BackColor = Color.FromArgb(100, 149, 237);
l.Location = new Point(10 + (j - 1) * 25, 15 + (i - 1) * 25);
pole[a] = l;
this.Controls.Add(l);
a++;
}
}
this.Width = 460;
this.Height = 380;
this.Controls.Add(l2);
this.Controls.Add(l4);
this.Controls.Add(lblResultE);
this.Controls.Add(lblTimeE);
this.Controls.Add(bStart);
this.Controls.Add(bReset);
this.Controls.Add(txtGoal);
this.Controls.Add(txtVnes);
this.Controls.Add(bUp);
this.Controls.Add(bLeft);
this.Controls.Add(bRight);
this.Controls.Add(bDown);
this.Controls.Add(bCheck);
bStart.Click+=new EventHandler(bStart_Click);
bUp.Click+=new EventHandler(bUp_Click);
bDown.Click+=new EventHandler(bDown_Click);
bLeft.Click+=new EventHandler(bLeft_Click);
bRight.Click+=new EventHandler(bRight_Click);
bCheck.Click+=new EventHandler(bZemaj_Click);
bReset.Click+=new EventHandler(bReset_Click);
}
private void bStart_Click(object sender, EventArgs e)
{
string Str = txtGoal.Text.Trim();
int Num;
bool isNum = int.TryParse(Str, out Num);
if (isNum && Str.Length == 1)
{
string[] ts = txtVnes.Text.Split(
new string[] { "\r\n" },
StringSplitOptions.RemoveEmptyEntries);
int row = 0;
for (int i = 0; i < ts.Length && row < 6; i++)
{
if (LineIsValid(ts[i]))
{
for (int col = 0; col < 7; col++)
{
pole[row * 7 + col].Text = ts[i][2 * col].ToString();
}
row++;
}
}
for (; row < 6; row++)
{
for (int col = 0; col < 7; col++)
{
pole[row * 7 + col].Text = "Z";
}
}
}
else
{
MessageBox.Show("Invalid Input");
}
}
private static Regex regex = new Regex(#"^(\s)*(\d ){6}\d(\s)*$");
private static bool LineIsValid(string line)
{
return regex.IsMatch(line);
}
private void bReset_Click(object sender, EventArgs e)
{
txtVnes.Clear();
string[] ts = txtVnes.Text.Split(new string[] { "\r\n" },
StringSplitOptions.RemoveEmptyEntries);
int row = 0;
for (int i = 0; i < ts.Length && row < 6; i++)
{
for (int col = 0; col < 7; col++)
{
pole[row * 7 + col].Text = "Z";
pole[row * 7 + col].BackColor=Color.FromArgb(100, 149, 237);
}
row++;
}
for (; row < 6; row++)
{
for (int col = 0; col < 7; col++)
{
pole[row * 7 + col].Text = "Z";
pole[row * 7 + col].BackColor = Color.FromArgb(100, 149, 237);
}
}
txtGoal.Clear();
lblCovece.Location=new Point(160,165);
}
private void bUp_Click(object sender, EventArgs e)
{
lblCovece.Location = new Point(lblCovece.Location.X, lblCovece.Location.Y -
25);
}
private void bDown_Click(object sender, EventArgs e)
{
lblCovece.Location = new Point(lblCovece.Location.X, lblCovece.Location.Y +
25);
}
private void bLeft_Click(object sender, EventArgs e)
{
lblCovece.Location = new Point(lblCovece.Location.X - 25,
lblCovece.Location.Y);
}
private void bRight_Click(object sender, EventArgs e)
{
lblCovece.Location = new Point(lblCovece.Location.X + 25,
lblCovece.Location.Y);
}
private void bCheck_Click(object sender, EventArgs e)
{
}
private void Form1_Load(object sender, EventArgs e)
{
}
}
}
What makes your programm complicated and difficult to understand, is that you mix game logic with display logic.
I suggest you to redesign your game. It could look something like this:
public enum State
{
Empty, // Displays "Z"
Neutral, // Blue
Good, // Green
Bad // Red
}
public class Square
{
public int Number { get; set; }
public State State { get; set; }
}
public class Game
{
public const int Width = 7, Height = 6;
public Game()
{
Board = new Square[Width, Height];
}
public event EventHandler GameChanged;
public Square[,] Board { get; private set; }
public int CurrentX { get; private set; }
public int CurrentY { get; private set; }
public void Reset()
{
for (int x = 0; x < Width; x++) {
for (int y = 0; y < Height; y++) {
Board[x, y].State = State.Empty; // Always displayed in blue as "Z"
}
}
OnGameChanged();
}
public void MoveLeft()
{
if (CurrentX > 0) {
CurrentX--;
OnGameChanged();
}
}
public void MoveRight()
{
if (CurrentX < Width - 1) {
CurrentX++;
OnGameChanged();
}
}
// and so on
private void OnGameChanged()
{
EventHandler eh = GameChanged;
if (eh != null) {
eh(this, EventArgs.Empty);
}
}
}
In the form I would define pole to be a matrix as well (like the board). I show only a few relevant parts of the form code, to give you an idea of what I mean:
public class Form1 : Form
{
private Game game;
private Label[,] pole;
public Form1()
{
game = new Game();
game.GameChanged += new EventHandler(Game_GameChanged);
pole = new Label[Game.Width, Game.Height];
// Intialize pole.
// ...
}
void Game_GameChanged(object sender, EventArgs e)
{
for (int x = 0; x < Game.Width; x++) {
for (int y = 0; y < Game.Height; y++) {
Square square = game.Board[x, y];
Label label = pole[x,y];
switch (square.State) {
case State.Empty:
label.BackColor = Color.Blue;
label.Text = "Z";
break;
case State.Neutral:
label.BackColor = Color.Blue;
label.Text = square.Number.ToString();
break;
case State.Good:
label.BackColor = Color.Green;
label.Text = square.Number.ToString();
break;
case State.Bad:
label.BackColor = Color.Red;
label.Text = square.Number.ToString();
break;
default:
break;
}
}
}
// Place lblCovece according to game.CurrentX and game.CurrentY
// ...
}
private void bReset_Click(object sender, EventArgs e)
{
game.Reset();
}
private void bLeft_Click(object sender, EventArgs e)
{
game.MoveLeft();
}
private void bRight_Click(object sender, EventArgs e)
{
game.MoveRight();
}
}
Note how the Game class tells the form when changes happen through the event GameChanged. The form then updates the game display. In the game class, you can now concentrate on the game logic and do not have to deal with buttons and labels. You can also work with logical coordinates in the range [0..6] and [0..5] of the game board. This is easier than working with pixels. You delegate all the pixel calculations to the form.
My example is not complete, but when you try to implement it, you will see that it will be much easier to think about how the logic of the game should work.
Add an int score;
private void bCheck_Click(object sender, EventArgs e)
{
bool found = false;
foreach (Label l in pole)
{
if (l.Location == lblCovece.Location && l.Text == txtGoal.Text)
{
l.BackColor = Color.Green;
score += int.Parse(l.Text);
lblResultE.Text = score.ToString();
found = true;
}
}
if (!found)
{
score -= 10;
lblResultE.Text = score.ToString();
}
}