【问题标题】:How to draw one line only one time如何一次只画一条线
【发布时间】:2014-10-26 12:26:29
【问题描述】:

我正在使用GDI+ 来可视化某些用户指定建筑物的架构。 里面没有复杂的对象——它们都可以用矩形来表示。 我这样做了,但有一个问题:许多矩形是重叠的,例如然后房间是相邻的。 所以有些线条画了很多次! 这看起来很糟糕(粗线)并降低了应用程序的性能(额外的工作)。

有没有办法在屏幕上每条线只画一次?

我的代码(简化)如下所示:

 private void Visualizator_Paint( object sender, PaintEventArgs e )
 {
        if ( m_building == null ) return;

        var g = e.Graphics;

        // Smooth graphics output and scale
        g.SmoothingMode = SmoothingMode.HighQuality;
        ScaleGraphics( g );

        ...

        foreach( var room in m_rooms )
        {
            RectangleF extent = room.Extent;
            g.DrawRectangle( brownPen, extent.X, extent.Y, extent.Width, extent.Height );
        }

        ...
  }

  void ScaleGraphics( Graphics g )
  {
        // Set margins inside the control client area in pixels
        var margin = new Margins( 16, 16, 16, 16 );

        // Set the domain of (x,y) values
        var range = m_building.Extents;

        // Make it smaller by 5%
        range.Inflate( 0.05f * range.Width, 0.05f * range.Height );

        // Scale graphics
        ScaleGraphics( g, Visualizator, range, margin );
  }

  void ScaleGraphics( Graphics g, Control control, RectangleF domain, Margins margin )
    {
        // Find the drawable area in pixels (control-margins)
        int W = control.Width - margin.Left - margin.Right;
        int H = control.Height - margin.Bottom - margin.Top;

        // Ensure drawable area is at least 1 pixel wide
        W = Math.Max( 1, W );
        H = Math.Max( 1, H );

        // Find the origin (0,0) in pixels
        float OX = margin.Left - W * ( domain.Left / domain.Width );
        float OY = margin.Top + H * ( 1 + domain.Top / domain.Height );

        // Find the scale to fit the control
        float SX = W / domain.Width;
        float SY = H / domain.Height;

        // Transform the Graphics scene
        if ( m_panPoint.IsEmpty )
            m_panPoint = new PointF( OX, OY );

        g.TranslateTransform( m_panPoint.X, m_panPoint.Y, MatrixOrder.Append );
        g.ScaleTransform( SX * m_scale, -SY * m_scale );
    }

缺陷截图:

【问题讨论】:

  • 我不明白你的问题是什么。但是你可以在绘制矩形之前清除 g.Clear(Color.) 加上更好地使用双缓冲
  • @qwr 许多矩形(房间)是相邻的,所以一些(很多)线绘制了不止一次。我只想画这样的“共享”线。
  • 性能不是问题,相信我。如果这些线变粗,房间可能应该画在稍微不同的位置,你不觉得吗?你用什么笔宽?你可以上传问题的图像吗?您可以尝试创建一个与其他房间合作的房间类,以实际更改您在标题中所写的绘图,但这将是很多工作。我会说,在像素栅格上正确拟合所有内容似乎更容易。
  • 抗锯齿是核心问题,当线条被过度绘制时,锯齿像素会变得混乱,因为它将前一行中的现有像素视为背景。这会扰乱视觉效果并使线条看起来太胖。不是一个简单的问题要解决,只有一部分线重合并且线在世界模型中不重叠但在视图中(部分)重叠的极端情况是需要解决的棘手问题。进行剔除是一种二次算法,这很痛苦。关闭抗锯齿是唯一的快速解决方法。
  • @HansPassant 谢谢,禁用抗锯齿真的很有帮助。但是,我可以阅读更多关于这个问题和解决方法的信息吗?我的想法:将所有行存储在一个全局唯一项容器中(如 C++ std::set),并绘制它而不是自己绘制房间

标签: c# .net winforms graphics gdi+


【解决方案1】:

我无法重现问题中描述的模糊/拖尾效果。然而,能够避免过度绘制线条的基本要求似乎相当清楚,并且解决起来并不十分复杂。所以我提供了这个可以完成这项工作的课程:

/// <summary>
/// Consolidates horizontal and vertical lines.
/// </summary>
class LineConsolidator : IEnumerable<LineConsolidator.Line>
{
    /// <summary>
    /// A pair of points defining a line
    /// </summary>
    public struct Line
    {
        public Point Start { get; private set; }
        public Point End { get; private set; }

        public Line(Point start, Point end)
            : this()
        {
            Start = start;
            End = end;
        }
    }

    private struct Segment
    {
        public int Start { get; private set; }
        public int End { get; private set; }

        public Segment(int start, int end)
            : this()
        {
            if (end < start)
            {
                throw new ArgumentException("start must be less than or equal to end");
            }

            Start = start;
            End = end;
        }

        public Segment Union(Segment other)
        {
            if (End < other.Start || other.End < Start)
            {
                throw new ArgumentException("Only overlapping segments may be consolidated");
            }

            return new Segment(
                    Math.Min(Start, other.Start),
                    Math.Max(End, other.End));
        }

        public Segment? Intersect(Segment other)
        {
            int start = Math.Max(Start, other.Start),
                end = Math.Min(End, other.End);

            if (end < start)
            {
                return null;
            }

            return new Segment(start, end);
        }
    }

    private Dictionary<int, List<Segment>> _horizontalLines = new Dictionary<int, List<Segment>>();
    private Dictionary<int, List<Segment>> _verticalLines = new Dictionary<int, List<Segment>>();

    /// <summary>
    /// Add horizontal line
    /// </summary>
    /// <param name="y">The Y coordinate of the line to add</param>
    /// <param name="start">The first X coordinate of the line to add (must not be larger than <paramref name="end"/></param>
    /// <param name="end">The second X coordinate of the line to add (must not be smaller than <paramref name="start"/></param>
    /// <remarks>
    /// This method submits a new horizontal line to the collection. It is merged with any other
    /// horizontal lines with exactly the same Y coordinate that it overlaps.
    /// </remarks>
    public void AddHorizontal(int y, int start, int end)
    {
        _AddLine(y, new Segment(start, end), _horizontalLines);
    }

    /// <summary>
    /// Add vertical line
    /// </summary>
    /// <param name="y">The X coordinate of the line to add</param>
    /// <param name="start">The first Y coordinate of the line to add (must not be larger than <paramref name="end"/></param>
    /// <param name="end">The second Y coordinate of the line to add (must not be smaller than <paramref name="start"/></param>
    /// <remarks>
    /// This method submits a new vertical line to the collection. It is merged with any other
    /// vertical lines with exactly the same X coordinate that it overlaps.
    /// </remarks>
    public void AddVertical(int x, int start, int end)
    {
        _AddLine(x, new Segment(start, end), _verticalLines);
    }

    /// <summary>
    /// Add all four sides of a rectangle as individual lines
    /// </summary>
    /// <param name="rect">The rectangle containing the lines to add</param>
    public void AddRectangle(Rectangle rect)
    {
        AddHorizontal(rect.Top, rect.Left, rect.Right);
        AddHorizontal(rect.Bottom, rect.Left, rect.Right);
        AddVertical(rect.Left, rect.Top, rect.Bottom);
        AddVertical(rect.Right, rect.Top, rect.Bottom);
    }

    /// <summary>
    /// Gets all of the horizontal lines in the collection
    /// </summary>
    public IEnumerable<Line> HorizontalLines
    {
        get
        {
            foreach (var kvp in _horizontalLines)
            {
                foreach (var segment in kvp.Value)
                {
                    yield return new Line(new Point(segment.Start, kvp.Key), new Point(segment.End, kvp.Key));
                }
            }
        }
    }

    /// <summary>
    /// Gets all of the vertical lines in the collection
    /// </summary>
    public IEnumerable<Line> VerticalLines
    {
        get
        {
            foreach (var kvp in _verticalLines)
            {
                foreach (var segment in kvp.Value)
                {
                    yield return new Line(new Point(kvp.Key, segment.Start), new Point(kvp.Key, segment.End));
                }
            }
        }
    }

    private static void _AddLine(int lineKey, Segment newSegment, Dictionary<int, List<Segment>> segmentKeyToSegments)
    {
        // Get the list of segments for the given key (X for vertical lines, Y for horizontal lines)
        List<Segment> segments;

        if (!segmentKeyToSegments.TryGetValue(lineKey, out segments))
        {
            segments = new List<Segment>();
            segmentKeyToSegments[lineKey] = segments;
        }

        int isegmentInsert = 0, isegmentMergeFirst = -1, ilineSegmentLast = -1;

        // Find all existing segments that should be merged with the new one
        while (isegmentInsert < segments.Count && segments[isegmentInsert].Start <= newSegment.End)
        {
            Segment? intersectedSegment = newSegment.Intersect(segments[isegmentInsert]);

            if (intersectedSegment != null)
            {
                // If they overlap, merge them together, keeping track of all the existing
                // segments which were merged
                newSegment = newSegment.Union(segments[isegmentInsert]);

                if (isegmentMergeFirst == -1)
                {
                    isegmentMergeFirst = isegmentInsert;
                }

                ilineSegmentLast = isegmentInsert;
            }

            isegmentInsert++;
        }

        if (isegmentMergeFirst == -1)
        {
            // If there was no merge, just insert the new segment
            segments.Insert(isegmentInsert, newSegment);
        }
        else
        {
            // If more than one segment was merged, remove all but one
            if (ilineSegmentLast > isegmentMergeFirst)
            {
                segments.RemoveRange(isegmentMergeFirst + 1, ilineSegmentLast - isegmentMergeFirst);
            }

            // Copy the new, merged segment back to the first original segment's slot
            segments[isegmentMergeFirst] = newSegment;
        }
    }

    public IEnumerator<LineConsolidator.Line> GetEnumerator()
    {
        return HorizontalLines.Concat(VerticalLines).GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

请注意,这是基于整数坐标。将这种逻辑应用于浮点坐标会有点棘手,如果有人真的关心适应舍入误差。但是如果保证浮点坐标在重叠时总是来自同一个源,那么它们将满足此实现所需的相等条件,您只需将类型更改为浮点即可。

我包含了仅检索水平线或垂直线的属性,以便我可以以不同的方式绘制它们(不同的线端盖)以验证算法是否有效。通常我认为你在绘图时会枚举整个集合。

您首先创建一个集合的空实例,然后通过AddRectangle() 方法添加您的矩形(或单独的行,如果需要),最后枚举所有结果行来使用它。

我希望它在数千行左右的情况下表现得很好。在我的测试中,我只是在每次绘制窗口时从头开始重新创建集合。

根据 PC 的不同,即使在更高的幅度下它也可能表现得足够好,但我没有尝试进行任何特定的优化,而是选择了易于理解的代码。在处理大量矩形的情况下,您可能希望保留一个持久实例来收集生成的线/矩形。然后你不必在每个绘画事件中重新生成它。这可能需要也可能不需要向类添加功能以支持删除线。

【讨论】:

  • 哦,感谢您的大量工作!我会在周末尝试你的代码(并显示问题截图)。他们幸运地有 4 天之久:D P.S.我可以添加指向我的 GitGub 存储库的链接,我的所有代码都是开源的
  • 我在 OP 帖子中添加了问题截图。
  • 我的数据包含浮点矩形。那么,我必须实现“棘手”的逻辑吗? :)
  • 我不知道...就像我说的,这取决于这些浮点值的来源。编写的代码依赖于相等来收集应该合并的行。如果您的代码以不存在舍入误差的方式生成这些值(例如,该值被计算一次,然后分配给同一位置的所有行)或舍入误差始终相同(例如,每个位置同样将经历完全相同的计算),那么你可能会在没有它的情况下逃脱。否则,您将不得不量化坐标,以便它们可以通过相等性进行比较
  • 好的,我明白了。我的建筑坐标来自数据文件,它们在应用程序生命周期内不会更改。移动的建筑物看起来很奇怪:)
猜你喜欢
  • 2021-06-18
  • 1970-01-01
  • 2016-07-27
  • 1970-01-01
  • 1970-01-01
  • 2021-02-07
  • 1970-01-01
  • 2020-03-08
  • 2020-11-22
相关资源
最近更新 更多