You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Framework 4.8中Graphics绘制线条不均匀,疑为单精度转整数舍入错误?

问题:GDI+绘制线条宽窄不均,窗体宽度变化时图案异常(.NET Framework 4.8)

绘制的线条宽窄不均,调整窗体宽度时图案会发生变形,具体表现为部分线条变宽、部分变窄,呈现不均匀的条纹状。当前使用.NET Framework 4.8环境。

复现代码

protected override void OnPaint(PaintEventArgs e)
{
    Graphics g = e.Graphics;
    Color c;
    int offsetY;

    g.DrawString("Method 1 - Using ScaleTransform", this.Font, Brushes.Black, 10, 15);
    g.DrawString("Method 2 - Using ScaleTransform and bitmap", this.Font, Brushes.Black, 10, 100);
    g.DrawString("Method 3 - Proves it's not a ScaleTransform issue" + Environment.NewLine + "as not using ScaleTransform and doing the scaling manualy", this.Font, Brushes.Black, 10, 185);

    g.ScaleTransform(System.Convert.ToSingle(this.Width / (double)150), 1);

    // Method 1 - Using ScaleTransform
    offsetY = 35;
    for (int row = 0; row <= 49; row++)
    {
        for (int col = 0; col <= 99; col++)
        {
            if (col % 2 == 0)
                c = Color.SkyBlue;
            else
                c = Color.Black;
            g.FillRectangle(new SolidBrush(c), col + 10, row + offsetY, 1, 1);
        }
    }

    // Method 2 - Using ScaleTransform and bitmap
    offsetY = 120;
    g.InterpolationMode = InterpolationMode.NearestNeighbor; // Without this, lines appear even but are burry (lines blended together)
    using (Bitmap bmp = new Bitmap(100, 50))
    {
        using (Graphics g1 = Graphics.FromImage(bmp))
        {
            for (int row = 0; row <= 49; row++)
            {
                for (int col = 0; col <= 99; col++)
                {
                    if (col % 2 == 0)
                        c = Color.SkyBlue;
                    else
                        c = Color.Black;
                    g1.FillRectangle(new SolidBrush(c), col, row, 1, 1); // Same effect using SetPixel(col, row, c)
                }
            }
        }
        g.DrawImage(bmp, new Rectangle(10, offsetY, bmp.Width, bmp.Height), 0, 0, bmp.Width, bmp.Height, GraphicsUnit.Pixel);
    }


    // Method 3 - Proves it's not a ScaleTransform issue as not using ScaleTransform and doing the scaling manualy
    offsetY = 220;
    g.ResetTransform();
    for (int row = 0; row <= 49; row++)
    {
        for (int col = 0; col <= 99; col++)
        {
            if (col % 2 == 0)
                c = Color.SkyBlue;
            else
                c = Color.Black;
            g.FillRectangle(new SolidBrush(c), System.Convert.ToSingle((col + 10) * (this.Width / (double)150)), System.Convert.ToSingle(row + offsetY), System.Convert.ToSingle(1 * (this.Width / (double)150)), System.Convert.ToSingle(1 * (this.Height / (double)150)));
        }
    }

    base.OnPaint(e);
}

已尝试的方案

  • 切换多种InterpolationMode模式
  • 先绘制到Bitmap再进行缩放绘制

预期线条宽度保持均匀,但问题仍存在。


解决方案

这个问题的核心是浮点缩放时的像素对齐误差:当缩放比例不是整数倍时,单个逻辑像素被映射到非整数个物理像素,GDI+的默认绘制逻辑会因浮点坐标的取整/对齐规则,导致部分像素块被拉伸、部分被压缩,最终呈现宽窄不均的条纹。以下是几种可行的解决思路:

1. 优先使用整数倍缩放

如果可以控制窗体尺寸,尽量让this.Width / 150的结果为整数(比如将窗体宽度设为150、300、450等),这样每个逻辑像素对应整数个物理像素,从根源上避免对齐误差。

2. 启用GDI+像素对齐优化

在绘制前设置PixelOffsetMode.Half,让绘制的图形偏移半个像素,减少浮点坐标带来的锯齿和宽窄不均:

// 在绘制前添加这行代码
g.PixelOffsetMode = PixelOffsetMode.Half;
// 可选:如果可以接受轻微模糊,开启抗锯齿进一步优化
g.SmoothingMode = SmoothingMode.AntiAlias;

如果需要保持绝对清晰,可配合SmoothingMode.None使用,平衡清晰度和线条均匀性。

3. 直接绘制适配目标尺寸的Bitmap

不要先绘制小尺寸Bitmap再缩放,而是直接创建与最终显示尺寸一致的Bitmap,在Bitmap内部完成逻辑到物理坐标的转换:

// 示例:针对Method 2修改的代码
offsetY = 120;
float scaleX = (float)(this.Width / 150.0);
int targetWidth = (int)(100 * scaleX);
int targetHeight = 50;

using (Bitmap bmp = new Bitmap(targetWidth, targetHeight))
{
    using (Graphics g1 = Graphics.FromImage(bmp))
    {
        for (int row = 0; row <= 49; row++)
        {
            for (int col = 0; col <= 99; col++)
            {
                Color c = col % 2 == 0 ? Color.SkyBlue : Color.Black;
                // 直接绘制缩放后的矩形,避免二次缩放
                g1.FillRectangle(new SolidBrush(c), col * scaleX, row, scaleX, 1);
            }
        }
    }
    g.DrawImage(bmp, 10, offsetY);
}

这种方式让每个逻辑列对应连续的物理像素块,不会出现宽窄差异。

4. 手动计算像素范围,避免浮点截断

在手动缩放时,不要直接用(col +10)*scale作为坐标,而是计算每个逻辑矩形的物理像素起始/结束位置,确保宽度均匀:

// 示例:针对Method 3修改的代码
offsetY = 220;
g.ResetTransform();
float scaleX = (float)(this.Width / 150.0);

for (int row = 0; row <= 49; row++)
{
    float y = row + offsetY;
    for (int col = 0; col <= 99; col++)
    {
        Color c = col % 2 == 0 ? Color.SkyBlue : Color.Black;
        // 计算当前列的物理像素起始和结束位置
        float xStart = (col + 10) * scaleX;
        float xEnd = (col + 11 + 10) * scaleX - 0.001f; // 减去极小值避免重叠
        float width = xEnd - xStart;
        
        g.FillRectangle(new SolidBrush(c), xStart, y, width, 1);
    }
}

这种方式让每个逻辑列的宽度尽可能接近平均缩放值,避免因浮点取整导致的宽度差异。


内容的提问来源于stack exchange,提问作者Bob Aune

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 06:24:55