初步版本
This commit is contained in:
234
main.py
Normal file
234
main.py
Normal file
@@ -0,0 +1,234 @@
|
||||
import time
|
||||
import tkinter as tk
|
||||
|
||||
import keyboard
|
||||
import win32api
|
||||
import win32con
|
||||
import win32gui
|
||||
|
||||
|
||||
POTPLAYER_CLASSES = {"PotPlayer64", "PotPlayer"}
|
||||
APP_TITLE = "PotPlayer 同步器"
|
||||
WINDOW_SIZE = "420x240"
|
||||
PLAY_PAUSE_COOLDOWN = 0.25
|
||||
|
||||
WM_APPCOMMAND = 0x0319
|
||||
APPCOMMAND_MEDIA_PLAY_PAUSE = 14
|
||||
|
||||
|
||||
def safe_get_class_name(hwnd):
|
||||
try:
|
||||
return win32gui.GetClassName(hwnd)
|
||||
except win32gui.error:
|
||||
return ""
|
||||
|
||||
|
||||
def safe_get_window_text(hwnd):
|
||||
try:
|
||||
return win32gui.GetWindowText(hwnd)
|
||||
except win32gui.error:
|
||||
return ""
|
||||
|
||||
|
||||
def is_potplayer_window(hwnd):
|
||||
return safe_get_class_name(hwnd) in POTPLAYER_CLASSES
|
||||
|
||||
|
||||
def build_key_lparam(vk_code, is_keydown):
|
||||
scan_code = win32api.MapVirtualKey(vk_code, 0)
|
||||
is_extended = vk_code in {
|
||||
win32con.VK_LEFT,
|
||||
win32con.VK_RIGHT,
|
||||
win32con.VK_UP,
|
||||
win32con.VK_DOWN,
|
||||
win32con.VK_CONTROL,
|
||||
win32con.VK_RCONTROL,
|
||||
win32con.VK_MENU,
|
||||
win32con.VK_RMENU,
|
||||
}
|
||||
|
||||
lparam = 1 | (scan_code << 16) | (int(is_extended) << 24)
|
||||
if not is_keydown:
|
||||
lparam |= (1 << 30) | (1 << 31)
|
||||
return lparam
|
||||
|
||||
|
||||
def get_potplayer_windows():
|
||||
hwnds = []
|
||||
|
||||
def callback(hwnd, _extra):
|
||||
if win32gui.IsWindowVisible(hwnd) and is_potplayer_window(hwnd):
|
||||
hwnds.append(hwnd)
|
||||
return True
|
||||
|
||||
win32gui.EnumWindows(callback, None)
|
||||
return hwnds
|
||||
|
||||
|
||||
class PotPlayerSyncApp:
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title(APP_TITLE)
|
||||
self.root.geometry(WINDOW_SIZE)
|
||||
self.root.resizable(False, False)
|
||||
self.root.attributes("-topmost", True)
|
||||
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
|
||||
|
||||
self.status_var = tk.StringVar(value="等待操作...")
|
||||
self.last_play_pause_at = 0.0
|
||||
self.hotkey_handles = []
|
||||
|
||||
self.build_ui()
|
||||
self.register_hotkeys()
|
||||
self.refresh_initial_status()
|
||||
|
||||
def build_ui(self):
|
||||
tk.Label(
|
||||
self.root,
|
||||
text="PotPlayer 联动控制台",
|
||||
font=("Microsoft YaHei", 13, "bold"),
|
||||
pady=10,
|
||||
).pack()
|
||||
|
||||
tips = (
|
||||
"只在 PotPlayer 窗口或本工具窗口内操作时生效\n"
|
||||
"\n"
|
||||
"空格: 同步播放 / 暂停\n"
|
||||
"Left / Right: 同步快退 / 快进\n"
|
||||
"Shift + Left / Right: 同步微调进度\n"
|
||||
"Ctrl + Left / Right: 同步大幅调整进度"
|
||||
)
|
||||
tk.Label(
|
||||
self.root,
|
||||
text=tips,
|
||||
font=("Microsoft YaHei", 10),
|
||||
justify="left",
|
||||
fg="#333333",
|
||||
padx=16,
|
||||
pady=4,
|
||||
).pack(anchor="w")
|
||||
|
||||
tk.Label(
|
||||
self.root,
|
||||
textvariable=self.status_var,
|
||||
font=("Microsoft YaHei", 10, "bold"),
|
||||
pady=10,
|
||||
fg="#0066CC",
|
||||
wraplength=380,
|
||||
justify="left",
|
||||
).pack()
|
||||
|
||||
def register_hotkeys(self):
|
||||
self.hotkey_handles.extend(
|
||||
[
|
||||
keyboard.on_press_key("space", self.handle_space, suppress=False),
|
||||
keyboard.on_press_key("left", self.handle_left, suppress=False),
|
||||
keyboard.on_press_key("right", self.handle_right, suppress=False),
|
||||
]
|
||||
)
|
||||
|
||||
def refresh_initial_status(self):
|
||||
count = len(get_potplayer_windows())
|
||||
if count:
|
||||
self.update_status(f"已检测到 {count} 个 PotPlayer 窗口,等待操作...")
|
||||
else:
|
||||
self.update_status("暂未检测到 PotPlayer 窗口。")
|
||||
|
||||
def update_status(self, message):
|
||||
self.root.after(0, lambda: self.status_var.set(message))
|
||||
|
||||
def is_app_window(self, hwnd):
|
||||
return safe_get_window_text(hwnd) == APP_TITLE
|
||||
|
||||
def get_target_windows(self):
|
||||
foreground_hwnd = win32gui.GetForegroundWindow()
|
||||
in_potplayer = is_potplayer_window(foreground_hwnd)
|
||||
in_app = self.is_app_window(foreground_hwnd)
|
||||
|
||||
if not (in_potplayer or in_app):
|
||||
return [], "请先聚焦 PotPlayer 窗口或同步器窗口。"
|
||||
|
||||
hwnds = get_potplayer_windows()
|
||||
if not hwnds:
|
||||
return [], "未检测到可同步的 PotPlayer 窗口。"
|
||||
|
||||
targets = [hwnd for hwnd in hwnds if hwnd != foreground_hwnd] if in_potplayer else hwnds
|
||||
if not targets:
|
||||
return [], "当前只有一个 PotPlayer 窗口,无需同步。"
|
||||
|
||||
return targets, ""
|
||||
|
||||
def post_play_pause(self, hwnd):
|
||||
lparam = APPCOMMAND_MEDIA_PLAY_PAUSE << 16
|
||||
win32gui.PostMessage(hwnd, WM_APPCOMMAND, 0, lparam)
|
||||
|
||||
def post_combo(self, hwnd, vk_codes):
|
||||
for vk_code in vk_codes:
|
||||
win32gui.PostMessage(hwnd, win32con.WM_KEYDOWN, vk_code, build_key_lparam(vk_code, True))
|
||||
for vk_code in reversed(vk_codes):
|
||||
win32gui.PostMessage(hwnd, win32con.WM_KEYUP, vk_code, build_key_lparam(vk_code, False))
|
||||
|
||||
def sync_action(self, action_name, sender):
|
||||
target_hwnds, message = self.get_target_windows()
|
||||
if not target_hwnds:
|
||||
self.update_status(message)
|
||||
return
|
||||
|
||||
for hwnd in target_hwnds:
|
||||
try:
|
||||
sender(hwnd)
|
||||
except win32gui.error:
|
||||
continue
|
||||
|
||||
self.update_status(f"已同步: {action_name} -> {len(target_hwnds)} 个窗口")
|
||||
|
||||
def handle_space(self, _event):
|
||||
now = time.monotonic()
|
||||
if now - self.last_play_pause_at < PLAY_PAUSE_COOLDOWN:
|
||||
return
|
||||
|
||||
self.last_play_pause_at = now
|
||||
self.sync_action("空格(播放/暂停)", self.post_play_pause)
|
||||
|
||||
def handle_left(self, _event):
|
||||
if keyboard.is_pressed("shift"):
|
||||
vk_codes = [win32con.VK_SHIFT, win32con.VK_LEFT]
|
||||
action_name = "Shift + Left(微调后退)"
|
||||
elif keyboard.is_pressed("ctrl"):
|
||||
vk_codes = [win32con.VK_CONTROL, win32con.VK_LEFT]
|
||||
action_name = "Ctrl + Left(大步后退)"
|
||||
else:
|
||||
vk_codes = [win32con.VK_LEFT]
|
||||
action_name = "Left(后退)"
|
||||
|
||||
self.sync_action(action_name, lambda hwnd: self.post_combo(hwnd, vk_codes))
|
||||
|
||||
def handle_right(self, _event):
|
||||
if keyboard.is_pressed("shift"):
|
||||
vk_codes = [win32con.VK_SHIFT, win32con.VK_RIGHT]
|
||||
action_name = "Shift + Right(微调前进)"
|
||||
elif keyboard.is_pressed("ctrl"):
|
||||
vk_codes = [win32con.VK_CONTROL, win32con.VK_RIGHT]
|
||||
action_name = "Ctrl + Right(大步前进)"
|
||||
else:
|
||||
vk_codes = [win32con.VK_RIGHT]
|
||||
action_name = "Right(前进)"
|
||||
|
||||
self.sync_action(action_name, lambda hwnd: self.post_combo(hwnd, vk_codes))
|
||||
|
||||
def on_close(self):
|
||||
for handle in self.hotkey_handles:
|
||||
keyboard.unhook(handle)
|
||||
self.root.destroy()
|
||||
|
||||
def run(self):
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def main():
|
||||
app = PotPlayerSyncApp()
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user