C# 在UserControl中渲染大型画布

C# 在UserControl中渲染大型画布,c#,gdi+,doublebuffered,C#,Gdi+,Doublebuffered,这几天来,我一直很难实现这一点。我已经搜索了很多关于我想做什么的类似问题,但是我没有遇到一个能直接帮助我解决问题的问题 基本上,我是在我的UserControl类中将平铺渲染到网格上。这是我正在开发的基于平铺引擎的世界编辑器。这是一个开放世界文档的屏幕截图和一些涂刷过的瓷砖 最初,我打算在我的控件中使用一个位图,这将是世界的预览画布。例如,使用画笔工具,当您移动鼠标并按下左键时,它会将光标下方最近的瓷砖设置为画笔的瓷砖,并将其绘制在层位图上。控件的OnPaint方法将被重写,以相对于绘制事件的

这几天来,我一直很难实现这一点。我已经搜索了很多关于我想做什么的类似问题,但是我没有遇到一个能直接帮助我解决问题的问题

基本上,我是在我的
UserControl
类中将平铺渲染到网格上。这是我正在开发的基于平铺引擎的世界编辑器。这是一个开放世界文档的屏幕截图和一些涂刷过的瓷砖

最初,我打算在我的控件中使用一个
位图
,这将是世界的预览画布。例如,使用画笔工具,当您移动鼠标并按下左键时,它会将光标下方最近的瓷砖设置为画笔的瓷砖,并将其绘制在
位图上。控件的
OnPaint
方法将被重写,以相对于绘制事件的剪切矩形绘制
位图

这种方法的问题是,当处理大世界时,位图将非常大。我需要这个应用程序能够适应世界大小,而且很明显,每次在控件上渲染大型位图无效时,都会出现性能问题

目前,我正在控件的重写
OnPaint
事件中直接将平铺绘制到控件上。这很好,因为它不需要很多内存。例如,一个
(10001000)
世界在
(20,20)
每个磁贴(总画布大小为
(20000,20000)
)上运行,整个应用程序的内存约为18mb。虽然不是内存密集型,但它相当占用处理器,因为每次控件失效时,它都会遍历视口中的每个分片。这会产生非常恼人的闪烁

我想完成的是一种在内存使用和性能方面达到中间的方法。本质上是双重缓冲世界,这样当控件被重画时就不会闪烁(窗体大小调整、焦点和模糊、滚动等)。以Photoshop为例,当打开的文档溢出容器视口时,它如何呈现该文档

作为参考,这里是我的控件的
OnPaint
覆盖,它使用上面提到的直接绘制方法

getRenderBounds
返回一个相对于
PaintEventArgs.ClipRectangle
的矩形,用于渲染可见的分幅,而不是在世界上的所有分幅中循环并检查它是否可见

protected override void OnPaint(PaintEventArgs e)
{
    WorldSettings settings = worldSettings();

    Rectangle bounds = getRenderBounds(e.ClipRectangle),
        drawLocation = new Rectangle(Point.Empty, settings.TileSize);

    e.Graphics.InterpolationMode = 
        System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    e.Graphics.SmoothingMode = 
        System.Drawing.Drawing2D.SmoothingMode.None;
    e.Graphics.PixelOffsetMode = 
        System.Drawing.Drawing2D.PixelOffsetMode.None;
    e.Graphics.CompositingQuality = 
        System.Drawing.Drawing2D.CompositingQuality.HighSpeed;

    for (int x = bounds.X; x < bounds.Width; x++)
    {
        for (int y = bounds.Y; y < bounds.Height; y++)
        {
            if (!inWorld(x, y))
                continue;

            Tile tile = getTile(x, y);

            if (tile == null)
                continue;

            drawLocation.X = x * settings.TileSize.Width;
            drawLocation.Y = y * settings.TileSize.Height;

            e.Graphics.DrawImage(img, 
                drawLocation, 
                tileRectangle, 
                GraphicsUnit.Pixel);
        }
    }
}
protected override void OnPaint(PaintEventArgs e)
{
WorldSettings=WorldSettings();
矩形边界=getRenderBounds(例如ClipRectangle),
drawLocation=新矩形(Point.Empty,settings.TileSize);
e、 Graphics.InterpolationMode=
System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
e、 Graphics.SmoothingMode=
System.Drawing.Drawing2D.SmoothingMode.None;
e、 Graphics.PixelOffsetMode=
System.Drawing.Drawing2D.PixelOffsetMode.None;
e、 Graphics.CompositingQuality=
系统。绘图。绘图2D。合成质量。高速;
for(int x=bounds.x;x
如果您需要我的代码中的更多上下文,只需进行注释。

诀窍是根本不使用大位图。您只需要覆盖可见区域的位图。然后你画任何可见的东西

protected override void OnPaint(PaintEventArgs e)
{
    WorldSettings settings = worldSettings();

    Rectangle bounds = getRenderBounds(e.ClipRectangle),
        drawLocation = new Rectangle(Point.Empty, settings.TileSize);

    e.Graphics.InterpolationMode = 
        System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    e.Graphics.SmoothingMode = 
        System.Drawing.Drawing2D.SmoothingMode.None;
    e.Graphics.PixelOffsetMode = 
        System.Drawing.Drawing2D.PixelOffsetMode.None;
    e.Graphics.CompositingQuality = 
        System.Drawing.Drawing2D.CompositingQuality.HighSpeed;

    for (int x = bounds.X; x < bounds.Width; x++)
    {
        for (int y = bounds.Y; y < bounds.Height; y++)
        {
            if (!inWorld(x, y))
                continue;

            Tile tile = getTile(x, y);

            if (tile == null)
                continue;

            drawLocation.X = x * settings.TileSize.Width;
            drawLocation.Y = y * settings.TileSize.Height;

            e.Graphics.DrawImage(img, 
                drawLocation, 
                tileRectangle, 
                GraphicsUnit.Pixel);
        }
    }
}
要实现这一点,需要将数据与位图分开进行维护。这可以是一个简单的数组,也可以是一个带有简单类的数组/列表,其中包含每个块的信息,例如世界位置

当块位于可见区域内时,可以绘制它。您可能需要迭代整个数组,也可能不需要迭代整个数组,但这并不是一个真正的问题(您还可以在单独的线程上计算可见数组)。还可以通过创建区域索引使函数更加智能,这样就不会迭代所有块

若要将新块添加到阵列,请将其画布位置计算为世界坐标,添加它,然后再次渲染阵列(或绘制块的区域)

这也是系统绘制具有可滚动区域的控件的方式

启用双缓冲将使其保持清晰且闪烁较少

在本例中,我还将使用一个带有单独滚动条的面板,并计算滚动条的相对位置。

诀窍是根本不使用大位图。您只需要覆盖可见区域的位图。然后你画任何可见的东西

protected override void OnPaint(PaintEventArgs e)
{
    WorldSettings settings = worldSettings();

    Rectangle bounds = getRenderBounds(e.ClipRectangle),
        drawLocation = new Rectangle(Point.Empty, settings.TileSize);

    e.Graphics.InterpolationMode = 
        System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    e.Graphics.SmoothingMode = 
        System.Drawing.Drawing2D.SmoothingMode.None;
    e.Graphics.PixelOffsetMode = 
        System.Drawing.Drawing2D.PixelOffsetMode.None;
    e.Graphics.CompositingQuality = 
        System.Drawing.Drawing2D.CompositingQuality.HighSpeed;

    for (int x = bounds.X; x < bounds.Width; x++)
    {
        for (int y = bounds.Y; y < bounds.Height; y++)
        {
            if (!inWorld(x, y))
                continue;

            Tile tile = getTile(x, y);

            if (tile == null)
                continue;

            drawLocation.X = x * settings.TileSize.Width;
            drawLocation.Y = y * settings.TileSize.Height;

            e.Graphics.DrawImage(img, 
                drawLocation, 
                tileRectangle, 
                GraphicsUnit.Pixel);
        }
    }
}
要实现这一点,需要将数据与位图分开进行维护。这可以是一个简单的数组,也可以是一个带有简单类的数组/列表,其中包含每个块的信息,例如世界位置

当块位于可见区域内时,可以绘制它。您可能需要迭代整个数组,也可能不需要迭代整个数组,但这并不是一个真正的问题(您还可以在单独的线程上计算可见数组)。还可以通过创建区域索引使函数更加智能,这样就不会迭代所有块

若要将新块添加到阵列,请将其画布位置计算为世界坐标,添加它,然后再次渲染阵列(或绘制块的区域)

这也是系统绘制具有可滚动区域的控件的方式

启用双缓冲将使其保持清晰且闪烁较少

在本例中,我还将使用带有单独滚动条的面板并计算滚动条