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
524 lines
20 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.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_())
|