在Windows Forms上绘制单个像素

100

我卡在了尝试在Windows Form上打开单个像素的问题上。

graphics.DrawLine(Pens.Black, 50, 50, 51, 50); // draws two pixels

graphics.DrawLine(Pens.Black, 50, 50, 50, 50); // draws no pixels

API确实应该有一个方法来设置一个像素的颜色,但我没有找到这样的方法。

我正在使用C#。


我制作了一个控件,可以绘制一些非常漂亮的科学图表(具有商业控件中没有的附加功能)。我可以用X或+或小方框绘制数据点。但对于不重要的数据,我只想要一个单像素。 - Mark T
我看到的所有答案似乎都对于单个像素来说过于复杂了?为什么直接执行buffer[y*width+x]=color似乎更容易呢? - NoMoreZealots
10个回答

121

这将设置单个像素:

e.Graphics.FillRectangle(aBrush, x, y, 1, 1);

20
谁能想到DrawRectangle使用相同的参数会画出一个2x2的矩形,但FillRectange却只会画出一个1x1的矩形呢?谢谢。 - Mark T
10
@Mark:这种做法有一定的道理... DrawRectangle 表示“从 x 和 y 开始画一个宽度和高度为此的轮廓”,因此它会绘制起点和终点(x,y;x+1,y;x,y+1;x+1,y+1)以及它们之间的线条。FillRectangle 表示“从 x 和 y 开始画一个宽度和高度为此的填充表面”。顺便说一下,FillRectangle 需要一个 Brush,而不是 Pen。 - Powerlord
6
@R. Bemrose: 实际上,这取决于您传递给DrawRectangle()的笔对象。 如果它具有Pen.Alignment = PenAlignment.Outset,它将会在所给出的矩形周围绘制 Pen.Width 像素粗的轮廓线。但是,如果您指定了 Pen.Alignment = PenAlignment.Inset,它将在给定的矩形内部绘制与 Pen.Width 相同像素粗度的“轮廓线”。 - Cipi
2
这是一个非常不令人满意的答案。虽然它设置了单个像素,但要求并不总是设置字面像素。每次我需要这个功能时(而且经常需要),我需要用绘制线条的同一支笔来绘制点,而不是画刷。换句话说,我们需要一条长度小于2像素的线。我猜这就是原帖作者所需要的。 - RobertT
7
我不喜欢这个回答。这种方式非常低效。 - user626528
如果使用SmoothingMode = AntiAlias在图形上绘制,填充的矩形将从像素的中心点(x,y)开始,这意味着它会在4个像素之间绘制锯齿。为了使矩形居中于(x,y),从x和y中减去0.5。e.Graphics.FillRectangle(aBrush, x - 0.5f, y - 0.5f, 1.0f, 1.0f); - Adam

20
Graphics对象并没有这个功能,因为它是一个抽象的对象,可以用来覆盖矢量图形格式。在这种情况下,设置单个像素是没有意义的。Bitmap图像格式确实有GetPixel()SetPixel()方法,但没有建立在此之上的图形对象。对于您的情况,您的选择似乎真的是唯一的,因为没有通用的方法来设置通用图形对象的单个像素(而且您不知道它到底是什么,因为您的控件/表单可能已经双缓冲等)。

你为什么需要设置单个像素?


19

为了展示Henk Holterman回答的完整代码:

Brush aBrush = (Brush)Brushes.Black;
Graphics g = this.CreateGraphics();

g.FillRectangle(aBrush, x, y, 1, 1);

11

当我绘制许多单像素点(用于各种自定义数据显示)时,我倾向于将它们绘制到一个位图中,然后将其贴在屏幕上。

由于Bitmap GetPixel和SetPixel操作进行了大量的边界检查,因此它们不是特别快。但相对容易创建一个“快速位图”类,该类可以快速访问位图。


4

GetHdc 的 MSDN 页面

我认为这是您正在寻找的内容。您需要获取 HDC,然后使用 GDI 调用来使用 SetPixel。请注意,GDI 中的 COLORREF 是存储 BGR 颜色的 DWORD。它没有 Alpha 通道,并且不像 GDI+ 的 Color 结构体那样是 RGB 格式。

这是我编写的一小段代码,以完成相同的任务:

public class GDI
{
    [System.Runtime.InteropServices.DllImport("gdi32.dll")]
    internal static extern bool SetPixel(IntPtr hdc, int X, int Y, uint crColor);
}

{
    ...
    private void OnPanel_Paint(object sender, PaintEventArgs e)
    {
        int renderWidth = GetRenderWidth();
        int renderHeight = GetRenderHeight();
        IntPtr hdc = e.Graphics.GetHdc();

        for (int y = 0; y < renderHeight; y++)
        {
            for (int x = 0; x < renderWidth; x++)
            {
                Color pixelColor = GetPixelColor(x, y);

                // NOTE: GDI colors are BGR, not ARGB.
                uint colorRef = (uint)((pixelColor.B << 16) | (pixelColor.G << 8) | (pixelColor.R));
                GDI.SetPixel(hdc, x, y, colorRef);
            }
        }

        e.Graphics.ReleaseHdc(hdc);
    }
    ...
}

3
绝佳的方法是创建一个位图并将其传递给现有数组的intptr(指针)。这样,数组和位图数据可以共享相同的内存...不需要使用Bitmap.lockbits/Bitmap.unlockbits,这两者都很慢。
下面是大体的概述:
1. 将函数标记为“unsafe”,并设置项目构建设置以允许“unsafe”代码!(C#指针) 2. 创建您的数组[,]. 使用Uint32、Bytes或允许通过Uint32或单独的Uint8访问的结构来创建(通过使用显式字段偏移量) 3. 使用System.Runtime.InteropServices.Marshal.UnsafeAddrOfPinnedArrayElement获取指向数组开头的Intptr。 4. 使用带有IntPtr和Stride参数的构造函数创建位图。这将使新位图与现有数组数据重叠。 现在您可以永久直接访问像素数据!
底层数组
底层数组可能是用户结构Pixel的二维数组。为什么?好吧...结构可以通过使用显式固定偏移量使多个成员变量共享相同的空间!这意味着该结构可以具有4个单字节成员(.R, .G, .B和.A),以及3个重叠的Uint16(.AR, .RG和.GB)...和一个单个Uint32(.ARGB)...这可以使颜色平面操作更快。
由于R、G、B、AR、RG、GB和ARGB都访问相同32位像素的不同部分,因此可以以高度灵活的方式操作像素!
因为Pixel[,]的数组与位图本身共享相同的内存,所以图形操作立即更新Pixel数组-并且对数组的Pixel[,]操作立即更新位图!现在您有多种操作位图的方法。
请记住,使用此技术,您无需使用“lockbits”将位图数据编组到缓冲区中...这很好,因为lockbits非常慢。
您也不需要使用刷子并调用复杂的框架代码来绘制图案、可缩放、可旋转、可平移、可别名的矩形...只需写一个像素。相信我-图形类中所有这些灵活性使得使用Graphics.FillRect绘制单个像素成为一个非常缓慢的过程。
其他好处
超级流畅的滚动!您的Pixel缓冲区可以比画布/位图更大,无论是高度还是宽度!这使得滚动非常有效!
如何?
好的,当你从数组创建一个位图时,可以通过获取该Pixel[,]的IntPtr将位图的左上坐标指向某个任意的[y,x]坐标。
然后,通过有意地设置位图的“步幅”以匹配数组的宽度(而不是位图的宽度),你可以渲染较大数组的预定义子集矩形...同时在看不见的边缘绘制(提前)!这就是平滑滚动中“离屏绘制”的原理。
最后
你真的应该将位图和数组封装到FastBitmap类中。这将帮助你控制数组/位图对的生命周期。显然,如果数组超出范围或被销毁 - 位图将指向非法内存地址。通过将它们包装在FastBitmap类中,你可以确保这种情况不会发生......这也是放置各种你不可避免想要添加的实用程序的非常方便的地方...比如滚动、淡入淡出、处理颜色平面等等。
记住:
1. 从MemoryStream创建位图非常慢 2. 使用Graphics.FillRect绘制像素非常低效 3. 使用lockpixels/unlockpixels访问底层位图数据非常慢 4. 如果你正在使用System.Runtime.InteropServices.Marshal.Copy,那就停止吧!
将位图映射到一些现有的数组内存中是正确的方法。做得好,你将永远不需要/想使用框架位图了。

2
我在12年前提出了这个问题,现在仍然在得到答案。 这听起来像是一个非常有价值的方法,我同意应该将其制作成一个类...我希望你已经做到了。如果您能发布这样一个类的代码并使我们免于“重新发明轮子”,那将真正有所帮助。(即使它不完整。) - Mark T
工作正常的代码会非常好。 - john k

2
使用DashStyle.DashStyle.Dot的画笔绘制2px的线条,会绘制单个像素。
    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        using (Pen p = new Pen(Brushes.Black))
        {
            p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
            e.Graphics.DrawLine(p, 10, 10, 11, 10);
        }
    }

2

如果您正在使用SmoothingMode = AntiAlias绘制图形,大多数绘图方法将绘制多个像素。如果您只想绘制一个像素,请创建一个1x1位图,将位图的像素设置为所需的颜色,然后在图形上绘制该位图。

using (var pixel = new Bitmap(1, 1, e.Graphics))
{
    pixel.SetPixel(0, 0, color);
    e.Graphics.DrawImage(pixel, x, y);
}

1

显然,DrawLine 画的线比实际指定的长度短一个像素。似乎没有 DrawPoint/DrawPixel/等等这样的函数,但是你可以使用将宽度和高度设置为1的 DrawRectangle 函数来绘制单个像素。


2
不错的尝试,但大小为1时它会绘制2x2像素。(将其绘制在一条线旁边,您可以看到它的宽度是两倍。) 当大小为零时,它当然什么也不绘制。 - Mark T
好的,我的错误。 :) FillRectangle 很管用。 - Rytmis

0
你应该将代码放在 Paint 事件中,否则它不会被刷新并会自我删除。
示例:
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {

    }

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        Brush aBrush = (Brush)Brushes.Red;
        Graphics g = this.CreateGraphics();

        g.FillRectangle(aBrush, 10, 10, 1, 1);
    }
}

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