# 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_())