Click here to Skip to main content
15,867,756 members
Articles / Programming Languages / VBScript

A Simple Utility for Drawing Function Curves

Rate me:
Please Sign up or sign in to vote.
4.69/5 (11 votes)
11 May 2020CPOL8 min read 13.6K   630   22   3
Small utility that takes in a formula as function f(x) and draws a graph based on it
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:

Image 1

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):

Image 2

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.

C#
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).

C#
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).

C#
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.

C#
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.

C#
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.

C#
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.

C#
private List<double[]> CreateOutput()
{
    // runs generated VBS code, and reads the output (points of graph)

    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(); // run VBS
    p.WaitForExit();
    if (p.ExitCode > 0) // error
        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.

C#
    private void DrawGraph()
    {
        // draws 2d graph from a list of points

        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.

C#
private void DrawGrid(double corr_factor = 1)
 {
     // calculates section sizes and draws a grid for picturebox_graph background

     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;
     // axis
     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));
     // draw zero
     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;
     // left half
     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)));
         }
     }
     // upper half
     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)));
         }
     }
     // right half
     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)));
         }
     }
     // lower half
     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.
C#
private void ZoomGraph(int sgn)
 {
     // calculates new width, height, and location of picturebox_graph (for zooming)
     // sgn = 1 is zoom in
     // sgn = -1 is zoom out

     double width = pictureBox_graph.Width +
                    sgn * ZOOM_STEP * pictureBox_container.Width;
     double height;
     if (width > pictureBox_container.Width * ZOOM_MAX) // if out of upper bounds,
                                                        // set to max
     {
         width = pictureBox_container.Width * ZOOM_MAX;
         height = pictureBox_container.Height * ZOOM_MAX;
     }
     else if (width < pictureBox_container.Width)       // if less then lower bounds,
                                                        // set to min
     {
         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);
     //stop crossing container border inward
     LocationCorrection(ref location);
     pictureBox_graph.Location = location;
     // Redraw grid on every round zoom factor
     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:

  • MoveGraph(Point loc) – moves the pictureBox_graph to a new location loc:
    C#
    private void MoveGraph(Point loc)
    {
        // changes location of picturebox_graph based on (mouse) location
    
        Point location = pictureBox_graph.Location;
        location.Offset(loc.X - previousLocation.X, loc.Y - previousLocation.Y);
        LocationCorrection(ref location);
        pictureBox_graph.Location = location;
    }
    
  • LocationCorrection(ref Point location) – auxiliary function that makes sure that pictureBox_graph's boundaries do not “enter“ pictureBox_container.
  • IsMouseOutside – used in event handlers for showing coordinates – checks whether the mouse is inside the pictureBox_graph boundaries.

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:
    C#
    private void ShowCoord()
    {
        // calculates and shows coordinates based on mouse position
    
        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:
    C#
    private void ShowCoordOnGraph()
    {
        // calculates and shows the point on graph closest to the mouse position
    
        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:
    C#
    private double[] GetClosestPoint(ref Point position)
    {
        // returns both coordinates on graph and Point on picturebox_graph
        // of closest graph point to 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
User Interface Analyst Raiffeisenbank Austria
Croatia Croatia
I acquired Masters degree in computing science at the Faculty of Electrical Engineering and Computing in Zagreb, Croatia in 2009. Following my studies, I got a job in a Croatian branch of Austrian-based CEE Raiffeisen Bank as an MIS (Management information system) analyst.
I have been working there since 2010, as an IT expert within the Controlling department, maintaining the Oracle's OFSA system, underlying interfaces and databases.
Throughout that time, I have worked with several different technologies, which include SQL & PL/SQL (mostly), postgres, Cognos BI, Apparo, Datastage, ODI, Jenkins, Qlik, ...
I am doing a lot of automation with scripting in batch / shell and VBscript (mostly) - data analysis and processing, automated DB imports and exports, Jenkins automation etc.
Privately, I was mostly doing Windows Forms and Console app tools in Visual Studio, C#.

Comments and Discussions

 
PraiseNice application Pin
CometFather12-May-20 6:57
CometFather12-May-20 6:57 
SuggestionScripting Pin
BManfred12-May-20 2:56
BManfred12-May-20 2:56 
QuestionWhere do I find VBScript syntax? Pin
Patrick Skelton12-May-20 1:22
Patrick Skelton12-May-20 1:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.