commit e062317ce4af5f5731dbc2f3c909534195191c71 Author: admin Date: Mon Aug 10 22:24:17 2026 +0800 first commit diff --git a/ChartAnalysis.exe b/ChartAnalysis.exe new file mode 100644 index 0000000..543f0ba Binary files /dev/null and b/ChartAnalysis.exe differ diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..7870e44 --- /dev/null +++ b/Program.cs @@ -0,0 +1,2951 @@ +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 tableList = new List(); + 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 cols = new List(); + 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 allColumns = new List(); + List calcSeriesNames = new List(); + List lastRows; + string lastXcol = ""; + List lastYcols = new List(); + 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 dbs = new List(); + 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 tables = new List(); + 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 cols = new List(); + 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 CollectFilterItems() + { + List items = new List(); + 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 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 ycols = new List(); + 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 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 ycols, List 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 rows = new List(); + 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 rows, string xcol, List 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 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 gaps = new List(); + 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 tables = new List(); + while (r.Read()) tables.Add(r.GetString(0)); + if (tables.Count == 0) throw new Exception("数据库中没有表"); + table = tables[0]; + } + List cols = new List(); + 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 ycols = new List(); + 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 rows = new List(); + 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(); + } + } +} diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..f6c78cd --- /dev/null +++ b/README.txt @@ -0,0 +1,277 @@ +================================================================================ +趋势预测分析工具 ChartAnalysis - 使用教学与部署说明 +================================================================================ + +目录 +-------------------------------------------------------------------------------- + 一、程序简介与功能总览 + 二、运行环境要求 + 三、部署文件清单 + 四、快速上手(5 步出图) + 五、界面布局说明 + 六、详细操作教学 + 七、config.ini 配置说明 + 八、测试模式(开发用) + 九、常见问题 FAQ + 十、源码与重新编译 + +================================================================================ +一、程序简介与功能总览 +================================================================================ + +ChartAnalysis.exe 是一个连接 MySQL 数据库的趋势分析与预测绘图工具。 +单文件绿色程序(C# WinForms 编写),无需安装、无需任何运行库。 + +主要功能: +-------------------------------------------------------------------------------- + 1. 数据库连接: 连接任意 MySQL 5.5~8.x 服务器,浏览数据库/表/列结构。 + 2. 灵活查询: X 轴单选、Y 轴多选、过滤规则、排序方向、条数/偏移/起点控制。 + 3. 智能绘图: 自动识别时间(X)轴(23 种时间格式),支持数值轴/文本轴。 + 4. 运算曲线: 13 种数学运算(含 6 种 AI 算法),可叠加在原始曲线上。 + 5. 图表交互: 十字准线、点击数据点查看数值、图例固定显示、图表设置。 + 6. 图片导出: 一键保存 PNG / JPG / BMP 图片。 + 7. 窗口自适应: 左侧配置面板可拖拽宽度,控件随宽度自动伸缩布局。 + +================================================================================ +二、运行环境要求 +================================================================================ + + 1. 操作系统: Windows 7 SP1 / 8 / 8.1 / 10 / 11(32/64 位均可)。 + - 注意: Windows 7 必须为 SP1 或以上版本。 + 2. .NET Framework: 4.6.2 或更高(推荐 4.8)。 + - Win10 / 11 系统自带 4.8,开箱即用,无需安装。 + - Win7 SP1 / 8 / 8.1 若缺失,用 Start.bat 启动会自动下载安装。 + 3. 数据库: MySQL 5.5 ~ 8.x(本机或远程服务器均可)。 + - MySQL 连接驱动已内嵌进 exe,客户端无需安装任何驱动或 ODBC。 + 4. 无需安装: 不依赖 VC++ 运行库、Java、Python 等其他任何组件。 + +================================================================================ +三、部署文件清单 +================================================================================ + + [必需] ChartAnalysis.exe 主程序(单文件,约 1.5MB) + [必需] config.ini 数据库连接配置(与 exe 同目录) + + [可选] Start.bat 引导启动器: 自动检测并安装 .NET Framework, + 解决"没有 .NET 环境"电脑无法运行的问题。 + Win7 SP1 ~ Win11 全兼容。 + [可选] README.txt 本说明文档 + + 发布时只需 exe + config.ini 两个文件即可运行。 + +================================================================================ +四、快速上手(5 步出图) +================================================================================ + + 第 1 步: 双击 ChartAnalysis.exe(或用 Start.bat)启动程序。 + 第 2 步: 左侧"服务器连接"区填好 主机/端口/用户名/密码,点击"连 接"。 + (config.ini 已配置时自动填入,可直接连接) + 第 3 步: 连接后自动加载数据库列表,选中数据库后自动加载表列表; + 再选"存储表"→ 自动加载全部列;指定"横坐标"(X 轴); + 按住 Ctrl 键多选"纵坐标"(Y 轴曲线)。 + 第 4 步: 按需设置过滤规则 / 排序 / 条数等,点击"绘制曲线"。 + 第 5 步: 等待查询完成,图表自动渲染;可继续叠加运算曲线或保存图片。 + +================================================================================ +五、界面布局说明 +================================================================================ + +┌────────────┬──────────────────────────────────────────────┐ +│ 折叠 | 钉住 │ 标题栏 (表名 曲线图) │ +│ ────────── │ 图例条 (色块+曲线名, 固定在标题下方) │ +│ 服务器连接 │ 工具栏: 曲线 | 运算 | 参数 | 生成/清除/图例/设置/十字线 │ +│ 排序规则 │ │ +│ 存储表坐标 │ 图表绘图区 │ +│ 过滤规则 │ (十字线/点击数据点/缩放自由) │ +│ 绘制/保存 │ │ +└────────────┴──────────────────────────────────────────────┘ + + 左侧配置面板: 可拖动中间分隔条改变宽度 (380~800px)。 + - 面板宽度 >= 380px 时,控件宽度随面板自适应伸缩; + - 面板拖到 380px 以下时,控件保持最小布局,右侧开始被遮蔽 + (出现横向滚动条)。 + 折叠按钮: 隐藏左侧面板,腾出整个窗口给图表;点"展开配置"恢复。 + 钉住按钮: 锁定左侧面板宽度,拖动分隔条不再生效。 + +================================================================================ +六、详细操作教学 +================================================================================ + +6.1 服务器连接 +-------------------------------------------------------------------------------- + - 主机地址 / 端口 / 用户名 / 密码: 直接填写;密码框为掩码输入。 + - DB 下拉框: 支持输入过滤,回车或选中即加载该库的表。 + - 点"连 接": 连接服务器并刷新数据库列表(自动选中 config.ini 中的库)。 + - 连接信息会自动保存到 config.ini,下次启动自动填入。 + +6.2 排序规则 +-------------------------------------------------------------------------------- + - 方向: 正序(最早在前) / 倒序(最新在前),默认正序。 + * 注意: 无论哪种方向,图表 X 轴始终按时间(数值)从左到右递增显示。 + 倒序模式 = 只取最新的 N 条数据来画图。 + - 条数: 查询多少行,默认 2000(0 = 不限制)。 + - 偏移: 跳过前 N 行(从最早/最新一侧开始跳,跟随排序方向)。 + - 起点: 从最新数据往回数 N 条作为查询起点,再取"条数"行。 + 例: 条数=2000、起点=5000 → 取倒数第 5000 条往前共 2000 条数据。 + +6.3 存储表与坐标轴 +-------------------------------------------------------------------------------- + - 存储表: 选择要查询的表,自动列出所有列。 + - 横坐标: 单选 X 轴列(时间列 / 数值列 / 文本列均可自动识别)。 + - 纵坐标: 按住 Ctrl 可多选,画多条曲线。 + 提示: 下方小字"提示: 按住 Ctrl 键可多选纵坐标"。 + +6.4 过滤规则(多行按 AND/OR 连接) +-------------------------------------------------------------------------------- + 每行规则由 4 列组成: 列 | 操作符 | 值 | 逻辑。 + - 操作符: = != > >= < <= LIKE BETWEEN IS NULL IS NOT NULL + - 值: 普通条件直接填值;BETWEEN 用逗号分隔两个值,如 10,20; + LIKE 可直接写 % 通配符,如 'AB%' 可填 AB%。 + - 逻辑: 该行与下一行之间的连接方式,AND 或 OR,默认 AND。 + 例: + 列=类型 = 设备1 AND + 列=温度 >= 100 OR + 列=压力 <= 5 + 等价 SQL: WHERE 类型='设备1' AND 温度>=100 OR 压力<=5 + - 按钮: "+ 添加规则" 加一行;"- 删除" 删选中行;"清空" 删除全部。 + - 注意: 多行混用 AND/OR 时按行顺序连接,建议用括号语义自行组织顺序。 + +6.5 绘制曲线与保存图片 +-------------------------------------------------------------------------------- + - "绘制曲线": 后台线程查询并渲染,期间按钮禁用,完成后状态栏提示。 + - "保存图片": 弹出保存对话框,可选 PNG / JPG / BMP 格式。 + +6.6 图表交互 +-------------------------------------------------------------------------------- + (1) 十字准线: 鼠标移入绘图区,显示跟随鼠标的灰色虚线十字架; + 离开绘图区自动消失。工具栏"隐藏十字线/显示十字线"可开关。 + (2) 点击数据点: 鼠标左键点击曲线上的数据点,弹出浮动信息窗,显示: + 曲线名 / X 值 / Y 值 / 数据序号。 + 时间 X 轴自动格式化为 yyyy-MM-dd HH:mm:ss。 + 点击图表空白处或切换窗口自动关闭。 + (3) 图例: 固定在标题下方,色块+曲线名,自动换行、高度自适应。 + 工具栏"隐藏图例/显示图例"可开关。 + (4) 工具栏: + - 曲线: 选择目标曲线(仅"线性变换/二次变换"需要指定)。 + - 运算: 选择运算类型(详见 6.7)。 + - 参数: 输入所选运算的参数(窗口/阶数/迭代/α/a,b)。 + - "生成运算曲线": 对已绘制曲线执行运算并叠加显示。 + - "清除运算": 移除全部运算曲线。 + - "图表设置": 打开设置对话框(详见 6.8)。 + +6.7 运算功能详解(13 种,按拼音排序) +-------------------------------------------------------------------------------- + 运算列表(默认选中"平滑曲线"): + + 名称 参数 说明 + ───────────────────────────────────────────────────────────── + 贝叶斯回归 (AI) α:1 带正则的贝叶斯线性回归,α 越大曲线越平缓 + 常用对数 (log10) - 对 Y 值取以 10 为底的对数(需 Y>0) + 对数 (ln) - 对 Y 值取自然对数(需 Y>0) + 多项式拟合 (Polyfit) (AI) 阶数:3 最小二乘多项式拟合,阶数 1~10 + 二次变换 (a*y²+b) (AI) a,b:1,0 对指定曲线做二次变换 + 积分 (累积) - 梯形法累积积分(∫ydx) + 开方 (sqrt) - 对 Y 值开平方(需 Y>=0) + 平滑曲线 (滑动平均) 窗口:5 滑动平均滤波,窗口 3~101(奇数值) + 梯度下降 (AI) 迭代:200 数据归一化后用梯度下降迭代拟合回归线 + 微分 (差分) - 一阶差分 dy/dx + 线性回归 (AI) - 最小二乘线性回归直线 + 线性变换 (a*y+b) (AI) a,b:1,0 对指定曲线做线性变换 + 指数 (exp) - 对 Y 值取指数 + + - 使用步骤: 先"绘制曲线"→ 选择运算与参数 → "生成运算曲线"。 + - "线性变换/二次变换"需先在"曲线"下拉框指定目标曲线(变换只作用于 + 该曲线,其余运算对全部曲线各生成一条结果)。 + - (AI) 标记: 表示智能拟合/迭代类算法。 + +6.8 图表设置对话框 +-------------------------------------------------------------------------------- + "常规"页: + - 标题 / 横轴名称 / 纵轴名称(修改立即应用于图表)。 + - 图例位置: 固定于标题下方(不可调)。 + - 横轴/纵轴网格: 显示开关 + 网格间距(留空 = 自动)。 + - 次网格: 加密网格线开关 + 细分数(默认 4 等分)。 + "坐标轴"页: + - 横轴/纵轴最小/最大值: 留空 = 自动。 + - 横轴/纵轴对数刻度 (Log): 要求数据均为正值。 + "曲线样式"页: + - 逐条曲线设置: 颜色 / 线宽(1~8) / 线型(实线/虚线/点线/点划线) / + 标记(无/圆点/方块/三角/菱形)。 + - 点"应用"生效并关闭;"取消"放弃修改。 + +================================================================================ +七、config.ini 配置说明 +================================================================================ + + [mysql] + host = 127.0.0.1 数据库服务器地址 (远程填 IP 或域名) + port = 3306 端口 + user = root 用户名 + password = 密码 + database = 数据库名 + charset = utf8mb4 字符集 + + - 也可在程序界面的"连接"区直接填写,点"连接"后自动保存到本文件。 + - 首次运行没有 config.ini 时程序按默认值 (127.0.0.1/3306/root/空) 启动。 + +================================================================================ +八、测试模式(开发用) +================================================================================ + + ChartAnalysis.exe --selftest 自检: 连接数据库、枚举表/列、查询数据, + 结果写入 selftest_result.txt + ChartAnalysis.exe --drawtest 绘图测试: 用数据库中第一张表画 1 条曲线, + 导出 drawtest.png 验证图表渲染 + +================================================================================ +九、常见问题 FAQ +================================================================================ + +Q1. 双击 exe 无反应/报错提示缺少 .NET Framework? +A1. 用 Start.bat 启动会自动安装 .NET 4.8;或按第二节手动安装。 + +Q2. 报错"无法连接数据库"? +A2. 检查 config.ini 或界面中填写的地址/端口/账号/密码/数据库名是否正确, + 并确认服务器防火墙放行了对应端口(默认 3306)。 + +Q3. Win7 安装 .NET 4.8 失败? +A3. 确认系统是 Win7 SP1,且已安装最新 Windows Update 补丁。 + .NET 4.8 官方最低支持 Win7 SP1。 + +Q4. 查询很慢或图表点很多很卡? +A4. 在"条数"里减小查询行数(如 500/1000);或配合"起点"只看最近一段。 + +Q5. 为什么方向选"倒序"后图表还是从左到右时间递增? +A5. 这是正常设计: 图表 X 轴始终按时间递增显示,"倒序"仅表示取最新的 + 数据段来画图(等价于只查看最近 N 条)。 + +Q6. 对数刻度 / 对数运算为什么有的点不显示? +A6. 对数要求数值为正;sqrt 要求非负。非法点会被自动跳过,曲线相应断开。 + +Q7. 左侧面板变窄后控件看不全? +A7. 面板小于 380px 时控件保持最小布局并出现横向滚动条,属设计行为; + 拖宽面板即可完整显示。 + +================================================================================ +十、源码与重新编译(开发用) +================================================================================ + + Program.cs 主程序源码(C#,单文件,含全部窗体逻辑) + tools\ref\ .NET 4.6.2 参考程序集(编译时引用) + lib\ 依赖 DLL(编译时内嵌进 exe) + build.bat 一键重新编译 ChartAnalysis.exe(本机需装 .NET 4.x) + config.ini 数据库连接配置 + + 编译命令: 双击 build.bat,或命令行执行: + build.bat + 产物: ChartAnalysis.exe(内嵌 MySqlConnector 等 10 个 DLL 及程序图标) + + 依赖 DLL 明细(已内嵌,仅供说明): + MySqlConnector.dll、Microsoft.Bcl.AsyncInterfaces.dll、 + Microsoft.Extensions.DependencyInjection.Abstractions.dll、 + Microsoft.Extensions.Logging.Abstractions.dll、System.Buffers.dll、 + System.Diagnostics.DiagnosticSource.dll、System.Memory.dll、 + System.Numerics.Vectors.dll、System.Runtime.CompilerServices.Unsafe.dll、 + System.Threading.Tasks.Extensions.dll + +================================================================================ diff --git a/Start.bat b/Start.bat new file mode 100644 index 0000000..1c4c2e8 --- /dev/null +++ b/Start.bat @@ -0,0 +1,71 @@ +@echo off +setlocal EnableDelayedExpansion +title Trend Chart Launcher +set "APP_DIR=%~dp0" +set "APP_EXE=%APP_DIR%ChartAnalysis.exe" + +if not exist "%APP_EXE%" ( + echo [ERROR] ChartAnalysis.exe not found in this folder. + pause + exit /b 1 +) + +echo Checking .NET Framework version... + +set "RELHEX=" +for /f "tokens=3" %%v in ('reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" /v Release 2^>nul') do set "RELHEX=%%v" + +if defined RELHEX ( + set /a RELDEC=RELHEX 2>nul + if defined RELDEC ( + if !RELDEC! GEQ 394806 ( + echo [OK] .NET Framework 4.6.2+ detected. Starting program... + start "" "%APP_EXE%" + exit /b 0 + ) + ) +) + +echo. +echo [INFO] .NET Framework 4.6.2 or higher is required. +echo This launcher will download and install .NET Framework 4.8 +echo (supports Windows 7 SP1 to Windows 11), then start the program. +echo. +echo Downloading .NET Framework 4.8 (about 120MB)... +set "DOTNET_URL=https://go.microsoft.com/fwlink/?linkid=2088631" +set "INSTALLER=%TEMP%\dotnetfx48_setup.exe" + +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$ErrorActionPreference='Stop'; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $wc = New-Object System.Net.WebClient; $wc.DownloadFile('%DOTNET_URL%','%INSTALLER%')" + +if not exist "%INSTALLER%" ( + echo. + echo [FAILED] Download failed. Please install manually: + echo https://dotnet.microsoft.com/download/dotnet-framework/net48 + echo Then run Start.bat again. + start "" "https://dotnet.microsoft.com/download/dotnet-framework/net48" + pause + exit /b 1 +) + +echo Installing .NET Framework 4.8 silently, please wait... +"%INSTALLER%" /q /norestart +set "RC=%ERRORLEVEL%" + +if %RC% NEQ 0 ( + echo. + echo [WARN] Installer returned code %RC% (0 = success). + if %RC% EQU 3010 ( + echo Reboot required. Please restart the computer, then run Start.bat again. + pause + exit /b 1 + ) + echo Installation may have failed. Please retry after reboot. + pause + exit /b 1 +) + +echo. +echo [DONE] .NET Framework ready. Starting program... +start "" "%APP_EXE%" +exit /b 0 \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..bf614ea --- /dev/null +++ b/build.bat @@ -0,0 +1,57 @@ +@echo off +setlocal +title Build ChartAnalysis.exe + +set "APP_DIR=%~dp0" +set "CSC=C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe" +if not exist "%CSC%" set "CSC=C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe" + +if not exist "%CSC%" ( + echo [ERROR] csc.exe not found. .NET Framework 4.x required to build. + pause + exit /b 1 +) + +set "REF=%APP_DIR%tools\ref" +set "LIB=%APP_DIR%lib" + +echo Building ChartAnalysis.exe ... +pushd "%LIB%" + +"%CSC%" /nologo /noconfig /nostdlib+ /target:winexe /platform:anycpu /optimize+ ^ + /out:"%APP_DIR%ChartAnalysis.exe" ^ + /reference:"%REF%\mscorlib.dll" ^ + /reference:"%REF%\System.dll" ^ + /reference:"%REF%\System.Core.dll" ^ + /reference:"%REF%\System.Data.dll" ^ + /reference:"%REF%\System.Drawing.dll" ^ + /reference:"%REF%\System.Windows.Forms.dll" ^ + /reference:"%REF%\System.Windows.Forms.DataVisualization.dll" ^ + /reference:"%LIB%\MySqlConnector.dll" ^ + /resource:"%LIB%\MySqlConnector.dll" ^ + /resource:"%LIB%\Microsoft.Extensions.Logging.Abstractions.dll" ^ + /resource:"%LIB%\Microsoft.Extensions.DependencyInjection.Abstractions.dll" ^ + /resource:"%LIB%\System.Diagnostics.DiagnosticSource.dll" ^ + /resource:"%LIB%\System.Threading.Tasks.Extensions.dll" ^ + /resource:"%LIB%\System.Runtime.CompilerServices.Unsafe.dll" ^ + /resource:"%LIB%\Microsoft.Bcl.AsyncInterfaces.dll" ^ + /resource:"%LIB%\System.Memory.dll" ^ + /resource:"%LIB%\System.Buffers.dll" ^ + /resource:"%LIB%\System.Numerics.Vectors.dll" ^ + /win32icon:"%APP_DIR%hamster.ico" ^ + /resource:"%APP_DIR%hamster.ico" ^ + "%APP_DIR%Program.cs" + +set "RC=%ERRORLEVEL%" +popd + +if %RC% NEQ 0 ( + echo [FAILED] Build failed. See errors above. + pause + exit /b 1 +) + +echo [OK] Built: %APP_DIR%ChartAnalysis.exe +echo Target: .NET Framework 4.6.2 (Win7 SP1 - Win11) +pause +exit /b 0 \ No newline at end of file diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..203d677 --- /dev/null +++ b/config.ini @@ -0,0 +1,7 @@ +[mysql] +host = 47.116.138.143 +port = 6436 +user = sdbc +password = sdbc@88031111 +database = sdbc_datalog +charset = utf8mb4 diff --git a/drawtest.png b/drawtest.png new file mode 100644 index 0000000..2ad20b0 Binary files /dev/null and b/drawtest.png differ diff --git a/hamster.ico b/hamster.ico new file mode 100644 index 0000000..b05bf45 Binary files /dev/null and b/hamster.ico differ diff --git a/lib/Microsoft.Bcl.AsyncInterfaces.dll b/lib/Microsoft.Bcl.AsyncInterfaces.dll new file mode 100644 index 0000000..6031ba1 Binary files /dev/null and b/lib/Microsoft.Bcl.AsyncInterfaces.dll differ diff --git a/lib/Microsoft.Extensions.DependencyInjection.Abstractions.dll b/lib/Microsoft.Extensions.DependencyInjection.Abstractions.dll new file mode 100644 index 0000000..9428609 Binary files /dev/null and b/lib/Microsoft.Extensions.DependencyInjection.Abstractions.dll differ diff --git a/lib/Microsoft.Extensions.Logging.Abstractions.dll b/lib/Microsoft.Extensions.Logging.Abstractions.dll new file mode 100644 index 0000000..dfa6c44 Binary files /dev/null and b/lib/Microsoft.Extensions.Logging.Abstractions.dll differ diff --git a/lib/MySqlConnector.dll b/lib/MySqlConnector.dll new file mode 100644 index 0000000..16968c6 Binary files /dev/null and b/lib/MySqlConnector.dll differ diff --git a/lib/System.Buffers.dll b/lib/System.Buffers.dll new file mode 100644 index 0000000..f2d83c5 Binary files /dev/null and b/lib/System.Buffers.dll differ diff --git a/lib/System.Diagnostics.DiagnosticSource.dll b/lib/System.Diagnostics.DiagnosticSource.dll new file mode 100644 index 0000000..b58b31a Binary files /dev/null and b/lib/System.Diagnostics.DiagnosticSource.dll differ diff --git a/lib/System.Memory.dll b/lib/System.Memory.dll new file mode 100644 index 0000000..4617199 Binary files /dev/null and b/lib/System.Memory.dll differ diff --git a/lib/System.Numerics.Vectors.dll b/lib/System.Numerics.Vectors.dll new file mode 100644 index 0000000..0865972 Binary files /dev/null and b/lib/System.Numerics.Vectors.dll differ diff --git a/lib/System.Runtime.CompilerServices.Unsafe.dll b/lib/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 0000000..c5ba4e4 Binary files /dev/null and b/lib/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/lib/System.Threading.Tasks.Extensions.dll b/lib/System.Threading.Tasks.Extensions.dll new file mode 100644 index 0000000..eeec928 Binary files /dev/null and b/lib/System.Threading.Tasks.Extensions.dll differ diff --git a/selftest_result.txt b/selftest_result.txt new file mode 100644 index 0000000..a6fcc31 --- /dev/null +++ b/selftest_result.txt @@ -0,0 +1,3 @@ +查询行数: 300 +图表已保存 drawtest.png, 系列数=1, 图例启用=True +DRAWTEST PASS diff --git a/tools/ref/System.Core.dll b/tools/ref/System.Core.dll new file mode 100644 index 0000000..8e842b6 Binary files /dev/null and b/tools/ref/System.Core.dll differ diff --git a/tools/ref/System.Data.dll b/tools/ref/System.Data.dll new file mode 100644 index 0000000..f006b6a Binary files /dev/null and b/tools/ref/System.Data.dll differ diff --git a/tools/ref/System.Drawing.dll b/tools/ref/System.Drawing.dll new file mode 100644 index 0000000..6e00503 Binary files /dev/null and b/tools/ref/System.Drawing.dll differ diff --git a/tools/ref/System.Windows.Forms.DataVisualization.dll b/tools/ref/System.Windows.Forms.DataVisualization.dll new file mode 100644 index 0000000..4f4f465 Binary files /dev/null and b/tools/ref/System.Windows.Forms.DataVisualization.dll differ diff --git a/tools/ref/System.Windows.Forms.dll b/tools/ref/System.Windows.Forms.dll new file mode 100644 index 0000000..dada9a4 Binary files /dev/null and b/tools/ref/System.Windows.Forms.dll differ diff --git a/tools/ref/System.dll b/tools/ref/System.dll new file mode 100644 index 0000000..bc7480d Binary files /dev/null and b/tools/ref/System.dll differ diff --git a/tools/ref/mscorlib.dll b/tools/ref/mscorlib.dll new file mode 100644 index 0000000..7d5a54c Binary files /dev/null and b/tools/ref/mscorlib.dll differ