Files
guba-indicator/main_window.py

363 lines
12 KiB
Python

"""
PySide6 GUI界面模块
"""
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSlider, QDialog, QFormLayout,
QLineEdit, QSpinBox, QMessageBox, QSystemTrayIcon,
QMenu, QTextEdit, QGroupBox, QDialogButtonBox)
from PySide6.QtCore import Qt, QTimer, Signal, QPoint
from PySide6.QtGui import QFont, QColor, QPainter, QBrush, QPen, QIcon, QAction
from typing import Callable, Optional
class SentimentIndicator(QWidget):
"""情感指示灯组件"""
def __init__(self, parent=None):
super().__init__(parent)
self.score = 50
self.label_text = "中性"
self.setMinimumSize(100, 100)
def set_value(self, score: int, label: str = None):
"""设置数值和标签"""
self.score = max(0, min(100, score))
if label:
self.label_text = label
self.update()
def paintEvent(self, event):
"""绘制指示灯"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
center = self.rect().center()
radius = min(self.width(), self.height()) // 2 - 10
# 根据分数确定颜色
color = self._get_color(self.score)
# 绘制外圈
painter.setPen(QPen(QColor(100, 100, 100), 2))
painter.setBrush(QBrush(QColor(30, 30, 30)))
painter.drawEllipse(center, radius, radius)
# 绘制内圈(渐变效果)
gradient_color = QColor(color)
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(gradient_color))
painter.drawEllipse(center, radius - 4, radius - 4)
# 绘制发光效果
for i in range(3, 0, -1):
glow_color = QColor(color)
glow_color.setAlpha(50 // i)
painter.setBrush(QBrush(glow_color))
painter.drawEllipse(center, radius - i * 5, radius - i * 5)
def _get_color(self, score: int) -> QColor:
"""根据分数获取颜色"""
if score < 30:
# 冷色系 - 蓝色/青色
ratio = score / 30
return QColor(int(0 + 100 * ratio), int(150 + 50 * ratio), 255)
elif score < 70:
# 中性 - 灰色/绿色
if score < 50:
ratio = (score - 30) / 20
return QColor(int(100 + 50 * ratio), int(200 + 20 * ratio), int(200 - 50 * ratio))
else:
ratio = (score - 50) / 20
return QColor(int(150 + 50 * ratio), int(220 - 20 * ratio), int(150 - 50 * ratio))
else:
# 暖色系 - 橙色/红色
ratio = (score - 70) / 30
return QColor(255, int(200 - 100 * ratio), int(50 + 50 * ratio))
def get_description(self, score: int) -> str:
"""获取描述文本"""
if score < 20:
return "极度看跌"
elif score < 40:
return "偏悲观"
elif score < 60:
return "中性"
elif score < 80:
return "偏乐观"
else:
return "极度看涨"
class ConfigDialog(QDialog):
"""配置对话框"""
def __init__(self, config_manager, parent=None):
super().__init__(parent)
self.config_manager = config_manager
self.setWindowTitle("配置")
self.setMinimumWidth(400)
self._init_ui()
def _init_ui(self):
layout = QFormLayout(self)
# LLM API 配置
llm_config = self.config_manager.llm_api_config
self.base_url_edit = QLineEdit(llm_config.get('base_url', ''))
self.api_key_edit = QLineEdit(llm_config.get('api_key', ''))
self.api_key_edit.setEchoMode(QLineEdit.Password)
self.model_edit = QLineEdit(llm_config.get('model', ''))
self.timeout_spin = QSpinBox()
self.timeout_spin.setRange(10, 300)
self.timeout_spin.setValue(llm_config.get('timeout', 30))
layout.addRow("API Base URL:", self.base_url_edit)
layout.addRow("API Key:", self.api_key_edit)
layout.addRow("Model:", self.model_edit)
layout.addRow("Timeout (s):", self.timeout_spin)
# 爬虫配置
spider_config = self.config_manager.spider_config
self.url_edit = QLineEdit(spider_config.get('target_url', ''))
self.xpath_edit = QLineEdit(spider_config.get('xpath', ''))
self.user_agent_edit = QLineEdit(spider_config.get('user_agent', ''))
self.interval_spin = QSpinBox()
self.interval_spin.setRange(10, 3600)
self.interval_spin.setValue(spider_config.get('fetch_interval', 60))
layout.addRow("目标URL:", self.url_edit)
layout.addRow("XPath:", self.xpath_edit)
layout.addRow("User Agent:", self.user_agent_edit)
layout.addRow("刷新间隔(s):", self.interval_spin)
# UI 配置
ui_config = self.config_manager.ui_config
self.opacity_slider = QSlider(Qt.Horizontal)
self.opacity_slider.setRange(30, 100)
self.opacity_slider.setValue(int(ui_config.get('opacity', 0.9) * 100))
self.ontop_check = QCheckBox() if hasattr(self, 'QCheckBox') else None
# 使用 QPushButton 替代 QCheckBox
self.ontop_btn = QPushButton("置顶")
self.ontop_btn.setCheckable(True)
self.ontop_btn.setChecked(ui_config.get('is_on_top', True))
layout.addRow("透明度:", self.opacity_slider)
layout.addRow("窗口置顶:", self.ontop_btn)
# 阈值配置
thresholds = ui_config.get('thresholds', {})
self.cold_spin = QSpinBox()
self.cold_spin.setRange(0, 50)
self.cold_spin.setValue(thresholds.get('cold', 30))
self.warm_spin = QSpinBox()
self.warm_spin.setRange(50, 100)
self.warm_spin.setValue(thresholds.get('warm', 70))
layout.addRow("寒冷阈值:", self.cold_spin)
layout.addRow("温暖阈值:", self.warm_spin)
# 按钮
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self._save_config)
button_box.rejected.connect(self.reject)
layout.addRow(button_box)
def _save_config(self):
"""保存配置"""
# LLM API
self.config_manager.update_llm_api(
base_url=self.base_url_edit.text(),
api_key=self.api_key_edit.text(),
model=self.model_edit.text(),
timeout=self.timeout_spin.value()
)
# 爬虫
self.config_manager.update_spider(
target_url=self.url_edit.text(),
xpath=self.xpath_edit.text(),
user_agent=self.user_agent_edit.text(),
fetch_interval=self.interval_spin.value()
)
# UI
self.config_manager.update_ui(
opacity=self.opacity_slider.value() / 100.0,
is_on_top=self.ontop_btn.isChecked(),
cold_threshold=self.cold_spin.value(),
warm_threshold=self.warm_spin.value()
)
self.accept()
class MainWindow(QWidget):
"""主窗口"""
def __init__(self, config_manager, parent=None):
super().__init__(parent)
self.config_manager = config_manager
self.setWindowTitle("股吧人气指示器")
self._init_ui()
self._apply_config()
# 拖拽相关
self.dragging = False
self.drag_position = QPoint()
# 系统托盘
self._init_tray_icon()
def _init_ui(self):
"""初始化UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 10, 10, 10)
# 标题
self.title_label = QLabel("股吧人气")
self.title_label.setAlignment(Qt.AlignCenter)
title_font = QFont()
title_font.setPointSize(14)
title_font.setBold(True)
self.title_label.setFont(title_font)
# 指示灯
self.indicator = SentimentIndicator()
self.score_label = QLabel("50 - 中性")
self.score_label.setAlignment(Qt.AlignCenter)
# 状态信息
self.status_label = QLabel("等待数据...")
self.status_label.setAlignment(Qt.AlignCenter)
status_font = QFont()
status_font.setPointSize(10)
self.status_label.setFont(status_font)
# 按钮
btn_layout = QHBoxLayout()
self.refresh_btn = QPushButton("刷新")
self.config_btn = QPushButton("配置")
btn_layout.addWidget(self.refresh_btn)
btn_layout.addWidget(self.config_btn)
# 添加到主布局
layout.addWidget(self.title_label)
layout.addWidget(self.indicator)
layout.addWidget(self.score_label)
layout.addWidget(self.status_label)
layout.addLayout(btn_layout)
# 设置窗口标志(无边框、可拖拽)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
def _init_tray_icon(self):
"""初始化系统托盘"""
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setToolTip("股吧人气指示器")
# 创建托盘菜单
tray_menu = QMenu()
show_action = QAction("显示", self)
hide_action = QAction("隐藏", self)
quit_action = QAction("退出", self)
show_action.triggered.connect(self.show)
hide_action.triggered.connect(self.hide)
quit_action.triggered.connect(self.quit_app)
tray_menu.addAction(show_action)
tray_menu.addAction(hide_action)
tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.show()
def quit_app(self):
"""退出应用"""
self.close()
import sys
sys.exit(0)
def _apply_config(self):
"""应用配置"""
ui_config = self.config_manager.ui_config
self.setWindowOpacity(ui_config.get('opacity', 0.9))
if ui_config.get('is_on_top', True):
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
else:
self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
thresholds = ui_config.get('thresholds', {})
def mousePressEvent(self, event):
"""鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.dragging = True
self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
def mouseMoveEvent(self, event):
"""鼠标移动事件"""
if self.dragging:
self.move(event.globalPosition().toPoint() - self.drag_position)
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
if event.button() == Qt.LeftButton:
self.dragging = False
def contextMenuEvent(self, event):
"""右键菜单"""
context_menu = QMenu(self)
config_action = QAction("配置", self)
opacity_action = QAction("透明度", self)
quit_action = QAction("退出", self)
config_action.triggered.connect(self.show_config)
quit_action.triggered.connect(self.quit_app)
context_menu.addAction(config_action)
context_menu.addAction(quit_action)
context_menu.exec(event.globalPosition().toPoint())
def show_config(self):
"""显示配置对话框"""
dialog = ConfigDialog(self.config_manager, self)
if dialog.exec() == QDialog.Accepted:
self._apply_config()
def update_indicator(self, score: int, label: str = None):
"""更新指示灯"""
if label is None:
label = self.indicator.get_description(score)
self.indicator.set_value(score, label)
self.score_label.setText(f"{score} - {label}")
def update_status(self, text: str):
"""更新状态"""
self.status_label.setText(text)
def set_refresh_callback(self, callback: Callable):
"""设置刷新按钮回调"""
self.refresh_btn.clicked.connect(callback)
def set_config_callback(self, callback: Callable):
"""设置配置按钮回调"""
self.config_btn.clicked.connect(callback)
def show_message(self, title: str, message: str, icon=QMessageBox.Information):
"""显示消息"""
QMessageBox.information(self, title, message)
class QCheckBox(QPushButton):
"""自定义复选框"""
def __init__(self, text=""):
super().__init__(text)
self.setCheckable(True)
self.setChecked(False)