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






4.69/5 (11投票s)
一个小实用程序,
引言
这是一个用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_graph
的Size
属性来“模拟”放大/缩小功能,并通过更改pictureBox_graph
的Location
属性来移动功能(确保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_LeftMouseMove
或PictureBox_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
上的点(它们的像素表示),还将点计算为bitmap
的width
/height
的百分比(变量values_perc
– 用于计算最接近鼠标位置的曲线点),将曲线绘制为点之间的线系列,并将bitmap
设置为pictureBox_graph
的Image
属性。
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_factor
的DrawGrid
。
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日:初始版本