This is a small utility that takes a formula of f(x) function from a textbox, calculates points in background using VBS, and draws a graph based on this function in a PictureBox control, with gridline and coordinate system. The utility also enables the user to zoom in and out of the graph, to move it around, and also can show specific points of the curve using the mouse.
Introduction
This is a simple 2D graph plotting Windows Forms application written in C#.
It takes as input:
- A formula as a function of variable x - f(x). The formula needs to be compliant to VBscript expression rules, since the points of the function graph are calculated using dynamically created VBscript.
- Minimum (
xmin
), maximum (xmax
) and step (xstep
) values for calculation of points on the graph. Meaning the graph will be calculated only between these points, and the precision of the curve will depend on the density of the points (the lower the step, the higher the density). (Note that higher density means more complex graph bitmap, so it will take up more memory and processor power.)
The initial size of the coordinate system will be determined by the largest point (coordinate) calculated. The application supports zoom in/out functionality by using the mouse wheel, moving functionality by using mouse drag in combination with left click, and curve point tracking functionality by using mouse drag in combination with right mouse click. It also tracks the position of the mouse inside the coordinate system.
All of the functionalities shall be thoroughly explained in further text.
Using the Utility
This is what the application looks like when opened:
After entering the needed parameters, and hitting button Plot, a curve will be drawn in the upper Picturebox, with an appropriate grid and numbering. The following is an example for f(x)=Sin(x):
At the right of the xstep
textbox, you can see a label where the current position of the mouse in the coordinate system is shown.
If you turn the mouse wheel while hovering over the graph, it will zoom in/out. The min zoom factor will be the initial display (shown in picture), and the max zoom factor will be 5x. This can be changed by changing the ZOOM_MAX
constant in “Zoom.cs“.
The graph can be explored (moved around) by moving the mouse while holding the left button.
If you move the mouse while holding down the right mouse button, a red dot with coordinates will show up over a point of the curve that is closest to the mouse position.
At any time, it is possible to change the parameters and the formula, and click the Plot button to redraw the graph.
Using the Code
The code is divided into six .cs files, which are all actually parts of the same class Form1
.
The main file is the "Form1.cs", where the form is instantiated, and where all the form's event handler methods can be found.
Form1.cs
After the form is initialized, there are a few things that need to be set up.
First, I have to explain how the graph part of the form is constructed, in order to support zoom in/out and moving functionalities.
The graph part actually consists of two different PictureBox
controls, that are initially the same size, drawn one over the other. The upper PictureBox (pictureBox_container
) is a container, which always maintains its original size, and the other picturebox (pictureBox_graph
) will contain the graph (as Image
property) and the gridline (as BackgroundImage
property). The pictureBox_graph
is added to pictureBox_container.Controls
array, so that it will always stay inside the boundaries of the pictureBox_container
. That way we can “fake“ zoom in/out functionality by changing the Size
property of the pictureBox_graph
, and also moving functionality by changing the Location
property of the pictureBox_graph
(making sure that the boundaries of the pictureBox_graph
never “get inside“ the pictureBox_container
). By making pictureBox_graph
a part of pictureBox_container
, we are making sure that everything that is outside of the pictureBox_container
is not visible, thus making it like zooming or moving the graph.
You can imagine it like pictureBox_container
being a window through which you are looking at the graph, which is pictureBox_graph
. If the graph gets bigger, you can only see the part that is inside the window, while the rest still exists, but is not visible.
pictureBox_graph
is also anchored to the pictureBox_container
, and the pictureBox_container
to the form, to support the resize functionality.
Two other things are also happening during initialization, and those are:
- Initialization of
bwCoord BackgroundWorker
, which is responsible for showing the current position of the mouse in the coordinate system, and - Control
dot
, which is actually a button that is misused as a red dot to show the closest point of the curve (the right click functionality). dot
is initialized as not visible, and becomes visible when the user holds the right mouse button.
All of the mouse event handlers are written and registered inside of this .cs file:
PictureBox_MouseWheel
– calls the ZoomGraph
method which handles zoom in/out functionality based on the direction of the turning of the mouse wheel.
private void PictureBox_MouseWheel(object sender, MouseEventArgs e)
{
ZoomGraph(e.Delta > 0 ? 1 : -1);
}
PictureBox_MouseUp
– unregisters PictureBox_LeftMouseMove
or PictureBox_RightMouseMove
event handler
(depending on which button is pressed).
private void PictureBox_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
pictureBox_graph.MouseMove -= PictureBox_LeftMouseMove;
Cursor = Cursors.Default;
}
else if (e.Button == MouseButtons.Right)
{
pictureBox_graph.MouseMove -= PictureBox_RightMouseMove;
dot.Visible = false;
}
}
PictureBox_MouseDown
– registers the proper MouseMove
event handler (depending on which button is pressed).
private void PictureBox_MouseDown(object sender, MouseEventArgs e)
{
if (pictureBox_graph.Image == null)
return;
if (e.Button == MouseButtons.Left)
{
pictureBox_graph.MouseMove += PictureBox_LeftMouseMove;
previousLocation = e.Location;
Cursor = Cursors.Hand;
}
else if (e.Button == MouseButtons.Right)
{
pictureBox_graph.MouseMove += PictureBox_RightMouseMove;
}
}
PictureBox_LeftMouseMove
– calls MoveGraph
method that moves the pictureBox_graph
based on the mouse position.
private void PictureBox_LeftMouseMove(object sender, MouseEventArgs e)
{
MoveGraph(e.Location);
}
PictureBox_RightMouseMove
– calls ShowCoordOnGraph
method which will calculate the closest point on the curve to the current mouse position, and highlight it by placing the dot
button on top of it.
private void PictureBox_RightMouseMove(object sender, MouseEventArgs e)
{
if (IsMouseOutside())
return;
ShowCoordOnGraph();
}
The BackgroundWorker
will run in an indefinite loop, where it will continuously report progress, and the ProgressChanged
event handler will call ShowCoord
method, which will calculate and show the current position of the mouse within the coordinate system.
private void BwCoord_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
Thread.Sleep(1);
if (!coord_set) continue;
if (bwCoord.CancellationPending) return;
bwCoord.ReportProgress(0);
}
}
private void BwCoord_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
try
{
if(IsMouseOutside())
return;
ShowCoord();
}
catch { }
}
Calc.cs
The whole process starts with the calculation of the points.
After hitting the Plot button, first the parameters are collected from the textboxes by calling the GetParams
method.
Then, the method CreateOutput
is called, which will create a temporary VBS script; then it will run this script to get the output of the formula in a temp .txt file. Each point in the file is written as two double precision numbers separated by a single space; the points are read from the file and returned as a List of double[2]
arrays.
private List<double[]> CreateOutput()
{
List<double[]> retValue = new List<double[]>();
double[] tmp;
string wscript = Environment.GetEnvironmentVariable("windir") +
"\\SysWOW64\\wscript.exe";
wscript = File.Exists(wscript) ? wscript : "wscript.exe";
CreateVBS();
Process p = new Process();
p.StartInfo.FileName = wscript;
p.StartInfo.Arguments = VBS_file;
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
p.Start();
p.WaitForExit();
if (p.ExitCode > 0)
return null;
FileStream fs = new FileStream(output_file, FileMode.Open);
StreamReader sr = new StreamReader(fs);
string line;
while (!sr.EndOfStream)
{
line = sr.ReadLine();
try
{
tmp = line.Split(' ').Select
(x => double.Parse(x, CultureInfo.CurrentCulture)).ToArray();
}
catch
{
return null;
}
retValue.Add(tmp);
}
sr.Close();
fs.Close();
return retValue;
}
After that, CalcRange
method is called which sets up the variables needed to draw the gridline
, and convert all the points of the curve into pixel representations.
Draw.cs
Next step is the drawing part.
The method DrawGraph
is called, which will create a bitmap
, convert all the points collected from VBS into points on the bitmap
(their pixel representations), also calculate the points as the percentages of the bitmap
's width
/height
(variable values_perc
– which is used in calculation of the closest point of the curve to the mouse position), draw the curve as series of lines between the points of the curve, and set the bitmap
as Image
property of the pictureBox_graph
.
private void DrawGraph()
{
Bitmap bmp = new Bitmap((int)Math.Round(pictureBox_container.Width * ZOOM_MAX),
(int)Math.Round(pictureBox_container.Height * ZOOM_MAX));
List<Point> points = values.Select(t =>
new Point(
(int)Math.Round(bmp.Width *
(t[0] - min) / range),
(int)Math.Round(bmp.Height *
(1 - (t[1] - min) / range))
)
).ToList();
values_perc = points.Select(t => new double[]
{
(double)t.X / bmp.Width,
(double)t.Y / bmp.Height
}
).ToList();
Graphics g = Graphics.FromImage(bmp);
Pen pen = new Pen(Brushes.Black);
g.DrawLines(pen, points.ToArray<Point>());
g.Dispose();
pictureBox_graph.SizeMode = PictureBoxSizeMode.StretchImage;
pictureBox_graph.Image = bmp;
}
}
After the DrawGraph
method, the method DrawGrid
is called, which will calculate the best fitted section size (width
) between the grid lines, draw the grid, and also draw the corresponding numbers on the abscissa and the ordinate.
private void DrawGrid(double corr_factor = 1)
{
int w = (int)Math.Round(pictureBox_container.Width * ZOOM_MAX);
int h = (int)Math.Round(pictureBox_container.Height * ZOOM_MAX);
int _section_w = (int)((Math.Round(range / NUM_SECTION,
GetDecimalPlaces((decimal)xstep)) / range) * w / corr_factor);
int _section_h = (int)((Math.Round(range / NUM_SECTION,
GetDecimalPlaces((decimal)xstep)) / range) * h / corr_factor);
if (_section_w == section_w && _section_h == section_h &&
corr_factor != 1) return;
section_h = _section_h;
section_w = _section_w;
Bitmap bmp = new Bitmap(w, h);
Pen pen = new Pen(Brushes.LightSeaGreen);
Pen pen_axis = new Pen(Brushes.Black);
Graphics g = Graphics.FromImage(bmp);
Font font = new Font("Arial", (float)section_w / 4);
int i;
g.DrawLine(pen_axis, new Point(w / 2 - 1, 0), new Point(w / 2 - 1, h));
g.DrawLine(pen_axis, new Point(w / 2, 0), new Point(w / 2, h));
g.DrawLine(pen_axis, new Point(w / 2 + 1, 0), new Point(w / 2 + 1, h));
g.DrawLine(pen_axis, new Point(0, h / 2 - 1), new Point(w, h / 2 - 1));
g.DrawLine(pen_axis, new Point(0, h / 2), new Point(w, h / 2));
g.DrawLine(pen_axis, new Point(0, h / 2 + 1), new Point(w, h / 2 + 1));
g.DrawString("0", font, Brushes.Black, new Point((int)Math.Round((double)w / 2 -
((double)section_w / 4)), (int)Math.Round((double)h / 2 + (double)section_h / 4)));
string format;
double number;
for (i = w / 2 - section_w; i >= 0; i -= section_w)
{
g.DrawLine(pen, new Point(i, 0), new Point(i, h));
if ((i - w / 2) % (2 * section_w) == 0)
{
number = (min + (double)i / w * range);
format = GetFormat(number);
g.DrawString(number.ToString(format), font, Brushes.Black,
new Point(Math.Max((int)Math.Round((double)i - ((double)section_w / 4)),
0), (int)Math.Round((double)h / 2 + (double)section_h / 4)));
}
}
for (i = h / 2 - section_h; i >= 0; i -= section_h)
{
g.DrawLine(pen, new Point(0, i), new Point(w, i));
if ((i - h / 2) % (2 * section_h) == 0)
{
number = (-min - (double)i / h * range);
format = GetFormat(number);
g.DrawString(number.ToString(format), font, Brushes.Black,
new Point((int)Math.Round((double)w / 2 + (double)section_w / 4),
Math.Max((int)Math.Round(i - ((double)section_h / 4)), 0)));
}
}
for (i = w / 2 + section_w; i <= w; i += section_w)
{
g.DrawLine(pen, new Point(i, 0), new Point(i, h));
if ((i - w / 2) % (2 * section_w) == 0)
{
number = (min + (double)i / w * range);
format = GetFormat(number);
g.DrawString(number.ToString(format), font, Brushes.Black,
new Point((int)Math.Round((double)i - ((double)section_w / 4)),
(int)Math.Round((double)h / 2 + (double)section_h / 4)));
}
}
for (i = h / 2 + section_h; i <= h; i += section_h)
{
g.DrawLine(pen, new Point(0, i), new Point(w, i));
if ((i - h / 2) % (2 * section_h) == 0)
{
number = (-min - (double)i / h * range);
format = GetFormat(number);
g.DrawString(number.ToString(format), font, Brushes.Black,
new Point((int)Math.Round((double)w / 2 + (double)section_w / 4),
(int)Math.Round(i - ((double)section_h / 4))));
}
}
g.Dispose();
if (bmp != null)
{
if(pictureBox_graph.BackgroundImage != null)
pictureBox_graph.BackgroundImage.Dispose();
pictureBox_graph.BackgroundImage = bmp;
pictureBox_graph.BackgroundImageLayout = ImageLayout.Stretch;
}
}
The grid lines for the upper and lower half, and for the left and right half are drawn in separate loops in order to make sure the zero is in the center of the picture (we start drawing from the middle).
The method also takes in an optional parameter corr_factor
, which is used when resizing ("zooming") pictureBox_graph
.
Zoom.cs
This .cs file contains method ZoomGraph
that handles the zoom in/out functionality.
This method:
- calculates the new size of the
pictureBox_graph
based on the ZOOM_STEP
constant – which can be modified to make the zooming slower (lower number), or faster (higher number) - makes sure that the boundaries of the
pictureBox_graph
do not “enter“ the boundaries of the pictureBox_container
- calls
DrawGrid
with corr_factor
each time the size of the pictureBox_graph
passes a zoom factor divisible by 2
.
private void ZoomGraph(int sgn)
{
double width = pictureBox_graph.Width +
sgn * ZOOM_STEP * pictureBox_container.Width;
double height;
if (width > pictureBox_container.Width * ZOOM_MAX)
{
width = pictureBox_container.Width * ZOOM_MAX;
height = pictureBox_container.Height * ZOOM_MAX;
}
else if (width < pictureBox_container.Width)
{
width = pictureBox_container.Width;
height = pictureBox_container.Height;
}
else
{
height = pictureBox_graph.Height +
sgn * ZOOM_STEP * pictureBox_container.Height;
}
if (width == pictureBox_graph.Width) return;
Point location = pictureBox_graph.Location;
location = new Point(
location.X + (int)Math.Round((pictureBox_graph.Width - width) / 2),
location.Y + (int)Math.Round((pictureBox_graph.Height - height) / 2)
);
pictureBox_graph.Width = (int)Math.Round(width);
pictureBox_graph.Height = (int)Math.Round(height);
LocationCorrection(ref location);
pictureBox_graph.Location = location;
double corr_factor = (double)pictureBox_graph.Width /
(double)pictureBox_container.Width;
corr_factor = Math.Floor(corr_factor);
if (corr_factor % 2 == 0 && sgn > 0 || corr_factor % 2 == 1 && sgn < 0)
{
DrawGrid(corr_factor);
}
}
Move.cs
This file contains three methods:
Coord.cs
This file contains several methods that are responsible for showing the coordinates (either general coordinates of the mouse while hovering over the picturebox(es), or the coordinates of the closest point of the curve).
ShowCoord
– calculates and shows general coordinates of the current mouse position:
private void ShowCoord()
{
Point mousePos_graph = pictureBox_graph.PointToClient(Control.MousePosition);
labelPos.Text = Coord2String(GetCoordinates(mousePos_graph));
}
ShowCoordOnGraph
– calculates the point on the graph that is closest to the mouse position, positions and shows the dot object over this point:
private void ShowCoordOnGraph()
{
Point mousePos_graph = pictureBox_graph.PointToClient(Control.MousePosition);
Point closest_point = mousePos_graph;
dot.Text = Coord2String(GetClosestPoint(ref closest_point));
closest_point.Offset(-(int)Math.Round((double)dot.Size.Height / 2),
-(int)Math.Round((double)dot.Size.Height / 2));
dot.Location = closest_point;
if (!dot.Visible)
dot.Visible = true;
}
GetFormat
– auxiliary method that gets the format for the method ToString(format)
for conversion of a double precision number to a string
, based on the number of decimal places of the number itself and the xstep
parameter GetDecimalPlaces
– auxiliary method used by the GetFormat
method, to get the number of decimal places in a double precision number Coord2String
– method to convert double[2]
array of point coordinates into a string
representation GetClosestPoint
– method that calculates the distance between the current mouse position and all of the points of the curve, and chooses the closest one:
private double[] GetClosestPoint(ref Point position)
{
Point tmp = position;
position = (Point)values_perc.Select(t => new Point((int)Math.Round(t[0] *
pictureBox_graph.Width), (int)Math.Round(t[1] * pictureBox_graph.Height)))
.Select(t => new object[]
{
t,
Math.Sqrt(Math.Pow(tmp.X - t.X, 2) +
Math.Pow(tmp.Y - t.Y, 2))
})
.OrderBy(x => (double)x[1])
.ToList()[0][0];
return GetCoordinates(position);
}
History
- 11th May, 2020: Initial version