Files
ChartAnalysisEngineering/Program.cs
T

2952 lines
116 KiB
C#
Raw Normal View History

2026-08-10 22:24:17 +08:00
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
using MySqlConnector;
namespace MysqlChart
{
static class Program
{
[STAThread]
static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (HasArg("--selftest"))
{
SelfTest();
return;
}
if (HasArg("--drawtest"))
{
MainForm mf = new MainForm();
mf.Show();
Application.DoEvents();
mf.RunDrawTest();
Application.DoEvents();
Thread.Sleep(2000);
return;
}
Application.Run(new MainForm());
}
static bool HasArg(string name)
{
string[] args = Environment.GetCommandLineArgs();
foreach (string a in args)
if (a.Equals(name, StringComparison.OrdinalIgnoreCase)) return true;
return false;
}
static void SelfTest()
{
StringBuilder log = new StringBuilder();
try
{
string configPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "config.ini");
ConfigData config = ConfigData.Load(configPath);
log.AppendLine("config: host=" + config.Host + " db=" + config.Database);
MySqlConnectionStringBuilder b = new MySqlConnectionStringBuilder();
b.Server = config.Host;
b.Port = uint.Parse(config.Port);
b.UserID = config.User;
b.Password = config.Password;
b.Database = config.Database;
b.CharacterSet = config.Charset;
b.ConnectionTimeout = 10;
using (MySqlConnection conn = new MySqlConnection(b.ConnectionString))
{
conn.Open();
log.AppendLine("1. 连接成功");
List<string> tableList = new List<string>();
using (MySqlCommand cmd = new MySqlCommand("SHOW TABLES", conn))
using (IDataReader r = cmd.ExecuteReader())
{
while (r.Read()) tableList.Add(r.GetString(0));
log.AppendLine("2. 表列表: " + string.Join(", ", tableList.ToArray()));
}
string table = "mixer_runtime";
if (!tableList.Contains("mixer_runtime") && tableList.Count > 0) table = tableList[0];
List<string> cols = new List<string>();
using (MySqlCommand cmd = new MySqlCommand(
"SELECT * FROM `" + table + "` LIMIT 0", conn))
using (IDataReader r = cmd.ExecuteReader())
{
for (int i = 0; i < r.FieldCount; i++) cols.Add(r.GetName(i));
log.AppendLine("3. 列列表: " + string.Join(", ", cols.ToArray()));
}
string xc = cols.Count > 0 ? cols[0] : "1";
string yc = cols.Count > 1 ? cols[1] : xc;
string qx = "`" + xc.Replace("`", "``") + "`";
string qy = "`" + yc.Replace("`", "``") + "`";
using (MySqlCommand cmd = new MySqlCommand(
"SELECT " + qx + ", " + qy + " FROM `" + table + "` ORDER BY " + qx + " ASC LIMIT 100", conn))
using (IDataReader r = cmd.ExecuteReader())
{
int count = 0;
object[] first = null;
object[] last = null;
while (r.Read())
{
object[] vals = new object[r.FieldCount];
r.GetValues(vals);
if (first == null) first = vals;
last = vals;
count++;
}
log.AppendLine("4. 查询100行成功, 行数=" + count);
if (first != null)
log.AppendLine(" 首行: x=" + first[0] + " y=" + first[1]);
if (last != null)
log.AppendLine(" 末行: x=" + last[0] + " y=" + last[1]);
}
using (MySqlCommand cmd = new MySqlCommand(
"SELECT COUNT(*) FROM `" + table + "`", conn))
{
object c = cmd.ExecuteScalar();
log.AppendLine("5. 全表行数: " + c);
}
}
log.AppendLine("SELFTEST PASS");
}
catch (Exception ex)
{
log.AppendLine("SELFTEST FAIL: " + ex.Message);
log.AppendLine(ex.StackTrace);
try
{
string configPath2 = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "config.ini");
ConfigData cfg2 = ConfigData.Load(configPath2);
MySqlConnectionStringBuilder b2 = new MySqlConnectionStringBuilder();
b2.Server = cfg2.Host;
b2.Port = uint.Parse(cfg2.Port);
b2.UserID = cfg2.User;
b2.Password = cfg2.Password;
b2.CharacterSet = cfg2.Charset;
b2.ConnectionTimeout = 10;
using (MySqlConnection conn2 = new MySqlConnection(b2.ConnectionString))
{
conn2.Open();
log.AppendLine("(无库连接成功, 服务器上数据库列表:)");
using (MySqlCommand cmd = new MySqlCommand("SHOW DATABASES", conn2))
using (IDataReader r = cmd.ExecuteReader())
{
while (r.Read()) log.AppendLine(" DB: " + r.GetString(0));
}
}
}
catch (Exception ex2)
{
log.AppendLine("(无法列出数据库: " + ex2.Message + ")");
}
}
string outFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "selftest_result.txt");
File.WriteAllText(outFile, log.ToString(), new UTF8Encoding(false));
}
static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
{
string simpleName = new AssemblyName(args.Name).Name;
string resName = simpleName + ".dll";
Assembly asm = Assembly.GetExecutingAssembly();
string[] names = asm.GetManifestResourceNames();
foreach (string n in names)
{
if (n.EndsWith(resName, StringComparison.OrdinalIgnoreCase))
{
using (Stream s = asm.GetManifestResourceStream(n))
{
if (s == null) return null;
byte[] buf = new byte[s.Length];
s.Read(buf, 0, buf.Length);
return Assembly.Load(buf);
}
}
}
return null;
}
}
class ConfigData
{
public string Host = "127.0.0.1";
public string Port = "3306";
public string User = "root";
public string Password = "";
public string Database = "";
public string Charset = "utf8mb4";
public static ConfigData Load(string path)
{
ConfigData c = new ConfigData();
if (!File.Exists(path)) return c;
string[] lines = File.ReadAllLines(path, Encoding.UTF8);
bool inMysql = false;
foreach (string line in lines)
{
string t = line.Trim();
if (t.Length == 0 || t.StartsWith(";") || t.StartsWith("#")) continue;
if (t.StartsWith("[") && t.EndsWith("]"))
{
inMysql = t.Equals("[mysql]", StringComparison.OrdinalIgnoreCase);
continue;
}
if (!inMysql) continue;
int idx = t.IndexOf('=');
if (idx <= 0) continue;
string key = t.Substring(0, idx).Trim().ToLowerInvariant();
string val = t.Substring(idx + 1).Trim();
switch (key)
{
case "host": c.Host = val; break;
case "port": c.Port = val; break;
case "user": c.User = val; break;
case "password": c.Password = val; break;
case "database": c.Database = val; break;
case "charset": c.Charset = val; break;
}
}
return c;
}
public void Save(string path)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("[mysql]");
sb.AppendLine("host = " + Host);
sb.AppendLine("port = " + Port);
sb.AppendLine("user = " + User);
sb.AppendLine("password = " + Password);
sb.AppendLine("database = " + Database);
sb.AppendLine("charset = " + Charset);
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
}
class MainForm : Form
{
ConfigData config;
string configPath;
TextBox txtHost, txtPort, txtUser, txtPassword;
ComboBox cmbDatabase, cmbTable, cmbX, cmbXType, cmbOrder, cmbCalc, cmbCalcTarget;
TextBox txtLimit, txtOffset, txtTailOffset, txtCalcParam;
Label lblCalcParam;
ListBox lbY;
DataGridView dgvFilter;
Button btnConnect, btnAddRule, btnDelRule, btnPlot, btnClear, btnSave;
Button btnCollapse, btnPin, btnExpand, btnApplyCalc, btnClearCalc, btnLegend, btnSettings, btnCross;
Label lblChartTitle, lblStatus;
Chart chart;
Panel left, right, splitterRef;
List<string> allColumns = new List<string>();
List<string> calcSeriesNames = new List<string>();
List<object[]> lastRows;
string lastXcol = "";
List<string> lastYcols = new List<string>();
string lastTable = "";
bool loadingDbs = false;
bool pinned = false;
int paletteCounter = 0;
bool draggingLegend = false;
Point dragStart = Point.Empty;
PointF crossPos = new PointF(-1, -1);
Form floatTip;
bool showCross = true;
bool legendHiddenByUser = false;
Panel legendBar;
FlowLayoutPanel legendFlow;
static readonly Color ThemeBlue = Color.FromArgb(41, 101, 173);
static readonly Color PanelBg = Color.FromArgb(246, 248, 251);
static readonly Color BorderLine = Color.FromArgb(214, 221, 230);
public MainForm()
{
configPath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "config.ini");
config = ConfigData.Load(configPath);
InitUI();
FillConnFields();
Shown += delegate { FitWindowToScreen(); };
}
void FitWindowToScreen()
{
try
{
Screen scr = Screen.FromControl(this);
int w = (int)(scr.WorkingArea.Width * 0.92);
int h = (int)(scr.WorkingArea.Height * 0.92);
if (w < 1000) w = 1000;
if (h < 700) h = 700;
this.Width = w;
this.Height = h;
this.StartPosition = FormStartPosition.CenterScreen;
}
catch { }
}
void InitUI()
{
Text = "趋势分析预测工具";
Size = new Size(1240, 820);
MinimumSize = new Size(1000, 700);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Microsoft YaHei UI", 9F);
BackColor = PanelBg;
try
{
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream("hamster.ico"))
{
if (s != null)
this.Icon = new Icon(s);
}
}
catch { }
left = new Panel();
left.Width = 380;
left.BackColor = PanelBg;
left.AutoScroll = true;
Controls.Add(left);
Panel right = new Panel();
right.BackColor = Color.White;
Controls.Add(right);
this.right = right;
Panel splitter = new Panel();
splitter.BackColor = BorderLine;
splitter.Cursor = Cursors.SizeWE;
Controls.Add(splitter);
splitterRef = splitter;
SetupSplitterDrag(splitter);
this.Resize += delegate { LayoutPanels(left, right, splitter); };
LayoutPanels(left, right, splitter);
left.Resize += delegate { LayoutLeftPanel(); };
int x2 = 8;
int iw = 406;
int y = 4;
btnCollapse = MakeIconButton("折叠", 0);
btnCollapse.Location = new Point(x2, y);
btnCollapse.Size = new Size(200, 30);
btnCollapse.Click += delegate
{
left.Visible = false;
LayoutPanels(left, right, splitterRef);
btnExpand.Visible = true;
btnExpand.BringToFront();
};
left.Controls.Add(btnCollapse);
btnPin = MakeIconButton("钉住", 1);
btnPin.Location = new Point(x2 + 208, y);
btnPin.Size = new Size(206, 30);
btnPin.Click += delegate { TogglePin(); };
left.Controls.Add(btnPin);
y += 40;
Label title1 = new Label();
title1.Text = "服务器连接";
title1.Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold);
title1.ForeColor = ThemeBlue;
title1.Location = new Point(x2, y);
title1.AutoSize = true;
left.Controls.Add(title1);
y += 30;
AddLabel(left, "主机地址:", x2, y);
txtHost = AddText(left, config.Host, x2 + 88, y, 132);
AddLabel2(left, "端口:", x2 + 226, y);
txtPort = AddText(left, config.Port, x2 + 270, y, 144);
y += 36;
AddLabel(left, "用户名:", x2, y);
txtUser = AddText(left, config.User, x2 + 88, y, 132);
AddLabel2(left, "密码:", x2 + 226, y);
txtPassword = AddText(left, config.Password, x2 + 270, y, 144, true);
y += 36;
AddLabel(left, "DB:", x2, y);
cmbDatabase = MakeCombo();
cmbDatabase.DropDownStyle = ComboBoxStyle.DropDown;
cmbDatabase.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
cmbDatabase.AutoCompleteSource = AutoCompleteSource.ListItems;
cmbDatabase.Location = new Point(x2 + 88, y);
cmbDatabase.Size = new Size(iw - 88, 28);
cmbDatabase.SelectedIndexChanged += delegate { DatabaseSelected(); };
cmbDatabase.KeyDown += delegate(object s, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
e.SuppressKeyPress = true;
DatabaseSelected();
}
};
left.Controls.Add(cmbDatabase);
y += 38;
btnConnect = new Button();
btnConnect.Text = "连 接";
btnConnect.Location = new Point(x2, y);
btnConnect.Size = new Size(iw, 34);
btnConnect.FlatStyle = FlatStyle.Flat;
btnConnect.FlatAppearance.BorderSize = 0;
btnConnect.BackColor = ThemeBlue;
btnConnect.ForeColor = Color.White;
btnConnect.Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold);
btnConnect.Cursor = Cursors.Hand;
btnConnect.Click += delegate { ConnectAndLoad(); };
left.Controls.Add(btnConnect);
y += 46;
Label titleQ = new Label();
titleQ.Text = "排序规则";
titleQ.Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold);
titleQ.ForeColor = ThemeBlue;
titleQ.Location = new Point(x2, y);
titleQ.AutoSize = true;
left.Controls.Add(titleQ);
y += 30;
AddLabel(left, "方向:", x2, y);
cmbOrder = MakeCombo();
cmbOrder.Location = new Point(x2 + 88, y);
cmbOrder.Size = new Size(156, 28);
cmbOrder.Items.Add("正序 (最早在前)");
cmbOrder.Items.Add("倒序 (最新在前)");
cmbOrder.SelectedIndex = 0;
left.Controls.Add(cmbOrder);
AddLabel2(left, "条数:", x2 + 246, y);
txtLimit = AddText(left, "2000", x2 + 288, y, 126);
y += 36;
AddLabel(left, "偏移:", x2, y);
txtOffset = AddText(left, "0", x2 + 88, y, 156);
Label tipL = new Label();
tipL.Text = "跳过前 N 行";
tipL.Font = new Font("Microsoft YaHei UI", 8.5F);
tipL.ForeColor = Color.FromArgb(140, 150, 160);
tipL.Location = new Point(x2 + 250, y + 6);
tipL.AutoSize = true;
left.Controls.Add(tipL);
y += 42;
AddLabel(left, "起点:", x2, y);
txtTailOffset = AddText(left, "0", x2 + 88, y, 156);
y += 22;
Label tipT = new Label();
tipT.Text = "从最新数据往回数 N 条作为查询起点";
tipT.Font = new Font("Microsoft YaHei UI", 8.5F);
tipT.ForeColor = Color.FromArgb(140, 150, 160);
tipT.Location = new Point(x2 + 88, y);
tipT.AutoSize = true;
left.Controls.Add(tipT);
y += 20;
Label title2 = new Label();
title2.Text = "存储表与坐标轴";
title2.Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold);
title2.ForeColor = ThemeBlue;
title2.Location = new Point(x2, y);
title2.AutoSize = true;
left.Controls.Add(title2);
y += 30;
AddLabel(left, "存储表:", x2, y);
cmbTable = MakeCombo();
cmbTable.Location = new Point(x2 + 88, y);
cmbTable.Size = new Size(iw - 88, 28);
cmbTable.SelectedIndexChanged += delegate { TableSelected(); };
left.Controls.Add(cmbTable);
y += 36;
AddLabel(left, "横坐标:", x2, y);
cmbX = MakeCombo();
cmbX.Location = new Point(x2 + 88, y);
cmbX.Size = new Size(iw - 88, 28);
left.Controls.Add(cmbX);
y += 36;
AddLabel(left, "横轴类型:", x2, y);
cmbXType = MakeCombo();
cmbXType.Location = new Point(x2 + 88, y);
cmbXType.Size = new Size(iw - 88, 28);
cmbXType.Items.Add("自动识别");
cmbXType.Items.Add("时间");
cmbXType.Items.Add("数值");
cmbXType.Items.Add("文本");
cmbXType.SelectedIndex = 0;
left.Controls.Add(cmbXType);
y += 36;
AddLabel(left, "纵坐标:", x2, y);
lbY = new ListBox();
lbY.BorderStyle = BorderStyle.FixedSingle;
lbY.Location = new Point(x2 + 88, y);
lbY.Size = new Size(iw - 88, 96);
lbY.HorizontalScrollbar = true;
lbY.SelectionMode = SelectionMode.MultiExtended;
lbY.BackColor = Color.White;
lbY.IntegralHeight = false;
left.Controls.Add(lbY);
y += 104;
Label tipY = new Label();
tipY.Text = "提示: 按住 Ctrl 键可多选纵坐标";
tipY.Font = new Font("Microsoft YaHei UI", 8.5F);
tipY.ForeColor = Color.FromArgb(140, 150, 160);
tipY.Location = new Point(x2 + 88, y);
tipY.AutoSize = true;
left.Controls.Add(tipY);
y += 24;
Label title3 = new Label();
title3.Text = "过滤规则 (多行按 AND/OR 连接)";
title3.Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold);
title3.ForeColor = ThemeBlue;
title3.Location = new Point(x2, y);
title3.AutoSize = true;
left.Controls.Add(title3);
y += 30;
dgvFilter = new DataGridView();
dgvFilter.Location = new Point(x2, y);
dgvFilter.Size = new Size(iw, 140);
dgvFilter.AllowUserToAddRows = false;
dgvFilter.AllowUserToDeleteRows = false;
dgvFilter.AllowUserToResizeRows = false;
dgvFilter.RowHeadersVisible = false;
dgvFilter.BackgroundColor = Color.White;
dgvFilter.BorderStyle = BorderStyle.FixedSingle;
dgvFilter.EnableHeadersVisualStyles = false;
dgvFilter.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(52, 73, 94);
dgvFilter.ColumnHeadersDefaultCellStyle.ForeColor = Color.White;
dgvFilter.ColumnHeadersDefaultCellStyle.Font = new Font("Microsoft YaHei UI", 8.5F, FontStyle.Bold);
dgvFilter.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
dgvFilter.ColumnHeadersHeight = 24;
dgvFilter.DefaultCellStyle.Font = new Font("Microsoft YaHei UI", 8.5F);
dgvFilter.DefaultCellStyle.SelectionBackColor = Color.FromArgb(191, 215, 245);
dgvFilter.DefaultCellStyle.SelectionForeColor = Color.Black;
dgvFilter.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(244, 247, 251);
dgvFilter.GridColor = Color.FromArgb(221, 228, 236);
dgvFilter.RowTemplate.Height = 22;
DataGridViewComboBoxColumn colCol = new DataGridViewComboBoxColumn();
colCol.HeaderText = "列";
colCol.Width = 132;
colCol.FlatStyle = FlatStyle.Flat;
dgvFilter.Columns.Add(colCol);
DataGridViewComboBoxColumn colOp = new DataGridViewComboBoxColumn();
colOp.HeaderText = "操作符";
colOp.Width = 92;
colOp.FlatStyle = FlatStyle.Flat;
colOp.Items.Add("=");
colOp.Items.Add("!=");
colOp.Items.Add(">");
colOp.Items.Add(">=");
colOp.Items.Add("<");
colOp.Items.Add("<=");
colOp.Items.Add("LIKE");
colOp.Items.Add("BETWEEN");
colOp.Items.Add("IS NULL");
colOp.Items.Add("IS NOT NULL");
dgvFilter.Columns.Add(colOp);
DataGridViewTextBoxColumn colVal = new DataGridViewTextBoxColumn();
colVal.HeaderText = "值";
colVal.Width = 120;
colVal.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
colVal.ToolTipText = "BETWEEN 用逗号分隔两个值";
dgvFilter.Columns.Add(colVal);
DataGridViewComboBoxColumn colLogic = new DataGridViewComboBoxColumn();
colLogic.HeaderText = "逻辑";
colLogic.Width = 60;
colLogic.FlatStyle = FlatStyle.Flat;
colLogic.Items.Add("AND");
colLogic.Items.Add("OR");
dgvFilter.Columns.Add(colLogic);
left.Controls.Add(dgvFilter);
y += 148;
btnAddRule = MakeSmallButton("+ 添加规则");
btnAddRule.Location = new Point(x2, y);
btnAddRule.Size = new Size(130, 28);
btnAddRule.BackColor = Color.FromArgb(39, 174, 96);
btnAddRule.Click += delegate { AddRule(); };
left.Controls.Add(btnAddRule);
btnDelRule = MakeSmallButton("- 删除");
btnDelRule.Location = new Point(x2 + 138, y);
btnDelRule.Size = new Size(130, 28);
btnDelRule.BackColor = Color.FromArgb(192, 57, 43);
btnDelRule.Click += delegate { DelRule(); };
left.Controls.Add(btnDelRule);
btnClear = MakeSmallButton("清空");
btnClear.Location = new Point(x2 + 276, y);
btnClear.Size = new Size(138, 28);
btnClear.BackColor = Color.FromArgb(127, 140, 141);
btnClear.Click += delegate { dgvFilter.Rows.Clear(); };
left.Controls.Add(btnClear);
y += 38;
btnPlot = new Button();
btnPlot.Text = "绘制曲线";
btnPlot.Location = new Point(x2, y);
btnPlot.Size = new Size(204, 42);
btnPlot.FlatStyle = FlatStyle.Flat;
btnPlot.FlatAppearance.BorderSize = 0;
btnPlot.BackColor = Color.FromArgb(39, 174, 96);
btnPlot.ForeColor = Color.White;
btnPlot.Font = new Font("Microsoft YaHei UI", 11F, FontStyle.Bold);
btnPlot.Cursor = Cursors.Hand;
btnPlot.Click += delegate { Plot(); };
left.Controls.Add(btnPlot);
btnSave = new Button();
btnSave.Text = "保存图片";
btnSave.Location = new Point(x2 + 210, y);
btnSave.Size = new Size(204, 42);
btnSave.FlatStyle = FlatStyle.Flat;
btnSave.FlatAppearance.BorderSize = 0;
btnSave.BackColor = ThemeBlue;
btnSave.ForeColor = Color.White;
btnSave.Font = new Font("Microsoft YaHei UI", 11F, FontStyle.Bold);
btnSave.Cursor = Cursors.Hand;
btnSave.Click += delegate { SaveImage(); };
left.Controls.Add(btnSave);
y += 52;
lblStatus = new Label();
lblStatus.Text = "请填写服务器连接信息后点击连接";
lblStatus.Location = new Point(x2, y);
lblStatus.Size = new Size(iw, 18);
lblStatus.ForeColor = Color.FromArgb(120, 130, 140);
left.Controls.Add(lblStatus);
lblChartTitle = new Label();
lblChartTitle.Dock = DockStyle.Top;
lblChartTitle.Height = 36;
lblChartTitle.TextAlign = ContentAlignment.MiddleCenter;
lblChartTitle.Font = new Font("Microsoft YaHei UI", 12F, FontStyle.Bold);
lblChartTitle.ForeColor = ThemeBlue;
lblChartTitle.Text = "";
right.Controls.Add(lblChartTitle);
legendBar = new Panel();
legendBar.Dock = DockStyle.Top;
legendBar.Height = 30;
legendBar.BackColor = Color.FromArgb(250, 250, 252);
legendBar.Visible = false;
right.Controls.Add(legendBar);
legendFlow = new FlowLayoutPanel();
legendFlow.Dock = DockStyle.Fill;
legendFlow.BackColor = Color.FromArgb(250, 250, 252);
legendFlow.Padding = new Padding(10, 4, 10, 4);
legendBar.Controls.Add(legendFlow);
Panel bar = new Panel();
bar.Dock = DockStyle.Top;
bar.Height = 44;
bar.BackColor = Color.FromArgb(240, 243, 247);
right.Controls.Add(bar);
Label lTarget = new Label();
lTarget.Text = "曲线:";
lTarget.Location = new Point(12, 14);
lTarget.AutoSize = true;
lTarget.ForeColor = Color.FromArgb(60, 70, 80);
bar.Controls.Add(lTarget);
cmbCalcTarget = new ComboBox();
cmbCalcTarget.DropDownStyle = ComboBoxStyle.DropDownList;
cmbCalcTarget.FlatStyle = FlatStyle.Flat;
cmbCalcTarget.Location = new Point(52, 8);
cmbCalcTarget.Size = new Size(86, 26);
bar.Controls.Add(cmbCalcTarget);
Label lCalc = new Label();
lCalc.Text = "运算:";
lCalc.Location = new Point(142, 14);
lCalc.AutoSize = true;
lCalc.ForeColor = Color.FromArgb(60, 70, 80);
bar.Controls.Add(lCalc);
cmbCalc = new ComboBox();
cmbCalc.DropDownStyle = ComboBoxStyle.DropDownList;
cmbCalc.FlatStyle = FlatStyle.Flat;
cmbCalc.Location = new Point(184, 8);
cmbCalc.Size = new Size(160, 26);
cmbCalc.Items.Add("贝叶斯回归 (AI)");
cmbCalc.Items.Add("常用对数 (log10)");
cmbCalc.Items.Add("对数 (ln)");
cmbCalc.Items.Add("多项式拟合 (Polyfit) (AI)");
cmbCalc.Items.Add("二次变换 (a*y²+b) (AI)");
cmbCalc.Items.Add("积分 (累积)");
cmbCalc.Items.Add("开方 (sqrt)");
cmbCalc.Items.Add("平滑曲线 (滑动平均)");
cmbCalc.Items.Add("梯度下降 (AI)");
cmbCalc.Items.Add("微分 (差分)");
cmbCalc.Items.Add("线性回归 (AI)");
cmbCalc.Items.Add("线性变换 (a*y+b) (AI)");
cmbCalc.Items.Add("指数 (exp)");
int defCalc = cmbCalc.Items.IndexOf("平滑曲线 (滑动平均)");
cmbCalc.SelectedIndex = defCalc >= 0 ? defCalc : 0;
cmbCalc.SelectedIndexChanged += delegate { UpdateCalcParamUI(); };
bar.Controls.Add(cmbCalc);
Label lParam = new Label();
lParam.Text = "无参数";
lParam.Location = new Point(350, 14);
lParam.AutoSize = true;
lParam.ForeColor = Color.FromArgb(60, 70, 80);
bar.Controls.Add(lParam);
this.lblCalcParam = lParam;
txtCalcParam = new TextBox();
txtCalcParam.Text = "5";
txtCalcParam.Location = new Point(398, 8);
txtCalcParam.Width = 50;
txtCalcParam.BorderStyle = BorderStyle.FixedSingle;
txtCalcParam.Enabled = false;
bar.Controls.Add(txtCalcParam);
UpdateCalcParamUI();
btnApplyCalc = MakeSmallButton("生成运算曲线");
btnApplyCalc.Location = new Point(456, 6);
btnApplyCalc.Size = new Size(104, 30);
btnApplyCalc.BackColor = ThemeBlue;
btnApplyCalc.Click += delegate { ApplyCalc(); };
bar.Controls.Add(btnApplyCalc);
btnClearCalc = MakeSmallButton("清除运算");
btnClearCalc.Location = new Point(490, 6);
btnClearCalc.Size = new Size(92, 30);
btnClearCalc.BackColor = Color.FromArgb(127, 140, 141);
btnClearCalc.Click += delegate { ClearCalcSeries(); };
bar.Controls.Add(btnClearCalc);
btnLegend = MakeSmallButton("隐藏图例");
btnLegend.Location = new Point(588, 6);
btnLegend.Size = new Size(92, 30);
btnLegend.BackColor = Color.FromArgb(52, 73, 94);
btnLegend.Click += delegate { ToggleLegend(); };
bar.Controls.Add(btnLegend);
btnSettings = MakeSmallButton("图表设置");
btnSettings.Location = new Point(686, 6);
btnSettings.Size = new Size(92, 30);
btnSettings.BackColor = Color.FromArgb(52, 73, 94);
btnSettings.Click += delegate { OpenChartSettings(); };
bar.Controls.Add(btnSettings);
btnCross = MakeSmallButton("隐藏十字线");
btnCross.Location = new Point(786, 6);
btnCross.Size = new Size(104, 30);
btnCross.BackColor = Color.FromArgb(52, 73, 94);
btnCross.Click += delegate { ToggleCross(); };
bar.Controls.Add(btnCross);
chart = new Chart();
chart.Dock = DockStyle.Fill;
chart.BackColor = Color.White;
chart.BorderlineColor = BorderLine;
chart.BorderlineWidth = 1;
ChartArea area = new ChartArea("main");
area.BackColor = Color.White;
area.AxisX.MajorGrid.LineColor = Color.FromArgb(235, 238, 242);
area.AxisY.MajorGrid.LineColor = Color.FromArgb(235, 238, 242);
area.AxisX.LineColor = BorderLine;
area.AxisY.LineColor = BorderLine;
area.AxisX.LabelStyle.Font = new Font("Microsoft YaHei UI", 9F);
area.AxisY.LabelStyle.Font = new Font("Microsoft YaHei UI", 9F);
area.AxisX.TitleFont = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
area.AxisY.TitleFont = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
area.AxisX.TitleForeColor = ThemeBlue;
area.AxisY.TitleForeColor = ThemeBlue;
chart.ChartAreas.Add(area);
chart.Legends.Add(new Legend("legend"));
chart.Legends["legend"].Enabled = false;
chart.Legends["legend"].Font = new Font("Microsoft YaHei UI", 9F);
chart.Legends["legend"].BackColor = Color.FromArgb(250, 250, 252);
chart.Legends["legend"].BorderColor = BorderLine;
chart.Legends["legend"].BorderWidth = 1;
chart.Legends["legend"].Alignment = StringAlignment.Near;
right.Controls.Add(chart);
SetupLegendDrag();
chart.MouseMove += Chart_MouseMove;
chart.MouseClick += Chart_MouseClick;
chart.MouseLeave += delegate { crossPos = new PointF(-1, -1); chart.Invalidate(); };
chart.PostPaint += Chart_PostPaint;
btnExpand = MakeIconButton("展开配置", 2);
btnExpand.Location = new Point(4, 50);
btnExpand.Size = new Size(110, 32);
btnExpand.FlatStyle = FlatStyle.Flat;
btnExpand.FlatAppearance.BorderSize = 0;
btnExpand.BackColor = ThemeBlue;
btnExpand.ForeColor = Color.White;
btnExpand.Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
btnExpand.Cursor = Cursors.Hand;
btnExpand.Visible = false;
btnExpand.Click += delegate
{
left.Visible = true;
LayoutPanels(left, right, splitterRef);
btnExpand.Visible = false;
}; right.Controls.Add(btnExpand);
btnExpand.BringToFront();
LayoutLeftPanel();
}
void SetupLegendDrag()
{
chart.MouseDown += delegate(object s, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
try
{
Legend lg = chart.Legends["legend"];
if (!lg.Enabled) return;
HitTestResult hr = chart.HitTest(e.X, e.Y);
if (hr != null && hr.ChartElementType == ChartElementType.LegendArea)
{
draggingLegend = true;
dragStart = e.Location;
chart.Cursor = Cursors.Hand;
}
}
catch { }
};
chart.MouseMove += delegate(object s, MouseEventArgs e)
{
if (!draggingLegend) return;
try
{
if (chart.Width <= 0 || chart.Height <= 0) return;
Legend lg = chart.Legends["legend"];
ElementPosition p = lg.Position;
p.Auto = false;
float dx = e.X - dragStart.X;
float dy = e.Y - dragStart.Y;
p.X += dx / chart.Width * 100f;
p.Y += dy / chart.Height * 100f;
if (p.X < -p.Width) p.X = -p.Width + 2;
if (p.X > 100) p.X = 98;
if (p.Y < 0) p.Y = 0;
if (p.Y > 100) p.Y = 98;
dragStart = e.Location;
chart.Invalidate();
}
catch { draggingLegend = false; }
};
chart.MouseUp += delegate(object s, MouseEventArgs e)
{
draggingLegend = false;
chart.Cursor = Cursors.Default;
};
}
void Chart_MouseMove(object sender, MouseEventArgs e)
{
if (draggingLegend) return;
try
{
ChartArea area = chart.ChartAreas["main"];
bool inside = IsInPlotArea(e.Location, area);
PointF np = inside ? new PointF(e.X, e.Y) : new PointF(-1, -1);
if (np.X != crossPos.X || np.Y != crossPos.Y)
{
crossPos = np;
chart.Invalidate();
}
}
catch { }
}
void Chart_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
try
{
HitTestResult hr = chart.HitTest(e.X, e.Y);
if (hr != null && hr.ChartElementType == ChartElementType.DataPoint &&
hr.Series != null && hr.PointIndex >= 0 &&
hr.PointIndex < hr.Series.Points.Count)
{
ShowFloatTip(e.Location, hr.Series.Name, hr.Series.Points[hr.PointIndex], hr.PointIndex);
}
else
{
CloseFloatTip();
}
}
catch { }
}
void Chart_PostPaint(object sender, ChartPaintEventArgs e)
{
if (e.ChartElement != chart || !showCross || crossPos.X < 0) return;
try
{
ChartArea area = chart.ChartAreas["main"];
RectangleF pa = GetPlotAreaRect(area);
if (pa.Width <= 0 || pa.Height <= 0) return;
if (!pa.Contains(crossPos)) return;
using (Pen pen = new Pen(Color.FromArgb(150, 90, 90, 90)))
{
pen.DashStyle = DashStyle.Dash;
Graphics g = e.ChartGraphics.Graphics;
g.DrawLine(pen, pa.Left, crossPos.Y, pa.Right, crossPos.Y);
g.DrawLine(pen, crossPos.X, pa.Top, crossPos.X, pa.Bottom);
}
}
catch { }
}
RectangleF GetPlotAreaRect(ChartArea area)
{
ElementPosition areaPos = area.Position;
ElementPosition inner = area.InnerPlotPosition;
if (areaPos == null || inner == null || inner.Width <= 0 || inner.Height <= 0)
return RectangleF.Empty;
float ax = chart.Width * areaPos.X / 100f;
float ay = chart.Height * areaPos.Y / 100f;
float aw = chart.Width * areaPos.Width / 100f;
float ah = chart.Height * areaPos.Height / 100f;
return new RectangleF(
ax + aw * inner.X / 100f,
ay + ah * inner.Y / 100f,
aw * inner.Width / 100f,
ah * inner.Height / 100f);
}
bool IsInPlotArea(Point p, ChartArea area)
{
RectangleF pa = GetPlotAreaRect(area);
return pa.Width > 0 && pa.Height > 0 && pa.Contains(p);
}
void ShowFloatTip(Point chartPoint, string seriesName, DataPoint dp, int pointIndex)
{
CloseFloatTip();
string xStr;
if (!string.IsNullOrEmpty(dp.AxisLabel))
xStr = dp.AxisLabel;
else if (dp.XValue > 1000 && dp.XValue < 400000)
{
try { xStr = DateTime.FromOADate(dp.XValue).ToString("yyyy-MM-dd HH:mm:ss"); }
catch { xStr = dp.XValue.ToString("0.####"); }
}
else
xStr = dp.XValue.ToString("0.####");
string yStr = dp.YValues.Length > 0 ? dp.YValues[0].ToString("0.####") : "N/A";
string idxStr = pointIndex.ToString();
Form f = new Form();
f.FormBorderStyle = FormBorderStyle.None;
f.ShowInTaskbar = false;
f.TopMost = true;
f.StartPosition = FormStartPosition.Manual;
f.BackColor = Color.FromArgb(48, 56, 66);
f.AutoSize = true;
f.Padding = new Padding(10, 8, 10, 8);
f.Font = new Font("Microsoft YaHei UI", 9F);
TableLayoutPanel tbl = new TableLayoutPanel();
tbl.AutoSize = true;
tbl.AutoSizeMode = AutoSizeMode.GrowAndShrink;
tbl.ColumnCount = 2;
tbl.RowCount = 4;
tbl.Dock = DockStyle.Fill;
f.Controls.Add(tbl);
AddTipCell(tbl, 0, 0, "曲线:", true, Color.White);
AddTipCell(tbl, 1, 0, seriesName, false, Color.FromArgb(110, 190, 255));
AddTipCell(tbl, 0, 1, "X:", true, Color.White);
AddTipCell(tbl, 1, 1, xStr, false, Color.White);
AddTipCell(tbl, 0, 2, "Y:", true, Color.White);
AddTipCell(tbl, 1, 2, yStr, false, Color.FromArgb(255, 220, 120));
AddTipCell(tbl, 0, 3, "序号:", true, Color.White);
AddTipCell(tbl, 1, 3, idxStr, false, Color.White);
Point sp = chart.PointToScreen(chartPoint);
f.Location = new Point(sp.X + 16, sp.Y + 16);
f.Deactivate += delegate { CloseFloatTip(); };
f.Show();
floatTip = f;
Screen sc = Screen.FromPoint(sp);
if (f.Location.X + f.Width > sc.WorkingArea.Right)
f.Location = new Point(sp.X - f.Width - 16, f.Location.Y);
if (f.Location.Y + f.Height > sc.WorkingArea.Bottom)
f.Location = new Point(f.Location.X, sp.Y - f.Height - 16);
}
void AddTipCell(TableLayoutPanel tbl, int col, int row, string text, bool bold, Color color)
{
Label l = new Label();
l.Text = text;
l.AutoSize = true;
l.Margin = new Padding(2);
l.ForeColor = color;
l.Font = new Font("Microsoft YaHei UI", 9F, bold ? FontStyle.Bold : FontStyle.Regular);
l.BackColor = Color.Transparent;
tbl.Controls.Add(l, col, row);
}
void CloseFloatTip()
{
if (floatTip != null)
{
Form f = floatTip;
floatTip = null;
try { f.Close(); }
catch { }
}
}
Button MakeToolButton(string text)
{
Button b = new Button();
b.Text = text;
b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderColor = BorderLine;
b.FlatAppearance.BorderSize = 1;
b.BackColor = Color.White;
b.ForeColor = Color.FromArgb(60, 70, 80);
b.Font = new Font("Microsoft YaHei UI", 9F);
b.Cursor = Cursors.Hand;
return b;
}
Button MakeIconButton(string text, int iconType)
{
Button b = new Button();
b.Text = "";
b.Tag = text;
b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderColor = BorderLine;
b.FlatAppearance.BorderSize = 1;
b.BackColor = Color.White;
b.ForeColor = Color.FromArgb(60, 70, 80);
b.Font = new Font("Microsoft YaHei UI", 9F);
b.Cursor = Cursors.Hand;
b.Paint += delegate(object s, PaintEventArgs e)
{
Button bb = (Button)s;
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
string t = bb.Tag as string;
if (string.IsNullOrEmpty(t)) return;
SizeF ts = g.MeasureString(t, bb.Font);
int totalW = 16 + 6 + (int)Math.Ceiling(ts.Width);
int x0 = (bb.ClientSize.Width - totalW) / 2;
if (x0 < 2) x0 = 2;
int y0 = (bb.ClientSize.Height - 16) / 2;
using (Pen p = new Pen(bb.ForeColor, 2))
{
p.StartCap = LineCap.Round;
p.EndCap = LineCap.Round;
if (iconType == 0)
{
g.DrawLine(p, x0 + 13, y0 + 8, x0 + 3, y0 + 8);
g.DrawLine(p, x0 + 3, y0 + 8, x0 + 8, y0 + 3);
g.DrawLine(p, x0 + 3, y0 + 8, x0 + 8, y0 + 13);
}
else if (iconType == 1)
{
g.DrawEllipse(p, x0 + 5, y0 + 1, 6, 6);
g.DrawLine(p, x0 + 8, y0 + 7, x0 + 8, y0 + 13);
g.DrawLine(p, x0 + 4, y0 + 13, x0 + 12, y0 + 13);
}
else
{
g.DrawLine(p, x0 + 3, y0 + 8, x0 + 13, y0 + 8);
g.DrawLine(p, x0 + 13, y0 + 8, x0 + 8, y0 + 3);
g.DrawLine(p, x0 + 13, y0 + 8, x0 + 8, y0 + 13);
}
}
using (SolidBrush br = new SolidBrush(bb.ForeColor))
{
g.DrawString(t, bb.Font, br, x0 + 22, (bb.ClientSize.Height - ts.Height) / 2f);
}
};
return b;
}
ComboBox MakeCombo()
{
ComboBox c = new ComboBox();
c.DropDownStyle = ComboBoxStyle.DropDownList;
c.FlatStyle = FlatStyle.Flat;
c.BackColor = Color.White;
return c;
}
Button MakeSmallButton(string text)
{
Button b = new Button();
b.Text = text;
b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderSize = 0;
b.ForeColor = Color.White;
b.Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Bold);
b.Cursor = Cursors.Hand;
return b;
}
void AddLabel(Control parent, string text, int x, int y)
{
Label l = new Label();
l.Text = text;
l.Location = new Point(x, y + 4);
l.AutoSize = false;
l.Width = 84;
l.Height = 24;
l.TextAlign = ContentAlignment.MiddleRight;
l.ForeColor = Color.FromArgb(60, 70, 80);
parent.Controls.Add(l);
}
void AddLabel2(Control parent, string text, int x, int y)
{
Label l = new Label();
l.Text = text;
l.Location = new Point(x, y + 4);
l.AutoSize = false;
l.Width = 38;
l.Height = 24;
l.TextAlign = ContentAlignment.MiddleRight;
l.ForeColor = Color.FromArgb(60, 70, 80);
parent.Controls.Add(l);
}
void AddLabel3(Control parent, string text, int x, int y)
{
Label l = new Label();
l.Text = text;
l.Location = new Point(x, y + 4);
l.AutoSize = false;
l.Width = 90;
l.Height = 24;
l.TextAlign = ContentAlignment.MiddleRight;
l.ForeColor = Color.FromArgb(60, 70, 80);
parent.Controls.Add(l);
}
TextBox AddText(Control parent, string text, int x, int y, int width)
{
return AddText(parent, text, x, y, width, false);
}
TextBox AddText(Control parent, string text, int x, int y, int width, bool password)
{
TextBox t = new TextBox();
t.Text = text;
t.Location = new Point(x, y);
t.Width = width;
t.BorderStyle = BorderStyle.FixedSingle;
t.UseSystemPasswordChar = password;
parent.Controls.Add(t);
return t;
}
void LayoutPanels(Panel leftP, Panel rightP, Panel splitterP)
{
int w = ClientSize.Width;
int h = ClientSize.Height;
if (w < 100 || h < 100) return;
int lw = leftP.Visible ? leftP.Width : 0;
if (lw > 0)
{
leftP.Location = new Point(0, 0);
leftP.Size = new Size(lw, h);
leftP.Padding = new Padding(12, 8, 12, 8);
splitterP.Location = new Point(lw, 0);
splitterP.Size = new Size(5, h);
splitterP.Visible = true;
rightP.Location = new Point(lw + 5, 0);
}
else
{
splitterP.Visible = false;
rightP.Location = new Point(0, 0);
}
rightP.Size = new Size(w - rightP.Location.X, h);
}
const int MinLeftLayoutW = 380;
void LayoutLeftPanel()
{
try
{
int layW = Math.Max(left.ClientSize.Width, MinLeftLayoutW);
int x2 = 8;
int availW = layW - 12 - x2;
int inputX = x2 + 88;
int inputW = availW - 88;
int gap = 12;
int leftColW = (int)(availW * 0.54);
int w1 = leftColW - 88 - gap;
if (w1 < 100) w1 = 100;
int rightLabelX = x2 + leftColW;
int rightInputX = rightLabelX + 38 + gap;
int w2 = availW - leftColW - 38 - gap;
if (w2 < 90) w2 = 90;
int bw2 = (availW - 8) / 2;
btnCollapse.Location = new Point(x2, 4);
btnCollapse.Size = new Size(bw2, 30);
btnPin.Location = new Point(x2 + bw2 + 8, 4);
btnPin.Size = new Size(availW - bw2 - 8, 30);
txtHost.Location = new Point(inputX, 74);
txtHost.Width = w1;
txtPort.Location = new Point(rightInputX, 74);
txtPort.Width = w2;
txtUser.Location = new Point(inputX, 110);
txtUser.Width = w1;
txtPassword.Location = new Point(rightInputX, 110);
txtPassword.Width = w2;
cmbDatabase.Location = new Point(inputX, 146);
cmbDatabase.Width = inputW;
btnConnect.Location = new Point(x2, 184);
btnConnect.Width = availW;
cmbOrder.Location = new Point(inputX, 260);
cmbOrder.Width = w1;
txtLimit.Location = new Point(rightInputX, 260);
txtLimit.Width = w2;
txtOffset.Location = new Point(inputX, 296);
txtOffset.Width = inputW;
txtTailOffset.Location = new Point(inputX, 338);
txtTailOffset.Width = inputW;
cmbTable.Location = new Point(inputX, 410);
cmbTable.Width = inputW;
cmbX.Location = new Point(inputX, 446);
cmbX.Width = inputW;
cmbXType.Location = new Point(inputX, 482);
cmbXType.Width = inputW;
lbY.Location = new Point(inputX, 518);
lbY.Width = inputW;
dgvFilter.Location = new Point(x2, 676);
dgvFilter.Width = availW;
int bw3 = (availW - 16) / 3;
btnAddRule.Location = new Point(x2, 824);
btnAddRule.Width = bw3;
btnDelRule.Location = new Point(x2 + bw3 + 8, 824);
btnDelRule.Width = bw3;
btnClear.Location = new Point(x2 + bw3 * 2 + 16, 824);
btnClear.Width = availW - bw3 * 2 - 16;
btnPlot.Location = new Point(x2, 862);
btnPlot.Width = bw2;
btnSave.Location = new Point(x2 + bw2 + 8, 862);
btnSave.Width = availW - bw2 - 8;
lblStatus.Location = new Point(x2, 914);
lblStatus.Width = availW;
SetLabelX("端口:", rightLabelX);
SetLabelX("密码:", rightLabelX);
SetLabelX("条数:", rightLabelX);
SetLabelX("跳过前 N 行", inputX + inputW + 8);
SetLabelX("从最新数据往回数 N 条作为查询起点", inputX);
}
catch { }
}
Label FindLabel(string text)
{
foreach (Control c in left.Controls)
{
if (c is Label && c.Text == text) return (Label)c;
}
return null;
}
void SetLabelX(string text, int x)
{
Label l = FindLabel(text);
if (l != null) l.Location = new Point(x, l.Location.Y);
}
void TogglePin()
{
pinned = !pinned;
if (pinned)
{
btnPin.BackColor = ThemeBlue;
btnPin.ForeColor = Color.White;
btnPin.Tag = "已钉住";
}
else
{
btnPin.BackColor = Color.White;
btnPin.ForeColor = Color.FromArgb(60, 70, 80);
btnPin.Tag = "钉住";
}
}
void SetupSplitterDrag(Panel splitterP)
{
bool dragging = false;
int startX = 0;
int startWidth = 0;
splitterP.MouseDown += delegate(object s, MouseEventArgs e)
{
if (pinned) return;
dragging = true;
startX = e.X;
startWidth = left.Width;
splitterP.Capture = true;
};
splitterP.MouseMove += delegate(object s, MouseEventArgs e)
{
if (!dragging) return;
int newWidth = startWidth + (e.X - startX);
if (newWidth < 380) newWidth = 380;
if (newWidth > 800) newWidth = 800;
left.Width = newWidth;
LayoutPanels(left, (Panel)right, splitterP);
};
splitterP.MouseUp += delegate(object s, MouseEventArgs e)
{
dragging = false;
splitterP.Capture = false;
};
}
void FillConnFields()
{
txtHost.Text = config.Host;
txtPort.Text = config.Port;
txtUser.Text = config.User;
txtPassword.Text = config.Password;
cmbDatabase.Items.Clear();
if (!string.IsNullOrEmpty(config.Database))
{
cmbDatabase.Items.Add(config.Database);
cmbDatabase.SelectedIndex = 0;
}
}
void SaveConfigFromFields()
{
config.Host = txtHost.Text.Trim();
config.Port = txtPort.Text.Trim();
config.User = txtUser.Text.Trim();
config.Password = txtPassword.Text;
string db = cmbDatabase.SelectedItem as string;
if (string.IsNullOrEmpty(db)) db = cmbDatabase.Text.Trim();
config.Database = db == null ? "" : db;
try { config.Save(configPath); }
catch { }
}
void SetStatus(string text, bool error)
{
lblStatus.Text = text;
lblStatus.ForeColor = error ? Color.FromArgb(192, 57, 43) : Color.FromArgb(39, 120, 60);
}
MySqlConnection OpenConn()
{
SaveConfigFromFields();
return OpenConn(config.Database);
}
MySqlConnection OpenConn(string db)
{
try
{
MySqlConnectionStringBuilder b = new MySqlConnectionStringBuilder();
b.Server = config.Host;
b.Port = uint.Parse(config.Port);
b.UserID = config.User;
b.Password = config.Password;
if (!string.IsNullOrEmpty(db)) b.Database = db;
b.CharacterSet = config.Charset;
b.ConnectionTimeout = 10;
MySqlConnection conn = new MySqlConnection(b.ConnectionString);
conn.Open();
return conn;
}
catch (Exception ex)
{
SetStatus("连接失败: " + ex.Message, true);
MessageBox.Show(this, "无法连接数据库:\n" + ex.Message, "连接失败",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
void ConnectAndLoad()
{
SaveConfigFromFields();
MySqlConnection conn = OpenConn("");
if (conn == null) return;
try
{
List<string> dbs = new List<string>();
using (MySqlCommand cmd = new MySqlCommand("SHOW DATABASES", conn))
using (IDataReader r = cmd.ExecuteReader())
{
while (r.Read()) dbs.Add(r.GetString(0));
}
string prevDb = config.Database;
loadingDbs = true;
cmbDatabase.Items.Clear();
foreach (string d in dbs) cmbDatabase.Items.Add(d);
int sel = dbs.IndexOf(prevDb);
if (sel >= 0) cmbDatabase.SelectedIndex = sel;
else if (dbs.Count > 0) cmbDatabase.SelectedIndex = 0;
loadingDbs = false;
config.Database = cmbDatabase.SelectedItem as string;
if (string.IsNullOrEmpty(config.Database))
{
SetStatus("连接成功,但没有可用数据库", true);
return;
}
try { config.Save(configPath); } catch { }
}
catch (Exception ex)
{
SetStatus("读取数据库列表失败: " + ex.Message, true);
return;
}
finally
{
conn.Close();
}
MySqlConnection conn2 = OpenConn(config.Database);
if (conn2 == null) return;
try { LoadTables(conn2); }
finally { conn2.Close(); }
}
void DatabaseSelected()
{
if (loadingDbs) return;
string db = cmbDatabase.SelectedItem as string;
if (string.IsNullOrEmpty(db)) db = cmbDatabase.Text.Trim();
if (string.IsNullOrEmpty(db)) return;
SaveConfigFromFields();
config.Database = db;
try { config.Save(configPath); } catch { }
MySqlConnection conn = OpenConn(db);
if (conn == null) return;
try { LoadTables(conn); }
finally { conn.Close(); }
}
void LoadTables(MySqlConnection conn)
{
try
{
List<string> tables = new List<string>();
using (MySqlCommand cmd = new MySqlCommand("SHOW TABLES", conn))
using (IDataReader r = cmd.ExecuteReader())
{
while (r.Read()) tables.Add(r.GetString(0));
}
cmbTable.Items.Clear();
foreach (string t in tables) cmbTable.Items.Add(t);
if (tables.Count > 0) cmbTable.SelectedIndex = 0;
SetStatus("已连接,共 " + tables.Count + " 张表", false);
}
catch (Exception ex)
{
SetStatus("读取表列表失败: " + ex.Message, true);
}
}
void TableSelected()
{
string table = cmbTable.SelectedItem as string;
if (string.IsNullOrEmpty(table)) return;
MySqlConnection conn = OpenConn();
if (conn == null) return;
try
{
List<string> cols = new List<string>();
using (MySqlCommand cmd = new MySqlCommand(
"SELECT * FROM `" + EscapeIdent(table) + "` LIMIT 0", conn))
using (IDataReader r = cmd.ExecuteReader())
{
for (int i = 0; i < r.FieldCount; i++)
cols.Add(r.GetName(i));
}
allColumns = cols;
cmbX.Items.Clear();
foreach (string c in cols) cmbX.Items.Add(c);
if (cols.Count > 0) cmbX.SelectedIndex = 0;
lbY.Items.Clear();
foreach (string c in cols) lbY.Items.Add(c);
DataGridViewComboBoxColumn colCol = (DataGridViewComboBoxColumn)dgvFilter.Columns[0];
colCol.Items.Clear();
foreach (string c in cols) colCol.Items.Add(c);
dgvFilter.Rows.Clear();
}
catch (Exception ex)
{
SetStatus("读取列失败: " + ex.Message, true);
}
finally
{
conn.Close();
}
}
static string EscapeIdent(string name)
{
return name.Replace("`", "``");
}
void AddRule()
{
if (allColumns.Count == 0)
{
MessageBox.Show(this, "请先连接并选择存储表", "提示");
return;
}
int idx = dgvFilter.Rows.Add();
dgvFilter.Rows[idx].Cells[0].Value = allColumns[0];
dgvFilter.Rows[idx].Cells[1].Value = "=";
dgvFilter.Rows[idx].Cells[3].Value = "AND";
}
void DelRule()
{
if (dgvFilter.SelectedRows.Count > 0)
dgvFilter.Rows.Remove(dgvFilter.SelectedRows[0]);
else if (dgvFilter.Rows.Count > 0)
dgvFilter.Rows.RemoveAt(dgvFilter.Rows.Count - 1);
}
string BuildFilterCondition(DataGridViewRow row)
{
string col = (row.Cells[0].Value == null ? "" : row.Cells[0].Value.ToString().Trim());
string op = (row.Cells[1].Value == null ? "" : row.Cells[1].Value.ToString().Trim());
string val = (row.Cells[2].Value == null ? "" : row.Cells[2].Value.ToString().Trim());
if (col.Length == 0 || op.Length == 0) return null;
string colId = "`" + EscapeIdent(col) + "`";
string esc = val.Replace("'", "''");
switch (op)
{
case "IS NULL": return colId + " IS NULL";
case "IS NOT NULL": return colId + " IS NOT NULL";
case "BETWEEN":
string[] parts = val.Split(',');
if (parts.Length != 2) return null;
return colId + " BETWEEN '" + parts[0].Trim().Replace("'", "''")
+ "' AND '" + parts[1].Trim().Replace("'", "''") + "'";
case "LIKE":
if (val.Length == 0) return null;
return colId + " LIKE '" + esc + "'";
default:
if (val.Length == 0) return null;
return colId + " " + op + " '" + esc + "'";
}
}
class FilterItem
{
public string Cond;
public bool IsOr;
}
List<FilterItem> CollectFilterItems()
{
List<FilterItem> items = new List<FilterItem>();
foreach (DataGridViewRow row in dgvFilter.Rows)
{
if (row.IsNewRow) continue;
string cond = BuildFilterCondition(row);
if (cond == null) continue;
FilterItem it = new FilterItem();
it.Cond = cond;
object lv = row.Cells[3].Value;
it.IsOr = (lv != null && lv.ToString().Trim().Equals("OR", StringComparison.OrdinalIgnoreCase));
items.Add(it);
}
return items;
}
string BuildWhere(List<FilterItem> items)
{
if (items.Count == 0) return "";
StringBuilder sb = new StringBuilder(" WHERE ");
for (int i = 0; i < items.Count; i++)
{
if (i > 0) sb.Append(items[i - 1].IsOr ? " OR " : " AND ");
sb.Append(items[i].Cond);
}
return sb.ToString();
}
string BuildOrderLimit(string xcol)
{
bool desc = cmbOrder.SelectedIndex == 1;
int limit = 0, offset = 0, tail = 0;
int.TryParse(txtLimit.Text.Trim(), out limit);
int.TryParse(txtOffset.Text.Trim(), out offset);
int.TryParse(txtTailOffset.Text.Trim(), out tail);
StringBuilder sb = new StringBuilder();
if (tail > 0)
{
sb.Append(" ORDER BY `").Append(EscapeIdent(xcol)).Append("` DESC");
if (limit > 0)
{
sb.Append(" LIMIT ").Append(limit);
sb.Append(" OFFSET ").Append(tail);
}
return sb.ToString();
}
sb.Append(" ORDER BY `").Append(EscapeIdent(xcol)).Append("` ");
sb.Append(desc ? "DESC" : "ASC");
if (limit > 0)
{
sb.Append(" LIMIT ").Append(limit);
if (offset > 0)
sb.Append(" OFFSET ").Append(offset);
}
return sb.ToString();
}
void Plot()
{
string table = cmbTable.SelectedItem as string;
string xcol = cmbX.SelectedItem as string;
List<string> ycols = new List<string>();
foreach (object o in lbY.SelectedItems) ycols.Add(o.ToString());
if (string.IsNullOrEmpty(table) || string.IsNullOrEmpty(xcol))
{
MessageBox.Show(this, "请先选择存储表和横坐标", "提示");
return;
}
if (ycols.Count == 0)
{
MessageBox.Show(this, "请至少选择一条 Y 轴曲线列", "提示");
return;
}
List<FilterItem> filters = CollectFilterItems();
btnPlot.Enabled = false;
Thread t = new Thread(delegate () { DoPlot(table, xcol, ycols, filters); });
t.IsBackground = true;
t.Start();
}
void DoPlot(string table, string xcol, List<string> ycols, List<FilterItem> filters)
{
MySqlConnection conn = null;
try
{
conn = OpenConn();
if (conn == null) return;
StringBuilder sb = new StringBuilder();
sb.Append("SELECT `").Append(EscapeIdent(xcol)).Append("`");
foreach (string y in ycols)
sb.Append(", `").Append(EscapeIdent(y)).Append("`");
sb.Append(" FROM `").Append(EscapeIdent(table)).Append("`");
sb.Append(BuildWhere(filters));
sb.Append(BuildOrderLimit(xcol));
List<object[]> rows = new List<object[]>();
using (MySqlCommand cmd = new MySqlCommand(sb.ToString(), conn))
using (IDataReader r = cmd.ExecuteReader())
{
while (r.Read())
{
object[] vals = new object[r.FieldCount];
r.GetValues(vals);
rows.Add(vals);
}
}
BeginInvoke(new Action(delegate ()
{
lastRows = rows;
lastXcol = xcol;
lastYcols = ycols;
lastTable = table;
int tailOfs = 0;
int.TryParse(txtTailOffset.Text.Trim(), out tailOfs);
if (cmbOrder.SelectedIndex == 1 || tailOfs > 0)
rows.Reverse();
RenderPlot(rows, xcol, ycols, table);
}));
}
catch (Exception ex)
{
BeginInvoke(new Action(delegate ()
{
SetStatus("查询失败: " + ex.Message, true);
MessageBox.Show(this, "查询失败:\n" + ex.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}));
}
finally
{
if (conn != null) conn.Close();
BeginInvoke(new Action(delegate () { btnPlot.Enabled = true; }));
}
}
void RenderPlot(List<object[]> rows, string xcol, List<string> ycols, string table)
{
CloseFloatTip();
chart.Series.Clear();
calcSeriesNames.Clear();
paletteCounter = 0;
lblChartTitle.Text = table + " 曲线图";
if (rows.Count == 0)
{
SetStatus("没有符合条件的数据", true);
return;
}
cmbCalcTarget.Items.Clear();
foreach (string y in ycols) cmbCalcTarget.Items.Add(y);
if (cmbCalcTarget.Items.Count > 0) cmbCalcTarget.SelectedIndex = 0;
XAxisInfo xa = AnalyzeXAxis(rows);
bool xNumeric = xa.Numeric;
bool xIsTime = xa.IsTime;
bool xSparseTime = xa.Sparse;
bool hasTime = xa.HasTime;
DateTime[] xTimes = xa.Times;
bool[] xTimeOk = xa.Ok;
foreach (string y in ycols)
{
Series s = new Series(y);
s.ChartType = SeriesChartType.Line;
s.BorderWidth = 2;
s.Color = NextColor();
for (int i = 0; i < rows.Count; i++)
{
object[] row = rows[i];
object xv = row[0];
object yv = row[1 + ycols.IndexOf(y)];
double yNum;
if (yv == null || yv == DBNull.Value || !TryToDouble(yv, out yNum))
continue;
if (xIsTime)
{
if (xSparseTime)
{
string xStr = xv == null ? "" : xv.ToString();
s.Points.AddXY(xStr, yNum);
}
else
{
if (!xTimeOk[i]) continue;
s.Points.AddXY(xTimes[i], yNum);
}
}
else if (xNumeric)
{
double xNum;
if (xv == null || xv == DBNull.Value || !TryToDouble(xv, out xNum))
continue;
s.Points.AddXY(xNum, yNum);
}
else
{
string xStr = xv == null ? "" : xv.ToString();
s.Points.AddXY(xStr, yNum);
}
}
chart.Series.Add(s);
}
ChartArea area = chart.ChartAreas["main"];
area.AxisX.Title = xcol;
area.AxisY.Title = "数值";
area.AxisX.LabelStyle.Angle = xNumeric ? 0 : -45;
if (xIsTime)
{
if (xSparseTime)
{
area.AxisX.LabelStyle.Format = "";
area.AxisX.LabelStyle.Angle = -45;
area.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount;
SetStatus("绘制完成: 表 " + table + ", X 轴 " + xcol + " (批次时间轴)", false);
}
else
{
area.AxisX.LabelStyle.Format = hasTime ? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd";
area.AxisX.IntervalAutoMode = IntervalAutoMode.VariableCount;
SetStatus("绘制完成: 表 " + table + ", X 轴 " + xcol + ", 曲线 " + ycols.Count + " 条", false);
}
}
else
{
area.AxisX.LabelStyle.Format = "";
SetStatus("绘制完成: 表 " + table + ", X 轴 " + xcol + ", 曲线 " + ycols.Count + " 条", false);
}
RenderLegend();
}
class XAxisInfo
{
public bool Numeric;
public bool IsTime;
public bool Sparse;
public bool HasTime;
public DateTime[] Times;
public bool[] Ok;
}
XAxisInfo AnalyzeXAxis(List<object[]> rows)
{
XAxisInfo info = new XAxisInfo();
int n = rows.Count;
info.Times = new DateTime[n];
info.Ok = new bool[n];
int mode = cmbXType.SelectedIndex;
if (mode < 0) mode = 0;
if (mode == 2)
{
info.Numeric = true;
return info;
}
if (mode == 3)
{
info.Numeric = false;
return info;
}
bool forceTime = (mode == 1);
bool xNumeric = true;
int timeCount = 0, badCount = 0;
for (int i = 0; i < n; i++)
{
object xv = rows[i][0];
if (xv == null || xv == DBNull.Value) continue;
if (xv is DateTime)
{
timeCount++;
info.Times[i] = (DateTime)xv;
info.Ok[i] = true;
if (((DateTime)xv).TimeOfDay != TimeSpan.Zero) info.HasTime = true;
}
else
{
string s = xv as string;
if (s != null)
{
DateTime dt;
if (TryParseTime(s, out dt))
{
timeCount++;
info.Times[i] = dt;
info.Ok[i] = true;
if (dt.TimeOfDay != TimeSpan.Zero) info.HasTime = true;
continue;
}
badCount++;
}
else if (!IsNumeric(xv))
{
badCount++;
}
if (!forceTime && !IsNumeric(xv)) xNumeric = false;
}
}
if (timeCount > 0)
{
if (!forceTime && badCount > 0 && (double)timeCount / (timeCount + badCount) < 0.8)
{
info.Numeric = false;
return info;
}
info.IsTime = true;
info.Numeric = true;
info.Sparse = IsTimeSparse(info.Times, info.Ok, n);
return info;
}
info.Numeric = xNumeric;
return info;
}
bool IsTimeSparse(DateTime[] times, bool[] ok, int count)
{
List<double> gaps = new List<double>();
bool hasPrev = false;
DateTime prev = DateTime.MinValue;
for (int i = 0; i < count; i++)
{
if (!ok[i]) continue;
if (hasPrev) gaps.Add((times[i] - prev).TotalSeconds);
prev = times[i];
hasPrev = true;
}
if (gaps.Count < 5) return false;
double[] sorted = new double[gaps.Count];
gaps.CopyTo(sorted);
Array.Sort(sorted);
double median = sorted[sorted.Length / 2];
double maxGap = sorted[sorted.Length - 1];
if (median <= 0) return false;
return maxGap > median * 30 && maxGap > 3600;
}
void RenderLegend()
{ try
{
legendFlow.Controls.Clear();
foreach (Series s in chart.Series)
{
Panel item = new Panel();
item.AutoSize = true;
item.BackColor = Color.Transparent;
item.Margin = new Padding(0, 0, 12, 0);
Label sw = new Label();
sw.AutoSize = false;
sw.Width = 14;
sw.Height = 14;
sw.BackColor = s.Color;
sw.Location = new Point(0, 3);
Label nm = new Label();
nm.Text = s.Name;
nm.AutoSize = true;
nm.Font = new Font("Microsoft YaHei UI", 9F);
nm.ForeColor = Color.FromArgb(60, 70, 80);
nm.Location = new Point(18, 1);
item.Controls.Add(sw);
item.Controls.Add(nm);
legendFlow.Controls.Add(item);
}
int lines = 1;
float totalW = 0;
float availW = Math.Max(200, right.ClientSize.Width - 30);
foreach (Control c in legendFlow.Controls)
{
Label nm = (Label)c.Controls[1];
totalW += 18 + nm.PreferredSize.Width + 12;
}
if (totalW > availW && chart.Series.Count > 0)
lines = Math.Max(1, (int)Math.Ceiling(totalW / availW));
legendBar.Height = lines * 26 + 8;
legendBar.Visible = chart.Series.Count > 0 && !legendHiddenByUser;
}
catch { }
}
Color NextColor()
{
Color[] palette = {
Color.RoyalBlue, Color.OrangeRed, Color.ForestGreen, Color.Purple,
Color.DarkOrange, Color.Teal, Color.Crimson, Color.DarkGreen,
Color.SteelBlue, Color.IndianRed, Color.Olive, Color.MediumVioletRed,
Color.SlateBlue, Color.SeaGreen, Color.Chocolate, Color.CadetBlue
};
Color c = palette[paletteCounter % palette.Length];
paletteCounter++;
return c;
}
public void RunDrawTest()
{
try
{
StringBuilder log = new StringBuilder();
MySqlConnection conn = OpenConn();
if (conn == null) return;
string table = "";
using (MySqlCommand cmd = new MySqlCommand("SHOW TABLES", conn))
using (IDataReader r = cmd.ExecuteReader())
{
List<string> tables = new List<string>();
while (r.Read()) tables.Add(r.GetString(0));
if (tables.Count == 0) throw new Exception("数据库中没有表");
table = tables[0];
}
List<string> cols = new List<string>();
using (MySqlCommand cmd = new MySqlCommand("SELECT * FROM `" + EscapeIdent(table) + "` LIMIT 0", conn))
using (IDataReader r = cmd.ExecuteReader())
{
for (int i = 0; i < r.FieldCount; i++) cols.Add(r.GetName(i));
}
string xcol = cols.IndexOf("Time") >= 0 ? "Time" : (cols.Count > 0 ? cols[0] : "");
List<string> ycols = new List<string>();
if (cols.IndexOf("DataValue") >= 0) ycols.Add("DataValue");
if (ycols.Count == 0 && cols.Count > 1) ycols.Add(cols[1]);
if (xcol.Length == 0 || ycols.Count == 0) throw new Exception("无法确定横/纵坐标列");
StringBuilder sb = new StringBuilder();
sb.Append("SELECT `").Append(EscapeIdent(xcol)).Append("`");
foreach (string y in ycols)
sb.Append(", `").Append(EscapeIdent(y)).Append("`");
sb.Append(" FROM `").Append(EscapeIdent(table)).Append("`");
sb.Append(" ORDER BY `").Append(EscapeIdent(xcol)).Append("` ASC");
sb.Append(" LIMIT 300");
List<object[]> rows = new List<object[]>();
using (MySqlCommand cmd = new MySqlCommand(sb.ToString(), conn))
using (IDataReader r = cmd.ExecuteReader())
{
while (r.Read())
{
object[] vals = new object[r.FieldCount];
r.GetValues(vals);
rows.Add(vals);
}
}
conn.Close();
log.AppendLine("查询行数: " + rows.Count);
lastRows = rows;
lastXcol = xcol;
lastYcols = ycols;
lastTable = table;
RenderPlot(rows, xcol, ycols, table);
Application.DoEvents();
Thread.Sleep(500);
lblChartTitle.Text = table + " 曲线图 (drawtest)";
chart.SaveImage(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "drawtest.png"), ChartImageFormat.Png);
log.AppendLine("图表已保存 drawtest.png, 系列数=" + chart.Series.Count + ", 图例启用=" + chart.Legends["legend"].Enabled);
log.AppendLine("DRAWTEST PASS");
File.WriteAllText(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "selftest_result.txt"), log.ToString(), new UTF8Encoding(false));
}
catch (Exception ex)
{
try
{
string outFile = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "selftest_result.txt");
File.WriteAllText(outFile, "DRAWTEST FAIL: " + ex.Message + "\n" + ex.StackTrace, new UTF8Encoding(false));
}
catch { }
}
Application.Exit();
}
void UpdateCalcParamUI()
{
string sel = cmbCalc.SelectedItem as string;
if (sel == "平滑曲线 (滑动平均)")
{
lblCalcParam.Text = "窗口:";
txtCalcParam.Enabled = true;
txtCalcParam.Text = "5";
}
else if (sel == "多项式拟合 (Polyfit) (AI)")
{
lblCalcParam.Text = "阶数:";
txtCalcParam.Enabled = true;
txtCalcParam.Text = "3";
}
else if (sel == "梯度下降 (AI)")
{
lblCalcParam.Text = "迭代:";
txtCalcParam.Enabled = true;
txtCalcParam.Text = "200";
}
else if (sel == "贝叶斯回归 (AI)")
{
lblCalcParam.Text = "α:";
txtCalcParam.Enabled = true;
txtCalcParam.Text = "1";
}
else if (sel == "线性变换 (a*y+b) (AI)" || sel == "二次变换 (a*y²+b) (AI)")
{
lblCalcParam.Text = "a,b:";
txtCalcParam.Enabled = true;
txtCalcParam.Text = "1,0";
}
else
{
lblCalcParam.Text = "无参数";
txtCalcParam.Enabled = false;
}
cmbCalcTarget.Enabled = (sel == "线性变换 (a*y+b) (AI)" || sel == "二次变换 (a*y²+b) (AI)");
}
void ToggleCross()
{
showCross = !showCross;
btnCross.Text = showCross ? "隐藏十字线" : "显示十字线";
btnCross.BackColor = showCross ? Color.FromArgb(52, 73, 94) : Color.FromArgb(127, 140, 141);
if (!showCross) crossPos = new PointF(-1, -1);
chart.Invalidate();
}
void ToggleLegend()
{
legendHiddenByUser = !legendHiddenByUser;
legendBar.Visible = chart.Series.Count > 0 && !legendHiddenByUser;
btnLegend.Text = legendBar.Visible ? "隐藏图例" : "显示图例";
btnLegend.BackColor = legendBar.Visible ? Color.FromArgb(52, 73, 94) : Color.FromArgb(127, 140, 141);
}
void OpenChartSettings()
{
ChartSettingsForm f = new ChartSettingsForm(chart, lblChartTitle, this);
f.ShowDialog(this);
}
void ApplyCalc()
{
if (lastRows == null || lastRows.Count == 0)
{
MessageBox.Show(this, "请先绘制曲线", "提示");
return;
}
int param = 5;
int.TryParse(txtCalcParam.Text.Trim(), out param);
string calcName = cmbCalc.SelectedItem as string;
bool singleCurve = (calcName == "线性变换 (a*y+b) (AI)" || calcName == "二次变换 (a*y²+b) (AI)");
double a = 1, b = 0;
string target = "";
if (singleCurve)
{
target = cmbCalcTarget.SelectedItem as string;
if (string.IsNullOrEmpty(target))
{
MessageBox.Show(this, "请先在\"曲线\"下拉框选择目标曲线", "提示");
return;
}
string[] ab = txtCalcParam.Text.Replace(",", ",").Split(',');
if (ab.Length >= 2)
{
double.TryParse(ab[0].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out a);
double.TryParse(ab[1].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out b);
}
}
if (calcName == "平滑曲线 (滑动平均)")
{
if (param < 3) param = 3;
if (param > 101) param = 101;
}
else if (calcName == "多项式拟合 (Polyfit) (AI)")
{
if (param < 1) param = 1;
if (param > 10) param = 10;
}
else if (calcName == "梯度下降 (AI)")
{
if (param < 1) param = 1;
if (param > 5000) param = 5000;
}
else if (calcName == "贝叶斯回归 (AI)")
{
if (param < 1) param = 1;
if (param > 1000) param = 1000;
}
else
{
param = 1;
}
XAxisInfo xa = AnalyzeXAxis(lastRows);
bool xNumeric = xa.Numeric;
bool xSparseTime = xa.Sparse;
foreach (string y in lastYcols)
{
if (singleCurve && y != target) continue;
double[] xs = new double[lastRows.Count];
double[] ys = new double[lastRows.Count];
int[] srcIdx = new int[lastRows.Count];
int n = 0;
for (int i = 0; i < lastRows.Count; i++)
{
object[] row = lastRows[i];
double xv = n;
double yv;
if (!TryToDouble(row[1 + lastYcols.IndexOf(y)], out yv)) continue;
if (xNumeric && !TryToDouble(row[0], out xv))
{
string sx = row[0] as string;
if (sx == null) continue;
DateTime dt;
if (!TryParseTime(sx, out dt)) continue;
xv = dt.ToOADate();
}
xs[n] = xv;
ys[n] = yv;
srcIdx[n] = i;
n++;
}
double[] nx = new double[n];
double[] ny = new double[n];
Array.Copy(xs, nx, n);
Array.Copy(ys, ny, n);
if (n == 0) continue;
string seriesName = "";
double[] outY = ComputeCalc(calcName, nx, ny, param, a, b, out seriesName);
if (outY == null) continue;
Series s = new Series(seriesName + "(" + y + ")");
s.ChartType = SeriesChartType.Line;
s.BorderWidth = 2;
s.Color = NextColor();
for (int i = 0; i < n; i++)
{
if (double.IsNaN(outY[i])) continue;
if (xSparseTime || !xNumeric)
s.Points.AddXY(lastRows[srcIdx[i]][0] == null ? "" : lastRows[srcIdx[i]][0].ToString(), outY[i]);
else s.Points.AddXY(nx[i], outY[i]);
}
chart.Series.Add(s);
calcSeriesNames.Add(s.Name);
}
if (calcSeriesNames.Count > 0)
SetStatus("已生成运算曲线: " + calcName, false);
}
double[] ComputeCalc(string calcName, double[] xs, double[] ys, int param, double a, double b, out string seriesName)
{
int n = ys.Length;
double[] outY = new double[n];
for (int i = 0; i < n; i++) outY[i] = double.NaN;
if (calcName == "对数 (ln)")
{
seriesName = "ln";
for (int i = 0; i < n; i++)
if (ys[i] > 0) outY[i] = Math.Log(ys[i]);
}
else if (calcName == "常用对数 (log10)")
{
seriesName = "log10";
for (int i = 0; i < n; i++)
if (ys[i] > 0) outY[i] = Math.Log10(ys[i]);
}
else if (calcName == "指数 (exp)")
{
seriesName = "exp";
for (int i = 0; i < n; i++)
outY[i] = Math.Exp(ys[i]);
}
else if (calcName == "开方 (sqrt)")
{
seriesName = "sqrt";
for (int i = 0; i < n; i++)
if (ys[i] >= 0) outY[i] = Math.Sqrt(ys[i]);
}
else if (calcName == "微分 (差分)")
{
seriesName = "dy/dx";
for (int i = 1; i < n; i++)
{
double dx = xs[i] - xs[i - 1];
if (dx != 0) outY[i - 1] = (ys[i] - ys[i - 1]) / dx;
}
}
else if (calcName == "积分 (累积)")
{
seriesName = "∫ydx";
double sum = 0;
for (int i = 1; i < n; i++)
{
double dx = xs[i] - xs[i - 1];
sum += (ys[i] + ys[i - 1]) / 2.0 * dx;
outY[i] = sum;
}
outY[0] = 0;
}
else if (calcName == "线性回归 (AI)")
{
seriesName = "reg";
double sx = 0, sy = 0, sxx = 0, sxy = 0;
for (int i = 0; i < n; i++)
{
sx += xs[i]; sy += ys[i]; sxx += xs[i] * xs[i]; sxy += xs[i] * ys[i];
}
double denom = n * sxx - sx * sx;
double coefA = 0, coefB = 0;
if (Math.Abs(denom) > 1e-12)
{
coefA = (n * sxy - sx * sy) / denom;
coefB = (sy - coefA * sx) / n;
}
for (int i = 0; i < n; i++)
outY[i] = coefA * xs[i] + coefB;
}
else if (calcName == "梯度下降 (AI)")
{
seriesName = "gd";
int iters = Math.Max(1, param);
double mx = 0, my = 0;
for (int i = 0; i < n; i++) { mx += xs[i]; my += ys[i]; }
mx /= n; my /= n;
double xstd = 0, ystd = 0;
for (int i = 0; i < n; i++)
{
xstd += (xs[i] - mx) * (xs[i] - mx);
ystd += (ys[i] - my) * (ys[i] - my);
}
xstd = Math.Sqrt(xstd / n);
ystd = Math.Sqrt(ystd / n);
if (xstd < 1e-12) xstd = 1e-12;
if (ystd < 1e-12) ystd = 1e-12;
double w = 0, bias = 0;
double lr = 0.1;
for (int it = 0; it < iters; it++)
{
double gw = 0, gb = 0;
for (int i = 0; i < n; i++)
{
double xn = (xs[i] - mx) / xstd;
double yn = (ys[i] - my) / ystd;
double pred = w * xn + bias;
gw += (pred - yn) * xn;
gb += (pred - yn);
}
gw = 2.0 * gw / n;
gb = 2.0 * gb / n;
w -= lr * gw;
bias -= lr * gb;
}
for (int i = 0; i < n; i++)
{
double xn = (xs[i] - mx) / xstd;
outY[i] = (w * xn + bias) * ystd + my;
}
}
else if (calcName == "贝叶斯回归 (AI)")
{
seriesName = "bayes";
double alpha = Math.Max(1e-6, param);
double a00 = alpha + n, a01 = 0, a11 = alpha, b0 = 0, b1 = 0;
for (int i = 0; i < n; i++)
{
a01 += xs[i];
a11 += xs[i] * xs[i];
b0 += ys[i];
b1 += xs[i] * ys[i];
}
double det = a00 * a11 - a01 * a01;
double w0 = 0, w1 = 0;
if (Math.Abs(det) > 1e-15)
{
w0 = (b0 * a11 - a01 * b1) / det;
w1 = (a00 * b1 - a01 * b0) / det;
}
for (int i = 0; i < n; i++)
outY[i] = w0 + w1 * xs[i];
}
else if (calcName == "多项式拟合 (Polyfit) (AI)")
{
int degree = param;
if (degree < 1) degree = 1;
if (degree > 10) degree = 10;
seriesName = "poly" + degree;
double[] coeff = PolyFit(xs, ys, n, degree);
for (int i = 0; i < n; i++)
{
double v = 0;
for (int k = 0; k <= degree; k++)
v += coeff[k] * Math.Pow(xs[i], k);
outY[i] = v;
}
}
else if (calcName == "平滑曲线 (滑动平均)")
{
seriesName = "smooth";
for (int i = 0; i < n; i++)
{
int half = (param - 1) / 2;
int start = Math.Max(0, i - half);
int end = Math.Min(n - 1, i + half);
double sum = 0;
int cnt = 0;
for (int k = start; k <= end; k++)
{
if (!double.IsNaN(ys[k])) { sum += ys[k]; cnt++; }
}
if (cnt > 0) outY[i] = sum / cnt;
}
}
else if (calcName == "线性变换 (a*y+b) (AI)")
{
seriesName = "a*y+b";
for (int i = 0; i < n; i++)
if (!double.IsNaN(ys[i])) outY[i] = a * ys[i] + b;
}
else if (calcName == "二次变换 (a*y²+b) (AI)")
{
seriesName = "a*y²+b";
for (int i = 0; i < n; i++)
if (!double.IsNaN(ys[i])) outY[i] = a * ys[i] * ys[i] + b;
}
else
{
seriesName = "";
return null;
}
return outY;
}
double[] PolyFit(double[] xs, double[] ys, int n, int degree)
{
int m = degree + 1;
double[,] a = new double[m, m];
double[] bvec = new double[m];
for (int i = 0; i < n; i++)
{
double xp = 1;
for (int j = 0; j < m; j++)
{
for (int k = 0; k < m; k++)
a[j, k] += xp * Math.Pow(xs[i], k);
bvec[j] += xp * ys[i];
xp *= xs[i];
}
}
return GaussSolve(a, bvec, m);
}
double[] GaussSolve(double[,] a, double[] b, int m)
{
for (int col = 0; col < m; col++)
{
int pivot = col;
double maxv = Math.Abs(a[col, col]);
for (int r = col + 1; r < m; r++)
{
if (Math.Abs(a[r, col]) > maxv)
{
maxv = Math.Abs(a[r, col]);
pivot = r;
}
}
if (maxv < 1e-15)
{
double[] zero = new double[m];
return zero;
}
if (pivot != col)
{
for (int k = col; k < m; k++)
{
double t = a[col, k]; a[col, k] = a[pivot, k]; a[pivot, k] = t;
}
double tb = b[col]; b[col] = b[pivot]; b[pivot] = tb;
}
double diag = a[col, col];
for (int k = col; k < m; k++) a[col, k] /= diag;
b[col] /= diag;
for (int r = 0; r < m; r++)
{
if (r == col) continue;
double f = a[r, col];
if (Math.Abs(f) < 1e-15) continue;
for (int k = col; k < m; k++) a[r, k] -= f * a[col, k];
b[r] -= f * b[col];
}
}
return b;
}
void ClearCalcSeries()
{
foreach (string name in calcSeriesNames)
{
for (int i = chart.Series.Count - 1; i >= 0; i--)
{
if (chart.Series[i].Name == name)
{
chart.Series.RemoveAt(i);
break;
}
}
}
calcSeriesNames.Clear();
SetStatus("已清除运算曲线", false);
}
static readonly string[] TimeFormats = {
"yyyy-MM-dd HH:mm:ss.fff", "yyyy/MM/dd HH:mm:ss.fff",
"yyyy-MM-dd HH:mm:ss", "yyyy/MM/dd HH:mm:ss",
"yyyy-M-d H:m:s", "yyyy/M/d H:m:s",
"yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm",
"yy-MM-dd HH:mm:ss", "yy/MM/dd HH:mm:ss",
"yy-M-d H:m:s", "yy/M/d H:m:s",
"yy-MM-dd HH:mm", "yy/MM/dd HH:mm",
"yyyy-MM-dd", "yyyy/MM/dd", "yy-MM-dd", "yy/MM/dd",
"yyyyMMddHHmmss", "yyMMddHHmmss", "yyyyMMddHHmm", "yyMMddHHmm",
"yyyyMMdd", "yyMMdd"
};
static bool TryParseTime(string s, out DateTime dt)
{
dt = DateTime.MinValue;
string t = s.Trim();
if (t.Length < 6) return false;
return DateTime.TryParseExact(t, TimeFormats,
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out dt);
}
static bool IsNumeric(object o)
{
double d;
return TryToDouble(o, out d);
}
static bool TryToDouble(object o, out double val)
{
val = 0;
if (o is double) { val = (double)o; return true; }
if (o is float) { val = (float)o; return true; }
if (o is int) { val = (int)o; return true; }
if (o is long) { val = (long)o; return true; }
if (o is decimal) { val = (double)(decimal)o; return true; }
if (o is sbyte) { val = (sbyte)o; return true; }
if (o is byte) { val = (byte)o; return true; }
if (o is short) { val = (short)o; return true; }
if (o is ushort) { val = (ushort)o; return true; }
if (o is uint) { val = (uint)o; return true; }
if (o is ulong) { val = (ulong)o; return true; }
if (o is DateTime) { val = ((DateTime)o).ToOADate(); return true; }
if (o is TimeSpan) { val = ((TimeSpan)o).TotalSeconds; return true; }
return double.TryParse(o.ToString(), System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out val);
}
void SaveImage()
{
if (chart.Series.Count == 0)
{
MessageBox.Show(this, "请先绘制曲线", "提示");
return;
}
SaveFileDialog dlg = new SaveFileDialog();
dlg.Filter = "PNG 图片|*.png|JPEG 图片|*.jpg|BMP 图片|*.bmp";
dlg.FileName = "chart.png";
if (dlg.ShowDialog(this) == DialogResult.OK)
{
try
{
chart.SaveImage(dlg.FileName, ChartImageFormat.Png);
MessageBox.Show(this, "图片已保存到:\n" + dlg.FileName, "完成");
}
catch (Exception ex)
{
MessageBox.Show(this, "保存失败: " + ex.Message, "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
class ChartSettingsForm : Form
{
Chart chart;
Label lblTitleTarget;
MainForm owner;
TextBox txtTitle, txtXTitle, txtYTitle;
TextBox txtXMin, txtXMax, txtYMin, txtYMax;
TextBox txtXGrid, txtYGrid, txtMinorDiv;
CheckBox chkXLog, chkYLog, chkMinor, chkGridX, chkGridY;
DataGridView dgvSeries;
public ChartSettingsForm(Chart ch, Label lblTitle, MainForm ownerForm)
{
chart = ch;
lblTitleTarget = lblTitle;
owner = ownerForm;
Text = "图表设置";
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Microsoft YaHei UI", 9F);
ClientSize = new Size(560, 480);
TabControl tabs = new TabControl();
tabs.Location = new Point(10, 10);
tabs.Size = new Size(540, 410);
Controls.Add(tabs);
TabPage pgGeneral = new TabPage("常规");
tabs.TabPages.Add(pgGeneral);
TabPage pgAxis = new TabPage("坐标轴");
tabs.TabPages.Add(pgAxis);
TabPage pgSeries = new TabPage("曲线样式");
tabs.TabPages.Add(pgSeries);
ChartArea area = chart.ChartAreas["main"];
AddRow(pgGeneral, "标题:", 16, lblTitle.Text, out txtTitle);
AddRow(pgGeneral, "横轴名称:", 52, area.AxisX.Title, out txtXTitle);
AddRow(pgGeneral, "纵轴名称:", 88, area.AxisY.Title, out txtYTitle);
AddLabelTo(pgGeneral, "图例位置:", 16, 128);
Label lLegendFixed = new Label();
lLegendFixed.Text = "固定于标题下方 (不可调)";
lLegendFixed.Location = new Point(110, 124);
lLegendFixed.AutoSize = true;
lLegendFixed.ForeColor = Color.FromArgb(140, 150, 160);
pgGeneral.Controls.Add(lLegendFixed);
AddLabelTo(pgGeneral, "横轴网格:", 16, 164);
chkGridX = new CheckBox();
chkGridX.Text = "显示";
chkGridX.Location = new Point(110, 160);
chkGridX.Checked = area.AxisX.MajorGrid.Enabled;
pgGeneral.Controls.Add(chkGridX);
AddLabelTo(pgGeneral, "纵轴网格:", 16, 196);
chkGridY = new CheckBox();
chkGridY.Text = "显示";
chkGridY.Location = new Point(110, 192);
chkGridY.Checked = area.AxisY.MajorGrid.Enabled;
pgGeneral.Controls.Add(chkGridY);
AddLabelTo(pgGeneral, "横轴网格间距:", 16, 232);
txtXGrid = AddSmallText(pgGeneral, 110, 228, 220);
txtXGrid.Text = area.AxisX.MajorGrid.Interval == 0 ? "" : area.AxisX.MajorGrid.Interval.ToString();
AddLabelTo(pgGeneral, "纵轴网格间距:", 16, 268);
txtYGrid = AddSmallText(pgGeneral, 110, 264, 220);
txtYGrid.Text = area.AxisY.MajorGrid.Interval == 0 ? "" : area.AxisY.MajorGrid.Interval.ToString();
AddLabelTo(pgGeneral, "次网格细分数:", 16, 304);
txtMinorDiv = AddSmallText(pgGeneral, 110, 300, 220);
double minorDiv = 4;
if (area.AxisX.MinorGrid.Enabled && area.AxisX.MajorGrid.Interval > 0)
minorDiv = area.AxisX.MajorGrid.Interval / area.AxisX.MinorGrid.Interval;
txtMinorDiv.Text = minorDiv.ToString("0.#");
chkMinor = new CheckBox();
chkMinor.Text = "加密网格线 (启用次网格线)";
chkMinor.Location = new Point(16, 334);
chkMinor.AutoSize = true;
chkMinor.Checked = area.AxisX.MinorGrid.Enabled;
pgGeneral.Controls.Add(chkMinor);
AddLabelTo(pgAxis, "横轴最小值 (留空=自动):", 16, 24);
txtXMin = AddSmallText(pgAxis, 190, 20, 150);
if (!double.IsNaN(area.AxisX.Minimum) && area.AxisX.Minimum != double.NegativeInfinity)
txtXMin.Text = area.AxisX.Minimum.ToString();
AddLabelTo(pgAxis, "横轴最大值 (留空=自动):", 16, 60);
txtXMax = AddSmallText(pgAxis, 190, 56, 150);
if (!double.IsNaN(area.AxisX.Maximum) && area.AxisX.Maximum != double.PositiveInfinity)
txtXMax.Text = area.AxisX.Maximum.ToString();
AddLabelTo(pgAxis, "纵轴最小值 (留空=自动):", 16, 96);
txtYMin = AddSmallText(pgAxis, 190, 92, 150);
if (!double.IsNaN(area.AxisY.Minimum) && area.AxisY.Minimum != double.NegativeInfinity)
txtYMin.Text = area.AxisY.Minimum.ToString();
AddLabelTo(pgAxis, "纵轴最大值 (留空=自动):", 16, 132);
txtYMax = AddSmallText(pgAxis, 190, 128, 150);
if (!double.IsNaN(area.AxisY.Maximum) && area.AxisY.Maximum != double.PositiveInfinity)
txtYMax.Text = area.AxisY.Maximum.ToString();
chkXLog = new CheckBox();
chkXLog.Text = "横轴使用对数刻度 (Log)";
chkXLog.Location = new Point(16, 176);
chkXLog.AutoSize = true;
chkXLog.Checked = area.AxisX.IsLogarithmic;
pgAxis.Controls.Add(chkXLog);
chkYLog = new CheckBox();
chkYLog.Text = "纵轴使用对数刻度 (Log)";
chkYLog.Location = new Point(16, 208);
chkYLog.AutoSize = true;
chkYLog.Checked = area.AxisY.IsLogarithmic;
pgAxis.Controls.Add(chkYLog);
Label lTip = new Label();
lTip.Text = "提示: 对数刻度要求数据均为正值";
lTip.Font = new Font("Microsoft YaHei UI", 8.5F);
lTip.ForeColor = Color.FromArgb(140, 150, 160);
lTip.Location = new Point(16, 240);
lTip.AutoSize = true;
pgAxis.Controls.Add(lTip);
dgvSeries = new DataGridView();
dgvSeries.Dock = DockStyle.Fill;
dgvSeries.Padding = new Padding(6);
dgvSeries.AllowUserToAddRows = false;
dgvSeries.AllowUserToDeleteRows = false;
dgvSeries.RowHeadersVisible = false;
dgvSeries.BackgroundColor = Color.White;
dgvSeries.BorderStyle = BorderStyle.FixedSingle;
dgvSeries.EnableHeadersVisualStyles = false;
dgvSeries.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(52, 73, 94);
dgvSeries.ColumnHeadersDefaultCellStyle.ForeColor = Color.White;
dgvSeries.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
dgvSeries.ColumnHeadersHeight = 26;
dgvSeries.DefaultCellStyle.Font = new Font("Microsoft YaHei UI", 8.5F);
dgvSeries.GridColor = Color.FromArgb(221, 228, 236);
dgvSeries.RowTemplate.Height = 26;
pgSeries.Controls.Add(dgvSeries);
DataGridViewTextBoxColumn cName = new DataGridViewTextBoxColumn();
cName.HeaderText = "曲线名称";
cName.Width = 130;
cName.ReadOnly = true;
dgvSeries.Columns.Add(cName);
DataGridViewComboBoxColumn cColor = new DataGridViewComboBoxColumn();
cColor.HeaderText = "颜色";
cColor.Width = 90;
cColor.FlatStyle = FlatStyle.Flat;
foreach (KnownColor kc in Enum.GetValues(typeof(KnownColor)))
{
Color cc = Color.FromKnownColor(kc);
if (cc.IsSystemColor || cc.A != 255) continue;
cColor.Items.Add(kc.ToString());
}
dgvSeries.Columns.Add(cColor);
DataGridViewComboBoxColumn cWidth = new DataGridViewComboBoxColumn();
cWidth.HeaderText = "线宽";
cWidth.Width = 60;
cWidth.FlatStyle = FlatStyle.Flat;
for (int i = 1; i <= 8; i++) cWidth.Items.Add(i.ToString());
dgvSeries.Columns.Add(cWidth);
DataGridViewComboBoxColumn cStyle = new DataGridViewComboBoxColumn();
cStyle.HeaderText = "线型";
cStyle.Width = 80;
cStyle.FlatStyle = FlatStyle.Flat;
cStyle.Items.Add("实线");
cStyle.Items.Add("虚线");
cStyle.Items.Add("点线");
cStyle.Items.Add("点划线");
dgvSeries.Columns.Add(cStyle);
DataGridViewComboBoxColumn cMarker = new DataGridViewComboBoxColumn();
cMarker.HeaderText = "标记";
cMarker.Width = 80;
cMarker.FlatStyle = FlatStyle.Flat;
cMarker.Items.Add("无");
cMarker.Items.Add("圆点");
cMarker.Items.Add("方块");
cMarker.Items.Add("三角");
cMarker.Items.Add("菱形");
dgvSeries.Columns.Add(cMarker);
RefreshSeriesGrid();
Button ok = new Button();
ok.Text = "应用";
ok.Location = new Point(250, 432);
ok.Size = new Size(120, 34);
ok.FlatStyle = FlatStyle.Flat;
ok.FlatAppearance.BorderSize = 0;
ok.BackColor = Color.FromArgb(41, 101, 173);
ok.ForeColor = Color.White;
ok.Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold);
ok.Click += delegate { ApplyAndClose(); };
Controls.Add(ok);
Button cancel = new Button();
cancel.Text = "取消";
cancel.Location = new Point(385, 432);
cancel.Size = new Size(120, 34);
cancel.DialogResult = DialogResult.Cancel;
Controls.Add(cancel);
AcceptButton = ok;
CancelButton = cancel;
}
void RefreshSeriesGrid()
{
dgvSeries.Rows.Clear();
foreach (Series s in chart.Series)
{
int idx = dgvSeries.Rows.Add();
dgvSeries.Rows[idx].Cells[0].Value = s.Name;
dgvSeries.Rows[idx].Cells[1].Value = s.Color.Name;
dgvSeries.Rows[idx].Cells[2].Value = s.BorderWidth.ToString();
dgvSeries.Rows[idx].Cells[3].Value = StyleToCn(s.BorderDashStyle);
dgvSeries.Rows[idx].Cells[4].Value = MarkerToCn(s.MarkerStyle);
}
}
static string StyleToCn(ChartDashStyle st)
{
switch (st)
{
case ChartDashStyle.Dash: return "虚线";
case ChartDashStyle.Dot: return "点线";
case ChartDashStyle.DashDot: return "点划线";
default: return "实线";
}
}
static string MarkerToCn(MarkerStyle m)
{
switch (m)
{
case MarkerStyle.Circle: return "圆点";
case MarkerStyle.Square: return "方块";
case MarkerStyle.Triangle: return "三角";
case MarkerStyle.Diamond: return "菱形";
default: return "无";
}
}
static ChartDashStyle CnToStyle(string s)
{
if (s == "虚线") return ChartDashStyle.Dash;
if (s == "点线") return ChartDashStyle.Dot;
if (s == "点划线") return ChartDashStyle.DashDot;
return ChartDashStyle.Solid;
}
static MarkerStyle CnToMarker(string s)
{
if (s == "圆点") return MarkerStyle.Circle;
if (s == "方块") return MarkerStyle.Square;
if (s == "三角") return MarkerStyle.Triangle;
if (s == "菱形") return MarkerStyle.Diamond;
return MarkerStyle.None;
}
void AddLabelTo(Control parent, string text, int x, int y)
{
Label l = new Label();
l.Text = text;
l.Location = new Point(x, y + 4);
l.AutoSize = true;
l.ForeColor = Color.FromArgb(60, 70, 80);
parent.Controls.Add(l);
}
void AddRow(Control parent, string label, int y, string value, out TextBox box)
{
AddLabelTo(parent, label, 16, y);
box = new TextBox();
box.Location = new Point(110, y);
box.Width = 220;
box.BorderStyle = BorderStyle.FixedSingle;
box.Text = value;
parent.Controls.Add(box);
}
TextBox AddSmallText(Control parent, int x, int y, int width)
{
TextBox t = new TextBox();
t.Location = new Point(x, y);
t.Width = width;
t.BorderStyle = BorderStyle.FixedSingle;
parent.Controls.Add(t);
return t;
}
void ApplyAndClose()
{
ChartArea area = chart.ChartAreas["main"];
lblTitleTarget.Text = txtTitle.Text.Trim();
area.AxisX.Title = txtXTitle.Text.Trim();
area.AxisY.Title = txtYTitle.Text.Trim();
double xGrid = 0, yGrid = 0;
if (double.TryParse(txtXGrid.Text.Trim(), out xGrid) && xGrid > 0)
area.AxisX.MajorGrid.Interval = xGrid;
else
area.AxisX.MajorGrid.Interval = 0;
if (double.TryParse(txtYGrid.Text.Trim(), out yGrid) && yGrid > 0)
area.AxisY.MajorGrid.Interval = yGrid;
else
area.AxisY.MajorGrid.Interval = 0;
area.AxisX.MajorGrid.Enabled = chkGridX.Checked;
area.AxisY.MajorGrid.Enabled = chkGridY.Checked;
double xMin, xMax, yMin, yMax;
if (double.TryParse(txtXMin.Text.Trim(), out xMin))
area.AxisX.Minimum = xMin;
else
area.AxisX.Minimum = double.NaN;
if (double.TryParse(txtXMax.Text.Trim(), out xMax))
area.AxisX.Maximum = xMax;
else
area.AxisX.Maximum = double.NaN;
if (double.TryParse(txtYMin.Text.Trim(), out yMin))
area.AxisY.Minimum = yMin;
else
area.AxisY.Minimum = double.NaN;
if (double.TryParse(txtYMax.Text.Trim(), out yMax))
area.AxisY.Maximum = yMax;
else
area.AxisY.Maximum = double.NaN;
area.AxisX.IsLogarithmic = chkXLog.Checked;
area.AxisY.IsLogarithmic = chkYLog.Checked;
bool minor = chkMinor.Checked;
double div = 4;
double.TryParse(txtMinorDiv.Text.Trim(), out div);
if (div < 2) div = 2;
area.AxisX.MinorGrid.Enabled = minor;
area.AxisY.MinorGrid.Enabled = minor;
if (minor)
{
Color minorColor = Color.FromArgb(215, 220, 228);
if (xGrid > 0)
area.AxisX.MinorGrid.Interval = xGrid / div;
else
area.AxisX.MinorGrid.Interval = 0;
if (yGrid > 0)
area.AxisY.MinorGrid.Interval = yGrid / div;
else
area.AxisY.MinorGrid.Interval = 0;
area.AxisX.MinorGrid.LineColor = minorColor;
area.AxisY.MinorGrid.LineColor = minorColor;
area.AxisX.MinorGrid.LineDashStyle = ChartDashStyle.Dash;
area.AxisY.MinorGrid.LineDashStyle = ChartDashStyle.Dash;
}
foreach (DataGridViewRow row in dgvSeries.Rows)
{
if (row.IsNewRow) continue;
string sname = row.Cells[0].Value == null ? "" : row.Cells[0].Value.ToString();
if (chart.Series.IndexOf(sname) < 0) continue;
Series s = chart.Series[sname];
string colorName = row.Cells[1].Value == null ? "" : row.Cells[1].Value.ToString();
if (colorName.Length > 0)
{
try { s.Color = Color.FromName(colorName); }
catch { }
}
int bw = 2;
int.TryParse(row.Cells[2].Value == null ? "2" : row.Cells[2].Value.ToString(), out bw);
s.BorderWidth = bw;
string style = row.Cells[3].Value == null ? "" : row.Cells[3].Value.ToString();
if (style.Length > 0) s.BorderDashStyle = CnToStyle(style);
string marker = row.Cells[4].Value == null ? "" : row.Cells[4].Value.ToString();
if (marker.Length > 0)
{
s.MarkerStyle = CnToMarker(marker);
s.MarkerSize = 6;
s.MarkerColor = s.Color;
}
}
chart.Invalidate();
DialogResult = DialogResult.OK;
Close();
}
}
}