勇哥注: 这个系列是勇哥的小娱乐。这么多年来感觉自己就是工作机器,没有一点属于自己的乐子。 突然想到儿时的小梦想是写个fc的坦克大战。要不,先弄个AI,让它自动玩坦克大战吧。
fc的坦克大战是fc模拟器的游戏,要做自动打怪的AI,可以考虑连续窗口截图,然后交由halcon进行处理。
处理结果交由策略代码处理,最后发布手柄控制指令。
因此,首先先要考虑窗口的连续帧如何实时截取,然后就是如何实现虚拟手柄(或者虚拟键盘按键也可以)。
这两点做不到,就玩不下去了。
图1 fc游戏:坦克大战
勇哥首先试一下截取窗体显示的图像。
写了一个小程序,其中Start Grab使用的win32API来取屏幕快照。
Start Grab2是使用了netMarketing类库的WindowSnap类来截屏幕快照。
代码如下:
using netMarketing.winAPI; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { [DllImport("user32.dll")] public static extern bool PrintWindow( IntPtr hwnd, // Window to copy,Handle to the window that will be copied. IntPtr hdcBlt, // HDC to print into,Handle to the device context. UInt32 nFlags // Optional flags,Specifies the drawing options. It can be one of the following values. ); [DllImport("User32.dll", EntryPoint = "FindWindow")] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { IntPtr handle = IntPtr.Zero; try { handle = FindWindow(null, textBox1.Text); pictureBox1.Image = GetWindowImage(handle); } catch (Exception ex) { MessageBox.Show($"{ex.Message}" ); } } //传入窗口句柄,获取该窗口的图像信息 private Image GetWindowImage(IntPtr windownHandle) { Bitmap image = new Bitmap(600, 600); Graphics gp = Graphics.FromImage(image); IntPtr dc = gp.GetHdc(); PrintWindow(windownHandle, dc, 0); gp.ReleaseHdc(); gp.Dispose(); return image; } private void button2_Click(object sender, EventArgs e) { WindowSnap grab = new WindowSnap(); grab.Title = @"SMYNES 执行H:\娱乐\游戏\FC任天堂红白机中文模拟器\FC任天堂红白机中文模拟器\任天堂FC全集\任天堂FC全集\坦克仔.nes"; grab.GetWindowSnap(); pictureBox1.Image = grab.Image; } } }
由于需要知道待截窗口的标题,可以用C++的 spy++工具来取。
这个小工具是vs2017自带的。
如下所示的办法取得窗体标题,即下图中的“文本”
程序跑的结果来看,我们只截取到了窗体,但是显示内容为空白。
即使是netMarketing类的WindowSnap类也取不到内容。
因为上面是使用Win32API, PrintWindow()进行的抓图。
我们知道按键盘的 PrtSc键,就是屏幕取图, 用的也应该是PrintWindow(猜测,没有证实)。
所以勇哥试着按了一下PrtSc键,然后粘贴到“画图”程序中,发现是可以看到游戏窗口的内容的。
这暗示了可能PrintWindow的调用参数有问题。
查了一下微软的api说明,说这个PrintWindow的第三个参数,可以取值0,1
说的是默认为0, 即打印整个窗口内容,而为1则只复制工作区的内容(即除标题栏和状态栏之外的部分)。
勇哥试了一下,无论是0,还1都不行。
后来参考了一位大神的文章,人家说,还有一个值是2,即这个api后来升级了,提供了对DirecX的支持。
如果没有这个支持,则是截取不到使用DirecX显示的游戏画面的。
(微软官方文档居然这一点没写清楚,真是少见情况。。。。。)
如下:
//只有窗口的工作区被复制到hdcBlt。默认情况下,复制整个窗口 [Description( "Only the client area of the window is copied to hdcBlt. By default, the entire window is copied.")] PW_CLIENTONLY = 0x00000001, //适用于使用DirectX或DirectComposition的窗口 [Description("works on windows that use DirectX or DirectComposition")] PW_RENDERFULLCONTENT = 0x00000002
在winapi中有这么一种设计,使用 | 逻辑运算叠加两个属性功能。
比如上面的参数,我希望打印窗口的工作区,并且可以复制DirectX画面。
这个时候可以 1 | 2,结果就是3。
用windows定义的常量来表示,就是:
(uint)Win32Consts.PrintWindowMode.PW_CLIENTONLY | (uint)Win32Consts.PrintWindowMode.PW_RENDERFULLCONTENT
现在可以了。
程序计时发现,拍一帧花了22毫秒。
这样一秒最快可以截图45帧,这个速度是满足PAL制式的帧速要求的。
我们知道中国大陆的电视采用PAL制式的帧率是25帧/每秒。
当年FC的红白机就是用电视机做显示终端的。
因此,连续采图的要求使用最简单的PrintWindow就可以满足要求了。
下一篇我们讨论一下如果虚拟按键盘(或者虚拟按手柄)
改动后的代码:
using netMarketing.winAPI; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { [DllImport("user32.dll")] public static extern bool PrintWindow( IntPtr hwnd, // Window to copy,Handle to the window that will be copied. IntPtr hdcBlt, // HDC to print into,Handle to the device context. UInt32 nFlags // Optional flags,Specifies the drawing options. It can be one of the following values. ); [DllImport("User32.dll", EntryPoint = "FindWindow")] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { IntPtr handle = IntPtr.Zero; var sw1 = new Stopwatch(); try { sw1.Start(); handle = FindWindow(null, textBox1.Text); pictureBox1.Image = GetWindowImage(handle); var t1 = sw1.ElapsedTicks; var f1 = Stopwatch.IsHighResolution; var t2= (1000L * 1000L * 1000L) / Stopwatch.Frequency; var t3 = t1 * t2 / (1000L * 1000L); //毫秒 sw1.Stop(); } catch (Exception ex) { MessageBox.Show($"{ex.Message}" ); } } //传入窗口句柄,获取该窗口的图像信息 private Image GetWindowImage(IntPtr windownHandle) { Bitmap image = new Bitmap(600, 600); Graphics gp = Graphics.FromImage(image); IntPtr dc = gp.GetHdc(); PrintWindow(windownHandle, dc, (uint)Win32Consts.PrintWindowMode.PW_CLIENTONLY | (uint)Win32Consts.PrintWindowMode.PW_RENDERFULLCONTENT); gp.ReleaseHdc(); gp.Dispose(); return image; } private void button2_Click(object sender, EventArgs e) { WindowSnap grab = new WindowSnap(); grab.Title = @"SMYNES 执行H:\娱乐\游戏\FC任天堂红白机中文模拟器\FC任天堂红白机中文模拟器\任天堂FC全集\任天堂FC全集\坦克仔.nes"; grab.GetWindowSnap(); pictureBox1.Image = grab.Image; } } public sealed class Win32Consts { public enum DibColorMode : uint { DIB_RGB_COLORS = 0x00, DIB_PAL_COLORS = 0x01, DIB_PAL_INDICES = 0x02 } public enum BitmapCompressionMode : uint { BI_RGB = 0, BI_RLE8 = 1, BI_RLE4 = 2, BI_BITFIELDS = 3, BI_JPEG = 4, BI_PNG = 5 } public enum RasterOperationMode : uint { SRCCOPY = 0x00CC0020, SRCPAINT = 0x00EE0086, SRCAND = 0x008800C6, SRCINVERT = 0x00660046, SRCERASE = 0x00440328, NOTSRCCOPY = 0x00330008, NOTSRCERASE = 0x001100A6, MERGECOPY = 0x00C000CA, MERGEPAINT = 0x00BB0226, PATCOPY = 0x00F00021, PATPAINT = 0x00FB0A09, PATINVERT = 0x005A0049, DSTINVERT = 0x00550009, BLACKNESS = 0x00000042, WHITENESS = 0x00FF0062, CAPTUREBLT = 0x40000000 //only if WinVer >= 5.0.0 (see wingdi.h) } public enum PrintWindowMode : uint { //只有窗口的工作区被复制到hdcBlt。默认情况下,复制整个窗口 [Description( "Only the client area of the window is copied to hdcBlt. By default, the entire window is copied.")] PW_CLIENTONLY = 0x00000001, //适用于使用DirectX或DirectComposition的窗口 [Description("works on windows that use DirectX or DirectComposition")] PW_RENDERFULLCONTENT = 0x00000002 } } }
参考资料:
C#使用BitBlt进行窗口抓图 https://www.cnblogs.com/xhubobo/p/12789466.html
C#抓图服务 https://www.cnblogs.com/xhubobo/p/12809988.html
C#使用PrintWindow进行窗口抓图 https://www.cnblogs.com/xhubobo/p/12789482.html
源码下载:
------------------------------
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!

