長い間テクノロジー関連の記事を書いていませんでしたが、今回は興味深いゲーム - QQ Find Difference、興味深い言語 - Python、そして興味深いライブラリ - Qt についての興味深い記事を書きます。 。
QQのみんなであら探し(美少女あら探し)の補助プラグインです。 開発のきっかけは、父が毎日このゲームをプレイしていて、スコアがマイナス4000というひどい結果だったことがきっかけです。ゲームをするのは彼自身の楽しみであり、私は自分の楽しみと彼を喜ばせるためにこれをやっているだけです。私たちの両親の前で。もともとはとてもシンプルなものを書きたかったのですが、途中で父に何度も冷やかされたりしたため、必死に改善し、最終的に小さな製品を形にしました。
私が Python に出会ったのは 2010 年で、昨年退職する前に、非常に人気があった Python+wxPython 作業ツールを退職しました。 。会社を変えた後、C++&Qtを一生懸命勉強しましたが、PyQtではなくwxPythonを選択したことを後悔し、同じ仕事を続けることができませんでした。 Qt を長く使うほどに、この記事を書いたときにも Qt を使用するようになりました。
早速、本題に入りましょう。これは完全なコードの説明ではありませんが、後に放棄されたいくつかの技術的なポイントを含む、プロセス内のいくつかのテクノロジーを共有しています。最初にこれらを探すのは非常に面倒でしたので、ここにメモしておきます。後発の人はいくつかの利点を見つけることができるかもしれません。
最初の写真:
そういえば、この女の子はゲーム内で最も写真を撮られている女の子ですが、QQ と何の関係があるのでしょうか?
補助ツールはゲーム内に 2 つのボタンを追加します。「比較」をクリックすると、「相違点」が自動的に検出され、小さな青い枠でマークが付けられます。「消去」をクリックすると、マークが消えます。
ゲームウィンドウの探索
これには、Windows インターフェイスの Python カプセル化である PyWin32 ライブラリを使用する必要があります。基本的に、VC で実行できることはすべて実行できます。
ダウンロード アドレス: http://sourceforge.net/projects/pywin32/ ですが、[ダウンロード] アイコンを直接クリックすることはできません。直接クリックしないと、Readme.txt が表示されます。[すべてのファイルを参照] をクリックして必要なバージョンを見つけてください。
#coding=gbk import win32gui game_hwnd = win32gui.FindWindow("#32770", "大家来找茬") print game_hwnd
QQ Find Differences は、クラス「#32770」のダイアログ ウィンドウです。デスクトップ上にこのようなウィンドウが多数あるため、「Let's Find Differences」というタイトルにも一致します。また、中国語であるため、最初の行で gbk の使用を指定しています。エンコーディング、それ以外の場合は、見つからないか、実行時にエラーが発生します。
ゲーム画像抽出
写真を抽出する方法は、ウィンドウを見つけたら、そのウィンドウを最前面に持ってきて、そのウィンドウのスクリーンショットを撮ります。画面キャプチャには有名な Python Imaging Library (PIL) ライブラリが使用されます。
import ImageGrab import win32con win32gui.ShowWindow(game_hwnd, win32con.SW_RESTORE) # 强行显示界面后才好截图 win32gui.SetForegroundWindow(game_hwnd) # 将游戏窗口提到最前 # 裁剪得到全图 game_rect = win32gui.GetWindowRect(game_hwnd) src_image = ImageGrab.grab((game_rect[0] + 9, game_rect[1] + 190, game_rect[2] - 9, game_rect[1] + 190 + 450)) # src_image.show() # 分别裁剪左右内容图片 left_box = (9, 0, 500, 450) right_box = (517, 0, 517 + 500, 450) image_left = src_image.crop(left_box) image_right = src_image.crop(right_box) # image_left.show() # image_right.show()
上記で使用されている座標は、コードを示すために単に入力されているだけであり、実際には可変パラメーターが使用されており、解像度を区別する必要があります。
PIL は強力な Python グラフィック ライブラリ (使用法ドキュメント) であり、後の比較分析にも使用されます。 ImageGrab は、画像の取得に使用される PIL のモジュールです。パラメータなしの ImageGrab.grab() は、全画面スクリーンショットを取得し、Image オブジェクトを返します。タプルをパラメータとして使用して、インターセプトする範囲 (両方のスクリーンショットの左上と右下の点の座標) を指定することもできます。マウスポインタについては、システムクリップボードから画像を収集できる ImageGrab.grabclipboard() もあります。
イメージを取得した後、デバッグを容易にするために、show() メソッドを使用してシステムのデフォルトのイメージ表示ツールを使用してイメージを開くことができます。また、save(filename) を使用してイメージをファイルとして保存し、対応するイメージを次のコマンドで開くことができます。画像.open(ファイル名)。
左と右の画像を含む Image オブジェクトを取得した後、crop(box) メソッドを使用して、指定された領域をトリミングし、左と右のゲーム画像をそれぞれ取得できます。
2 つの画像を比較して、内容が異なる領域を取得します
2 つの写真を N 個の小さな写真に切り取って、それらを別々に比較することを考えるのは自然です。左右の同じ領域に対応する小さな写真が等しくない場合、唯一の問題は「切り株」の領域です。 2 つの写真の内容が矛盾しているかどうかを判断するには?
最初は少し面倒だろうと思っていましたが、画像の色のヒストグラムを取得するために使用される Image.histogram() 関数を発見しました。私は写真も好きで、ヒストグラムは写真内のさまざまな明るさ (または色) の数を表すことができることを知っています。2 つの自然写真のヒストグラムは、2 つの写真が対称で同じ色で配置が異なる場合を除き、基本的に異なります。それでも、2 つの画像をさらに分割すると、サブ画像のヒストグラムは異なります。ヒストグラムは、グラフを数値に変換したもので、2 つのグラフの色の値を比較することで、違いがあるかどうかを確認できます。
RBG カラー形式の画像。histogram() 関数は長さ 768 の配列を返します。0 ~ 255 は赤の 0 ~ 255 を表し、256 ~ 511 は緑の 0 ~ 255 を表し、512 ~ 767 は青の色を表します。 0 ~ 255 で、数値はその色のピクセル数を表します。したがって、 histogram() リストのすべてのメンバーの合計は、画像のピクセル値 x 3 に等しくなります。
2 つの画像間の数値の差を取得する関数を作成しました:
ef compare(image_a, image_b): '''返回两图的差异值 返回两图红绿蓝差值万分比之和''' histogram_a = image_a.histogram() histogram_b = image_b.histogram() if len(histogram_a) != 768 or len(histogram_b) != 768: return None red_a = 0 red_b = 0 for i in xrange(0, 256): red_a += histogram_a[i + 0] * i red_b += histogram_b[i + 0] * i diff_red = 0 if red_a + red_b > 0: diff_red = abs(red_a - red_b) * 10000 / max(red_a, red_b) green_a = 0 green_b = 0 for i in xrange(0, 256): green_a += histogram_a[i + 256] * i green_b += histogram_b[i + 256] * i diff_green = 0 if green_a + green_b > 0: diff_green = abs(green_a - green_b) * 10000 / max(green_a, green_b) blue_a = 0 blue_b = 0 for i in xrange(0, 256): blue_a += histogram_a[i + 512] * i blue_b += histogram_b[i + 512] * i diff_blue = 0 if blue_a + blue_b > 0: diff_blue = abs(blue_a - blue_b) * 10000 / max(blue_a, blue_b) return diff_red, diff_green, diff_blue
将函数返回的红绿蓝差值相加,如果超过了预定定的阀值2000,则表示该区域不同。这个计算方式有点“土”,但对这次要解决的问题很有效,就没再继续改进。
将左右大图裁剪成多个小图分别进行对比 result = [[0 for a in xrange(0, 50)] for b in xrange(0, 45)] for col in xrange(0, 50): for row in xrange(0, 45): clip_box = (col * 10, row * 10, (col + 1) * 10, (row + 1) * 10) clip_image_left = image_left.crop(clip_box) clip_image_right = image_right.crop(clip_box) clip_diff = self.compare(clip_image_left, clip_image_right) if sum(clip_diff) > 2000: result[row][col] = 1
大图是500x450,分隔成10x10的小块,定义一个50x45的二位数组存储结果,分别比较后将差值大于阀值的数组区域标记为1.
在游戏上标记两边不同的区域
最初我用了PyWin32的一些函数,获得游戏窗口句柄后直接在上面绘制,但我不太熟悉Windows编程,不知道如何解决游戏自身重绘后将我的标记擦除的问题,然后搬来了Qt。用Qt创建了一个和游戏大小一样透明的QWidget窗口,叠加在游戏窗口上,用遮罩来绘制标记。标记数据已记录在result数组中,在指定的位置绘制一个方格则表示该区域左右不同,要注意两个方格间的边界不要绘制,避免格子太多干扰了游戏。除标记外,还绘制了两个按钮来触发对比与擦除。
ef paintEvent(self, event): # 重置遮罩图像 self.pixmap.fill() # 创建绘制用的QPainter,笔画粗细为2像素 # 事先已经在Qt窗体上铺了一个蓝色的背景图片,因此投过遮罩图案看下去标记线条是蓝色的 p = QPainter(self.pixmap) p.setPen(QPen(QBrush(QColor(0, 0, 0)), 2)) for row in xrange(len(self.result)): for col in xrange(len(self.result[0])): if self.result[row][col] != 0: # 定一个基点,避免算数太难看 base_l_x = self.ANCHOR_LEFT_X + self.CLIP_WIDTH * col base_r_x = self.ANCHOR_RIGHT_X + self.CLIP_WIDTH * col base_y = self.ANCHOR_Y + self.CLIP_HEIGHT * row if row == 0 or self.result[row - 1][col] == 0: # 如果是第一行,或者上面的格子为空,画一条上边 p.drawLine(base_l_x, base_y, base_l_x + self.CLIP_WIDTH, base_y) p.drawLine(base_r_x, base_y, base_r_x + self.CLIP_WIDTH, base_y) if row == len(self.result) - 1 or self.result[row + 1][col] == 0: # 如果是最后一行,或者下面的格子为空,画一条下边 p.drawLine(base_l_x, base_y + self.CLIP_HEIGHT, base_l_x + self.CLIP_WIDTH, base_y + self.CLIP_HEIGHT) p.drawLine(base_r_x, base_y + self.CLIP_HEIGHT, base_r_x + self.CLIP_WIDTH, base_y + self.CLIP_HEIGHT) if col == 0 or self.result[row][col - 1] == 0: # 如果是第一列,或者左边的格子为空,画一条左边 p.drawLine(base_l_x, base_y, base_l_x, base_y + self.CLIP_HEIGHT) p.drawLine(base_r_x, base_y, base_r_x, base_y + self.CLIP_HEIGHT) if col == len(self.result[0]) - 1 or self.result[row][col + 1] == 0: # 如果是第一列,或者右边的格子为空,画一条右边 p.drawLine(base_l_x + self.CLIP_WIDTH, base_y, base_l_x + self.CLIP_WIDTH, base_y + self.CLIP_HEIGHT) p.drawLine(base_r_x + self.CLIP_WIDTH, base_y, base_r_x + self.CLIP_WIDTH, base_y + self.CLIP_HEIGHT) # 在遮罩上绘制按钮区域,避免按钮被遮罩挡住看不见 p.fillRect(self.btn_compare.geometry(), QBrush(QColor(0, 0, 0))) p.fillRect(self.btn_toggle.geometry(), QBrush(QColor(0, 0, 0))) # 将遮罩图像作为遮罩 self.setMask(QBitmap(self.pixmap))
这里我没有替换变量,太麻烦了,能看清楚算法就行。
让PyQt程序在任务栏隐藏
为了让PyQt程序不出现在任务栏,构造QWidget设置了这些属性
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Popup | Qt.Tool)
让PyQt程序加入系统托盘、资源文件使用
PyQt添加托盘菜单非常容易,几行代码就可以
创建托盘 self.icon = QIcon(":\icon.png") self.trayIcon = QSystemTrayIcon(self) self.trayIcon.setIcon(self.icon) self.trayIcon.setToolTip(u"QQ找茬助手") self.trayIcon.show() # 托盘气泡消息 self.trayIcon.showMessage(u"QQ找茬助手", u"QQ找茬助手已经待命,进入游戏即可激活") # 托盘菜单 self.action = QAction(u"退出QQ找茬助手", self, triggered = sys.exit) # 触发点击后调用sys.exit()命令,即退出 self.menu = QMenu(self) self.menu.addAction(self.action) self.trayIcon.setContextMenu(self.menu)
最初我是用的托盘图标是一个.ico文件,执行脚本可以正常显示,但打包成exe后执行在托盘上显示为一个空白图标,用Python的idle工具编译运行也是空白。尝试多次后发现:PyQt的托盘图标不能使用.ico文件,否则会显示空白,换成png格式素材就没问题!
PyQt资源文件打包
Qt使用一个.qrc格式的xml文件管理素材,代码用可用:\xxx\xxx.png的方式引用资源文件中的素材,这在PyQt中同样支持。
这里我创建了一个resources.qrc文件
<!DOCTYPE RCC> <RCC version="1.0"> <qresource> <file>icon.png</file> </qresource> </RCC>
然后用
pyrcc4 resources.qrc > resources.py
命令,将资源文件转成一个python模块,在代码中import resources,则可以用这样的方式使用图像素材
self.icon = QIcon(":\icon.png")
打包成可执行程序
这个工具是给别人用的,肯定不能以py脚本的形式发布,我使用了cx_Freeze来打包为可执行程序。
为此要写一个打包命令脚本convert2exe.py
#!Python #coding=gbk # python转exe脚本 # # 安装cx_Freeze # 执行 python convert2exe.py build # 将自动生成build目录, 其下所有文件都必须打包 # import sys from cx_Freeze import setup, Executable base = None if sys.platform == "win32": base = "Win32GUI" buildOptions = dict( compressed = True) setup( name = "ZhaoChaAssistant", version = "1.0", description = "ZhaoChaAssistant", options = dict(build_exe = buildOptions), executables = [Executable("zhaochaassistant.py", base = base, icon = "icon.ico")])
最后执行一个命令
python convert2exe.py build
则会在当前路径下创建个build目录,打包的程序就在其中一个exe.win-amd64-2.7的目录中,运行exe即可执行,与Python无二。可惜这个包太大了一些,整个目录达到了30M。
为了让exe程序也有一个好看的图标,在最后一行中的executables参数中指定了icon = "icon.ico",这个图标就最好使用多页的.ico格式(16x16,32x32,48x48...),让程序在各种显示环境下(桌面、文件夹)都有原生的显示。
如果打包的时候必须使用独立的资源,可在buildOptions字典参数中增加一条include_files = ['xxx.dat']配置,这样在打包时会将python脚本目录中的xxx.dat文件拷贝到exe目录中,不写的话就得人工拷贝了。
小技巧:Python获得自己的绝对路径
Python中有个魔术变量可以得到脚本自身的名称,但转换成exe后该变量失效,这时得改用sys.executable获得可执行程序的名称,可用hasattr(sys, "frozen")判断自己是否已被打包,下面是一个方便取绝对路径的函数:
import sys def module_path(): if hasattr(sys, "frozen"): return os.path.dirname(os.path.abspath(unicode(sys.executable, sys.getfilesystemencoding()))) return os.path.dirname(os.path.abspath(unicode(__file__, sys.getfilesystemencoding())))
结束语
Python可能是程序员最好的玩具,什么都能粘起来,日常写点小工具再合适不过了。
文中的第三方模块都可以Google获得下载地址,有些库没有Win7 64位的原始版本(比如PIL),但可到
http://www.lfd.uci.edu/~gohlke/pythonlibs/
下载别人编译好的,也很方便。