初始化项目以及GUI简单实现

This commit is contained in:
moweishan
2026-03-18 15:48:40 +08:00
parent f7e429a9b3
commit b67c5be2f3
25 changed files with 1650 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

47
pyproject.toml Normal file
View File

@@ -0,0 +1,47 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "LuoLuoTool"
version = "0.1"
description = "《桃源深处有人家》日常任务挂机工具"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "MIT"}
authors = [
{name = "moweishan", email = ""}
]
keywords = ["game", "automation", "挂机", "桃源深处有人家"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"customtkinter>=5.2.2",
"Pillow>=10.0.0",
"pyautogui>=0.9.54",
"pywin32>=306",
"opencv-python>=4.8.0",
"numpy>=1.24.0",
"loguru>=0.7.0",
]
[project.gui-scripts]
luoluo = "src.main:main"
[project.urls]
Homepage = "https://bk.moweishan.top/"
Repository = "https://github.com/moweishan/LuoLuoTool"
[tool.setuptools.packages.find]
where = ["."]
include = ["src*"]

12
requirements.txt Normal file
View File

@@ -0,0 +1,12 @@
# LuoLuoTool 依赖列表
# 《桃源深处有人家》挂机工具
# ========== UI框架 ==========
customtkinter>=5.2.2 # 现代化UI框架
Pillow>=10.0.0 # 图像处理
# ========== 游戏自动化 ==========
pyautogui>=0.9.54 # 鼠标键盘模拟
pywin32>=306 # Windows API
opencv-python>=4.8.0 # 图像识别
numpy>=1.24.0 # 数值计算

14
src/core/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
"""
LuoLuoTool 核心模块
游戏自动化控制核心
"""
from .automation import AutomationController
from .game_window import GameWindowManager
from .actions import GameActions
__all__ = [
"AutomationController",
"GameWindowManager",
"GameActions",
]

41
src/core/actions.py Normal file
View File

@@ -0,0 +1,41 @@
"""
游戏动作定义
"""
import time
from typing import Optional
from src.utils.logger import logger
from .game_window import GameWindowManager
class GameActions:
"""游戏动作执行器"""
def __init__(self, window_manager: GameWindowManager):
self.window = window_manager
def click(self, x: int, y: int, delay: float = 0.1):
"""点击指定坐标"""
self.window.click(x, y)
time.sleep(delay)
def click_template(self, template_name: str, timeout: float = 5.0) -> bool:
"""点击匹配到的模板图像"""
# TODO: 实现模板匹配点击
logger.debug(f"尝试点击模板: {template_name}")
return False
def wait(self, seconds: float):
"""等待指定时间"""
time.sleep(seconds)
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float = 0.5):
"""滑动操作"""
# TODO: 实现滑动
logger.debug(f"滑动: ({x1}, {y1}) -> ({x2}, {y2})")
def press_key(self, key: str):
"""按下键盘按键"""
# TODO: 实现按键
logger.debug(f"按键: {key}")

87
src/core/automation.py Normal file
View File

@@ -0,0 +1,87 @@
"""
自动化控制主类
"""
import threading
import time
from typing import Optional, Callable
from src.utils.logger import logger
from .game_window import GameWindowManager
from .actions import GameActions
class AutomationController:
"""自动化控制器"""
def __init__(self):
self.window_manager = GameWindowManager()
self.actions = GameActions(self.window_manager)
self._running = False
self._paused = False
self._thread: Optional[threading.Thread] = None
self._callback: Optional[Callable] = None
@property
def is_running(self) -> bool:
return self._running
@property
def is_paused(self) -> bool:
return self._paused
def set_callback(self, callback: Callable):
"""设置状态回调函数"""
self._callback = callback
def _notify(self, message: str):
"""通知UI更新"""
logger.info(message)
if self._callback:
self._callback(message)
def start(self):
"""开始自动化"""
if self._running:
return
if not self.window_manager.is_window_captured:
self._notify("请先捕获游戏窗口")
return
self._running = True
self._paused = False
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
self._notify("自动化已启动")
def stop(self):
"""停止自动化"""
self._running = False
self._paused = False
if self._thread:
self._thread.join(timeout=2)
self._notify("自动化已停止")
def pause(self):
"""暂停/继续"""
if not self._running:
return
self._paused = not self._paused
status = "已暂停" if self._paused else "已继续"
self._notify(f"自动化{status}")
def _run_loop(self):
"""主运行循环"""
while self._running:
if self._paused:
time.sleep(0.1)
continue
try:
# TODO: 实现具体的任务执行逻辑
time.sleep(0.1)
except Exception as e:
logger.error(f"运行错误: {e}")
self._notify(f"错误: {e}")

97
src/core/game_window.py Normal file
View File

@@ -0,0 +1,97 @@
"""
游戏窗口管理
"""
import win32gui
import win32con
from typing import Optional, Tuple
from src.utils.logger import logger
class GameWindowManager:
"""游戏窗口管理器"""
# 游戏窗口类名和标题
GAME_CLASS_NAME = "UnityWndClass" # Unity游戏常用类名
GAME_WINDOW_TITLE = "桃源深处有人家"
def __init__(self):
self._hwnd: Optional[int] = None
self._window_rect: Tuple[int, int, int, int] = (0, 0, 0, 0)
@property
def is_window_captured(self) -> bool:
"""是否已捕获窗口"""
if self._hwnd is None:
return False
return win32gui.IsWindow(self._hwnd)
@property
def hwnd(self) -> Optional[int]:
"""窗口句柄"""
return self._hwnd
@property
def window_rect(self) -> Tuple[int, int, int, int]:
"""窗口矩形 (left, top, right, bottom)"""
if self.is_window_captured:
self._window_rect = win32gui.GetWindowRect(self._hwnd)
return self._window_rect
@property
def client_size(self) -> Tuple[int, int]:
"""客户端区域大小 (width, height)"""
if not self.is_window_captured:
return (0, 0)
left, top, right, bottom = self.window_rect
return (right - left, bottom - top)
def find_window(self) -> bool:
"""查找游戏窗口"""
# 先尝试精确匹配
hwnd = win32gui.FindWindow(None, self.GAME_WINDOW_TITLE)
# 如果没找到,尝试模糊匹配
if hwnd == 0:
def callback(hwnd, extra):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
if self.GAME_WINDOW_TITLE in title:
extra.append(hwnd)
return True
windows = []
win32gui.EnumWindows(callback, windows)
if windows:
hwnd = windows[0]
if hwnd != 0:
self._hwnd = hwnd
self._window_rect = win32gui.GetWindowRect(hwnd)
logger.info(f"找到游戏窗口: {hwnd}, 大小: {self.client_size}")
return True
logger.warning("未找到游戏窗口")
return False
def capture_window(self) -> bool:
"""捕获游戏窗口"""
return self.find_window()
def bring_to_front(self):
"""将窗口置前"""
if self.is_window_captured:
win32gui.SetForegroundWindow(self._hwnd)
def click(self, x: int, y: int):
"""在窗口内点击"""
if not self.is_window_captured:
return
left, top, _, _ = self.window_rect
# 转换为屏幕坐标
screen_x = left + x
screen_y = top + y
# TODO: 使用pyautogui或win32api发送点击
logger.debug(f"点击坐标: ({screen_x}, {screen_y})")

View File

@@ -0,0 +1,14 @@
"""
挂机任务模块
所有任务逻辑代码写死实现
"""
from .daily_tasks import DailyTaskRunner
from .misc_tasks import MiscTaskRunner
from .pending_tasks import PendingTaskRunner
__all__ = [
"DailyTaskRunner",
"MiscTaskRunner",
"PendingTaskRunner",
]

View File

@@ -0,0 +1,69 @@
"""
日常挂机任务
"""
from typing import Dict, Any
from src.utils.logger import logger
class DailyTaskRunner:
"""日常任务执行器"""
# 任务配置 - 代码写死
TASKS = {
"daily_mission": {
"name": "每日委托",
"enabled": True,
"description": "完成每日任务",
},
"resin_farming": {
"name": "清体力",
"enabled": True,
"description": "消耗体力刷资源",
},
"monthly_card": {
"name": "领月卡",
"enabled": False,
"description": "领取月卡奖励",
},
"friend_gift": {
"name": "好友礼物",
"enabled": True,
"description": "领取好友赠送的礼物",
},
"shop_daily": {
"name": "每日商店",
"enabled": False,
"description": "购买每日商店物品",
},
}
def __init__(self):
self._config = self.TASKS.copy()
def get_tasks(self) -> Dict[str, Any]:
"""获取所有任务配置"""
return self._config
def update_task(self, task_id: str, enabled: bool):
"""更新任务启用状态"""
if task_id in self._config:
self._config[task_id]["enabled"] = enabled
logger.info(f"任务 {task_id} 状态更新为: {enabled}")
def run(self, actions):
"""执行启用的任务"""
for task_id, config in self._config.items():
if not config["enabled"]:
continue
logger.info(f"执行任务: {config['name']}")
try:
self._execute_task(task_id, actions)
except Exception as e:
logger.error(f"任务 {config['name']} 执行失败: {e}")
def _execute_task(self, task_id: str, actions):
"""执行具体任务"""
# TODO: 实现具体任务逻辑
logger.debug(f"执行任务逻辑: {task_id}")

View File

@@ -0,0 +1,64 @@
"""
杂项功能任务
"""
from typing import Dict, Any
from src.utils.logger import logger
class MiscTaskRunner:
"""杂项功能执行器"""
# 功能配置 - 代码写死
FEATURES = {
"auto_pickup": {
"name": "自动拾取",
"enabled": True,
"description": "自动拾取掉落物品",
},
"auto_skip": {
"name": "自动跳过对话",
"enabled": False,
"description": "自动跳过游戏对话",
},
"auto_heal": {
"name": "自动回血",
"enabled": True,
"description": "低血量自动回血",
},
"auto_repair": {
"name": "自动修理",
"enabled": False,
"description": "装备损坏自动修理",
},
}
def __init__(self):
self._config = self.FEATURES.copy()
def get_features(self) -> Dict[str, Any]:
"""获取所有功能配置"""
return self._config
def update_feature(self, feature_id: str, enabled: bool):
"""更新功能启用状态"""
if feature_id in self._config:
self._config[feature_id]["enabled"] = enabled
logger.info(f"功能 {feature_id} 状态更新为: {enabled}")
def run(self, actions):
"""执行启用的功能"""
for feature_id, config in self._config.items():
if not config["enabled"]:
continue
logger.info(f"执行功能: {config['name']}")
try:
self._execute_feature(feature_id, actions)
except Exception as e:
logger.error(f"功能 {config['name']} 执行失败: {e}")
def _execute_feature(self, feature_id: str, actions):
"""执行具体功能"""
# TODO: 实现具体功能逻辑
logger.debug(f"执行功能逻辑: {feature_id}")

View File

@@ -0,0 +1,48 @@
"""
待定功能任务
预留功能占位
"""
from typing import Dict, Any
from src.utils.logger import logger
class PendingTaskRunner:
"""待定功能执行器"""
# 待定功能配置 - 代码写死
PENDING_FEATURES = {
"feature_a": {
"name": "功能A待开发",
"enabled": False,
"description": "预留功能A",
},
"feature_b": {
"name": "功能B待开发",
"enabled": False,
"description": "预留功能B",
},
"feature_c": {
"name": "功能C待开发",
"enabled": False,
"description": "预留功能C",
},
}
def __init__(self):
self._config = self.PENDING_FEATURES.copy()
def get_features(self) -> Dict[str, Any]:
"""获取所有待定功能"""
return self._config
def update_feature(self, feature_id: str, enabled: bool):
"""更新功能状态"""
if feature_id in self._config:
self._config[feature_id]["enabled"] = enabled
logger.info(f"待定功能 {feature_id} 状态更新为: {enabled}")
def run(self, actions):
"""执行启用的功能"""
logger.info("待定功能模块 - 暂无实现")
# 待定功能暂不执行任何操作

30
src/main.py Normal file
View File

@@ -0,0 +1,30 @@
"""
LuoLuoTool - 《桃源深处有人家》挂机工具
作者: moweishan
博客: https://bk.moweishan.top/
版本: 0.1
"""
import sys
from pathlib import Path
# 添加项目根目录到路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.utils.logger import setup_logger
from src.ui import LuoLuoApp
def main():
"""程序入口"""
# 初始化日志
setup_logger()
# 启动应用
app = LuoLuoApp()
app.run()
if __name__ == "__main__":
main()

8
src/ui/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
"""
LuoLuoTool UI模块
使用CustomTkinter构建现代化界面
"""
from .app import LuoLuoApp
__all__ = ["LuoLuoApp"]

229
src/ui/app.py Normal file
View File

@@ -0,0 +1,229 @@
"""
LuoLuoTool 主应用类
窗口标题: LuoLuo
"""
import customtkinter as ctk
from pathlib import Path
from src.utils.logger import logger
from .pages.run_page import RunPage
from .pages.daily_config_page import DailyConfigPage
from .pages.misc_config_page import MiscConfigPage
from .pages.pending_config_page import PendingConfigPage
from .pages.about_page import AboutPage
class LuoLuoApp:
"""LuoLuoTool主应用"""
# 窗口配置
WINDOW_TITLE = "LuoLuo"
WINDOW_WIDTH = 900
WINDOW_HEIGHT = 650
# 侧边栏配置
SIDEBAR_WIDTH = 140
# 主题配置
THEME_COLOR = ("#3B8ED0", "#1F6AA5") # 浅色, 深色
def __init__(self):
# 设置主题
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
# 创建主窗口
self.root = ctk.CTk()
self.root.title(self.WINDOW_TITLE)
self.root.geometry(f"{self.WINDOW_WIDTH}x{self.WINDOW_HEIGHT}")
self.root.minsize(800, 550)
# 当前页面
self.current_page = None
self.pages = {}
# 初始化UI
self._setup_ui()
# 设置窗口图标在UI初始化后设置
self._set_window_icon()
logger.info("LuoLuoApp初始化完成")
def _set_window_icon(self):
"""设置窗口图标(仅左上角)"""
assets_dir = Path(__file__).parent.parent.parent / "assets" / "images"
ico_path = assets_dir / "luoluoTool.ico"
png_path = assets_dir / "luoluoTool.png"
try:
# 优先使用 ICO 文件Windows 原生支持)
if ico_path.exists():
self.root.iconbitmap(str(ico_path))
logger.info(f"已设置窗口图标(ICO): {ico_path}")
elif png_path.exists():
# 备用:使用 PNG
from PIL import Image, ImageTk
icon_image = Image.open(png_path)
if icon_image.mode != 'RGBA':
icon_image = icon_image.convert('RGBA')
icon_image = icon_image.resize((32, 32), Image.Resampling.LANCZOS)
self._icon_photo = ImageTk.PhotoImage(icon_image)
self.root.iconphoto(True, self._icon_photo)
logger.info(f"已设置窗口图标(PNG): {png_path}")
else:
logger.warning("未找到图标文件")
except Exception as e:
logger.warning(f"加载图标失败: {e}")
def _setup_ui(self):
"""初始化界面"""
# 配置网格布局
self.root.grid_columnconfigure(1, weight=1)
self.root.grid_rowconfigure(0, weight=1)
# 创建侧边栏
self._create_sidebar()
# 创建主内容区域
self._create_main_content()
# 创建底部状态栏
self._create_statusbar()
# 初始化页面
self._init_pages()
# 默认显示运行页面
self.show_page("run")
def _create_sidebar(self):
"""创建侧边栏"""
self.sidebar = ctk.CTkFrame(
self.root,
width=self.SIDEBAR_WIDTH,
corner_radius=0
)
self.sidebar.grid(row=0, column=0, rowspan=2, sticky="nsew")
self.sidebar.grid_rowconfigure(6, weight=1)
# Logo/标题
self.logo_label = ctk.CTkLabel(
self.sidebar,
text="🎮 LuoLuo",
font=ctk.CTkFont(size=18, weight="bold")
)
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
# 侧边栏按钮
self.nav_buttons = {}
nav_items = [
("run", "▶ 运行", 1),
("daily", "📋 日常", 2),
("misc", "🔧 杂项", 3),
("pending", "⏳ 待定", 4),
("about", " 关于", 5),
]
for page_id, text, row in nav_items:
btn = ctk.CTkButton(
self.sidebar,
text=text,
anchor="w",
fg_color="transparent",
text_color=("gray10", "gray90"),
hover_color=("gray70", "gray30"),
height=40,
command=lambda p=page_id: self.show_page(p)
)
btn.grid(row=row, column=0, padx=10, pady=5, sticky="ew")
self.nav_buttons[page_id] = btn
# 主题切换
self.theme_switch = ctk.CTkSwitch(
self.sidebar,
text="🌙 深色",
command=self._toggle_theme
)
self.theme_switch.grid(row=7, column=0, padx=20, pady=20, sticky="s")
def _create_main_content(self):
"""创建主内容区域"""
self.main_frame = ctk.CTkFrame(self.root, corner_radius=0, fg_color="transparent")
self.main_frame.grid(row=0, column=1, sticky="nsew", padx=10, pady=10)
self.main_frame.grid_columnconfigure(0, weight=1)
self.main_frame.grid_rowconfigure(0, weight=1)
def _create_statusbar(self):
"""创建状态栏"""
self.statusbar = ctk.CTkFrame(self.root, height=30, corner_radius=0)
self.statusbar.grid(row=1, column=1, sticky="ew", padx=10, pady=(0, 5))
self.status_label = ctk.CTkLabel(
self.statusbar,
text="就绪",
font=ctk.CTkFont(size=12)
)
self.status_label.pack(side="left", padx=10)
self.version_label = ctk.CTkLabel(
self.statusbar,
text="v0.1",
font=ctk.CTkFont(size=12),
text_color="gray"
)
self.version_label.pack(side="right", padx=10)
def _init_pages(self):
"""初始化所有页面 - 预加载提升切换速度"""
self.pages["run"] = RunPage(self.main_frame, self)
self.pages["daily"] = DailyConfigPage(self.main_frame, self)
self.pages["misc"] = MiscConfigPage(self.main_frame, self)
self.pages["pending"] = PendingConfigPage(self.main_frame, self)
self.pages["about"] = AboutPage(self.main_frame, self)
# 预构建所有页面,避免懒加载导致的卡顿
for page_id, page in self.pages.items():
if page.frame is None:
page.frame = page.build()
def show_page(self, page_id: str):
"""显示指定页面 - 优化切换性能"""
if self.current_page == page_id:
return
# 使用 after 延迟更新UI避免阻塞
self.root.after(0, lambda: self._do_show_page(page_id))
def _do_show_page(self, page_id: str):
"""实际执行页面切换"""
# 隐藏当前页面
if self.current_page:
self.pages[self.current_page].hide()
self.nav_buttons[self.current_page].configure(fg_color="transparent")
# 显示新页面
self.current_page = page_id
self.pages[page_id].show()
self.nav_buttons[page_id].configure(fg_color=self.THEME_COLOR)
logger.debug(f"切换到页面: {page_id}")
def _toggle_theme(self):
"""切换主题"""
if self.theme_switch.get():
ctk.set_appearance_mode("Dark")
self.theme_switch.configure(text="☀ 浅色")
else:
ctk.set_appearance_mode("Light")
self.theme_switch.configure(text="🌙 深色")
def set_status(self, text: str):
"""设置状态栏文本"""
self.status_label.configure(text=text)
def run(self):
"""运行应用"""
logger.info("启动LuoLuo应用")
self.root.mainloop()

19
src/ui/pages/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
"""
UI页面模块
"""
from .base_page import BasePage
from .run_page import RunPage
from .daily_config_page import DailyConfigPage
from .misc_config_page import MiscConfigPage
from .pending_config_page import PendingConfigPage
from .about_page import AboutPage
__all__ = [
"BasePage",
"RunPage",
"DailyConfigPage",
"MiscConfigPage",
"PendingConfigPage",
"AboutPage",
]

154
src/ui/pages/about_page.py Normal file
View File

@@ -0,0 +1,154 @@
"""
关于页面
"""
import customtkinter as ctk
import webbrowser
from pathlib import Path
from PIL import Image
from .base_page import BasePage
class AboutPage(BasePage):
"""关于页面"""
# 项目信息
VERSION = "0.1"
AUTHOR = "moweishan"
BLOG_URL = "https://bk.moweishan.top/"
DESCRIPTION = "专为《桃源深处有人家》打造的\n日常任务挂机工具"
def build(self) -> ctk.CTkFrame:
"""构建关于页面"""
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
frame.grid_rowconfigure(0, weight=1)
# 居中容器
center_frame = ctk.CTkFrame(frame, fg_color="transparent")
center_frame.grid(row=0, column=0, pady=50)
# 加载并显示图标
self._load_icon(center_frame)
# 应用名称
name_label = ctk.CTkLabel(
center_frame,
text="LuoLuoTool",
font=ctk.CTkFont(size=28, weight="bold")
)
name_label.pack()
# 版本号
version_label = ctk.CTkLabel(
center_frame,
text=f"版本 {self.VERSION}",
font=ctk.CTkFont(size=14),
text_color="gray"
)
version_label.pack(pady=(5, 20))
# 分隔线
separator = ctk.CTkFrame(center_frame, height=2, width=200)
separator.pack(pady=10)
# 描述
desc_label = ctk.CTkLabel(
center_frame,
text=self.DESCRIPTION,
font=ctk.CTkFont(size=13),
justify="center"
)
desc_label.pack(pady=20)
# 分隔线
separator2 = ctk.CTkFrame(center_frame, height=2, width=200)
separator2.pack(pady=10)
# 作者信息
author_frame = ctk.CTkFrame(center_frame, fg_color="transparent")
author_frame.pack(pady=20)
author_label = ctk.CTkLabel(
author_frame,
text=f"作者: {self.AUTHOR}",
font=ctk.CTkFont(size=13)
)
author_label.pack()
# 博客链接
blog_btn = ctk.CTkButton(
author_frame,
text="🌐 访问博客",
command=self._open_blog
)
blog_btn.pack(pady=10)
# 按钮区域
btn_frame = ctk.CTkFrame(center_frame, fg_color="transparent")
btn_frame.pack(pady=20)
check_update_btn = ctk.CTkButton(
btn_frame,
text="🔍 检查更新",
command=self._check_update
)
check_update_btn.pack(side="left", padx=5)
return frame
def _load_icon(self, parent):
"""加载并显示图标"""
assets_dir = Path(__file__).parent.parent.parent.parent / "assets" / "images"
# 优先使用 PNG 格式在关于页面显示
png_path = assets_dir / "luoluoTool.png"
ico_path = assets_dir / "luoluoTool.ico"
try:
if png_path.exists():
# 使用 PNG 图标
icon_image = Image.open(png_path)
# 调整大小为 100x100
icon_image = icon_image.resize((100, 100), Image.Resampling.LANCZOS)
icon_ctk = ctk.CTkImage(light_image=icon_image, dark_image=icon_image, size=(100, 100))
icon_label = ctk.CTkLabel(parent, image=icon_ctk, text="")
icon_label.pack(pady=(0, 20))
# 保持引用防止GC
self._about_icon = icon_ctk
elif ico_path.exists():
# 如果没有PNG尝试ICO
icon_image = Image.open(ico_path)
icon_image = icon_image.resize((100, 100), Image.Resampling.LANCZOS)
icon_ctk = ctk.CTkImage(light_image=icon_image, dark_image=icon_image, size=(100, 100))
icon_label = ctk.CTkLabel(parent, image=icon_ctk, text="")
icon_label.pack(pady=(0, 20))
self._about_icon = icon_ctk
else:
# 使用默认emoji
icon_label = ctk.CTkLabel(
parent,
text="🎮",
font=ctk.CTkFont(size=64)
)
icon_label.pack(pady=(0, 20))
except Exception as e:
# 出错时使用默认emoji
icon_label = ctk.CTkLabel(
parent,
text="🎮",
font=ctk.CTkFont(size=64)
)
icon_label.pack(pady=(0, 20))
def _open_blog(self):
"""打开作者博客"""
webbrowser.open(self.BLOG_URL)
def _check_update(self):
"""检查更新"""
# TODO: 实现检查更新功能
self.app.set_status("当前已是最新版本")

40
src/ui/pages/base_page.py Normal file
View File

@@ -0,0 +1,40 @@
"""
页面基类
"""
import customtkinter as ctk
from typing import Optional
class BasePage:
"""页面基类"""
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
self.parent = parent
self.app = app
self.frame: Optional[ctk.CTkFrame] = None
def build(self) -> ctk.CTkFrame:
"""构建页面,子类重写"""
raise NotImplementedError
def show(self):
"""显示页面"""
if self.frame is None:
self.frame = self.build()
self.frame.grid(row=0, column=0, sticky="nsew")
self.on_show()
def hide(self):
"""隐藏页面"""
if self.frame:
self.frame.grid_forget()
self.on_hide()
def on_show(self):
"""页面显示时的回调,子类可重写"""
pass
def on_hide(self):
"""页面隐藏时的回调,子类可重写"""
pass

View File

@@ -0,0 +1,99 @@
"""
日常挂机配置页面
"""
import customtkinter as ctk
from .base_page import BasePage
from ...core.tasks.daily_tasks import DailyTaskRunner
class DailyConfigPage(BasePage):
"""日常挂机配置页面"""
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
super().__init__(parent, app)
self.task_runner = DailyTaskRunner()
self.switches = {}
def build(self) -> ctk.CTkFrame:
"""构建页面"""
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
frame,
text="📋 日常挂机配置",
font=ctk.CTkFont(size=18, weight="bold")
)
title.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="w")
# 说明
desc = ctk.CTkLabel(
frame,
text="配置每日自动执行的日常任务",
font=ctk.CTkFont(size=12),
text_color="gray"
)
desc.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="w")
# 任务列表
tasks_frame = ctk.CTkFrame(frame)
tasks_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=10)
tasks_frame.grid_columnconfigure(0, weight=1)
tasks = self.task_runner.get_tasks()
for idx, (task_id, config) in enumerate(tasks.items()):
self._create_task_item(tasks_frame, task_id, config, idx)
# 保存按钮
save_btn = ctk.CTkButton(
frame,
text="💾 保存配置",
command=self._save_config
)
save_btn.grid(row=3, column=0, padx=20, pady=20, sticky="e")
return frame
def _create_task_item(self, parent, task_id: str, config: dict, row: int):
"""创建任务项"""
frame = ctk.CTkFrame(parent, fg_color="transparent")
frame.grid(row=row, column=0, sticky="ew", padx=10, pady=5)
frame.grid_columnconfigure(1, weight=1)
# 开关
switch = ctk.CTkSwitch(
frame,
text="",
width=50
)
switch.grid(row=0, column=0, padx=(0, 10))
if config["enabled"]:
switch.select()
self.switches[task_id] = switch
# 名称
name_label = ctk.CTkLabel(
frame,
text=config["name"],
font=ctk.CTkFont(size=13, weight="bold")
)
name_label.grid(row=0, column=1, sticky="w")
# 描述
desc_label = ctk.CTkLabel(
frame,
text=config["description"],
font=ctk.CTkFont(size=11),
text_color="gray"
)
desc_label.grid(row=1, column=1, sticky="w")
def _save_config(self):
"""保存配置"""
for task_id, switch in self.switches.items():
enabled = switch.get() == 1
self.task_runner.update_task(task_id, enabled)
self.app.set_status("日常配置已保存")

View File

@@ -0,0 +1,99 @@
"""
杂项功能配置页面
"""
import customtkinter as ctk
from .base_page import BasePage
from ...core.tasks.misc_tasks import MiscTaskRunner
class MiscConfigPage(BasePage):
"""杂项功能配置页面"""
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
super().__init__(parent, app)
self.task_runner = MiscTaskRunner()
self.switches = {}
def build(self) -> ctk.CTkFrame:
"""构建页面"""
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
frame,
text="🔧 杂项功能配置",
font=ctk.CTkFont(size=18, weight="bold")
)
title.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="w")
# 说明
desc = ctk.CTkLabel(
frame,
text="配置挂机时的辅助功能",
font=ctk.CTkFont(size=12),
text_color="gray"
)
desc.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="w")
# 功能列表
features_frame = ctk.CTkFrame(frame)
features_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=10)
features_frame.grid_columnconfigure(0, weight=1)
features = self.task_runner.get_features()
for idx, (feature_id, config) in enumerate(features.items()):
self._create_feature_item(features_frame, feature_id, config, idx)
# 保存按钮
save_btn = ctk.CTkButton(
frame,
text="💾 保存配置",
command=self._save_config
)
save_btn.grid(row=3, column=0, padx=20, pady=20, sticky="e")
return frame
def _create_feature_item(self, parent, feature_id: str, config: dict, row: int):
"""创建功能项"""
frame = ctk.CTkFrame(parent, fg_color="transparent")
frame.grid(row=row, column=0, sticky="ew", padx=10, pady=5)
frame.grid_columnconfigure(1, weight=1)
# 开关
switch = ctk.CTkSwitch(
frame,
text="",
width=50
)
switch.grid(row=0, column=0, padx=(0, 10))
if config["enabled"]:
switch.select()
self.switches[feature_id] = switch
# 名称
name_label = ctk.CTkLabel(
frame,
text=config["name"],
font=ctk.CTkFont(size=13, weight="bold")
)
name_label.grid(row=0, column=1, sticky="w")
# 描述
desc_label = ctk.CTkLabel(
frame,
text=config["description"],
font=ctk.CTkFont(size=11),
text_color="gray"
)
desc_label.grid(row=1, column=1, sticky="w")
def _save_config(self):
"""保存配置"""
for feature_id, switch in self.switches.items():
enabled = switch.get() == 1
self.task_runner.update_feature(feature_id, enabled)
self.app.set_status("杂项配置已保存")

View File

@@ -0,0 +1,96 @@
"""
待定功能配置页面
"""
import customtkinter as ctk
from .base_page import BasePage
from ...core.tasks.pending_tasks import PendingTaskRunner
class PendingConfigPage(BasePage):
"""待定功能配置页面"""
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
super().__init__(parent, app)
self.task_runner = PendingTaskRunner()
self.switches = {}
def build(self) -> ctk.CTkFrame:
"""构建页面"""
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
# 标题
title = ctk.CTkLabel(
frame,
text="⏳ 待定功能配置",
font=ctk.CTkFont(size=18, weight="bold")
)
title.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="w")
# 说明
desc = ctk.CTkLabel(
frame,
text="预留功能配置(待开发)",
font=ctk.CTkFont(size=12),
text_color="gray"
)
desc.grid(row=1, column=0, padx=20, pady=(0, 20), sticky="w")
# 功能列表
features_frame = ctk.CTkFrame(frame)
features_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=10)
features_frame.grid_columnconfigure(0, weight=1)
features = self.task_runner.get_features()
for idx, (feature_id, config) in enumerate(features.items()):
self._create_feature_item(features_frame, feature_id, config, idx)
# 提示信息
info_frame = ctk.CTkFrame(frame, fg_color=("#fff3cd", "#3d3a2a"))
info_frame.grid(row=3, column=0, sticky="ew", padx=20, pady=20)
info_label = ctk.CTkLabel(
info_frame,
text="💡 这些功能正在开发中,敬请期待!",
font=ctk.CTkFont(size=12)
)
info_label.pack(padx=20, pady=15)
return frame
def _create_feature_item(self, parent, feature_id: str, config: dict, row: int):
"""创建功能项"""
frame = ctk.CTkFrame(parent, fg_color="transparent")
frame.grid(row=row, column=0, sticky="ew", padx=10, pady=5)
frame.grid_columnconfigure(1, weight=1)
# 开关(禁用状态)
switch = ctk.CTkSwitch(
frame,
text="",
width=50,
state="disabled"
)
switch.grid(row=0, column=0, padx=(0, 10))
if config["enabled"]:
switch.select()
self.switches[feature_id] = switch
# 名称
name_label = ctk.CTkLabel(
frame,
text=config["name"],
font=ctk.CTkFont(size=13, weight="bold")
)
name_label.grid(row=0, column=1, sticky="w")
# 描述
desc_label = ctk.CTkLabel(
frame,
text=config["description"],
font=ctk.CTkFont(size=11),
text_color="gray"
)
desc_label.grid(row=1, column=1, sticky="w")

200
src/ui/pages/run_page.py Normal file
View File

@@ -0,0 +1,200 @@
"""
运行页面 - 主控制面板
包含基础配置和运行控制
"""
import customtkinter as ctk
from src.utils.logger import logger
from .base_page import BasePage
from ...core.automation import AutomationController
class RunPage(BasePage):
"""运行页面"""
def __init__(self, parent: ctk.CTkFrame, app: "LuoLuoApp"):
super().__init__(parent, app)
self.automation = AutomationController()
self.automation.set_callback(self._on_automation_message)
self.log_textbox: Optional[ctk.CTkTextbox] = None
def build(self) -> ctk.CTkFrame:
"""构建运行页面"""
frame = ctk.CTkFrame(self.parent, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
frame.grid_rowconfigure(3, weight=1)
# ===== 基础配置区域 =====
config_frame = ctk.CTkFrame(frame)
config_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
config_frame.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(
config_frame,
text="⚙️ 基础配置",
font=ctk.CTkFont(size=14, weight="bold")
).grid(row=0, column=0, columnspan=3, padx=10, pady=(10, 5), sticky="w")
# 游戏窗口
ctk.CTkLabel(config_frame, text="游戏窗口:").grid(row=1, column=0, padx=10, pady=5, sticky="w")
self.window_label = ctk.CTkLabel(config_frame, text="未捕获", text_color="gray")
self.window_label.grid(row=1, column=1, padx=10, pady=5, sticky="w")
self.capture_btn = ctk.CTkButton(
config_frame,
text="🔍 重新捕获",
width=100,
command=self._capture_window
)
self.capture_btn.grid(row=1, column=2, padx=10, pady=5)
# 运行热键
ctk.CTkLabel(config_frame, text="运行热键:").grid(row=2, column=0, padx=10, pady=5, sticky="w")
self.hotkey_entry = ctk.CTkEntry(config_frame, placeholder_text="F9")
self.hotkey_entry.insert(0, "F9")
self.hotkey_entry.grid(row=2, column=1, padx=10, pady=5, sticky="w")
# 日志等级
ctk.CTkLabel(config_frame, text="日志等级:").grid(row=3, column=0, padx=10, pady=5, sticky="w")
self.loglevel_combo = ctk.CTkComboBox(
config_frame,
values=["DEBUG", "INFO", "WARNING", "ERROR"],
width=120
)
self.loglevel_combo.set("INFO")
self.loglevel_combo.grid(row=3, column=1, padx=10, pady=5, sticky="w")
# ===== 窗口状态区域 =====
status_frame = ctk.CTkFrame(frame)
status_frame.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
self.status_label = ctk.CTkLabel(
status_frame,
text="🎮 等待捕获游戏窗口...",
font=ctk.CTkFont(size=12)
)
self.status_label.pack(padx=20, pady=15)
# ===== 控制按钮区域 =====
control_frame = ctk.CTkFrame(frame, fg_color="transparent")
control_frame.grid(row=2, column=0, sticky="ew", padx=5, pady=10)
self.start_btn = ctk.CTkButton(
control_frame,
text="🚀 开始挂机",
font=ctk.CTkFont(size=14, weight="bold"),
height=40,
fg_color="#2ecc71",
hover_color="#27ae60",
command=self._start_automation
)
self.start_btn.pack(side="left", padx=5)
self.stop_btn = ctk.CTkButton(
control_frame,
text="⏹ 停止",
font=ctk.CTkFont(size=14),
height=40,
fg_color="#e74c3c",
hover_color="#c0392b",
state="disabled",
command=self._stop_automation
)
self.stop_btn.pack(side="left", padx=5)
self.pause_btn = ctk.CTkButton(
control_frame,
text="⏸ 暂停",
font=ctk.CTkFont(size=14),
height=40,
state="disabled",
command=self._pause_automation
)
self.pause_btn.pack(side="left", padx=5)
# ===== 日志区域 =====
log_frame = ctk.CTkFrame(frame)
log_frame.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
log_frame.grid_columnconfigure(0, weight=1)
log_frame.grid_rowconfigure(1, weight=1)
ctk.CTkLabel(
log_frame,
text="📋 运行日志",
font=ctk.CTkFont(size=12, weight="bold")
).grid(row=0, column=0, padx=10, pady=(10, 5), sticky="w")
self.log_textbox = ctk.CTkTextbox(
log_frame,
wrap="word",
state="disabled",
font=ctk.CTkFont(size=11)
)
self.log_textbox.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 10))
# 尝试自动捕获窗口
self._capture_window()
return frame
def _capture_window(self):
"""捕获游戏窗口"""
if self.automation.window_manager.capture_window():
size = self.automation.window_manager.client_size
self.window_label.configure(
text=f"桃源深处有人家 - {size[0]}x{size[1]}",
text_color="#2ecc71"
)
self.status_label.configure(
text=f"✅ 已捕获游戏窗口 ({size[0]}x{size[1]})",
text_color="#2ecc71"
)
self.app.set_status("窗口已捕获")
else:
self.window_label.configure(text="未捕获", text_color="#e74c3c")
self.status_label.configure(
text="❌ 未找到游戏窗口,请确保游戏已运行",
text_color="#e74c3c"
)
self.app.set_status("未找到游戏窗口")
def _start_automation(self):
"""开始自动化"""
self.automation.start()
self.start_btn.configure(state="disabled")
self.stop_btn.configure(state="normal")
self.pause_btn.configure(state="normal", text="⏸ 暂停")
self.app.set_status("运行中")
def _stop_automation(self):
"""停止自动化"""
self.automation.stop()
self.start_btn.configure(state="normal")
self.stop_btn.configure(state="disabled")
self.pause_btn.configure(state="disabled", text="⏸ 暂停")
self.app.set_status("已停止")
def _pause_automation(self):
"""暂停/继续"""
self.automation.pause()
if self.automation.is_paused:
self.pause_btn.configure(text="▶ 继续")
self.app.set_status("已暂停")
else:
self.pause_btn.configure(text="⏸ 暂停")
self.app.set_status("运行中")
def _on_automation_message(self, message: str):
"""接收自动化消息"""
self._add_log(message)
def _add_log(self, message: str):
"""添加日志"""
if self.log_textbox:
from datetime import datetime
timestamp = datetime.now().strftime("%H:%M:%S")
self.log_textbox.configure(state="normal")
self.log_textbox.insert("end", f"[{timestamp}] {message}\n")
self.log_textbox.see("end")
self.log_textbox.configure(state="disabled")

8
src/utils/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
"""
工具模块
"""
from .logger import setup_logger
from .config import ConfigManager
__all__ = ["setup_logger", "ConfigManager"]

93
src/utils/config.py Normal file
View File

@@ -0,0 +1,93 @@
"""
配置管理
"""
import json
from pathlib import Path
from typing import Any, Dict
from src.utils.logger import logger
class ConfigManager:
"""配置管理器"""
# 默认配置
DEFAULT_CONFIG = {
"version": "0.1",
"window": {
"width": 900,
"height": 650,
"theme": "System", # System, Light, Dark
},
"automation": {
"hotkey": "F9",
"log_level": "INFO",
},
"tasks": {
"daily": {},
"misc": {},
"pending": {},
}
}
def __init__(self, config_path: Path = None):
if config_path is None:
config_path = Path(__file__).parent.parent.parent / "configs" / "settings.json"
self.config_path = config_path
self.config_path.parent.mkdir(exist_ok=True)
self._config = self.DEFAULT_CONFIG.copy()
self.load()
def load(self):
"""加载配置"""
if self.config_path.exists():
try:
with open(self.config_path, "r", encoding="utf-8") as f:
loaded = json.load(f)
self._config.update(loaded)
logger.info(f"配置已加载: {self.config_path}")
except Exception as e:
logger.error(f"加载配置失败: {e}")
self.save() # 保存默认配置
else:
self.save() # 首次运行,保存默认配置
def save(self):
"""保存配置"""
try:
with open(self.config_path, "w", encoding="utf-8") as f:
json.dump(self._config, f, indent=4, ensure_ascii=False)
logger.info(f"配置已保存: {self.config_path}")
except Exception as e:
logger.error(f"保存配置失败: {e}")
def get(self, key: str, default: Any = None) -> Any:
"""获取配置项"""
keys = key.split(".")
value = self._config
for k in keys:
if isinstance(value, dict) and k in value:
value = value[k]
else:
return default
return value
def set(self, key: str, value: Any):
"""设置配置项"""
keys = key.split(".")
config = self._config
for k in keys[:-1]:
if k not in config:
config[k] = {}
config = config[k]
config[keys[-1]] = value
def get_all(self) -> Dict:
"""获取所有配置"""
return self._config.copy()

82
src/utils/logger.py Normal file
View File

@@ -0,0 +1,82 @@
"""
日志配置 - 使用标准库logging
"""
import sys
import logging
from pathlib import Path
from datetime import datetime
def setup_logger(log_dir: Path = None, level: str = "INFO"):
"""
配置日志系统
Args:
log_dir: 日志文件目录默认为项目根目录下的logs文件夹
level: 日志级别
"""
if log_dir is None:
log_dir = Path(__file__).parent.parent.parent / "logs"
log_dir.mkdir(exist_ok=True)
# 设置日志级别
log_level = getattr(logging, level.upper(), logging.INFO)
# 配置根日志记录器
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# 清除现有处理器
logger.handlers.clear()
# 控制台处理器
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(log_level)
console_format = logging.Formatter(
'%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s',
datefmt='%H:%M:%S'
)
console_handler.setFormatter(console_format)
logger.addHandler(console_handler)
# 文件处理器
log_file = log_dir / f"luoluo_{datetime.now().strftime('%Y-%m-%d')}.log"
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_format = logging.Formatter(
'%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(file_format)
logger.addHandler(file_handler)
return logger
# 创建一个简单的logger别名兼容loguru的接口
class SimpleLogger:
"""简单的logger包装类提供类似loguru的接口"""
def __init__(self):
self._logger = logging.getLogger("LuoLuoTool")
def debug(self, message):
self._logger.debug(message)
def info(self, message):
self._logger.info(message)
def warning(self, message):
self._logger.warning(message)
def error(self, message):
self._logger.error(message)
def critical(self, message):
self._logger.critical(message)
# 全局logger实例
logger = SimpleLogger()