大屏分屏控制软件
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.

524 lines
20 KiB

3 months ago
# 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.protocol import build_mosaic_packet
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
from core.serial_manager import SerialManager
class ScreenLabel(QLabel):
split_solution = []
click_labels = []
"""自定义 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.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 # 保存状态
self.update_style()
# if selected:
# # 如果选中,设置一个红色/蓝色的边框,或者背景色
# self.setStyleSheet("""
# border: 2px solid red;
# background-color: rgba(255, 0, 0, 0.1);
# """)
# else:
# # 如果取消选中,恢复默认样式(假设默认是灰色边框)
# self.setStyleSheet("""
# border: 1px solid gray;
# background-color: white;
# """)
# 强制刷新界面
self.update()
def update_style(self):
if self.is_selected:
ScreenLabel.click_labels.append(self.screen_id)
ScreenLabel.split_solution[self.screen_id - 1]['status'] = 1
ScreenLabel.split_solution[self.screen_id - 1]['pos'] = ''
# 选中状态:蓝色背景
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
ScreenLabel.split_solution[self.screen_id - 1]['pos'] = ''
self.init_style()
class MatrixControlApp(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
# --- 核心控制器 ---
self.matrix = MatrixController()
self.serial_manager = SerialManager()
# --- 屏幕管理 ---
# 假设你在 Designer 里把 12 个 Label 的名字分别命名为 screen_1 到 screen_12
self.screen_labels = []
# --- 新增:初始化屏幕列表 ---
self.screen_widgets_list = []
self.init_screen_widgets()
# 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
# --- 修改这里:获取全局实例,而不是自己 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为已设定
label_screen['pos'] = ''
# 保存分屏方案
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} 被右键点击了")
self.serial_manager.power_on_all()
# 1. 创建菜单
menu = QMenu(self)
menu.addAction(f"--- 屏幕 {screen_id} ---")
menu.addSeparator()
# 2. 模拟信号源选项
source_group = QActionGroup(self) # 使用 QActionGroup 来实现单选
new_var_name = []
# 模拟信号源列表,实际可以从设备读取
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, self)
# 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 on_context_menu_requested(self, pos):
# 获取当前选中的控件列表 (self.selected_panels)
selected = self.selected_panels
if len(selected) < 1:
return
# --- 调用计算逻辑 ---
params = self.device_manager.calculate_mosaic_params(selected)
if not params:
return
# --- 智能提示:如果用户漏选了部分屏幕 ---
if params["missing_screens"]:
# 询问用户是否自动补全
reply = QMessageBox.question(
self,
"拼接提示",
f"检测到您选择了不完整的矩形区域。\n系统将自动为您补全为 {params['Vcount']}x{params['Hcount']} 拼接。\n缺少的屏幕:{params['missing_screens']}\n是否继续?"
)
if reply != QMessageBox.Yes:
return
# --- 生成指令 ---
# 这里假设信号源固定为 HDMI (0x33),实际可由菜单选项决定
source_type = 0x33
# 调用协议层生成指令
cmd = MatrixCommands.create_mosaic_command(
start=params["start"],
hcount=params["Hcount"],
vcount=params["Vcount"],
big_pic_id=params["BigPicID"],
source_type=source_type
)
# --- 发送指令 ---
# 注意:这里只是发送了拼接指令,实际使用中可能还需要发送信号源切换指令
self.hw_handler.send_command(cmd)
# --- 界面反馈 ---
# 可选:将自动补全的屏幕也高亮显示,或者打印日志
print(f"发送拼接指令: START={params['start']}, {params['Hcount']}x{params['Vcount']}")
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):
"""分配信号源给屏幕"""
click_labels = ScreenLabel.click_labels
# 1. 查找对应的 Label 对象
# label = self.screen_labels[screen_id - 1] # 数组下标从0开始
if click_labels and screen_id in click_labels:
for click_label in click_labels:
label = self.screen_labels[click_label - 1] # 数组下标从0开始
# 2. 修改显示文字和样式
label.setText(f"Screen {click_label}\n[{source}]\n(已分配)")
label.setStyleSheet("""
background-color: rgb(0, 255, 127); /* 绿色背景 */
color: black;
border: 2px solid white;
font-weight: bold;
""")
# 3. 这里调用核心控制器发送指令
# self.matrix.send_layout_command([screen_id], source)
if ScreenLabel.split_solution[click_label - 1]:
ScreenLabel.split_solution[click_label - 1]['status'] = 2
ScreenLabel.split_solution[click_label - 1]['pos'] = source
print(f"【指令】屏幕 {click_label} 已切换至 {source}")
else:
label = self.screen_labels[screen_id - 1] # 数组下标从0开始
# 2. 修改显示文字和样式
label.setText(f"Screen {screen_id}\n[{source}]\n(已分配)")
label.setStyleSheet("""
background-color: rgb(0, 255, 127); /* 绿色背景 */
color: black;
border: 2px solid white;
font-weight: bold;
""")
# 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]['pos'] = source
print(f"【指令】屏幕 {screen_id} 已切换至 {source}")
ScreenLabel.click_labels = []
print(ScreenLabel.split_solution)
# # 3. 这里调用核心控制器发送指令
# # self.matrix.send_layout_command([screen_id], source)
# print(f"【指令】屏幕 {screen_id} 已切换至 {source}")
def clear_screen(self, screen_id):
screen_pos = ScreenLabel.split_solution[screen_id - 1]['pos']
if screen_pos:
for key, value in enumerate(ScreenLabel.split_solution):
if value['pos'] == screen_pos:
ScreenLabel.split_solution[key]['status'] = 0
ScreenLabel.split_solution[key]['pos'] = ''
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]['pos'] = ''
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)}")
# 遍历所有屏幕,检查是否被选框包含
for screen_widget in self.screen_labels: # 假设你把所有屏幕实例存进了这个列表
# 获取屏幕在容器内的位置
screen_rect = screen_widget.geometry()
# 如果屏幕在选框内
if select_rect.contains(screen_rect.center()):
screen_widget.set_selected(True) # 调用你之前写的选中方法
if self.rubber_band.isVisible():
# 1. 获取框选内的屏幕列表 (self.selected_panels)
selected_ids = [screen.id for screen in self.selected_panels]
# 2. 调用解算器
layout_type, params = analyze_selection(self.selected_panels)
# 3. 更新右键菜单 (动态显示:是创建2x2拼接,还是漫游?)
self.update_context_menu(layout_type, params)
# 4. 存储当前选区的逻辑信息,供右键点击时使用
self.current_logic_layout = params
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)
print(command_bytes)
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_())