这是我2005年写的一个小程序,原来放在BOKEE上的,最近BOKEE经常打不开,打开的时候也是极慢、图片显不出来,中国最早的博客要关门了?为了不把程序弄丢了,还是转移到这里来吧。
原文:
由于前段时间玩起了QQ斗地主,从网上下载了一个QQ斗地主记牌器,但无法使用,于是就想自己开发一个。
一、记牌器实现的原理
当时查阅了一些资料,想到了三种实现的方法:
1、拦截IP包。这种方法应该是可行的,只是从查阅的资料来看,现在的游戏IP包都经过加密处理了,而且分析IP包的工作量比较大,所以就没采用这种方法。这方面使用的软件有:WPE(拦截发送封包),m2m封包分析。
2、拦截QQ斗地主显示扑克牌的函数。用W32DSM分析了斗地主的几个相关exe和dll竟然没找到显示扑克牌的函数:(。一开始觉得最可疑的应该是QQGame目录下的CardRes.dll,但分析后发现它竟没有程序出口。
3、屏幕抓图。感觉这种方法是最简单的了,觉得纯粹就是抓图,然后分析图形就行了。当时下载的记牌器应该也是应用这种方法实现的,但是它又不能用,网上又有评论说它被腾讯怎么给弄失效了。不管了,试试再说。
二、从图形分析开始
这是抓获的每张牌的区域,下图为放大后的效果,图中绿色网格为参考坐标,经过分析,发现只要使用12行X4列的区域就可以确定出牌的大小,使用20行X4列的区域就可以确定出大小和类型了。刚开始的时候,打算每次捕抓一张牌的区域,但是这样要想定的定义一系列的HBITMAP,不方便,所以就改为一列牌捕捉分析一次。考虑到一家的牌最多20张,而20张牌的屏幕区域为356X20像素。
分析牌大小和类型的函数如下:
//判断hdc中的牌
//////////////////////////////////////////////////////
BOOL CCard::Check(HDC hdc)
{
BYTE g_color, color1, color2;
// int i=1;
int nCardWidth=15; //每张牌的宽度为15像素
int nOffset = 2; //牌边框与值之类的像素,见图片说明
for(int i=0; i<20; i++)
{
if(GetColor(hdc, 13, 1+i*nCardWidth)!= 0) //找不到黑色的边框,说明没有牌
continue;
color1=GetColor(hdc, 1, 68+i*nCardWidth);
if(color1 >20 && color1 <150) //表示为底色,此值为随便取的,视底色范围
continue;
if(GetColor(hdc, 1, 1+nOffset+i*nCardWidth) ==0)
{
value[i] = 13; //K
continue;
}
if(GetColor(hdc, 1,2+nOffset+i*nCardWidth) ==0 )
{
if((color1=GetColor(hdc,7,2+nOffset+i*nCardWidth))==0)
{
value[i] = 10; //10
continue;
}
value[i] = 5; //5
continue;
}
if((g_color=GetColor(hdc, 2,2+nOffset+i*nCardWidth)) ==0)
{
value[i] = 12; //Q
continue;
}
if(GetColor(hdc, 12, 2+nOffset+i*nCardWidth) == 0)
{
value[i] = 1; //A
continue;
}
if(GetColor(hdc, 12, 1+nOffset+i*nCardWidth) == 0)
{
value[i] = 1; //A
continue;
}
if(GetColor(hdc, 6, 2+nOffset+i*nCardWidth) == 0)
{
value[i] = 4; //4
continue;
}
if(GetColor(hdc, 9, 2+nOffset+i*nCardWidth)== 0)
{
value[i] =11; //J
continue;
}
if((g_color=GetColor(hdc, 1, 3+nOffset+i*nCardWidth)) == 0)
{
if((color1=GetColor(hdc, 4,3+nOffset+i*nCardWidth)) ==0)
{
value[i] = 5; //5
continue;
}
if((color2=GetColor(hdc, 10, 3+nOffset+i*nCardWidth)) == 0)
{
value[i] = 3; //3
continue;
}
value[i] =7; //7
continue;
}
if(GetColor(hdc, 12, 3+nOffset+i*nCardWidth) ==0)
{
value[i] = 2; //2
continue;
}
if((color1=GetColor(hdc, 2, 3+nOffset+i*nCardWidth)) == 0)
{
if((GetColor(hdc, 6, 3+nOffset+i*nCardWidth)) ==0)
{
value[i] = 9; //9
continue;
}
value[i] = 8; //8
continue;
}
if(GetColor(hdc, 3, 3+nOffset+i*nCardWidth)==0)
{
value[i] = 6; //6
continue;
}
if(GetColor(hdc,1,1+nOffset+i*nCardWidth) == 255)
{
value[i] = 0; //大小王
continue;
}
value[i] = 14;
}
//判断花色
////////////////////////////////////////////////////////////////////
for(i=0; i<20; i++)
{
if(value[i] == 14) //无牌
{
type[i]=4;
continue;
}
if(value[i] == 0) //王
{
type[i]=0; //先默认为大王
if(GetColor(hdc, 8, 4+nOffset+i*nCardWidth) < 250)
type[i]=1; //小王
continue;
}
if(value[i]>0 && value[i]<11) //牌为A、2-10时,花色的区域相同,为一种
{
if(GetColor(hdc, 20, 2+nOffset+i*nCardWidth) == 255) //方片A
{
type[i]=3;
continue;
}
if(GetColor(hdc, 17, 2+nOffset+i*nCardWidth) == 0) //红桃
{
type[i]=1;
continue;
}
if(GetColor(hdc, 18, 2+nOffset+i*nCardWidth) == 0) //方片
{
type[i]=3;
continue;
}
if(GetColor(hdc, 18, 4+nOffset+i*nCardWidth) == 0) //黑桃
{
type[i]=0;
continue;
}
type[i]=2; //茶花
continue;
}
if(value[i]>=11 && value[i]<=13) //牌值为J-K时,花色区域为另一种
{
if(GetColor(hdc, 17, 1+nOffset+i*nCardWidth) == 0) //红桃
{
type[i]=1;
continue;
}
if(GetColor(hdc, 19, 1+nOffset+i*nCardWidth) == 255)//方片
{
type[i]=3;
continue;
}
if(GetColor(hdc, 16, 3+nOffset+i*nCardWidth) == 0) //黑桃
{
type[i]=2;
continue;
}
type[i]=0; //茶花
continue;
}
}
return TRUE;
}
//取得第row行,col列的G值,注意参数为先行后列,而且是1开始而非从0开始的
//原先是为了计算单张牌的方便,才采用了行列而非坐标,以至后来调试多花了好多时间^-^(是忘了这个规则了)
BYTE CCard::GetColor(HDC hdc, int row, int col)
{
COLORREF crColor;
BYTE temp;
crColor = GetPixel(hdc, col-1, row-1); //取得第row行,col列的RGB值
//分离出蓝色值,为什么用蓝色呢,看看抓图的像素就知道了
//红色的G值为0,黑色的G值为0,而白色的G值为255,所以判断一下G值就知道是什么牌了
temp=GetGValue(crColor); //分离出蓝色值
return temp;
}
上图为各捕捉区域左上角的屏幕坐标,自己、左家、右家的牌一次捕捉356X20的区域,进行一次分析。底牌区域的坐标仅用于参考,视算法而定要不要进行分析(我的程序因为是旁观别人的牌进行分析的,自己的牌是一次分析出来的,后来自己玩的时候发现了一个问题,自己当地主时,底牌拿到手时跟其它的牌不是成一条线的!所以还要用到底牌区的区域来分析)。
三、屏幕抓图函数
可参见我的博客中其它的文章。我的思路是自己的牌抓一次,左右家的牌两家一起抓一次。因为左右家出的牌较多时,两家的牌会有一部分重叠在一起,但是经过分析,可以知道这两部分每张牌之间都是错开的,所以还是可以一次处理的。
另:使用spy++分析QQ斗地主的各个窗口坐标,可知:主窗口的左上角坐标为(-4,-4),而牌区的左上角坐标为(1,30),牌区的长宽为736X696像素。
我用的函数如下:
void CCaptureDlg::OnMiddle()
{
// TODO: Add your control notification handler code here
CCard cardLord;
int nLordX=212, nLordY=554;
/*
HDC hdcScreen, hMemDC;
HBITMAP hBitmap, hOldBitmap;
HBITMAP hBitmap1, hOldBitmap1;
//建立一个屏幕设备环境句柄
hdcScreen = CreateDC("DISPLAY", NULL, NULL, NULL);
hMemDC = CreateCompatibleDC(hdcScreen);
hMiddleDC = CreateCompatibleDC(hdcScreen);
*/ //建立一个与屏幕设备环境句柄兼容、与鼠标所在处的窗口的区域等大的位图
hBitmap = CreateCompatibleBitmap(hdcScreen, nWidth, nHeight);
hBitmap1 = CreateCompatibleBitmap(hdcScreen, 356, 20);
// 把屏幕设备描述表拷贝到内存设备描述表中
hOldBitmap = (HBITMAP) SelectObject(hMemDC, hBitmap);
hOldBitmap1 = (HBITMAP) SelectObject(hMiddleDC, hBitmap1);
BitBlt(hMemDC, 0, 0, nWidth, nHeight, hdcScreen, rectCapture.left+5, rectCapture.top+34,SRCCOPY);
if(cardLord.GetColor(hMemDC, nLordY, nLordX-1) ==255)
nLordX -= 22;
BitBlt(hMiddleDC, 0, 0, 356,20, hMemDC, nLordX, nLordY, SRCCOPY);
cardLord.Check(hMiddleDC);
/* CString str1, str2, str3;
str2.Empty();
str2.Format(_T("自己的牌:\n"));
for(int i=0; i<20; i++)
{
str1.Format(_T("%4d"), cardLord.GetValue(i));
str2 += str1;
str1.Format(_T("%4d"), cardLord.GetType(i));
str3 += str1;
}
str2+="\n";
str2+=str3;
AfxMessageBox(str2);
*/
hBitmap =(HBITMAP)SelectObject(hMemDC, hOldBitmap);
hBitmap1 =(HBITMAP)SelectObject(hMiddleDC, hOldBitmap1);
/*
DeleteDC(hMiddleDC);
DeleteDC(hdcScreen);
DeleteDC(hMemDC);
// 返回位图句柄
//打开剪贴板,并将位图拷到剪贴板上
OpenClipboard() ;
EmptyClipboard();
SetClipboardData(CF_BITMAP, hBitmap1);
//关闭剪贴板
CloseClipboard();
*/
// MessageBox("屏幕内容已经拷到剪贴板上!");
//终止鼠标捕获
ReleaseCapture();
//恢复窗口显示模式
// ShowWindow(SW_NORMAL);
DeleteItem(cardLord);
}
void CCaptureDlg::OnLeft()
{
// TODO: Add your control notification handler code here
// KillTimer(1);
CCard cardLeft, cardRight;
int nLeftX=172, nLeftY=272;
int nRightX=208, nRightY=272;
/* HDC hdcScreen, hMemDC;
HBITMAP hBitmap, hOldBitmap;
HBITMAP hBitmap1, hOldBitmap1;
HBITMAP hBitmap2, hOldBitmap2;
//建立一个屏幕设备环境句柄
hdcScreen = CreateDC("DISPLAY", NULL, NULL, NULL);
hMemDC = CreateCompatibleDC(hdcScreen);
hLeftDC = CreateCompatibleDC(hdcScreen);
hRightDC = CreateCompatibleDC(hdcScreen);
*/
//建立一个与屏幕设备环境句柄兼容、与鼠标所在处的窗口的区域等大的位图
hBitmap = CreateCompatibleBitmap(hdcScreen, nWidth, nHeight);
hBitmap1 = CreateCompatibleBitmap(hdcScreen, 356, 20);
hBitmap2 = CreateCompatibleBitmap(hdcScreen, 356, 20);
// 把屏幕设备描述表拷贝到内存设备描述表中
hOldBitmap = (HBITMAP) SelectObject(hMemDC, hBitmap);
hOldBitmap1 = (HBITMAP) SelectObject(hLeftDC, hBitmap1);
hOldBitmap2 = (HBITMAP) SelectObject(hRightDC, hBitmap2);
BitBlt(hMemDC, 0, 0, nWidth, nHeight, hdcScreen, rectCapture.left+5, rectCapture.top+34,SRCCOPY);
BitBlt(hLeftDC, 0, 0, 356,20, hMemDC, nLeftX, nLeftY, SRCCOPY);
BitBlt(hRightDC, 0, 0, 356,20, hMemDC, nRightX, nRightY, SRCCOPY);
cardLeft.Check(hLeftDC);
cardRight.Check(hRightDC);
DeleteItem(cardLeft);
DeleteItem(cardRight);
/*
DeleteDC(hdcScreen);
DeleteDC(hMemDC);
DeleteDC(hLeftDC);
DeleteDC(hRightDC);
*/
hBitmap =(HBITMAP)SelectObject(hMemDC, hOldBitmap);
hBitmap1 =(HBITMAP)SelectObject(hLeftDC, hOldBitmap1);
hBitmap2 =(HBITMAP)SelectObject(hRightDC, hOldBitmap2);
// SetTimer(1, 2000, NULL);
}
四、主程序
主程序采用4个列表来记牌,为什么不用一个列表来实现呢?因为列表只能在第一列能够同时显示图标和文字,所以用了4个列表。下方的5个按钮是用来测试各个函数的,调试用的。代码如下:
BOOL CCaptureDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
//加入WS_EX_LAYERED扩展属性
//设置窗口为半透明
SetWindowLong(this->GetSafeHwnd(),GWL_EXSTYLE,
GetWindowLong(this->GetSafeHwnd(),GWL_EXSTYLE)^0x80000);
HINSTANCE hInst = LoadLibrary("User32.DLL");
if(hInst)
{
typedef BOOL (WINAPI *MYFUNC)(HWND,COLORREF,BYTE,DWORD);
MYFUNC fun = NULL;
//取得SetLayeredWindowAttributes函数指针
fun=(MYFUNC)GetProcAddress(hInst, "SetLayeredWindowAttributes");
if(fun)fun(this->GetSafeHwnd(),0,255,2);
FreeLibrary(hInst);
}
//将窗口设为最前
::SetWindowPos(this->m_hWnd,HWND_TOPMOST,815,395,0,0,SWP_NOSIZE);
// return TRUE unless you set the focus to a control
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
bCapture=FALSE;
bLord = FALSE;
bMiddle = FALSE;
bLeft = FALSE;
bRight = FALSE;
bMiddle1 = FALSE;
bLeft1 = FALSE;
bRight1 = FALSE;
hwndCapture = ::FindWindow(NULL, "斗地主");
::GetWindowRect(hwndCapture,&rectCapture);
nWidth=rectCapture.Width();
nHeight=rectCapture.Height();
if(nWidth != 1032 || nHeight != 746)
{
MessageBox("斗地主的主窗口尺寸不对!\n\n检查显示器分辨率:请调整为1024*768\n并将斗地主窗口最大化\n\n不便之处,敬请原谅!");
exit(0);
}
InitList();
//建立一个屏幕设备环境句柄
hdcScreen = CreateDC("DISPLAY", NULL, NULL, NULL);
hMemDC = CreateCompatibleDC(hdcScreen);
hMiddleDC = CreateCompatibleDC(hdcScreen);
hLeftDC = CreateCompatibleDC(hdcScreen);
hRightDC = CreateCompatibleDC(hdcScreen);
SetTimer(1,2000,NULL);
return TRUE; // return TRUE unless you set the focus to a control
}
//初始化4个列表
////////////////////////////////////////////////////////////////////////////////////////////////
void CCaptureDlg::InitList()
{
int i,j;
char str1[15][5]={"3","4","5","6","7","8","9","10","J","Q","K","A","2","王","无牌"};
//载入4种花色的图标
SmallImage.Create(14, 14, ILC_COLOR8 | ILC_MASK, 4, 1);
CBitmap cBmp;
cBmp.LoadBitmap(IDB_BITMAP1);
SmallImage.Add(&cBmp, RGB(255,255,255));
cBmp.DeleteObject();
cBmp.LoadBitmap(IDB_BITMAP2);
SmallImage.Add(&cBmp, RGB(255,255,255));
cBmp.DeleteObject();
cBmp.LoadBitmap(IDB_BITMAP3);
SmallImage.Add(&cBmp, RGB(255,255,255));
cBmp.DeleteObject();
cBmp.LoadBitmap(IDB_BITMAP4);
SmallImage.Add(&cBmp, RGB(255,255,255));
cBmp.DeleteObject();
//初始化4个列表
for(i=0; i<4; i++)
{
m_list[i].SetImageList(&SmallImage, LVSIL_SMALL);
m_list[i].InsertColumn(0,"");
m_list[i].SetColumnWidth(0,50);
}
LVITEM lvi;
CString strItem;
for(j=0; j<4; j++)
for(i=0;i<13;i++)
{
lvi.mask = LVIF_IMAGE|LVIF_TEXT;
lvi.iItem = i;
lvi.iSubItem = 0;
lvi.iImage=j;
strItem.Format(_T("%s"),str1[i]);
lvi.pszText = (LPTSTR)(LPCTSTR)(strItem);
m_list[j].InsertItem(&lvi);
}
//记下大小王
iconJoke1 = m_joke1.GetIcon();
iconJoke2 = m_joke2.GetIcon();
bMiddle1 = TRUE;
}
使用主界面下方的按钮来测试各个函数,可以正常捕捉记牌,但是使用OnTimer()来自动记牌时,记一段时间就出错了,是什么问题呢?
该采用什么算法来激活捕捉分析程序呢?欢迎大家跟我探讨。