WPF中的像素绘制

32

我该如何在WPF中进行像素级渲染(比如用于光线追踪)?我的初步想法是创建一个BitmapImage,修改缓冲区并在Image控件中显示它,但我不知道如何创建它(create方法需要一块非托管内存)。

5个回答

39

我强烈反对前两个建议。WriteableBitmap类提供了您需要的功能,假设您正在使用3.5 SP1。在SP1之前,在WPF中没有真正好的答案,除非您采用一些严重的诡计。


7
WriteableBitmap类自.NET 3.0版本及WPF的第一个版本就已经存在了,您无需使用.NET 3.5就可以使用此类。 - Eriawan Kusumawardhono

12

如果您正在考虑进行类似光线追踪器的工作,需要绘制大量点,则可能希望直接在位图的内存空间中绘制,而不是通过抽象层绘制。这段代码将为您提供显着更好的渲染时间,但代价是需要使用“不安全”的代码,这可能或可能不是您的选择。


            unsafe
            {
                System.Drawing.Point point = new System.Drawing.Point(320, 240);
                IntPtr hBmp;
                Bitmap bmp = new Bitmap(640, 480);
                Rectangle lockRegion = new Rectangle(0, 0, 640, 480);
                BitmapData data = bmp.LockBits(lockRegion, ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
                byte* p;

                p = (byte*)data.Scan0 + (point.Y * data.Stride) + (point.X * 3);
                p[0] = 0; //B pixel
                p[1] = 255; //G pixel
                p[2] = 255; //R pixel

                bmp.UnlockBits(data);

                //Convert the bitmap to BitmapSource for use with WPF controls
                hBmp = bmp.GetHbitmap();
                Canvas.Source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hbmpCanvas, IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
                Canvas.Source.Freeze();
                DeleteObject(hBmp); //Clean up original bitmap
            }

如果您要清理 hBitmap,您需要在类文件的顶部声明以下内容:


        [System.Runtime.InteropServices.DllImport("gdi32.dll")]
        public static extern bool DeleteObject(IntPtr hObject);

还要注意,您需要设置项目属性以允许不安全的代码。您可以通过在解决方案资源管理器中右键单击项目并选择属性来执行此操作。 转到生成选项卡。 选中“允许不安全的代码”框。 我也认为您需要添加对System.Drawing的引用。


许多错误。无法找到BitmapData,ImageLockMode不存在,PixelFormat不包含Format24bppRgb的定义,Canvas不包含Source的定义。 - john k

8

如果需要,您可以添加小矩形,但每个小矩形都是 FrameworkElement ,所以这可能会有点重。另一个选项是创建一个 DrawingVisual,绘制它,渲染它,然后将其放入图像中:

private void DrawRubbish()
{
    DrawingVisual dv = new DrawingVisual();
    using (DrawingContext dc = dv.RenderOpen())
    {
        Random rand = new Random();

        for (int i = 0; i < 200; i++)
            dc.DrawRectangle(Brushes.Red, null, new Rect(rand.NextDouble() * 200, rand.NextDouble() * 200, 1, 1));

        dc.Close();
    }
    RenderTargetBitmap rtb = new RenderTargetBitmap(200, 200, 96, 96, PixelFormats.Pbgra32);
    rtb.Render(dv);
    Image img = new Image();
    img.Source = rtb;
    MainGrid.Children.Add(img);
}

1
很棒的解决方案。像魔法一样奏效。只有一个小问题:不需要使用dc.Close();,可以使用调用Dispose(),它与Close()执行的操作相同。我已经检查了WPF源代码 :-) - Peter Huber

7
您可以将1X1的矩形对象放置在画布上。
  private void AddPixel(double x, double y)
  {
     Rectangle rec = new Rectangle();
     Canvas.SetTop(rec, y);
     Canvas.SetLeft(rec, x);
     rec.Width = 1;
     rec.Height = 1;
     rec.Fill = new SolidColorBrush(Colors.Red);
     myCanvas.Children.Add(rec);
  }

那应该非常接近你想要的。

使用矩形绘制像素会产生巨大的开销,因为矩形继承自Shape,Shape还定义了9个属性来绘制矩形周围的边框(Stroke)和其他不需要的功能。像素将由Geometry定义,它有自己的开销。但最糟糕的是,矩形还继承自FrameworkElement,它添加了100多个(!)支持布局、鼠标和键盘交互、ContextMenu、ToolTip等等的属性。 - Peter Huber

2
这里有一种使用WritableBitmap的方法。
public void DrawRectangle(WriteableBitmap writeableBitmap, int left, int top, int width, int height, Color color)
{
    // Compute the pixel's color
    int colorData = color.R << 16; // R
    colorData |= color.G << 8; // G
    colorData |= color.B << 0; // B
    int bpp = writeableBitmap.Format.BitsPerPixel / 8;

    unsafe
    {
        for (int y = 0; y < height; y++)
        {
            // Get a pointer to the back buffer
            int pBackBuffer = (int)writeableBitmap.BackBuffer;

            // Find the address of the pixel to draw
            pBackBuffer += (top + y) * writeableBitmap.BackBufferStride;
            pBackBuffer += left * bpp;

            for (int x = 0; x < width; x++)
            {
                // Assign the color data to the pixel
                *((int*)pBackBuffer) = colorData;

                // Increment the address of the pixel to draw
                pBackBuffer += bpp;
            }
        }
    }

    writeableBitmap.AddDirtyRect(new Int32Rect(left, top, width, height));
}

使用这个功能:

private WriteableBitmap bitmap = new WriteableBitmap(1100, 1100, 96d, 96d, PixelFormats.Bgr24, null);

private void Button_Click(object sender, RoutedEventArgs e)
{
    int size = 10;

    Random rnd = new Random(DateTime.Now.Millisecond);
    bitmap.Lock(); // Lock() and Unlock() could be moved to the DrawRectangle() method. Just do some performance tests.

    for (int y = 0; y < 99; y++)
    {
        for (int x = 0; x < 99; x++)
        {
            byte colR = (byte)rnd.Next(256);
            byte colG = (byte)rnd.Next(256);
            byte colB = (byte)rnd.Next(256);

            DrawRectangle(bitmap, (size + 1) * x, (size + 1) * y, size, size, Color.FromRgb(colR, colG, colB));
        }
    }

    bitmap.Unlock(); // Lock() and Unlock() could be moved to the DrawRectangle() method. Just do some performance tests.

  image.Source = bitmap; // This should be done only once
}

注意:请勿包含 system.drawing,否则无法使用“Color.RGB”调用。 - john k
我遇到了一个错误:在 System.Private.CoreLib.dll 中发生了未处理的类型为 'System.OverflowException' 的异常 :( - john k
为了支持64位,您必须将 int pBackBuffer = (int)writeableBitmap.BackBuffer; 替换为 var pBackBuffer = writeableBitmap.BackBuffer。此外,您可以通过将 *((int*)pBackBuffer) = colorData; 替换为 Marshal.WriteInt32(pBackBuffer, colorData) 来删除对 unsafe 的要求。 - Simon Mourier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接