【发布时间】:2018-07-04 23:24:38
【问题描述】:
我有像素艺术创作程序,并且我在画布上有一个字段(像素?)的矩形。这是一个很好的解决方案,数量不多(例如 128x128)。如果我想在画布上创建 1024x1024 矩形,这个过程很长,内存使用量约为 1-2 GB,之后程序运行非常缓慢。如何优化这一点,或创建更好的解决方案?
【问题讨论】:
我有像素艺术创作程序,并且我在画布上有一个字段(像素?)的矩形。这是一个很好的解决方案,数量不多(例如 128x128)。如果我想在画布上创建 1024x1024 矩形,这个过程很长,内存使用量约为 1-2 GB,之后程序运行非常缓慢。如何优化这一点,或创建更好的解决方案?
【问题讨论】:
使用Rectangle 表示每个像素是错误的方法。作为FrameworkElement,每个矩形都参与布局和输入命中测试。这种方法太重,无法扩展。 现在就放弃它。
我建议直接绘制到WriteableBitmap 并使用自定义表面在用户绘制时呈现位图。
下面是一个最低限度的概念证明,它允许以单一颜色进行简单的绘图。它需要 WriteableBitmapEx 库,该库可从 NuGet 获得。
public class PixelEditor : FrameworkElement
{
private readonly Surface _surface;
private readonly Visual _gridLines;
public int PixelWidth { get; } = 128;
public int PixelHeight { get; } = 128;
public int Magnification { get; } = 10;
public PixelEditor()
{
_surface = new Surface(this);
_gridLines = CreateGridLines();
Cursor = Cursors.Pen;
AddVisualChild(_surface);
AddVisualChild(_gridLines);
}
protected override int VisualChildrenCount => 2;
protected override Visual GetVisualChild(int index)
{
return index == 0 ? _surface : _gridLines;
}
private void Draw()
{
var p = Mouse.GetPosition(_surface);
var magnification = Magnification;
var surfaceWidth = PixelWidth * magnification;
var surfaceHeight = PixelHeight * magnification;
if (p.X < 0 || p.X >= surfaceWidth || p.Y < 0 || p.Y >= surfaceHeight)
return;
_surface.SetColor(
(int)(p.X / magnification),
(int)(p.Y / magnification),
Colors.DodgerBlue);
_surface.InvalidateVisual();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed && IsMouseCaptured)
Draw();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
CaptureMouse();
Draw();
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
ReleaseMouseCapture();
}
protected override Size MeasureOverride(Size availableSize)
{
var magnification = Magnification;
var size = new Size(PixelWidth* magnification, PixelHeight * magnification);
_surface.Measure(size);
return size;
}
protected override Size ArrangeOverride(Size finalSize)
{
_surface.Arrange(new Rect(finalSize));
return finalSize;
}
private Visual CreateGridLines()
{
var dv = new DrawingVisual();
var dc = dv.RenderOpen();
var w = PixelWidth;
var h = PixelHeight;
var m = Magnification;
var d = -0.5d; // snap gridlines to device pixels
var pen = new Pen(new SolidColorBrush(Color.FromArgb(63, 63, 63, 63)), 1d);
pen.Freeze();
for (var x = 1; x < w; x++)
dc.DrawLine(pen, new Point(x * m + d, 0), new Point(x * m + d, h * m));
for (var y = 1; y < h; y++)
dc.DrawLine(pen, new Point(0, y * m + d), new Point(w * m, y * m + d));
dc.Close();
return dv;
}
private sealed class Surface : FrameworkElement
{
private readonly PixelEditor _owner;
private readonly WriteableBitmap _bitmap;
public Surface(PixelEditor owner)
{
_owner = owner;
_bitmap = BitmapFactory.New(owner.PixelWidth, owner.PixelHeight);
_bitmap.Clear(Colors.White);
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.NearestNeighbor);
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
var magnification = _owner.Magnification;
var width = _bitmap.PixelWidth * magnification;
var height = _bitmap.PixelHeight * magnification;
dc.DrawImage(_bitmap, new Rect(0, 0, width, height));
}
internal void SetColor(int x, int y, Color color)
{
_bitmap.SetPixel(x, y, color);
}
}
}
只需将其导入您的 Xaml,最好在 ScrollViewer 中:
<Window x:Class="WpfTest.PixelArtEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfTest"
Title="PixelArtEditor"
Width="640"
Height="480">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<l:PixelEditor />
</ScrollViewer>
</Window>
显然,这与功能齐全的像素艺术编辑器相去甚远,但它功能齐全,足以让您走上正轨。编辑 128x128 图像与 1024x1024 图像之间的内存使用差异约为 30mb。启动它并查看它的实际效果:
嘿,这很有趣!感谢您的转移。
【讨论】:
WriteableBitmap 中维护像素,而不是在一个充满Rectangle 对象的画布中,并且表面绘制了该位图的放大版本。如何设置这些像素取决于您。我用来放置像素的方法只是一个例子。
Draw 方法只是在用户按下或拖动鼠标的地方放置一个彩色像素。这不是示例中有意义的部分。您可以根据需要在位图中放置您想要的任何像素。您问如何优化内存管理,这就是我向您展示的内容:如何将像素艺术数据存储在 WriteableBitmap 中并将其显示在绘图表面中。绘图表面的工作方式取决于您。重要的部分是像素数据存储在位图中。有很多方法可以在位图中放置像素。请参阅WriteableBitmapEx 文档。
只是为了改进 Mike Strobel 解决方案以将网格线捕捉到设备像素。
var d = -0.5d; // snap gridlines to device pixels
using (DrawingContext dc = _dv.RenderOpen())
{
GuidelineSet guidelineSet = new GuidelineSet();
guidelineSet.GuidelinesX.Add(0.5);
guidelineSet.GuidelinesY.Add(0.5);
dc.PushGuidelineSet(guidelineSet);
// Draw grid
}
【讨论】: