// tools/function_plotter.dart import 'package:flutter/material.dart'; import 'package:math_expressions/math_expressions.dart'; import 'package:fl_chart/fl_chart.dart'; class FunctionPlotterPage extends StatefulWidget { @override _FunctionPlotterPageState createState() => _FunctionPlotterPageState(); } class _FunctionPlotterPageState extends State { final List _functionControllers = [ TextEditingController(text: 'x^2'), ]; final TextEditingController _xMinController = TextEditingController(text: '-5'); final TextEditingController _xMaxController = TextEditingController(text: '5'); final TextEditingController _yMinController = TextEditingController(text: '-10'); final TextEditingController _yMaxController = TextEditingController(text: '10'); List _functionColors = [ Colors.blue, Colors.red, Colors.green, Colors.orange, Colors.purple, ]; List _functionErrors = []; @override void dispose() { for (var controller in _functionControllers) { controller.dispose(); } _xMinController.dispose(); _xMaxController.dispose(); _yMinController.dispose(); _yMaxController.dispose(); super.dispose(); } void _addFunction() { if (_functionControllers.length < 5) { setState(() { _functionControllers.add(TextEditingController()); }); } } void _removeFunction(int index) { if (_functionControllers.length > 1) { setState(() { _functionControllers.removeAt(index); if (_functionErrors.length > index) { _functionErrors.removeAt(index); } }); } } List _evaluateFunction(String function, double xMin, double xMax, int functionIndex) { List points = []; try { Parser p = Parser(); Expression exp = p.parse(function); ContextModel cm = ContextModel(); int numberOfPoints = 200; double step = (xMax - xMin) / numberOfPoints; for (int i = 0; i <= numberOfPoints; i++) { double xValue = xMin + i * step; try { cm.bindVariable(Variable('x'), Number(xValue)); double yValue = exp.evaluate(EvaluationType.REAL, cm); if (yValue.isFinite && yValue.abs() < 1e10) { points.add(FlSpot(xValue, yValue)); } else { points.add(FlSpot.nullSpot); } } catch (e) { points.add(FlSpot.nullSpot); } } } catch (e) { if (_functionErrors.length <= functionIndex) { _functionErrors.addAll(List.filled(functionIndex - _functionErrors.length + 1, '')); } _functionErrors[functionIndex] = '函数解析错误: ${e.toString()}'; } return points; } void _plotFunctions() { setState(() { _functionErrors.clear(); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('函数图像绘制', style: TextStyle(color: Colors.black)), backgroundColor: Colors.white, elevation: 1, actions: [ IconButton( icon: Icon(Icons.refresh), onPressed: _plotFunctions, tooltip: '重新绘制', ), ], ), body: SingleChildScrollView( // 添加外层滚动 child: Padding( padding: EdgeInsets.all(16), child: Column( children: [ // 函数输入区域 - 移除固定高度约束 _buildFunctionInputs(), SizedBox(height: 16), // 添加间距 // 绘图区域 - 设置固定高度 Container( height: MediaQuery.of(context).size.height * 0.5, // 使用屏幕高度的一半 child: _buildPlotArea(), ), ], ), ), ), ); } Widget _buildFunctionInputs() { return Card( elevation: 4, child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '函数表达式 (使用 x 作为变量)', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), SizedBox(height: 10), Text( '支持: +, -, *, /, ^, sin, cos, tan, ln, sqrt, e', style: TextStyle(fontSize: 12, color: Colors.grey), ), SizedBox(height: 10), // 坐标范围设置 - 在小屏幕上改为垂直布局 LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < 600) { // 小屏幕垂直布局 return Column( children: [ _buildRangeTextField(_xMinController, 'x最小值'), SizedBox(height: 10), _buildRangeTextField(_xMaxController, 'x最大值'), SizedBox(height: 10), _buildRangeTextField(_yMinController, 'y最小值'), SizedBox(height: 10), _buildRangeTextField(_yMaxController, 'y最大值'), ], ); } else { // 大屏幕水平布局 return Row( children: [ Expanded(child: _buildRangeTextField(_xMinController, 'x最小值')), SizedBox(width: 10), Expanded(child: _buildRangeTextField(_xMaxController, 'x最大值')), SizedBox(width: 10), Expanded(child: _buildRangeTextField(_yMinController, 'y最小值')), SizedBox(width: 10), Expanded(child: _buildRangeTextField(_yMaxController, 'y最大值')), ], ); } }, ), SizedBox(height: 10), // 函数输入列表 - 设置最大高度并允许滚动 Container( constraints: BoxConstraints( maxHeight: 200, // 限制最大高度 ), child: ListView.builder( shrinkWrap: true, itemCount: _functionControllers.length, itemBuilder: (context, index) { return Padding( padding: EdgeInsets.symmetric(vertical: 5), child: Row( children: [ Container( width: 20, height: 20, decoration: BoxDecoration( color: _functionColors[index], shape: BoxShape.circle, ), ), SizedBox(width: 10), Expanded( child: TextField( controller: _functionControllers[index], decoration: InputDecoration( hintText: '例如: x^2, sin(x), ln(x)', border: OutlineInputBorder(), errorText: _functionErrors.length > index ? _functionErrors[index] : null, ), onChanged: (value) => _plotFunctions(), ), ), SizedBox(width: 10), if (_functionControllers.length > 1) IconButton( icon: Icon(Icons.remove, color: Colors.red), onPressed: () => _removeFunction(index), ), ], ), ); }, ), ), SizedBox(height: 10), // 添加函数按钮 if (_functionControllers.length < 5) Center( child: ElevatedButton.icon( onPressed: () { _addFunction(); _plotFunctions(); }, icon: Icon(Icons.add), label: Text('添加函数'), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, ), ), ), ], ), ), ); } // 辅助方法:构建范围输入框 Widget _buildRangeTextField(TextEditingController controller, String label) { return TextField( controller: controller, decoration: InputDecoration( labelText: label, border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true), onChanged: (value) => _plotFunctions(), ); } Widget _buildPlotArea() { return Card( elevation: 4, child: Padding( padding: EdgeInsets.all(16), child: Column( children: [ Text( '函数图像', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), SizedBox(height: 10), Expanded( child: _buildPlot(), ), ], ), ), ); } Widget _buildPlot() { double xMin, xMax, yMin, yMax; try { xMin = double.parse(_xMinController.text); xMax = double.parse(_xMaxController.text); yMin = double.parse(_yMinController.text); yMax = double.parse(_yMaxController.text); } catch (e) { return Center( child: Text('请输入有效的坐标范围', style: TextStyle(color: Colors.red)), ); } if (xMin >= xMax || yMin >= yMax) { return Center( child: Text('最小值必须小于最大值', style: TextStyle(color: Colors.red)), ); } List lineBars = []; for (int i = 0; i < _functionControllers.length; i++) { String function = _functionControllers[i].text.trim(); if (function.isEmpty) continue; List spots = _evaluateFunction(function, xMin, xMax, i); if (spots.isNotEmpty) { lineBars.add( LineChartBarData( spots: spots, isCurved: true, color: _functionColors[i], barWidth: 2, isStrokeCapRound: true, dotData: FlDotData(show: false), belowBarData: BarAreaData(show: false), ), ); } } if (lineBars.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.functions, size: 50, color: Colors.grey), SizedBox(height: 10), Text('暂无函数图像可显示', style: TextStyle(color: Colors.grey)), Text('请输入有效的函数表达式', style: TextStyle(color: Colors.grey, fontSize: 12)), ], ), ); } return LineChart( LineChartData( clipData: FlClipData.all(), gridData: FlGridData( show: true, drawVerticalLine: true, drawHorizontalLine: true, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey.withOpacity(0.3), strokeWidth: 1, ); }, getDrawingVerticalLine: (value) { return FlLine( color: Colors.grey.withOpacity(0.3), strokeWidth: 1, ); }, ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 30, interval: _calculateInterval(xMin, xMax), getTitlesWidget: (value, meta) { return Text( value.toStringAsFixed(1), style: TextStyle(fontSize: 10, color: Colors.black), ); }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 40, interval: _calculateInterval(yMin, yMax), getTitlesWidget: (value, meta) { return Text( value.toStringAsFixed(1), style: TextStyle(fontSize: 10, color: Colors.black), ); }, ), ), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), ), borderData: FlBorderData( show: true, border: Border.all(color: Colors.black, width: 2), ), minX: xMin, maxX: xMax, minY: yMin, maxY: yMax, lineBarsData: lineBars, lineTouchData: LineTouchData(enabled: false), extraLinesData: ExtraLinesData( horizontalLines: [ HorizontalLine( y: 0, color: Colors.black, strokeWidth: 1, ), ], verticalLines: [ VerticalLine( x: 0, color: Colors.black, strokeWidth: 1, ), ], ), ), ); } double _calculateInterval(double min, double max) { double range = max - min; if (range <= 0) return 1.0; if (range <= 2) return 0.5; if (range <= 5) return 1.0; if (range <= 10) return 2.0; if (range <= 20) return 5.0; if (range <= 50) return 10.0; if (range <= 100) return 20.0; return 50.0; } }