I try to create simple winform report(s) based on table from my database. Because there are many aggregations I want to create one parent chart and optional child chart.
Below is the core which draws "parent" report
private bool DrawReport(DataTable dt)
{
string serieName;
foreach (object itemChecked in checkedListBox1.CheckedItems)
{
serieName = itemChecked.ToString();
chart1.Series.Add(serieName);
chart1.Series[serieName].ChartType = SeriesChartType.Column;
//chart1.Series[0].IsValueShownAsLabel = true;
}
chart1.ChartAreas[0].AxisX.LabelStyle.Angle = 90;
chart1.ChartAreas[0].AxisX.Interval = 1;
chart1.ChartAreas[0].AxisY.MajorGrid.LineWidth = 0;
chart1.ChartAreas[0].AxisX.MajorGrid.LineWidth = 0;
foreach (DataRow dr in dt.Rows)
{
string X = dr["DATE"].ToString() + " " + dr["HOUR"].ToString();
string Y = dr["MED"].ToString();
try
{
chart1.Series[SER].Points.AddXY(X, Y);
}
catch (Exception)
{
throw;
}
}
chart1.DataBind();
chart1.Visible = true;
return true;
}
Until now everything works fine.
As you see X axis labels contains periods of time - in fact 4 or 6 hours periods.
The point is to create another chart "child one". It would appears when I click on element of serie. So for now question is how to read value of clicked x axis element ( which is in fact string of dates) to variable. I am able to show this value on tooltip
chart1.Series[0].ToolTip = "#VALX"
but I cant find the way to assign it to variable.
I suggest you download the chart samples from MSDN, as it can help quite a bit with these charts. For your specific problem, you can use Chart.HitTest to figure out which charting element the mouse was clicked on (or moved over, etc). Something like (I used this for MouseMove, but you can easily adapt it for MouseClick or another event):
private void ChartMouseMove(object sender, MouseEventArgs e)
{
try
{
HitTestResult[] htrList = Chart.HitTest(e.X, e.Y, false, ChartElementType.DataPoint);
// loop through all of the elements in htrList, and make the "child" chart
}
catch (Exception)
{
StatusLabelText = "";
}
}
If the chart element that was clicked was a DataPoint, then you can get the x and/or y value as needed. One thing that you may want to consider: instead of constructing a date representation yourself (string X = dr["DATE"].ToString() + " " + dr["HOUR"].ToString();), you may want to use a DateTime object as the x value of the DataPoint, and just have the chart format it appropriately for display (chart.AxisX.Format = "mmddyy hh" in pseudo-code). This will make it easier to match the point when creating the child chart.
One other point: one of your last lines (chart.DataBind();) is not needed based on the code presented. You never specify a DataSource for the chart, and you add DataPoints manually, so data binding is not needed.
Related
I am new in Winform Application. I am trying to implement a line chart which have multiple series and have a checkedListbox to select the particular series.
Code:
if (tbROI.SelectedTab == tbROI.TabPages["tbPageROIPoint"])
{
//If all ROI TAB
myIndex = GetMyChartIndex(mSeries, chartPointROI); // 4 for Point ROI tab
m_PointDataCounter++;
if (m_PointDataCounter > 15)
{
if (myIndex > 5)
{
chartPointROI.Series[mSeries].Points.RemoveAt(0);
m_PointDataCounter--;
}
}
if (cbListPOI.GetItemChecked(ROIIndex))
{
chartPointROI.Series[mSeries].Points.AddXY(timestring, mData);
chartPointROI.ResetAutoValues();
}
}
Using this code I am putting the data on the chart control.
The X-axis representing Time and Y-axis representing Data.
Initailly when I select any item of listbox the series starts from left side, but after sometime if I start one more series it will also start from left side but I want to start that from the current time which is representing on X-axis.
And when I stop any series after some time if I again start the same series I want to some gap in the series so that it can be clearly seen that series is been stopped.
In my case the series always starts from left side. And if I stop any series and start that again It will continue where it stopped.
Thanks in advance
EDIT:
It is showing that
Here is an example of how to remove a few DataPoints and also of how to restore them.
Note the flat line in the gap. If you want to 'remove' that line best color the last point transparent; I have added commented code for this.
List<DataPoint> marked = new List<DataPoint>();
int markedStartIndex = -1;
private void button1_Click(object sender, EventArgs e)
{
// I create a testperiod to remove
DateTime dt0 = DateTime.Now.AddMonths(2);
DateTime dt1 = dt0.AddHours(123);
DateTime dt2 = dt0.AddHours(173);
// convert to doubles:
double startPeriod = dt1.ToOADate();
double endPeriod = dt2.ToOADate();
// short reference
Series s = chart1.Series[0];
// select the points in the period. pick your border conditions!
marked = s.Points.Cast<DataPoint>()
.Where(x => x.XValue > startPeriod && x.XValue < endPeriod)
.ToList();
if (marked.Count < 1) return;
// remember where we started to remove
markedStartIndex = s.Points.IndexOf(marked.First());
foreach (DataPoint dp in marked) s.Points.Remove(dp);
// Optionally 'hide' the gap line
//if (markedStartIndex > 0) s.Points[markedStartIndex].Color = Color.Transparent;
}
The code to bring them back inserts them at the right spot and then clears the points.:
private void button2_Click(object sender, EventArgs e)
{
Series s = chart1.Series[0];
// optionally re-color the gap-line
//if (markedStartIndex > 0) s.Points[markedStartIndex].Color = s.Color;
foreach (DataPoint dp in marked) s.Points.Insert(markedStartIndex++, dp);
marked.Clear();
}
Result with a transparent gap:
You could also color the gap in red and also store more than one set of points; for this you would have to store the starting points as well as make sure to manage multiple periods when you re-insert them!
As an alternative to actually removing the points you could also choose to simply color them transparent..
This is my first post. So..critique is always welcomed.
My question is straight forward just by looking at the title.
How can I use a loop to insert values to different labels by using their reference (get,set methods are in a different form)
What I've tried is to create an array with the references of the labels. The thing is.. it assigns the new values to the array rather than changing the reference which will change the label.
I find it a bit difficult to explain it further than that.
If you have any questions, I will try to answer them best to my knowledge
private void button1_Click(object sender, EventArgs e)
{
int numOfPeriods = Convert.ToInt32(cmbPeriods.Text)-1;
string initialString = cmbStartTime.Text; //Gets the input from combo box "cmbStartTime".
string newTime;
decimal dec = Convert.ToDecimal(TimeSpan.Parse(initialString).TotalHours); //Converts the set by user Lesson start time to a decimal value.
decimal dec2;
decimal lessonLength = 1; // Default lesson length is set to 1 hour.
TimeSpan time;
Test FormOpen = new Test();
string[] periodStartTimes = new string[9] //Loop referencing the labels on Form TEST
{
FormOpen.startTime,FormOpen.startTime2, FormOpen.startTime3, FormOpen.startTime4,
FormOpen.startTime5, FormOpen.startTime6, FormOpen.startTime7, FormOpen.startTime8,
FormOpen.startTime9
};
if (cmbLessonLength.Text != "") //If the combo box "cmbLessonLength" is not empty, use that value instead of the default lesson lenght.
{
lessonLength = Convert.ToDecimal(cmbLessonLength.Text)/60; //Converts minutes to hours.
}
dec2 = dec + lessonLength;
time = TimeSpan.FromHours(Double.Parse(dec2.ToString()));
newTime = time.ToString();
if (newTime[0] == '0')
{
FormOpen.startTime = initialString + " - " + newTime.Remove(5).Remove(0, 1);
}
else
{
FormOpen.startTime = initialString + " - " + newTime.Remove(5);
}
for (int x = 1; x <= numOfPeriods; x++) //LOOP THAT ASSIGNS VALUES TO THE ARRAY AND NOT THE REFERENCE INSIDE IT
{
decimal workingNumber = lessonLength*x;
decimal Convert0 = dec + workingNumber;
TimeSpan Convert1 = TimeSpan.FromHours(Double.Parse(Convert0.ToString()));
string Convert2 = Convert1.ToString().Remove(5);
periodStartTimes[x] = Convert2;
}
FormOpen.subjectName = cmbSubjects.Text;
FormOpen.startTime2 = periodStartTimes[1]; //IT DOES WORK LIKE THIS
FormOpen.startTime3 = periodStartTimes[2];
FormOpen.ShowDialog();
}
I have provided the whole code, so it's clearer and if there's a more efficient way of coding this I would be really thankful if someone could give me a few tips.
Your code cannot work using that array periodStartTimes This is an array of strings and when you initialize it you get the value of the property (IE the current text of the labels) not a reference to a property that you can use to change the labels.
In any case it is a bad practice to allow a different class instance change the internal values of another class. It is better to provide a public method used by the external classes to change the internal properties.
So for example your Test form class could have a public method named as SetTextLabel
public class Test : Form
{
....
public void SetLabelText(string name, string value)
{
switch(name)
{
case "startTime":
this.labelForStartTime.Text = value;
break;
case "label1":
this.firstLabelToUpdate.Text = value;
break;
...
// and so on for all other labels
}
}
}
In this way the code that changes the label is inside the Test class and you can add all the checks and validations required with full access to all the internal variables of the Test class.
Now your loop can be rewritten as
for (int x = 1; x <= numOfPeriods; x++)
{
decimal workingNumber = lessonLength*x;
decimal Convert0 = dec + workingNumber;
TimeSpan Convert1 = TimeSpan.FromHours(Double.Parse(Convert0.ToString()));
string Convert2 = Convert1.ToString().Remove(5);
FormOpen.SetLabelText("label" + x.ToString, Convert2;
}
Of course now you don't need anymore the array, and this will probably remove another error present in your actual code. The array is fixed at 9 elements but you loop for numOfPeriods starting from 1 (thus skipping the first element) and if numOfPeriods is bigger than 8 your code will crash with IndexOutOfRange exception
I'm making a sudoku in Windows Form Application.
I have 81 textboxes and I have named them all textBox1a, textBox1b... textBox2a, textBox2b...
I want to make it so that if any of the textboxes, in any of the rows, is equal to any other textbox in the same row, then both will get the background color red while the textboxes are equal.
I tried using this code just for test:
private void textBox1a_TextChanged_1(object sender, EventArgs e)
{
while (textBox1a.Text == textBox1b.Text)
{
textBox1a.BackColor = System.Drawing.Color.Red;
textBox1b.BackColor = System.Drawing.Color.Red;
}
It didn't work, and I don't know where I should put all this code, I know I shouldn't have it in the textboxes.
Should I use a code similar to this or is it totally wrong?
You want to iterate over the collection of text boxes just once, comparing it to those that haven't yet been compared against. If you have your textboxes in an array (let's call it textBoxes), and know which one was just changed (e.g. from the textChanged handler), you could do:
void highlightDuplicates(int i) // i is the index of the box that was changed
{
int iVal = textBoxes[i].Text;
for (int j = 0; j < 82; j++)
{
// don't compare to self
if (i == j) return;
if (textBoxes[j].Text == iVal)
{
textBoxes[i].BackgroundColor = System.Drawing.Color.Red;
textBoxes[j].BackgroundColor = System.Drawing.Color.Red;
}
}
}
If you wanted to get fancier, you could put your data in something like: Dictionary<int, TextBox>, where the key is the value and the TextBox is a reference to the text box with that value. Then you can quickly test for duplicate values with Dictionary.Contains() and color the matching text box by getting its value.
I think your current code would result in an infinite loop. The textboxes' values can't change while you are still in the event handler, so that loop would never exit.
If all of your boxes are named according to one convention, you could do something like this. More than one input can use the same handler, so you can just assign this handler to all the boxes.
The following code is not tested and may contain errors
private void textBox_TextChanged(object sender, EventArgs e){
var thisBox = sender as TextBox;
//given name like "textBox1a"
var boxNumber = thisBox.Name.SubString(7,1);
var boxLetter = thisBox.Name.SubString(8,1);
//numbers (horizontal?)
for(int i = 1; i<=9; i++){
if(i.ToString() == boxNumber)
continue; //don't compare to self
var otherBox = Page.FindControl("textBox" + i + boxLetter) as TextBox;
if (otherBox.Text == thisBox.Text)
{
thisBox.BackColor = System.Drawing.Color.Red;
otherBox.BackColor = System.Drawing.Color.Red;
}
}
//letters (vertical?)
for(int i = 1; i<=9; i++){
var j = ConvertNumberToLetter(i); //up to you how to do this
if(j == boxLetter)
continue; //don't compare to self
var otherBox = Page.FindControl("textBox" + boxNumber + j) as TextBox;
if (otherBox.Text == thisBox.Text)
{
thisBox.BackColor = System.Drawing.Color.Red;
otherBox.BackColor = System.Drawing.Color.Red;
}
}
}
I believe you will be more effective if create an Array (or a List) of Integers and compare them in memory, against compare them in UI (User Interface).
For instance, you could:
1) Create an Array of 81 integers.
2) Everytime the user input a new number, you search for it in that Array. If found, set the textbox as RED, otherwise, add the new value to that array.
3) The ENTER event may be allocated fot the entire Textboxes (utilize the Handles keyword with all Textboxes; like handles Text1.enter, Text2.enter, Text3.enter ... and so forth)
Something like:
int[] NumbersByUser = new int[81];
Private Sub Textbox1.Enter(sender as object, e as EventArgs) handles Textbox1.Enter, Textbox2.Enter, Textbox3.enter ...
int UserEntry = Convert.ToInt32(Sender.text);
int ValorSelecionado = Array.Find(NumbersByUser, Z => (Z == UserEntry));
if (ValorSelecionado > 0) {
Sender.forecolor = Red;
}
else
{
NumbersByUser(Index) = UserEntry;
}
You should have a 2 dimensional array of numbers (could be one dimensional, but 2 makes more sense) let's assume its called Values. I suggest that you have each textbox have a incrementing number (starting top left, going right, then next row). Now you can do the following:
All TextBox Changed events can point to the same function. The function then takes the tag to figure out the position in the 2dim array. (X coordinate is TAG % 9 and Y coordinate is TAG / 9)
In the callback you can loop over the textboxes and colorize all boxes as you like. First do the "check row" loop (pseudo code)
var currentTextBox = ((TextBox)sender)
var x = ((int)currentTextBox.Tag) % 9
var y = ((int)currentTextBox.Tag) / 9
// First assign the current value to the backing store
Values[currentTextBox] = int.parse(currentTextBox.Text)
// assuming variable x holding the column and y holding the row of current box
// Array to hold the status of a number (is it already used?)
bool isUsed[9] = {false, false, ...}
for(int col = 0; col <= 9; i++)
{
// do not compare with self
if(col == x) continue;
isUsed[textBox] = true;
}
// now we have the status of all other boxes
if( isUsed[Values[x,y]] ) currentTextBox.Background = Red else currentTextBox.Background = Green
// now repeat the procedure for the column iterating the rows and for the blocks
I would suggest a dynamic approach to this. Consider each board item as a cell (this would be it's own class). The class would contain a numeric value and other properties that could be useful (i.e. a list of possible values).
You would then create 3 collections of the cells, these would be:
A collection of rows of 9 cells (for tracking each row)
A collection of columns of 9 cells (for tracking each column)
A collection of 3x3 cells
These collections would share references - each cell object would appear once in each collection. Each cell could also contain a reference to each of the 3 collections.
Now, when a cell value is changed, you can get references to each of the 3 collections and then apply a standard set of Sudoku logic against any of those collections.
You then have some display logic that can walk the boards of cells and output to the display (your View) your values.
Enjoy - this is a fun project.
I'm trying to do a range column plot of a set of agents' tasks using the Chart control in C# .NET. I plot agent number across the x axis and task time along the y axis. My only problem is that the column data will not align properly with the agent numbers on the x axis. Does anyone know how to align the columns with their corresponding x axis labels?
Here is an image of my graph:
Here is my code:
chartSchedule.Titles.Add("Agent / Task Schedule");
chartSchedule.ChartAreas[0].AxisX.Title = "Agent";
chartSchedule.ChartAreas[0].AxisY.Title = "Time";
int index = 0;
foreach ( Agent a in _agents )
{
// Create a series for each agent and set up display details
Series agentSeries = chartSchedule.Series.Add("Agent " + a.Id);
agentSeries.ChartType = SeriesChartType.RangeColumn;
// Alternate colours of series lines
if ( index % 2 > 0 )
agentSeries.Color = Color.DodgerBlue;
else
agentSeries.Color = Color.Blue;
// Display start and end columns of every task
List<DataPoint> timeData = new List<DataPoint>();
foreach ( NodeTask t in a.AssignedTasks )
{
agentSeries.Points.AddXY(index + 1, t.StartTime, t.EndTime);
}
index++;
}
The reason for the seeming 'misalignment' is that you are adding a total of five series but each has only one (set of) Datapoint(s) per X-Value.
This existent DataPoint is then combined wih the the four non-existent DataPoints and the five of them are displayed side by side as one block centered at the X-Values/Labels. This looks only right for the middle Series, which actually has the middle point.
You could add a few the other Points to see the effect..:
agentSeries.Points.AddXY(1, 1, 4);
agentSeries.Points.AddXY(2, 1, 2);
agentSeries.Points.AddXY(4, 1, 3);
So the most natural solution is to not add Series with missing data.
Not sure if you are happy with this solution or if there is a better way to do it, but the result looks not so bad..
I have done away with adding all those series and instead add all data to one and same Series.
To create the Legends I hide the regular one by setting its color to transparent. (It needs to be there.) Then I add new Legend CustomItems and give them the colors and names as you did.
Here is the code I used, except for the actual data, which I have simulated:
chartSchedule.Series.Clear();
ChartArea CA = chartSchedule.ChartAreas[0];
chartSchedule.Titles.Add("Agent / Task Schedule");
chartSchedule.ChartAreas[0].AxisX.Title = "Agent";
chartSchedule.ChartAreas[0].AxisY.Title = "Time";
// our only Series
Series agentSeries = chartSchedule.Series.Add(" " );
agentSeries.ChartType = SeriesChartType.RangeColumn;
agentSeries.Color = Color.Transparent; // hide the default series entry!
agentSeries["PixelPointWidth"] = "20"; // <- your choice of width!
int index = 0;
foreach (Agent a in _agents)
{
// Alternate colours
Color color = index % 2 == 0 ? Color.DodgerBlue : Color.Blue;
// Display start and end columns of every task
List<DataPoint> timeData = new List<DataPoint>(); ///???
foreach (NodeTask t in a.AssignedTasks)
{
int p = agentSeries.Points.AddXY(index +1, t.StartTime, t.EndTime);
agentSeries.Points[p].Color = color;
}
chartSchedule.Legends[0].CustomItems.Add(color, "Agent " + index);
index++;
}
I use Microsoft.DataVisualization.Charting and want to get the value of the point when i click on it.
My problem: i want exactly that value i clicked, even if its only a value calculated by the Chart and between 2 points.
Example: 3 points: P(0;3), P(1;6), P(3;12)
When i click at x-Value 2 i want to get 9 as result if the line is linear.
Currently i do that:
HitTestResult[] hits = chart.HitTest(e.X, e.Y, false, ChartElementType.PlottingArea);
//DataInformation save the DateTime and Value for later use
DataInformation[] dinfo = new DataInformation[hits.Length];
foreach (ChartArea area in chart.ChartAreas)
{
area.CursorX.LineWidth = 0; //clear old lines
}
for (int i = 0; i < hits.Length; i++) //for all hits
{
if (hits[i].ChartElementType == ChartElementType.PlottingArea)
{
//val saves the x-value clicked in the ChartArea
double val = hits[i].ChartArea.AxisX.PixelPositionToValue(e.X);
DataPoint pt = chart.Series[hits[i].ChartArea.Name].Points.Last(elem => elem.XValue < val);
dinfo[i].caption = hits[i].ChartArea.Name;
dinfo[i].value = pt.YValues[0].ToString();
//hits[i].ChartArea.CursorX.Position = pt.XValue;
}
}
This show the right values for every existing data point but not that clicked point.
How can i get the exact value?
It seems, there is no way to get the exact value. I changed to OxyPlot. OxyPlot can show the data much faster and you can get the exact value for any point.