|
|
using DevExpress.Utils.Drawing;
|
|
|
using DevExpress.XtraEditors;
|
|
|
using GeoSigmaDrawLib;
|
|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
using System.Drawing;
|
|
|
using System.Drawing.Drawing2D;
|
|
|
using System.Linq;
|
|
|
using System.Windows.Forms;
|
|
|
|
|
|
namespace GeoSigma.SigmaDrawerStyle
|
|
|
{
|
|
|
public class FrmGradientRulerEditor : XtraUserControl
|
|
|
{
|
|
|
public class MarkItem
|
|
|
{
|
|
|
public ColorScaleEntry RawData;
|
|
|
public double NormalizedPosition { get; set; }
|
|
|
internal float CurrentY { get; set; }
|
|
|
internal RectangleF Bounds { get; set; }
|
|
|
internal RectangleF TextBounds { get; set; }
|
|
|
internal bool IsHovered { get; set; }
|
|
|
internal bool IsSelected { get; set; }
|
|
|
}
|
|
|
|
|
|
private List<MarkItem> _items = new List<MarkItem>();
|
|
|
private MarkItem _draggingItem = null;
|
|
|
private MarkItem _editingItem = null;
|
|
|
private MarkItem _selectedItem = null;
|
|
|
private TextBox _editTextBox;
|
|
|
|
|
|
private double _globalMin, _globalMax;
|
|
|
private bool _isReversed; // 标记是否为反转状态
|
|
|
private ColorBlend _fixedGradientBlend;
|
|
|
private const int GradientWidth = 40;
|
|
|
private const int PaddingTopBottom = 30;
|
|
|
private Rectangle _gradRect;
|
|
|
|
|
|
public FrmGradientRulerEditor()
|
|
|
{
|
|
|
this.DoubleBuffered = true;
|
|
|
this.Font = new Font("Tahoma", 9F);
|
|
|
|
|
|
_editTextBox = new TextBox { Visible = false, Width = 60 };
|
|
|
_editTextBox.Leave += (s, e) => EndEdit(true);
|
|
|
_editTextBox.KeyDown += (s, e) => {
|
|
|
if (e.KeyCode == Keys.Enter) EndEdit(true);
|
|
|
if (e.KeyCode == Keys.Escape) EndEdit(false);
|
|
|
};
|
|
|
this.Controls.Add(_editTextBox);
|
|
|
}
|
|
|
|
|
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
|
{
|
|
|
if (keyData == Keys.Delete)
|
|
|
{
|
|
|
DeleteSelected();
|
|
|
return true;
|
|
|
}
|
|
|
return base.ProcessCmdKey(ref msg, keyData);
|
|
|
}
|
|
|
|
|
|
// 接收是否反转的参数
|
|
|
public void SetData(IEnumerable<ColorScaleEntry> bgEntries, IEnumerable<ColorScaleEntry> markEntries, double min, double max, bool isReversed)
|
|
|
{
|
|
|
_globalMin = min;
|
|
|
_globalMax = max;
|
|
|
_isReversed = isReversed;
|
|
|
double range = max - min;
|
|
|
|
|
|
UpdateGradientBlend(bgEntries, min, max);
|
|
|
|
|
|
_items.Clear();
|
|
|
foreach (var entry in markEntries)
|
|
|
{
|
|
|
double pos = (range == 0) ? 0.5 : (entry.z - min) / range;
|
|
|
_items.Add(new MarkItem { RawData = entry, NormalizedPosition = pos });
|
|
|
}
|
|
|
Invalidate();
|
|
|
}
|
|
|
|
|
|
private void UpdateGradientBlend(IEnumerable<ColorScaleEntry> entries, double min, double max)
|
|
|
{
|
|
|
var baseSorted = entries.OrderBy(e => e.z).ToList();
|
|
|
if (baseSorted.Count > 1)
|
|
|
{
|
|
|
float[] posArray = new float[baseSorted.Count];
|
|
|
Color[] colorArray = new Color[baseSorted.Count];
|
|
|
double range = max - min;
|
|
|
|
|
|
for (int i = 0; i < baseSorted.Count; i++)
|
|
|
{
|
|
|
float pos = (range == 0) ? 0 : (float)((baseSorted[i].z - min) / range);
|
|
|
//强制限制在 0.0 ~ 1.0 之间,防越界崩溃
|
|
|
posArray[i] = Math.Max(0.0f, Math.Min(1.0f, pos));
|
|
|
colorArray[i] = Color.FromArgb(baseSorted[i].a, baseSorted[i].r, baseSorted[i].g, baseSorted[i].b);
|
|
|
}
|
|
|
|
|
|
if (!_isReversed)
|
|
|
{
|
|
|
for (int i = 0; i < posArray.Length; i++) posArray[i] = 1.0f - posArray[i];
|
|
|
}
|
|
|
|
|
|
Array.Sort(posArray, colorArray);
|
|
|
|
|
|
// GDI+ 终极保险:强制首尾两端绝对等于 0.0 和 1.0
|
|
|
posArray[0] = 0.0f;
|
|
|
posArray[posArray.Length - 1] = 1.0f;
|
|
|
|
|
|
// 防重叠崩溃:GDI+ 要求位置必须严格递增,如果有两个节点在同一位置会直接崩溃
|
|
|
for (int i = 1; i < posArray.Length; i++)
|
|
|
{
|
|
|
if (posArray[i] <= posArray[i - 1])
|
|
|
{
|
|
|
posArray[i] = posArray[i - 1] + 0.00001f; // 微微偏移
|
|
|
if (posArray[i] > 1.0f) posArray[i] = 1.0f;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 再次确保最后一位是 1.0f (防偏移挤压)
|
|
|
posArray[posArray.Length - 1] = 1.0f;
|
|
|
|
|
|
_fixedGradientBlend = new ColorBlend { Colors = colorArray, Positions = posArray };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public void AddPointByValue(double val)
|
|
|
{
|
|
|
val = Math.Max(_globalMin, Math.Min(_globalMax, val));
|
|
|
|
|
|
// 【新增防重叠】:检查是否已经存在非常接近该 Z 值的节点
|
|
|
if (_items.Any(item => Math.Abs(item.RawData.z - val) < 1e-5))
|
|
|
{
|
|
|
// 如果是用户点击按钮手动输入,给个弹窗提示;双击误触则静默忽略
|
|
|
XtraMessageBox.Show("已存在相同数值的标注点!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
double range = _globalMax - _globalMin;
|
|
|
double pos = (range == 0) ? 0.5 : (val - _globalMin) / range;
|
|
|
|
|
|
// 获取采样色
|
|
|
float blendPos = _isReversed ? (float)pos : 1.0f - (float)pos;
|
|
|
Color sampled = GetInterpolatedColor(_fixedGradientBlend, blendPos);
|
|
|
|
|
|
var entry = new ColorScaleEntry { z = val, r = sampled.R, g = sampled.G, b = sampled.B, a = sampled.A, isContinue = 1 };
|
|
|
var newItem = new MarkItem { RawData = entry, NormalizedPosition = pos };
|
|
|
_items.Add(newItem);
|
|
|
SetSelectedItem(newItem);
|
|
|
Invalidate();
|
|
|
}
|
|
|
|
|
|
public void DeleteSelected()
|
|
|
{
|
|
|
if (_selectedItem != null && _items.Count > 1)
|
|
|
{
|
|
|
_items.Remove(_selectedItem);
|
|
|
_selectedItem = null;
|
|
|
Invalidate();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void SetSelectedItem(MarkItem item)
|
|
|
{
|
|
|
_items.ForEach(i => i.IsSelected = false);
|
|
|
if (item != null) item.IsSelected = true;
|
|
|
_selectedItem = item;
|
|
|
Invalidate();
|
|
|
}
|
|
|
|
|
|
protected override void OnMouseDown(MouseEventArgs e)
|
|
|
{
|
|
|
if (_editTextBox.Visible) { EndEdit(true); return; }
|
|
|
var hit = _items.FirstOrDefault(p => p.Bounds.Contains(e.Location));
|
|
|
if (hit != null)
|
|
|
{
|
|
|
SetSelectedItem(hit);
|
|
|
_draggingItem = hit;
|
|
|
this.Focus();
|
|
|
Capture = true;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
SetSelectedItem(null);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
protected override void OnMouseDoubleClick(MouseEventArgs e)
|
|
|
{
|
|
|
var hitText = _items.FirstOrDefault(p => p.TextBounds.Contains(e.Location));
|
|
|
if (hitText != null) { StartEdit(hitText); return; }
|
|
|
if (_gradRect.Contains(e.Location))
|
|
|
{
|
|
|
float h = Height - (PaddingTopBottom * 2);
|
|
|
double ratio = (e.Y - PaddingTopBottom) / h;
|
|
|
|
|
|
// 正向时,从上往下数值递减;反向时,从上往下数值递增
|
|
|
if (!_isReversed) ratio = 1.0 - ratio;
|
|
|
|
|
|
double val = _globalMin + (_globalMax - _globalMin) * ratio;
|
|
|
AddPointByValue(val);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
protected override void OnMouseMove(MouseEventArgs e)
|
|
|
{
|
|
|
if (_editTextBox.Visible) return;
|
|
|
foreach (var item in _items) item.IsHovered = item.Bounds.Contains(e.Location);
|
|
|
|
|
|
if (_draggingItem != null)
|
|
|
{
|
|
|
float h = Height - (PaddingTopBottom * 2);
|
|
|
double newPos = (e.Y - PaddingTopBottom) / h;
|
|
|
if (!_isReversed) newPos = 1.0 - newPos; // 正反转映射
|
|
|
newPos = Math.Max(0.0, Math.Min(1.0, newPos));
|
|
|
|
|
|
_draggingItem.NormalizedPosition = newPos;
|
|
|
var data = _draggingItem.RawData;
|
|
|
data.z = _globalMin + (_globalMax - _globalMin) * newPos;
|
|
|
|
|
|
// 实时采样颜色
|
|
|
float blendPos = _isReversed ? (float)newPos : 1.0f - (float)newPos;
|
|
|
Color sampled = GetInterpolatedColor(_fixedGradientBlend, blendPos);
|
|
|
data.r = sampled.R;
|
|
|
data.g = sampled.G;
|
|
|
data.b = sampled.B;
|
|
|
|
|
|
_draggingItem.RawData = data;
|
|
|
}
|
|
|
Invalidate();
|
|
|
}
|
|
|
|
|
|
protected override void OnMouseUp(MouseEventArgs e) { _draggingItem = null; Capture = false; }
|
|
|
|
|
|
private void StartEdit(MarkItem item)
|
|
|
{
|
|
|
_editingItem = item;
|
|
|
_editTextBox.Text = item.RawData.z.ToString("F3");
|
|
|
_editTextBox.Location = new Point((int)item.TextBounds.X, (int)item.TextBounds.Y);
|
|
|
_editTextBox.Visible = true;
|
|
|
_editTextBox.Focus(); _editTextBox.SelectAll();
|
|
|
}
|
|
|
|
|
|
private void EndEdit(bool commit)
|
|
|
{
|
|
|
if (_editingItem != null && commit && double.TryParse(_editTextBox.Text, out double newVal))
|
|
|
{
|
|
|
newVal = Math.Max(_globalMin, Math.Min(_globalMax, newVal));
|
|
|
if (_items.Any(item => item != _editingItem && Math.Abs(item.RawData.z - newVal) < 1e-5))
|
|
|
{
|
|
|
XtraMessageBox.Show("已存在相同数值的标注点,修改被取消!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
var data = _editingItem.RawData;
|
|
|
data.z = newVal;
|
|
|
|
|
|
double range = _globalMax - _globalMin;
|
|
|
double pos = (range == 0) ? 0.5 : (newVal - _globalMin) / range;
|
|
|
_editingItem.NormalizedPosition = pos;
|
|
|
|
|
|
// 实时采样颜色
|
|
|
float blendPos = _isReversed ? (float)pos : 1.0f - (float)pos;
|
|
|
Color sampled = GetInterpolatedColor(_fixedGradientBlend, blendPos);
|
|
|
data.r = sampled.R;
|
|
|
data.g = sampled.G;
|
|
|
data.b = sampled.B;
|
|
|
|
|
|
_editingItem.RawData = data;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
_editTextBox.Visible = false;
|
|
|
_editingItem = null;
|
|
|
Invalidate();
|
|
|
}
|
|
|
|
|
|
protected override void OnPaint(PaintEventArgs e)
|
|
|
{
|
|
|
base.OnPaint(e);
|
|
|
Graphics g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias;
|
|
|
Rectangle rect = ClientRectangle;
|
|
|
_gradRect = new Rectangle(rect.X + 40, rect.Y + PaddingTopBottom, GradientWidth, rect.Height - (PaddingTopBottom * 2));
|
|
|
|
|
|
if (_fixedGradientBlend != null)
|
|
|
{
|
|
|
using (LinearGradientBrush brush = new LinearGradientBrush(_gradRect, Color.Black, Color.White, 90f))
|
|
|
{
|
|
|
brush.InterpolationColors = _fixedGradientBlend;
|
|
|
g.FillRectangle(brush, _gradRect);
|
|
|
}
|
|
|
g.DrawRectangle(Pens.DimGray, _gradRect);
|
|
|
foreach (var item in _items)
|
|
|
{
|
|
|
// 【关键】:反转时 Min 在顶,Max 在底;正向时 Max 在顶,Min 在底
|
|
|
if (_isReversed)
|
|
|
item.CurrentY = _gradRect.Top + (float)(item.NormalizedPosition * _gradRect.Height);
|
|
|
else
|
|
|
item.CurrentY = _gradRect.Bottom - (float)(item.NormalizedPosition * _gradRect.Height);
|
|
|
|
|
|
item.Bounds = new RectangleF(_gradRect.Right, item.CurrentY - 8, 150, 16);
|
|
|
item.TextBounds = new RectangleF(_gradRect.Right + 30, item.CurrentY - 8, 120, 16);
|
|
|
|
|
|
// 绘制横线色 (保持实时采样色)
|
|
|
float blendPos = _isReversed ? (float)item.NormalizedPosition : 1.0f - (float)item.NormalizedPosition;
|
|
|
Color sampledColor = GetInterpolatedColor(_fixedGradientBlend, blendPos);
|
|
|
|
|
|
using (Pen p = new Pen(sampledColor, item.IsHovered ? 2.5f : 1.5f))
|
|
|
{
|
|
|
g.DrawLine(p, _gradRect.Right, item.CurrentY, _gradRect.Right + 25, item.CurrentY);
|
|
|
}
|
|
|
|
|
|
if (_editingItem != item)
|
|
|
{
|
|
|
string label = item.RawData.z.ToString("F3");
|
|
|
Brush textBrush = item.IsSelected ? Brushes.Blue : (item.IsHovered ? Brushes.Red : Brushes.Black);
|
|
|
using (Font textFont = new Font(this.Font, item.IsSelected ? FontStyle.Bold : FontStyle.Regular))
|
|
|
{
|
|
|
g.DrawString(label, textFont, textBrush, item.TextBounds.X, item.TextBounds.Y);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private Color GetInterpolatedColor(ColorBlend blend, float pos)
|
|
|
{
|
|
|
if (pos <= blend.Positions[0]) return blend.Colors[0];
|
|
|
if (pos >= blend.Positions.Last()) return blend.Colors.Last();
|
|
|
for (int i = 1; i < blend.Positions.Length; i++)
|
|
|
{
|
|
|
if (pos < blend.Positions[i])
|
|
|
{
|
|
|
float t = (pos - blend.Positions[i - 1]) / (blend.Positions[i] - blend.Positions[i - 1]);
|
|
|
Color c1 = blend.Colors[i - 1], c2 = blend.Colors[i];
|
|
|
return Color.FromArgb((int)(c1.A + (c2.A - c1.A) * t), (int)(c1.R + (c2.R - c1.R) * t), (int)(c1.G + (c2.G - c1.G) * t), (int)(c1.B + (c2.B - c1.B) * t));
|
|
|
}
|
|
|
}
|
|
|
return blend.Colors.Last();
|
|
|
}
|
|
|
|
|
|
public List<ColorScaleEntry> GetResult() => _items.Select(i => i.RawData).OrderBy(d => d.z).ToList();
|
|
|
|
|
|
// 接收是否反转的参数 isReversed
|
|
|
public static List<ColorScaleEntry> ShowEditor(IWin32Window owner, IEnumerable<ColorScaleEntry> bgEntries, IEnumerable<ColorScaleEntry> markEntries, double min, double max, bool isReversed)
|
|
|
{
|
|
|
using (XtraForm form = new XtraForm())
|
|
|
{
|
|
|
form.Text = "色标编辑器";
|
|
|
form.Size = new Size(315, 600);
|
|
|
form.StartPosition = FormStartPosition.CenterParent;
|
|
|
form.FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
|
form.MaximizeBox = false;
|
|
|
form.MinimizeBox = false;
|
|
|
|
|
|
var editor = new FrmGradientRulerEditor { Dock = DockStyle.Fill };
|
|
|
editor.SetData(bgEntries, markEntries, min, max, isReversed);
|
|
|
|
|
|
PanelControl pnlBottom = new PanelControl { Dock = DockStyle.Bottom, Height = 60 };
|
|
|
|
|
|
SimpleButton btnAdd = new SimpleButton { Text = "增加", Size = new Size(80, 30), Location = new Point(15, 15) };
|
|
|
SimpleButton btnDel = new SimpleButton { Text = "删除", Size = new Size(80, 30), Location = new Point(105, 15) };
|
|
|
SimpleButton btnOk = new SimpleButton { Text = "保存", Size = new Size(100, 30), Location = new Point(195, 15), DialogResult = DialogResult.OK };
|
|
|
|
|
|
btnAdd.Click += (s, e) => {
|
|
|
XtraInputBoxArgs args = new XtraInputBoxArgs();
|
|
|
args.Caption = "增加标注";
|
|
|
args.Prompt = "请输入Z值";
|
|
|
args.DefaultResponse = min.ToString("F3");
|
|
|
args.Buttons = new DialogResult[] { DialogResult.OK, DialogResult.Cancel };
|
|
|
var result = XtraInputBox.Show(args);
|
|
|
|
|
|
if (result != null && double.TryParse(result.ToString(), out double dVal))
|
|
|
{
|
|
|
editor.AddPointByValue(dVal);
|
|
|
}
|
|
|
};
|
|
|
btnDel.Click += (s, e) => editor.DeleteSelected();
|
|
|
|
|
|
pnlBottom.Controls.AddRange(new Control[] { btnAdd, btnDel, btnOk });
|
|
|
form.Controls.Add(editor);
|
|
|
form.Controls.Add(pnlBottom);
|
|
|
|
|
|
return (form.ShowDialog(owner) == DialogResult.OK) ? editor.GetResult() : null;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
} |