65.9K
CodeProject 正在变化。 阅读更多。
Home

绘制函数曲线的简单实用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (11投票s)

2020年5月11日

CPOL

8分钟阅读

viewsIcon

15758

downloadIcon

711

一个小实用程序,接受一个公式作为函数 f(x),并基于此绘制图形

引言

这是一个用C#编写的简单的2D图形绘制Windows Forms应用程序。

它接受以下输入:

  • 一个作为变量x函数(f(x))的公式。该公式需要符合VBscript表达式规则,因为函数图形的点是使用动态创建的VBscript计算的。
  • 用于计算图形上点的最小(xmin)、最大(xmax)和步长(xstep)值。这意味着图形仅在这些点之间计算,曲线的精度将取决于点的密度(步长越小,密度越高)。(请注意,密度越高意味着图形位图越复杂,会占用更多内存和处理器资源。)

坐标系统的初始大小将由计算出的最大点(坐标)确定。该应用程序支持使用鼠标滚轮进行缩放(放大/缩小),通过鼠标拖动结合左键单击进行移动,以及通过鼠标拖动结合右键单击进行曲线点跟踪。它还跟踪鼠标在坐标系统内的位置。

所有功能将在后续文本中得到详细解释。

使用该实用工具

应用程序打开时的样子

输入所需参数并点击“绘制”按钮后,将在顶部的Picturebox中绘制一条曲线,并附有适当的网格和数字。以下是f(x)=Sin(x)的示例:

xstep文本框的右侧,您可以看到一个标签,显示鼠标在坐标系统中的当前位置。

当鼠标悬停在图形上时,转动鼠标滚轮即可放大/缩小。最小缩放因子为初始显示(如图所示),最大缩放因子为5倍。这可以通过更改“Zoom.cs”中的ZOOM_MAX常量来修改。

可以通过按住鼠标左键并移动鼠标来探索(移动)图形。

按住鼠标右键并移动鼠标,会在最接近鼠标位置的曲线上显示一个带坐标的红点。

随时可以更改参数和公式,然后点击绘制按钮重新绘制图形。

Using the Code

代码被分成六个*.cs文件,它们实际上都是同一个类Form1的组成部分。

主文件是“Form1.cs”,其中实例化了窗体,并且可以找到窗体所有事件处理程序方法。

Form1.cs

窗体初始化后,需要进行一些设置。

首先,我需要解释窗体的图形部分是如何构建的,以便支持放大/缩小和移动功能。

图形部分实际上由两个大小初始相同的PictureBox控件组成,它们叠加在一起。上面的PictureBox(pictureBox_container)是一个容器,始终保持其原始大小,而另一个PictureBox(pictureBox_graph)将包含图形(作为Image属性)和网格线(作为BackgroundImage属性)。pictureBox_graph被添加到pictureBox_container.Controls数组中,因此它将始终保留在pictureBox_container的边界内。这样,我们就可以通过更改pictureBox_graphSize属性来“模拟”放大/缩小功能,并通过更改pictureBox_graphLocation属性来移动功能(确保pictureBox_graph的边界永远不会“进入”pictureBox_container)。通过将pictureBox_graph设为pictureBox_container的一部分,我们确保了pictureBox_container外部的所有内容都不可见,从而使其看起来像是对图形进行缩放或移动。

您可以将其想象成pictureBox_container是一个窗口,您通过它观察图形,而图形就是pictureBox_graph。如果图形变大,您只能看到窗口内的部分,其余部分仍然存在,但不可见。

pictureBox_graph也锚定到pictureBox_container,而pictureBox_container锚定到窗体,以支持调整大小功能。

初始化过程中还会发生另外两件事:

  • 初始化bwCoord BackgroundWorker,它负责显示鼠标在坐标系统中的当前位置;
  • 控件dot,它实际上是一个按钮,被滥用为红点,用于显示曲线上最接近的点(右键单击功能)。dot初始化时不可见,当用户按住鼠标右键时才会显示。

所有鼠标事件处理程序都写在这个*.cs文件并注册在其中。

PictureBox_MouseWheel – 调用ZoomGraph方法,该方法根据鼠标滚轮的转动方向处理放大/缩小功能。

private void PictureBox_MouseWheel(object sender, MouseEventArgs e)
{
    ZoomGraph(e.Delta > 0 ? 1 : -1);
}

PictureBox_MouseUp – 注销PictureBox_LeftMouseMovePictureBox_RightMouseMove事件处理程序(取决于按下的按钮)。

        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 – 注册适当的MouseMove事件处理程序(取决于按下的按钮)。

        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 – 调用MoveGraph方法,该方法根据鼠标位置移动pictureBox_graph

        private void PictureBox_LeftMouseMove(object sender, MouseEventArgs e)
        {
            MoveGraph(e.Location);
        }

PictureBox_RightMouseMove – 调用ShowCoordOnGraph方法,该方法将计算曲线上最接近当前鼠标位置的点,并通过将dot按钮放置在其上方来高亮显示。

        private void PictureBox_RightMouseMove(object sender, MouseEventArgs e)
        {
            if (IsMouseOutside())
                return;
            ShowCoordOnGraph();
        }

BackgroundWorker将在一个无限循环中运行,它将持续报告进度,而ProgressChanged事件处理程序将调用ShowCoord方法,该方法将计算并显示鼠标在坐标系统中的当前位置。

        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

整个过程从计算点开始。

点击绘制按钮后,首先通过调用GetParams方法从文本框收集参数。

然后,调用CreateOutput方法,该方法将创建一个临时的VBScript;然后运行该脚本以在临时*.txt文件中获取公式的输出。文件中的每个点都写为两个双精度数字,中间用单个空格分隔;这些点从文件中读取并作为double[2]数组的列表返回。

        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;
        }

之后,调用CalcRange方法,该方法设置绘制gridline所需的变量,并将曲线的所有点转换为像素表示。

Draw.cs

下一步是绘制部分。

调用DrawGraph方法,该方法将创建一个bitmap,将从VBS收集的所有点转换为bitmap上的点(它们的像素表示),还将点计算为bitmapwidth/height的百分比(变量values_perc – 用于计算最接近鼠标位置的曲线点),将曲线绘制为点之间的线系列,并将bitmap设置为pictureBox_graphImage属性。

        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;
        }
    }

DrawGraph方法之后,调用DrawGrid方法,该方法将计算网格线之间的最佳拟合段大小(width),绘制网格,并在横轴和纵轴上绘制相应的数字。

       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;
            }
        }

上半部分和下半部分的网格线,以及左半部分和右半部分的网格线是分开绘制的,以确保零点位于图像的中心(我们从中点开始绘制)。

该方法还接受一个可选参数corr_factor,该参数在调整pictureBox_graph大小时(“缩放”)使用。

Zoom.cs

此*.cs文件包含ZoomGraph方法,该方法处理放大/缩小功能。

此方法:

  • 根据ZOOM_STEP常量计算pictureBox_graph的新大小 – 该常量可以修改以使缩放变慢(数字越小)或变快(数字越大)。
  • 确保pictureBox_graph的边界不会“进入”pictureBox_container的边界。
  • 每次pictureBox_graph的大小经过能被2整除的缩放因子时,都调用带corr_factorDrawGrid
       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

此文件包含三个方法:

  • MoveGraph(Point loc) – 将pictureBox_graph移动到新位置loc
            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) – 辅助函数,确保pictureBox_graph的边界不会“进入”pictureBox_container
  • IsMouseOutside – 用于显示坐标的事件处理程序中 – 检查鼠标是否在pictureBox_graph边界内。

Coord.cs

此文件包含几个方法,负责显示坐标(无论是鼠标悬停在picturebox(es)上的通用坐标,还是曲线上最接近点的坐标)。

  • ShowCoord – 计算并显示当前鼠标位置的通用坐标。
            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 – 计算曲线上最接近鼠标位置的点,并定位dot对象在其上方并显示。
            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 – 辅助方法,根据数字本身的小数位数和xstep参数,获取用于ToString(format)方法将双精度数转换为string的格式。
  • GetDecimalPlaces – 辅助方法,由GetFormat方法使用,用于获取双精度数的小数位数。
  • Coord2String – 将点的坐标的double[2]数组转换为string表示形式的方法。
  • GetClosestPoint – 计算当前鼠标位置与曲线上所有点之间距离并选择最近一个的方法。
            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);
            }

历史

  • 2020年5月11日:初始版本
© . All rights reserved.