如何读取屏幕像素的颜色

53

好的,我正在寻找一个函数或其他东西,可以读取我的显示器上某个像素点的颜色,当检测到该颜色时,将启用另一个函数。我想使用RGB来实现。感谢所有的帮助。


1
您想要监视整个桌面还是特定应用程序的像素变化? - Eric J.
某个像素。比如说在125, 130这个位置的像素,我需要程序等待,直到检测到该像素的RGB值达到特定的RGB值。 - Brandon
3
为什么你需要这样做呢?看起来似乎有某种潜在的原因,如果您能提供更多信息,我们可能会能够给出更好的方法来达到同样的目的。您是在编写游戏并需要确定两个对象何时碰撞吗?还是您正在尝试确定某个程序是否已启动? - Lee
这是一个游戏,用于检查它是否处于特定的屏幕上,如果是,则开始执行一个函数。 - Brandon
3
你应该知道游戏进展到了哪个屏幕,因为你掌控着代码... - ChrisF
6个回答

55

这是最有效率的方法:它获取光标位置的像素,而不仅仅依赖于有一个显示器。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Diagnostics;

namespace FormTest
{
    public partial class Form1 : Form
    {
        [DllImport("user32.dll")]
        static extern bool GetCursorPos(ref Point lpPoint);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
        public static extern int BitBlt(IntPtr hDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, int dwRop);

        public Form1()
        {
            InitializeComponent();
        }

        private void MouseMoveTimer_Tick(object sender, EventArgs e)
        {
            Point cursor = new Point();
            GetCursorPos(ref cursor);

            var c = GetColorAt(cursor);
            this.BackColor = c;

            if (c.R == c.G && c.G < 64 && c.B > 128)
            {
                MessageBox.Show("Blue");
            }
        }

        Bitmap screenPixel = new Bitmap(1, 1, PixelFormat.Format32bppArgb);
        public Color GetColorAt(Point location)
        {
            using (Graphics gdest = Graphics.FromImage(screenPixel))
            {
                using (Graphics gsrc = Graphics.FromHwnd(IntPtr.Zero))
                {
                    IntPtr hSrcDC = gsrc.GetHdc();
                    IntPtr hDC = gdest.GetHdc();
                    int retval = BitBlt(hDC, 0, 0, 1, 1, hSrcDC, location.X, location.Y, (int)CopyPixelOperation.SourceCopy);
                    gdest.ReleaseHdc();
                    gsrc.ReleaseHdc();
                }
            }

            return screenPixel.GetPixel(0, 0);
        }
    }
}
现在,显然,您不必使用光标的当前位置,但这是一般想法。
编辑:
鉴于上述GetColorAt函数,您可以像这样在屏幕上轮询特定像素,以安全、性能友好的方式。
private void PollPixel(Point location, Color color)
{
    while(true)
    {
        var c = GetColorAt(location);

        if (c.R == color.R && c.G == color.G && c.B == color.B)
        {
            DoAction();
            return;
        }

        // By calling Thread.Sleep() without a parameter, we are signaling to the
        // operating system that we only want to sleep long enough for other
        // applications.  As soon as the other apps yield their CPU time, we will
        // regain control.
        Thread.Sleep()
    }
}

如果你愿意,可以把它放在一个线程里执行,或者从控制台应用程序中执行。我想“随你喜欢”。


+1 指出 CopyFromScreen 可以用于仅捕获屏幕的一小部分区域。 - tster
1
但请注意,每次调用CopyFormScreen都会泄漏一个句柄。 - Lasse V. Karlsen
谢谢你,约翰。这个很好用。我还有一个问题。如果我想让它不断地搜索像素,我该怎么做? - Brandon
我知道这已经晚了10年,但如果有人对@Brandon的问题感兴趣:我使用了一个计时器,设置了每250毫秒(或者您想要搜索的速度)的间隔,然后在计时器触发时进行实际读取。[链接](https://learn.microsoft.com/en-us/dotnet/api/system.timers.timer?view=netframework-4.7.2) - TulsaNewbie

22

这里大多数答案使用的是同一个像素源(桌面 DC)。
关键函数是GetPixel

[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetDesktopWindow();
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetWindowDC(IntPtr window);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern uint GetPixel(IntPtr dc, int x, int y);
[DllImport("user32.dll", SetLastError = true)]
public static extern int ReleaseDC(IntPtr window, IntPtr dc);

public static Color GetColorAt(int x, int y)
{
    IntPtr desk = GetDesktopWindow();
    IntPtr dc = GetWindowDC(desk);
    int a = (int) GetPixel(dc, x, y);
    ReleaseDC(desk, dc);
    return Color.FromArgb(255, (a >> 0) & 0xff, (a >> 8) & 0xff, (a >> 16) & 0xff);
}

我认为这是最干净、最快速的方法。

注意:

如果您在Windows的显示设置中修改了默认文本大小以增强高分辨率显示器上的可读性,则需要相应地调整GetPixel()函数的坐标参数。例如,如果在Windows 7上,光标位置为(x,y),并且文本大小为150%,则需要调用GetPixel(x*1.5, y*1.5)来获取光标下像素的颜色。


1
Habib,您添加的注释不是我的答案的一部分。这不是我的专业知识,而是您的。如果可以,请发表评论。然后您可以获得评论的赞数。此外,我认为在GetPixel中调整x和y是不合适的。最好在GetColorAt方法之外的某个地方检查分辨率调整和光标位置。 - Bitterblue
对于显示器上的文本大小注意事项加1分。如果文本大小不是100%,并且没有正确调整,则该方法将从错误的像素中获取颜色。 - marcus

14

这个函数可以用更短的代码,而且不需要使用 Pinvoke,通过 System.Drawing 实现相同的结果。

Color GetColorAt(int x, int y)
{
    Bitmap bmp = new Bitmap(1, 1);
    Rectangle bounds = new Rectangle(x, y, 1, 1);
    using (Graphics g = Graphics.FromImage(bmp))
        g.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
    return bmp.GetPixel(0, 0);
}

1
完美,谢谢。----- Point CursorPosition = Cursor.Position; this.BackColor = GetColorAt(CursorPosition.X, CursorPosition.Y); - user1853517

6
请检查我之前某个项目中使用的这两个不同函数:
1)该函数获取桌面快照
private void CaptureScreenAndSave(string strSavePath)
        {

            //SetTitle("Capturing Screen...");

            Bitmap bmpScreenshot;

            Graphics gfxScreenshot;
            bmpScreenshot = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height,System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            gfxScreenshot = Graphics.FromImage(bmpScreenshot);
            gfxScreenshot.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
            MemoryStream msIn = new MemoryStream();
            bmpScreenshot.Save(msIn, System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders()[0], null);

            msIn.Close();

            byte[] buf = msIn.ToArray();

            MemoryStream msOut = new MemoryStream();

            msOut.Write(buf, 0, buf.Length);

            msOut.Position = 0;

            Bitmap bmpOut = new Bitmap(msOut);

            try
            {
                bmpOut.Save(strSavePath, System.Drawing.Imaging.ImageFormat.Bmp);
                //SetTitle("Capturing Screen Image Saved...");
            }

            catch (Exception exp)
            {

            }

            finally
            {
                msOut.Close();
            }
        }

2) 此函数接受输入的图像并计算给定像素范围的RGB平均值。

double GetRGBAverageForPixelRange( int istartRange, int iEndRange,  Bitmap oBitmap )
        {
            double dRetnVal = 0 ;
            Color oTempColor ; 
            int i, j ;
            for( int iCounter = istartRange ; iCounter < iEndRange ; iCounter++ )
            {
                i = (iCounter % (oBitmap.Width));
                j = ( iCounter / ( oBitmap.Width ) ) ;
                if (i >= 0 && j >= 0 && i < oBitmap.Width && j < oBitmap.Height )
                {
                    oTempColor = oBitmap.GetPixel(i, j);
                    dRetnVal = dRetnVal + oTempColor.ToArgb();
                }

            }
            return dRetnVal ;
        }

这两个函数一起使用可能会解决你的问题。愉快编码 :)
编辑:请注意,GetPixel函数非常慢。在使用之前要三思。

4
据我所知,最简单的方法是:
  1. 截屏
  2. 查看位图并获取像素颜色
编辑 可能没有办法“等待”像素变成特定颜色。您的程序可能只能循环并定期检查它,直到看到该颜色为止。
例如:
while(!IsPixelColor(x, y, color))
{
    //probably best to add a sleep here so your program doesn't use too much CPU
}
DoAction();

编辑2

以下是一些您可以修改的示例代码。该代码根据给定像素中的当前颜色仅更改标签的颜色。此代码避免了提到的句柄泄漏。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{

    [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
    public static extern int BitBlt(IntPtr hDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, int dwRop);


    Thread t;
    int x, y;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        x = 20;
        y = 50;
        t = new Thread(update);
        t.Start();
    }

    private void update()
    {
        Bitmap screenCopy = new Bitmap(1, 1);
        using (Graphics gdest = Graphics.FromImage(screenCopy))
        {
            while (true)
            {
                //g.CopyFromScreen(new Point(0, 0), new Point(0, 0), new Size(256, 256));
                using (Graphics gsrc = Graphics.FromHwnd(IntPtr.Zero))
                {
                    IntPtr hSrcDC = gsrc.GetHdc();
                    IntPtr hDC = gdest.GetHdc();
                    int retval = BitBlt(hDC, 0, 0, 1, 1, hSrcDC, x, y, (int)CopyPixelOperation.SourceCopy);
                    gdest.ReleaseHdc();
                    gsrc.ReleaseHdc();
                }
                Color c = Color.FromArgb(screenCopy.GetPixel(0, 0).ToArgb());
                label1.ForeColor = c;
            }
        }
    }
}

}


我宁愿不这样做,虽然我已经考虑过了,但这并不是一个选项。像素必须被不断地观察,因为它会在不到一分钟的时间内找到它正在寻找的像素,而且要一直保持这种状态。 - Brandon
那就不要加上睡眠。这个循环最坏情况下可能只需要0.1秒就能完成。 - tster
1
请注意不要运行.CopyFromScreen,它会出现句柄泄漏的问题。最好的方法是使用Win32 API自己实现这段代码。 - Lasse V. Karlsen
1
Bitmap.GetPixel() returns a Color so i see no reason to call ToArgb() and passing it into Color.FromArgb() - Raphael Smit

-1

这行代码大约需要10毫秒。

int retval = BitBlt(hDC, 0, 0, 1, 1, hSrcDC, location.X, location.Y, (int)CopyPixelOperation.SourceCopy);

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