大屏分屏控制软件
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

742 lines
29 KiB

# main.py
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenu, QAction,QActionGroup,
QWidget, QGridLayout, QLabel, QRubberBand)
from PyQt5.QtCore import QObject, Qt, pyqtSignal, QPoint, QRect, QSize
from PyQt5.QtGui import QMouseEvent, QCursor
from core.preset_manager import PresetManager
from panel.preset_panel import PresetPanel
from panel.log_panel import LogPanel
# 如果你上面用了纯Python写法没用PyQt信号,这里还需要:
from utils.logger import logger
from core.serial_protocol import SerialProtocol
from panel.connection_panel import ConnectionPanel
from utils.globals import get_serial_manager
# 导入 UI
from ui.ui_main import Ui_MainWindow
# 导入核心控制器(假设你已经写好了)
from core.device_manager import MatrixController
class ScreenLabel(QLabel):
split_solution = []
click_labels = []
big_pic_id = 0
"""自定义 Label,增加了右键信号"""
# 定义一个右键信号,携带当前屏幕的 ID
rightClicked = pyqtSignal(int)
def __init__(self, screen_id, text=""):
super().__init__(text)
self.screen_id = screen_id
self.is_selected = False
self.init_style()
def init_style(self):
"""初始化样式:灰色背景"""
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("""
background-color: rgb(85, 85, 85);
color: white;
border: 1px solid rgb(50, 50, 50);
font-weight: bold;
""")
def mousePressEvent(self, event: QMouseEvent):
"""
设置鼠标点击动作
"""
if event.button() == Qt.RightButton:
# 发射右键信号
self.rightClicked.emit(self.screen_id)
# if event.button() == Qt.LeftButton:
# if ScreenLabel.split_solution[self.screen_id - 1]['status'] < 2:
# # 切换选中状态
# self.is_selected = not self.is_selected
# self.update_style()
#
# # 这里可以 emit 一个信号告诉主窗口
# elif event.button() == Qt.RightButton:
# # 发射右键信号
# self.rightClicked.emit(self.screen_id)
def set_selected(self, selected):
"""
设置屏幕控件的选中状态
:param selected: bool, True为选中(高亮),False为取消选中(正常)
"""
self.is_selected = selected # 保存状态
if self.is_selected:
if self.screen_id not in ScreenLabel.click_labels:
ScreenLabel.click_labels.append(self.screen_id)
ScreenLabel.split_solution[self.screen_id - 1]['status'] = 1
self.update_style(1)
else:
if self.screen_id in ScreenLabel.click_labels:
ScreenLabel.click_labels.remove(self.screen_id)
if ScreenLabel.split_solution[self.screen_id - 1]:
ScreenLabel.split_solution[self.screen_id - 1]['status'] = 0
self.update_style()
# 强制刷新界面
self.update()
def update_style(self, status = 0):
if status == 0:
self.init_style()
elif status == 1:
# 选中状态:蓝色背景
self.setStyleSheet("""
background-color: rgb(0, 170, 255); /* 蓝色背景 */
color: white;
border: 2px solid yellow;
font-weight: bold;
""")
elif status == 2:
# 锁定状态:绿色背景
self.setStyleSheet("""
background-color: rgb(0, 255, 127); /* 绿色背景 */
color: black;
border: 2px solid white;
font-weight: bold;
""")
# if self.is_selected:
#
# if self.screen_id not in ScreenLabel.click_labels:
# ScreenLabel.click_labels.append(self.screen_id)
#
# ScreenLabel.split_solution[self.screen_id - 1]['status'] = 1
# # 选中状态:蓝色背景
# self.setStyleSheet("""
# background-color: rgb(0, 170, 255);
# color: white;
# border: 2px solid yellow;
# font-weight: bold;
# """)
# else:
# if self.screen_id in ScreenLabel.click_labels:
# ScreenLabel.click_labels.remove(self.screen_id)
#
# if ScreenLabel.split_solution[self.screen_id - 1]:
# ScreenLabel.split_solution[self.screen_id - 1]['status'] = 0
#
# self.init_style()
class MatrixControlApp(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
# --- 核心控制器 ---
self.matrix = MatrixController()
# --- 屏幕管理 ---
# 假设你在 Designer 里把 12 个 Label 的名字分别命名为 screen_1 到 screen_12
self.screen_labels = []
# --- 新增:初始化屏幕列表 ---
self.screen_widgets_list = []
self.init_screen_widgets()
self.serial_protocol = SerialProtocol()
# 1. 初始化预案管理器
self.preset_manager = PresetManager()
# 2. 初始化预案面板
self.preset_panel = PresetPanel(self.preset_manager, self)
# 3. 将面板添加到主窗口布局中
self.main_layout.addWidget(self.preset_panel)
# --- 新增:用于框选的变量 ---
self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.widget_container)
self.rubber_band.setStyleSheet("border: 2px solid red; background-color: rgba(255, 0, 0, 50);")
self.origin = QPoint()
# --- 新增:连接容器的鼠标事件 ---
self.widget_container.mousePressEvent = self.mouse_press_event
self.widget_container.mouseMoveEvent = self.mouse_move_event
self.widget_container.mouseReleaseEvent = self.mouse_release_event
# --- 1. 初始化串口管理器 ---
# 注意:这里我们把串口管理器作为实例属性,方便全局调用
# from core.connection_manager import SerialManager
# self.serial_manager = SerialManager()
# --- 修改这里:获取全局实例,而不是自己 new ---
self.serial_manager = get_serial_manager()
# 初始化连接面板时,也传这个实例进去
self.conn_panel = ConnectionPanel(self.serial_manager)
# ... 初始化 screen_widgets_list 等逻辑 ...
# --- 2. 创建面板实例 ---
# self.conn_panel = ConnectionPanel()
# --- 3. 将面板加入布局 ---
# 建议放在 PresetPanel 上方
self.main_layout.addWidget(self.conn_panel) # 新增:连接面板
self.main_layout.addWidget(self.preset_panel)
# --- 集成日志面板 ---
self.log_panel = LogPanel()
# 假设你使用 QVBoxLayout 作为主布局
self.main_layout.addWidget(self.log_panel)
# --- 发送一条测试日志 ---
logger.info("矩阵控制软件启动成功!")
logger.debug("调试模式已开启")
self.show()
def init_screen_widgets(self):
"""初始化 12 个屏幕控件,替换为自定义的 ScreenLabel"""
grid_layout = self.gridLayout_screens # 假设你把 12 个 Label 放在一个名为 gridLayout_screens 的布局里
for i in range(1, 13): # 1 到 12
# 获取 Designer 中的占位 Label (或者你直接在 Designer 中提升为 ScreenLabel)
# 这里为了演示,我们直接创建新的
label = ScreenLabel(screen_id=i, text=f"Screen {i}\n(空闲)")
# 连接右键信号
label.rightClicked.connect(self.on_screen_right_clicked)
# 假设是 4列 x 3行 布局
row = (i - 1) // 4 # 0,0,0,0, 1,1,1,1, 2,2,2,2
col = (i - 1) % 4 # 0,1,2,3, 0,1,2,3, 0,1,2,3
grid_layout.addWidget(label, row, col)
self.screen_labels.append(label)
label_screen = {}
label_screen['screen_id'] = i
label_screen['status'] = 0 # 屏幕状态:0为空闲、1为已选中、2为已设定、3为已拼接
label_screen['source_type'] = 0
label_screen['source_id'] = 0
label_screen['big_pic_id'] = 0
# 保存分屏方案
ScreenLabel.split_solution.append(label_screen)
# 如果你在 Designer 里已经画好了 Label,可以用下面这种方法替换:
# self.ui.label_1.setParent(None)
# grid_layout.addWidget(ScreenLabel(1), 0, 0)
def on_screen_right_clicked(self, screen_id):
"""处理右键点击事件"""
print(f"屏幕 {screen_id} 被右键点击了")
# 1. 创建菜单
menu = QMenu(self)
menu.addAction(f"--- 屏幕 {screen_id} ---")
menu.addSeparator()
# 2. 模拟信号源选项
source_group = QActionGroup(self) # 使用 QActionGroup 来实现单选
new_var_name = []
available_sources = [
{"type": 0x33, "id": 0x01, "name": "HDMI 1"},
{"type": 0x35, "id": 0x01, "name": "DVI 1"},
{"type": 0x32, "id": 0x01, "name": "AV 1"}, # SourceType=0x32, Sourceld=0x01
{"type": 0x32, "id": 0x02, "name": "AV 2"},
]
# 模拟信号源列表,实际可以从设备读取
# available_sources = ["HDMI 1", "HDMI 2", "DP 1", "DP 2", "Camera 1", "Test Pattern"]
for item in available_sources: # 这里用 enumerate 替代手动 k+=1
# 1. 创建 QAction 对象
action = QAction(item['name'], self)
action.setData(item)
# 2. 如果需要动态变量名存入全局(虽然不推荐,但如果你必须用)
new_var_name.append(action)
# 3. 将 QAction 对象添加到菜单 (关键修改在这里)
menu.addAction(action)
menu.addSeparator()
cancel_act = QAction("清除信号", self)
menu.addAction(cancel_act)
# 3. 显示菜单并获取结果
action = menu.exec_(QCursor.pos())
if action == cancel_act:
self.clear_screen(screen_id)
for k, item in enumerate(available_sources, start = 0):
if action == new_var_name[k]:
self.assign_source_to_screen(screen_id, item)
def get_current_layout_params(self):
"""
获取当前的拼接布局参数。
这里是一个示例实现,你需要根据你之前的“框选逻辑”调整。
"""
# --- 方案 A:如果你在框选结束后,把参数存到了实例变量里(推荐)---
# 比如你在 mouse_release_event 里计算完后,执行了: self.current_mosaic_params = params
if hasattr(self, 'current_mosaic_params'):
return [self.current_mosaic_params] # 注意:通常返回一个列表,因为可能有多组拼接
else:
# 如果没有选中任何东西,返回一个默认值或空列表
print("Warning: 没有找到当前布局参数,返回默认单屏")
return [{
"start": 0,
"hcount": 1,
"vcount": 1
}]
# --- 方案 B:如果你需要实时遍历所有屏幕去计算(复杂,但数据最准)---
# 这部分逻辑比较重,通常我们用 方案 A 的缓存方式。
def assign_source_to_screen(self, screen_id, source):
"""
分配信号源给屏幕
"""
# 2. 【新增】通过串口管理器发送
# 这里的 self.serial_manager 来自于我们在 main.py 中的初始化
if hasattr(self, 'serial_manager') and self.serial_manager.is_connected:
# 1. 获取点击的屏幕
# screen_status = ScreenLabel.split_solution[screen_id - 1]['status']
# screen_big_pic_id = ScreenLabel.split_solution[screen_id - 1]['big_pic_id']
#
# if screen_status == 3:
# for split_screen in ScreenLabel.split_solution:
# if split_screen['status'] == 3 and split_screen['big_pic_id'] == screen_big_pic_id:
# self.serial_manager.cancel_all()
# break
if len(ScreenLabel.click_labels) > 0 and screen_id in ScreenLabel.click_labels:
"""
设置屏幕控件的拼接状态
"""
# 获取选中的屏幕控件
selected_widgets = [self.screen_labels[value - 1] for value in ScreenLabel.click_labels]
# 生成拼接指令参数
params = self.serial_protocol.calculate_mosaic_params(selected_widgets)
# 2. 【新增】通过串口管理器发送
# 这里的 self.serial_manager 来自于我们在 main.py 中的初始化
if hasattr(self, 'serial_manager') and self.serial_manager.is_connected:
start = params['start']
v_count = params['v_count']
h_count = params['h_count']
# 计算拼接id
ScreenLabel.big_pic_id += 1
# 发送全部取消选择指令
cancel_all_bytes = self.serial_protocol.cancel_all()
if not cancel_all_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(cancel_all_bytes)
if success:
logger.info(f"✅ 取消选择指令发送成功")
else:
logger.error("取消选择指令发送失败")
# 生成拼接指令
mosaic_packet_bytes = self.serial_protocol.build_mosaic_packet(start, v_count, h_count, ScreenLabel.big_pic_id)
if not mosaic_packet_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(mosaic_packet_bytes)
if success:
logger.info(f"✅ 拼接指令发送成功")
else:
logger.error("拼接指令发送失败")
# 发送设置窗口信号源改变标志指令
change_signal_bytes = self.serial_protocol.change_signal(ScreenLabel.big_pic_id)
if not change_signal_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(change_signal_bytes)
if success:
logger.info(f"✅ 设置窗口信号源改变标志指令发送成功")
else:
logger.error("设置窗口信号源改变标志指令发送失败")
# 发送信号源切换指令
send_signal_bytes = self.serial_protocol.send_signal(source['type'], source['id'])
if not send_signal_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(send_signal_bytes)
if success:
logger.info(f"✅ 设置窗口信号源改变标志指令发送成功")
else:
logger.error("设置窗口信号源改变标志指令发送失败")
if success:
for i in ScreenLabel.click_labels:
ScreenLabel.split_solution[i - 1]["status"] = 2
ScreenLabel.split_solution[i - 1]['source_type'] = source['type']
ScreenLabel.split_solution[i - 1]['source_id'] = source['id']
ScreenLabel.split_solution[i - 1]["big_pic_id"] = ScreenLabel.big_pic_id
label = self.screen_labels[i - 1] # 数组下标从0开始
label.setText(f"Screen {i}\n[分组_{ScreenLabel.big_pic_id}]\n{source['name']}\n(已分配)")
label.update_style(2)
# 清空选中的屏幕控件
ScreenLabel.click_labels = []
logger.info(f"✅ 指令发送成功")
else:
logger.error("指令发送失败")
else:
logger.error("指令发送失败,未连接数据大屏")
return
elif ScreenLabel.split_solution[screen_id - 1]['status'] == 2 and ScreenLabel.split_solution[screen_id - 1]['big_pic_id'] > 0:
"""
解除屏幕拼接状态并切换信号
"""
# 获取选中的屏幕控件
ids = []
for i in ScreenLabel.split_solution:
if i['status'] == 2 and i['big_pic_id'] == ScreenLabel.split_solution[screen_id - 1]['big_pic_id']:
ids.append(i['screen_id'])
selected_widgets = [self.screen_labels[value - 1] for value in ids]
# 生成拼接指令参数
params = self.serial_protocol.calculate_mosaic_params(selected_widgets)
# 2. 【新增】通过串口管理器发送
# 这里的 self.serial_manager 来自于我们在 main.py 中的初始化
if hasattr(self, 'serial_manager') and self.serial_manager.is_connected:
start = params['start']
v_count = params['v_count']
h_count = params['h_count']
# 计算拼接id
big_pic_id = ScreenLabel.split_solution[screen_id - 1]['big_pic_id']
# 发送全部取消选择指令
cancel_all_bytes = self.serial_protocol.cancel_all()
if not cancel_all_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(cancel_all_bytes)
if success:
logger.info(f"✅ 取消选择指令发送成功")
else:
logger.error("取消选择指令发送失败")
# 生成拼接指令
mosaic_packet_bytes = self.serial_protocol.build_mosaic_packet(start, v_count, h_count, big_pic_id, 0x11)
if not mosaic_packet_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(mosaic_packet_bytes)
if success:
logger.info(f"✅ 拼接指令发送成功")
else:
logger.error("拼接指令发送失败")
# 发送设置窗口信号源改变标志指令
cancel_signal_bytes = self.serial_protocol.cancel_signal()
if not cancel_signal_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(cancel_signal_bytes)
if success:
logger.info(f"✅ 取消设置窗口信号源改变标志指令发送成功")
else:
logger.error("取消设置窗口信号源改变标志指令发送失败")
# 发送信号源切换指令
send_signal_bytes = self.serial_protocol.build_mosaic_packet(start, v_count, h_count, big_pic_id, 0x12)
if not send_signal_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(send_signal_bytes)
if success:
logger.info(f"✅ 还原拼接前的信号源指令发送成功")
else:
logger.error("还原拼接前的信号源指令发送失败")
if success:
for i in ids:
ScreenLabel.split_solution[i - 1]["status"] = 0
ScreenLabel.split_solution[i - 1]['source_type'] = 0
ScreenLabel.split_solution[i - 1]['source_id'] = 0
ScreenLabel.split_solution[i - 1]["big_pic_id"] = 0
label = self.screen_labels[i - 1] # 数组下标从0开始
label.setText(f"Screen {i}\n(空闲)")
label.update_style(0)
# 清空选中的屏幕控件
ScreenLabel.click_labels = []
logger.info(f"✅ 指令发送成功")
else:
logger.error("指令发送失败")
else:
logger.error("指令发送失败,未连接数据大屏")
return
else:
label = self.screen_labels[screen_id - 1] # 数组下标从0开始
# 2. 修改显示文字和样式
label.setText(f"Screen {screen_id}\n[{source['name']}]\n(已分配)")
label.update_style(2)
# 3. 这里调用核心控制器发送指令
# self.matrix.send_layout_command([screen_id], source)
if ScreenLabel.split_solution[screen_id - 1]:
ScreenLabel.split_solution[screen_id - 1]['status'] = 2
ScreenLabel.split_solution[screen_id - 1]['source_type'] = source['type']
ScreenLabel.split_solution[screen_id - 1]['source_id'] = source['id']
print(f"【指令】屏幕 {screen_id} 已切换至 {source['name']}")
ScreenLabel.click_labels = []
# send_command 方法来自 SerialManager 类
select_all_bytes = self.serial_protocol.select_all()
print(select_all_bytes)
if not select_all_bytes:
logger.error("指令封装失败,数据无效")
return
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(select_all_bytes)
if success:
# 1. 查找对应的 Label 对象
# label = self.screen_labels[screen_id - 1] # 数组下标从0开始
logger.info(f"✅ 指令发送成功")
else:
logger.error("指令发送失败")
else:
logger.error("指令发送失败,未连接数据大屏")
return
def clear_screen(self, screen_id):
"""
清空屏幕
"""
screen_pos = ScreenLabel.split_solution[screen_id - 1]['big_pic_id']
screen_status = ScreenLabel.split_solution[screen_id - 1]['status']
if screen_pos > 0 and screen_status > 0:
for key, value in enumerate(ScreenLabel.split_solution):
if value['big_pic_id'] == screen_pos:
ScreenLabel.split_solution[key]['status'] = 0
ScreenLabel.split_solution[key]['big_pic_id'] = 0
label = self.screen_labels[key]
# 切换选中状态
label.setText(f"Screen {key + 1}\n(空闲)")
label.set_selected(False)
label.update_style()
print(f"【指令】屏幕 {key+1} 已清屏")
else:
ScreenLabel.split_solution[screen_id - 1]['status'] = 0
ScreenLabel.split_solution[screen_id - 1]['big_pic_id'] = 0
label = self.screen_labels[screen_id - 1]
# 切换选中状态
label.setText(f"Screen {screen_id}\n(空闲)")
label.set_selected(False)
label.update_style()
print(f"【指令】屏幕 {screen_id} 已清屏")
def mouse_press_event(self, event):
"""
鼠标按下时,显示选框
"""
# 鼠标按下时记录起点,显示选框
self.origin = event.pos()
self.rubber_band.setGeometry(QRect(self.origin, QSize()))
self.rubber_band.show()
def mouse_move_event(self, event):
"""
鼠标移动时,显示选框
"""
# 鼠标移动时调整选框大小
if self.rubber_band.isVisible():
rect = QRect(self.origin, event.pos()).normalized()
self.rubber_band.setGeometry(rect)
def mouse_release_event(self, event):
"""
鼠标释放后,执行操作
"""
# 鼠标释放时,隐藏选框,并检查选中了哪些屏幕
self.rubber_band.hide()
# 获取选框的范围
select_rect = self.rubber_band.geometry()
try:
# ... 你的框选逻辑 ...
logger.debug(f"鼠标释放,开始计算拼接参数")
except Exception as e:
logger.error(f"框选事件处理失败: {str(e)}")
# 1. 先取消所有屏幕的选中状态
# for screen_widget in self.screen_labels:
# screen_widget.set_selected(False)
# 遍历所有屏幕,检查是否被选框包含
for key, screen_widget in enumerate(self.screen_labels): # 假设你把所有屏幕实例存进了这个列表
# 获取屏幕在容器内的位置
screen_rect = screen_widget.geometry()
# 如果屏幕在选框内
if select_rect.contains(screen_rect.center()):
screen_widget.set_selected(True) # 调用你之前写的选中方法
else:
if ScreenLabel.split_solution[key]['status'] < 2:
screen_widget.set_selected(False)
def send_mosaic_command(self, start, hcount, vcount):
"""
调用预设方案
"""
print(f"当前 serial_manager 对象: {id(self.serial_manager)}")
print(f"当前 is_connected 状态: {self.serial_manager.is_connected}")
try:
print(f"准备发送指令: Start={start}, {hcount}x{vcount}")
# 1. 【新增】使用协议模块封装数据
command_bytes = build_mosaic_packet(start, hcount, vcount)
if not command_bytes:
logger.error("指令封装失败,数据无效")
return
# 2. 【新增】通过串口管理器发送
# 这里的 self.serial_manager 来自于我们在 main.py 中的初始化
if hasattr(self, 'serial_manager') and self.serial_manager.is_connected:
# send_command 方法来自 SerialManager 类
success = self.serial_manager.send_command(command_bytes)
if success:
logger.info(f"✅ 指令发送成功: {hcount}x{vcount} 拼接")
else:
logger.error("指令发送失败")
else:
logger.warning("⚠️ 未连接设备,请先在连接面板中连接串口")
# 可以弹窗提示用户
# QMessageBox.warning(self, "警告", "设备未连接!")
except Exception as e:
logger.error(f"执行拼接指令时出错: {e}")
# 2. 【核心】发送硬件指令:调用你之前的串口/网络发送函数
# 假设你有一个 send_to_device(data) 函数
# command_data = self.generate_command_data(start, hcount, vcount)
# self.send_to_device(command_data)
# 3. 【提示】给用户反馈
# QMessageBox.information(self, "成功", f"已切换到布局: {hcount}x{vcount}")
def update_screen_selection_visual(self, start, hcount, vcount):
"""
根据拼接参数,更新界面上屏幕的选中(高亮)状态。
"""
# 1. 先取消所有屏幕的选中状态
for screen_widget in self.screen_widgets_list:
screen_widget.set_selected(False)
# 2. 计算当前拼接区域覆盖了哪些屏幕
# 假设总共有 4 列(根据你的实际情况修改)
cols = 4
start_row = start // cols
start_col = start % cols
# 3. 遍历逻辑矩阵,将对应的物理屏幕设置为选中
for vr in range(vcount): # 垂直方向
for hr in range(hcount): # 水平方向
phy_row = start_row + vr
phy_col = start_col + hr
index = phy_row * cols + phy_col
# 防止越界
if 0 <= index < len(self.screen_widgets_list):
self.screen_widgets_list[index].set_selected(True)
print(f"视觉更新完成: Start={start}, {hcount}x{vcount}")
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MatrixControlApp()
window.show()
sys.exit(app.exec_())