🔥 新作首發 🎮 天堂私服 YOLOv8 物件偵測實戰 — 從資料蒐集、模型訓練到即時偵測 立即閱讀 →
熱門系列
Like Share Discussion Bookmark Smile

J.J. Huang   2026-04-19   Python OpenCV 08.專案實作篇   瀏覽次數:次   DMCA.com Protection Status

Python | OpenCV 專案:天堂私服遊戲輔助(一)主程式 GUI 框架與 HP/MP 血量監控

⚠️ 免責聲明

本文章內容僅供學術研究與電腦視覺技術學習之用途,所有程式碼與技術說明均以教育目的為出發點。

  • 本文作者不提供任何形式的輔助程式販售、散佈或商業服務
  • 本文所有範例程式僅限在自行架設的私有伺服器環境中測試,不得用於任何正式營運的線上遊戲伺服器。
  • 使用遊戲輔助程式可能違反個別遊戲的使用者條款,並導致帳號封鎖、法律責任等後果,讀者須自行承擔一切相關風險與責任
  • 本文技術內容若被用於任何違法或損害他人利益之行為,作者概不負責
  • 私有伺服器的架設與使用涉及遊戲著作權相關法律問題,讀者應自行評估所在地區的法規,並確認於合法範圍內使用。

本系列的核心目的是展示 GUI 設計、多執行緒架構、螢幕擷取、影像分析 等電腦視覺技術的整合應用,相同技術同樣適用於工廠監控、遠端桌面分析等正當場景。

📚 前言

在上一篇 天堂私服 YOLOv8 物件偵測實戰 中,我們完成了 YOLOv8 怪物偵測模型的訓練與即時推論。

這個系列(共 5 篇)將把訓練好的模型整合進一套完整的遊戲輔助程式,以逐步增加功能的方式帶你理解整個系統架構,每篇都附上可直接執行的完整 main.py

系列總覽:

篇次 主題 重點功能
本篇(一) 主程式 GUI 框架與 HP/MP 血量監控 tkinter UI、螢幕擷取、血量偵測
自動補藥 閾值設定、熱鍵模擬、補藥邏輯
YOLOv8 怪物偵測整合 偵測執行緒、即時預覽視窗
四(最終篇) 目標優先序選擇 優先類別、距離排序、目標鎖定

🎯 本篇目標

  • 建立 tkinter 暗色主題遊戲輔助視窗
  • 實作「🎯 選取視窗」按鈕,讓使用者點擊即鎖定任一目標視窗(取得 hwnd、標題、PID)
  • 用 pywin32 PrintWindow 擷取選定視窗畫面
  • 用 HSV 色相範圍 + 最右側有色像素比例偵測 HP/MP 血量
  • 在主程式內整合「⚙ 校準血條」按鈕,框選 HP / MP 血條位置
  • 設計 SharedState + threading.Lock 的執行緒安全資料結構

🗂️ 本篇專案結構

1
2
3
4
5
lineage_assistant/
├── main.py ← 主程式:UI、視窗選取、血條校準(本篇完成)
├── window_capture.py ← 視窗擷取與滑鼠選窗模組(本篇完成)
├── hp_monitor.py ← HP/MP 血量監控模組(本篇完成)
└── config.json ← 由主程式「⚙ 校準血條」按鈕寫入

後續每篇會在同一目錄中新增模組,並更新 main.py

🛠️ 套件安裝

1
pip install opencv-python pywin32

💡 pywin32 提供 win32guiwin32uiwin32apiwin32process,用來呼叫 PrintWindowWindowFromPointGetAsyncKeyStateGetWindowThreadProcessId 等 Windows API。若 import win32gui 失敗,補跑一次 post-install:

1
python -m pywin32_postinstall -install


💻 視窗擷取模組:window_capture.py

用 Windows PrintWindow API 擷取指定 hwnd 的視窗畫面,即使視窗被其他視窗遮擋也能拿到完整畫面(最小化除外)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# window_capture.py
from ctypes import windll
import numpy as np
import win32gui
import win32process
import win32ui

GA_ROOT = 2 # GetAncestor flag:取頂層視窗

def window_from_point(x, y):
"""回傳 (x, y) 螢幕座標下的頂層視窗 (hwnd, title, pid);找不到回傳 (None, '', 0)"""
hwnd = win32gui.WindowFromPoint((x, y))
if not hwnd:
return None, "", 0
hwnd = win32gui.GetAncestor(hwnd, GA_ROOT)
_, pid = win32process.GetWindowThreadProcessId(hwnd)
return hwnd, win32gui.GetWindowText(hwnd), pid

def capture_window(hwnd):
"""用 PrintWindow 擷取視窗畫面:被遮擋也能抓,最小化會拿到黑畫面"""
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
w, h = right - left, bottom - top
if w <= 0 or h <= 0:
return None

hwnd_dc = win32gui.GetWindowDC(hwnd)
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
save_dc = mfc_dc.CreateCompatibleDC()
save_bmp = win32ui.CreateBitmap()
save_bmp.CreateCompatibleBitmap(mfc_dc, w, h)
save_dc.SelectObject(save_bmp)

# flag=2 (PW_RENDERFULLCONTENT):支援 DWM 合成畫面,對多數現代應用必要
result = windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), 2)
img = np.frombuffer(save_bmp.GetBitmapBits(True), dtype=np.uint8).reshape((h, w, 4))

win32gui.DeleteObject(save_bmp.GetHandle())
save_dc.DeleteDC()
mfc_dc.DeleteDC()
win32gui.ReleaseDC(hwnd, hwnd_dc)

return img[:, :, :3].copy() if result == 1 else None # BGRA → BGR

💻 血量監控模組:hp_monitor.py

HPMonitor 透過 HSV 色彩比例 推算血條填充:

  1. 從畫面截出血條 ROI
  2. 轉換為 HSV 色彩空間
  3. 對目標色(HP 紅 / MP 藍)做閾值遮罩
  4. 找最右側有色像素欄位,除以總寬度得到填充比例(0.0 ~ 1.0)

HP / MP 兩組 ROI 各自儲存在 config.json,可用「⚙ 校準血條」按鈕隨時重新框選。

⚠️ HSV 的紅色色相橫跨 0 與 180 兩端,本範例只取 H=0~10 足以應付多數血條;若你的遊戲血條偏紫紅(H > 170),要同時 cv2.inRange 兩段再用 |(OR)合併成一張遮罩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# hp_monitor.py
import cv2
import json
import numpy as np
import os

CONFIG_FILE = "config.json"
DEFAULT_HP_ROI = (10, 10, 200, 20)
DEFAULT_MP_ROI = (10, 35, 200, 20)


class HPMonitor:
def __init__(self):
self.hp_roi = DEFAULT_HP_ROI
self.mp_roi = DEFAULT_MP_ROI
self._load_config()

def _load_config(self):
if not os.path.exists(CONFIG_FILE):
return
with open(CONFIG_FILE) as f:
cfg = json.load(f)
for key in ("hp_roi", "mp_roi"):
if key in cfg:
setattr(self, key, tuple(cfg[key]))

def _color_ratio(self, frame, roi, lower, upper) -> float:
"""HSV 色相範圍遮罩 + 最右側有色像素欄 / 總寬度"""
x, y, w, h = roi
if w == 0 or h == 0:
return 0.0
hsv = cv2.cvtColor(frame[y:y+h, x:x+w], cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, np.array(lower), np.array(upper))
cols = np.any(mask > 0, axis=0)
if not np.any(cols):
return 0.0
return min(1.0, (int(np.max(np.where(cols))) + 1) / w)

def read(self, frame):
hp = self._color_ratio(frame, self.hp_roi,
[0, 100, 80], [10, 255, 255])
mp = self._color_ratio(frame, self.mp_roi,
[100, 100, 80], [130, 255, 255])
return hp, mp

💻 主程式:main.py

本篇只啟動一條 Worker Thread,負責螢幕擷取與血量偵測,主執行緒專跑 tkinter UI。

執行緒架構(本篇):

1
2
3
4
5
Worker Thread 1 (monitor_loop)
└── capture_window → HPMonitor → SharedState.hp / mp

Main Thread (tkinter)
└── root.after(200ms) → 讀 SharedState → 更新 HP/MP 進度條
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# main.py(第一篇版本)
import json
import queue
import threading
import time
import tkinter as tk
from tkinter import ttk, scrolledtext

import cv2
import win32api

from window_capture import capture_window, window_from_point
from hp_monitor import HPMonitor, CONFIG_FILE

VK_LBUTTON = 0x01
VK_ESCAPE = 0x1B


# ── Shared State ───────────────────────────────────────────────
class SharedState:
"""執行緒安全的共享資料容器"""
def __init__(self):
self._lock = threading.Lock()
self.hp = 1.0
self.mp = 1.0
self.hwnd = None # 由「🎯 選取視窗」鎖定的目標視窗 handle
self.running = False

def update(self, **kwargs):
with self._lock:
for k, v in kwargs.items():
setattr(self, k, v)

def get(self, *keys):
with self._lock:
if len(keys) == 1:
return getattr(self, keys[0])
return tuple(getattr(self, k) for k in keys)


# ── Worker Thread 1:監控迴圈 ──────────────────────────────────
def monitor_loop(state: SharedState, monitor: HPMonitor, log_q: queue.Queue):
hwnd = state.get("hwnd")
log_q.put(f"監控執行緒啟動,hwnd={hwnd}")
# 正常流程下 hwnd 不會為 None(_start 已擋),這裡直接進主迴圈

while state.get("running"):
frame = capture_window(hwnd)
if frame is None:
time.sleep(0.2)
continue
hp, mp = monitor.read(frame)
state.update(hp=hp, mp=mp)
time.sleep(0.2)

log_q.put("監控執行緒結束")


# ── tkinter 主視窗 ─────────────────────────────────────────────
class LineageAssistant:
BG = "#1a1a2e"
FG = "#e0e0e0"
RED = "#e94560"
BLUE = "#4fc3f7"
GREEN = "#00ff99"
AMBER = "#f5a623" # 圖示靜態色(參考私服輔助面板的琥珀風格)
AMBER_HOVER = "#ffc857" # 滑鼠移入時變亮金
MUTED = "#555577" # 禁用狀態

def __init__(self):
self.state = SharedState()
self.monitor = HPMonitor() # 主執行緒 + Worker 共用
self.log_q = queue.Queue()
self.t_monitor = None
self._build_ui()
self._poll()
self.root.mainloop()

# ── 扁平化圖示按鈕:與面板同色、琥珀色 icon、hover 變亮金 ──
def _make_icon_btn(self, parent, text, cmd, font):
btn = tk.Button(
parent, text=text,
bg=self.BG, fg=self.AMBER,
activebackground=self.BG, activeforeground=self.AMBER_HOVER,
disabledforeground=self.MUTED,
font=font, relief="flat", bd=0, cursor="hand2",
command=cmd)

def on_enter(_):
if btn["state"] != "disabled":
btn.config(fg=self.AMBER_HOVER)

def on_leave(_):
if btn["state"] != "disabled":
btn.config(fg=self.AMBER)

btn.bind("<Enter>", on_enter)
btn.bind("<Leave>", on_leave)
return btn

def _build_ui(self):
self.root = tk.Tk()
self.root.title("天堂私服輔助 v0.1")
self.root.configure(bg=self.BG)
self.root.resizable(False, False)

# ── 標題 ──
tk.Label(self.root, text="⚔ 天堂私服輔助",
bg=self.BG, fg=self.FG,
font=("Consolas", 16, "bold")).pack(pady=10)

# ── 工具列:🎯 靠左、⚙ 校準 靠右,中間不放任何會撐寬的元件 ──
toolbar = tk.Frame(self.root, bg=self.BG)
toolbar.pack(padx=20, pady=(0, 4), fill="x")

self.btn_pick = self._make_icon_btn(
toolbar, "🎯 選取視窗", self._pick_window,
font=("Consolas", 11, "bold"))
self.btn_pick.pack(side="left")

self.btn_calib = self._make_icon_btn(
toolbar, "⚙ 校準血條", self._calibrate,
font=("Consolas", 11, "bold"))
self.btn_calib.pack(side="right")

# ── 目標視窗資訊:獨立一列,長標題不會擠到按鈕 ──
self.target_var = tk.StringVar(value="目標視窗:未選擇")
tk.Label(self.root, textvariable=self.target_var,
bg=self.BG, fg=self.FG,
font=("Consolas", 10), anchor="w"
).pack(padx=20, pady=(0, 6), fill="x")

# ── HP / MP 血條顯示 ──
bar_frame = tk.Frame(self.root, bg=self.BG)
bar_frame.pack(padx=20, fill="x")

style = ttk.Style()
style.theme_use("default")
style.configure("Red.Horizontal.TProgressbar",
troughcolor="#330000", background="#e94560")
style.configure("Blue.Horizontal.TProgressbar",
troughcolor="#001133", background="#4fc3f7")

tk.Label(bar_frame, text="HP", bg=self.BG, fg=self.RED,
font=("Consolas", 11, "bold"), width=4
).grid(row=0, column=0, sticky="w")
self.hp_var = tk.DoubleVar(value=1.0)
ttk.Progressbar(bar_frame, variable=self.hp_var, maximum=1.0,
length=300, style="Red.Horizontal.TProgressbar"
).grid(row=0, column=1, padx=5, pady=4)
self.hp_lbl = tk.Label(bar_frame, text="100%",
bg=self.BG, fg=self.FG,
font=("Consolas", 10), width=6)
self.hp_lbl.grid(row=0, column=2)

tk.Label(bar_frame, text="MP", bg=self.BG, fg=self.BLUE,
font=("Consolas", 11, "bold"), width=4
).grid(row=1, column=0, sticky="w")
self.mp_var = tk.DoubleVar(value=1.0)
ttk.Progressbar(bar_frame, variable=self.mp_var, maximum=1.0,
length=300, style="Blue.Horizontal.TProgressbar"
).grid(row=1, column=1, padx=5, pady=4)
self.mp_lbl = tk.Label(bar_frame, text="100%",
bg=self.BG, fg=self.FG,
font=("Consolas", 10), width=6)
self.mp_lbl.grid(row=1, column=2)

# ── 控制按鈕:單顆 ▶ / ■ toggle ──
btn_frame = tk.Frame(self.root, bg=self.BG)
btn_frame.pack(pady=10)

self.btn_toggle = self._make_icon_btn(
btn_frame, "▶ 啟動", self._toggle,
font=("Consolas", 13, "bold"))
self.btn_toggle.pack()

# ── 狀態列 ──
self.status_var = tk.StringVar(value="就緒")
tk.Label(self.root, textvariable=self.status_var,
bg=self.BG, fg="#888888",
font=("Consolas", 9)).pack()

# ── 日誌視窗 ──
self.log_box = scrolledtext.ScrolledText(
self.root, width=52, height=10, state="disabled",
bg="#0d0d1a", fg=self.GREEN,
font=("Consolas", 9), insertbackground="white")
self.log_box.pack(padx=10, pady=(5, 10))

def _log(self, msg: str):
self.log_q.put(msg)

def _flush_log(self):
while not self.log_q.empty():
msg = self.log_q.get_nowait()
ts = time.strftime("%H:%M:%S")
self.log_box.config(state="normal")
self.log_box.insert("end", f"{ts} {msg}\n")
self.log_box.see("end")
self.log_box.config(state="disabled")

# ── 🎯 滑鼠點擊選窗 ───────────────────────────────────────
def _pick_window(self):
"""進入選窗模式:滑鼠變十字,下一次左鍵點擊即抓取該位置的頂層視窗"""
if self.state.get("running"):
self._log("請先停止監控再重選目標視窗")
return
self._log("🎯 請點擊目標遊戲視窗(ESC 可取消)")
# 選窗期間把整排按鈕都鎖住,避免 crosshair 狀態下誤點其他功能
for btn in (self.btn_pick, self.btn_calib, self.btn_toggle):
btn.config(state="disabled")
self.root.config(cursor="crosshair")
self._poll_pick()

def _poll_pick(self):
"""輪詢全域滑鼠狀態:偵測使用者在哪個視窗按下左鍵"""
# ESC 取消
if win32api.GetAsyncKeyState(VK_ESCAPE) & 0x8000:
self._reset_pick()
self._log("已取消選擇視窗")
return
# 左鍵按下
if win32api.GetAsyncKeyState(VK_LBUTTON) & 0x8000:
x, y = win32api.GetCursorPos()
hwnd, title, pid = window_from_point(x, y)
self._reset_pick()
if hwnd:
self.state.update(hwnd=hwnd)
label = title or "<無標題>"
short = label if len(label) <= 30 else label[:28] + "…"
self.target_var.set(f"目標:{short} (pid={pid})")
self._log(f"已鎖定:{label} hwnd={hwnd} pid={pid}")
else:
self._log("未取得有效視窗,請再試一次")
return
self.root.after(30, self._poll_pick)

def _reset_pick(self):
self.root.config(cursor="")
for btn in (self.btn_pick, self.btn_calib, self.btn_toggle):
btn.config(state="normal")

# ── ⚙ ROI 校準 ───────────────────────────────────────
def _calibrate(self):
if self.state.get("running"):
self._log("請先停止監控再校準")
return
hwnd = self.state.get("hwnd")
if not hwnd:
self._log("請先按「🎯 選取視窗」選取目標視窗")
return

frame = capture_window(hwnd)
if frame is None:
self._log("擷取失敗,視窗可能被最小化")
return

self._log("框選 HP 血條(紅色),Enter 確認")
hp = cv2.selectROI("校準 HP 血條(Enter 確認 / C 重選)",
frame, fromCenter=False, showCrosshair=True)
cv2.destroyAllWindows()

self._log("框選 MP 魔力條(藍色),Enter 確認")
mp = cv2.selectROI("校準 MP 魔力條(Enter 確認 / C 重選)",
frame, fromCenter=False, showCrosshair=True)
cv2.destroyAllWindows()

if hp[2] == 0 or mp[2] == 0:
self._log("校準已取消(未框選有效範圍)")
return

self.monitor.hp_roi = tuple(hp)
self.monitor.mp_roi = tuple(mp)
self._save_config()
self._log(f"✅ 校準完成:HP={hp}, MP={mp}")

def _save_config(self):
cfg = {
"hp_roi": list(self.monitor.hp_roi),
"mp_roi": list(self.monitor.mp_roi),
}
with open(CONFIG_FILE, "w") as f:
json.dump(cfg, f, indent=2)
self._log(f"已寫入 {CONFIG_FILE}")

# ── ▶ / ■ 切換:一顆按鈕 toggle ──────────────────────────
def _toggle(self):
if self.state.get("running"):
self._stop()
else:
self._start()

def _start(self):
if not self.state.get("hwnd"):
self._log("請先按「🎯 選取視窗」選取目標視窗")
return
self.state.update(running=True)
self.t_monitor = threading.Thread(
target=monitor_loop,
args=(self.state, self.monitor, self.log_q),
daemon=True)
self.t_monitor.start()
self.btn_toggle.config(text="■ 停止")
self.status_var.set("運行中...")

def _stop(self):
self.state.update(running=False)
self.btn_toggle.config(text="▶ 啟動")
self.status_var.set("已停止")
self._log("使用者手動停止")

def _poll(self):
"""每 200ms 將 SharedState 的值同步到 UI"""
hp, mp = self.state.get("hp", "mp")
self.hp_var.set(hp)
self.mp_var.set(mp)
self.hp_lbl.config(text=f"{hp * 100:.0f}%")
self.mp_lbl.config(text=f"{mp * 100:.0f}%")
self._flush_log()
self.root.after(200, self._poll)


if __name__ == "__main__":
LineageAssistant()


圖:主程式按下「🎯 選取視窗」後滑鼠變十字,點擊遊戲視窗即鎖定 hwnd / 標題 / PID;再按「⚙ 校準血條」彈出 OpenCV 視窗框選 HP、MP 血條後自動寫回 config.json


圖:以 tkinter 建立暗色主題輔助視窗,Worker Thread 即時擷取畫面並偵測 HP/MP,每 200ms 更新 UI 進度條與日誌

🚀 執行流程

1
python main.py
  1. 🎯 選取視窗 → 滑鼠變十字,點擊目標遊戲視窗即鎖定(hwnd、標題、PID)
  2. ⚙ 校準血條 → 彈出 OpenCV 視窗,依序框選 HP 血條與 MP 魔力條(Enter 確認 / C 重選)
  3. ▶ 啟動 → HP/MP 以 5fps 即時更新

下次啟動會自動讀取 config.json 裡已儲存的 ROI,不用重新校準。

⚠️ 常見問題

  • 血條偵測不準確:再按一次 ⚙ 校準血條 重新框選,範圍要涵蓋整條血條但不超出 UI 邊框。
  • HP 一直顯示 0%:調整 hp_monitor.py 的 H(色相)範圍,不同遊戲血條的紅 / 藍色調差異頗大。
  • 按 🎯 選取視窗 後點到錯的子視窗:程式已用 GetAncestor(GA_ROOT) 自動取頂層,若仍不正確就再按一次重選。
  • 按 🎯 選取視窗 沒反應:本功能用 GetAsyncKeyState 輪詢全域滑鼠,若被其他程式攔截(例如部分遠端桌面),改用滑鼠直接點實體視窗。
  • 畫面是黑的:視窗被最小化時 Windows 不 render 畫面,PrintWindow 拿不到內容;把視窗從工作列還原即可。

🎯 結語

本篇建立了整個系列的骨架:tkinter 暗色 UI、SharedState 執行緒安全架構、pywin32 視窗擷取,以及用 HSV 色彩比例偵測 HP/MP 的血量監控模組。

下一篇將加入 (二)自動補藥 功能,當 HP 或 MP 低於設定閾值時自動按下補藥熱鍵,並為 UI 增加閾值與熱鍵設定面板。

📖 如在學習過程中遇到疑問,或是想了解更多相關主題,建議回顧一下 Python | OpenCV 系列導讀,掌握完整的章節目錄,方便快速找到你需要的內容。

註:以上參考了
pywin32 GitHub
tkinter 官方文件
WindowFromPoint - Win32 API
GetAsyncKeyState - Win32 API