Commit 13c76314 by Yuhaibo

修改

parent f6177d04
...@@ -178,7 +178,7 @@ except ImportError: ...@@ -178,7 +178,7 @@ except ImportError:
MenuBarHandler, MenuBarHandler,
) )
from handlers.datasetpage import DataCollectionChannelHandler from handlers.datasetpage import DataCollectionChannelHandler
from handlers.videopage import GeneralSetPanelHandler, ModelSettingHandler, TestHandler, CurvePanelHandler, MissionPanelHandler, HistoryPanelHandler from handlers.videopage import GeneralSetPanelHandler, ModelSettingHandler, TestHandler, CurvePanelHandler, MissionPanelHandler
from handlers.modelpage import ( from handlers.modelpage import (
ModelSyncHandler, ModelSyncHandler,
ModelSignalHandler, ModelSignalHandler,
...@@ -528,7 +528,6 @@ class MainWindow( ...@@ -528,7 +528,6 @@ class MainWindow(
ModelSettingsHandler, ModelSettingsHandler,
ModelTrainingHandler, ModelTrainingHandler,
MissionPanelHandler, # 任务面板处理器 MissionPanelHandler, # 任务面板处理器
HistoryPanelHandler, # 历史回放面板处理器
QtWidgets.QMainWindow QtWidgets.QMainWindow
): ):
""" """
...@@ -560,10 +559,7 @@ class MainWindow( ...@@ -560,10 +559,7 @@ class MainWindow(
# 设置窗口标题 # 设置窗口标题
self.setWindowTitle(self.tr("帕特智能油液位检测")) self.setWindowTitle(self.tr("帕特智能油液位检测"))
print(f"[MainWindow] 窗口标题已设置: 帕特智能油液位检测")
print(f"[MainWindow] 注意:窗口标题栏由操作系统绘制,不受Qt字体设置影响")
print(f" - 标题栏字体由Windows系统设置控制")
print(f" - 应用字体只影响窗口内部的Qt控件(按钮、标签、表格等)")
# 设置窗口图标(用于左上角和任务栏) # 设置窗口图标(用于左上角和任务栏)
self._setWindowIcon() self._setWindowIcon()
...@@ -783,6 +779,24 @@ class MainWindow( ...@@ -783,6 +779,24 @@ class MainWindow(
# 保存第一个面板的引用(兼容现有代码) # 保存第一个面板的引用(兼容现有代码)
self.channelPanel = self.channelPanels[0] if self.channelPanels else None self.channelPanel = self.channelPanels[0] if self.channelPanels else None
# 🔥 创建4个历史视频面板(用于曲线模式的历史回放布局)
try:
from .widgets.videopage import HistoryVideoPanel
except ImportError:
from widgets.videopage import HistoryVideoPanel
self.historyVideoPanels = []
for i in range(4):
channel_id = f'channel{i+1}'
channel_name = self.getChannelDisplayName(channel_id, i+1)
# 🔥 创建历史视频面板,不设置父窗口(避免自动显示),但传入主窗口引用以访问 curvemission
history_panel = HistoryVideoPanel(title=channel_name, parent=None, debug_mode=False, main_window=self)
history_panel.setObjectName(f"HistoryVideoPanel_{i+1}")
self.historyVideoPanels.append(history_panel)
print(f"[MainWindow] 已创建 {len(self.historyVideoPanels)} 个历史视频面板")
# 通过handler初始化通道面板数据 # 通过handler初始化通道面板数据
if hasattr(self, 'initializeChannelPanels'): if hasattr(self, 'initializeChannelPanels'):
self.initializeChannelPanels(self.channelPanels) self.initializeChannelPanels(self.channelPanels)
...@@ -831,7 +845,7 @@ class MainWindow( ...@@ -831,7 +845,7 @@ class MainWindow(
# === 子布局0:实时检测模式(左侧通道列表)=== # === 子布局0:实时检测模式(左侧通道列表)===
self._createRealtimeCurveSubLayout() self._createRealtimeCurveSubLayout()
# === 子布局1:历史回放模式(左侧HistoryPanel)=== # === 子布局1:历史回放模式(左侧历史视频面板容器)===
self._createHistoryCurveSubLayout() self._createHistoryCurveSubLayout()
# 布局结构:左侧子布局栈 + 右侧共用CurvePanel # 布局结构:左侧子布局栈 + 右侧共用CurvePanel
...@@ -843,7 +857,10 @@ class MainWindow( ...@@ -843,7 +857,10 @@ class MainWindow(
print(f"[MainWindow] 曲线模式布局已创建:左侧子布局栈(实时/历史) + 右侧共用CurvePanel") print(f"[MainWindow] 曲线模式布局已创建:左侧子布局栈(实时/历史) + 右侧共用CurvePanel")
def _createRealtimeCurveSubLayout(self): def _createRealtimeCurveSubLayout(self):
"""创建实时检测曲线子布局(索引0)- 仅左侧通道列表""" """创建实时检测曲线子布局(索引0)- 左侧通道列表"""
# 🔥 保留固定通道容器系统,但改为基于CSV文件动态显示
# 不再从任务配置读取通道筛选,而是显示所有容器,由CSV文件数量决定实际显示
sublayout_widget = QtWidgets.QWidget() sublayout_widget = QtWidgets.QWidget()
sublayout = QtWidgets.QVBoxLayout(sublayout_widget) sublayout = QtWidgets.QVBoxLayout(sublayout_widget)
sublayout.setContentsMargins(0, 0, 0, 0) sublayout.setContentsMargins(0, 0, 0, 0)
...@@ -855,7 +872,7 @@ class MainWindow( ...@@ -855,7 +872,7 @@ class MainWindow(
self.curve_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.curve_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.curve_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.curve_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
# 创建通道容器(初始为空) # 创建通道容器
self.curve_channel_container = QtWidgets.QWidget() self.curve_channel_container = QtWidgets.QWidget()
self.curve_channel_layout = QtWidgets.QVBoxLayout(self.curve_channel_container) self.curve_channel_layout = QtWidgets.QVBoxLayout(self.curve_channel_container)
self.curve_channel_layout.setContentsMargins(5, 5, 5, 5) self.curve_channel_layout.setContentsMargins(5, 5, 5, 5)
...@@ -864,7 +881,7 @@ class MainWindow( ...@@ -864,7 +881,7 @@ class MainWindow(
# 初始化通道包裹容器列表(实时检测模式) # 初始化通道包裹容器列表(实时检测模式)
self.channel_widgets_for_curve = [] self.channel_widgets_for_curve = []
# 初始创建所有4个通道(隐藏状态 # 创建4个通道容器(初始隐藏,等待CSV文件加载
for i in range(4): for i in range(4):
wrapper = QtWidgets.QWidget() wrapper = QtWidgets.QWidget()
wrapper.setFixedSize(620, 465) wrapper.setFixedSize(620, 465)
...@@ -881,117 +898,250 @@ class MainWindow( ...@@ -881,117 +898,250 @@ class MainWindow(
sublayout.addWidget(self.curve_scroll_area) sublayout.addWidget(self.curve_scroll_area)
self.curveLayoutStack.addWidget(sublayout_widget) self.curveLayoutStack.addWidget(sublayout_widget)
print(f"[MainWindow] 实时检测曲线子布局已创建(索引0)- 通道列表") print(f"[MainWindow] 实时检测曲线子布局已创建(索引0)- 基于CSV文件的动态通道系统")
def _createHistoryCurveSubLayout(self): def _createHistoryCurveSubLayout(self):
"""创建历史回放曲线子布局(索引1)- 仅左侧HistoryPanel""" """创建历史回放曲线子布局(索引1)- 使用历史视频面板容器"""
try:
from .widgets import HistoryPanel
except ImportError:
from widgets import HistoryPanel
sublayout_widget = QtWidgets.QWidget() sublayout_widget = QtWidgets.QWidget()
sublayout = QtWidgets.QVBoxLayout(sublayout_widget) sublayout = QtWidgets.QVBoxLayout(sublayout_widget)
sublayout.setContentsMargins(0, 0, 0, 0) sublayout.setContentsMargins(0, 0, 0, 0)
sublayout.setSpacing(0) sublayout.setSpacing(0)
# === 历史回放面板(HistoryPanel)=== # === 带滚动条的垂直历史视频面板区域 ===
self.historyPanel = HistoryPanel() self.history_scroll_area = QtWidgets.QScrollArea()
sublayout.addWidget(self.historyPanel, alignment=QtCore.Qt.AlignTop) self.history_scroll_area.setWidgetResizable(False)
self.history_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
# 连接历史回放面板到Handler self.history_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
if hasattr(self, 'connectHistoryPanel'):
self.connectHistoryPanel(self.historyPanel) # 创建历史视频容器
self.history_channel_container = QtWidgets.QWidget()
# 🔥 为了兼容性,仍然创建 history_channel_widgets_for_curve # 🔥 设置容器最小高度,确保能容纳所有wrapper
# 但在历史回放模式下,这些容器不会被使用 # 4个wrapper * 465高度 + 3个间距 * 10 + 上下边距 * 2 * 5 = 1860 + 30 + 10 = 1900
self.history_channel_container.setMinimumHeight(1900)
self.history_channel_layout = QtWidgets.QVBoxLayout(self.history_channel_container)
self.history_channel_layout.setContentsMargins(5, 5, 5, 5)
self.history_channel_layout.setSpacing(10)
# 初始化历史视频包裹容器列表(历史回放模式)
self.history_channel_widgets_for_curve = [] self.history_channel_widgets_for_curve = []
# 创建4个历史视频容器(初始隐藏,等待CSV文件加载)
for i in range(4): for i in range(4):
wrapper = QtWidgets.QWidget() wrapper = QtWidgets.QWidget()
wrapper.setFixedSize(620, 465) wrapper.setFixedSize(620, 465)
wrapper.setVisible(False) wrapper.setVisible(False) # 初始隐藏
wrapper_layout = QtWidgets.QVBoxLayout(wrapper) wrapper_layout = QtWidgets.QVBoxLayout(wrapper)
wrapper_layout.setContentsMargins(0, 0, 0, 0) wrapper_layout.setContentsMargins(0, 0, 0, 0)
wrapper_layout.setSpacing(0) wrapper_layout.setSpacing(0)
self.history_channel_widgets_for_curve.append(wrapper) self.history_channel_widgets_for_curve.append(wrapper)
self.history_channel_layout.addWidget(wrapper)
self.history_scroll_area.setWidget(self.history_channel_container)
sublayout.addWidget(self.history_scroll_area)
self.curveLayoutStack.addWidget(sublayout_widget) self.curveLayoutStack.addWidget(sublayout_widget)
print(f"[MainWindow] 历史回放曲线子布局已创建(索引1)- HistoryPanel") print(f"[MainWindow] 历史回放曲线子布局已创建(索引1)- 历史视频面板容器系统")
def _onChannelCurveClicked(self, task_name):
"""
处理通道面板的查看曲线按钮点击(来源2)
Args:
task_name: 通道面板的任务名称
"""
print(f"🔄 [主窗口] 通道面板查看曲线按钮被点击,任务名称: {task_name}")
# 设置 curvemission 的值
if hasattr(self, 'curvePanel') and self.curvePanel:
success = self.curvePanel.setMissionFromTaskName(task_name)
if success:
print(f"✅ [主窗口] 已设置 curvemission 为: {task_name}")
else:
print(f"⚠️ [主窗口] 设置 curvemission 失败: {task_name}")
# 切换到曲线模式
self.toggleVideoPageMode()
def _onCurveMissionChanged(self, mission_name): def _onCurveMissionChanged(self, mission_name):
"""曲线任务选择变化,更新显示的通道""" """曲线任务选择变化(基于CSV文件动态显示)"""
if not mission_name or mission_name == "请选择任务": if not mission_name or mission_name == "请选择任务":
# 隐藏所有通道 print(f"[曲线布局] 未选择任务,隐藏所有通道容器")
self._updateCurveChannelDisplay([]) self._updateCurveChannelDisplay([])
return return
# 获取任务使用的通道列表 # 🔥 根据 curve_load_mode 决定显示逻辑
if hasattr(self, 'curvePanelHandler'):
curve_mode = self.curvePanelHandler.getCurveLoadMode()
else:
curve_mode = 'realtime'
if curve_mode == 'realtime':
# 🔥 实时模式:只显示任务配置中使用的通道
selected_channels = self._getTaskChannels(mission_name) selected_channels = self._getTaskChannels(mission_name)
print(f"📊 [曲线布局-实时模式] 任务 {mission_name},显示任务使用的通道: {selected_channels}")
else:
# 🔥 历史模式:显示所有通道容器
selected_channels = ['通道1', '通道2', '通道3', '通道4']
print(f"📊 [曲线布局-历史模式] 任务 {mission_name},显示所有通道容器")
if selected_channels:
print(f"📊 [曲线布局] 任务 {mission_name} 使用通道: {selected_channels}")
self._updateCurveChannelDisplay(selected_channels) self._updateCurveChannelDisplay(selected_channels)
else:
print(f"[曲线布局] 任务 {mission_name} 无通道配置")
self._updateCurveChannelDisplay([])
def _getTaskChannels(self, mission_name): def _getTaskChannels(self, mission_name):
"""从任务配置文件获取使用的通道列表""" """
从任务配置文件中获取使用的通道列表
Args:
mission_name: 任务名称
Returns:
list: 任务使用的通道名称列表,如 ['通道1', '通道2']
"""
import os import os
import yaml import yaml
try: try:
# 构建任务配置文件路径 # 构建任务文件夹路径
if getattr(sys, 'frozen', False):
project_root = os.path.dirname(sys.executable)
else:
try: try:
from database.config import get_project_root from database.config import get_project_root
project_root = get_project_root() project_root = get_project_root()
except ImportError: except ImportError:
project_root = os.getcwd() project_root = os.getcwd()
config_path = os.path.join(project_root, 'database', 'config', 'mission', f'{mission_name}.yaml') mission_path = os.path.join(project_root, 'database', 'mission_result', mission_name)
if not os.path.exists(mission_path):
print(f"⚠️ [通道筛选] 任务文件夹不存在: {mission_path}")
return []
# 查找任务配置文件(.yaml文件,文件名与任务名相同)
config_file = os.path.join(mission_path, f"{mission_name}.yaml")
if not os.path.exists(config_path): if not os.path.exists(config_file):
print(f"[曲线布局] 任务配置文件不存在: {config_path}") print(f"⚠️ [通道筛选] 任务配置文件不存在: {config_file}")
# 如果没有配置文件,返回空列表
return [] return []
# 读取任务配置 # 读取任务配置
with open(config_path, 'r', encoding='utf-8') as f: with open(config_file, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f) task_config = yaml.safe_load(f)
if not task_config:
print(f"⚠️ [通道筛选] 任务配置为空: {config_file}")
return []
# 获取选中的通道列表 # 🔍 调试:打印配置文件的所有键
selected_channels = config.get('selected_channels', []) print(f"🔍 [通道筛选] 配置文件键: {list(task_config.keys())}")
return selected_channels
# 从配置中提取使用的通道
# 配置格式可能是:selected_channels: ['通道2', '通道3'] 或 channels: ['channel1', 'channel2']
used_channels = []
# 🔥 优先检查 selected_channels 字段(最常用的格式)
if 'selected_channels' in task_config:
# 格式1: selected_channels: ['通道2', '通道3']
channel_list = task_config['selected_channels']
if isinstance(channel_list, list):
used_channels = [ch for ch in channel_list if isinstance(ch, str) and '通道' in ch]
print(f"🔍 [通道筛选] 从 selected_channels 读取: {used_channels}")
# 尝试其他可能的配置键名
elif 'channels' in task_config:
# 格式2: channels: ['channel1', 'channel2']
channel_list = task_config['channels']
if isinstance(channel_list, list):
for ch in channel_list:
if isinstance(ch, str) and 'channel' in ch.lower():
# 提取通道编号
ch_num = ''.join(filter(str.isdigit, ch))
if ch_num:
used_channels.append(f'通道{ch_num}')
elif isinstance(ch, int):
used_channels.append(f'通道{ch}')
print(f"🔍 [通道筛选] 从 channels 读取: {used_channels}")
elif 'channel_list' in task_config:
# 格式3: channel_list: [1, 2, 3]
channel_list = task_config['channel_list']
if isinstance(channel_list, list):
for ch_num in channel_list:
used_channels.append(f'通道{ch_num}')
print(f"🔍 [通道筛选] 从 channel_list 读取: {used_channels}")
elif 'task_channels' in task_config:
# 格式4: task_channels: '通道1, 通道2'
channels_str = task_config['task_channels']
if isinstance(channels_str, str):
used_channels = [ch.strip() for ch in channels_str.split(',')]
print(f"🔍 [通道筛选] 从 task_channels 读取: {used_channels}")
else:
# 如果没有明确的通道配置,尝试从其他字段推断
# 检查是否有 channel1, channel2 等键
for i in range(1, 5):
if f'channel{i}' in task_config:
used_channels.append(f'通道{i}')
if used_channels:
print(f"🔍 [通道筛选] 从 channel 字段推断: {used_channels}")
# 去重并排序
used_channels = sorted(list(set(used_channels)))
if used_channels:
print(f"✅ [通道筛选] 任务 {mission_name} 使用的通道: {used_channels}")
else:
print(f"⚠️ [通道筛选] 任务 {mission_name} 配置中未找到通道信息,显示所有通道")
# 如果配置中没有通道信息,返回所有通道
used_channels = ['通道1', '通道2', '通道3', '通道4']
return used_channels
except Exception as e: except Exception as e:
print(f"[曲线布局] 读取任务配置失败: {e}") print(f"❌ [通道筛选] 获取通道列表失败: {e}")
return [] import traceback
traceback.print_exc()
# 出错时返回所有通道
return ['通道1', '通道2', '通道3', '通道4']
def _updateCurveChannelDisplay(self, selected_channels): def _updateCurveChannelDisplay(self, selected_channels):
"""更新曲线布局中显示的通道""" """更新曲线布局中显示的通道"""
if not hasattr(self, 'channel_widgets_for_curve'): # 🔥 根据当前曲线子布局模式选择要操作的容器
if hasattr(self, '_curve_sub_layout_mode'):
if self._curve_sub_layout_mode == 0:
# 实时检测模式:操作channel_widgets_for_curve
target_widgets = self.channel_widgets_for_curve if hasattr(self, 'channel_widgets_for_curve') else []
target_container = self.curve_channel_container if hasattr(self, 'curve_channel_container') else None
else:
# 历史回放模式:操作history_channel_widgets_for_curve
target_widgets = self.history_channel_widgets_for_curve if hasattr(self, 'history_channel_widgets_for_curve') else []
target_container = self.history_channel_container if hasattr(self, 'history_channel_container') else None
else:
# 默认使用实时检测容器
target_widgets = self.channel_widgets_for_curve if hasattr(self, 'channel_widgets_for_curve') else []
target_container = self.curve_channel_container if hasattr(self, 'curve_channel_container') else None
if not target_widgets:
return return
# 通道名称到索引的映射 # 通道名称到索引的映射
channel_name_to_index = { channel_name_to_index = {
'通道_1': 0, '通道1': 0,
'通道_2': 1, '通道2': 1,
'通道_3': 2, '通道3': 2,
'通道_4': 3 '通道4': 3
} }
# 首先隐藏所有通道 # 首先隐藏所有通道
for wrapper in self.channel_widgets_for_curve: for wrapper in target_widgets:
wrapper.setVisible(False) wrapper.setVisible(False)
# 显示选中的通道 # 显示选中的通道
visible_count = 0 visible_count = 0
for channel_name in selected_channels: for channel_name in selected_channels:
channel_index = channel_name_to_index.get(channel_name) channel_index = channel_name_to_index.get(channel_name)
if channel_index is not None and channel_index < len(self.channel_widgets_for_curve): if channel_index is not None and channel_index < len(target_widgets):
self.channel_widgets_for_curve[channel_index].setVisible(True) target_widgets[channel_index].setVisible(True)
visible_count += 1 visible_count += 1
# 调整容器高度 # 调整容器高度
...@@ -1000,7 +1150,8 @@ class MainWindow( ...@@ -1000,7 +1150,8 @@ class MainWindow(
else: else:
total_height = 100 # 最小高度 total_height = 100 # 最小高度
self.curve_channel_container.setFixedSize(640, total_height) if target_container:
target_container.setFixedSize(640, total_height)
print(f"[曲线布局] 已更新通道显示,显示 {visible_count} 个通道") print(f"[曲线布局] 已更新通道显示,显示 {visible_count} 个通道")
...@@ -1120,7 +1271,7 @@ class MainWindow( ...@@ -1120,7 +1271,7 @@ class MainWindow(
# panel.channelEdited.connect(self.onChannelEdited) # 已在 _connectChannelPanelSignals 中连接 # panel.channelEdited.connect(self.onChannelEdited) # 已在 _connectChannelPanelSignals 中连接
# ========== 通道面板按钮信号 ========== # ========== 通道面板按钮信号 ==========
panel.curveClicked.connect(self.toggleVideoPageMode) # 切换视频页面模式(委托ViewHandler) panel.curveClicked.connect(self._onChannelCurveClicked) # 通道面板查看曲线按钮
# amplifyClicked 信号在 ChannelPanelHandler._connectChannelPanelSignals 中已连接 # amplifyClicked 信号在 ChannelPanelHandler._connectChannelPanelSignals 中已连接
# ========== 曲线面板信号 ========== # ========== 曲线面板信号 ==========
...@@ -1342,6 +1493,18 @@ class MainWindow( ...@@ -1342,6 +1493,18 @@ class MainWindow(
print(f"_installEventFilterRecursive 失败: {e}") print(f"_installEventFilterRecursive 失败: {e}")
return 0 return 0
def _updateChannelColumnColor(self):
"""
更新任务面板中通道列的颜色
当通道的检测状态改变时调用此方法,更新任务面板中对应通道列的背景色
"""
try:
if hasattr(self, '_updateChannelCellColors'):
self._updateChannelCellColors()
except Exception as e:
print(f"⚠️ [更新通道列颜色] 失败: {e}")
def closeEvent(self, event): def closeEvent(self, event):
"""窗口关闭事件""" """窗口关闭事件"""
try: try:
......
...@@ -850,55 +850,22 @@ class MainWindow( ...@@ -850,55 +850,22 @@ class MainWindow(
self.videoLayoutStack.addWidget(layout_widget) self.videoLayoutStack.addWidget(layout_widget)
def _onCurveMissionChanged(self, mission_name): def _onCurveMissionChanged(self, mission_name):
"""曲线任务选择变化,更新显示的通道""" """曲线任务选择变化(基于CSV文件动态显示,不做筛选)"""
# 🔥 不再从任务配置读取通道列表,改为基于CSV文件数量动态显示
# 曲线线程会读取所有CSV文件,这里只需要确保有足够的容器显示
if not mission_name or mission_name == "请选择任务": if not mission_name or mission_name == "请选择任务":
# 隐藏所有通道 print(f"[曲线布局] 未选择任务,隐藏所有通道容器")
self._updateCurveChannelDisplay([]) self._updateCurveChannelDisplay([])
return return
# 获取任务使用的通道列表 # 🔥 显示所有4个通道容器,让曲线系统自动填充
selected_channels = self._getTaskChannels(mission_name) # 实际显示的曲线数量由CSV文件数量决定
all_channels = ['通道1', '通道2', '通道3', '通道4']
print(f"📊 [曲线布局] 任务 {mission_name} 已选择,显示所有通道容器,等待CSV文件加载")
self._updateCurveChannelDisplay(all_channels)
if selected_channels: # 🔥 删除 _getTaskChannels 方法,不再需要从配置文件读取通道列表
print(f"📊 [曲线布局] 任务 {mission_name} 使用通道: {selected_channels}") # 改为显示所有通道容器,由CSV文件动态驱动曲线显示
self._updateCurveChannelDisplay(selected_channels)
else:
print(f"[曲线布局] 任务 {mission_name} 无通道配置")
self._updateCurveChannelDisplay([])
def _getTaskChannels(self, mission_name):
"""从任务配置文件获取使用的通道列表"""
import os
import yaml
try:
# 构建任务配置文件路径
if getattr(sys, 'frozen', False):
project_root = os.path.dirname(sys.executable)
else:
try:
from database.config import get_project_root
project_root = get_project_root()
except ImportError:
project_root = os.getcwd()
config_path = os.path.join(project_root, 'database', 'config', 'mission', f'{mission_name}.yaml')
if not os.path.exists(config_path):
print(f"[曲线布局] 任务配置文件不存在: {config_path}")
return []
# 读取任务配置
with open(config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
# 获取选中的通道列表
selected_channels = config.get('selected_channels', [])
return selected_channels
except Exception as e:
print(f"[曲线布局] 读取任务配置失败: {e}")
return []
def _updateCurveChannelDisplay(self, selected_channels): def _updateCurveChannelDisplay(self, selected_channels):
"""更新曲线布局中显示的通道""" """更新曲线布局中显示的通道"""
...@@ -907,10 +874,10 @@ class MainWindow( ...@@ -907,10 +874,10 @@ class MainWindow(
# 通道名称到索引的映射 # 通道名称到索引的映射
channel_name_to_index = { channel_name_to_index = {
'通道_1': 0, '通道1': 0,
'通道_2': 1, '通道2': 1,
'通道_3': 2, '通道3': 2,
'通道_4': 3 '通道4': 3
} }
# 首先隐藏所有通道 # 首先隐藏所有通道
......
qweqwrfwadfsafa
duaisfdhuahofhaofhoa
yuhaibo
\ No newline at end of file
...@@ -601,16 +601,15 @@ class ChannelPanelHandler: ...@@ -601,16 +601,15 @@ class ChannelPanelHandler:
if channel_str.startswith('channel') and channel_str[7:].isdigit(): if channel_str.startswith('channel') and channel_str[7:].isdigit():
return channel_str return channel_str
if channel_str.startswith('通道_'): # 支持新格式:'通道1', '通道2'(推荐)
suffix = channel_str.split('_', 1)[1] if channel_str.startswith('通道'):
# 移除'通道'前缀,获取数字部分
suffix = channel_str.replace('通道', '').strip()
# 如果包含下划线(旧格式'通道_1'),去掉下划线
suffix = suffix.replace('_', '').strip()
if suffix.isdigit(): if suffix.isdigit():
return f"channel{suffix}" return f"channel{suffix}"
if channel_str.startswith('通道'):
digits = ''.join(ch for ch in channel_str if ch.isdigit())
if digits:
return f"channel{digits}"
return None return None
def _getChannelConfigFromFile(self, channel_id): def _getChannelConfigFromFile(self, channel_id):
......
...@@ -233,6 +233,9 @@ class CurvePanelHandler: ...@@ -233,6 +233,9 @@ class CurvePanelHandler:
# - 'history':历史回放模式,加载所有数据点,不做限制 # - 'history':历史回放模式,加载所有数据点,不做限制
self.curve_load_mode = 'realtime' self.curve_load_mode = 'realtime'
# 🔥 历史数据加载标志(避免重复加载)
self._history_data_loaded = False
# UI组件引用 # UI组件引用
self.curve_panel = None self.curve_panel = None
...@@ -559,25 +562,29 @@ class CurvePanelHandler: ...@@ -559,25 +562,29 @@ class CurvePanelHandler:
folder_path = None folder_path = None
mission_name = None mission_name = None
# 更新 current_mission 全局变量(会自动同步到配置文件)
if hasattr(self, 'current_mission'):
self.current_mission = folder_path
# 🔥 同步任务信息到所有通道面板(从current_mission读取)
if hasattr(self, 'syncTaskInfoToAllPanels'):
self.syncTaskInfoToAllPanels()
# 清除当前数据 # 清除当前数据
self.clearAllData() self.clearAllData()
# 加载新任务的历史数据 # 🔥 只在曲线模式布局时才启动曲线线程
if folder_path and os.path.exists(folder_path): # 检查是否在曲线模式(_video_layout_mode == 1)
print(f"📊 [曲线面板Handler] 开始加载任务历史数据: {folder_path}") is_curve_mode = hasattr(self, '_video_layout_mode') and self._video_layout_mode == 1
success = self.loadHistoricalCurveData(folder_path)
if success: if not is_curve_mode:
print(f"✅ [曲线面板Handler] 历史数据加载成功") print(f"🔄 [曲线面板Handler] 不在曲线模式,跳过启动曲线线程")
else: return
print(f"⚠️ [曲线面板Handler] 历史数据加载失败或无数据")
# 🔥 重新启动曲线线程以监控新任务的CSV文件变化
if hasattr(self, 'thread_manager') and folder_path:
# 先停止旧的曲线线程
self.thread_manager.stop_all_curve_threads()
print(f"🔄 [曲线面板Handler] 已停止旧的曲线线程")
# 启动新的曲线线程监控新任务
import time
time.sleep(0.1) # 短暂延迟确保线程完全停止
self.thread_manager.start_all_curve_threads()
print(f"🚀 [曲线面板Handler] 已重新启动曲线线程,监控任务: {folder_path}")
print(f"📊 [曲线面板Handler] 曲线线程将自动加载历史数据并监控新数据")
else: else:
print(f"🔄 [曲线面板Handler] 清空曲线数据(无有效任务路径)") print(f"🔄 [曲线面板Handler] 清空曲线数据(无有效任务路径)")
...@@ -840,7 +847,7 @@ class CurvePanelHandler: ...@@ -840,7 +847,7 @@ class CurvePanelHandler:
- 只需要一次性读取当前任务文件夹下的所有CSV文件并显示 - 只需要一次性读取当前任务文件夹下的所有CSV文件并显示
Args: Args:
data_directory: 数据目录路径(通常传入 current_mission 路径,如果为None则使用save_base_dir data_directory: 数据目录路径(通常从 curvemission 获取
Returns: Returns:
bool: 是否成功启动加载任务 bool: 是否成功启动加载任务
...@@ -859,7 +866,27 @@ class CurvePanelHandler: ...@@ -859,7 +866,27 @@ class CurvePanelHandler:
if not csv_files: if not csv_files:
return False return False
# 创建进度对话框 # 🔥 检查是否需要显示进度条
# 触发条件:CSV文件数量 > 1 或 单个CSV文件大小 > 8MB
show_progress = False
if len(csv_files) > 1:
# 条件1:多个CSV文件
show_progress = True
print(f"📊 [进度条触发] CSV文件数量: {len(csv_files)} > 1")
else:
# 条件2:单个CSV文件大小超过8MB
csv_path = os.path.join(data_directory, csv_files[0])
file_size_mb = os.path.getsize(csv_path) / (1024 * 1024)
if file_size_mb > 8:
show_progress = True
print(f"📊 [进度条触发] 单个CSV文件大小: {file_size_mb:.2f}MB > 8MB")
else:
print(f"ℹ️ [跳过进度条] 单个CSV文件 ({file_size_mb:.2f}MB),直接加载")
# 创建进度对话框(仅在需要时)
progress_dialog = None
if show_progress:
progress_dialog = QtWidgets.QProgressDialog( progress_dialog = QtWidgets.QProgressDialog(
"正在加载曲线数据...", "正在加载曲线数据...",
"取消", "取消",
...@@ -867,14 +894,21 @@ class CurvePanelHandler: ...@@ -867,14 +894,21 @@ class CurvePanelHandler:
100 # 使用百分比进度 100 # 使用百分比进度
) )
progress_dialog.setWindowTitle("曲线数据加载进度") progress_dialog.setWindowTitle("曲线数据加载进度")
progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # 🔥 使用应用程序模态,确保进度条显示在最前面
progress_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
progress_dialog.setMinimumWidth(400) progress_dialog.setMinimumWidth(400)
# 🔥 设置最小显示时间(毫秒),避免闪烁
progress_dialog.setMinimumDuration(0) # 立即显示
progress_dialog.setWindowIcon(newIcon("动态曲线")) progress_dialog.setWindowIcon(newIcon("动态曲线"))
progress_dialog.setWindowFlags( progress_dialog.setWindowFlags(
progress_dialog.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint progress_dialog.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint
) )
progress_dialog.setCancelButton(None) progress_dialog.setCancelButton(None)
progress_dialog.setValue(0) # 设置初始值
progress_dialog.show() progress_dialog.show()
# 🔥 强制刷新UI,确保进度条立即显示
QtWidgets.QApplication.processEvents()
print(f"✅ [进度条] 已显示进度对话框")
# 创建并启动后台加载线程 # 创建并启动后台加载线程
self._load_thread = CurveDataLoadThread( self._load_thread = CurveDataLoadThread(
...@@ -931,10 +965,19 @@ class CurvePanelHandler: ...@@ -931,10 +965,19 @@ class CurvePanelHandler:
def _onLoadFinished(self, progress_dialog, success, count): def _onLoadFinished(self, progress_dialog, success, count):
"""处理加载完成""" """处理加载完成"""
if progress_dialog: if progress_dialog:
progress_dialog.close() # 🔥 设置进度为100%,确保用户看到完成状态
progress_dialog.setValue(100)
QtWidgets.QApplication.processEvents()
# 🔥 延迟关闭进度条,确保用户能看到(至少显示500ms)
from PyQt5.QtCore import QTimer
QTimer.singleShot(500, progress_dialog.close)
print(f"✅ [进度条] 将在500ms后关闭")
if success: if success:
print(f"✅ [曲线数据加载] 成功加载 {count} 个文件") print(f"✅ [曲线数据加载] 成功加载 {count} 个文件")
# 🔥 设置历史数据已加载标志
self._history_data_loaded = True
else: else:
print(f"⚠️ [曲线数据加载] 加载失败") print(f"⚠️ [曲线数据加载] 加载失败")
...@@ -1114,6 +1157,8 @@ class CurvePanelHandler: ...@@ -1114,6 +1157,8 @@ class CurvePanelHandler:
mode = 'realtime' mode = 'realtime'
self.curve_load_mode = mode self.curve_load_mode = mode
# 🔥 切换模式时重置历史数据加载标志
self._history_data_loaded = False
print(f"✅ [曲线加载模式] 已切换到: {mode}") print(f"✅ [曲线加载模式] 已切换到: {mode}")
def getCurveLoadMode(self) -> str: def getCurveLoadMode(self) -> str:
...@@ -1125,3 +1170,11 @@ class CurvePanelHandler: ...@@ -1125,3 +1170,11 @@ class CurvePanelHandler:
""" """
return self.curve_load_mode return self.curve_load_mode
def isHistoryDataLoaded(self) -> bool:
"""
检查历史数据是否已加载
Returns:
bool: True 如果已加载,False 否则
"""
return self._history_data_loaded
\ No newline at end of file
...@@ -611,9 +611,7 @@ class LiquidDetectionEngine: ...@@ -611,9 +611,7 @@ class LiquidDetectionEngine:
masks = mission_result.masks.data.cpu().numpy() > 0.5 masks = mission_result.masks.data.cpu().numpy() > 0.5
classes = mission_result.boxes.cls.cpu().numpy().astype(int) classes = mission_result.boxes.cls.cpu().numpy().astype(int)
confidences = mission_result.boxes.conf.cpu().numpy() confidences = mission_result.boxes.conf.cpu().numpy()
print(f"[检测-目标{idx}] YOLO检测到 {len(masks)} 个对象")
else: else:
print(f"[检测-目标{idx}] ⚠️ YOLO未检测到任何mask")
return None return None
# 收集所有mask信息 # 收集所有mask信息
...@@ -621,7 +619,6 @@ class LiquidDetectionEngine: ...@@ -621,7 +619,6 @@ class LiquidDetectionEngine:
for i in range(len(masks)): for i in range(len(masks)):
class_name = self.model.names[classes[i]] class_name = self.model.names[classes[i]]
conf = confidences[i] conf = confidences[i]
print(f"[检测-目标{idx}] 对象{i+1}: {class_name} (置信度: {conf:.3f})")
if confidences[i] >= 0.5: if confidences[i] >= 0.5:
resized_mask = cv2.resize( resized_mask = cv2.resize(
...@@ -630,10 +627,7 @@ class LiquidDetectionEngine: ...@@ -630,10 +627,7 @@ class LiquidDetectionEngine:
) > 0.5 ) > 0.5
all_masks_info.append((resized_mask, class_name, confidences[i])) all_masks_info.append((resized_mask, class_name, confidences[i]))
print(f"[检测-目标{idx}] 收集到 {len(all_masks_info)} 个有效mask (置信度>=0.5)")
if len(all_masks_info) == 0: if len(all_masks_info) == 0:
print(f"[检测-目标{idx}] ⚠️ 没有置信度>=0.5的对象,无法计算液位")
return None return None
# ️ 关键修复:将原图坐标转换为裁剪图像坐标 # ️ 关键修复:将原图坐标转换为裁剪图像坐标
...@@ -656,11 +650,6 @@ class LiquidDetectionEngine: ...@@ -656,11 +650,6 @@ class LiquidDetectionEngine:
idx idx
) )
if liquid_height is None:
print(f"[检测-目标{idx}] ⚠️ _enhanced_liquid_detection返回None,无法确定液位")
else:
print(f"[检测-目标{idx}] ✅ 检测到液位: {liquid_height:.2f}mm")
return liquid_height return liquid_height
except Exception as e: except Exception as e:
...@@ -684,14 +673,6 @@ class LiquidDetectionEngine: ...@@ -684,14 +673,6 @@ class LiquidDetectionEngine:
""" """
pixel_per_mm = container_pixel_height / container_height_mm pixel_per_mm = container_pixel_height / container_height_mm
print(f"\n[液位分析-目标{idx}] ========== 开始分析 ==========")
print(f"[液位分析-目标{idx}] 输入参数:")
print(f" - all_masks_info数量: {len(all_masks_info)}")
print(f" - container_bottom: {container_bottom}px (裁剪图像坐标)")
print(f" - container_pixel_height: {container_pixel_height}px")
print(f" - container_height_mm: {container_height_mm}mm")
print(f" - pixel_per_mm: {pixel_per_mm:.3f}px/mm")
# 分离不同类别的mask # 分离不同类别的mask
liquid_masks = [] liquid_masks = []
foam_masks = [] foam_masks = []
...@@ -705,14 +686,8 @@ class LiquidDetectionEngine: ...@@ -705,14 +686,8 @@ class LiquidDetectionEngine:
elif class_name == 'air': elif class_name == 'air':
air_masks.append(mask) air_masks.append(mask)
print(f"[液位分析-目标{idx}] mask分类:")
print(f" - liquid: {len(liquid_masks)}个")
print(f" - foam: {len(foam_masks)}个")
print(f" - air: {len(air_masks)}个")
# 方法1:直接liquid检测(优先) # 方法1:直接liquid检测(优先)
if liquid_masks: if liquid_masks:
print(f"[液位分析-目标{idx}] 使用方法1: 直接liquid检测")
# 找到最上层的液体mask # 找到最上层的液体mask
topmost_y = float('inf') topmost_y = float('inf')
for i, mask in enumerate(liquid_masks): for i, mask in enumerate(liquid_masks):
...@@ -727,27 +702,9 @@ class LiquidDetectionEngine: ...@@ -727,27 +702,9 @@ class LiquidDetectionEngine:
liquid_height_px = container_bottom - topmost_y liquid_height_px = container_bottom - topmost_y
liquid_height_mm = liquid_height_px / pixel_per_mm liquid_height_mm = liquid_height_px / pixel_per_mm
print(f"[液位分析-目标{idx}] ========== 计算结果 ==========")
print(f"[液位分析-目标{idx}] 坐标信息(裁剪图像坐标系):")
print(f" - 液面最上层y: {topmost_y}px")
print(f" - 容器底部y: {container_bottom}px")
print(f"[液位分析-目标{idx}] 计算过程:")
print(f" - 液位像素高度 = {container_bottom}px - {topmost_y}px = {liquid_height_px}px")
print(f" - 像素/毫米比例 = {container_pixel_height}px / {container_height_mm}mm = {pixel_per_mm:.3f}px/mm")
print(f" - 液位毫米高度 = {liquid_height_px}px / {pixel_per_mm:.3f}px/mm = {liquid_height_mm:.2f}mm")
print(f"[液位分析-目标{idx}] 边界检查:")
print(f" - 原始值: {liquid_height_mm:.2f}mm")
print(f" - 容器总高度: {container_height_mm}mm")
print(f" - 最终返回值: {max(0, min(liquid_height_mm, container_height_mm)):.2f}mm")
print(f"[液位分析-目标{idx}] ========== 计算完成 ==========\n")
return max(0, min(liquid_height_mm, container_height_mm)) return max(0, min(liquid_height_mm, container_height_mm))
else:
print(f"[液位分析-目标{idx}] ⚠️ liquid mask存在但无法找到有效的顶部y坐标")
# 方法2:foam边界分析(备选)- 连续3帧未检测到liquid时启用 # 方法2:foam边界分析(备选)- 连续3帧未检测到liquid时启用
print(f"[液位分析-目标{idx}] 方法1失败,尝试方法2: foam边界分析")
print(f"[液位分析-目标{idx}] no_liquid_count={self.no_liquid_count[idx]}, 需要>=3才启用foam分析")
if self.no_liquid_count[idx] >= 3: if self.no_liquid_count[idx] >= 3:
if len(foam_masks) >= 2: if len(foam_masks) >= 2:
...@@ -777,7 +734,6 @@ class LiquidDetectionEngine: ...@@ -777,7 +734,6 @@ class LiquidDetectionEngine:
liquid_height_mm = liquid_height_px / pixel_per_mm liquid_height_mm = liquid_height_px / pixel_per_mm
return max(0, min(liquid_height_mm, container_height_mm)) return max(0, min(liquid_height_mm, container_height_mm))
print(f"[液位分析-目标{idx}] ⚠️ 所有方法都无法确定液位,返回None")
return None return None
def _apply_kalman_filter(self, observation, idx, container_height_mm): def _apply_kalman_filter(self, observation, idx, container_height_mm):
......
...@@ -70,7 +70,7 @@ class MissionPanelHandler: ...@@ -70,7 +70,7 @@ class MissionPanelHandler:
def _handleButtonClicked(self, row_index, column_index): def _handleButtonClicked(self, row_index, column_index):
""" """
处理任务面板按钮点击 处理任务面板按钮点击(来源1)
Args: Args:
row_index: 行索引 row_index: 行索引
...@@ -83,16 +83,37 @@ class MissionPanelHandler: ...@@ -83,16 +83,37 @@ class MissionPanelHandler:
# 如果点击的是曲线按钮 # 如果点击的是曲线按钮
if column_index == CURVE_BUTTON_COLUMN: if column_index == CURVE_BUTTON_COLUMN:
print(f"📊 曲线按钮被点击 (行: {row_index})") print(f"📊 [任务面板] 曲线按钮被点击 (行: {row_index})")
# 获取该行的任务信息
if hasattr(self, 'mission_panel') and self.mission_panel:
# 从表格获取任务编号和任务名称
task_id_item = self.mission_panel.table.item(row_index, 0) # 任务编号列
task_name_item = self.mission_panel.table.item(row_index, 1) # 任务名称列
if task_id_item and task_name_item:
task_id = task_id_item.text()
task_name = task_name_item.text()
# 组合任务文件夹名称
mission_folder_name = f"{task_id}_{task_name}"
print(f"📁 [任务面板] 任务文件夹名称: {mission_folder_name}")
# 设置 curvemission 的值
if hasattr(self, 'curvePanel') and self.curvePanel:
success = self.curvePanel.setMissionFromTaskName(mission_folder_name)
if success:
print(f"✅ [任务面板] 已设置 curvemission 为: {mission_folder_name}")
else:
print(f"⚠️ [任务面板] 设置 curvemission 失败: {mission_folder_name}")
# 调用ViewHandler的toggleVideoPageMode方法切换布局 # 调用ViewHandler的toggleVideoPageMode方法切换布局
if hasattr(self, 'toggleVideoPageMode'): if hasattr(self, 'toggleVideoPageMode'):
self.toggleVideoPageMode() self.toggleVideoPageMode()
else: else:
print("toggleVideoPageMode 方法不存在") print("⚠️ toggleVideoPageMode 方法不存在")
except Exception as e: except Exception as e:
print(f"处理按钮点击失败: {e}") print(f"处理按钮点击失败: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
...@@ -188,7 +209,7 @@ class MissionPanelHandler: ...@@ -188,7 +209,7 @@ class MissionPanelHandler:
task_info: 任务信息字典,包含: task_info: 任务信息字典,包含:
- task_id: 任务编号 - task_id: 任务编号
- task_name: 任务名称 - task_name: 任务名称
- selected_channels: 选中的通道列表(如 ['通道_1', '通道_2']) - selected_channels: 选中的通道列表(如 ['通道1', '通道2'])
""" """
try: try:
task_id = task_info.get('task_id', '') task_id = task_info.get('task_id', '')
...@@ -292,17 +313,20 @@ class MissionPanelHandler: ...@@ -292,17 +313,20 @@ class MissionPanelHandler:
clicked_button = msg_box.clickedButton() clicked_button = msg_box.clickedButton()
if clicked_button == btn_cancel: if clicked_button == btn_cancel:
print("用户取消任务重新分配") print("❌ [任务分配] 用户取消任务重新分配")
# 🔥 通知Widget取消任务分配,不高亮行
if self.mission_panel:
self.mission_panel.cancelTaskAssignment()
return return
print("用户确认重新分配任务") print("✅ [任务分配] 用户确认重新分配任务")
# 🔥 遍历选中的通道,只更新这些通道的任务标签(不影响其他通道) # 🔥 遍历选中的通道,只更新这些通道的任务标签(不影响其他通道)
for channel_key in selected_channels: for channel_key in selected_channels:
# 将 '通道_1' 转换为 'channel1' # 将 '通道1' 转换为 'channel1'
# 格式:'通道_X' -> 'channelX' # 格式:'通道X' -> 'channelX'
if channel_key.startswith('通道_'): if channel_key.startswith('通道'):
channel_num = channel_key.split('_')[1] channel_num = channel_key.replace('通道', '').strip()
channel_id = f'channel{channel_num}' channel_id = f'channel{channel_num}'
# 🔥 直接更新通道任务标签(使用新的变量名方式) # 🔥 直接更新通道任务标签(使用新的变量名方式)
...@@ -313,6 +337,10 @@ class MissionPanelHandler: ...@@ -313,6 +337,10 @@ class MissionPanelHandler:
else: else:
print(f"无法解析通道ID: {channel_key}") print(f"无法解析通道ID: {channel_key}")
# 🔥 确认任务分配,高亮选中的行
if self.mission_panel:
self.mission_panel.confirmTaskAssignment()
# 🔥 恢复自动状态刷新:任务分配后刷新所有任务状态 # 🔥 恢复自动状态刷新:任务分配后刷新所有任务状态
self._refreshAllTaskStatus() self._refreshAllTaskStatus()
print(f"✅ [状态刷新] 已刷新所有任务状态显示") print(f"✅ [状态刷新] 已刷新所有任务状态显示")
...@@ -751,7 +779,15 @@ class MissionPanelHandler: ...@@ -751,7 +779,15 @@ class MissionPanelHandler:
return False return False
def _handleRemoveTask(self, row_index): def _handleRemoveTask(self, row_index):
"""处理删除任务请求""" """
处理删除任务请求
注意:此方法由Widget的右键菜单调用,Widget已经显示了确认对话框
所以这里不需要再次确认,直接执行删除操作
Args:
row_index: 要删除的任务行索引
"""
if not self.mission_panel: if not self.mission_panel:
return return
...@@ -762,18 +798,24 @@ class MissionPanelHandler: ...@@ -762,18 +798,24 @@ class MissionPanelHandler:
task_id = row_data[0] if len(row_data) > 0 else '' task_id = row_data[0] if len(row_data) > 0 else ''
task_name = row_data[1] if len(row_data) > 1 else f"任务 {row_index}" task_name = row_data[1] if len(row_data) > 1 else f"任务 {row_index}"
# 显示确认对话框 try:
if self.mission_panel.showConfirmDialog(
"确认删除", f"确定要删除任务 '{task_name}' 吗?\n这将同时删除配置文件和结果文件夹!"
):
# 🔥 删除结果文件夹 # 🔥 删除结果文件夹
self._deletemission_resultFolder(task_id, task_name) self._deletemission_resultFolder(task_id, task_name)
# 删除对应的YAML配置文件 # 删除对应的YAML配置文件
self._deleteMissionConfig(task_id, task_name) self._deleteMissionConfig(task_id, task_name)
# 从表格中删除 # 🔥 重新加载任务列表(而不是只删除单行)
self.mission_panel.removeTaskRow(row_index) # 这样可以确保分页数据和UI完全同步
self._loadAllMissions()
print(f"✅ [任务删除] 成功删除任务: {task_id}_{task_name}")
print(f"✅ [任务删除] 已重新加载任务列表")
except Exception as e:
print(f"❌ [任务删除] 删除任务失败: {e}")
import traceback
traceback.print_exc()
def _handleClearTable(self): def _handleClearTable(self):
"""处理清空表格请求""" """处理清空表格请求"""
...@@ -815,6 +857,10 @@ class MissionPanelHandler: ...@@ -815,6 +857,10 @@ class MissionPanelHandler:
在程序启动时自动调用,恢复之前保存的任务 在程序启动时自动调用,恢复之前保存的任务
""" """
try: try:
# 🔥 先清空现有任务列表,避免重复添加
if self.mission_panel:
self.mission_panel.clearTable()
# 获取任务配置路径 # 获取任务配置路径
mission_dir = self._getMissionConfigPath() mission_dir = self._getMissionConfigPath()
if not mission_dir or not os.path.exists(mission_dir): if not mission_dir or not os.path.exists(mission_dir):
...@@ -875,11 +921,28 @@ class MissionPanelHandler: ...@@ -875,11 +921,28 @@ class MissionPanelHandler:
is_task_in_use = self._isTaskInUse(task_folder_name) is_task_in_use = self._isTaskInUse(task_folder_name)
actual_status = "已配置" if is_task_in_use else "未配置" actual_status = "已配置" if is_task_in_use else "未配置"
# 🔥 将通道列表拆分为4个独立的列
selected_channels = task_info.get('selected_channels', [])
channel_cols = ['', '', '', ''] # 4个通道列,默认为空
for ch in selected_channels:
# 提取通道编号(如"通道1" -> 0, "通道2" -> 1)
if ch.startswith('通道'):
try:
ch_num = int(ch.replace('通道', '')) - 1
if 0 <= ch_num < 4:
# 🔥 显示完整的通道名称(如 "通道1", "通道2", "通道3", "通道4")
channel_cols[ch_num] = '通道' + str(ch_num + 1)
except ValueError:
pass
row_data = [ row_data = [
task_id, task_id,
task_name, task_name,
actual_status, # 使用动态计算的状态 actual_status, # 使用动态计算的状态
', '.join(task_info.get('selected_channels', [])), channel_cols[0], # 通道1
channel_cols[1], # 通道2
channel_cols[2], # 通道3
channel_cols[3], # 通道4
'' # 曲线列 '' # 曲线列
] ]
# update_display=False:不立即刷新 # update_display=False:不立即刷新
...@@ -889,11 +952,131 @@ class MissionPanelHandler: ...@@ -889,11 +952,131 @@ class MissionPanelHandler:
# 第三步:统一刷新显示 # 第三步:统一刷新显示
self.mission_panel.refreshDisplay() self.mission_panel.refreshDisplay()
# 🔥 第四步:更新通道列的背景色(根据检测状态)
self._updateChannelCellColors()
except Exception as e: except Exception as e:
print(f" 加载任务配置失败: {e}") print(f" 加载任务配置失败: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def _updateChannelCellColors(self):
"""
更新任务面板中通道列的文本颜色
只改变当前正在运行该通道任务的那一行的通道标签颜色:
- 检测中(detection_flag=True)且是该通道当前任务:绿色文本
- 其他情况:默认黑色文本
"""
try:
print(f"\n{'='*60}")
print(f"🔍 [调试] 开始更新通道列颜色")
print(f"{'='*60}")
# 获取所有任务的行数据
if not hasattr(self.mission_panel, '_all_rows_data'):
print(f"⚠️ [调试] mission_panel 没有 _all_rows_data 属性")
return
all_rows = self.mission_panel._all_rows_data
print(f"📊 [调试] 任务面板总行数: {len(all_rows)}")
# 遍历4个通道,获取每个通道当前运行的任务
channel_current_missions = {}
for ch_idx in range(4):
channel_id = f'channel{ch_idx + 1}'
mission_var_name = f'{channel_id}mission'
# 从主窗口获取当前通道的任务
if hasattr(self, mission_var_name):
current_mission_obj = getattr(self, mission_var_name, None)
if current_mission_obj:
# 🔥 如果是QLabel对象,获取其文本内容
if hasattr(current_mission_obj, 'text'):
current_mission = current_mission_obj.text()
else:
current_mission = str(current_mission_obj)
channel_current_missions[ch_idx] = current_mission
print(f"📝 [调试] {mission_var_name} = {current_mission} (类型: {type(current_mission_obj).__name__})")
else:
print(f"⚪ [调试] {mission_var_name} = None")
else:
print(f"❌ [调试] 没有 {mission_var_name} 属性")
print(f"📋 [调试] 当前运行的任务: {channel_current_missions}")
# 遍历每一行任务
for row_idx, row_info in enumerate(all_rows):
user_data = row_info.get('user_data', {})
selected_channels = user_data.get('selected_channels', [])
task_folder_name = user_data.get('mission_result_folder_path', '')
# 提取任务名称(如 "1_2")
if task_folder_name:
task_name = os.path.basename(task_folder_name)
else:
task_name = None
print(f"\n🔹 [调试] 行{row_idx}: 任务={task_name}, 使用通道={selected_channels}")
# 遍历4个通道列
for ch_idx in range(4):
channel_name = f'通道{ch_idx + 1}'
col_idx = 3 + ch_idx # 通道列从第3列开始
# 检查该通道是否被任务使用
if channel_name in selected_channels:
# 检查该通道是否正在检测
channel_id = f'channel{ch_idx + 1}'
is_detecting = self._isChannelDetecting(channel_id)
# 检查该任务是否是该通道当前运行的任务
current_mission_for_channel = channel_current_missions.get(ch_idx)
is_current_mission = (current_mission_for_channel == task_name)
print(f" 🔸 {channel_name}: 检测中={is_detecting}, 当前任务={current_mission_for_channel}, 是当前任务={is_current_mission}")
if is_detecting and is_current_mission:
# 检测中且是当前任务:设置文本为绿色
self.mission_panel.setCellTextColor(row_idx, col_idx, '#00AA00')
print(f" ✅ [设置绿色] 行{row_idx} 列{col_idx} {channel_name}")
else:
# 其他情况:恢复默认黑色
self.mission_panel.setCellTextColor(row_idx, col_idx, '#000000')
print(f" ⚫ [设置黑色] 行{row_idx} 列{col_idx} {channel_name} (检测={is_detecting}, 当前={is_current_mission})")
print(f"{'='*60}")
print(f"✅ [调试] 通道列颜色更新完成")
print(f"{'='*60}\n")
except Exception as e:
print(f"⚠️ [更新通道颜色] 失败: {e}")
import traceback
traceback.print_exc()
def _isChannelDetecting(self, channel_id):
"""
检查指定通道是否正在检测
Args:
channel_id: 通道ID(如'channel1')
Returns:
bool: True表示正在检测,False表示未检测
"""
try:
# 🔥 直接从主窗口获取 channelXdetect 变量
detect_var_name = f'{channel_id}detect'
if hasattr(self, detect_var_name):
is_detecting = getattr(self, detect_var_name, False)
print(f"🔍 [检测状态] {channel_id}: {detect_var_name}={is_detecting}")
return is_detecting
return False
except Exception as e:
print(f"⚠️ [检查检测状态] {channel_id} 失败: {e}")
return False
def _deleteMissionConfig(self, task_id, task_name): def _deleteMissionConfig(self, task_id, task_name):
""" """
删除单个任务的YAML配置文件 删除单个任务的YAML配置文件
...@@ -1329,7 +1512,7 @@ class MissionPanelHandler: ...@@ -1329,7 +1512,7 @@ class MissionPanelHandler:
检查选中的通道是否已有任务(且不是当前要分配的任务) 检查选中的通道是否已有任务(且不是当前要分配的任务)
Args: Args:
selected_channels: 选中的通道列表(如 ['通道_1', '通道_2']) selected_channels: 选中的通道列表(如 ['通道1', '通道2'])
new_task_name: 新任务名称(如 '1_1') new_task_name: 新任务名称(如 '1_1')
Returns: Returns:
...@@ -1340,11 +1523,11 @@ class MissionPanelHandler: ...@@ -1340,11 +1523,11 @@ class MissionPanelHandler:
try: try:
for channel_key in selected_channels: for channel_key in selected_channels:
# 将 '通道_1' 转换为 'channel1' # 将 '通道1' 转换为 'channel1'
if not channel_key.startswith('通道_'): if not channel_key.startswith('通道'):
continue continue
channel_num = channel_key.split('_')[1] channel_num = channel_key.replace('通道', '').strip()
channel_id = f'channel{channel_num}' channel_id = f'channel{channel_num}'
# 获取通道当前的任务信息 # 获取通道当前的任务信息
...@@ -1466,7 +1649,6 @@ class MissionPanelHandler: ...@@ -1466,7 +1649,6 @@ class MissionPanelHandler:
mission_label = getattr(self, mission_var_name) mission_label = getattr(self, mission_var_name)
current_task = mission_label.text() current_task = mission_label.text()
if current_task == task_folder_name: if current_task == task_folder_name:
print(f"🔍 [状态检查] 任务 {task_folder_name} 被 {channel_id} 使用")
return True return True
# 方法2:检查通道面板内存(备用) # 方法2:检查通道面板内存(备用)
...@@ -1475,10 +1657,8 @@ class MissionPanelHandler: ...@@ -1475,10 +1657,8 @@ class MissionPanelHandler:
if panel and hasattr(panel, 'getTaskInfo'): if panel and hasattr(panel, 'getTaskInfo'):
panel_task = panel.getTaskInfo() panel_task = panel.getTaskInfo()
if panel_task == task_folder_name: if panel_task == task_folder_name:
print(f"🔍 [状态检查] 任务 {task_folder_name} 被 {channel_id} 面板使用")
return True return True
print(f"🔍 [状态检查] 任务 {task_folder_name} 未被任何通道使用")
return False return False
except Exception as e: except Exception as e:
...@@ -1732,80 +1912,87 @@ class MissionPanelHandler: ...@@ -1732,80 +1912,87 @@ class MissionPanelHandler:
def _updateChannelColumnColor(self): def _updateChannelColumnColor(self):
""" """
🔥 根据通道检测状态更新任务面板中通道列的颜色和状态列 🔥 根据通道检测状态更新任务面板中状态列
当 channelXdetect 为 True 时,对应通道的文字显示为绿色
当 channelXdetect 为 False 时,对应通道的文字显示为默认颜色
当任务行的所有通道都在检测时,状态列显示"检测中"并用绿色显示 只更新当前通道正在执行的任务行:
- 获取每个通道当前执行的任务(从channelXmission标签)
- 只检查这些任务行,而不是所有使用该通道的任务行
- 当任务的所有通道都在检测时,状态列显示"检测中"(绿色)
""" """
try: try:
if not hasattr(self, 'mission_panel'): if not hasattr(self, 'mission_panel'):
print(f"⚠️ [通道列颜色] mission_panel不存在,退出")
return return
table = self.mission_panel.table table = self.mission_panel.table
# 遍历所有行 # 🔥 第一步:收集所有通道当前正在执行的任务
active_tasks = set() # 存储正在执行的任务名称
channel_task_map = {} # 通道 -> 任务映射
for channel_num in range(1, 5):
channel_id = f'channel{channel_num}'
mission_var_name = f'{channel_id}mission'
if hasattr(self, mission_var_name):
mission_label = getattr(self, mission_var_name)
current_task = mission_label.text()
if current_task and current_task != "未分配任务":
active_tasks.add(current_task)
channel_task_map[channel_id] = current_task
# 🔥 第二步:遍历所有任务行,只更新正在执行的任务
for row in range(table.rowCount()): for row in range(table.rowCount()):
# 获取通道列(第3列,索引为3) task_id_item = table.item(row, 0)
task_name_item = table.item(row, 1)
status_item = table.item(row, 2)
channel_item = table.item(row, 3) channel_item = table.item(row, 3)
if not channel_item:
continue
channel_text = channel_item.text() if not (task_id_item and task_name_item and status_item):
if not channel_text:
continue continue
# 解析通道文本,可能是 "通道_1, 通道_2" 的格式 # 获取任务文件夹名称
channels = [ch.strip() for ch in channel_text.split(',')] task_folder_name = f"{task_id_item.text()}_{task_name_item.text()}"
# 🔥 只处理正在执行的任务
if task_folder_name in active_tasks:
# 解析该任务使用的通道
channel_text = channel_item.text() if channel_item else ""
channels = [ch.strip() for ch in channel_text.split(',')] if channel_text else []
# 检查通道检测状态 # 检查该任务使用的所有通道是否都在检测
detecting_channels = []
all_channels_detecting = True all_channels_detecting = True
has_detecting_channels = False
for channel_key in channels: for channel_key in channels:
# 将 '通道_1' 转换为 'channel1' if channel_key.startswith('通道'):
if channel_key.startswith('通道_'): channel_num = channel_key.replace('通道', '').strip()
channel_num = channel_key.split('_')[1]
channel_id = f'channel{channel_num}' channel_id = f'channel{channel_num}'
# 检查该通道是否正在执行这个任务
if channel_task_map.get(channel_id) == task_folder_name:
# 检查该通道的检测状态 # 检查该通道的检测状态
detect_var_name = f'{channel_id}detect' detect_var_name = f'{channel_id}detect'
if hasattr(self, detect_var_name): if hasattr(self, detect_var_name):
is_detecting = getattr(self, detect_var_name) is_detecting = getattr(self, detect_var_name)
if is_detecting: if is_detecting:
detecting_channels.append(channel_key) has_detecting_channels = True
else: else:
all_channels_detecting = False all_channels_detecting = False
else: else:
all_channels_detecting = False all_channels_detecting = False
# 🔥 更新通道列颜色 # 更新状态列
if detecting_channels: if has_detecting_channels and all_channels_detecting:
# 有通道在检测:绿色 # 所有执行该任务的通道都在检测:显示"检测中",绿色
channel_item.setForeground(QtGui.QColor(0, 128, 0)) # 绿色
print(f"🟢 [通道列颜色] 行{row}通道列设置为绿色(检测中)")
else:
# 没有通道在检测:默认颜色(黑色)
channel_item.setForeground(QtGui.QColor(0, 0, 0)) # 黑色
print(f"⚫ [通道列颜色] 行{row}通道列设置为黑色(未检测)")
# 🔥 更新状态列(第2列,索引为2)
status_item = table.item(row, 2)
if status_item:
if all_channels_detecting and len(detecting_channels) == len(channels):
# 所有通道都在检测:显示"检测中",绿色
status_item.setText("检测中") status_item.setText("检测中")
status_item.setForeground(QtGui.QColor(0, 128, 0)) # 绿色 status_item.setForeground(QtGui.QColor(0, 128, 0)) # 绿色
print(f"🟢 [状态列颜色] 行{row}状态列设置为检测中(绿色)")
else: else:
# 有通道未检测或没有通道检测:显示"已配置"或"未配置",黑色 # 有通道未检测:显示"已配置"
# 检查是否有任何通道在使用该任务 status_item.setText("已配置")
task_id_item = table.item(row, 0) status_item.setForeground(QtGui.QColor(0, 0, 0)) # 黑色
task_name_item = table.item(row, 1) else:
if task_id_item and task_name_item: # 不是正在执行的任务,检查是否被配置
task_folder_name = f"{task_id_item.text()}_{task_name_item.text()}"
is_task_in_use = self._isTaskInUse(task_folder_name) is_task_in_use = self._isTaskInUse(task_folder_name)
status_text = "已配置" if is_task_in_use else "未配置" status_text = "已配置" if is_task_in_use else "未配置"
status_item.setText(status_text) status_item.setText(status_text)
...@@ -1813,15 +2000,11 @@ class MissionPanelHandler: ...@@ -1813,15 +2000,11 @@ class MissionPanelHandler:
# 设置颜色:已配置为黑色,未配置为灰色 # 设置颜色:已配置为黑色,未配置为灰色
if status_text == "已配置": if status_text == "已配置":
status_item.setForeground(QtGui.QColor(0, 0, 0)) # 黑色 status_item.setForeground(QtGui.QColor(0, 0, 0)) # 黑色
print(f"⚫ [状态列颜色] 行{row}状态列设置为已配置(黑色)")
else: else:
status_item.setForeground(QtGui.QColor(128, 128, 128)) # 灰色 status_item.setForeground(QtGui.QColor(128, 128, 128)) # 灰色
print(f"⚫ [状态列颜色] 行{row}状态列设置为未配置(灰色)")
print(f"✅ [通道列颜色] 已更新所有行的通道列颜色和状态列")
except Exception as e: except Exception as e:
print(f"❌ [通道列颜色] 更新通道列颜色失败: {e}") print(f"❌ [状态列更新] 更新状态列失败: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
......
...@@ -90,13 +90,6 @@ class SimpleScheduler: ...@@ -90,13 +90,6 @@ class SimpleScheduler:
schedule_time = time.time() - schedule_start schedule_time = time.time() - schedule_start
self.stats['schedule_times'].append(schedule_time) self.stats['schedule_times'].append(schedule_time)
# 打印调度信息(调试用)
if scheduled_batches:
batch_summary = []
for batch in scheduled_batches:
batch_summary.append(f"{batch['model_id']}({batch['batch_size']}帧)")
print(f"⚡ [简单调度器] 创建批次: {' | '.join(batch_summary)}")
return scheduled_batches return scheduled_batches
except Exception as e: except Exception as e:
......
...@@ -68,7 +68,7 @@ class ChannelThreadManager: ...@@ -68,7 +68,7 @@ class ChannelThreadManager:
# 曲线模式标记(由外部设置,用于检测线程启动时自动启动曲线线程) # 曲线模式标记(由外部设置,用于检测线程启动时自动启动曲线线程)
self.is_curve_mode = False self.is_curve_mode = False
# 主窗口引用(用于访问 current_mission) # 主窗口引用(用于访问 curvemission)
self.main_window = None self.main_window = None
# 应用配置(用于获取编译模式) # 应用配置(用于获取编译模式)
...@@ -337,7 +337,8 @@ class ChannelThreadManager: ...@@ -337,7 +337,8 @@ class ChannelThreadManager:
return True return True
def start_global_curve_thread(self): def start_global_curve_thread(self):
"""启动全局单例曲线线程(基于CSV文件的增量读取)""" """启动全局单例曲线线程(基于CSV文件的增量读取,支持进度条)"""
import os
from .threads.curve_thread import CurveThread from .threads.curve_thread import CurveThread
# 如果已经运行,直接返回 # 如果已经运行,直接返回
...@@ -345,15 +346,71 @@ class ChannelThreadManager: ...@@ -345,15 +346,71 @@ class ChannelThreadManager:
print(f"✅ [线程管理器] 全局曲线线程已在运行") print(f"✅ [线程管理器] 全局曲线线程已在运行")
return True return True
# 🔥 从主窗口获取 current_mission 路径 # 🔥 从主窗口的 curvemission 获取任务路径
current_mission_path = None current_mission_path = None
if self.main_window and hasattr(self.main_window, 'current_mission'): if self.main_window and hasattr(self.main_window, '_getCurveMissionPath'):
current_mission_path = self.main_window.current_mission current_mission_path = self.main_window._getCurveMissionPath()
print(f"📁 [线程管理器] 启动全局曲线线程,current_mission路径: {current_mission_path}") print(f"📁 [线程管理器] 启动全局曲线线程,从 curvemission 获取路径: {current_mission_path}")
else: else:
print(f"⚠️ [线程管理器] 无法获取current_mission,main_window未设置") print(f"⚠️ [线程管理器] 无法获取任务路径,main_window 或 _getCurveMissionPath 未设置")
return False return False
# 🔥 检查是否需要显示进度条
show_progress = False
progress_dialog = None
if current_mission_path and os.path.exists(current_mission_path):
# 查找所有CSV文件
csv_files = [f for f in os.listdir(current_mission_path) if f.endswith('.csv')]
if len(csv_files) > 1:
# 条件1:多个CSV文件
show_progress = True
print(f"📊 [进度条触发] CSV文件数量: {len(csv_files)} > 1")
elif len(csv_files) == 1:
# 条件2:单个CSV文件大小超过8MB
csv_path = os.path.join(current_mission_path, csv_files[0])
file_size_mb = os.path.getsize(csv_path) / (1024 * 1024)
if file_size_mb > 8:
show_progress = True
print(f"📊 [进度条触发] 单个CSV文件大小: {file_size_mb:.2f}MB > 8MB")
else:
print(f"ℹ️ [跳过进度条] 单个CSV文件 ({file_size_mb:.2f}MB),直接加载")
# 🔥 创建进度对话框(仅在需要时)
if show_progress:
try:
from qtpy import QtWidgets, QtCore
from widgets.style_manager import newIcon
progress_dialog = QtWidgets.QProgressDialog(
"正在加载曲线数据...",
"取消",
0,
100 # 使用百分比进度
)
progress_dialog.setWindowTitle("曲线数据加载进度")
progress_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
progress_dialog.setMinimumWidth(400)
progress_dialog.setMinimumDuration(0) # 立即显示
progress_dialog.setWindowIcon(newIcon("动态曲线"))
progress_dialog.setWindowFlags(
progress_dialog.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint
)
progress_dialog.setCancelButton(None)
progress_dialog.setValue(0)
progress_dialog.show()
QtWidgets.QApplication.processEvents()
print(f"✅ [进度条] 已显示进度对话框")
# 🔥 设置进度回调到全局曲线线程
CurveThread.set_progress_callback(
lambda value, text: self._onCurveLoadProgress(progress_dialog, value, text)
)
except Exception as e:
print(f"⚠️ [进度条] 创建失败: {e}")
progress_dialog = None
# 使用统一的回调函数(回调函数内部根据channel_id参数区分通道) # 使用统一的回调函数(回调函数内部根据channel_id参数区分通道)
callback = self.on_curve_updated if self.on_curve_updated else None callback = self.on_curve_updated if self.on_curve_updated else None
...@@ -368,12 +425,41 @@ class ChannelThreadManager: ...@@ -368,12 +425,41 @@ class ChannelThreadManager:
return True return True
def _onCurveLoadProgress(self, progress_dialog, value, text):
"""处理曲线加载进度更新(确保在主线程执行)"""
try:
if not progress_dialog:
return
# 🔥 使用 QTimer.singleShot 确保在主线程执行UI更新
from qtpy import QtCore
def update_ui():
try:
if progress_dialog:
progress_dialog.setValue(value)
progress_dialog.setLabelText(text)
# 加载完成时关闭进度条
if value >= 100:
progress_dialog.close()
progress_dialog.deleteLater()
print(f"✅ [进度条] 加载完成,已关闭进度对话框")
except Exception as e:
print(f"⚠️ [进度条] UI更新失败: {e}")
# 在主线程执行UI更新(延迟0ms,确保在主线程的事件循环中执行)
QtCore.QTimer.singleShot(0, update_ui)
except Exception as e:
print(f"⚠️ [进度条] 更新失败: {e}")
def start_storage_thread(self, channel_id: str, storage_path: str = None): def start_storage_thread(self, channel_id: str, storage_path: str = None):
"""启动存储线程 """启动存储线程
Args: Args:
channel_id: 通道ID channel_id: 通道ID
storage_path: 存储路径(已废弃,现在从 current_mission 读取) storage_path: 存储路径(已废弃,现在从通道配置读取)
""" """
context = self.get_channel_context(channel_id) context = self.get_channel_context(channel_id)
if not context: if not context:
...@@ -382,7 +468,7 @@ class ChannelThreadManager: ...@@ -382,7 +468,7 @@ class ChannelThreadManager:
if context.storage_flag: if context.storage_flag:
return True return True
# 存储路径现在从 current_mission 读取,不再使用 recordings 路径 # 存储路径现在从通道配置读取,不再使用 recordings 路径
# storage_path 参数保留但不使用,保持向后兼容 # storage_path 参数保留但不使用,保持向后兼容
context.storage_flag = True context.storage_flag = True
...@@ -480,6 +566,10 @@ class ChannelThreadManager: ...@@ -480,6 +566,10 @@ class ChannelThreadManager:
def stop_global_curve_thread(self): def stop_global_curve_thread(self):
"""停止全局单例曲线线程""" """停止全局单例曲线线程"""
from .threads.curve_thread import CurveThread from .threads.curve_thread import CurveThread
# 清除进度回调
CurveThread.clear_progress_callback()
CurveThread.stop() CurveThread.stop()
# 等待线程结束 # 等待线程结束
...@@ -522,10 +612,8 @@ class ChannelThreadManager: ...@@ -522,10 +612,8 @@ class ChannelThreadManager:
用于切换到曲线模式布局时启动全局曲线线程 用于切换到曲线模式布局时启动全局曲线线程
""" """
# 设置统一的回调函数(如果还没有设置) # 🔥 不要在这里设置回调,start_global_curve_thread 会处理
if self.on_curve_updated: # 避免重复设置导致数据被处理两次
from .threads.curve_thread import CurveThread
CurveThread.set_callback(self.on_curve_updated)
# 启动全局曲线线程(如果尚未启动) # 启动全局曲线线程(如果尚未启动)
return self.start_global_curve_thread() return self.start_global_curve_thread()
......
...@@ -57,12 +57,15 @@ class CurveThread: ...@@ -57,12 +57,15 @@ class CurveThread:
# 全局统一的回调函数(所有数据都发送到这个函数,函数内部根据csv_filepath或area_name处理) # 全局统一的回调函数(所有数据都发送到这个函数,函数内部根据csv_filepath或area_name处理)
_global_callback: Optional[Callable] = None _global_callback: Optional[Callable] = None
# 🔥 进度回调函数(用于显示进度条)
_progress_callback: Optional[Callable] = None
@staticmethod @staticmethod
def run(current_mission_path: str = None, callback: Optional[Callable] = None): def run(current_mission_path: str = None, callback: Optional[Callable] = None):
"""曲线绘制线程主循环(全局单例版本) """曲线绘制线程主循环(全局单例版本)
Args: Args:
current_mission_path: 当前任务路径(从全局变量 current_mission 读取) current_mission_path: 当前任务路径(从 curvemission 下拉框获取)
callback: 统一的回调函数 callback(csv_filepath, area_name, area_idx, curve_points) callback: 统一的回调函数 callback(csv_filepath, area_name, area_idx, curve_points)
曲线线程读取到新数据后,直接调用此函数 曲线线程读取到新数据后,直接调用此函数
参数说明: 参数说明:
...@@ -79,7 +82,7 @@ class CurveThread: ...@@ -79,7 +82,7 @@ class CurveThread:
if callback: if callback:
CurveThread._global_callback = callback CurveThread._global_callback = callback
# 从全局变量 current_mission 获取曲线路径 # 从 curvemission 获取曲线路径
curve_path = current_mission_path curve_path = current_mission_path
# 从配置文件读取曲线帧率 # 从配置文件读取曲线帧率
...@@ -148,14 +151,31 @@ class CurveThread: ...@@ -148,14 +151,31 @@ class CurveThread:
for csv_filepath in available_csv_files: for csv_filepath in available_csv_files:
_ensure_file_state(csv_filepath) _ensure_file_state(csv_filepath)
# 🔥 发送所有文件的历史数据(启动时一次性发送) # 🔥 发送所有文件的历史数据(启动时一次性发送,支持进度报告
print(f"🔥 [全局曲线线程] 开始发送历史数据,共 {len(available_csv_files)} 个CSV文件") print(f"🔥 [全局曲线线程] 开始发送历史数据,共 {len(available_csv_files)} 个CSV文件")
for csv_filepath in available_csv_files: total_files = len(available_csv_files)
for idx, csv_filepath in enumerate(available_csv_files):
state = file_states.get(csv_filepath) state = file_states.get(csv_filepath)
if state and state['cached_data']: if state and state['cached_data']:
data_count = len(state['cached_data']) data_count = len(state['cached_data'])
print(f"📤 [全局曲线线程] 发送历史数据: {csv_filepath}") print(f"📤 [全局曲线线程] 发送历史数据: {csv_filepath}")
print(f" 文件名: {state['area_name']}, 区域索引: {state['area_idx']}, 数据点数: {data_count}") print(f" 文件名: {state['area_name']}, 区域索引: {state['area_idx']}, 数据点数: {data_count}")
# 🔥 报告进度
if CurveThread._progress_callback:
try:
progress_value = int((idx + 1) / total_files * 100)
progress_text = f"正在加载曲线数据... ({idx + 1}/{total_files})"
print(f"📊 [进度回调] 调用进度更新: {progress_value}% - {progress_text}")
CurveThread._progress_callback(progress_value, progress_text)
except Exception as e:
print(f"⚠️ [进度回调] 失败: {e}")
import traceback
traceback.print_exc()
else:
print(f"⚠️ [进度回调] 回调函数未设置,跳过进度报告")
if CurveThread._global_callback: if CurveThread._global_callback:
try: try:
CurveThread._global_callback( CurveThread._global_callback(
...@@ -172,6 +192,18 @@ class CurveThread: ...@@ -172,6 +192,18 @@ class CurveThread:
else: else:
print(f"⚠️ [全局曲线线程] 文件无数据或状态不存在: {csv_filepath}") print(f"⚠️ [全局曲线线程] 文件无数据或状态不存在: {csv_filepath}")
# 🔥 加载完成,报告100%进度
if CurveThread._progress_callback:
try:
print(f"📊 [进度回调] 调用完成通知: 100%")
CurveThread._progress_callback(100, "加载完成")
except Exception as e:
print(f"⚠️ [进度回调] 完成通知失败: {e}")
import traceback
traceback.print_exc()
else:
print(f"⚠️ [进度回调] 回调函数未设置,跳过完成通知")
# 文件扫描计数器(每10个周期重新扫描一次文件夹) # 文件扫描计数器(每10个周期重新扫描一次文件夹)
scan_counter = 0 scan_counter = 0
scan_interval = 10 # 每10个周期扫描一次 scan_interval = 10 # 每10个周期扫描一次
...@@ -526,3 +558,19 @@ class CurveThread: ...@@ -526,3 +558,19 @@ class CurveThread:
def clear_callback(): def clear_callback():
"""清除回调函数""" """清除回调函数"""
CurveThread._global_callback = None CurveThread._global_callback = None
@staticmethod
def set_progress_callback(callback: Optional[Callable]):
"""设置进度回调函数
Args:
callback: 进度回调函数 callback(value, text)
- value: 进度值 (0-100)
- text: 进度文本描述
"""
CurveThread._progress_callback = callback
@staticmethod
def clear_progress_callback():
"""清除进度回调函数"""
CurveThread._progress_callback = None
...@@ -344,11 +344,6 @@ class GlobalDetectionThread: ...@@ -344,11 +344,6 @@ class GlobalDetectionThread:
# 使用优化的帧收集方法 # 使用优化的帧收集方法
collected_data = self.frame_collector.optimize_frame_collection(self.channel_contexts) collected_data = self.frame_collector.optimize_frame_collection(self.channel_contexts)
# 如果收集到帧,打印简要信息(调试用)
if collected_data.get('total_frames', 0) > 0:
summary = self.frame_collector.get_frame_groups_summary(collected_data.get('model_groups', {}))
print(f"📥 [全局检测线程] 收集帧: {summary}")
return collected_data return collected_data
except Exception as e: except Exception as e:
...@@ -382,11 +377,6 @@ class GlobalDetectionThread: ...@@ -382,11 +377,6 @@ class GlobalDetectionThread:
results, self.channel_contexts, self.channel_callbacks results, self.channel_contexts, self.channel_callbacks
) )
# 打印分发摘要(调试用)
if results:
summary = self.result_distributor.get_distribution_summary(results)
print(f"📤 [全局检测线程] 分发结果: {summary}")
except Exception as e: except Exception as e:
print(f"❌ [全局检测线程] 结果分发失败: {e}") print(f"❌ [全局检测线程] 结果分发失败: {e}")
import traceback import traceback
......
...@@ -32,10 +32,6 @@ class ViewHandler: ...@@ -32,10 +32,6 @@ class ViewHandler:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# 曲线分析模式状态(False=实时检测模式, True=曲线分析模式) # 曲线分析模式状态(False=实时检测模式, True=曲线分析模式)
self._is_curve_mode_active = False self._is_curve_mode_active = False
# 当前任务标识(存储完整的任务文件夹路径,未选择时为None)
# 例如:'d:\restructure\liquid_level_line_detection_system\database\mission_result\12_13'
# 注意:初始化时设置为None,只有在用户手动选择任务后才会设置为有效值
self._current_mission = None # 使用私有变量,通过属性访问
@property @property
def is_curve_mode_active(self): def is_curve_mode_active(self):
...@@ -47,21 +43,35 @@ class ViewHandler: ...@@ -47,21 +43,35 @@ class ViewHandler:
"""设置曲线模式状态""" """设置曲线模式状态"""
self._is_curve_mode_active = value self._is_curve_mode_active = value
@property def _getCurveMissionPath(self):
def current_mission(self): """从 curvemission 下拉框获取当前任务路径
"""获取当前任务路径"""
# 如果为None,返回无效路径(包含None) Returns:
if self._current_mission is None: str: 任务文件夹完整路径,如果未选择则返回None
return r"D:\restructure\liquid_level_line_detection_system\database\mission_result\None" """
return self._current_mission if not hasattr(self, 'curvemission'):
return None
@current_mission.setter
def current_mission(self, value): mission_name = self.curvemission.currentText()
"""设置当前任务路径""" if not mission_name or mission_name == "请选择任务":
# print(f"[DEBUG] current_mission.setter: 设置current_mission = {value}") return None
old_value = self._current_mission
self._current_mission = value # 构建完整路径
# print(f"[DEBUG] current_mission.setter: 从 {old_value} 更新为 {value}") import os
import sys
# 动态获取项目根目录
if getattr(sys, 'frozen', False):
project_root = os.path.dirname(sys.executable)
else:
try:
from database.config import get_project_root
project_root = get_project_root()
except ImportError:
project_root = os.getcwd()
mission_path = os.path.join(project_root, 'database', 'mission_result', mission_name)
return mission_path if os.path.exists(mission_path) else None
def toggleToolBar(self): def toggleToolBar(self):
"""切换工具栏显示""" """切换工具栏显示"""
...@@ -137,53 +147,72 @@ class ViewHandler: ...@@ -137,53 +147,72 @@ class ViewHandler:
# 🔥 根据检测线程状态选择通道容器 # 🔥 根据检测线程状态选择通道容器
if detection_running: if detection_running:
# 实时检测模式:使用带底部按钮的通道容器 # 实时检测模式:使用通道面板容器(ChannelPanel)
target_channel_widgets = self.channel_widgets_for_curve target_channel_widgets = self.channel_widgets_for_curve
layout_description = "实时检测模式(带底部按钮)" layout_description = "实时检测模式"
else: else:
# 历史回放模式:使用无底部按钮的通道容器 # 历史回放模式:使用历史视频面板容器(HistoryVideoPanel)
target_channel_widgets = self.history_channel_widgets_for_curve target_channel_widgets = self.history_channel_widgets_for_curve
layout_description = "历史回放模式(无底部按钮)" layout_description = "历史回放模式"
print(f"📋 [曲线模式] 使用{layout_description}") print(f"📋 [曲线模式] 使用{layout_description}")
# 🔥 只移动通道面板到对应的容器,不显示所有通道 # 🔥 根据检测线程状态选择要显示的面板类型
# 通道面板的显示由 _updateCurveChannelDisplay 根据任务配置控制 if detection_running:
for i, channel_panel in enumerate(self.channelPanels): # 实时检测模式:使用通道面板(ChannelPanel)
panels_to_use = self.channelPanels
print(f"📋 [曲线模式] 使用通道面板(ChannelPanel)")
else:
# 历史回放模式:使用历史视频面板(HistoryVideoPanel)
if hasattr(self, 'historyVideoPanels'):
panels_to_use = self.historyVideoPanels
print(f"📋 [曲线模式] 使用历史视频面板(HistoryVideoPanel)")
else:
print(f"⚠️ [曲线模式] 未找到历史视频面板,回退到通道面板")
panels_to_use = self.channelPanels
# 🔥 先隐藏所有wrapper
for wrapper in target_channel_widgets:
wrapper.setVisible(False)
# 🔥 移动面板到对应的容器
for i, panel in enumerate(panels_to_use):
if i < len(target_channel_widgets): if i < len(target_channel_widgets):
wrapper = target_channel_widgets[i] wrapper = target_channel_widgets[i]
# 🔥 先从当前父级中移除通道面板 # 🔥 先从当前父级中移除面板
current_parent = channel_panel.parent() current_parent = panel.parent()
if current_parent and current_parent != wrapper: if current_parent and current_parent != wrapper:
current_parent_layout = current_parent.layout() current_parent_layout = current_parent.layout()
if current_parent_layout: if current_parent_layout:
current_parent_layout.removeWidget(channel_panel) current_parent_layout.removeWidget(panel)
# 将 ChannelPanel 添加到wrapper的布局中 # 将面板添加到wrapper的布局中
wrapper_layout = wrapper.layout() wrapper_layout = wrapper.layout()
if wrapper_layout: if wrapper_layout:
# 检查是否已经在布局中 # 检查是否已经在布局中
if channel_panel.parent() != wrapper: if panel.parent() != wrapper:
channel_panel.setParent(wrapper) panel.setParent(wrapper)
wrapper_layout.addWidget(channel_panel) wrapper_layout.addWidget(panel)
else: else:
# 如果没有布局,使用绝对定位 # 如果没有布局,使用绝对定位
channel_panel.setParent(wrapper) panel.setParent(wrapper)
channel_panel.move(0, 0) panel.move(0, 0)
# 🔥 根据检测线程状态决定是否显示底部按钮 # 🔥 先显示wrapper,再显示面板(顺序很重要!)
if hasattr(channel_panel, 'bottom_button_widget'): wrapper.setVisible(True)
if detection_running: panel.show()
# 实时检测模式:显示底部按钮
channel_panel.bottom_button_widget.setVisible(True)
else:
# 历史回放模式:隐藏底部按钮
channel_panel.bottom_button_widget.setVisible(False)
# 🔥 通道面板始终显示,但wrapper的可见性由任务配置控制 # 🔥 强制更新布局
channel_panel.show() wrapper.updateGeometry()
# 不要在这里显示wrapper,让 _updateCurveChannelDisplay 控制 panel.updateGeometry()
# 🔥 调试信息
print(f" ✅ 已将 {panel.objectName()} 添加到 wrapper[{i}]")
print(f" - 面板尺寸: {panel.size().width()}x{panel.size().height()}")
print(f" - wrapper尺寸: {wrapper.size().width()}x{wrapper.size().height()}")
print(f" - 面板可见: {panel.isVisible()}")
print(f" - wrapper可见: {wrapper.isVisible()}")
# 先设置模式为曲线模式(_video_layout_mode = 1) # 先设置模式为曲线模式(_video_layout_mode = 1)
self._video_layout_mode = 1 self._video_layout_mode = 1
...@@ -222,20 +251,22 @@ class ViewHandler: ...@@ -222,20 +251,22 @@ class ViewHandler:
self._updateCurveChannelDisplay([]) self._updateCurveChannelDisplay([])
print(f"🔄 [曲线模式] 实时检测模式:未选择任务,隐藏所有通道") print(f"🔄 [曲线模式] 实时检测模式:未选择任务,隐藏所有通道")
else: else:
# 🔥 历史回放模式:独立显示一个通道面板,不受任务选择限制 # 🔥 历史回放模式:显示所有历史视频面板
if hasattr(self, '_updateCurveChannelDisplay'): if hasattr(self, '_updateCurveChannelDisplay'):
self._updateCurveChannelDisplay([]) # 传递空列表,_updateCurveChannelDisplay 会检查模式自动显示第一个通道 all_channels = ['通道1', '通道2', '通道3', '通道4']
print(f"🔄 [曲线模式] 历史回放模式:独立显示第一个通道面板(不受任务限制)") self._updateCurveChannelDisplay(all_channels) # 显示所有4个历史视频面板
print(f"🔄 [曲线模式] 历史回放模式:显示所有历史视频面板")
# 智能选择:历史数据加载 vs 实时曲线线程
if self._video_layout_mode == 1:
self._loadCurveDataOrStartThreads()
# 强制刷新整个布局 # 强制刷新整个布局
self.videoLayoutStack.currentWidget().updateGeometry() self.videoLayoutStack.currentWidget().updateGeometry()
from qtpy.QtWidgets import QApplication from qtpy.QtWidgets import QApplication
QApplication.processEvents() QApplication.processEvents()
# 🔥 延迟启动曲线线程,确保布局切换完成后再启动(避免进度条在切换前弹出)
if self._video_layout_mode == 1:
from qtpy import QtCore
QtCore.QTimer.singleShot(100, self._loadCurveDataOrStartThreads)
def _switchToDefaultLayout(self): def _switchToDefaultLayout(self):
"""切换到默认布局(实时检测模式)""" """切换到默认布局(实时检测模式)"""
print(f"\n{'='*60}") print(f"\n{'='*60}")
...@@ -255,9 +286,6 @@ class ViewHandler: ...@@ -255,9 +286,6 @@ class ViewHandler:
widget = self.videoLayoutStack.widget(i) widget = self.videoLayoutStack.widget(i)
print(f" 索引{i}: {widget} (可见: {widget.isVisible() if widget else 'None'})") print(f" 索引{i}: {widget} (可见: {widget.isVisible() if widget else 'None'})")
# 禁用历史回放模式
print(f"[布局切换] 禁用历史回放模式...")
self._setChannelPanelsHistoryMode(False)
# 🔥 切换曲线绘制模式为实时模式 # 🔥 切换曲线绘制模式为实时模式
if hasattr(self, 'curvePanelHandler'): if hasattr(self, 'curvePanelHandler'):
...@@ -342,8 +370,6 @@ class ViewHandler: ...@@ -342,8 +370,6 @@ class ViewHandler:
curve_mode = 'realtime' curve_mode = 'realtime'
print(f"🔄 [曲线子布局] 检测线程运行中,切换到{layout_name}") print(f"🔄 [曲线子布局] 检测线程运行中,切换到{layout_name}")
# 禁用历史回放模式(恢复正常显示)
self._setChannelPanelsHistoryMode(False)
else: else:
# 切换到历史回放布局(索引1) # 切换到历史回放布局(索引1)
target_index = 1 target_index = 1
...@@ -353,8 +379,6 @@ class ViewHandler: ...@@ -353,8 +379,6 @@ class ViewHandler:
curve_mode = 'history' curve_mode = 'history'
print(f"🔄 [曲线子布局] 检测线程停止,切换到{layout_name}") print(f"🔄 [曲线子布局] 检测线程停止,切换到{layout_name}")
# 启用历史回放模式
self._setChannelPanelsHistoryMode(True)
# 🔥 同步切换曲线绘制模式 # 🔥 同步切换曲线绘制模式
if hasattr(self, 'curvePanelHandler'): if hasattr(self, 'curvePanelHandler'):
...@@ -384,43 +408,28 @@ class ViewHandler: ...@@ -384,43 +408,28 @@ class ViewHandler:
else: else:
print(f"📋 [曲线子布局] 已经是{layout_name},无需切换") print(f"📋 [曲线子布局] 已经是{layout_name},无需切换")
def _setChannelPanelsHistoryMode(self, is_history_mode): # 🔥 切换模式后,重新应用通道筛选逻辑
"""设置所有通道面板的历史回放模式 if hasattr(self, 'curvemission') and hasattr(self, '_onCurveMissionChanged'):
current_mission = self.curvemission.currentText()
Args: if current_mission and current_mission != "请选择任务":
is_history_mode: bool, True=启用历史回放模式, False=禁用历史回放模式 print(f"🔄 [通道筛选] 模式切换后,重新筛选通道: {current_mission}")
""" self._onCurveMissionChanged(current_mission)
if not hasattr(self, 'channelPanels'):
return
for channel_panel in self.channelPanels:
if hasattr(channel_panel, 'setHistoryPlaybackMode'):
channel_panel.setHistoryPlaybackMode(is_history_mode)
def _loadCurveDataOrStartThreads(self): def _loadCurveDataOrStartThreads(self):
""" """
智能选择:加载历史数据 或 启动实时曲线线程 智能选择:加载历史数据 或 启动实时曲线线程
业务逻辑: 业务逻辑:
- 如果有检测线程正在运行 → 启动曲线线程(实时监控CSV文件变化) - 启动全局曲线线程,由线程负责加载历史数据和监控新数据
- 如果没有检测线程运行 → 一次性加载历史CSV数据(无需后台线程) - 避免重复加载:不再单独调用 _loadHistoricalCurveData()
""" """
if not hasattr(self, 'thread_manager'): if not hasattr(self, 'thread_manager'):
return return
# 检查是否有任何检测线程正在运行 # 🔥 启动全局曲线线程
has_running_detection = False # 线程启动时会自动发送历史数据,然后监控CSV文件变化
for context in self.thread_manager.contexts.values():
if context and context.detection_flag:
has_running_detection = True
break
if has_running_detection:
# 场景1:有检测线程运行 → 启动曲线线程(实时监控)
self._startAllCurveThreads() self._startAllCurveThreads()
else:
# 场景2:无检测线程运行 → 一次性加载历史数据
self._loadHistoricalCurveData()
def _startAllCurveThreads(self): def _startAllCurveThreads(self):
"""启动所有已打开通道的曲线线程(仅在曲线模式下)""" """启动所有已打开通道的曲线线程(仅在曲线模式下)"""
...@@ -446,8 +455,8 @@ class ViewHandler: ...@@ -446,8 +455,8 @@ class ViewHandler:
if not hasattr(self, 'loadHistoricalCurveData'): if not hasattr(self, 'loadHistoricalCurveData'):
return return
# 从 current_mission 变量读取当前任务目录 # 从 curvemission 获取当前任务目录
data_directory = self.current_mission if hasattr(self, 'current_mission') else None data_directory = self._getCurveMissionPath()
if data_directory: if data_directory:
success = self.loadHistoricalCurveData(data_directory) success = self.loadHistoricalCurveData(data_directory)
...@@ -461,19 +470,19 @@ class ViewHandler: ...@@ -461,19 +470,19 @@ class ViewHandler:
self.thread_manager.stop_all_curve_threads() self.thread_manager.stop_all_curve_threads()
def _updateCurvePanelFolderName(self): def _updateCurvePanelFolderName(self):
"""从全局变量 current_mission 获取任务路径并设置到曲线面板(文本框会自动提取文件夹名称)""" """从 curvemission 获取任务路径并设置到曲线面板(文本框会自动提取文件夹名称)"""
try: try:
# 检查曲线面板是否存在 # 检查曲线面板是否存在
if not hasattr(self, 'curvePanel') or self.curvePanel is None: if not hasattr(self, 'curvePanel') or self.curvePanel is None:
return return
# 设置到曲线面板(传递完整路径,面板会自动提取文件夹名称) # 从 curvemission 获取任务路径
self.curvePanel.setFolderName(self.current_mission) mission_path = self._getCurveMissionPath()
if mission_path:
self.curvePanel.setFolderName(mission_path)
except Exception as e: except Exception as e:
# 出错时设置为默认路径 pass
if hasattr(self, 'curvePanel') and self.curvePanel:
self.curvePanel.setFolderName(r"D:\restructure\liquid_level_line_detection_system\database\mission_result\None")
def isCurrentMissionValid(self): def isCurrentMissionValid(self):
""" """
...@@ -483,32 +492,15 @@ class ViewHandler: ...@@ -483,32 +492,15 @@ class ViewHandler:
bool: 如果任务有效返回True,否则返回False bool: 如果任务有效返回True,否则返回False
""" """
try: try:
import os # 从 curvemission 获取任务路径
mission_path = self._getCurveMissionPath()
# 首先检查私有变量是否为None(最直接的检查)
if self._current_mission is None:
return False
# 获取 current_mission 值
mission = self._current_mission
# 如果为空字符串,视为无效 # 如果路径为None或不存在,视为无效
if not mission: if not mission_path:
return False return False
# 如果是 "0000",视为无效 import os
if mission == "0000": if not os.path.exists(mission_path):
return False
# 检查路径的最后一部分是否是 "None"
# 使用 os.path.basename 获取路径的最后一部分
last_part = os.path.basename(mission)
if last_part == "None":
return False
# 检查是否是默认的无效路径(完整匹配)
default_invalid_path = r"D:\restructure\liquid_level_line_detection_system\database\mission_result\None"
if mission == default_invalid_path:
return False return False
# 其他情况视为有效 # 其他情况视为有效
......
...@@ -8,7 +8,6 @@ from .videopage import ( ...@@ -8,7 +8,6 @@ from .videopage import (
CurvePanel, CurvePanel,
MissionPanel, MissionPanel,
ModelSettingDialog, ModelSettingDialog,
HistoryPanel,
) )
# 数据集页面组件(从 datasetpage 子模块导入) # 数据集页面组件(从 datasetpage 子模块导入)
......
...@@ -231,10 +231,11 @@ class DialogManager: ...@@ -231,10 +231,11 @@ class DialogManager:
# 🔥 移除了font-size设置,改用FontManager统一管理字体 # 🔥 移除了font-size设置,改用FontManager统一管理字体
DEFAULT_STYLE = """ DEFAULT_STYLE = """
QMessageBox { QMessageBox {
min-height: 100px; min-width: 400px;
min-height: 180px;
} }
QLabel { QLabel {
min-height: 50px; min-height: 60px;
qproperty-alignment: 'AlignCenter'; qproperty-alignment: 'AlignCenter';
} }
""" """
......
...@@ -10,7 +10,8 @@ from .channelpanel import ChannelPanel ...@@ -10,7 +10,8 @@ from .channelpanel import ChannelPanel
from .curvepanel import CurvePanel from .curvepanel import CurvePanel
from .missionpanel import MissionPanel from .missionpanel import MissionPanel
from .modelsetting_dialogue import ModelSettingDialog from .modelsetting_dialogue import ModelSettingDialog
from .historypanel import HistoryPanel from .historyvideopanel import HistoryVideoPanel
from .general_set import GeneralSetPanel, GeneralSetDialog from .general_set import GeneralSetPanel, GeneralSetDialog
...@@ -19,7 +20,7 @@ __all__ = [ ...@@ -19,7 +20,7 @@ __all__ = [
'CurvePanel', 'CurvePanel',
'MissionPanel', 'MissionPanel',
'ModelSettingDialog', 'ModelSettingDialog',
'HistoryPanel', 'HistoryVideoPanel',
'RtspDialog', 'RtspDialog',
'GeneralSetPanel', 'GeneralSetPanel',
'GeneralSetDialog', 'GeneralSetDialog',
......
...@@ -49,7 +49,7 @@ class ChannelPanel(QtWidgets.QWidget): ...@@ -49,7 +49,7 @@ class ChannelPanel(QtWidgets.QWidget):
channelAdded = QtCore.Signal(dict) # 通道添加信号 channelAdded = QtCore.Signal(dict) # 通道添加信号
channelRemoved = QtCore.Signal(str) # 通道移除信号 channelRemoved = QtCore.Signal(str) # 通道移除信号
channelEdited = QtCore.Signal(str, dict) # 通道编辑信号 channelEdited = QtCore.Signal(str, dict) # 通道编辑信号
curveClicked = QtCore.Signal() # 曲线按钮点击信号 curveClicked = QtCore.Signal(str) # 曲线按钮点击信号,传递任务名称
amplifyClicked = QtCore.Signal(str) # 放大按钮点击信号,传递channel_id amplifyClicked = QtCore.Signal(str) # 放大按钮点击信号,传递channel_id
channelNameChanged = QtCore.Signal(str, str) # 通道名称改变信号(channel_id, new_name) channelNameChanged = QtCore.Signal(str, str) # 通道名称改变信号(channel_id, new_name)
...@@ -665,8 +665,10 @@ class ChannelPanel(QtWidgets.QWidget): ...@@ -665,8 +665,10 @@ class ChannelPanel(QtWidgets.QWidget):
def _onCurveClicked(self): def _onCurveClicked(self):
"""曲线按钮点击""" """曲线按钮点击"""
# 发射曲线按钮点击信号 # 获取当前面板的任务信息
self.curveClicked.emit() task_name = self.getTaskInfo()
# 发射曲线按钮点击信号,传递任务名称
self.curveClicked.emit(task_name if task_name else "")
def _onAmplifyClicked(self): def _onAmplifyClicked(self):
"""放大按钮点击""" """放大按钮点击"""
...@@ -738,80 +740,6 @@ class ChannelPanel(QtWidgets.QWidget): ...@@ -738,80 +740,6 @@ class ChannelPanel(QtWidgets.QWidget):
if channel_id: if channel_id:
self.channelNameChanged.emit(channel_id, new_name) self.channelNameChanged.emit(channel_id, new_name)
def setHistoryPlaybackMode(self, is_history_mode):
"""
设置历史回放模式
当曲线布局为历史回放布局时调用此方法:
- 隐藏通道名称标签(左上角)
- 隐藏任务信息标签(右上角)
- 在中间显示"历史回放"文本标签
Args:
is_history_mode: bool, True=启用历史回放模式, False=禁用历史回放模式
"""
if is_history_mode:
# 启用历史回放模式
print(f"[ChannelPanel] 启用历史回放模式: {self._title}")
# 隐藏通道名称标签(左上角)
if hasattr(self, 'nameLabel'):
self.nameLabel.hide()
# 隐藏任务信息标签(右上角)
if hasattr(self, 'taskLabel'):
self.taskLabel.hide()
# 创建或显示历史回放文本标签(中间)
if not hasattr(self, 'historyPlaybackLabel'):
self.historyPlaybackLabel = QtWidgets.QLabel(self.videoLabel)
self.historyPlaybackLabel.setText("历史回放")
self.historyPlaybackLabel.setStyleSheet("""
QLabel {
background-color: transparent;
color: white;
font-size: 16pt;
font-weight: bold;
padding: 10px 20px;
}
""")
self.historyPlaybackLabel.setAlignment(Qt.AlignCenter)
# 显示历史回放标签
self.historyPlaybackLabel.show()
self.historyPlaybackLabel.adjustSize()
# 定位到中间
self._positionHistoryPlaybackLabel()
else:
# 禁用历史回放模式(恢复正常显示)
print(f"[ChannelPanel] 禁用历史回放模式: {self._title}")
# 显示通道名称标签
if hasattr(self, 'nameLabel'):
self.nameLabel.show()
# 显示任务信息标签
if hasattr(self, 'taskLabel'):
self.taskLabel.show()
# 隐藏历史回放标签
if hasattr(self, 'historyPlaybackLabel'):
self.historyPlaybackLabel.hide()
def _positionHistoryPlaybackLabel(self):
"""定位历史回放标签到顶部居中"""
if hasattr(self, 'historyPlaybackLabel') and hasattr(self, 'videoLabel'):
video_width = self.videoLabel.width()
label_width = self.historyPlaybackLabel.width()
# 🔥 定位到顶部居中(距离顶部20px)
x = (video_width - label_width) // 2
y = 20 # 距离顶部20px
self.historyPlaybackLabel.move(x, y)
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -236,7 +236,7 @@ class CurvePanel(QtWidgets.QWidget): ...@@ -236,7 +236,7 @@ class CurvePanel(QtWidgets.QWidget):
self.spn_upper_limit.setSuffix("mm") self.spn_upper_limit.setSuffix("mm")
self.spn_upper_limit.setDecimals(1) self.spn_upper_limit.setDecimals(1)
self.spn_upper_limit.setSingleStep(0.5) # 设置步长为0.5mm self.spn_upper_limit.setSingleStep(0.5) # 设置步长为0.5mm
self.spn_upper_limit.setFixedWidth(scale_w(80)) # 🔥 响应式宽度 self.spn_upper_limit.setFixedWidth(scale_w(85)) # 🔥 响应式宽度
self.spn_upper_limit.setToolTip("设置安全上限") self.spn_upper_limit.setToolTip("设置安全上限")
toolbar_layout.addWidget(self.spn_upper_limit) toolbar_layout.addWidget(self.spn_upper_limit)
...@@ -250,7 +250,7 @@ class CurvePanel(QtWidgets.QWidget): ...@@ -250,7 +250,7 @@ class CurvePanel(QtWidgets.QWidget):
self.spn_lower_limit.setSuffix("mm") self.spn_lower_limit.setSuffix("mm")
self.spn_lower_limit.setDecimals(1) self.spn_lower_limit.setDecimals(1)
self.spn_lower_limit.setSingleStep(0.5) # 设置步长为0.5mm self.spn_lower_limit.setSingleStep(0.5) # 设置步长为0.5mm
self.spn_lower_limit.setFixedWidth(scale_w(80)) # 🔥 响应式宽度 self.spn_lower_limit.setFixedWidth(scale_w(85)) # 🔥 响应式宽度
self.spn_lower_limit.setToolTip("设置安全下限") self.spn_lower_limit.setToolTip("设置安全下限")
toolbar_layout.addWidget(self.spn_lower_limit) toolbar_layout.addWidget(self.spn_lower_limit)
...@@ -506,6 +506,31 @@ class CurvePanel(QtWidgets.QWidget): ...@@ -506,6 +506,31 @@ class CurvePanel(QtWidgets.QWidget):
# 如果找不到,选中默认选项 # 如果找不到,选中默认选项
self.curvemission.setCurrentIndex(0) self.curvemission.setCurrentIndex(0)
def setMissionFromTaskName(self, task_name):
"""
从任务名称设置 curvemission(来源1和来源2使用)
Args:
task_name: 任务名称(如 "1_1", "32_23")
"""
if not task_name or task_name == "未分配任务" or task_name == "None":
# 选中默认选项
self.curvemission.setCurrentIndex(0)
print(f"⚠️ [CurvePanel] 无效任务名称,重置为默认选项: {task_name}")
return False
# 在下拉框中查找并选中对应的任务
index = self.curvemission.findText(task_name)
if index >= 0:
self.curvemission.setCurrentIndex(index)
print(f"✅ [CurvePanel] 已设置任务: {task_name} (索引: {index})")
return True
else:
# 如果找不到,选中默认选项
self.curvemission.setCurrentIndex(0)
print(f"⚠️ [CurvePanel] 未找到任务 '{task_name}',重置为默认选项")
return False
def addChannel(self, channel_id, channel_name=None, window_name=None, color=None): def addChannel(self, channel_id, channel_name=None, window_name=None, color=None):
""" """
添加一个通道(UI创建) 添加一个通道(UI创建)
......
"""
"""
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtCore import QUrl, Qt
import os
# 导入图标工具和响应式布局
try:
from widgets.style_manager import newIcon
from widgets.responsive_layout import ResponsiveLayout, scale_w, scale_h
except ImportError:
try:
from style_manager import newIcon
from responsive_layout import ResponsiveLayout, scale_w, scale_h
except ImportError:
def newIcon(icon):
from PyQt5 import QtGui
return QtGui.QIcon()
ResponsiveLayout = None
scale_w = lambda x: x
scale_h = lambda x: x
class HistoryPanel(QtWidgets.QWidget):
""""""
def __init__(self, parent=None):
super().__init__(parent)
self._media_player = None
self._video_widget = None
self._is_seeking = False #
self._initUI()
def _initUI(self):
"""UI - 使用固定大小和绝对位置布局"""
# 🔥 设置面板固定大小 - 响应式布局
self.setFixedSize(scale_w(660), scale_h(380))
# 不使用布局管理器,直接使用绝对位置
self._createVideoArea()
self._createControlsArea()
self._createInfoArea()
def _createVideoArea(self):
"""创建视频显示区域 - 固定位置和大小"""
self._video_widget = QVideoWidget(self)
# 固定位置和大小:左上角(5, 5),宽度650,高度300
self._video_widget.setGeometry(5, 5, 650, 300)
self._video_widget.setStyleSheet(
"background: black; "
"border: 2px solid #dee2e6; "
"border-radius: 4px;"
)
#
self._media_player = QMediaPlayer(None, QMediaPlayer.VideoSurface)
self._media_player.setVideoOutput(self._video_widget)
#
self._media_player.positionChanged.connect(self._onPositionChanged)
self._media_player.durationChanged.connect(self._onDurationChanged)
self._media_player.stateChanged.connect(self._onStateChanged)
self._media_player.error.connect(self._onError)
def _createControlsArea(self):
"""创建控制区域 - 固定位置和大小"""
button_style = """
QPushButton {
background-color: transparent;
border: 1px solid transparent;
border-radius: 3px;
padding: 5px;
}
QPushButton:hover {
background-color: palette(light);
border: 1px solid palette(mid);
}
QPushButton:pressed {
background-color: palette(midlight);
}
QPushButton:disabled {
opacity: 0.5;
}
"""
# 播放/暂停按钮 - 固定位置(5, 310),大小35x35
self.play_pause_button = QtWidgets.QPushButton(self)
self.play_pause_button.setGeometry(5, 310, 35, 35)
self.play_pause_button.setIcon(newIcon('开始'))
self.play_pause_button.setIconSize(QtCore.QSize(24, 24))
self.play_pause_button.setToolTip('播放')
self.play_pause_button.setStyleSheet(button_style)
# 进度条 - 固定位置(45, 318),宽度380
self.position_slider = QtWidgets.QSlider(Qt.Horizontal, self)
self.position_slider.setGeometry(45, 318, 380, 20)
self.position_slider.setRange(0, 0)
self.position_slider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #999;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #2196F3;
border: 1px solid #1976D2;
width: 18px;
margin: -5px 0;
border-radius: 9px;
}
QSlider::sub-page:horizontal {
background: #2196F3;
border-radius: 4px;
}
""")
self.position_slider.sliderPressed.connect(self._onSliderPressed)
self.position_slider.sliderReleased.connect(self._onSliderReleased)
self.position_slider.sliderMoved.connect(self._onSliderMoved)
# 时间标签 - 固定位置(430, 315),宽度85
self.time_label = QtWidgets.QLabel("00:00 / 00:00", self)
self.time_label.setGeometry(430, 315, 85, 25)
self.time_label.setStyleSheet("color: #666; font-size: 8pt;")
self.time_label.setAlignment(Qt.AlignCenter)
# 音量控制器已删除,不再需要
# 设置默认音量
self._media_player.setVolume(50)
def _createInfoArea(self):
"""创建信息区域(已隐藏)"""
self.info_label = QtWidgets.QLabel("", self)
self.info_label.setGeometry(5, 350, 650, 25)
self.info_label.setStyleSheet(
"color: #666; "
"font-size: 7pt; "
"padding: 3px;"
)
self.info_label.setWordWrap(True)
self.info_label.setVisible(False) # 隐藏文件路径标签
def loadVideo(self, video_path, title=None, info_text=None):
"""
Args:
video_path:
title:
info_text:
"""
print(f"[HistoryPanel] ========== loadVideo ==========")
print(f"[HistoryPanel] : {video_path}")
print(f"[HistoryPanel] : {os.path.exists(video_path)}")
if not os.path.exists(video_path):
print(f"[HistoryPanel] : ")
QtWidgets.QMessageBox.warning(self, "", f":\n{video_path}")
return False
try:
#
if info_text:
self.info_label.setText(info_text)
print(f"[HistoryPanel] : {info_text}")
else:
self.info_label.setText(f": {video_path}")
print(f"[HistoryPanel] ")
#
abs_path = os.path.abspath(video_path)
print(f"[HistoryPanel] : {abs_path}")
video_url = QUrl.fromLocalFile(abs_path)
print(f"[HistoryPanel] URL: {video_url.toString()}")
print(f"[HistoryPanel] URL: {video_url.isValid()}")
media_content = QMediaContent(video_url)
print(f"[HistoryPanel] : {media_content}")
print(f"[HistoryPanel] : {media_content.isNull()}")
print(f"[HistoryPanel] ...")
self._media_player.setMedia(media_content)
print(f"[HistoryPanel] : {self._media_player.state()}")
print(f"[HistoryPanel] : {self._media_player.mediaStatus()}")
print(f"[HistoryPanel] : {self._media_player.error()}")
if self._media_player.error() != QMediaPlayer.NoError:
print(f"[HistoryPanel] : {self._media_player.errorString()}")
print(f"[HistoryPanel] ")
return True
except Exception as e:
print(f"[HistoryPanel] : {e}")
import traceback
traceback.print_exc()
QtWidgets.QMessageBox.warning(self, "", f":\n{str(e)}")
return False
def play(self):
""""""
self._media_player.play()
def pause(self):
""""""
self._media_player.pause()
def stop(self):
""""""
self._media_player.stop()
def _onPositionChanged(self, position):
""""""
if not self._is_seeking:
self.position_slider.setValue(position)
#
current_time = position // 1000
total_time = self._media_player.duration() // 1000
self.time_label.setText(
f"{current_time//60:02d}:{current_time%60:02d} / "
f"{total_time//60:02d}:{total_time%60:02d}"
)
def _onDurationChanged(self, duration):
""""""
self.position_slider.setRange(0, duration)
def _onStateChanged(self, state):
"""handler"""
#
pass
def _onError(self, error):
""""""
error_string = self._media_player.errorString()
print(f"[HistoryPanel] : {error_string}")
QtWidgets.QMessageBox.warning(self, "", f":\n{error_string}")
def _onSliderPressed(self):
""""""
self._is_seeking = True
def _onSliderReleased(self):
""""""
self._is_seeking = False
self._media_player.setPosition(self.position_slider.value())
def _onSliderMoved(self, position):
""""""
#
current_time = position // 1000
total_time = self._media_player.duration() // 1000
self.time_label.setText(
f"{current_time//60:02d}:{current_time%60:02d} / "
f"{total_time//60:02d}:{total_time%60:02d}"
)
def _onVolumeChanged(self, value):
""""""
self._media_player.setVolume(value)
def setStatistics(self, stats_dict):
"""
Args:
stats_dict: {
'total_frames': 100,
'detection_count': 33,
'success_count': 30,
'fail_count': 3,
'success_rate': 90.9
}
"""
#
stats_text = (
f": {stats_dict.get('total_frames', 0)} | "
f": {stats_dict.get('detection_count', 0)} | "
f": {stats_dict.get('success_count', 0)} | "
f": {stats_dict.get('fail_count', 0)} | "
f": {stats_dict.get('success_rate', 0):.1f}%"
)
#
if not hasattr(self, 'stats_label'):
#
stats_widget = QtWidgets.QWidget()
stats_widget.setStyleSheet(
"background: #f8f9fa; "
"border: 1px solid #dee2e6; "
"border-radius: 5px; "
"padding: 12px;"
)
stats_layout = QtWidgets.QVBoxLayout(stats_widget)
stats_layout.setContentsMargins(0, 0, 0, 0)
stats_title = QtWidgets.QLabel("")
stats_title.setStyleSheet(
"color: #333; "
"font-size: 10pt; "
"font-weight: bold; "
"background: transparent; "
"border: none; "
"padding: 0;"
)
stats_layout.addWidget(stats_title)
self.stats_label = QtWidgets.QLabel(stats_text)
self.stats_label.setStyleSheet(
"color: #666; "
"font-size: 8pt; "
"background: transparent; "
"border: none; "
"padding: 3px 0;"
)
stats_layout.addWidget(self.stats_label)
#
main_layout = self.layout()
main_layout.insertWidget(1, stats_widget)
else:
self.stats_label.setText(stats_text)
...@@ -5,6 +5,20 @@ from qtpy import QtCore ...@@ -5,6 +5,20 @@ from qtpy import QtCore
from qtpy import QtGui from qtpy import QtGui
from qtpy.QtCore import Qt from qtpy.QtCore import Qt
# 导入视频播放相关组件
try:
from qtpy.QtMultimedia import QMediaPlayer, QMediaContent
from qtpy.QtMultimediaWidgets import QVideoWidget
from qtpy.QtCore import QUrl
MULTIMEDIA_AVAILABLE = True
except ImportError:
print("⚠️ [HistoryVideoPanel] QtMultimedia 不可用,将使用 QLabel 作为后备")
MULTIMEDIA_AVAILABLE = False
QMediaPlayer = None
QMediaContent = None
QVideoWidget = None
QUrl = None
# 导入图标工具和响应式布局(支持相对导入和独立运行) # 导入图标工具和响应式布局(支持相对导入和独立运行)
try: try:
# 从父目录(widgets)导入 # 从父目录(widgets)导入
...@@ -35,9 +49,10 @@ class HistoryVideoPanel(QtWidgets.QWidget): ...@@ -35,9 +49,10 @@ class HistoryVideoPanel(QtWidgets.QWidget):
amplifyClicked = QtCore.Signal(str) # 放大按钮点击信号,传递channel_id amplifyClicked = QtCore.Signal(str) # 放大按钮点击信号,传递channel_id
channelNameChanged = QtCore.Signal(str, str) # 通道名称改变信号(channel_id, new_name) channelNameChanged = QtCore.Signal(str, str) # 通道名称改变信号(channel_id, new_name)
def __init__(self, title="历史视频", parent=None, debug_mode=False): def __init__(self, title="历史视频", parent=None, debug_mode=False, main_window=None):
super(HistoryVideoPanel, self).__init__(parent) super(HistoryVideoPanel, self).__init__(parent)
self._parent = parent self._parent = parent
self._main_window = main_window # 存储主窗口引用以访问 curvemission
self._title = title # 保存标题但不显示 self._title = title # 保存标题但不显示
self._channels = {} # 存储通道信息 {channel_id: channel_data} self._channels = {} # 存储通道信息 {channel_id: channel_data}
self._current_channel_id = None # 当前选中的通道ID self._current_channel_id = None # 当前选中的通道ID
...@@ -51,8 +66,8 @@ class HistoryVideoPanel(QtWidgets.QWidget): ...@@ -51,8 +66,8 @@ class HistoryVideoPanel(QtWidgets.QWidget):
def _initUI(self): def _initUI(self):
"""初始化UI布局 - 简洁设计,无底部按钮""" """初始化UI布局 - 简洁设计,无底部按钮"""
# 🔥 设置固定大小 - 响应式布局 # 🔥 设置固定大小 - 不使用响应式布局,直接使用固定尺寸
self.setFixedSize(scale_w(620), scale_h(465)) self.setFixedSize(620, 465)
# 设置黑色背景(QWidget需要autoFillBackground) # 设置黑色背景(QWidget需要autoFillBackground)
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
...@@ -64,46 +79,63 @@ class HistoryVideoPanel(QtWidgets.QWidget): ...@@ -64,46 +79,63 @@ class HistoryVideoPanel(QtWidgets.QWidget):
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0) layout.setSpacing(0)
# 视频显示区域(QLabel)- 占据整个面板 # 🔥 导入字体管理器(在if-else之前导入,避免作用域问题)
try:
from ..style_manager import FontManager
except ImportError:
from widgets.style_manager import FontManager
# 🔥 视频显示区域(使用 QVideoWidget 或 QLabel)
if MULTIMEDIA_AVAILABLE:
# 使用 QVideoWidget 显示视频
self.videoWidget = QVideoWidget()
self.videoWidget.setStyleSheet("background-color: black;")
self.videoWidget.setSizePolicy(
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding
)
# 创建媒体播放器
self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface)
self.mediaPlayer.setVideoOutput(self.videoWidget)
# 连接媒体播放器信号
self.mediaPlayer.stateChanged.connect(self._onMediaStateChanged)
self.mediaPlayer.positionChanged.connect(self._onMediaPositionChanged)
self.mediaPlayer.durationChanged.connect(self._onMediaDurationChanged)
self.mediaPlayer.error.connect(self._onMediaError)
# 使用 videoWidget 作为视频显示区域
self.videoLabel = self.videoWidget
print("✅ [HistoryVideoPanel] 使用 QVideoWidget 显示视频")
else:
# 后备方案:使用 QLabel
self.videoLabel = QtWidgets.QLabel() self.videoLabel = QtWidgets.QLabel()
self.videoLabel.setStyleSheet("background-color: black;") self.videoLabel.setStyleSheet("background-color: black;")
self.videoLabel.setAlignment(Qt.AlignCenter) self.videoLabel.setAlignment(Qt.AlignCenter)
self.videoLabel.setScaledContents(False) # 保持宽高比,不拉伸 self.videoLabel.setScaledContents(False)
# 设置videoLabel的尺寸策略,防止被拉伸
self.videoLabel.setSizePolicy( self.videoLabel.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding QtWidgets.QSizePolicy.Expanding
) )
self.videoLabel.setText("历史数据加载中...") self.videoLabel.setText("历史回放视频")
self.videoLabel.setFont(FontManager.getLargeFont())
self.videoLabel.setStyleSheet(""" self.videoLabel.setStyleSheet("""
QLabel { QLabel {
background-color: black; background-color: black;
color: white; color: white;
font-size: 14px;
} }
""") """)
# 创建名称显示标签(叠加在视频区域左上角) self.mediaPlayer = None
self.nameLabel = QtWidgets.QLabel(self.videoLabel) print("⚠️ [HistoryVideoPanel] 使用 QLabel 作为后备显示")
self.nameLabel.setText(self._title if self._title else "")
self.nameLabel.setStyleSheet("""
QLabel {
background-color: transparent;
color: white;
font-size: 12pt;
font-weight: bold;
padding: 5px 10px;
}
""")
self.nameLabel.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.nameLabel.adjustSize()
self.nameLabel.move(0, 0) # 左上角位置
self.nameLabel.setCursor(Qt.PointingHandCursor) # 设置鼠标指针为手型
# 安装事件过滤器以捕获双击事件 # 🔥 历史视频面板不显示通道名称标签
self.nameLabel.installEventFilter(self) # 创建名称显示标签(隐藏,保留用于兼容性)
self.nameLabel = QtWidgets.QLabel(self.videoLabel)
self.nameLabel.setText("")
self.nameLabel.hide() # 隐藏标签
# 创建debug模式静态文本控件(叠加在视频区域顶部中间,仅在debug模式下显示) # 创建debug模式静态文本控件(叠加在视频区域顶部中间,仅在debug模式下显示)
self.debugLabel = QtWidgets.QLabel(self.videoLabel) self.debugLabel = QtWidgets.QLabel(self.videoLabel)
...@@ -124,22 +156,11 @@ class HistoryVideoPanel(QtWidgets.QWidget): ...@@ -124,22 +156,11 @@ class HistoryVideoPanel(QtWidgets.QWidget):
# 根据debug_mode控制显示/隐藏 # 根据debug_mode控制显示/隐藏
self.debugLabel.setVisible(self._debug_mode) self.debugLabel.setVisible(self._debug_mode)
# 创建任务信息显示标签(叠加在视频区域右上角,与通道名称标签样式一致) # 🔥 历史视频面板不显示任务信息标签
# 创建任务信息显示标签(隐藏,保留用于兼容性)
self.taskLabel = QtWidgets.QLabel(self.videoLabel) self.taskLabel = QtWidgets.QLabel(self.videoLabel)
self.taskLabel.setText("历史数据") # 初始显示历史数据 self.taskLabel.setText("")
self.taskLabel.setStyleSheet(""" self.taskLabel.hide() # 隐藏标签
QLabel {
background-color: transparent;
color: white;
font-size: 12pt;
font-weight: bold;
padding: 5px 10px;
}
""")
self.taskLabel.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.taskLabel.adjustSize()
# 定位到右上角
self._positionTaskLabel()
# 创建名称编辑框(初始隐藏) # 创建名称编辑框(初始隐藏)
self.nameEdit = QtWidgets.QLineEdit(self.videoLabel) self.nameEdit = QtWidgets.QLineEdit(self.videoLabel)
...@@ -158,15 +179,132 @@ class HistoryVideoPanel(QtWidgets.QWidget): ...@@ -158,15 +179,132 @@ class HistoryVideoPanel(QtWidgets.QWidget):
self.nameEdit.hide() self.nameEdit.hide()
self.nameEdit.setMaxLength(50) # 限制最大长度 self.nameEdit.setMaxLength(50) # 限制最大长度
# 视频区域占据整个面板(无底部按钮) # 🔥 视频区域
layout.addWidget(self.videoLabel, 1) layout.addWidget(self.videoLabel, 1)
# 🔥 底部控件区域
bottom_widget = QtWidgets.QWidget()
bottom_layout = QtWidgets.QVBoxLayout(bottom_widget)
bottom_layout.setContentsMargins(5, 5, 5, 5)
bottom_layout.setSpacing(5)
# NVR地址输入框
nvr_layout = QtWidgets.QHBoxLayout()
nvr_layout.setSpacing(5)
nvr_label = QtWidgets.QLabel("NVR地址:")
nvr_label.setFont(FontManager.getMediumFont())
nvr_label.setStyleSheet("color: white;")
nvr_layout.addWidget(nvr_label)
self.nvrAddressEdit = QtWidgets.QLineEdit()
self.nvrAddressEdit.setPlaceholderText("请输入NVR地址...")
self.nvrAddressEdit.setFont(FontManager.getMediumFont())
self.nvrAddressEdit.setStyleSheet("""
QLineEdit {
background-color: #2b2b2b;
color: white;
border: 1px solid #555555;
border-radius: 3px;
padding: 5px;
}
QLineEdit:focus {
border: 1px solid #4682B4;
}
""")
nvr_layout.addWidget(self.nvrAddressEdit, 1)
bottom_layout.addLayout(nvr_layout)
# 进度条
progress_layout = QtWidgets.QHBoxLayout()
progress_layout.setSpacing(5)
# 播放/暂停按钮(使用图标)
self.btnPlayPause = QtWidgets.QPushButton()
self.btnPlayPause.setIcon(newIcon("开始")) # 初始为播放图标
self.btnPlayPause.setIconSize(QtCore.QSize(24, 24))
self.btnPlayPause.setFixedSize(32, 32)
self.btnPlayPause.setToolTip("播放")
self.btnPlayPause.setStyleSheet("""
QPushButton {
background-color: transparent;
border: 1px solid #555555;
border-radius: 4px;
}
QPushButton:hover {
background-color: rgba(70, 130, 180, 0.3);
border: 1px solid #4682B4;
}
QPushButton:pressed {
background-color: rgba(70, 130, 180, 0.5);
}
""")
self._is_playing = False # 播放状态标志
progress_layout.addWidget(self.btnPlayPause)
self.progressSlider = QtWidgets.QSlider(Qt.Horizontal)
self.progressSlider.setMinimum(0)
self.progressSlider.setMaximum(100)
self.progressSlider.setValue(0)
self.progressSlider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #555555;
height: 8px;
background: #2b2b2b;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #4682B4;
border: 1px solid #4682B4;
width: 16px;
margin: -4px 0;
border-radius: 8px;
}
QSlider::handle:horizontal:hover {
background: #5a9fd4;
}
QSlider::sub-page:horizontal {
background: #4682B4;
border-radius: 4px;
}
""")
progress_layout.addWidget(self.progressSlider, 1)
self.progressLabel = QtWidgets.QLabel("00:00 / 00:00")
self.progressLabel.setFont(FontManager.getMediumFont())
self.progressLabel.setStyleSheet("color: white;")
self.progressLabel.setMinimumWidth(100)
progress_layout.addWidget(self.progressLabel)
bottom_layout.addLayout(progress_layout)
# 设置底部控件区域的背景色
bottom_widget.setStyleSheet("""
QWidget {
background-color: #1e1e1e;
}
""")
bottom_widget.setFixedHeight(80)
layout.addWidget(bottom_widget)
def _connectSignals(self): def _connectSignals(self):
"""连接信号槽""" """连接信号槽"""
# 连接名称编辑框的信号 # 连接名称编辑框的信号
self.nameEdit.returnPressed.connect(self._onNameEditFinished) self.nameEdit.returnPressed.connect(self._onNameEditFinished)
self.nameEdit.editingFinished.connect(self._onNameEditFinished) self.nameEdit.editingFinished.connect(self._onNameEditFinished)
# 连接播放/暂停按钮信号
if hasattr(self, 'btnPlayPause'):
self.btnPlayPause.clicked.connect(self._onPlayPauseClicked)
# 连接进度条信号
if hasattr(self, 'progressSlider'):
self.progressSlider.sliderPressed.connect(self._onProgressSliderPressed)
self.progressSlider.sliderReleased.connect(self._onProgressSliderReleased)
self.progressSlider.valueChanged.connect(self._onProgressValueChanged)
def addChannel(self, channel_id, channel_data): def addChannel(self, channel_id, channel_data):
""" """
添加通道 添加通道
...@@ -356,8 +494,237 @@ class HistoryVideoPanel(QtWidgets.QWidget): ...@@ -356,8 +494,237 @@ class HistoryVideoPanel(QtWidgets.QWidget):
self.debugLabel.adjustSize() self.debugLabel.adjustSize()
self._positionDebugLabel() self._positionDebugLabel()
def _onPlayPauseClicked(self):
"""播放/暂停按钮点击"""
if not self.mediaPlayer:
print(f"⚠️ [HistoryVideoPanel] 媒体播放器不可用")
return
# 🔥 检查是否已加载视频
if self.mediaPlayer.media().isNull():
# 没有加载视频,尝试从 curvemission 加载
task_name = self._getCurrentMissionFromParent()
if task_name:
print(f"📹 [HistoryVideoPanel] 未加载视频,尝试从任务加载: {task_name}")
success = self.loadVideoFromTask(task_name)
if not success:
print(f"❌ [HistoryVideoPanel] 无法加载视频,任务: {task_name}")
return
else:
print(f"⚠️ [HistoryVideoPanel] 未设置任务,无法加载视频")
return
# 根据当前播放状态切换
if self.mediaPlayer.state() == QMediaPlayer.PlayingState:
# 当前正在播放,暂停
self.mediaPlayer.pause()
print(f"⏸️ [HistoryVideoPanel] 暂停播放")
else:
# 当前暂停或停止,开始播放
self.mediaPlayer.play()
print(f"▶️ [HistoryVideoPanel] 开始播放")
def _onMediaStateChanged(self, state):
"""媒体播放器状态改变"""
if state == QMediaPlayer.PlayingState:
# 播放中,显示暂停图标
self.btnPlayPause.setIcon(newIcon("停止1"))
self.btnPlayPause.setToolTip("暂停")
self._is_playing = True
else:
# 暂停或停止,显示播放图标
self.btnPlayPause.setIcon(newIcon("开始"))
self.btnPlayPause.setToolTip("播放")
self._is_playing = False
def _onMediaPositionChanged(self, position):
"""媒体播放位置改变"""
if not self.mediaPlayer:
return
duration = self.mediaPlayer.duration()
if duration > 0:
# 更新进度条(避免用户拖动时的冲突)
if not self.progressSlider.isSliderDown():
progress = int(position * 100 / duration)
self.progressSlider.setValue(progress)
# 更新时间显示
current_seconds = position // 1000
total_seconds = duration // 1000
current_time = f"{current_seconds // 60:02d}:{current_seconds % 60:02d}"
total_time = f"{total_seconds // 60:02d}:{total_seconds % 60:02d}"
if hasattr(self, 'progressLabel'):
self.progressLabel.setText(f"{current_time} / {total_time}")
def _onMediaDurationChanged(self, duration):
"""媒体总时长改变"""
if duration > 0:
total_seconds = duration // 1000
total_time = f"{total_seconds // 60:02d}:{total_seconds % 60:02d}"
print(f"📹 [HistoryVideoPanel] 视频总时长: {total_time}")
def _onMediaError(self, error):
"""媒体播放错误"""
if self.mediaPlayer:
error_string = self.mediaPlayer.errorString()
print(f"❌ [HistoryVideoPanel] 播放错误: {error_string}")
def _onProgressSliderPressed(self):
"""进度条按下"""
print(f"[HistoryVideoPanel] 进度条被按下")
def _onProgressSliderReleased(self):
"""进度条释放"""
if not self.mediaPlayer:
return
value = self.progressSlider.value()
duration = self.mediaPlayer.duration()
if duration > 0:
# 计算目标位置(毫秒)
position = int(duration * value / 100)
self.mediaPlayer.setPosition(position)
print(f"⏩ [HistoryVideoPanel] 跳转到: {value}% (位置: {position}ms)")
def _onProgressValueChanged(self, value):
"""进度条值改变(用户拖动时)"""
# 只在用户拖动时更新显示,播放时由 _onMediaPositionChanged 更新
if self.progressSlider.isSliderDown() and self.mediaPlayer:
duration = self.mediaPlayer.duration()
if duration > 0:
current_seconds = int(duration * value / 100) // 1000
total_seconds = duration // 1000
current_time = f"{current_seconds // 60:02d}:{current_seconds % 60:02d}"
total_time = f"{total_seconds // 60:02d}:{total_seconds % 60:02d}"
if hasattr(self, 'progressLabel'):
self.progressLabel.setText(f"{current_time} / {total_time}")
def setProgress(self, value):
"""设置进度条值(0-100)"""
if hasattr(self, 'progressSlider'):
self.progressSlider.setValue(value)
def setPlaying(self, is_playing):
"""
设置播放状态(外部调用)
Args:
is_playing: True表示播放中,False表示暂停
"""
self._is_playing = is_playing
if hasattr(self, 'btnPlayPause'):
if is_playing:
self.btnPlayPause.setIcon(newIcon("停止1"))
self.btnPlayPause.setToolTip("暂停")
else:
self.btnPlayPause.setIcon(newIcon("开始"))
self.btnPlayPause.setToolTip("播放")
def isPlaying(self):
"""
获取当前播放状态
Returns:
bool: True表示播放中,False表示暂停
"""
return self._is_playing
def loadVideo(self, video_path):
"""
加载视频文件
Args:
video_path: 视频文件路径
"""
if not self.mediaPlayer:
print(f"⚠️ [HistoryVideoPanel] 媒体播放器不可用,无法加载视频")
return False
import os
if not os.path.exists(video_path):
print(f"❌ [HistoryVideoPanel] 视频文件不存在: {video_path}")
return False
# 设置媒体内容
media_url = QUrl.fromLocalFile(video_path)
media_content = QMediaContent(media_url)
self.mediaPlayer.setMedia(media_content)
print(f"📹 [HistoryVideoPanel] 已加载视频: {video_path}")
return True
def loadVideoFromTask(self, task_folder_name):
"""
从任务文件夹加载视频
Args:
task_folder_name: 任务文件夹名称(如 "1_2")
"""
if not task_folder_name or task_folder_name == "None":
print(f"⚠️ [HistoryVideoPanel] 无效的任务名称: {task_folder_name}")
return False
import os
import sys
# 获取项目根目录
if getattr(sys, 'frozen', False):
project_root = os.path.dirname(sys.executable)
else:
try:
from database.config import get_project_root
project_root = get_project_root()
except ImportError:
project_root = os.getcwd()
# 构建任务文件夹路径
task_folder = os.path.join(project_root, 'database', 'mission_result', task_folder_name)
if not os.path.exists(task_folder):
print(f"❌ [HistoryVideoPanel] 任务文件夹不存在: {task_folder}")
return False
# 查找视频文件(支持常见格式)
video_extensions = ['.mp4', '.avi', '.mkv', '.mov', '.flv', '.wmv']
video_files = []
for file in os.listdir(task_folder):
file_lower = file.lower()
if any(file_lower.endswith(ext) for ext in video_extensions):
video_files.append(os.path.join(task_folder, file))
if not video_files:
print(f"⚠️ [HistoryVideoPanel] 任务文件夹中没有找到视频文件: {task_folder}")
return False
# 加载第一个视频文件
video_path = video_files[0]
return self.loadVideo(video_path)
def setNvrAddress(self, address):
"""设置NVR地址"""
if hasattr(self, 'nvrAddressEdit'):
self.nvrAddressEdit.setText(address)
def getNvrAddress(self):
"""获取NVR地址"""
if hasattr(self, 'nvrAddressEdit'):
return self.nvrAddressEdit.text()
return ""
def setTaskInfo(self, task_folder_name): def setTaskInfo(self, task_folder_name):
"""设置任务信息显示""" """
设置任务信息显示(仅用于显示,不存储任务名称)
Args:
task_folder_name: 任务文件夹名称
"""
if task_folder_name and task_folder_name != "None": if task_folder_name and task_folder_name != "None":
self.taskLabel.setText(task_folder_name) self.taskLabel.setText(task_folder_name)
print(f"[HistoryVideoPanel.setTaskInfo] 显示任务信息: {task_folder_name}") print(f"[HistoryVideoPanel.setTaskInfo] 显示任务信息: {task_folder_name}")
...@@ -365,7 +732,7 @@ class HistoryVideoPanel(QtWidgets.QWidget): ...@@ -365,7 +732,7 @@ class HistoryVideoPanel(QtWidgets.QWidget):
self._setDisabled(False) self._setDisabled(False)
else: else:
self.taskLabel.setText("历史数据") self.taskLabel.setText("历史数据")
print(f"[HistoryVideoPanel.setTaskInfo] 任务为空,显示'历史数据',输入参数: task_folder_name={task_folder_name}") print(f"[HistoryVideoPanel.setTaskInfo] 任务为空,显示'历史数据'")
# 清空任务时禁用面板 # 清空任务时禁用面板
self._setDisabled(True) self._setDisabled(True)
...@@ -382,18 +749,29 @@ class HistoryVideoPanel(QtWidgets.QWidget): ...@@ -382,18 +749,29 @@ class HistoryVideoPanel(QtWidgets.QWidget):
self.taskLabel.adjustSize() self.taskLabel.adjustSize()
self._positionTaskLabel() self._positionTaskLabel()
def getTaskInfo(self): def _getCurrentMissionFromParent(self):
""" """
获取当前显示的任务文件夹名称 从主窗口的 curvemission 获取当前任务名称
Returns: Returns:
str: 任务文件夹名称,如果不存在或为"None"或"历史数据"则返回None str: 任务文件夹名称,如果不存在或为"请选择任务"则返回None
""" """
text = self.taskLabel.text() try:
if not text or text == "None" or text == "历史数据": # 🔥 优先使用主窗口引用
if self._main_window and hasattr(self._main_window, 'curvemission'):
mission_name = self._main_window.curvemission.currentText()
if mission_name and mission_name != "请选择任务":
print(f"🔍 [HistoryVideoPanel] 从 curvemission 获取任务: {mission_name}")
return mission_name
else:
print(f"⚠️ [HistoryVideoPanel] curvemission 未选择任务")
return None return None
return text.strip() print(f"⚠️ [HistoryVideoPanel] 未设置主窗口引用")
return None
except Exception as e:
print(f"❌ [HistoryVideoPanel] 获取任务名称失败: {e}")
return None
def _setDisabled(self, disabled): def _setDisabled(self, disabled):
"""设置面板禁用状态""" """设置面板禁用状态"""
......
...@@ -41,9 +41,11 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -41,9 +41,11 @@ class MissionPanel(QtWidgets.QWidget):
""" """
# ========== 固定配置(修改这里即可全局同步) ========== # ========== 固定配置(修改这里即可全局同步) ==========
DEFAULT_COLUMNS = ['任务编号', '任务名称', '状态', '通道', '曲线'] DEFAULT_COLUMNS = ['任务编号', '任务名称', '状态', '通道1', '通道2', '通道3', '通道4', '曲线']
DEFAULT_WIDTHS = [80, 140, 70, 210, 50] DEFAULT_WIDTHS = [80, 140, 70, 50, 50, 50, 50, 50]
CURVE_BUTTON_COLUMN = 4 # 曲线按钮所在列(使用动态曲线图标) CURVE_BUTTON_COLUMN = 7 # 曲线按钮所在列(使用动态曲线图标)
CHANNEL_START_COLUMN = 3 # 通道列开始索引
CHANNEL_COUNT = 4 # 通道数量
# ==================================================== # ====================================================
# 自定义信号 # 自定义信号
...@@ -158,6 +160,13 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -158,6 +160,13 @@ class MissionPanel(QtWidgets.QWidget):
self.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) # 禁用选择,取消高亮 self.table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) # 禁用选择,取消高亮
self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # 默认不可编辑 self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # 默认不可编辑
# 🔥 启用右键菜单
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self._showContextMenu)
# 🔥 待高亮的行索引(用于延迟高亮,等待用户确认)
self._pending_highlight_row = None
# 安装事件过滤器,拦截单击事件(只允许双击选中) # 安装事件过滤器,拦截单击事件(只允许双击选中)
self.table.viewport().installEventFilter(self) self.table.viewport().installEventFilter(self)
...@@ -637,6 +646,41 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -637,6 +646,41 @@ class MissionPanel(QtWidgets.QWidget):
"""手动刷新显示(用于批量操作后)""" """手动刷新显示(用于批量操作后)"""
self._updatePagination() self._updatePagination()
def setCellTextColor(self, row, col, color):
"""
设置指定单元格的文本颜色
Args:
row: 行索引(在全部数据中的索引)
col: 列索引
color: 颜色(QColor或颜色字符串,如'#00FF00')
"""
# 检查行索引是否有效
if row < 0 or row >= len(self._all_rows_data):
print(f"⚠️ [setCellTextColor] 行索引无效: row={row}, 总行数={len(self._all_rows_data)}")
return
# 计算在当前页面中的行索引
start_idx = (self._current_page - 1) * self._page_size
end_idx = start_idx + self._page_size
print(f"🔍 [setCellTextColor] 行{row} 列{col}, 当前页={self._current_page}, 页面范围=[{start_idx}, {end_idx})")
# 检查该行是否在当前页面中
if start_idx <= row < end_idx:
table_row = row - start_idx
item = self.table.item(table_row, col)
if item:
if isinstance(color, str):
item.setForeground(QtGui.QColor(color))
else:
item.setForeground(color)
print(f"✅ [setCellTextColor] 已设置 行{row}(表格行{table_row}) 列{col} 颜色={color}, 文本={item.text()}")
else:
print(f"⚠️ [setCellTextColor] 单元格为空: 行{row}(表格行{table_row}) 列{col}")
else:
print(f"⚠️ [setCellTextColor] 行{row}不在当前页面中")
def _addRowToTable(self, row_data, user_data=None, button_callback=None): def _addRowToTable(self, row_data, user_data=None, button_callback=None):
""" """
实际添加一行到表格(内部方法)- 使用纯QTableWidgetItem方案 实际添加一行到表格(内部方法)- 使用纯QTableWidgetItem方案
...@@ -771,7 +815,7 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -771,7 +815,7 @@ class MissionPanel(QtWidgets.QWidget):
删除指定行 删除指定行
Args: Args:
row_index: 行索引 row_index: 行索引(当前页面的行索引,不是全局索引)
Returns: Returns:
bool: 是否成功 bool: 是否成功
...@@ -779,7 +823,24 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -779,7 +823,24 @@ class MissionPanel(QtWidgets.QWidget):
if row_index < 0 or row_index >= self.table.rowCount(): if row_index < 0 or row_index >= self.table.rowCount():
return False return False
# 🔥 计算全局索引(考虑分页)
global_index = (self._current_page - 1) * self._page_size + row_index
# 🔥 从分页数据中删除
if 0 <= global_index < len(self._filtered_rows_data):
# 从过滤数据中删除
removed_item = self._filtered_rows_data.pop(global_index)
# 从全部数据中删除(需要找到对应项)
if removed_item in self._all_rows_data:
self._all_rows_data.remove(removed_item)
# 🔥 从表格UI中删除
self.table.removeRow(row_index) self.table.removeRow(row_index)
# 🔥 更新分页显示
self._updatePagination()
return True return True
def getRowData(self, row_index): def getRowData(self, row_index):
...@@ -1004,6 +1065,53 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -1004,6 +1065,53 @@ class MissionPanel(QtWidgets.QWidget):
# 设置列宽(所有列使用固定宽度) # 设置列宽(所有列使用固定宽度)
self.setColumnWidths(self.DEFAULT_WIDTHS) self.setColumnWidths(self.DEFAULT_WIDTHS)
# 🔥 合并通道列的表头(第一行显示"通道",覆盖4个子列)
self._mergeChannelHeader()
def _mergeChannelHeader(self):
"""
合并通道列的表头
将"通道1"、"通道2"、"通道3"、"通道4"的表头合并显示为"通道"
"""
# 🔥 创建自定义表头标签
# 由于QTableWidget不支持直接合并表头单元格,我们使用一个技巧:
# 在表头上方放置一个QLabel来显示"通道"
# 获取表头
header = self.table.horizontalHeader()
# 计算通道列的总宽度
channel_total_width = sum(self.DEFAULT_WIDTHS[self.CHANNEL_START_COLUMN:self.CHANNEL_START_COLUMN + self.CHANNEL_COUNT])
# 计算通道列的起始位置
channel_start_pos = sum(self.DEFAULT_WIDTHS[:self.CHANNEL_START_COLUMN])
# 创建一个QLabel显示"通道"
if not hasattr(self, '_channel_header_label'):
self._channel_header_label = QtWidgets.QLabel("通道", self.table)
self._channel_header_label.setAlignment(Qt.AlignCenter)
self._channel_header_label.setStyleSheet("""
QLabel {
background-color: #f0f0f0;
border: 1px solid #d0d0d0;
font-weight: bold;
padding: 5px;
}
""")
# 应用全局字体
FontManager.applyToWidget(self._channel_header_label)
# 设置位置和大小
self._channel_header_label.setGeometry(
channel_start_pos,
0,
channel_total_width,
header.height()
)
self._channel_header_label.show()
self._channel_header_label.raise_() # 确保在最上层
def _onItemDoubleClicked(self, item): def _onItemDoubleClicked(self, item):
...@@ -1019,10 +1127,9 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -1019,10 +1127,9 @@ class MissionPanel(QtWidgets.QWidget):
user_data = self.getUserData(row) user_data = self.getUserData(row)
if user_data: if user_data:
print(f"🔍 [调试] 发送任务选中信号: {user_data}") print(f"🔍 [调试] 发送任务选中信号: {user_data}")
# 🔥 保存当前双击的行索引,等待handler确认后再置黑
self._pending_highlight_row = row
self.taskSelected.emit(user_data) self.taskSelected.emit(user_data)
# 🔥 只对当前双击的任务行进行文字置黑
self._highlightSelectedRow(row)
else: else:
print(f"🔍 [调试] 警告:行{row}没有用户数据") print(f"🔍 [调试] 警告:行{row}没有用户数据")
...@@ -1043,6 +1150,27 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -1043,6 +1150,27 @@ class MissionPanel(QtWidgets.QWidget):
self.taskSelected.emit(user_data) self.taskSelected.emit(user_data)
print(f"✅ [任务分配] 手动分配任务: {user_data}") print(f"✅ [任务分配] 手动分配任务: {user_data}")
def confirmTaskAssignment(self):
"""
确认任务分配(由handler调用)
当用户在确认对话框中点击"确认"后,handler调用此方法来高亮选中的行
"""
if self._pending_highlight_row is not None:
self._highlightSelectedRow(self._pending_highlight_row)
self._pending_highlight_row = None
print(f"✅ [任务分配] 已确认并高亮任务行")
def cancelTaskAssignment(self):
"""
取消任务分配(由handler调用)
当用户在确认对话框中点击"取消"后,handler调用此方法来清除待高亮状态
"""
if self._pending_highlight_row is not None:
print(f"❌ [任务分配] 用户取消,不高亮行 {self._pending_highlight_row}")
self._pending_highlight_row = None
def _highlightSelectedRow(self, selected_row): def _highlightSelectedRow(self, selected_row):
"""简化高亮逻辑:只将双击行文字颜色置黑""" """简化高亮逻辑:只将双击行文字颜色置黑"""
try: try:
...@@ -1121,6 +1249,65 @@ class MissionPanel(QtWidgets.QWidget): ...@@ -1121,6 +1249,65 @@ class MissionPanel(QtWidgets.QWidget):
pass pass
self.channelManageClicked.emit() self.channelManageClicked.emit()
def _showContextMenu(self, pos):
"""
显示右键菜单
Args:
pos: 鼠标位置
"""
# 获取点击位置的item
item = self.table.itemAt(pos)
if not item:
return
# 获取行索引
row = item.row()
# 创建右键菜单
menu = QtWidgets.QMenu(self)
# 添加删除任务选项
delete_action = menu.addAction("删除任务")
delete_action.setIcon(newIcon("删除"))
# 显示菜单并获取选中的动作
action = menu.exec_(self.table.viewport().mapToGlobal(pos))
# 处理选中的动作
if action == delete_action:
self._deleteTaskAtRow(row)
def _deleteTaskAtRow(self, row):
"""
删除指定行的任务
Args:
row: 行索引
"""
# 获取任务信息
task_id_item = self.table.item(row, 0)
task_name_item = self.table.item(row, 1)
if not task_id_item or not task_name_item:
return
task_id = task_id_item.text()
task_name = task_name_item.text()
# 🔥 使用全局对话框管理器显示警告询问对话框
message = f"确定要删除任务 [{task_id}_{task_name}] 吗?\n\n此操作将删除任务配置文件和所有相关数据,且无法恢复!"
if DialogManager.show_question_warning(
self,
"确认删除",
message,
yes_text="是",
no_text="否"
):
# 发射删除任务信号,由handler处理实际的删除逻辑
self.removeTaskRequested.emit(row)
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
"""事件过滤器 - 处理Delete键删除 + 调试按钮的左右键点击 + 拦截表格单击""" """事件过滤器 - 处理Delete键删除 + 调试按钮的左右键点击 + 拦截表格单击"""
# 处理表格的键盘事件(Delete键删除选中行) # 处理表格的键盘事件(Delete键删除选中行)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment