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