这是我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()来自动记牌时,记一段时间就出错了,是什么问题呢?
该采用什么算法来激活捕捉分析程序呢?欢迎大家跟我探讨。
分享到:
相关推荐
易语言源码易语言QQ斗地主记牌器源码.rar
基于python+opencv实现的欢乐斗地主记牌器系统源码.zip 代码完整下载可用。 基于python+opencv实现的欢乐斗地主记牌器系统源码.zip 代码完整下载可用。基于python+opencv实现的欢乐斗地主记牌器系统源码.zip 代码...
迷你qq斗地主记牌器.zip
QQ斗地主记牌器源码.zip易语言项目例子源码下载QQ斗地主记牌器源码.zip易语言项目例子源码下载 1.合个人学习技术做项目参考 2.适合学生做毕业设计参考 3.适合小团队开发项目参考
我的知乎文章配套资料 供大家免费下载
斗地主小游戏手动记牌器
QQ斗地主记牌器源码
易语言QQ斗地主记牌器源码
基于python+opencv实现的欢乐斗地主记牌器系统源码
易语言斗地主记牌器源码
基于python+opencv实现的欢乐斗地主记牌器系统源码+项目说明(数字图像处理课程设计).zip 把图像分成上中下三块,分别是地主牌、对手出牌、自己手牌,计算出大致区域(直接切片选取) 读取图片,转为SHV提取白色区域...
HOOK欢乐斗地主内置自动记牌器 使用一个注入器即可 注入器我就不开源了 论坛大把的 CE注入也行!
AI欢乐斗地主Python项目是一个融合了先进人工智能技术的斗地主游戏实现。该项目不仅仅包括游戏的核心逻辑和界面设计,还涵盖了AI出牌策略等多个关键部分。通过Python编程语言,我们可以实现斗地主游戏的自动化运行,...
基于C#的扑克牌记牌器
在本文中,我们将深入探讨如何使用C++编程语言来实现斗地主的算法,并结合课程设计的背景,探讨计算机博弈的原理以及如何构建一个能够与其他程序对战的斗地主游戏。首先,我们要理解计算机博弈的基本概念,它涉及到...
QQ游戏“升级”的自动记牌器。 应用场景1:一边玩QQ游戏的升级,一边在python的运行环境里运行 recorder/SHENGJI.py。 该脚本可: 实时自动记录每局出牌,并推演剩下什么牌。(记牌器功能) 每局游戏结束时自动保存...
易语言QQ保皇记牌器源码
很早之前写的。。写了两种方法。都是图色。。w10电脑玩的。。换系统要重新做字库。。最近自己租了个TX云搭建游戏在玩雷霆。全套完整游戏图色脚本写的差不多了。...在调试bug 写的没问题....Tags:记牌器。
地主跑得快万能记牌器