QTimer.singleShot与QWidget初始化

本文介绍了一种在Qt程序中优化构造过程的方法,通过使用QTimer::singleShot避免UI显示延迟和参数获取错误的问题。

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

  1. 在exec或show一个QWidget(及其子类)时,如果构造函数中需要做的工作需要消耗较多时间,会导致用户启动程序后,很长时间看不到界面的弹出。特别是如果这个消耗时间很长的工作并不影响程序的UI,而让用户去等待较长的时间显然是不合适的。
  2. 一个带UI的程序,如果在构造时需要用到UI的一些geo相关参数(如size),可能会出现显示问题。因为在构造动作结束之前,UI是不显示的。而UI不显示,geo相关参数是默认值,并不是Ui设计师或构造函数里设置的值。

以上两个问题,可以使用QTimer::singleShot(time, receiver, slot)来解决。这个函数将使得slot函数在time毫秒后被调用。为了避免上述两个问题,可以如下编写构造函数:
假设构造函数为myClass,消耗时间较长的代码放在函数longTime中,需要UI参数的代码放在needUI中;

myClass::myClass()
{
    // 其他必要操作
   ...
   QTimer::singleShot(0, this, needUI());
}

myClass::needUI()
{
    // 构造函数已经结束,可以正确获得geo相关参数
    ...
    QTimer::singleShot(0, this, longTime())
}

myClass::longTime()
{
    // 这里放置需要较长时间执行代码,如IO相关内容
}

例如,假设myClass是个图片处理工具。构造函数中做必要设置,结束后程序立即显示,即使图片还没有加载;在needUI函数中就可以成功获取程序的size,然后将图片以合适的大小显示在程序中;最后longTime可能是一个图像处理算法,需要几秒钟来得到需要结果,然后再显示在程序中。

转载于:https://www.cnblogs.com/kohlrabi/p/7117693.html

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、付费专栏及课程。

余额充值