勇哥注: 这个系列是勇哥的小娱乐。这么多年来感觉自己就是工作机器,没有一点属于自己的乐子。 突然想到儿时的小梦想是写个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
版权声明:本文为博主原创文章,转载请附上博文链接!


少有人走的路


















