mirror of
https://github.com/Leonmmcoset/CMLeonOS.git
synced 2026-04-21 19:24:00 +00:00
函数方程式画图软件
This commit is contained in:
@@ -1 +1 @@
|
||||
2026-03-29 19:08:01
|
||||
2026-03-29 19:34:16
|
||||
@@ -1 +1 @@
|
||||
78bda7c
|
||||
3a9ff9c
|
||||
@@ -142,6 +142,7 @@ namespace CMLeonOS.Gui
|
||||
RegisterApp(new AppMetadata("Environment Variables", () => { return new EnvironmentVariables(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25)));
|
||||
RegisterApp(new AppMetadata("UILib Gallery", () => { return new UILibGallery(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25)));
|
||||
RegisterApp(new AppMetadata("Music Editor", () => { return new MusicEditor(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25)));
|
||||
RegisterApp(new AppMetadata("Function Grapher", () => { return new FunctionGrapher(); }, Icons.Icon_Default, Color.FromArgb(25, 25, 25)));
|
||||
|
||||
Logger.Logger.Instance.Info("AppManager", $"{AppMetadatas.Count} apps were registered.");
|
||||
|
||||
|
||||
533
Gui/Apps/FunctionGrapher.cs
Normal file
533
Gui/Apps/FunctionGrapher.cs
Normal file
@@ -0,0 +1,533 @@
|
||||
using CMLeonOS;
|
||||
using CMLeonOS.Gui.UILib;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace CMLeonOS.Gui.Apps
|
||||
{
|
||||
internal class FunctionGrapher : Process
|
||||
{
|
||||
internal FunctionGrapher() : base("Function Grapher", ProcessType.Application) { }
|
||||
|
||||
private AppWindow window;
|
||||
private readonly WindowManager wm = ProcessManager.GetProcess<WindowManager>();
|
||||
|
||||
private TextBox expressionBox;
|
||||
private NumericUpDown scaleInput;
|
||||
private TextBox statusBox;
|
||||
private Window canvas;
|
||||
|
||||
private string currentExpression = "sin(x)";
|
||||
private readonly Color canvasBg = Color.FromArgb(250, 252, 255);
|
||||
private readonly Color axisColor = Color.FromArgb(156, 170, 188);
|
||||
private readonly Color gridColor = Color.FromArgb(226, 233, 244);
|
||||
private readonly Color curveColor = Color.FromArgb(44, 102, 217);
|
||||
|
||||
private void Layout()
|
||||
{
|
||||
int top = 34;
|
||||
int side = 8;
|
||||
int inputH = 24;
|
||||
int statusH = 24;
|
||||
|
||||
expressionBox.MoveAndResize(side, top, window.Width - 200, inputH);
|
||||
scaleInput.MoveAndResize(window.Width - 184, top, 86, inputH);
|
||||
|
||||
int buttonY = top;
|
||||
int buttonW = 40;
|
||||
int buttonX = window.Width - 92;
|
||||
|
||||
int canvasY = top + inputH + 8;
|
||||
int canvasH = window.Height - canvasY - statusH - 10;
|
||||
canvas.MoveAndResize(side, canvasY, window.Width - (side * 2), canvasH);
|
||||
statusBox.MoveAndResize(side, window.Height - statusH - 6, window.Width - (side * 2), statusH);
|
||||
}
|
||||
|
||||
private void DrawGridAndAxes(int centerX, int centerY, int pixelPerUnit)
|
||||
{
|
||||
if (pixelPerUnit < 4)
|
||||
{
|
||||
pixelPerUnit = 4;
|
||||
}
|
||||
|
||||
for (int x = centerX; x < canvas.Width; x += pixelPerUnit)
|
||||
{
|
||||
canvas.DrawLine(x, 0, x, canvas.Height - 1, gridColor);
|
||||
}
|
||||
for (int x = centerX; x >= 0; x -= pixelPerUnit)
|
||||
{
|
||||
canvas.DrawLine(x, 0, x, canvas.Height - 1, gridColor);
|
||||
}
|
||||
for (int y = centerY; y < canvas.Height; y += pixelPerUnit)
|
||||
{
|
||||
canvas.DrawLine(0, y, canvas.Width - 1, y, gridColor);
|
||||
}
|
||||
for (int y = centerY; y >= 0; y -= pixelPerUnit)
|
||||
{
|
||||
canvas.DrawLine(0, y, canvas.Width - 1, y, gridColor);
|
||||
}
|
||||
|
||||
canvas.DrawLine(0, centerY, canvas.Width - 1, centerY, axisColor);
|
||||
canvas.DrawLine(centerX, 0, centerX, canvas.Height - 1, axisColor);
|
||||
}
|
||||
|
||||
private void RenderGraph()
|
||||
{
|
||||
if (canvas.Width < 8 || canvas.Height < 8)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.Clear(canvasBg);
|
||||
int centerX = canvas.Width / 2;
|
||||
int centerY = canvas.Height / 2;
|
||||
int pixelPerUnit = Math.Max(8, scaleInput.Value);
|
||||
|
||||
DrawGridAndAxes(centerX, centerY, pixelPerUnit);
|
||||
|
||||
ExpressionParser parser = new ExpressionParser(currentExpression);
|
||||
if (!parser.Parse())
|
||||
{
|
||||
statusBox.Text = "Parse error: " + parser.ErrorMessage;
|
||||
statusBox.Render();
|
||||
wm.Update(statusBox);
|
||||
wm.Update(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasLast = false;
|
||||
int lastX = 0;
|
||||
int lastY = 0;
|
||||
|
||||
for (int sx = 0; sx < canvas.Width; sx++)
|
||||
{
|
||||
double x = (sx - centerX) / (double)pixelPerUnit;
|
||||
double y;
|
||||
if (!parser.TryEvaluate(x, out y))
|
||||
{
|
||||
hasLast = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (double.IsNaN(y) || double.IsInfinity(y))
|
||||
{
|
||||
hasLast = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
int sy = centerY - (int)(y * pixelPerUnit);
|
||||
if (sy < -10000 || sy > 10000)
|
||||
{
|
||||
hasLast = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasLast)
|
||||
{
|
||||
if (Math.Abs(sy - lastY) < canvas.Height)
|
||||
{
|
||||
canvas.DrawLine(lastX, lastY, sx, sy, curveColor);
|
||||
}
|
||||
}
|
||||
hasLast = true;
|
||||
lastX = sx;
|
||||
lastY = sy;
|
||||
}
|
||||
|
||||
statusBox.Text = "OK: y = " + currentExpression;
|
||||
statusBox.Render();
|
||||
wm.Update(statusBox);
|
||||
wm.Update(canvas);
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
base.Start();
|
||||
|
||||
window = new AppWindow(this, 140, 80, 900, 560);
|
||||
window.Title = "Function Grapher";
|
||||
window.Icon = AppManager.DefaultAppIcon;
|
||||
window.CanResize = true;
|
||||
window.UserResized = () =>
|
||||
{
|
||||
Layout();
|
||||
RenderGraph();
|
||||
};
|
||||
window.Closing = TryStop;
|
||||
wm.AddWindow(window);
|
||||
|
||||
expressionBox = new TextBox(window, 8, 34, 680, 24);
|
||||
expressionBox.Text = currentExpression;
|
||||
expressionBox.PlaceholderText = "Enter expression, e.g. sin(x), x^2+2*x+1";
|
||||
expressionBox.Submitted = () =>
|
||||
{
|
||||
currentExpression = expressionBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(currentExpression))
|
||||
{
|
||||
currentExpression = "x";
|
||||
expressionBox.Text = currentExpression;
|
||||
}
|
||||
RenderGraph();
|
||||
};
|
||||
wm.AddWindow(expressionBox);
|
||||
|
||||
scaleInput = new NumericUpDown(window, 696, 34, 86, 24);
|
||||
scaleInput.Minimum = 8;
|
||||
scaleInput.Maximum = 80;
|
||||
scaleInput.Value = 24;
|
||||
scaleInput.Step = 2;
|
||||
scaleInput.Changed = (_) => RenderGraph();
|
||||
wm.AddWindow(scaleInput);
|
||||
|
||||
Button plotButton = new Button(window, 790, 34, 46, 24);
|
||||
plotButton.Text = "Plot";
|
||||
plotButton.OnClick = (_, _) =>
|
||||
{
|
||||
currentExpression = expressionBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(currentExpression))
|
||||
{
|
||||
currentExpression = "x";
|
||||
expressionBox.Text = currentExpression;
|
||||
}
|
||||
RenderGraph();
|
||||
};
|
||||
wm.AddWindow(plotButton);
|
||||
|
||||
Button clearButton = new Button(window, 842, 34, 50, 24);
|
||||
clearButton.Text = "Clear";
|
||||
clearButton.OnClick = (_, _) =>
|
||||
{
|
||||
expressionBox.Text = "x";
|
||||
currentExpression = "x";
|
||||
RenderGraph();
|
||||
};
|
||||
wm.AddWindow(clearButton);
|
||||
|
||||
canvas = new Window(this, window, 8, 66, window.Width - 16, window.Height - 98);
|
||||
wm.AddWindow(canvas);
|
||||
|
||||
statusBox = new TextBox(window, 8, window.Height - 30, window.Width - 16, 24);
|
||||
statusBox.ReadOnly = true;
|
||||
statusBox.Text = "Ready";
|
||||
wm.AddWindow(statusBox);
|
||||
|
||||
Layout();
|
||||
RenderGraph();
|
||||
wm.Update(window);
|
||||
}
|
||||
|
||||
public override void Run()
|
||||
{
|
||||
}
|
||||
|
||||
private class ExpressionParser
|
||||
{
|
||||
private readonly string text;
|
||||
private int pos;
|
||||
private Node root;
|
||||
|
||||
internal string ErrorMessage { get; private set; } = "Unknown";
|
||||
|
||||
internal ExpressionParser(string expr)
|
||||
{
|
||||
text = expr ?? string.Empty;
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
internal bool Parse()
|
||||
{
|
||||
try
|
||||
{
|
||||
pos = 0;
|
||||
root = ParseExpression();
|
||||
SkipSpaces();
|
||||
if (pos != text.Length)
|
||||
{
|
||||
ErrorMessage = "Unexpected token near position " + pos;
|
||||
return false;
|
||||
}
|
||||
return root != null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryEvaluate(double x, out double y)
|
||||
{
|
||||
y = 0;
|
||||
if (root == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
y = root.Eval(x);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SkipSpaces()
|
||||
{
|
||||
while (pos < text.Length && char.IsWhiteSpace(text[pos]))
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Match(char c)
|
||||
{
|
||||
SkipSpaces();
|
||||
if (pos < text.Length && text[pos] == c)
|
||||
{
|
||||
pos++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private string ParseIdentifier()
|
||||
{
|
||||
SkipSpaces();
|
||||
int start = pos;
|
||||
while (pos < text.Length && (char.IsLetter(text[pos]) || char.IsDigit(text[pos]) || text[pos] == '_'))
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
if (start == pos)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
return text.Substring(start, pos - start);
|
||||
}
|
||||
|
||||
private Node ParseNumber()
|
||||
{
|
||||
SkipSpaces();
|
||||
int start = pos;
|
||||
bool dot = false;
|
||||
while (pos < text.Length)
|
||||
{
|
||||
char c = text[pos];
|
||||
if (char.IsDigit(c))
|
||||
{
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
if (c == '.' && !dot)
|
||||
{
|
||||
dot = true;
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (start == pos)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
double value = double.Parse(text.Substring(start, pos - start));
|
||||
return new NumberNode(value);
|
||||
}
|
||||
|
||||
private Node ParsePrimary()
|
||||
{
|
||||
SkipSpaces();
|
||||
|
||||
if (Match('('))
|
||||
{
|
||||
Node inner = ParseExpression();
|
||||
if (!Match(')'))
|
||||
{
|
||||
throw new Exception("Missing ')'");
|
||||
}
|
||||
return inner;
|
||||
}
|
||||
|
||||
if (Match('+'))
|
||||
{
|
||||
return ParsePrimary();
|
||||
}
|
||||
if (Match('-'))
|
||||
{
|
||||
return new UnaryNode('-', ParsePrimary());
|
||||
}
|
||||
|
||||
Node number = ParseNumber();
|
||||
if (number != null)
|
||||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
string ident = ParseIdentifier();
|
||||
if (ident.Length > 0)
|
||||
{
|
||||
string id = ident.ToLower();
|
||||
if (id == "x")
|
||||
{
|
||||
return new VarNode();
|
||||
}
|
||||
if (id == "pi")
|
||||
{
|
||||
return new NumberNode(Math.PI);
|
||||
}
|
||||
if (id == "e")
|
||||
{
|
||||
return new NumberNode(Math.E);
|
||||
}
|
||||
|
||||
if (!Match('('))
|
||||
{
|
||||
throw new Exception("Function call expected for '" + ident + "'");
|
||||
}
|
||||
Node arg = ParseExpression();
|
||||
if (!Match(')'))
|
||||
{
|
||||
throw new Exception("Missing ')' after function " + ident);
|
||||
}
|
||||
return new FuncNode(id, arg);
|
||||
}
|
||||
|
||||
throw new Exception("Unexpected token at " + pos);
|
||||
}
|
||||
|
||||
private Node ParsePow()
|
||||
{
|
||||
Node left = ParsePrimary();
|
||||
SkipSpaces();
|
||||
if (Match('^'))
|
||||
{
|
||||
Node right = ParsePow();
|
||||
return new BinNode('^', left, right);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private Node ParseMulDiv()
|
||||
{
|
||||
Node left = ParsePow();
|
||||
while (true)
|
||||
{
|
||||
SkipSpaces();
|
||||
if (Match('*'))
|
||||
{
|
||||
left = new BinNode('*', left, ParsePow());
|
||||
}
|
||||
else if (Match('/'))
|
||||
{
|
||||
left = new BinNode('/', left, ParsePow());
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private Node ParseExpression()
|
||||
{
|
||||
Node left = ParseMulDiv();
|
||||
while (true)
|
||||
{
|
||||
SkipSpaces();
|
||||
if (Match('+'))
|
||||
{
|
||||
left = new BinNode('+', left, ParseMulDiv());
|
||||
}
|
||||
else if (Match('-'))
|
||||
{
|
||||
left = new BinNode('-', left, ParseMulDiv());
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
private abstract class Node
|
||||
{
|
||||
internal abstract double Eval(double x);
|
||||
}
|
||||
|
||||
private class NumberNode : Node
|
||||
{
|
||||
private readonly double v;
|
||||
internal NumberNode(double value) { v = value; }
|
||||
internal override double Eval(double x) { return v; }
|
||||
}
|
||||
|
||||
private class VarNode : Node
|
||||
{
|
||||
internal override double Eval(double x) { return x; }
|
||||
}
|
||||
|
||||
private class UnaryNode : Node
|
||||
{
|
||||
private readonly char op;
|
||||
private readonly Node arg;
|
||||
internal UnaryNode(char oper, Node node) { op = oper; arg = node; }
|
||||
internal override double Eval(double x)
|
||||
{
|
||||
double v = arg.Eval(x);
|
||||
return op == '-' ? -v : v;
|
||||
}
|
||||
}
|
||||
|
||||
private class BinNode : Node
|
||||
{
|
||||
private readonly char op;
|
||||
private readonly Node a;
|
||||
private readonly Node b;
|
||||
internal BinNode(char oper, Node left, Node right) { op = oper; a = left; b = right; }
|
||||
internal override double Eval(double x)
|
||||
{
|
||||
double lv = a.Eval(x);
|
||||
double rv = b.Eval(x);
|
||||
switch (op)
|
||||
{
|
||||
case '+': return lv + rv;
|
||||
case '-': return lv - rv;
|
||||
case '*': return lv * rv;
|
||||
case '/': return rv == 0 ? double.NaN : lv / rv;
|
||||
case '^': return Math.Pow(lv, rv);
|
||||
default: return double.NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FuncNode : Node
|
||||
{
|
||||
private readonly string fn;
|
||||
private readonly Node arg;
|
||||
internal FuncNode(string name, Node node) { fn = name; arg = node; }
|
||||
internal override double Eval(double x)
|
||||
{
|
||||
double v = arg.Eval(x);
|
||||
switch (fn)
|
||||
{
|
||||
case "sin": return Math.Sin(v);
|
||||
case "cos": return Math.Cos(v);
|
||||
case "tan": return Math.Tan(v);
|
||||
case "abs": return Math.Abs(v);
|
||||
case "sqrt": return v < 0 ? double.NaN : Math.Sqrt(v);
|
||||
case "log": return v <= 0 ? double.NaN : Math.Log(v);
|
||||
case "exp": return Math.Exp(v);
|
||||
default: return double.NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user