Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
O
Oil_Level_Recognition_System
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Oil_Level_Recognition_System
Commits
13c76314
Commit
13c76314
authored
Nov 26, 2025
by
Yuhaibo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
修改
parent
f6177d04
Show whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
1498 additions
and
879 deletions
+1498
-879
app.py
app.py
+225
-62
app.py
handlers/app.py
+15
-48
test123.py
handlers/test123.py
+0
-4
channelpanel_handler.py
handlers/videopage/channelpanel_handler.py
+6
-7
curvepanel_handler.py
handlers/videopage/curvepanel_handler.py
+74
-20
detection.py
handlers/videopage/detection.py
+0
-44
missionpanel_handler.py
handlers/videopage/missionpanel_handler.py
+256
-73
simple_scheduler.py
handlers/videopage/thread_manager/simple_scheduler.py
+0
-7
thread_manager.py
handlers/videopage/thread_manager/thread_manager.py
+101
-13
curve_thread.py
handlers/videopage/thread_manager/threads/curve_thread.py
+52
-4
global_detection_thread.py
...deopage/thread_manager/threads/global_detection_thread.py
+0
-10
view_handler.py
handlers/view_handler.py
+108
-116
__init__.py
widgets/__init__.py
+0
-1
style_manager.py
widgets/style_manager.py
+3
-2
__init__.py
widgets/videopage/__init__.py
+3
-2
channelpanel.py
widgets/videopage/channelpanel.py
+5
-77
curvepanel.py
widgets/videopage/curvepanel.py
+27
-2
historypanel.py
widgets/videopage/historypanel.py
+0
-329
historyvideopanel.py
widgets/videopage/historyvideopanel.py
+429
-51
missionpanel.py
widgets/videopage/missionpanel.py
+194
-7
No files found.
app.py
View file @
13c76314
...
@@ -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
:
...
...
handlers/app.py
View file @
13c76314
...
@@ -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
}
}
# 首先隐藏所有通道
# 首先隐藏所有通道
...
...
handlers/test123.py
deleted
100644 → 0
View file @
f6177d04
qweqwrfwadfsafa
duaisfdhuahofhaofhoa
yuhaibo
\ No newline at end of file
handlers/videopage/channelpanel_handler.py
View file @
13c76314
...
@@ -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
):
...
...
handlers/videopage/curvepanel_handler.py
View file @
13c76314
...
@@ -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
handlers/videopage/detection.py
View file @
13c76314
...
@@ -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
):
...
...
handlers/videopage/missionpanel_handler.py
View file @
13c76314
...
@@ -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
()
...
...
handlers/videopage/thread_manager/simple_scheduler.py
View file @
13c76314
...
@@ -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
:
...
...
handlers/videopage/thread_manager/thread_manager.py
View file @
13c76314
...
@@ -68,7 +68,7 @@ class ChannelThreadManager:
...
@@ -68,7 +68,7 @@ class ChannelThreadManager:
# 曲线模式标记(由外部设置,用于检测线程启动时自动启动曲线线程)
# 曲线模式标记(由外部设置,用于检测线程启动时自动启动曲线线程)
self
.
is_curve_mode
=
False
self
.
is_curve_mode
=
False
# 主窗口引用(用于访问 cur
rent_
mission)
# 主窗口引用(用于访问 cur
ve
mission)
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
()
...
...
handlers/videopage/thread_manager/threads/curve_thread.py
View file @
13c76314
...
@@ -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 获取曲线路径
# 从
curve
mission 获取曲线路径
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
handlers/videopage/thread_manager/threads/global_detection_thread.py
View file @
13c76314
...
@@ -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
...
...
handlers/view_handler.py
View file @
13c76314
...
@@ -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
# 从 cur
rent_mission 变量读
取当前任务目录
# 从 cur
vemission 获
取当前任务目录
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 获取任务路径并设置到曲线面板(文本框会自动提取文件夹名称)"""
"""从
curve
mission 获取任务路径并设置到曲线面板(文本框会自动提取文件夹名称)"""
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
# 其他情况视为有效
# 其他情况视为有效
...
...
widgets/__init__.py
View file @
13c76314
...
@@ -8,7 +8,6 @@ from .videopage import (
...
@@ -8,7 +8,6 @@ from .videopage import (
CurvePanel
,
CurvePanel
,
MissionPanel
,
MissionPanel
,
ModelSettingDialog
,
ModelSettingDialog
,
HistoryPanel
,
)
)
# 数据集页面组件(从 datasetpage 子模块导入)
# 数据集页面组件(从 datasetpage 子模块导入)
...
...
widgets/style_manager.py
View file @
13c76314
...
@@ -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:
5
0px;
min-height:
6
0px;
qproperty-alignment: 'AlignCenter';
qproperty-alignment: 'AlignCenter';
}
}
"""
"""
...
...
widgets/videopage/__init__.py
View file @
13c76314
...
@@ -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'
,
'History
Video
Panel'
,
'RtspDialog'
,
'RtspDialog'
,
'GeneralSetPanel'
,
'GeneralSetPanel'
,
'GeneralSetDialog'
,
'GeneralSetDialog'
,
...
...
widgets/videopage/channelpanel.py
View file @
13c76314
...
@@ -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__"
:
...
...
widgets/videopage/curvepanel.py
View file @
13c76314
...
@@ -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
(
8
0
))
# 🔥 响应式宽度
self
.
spn_upper_limit
.
setFixedWidth
(
scale_w
(
8
5
))
# 🔥 响应式宽度
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
(
8
0
))
# 🔥 响应式宽度
self
.
spn_lower_limit
.
setFixedWidth
(
scale_w
(
8
5
))
# 🔥 响应式宽度
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创建)
...
...
widgets/videopage/historypanel.py
deleted
100644 → 0
View file @
f6177d04
"""
"""
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
)
widgets/videopage/historyvideopanel.py
View file @
13c76314
...
@@ -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
):
"""设置面板禁用状态"""
"""设置面板禁用状态"""
...
...
widgets/videopage/missionpanel.py
View file @
13c76314
...
@@ -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键删除选中行)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment