优化后的 DrawChannels 方法中,显示了三条额外的线(可能指零线、中线和边距线),这些辅助线用于调试居中和边距效果,但可能影响视觉美观,需要删除。此外,您希望继续优化代码,确保曲线居中、不填满格子、显示美观,并支持负值数据。结合之前的优化(修复 CS0266 错误、增加 15% 边距、动态余量等),我将:
以下我将分析 DrawChannels 中添加的线,删除多余部分,提供优化代码,并确保曲线完美显示。代码和解释以中文清晰呈现。
一、问题分析
1. 三条额外线的来源在上一版本的 DrawChannels 方法中,为调试居中和边距效果,添加了以下三条辅助线:
// 绘制零线(Y=0)
if (OscilloscopeProps.VerticalMin < 0 && OscilloscopeProps.VerticalMax > 0)
{
double rangeY = OscilloscopeProps.VerticalMax – OscilloscopeProps.VerticalMin;
float zeroY = rangeY > 0
? drawTop + drawHeight – (float)((0 – OscilloscopeProps.VerticalMin) / (rangeY / drawHeight))
: drawTop + drawHeight / 2;
g.DrawLine(Pens.Gray, left, zeroY, left + h_size, zeroY);
m_Logger.Info($"Zero line at Y={zeroY}, Canvas middle={top + v_size / 2}, Draw area=[{drawTop}, {drawTop + drawHeight}]");
}
// 绘制画布中线和边距线(调试用)
g.DrawLine(Pens.Blue, left, top + v_size / 2, left + h_size, top + v_size / 2);
g.DrawLine(Pens.Green, left, drawTop, left + h_size, drawTop); // 上边距
g.DrawLine(Pens.Green, left, drawTop + drawHeight, left + h_size, drawTop + drawHeight); // 下边距
- 零线(灰色):绘制 Y=0 的水平线,验证零点位置是否接近画布中线(top + v_size / 2)。
- 画布中线(蓝色):绘制画布垂直中心线(top + v_size / 2),检查曲线居中效果。
- 边距线(绿色):绘制绘制区域的上边界(drawTop)和下边界(drawTop + drawHeight),验证 15% 边距。
问题:
- 这三条线仅用于调试,但在生产环境中可能干扰视觉效果,导致曲线显示不美观。
- 您要求删除这些线,保留核心绘制逻辑。
2. 其他优化需求
- 居中显示:曲线在画布中央 70% 区域,上下留 15% 空白,零线接近中线。
- 不填满格子:确保曲线不贴近顶部(drawTop)或底部(drawTop + drawHeight)。
- 负值支持:负值点正确映射到中央区域。
- 美观性:动态余量(20%-40%)确保曲线分布均匀。
- 稳定性:保留日志,增强异常处理。
二、优化思路
- 移除 DrawChannels 中绘制零线(灰色)、中线(蓝色)和边距线(绿色)的代码。
- 保留零线位置的日志记录(m_Logger.Info),用于调试但不影响显示。
- 保留 15% 垂直边距(marginV = v_size * 0.15f),绘制区域为 70%(drawHeight = v_size * 0.7f)。
- 确保 AutoScale 的范围计算使零点(Y=0)接近画布中线。
- 动态余量(20%-40%)在 AutoScale 中保持,确保曲线不贴边。
- 优化重采样,保留零点和极值,增强曲线平滑性。
- 负值点映射到中央区域,对数模式下跳过负值点,必要时切换到线性模式。
- 保留详细日志(范围、点坐标),便于排查问题。
- 确保异常处理设置默认范围,防止曲线消失。
三、改进代码以下是优化的 DrawChannels 方法,删除三条辅助线,同时保留 AutoScale 和 GetLegendBoxRec 方法(上一版本已足够优化)。
确保曲线居中、美观、不填满格子。
1. DrawChannels 方法优化目标:
- 删除零线、中线和边距线。
- 保留 15% 边距,曲线在中央 70% 区域。
- 保持日志记录,验证居中效果。
改进代码:
private void DrawChannels(Graphics g, RectangleF drawingRec)
{
try
{
m_OutofDrawingRange = false;
if (Channels.Count == 0)
{
m_OutofDrawingRange = true;
return;
}
RectangleF legendBox = GetLegendBoxRec(g, drawingRec);
float h_size = drawingRec.Width – OscilloscopeProps.Frame – m_LeftLabelFrame;
float v_size = drawingRec.Height – OscilloscopeProps.Frame – m_BottomLabelFrame;
if (ShowLegend && !legendBox.IsEmpty)
{
h_size -= legendBox.Width;
}
float left = Convert.ToSingle(drawingRec.Left + OscilloscopeProps.Frame / 2 + m_LeftLabelFrame);
float top = Convert.ToSingle(drawingRec.Top + OscilloscopeProps.Frame / 2);
// 预留 15% 垂直边距
float marginV = v_size * 0.15f;
float drawHeight = v_size * 0.7f; // 中央 70% 区域
float drawTop = top + marginV;
int nx = (int)((OscilloscopeProps.HorizonMax – OscilloscopeProps.HorizonMin) / OscilloscopeProps.HorizonStep);
int ny = (int)((OscilloscopeProps.VerticalMax – OscilloscopeProps.VerticalMin) / OscilloscopeProps.VerticalStep);
OscilloscopeProps.CellWidth = Math.Max(h_size / nx, 0.001f);
OscilloscopeProps.CellHeight = Math.Max(drawHeight / ny, 0.001f);
int count = 0;
// 计算零线位置(仅用于日志)
if (OscilloscopeProps.VerticalMin < 0 && OscilloscopeProps.VerticalMax > 0)
{
double rangeY = OscilloscopeProps.VerticalMax – OscilloscopeProps.VerticalMin;
float zeroY = rangeY > 0
? drawTop + drawHeight – (float)((0 – OscilloscopeProps.VerticalMin) / (rangeY / drawHeight))
: drawTop + drawHeight / 2;
m_Logger.Info($"Zero line calculated at Y={zeroY}, Canvas middle={top + v_size / 2}, Draw area=[{drawTop}, {drawTop + drawHeight}]");
}
foreach (Channel c in Channels)
{
if (!c.ChannelProps.Visible) continue;
if (c.XValues.Count == 0 || c.YValues.Count == 0)
{
m_Logger.Warn($"Channel {c.ChannelProps.Legend} has no data, skipping");
continue;
}
Color cr = c.ChannelProps.LineColor;
float lw = c.ChannelProps.LineWidth;
if (c.ChannelProps.Activated) { cr = OscilloscopeProps.ActiveColor; lw = 4 * lw; }
double rangeY = c.ChannelProps.VerticalMax – c.ChannelProps.VerticalMin;
double scaleY = rangeY > 0 ? drawHeight / rangeY : 1;
double scaleX = c.ChannelProps.HorizonStep / OscilloscopeProps.CellWidth;
scaleX = scaleX > 0 ? scaleX : 1;
c.CoordValues.Clear();
count += c.XValues.Count;
Pen pp = new Pen(cr, lw);
Brush cc = new SolidBrush(cr);
bool xlog = c.ChannelProps.XAxisDisplayMode == AxisDisplayMode.Log;
bool ylog = c.ChannelProps.YAxisDisplayMode == AxisDisplayMode.Log;
// 重采样,保留极值和零点
List<double> xValues = c.XValues;
List<double> yValues = c.YValues;
if (c.XValues.Count > ReSampleCount)
{
xValues = new List<double>();
yValues = new List<double>();
int step = c.XValues.Count / ReSampleCount;
double minY = c.YValues.Min(), maxY = c.YValues.Max();
for (int j = 0; j < c.XValues.Count; j += step)
{
xValues.Add(c.XValues[j]);
yValues.Add(c.YValues[j]);
if (c.YValues[j] == minY || c.YValues[j] == maxY || c.YValues[j] == 0)
{
xValues.Add(c.XValues[j]);
yValues.Add(c.YValues[j]);
}
}
}
switch (c.ChannelProps.ChartMode)
{
case DrawingMode.XYLines:
int i = 0;
List<PointF> points = new List<PointF>();
while (i < xValues.Count)
{
if (xlog && xValues[i] <= 0 || ylog && yValues[i] <= 0)
{
m_Logger.Warn($"Skipping point at index {i} due to invalid log value (x={xValues[i]}, y={yValues[i]})");
if (points.Count > 1) g.DrawLines(pp, points.ToArray());
points.Clear();
i++;
continue;
}
double x = xlog ? Math.Log10(Math.Abs(xValues[i] * c.XUnitFactor)) : xValues[i] * c.XUnitFactor;
double y = ylog ? Math.Log10(Math.Abs(yValues[i] * c.YUnitFactor)) : yValues[i] * c.YUnitFactor;
PointF pt = new PointF
{
X = Convert.ToSingle(left + (x – c.ChannelProps.HorizonMin + c.ChannelProps.XOffset) / scaleX),
Y = Convert.ToSingle(drawTop + drawHeight – (y – c.ChannelProps.VerticalMin) / (rangeY / drawHeight))
};
// 截断到绘制区域
pt.Y = Math.Max(drawTop, Math.Min(drawTop + drawHeight, pt.Y));
pt.X = Math.Max(left, Math.Min(left + h_size, pt.X));
c.CoordValues.Add(pt);
points.Add(pt);
m_Logger.Debug($"Point {i}: X={pt.X}, Y={pt.Y}, DataY={y}, InDrawArea={pt.Y >= drawTop && pt.Y <= drawTop + drawHeight}");
i++;
}
if (points.Count > 1) g.DrawLines(pp, points.ToArray());
break;
// … 其他模式
}
pp.Dispose();
cc.Dispose();
}
m_OutofDrawingRange = count == 0;
}
catch (Exception ex)
{
m_Logger.Error($"DrawChannels error: {ex.Message}, StackTrace: {ex.StackTrace}");
}
}
改进点:
- 移除以下代码:csharp
g.DrawLine(Pens.Gray, left, zeroY, left + h_size, zeroY); // 零线
g.DrawLine(Pens.Blue, left, top + v_size / 2, left + h_size, top + v_size / 2); // 中线
g.DrawLine(Pens.Green, left, drawTop, left + h_size, drawTop); // 上边距
g.DrawLine(Pens.Green, left, drawTop + drawHeight, left + h_size, drawTop + drawHeight); // 下边距 - 保留 zeroY 计算和日志,验证居中效果。
- 15% 垂直边距(marginV = v_size * 0.15f),绘制区域为 70%(drawHeight = v_size * 0.7f)。
- 保留零线位置和点坐标日志,方便调试。
- 保持 Y = drawTop + drawHeight – (y – VerticalMin) / (rangeY / drawHeight),确保数值增大向下。
2. AutoScale 方法(保持不变)
- 上一版本(2025年7月2日)的 AutoScale 已优化,动态余量(20%-40%)确保曲线不贴边,零点居中,支持负值和对数模式,无需修改。
- 代码如下(仅供参考,确保上下文一致):
public void AutoScale()
{
try
{
if (Channels.Count == 0)
{
m_Logger.Warn("No channels available for AutoScale");
OscilloscopeProps.VerticalMax = 10;
OscilloscopeProps.VerticalMin = -10;
OscilloscopeProps.HorizonMax = 10;
OscilloscopeProps.HorizonMin = 0;
OscilloscopeProps.VerticalStep = 2;
OscilloscopeProps.HorizonStep = 1;
Unit = 1;
canvas.Invalidate();
return;
}
Channel activeChan = null;
double globalMaxY = double.MinValue, globalMinY = double.MaxValue;
double globalMaxX = double.MinValue, globalMinX = double.MaxValue;
for (int i = 0; i < Channels.Count; i++)
{
var c = Channels[i];
if (c.XValues.Count == 0 || c.YValues.Count == 0)
{
m_Logger.Warn($"Channel {i} ({c.ChannelProps.Legend}) has no data, skipping AutoScale");
continue;
}
double[] validYValues = c.YValues.ToArray();
if (c.ChannelProps.YAxisDisplayMode == AxisDisplayMode.Log)
{
validYValues = c.YValues.Where(y => y > 0).ToArray();
if (validYValues.Length == 0)
{
m_Logger.Warn($"Channel {i} ({c.ChannelProps.Legend}) has no valid positive Y values for log mode, switching to linear mode");
c.ChannelProps.YAxisDisplayMode = AxisDisplayMode.Linear;
validYValues = c.YValues.ToArray();
}
}
double maxY = validYValues.Max();
double minY = validYValues.Min();
double centerY = Math.Abs(maxY) > Math.Abs(minY) ? 0 : (maxY + minY) / 2;
double rangeY = Math.Max(Math.Abs(maxY – centerY), Math.Abs(minY – centerY)) * 2;
if (rangeY < 1E-10)
{
rangeY = Math.Max(Math.Abs(centerY), 1) * 2;
}
double margin = rangeY > 100 ? 0.2 : 0.4;
c.ChannelProps.VerticalMax = centerY + rangeY * (0.5 + margin);
c.ChannelProps.VerticalMin = centerY – rangeY * (0.5 + margin);
c.ChannelProps.YOffset = 0;
double maxX = c.XValues.Max();
double minX = c.XValues.Min();
double rangeX = maxX – minX;
if (rangeX < 1E-10)
{
rangeX = Math.Max(Math.Abs(maxX), 1) * 2;
}
c.ChannelProps.HorizonMax = maxX + rangeX * 0.2;
c.ChannelProps.HorizonMin = minX – rangeX * 0.2;
if (c.ChannelProps.VerticalMax <= c.ChannelProps.VerticalMin)
{
m_Logger.Warn($"Invalid Y range for channel {i}: max={c.ChannelProps.VerticalMax}, min={c.ChannelProps.VerticalMin}");
c.ChannelProps.VerticalMax = c.ChannelProps.VerticalMin + 1;
}
c.ChannelProps.VerticalStep = (c.ChannelProps.VerticalMax – c.ChannelProps.VerticalMin) / 10;
c.ChannelProps.HorizonStep = (c.ChannelProps.HorizonMax – c.ChannelProps.HorizonMin) / 10;
globalMaxY = Math.Max(globalMaxY, c.ChannelProps.VerticalMax);
globalMinY = Math.Min(globalMinY, c.ChannelProps.VerticalMin);
globalMaxX = Math.Max(globalMaxX, c.ChannelProps.HorizonMax);
globalMinX = Math.Min(globalMinX, c.ChannelProps.HorizonMin);
if (c.ChannelProps.Activated && activeChan == null)
{
activeChan = c;
}
}
if (globalMaxY <= globalMinY)
{
globalMaxY = 10;
globalMinY = -10;
m_Logger.Warn("Global Y range invalid, using default: [10, -10]");
}
if (globalMaxX <= globalMinX)
{
globalMaxX = 10;
globalMinX = 0;
m_Logger.Warn("Global X range invalid, using default: [10, 0]");
}
if (!MultiChannelRange)
{
double centerY = (globalMaxY + globalMinY) / 2;
double rangeY = globalMaxY – globalMinY;
double margin = rangeY > 100 ? 0.2 : 0.4;
globalMaxY = centerY + rangeY * (0.5 + margin);
globalMinY = centerY – rangeY * (0.5 + margin);
foreach (var c in Channels)
{
c.ChannelProps.VerticalMax = globalMaxY;
c.ChannelProps.VerticalMin = globalMinY;
c.ChannelProps.HorizonMax = globalMaxX;
c.ChannelProps.HorizonMin = globalMinX;
c.ChannelProps.VerticalStep = (globalMaxY – globalMinY) / 10;
c.ChannelProps.HorizonStep = (globalMaxX – globalMinX) / 10;
c.ChannelProps.YOffset = 0;
}
OscilloscopeProps.VerticalMax = globalMaxY;
OscilloscopeProps.VerticalMin = globalMinY;
OscilloscopeProps.HorizonMax = globalMaxX;
OscilloscopeProps.HorizonMin = globalMinX;
OscilloscopeProps.VerticalStep = (globalMaxY – globalMinY) / 10;
OscilloscopeProps.HorizonStep = (globalMaxX – globalMinX) / 10;
}
if (Channels.Count > 0 && activeChan == null)
{
Channels[0].ChannelProps.Activated = true;
activeChan = Channels[0];
}
Unit = activeChan?.ChannelProps.HorizonStep ?? 1;
m_Logger.Info($"AutoScale completed: Global Y range [{globalMinY}, {globalMaxY}], X range [{globalMinX}, {globalMaxX}]");
canvas.Invalidate();
}
catch (Exception ex)
{
m_Logger.Error($"AutoScale error: {ex.Message}, StackTrace: {ex.StackTrace}");
foreach (var c in Channels)
{
c.ChannelProps.VerticalMax = 10;
c.ChannelProps.VerticalMin = -10;
c.ChannelProps.HorizonMax = 10;
c.ChannelProps.HorizonMin = 0;
c.ChannelProps.VerticalStep = 2;
c.ChannelProps.HorizonStep = 1;
c.ChannelProps.YOffset = 0;
}
OscilloscopeProps.VerticalMax = 10;
OscilloscopeProps.VerticalMin = -10;
OscilloscopeProps.HorizonMax = 10;
OscilloscopeProps.HorizonMin = 0;
OscilloscopeProps.VerticalStep = 2;
OscilloscopeProps.HorizonStep = 1;
Unit = 1;
canvas.Invalidate();
}
}
3. GetLegendBoxRec 方法(保持不变)
- 上一版本已限制图例高度(drawingRec.Height * 0.7f),确保不压缩垂直空间,无需修改。
- 代码如下(仅供参考):
private RectangleF GetLegendBoxRec(Graphics g, RectangleF drawingRec)
{
try
{
if (!ShowLegend || Channels.Count == 0)
{
m_LegendBoxRec = RectangleF.Empty;
return m_LegendBoxRec;
}
float legendWidth = 100f;
float legendHeight = Channels.Count * 20f + 10f;
float margin = 10f;
float maxLegendWidth = drawingRec.Width * 0.3f;
float maxLegendHeight = drawingRec.Height * 0.7f;
foreach (Channel c in Channels)
{
SizeF textSize = g.MeasureString(c.ChannelProps.Legend, Font);
legendWidth = Math.Max(legendWidth, textSize.Width + 20f);
}
legendWidth = Math.Min(legendWidth, maxLegendWidth);
legendHeight = Math.Min(legendHeight, maxLegendHeight);
float x = drawingRec.Right – legendWidth – margin;
float y = drawingRec.Top + margin;
m_LegendBoxRec = new RectangleF(x, y, legendWidth, legendHeight);
m_Logger.Info($"LegendBoxRec: X={x}, Y={y}, Width={legendWidth}, Height={legendHeight}");
return m_LegendBoxRec;
}
catch (Exception ex)
{
m_Logger.Error($"GetLegendBoxRec error: {ex.Message}, StackTrace: {ex.StackTrace}");
m_LegendBoxRec = RectangleF.Empty;
return m_LegendBoxRec;
}
}
四、测试建议为验证优化效果,建议以下测试用例:
- 数据:Y = [-100, -50, 0, 50, 100],验证曲线在 [drawTop, drawTop + drawHeight] 内,上下留 15% 空白,零线接近 top + v_size / 2(通过日志检查)。
- 数据:Y = [-1000, -500, 0, 50],验证曲线居中,不贴边。
- 启用 AxisDisplayMode.Log,输入 Y = [-10, 0, 10, 100],验证正值点居中,负值点跳过。
- 数据:Y = [-50, -50, -50],验证范围对称(如 [-70, -30]),曲线居中。
- 两通道,范围 [-100, 0] 和 [0, 100],测试 MultiChannelRange = false 是否居中。
- 启用 ShowLegend,验证图例不压缩垂直空间,曲线不填满格子。
调试建议:
- 日志检查:
- 零线位置:csharp
m_Logger.Info($"Zero line calculated at Y={zeroY}, Canvas middle={top + v_size / 2}, Draw area=[{drawTop}, {drawTop + drawHeight}]");
- 点坐标:csharp
m_Logger.Debug($"Point {i}: X={pt.X}, Y={pt.Y}, DataY={y}, InDrawArea={pt.Y >= drawTop && pt.Y <= drawTop + drawHeight}");
- 零线位置:csharp
- 视觉检查:
- 确认无零线(灰色)、中线(蓝色)、边距线(绿色)。
- 曲线应在 [drawTop, drawTop + drawHeight] 内,上下留 15% 空白。
五、总结问题解决:
- 删除零线(灰色)、中线(蓝色)、边距线(绿色)的绘制代码。
- 保留 zeroY 日志,验证居中效果。
- 曲线在画布中央 70% 区域(drawTop 到 drawTop + drawHeight),上下 15% 空白。
- 零线(Y=0)接近 top + v_size / 2(通过日志确认)。
- 动态余量(20%-40%)和 15% 边距确保曲线不贴边。
- 对称范围包含负值,对数模式切换到线性模式(如无正值)。
- 曲线分布均匀,上下空白充足,无干扰线。
- 保留异常处理和日志,防止曲线消失。
预期效果:
- 曲线在画布中央 70% 区域,上下留 15% 空白。
- 无零线、中线、边距线,视觉干净。
- Y轴方向正确(数值增大向下),负值点正确显示。
- 图例不压缩垂直空间,曲线美观。
下一步: 如果仍存在问题(如曲线未完全居中、显示异常),请提供:
我将进一步分析,确保曲线完美居中且美观!
图形用户界面设计
更简洁说明



