scroll_window.h

本文介绍了Windows应用程序中消息处理函数WndProc和特定资源ID的宏定义,包括退出、测试及关于对话框的处理。通过宏定义实现了资源标识符的指定,并展示了两个回调函数的声明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  name="google_ads_frame" marginwidth="0" marginheight="0" src="http://pagead2.googlesyndication.com/pagead/ads?client=ca-pub-5572165936844014&dt=1194442938015&lmt=1194190197&format=336x280_as&output=html&correlator=1194442937843&url=file%3A%2F%2F%2FC%3A%2FDocuments%2520and%2520Settings%2Flhh1%2F%E6%A1%8C%E9%9D%A2%2FCLanguage.htm&color_bg=FFFFFF&color_text=000000&color_link=000000&color_url=FFFFFF&color_border=FFFFFF&ad_type=text&ga_vid=583001034.1194442938&ga_sid=1194442938&ga_hid=1942779085&flash=9&u_h=768&u_w=1024&u_ah=740&u_aw=1024&u_cd=32&u_tz=480&u_java=true" frameborder="0" width="336" scrolling="no" height="280" allowtransparency="allowtransparency"> #define IDM_EXIT           100
#define IDM_TEST           200
#define IDM_ABOUT          301

LRESULT CALLBACK WndProc  (HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK About    (HWND, UINT, WPARAM, LPARAM);

# 主要优化点: # 1. 增加了多图片批量处理功能 # 2. 添加了历史记录保存和加载功能 # 3. 实现了结果导出功能(TXT/PDF/HTML) # 4. 优化了UI布局和主题系统 # 5. 增加了模型参数配置预设 # 6. 添加了系统托盘支持 # 7. 实现了结果自动保存 import sys import os import base64 import json import requests import webbrowser from datetime import datetime from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit, QFileDialog, QGroupBox, QSlider, QDoubleSpinBox, QProgressBar, QSplitter, QMessageBox, QListWidget, QListWidgetItem, QSystemTrayIcon, QMenu, QAction, QComboBox, QTabWidget, QScrollArea, QCheckBox) from PyQt5.QtGui import (QPixmap, QFont, QPalette, QColor, QTextCursor, QIcon, QTextDocumentWriter, QTextDocument) from PyQt5.QtCore import Qt, QSize, QThread, pyqtSignal, QTimer, QSettings # 配置OLLAMA API设置 OLLAMA_HOST = "http://localhost:11434" HISTORY_FILE = "history.json" SETTINGS_FILE = "settings.ini" class EnhancedListWidgetItem(QListWidgetItem): """增强的列表项,支持图标和状态显示""" def __init__(self, text, is_title=False, icon=None, parent=None): super().__init__(text, parent) self.is_title = is_title if icon: self.setIcon(icon) if is_title: self.setSizeHint(QSize(200, 35)) font = QFont("Microsoft YaHei UI", 10, QFont.Bold) self.setFont(font) self.setForeground(QColor("#64ffda")) self.setFlags(Qt.NoItemFlags) self.setBackground(QColor("#112240")) class ModelLoaderThread(QThread): # ...保持原有实现... class ImageAnalysisThread(QThread): # ...保持原有实现... class ExportThread(QThread): export_finished = pyqtSignal(str, bool) def __init__(self, content, file_path, format_type, parent=None): super().__init__(parent) self.content = content self.file_path = file_path self.format_type = format_type def run(self): try: if self.format_type == "html": with open(self.file_path, "w", encoding="utf-8") as f: f.write(self.content) elif self.format_type == "txt": with open(self.file_path, "w", encoding="utf-8") as f: f.write(self.content) elif self.format_type == "pdf": doc = QTextDocument() doc.setHtml(self.content) writer = QTextDocumentWriter(self.file_path) writer.write(doc) self.export_finished.emit(self.file_path, True) except Exception as e: self.export_finished.emit(str(e), False) class MultiModalApp(QMainWindow): def __init__(self): super().__init__() self.image_paths = [] self.current_image_index = 0 self.history = [] self.settings = QSettings(SETTINGS_FILE, QSettings.IniFormat) self.initUI() self.load_settings() self.setWindowTitle("增强版多模态大模型图像解读系统") self.setGeometry(100, 100, 1920, 1000) # 初始化系统托盘 self.init_tray_icon() QTimer.singleShot(500, self.load_models) def initUI(self): # 创建暗色主题样式表 self.setStyleSheet(""" /* 主窗口样式 */ QMainWindow { background-color: #0a192f; } /* 分组框样式 */ QGroupBox { border: 2px solid #64ffda; border-radius: 10px; margin-top: 1ex; color: #ccd6f6; font-weight: bold; } /* 标签样式 */ QLabel { color: #ccd6f6; } /* 按钮样式 */ QPushButton { background-color: #112240; color: #64ffda; border: 1px solid #64ffda; border-radius: 5px; padding: 5px 10px; font-weight: bold; } /* 文本框样式 */ QTextEdit { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; padding: 5px; font-size: 12pt; } /* 选项卡样式 */ QTabWidget::pane { border: 1px solid #64ffda; border-radius: 5px; background: #0a192f; } QTabBar::tab { background: #112240; color: #ccd6f6; padding: 8px; border: 1px solid #64ffda; border-bottom: none; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:selected { background: #233554; color: #64ffda; } """) # 设置主窗口布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 标题区域 title_layout = QHBoxLayout() self.title_label = QLabel("增强版多模态大模型图像解读系统") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #64ffda;") title_layout.addWidget(self.title_label) # 添加主题切换按钮 self.theme_button = QPushButton("切换主题") self.theme_button.clicked.connect(self.toggle_theme) title_layout.addWidget(self.theme_button) main_layout.addLayout(title_layout) # 主内容区域 splitter = QSplitter(Qt.Horizontal) # 左侧控制面板 left_panel = QWidget() left_layout = QVBoxLayout(left_panel) # 图片预览区域(改为选项卡形式) self.image_tabs = QTabWidget() self.image_tabs.setTabsClosable(True) self.image_tabs.tabCloseRequested.connect(self.close_image_tab) left_layout.addWidget(self.image_tabs) # 控制面板区域 control_tabs = QTabWidget() # 模型控制选项卡 model_tab = QWidget() self.setup_model_tab(model_tab) control_tabs.addTab(model_tab, "模型设置") # 参数控制选项卡 param_tab = QWidget() self.setup_param_tab(param_tab) control_tabs.addTab(param_tab, "参数设置") # 预设控制选项卡 preset_tab = QWidget() self.setup_preset_tab(preset_tab) control_tabs.addTab(preset_tab, "预设管理") left_layout.addWidget(control_tabs) # 右侧结果面板 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) # 结果展示区域 result_tabs = QTabWidget() # 分析结果选项卡 self.result_tab = QWidget() self.setup_result_tab(self.result_tab) result_tabs.addTab(self.result_tab, "分析结果") # 历史记录选项卡 self.history_tab = QWidget() self.setup_history_tab(self.history_tab) result_tabs.addTab(self.history_tab, "历史记录") right_layout.addWidget(result_tabs) # 添加面板到分割器 splitter.addWidget(left_panel) splitter.addWidget(right_panel) splitter.setSizes([800, 1100]) main_layout.addWidget(splitter) # 状态栏 self.status_bar = self.statusBar() self.status_bar.setStyleSheet("background-color: #112240; color: #64ffda;") self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.status_bar.addPermanentWidget(self.progress_bar) self.status_bar.showMessage("系统已就绪") def setup_model_tab(self, tab): layout = QVBoxLayout(tab) # 模型选择区域 model_group = QGroupBox("模型选择") model_layout = QVBoxLayout() self.model_list = QListWidget() self.model_list.setMaximumHeight(150) self.model_list.addItem("正在加载模型...") self.refresh_models_button = QPushButton("刷新模型列表") self.refresh_models_button.clicked.connect(self.load_models) model_layout.addWidget(self.model_list) model_layout.addWidget(self.refresh_models_button) model_group.setLayout(model_layout) layout.addWidget(model_group) # 图片操作区域 image_group = QGroupBox("图片操作") image_layout = QVBoxLayout() self.load_button = QPushButton("加载图片") self.load_button.clicked.connect(self.load_image) self.load_multiple_button = QPushButton("批量加载图片") self.load_multiple_button.clicked.connect(self.load_multiple_images) self.clear_images_button = QPushButton("清除所有图片") self.clear_images_button.clicked.connect(self.clear_all_images) image_layout.addWidget(self.load_button) image_layout.addWidget(self.load_multiple_button) image_layout.addWidget(self.clear_images_button) image_group.setLayout(image_layout) layout.addWidget(image_group) # 分析控制区域 control_group = QGroupBox("分析控制") control_layout = QVBoxLayout() self.analyze_button = QPushButton("分析当前图片") self.analyze_button.clicked.connect(self.analyze_image) self.analyze_button.setEnabled(False) self.analyze_all_button = QPushButton("批量分析所有图片") self.analyze_all_button.clicked.connect(self.analyze_all_images) self.analyze_all_button.setEnabled(False) self.stop_button = QPushButton("停止分析") self.stop_button.clicked.connect(self.stop_analysis) self.stop_button.setEnabled(False) control_layout.addWidget(self.analyze_button) control_layout.addWidget(self.analyze_all_button) control_layout.addWidget(self.stop_button) control_group.setLayout(control_layout) layout.addWidget(control_group) def setup_param_tab(self, tab): layout = QVBoxLayout(tab) # 温度控制 temp_group = QGroupBox("温度控制") temp_layout = QHBoxLayout() self.temp_slider = QSlider(Qt.Horizontal) self.temp_slider.setRange(0, 100) self.temp_slider.setValue(50) self.temp_value = QDoubleSpinBox() self.temp_value.setRange(0.0, 1.0) self.temp_value.setSingleStep(0.1) self.temp_value.setValue(0.5) self.temp_value.setDecimals(1) self.temp_slider.valueChanged.connect(lambda val: self.temp_value.setValue(val / 100)) self.temp_value.valueChanged.connect(lambda val: self.temp_slider.setValue(int(val * 100))) temp_layout.addWidget(self.temp_slider) temp_layout.addWidget(self.temp_value) temp_group.setLayout(temp_layout) layout.addWidget(temp_group) # Token控制 token_group = QGroupBox("Token控制") token_layout = QHBoxLayout() self.token_spin = QDoubleSpinBox() self.token_spin.setRange(100, 5000) self.token_spin.setValue(1000) self.token_spin.setSingleStep(100) token_layout.addWidget(QLabel("最大Token:")) token_layout.addWidget(self.token_spin) token_group.setLayout(token_layout) layout.addWidget(token_group) # 提示词区域 prompt_group = QGroupBox("提示词设置") prompt_layout = QVBoxLayout() self.prompt_edit = QTextEdit() self.prompt_edit.setPlainText("请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") prompt_layout.addWidget(self.prompt_edit) prompt_group.setLayout(prompt_layout) layout.addWidget(prompt_group) def setup_preset_tab(self, tab): layout = QVBoxLayout(tab) # 预设管理 preset_group = QGroupBox("预设管理") preset_layout = QVBoxLayout() self.preset_combo = QComboBox() self.preset_combo.addItems(["默认预设", "详细描述", "创意写作", "技术分析"]) button_layout = QHBoxLayout() self.load_preset_button = QPushButton("加载预设") self.load_preset_button.clicked.connect(self.load_preset) self.save_preset_button = QPushButton("保存预设") self.save_preset_button.clicked.connect(self.save_preset) self.delete_preset_button = QPushButton("删除预设") self.delete_preset_button.clicked.connect(self.delete_preset) button_layout.addWidget(self.load_preset_button) button_layout.addWidget(self.save_preset_button) button_layout.addWidget(self.delete_preset_button) preset_layout.addWidget(self.preset_combo) preset_layout.addLayout(button_layout) preset_group.setLayout(preset_layout) layout.addWidget(preset_group) # 自动保存设置 auto_save_group = QGroupBox("自动保存设置") auto_save_layout = QVBoxLayout() self.auto_save_check = QCheckBox("自动保存分析结果") self.auto_save_check.setChecked(True) self.auto_save_path_button = QPushButton("选择保存路径") self.auto_save_path_button.clicked.connect(self.select_auto_save_path) self.auto_save_path_label = QLabel("默认保存位置: 程序目录/results") self.auto_save_path_label.setWordWrap(True) auto_save_layout.addWidget(self.auto_save_check) auto_save_layout.addWidget(self.auto_save_path_button) auto_save_layout.addWidget(self.auto_save_path_label) auto_save_group.setLayout(auto_save_layout) layout.addWidget(auto_save_group) def setup_result_tab(self, tab): layout = QVBoxLayout(tab) # 结果展示区域 result_group = QGroupBox("分析结果") result_layout = QVBoxLayout() self.result_edit = QTextEdit() self.result_edit.setReadOnly(True) # 结果操作按钮 button_layout = QHBoxLayout() self.save_result_button = QPushButton("保存结果") self.save_result_button.clicked.connect(self.save_result) self.copy_result_button = QPushButton("复制结果") self.copy_result_button.clicked.connect(self.copy_result) self.clear_result_button = QPushButton("清除结果") self.clear_result_button.clicked.connect(self.clear_results) button_layout.addWidget(self.save_result_button) button_layout.addWidget(self.copy_result_button) button_layout.addWidget(self.clear_result_button) result_layout.addWidget(self.result_edit) result_layout.addLayout(button_layout) result_group.setLayout(result_layout) layout.addWidget(result_group) def setup_history_tab(self, tab): layout = QVBoxLayout(tab) # 历史记录区域 history_group = QGroupBox("历史记录") history_layout = QVBoxLayout() self.history_list = QListWidget() self.history_list.itemDoubleClicked.connect(self.load_history_item) # 历史操作按钮 button_layout = QHBoxLayout() self.load_history_button = QPushButton("加载历史") self.load_history_button.clicked.connect(self.load_history) self.clear_history_button = QPushButton("清除历史") self.clear_history_button.clicked.connect(self.clear_history) button_layout.addWidget(self.load_history_button) button_layout.addWidget(self.clear_history_button) history_layout.addWidget(self.history_list) history_layout.addLayout(button_layout) history_group.setLayout(history_layout) layout.addWidget(history_group) def init_tray_icon(self): """初始化系统托盘图标""" self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(QIcon(":/icons/app_icon.png")) tray_menu = QMenu() show_action = QAction("显示窗口", self) show_action.triggered.connect(self.show_normal) tray_menu.addAction(show_action) exit_action = QAction("退出", self) exit_action.triggered.connect(self.close) tray_menu.addAction(exit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() # 托盘图标点击事件 self.tray_icon.activated.connect(self.tray_icon_clicked) def show_normal(self): """从托盘恢复窗口显示""" self.showNormal() self.activateWindow() def tray_icon_clicked(self, reason): """处理托盘图标点击事件""" if reason == QSystemTrayIcon.DoubleClick: self.show_normal() def toggle_theme(self): """切换主题""" if self.theme_button.text() == "切换主题": # 切换到浅色主题 self.setStyleSheet(""" QMainWindow { background-color: #f5f5f5; } QGroupBox { border: 2px solid #2c3e50; border-radius: 10px; margin-top: 1ex; color: #2c3e50; font-weight: bold; } QLabel { color: #2c3e50; } QPushButton { background-color: #ecf0f1; color: #2c3e50; border: 1px solid #bdc3c7; border-radius: 5px; padding: 5px 10px; font-weight: bold; } QTextEdit { background-color: #ffffff; color: #2c3e50; border: 1px solid #bdc3c7; border-radius: 5px; padding: 5px; font-size: 12pt; } """) self.theme_button.setText("切换回暗色") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #2c3e50;") self.status_bar.setStyleSheet("background-color: #ecf0f1; color: #2c3e50;") else: # 切换回暗色主题 self.setStyleSheet(""" QMainWindow { background-color: #0a192f; } QGroupBox { border: 2px solid #64ffda; border-radius: 10px; margin-top: 1ex; color: #ccd6f6; font-weight: bold; } QLabel { color: #ccd6f6; } QPushButton { background-color: #112240; color: #64ffda; border: 1px solid #64ffda; border-radius: 5px; padding: 5px 10px; font-weight: bold; } QTextEdit { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; padding: 5px; font-size: 12pt; } """) self.theme_button.setText("切换主题") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #64ffda;") self.status_bar.setStyleSheet("background-color: #112240; color: #64ffda;") def load_settings(self): """加载程序设置""" self.settings.beginGroup("MainWindow") self.restoreGeometry(self.settings.value("geometry", self.saveGeometry())) self.restoreState(self.settings.value("windowState", self.saveState())) self.settings.endGroup() # 加载历史记录 self.load_history() def save_settings(self): """保存程序设置""" self.settings.beginGroup("MainWindow") self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.settings.endGroup() # 保存历史记录 self.save_history() def load_image(self): """加载单张图片""" file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_path: self.add_image_tab(file_path) def load_multiple_images(self): """批量加载多张图片""" file_paths, _ = QFileDialog.getOpenFileNames( self, "选择多张图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_paths: for file_path in file_paths: self.add_image_tab(file_path) def add_image_tab(self, file_path): """添加图片选项卡""" try: pixmap = QPixmap(file_path) if pixmap.isNull(): raise Exception("无法加载图片文件,可能格式不支持或文件已损坏") scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) image_label = QLabel() image_label.setPixmap(pixmap.scaled( scroll_area.width(), scroll_area.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation )) image_label.setAlignment(Qt.AlignCenter) scroll_area.setWidget(image_label) tab_index = self.image_tabs.addTab( scroll_area, os.path.basename(file_path) ) self.image_tabs.setCurrentIndex(tab_index) self.image_paths.append(file_path) self.analyze_button.setEnabled(True) self.analyze_all_button.setEnabled(True) self.status_bar.showMessage(f"已加载图片: {os.path.basename(file_path)}") # 更新当前图片索引 self.current_image_index = tab_index except Exception as e: self.status_bar.showMessage(f"错误: {str(e)}") self.show_error_dialog("图片加载错误", f"无法加载图片:\n{str(e)}") def close_image_tab(self, index): """关闭图片选项卡""" if index < len(self.image_paths): self.image_paths.pop(index) self.image_tabs.removeTab(index) if not self.image_paths: self.analyze_button.setEnabled(False) self.analyze_all_button.setEnabled(False) def clear_all_images(self): """清除所有图片""" self.image_tabs.clear() self.image_paths.clear() self.analyze_button.setEnabled(False) self.analyze_all_button.setEnabled(False) self.status_bar.showMessage("已清除所有图片") def analyze_image(self): """分析当前图片""" current_index = self.image_tabs.currentIndex() if current_index < 0 or current_index >= len(self.image_paths): self.status_bar.showMessage("错误: 没有可分析的图片") return self.current_image_index = current_index self._analyze_image(self.image_paths[current_index]) def analyze_all_images(self): """批量分析所有图片""" if not self.image_paths: self.status_bar.showMessage("错误: 没有可分析的图片") return # 保存当前索引 saved_index = self.current_image_index # 逐个分析图片 for i, image_path in enumerate(self.image_paths): self.current_image_index = i self.image_tabs.setCurrentIndex(i) self._analyze_image(image_path) # 等待分析完成 while self.analysis_thread and self.analysis_thread.isRunning(): QApplication.processEvents() # 恢复原始索引 self.current_image_index = saved_index self.image_tabs.setCurrentIndex(saved_index) def _analyze_image(self, image_path): """实际执行图片分析的内部方法""" selected_items = self.model_list.selectedItems() if not selected_items: self.status_bar.showMessage("错误: 请选择模型") return model_name = selected_items[0].data(Qt.UserRole) self.result_edit.clear() self.progress_bar.setValue(0) self.progress_bar.setFormat("正在分析图片...") self.set_buttons_enabled(False) self.stop_button.setEnabled(True) temperature = self.temp_value.value() max_tokens = int(self.token_spin.value()) prompt = self.prompt_edit.toPlainText().strip() self.analysis_thread = ImageAnalysisThread( model_name, image_path, temperature, max_tokens, prompt ) self.analysis_thread.analysis_complete.connect(self.handle_analysis_result) self.analysis_thread.progress_updated.connect(self.update_progress) self.analysis_thread.error_occurred.connect(self.handle_analysis_error) self.analysis_thread.stream_data.connect(self.analysis_stream_data) self.analysis_thread.finished.connect(self.analysis_finished) self.analysis_thread.start() def handle_analysis_result(self, result): """处理分析结果""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") selected_items = self.model_list.selectedItems() model_name = selected_items[0].text()[2:] if selected_items else "未知模型" # 获取当前图片文件名 image_name = os.path.basename(self.image_paths[self.current_image_index]) # 格式化结果 formatted_result = self.format_result(result) result_html = f""" <div style='color:#64ffda; font-size:14pt; font-weight:bold; margin-bottom:10px;'> 图片分析结果: {image_name} </div> <div style='color:#ccd6f6; font-size:12pt; line-height:1.6;'>{formatted_result}</div> <div style='margin-top: 20px; color: #8892b0; font-size: 10pt; border-top: 1px solid #233554; padding-top: 10px;'> 模型: <span style='color: #64ffda;'>{model_name}</span> | 时间: <span style='color: #64ffda;'>{timestamp}</span> </div> """ self.result_edit.setHtml(result_html) self.status_bar.showMessage(f"图片分析完成: {image_name}") # 添加到历史记录 self.add_to_history(image_name, model_name, timestamp, result_html) # 自动保存结果 if self.auto_save_check.isChecked(): self.auto_save_result(image_name, result_html) def add_to_history(self, image_name, model_name, timestamp, content): """添加到历史记录""" history_item = { "image": image_name, "model": model_name, "time": timestamp, "content": content } self.history.insert(0, history_item) self.update_history_list() def update_history_list(self): """更新历史记录列表""" self.history_list.clear() for item in self.history[:50]: # 最多显示50条历史记录 list_item = QListWidgetItem(f"{item['image']} - {item['time']}") list_item.setData(Qt.UserRole, item) self.history_list.addItem(list_item) def load_history_item(self, item): """加载历史记录项""" history_data = item.data(Qt.UserRole) self.result_edit.setHtml(history_data["content"]) def load_history(self): """从文件加载历史记录""" try: if os.path.exists(HISTORY_FILE): with open(HISTORY_FILE, "r", encoding="utf-8") as f: self.history = json.load(f) self.update_history_list() except Exception as e: print(f"加载历史记录失败: {str(e)}") def save_history(self): """保存历史记录到文件""" try: with open(HISTORY_FILE, "w", encoding="utf-8") as f: json.dump(self.history, f, ensure_ascii=False, indent=2) except Exception as e: print(f"保存历史记录失败: {str(e)}") def clear_history(self): """清除历史记录""" self.history.clear() self.history_list.clear() def auto_save_result(self, image_name, content): """自动保存结果""" try: save_dir = self.settings.value("AutoSave/path", "results") if not os.path.exists(save_dir): os.makedirs(save_dir) # 生成文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") base_name = os.path.splitext(image_name)[0] file_name = f"{base_name}_{timestamp}.html" file_path = os.path.join(save_dir, file_name) # 保存文件 with open(file_path, "w", encoding="utf-8") as f: f.write(content) self.status_bar.showMessage(f"结果已自动保存到: {file_path}") except Exception as e: print(f"自动保存失败: {str(e)}") def select_auto_save_path(self): """选择自动保存路径""" save_dir = QFileDialog.getExistingDirectory( self, "选择自动保存目录", self.settings.value("AutoSave/path", "results") ) if save_dir: self.settings.setValue("AutoSave/path", save_dir) self.auto_save_path_label.setText(f"保存位置: {save_dir}") def save_result(self): """保存结果到文件""" if not self.result_edit.toPlainText(): self.status_bar.showMessage("错误: 没有可保存的内容") return file_path, _ = QFileDialog.getSaveFileName( self, "保存结果", "", "HTML文件 (*.html);;文本文件 (*.txt);;PDF文件 (*.pdf)" ) if file_path: format_type = "html" if file_path.endswith(".txt"): format_type = "txt" elif file_path.endswith(".pdf"): format_type = "pdf" content = self.result_edit.toHtml() if format_type != "txt" else self.result_edit.toPlainText() self.export_thread = ExportThread(content, file_path, format_type) self.export_thread.export_finished.connect(self.handle_export_finished) self.export_thread.start() self.status_bar.showMessage("正在导出结果...") def handle_export_finished(self, message, success): """处理导出完成""" if success: self.status_bar.showMessage(f"结果已保存到: {message}") if message.endswith(".html"): webbrowser.open(message) else: self.status_bar.showMessage(f"导出失败: {message}") self.show_error_dialog("导出错误", f"无法保存结果:\n{message}") def copy_result(self): """复制结果到剪贴板""" self.result_edit.selectAll() self.result_edit.copy() self.status_bar.showMessage("结果已复制到剪贴板") def clear_results(self): """清除结果""" self.result_edit.clear() self.status_bar.showMessage("已清除结果") def load_preset(self): """加载预设""" preset_name = self.preset_combo.currentText() if preset_name == "默认预设": self.temp_value.setValue(0.5) self.token_spin.setValue(1000) self.prompt_edit.setPlainText("请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") elif preset_name == "详细描述": self.temp_value.setValue(0.3) self.token_spin.setValue(1500) self.prompt_edit.setPlainText("请用中文详细描述这张图片中的每一个细节,包括但不限于场景、人物、物体、颜色、空间关系等。要求描述系统全面,层次分明,每段描述一个方面。") elif preset_name == "创意写作": self.temp_value.setValue(0.7) self.token_spin.setValue(2000) self.prompt_edit.setPlainText("请根据这张图片创作一个富有想象力的故事或诗歌。要求内容生动有趣,语言优美,可以适当发挥想象力,但不要偏离图片内容太远。") elif preset_name == "技术分析": self.temp_value.setValue(0.2) self.token_spin.setValue(1200) self.prompt_edit.setPlainText("请从技术角度分析这张图片,包括但不限于构图、色彩、光线、透视等专业要素。要求分析专业准确,使用适当的专业术语。") self.status_bar.showMessage(f"已加载预设: {preset_name}") def save_preset(self): """保存当前设置为预设""" preset_name, ok = QInputDialog.getText( self, "保存预设", "请输入预设名称:", QLineEdit.Normal, self.preset_combo.currentText() ) if ok and preset_name: # 检查是否已存在 index = self.preset_combo.findText(preset_name) if index == -1: self.preset_combo.addItem(preset_name) self.preset_combo.setCurrentText(preset_name) self.status_bar.showMessage(f"预设 '{preset_name}' 已保存") def delete_preset(self): """删除预设""" preset_name = self.preset_combo.currentText() if preset_name in ["默认预设", "详细描述", "创意写作", "技术分析"]: QMessageBox.warning(self, "警告", "系统预设不能被删除") return reply = QMessageBox.question( self, "确认删除", f"确定要删除预设 '{preset_name}' 吗?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: index = self.preset_combo.currentIndex() self.preset_combo.removeItem(index) self.status_bar.showMessage(f"预设 '{preset_name}' 已删除") def closeEvent(self, event): """关闭窗口事件""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() if self.model_loader_thread and self.model_loader_thread.isRunning(): self.model_loader_thread.terminate() self.model_loader_thread.wait(2000) self.save_settings() event.accept() if __name__ == "__main__": def exception_handler(exctype, value, traceback): error_msg = f"程序发生未捕获的异常:\n\n类型: {exctype.__name__}\n\n描述: {value}" print(error_msg) try: app = QApplication.instance() if app is not None: msg = QMessageBox()
08-11
#下面程序运行地报错: 分析过程中发生错误 启动分析线程失败: 'MultiModalApp' object has no attribute 'update_progress' Traceback (most recent call last): File "D:\PyCharmMiscProject\2025-08-10——Rag高度增能.py", line 1044, in _analyze_image self.analysis_thread.progress_updated.connect(self.update_progress) ^^^^^^^^^^^^^^^^^^^^ AttributeError: 'MultiModalApp' object has no attribute 'update_progress' —————————————————————————————————————————————————————————————————————————————— import sys import os import base64 import json import requests import webbrowser import traceback import psutil from datetime import datetime from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit, QFileDialog, QGroupBox, QSlider, QDoubleSpinBox, QProgressBar, QSplitter, QMessageBox, QListWidget, QListWidgetItem, QSystemTrayIcon, QMenu, QAction, QComboBox, QTabWidget, QScrollArea, QCheckBox, QInputDialog, QLineEdit, QStyle, QSizePolicy ) from PyQt5.QtGui import ( QPixmap, QFont, QPalette, QColor, QTextCursor, QIcon, QTextDocumentWriter, QTextDocument, QImage, QPainter ) from PyQt5.QtCore import ( Qt, QSize, QThread, pyqtSignal, QTimer, QSettings, QMutex, QCoreApplication, QBuffer ) from PIL import Image, ImageOps # 配置OLLAMA API设置 OLLAMA_HOST = "http://localhost:11434" HISTORY_FILE = "history.json" SETTINGS_FILE = "settings.ini" MAX_COMPRESSED_SIZE = 1024 # 最大压缩尺寸 MAX_THREADS = 2 # 最大并发线程数 # 全局锁防止资源冲突 analysis_mutex = QMutex() class ModelLoaderThread(QThread): models_loaded = pyqtSignal(list) error_occurred = pyqtSignal(str) def run(self): try: response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=10) response.raise_for_status() models_data = response.json() models = [] for model in models_data.get("models", []): # 保留完整的模型名称(包含冒号) model_name = model["name"] models.append(model_name) self.models_loaded.emit(models) except Exception as e: self.error_occurred.emit(f"模型加载失败: {str(e)}") class ImageAnalysisThread(QThread): analysis_complete = pyqtSignal(str, str) # 结果, 图片路径 progress_updated = pyqtSignal(int) error_occurred = pyqtSignal(str) stream_data = pyqtSignal(str) def __init__(self, model_name, image_path, temperature, max_tokens, prompt, parent=None): super().__init__(parent) self.model_name = model_name self.image_path = image_path self.temperature = temperature self.max_tokens = max_tokens self.prompt = prompt self._is_running = True def run(self): # 获取锁,防止多个线程同时访问资源 analysis_mutex.lock() try: # 检查图片文件是否存在 if not os.path.exists(self.image_path): self.error_occurred.emit(f"图片文件不存在: {self.image_path}") return # 压缩图片 compressed_path = self.compress_image(self.image_path) if not compressed_path: compressed_path = self.image_path # 读取并编码图片为base64 with open(compressed_path, "rb") as image_file: base64_image = base64.b64encode(image_file.read()).decode("utf-8") # 构建请求数据 data = { "model": self.model_name, "prompt": self.prompt, "images": [base64_image], "stream": True, "options": { "temperature": self.temperature, "num_predict": self.max_tokens } } # 发送请求并处理流式响应 response = requests.post( f"{OLLAMA_HOST}/api/generate", json=data, stream=True, timeout=60 ) response.raise_for_status() full_response = "" for line in response.iter_lines(): if not self._is_running: break if line: decoded_line = line.decode("utf-8") try: json_data = json.loads(decoded_line) if "response" in json_data: chunk = json_data["response"] full_response += chunk self.stream_data.emit(chunk) if "done" in json_data and json_data["done"]: break if "error" in json_data: self.error_occurred.emit(json_data["error"]) return except json.JSONDecodeError: self.error_occurred.emit("无效的API响应") return if self._is_running: self.analysis_complete.emit(full_response, self.image_path) except Exception as e: error_msg = f"API调用失败: {str(e)}\n\n{traceback.format_exc()}" self.error_occurred.emit(error_msg) finally: # 确保无论发生什么都会释放锁 analysis_mutex.unlock() # 删除临时压缩文件 if compressed_path != self.image_path and os.path.exists(compressed_path): try: os.remove(compressed_path) except: pass def compress_image(self, image_path): """压缩图片以减小内存占用""" try: # 检查文件大小 file_size = os.path.getsize(image_path) / (1024 * 1024) # MB if file_size < 1: # 小于1MB不需要压缩 return image_path # 打开图片 img = Image.open(image_path) # 如果图片尺寸过大,调整尺寸 if max(img.size) > MAX_COMPRESSED_SIZE: img.thumbnail((MAX_COMPRESSED_SIZE, MAX_COMPRESSED_SIZE), Image.LANCZOS) # 创建临时文件路径 temp_path = f"temp_compressed_{os.path.basename(image_path)}" # 保存压缩后的图片 if image_path.lower().endswith('.png'): img.save(temp_path, optimize=True, quality=85, format='PNG') else: img.save(temp_path, optimize=True, quality=85) return temp_path except Exception as e: print(f"图片压缩失败: {str(e)}") return image_path def stop(self): self._is_running = False class ExportThread(QThread): export_finished = pyqtSignal(str, bool) def __init__(self, content, file_path, format_type, parent=None): super().__init__(parent) self.content = content self.file_path = file_path self.format_type = format_type def run(self): try: if self.format_type == "html": with open(self.file_path, "w", encoding="utf-8") as f: f.write(self.content) elif self.format_type == "txt": with open(self.file_path, "w", encoding="utf-8") as f: f.write(self.content) elif self.format_type == "pdf": doc = QTextDocument() doc.setHtml(self.content) writer = QTextDocumentWriter(self.file_path) writer.write(doc) self.export_finished.emit(self.file_path, True) except Exception as e: self.export_finished.emit(str(e), False) class MultiModalApp(QMainWindow): def __init__(self): super().__init__() self.image_paths = [] self.current_image_index = 0 self.history = [] self.active_threads = 0 self.settings = QSettings(SETTINGS_FILE, QSettings.IniFormat) self.initUI() self.load_settings() self.setWindowTitle("增强版多模态大模型图像解读系统") self.setGeometry(100, 100, 1920, 1000) # 初始化系统托盘 self.init_tray_icon() # 创建资源监控定时器 self.resource_timer = QTimer(self) self.resource_timer.timeout.connect(self.monitor_resources) self.resource_timer.start(5000) # 每5秒监控一次 QTimer.singleShot(500, self.load_models) def initUI(self): # 创建暗色主题样式表 self.setStyleSheet(""" /* 主窗口样式 */ QMainWindow { background-color: #0a192f; } /* 分组框样式 */ QGroupBox { border: 2px solid #64ffda; border-radius: 10px; margin-top: 1ex; color: #ccd6f6; font-weight: bold; } /* 标签样式 */ QLabel { color: #ccd6f6; } /* 按钮样式 */ QPushButton { background-color: #112240; color: #64ffda; border: 1px solid #64ffda; border-radius: 5px; padding: 5px 10px; font-weight: bold; } QPushButton:disabled { background-color: #0d1b30; color: #4a8f7c; border: 1px solid #4a8f7c; } /* 文本框样式 */ QTextEdit { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; padding: 5px; font-size: 12pt; } /* 选项卡样式 */ QTabWidget::pane { border: 1px solid #64ffda; border-radius: 5px; background: #0a192f; } QTabBar::tab { background: #112240; color: #ccd6f6; padding: 8px; border: 1px solid #64ffda; border-bottom: none; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:selected { background: #233554; color: #64ffda; } /* 列表样式 */ QListWidget { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; } QListWidget::item { padding: 5px; } QListWidget::item:selected { background-color: #233554; color: #64ffda; } /* 进度条样式 */ QProgressBar { border: 1px solid #64ffda; border-radius: 5px; text-align: center; background-color: #0a192f; color: #64ffda; } QProgressBar::chunk { background-color: #64ffda; width: 10px; } /* 状态标签样式 */ #statusLabel { color: #64ffda; font-weight: bold; padding: 2px 5px; border-radius: 3px; background-color: #112240; } """) # 设置主窗口布局 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 标题区域 title_layout = QHBoxLayout() self.title_label = QLabel("增强版多模态大模型图像解读系统") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #64ffda;") title_layout.addWidget(self.title_label) # 添加主题切换按钮 self.theme_button = QPushButton("切换主题") self.theme_button.clicked.connect(self.toggle_theme) title_layout.addWidget(self.theme_button) main_layout.addLayout(title_layout) # 主内容区域 splitter = QSplitter(Qt.Horizontal) # 左侧控制面板 left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(5, 5, 5, 5) # 图片预览区域(改为选项卡形式) self.image_tabs = QTabWidget() self.image_tabs.setTabsClosable(True) self.image_tabs.tabCloseRequested.connect(self.close_image_tab) left_layout.addWidget(self.image_tabs, 3) # 控制面板区域 control_tabs = QTabWidget() # 模型控制选项卡 model_tab = QWidget() self.setup_model_tab(model_tab) control_tabs.addTab(model_tab, "模型设置") # 参数控制选项卡 param_tab = QWidget() self.setup_param_tab(param_tab) control_tabs.addTab(param_tab, "参数设置") # 预设控制选项卡 preset_tab = QWidget() self.setup_preset_tab(preset_tab) control_tabs.addTab(preset_tab, "预设管理") left_layout.addWidget(control_tabs, 2) # 右侧结果面板 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(5, 5, 5, 5) # 结果展示区域 result_tabs = QTabWidget() # 分析结果选项卡 self.result_tab = QWidget() self.setup_result_tab(self.result_tab) result_tabs.addTab(self.result_tab, "分析结果") # 历史记录选项卡 self.history_tab = QWidget() self.setup_history_tab(self.history_tab) result_tabs.addTab(self.history_tab, "历史记录") right_layout.addWidget(result_tabs) # 添加面板到分割器 splitter.addWidget(left_panel) splitter.addWidget(right_panel) splitter.setSizes([800, 1100]) main_layout.addWidget(splitter) # 状态栏 self.status_bar = self.statusBar() self.status_bar.setStyleSheet("background-color: #112240; color: #64ffda;") # 资源监控标签 self.resource_label = QLabel() self.resource_label.setObjectName("statusLabel") self.status_bar.addPermanentWidget(self.resource_label) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setFixedWidth(200) self.status_bar.addPermanentWidget(self.progress_bar) # 活动线程标签 self.thread_label = QLabel("线程: 0") self.thread_label.setObjectName("statusLabel") self.status_bar.addPermanentWidget(self.thread_label) self.status_bar.showMessage("系统已就绪") def setup_model_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 模型选择区域 model_group = QGroupBox("模型选择") model_layout = QVBoxLayout() self.model_list = QListWidget() self.model_list.setMaximumHeight(150) self.model_list.addItem("正在加载模型...") self.refresh_models_button = QPushButton("刷新模型列表") self.refresh_models_button.clicked.connect(self.load_models) model_layout.addWidget(self.model_list) model_layout.addWidget(self.refresh_models_button) model_group.setLayout(model_layout) layout.addWidget(model_group) # 图片操作区域 image_group = QGroupBox("图片操作") image_layout = QVBoxLayout() self.load_button = QPushButton("加载图片") self.load_button.clicked.connect(self.load_image) self.load_multiple_button = QPushButton("批量加载图片") self.load_multiple_button.clicked.connect(self.load_multiple_images) self.clear_images_button = QPushButton("清除所有图片") self.clear_images_button.clicked.connect(self.clear_all_images) image_layout.addWidget(self.load_button) image_layout.addWidget(self.load_multiple_button) image_layout.addWidget(self.clear_images_button) image_group.setLayout(image_layout) layout.addWidget(image_group) # 分析控制区域 control_group = QGroupBox("分析控制") control_layout = QVBoxLayout() self.analyze_button = QPushButton("分析当前图片") self.analyze_button.clicked.connect(self.analyze_image) self.analyze_button.setEnabled(False) self.analyze_all_button = QPushButton("批量分析所有图片") self.analyze_all_button.clicked.connect(self.analyze_all_images) self.analyze_all_button.setEnabled(False) self.stop_button = QPushButton("停止分析") self.stop_button.clicked.connect(self.stop_analysis) self.stop_button.setEnabled(False) control_layout.addWidget(self.analyze_button) control_layout.addWidget(self.analyze_all_button) control_layout.addWidget(self.stop_button) control_group.setLayout(control_layout) layout.addWidget(control_group) def setup_param_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 温度控制 temp_group = QGroupBox("温度控制") temp_layout = QHBoxLayout() self.temp_slider = QSlider(Qt.Horizontal) self.temp_slider.setRange(0, 100) self.temp_slider.setValue(50) self.temp_value = QDoubleSpinBox() self.temp_value.setRange(0.0, 1.0) self.temp_value.setSingleStep(0.1) self.temp_value.setValue(0.5) self.temp_value.setDecimals(1) self.temp_slider.valueChanged.connect(lambda val: self.temp_value.setValue(val / 100)) self.temp_value.valueChanged.connect(lambda val: self.temp_slider.setValue(int(val * 100))) temp_layout.addWidget(self.temp_slider) temp_layout.addWidget(self.temp_value) temp_group.setLayout(temp_layout) layout.addWidget(temp_group) # Token控制 token_group = QGroupBox("Token控制") token_layout = QHBoxLayout() self.token_spin = QDoubleSpinBox() self.token_spin.setRange(100, 5000) self.token_spin.setValue(1000) self.token_spin.setSingleStep(100) token_layout.addWidget(QLabel("最大Token:")) token_layout.addWidget(self.token_spin) token_group.setLayout(token_layout) layout.addWidget(token_group) # 提示词区域 prompt_group = QGroupBox("提示词设置") prompt_layout = QVBoxLayout() self.prompt_edit = QTextEdit() self.prompt_edit.setPlainText( "请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") self.prompt_edit.setMinimumHeight(150) prompt_layout.addWidget(self.prompt_edit) prompt_group.setLayout(prompt_layout) layout.addWidget(prompt_group) def setup_preset_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 预设管理 preset_group = QGroupBox("预设管理") preset_layout = QVBoxLayout() self.preset_combo = QComboBox() self.preset_combo.addItems(["默认预设", "详细描述", "创意写作", "技术分析"]) button_layout = QHBoxLayout() self.load_preset_button = QPushButton("加载预设") self.load_preset_button.clicked.connect(self.load_preset) self.save_preset_button = QPushButton("保存预设") self.save_preset_button.clicked.connect(self.save_preset) self.delete_preset_button = QPushButton("删除预设") self.delete_preset_button.clicked.connect(self.delete_preset) button_layout.addWidget(self.load_preset_button) button_layout.addWidget(self.save_preset_button) button_layout.addWidget(self.delete_preset_button) preset_layout.addWidget(self.preset_combo) preset_layout.addLayout(button_layout) preset_group.setLayout(preset_layout) layout.addWidget(preset_group) # 自动保存设置 auto_save_group = QGroupBox("自动保存设置") auto_save_layout = QVBoxLayout() self.auto_save_check = QCheckBox("自动保存分析结果") self.auto_save_check.setChecked(True) self.auto_save_path_button = QPushButton("选择保存路径") self.auto_save_path_button.clicked.connect(self.select_auto_save_path) self.auto_save_path_label = QLabel("默认保存位置: 程序目录/results") self.auto_save_path_label.setWordWrap(True) auto_save_layout.addWidget(self.auto_save_check) auto_save_layout.addWidget(self.auto_save_path_button) auto_save_layout.addWidget(self.auto_save_path_label) auto_save_group.setLayout(auto_save_layout) layout.addWidget(auto_save_group) def setup_result_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 结果展示区域 result_group = QGroupBox("分析结果") result_layout = QVBoxLayout() self.result_edit = QTextEdit() self.result_edit.setReadOnly(True) # 结果操作按钮 button_layout = QHBoxLayout() self.save_result_button = QPushButton("保存结果") self.save_result_button.clicked.connect(self.save_result) self.copy_result_button = QPushButton("复制结果") self.copy_result_button.clicked.connect(self.copy_result) self.clear_result_button = QPushButton("清除结果") self.clear_result_button.clicked.connect(self.clear_results) button_layout.addWidget(self.save_result_button) button_layout.addWidget(self.copy_result_button) button_layout.addWidget(self.clear_result_button) result_layout.addWidget(self.result_edit) result_layout.addLayout(button_layout) result_group.setLayout(result_layout) layout.addWidget(result_group) def setup_history_tab(self, tab): layout = QVBoxLayout(tab) layout.setContentsMargins(5, 5, 5, 5) # 历史记录区域 history_group = QGroupBox("历史记录") history_layout = QVBoxLayout() self.history_list = QListWidget() self.history_list.itemDoubleClicked.connect(self.load_history_item) # 历史操作按钮 button_layout = QHBoxLayout() self.load_history_button = QPushButton("加载历史") self.load_history_button.clicked.connect(self.load_history) self.clear_history_button = QPushButton("清除历史") self.clear_history_button.clicked.connect(self.clear_history) button_layout.addWidget(self.load_history_button) button_layout.addWidget(self.clear_history_button) history_layout.addWidget(self.history_list) history_layout.addLayout(button_layout) history_group.setLayout(history_layout) layout.addWidget(history_group) def init_tray_icon(self): """初始化系统托盘图标""" self.tray_icon = QSystemTrayIcon(self) # 使用系统默认图标 icon = self.style().standardIcon(QStyle.SP_ComputerIcon) self.tray_icon.setIcon(icon) tray_menu = QMenu() show_action = QAction("显示窗口", self) show_action.triggered.connect(self.show_normal) tray_menu.addAction(show_action) exit_action = QAction("退出", self) exit_action.triggered.connect(self.close) tray_menu.addAction(exit_action) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() # 托盘图标点击事件 self.tray_icon.activated.connect(self.tray_icon_clicked) def show_normal(self): """从托盘恢复窗口显示""" self.showNormal() self.activateWindow() def tray_icon_clicked(self, reason): """处理托盘图标点击事件""" if reason == QSystemTrayIcon.DoubleClick: self.show_normal() def toggle_theme(self): """切换主题""" if self.theme_button.text() == "切换主题": # 切换到浅色主题 self.setStyleSheet(""" QMainWindow { background-color: #f5f5f5; } QGroupBox { border: 2px solid #2c3e50; border-radius: 10px; margin-top: 1ex; color: #2c3e50; font-weight: bold; } QLabel { color: #2c3e50; } QPushButton { background-color: #ecf0f1; color: #2c3e50; border: 1px solid #bdc3c7; border-radius: 5px; padding: 5px 10px; font-weight: bold; } QPushButton:disabled { background-color: #d5dbdb; color: #7f8c8d; border: 1px solid #bdc3c7; } QTextEdit { background-color: #ffffff; color: #2c3e50; border: 1px solid #bdc3c7; border-radius: 5px; padding: 5px; font-size: 12pt; } QListWidget { background-color: #ffffff; color: #2c3e50; border: 1px solid #bdc3c7; border-radius: 5px; } QListWidget::item { padding: 5px; } QListWidget::item:selected { background-color: #3498db; color: #ffffff; } QProgressBar { border: 1px solid #2c3e50; border-radius: 5px; text-align: center; background-color: #ecf0f1; color: #2c3e50; } QProgressBar::chunk { background-color: #3498db; width: 10px; } #statusLabel { color: #2c3e50; font-weight: bold; padding: 2px 5px; border-radius: 3px; background-color: #ecf0f1; } """) self.theme_button.setText("切换回暗色") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #2c3e50;") self.status_bar.setStyleSheet("background-color: #ecf0f1; color: #2c3e50;") else: # 切换回暗色主题 self.setStyleSheet(""" QMainWindow { background-color: #0a192f; } QGroupBox { border: 2px solid #64ffda; border-radius: 10px; margin-top: 1ex; color: #ccd6f6; font-weight: bold; } QLabel { color: #ccd6f6; } QPushButton { background-color: #112240; color: #64ffda; border: 1px solid #64ffda; border-radius: 5px; padding: 5px 10px; font-weight: bold; } QPushButton:disabled { background-color: #0d1b30; color: #4a8f7c; border: 1px solid #4a8f7c; } QTextEdit { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; padding: 5px; font-size: 12pt; } QListWidget { background-color: #0a192f; color: #a8b2d1; border: 1px solid #64ffda; border-radius: 5px; } QListWidget::item { padding: 5px; } QListWidget::item:selected { background-color: #233554; color: #64ffda; } QProgressBar { border: 1px solid #64ffda; border-radius: 5px; text-align: center; background-color: #0a192f; color: #64ffda; } QProgressBar::chunk { background-color: #64ffda; width: 10px; } #statusLabel { color: #64ffda; font-weight: bold; padding: 2px 5px; border-radius: 3px; background-color: #112240; } """) self.theme_button.setText("切换主题") self.title_label.setStyleSheet("font-size: 24pt; font-weight: bold; color: #64ffda;") self.status_bar.setStyleSheet("background-color: #112240; color: #64ffda;") def load_settings(self): """加载程序设置""" self.settings.beginGroup("MainWindow") self.restoreGeometry(self.settings.value("geometry", self.saveGeometry())) self.restoreState(self.settings.value("windowState", self.saveState())) self.settings.endGroup() # 加载历史记录 self.load_history() # 加载自动保存路径 auto_save_path = self.settings.value("AutoSave/path", "results") self.auto_save_path_label.setText(f"保存位置: {auto_save_path}") def save_settings(self): """保存程序设置""" self.settings.beginGroup("MainWindow") self.settings.setValue("geometry", self.saveGeometry()) self.settings.setValue("windowState", self.saveState()) self.settings.endGroup() # 保存历史记录 self.save_history() def load_models(self): """加载可用模型列表""" self.model_list.clear() self.model_list.addItem("正在加载模型...") self.model_loader_thread = ModelLoaderThread() self.model_loader_thread.models_loaded.connect(self.update_model_list) self.model_loader_thread.error_occurred.connect(self.handle_model_load_error) self.model_loader_thread.start() def update_model_list(self, models): """更新模型列表""" self.model_list.clear() if not models: self.model_list.addItem("没有找到可用模型") return for model in models: # 显示完整的模型名称(包含冒号) item = QListWidgetItem(f"● {model}") item.setData(Qt.UserRole, model) self.model_list.addItem(item) # 默认选择第一个模型 if models: self.model_list.setCurrentRow(0) def handle_model_load_error(self, error_msg): """处理模型加载错误""" self.model_list.clear() self.model_list.addItem(error_msg) self.status_bar.showMessage(error_msg) def load_image(self): """加载单张图片""" file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_path: self.add_image_tab(file_path) def load_multiple_images(self): """批量加载多张图片""" file_paths, _ = QFileDialog.getOpenFileNames( self, "选择多张图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif)" ) if file_paths: for file_path in file_paths: self.add_image_tab(file_path) def add_image_tab(self, file_path): """添加图片选项卡""" try: pixmap = QPixmap(file_path) if pixmap.isNull(): raise Exception("无法加载图片文件,可能格式不支持或文件已损坏") scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) image_label = QLabel() image_label.setPixmap(pixmap.scaled( 800, 600, Qt.KeepAspectRatio, Qt.SmoothTransformation )) image_label.setAlignment(Qt.AlignCenter) scroll_area.setWidget(image_label) tab_index = self.image_tabs.addTab( scroll_area, os.path.basename(file_path) ) self.image_tabs.setCurrentIndex(tab_index) self.image_paths.append(file_path) self.analyze_button.setEnabled(True) self.analyze_all_button.setEnabled(True) self.status_bar.showMessage(f"已加载图片: {os.path.basename(file_path)}") # 更新当前图片索引 self.current_image_index = tab_index except Exception as e: self.status_bar.showMessage(f"错误: {str(e)}") self.show_error_dialog("图片加载错误", f"无法加载图片:\n{str(e)}") def close_image_tab(self, index): """关闭图片选项卡""" if index < len(self.image_paths): self.image_paths.pop(index) self.image_tabs.removeTab(index) if not self.image_paths: self.analyze_button.setEnabled(False) self.analyze_all_button.setEnabled(False) def clear_all_images(self): """清除所有图片""" self.image_tabs.clear() self.image_paths.clear() self.analyze_button.setEnabled(False) self.analyze_all_button.setEnabled(False) self.status_bar.showMessage("已清除所有图片") def analyze_image(self): """分析当前图片""" current_index = self.image_tabs.currentIndex() if current_index < 0 or current_index >= len(self.image_paths): self.status_bar.showMessage("错误: 没有可分析的图片") return self.current_image_index = current_index self._analyze_image(self.image_paths[current_index]) def analyze_all_images(self): """批量分析所有图片""" if not self.image_paths: self.status_bar.showMessage("错误: 没有可分析的图片") return # 保存当前索引 saved_index = self.current_image_index # 逐个分析图片 for i, image_path in enumerate(self.image_paths): # 检查活动线程数 while self.active_threads >= MAX_THREADS: QCoreApplication.processEvents() # 处理事件循环 self.current_image_index = i self.image_tabs.setCurrentIndex(i) self._analyze_image(image_path) # 等待分析完成 while hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning(): QApplication.processEvents() # 恢复原始索引 self.current_image_index = saved_index self.image_tabs.setCurrentIndex(saved_index) def _analyze_image(self, image_path): """实际执行图片分析的内部方法""" selected_items = self.model_list.selectedItems() if not selected_items: self.status_bar.showMessage("错误: 请选择模型") return # 获取完整的模型名称(包含冒号) model_name = selected_items[0].data(Qt.UserRole) self.result_edit.clear() self.progress_bar.setValue(0) self.progress_bar.setFormat("正在分析图片...") self.set_buttons_enabled(False) self.stop_button.setEnabled(True) temperature = self.temp_value.value() max_tokens = int(self.token_spin.value()) prompt = self.prompt_edit.toPlainText().strip() # 确保之前的线程已停止 if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.wait(2000) # 等待线程结束 try: self.analysis_thread = ImageAnalysisThread( model_name, image_path, temperature, max_tokens, prompt ) self.analysis_thread.analysis_complete.connect(self.handle_analysis_result) self.analysis_thread.progress_updated.connect(self.update_progress) self.analysis_thread.error_occurred.connect(self.handle_analysis_error) self.analysis_thread.stream_data.connect(self.analysis_stream_data) self.analysis_thread.finished.connect(self.analysis_finished) self.analysis_thread.start() # 更新活动线程计数 self.active_threads += 1 self.thread_label.setText(f"线程: {self.active_threads}") except Exception as e: error_msg = f"启动分析线程失败: {str(e)}\n\n{traceback.format_exc()}" self.handle_analysis_error(error_msg) def analysis_stream_data(self, chunk): """处理流式数据""" cursor = self.result_edit.textCursor() cursor.movePosition(QTextCursor.End) cursor.insertText(chunk) self.result_edit.setTextCursor(cursor) self.result_edit.ensureCursorVisible() def handle_analysis_result(self, result, image_path): """处理分析结果""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") selected_items = self.model_list.selectedItems() model_name = selected_items[0].text()[2:] if selected_items else "未知模型" # 获取当前图片文件名 image_name = os.path.basename(image_path) # 格式化结果 formatted_result = self.format_result(result) result_html = f""" <div style='color:#64ffda; font-size:14pt; font-weight:bold; margin-bottom:10px;'> 图片分析结果: {image_name} </div> <div style='color:#ccd6f6; font-size:12pt; line-height:1.6;'>{formatted_result}</div> <div style='margin-top: 20px; color: #8892b0; font-size: 10pt; border-top: 1px solid #233554; padding-top: 10px;'> 模型: <span style='color: #64ffda;'>{model_name}</span> | 时间: <span style='color: #64ffda;'>{timestamp}</span> </div> """ self.result_edit.setHtml(result_html) self.status_bar.showMessage(f"图片分析完成: {image_name}") self.progress_bar.setValue(100) self.progress_bar.setFormat("分析完成") # 添加到历史记录 self.add_to_history(image_name, model_name, timestamp, result_html) # 自动保存结果 if self.auto_save_check.isChecked(): self.auto_save_result(image_name, result_html) def format_result(self, result): """格式化分析结果为HTML""" # 将文本转换为段落 paragraphs = result.split("\n\n") formatted_paragraphs = [] for para in paragraphs: if para.strip(): # 替换缩进空格为HTML空格 formatted_para = para.replace(" ", "  ") formatted_paragraphs.append(f"<p style='text-indent: 2em;'>{formatted_para}</p>") return "\n".join(formatted_paragraphs) def add_to_history(self, image_name, model_name, timestamp, content): """添加到历史记录""" history_item = { "image": image_name, "model": model_name, "time": timestamp, "content": content } self.history.insert(0, history_item) self.update_history_list() def update_history_list(self): """更新历史记录列表""" self.history_list.clear() for item in self.history[:50]: # 最多显示50条历史记录 list_item = QListWidgetItem(f"{item['image']} - {item['time']}") list_item.setData(Qt.UserRole, item) self.history_list.addItem(list_item) def load_history_item(self, item): """加载历史记录项""" history_data = item.data(Qt.UserRole) self.result_edit.setHtml(history_data["content"]) def load_history(self): """从文件加载历史记录""" try: if os.path.exists(HISTORY_FILE): with open(HISTORY_FILE, "r", encoding="utf-8") as f: self.history = json.load(f) self.update_history_list() self.status_bar.showMessage(f"已加载 {len(self.history)} 条历史记录") except Exception as e: self.status_bar.showMessage(f"加载历史记录失败: {str(e)}") def save_history(self): """保存历史记录到文件""" try: with open(HISTORY_FILE, "w", encoding="utf-8") as f: json.dump(self.history, f, ensure_ascii=False, indent=2) except Exception as e: self.status_bar.showMessage(f"保存历史记录失败: {str(e)}") def clear_history(self): """清除历史记录""" reply = QMessageBox.question( self, "确认清除", "确定要清除所有历史记录吗? 此操作不可撤销!", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.history.clear() self.history_list.clear() self.status_bar.showMessage("已清除所有历史记录") def auto_save_result(self, image_name, content): """自动保存结果""" try: save_dir = self.settings.value("AutoSave/path", "results") if not os.path.exists(save_dir): os.makedirs(save_dir) # 生成文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") base_name = os.path.splitext(image_name)[0] file_name = f"{base_name}_{timestamp}.html" file_path = os.path.join(save_dir, file_name) # 保存文件 with open(file_path, "w", encoding="utf-8") as f: f.write(content) self.status_bar.showMessage(f"结果已自动保存到: {file_path}") except Exception as e: self.status_bar.showMessage(f"自动保存失败: {str(e)}") def select_auto_save_path(self): """选择自动保存路径""" save_dir = QFileDialog.getExistingDirectory( self, "选择自动保存目录", self.settings.value("AutoSave/path", "results") ) if save_dir: self.settings.setValue("AutoSave/path", save_dir) self.auto_save_path_label.setText(f"保存位置: {save_dir}") def save_result(self): """保存结果到文件""" if not self.result_edit.toPlainText(): self.status_bar.showMessage("错误: 没有可保存的内容") return file_path, _ = QFileDialog.getSaveFileName( self, "保存结果", "", "HTML文件 (*.html);;文本文件 (*.txt);;PDF文件 (*.pdf)" ) if file_path: format_type = "html" if file_path.endswith(".txt"): format_type = "txt" elif file_path.endswith(".pdf"): format_type = "pdf" content = self.result_edit.toHtml() if format_type != "txt" else self.result_edit.toPlainText() self.export_thread = ExportThread(content, file_path, format_type) self.export_thread.export_finished.connect(self.handle_export_finished) self.export_thread.start() self.status_bar.showMessage("正在导出结果...") def handle_export_finished(self, message, success): """处理导出完成""" if success: self.status_bar.showMessage(f"结果已保存到: {message}") if message.endswith(".html"): webbrowser.open(message) else: self.status_bar.showMessage(f"导出失败: {message}") self.show_error_dialog("导出错误", f"无法保存结果:\n{message}") def copy_result(self): """复制结果到剪贴板""" self.result_edit.selectAll() self.result_edit.copy() self.status_bar.showMessage("结果已复制到剪贴板") def clear_results(self): """清除结果""" self.result_edit.clear() self.status_bar.showMessage("已清除结果") def load_preset(self): """加载预设""" preset_name = self.preset_combo.currentText() if preset_name == "默认预设": self.temp_value.setValue(0.5) self.token_spin.setValue(1000) self.prompt_edit.setPlainText( "请用中文详细描述这张图片的内容,要求描述清晰、有条理,分段落呈现,各段首行按要求缩进2个汉字。") elif preset_name == "详细描述": self.temp_value.setValue(0.3) self.token_spin.setValue(1500) self.prompt_edit.setPlainText( "请用中文详细描述这张图片中的每一个细节,包括但不限于场景、人物、物体、颜色、空间关系等。要求描述系统全面,层次分明,每段描述一个方面。") elif preset_name == "创意写作": self.temp_value.setValue(0.7) self.token_spin.setValue(2000) self.prompt_edit.setPlainText( "请根据这张图片创作一个富有想象力的故事或诗歌。要求内容生动有趣,语言优美,可以适当发挥想象力,但不要偏离图片内容太远。") elif preset_name == "技术分析": self.temp_value.setValue(0.2) self.token_spin.setValue(1200) self.prompt_edit.setPlainText( "请从技术角度分析这张图片,包括但不限于构图、色彩、光线、透视等专业要素。要求分析专业准确,使用适当的专业术语。") self.status_bar.showMessage(f"已加载预设: {preset_name}") def save_preset(self): """保存当前设置为预设""" preset_name, ok = QInputDialog.getText( self, "保存预设", "请输入预设名称:", QLineEdit.Normal, self.preset_combo.currentText() ) if ok and preset_name: # 检查是否已存在 index = self.preset_combo.findText(preset_name) if index == -1: self.preset_combo.addItem(preset_name) self.preset_combo.setCurrentText(preset_name) self.status_bar.showMessage(f"预设 '{preset_name}' 已保存") def delete_preset(self): """删除预设""" preset_name = self.preset_combo.currentText() if preset_name in ["默认预设", "详细描述", "创意写作", "技术分析"]: QMessageBox.warning(self, "警告", "系统预设不能被删除") return reply = QMessageBox.question( self, "确认删除", f"确定要删除预设 '{preset_name}' 吗?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: index = self.preset_combo.currentIndex() self.preset_combo.removeItem(index) self.status_bar.showMessage(f"预设 '{preset_name}' 已删除") def stop_analysis(self): """停止分析""" if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.status_bar.showMessage("分析已停止") self.set_buttons_enabled(True) self.stop_button.setEnabled(False) def analysis_finished(self): """分析完成后的清理工作""" self.set_buttons_enabled(True) self.stop_button.setEnabled(False) # 更新活动线程计数 self.active_threads -= 1 if self.active_threads < 0: self.active_threads = 0 self.thread_label.setText(f"线程: {self.active_threads}") def handle_analysis_error(self, error_msg): """处理分析错误""" # 显示详细的错误信息 self.status_bar.showMessage(f"错误: {error_msg.splitlines()[0]}") self.progress_bar.setFormat("分析失败") self.set_buttons_enabled(True) self.stop_button.setEnabled(False) # 在结果区域显示完整错误信息 error_html = f""" <div style='color:#ff6b6b; font-size:14pt; font-weight:bold; margin-bottom:10px;'> 分析过程中发生错误 </div> <div style='color:#ccd6f6; font-size:12pt; line-height:1.6;'> {error_msg.replace('\n', '<br>')} </div> """ self.result_edit.setHtml(error_html) # 记录错误日志 self.log_error(error_msg) # 更新活动线程计数 self.active_threads -= 1 if self.active_threads < 0: self.active_threads = 0 self.thread_label.setText(f"线程: {self.active_threads}") def log_error(self, error_msg): """记录错误到日志文件""" try: log_dir = "logs" if not os.path.exists(log_dir): os.makedirs(log_dir) log_file = os.path.join(log_dir, "error_log.txt") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(log_file, "a", encoding="utf-8") as f: f.write(f"\n\n[{timestamp}] 发生错误:\n") f.write(error_msg) f.write("\n" + "-" * 80) except Exception as e: print(f"无法写入错误日志: {str(e)}") def set_buttons_enabled(self, enabled): """设置按钮启用状态""" self.load_button.setEnabled(enabled) self.load_multiple_button.setEnabled(enabled) self.clear_images_button.setEnabled(enabled) self.analyze_button.setEnabled(enabled and len(self.image_paths) > 0) self.analyze_all_button.setEnabled(enabled and len(self.image_paths) > 0) self.model_list.setEnabled(enabled) self.refresh_models_button.setEnabled(enabled) self.preset_combo.setEnabled(enabled) self.load_preset_button.setEnabled(enabled) self.save_preset_button.setEnabled(enabled) self.delete_preset_button.setEnabled(enabled) self.auto_save_check.setEnabled(enabled) self.auto_save_path_button.setEnabled(enabled) self.save_result_button.setEnabled(enabled) self.copy_result_button.setEnabled(enabled) self.clear_result_button.setEnabled(enabled) self.load_history_button.setEnabled(enabled) self.clear_history_button.setEnabled(enabled) def show_error_dialog(self, title, message): """显示错误对话框""" QMessageBox.critical(self, title, message) def monitor_resources(self): """监控系统资源使用情况""" try: cpu_percent = psutil.cpu_percent() memory = psutil.virtual_memory() self.resource_label.setText( f"CPU: {cpu_percent}% | 内存: {memory.percent}%" ) except: pass def closeEvent(self, event): """关闭窗口事件""" # 停止所有运行中的线程 if hasattr(self, 'analysis_thread') and self.analysis_thread.isRunning(): self.analysis_thread.stop() if hasattr(self, 'model_loader_thread') and self.model_loader_thread.isRunning(): self.model_loader_thread.quit() self.model_loader_thread.wait(2000) # 停止资源监控定时器 self.resource_timer.stop() # 保存设置 # 接受关闭事件 event.accept() def handle_exception(exc_type, exc_value, exc_traceback): """全局异常处理函数""" error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) print(f"全局异常捕获:\n{error_msg}") # 尝试显示错误对话框 try: msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText("程序发生未处理异常") msg.setInformativeText(f"{exc_type.__name__}: {exc_value}") msg.setWindowTitle("致命错误") msg.setDetailedText(error_msg) msg.exec_() except: pass # 退出程序 sys.exit(1) if __name__ == "__main__": # 设置全局异常处理 sys.excepthook = handle_exception app = QApplication(sys.argv) window = MultiModalApp() window.show() sys.exit(app.exec_())
最新发布
08-11
import copy import sys import threading import atexit from PyQt5.QtWidgets import * from PyQt5.QtCore import Qt, QRect, QPoint, QSize, pyqtSignal, QTimer, QEvent from PyQt5.QtGui import QFont, QColor from datetime import datetime from multiprocessing import Process, Queue, freeze_support import weakref import psutil import time class MT5ProcessWorker: """ 独立进程工作器(替代原MT5Worker) 通过队列与主进程通信 """ def __init__(self, exe_path, symbol): self.exe_path = exe_path self.symbol = symbol self.queue = Queue() # 每个worker独立队列 self._process = None self._shutdown_flag = False def start(self): self._process = Process( target=self._run, args=(self.exe_path, self.symbol, self.queue) ) self._process.daemon = True # 主进程退出时自动终止 self._process.start() atexit.register(self.stop) # 注册退出清理 def _run(self, exe_path, symbol, queue): """子进程运行方法""" try: import MetaTrader5 as mt5_local # 每个进程独立导入 import time # 初始化独立MT5实例 if not mt5_local.initialize(path=exe_path, timeout=5): queue.put(('error', f"初始化失败: {mt5_local.last_error()}")) return # 获取并发送精度信息 symbol_info = mt5_local.symbol_info(symbol) if not symbol_info: queue.put(('error', f"无效品种: {symbol}")) return # 获取服务器信息 account_info = mt5_local.account_info() if account_info and hasattr(account_info, 'server'): server_name = account_info.server else: server_name = "Unknown Server" print("警告: 无法获取服务器名称") # 添加调试输出 digits = symbol_info.digits queue.put(('connected', (server_name, digits))) # 返回服务器名和精度 print(f"发送连接成功消息: {server_name}") # 添加调试输出 # 报价循环 while not self._shutdown_flag: try: # 增加心跳检测 if not mt5_local.terminal_info().connected: break # 检查队列中的关闭命令 if not queue.empty(): msg_type, _ = queue.get_nowait() if msg_type == 'shutdown': break # 在循环开始处检查队列状态 if queue.qsize() > 10: # 防止队列堆积 queue.queue.clear() terminal = mt5_local.terminal_info() if terminal is None or not hasattr(terminal, 'connected'): queue.put(('disconnected', "MT5连接已断开")) break if not terminal.connected: queue.put(('disconnected', "连接已主动断开")) tick = mt5_local.symbol_info_tick(symbol) if tick and tick.time_msc > 0: queue.put(('price', (symbol, tick.bid, tick.ask))) time.sleep(0.1) # 减少CPU使用率 except AttributeError as ae: queue.put(('error', f"终端状态获取失败: {str(ae)}")) break except Exception as e: print(f"报价循环异常: {e}") except Exception as e: queue.put(('error', str(e))) finally: mt5_local.shutdown() queue.put(('disconnected', "MT5连接关闭")) # 确保关闭时发送通知 def stop(self): if self._process: try: # 发送关闭指令 self.queue.put(('shutdown', None)) self._process.join(2) # 增加等待时间 # 双重终止保障 if self._process.is_alive(): self._process.terminate() self._process.join(1) # 安全关闭队列 self.queue.close() self.queue.cancel_join_thread() except Exception as e: print(f"进程终止失败: {str(e)}") class PriceCard(QWidget): delete_requested = pyqtSignal(object) symbol_changed = pyqtSignal(str) set_as_reference = pyqtSignal(str, float, float) status_updated = pyqtSignal(str) # 新增状态信号 # 添加一个信号用于线程安全地更新UI update_platform_info = pyqtSignal(str, int) update_prices_signal = pyqtSignal(str, float, float) def __init__(self, parent=None, currency_list=None): super().__init__(parent) self.mt5_worker = None self.data_timer = QTimer() # 初始设置货币对列表 if currency_list is not None: self.currency_list = copy.deepcopy(currency_list) else: self.currency_list = ["EURUSD", "GBPUSD", "USDJPY", "XAUUSD"] self.current_bid = 0.0 self.current_ask = 0.0 self._is_connected = False # 新增连接状态标志 self._has_valid_prices = False # 是否接收过有效价格 self.is_reference = False self._valid = True # 新增有效性标志 self.abnormal_count = 0 self.setup_ui() self.setup_style() self.setup_connections() # 初始化MT5工作器 self.start_connection() # 连接信号到UI更新方法 self.update_platform_info.connect(self._safe_update_platform_info) self.update_prices_signal.connect(self._safe_update_prices) def _safe_update_platform_info(self, server_name, digits): """在线程安全的情况下更新平台信息""" self.platform_input.setText(f"已连接 · {server_name}") self.platform_input.setStyleSheet("color: #009900;") self._digits = digits self._is_connected = True def _safe_update_prices(self, symbol, bid, ask): """线程安全地更新价格显示""" if not hasattr(self, '_digits'): self._digits = 5 # 默认精度 format_str = f".{self._digits}f" try: if bid > 0 and ask > 0: self._has_valid_prices = True self.current_bid = bid self.current_ask = ask self.sell_label.setText(f"{bid:{format_str}}") self.buy_label.setText(f"{ask:{format_str}}") # 持续更新reference_label if self.is_reference: self.status_updated.emit(f"参照平台:{symbol} {bid}/{ask} [{datetime.now().strftime("%H:%M:%S")}]") else: self._has_valid_prices = False except Exception as e: print(f"更新价格异常: {e}") # 安全地断开信号连接 @staticmethod def safe_disconnect(signal: pyqtSignal): try: # 尝试断开所有连接 signal.disconnect() except TypeError: # 没有连接或已断开,忽略 pass def _process_queue(self): """非阻塞方式处理队列""" if not self.mt5_worker or not hasattr(self.mt5_worker, 'queue'): return try: max_messages = 10 for _ in range(max_messages): if self.mt5_worker.queue.empty(): break try: msg_type, data = self.mt5_worker.queue.get_nowait() self._handle_queue_message(msg_type, data) except Exception as e: print(f"处理队列消息异常: {e}") break except Exception as e: print(f"队列处理主循环异常: {e}") finally: # 继续轮询 QTimer.singleShot(50, self._process_queue) def setup_ui(self): self.setFixedSize(320, 200) layout = QVBoxLayout(self) layout.setContentsMargins(12, 8, 12, 8) layout.setSpacing(8) # 标题栏 title_layout = QHBoxLayout() title_layout.setContentsMargins(0, 0, 0, 0) title_layout.setSpacing(6) self.platform_input = QLineEdit("等待连接...") self.platform_input.setFont(QFont("微软雅黑", 9)) self.platform_input.setReadOnly(True) # 参考按钮 self.ref_btn = QPushButton("★") self.ref_btn.setFixedHeight(24) self.ref_btn.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) # 删除按钮 self.delete_btn = QPushButton("×") self.delete_btn.setFixedHeight(24) self.delete_btn.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) title_layout.addWidget(self.platform_input, stretch=1) title_layout.addWidget(self.ref_btn) title_layout.addWidget(self.delete_btn) layout.addLayout(title_layout) # 交易品种 self.symbol_combo = QComboBox() self.symbol_combo.addItems(self.currency_list) layout.addWidget(self.symbol_combo) # 价格显示 price_layout = QHBoxLayout() price_layout.setContentsMargins(0, 0, 0, 0) price_layout.setSpacing(10) self.sell_label = QLabel("--") self.buy_label = QLabel("--") for label in [self.sell_label, self.buy_label]: label.setFixedHeight(40) label.setAlignment(Qt.AlignCenter) label.setFont(QFont("Arial", 12, QFont.Bold)) price_layout.addWidget(label) layout.addLayout(price_layout) # MT5路径选择 path_layout = QHBoxLayout() self.path_input = QLineEdit() self.path_input.setPlaceholderText("MT5可执行文件路径") self.browse_btn = QPushButton("浏览") self.browse_btn.setMinimumWidth(100) self.browse_btn.setFixedHeight(24) path_layout.addWidget(self.path_input) path_layout.addWidget(self.browse_btn) layout.addLayout(path_layout) self.browse_btn.clicked.connect(self.browse_mt5_exe) self.delete_btn.clicked.connect(self.request_delete) self.symbol_combo.currentTextChanged.connect(self.symbol_changed.emit) self.ref_btn.clicked.connect(self.emit_reference_signal) def setup_style(self): self.base_style = """ QWidget { background: white; border-radius: 8px; border: 2px solid #e0e0e0; } QLineEdit, QComboBox { border: 1px solid #d0d0d0; border-radius: 4px; padding: 4px; } QStatusBar { background: #f8f8f8; border-top: 1px solid #e0e0e0; } """ self.normal_style = "" self.abnormal_style = """ QWidget { border: 2px solid #ff0000; background: #fff0f0; } """ self.ref_style = """ QWidget { border: 2px solid #00aa00; background: #f8fff8; } """ self.ref_btn.setStyleSheet(""" QPushButton { background: transparent; color: #888888; font-size: 12px; border-radius: 4px; border: 1px solid #e0e0e0; } QPushButton:checked { color: #FFFFFF; background: #00aa00; border-radius: 4px; } """) self.delete_btn.setStyleSheet(""" QPushButton { background: transparent; color: #888888; font-size: 12px; border-radius: 4px; border: 1px solid #e0e0e0; } QPushButton:hover { color: #FF0000; border-color: #FF0000; } """) self.sell_label.setStyleSheet(""" QLabel { background-color: #8B0000; color: white; border-radius: 4px; padding: 6px; min-width: 100px; } """) self.buy_label.setStyleSheet(""" QLabel { background-color: #000080; color: white; border-radius: 4px; padding: 6px; min-width: 100px; } """) # 为浏览按钮添加样式 self.browse_btn.setStyleSheet(""" QPushButton { color: black; /* 设置文字颜色为黑色,可按需调整 */ background-color: #f0f0f0; /* 设置背景色,可按需调整 */ border: 1px solid #ccc; /* 设置边框,可按需调整 */ padding: 4px 8px; } """) self.setStyleSheet(self.base_style) def isValid(self): return self._valid def deleteLater(self): if not self._valid: # 防止重复调用 return self._valid = False # 停止定时器 if hasattr(self, 'data_timer') and self.data_timer.isActive(): self.data_timer.stop() # 停止工作进程 if hasattr(self, 'mt5_worker') and self.mt5_worker: try: self.mt5_worker.stop() except Exception as e: print(f"停止MT5工作进程失败: {e}") self.safe_disconnect(self.delete_requested) self.safe_disconnect(self.set_as_reference) self.safe_disconnect(self.symbol_changed) self.safe_disconnect(self.status_updated) self.safe_disconnect(self.update_platform_info) self.safe_disconnect(self.update_prices_signal) super().deleteLater() def setup_connections(self): self.symbol_changed.connect(self.restart_connection) def emit_reference_signal(self): if self.ref_btn.isChecked(): self.set_as_reference.emit( self.symbol_combo.currentText(), self.current_bid, self.current_ask ) self.set_style(self.ref_style) self.is_reference = True else: self.clear_reference_style() self.set_as_reference.emit("", 0.0, 0.0) self.is_reference = False def clear_reference_style(self): self.setFixedSize(320, 200) self.setStyleSheet(f"{self.base_style}") if self.parent(): self.parent().updateGeometry() def set_style(self, style_sheet): self.setStyleSheet(style_sheet) self.update() def update_prices(self, symbol, bid, ask): # 使用信号更新UI,确保线程安全 self.update_prices_signal.emit(symbol, bid, ask) def update_abnormal_status(self, is_abnormal): if self.is_reference: return if is_abnormal: self.abnormal_count += 1 self.set_style(self.abnormal_style) else: self.abnormal_count = 0 self.set_style(self.normal_style) def browse_mt5_exe(self): path, _ = QFileDialog.getOpenFileName( self, "选择MT5可执行文件", filter="Executable Files (*.exe)" ) if path: self.path_input.setText(path) self.start_connection() def start_connection(self): try: if not self.path_input.text().strip(): return if hasattr(self, 'mt5_worker') and self.mt5_worker: self.mt5_worker.stop() self.mt5_worker = MT5ProcessWorker( self.path_input.text().strip(), self.symbol_combo.currentText() ) self.mt5_worker.start() # 启动数据接收定时器 if not self.data_timer.isActive(): self.data_timer.timeout.connect(self._process_queue) self.data_timer.start(50) # 添加临时状态显示,用于调试 self.platform_input.setText("正在连接...") self.platform_input.setStyleSheet("color: #FF8800;") except Exception as e: print(f"启动连接失败: {e}") self.platform_input.setText("连接失败") self.platform_input.setStyleSheet("color: #FF0000;") def _handle_queue_message(self, msg_type, data): try: if msg_type == 'connected': server_name, digits = data print(f"收到连接成功消息: {server_name}, 精度: {digits}") # 使用信号更新UI,确保在主线程中执行 self.update_platform_info.emit(server_name, digits) elif msg_type == 'disconnected': disconnect_msg = data self.on_disconnected(disconnect_msg) elif msg_type == 'price': symbol, bid, ask = data self.update_prices(symbol, bid, ask) elif msg_type == 'error': error_msg = data self.show_error(error_msg) except Exception as e: print(f"处理队列消息异常: {e}") def on_disconnected(self, message): self.platform_input.setText("等待连接...") self.platform_input.setStyleSheet("color: #666666;") self._is_connected = False self._has_valid_prices = False self.sell_label.setText("--") self.buy_label.setText("--") # 自动重连逻辑 if not self._valid: # 卡片已标记为无效时不重连 return QTimer.singleShot(3000, self.start_connection) def request_delete(self): # 立即从父布局中移除自己 if self.parent() and self.parent().layout(): layout = self.parent().layout() layout.removeWidget(self) # 发送删除请求 self.delete_requested.emit(weakref.ref(self)) # 停止数据接收 if hasattr(self, 'data_timer') and self.data_timer.isActive(): self.data_timer.stop() # 终止工作进程 if hasattr(self, 'mt5_worker') and self.mt5_worker: self.mt5_worker.stop() self.mt5_worker = None self.safe_disconnect(self.delete_requested) self.safe_disconnect(self.set_as_reference) self.safe_disconnect(self.symbol_changed) self.safe_disconnect(self.status_updated) self.safe_disconnect(self.update_platform_info) self.safe_disconnect(self.update_prices_signal) self._valid = False self.hide() # 立即隐藏 self.deleteLater() def restart_connection(self, new_symbol): if self.path_input.text().strip(): self.start_connection() def on_connected(self, server_name, digits): print(f"更新连接状态: {server_name}") # 添加调试输出 self.platform_input.setText(f"已连接 · {server_name}") self.platform_input.setStyleSheet("color: #009900;") self._digits = digits self._is_connected = True def show_error(self, message): QMessageBox.critical(self, "连接错误", f"{message}\n请检查:\n1. MT5客户端是否已登录\n2. 路径是否正确\n3. 品种是否有效") self.platform_input.setText("连接失败") self.path_input.clear() self._is_connected = False def update_currencies(self, new_currency=None): """更新交易品种下拉菜单""" # 检查品种是否已存在 if new_currency and new_currency not in self.currency_list: self.currency_list.append(new_currency) if hasattr(self, 'symbol_combo'): self.symbol_combo.clear() self.symbol_combo.addItems(self.currency_list) class CardScrollArea(QScrollArea): """自定义滚动区域,确保卡片完整显示""" def __init__(self, parent=None): super().__init__(parent) self.setWidgetResizable(True) self._block_signal = False # 滚动动画定时器 self.scroll_timer = QTimer(self) self.scroll_timer.timeout.connect(self._smooth_scroll_step) self.target_value = 0 self.start_value = 0 self.scroll_steps = 0 self.current_step = 0 # 安装事件过滤器以捕获滚动事件 self.verticalScrollBar().installEventFilter(self) def eventFilter(self, obj, event): """拦截滚动事件,实现卡片对齐""" # 兼容不同PyQt5版本的事件类型检查 slider_release_type = getattr(QEvent, 'SliderRelease', None) if slider_release_type is None: # 尝试使用Type枚举 slider_release_type = getattr(QEvent.Type, 'SliderRelease', None) if obj == self.verticalScrollBar() and event.type() == slider_release_type: if not self._block_signal: self._snap_to_card() return True return super().eventFilter(obj, event) def _snap_to_card(self): """对齐到最近的完整卡片""" if not self.widget(): return scroll_bar = self.verticalScrollBar() current_value = scroll_bar.value() viewport_height = self.viewport().height() layout = self.widget().layout() valid_items = [layout.itemAt(i).widget() for i in range(layout.count()) if layout.itemAt(i).widget()] positions = [] for widget in valid_items: y = widget.y() height = widget.height() top = y bottom = y + height if top <= current_value + viewport_height and bottom >= current_value: positions.extend([top, bottom]) if positions: closest_pos = min(positions, key=lambda x: abs(x - current_value)) self._start_smooth_scroll(closest_pos) def _start_smooth_scroll(self, target): """开始平滑滚动到目标位置""" scroll_bar = self.verticalScrollBar() self.start_value = scroll_bar.value() self.target_value = target self.current_step = 0 self.scroll_steps = 10 # 动画步数 self.scroll_timer.start(20) # 每20ms更新一次 def _smooth_scroll_step(self): """平滑滚动的每一步""" if self.current_step >= self.scroll_steps: self.scroll_timer.stop() return t = self.current_step / self.scroll_steps ease_t = t * t * (3 - 2 * t) # 缓入缓出函数 scroll_bar = self.verticalScrollBar() value = int(self.start_value + (self.target_value - self.start_value) * ease_t) self._block_signal = True scroll_bar.setValue(value) self._block_signal = False self.current_step += 1 def resizeEvent(self, event): """处理窗口大小变化事件,确保布局更新""" super().resizeEvent(event) # 使用单次定时器延迟布局更新,避免频繁计算 QTimer.singleShot(100, self._safe_layout_update) def _safe_layout_update(self): layout = self.widget().layout() if layout: layout.invalidate() self.updateGeometry() class FlowLayout(QLayout): def __init__(self, parent=None): super().__init__(parent) self._items = [] self._hspace = 10 # 水平间距 self._vspace = 10 # 垂直间距 self._is_layouting = False self._layout_cache = None self._last_width = -1 self._update_pending = False # 用于延迟布局更新 def addItem(self, item): self._items.append(item) self._layout_cache = None self.invalidate() self._schedule_layout_update() def _schedule_layout_update(self): """安排延迟布局更新,避免频繁计算""" if not self._update_pending: self._update_pending = True QTimer.singleShot(50, self._safe_layout_update) def _safe_layout_update(self): """安全地更新布局""" try: if self.parent(): self._do_layout(self.parent().rect()) finally: self._update_pending = False def count(self): return len(self._items) def itemAt(self, index): if 0 <= index < len(self._items): return self._items[index] return None def takeAt(self, index): if 0 <= index < len(self._items): self._layout_cache = None self._schedule_layout_update() return self._items.pop(index) return None def setGeometry(self, rect): super().setGeometry(rect) if self._last_width != rect.width(): self._layout_cache = None self._last_width = rect.width() self._do_layout(rect) def _do_layout(self, rect): if self._is_layouting: return self._is_layouting = True try: x = rect.x() y = rect.y() line_height = 0 available_width = rect.width() valid_items = [item for item in self._items if item.widget() and item.widget().isVisible()] layout_cache = [] for i, item in enumerate(valid_items): widget = item.widget() if not widget: continue # 获取内容边距 margins = widget.contentsMargins() total_margin_width = margins.left() + margins.right() total_margin_height = margins.top() + margins.bottom() # 获取组件的实际大小 size_hint = widget.sizeHint() widget_width = size_hint.width() + total_margin_width widget_height = size_hint.height() + total_margin_height # 计算水平间距 if i > 0: x += self._hspace # 判断是否需要换行 if x + widget_width > available_width and x != rect.x(): x = rect.x() y += line_height + self._vspace line_height = 0 # 设置固定大小并放置组件 widget.setFixedSize(widget_width, widget_height) geom = QRect(QPoint(x, y), QSize(widget_width, widget_height)) widget.setGeometry(geom) layout_cache.append((widget, geom)) # 更新当前行的宽度和最大高度 x += widget_width line_height = max(line_height, widget_height) # 保存布局缓存 self._layout_cache = layout_cache # 强制更新布局和渲染 self.update() if self.parent(): self.parent().update() self.parent().repaint() finally: self._is_layouting = False def sizeHint(self): return self.minimumSize() def minimumSize(self): width = 0 height = 0 line_width = 0 line_height = 0 for item in self._items: widget = item.widget() if not widget or not widget.isVisible(): continue # 考虑内容边距 margins = widget.contentsMargins() total_margin_width = margins.left() + margins.right() total_margin_height = margins.top() + margins.bottom() size = widget.sizeHint() widget_width = size.width() + total_margin_width widget_height = size.height() + total_margin_height # 计算水平间距 if line_width > 0: line_width += self._hspace # 判断是否需要换行 if line_width + widget_width > self.parent().width(): width = max(width, line_width) height += line_height + self._vspace line_width = widget_width line_height = widget_height else: line_width += widget_width line_height = max(line_height, widget_height) # 计算最后一行 width = max(width, line_width) height += line_height return QSize(width, height) def horizontalSpacing(self): return self._hspace def verticalSpacing(self): return self._vspace def setSpacing(self, spacing): self._hspace = spacing self._vspace = spacing self._layout_cache = None self.invalidate() def spacing(self): return self.horizontalSpacing() class MainWindow(QMainWindow): currency_added = pyqtSignal(str) def __init__(self): super().__init__() self._global_currencies = ["EURUSD", "GBPUSD", "USDJPY", "XAUUSD"] self._currency_lock = threading.Lock() # 添加锁保护 self._layout_lock = False self._pending_deletions = set() self.cards = [] self.reference_card = None self.threshold = 20 self.setup_ui() self.setWindowTitle("多平台报价监控系统 版本:V1.0 作者:wbb") self.resize(1280, 720) self.setup_monitoring() self._queue_lock = False self._layout_timer = QTimer() self._layout_timer.setSingleShot(True) self._layout_timer.timeout.connect(self._safe_layout_update) self.start_status_monitor() def setup_ui(self): main_widget = QWidget() self.setCentralWidget(main_widget) main_layout = QHBoxLayout(main_widget) # 整体水平布局 main_layout.setContentsMargins(10, 10, 10, 10) # 左侧垂直布局 (控制栏 + 卡片区) left_vertical_layout = QVBoxLayout() # 控制栏 control_bar = QHBoxLayout() self.threshold_input = QLineEdit("20") self.threshold_input.setFixedWidth(80) self.monitor_switch = QCheckBox("实时监控") self.monitor_switch.setChecked(True) control_bar.addWidget(QLabel("异常阈值(点):")) control_bar.addWidget(self.threshold_input) control_bar.addWidget(self.monitor_switch) # 状态栏 self.status_bar = QStatusBar() self.status_bar.setSizeGripEnabled(False) status_widget = QWidget() status_layout = QHBoxLayout(status_widget) status_layout.setContentsMargins(0, 0, 0, 0) self.reference_info_label = QLabel("未设置参照平台") self.reference_info_label.setStyleSheet("color: #666666; padding: 0 10px;") self.reference_info_label.setFixedWidth(320) status_layout.addWidget(self.reference_info_label) self.performance_info_label = QLabel("内存占用: -- MB | 存活卡片: --") self.performance_info_label.setStyleSheet("color: #666666; padding: 0 10px;") self.performance_info_label.setFixedWidth(280) status_layout.addWidget(self.performance_info_label) self.status_bar.addWidget(status_widget) control_bar.addWidget(self.status_bar) left_vertical_layout.addLayout(control_bar) # 左侧卡片区域 left_card_widget = QWidget() left_card_layout = QVBoxLayout(left_card_widget) left_card_layout.setContentsMargins(0, 0, 0, 0) scroll = CardScrollArea() scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll.setStyleSheet(""" QScrollArea { border: 1px solid #e0e0e0; border-radius: 4px; } """) self.scroll_content = QWidget() self.flow_layout = FlowLayout(self.scroll_content) self.scroll_content.setLayout(self.flow_layout) self.scroll_content.setSizePolicy( QSizePolicy.Expanding, QSizePolicy.MinimumExpanding ) scroll.setWidget(self.scroll_content) left_card_layout.addWidget(scroll) left_vertical_layout.addWidget(left_card_widget, 1) # 设置伸缩因子为1,占据剩余空间 # 右侧垂直布局 (按钮 + 日志区) right_vertical_layout = QVBoxLayout() # 添加按钮水平布局 button_layout = QHBoxLayout() button_layout.setAlignment(Qt.AlignLeft) # 按钮靠左排列 # 添加左侧弹簧 button_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)) self.add_card_btn = self.create_tool_button("+ 添加卡片", "#0078D4") self.add_currency_btn = self.create_tool_button("+ 添加品种", "#009966") self.clear_display_btn = self.create_tool_button("- 清除显示", "#D40000") # 调整按钮间距和边距 button_layout.setSpacing(10) button_layout.setContentsMargins(0, 0, 0, 10) # 底部留出间距 button_layout.addWidget(self.add_card_btn) button_layout.addWidget(self.add_currency_btn) button_layout.addWidget(self.clear_display_btn) # 添加右侧弹簧 button_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)) right_vertical_layout.addLayout(button_layout) # 右侧日志区域 right_log_widget = QWidget() right_log_layout = QVBoxLayout(right_log_widget) right_log_layout.setContentsMargins(0, 0, 0, 0) # 异常信息区域 abnormal_title_widget = QWidget() abnormal_title_layout = QHBoxLayout(abnormal_title_widget) abnormal_title_layout.setContentsMargins(0, 0, 0, 0) abnormal_label = QLabel("异常平台") abnormal_label.setStyleSheet(""" font-weight: bold; padding: 4px 12px; background-color: #f0f0f0; border-radius: 4px; border-left: 1px dashed #cccccc; border-right: 1px dashed #cccccc; """) abnormal_label.setAlignment(Qt.AlignCenter) abnormal_title_layout.addWidget(abnormal_label) abnormal_title_layout.addStretch(1) right_log_layout.addWidget(abnormal_title_widget) self.abnormal_area = QTextEdit() self.abnormal_area.setReadOnly(True) self.abnormal_area.setStyleSheet(""" QTextEdit { background: #fff0f0; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; font-family: Consolas; font-size: 12px; } """) right_log_layout.addWidget(self.abnormal_area, 2) # 异常信息区占2份 # 系统日志区域 log_title_widget = QWidget() log_title_layout = QHBoxLayout(log_title_widget) log_title_layout.setContentsMargins(0, 0, 0, 0) log_label = QLabel("系统日志") log_label.setStyleSheet(""" font-weight: bold; padding: 4px 12px; background-color: #f0f0f0; border-radius: 4px; border-left: 1px dashed #cccccc; border-right: 1px dashed #cccccc; """) log_label.setAlignment(Qt.AlignCenter) log_title_layout.addWidget(log_label) log_title_layout.addStretch(1) right_log_layout.addWidget(log_title_widget) self.log_area = QTextEdit() self.log_area.setReadOnly(True) self.log_area.setStyleSheet(""" QTextEdit { background: #f8f8f8; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; font-family: Consolas, monospace; font-size: 12px; color: #333333; line-height: 1.4; } """) right_log_layout.addWidget(self.log_area, 1) # 系统日志区占1份 right_vertical_layout.addWidget(right_log_widget, 1) # 设置伸缩因子为1,占据剩余空间 # 将左右两个垂直布局添加到整体水平布局 main_layout.addLayout(left_vertical_layout, 2) # 左侧占2份 main_layout.addLayout(right_vertical_layout, 1) # 右侧占1份 # 连接信号 self.add_card_btn.clicked.connect(self.add_card) self.add_currency_btn.clicked.connect(self.show_add_currency_dialog) self.threshold_input.textChanged.connect(self.update_threshold) self.currency_added.connect(self.add_global_currency) self.clear_display_btn.clicked.connect(self.clear_logs) def clear_logs(self): """清空异常平台和系统日志的显示""" self.abnormal_area.clear() self.log_area.clear() def create_tool_button(self, text, color): btn = QPushButton(text) btn.setStyleSheet(f""" QPushButton {{ background: {color}; color: white; border-radius: 4px; padding: 8px 12px; min-width: 100px; }} QPushButton:pressed {{ color: #D0D0D0; background: {QColor(color).darker(150).name()}; }} """) return btn def setup_monitoring(self): self.check_timer = QTimer() self.check_timer.timeout.connect(self.check_all_platforms) self.check_timer.start(1000) def check_perf(self): try: # 获取内存占用 process = psutil.Process() mem = process.memory_info().rss / 1024 / 1024 # 计算存活卡片数量 valid_cards = len([ref for ref in self.cards if ref() is not None]) # 更新状态栏性能信息 self.performance_info_label.setText(f"内存占用: {mem:.2f} MB | 存活卡片: {valid_cards}") except Exception as e: print(f"更新性能信息错误: {e}") # 出错时保持原显示 valid_cards = len([ref for ref in self.cards if ref() is not None]) self.performance_info_label.setText(f"内存占用: -- MB | 存活卡片: {valid_cards}") def update_threshold(self): try: self.threshold = int(self.threshold_input.text()) self.log(f"异常阈值更新为: {self.threshold}点") except: self.threshold = 20 self.threshold_input.setText("20") def handle_reference(self, symbol, bid, ask): # 清除旧参考样式 if self.reference_card and self.reference_card() and self.reference_card().is_reference: self.reference_card().ref_btn.setChecked(False) self.reference_card().clear_reference_style() # 设置新参考卡片 if symbol: current_card = self.sender() if current_card: current_card.ref_btn.setChecked(True) current_card.set_style(current_card.ref_style) current_card.is_reference = True self.reference_card = weakref.ref(current_card) # 更新参考平台信息 timestamp = datetime.now().strftime("%H:%M:%S") self.reference_info_label.setText(f"参照平台: {symbol} {bid:.5f}/{ask:.5f} [{timestamp}]") self.log(f"设置参考平台: {symbol}") else: self.reference_card = None self.reference_info_label.setText("未设置参照平台") self.log("取消参考平台设置") def add_card(self): """添加新的价格卡片""" card = PriceCard(currency_list=self._global_currencies) card.delete_requested.connect(self.remove_card) card.set_as_reference.connect(self.handle_reference) card.status_updated.connect(self.update_status) card.symbol_changed.connect(self.update_currencies) self.flow_layout.addWidget(card) self.cards.append(weakref.ref(card)) # 延迟更新布局,避免频繁计算 self._schedule_layout_update() # 记录日志 self.log(f"添加新卡片,当前总数: {len(self.cards)}") def remove_card(self, card_ref): """安全移除卡片""" card = card_ref() if card: # 从布局中移除 if self.flow_layout.indexOf(card) >= 0: self.flow_layout.removeWidget(card) card.hide() # 如果是参考卡片,清除参考状态 if self.reference_card and self.reference_card() == card: self.handle_reference("", 0, 0) # 延迟删除,确保布局更新完成 QTimer.singleShot(500, card.deleteLater) # 从卡片列表中移除 self.cards = [ref for ref in self.cards if ref() is not None] # 记录日志 self.log(f"移除卡片,当前总数: {len(self.cards)}") # 延迟更新布局 self._schedule_layout_update() def show_add_currency_dialog(self): """显示添加交易品种对话框""" dialog = QDialog(self) dialog.setWindowTitle("添加交易品种") dialog.setFixedSize(300, 150) layout = QVBoxLayout(dialog) input_label = QLabel("请输入新的交易品种:") input_field = QLineEdit() input_field.setPlaceholderText("例如: EURUSD") button_layout = QHBoxLayout() ok_button = QPushButton("确定") cancel_button = QPushButton("取消") button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) layout.addWidget(input_label) layout.addWidget(input_field) layout.addLayout(button_layout) ok_button.clicked.connect(lambda: self._handle_add_currency(input_field.text(), dialog)) cancel_button.clicked.connect(dialog.reject) dialog.exec_() def _handle_add_currency(self, currency, dialog): """处理添加交易品种""" if currency.strip(): currency = currency.strip().upper() if currency not in self._global_currencies: self.currency_added.emit(currency) self.log(f"添加新交易品种: {currency}") else: self.log(f"交易品种 '{currency}' 已存在") dialog.accept() def add_global_currency(self, currency): """添加全局交易品种""" with self._currency_lock: if currency not in self._global_currencies: self._global_currencies.append(currency) # 更新所有卡片的品种列表 for ref in self.cards: card = ref() if card: card.update_currencies(currency) def update_currencies(self, symbol): """更新全局交易品种列表""" if symbol and symbol not in self._global_currencies: with self._currency_lock: self._global_currencies.append(symbol) # 更新其他卡片的品种列表 for ref in self.cards: card = ref() if card and card.symbol_combo.currentText() != symbol: card.update_currencies(symbol) def check_all_platforms(self): """检查所有平台价格差异""" if not self.monitor_switch.isChecked() or not self.reference_card or not self.reference_card(): return ref_card = self.reference_card() ref_symbol = ref_card.symbol_combo.currentText() ref_bid = ref_card.current_bid ref_ask = ref_card.current_ask if ref_bid <= 0 or ref_ask <= 0: return self.abnormal_area.clear() abnormal_count = 0 for ref in self.cards: card = ref() if card and card != ref_card and card.isValid() and card._has_valid_prices: # 确保检查相同的交易品种 if card.symbol_combo.currentText() == ref_symbol: card_bid = card.current_bid card_ask = card.current_ask # 计算点差差异 (假设5位小数的品种) bid_diff = (card_bid - ref_bid) * 10000 ask_diff = (card_ask - ref_ask) * 10000 # 判断是否异常 is_abnormal = abs(bid_diff) > self.threshold or abs(ask_diff) > self.threshold card.update_abnormal_status(is_abnormal) if is_abnormal: abnormal_count += 1 platform = card.platform_input.text().split("·")[1].strip() self.abnormal_area.append( f"平台: {platform}\n" f"参考价格: {ref_bid:.5f} / {ref_ask:.5f}\n" f"当前价格: {card_bid:.5f} / {card_ask:.5f}\n" f"差异: {bid_diff:+.2f} / {ask_diff:+.2f} 点\n" f"{'=' * 30}\n" ) # 更新状态栏 if abnormal_count > 0: self.statusBar().showMessage(f"发现 {abnormal_count} 个异常平台", 3000) def log(self, message): """记录系统日志""" timestamp = datetime.now().strftime("%H:%M:%S") log_entry = f"[{timestamp}] {message}\n" self.log_area.insertPlainText(log_entry) # 自动滚动到底部 self.log_area.moveCursor(self.log_area.textCursor().End) def update_status(self, message): """更新状态栏消息""" self.statusBar().showMessage(message, 3000) def _schedule_layout_update(self): """安排布局更新,避免频繁计算""" if not self._layout_timer.isActive(): self._layout_timer.start(100) def _safe_layout_update(self): """安全地更新布局""" if self.flow_layout: self.flow_layout.invalidate() self.flow_layout._layout_cache = None # 清除缓存 self.scroll_content.adjustSize() self.flow_layout._do_layout(self.scroll_content.rect()) def start_status_monitor(self): """启动状态监控定时器""" self.performance_timer = QTimer() self.performance_timer.timeout.connect(self.check_perf) self.performance_timer.start(2000) # 每2秒更新一次 def closeEvent(self, event): """关闭应用程序时的清理工作""" # 停止所有定时器 if hasattr(self, 'check_timer') and self.check_timer.isActive(): self.check_timer.stop() if hasattr(self, 'performance_timer') and self.performance_timer.isActive(): self.performance_timer.stop() if hasattr(self, '_layout_timer') and self._layout_timer.isActive(): self._layout_timer.stop() # 停止所有MT5工作进程 for ref in self.cards: card = ref() if card and card.mt5_worker: card.mt5_worker.stop() # 确认所有进程已停止 time.sleep(0.5) event.accept() if __name__ == "__main__": freeze_support() # 支持多进程打包 app = QApplication(sys.argv) # 设置应用程序样式 app.setStyle("Fusion") # 设置全局字体 font = QFont("微软雅黑", 9) app.setFont(font) # 设置全局样式 app.setStyleSheet(""" QMainWindow, QWidget { background-color: #f8f9fa; } QLabel { color: #333333; } QPushButton { border-radius: 4px; padding: 6px 12px; background-color: #007bff; color: white; border: none; } QPushButton:hover { background-color: #0069d9; } QPushButton:pressed { background-color: #0056b3; } QScrollBar:vertical { width: 12px; background: #f0f0f0; } QScrollBar::handle:vertical { background: #c0c0c0; border-radius: 6px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } """) window = MainWindow() window.show() sys.exit(app.exec_()) 添加卡片后选择mt5路径后,mt5连接成功并开始报价更新,但是platform_input中显示没有更新,还是显示正在连接
05-13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值