""" A13课堂点名系统 - 插件UI注入工具(增强版) 提供在主程序任何位置添加UI元素、修改布局、添加标签页的完整能力 """ from PyQt5.QtWidgets import ( QPushButton, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QFrame, QSizePolicy, QMenu, QAction, QLineEdit, QComboBox, QCheckBox, QSlider, QProgressBar, QTextEdit, QListWidget, QListWidgetItem, QTabWidget, QScrollArea, QGroupBox, QFormLayout, QGridLayout, QSplitter, QToolBar, QStatusBar, QDockWidget, QMessageBox, QFileDialog, QColorDialog, QFontDialog, QInputDialog, QSpinBox, QDoubleSpinBox, QDateEdit, QTimeEdit, QDateTimeEdit, QCalendarWidget, QDial, QLCDNumber, QTableWidget, QTableWidgetItem, QTreeWidget, QTreeWidgetItem, QHeaderView, QStackedWidget, QToolBox, QButtonGroup, QRadioButton, QPlainTextEdit, QGraphicsView, QGraphicsScene, QSystemTrayIcon, QMenuBar, QShortcut ) from PyQt5.QtCore import Qt, QSize, QTimer, pyqtSignal, QObject, QRect from PyQt5.QtGui import ( QFont, QIcon, QColor, QPalette, QPixmap, QPainter, QPen, QBrush, QLinearGradient, QRadialGradient, QFontDatabase, QCursor, QKeySequence ) import os import json class UIInjector: """UI注入工具类 - 增强版,支持复杂控件、布局修改、标签页添加""" def __init__(self, main_window): self.main_window = main_window self.injected_elements = [] self._custom_tabs = [] self._custom_panels = [] # ==================== 查找控件 ==================== def find_widget_by_name(self, name): return self.main_window.findChild(QWidget, name) def find_all_buttons(self): return self.main_window.findChildren(QPushButton) def find_button_by_text(self, text): for btn in self.find_all_buttons(): if btn.text() == text: return btn return None def find_all_labels(self): return self.main_window.findChildren(QLabel) def find_label_by_text(self, text): for label in self.find_all_labels(): if text in label.text(): return label return None def find_layout_by_widget(self, widget): if widget and widget.parent(): return widget.parent().layout() return None def get_all_ui_elements(self, include_layouts=True): elements = [] for widget in self.main_window.findChildren(QWidget): elements.append({ "type": type(widget).__name__, "objectName": widget.objectName(), "text": getattr(widget, 'text', lambda: '')() if callable(getattr(widget, 'text', None)) else '', "visible": widget.isVisible(), "enabled": widget.isEnabled(), "geometry": widget.geometry().getRect(), }) return elements def get_main_window(self): return self.main_window def get_central_widget(self): return self.main_window.centralWidget() def get_status_bar(self): return self.main_window.statusBar() def get_menu_bar(self): return self.main_window.menuBar() # ==================== 添加按钮(基础) ==================== def add_button_to_layout(self, layout, text, callback, position=None, style="primary", icon=None, tooltip=None, width=None, height=None): btn = QPushButton(text) btn.setStyleSheet(self._get_button_style(style)) if icon: btn.setIcon(icon) if tooltip: btn.setToolTip(tooltip) if width: btn.setFixedWidth(width) if height: btn.setFixedHeight(height) btn.clicked.connect(callback) if position is not None: layout.insertWidget(position, btn) else: layout.addWidget(btn) self.injected_elements.append(btn) return btn def add_button_to_top_bar(self, text, callback, position=None, style="primary", icon=None, tooltip=None, width=100, height=36): top_bar = self._get_top_bar() if not top_bar: return None layout = top_bar.layout() if not layout: layout = QHBoxLayout(top_bar) return self.add_button_to_layout(layout, text, callback, position, style, icon, tooltip, width, height) def add_button_to_bottom_bar(self, text, callback, position=None, style="ghost", icon=None, tooltip=None, width=90, height=32): bottom_bar = self._get_bottom_bar() if not bottom_bar: return None if isinstance(bottom_bar, QStatusBar): btn = QPushButton(text) btn.setStyleSheet(self._get_button_style(style)) if tooltip: btn.setToolTip(tooltip) btn.clicked.connect(callback) bottom_bar.addPermanentWidget(btn) self.injected_elements.append(btn) return btn layout = bottom_bar.layout() if not layout: layout = QHBoxLayout(bottom_bar) return self.add_button_to_layout(layout, text, callback, position, style, icon, tooltip, width, height) def add_button_to_sidebar(self, text, callback, position=None, style="ghost", icon=None, tooltip=None, width=120, height=40): sidebar = self._get_sidebar() if not sidebar: return None layout = sidebar.layout() if not layout: layout = QVBoxLayout(sidebar) return self.add_button_to_layout(layout, text, callback, position, style, icon, tooltip, width, height) def add_button_to_draw_area(self, text, callback, position=None, style="primary", icon=None, tooltip=None, width=120, height=48): draw_area = self._get_draw_area() if not draw_area: return None layout = draw_area.layout() if not layout: layout = QVBoxLayout(draw_area) return self.add_button_to_layout(layout, text, callback, position, style, icon, tooltip, width, height) def add_button_next_to(self, target_button, text, callback, side="right", style="success", icon=None, tooltip=None, width=None, height=None): if isinstance(target_button, str): target_button = self.find_button_by_text(target_button) if not target_button: return None parent_layout = self.find_layout_by_widget(target_button) if not parent_layout: return None index = parent_layout.indexOf(target_button) position = index + 1 if side == "right" else index return self.add_button_to_layout(parent_layout, text, callback, position, style, icon, tooltip, width, height) def create_floating_button(self, text, callback, x=20, y=100, size=(100, 40), style="warning", tooltip=None): btn = DraggableButton(text, self.main_window) btn.setStyleSheet(self._get_button_style(style)) btn.setGeometry(x, y, size[0], size[1]) if tooltip: btn.setToolTip(tooltip) btn.clicked.connect(callback) btn.show() self.injected_elements.append(btn) return btn # ==================== 添加复杂控件(新增) ==================== def add_input_to_layout(self, layout, placeholder="", callback=None, position=None, width=200, height=32, echo_normal=True): input_box = QLineEdit() input_box.setPlaceholderText(placeholder) input_box.setFixedHeight(height) if width: input_box.setFixedWidth(width) if not echo_normal: input_box.setEchoMode(QLineEdit.Password) if callback: input_box.textChanged.connect(callback) if position is not None: layout.insertWidget(position, input_box) else: layout.addWidget(input_box) self.injected_elements.append(input_box) return input_box def add_combo_to_layout(self, layout, items, callback=None, position=None, width=150, height=32, current_index=0): combo = QComboBox() combo.addItems(items) combo.setFixedHeight(height) if width: combo.setFixedWidth(width) combo.setCurrentIndex(current_index) if callback: combo.currentIndexChanged.connect(callback) if position is not None: layout.insertWidget(position, combo) else: layout.addWidget(combo) self.injected_elements.append(combo) return combo def add_checkbox_to_layout(self, layout, text, callback=None, position=None, checked=False, width=None): checkbox = QCheckBox(text) checkbox.setChecked(checked) if width: checkbox.setFixedWidth(width) if callback: checkbox.stateChanged.connect(callback) if position is not None: layout.insertWidget(position, checkbox) else: layout.addWidget(checkbox) self.injected_elements.append(checkbox) return checkbox def add_radio_to_layout(self, layout, text, callback=None, position=None, checked=False, group=None): radio = QRadioButton(text) radio.setChecked(checked) if group: group.addButton(radio) if callback: radio.toggled.connect(callback) if position is not None: layout.insertWidget(position, radio) else: layout.addWidget(radio) self.injected_elements.append(radio) return radio def add_slider_to_layout(self, layout, min_val=0, max_val=100, value=50, callback=None, position=None, orientation=Qt.Horizontal, width=200): slider = QSlider(orientation) slider.setRange(min_val, max_val) slider.setValue(value) if orientation == Qt.Horizontal and width: slider.setFixedWidth(width) if callback: slider.valueChanged.connect(callback) if position is not None: layout.insertWidget(position, slider) else: layout.addWidget(slider) self.injected_elements.append(slider) return slider def add_spinbox_to_layout(self, layout, min_val=0, max_val=100, value=0, callback=None, position=None, width=100, step=1, prefix="", suffix=""): spinbox = QSpinBox() spinbox.setRange(min_val, max_val) spinbox.setValue(value) spinbox.setSingleStep(step) spinbox.setFixedWidth(width) if prefix: spinbox.setPrefix(prefix) if suffix: spinbox.setSuffix(suffix) if callback: spinbox.valueChanged.connect(callback) if position is not None: layout.insertWidget(position, spinbox) else: layout.addWidget(spinbox) self.injected_elements.append(spinbox) return spinbox def add_progress_to_layout(self, layout, value=0, max_val=100, position=None, width=200, height=20, text_visible=True): progress = QProgressBar() progress.setRange(0, max_val) progress.setValue(value) progress.setTextVisible(text_visible) progress.setFixedHeight(height) if width: progress.setFixedWidth(width) if position is not None: layout.insertWidget(position, progress) else: layout.addWidget(progress) self.injected_elements.append(progress) return progress def add_list_to_layout(self, layout, items=None, callback=None, position=None, width=200, height=150, selection_mode=QListWidget.SingleSelection): list_widget = QListWidget() list_widget.setFixedHeight(height) if width: list_widget.setFixedWidth(width) list_widget.setSelectionMode(selection_mode) if items: list_widget.addItems(items) if callback: list_widget.itemClicked.connect(callback) if position is not None: layout.insertWidget(position, list_widget) else: layout.addWidget(list_widget) self.injected_elements.append(list_widget) return list_widget def add_table_to_layout(self, layout, rows=0, cols=0, headers=None, position=None, width=400, height=200): table = QTableWidget(rows, cols) if headers: table.setHorizontalHeaderLabels(headers) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setFixedHeight(height) if width: table.setFixedWidth(width) if position is not None: layout.insertWidget(position, table) else: layout.addWidget(table) self.injected_elements.append(table) return table def add_textarea_to_layout(self, layout, placeholder="", callback=None, position=None, width=300, height=100, read_only=False): textarea = QTextEdit() textarea.setPlaceholderText(placeholder) textarea.setReadOnly(read_only) textarea.setFixedHeight(height) if width: textarea.setFixedWidth(width) if callback: textarea.textChanged.connect(callback) if position is not None: layout.insertWidget(position, textarea) else: layout.addWidget(textarea) self.injected_elements.append(textarea) return textarea def add_label_to_layout(self, layout, text, style="normal", position=None, width=None, height=None, alignment=None): label = QLabel(text) label.setStyleSheet(self._get_label_style(style)) if width: label.setFixedWidth(width) if height: label.setFixedHeight(height) if alignment: label.setAlignment(alignment) if position is not None: layout.insertWidget(position, label) else: layout.addWidget(label) self.injected_elements.append(label) return label def add_separator(self, layout, position=None, orientation=Qt.Horizontal): line = QFrame() if orientation == Qt.Horizontal: line.setFrameShape(QFrame.HLine) else: line.setFrameShape(QFrame.VLine) line.setFrameShadow(QFrame.Sunken) if position is not None: layout.insertWidget(position, line) else: layout.addWidget(line) self.injected_elements.append(line) return line def add_groupbox(self, layout, title, position=None): group = QGroupBox(title) group_layout = QVBoxLayout(group) if position is not None: layout.insertWidget(position, group) else: layout.addWidget(group) self.injected_elements.append(group) return group, group_layout def add_custom_widget(self, layout, widget, position=None): if position is not None: layout.insertWidget(position, widget) else: layout.addWidget(widget) self.injected_elements.append(widget) return widget # ==================== 添加标签页/面板(新增) ==================== def add_tab_to_main(self, tab_title, icon=None): central = self.get_central_widget() if not central: return None tab_widget = central.findChild(QTabWidget) if not tab_widget: tab_widget = QTabWidget(central) layout = central.layout() if layout: layout.addWidget(tab_widget) else: new_layout = QVBoxLayout(central) new_layout.addWidget(tab_widget) new_tab = QWidget() tab_layout = QVBoxLayout(new_tab) if icon: index = tab_widget.addTab(new_tab, icon, tab_title) else: index = tab_widget.addTab(new_tab, tab_title) self._custom_tabs.append((tab_widget, new_tab, index)) self.injected_elements.append(new_tab) return new_tab, tab_layout, index def add_panel_to_main(self, title, area=Qt.LeftDockWidgetArea, floating=False): dock = QDockWidget(title, self.main_window) dock.setAllowedAreas(Qt.AllDockWidgetAreas) content = QWidget() content_layout = QVBoxLayout(content) dock.setWidget(content) self.main_window.addDockWidget(area, dock) if floating: dock.setFloating(True) self._custom_panels.append(dock) self.injected_elements.append(dock) return dock, content, content_layout def add_toolbar(self, title, area=Qt.TopToolBarArea): toolbar = QToolBar(title, self.main_window) toolbar.setObjectName(f"plugin_toolbar_{title}") self.main_window.addToolBar(area, toolbar) self.injected_elements.append(toolbar) return toolbar def add_status_widget(self, widget, permanent=True): status_bar = self.get_status_bar() if not status_bar: return None if permanent: status_bar.addPermanentWidget(widget) else: status_bar.addWidget(widget) self.injected_elements.append(widget) return widget # ==================== 菜单和快捷键(新增) ==================== def add_menu_action(self, menu, text, callback, icon=None, shortcut=None, tooltip=None): action = QAction(text, self.main_window) if icon: action.setIcon(icon) if shortcut: action.setShortcut(shortcut) if tooltip: action.setToolTip(tooltip) action.triggered.connect(callback) menu.addAction(action) self.injected_elements.append(action) return action def add_menu(self, menu_bar, title, icon=None): menu = menu_bar.addMenu(title) if icon: menu.setIcon(icon) self.injected_elements.append(menu) return menu def add_shortcut(self, key_sequence, callback, context=Qt.WindowShortcut): shortcut = QShortcut(QKeySequence(key_sequence), self.main_window) shortcut.setContext(context) shortcut.activated.connect(callback) self.injected_elements.append(shortcut) return shortcut # ==================== 修改现有控件 ==================== def modify_button(self, button, text=None, callback=None, style=None, icon=None, tooltip=None, enabled=None, visible=None): if isinstance(button, str): button = self.find_button_by_text(button) if not button: return False if text is not None: button.setText(text) if callback is not None: try: button.clicked.disconnect() except Exception: pass button.clicked.connect(callback) if style is not None: button.setStyleSheet(self._get_button_style(style)) if icon is not None: button.setIcon(icon) if tooltip is not None: button.setToolTip(tooltip) if enabled is not None: button.setEnabled(enabled) if visible is not None: button.setVisible(visible) return True def modify_label(self, label, text=None, style=None, color=None, font_size=None): if isinstance(label, str): label = self.find_label_by_text(label) if not label: return False if text is not None: label.setText(text) if style is not None: label.setStyleSheet(self._get_label_style(style)) if color is not None: label.setStyleSheet(f"color: {color};") if font_size is not None: font = label.font() font.setPointSize(font_size) label.setFont(font) return True def hide_button(self, button): return self.modify_button(button, visible=False) def show_button(self, button): return self.modify_button(button, visible=True) def disable_button(self, button): return self.modify_button(button, enabled=False) def enable_button(self, button): return self.modify_button(button, enabled=True) # ==================== 布局操作(新增) ==================== def get_layout_of_widget(self, widget): if widget and widget.parent(): return widget.parent().layout() return None def remove_widget_from_layout(self, layout, widget): if layout and widget: layout.removeWidget(widget) widget.setParent(None) return True return False def move_widget_in_layout(self, layout, widget, new_position): if not layout or not widget: return False current_index = layout.indexOf(widget) if current_index < 0: return False layout.removeWidget(widget) layout.insertWidget(new_position, widget) return True def swap_widgets_in_layout(self, layout, widget1, widget2): if not layout or not widget1 or not widget2: return False idx1 = layout.indexOf(widget1) idx2 = layout.indexOf(widget2) if idx1 < 0 or idx2 < 0: return False layout.removeWidget(widget1) layout.removeWidget(widget2) min_idx = min(idx1, idx2) layout.insertWidget(min_idx, widget2) layout.insertWidget(min_idx + 1, widget1) return True def insert_layout(self, parent_layout, position, orientation="vertical"): if orientation == "vertical": new_layout = QVBoxLayout() else: new_layout = QHBoxLayout() parent_layout.insertLayout(position, new_layout) return new_layout # ==================== 对话框和提示(新增) ==================== def show_message_box(self, title, message, icon=QMessageBox.Information, buttons=QMessageBox.Ok): msg_box = QMessageBox(self.main_window) msg_box.setWindowTitle(title) msg_box.setText(message) msg_box.setIcon(icon) msg_box.setStandardButtons(buttons) return msg_box.exec_() def show_input_dialog(self, title, label, default_text="", mode=QLineEdit.Normal): text, ok = QInputDialog.getText(self.main_window, title, label, mode, default_text) return text if ok else None def show_file_dialog(self, title, filter="所有文件 (*.*)", directory=""): file_path, _ = QFileDialog.getOpenFileName(self.main_window, title, directory, filter) return file_path def show_save_dialog(self, title, filter="所有文件 (*.*)", directory=""): file_path, _ = QFileDialog.getSaveFileName(self.main_window, title, directory, filter) return file_path def show_color_dialog(self, initial_color=QColor(255, 255, 255)): color = QColorDialog.getColor(initial_color, self.main_window) return color if color.isValid() else None def show_font_dialog(self, initial_font=None): if initial_font is None: initial_font = QFont() font, ok = QFontDialog.getFont(initial_font, self.main_window) return font if ok else None # ==================== 样式和主题(新增) ==================== def get_app_style(self): return self.main_window.styleSheet() def set_app_style(self, style_sheet): self.main_window.setStyleSheet(style_sheet) def append_app_style(self, style_sheet): current = self.main_window.styleSheet() self.main_window.setStyleSheet(current + "\n" + style_sheet) def set_widget_style(self, widget, style_sheet): if widget: widget.setStyleSheet(style_sheet) def get_palette(self): return self.main_window.palette() def set_palette_color(self, role, color, group=QPalette.Active): palette = self.main_window.palette() palette.setColor(group, role, QColor(color)) self.main_window.setPalette(palette) # ==================== 窗口操作(新增) ==================== def set_window_title(self, title): self.main_window.setWindowTitle(title) def get_window_title(self): return self.main_window.windowTitle() def set_window_size(self, width, height): self.main_window.resize(width, height) def get_window_size(self): return self.main_window.size() def set_window_position(self, x, y): self.main_window.move(x, y) def get_window_position(self): return self.main_window.pos() def minimize_window(self): self.main_window.showMinimized() def maximize_window(self): self.main_window.showMaximized() def restore_window(self): self.main_window.showNormal() def set_window_opacity(self, opacity): self.main_window.setWindowOpacity(opacity) def set_window_on_top(self, on_top=True): flags = self.main_window.windowFlags() if on_top: flags |= Qt.WindowStaysOnTopHint else: flags &= ~Qt.WindowStaysOnTopHint self.main_window.setWindowFlags(flags) self.main_window.show() # ==================== 定时器和动画(新增) ==================== def create_timer(self, interval, callback, single_shot=False): timer = QTimer(self.main_window) timer.setInterval(interval) timer.setSingleShot(single_shot) timer.timeout.connect(callback) self.injected_elements.append(timer) return timer def start_timer(self, timer): if timer: timer.start() def stop_timer(self, timer): if timer: timer.stop() def delayed_call(self, milliseconds, callback): QTimer.singleShot(milliseconds, callback) # ==================== 资源管理(新增) ==================== def load_icon(self, path): if os.path.exists(path): return QIcon(path) return None def load_pixmap(self, path): if os.path.exists(path): return QPixmap(path) return None def load_font(self, path): if os.path.exists(path): font_id = QFontDatabase.addApplicationFont(path) if font_id >= 0: return QFontDatabase.applicationFontFamilies(font_id)[0] return None def create_color(self, r, g, b, a=255): return QColor(r, g, b, a) def create_gradient(self, x1, y1, x2, y2, color1, color2): gradient = QLinearGradient(x1, y1, x2, y2) gradient.setColorAt(0, QColor(color1)) gradient.setColorAt(1, QColor(color2)) return gradient # ==================== 内部辅助方法 ==================== def _get_top_bar(self): for name in ["top_bar", "title_bar", "header", "top_frame"]: widget = self.find_widget_by_name(name) if widget: return widget return None def _get_bottom_bar(self): from PyQt5.QtWidgets import QStatusBar for name in ["status_bar", "bottom_bar", "statusBar", "footer"]: widget = self.find_widget_by_name(name) if widget: return widget status_bar = self.main_window.findChild(QStatusBar) if status_bar: return status_bar return None def _get_sidebar(self): for name in ["sidebar", "side_panel", "left_panel", "nav_panel"]: widget = self.find_widget_by_name(name) if widget: return widget return None def _get_draw_area(self): for name in ["draw_area", "draw_widget", "central_area", "main_area"]: widget = self.find_widget_by_name(name) if widget: return widget return None def _get_button_style(self, style): styles = { "primary": """ QPushButton { background: #0067c0; color: white; border: none; border-radius: 6px; padding: 8px 16px; font-weight: 600; font-size: 13px; } QPushButton:hover { background: #005a9e; } QPushButton:pressed { background: #004578; } QPushButton:disabled { background: #cccccc; color: #999999; } """, "success": """ QPushButton { background: #107c10; color: white; border: none; border-radius: 6px; padding: 8px 16px; font-weight: 600; font-size: 13px; } QPushButton:hover { background: #0e6c0e; } QPushButton:pressed { background: #0a5c0a; } """, "warning": """ QPushButton { background: #ca5010; color: white; border: none; border-radius: 6px; padding: 8px 16px; font-weight: 600; font-size: 13px; } QPushButton:hover { background: #b04810; } """, "danger": """ QPushButton { background: #c42b1c; color: white; border: none; border-radius: 6px; padding: 8px 16px; font-weight: 600; font-size: 13px; } QPushButton:hover { background: #a82318; } """, "ghost": """ QPushButton { background: transparent; color: #0067c0; border: 1px solid #0067c0; border-radius: 6px; padding: 8px 16px; font-weight: 600; font-size: 13px; } QPushButton:hover { background: #e5f1fb; } """, "small": """ QPushButton { background: #f0f0f0; color: #333; border: 1px solid #d1d1d1; border-radius: 4px; padding: 4px 10px; font-size: 11px; } QPushButton:hover { background: #e0e0e0; } """, } return styles.get(style, styles["primary"]) def _get_label_style(self, style): styles = { "normal": "color: #333333; font-size: 13px;", "title": "color: #1a1a1a; font-size: 18px; font-weight: 700;", "subtitle": "color: #555555; font-size: 14px; font-weight: 600;", "hint": "color: #999999; font-size: 12px;", "success": "color: #107c10; font-size: 13px; font-weight: 600;", "warning": "color: #ca5010; font-size: 13px; font-weight: 600;", "danger": "color: #c42b1c; font-size: 13px; font-weight: 600;", } return styles.get(style, styles["normal"]) # ==================== 清理 ==================== def cleanup(self): for element in self.injected_elements: try: if hasattr(element, 'deleteLater'): element.deleteLater() elif hasattr(element, 'close'): element.close() except Exception: pass self.injected_elements = [] self._custom_tabs = [] self._custom_panels = [] class DraggableButton(QPushButton): """可拖动的按钮类""" def __init__(self, text, parent=None): super().__init__(text, parent) self._drag_position = None self.setCursor(QCursor(Qt.OpenHandCursor)) def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self._drag_position = event.globalPos() - self.frameGeometry().topLeft() self.setCursor(QCursor(Qt.ClosedHandCursor)) super().mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton and self._drag_position: self.move(event.globalPos() - self._drag_position) super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): self._drag_position = None self.setCursor(QCursor(Qt.OpenHandCursor)) super().mouseReleaseEvent(event)