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(); } } }