[Python] 纯文本查看 复制代码
import os
import sys
import random
import math
import io
import time
import ctypes
import winreg # 用于读取 Windows 注册表
from PIL import Image
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QTimer, QPointF
from PyQt5.QtGui import QPainter, QColor, QImage, QPixmap, QCursor, QFont
# ================== 系统 API 封装 ==================
def set_cursor_pos(x, y):
"""强制设置系统真实鼠标的位置(用于重生阶段死锁鼠标)"""
ctypes.windll.user32.SetCursorPos(int(x), int(y))
def show_system_cursor(show=True):
"""显示或隐藏系统真实鼠标光标"""
# ShowCursor 是基于计数器的,必须循环调用直到状态符合预期
if show:
while ctypes.windll.user32.ShowCursor(True) < 0: pass
else:
while ctypes.windll.user32.ShowCursor(False) >= 0: pass
# ================== 注册表路径获取 ==================
def get_system_cursor_path():
"""从注册表获取用户当前正在使用的系统指针路径"""
try:
# 打开用户指针配置的注册表项
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Control Panel\Cursors")
# 精准获取 "Arrow" (标准选择) 的路径
value, reg_type = winreg.QueryValueEx(key, "Arrow")
winreg.CloseKey(key)
if isinstance(value, str):
# 展开路径中的环境变量(例如将 %SystemRoot% 替换为 C:\Windows)
real_path = os.path.expandvars(value)
if os.path.exists(real_path):
return real_path
except Exception as e:
print(f"获取注册表指针失败: {e}")
# 路径获取失败时的默认备选方案
fallbacks = [r"C:\Windows\Cursors\arrow.cur", r"C:\Windows\Cursors\arrow_m.cur"]
for path in fallbacks:
if os.path.exists(path): return path
return ""
# ================== 资源处理 ==================
def load_cursor_pixmap(cursor_path: str, scale: float = 0.2) -> QPixmap:
"""加载 .cur 文件并转换为 PyQt 可用的贴图,同时处理尺寸缩放和去黑底"""
if not cursor_path or not os.path.exists(cursor_path):
# 路径无效时返回一个透明占位符
pix = QPixmap(32, 32);
pix.fill(Qt.transparent);
return pix
try:
img = Image.open(cursor_path).convert("RGBA")
# 采用 1.32 系数来适配系统 DPI 视觉大小
w = img.size[0] * 1.32
h = img.size[1] * 1.32
# 缩放至程序设定的显示比例
filt = getattr(Image, 'Resampling', Image).LANCZOS
img = img.resize((int(w * scale), int(h * scale)), resample=filt)
# 算法去黑底:将接近纯黑的像素转为全透明,防止出现黑边
data = img.getdata()
new = [(0, 0, 0, 0) if r < 10 and g < 10 and b < 10 else (r, g, b, a) for (r, g, b, a) in data]
img.putdata(new)
# 将内存中的 Pillow 图像转为 QPixmap
buf = io.BytesIO()
img.save(buf, format='PNG')
return QPixmap.fromImage(QImage.fromData(buf.getvalue(), 'PNG'))
except:
pix = QPixmap(32, 32);
pix.fill(Qt.transparent);
return pix
# ================== 核心特效:重生覆盖层 ==================
class RebirthOverlay(QWidget):
"""当鼠标被“撞碎”后,覆盖全屏的点击重生互动层"""
def __init__(self, death_pos, on_finish_callback):
super().__init__()
# 设置:无边框、最顶层、工具窗口(不显示在任务栏)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground) # 背景透明
self.setGeometry(QApplication.primaryScreen().geometry()) # 全屏
self.death_pos = death_pos # 爆炸发生的位置
self.on_finish = on_finish_callback # 完成后的回调函数
self.progress = 0.0 # 重构进度 (0-100)
self.shake_val = 30.0 # 屏幕震动强度
self.particles = [] # 粒子列表
self.phase = 0 # 阶段:0爆炸, 1落地聚集, 2点击上升, 3最终重构
# 初始化 100 个粒子并赋予随机初速度
for _ in range(100):
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(10, 40)
self.particles.append({
'pos': QPointF(death_pos),
'vel': QPointF(math.cos(angle) * speed, math.sin(angle) * speed),
'size': random.uniform(1, 3),
'drift': random.uniform(0.6, 1.4), # 个体差异系数
'landed': False, 'arrived': False
})
self.timer = QTimer(self)
self.timer.timeout.connect(self.physics_tick)
self.timer.start(16) # 约 60 帧
# 强制拦截全局输入
self.setFocusPolicy(Qt.StrongFocus);
self.setFocus()
self.grabMouse() # 抢夺鼠标所有权,此时只有本窗口能响应点击
show_system_cursor(False) # 隐藏真实鼠标
def mousePressEvent(self, e):
"""处理点击重生的交互"""
if e.button() == Qt.LeftButton:
self.shake_val = 15.0 # 点击震动
if self.phase >= 1:
self.progress += 3.0 # 增加进度
for p in self.particles:
if self.phase == 1: # 聚集阶段:向爆炸点 X 轴靠拢
p['vel'].setX(p['vel'].x() + (self.death_pos.x() - p['pos'].x()) * 0.15)
elif self.phase == 2: # 上升阶段:未到达高度的粒子获得向上冲力
if not p['arrived']:
p['vel'].setY(p['vel'].y() - random.uniform(20, 38) * p['drift'])
if self.progress >= 100: self.complete()
def physics_tick(self):
"""重生层的物理模拟逻辑"""
# 关键:每一帧都把真实鼠标死锁在原地,防止玩家乱动
set_cursor_pos(self.death_pos.x(), self.death_pos.y())
screen_h = self.height()
gravity, friction = 1.0, 0.94 # 重力与摩擦力系数
# 自动切换阶段判定
if self.phase == 1 and self.progress >= 30:
self.phase = 2
elif self.phase == 2:
arrived_count = sum(1 for p in self.particles if p.get('arrived', False))
if self.progress >= 85 or arrived_count > 90: self.phase = 3
if not (QApplication.mouseButtons() & Qt.LeftButton):
self.progress = max(30.1, self.progress - 0.25) # 不点击进度会略微下降
for p in self.particles:
if not p.get('arrived', False):
p['pos'] += p['vel'];
p['vel'] *= friction # 运动更新
if self.phase == 0: # 爆炸溅射
p['vel'].setY(p['vel'].y() + gravity)
if p['pos'].y() > screen_h - 20: # 碰到屏幕底部
p['pos'].setY(screen_h - 20);
p['vel'] = QPointF(0, 0);
p['landed'] = True
if all(pt.get('landed', False) for pt in self.particles): self.phase = 1
elif self.phase == 1: # 地面蠕动
p['pos'].setY(screen_h - 20 + math.sin(time.time() * 12) * 1.2)
elif self.phase == 2: # 点击上升
if not p.get('arrived', False):
p['vel'].setY(p['vel'].y() + gravity)
if p['pos'].y() > screen_h - 15: p['pos'].setY(screen_h - 15) # 地板限制
if p['pos'].y() <= self.death_pos.y(): # 核心修正:到达爆炸点即停止
p['pos'].setY(self.death_pos.y());
p['vel'] = QPointF(0, 0);
p['arrived'] = True
else:
p['pos'] += (self.death_pos - p['pos']) * 0.1 # 到达后的吸附微调
elif self.phase == 3: # 螺旋聚合
angle = time.time() * 40 + random.random()
dist = 20 * (1.1 - self.progress / 100.0)
target = QPointF(self.death_pos.x() + math.cos(angle) * dist * 4,
self.death_pos.y() + math.sin(angle) * dist * 4)
p['pos'] += (target - p['pos']) * 0.4 # 快速向中心坍缩
if self.shake_val > 0.1: self.shake_val *= 0.9
self.update()
def complete(self):
"""重构完成,释放所有限制"""
self.timer.stop();
self.releaseMouse();
show_system_cursor(True)
self.on_finish();
self.close()
def paintEvent(self, e):
"""绘制粒子和进度文字"""
p = QPainter(self);
p.setRenderHint(QPainter.Antialiasing)
if self.shake_val > 0.5: # 处理屏幕震动位移
p.translate(random.uniform(-self.shake_val, self.shake_val),
random.uniform(-self.shake_val, self.shake_val))
# 极其透明的黑色覆盖,确保捕获所有点击
p.fillRect(self.rect(), QColor(0, 0, 0, 1))
p.setBrush(QColor(255, 255, 255, 240));
p.setPen(Qt.NoPen)
for pt in self.particles: p.drawEllipse(pt['pos'], pt['size'], pt['size'])
if self.phase >= 1: # 绘制引导文字
p.setFont(QFont("Microsoft YaHei", 24, QFont.Bold));
p.setPen(QColor(255, 255, 255, 220))
txt = "点击左键聚集..." if self.phase == 1 else "快速点击左键,重构指针!"
if self.phase == 3: txt = "汇聚中..."
p.drawText(self.rect(), Qt.AlignCenter, f"{txt}\n{int(self.progress)}%")
# ================== 核心逻辑:伪指针宠物 ==================
class FakeCursor(QWidget):
def __init__(self, cursor_path):
super().__init__()
self.cursor_pixmap = load_cursor_pixmap(cursor_path, 0.2)
# 窗口属性:无边框、顶层、透明
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Tool | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.resize(self.cursor_pixmap.width(), self.cursor_pixmap.height())
self.pos_f = QPointF(QCursor.pos()) # 当前坐标(浮点型以保证平滑)
self.velocity = QPointF(0, 0) # 速度向量
self.dragging = False # 是否正被拖拽
self.m_last = QPointF(0, 0) # 上一帧鼠标位置(用于计算拖拽速度)
self.bounced_from_drag = False # 标记是否是从拖拽状态甩出的
self.state = "IDLE" # 状态:IDLE(游荡), CHASING(追逐), STUNNED(晕眩), REBIRTH(重生)
self.anger = 0.0 # 愤怒值
self.anger_max = 200.0 # 愤怒阈值
self.difficulty = 0 # 难度(随重生次数增加追逐速度)
self.stun_timer = 0 # 晕眩计时
self.main_timer = QTimer();
self.main_timer.timeout.connect(self.game_loop);
self.main_timer.start(16)
def game_loop(self):
"""主逻辑循环(状态机)"""
dt = 0.016;
real_p = QPointF(QCursor.pos())
if self.state == "STUNNED": # 晕眩状态:红闪并原地不动
self.stun_timer += dt
if self.stun_timer > 2.0: self.state = "CHASING"; self.stun_timer = 0
self.update();
return
if self.state == "CHASING": # 追逐状态:高速撞向真实鼠标
vec = real_p - self.pos_f;
dist = math.hypot(vec.x(), vec.y())
if dist < 12: # 发生碰撞:触发重构事件
self.trigger_rebirth(real_p);
return
self.pos_f += (vec / dist) * (450 + self.difficulty * 45) * dt
self.move_and_clamp();
self.anger -= 12 * dt
if self.anger <= 0: self.state = "IDLE"
self.update();
return
if self.state == "REBIRTH": # 重生状态:在背景自由游荡
self.wander_logic(dt);
self.check_bounce();
self.move_and_clamp();
return
# IDLE 状态:自然游荡 + 处理愤怒值
self.anger = max(0, self.anger - 15 * dt)
if not self.dragging:
self.wander_logic(dt)
self.check_bounce()
self.move_and_clamp() # 执行位置更新及边界死锁
self.update()
def wander_logic(self, dt):
"""随机游荡逻辑"""
self.pos_f += self.velocity * dt;
self.velocity *= 0.95 # 惯性衰减
if math.hypot(self.velocity.x(), self.velocity.y()) < 5 and random.random() < 0.04:
# 随机选择新方向
ang = random.uniform(0, 2 * math.pi);
spd = random.uniform(300, 700)
self.velocity = QPointF(spd * math.cos(ang), spd * math.sin(ang))
def check_bounce(self):
"""边缘碰撞检测与反弹逻辑"""
geom = QApplication.primaryScreen().availableGeometry();
hit = False
# 核心:碰撞时立即将坐标回弹 1 像素,防止陷在墙里
if self.pos_f.x() <= geom.left():
self.pos_f.setX(geom.left() + 1);
self.velocity.setX(abs(self.velocity.x()));
hit = True
elif self.pos_f.x() >= geom.right() - self.width():
self.pos_f.setX(geom.right() - self.width() - 1);
self.velocity.setX(-abs(self.velocity.x()));
hit = True
if self.pos_f.y() <= geom.top():
self.pos_f.setY(geom.top() + 1);
self.velocity.setY(abs(self.velocity.y()));
hit = True
elif self.pos_f.y() >= geom.bottom() - self.height():
self.pos_f.setY(geom.bottom() - self.height() - 1);
self.velocity.setY(-abs(self.velocity.y()));
hit = True
# 如果是被玩家甩到墙上的,增加愤怒值
if hit and self.bounced_from_drag:
self.anger = min(self.anger_max, self.anger + 80)
if self.anger >= self.anger_max: self.state = "STUNNED"; self.velocity = QPointF(0, 0)
self.bounced_from_drag = False
def move_and_clamp(self):
"""位置死锁:严禁伪指针超出屏幕"""
geom = QApplication.primaryScreen().availableGeometry()
x = max(geom.left(), min(self.pos_f.x(), geom.right() - self.width()))
y = max(geom.top(), min(self.pos_f.y(), geom.bottom() - self.height()))
self.pos_f = QPointF(x, y);
self.move(self.pos_f.toPoint())
def trigger_rebirth(self, pos):
"""启动重生特效"""
self.state = "REBIRTH";
self.anger = 0
self.velocity = QPointF(random.uniform(-400, 400), random.uniform(-400, 400))
self.ov = RebirthOverlay(pos, self.finish_rebirth)
self.ov.show()
def finish_rebirth(self):
"""重生特效完成后的回调"""
self.state = "IDLE";
self.difficulty += 1
def mousePressEvent(self, e):
"""拖拽起始点"""
if e.button() == Qt.LeftButton and self.state == "IDLE":
self.dragging = True;
self.m_last = QPointF(e.globalPos())
def mouseMoveEvent(self, e):
"""处理拖拽过程中的位置更新"""
if self.dragging:
now = QPointF(e.globalPos());
delta = now - self.m_last
self.pos_f += delta
# 将拖拽速度保留,用于释放后的“甩射”效果
self.velocity = delta / 0.016
self.m_last = now
self.move_and_clamp()
def mouseReleaseEvent(self, e):
"""拖拽释放"""
if self.dragging: self.dragging = False; self.bounced_from_drag = True
def paintEvent(self, e):
"""绘制伪指针及愤怒颜色叠加层"""
p = QPainter(self);
ratio = self.anger / self.anger_max
# 准备一个带颜色的滤镜图层
pix = QPixmap(self.cursor_pixmap.size());
pix.fill(Qt.transparent);
pp = QPainter(pix)
pp.drawPixmap(0, 0, self.cursor_pixmap)
pp.setCompositionMode(QPainter.CompositionMode_SourceAtop) # 仅给非透明部分染色
if self.state == "STUNNED":
# 晕眩时剧烈红闪
blink = int(130 + 125 * math.sin(time.time() * 35))
pp.fillRect(pix.rect(), QColor(255, 0, 0, blink))
else:
# 平时根据愤怒值逐渐变红
pp.fillRect(pix.rect(), QColor(255, 0, 0, int(210 * ratio)))
pp.end();
p.drawPixmap(0, 0, pix)
# ================== 程序入口 ==================
if __name__ == '__main__':
app = QApplication(sys.argv)
# 动态抓取当前 Windows 系统的光标皮肤
system_cursor = get_system_cursor_path()
print(f"正在加载系统指针: {system_cursor}")
pet = FakeCursor(system_cursor)
pet.show()
sys.exit(app.exec_())