Files
2026-08-10 22:24:17 +08:00

2952 lines
116 KiB
C#
Raw Permalink Blame History

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