使用mouse_event来模拟一次选中文本框中的文本

博客介绍了使用mouse_event模拟鼠标在EDIT中选中文本的过程,包括将鼠标移到指定位置、按下鼠标、移动鼠标和松开鼠标左键。还给出了在DIALOG工程的BUTTON事件中的示例代码。此外,提到可利用此方法编写自动安装程序,甚至调侃可模拟刷卡。

使用mouse_event可以模拟一些通过鼠标执行的事情,下面我们就来模拟一次鼠标在EDIT中选择一段文本的过程。

首先我们来分解一下选中文本的鼠标动作,其包括基本方面:

  1. 将鼠标移动到指定的位置,文本的开始处
  2. 按下鼠标(WM_LBUTTONDOWN)
  3. 在按下鼠标的同时移动鼠标到指定的位置
  4. 松开鼠标左键(WM_LBUTTONUP)

知道了这个过程我们就来通过程序来模拟吧。我自己实验的时候建立了DIALOG工程,在DIALOG上放了一个BUTTON,一个EDIT,将程序写到BUTTON事件中,程序如下:

void CTestChkDlg: nBnClickedButton1()
{
 RECT rect;
 GetDlgItem(IDC_EDIT)->GetWindowRect(&rect);
 SetCursorPos(rect.left+5,rect.top+5);
 mouse_event(MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_LEFTDOWN,rect.left+1,rect.top+1,0,0);
 SetCursorPos(rect.left+60,rect.top+10);
 //mouse_event(MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_MOVE,500,600,0,0);
 mouse_event(MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_LEFTUP,rect.left+60,rect.top+5,0,0);

}

首先我们需要做的是取得EDIT的位置,然后将鼠标移动到那里(SetCursorPos),这里加5是让鼠标在EDIT里面,因为EDIT有边,如果按照RECT的值来可能选择不了;然后使用MOUSEEVENTF_LEFTDOWN来按下鼠标,在按下鼠标的时候再将鼠标移动到文本的指定位置,然后松开鼠标。

好了文本选中了。

既然可以通过程序来模拟真正的鼠标动作,那么我们就可以写一些自动安装程序就不是什么难事情了。记得以前我们公司采用LOTUS软件来拷勤,需要在上面刷卡,哈哈,既然鼠标能模拟,这种方法可以写个程序来帮你刷卡,你永远不会迟到。当然,如果你真的这么做,后果自负。

from PyQt6.QtWidgets import QMainWindow, QApplication,QTabWidget, QLineEdit, QSpinBox, QCheckBox, QPushButton,QTextEdit,QRadioButton,QComboBox, QMessageBox, QLabel, QSlider from PyQt6 import uic import sys import json import win32gui # 添加这行导入win32gui模块 import os # 添加这行导入os模块 import socket # 添加socket模块用于网络连接检测 from image_processor import ImageProcessor from PyQt6.QtWidgets import QFileDialog from mouse_controller import MouseController # 导入MouseController类 from laye import Laye # 导入Laye类 from PyQt6.QtGui import QKeySequence, QIntValidator # 添加QIntValidator导入 from PyQt6.QtCore import Qt ,QTimer import keyboard # 需要安装pywin32和keyboard库 from business_logic import BusinessLogic import threading # 添加这行导入threading模块 from datetime import datetime import time from PyQt6.QtCore import pyqtSignal, QObject import os import sys import uuid # 添加这行导入 import time from logger import Logger from delay_system import delay_system class MainWindow(QMainWindow): # 将信号声明为类属性(关键修复) show_message_signal = pyqtSignal(str, str, str) # 类型, 标题, 内容 def __init__(self): super().__init__() ui_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'wanhao.bak.ui') self.ui = uic.loadUi(ui_path) self.selected_image_path = None self.delay_system = delay_system # 提前初始化延时系统 # 移除所有授权验证相关的初始化 self.setup_ui() self.connect_signals() self.load_settings() # 先初始化logger log_widget = self.ui.findChild(QTextEdit, "findPicLog") self.logger = Logger(name='MainWindow', log_file='logs/app.log', max_lines=1000, ui=log_widget) # 然后注册热键(这样logger就可用了) self.register_hotkey() self.找图类 = ImageProcessor() # 添加信号与槽的连接 self.show_message_signal.connect(self.show_message_slot) # 为时间控件添加数字验证器 self.setup_number_validators() # 添加线程暂停控制变量 self.should_pause = False self.pause_event = threading.Event() self.pause_event.set() # 初始设置为运行状态 # 初始化序列号显示为永久有效 self._init_serial_number_display() def _init_serial_number_display(self): """初始化序列号显示为永久有效""" try: # 获取序列号输入控件 self.serial_input = self.ui.findChild(QLineEdit, "serialNumberInput") # 获取到期时间显示控件 self.endtime_label = self.ui.findChild(QLabel, "endtimeti") if self.serial_input: self.serial_input.setText("永久授权版") self.serial_input.setReadOnly(True) # 设置为只读 if self.endtime_label: self.endtime_label.setText("永久有效") self.endtime_label.setStyleSheet("color: green; font-weight: bold;") except Exception as e: print(f"初始化序列号显示失败: {str(e)}") def on_serial_number_changed(self): """序列号输入框内容变化时的处理 - 已禁用""" pass def verify_serial_and_update_display(self, serial): """验证序列号并更新显示 - 已禁用""" pass def _background_verify_serial_for_display(self, serial): """后台线程中查询序列号状态 - 已禁用""" pass def update_endtime_display(self, endtime_text, status_text): """更新到期时间显示 - 直接显示永久有效""" try: if self.endtime_label: self.endtime_label.setText("永久有效") self.endtime_label.setStyleSheet("color: green; font-weight: bold;") except Exception as e: print(f"更新显示失败: {str(e)}") def closeEvent(self, event): """程序关闭事件 - 移除授权验证清理""" try: # 移除所有授权验证相关的停止代码 if hasattr(self, 'token_timer'): self.token_timer.stop() if hasattr(self, 'worker_thread') and self.worker_thread.is_alive(): self.关闭线程() except Exception as e: print(f"关闭程序时出错: {str(e)}") finally: event.accept() def register_hotkey(self): """注册F10和F12全局热键(兼容版,处理不同keyboard库版本)""" try: import keyboard # 尝试清除之前的热键注册(兼容不同版本) try: keyboard.clear_all_hotkeys() except AttributeError: # 旧版本keyboard库可能没有这个方法 pass # 使用更兼容的热键注册方式 def safe_add_hotkey(key, callback): try: keyboard.add_hotkey(key, callback) return True except Exception as e: print(f"[WARNING] 热键 {key} 注册失败: {e}") return False # 注册热键 f10_success = safe_add_hotkey('F10', self.on_start_clicked) f12_success = safe_add_hotkey('F12', self.on_end_clicked) if f10_success and f12_success: self.logger.info("✅ F10和F12热键注册成功") elif f10_success: self.logger.info("✅ F10热键注册成功,F12注册失败") elif f12_success: self.logger.info("✅ F12热键注册成功,F10注册失败") else: self.logger.warning("⚠️ 热键注册失败,请使用界面按钮") except ImportError: self.logger.warning("⚠️ keyboard库未安装,热键功能不可用") except Exception as e: self.logger.error(f"❌ 热键注册失败: {str(e)}") self.logger.info("💡 可以使用界面按钮代替热键") def setup_ui(self): """设置UI界面""" self.ui.show() # 设置findPicLog控件为不可编辑 find_pic_log_widget = self.ui.findChild(QTextEdit, "findPicLog") if find_pic_log_widget: find_pic_log_widget.setReadOnly(True) # 添加morenwj控件的处理 self.morenwj_checkbox = self.ui.findChild(QCheckBox, "morenwj") if self.morenwj_checkbox: self.morenwj_checkbox.toggled.connect(self.on_morenwj_toggled) # 初始状态处理 self.on_morenwj_toggled(self.morenwj_checkbox.isChecked()) else: print("未找到morenwj控件") # 设置滑块控件与标签映射 self.setup_sliders() # 只连接必要信号,控件显示状态在load_settings中处理 for i in range(1, 6): tian_combo = self.ui.findChild(QComboBox, f"caijidl{i}tian") combo_name = f"caijidl{i}kj" combo = self.ui.findChild(QComboBox, combo_name) if tian_combo: tian_combo.currentTextChanged.connect( lambda text, idx=i: self.on_caijidl_tian_changed(text, idx) ) else: print(f"未找到caijidl{i}tian控件") if combo: combo.setVisible(False) else: print(f"未找到{combo_name}控件") # 获取start按钮并连接点击事件 start_btn = self.ui.findChild(QPushButton, "start") if start_btn: start_btn.clicked.connect(self.on_start_clicked) else: print("未找到start按钮") # 获取中心点扩散找图按钮并连接点击事件 start_btn = self.ui.findChild(QPushButton, "kuoFindPic") if start_btn: start_btn.clicked.connect(self.on_kuoFindPic_clicked) else: print("未找到kuoFindPic按钮") # 设置滑块控件与标签映射 self.setup_sliders() def setup_sliders(self): """设置滑块控件与标签的映射关系""" # 流畅度滑块 (0-100 整数) self.liucangdu_slider = self.ui.findChild(QSlider, "liucangdu") self.liucangdunum_label = self.ui.findChild(QLabel, "liucangdunum") if self.liucangdu_slider and self.liucangdunum_label: self.liucangdu_slider.setRange(0, 100) self.liucangdu_slider.setValue(20) # 默认值 self.liucangdunum_label.setText(f"{self.liucangdu_slider.value()}") self.liucangdu_slider.valueChanged.connect(self.on_liucangdu_changed) else: print("未找到流畅度滑块或标签控件") # Findhuadong滑块 (0-4 支持小数点,使用0-400表示0.00-4.00) self.Findhuadong_slider = self.ui.findChild(QSlider, "Findhuadong") self.Findhuadongnum_label = self.ui.findChild(QLabel, "Findhuadongnum") if self.Findhuadong_slider and self.Findhuadongnum_label: self.Findhuadong_slider.setRange(0, 400) # 设置默认值0.3 self.Findhuadong_slider.setValue(30) # 默认0.3 self.Findhuadongnum_label.setText("0.30") self.Findhuadong_slider.valueChanged.connect(self.on_Findhuadong_changed) else: print("未找到Findhuadong滑块或标签控件") # 随机因子滑块 (0-5 支持小数点,使用0-500表示0.00-5.00) self.suiji_slider = self.ui.findChild(QSlider, "suiji") self.suijinum_label = self.ui.findChild(QLabel, "suijinum") if self.suiji_slider and self.suijinum_label: self.suiji_slider.setRange(0, 500) self.suiji_slider.setValue(40) # suiji_value = self.suiji_slider.value() / 100 self.suijinum_label.setText(f"{suiji_value:.2f}") self.suiji_slider.valueChanged.connect(self.on_suiji_changed) else: print("未找到随机因子滑块或标签控件") # 延时滑块 (0-4 支持小数点,使用0-400表示0.00-4.00) self.yanshi_slider = self.ui.findChild(QSlider, "yanshi") self.yanshinum_label = self.ui.findChild(QLabel, "yanshinum") if self.yanshi_slider and self.yanshinum_label: self.yanshi_slider.setRange(0, 400) self.yanshi_slider.setValue(60) # 默认2.0 yanshi_value = self.yanshi_slider.value() / 100 self.yanshinum_label.setText(f"{yanshi_value:.2f}s") self.yanshi_slider.valueChanged.connect(self.on_yanshi_changed) else: print("未找到延时滑块或标签控件") # 添加startFindNPic按钮并连接点击事件 start_npic_btn = self.ui.findChild(QPushButton, "startFindNPic") if start_npic_btn: start_npic_btn.clicked.connect(self.on_startFindNPic_clicked) else: print("未找到startFindNPic按钮") # 添加CleanLog按钮并连接点击事件 clean_log_btn = self.ui.findChild(QPushButton, "CleanLog") if clean_log_btn: clean_log_btn.clicked.connect(self.on_clean_log_clicked) else: print("未找到CleanLog按钮") # 获取startFindPic按钮并连接点击事件 start_find_pic_btn = self.ui.findChild(QPushButton, "startFindPic") # 获取findYeman按钮并连接点击事件 find_yeman_btn = self.ui.findChild(QPushButton, "findYeman") if find_yeman_btn: find_yeman_btn.clicked.connect(self.on_findYeman_clicked) else: print("未找到findYeman按钮") # 添加selectPic1到selectPic8按钮连接 for i in range(1, 9): select_pic_btn = self.ui.findChild(QPushButton, f"selectPic{i}") if select_pic_btn: # 确保只连接一次 try: # 先断开所有连接,再重新连接 select_pic_btn.clicked.disconnect() except: pass # 动态连接到对应的事件处理方法 select_pic_btn.clicked.connect(lambda checked, idx=i: self.on_selectPic_clicked(idx)) if start_find_pic_btn: start_find_pic_btn.clicked.connect(self.on_start_find_pic_clicked) else: print("未找到startFindPic按钮") # 添加End按钮连接 stop_btn = self.ui.findChild(QPushButton, "End") if stop_btn: stop_btn.clicked.connect(self.on_end_clicked) else: print("未找到stopButton") def on_liucangdu_changed(self, value): """流畅度滑块值变化处理""" print(value) # 检查标签控件是否已成功初始化 if self.liucangdunum_label: self.liucangdunum_label.setText(f"{value}") else: print("流畅度标签控件未初始化") # 更新延时系统的帧率 fps = 5 + 55 * (value / 100) ** 1.5 self.delay_system.set_target_fps(int(round(fps))) def on_suiji_changed(self, value): """随机因子滑块值变化处理""" suiji_value = value / 100 self.suijinum_label.setText(f"{suiji_value:.2f}") # 更新延时系统的随机因子 self.delay_system.set_random_factor(min(5.0, max(0.0, suiji_value))) def on_yanshi_changed(self, value): """延时滑块值变化处理""" yanshi_value = value / 100 self.yanshinum_label.setText(f"{yanshi_value:.2f}s") # 更新延时系统的基础延时 self.delay_system.set_base_delay(yanshi_value) def on_Findhuadong_changed(self, value): """Findhuadong滑块值变化处理""" findhuadong_value = value / 100 self.Findhuadongnum_label.setText(f"{findhuadong_value:.2f}") # 保存设置 self.save_settings() def setup_number_validators(self): """为数字输入控件添加验证器""" # 创建验证器 (0-1000的整数范围) validator = QIntValidator(0, 1000, self) # 为三个时间控件添加验证器 time_controls = ["caiji_beisaotiantime", "jiancetime", "bangzhutime"] for control_name in time_controls: control = self.ui.findChild(QLineEdit, control_name) if control: control.setValidator(validator) # 确保现有文本是数字,如果不是则清空 text = control.text() if text and not text.isdigit(): control.clear() def on_selectPic_clicked(self, button_index=1): print(f"点击了选择图片按钮{button_index}") file_dialog = QFileDialog() # 获取对应按钮的已选路径 image_path_attr = f"selected_image_path{button_index}" if hasattr(self, image_path_attr): selected_path = getattr(self, image_path_attr) if selected_path: # 如果已有选择路径,则设为初始目录 file_dialog.setDirectory(os.path.dirname(selected_path)) else: file_dialog.setDirectory("img") # 默认目录 file_path, _ = file_dialog.getOpenFileName( self.ui, f"选择图片{button_index}", "", "图片文件 (*.bmp *.png *.jpg)" ) if file_path: # 保存到对应的属性 setattr(self, image_path_attr, file_path) log_message = f"已选择图片{button_index}: {file_path}" print(log_message) self.logger.info(log_message) # 将图片地址添加到对应的imgaddress控件中 imgaddress_widget = self.ui.findChild(QLineEdit, f"imgaddress{button_index}") if imgaddress_widget: imgaddress_widget.setText(file_path) return #点击开始 def on_start_clicked(self): # 检查是否已有线程在运行 if hasattr(self, 'worker_thread') and self.worker_thread.is_alive(): print("线程已在运行中") return # 创建并启动工作线程 self.worker_thread = threading.Thread(target=self.execute_business_logic) self.worker_thread.daemon = True # 设置为守护线程 self.worker_thread.start() # 切换到运行日志页面(tab_6) QTimer.singleShot(0, self.switch_to_log_tab) # 获取日志控件 self.logger.info("业务逻辑线程已启动") def switch_to_log_tab(self): """切换到运行日志标签页""" # 尝试通过self.ui查找QTabWidget控件 panel = self.ui.findChild(QTabWidget, "panel") if not panel: print("未找到QTabWidget控件: panel") self.logger.error("未找到QTabWidget控件: panel") # 打印UI的所有子控件名称,用于调试 print("UI子控件列表:") for child in self.ui.children(): print(f" {child.objectName()}") return # 检查标签页数量,避免潜在的索引错误 if panel.count() == 0: print("标签页控件为空") self.logger.error("标签页控件为空") return # 查找tab_6标签页 for i in range(panel.count()): if panel.widget(i).objectName() == "tab_6": panel.setCurrentIndex(i) return # 如果没找到tab_6,切换到第一个标签页 panel.setCurrentIndex(0) def execute_business_logic(self): """在新线程中执行业务逻辑""" # 直接执行业务逻辑,无需登录验证 self.logger.info("✅ 永久授权版 - 开始执行业务逻辑") # 创建BusinessLogic实例 business_logic = BusinessLogic(日志=self.logger) # 将暂停控制属性和终止信号传递给business_logic business_logic.should_pause = False business_logic.pause_event = self.pause_event business_logic.should_stop = False # 初始化终止标志 # 设置主线程关闭函数回调,让business_logic能够调用主程序的关闭线程 business_logic.main_thread_closer = self.关闭线程 # 保存business_logic引用,以便主线程能够控制它 self.business_logic = business_logic if business_logic.execute(): self.logger.info("✅ 业务逻辑执行成功") self.logger.info(f"开图设置: {'已启用' if business_logic.get_setting('kaitusetting') else '未启用'}") self.logger.info(f"清货设置: {'已启用' if business_logic.get_setting('qinghuosetting') else '未启用'}") self.logger.info(f"综合设置: {'已启用' if business_logic.get_setting('zonghesetting') else '未启用'}") self.logger.info(f"采集设置: {'已启用' if business_logic.get_setting('caijisetting') else '未启用'}") else: # 添加延时 self.logger.info("业务逻辑执行失败,开始停止") def on_end_clicked(self): """停止线程的执行(增强版,确保真正终止)""" try: # 添加调试信息 print("[DEBUG] on_end_clicked 被调用") self.logger.info("⏹️ 收到停止指令") # 检查是否有活跃线程 if hasattr(self, 'worker_thread') and self.worker_thread.is_alive(): print("[DEBUG] 发现活跃线程,开始停止流程") # 立即停止线程 self.关闭线程() else: print("[DEBUG] 没有活跃线程需要停止") self.logger.info("✅ 程序已停止") except Exception as e: print(f"[ERROR] on_end_clicked 执行失败: {str(e)}") self.logger.error(f"停止程序时出错: {str(e)}") # 强制重置状态 if hasattr(self, '_stopping'): self._stopping = False def on_start_clicked1(self): print("点击了开始按钮") hwnd = win32gui.FindWindow(None, "万国觉醒") if not hwnd: msg = "未找到目标窗口" print(msg) self.logger.info(msg) return Zd = Zhongdi(hwnd) result =Zd.get_ziyuan_cont("玉米") if result: print(f"找到玉米,位置: ({result['x']}, {result['y']})") self.logger.info(f"找到玉米,位置: ({result['x']}, {result['y']})") def on_start_find_pic_clicked(self): image_path = None imgaddress_widget = self.ui.findChild(QLineEdit, f"imgaddress1") if imgaddress_widget and imgaddress_widget.text(): image_path = imgaddress_widget.text() if not image_path: msg = "请先选择图片" self.logger.info(msg) return hwnd = win32gui.FindWindow(None, "万国觉醒") if not hwnd: msg = "未找到目标窗口" self.logger.info(msg) return # 获取SimScore输入框的值 sim_score_input = self.ui.findChild(QLineEdit, "SimScore") sim_score = 0.85 # 默认相似度 if sim_score_input and sim_score_input.text(): try: sim_score = float(sim_score_input.text()) except ValueError: msg = "相似度输入无效,使用默认值0.85" self.logger.info(msg) processor = ImageProcessor(hwnd,self.logger) # 调用找图功能时传入相似度参数 result = processor.find_image(image_path, sim_score) if not os.path.exists(image_path): msg = f"错误:图片不存在 {image_path}" self.logger.info(msg) return False if result: # 绘制红色矩形框标记找到的区域(使用文件顶部已导入的win32gui和win32con) #鼠标移动到图片位置 # 输出日志信息 msg = f"找到图片位置: ({result['x']}, {result['y']}),相似度: {sim_score}" self.logger.info(msg) else: msg = f"未找到图片(相似度: {sim_score})" self.logger.info(msg) def on_findYeman_clicked(self): self.logger.info("开始图片文字识别") # 移除登录验证 def on_kuoFindPic_clicked(self): image_path = None imgaddress_widget = self.ui.findChild(QLineEdit, f"imgaddress1") if imgaddress_widget and imgaddress_widget.text(): image_path = imgaddress_widget.text() if not image_path: msg = "请先选择图片" self.logger.info(msg) return hwnd = win32gui.FindWindow(None, "万国觉醒") if not hwnd: msg = "未找到目标窗口" self.logger.info(msg) return # 获取SimScore输入框的值 sim_score_input = self.ui.findChild(QLineEdit, "SimScore") sim_score = 0.85 # 默认相似度 if sim_score_input and sim_score_input.text(): try: sim_score = float(sim_score_input.text()) except ValueError: msg = "相似度输入无效,使用默认值0.85" self.logger.info(msg) processor = ImageProcessor(hwnd) # 调用找图功能时传入相似度参数 result = processor.find_image_from_center(self.selected_image_path, sim_score) if not os.path.exists(self.selected_image_path): msg = f"错误:图片不存在 {self.selected_image_path}" self.logger.info(msg) return if result: # 绘制红色矩形框标记找到的区域(使用文件顶部已导入的win32gui和win32con) #鼠标移动到图片位置 mouse = MouseController(hwnd) mouse.click_position( result) # 输出日志信息 msg = f"找到图片,位置: ({result['x']}, {result['y']}),相似度: {sim_score}" self.logger.info(msg) else: msg = f"未找到图片(相似度: {sim_score})" self.logger.info(msg) def on_startFindNPic_clicked(self): msg = "开始批量查找图片" self.logger.info(msg) hwnd = win32gui.FindWindow(None, "万国觉醒") if not hwnd: msg = "未找到目标窗口" self.logger.info(msg) return # 获取SimScore输入框的值 sim_score_input = self.ui.findChild(QLineEdit, "SimScore") sim_score = 0.85 # 默认相似度 if sim_score_input and sim_score_input.text(): try: sim_score = float(sim_score_input.text()) except ValueError: msg = "相似度输入无效,使用默认值0.85" self.logger.info(msg) processor = ImageProcessor(hwnd,self.logger) # 遍历imgaddress1到imgaddress8 for i in range(1, 9): # 获取图片地址 imgaddress_widget = self.ui.findChild(QLineEdit, f"imgaddress{i}") if imgaddress_widget and imgaddress_widget.text(): image_path = imgaddress_widget.text() if not os.path.exists(image_path): msg = f"错误:图片不存在 {image_path}" self.logger.info(msg) continue msg = f"正在查找图片 {i}: {image_path}" self.logger.info(msg) # 调用找图功能 result = processor.find_image(image_path, sim_score) if result: # 输出日志信息 msg = f"【找到图片 {i},位置: ({result['x']}, {result['y']}),相似度: {sim_score}】" self.logger.info(msg) else: msg = f"未找到图片 {i} (相似度: {sim_score})" self.logger.info(msg) else: msg = f"图片地址 {i} 为空,跳过查找" self.logger.info(msg) msg = "批量查找图片完成" self.logger.info(msg) def on_clean_log_clicked(self): """清空日志文本框""" self.logger.clear() self.logger.info("日志已清空") def on_selectGemeadd_clicked(self): """处理selectGemeadd按钮点击事件,弹出文件选择器并显示选择的文件路径""" file_dialog = QFileDialog() file_path, _ = file_dialog.getOpenFileName( self.ui, "选择游戏文件", "", "所有文件 (*.*)" ) if file_path: # 将选择的文件路径显示到gameaddress控件中 gameaddress_widget = self.ui.findChild(QLineEdit, "gameaddress") if gameaddress_widget: gameaddress_widget.setText(file_path) self.logger.info(f"已选择游戏文件: {file_path}") def on_caijidl_tian_changed(self, text, index): """处理采集队选择变化事件""" pass def load_settings(self): """加载设置文件 - 完整优化版""" try: # 检查并初始化设置文件 if not os.path.exists('settings.json') or os.path.getsize('settings.json') == 0: with open('settings.json', 'w', encoding='utf-8') as f: json.dump({ "kaitusetting": False, "qinghuosetting": False, "zonghesetting": False, "caijisetting": False, "tandong": False, "liucangdu": 20, "suiji": 30, "yanshi": 60, "Findhuadong": 30, "cundang": False, "bszhuzha": False }, f, ensure_ascii=False, indent=4) return # 读取并解析设置文件 with open('settings.json', 'r', encoding='utf-8') as f: content = f.read().strip() if not content: raise ValueError("设置文件为空") settings = json.loads(content) # 处理所有控件类型 widget_handlers = { QLineEdit: lambda w, v: w.setText(str(v)), QSpinBox: lambda w, v: w.setValue(int(v)), QCheckBox: lambda w, v: w.setChecked(bool(v)), QComboBox: lambda w, v: w.setCurrentText(str(v)), QSlider: lambda w, v: w.setValue(int(v)) } # 批量处理设置项 for name, value in settings.items(): if not name: continue try: # 查找控件 widget = None # 优先处理特定类型的控件 if name.endswith(('kj', 'tian')): widget = self.ui.findChild(QComboBox, name) elif name in ['liucangdu', 'suiji', 'yanshi', 'Findhuadong']: widget = self.ui.findChild(QSlider, name) else: # 通用查找 for widget_type in widget_handlers: widget = self.ui.findChild(widget_type, name) if widget: break if not widget: continue # 设置值 for widget_type, handler in widget_handlers.items(): if isinstance(widget, widget_type): try: handler(widget, value) # 特殊处理滑块控件的标签更新 if isinstance(widget, QSlider): value_int = int(value) if name == 'liucangdu': label = self.ui.findChild(QLabel, "liucangdunum") if label: label.setText(str(value_int)) if hasattr(self, 'delay_system') and self.delay_system: fps = 5 + 55 * (value_int / 100) ** 1.5 self.delay_system.set_target_fps(int(round(fps))) elif name == 'suiji': label = self.ui.findChild(QLabel, "suijinum") if label: label.setText(f"{value_int / 100:.2f}") if hasattr(self, 'delay_system') and self.delay_system: self.delay_system.set_random_factor(min(5.0, max(0.0, value_int / 100))) elif name == 'yanshi': label = self.ui.findChild(QLabel, "yanshinum") if label: label.setText(f"{value_int / 100:.2f}s") if hasattr(self, 'delay_system') and self.delay_system: self.delay_system.set_base_delay(value_int / 100) elif name == 'Findhuadong': label = self.ui.findChild(QLabel, "Findhuadongnum") if label: label.setText(f"{value_int / 100:.2f}") except (ValueError, TypeError) as e: print(f"设置控件{name}的值失败: {e}") break except Exception as e: print(f"处理设置项{name}时出错: {str(e)}") continue except Exception as e: print(f"加载设置文件出错: {str(e)}") with open('settings.json', 'w', encoding='utf-8') as f: json.dump({}, f, ensure_ascii=False, indent=4) # ===== 采集队控件显示逻辑初始化 ===== try: # 获取控制复选框 cundang = self.ui.findChild(QCheckBox, "cundang") bszhuzha = self.ui.findChild(QCheckBox, "bszhuzha") if cundang and bszhuzha: huncun_checked = cundang.isChecked() bszhuzha_checked = bszhuzha.isChecked() # 处理所有采集队控件 for i in range(1, 8): # 宝石采集队(要被控制的控件) caijidl_kj = self.ui.findChild(QComboBox, f"caijidl{i}kj") # 存储采集队(控制源控件) caijidl_tian = self.ui.findChild(QComboBox, f"caijidl{i}tian") # 根据需求设置宝石采集队的可见性 if caijidl_kj: # 当cundang勾选时显示宝石采集队 # 或者当bszhuzha勾选时也显示宝石采集队(但值由caijidl_tian决定) should_show = huncun_checked or bszhuzha_checked caijidl_kj.setVisible(should_show) # 当bszhuzha勾选时,同步存储采集队的值到宝石采集队 if bszhuzha_checked and caijidl_tian and caijidl_kj: current_text = caijidl_tian.currentText() index = caijidl_kj.findText(current_text) if index >= 0: caijidl_kj.setCurrentIndex(index) else: caijidl_kj.setCurrentText(current_text) except Exception as e: print(f"初始化采集队控件显示失败: {str(e)}") def connect_signals(self): """连接所有控件的值变化信号""" # 连接采集队控制复选框 cundang = self.ui.findChild(QCheckBox, "cundang") bszhuzha = self.ui.findChild(QCheckBox, "bszhuzha") morenwj = self.ui.findChild(QCheckBox, "morenwj") if cundang: cundang.toggled.connect(self.save_settings) cundang.toggled.connect(self.update_caijidl_visibility) if bszhuzha: bszhuzha.toggled.connect(self.save_settings) bszhuzha.toggled.connect(self.update_caijidl_visibility) if morenwj: # 添加morenwj控件的信号连接到update_caijidl_visibility方法 morenwj.toggled.connect(self.update_caijidl_visibility) # 添加selectGemeadd按钮的信号连接 select_gemeadd_btn = self.ui.findChild(QPushButton, "selectGemeadd") if select_gemeadd_btn: select_gemeadd_btn.clicked.connect(self.on_selectGemeadd_clicked) # 连接存储采集队控件的变化信号 for i in range(1, 8): caijidl_tian = self.ui.findChild(QComboBox, f"caijidl{i}tian") if caijidl_tian: caijidl_tian.currentIndexChanged.connect(self.update_caijidl_visibility) # 初始化采集队显示 self.update_caijidl_visibility() # 连接其他控件信号 for widget in self.ui.findChildren((QLineEdit, QSpinBox, QCheckBox, QPushButton, QComboBox, QSlider)): name = widget.objectName() if not name: # 跳过没有名称的控件 continue if isinstance(widget, QLineEdit): widget.textChanged.connect(self.save_settings) elif isinstance(widget, QSpinBox): widget.valueChanged.connect(self.save_settings) elif isinstance(widget, QCheckBox): widget.toggled.connect(self.save_settings) elif isinstance(widget, QPushButton) and widget.isCheckable(): widget.toggled.connect(self.save_settings) elif isinstance(widget, QComboBox): widget.currentIndexChanged.connect(self.save_settings) elif isinstance(widget, QSlider): widget.valueChanged.connect(self.save_settings) def on_morenwj_toggled(self, checked): """处理morenwj复选框的勾选状态变化""" # 禁用或启用采集队主将控件 for i in range(1, 8): # 主将控件 zhu_combo = self.ui.findChild(QComboBox, f"caijidl{i}zhu") if zhu_combo: zhu_combo.setEnabled(not checked) # 副将控件 fu_combo = self.ui.findChild(QComboBox, f"caijidl{i}fu") if fu_combo: fu_combo.setEnabled(not checked) def update_caijidl_visibility(self): """更新采集队控件显示状态""" try: cundang = self.ui.findChild(QCheckBox, "cundang") bszhuzha = self.ui.findChild(QCheckBox, "bszhuzha") if not cundang or not bszhuzha: return huncun_checked = cundang.isChecked() bszhuzha_checked = bszhuzha.isChecked() # 查找morenwj控件并根据其状态设置bszhuzha和cundang的可设置性 morenwj = self.ui.findChild(QCheckBox, "morenwj") if morenwj and morenwj.isChecked(): # 当morenwj被勾选时,bszhuzha和cundang更改为不可设置 cundang.setEnabled(False) bszhuzha.setEnabled(False) else: # 当morenwj未被勾选时,bszhuzha和cundang保持可设置 cundang.setEnabled(True) bszhuzha.setEnabled(True) # 当bszhuzha被勾选时,自动勾选cundang if bszhuzha_checked and not huncun_checked: cundang.setChecked(True) huncun_checked = True # 处理所有采集队控件 for i in range(1, 8): # 宝石采集队控件 caijidl_kj = self.ui.findChild(QComboBox, f"caijidl{i}kj") # 存储采集队控件 caijidl_tian = self.ui.findChild(QComboBox, f"caijidl{i}tian") # 控制宝石采集队的可见性 if caijidl_kj: # 检查cundang是否可设置 if not cundang.isEnabled(): # 当cundang不可设置时,隐藏宝石采集队控件 caijidl_kj.setVisible(False) else: # 当cundang可设置时,根据cundang是否被勾选决定显示/隐藏 should_show = huncun_checked caijidl_kj.setVisible(should_show) # 不对caijidl_tian进行隐藏操作,始终显示 # 当bszhuzha勾选且宝石采集队可见时,同步存储采集队的值到宝石采集队 if bszhuzha_checked and caijidl_tian and caijidl_kj and caijidl_kj.isVisible(): current_text = caijidl_tian.currentText() index = caijidl_kj.findText(current_text) if index >= 0: caijidl_kj.setCurrentIndex(index) else: caijidl_kj.setCurrentText(current_text) except Exception as e: print(f"更新采集队控件显示失败: {str(e)}") def save_settings(self): """保存所有UI控件的设置到文件""" settings = {} # 保存所有控件的设置,不再限制特定名称 widgets = self.ui.findChildren((QLineEdit, QSpinBox, QCheckBox, QPushButton, QRadioButton, QComboBox, QSlider)) for widget in widgets: name = widget.objectName() if name: # 只要有名称的控件都保存 if isinstance(widget, QLineEdit): settings[name] = widget.text() elif isinstance(widget, QSpinBox): settings[name] = widget.value() elif isinstance(widget, QCheckBox): settings[name] = widget.isChecked() elif isinstance(widget, QPushButton) and widget.isCheckable(): settings[name] = widget.isChecked() elif isinstance(widget, QRadioButton): settings[name] = widget.isChecked() elif isinstance(widget, QComboBox): settings[name] = widget.currentText() # 保存当前选中文本 elif isinstance(widget, QSlider): settings[name] = widget.value() # 保存滑块的值 try: with open('settings.json', 'w', encoding='utf-8') as f: json.dump(settings, f, indent=4, ensure_ascii=False) # 关键修改 print("设置保存成功") except Exception as e: print(f"保存设置失败: {str(e)}") def handle_login(self): """F10登录功能 - 已移除授权验证,直接返回成功""" self.logger.info("✅ 永久授权版 - 无需登录验证") return True def show_message_slot(self, msg_type, title, content): """主线程中显示消息框的槽函数""" if msg_type == 'information': QMessageBox.information(self, title, content) elif msg_type == 'warning': # 显示警告消息框,但不关闭程序 result = QMessageBox.warning(self, title, content, QMessageBox.StandardButton.Ok) elif msg_type == 'critical': # 显示错误消息框,但不关闭程序 QMessageBox.critical(self, title, content) else: QMessageBox.about(self, title, content) def 关闭线程(self): """停止线程执行和心跳定时器(快速响应版)""" print("[DEBUG] 关闭线程函数被调用") self.logger.info("⏹️ 正在停止程序运行...") # 停止工作线程 if hasattr(self, 'worker_thread') and self.worker_thread.is_alive(): print("[DEBUG] 工作线程存在且活跃,开始停止") # 通知业务逻辑线程终止 - 立即设置 if hasattr(self, 'business_logic'): print("[DEBUG] 设置business_logic.should_stop = True") self.business_logic.should_stop = True # 如果有暂停事件,确保线程能从暂停状态恢复并退出 if hasattr(self.business_logic, 'pause_event') and self.business_logic.pause_event: print("[DEBUG] 设置business_logic.pause_event唤醒暂停线程") self.business_logic.pause_event.set() else: print("[DEBUG] 没有找到business_logic实例") # 添加线程安全终止标志 if hasattr(self, 'should_stop'): print("[DEBUG] 设置should_stop = True") self.should_stop = True # 确保线程能从暂停状态恢复并退出 if hasattr(self, 'pause_event'): print("[DEBUG] 设置pause_event唤醒暂停线程") self.pause_event.set() # 唤醒暂停的线程 # 快速等待 - 只等待3秒 print("[DEBUG] 快速等待线程结束...") import threading current_thread_id = threading.get_ident() worker_thread_id = self.worker_thread.ident print(f"[DEBUG] 当前线程ID: {current_thread_id}, 工作线程ID: {worker_thread_id}") if current_thread_id != worker_thread_id: self.worker_thread.join(timeout=3.0) # 只等待3秒 # 如果还在运行,立即强制终止 if self.worker_thread.is_alive(): print("[DEBUG] 线程在3秒内未停止,立即强制终止") # 强制终止线程 import ctypes try: tid = self.worker_thread.ident res = ctypes.pythonapi.PyThreadState_SetAsyncExc( ctypes.c_long(tid), ctypes.py_object(SystemExit) ) if res == 0: print("[DEBUG] 强制终止失败: 无效的线程ID") elif res != 1: ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, 0) print("[DEBUG] 强制终止失败: 系统错误") elif res == 1: print("[DEBUG] 强制终止成功") self.logger.info("✅ 程序已强制停止") except Exception as e: print(f"[DEBUG] 强制终止异常: {e}") self.logger.warning("⚠️ 程序停止时遇到问题") else: print("[DEBUG] 线程已正常结束") self.logger.info("✅ 程序已完全停止") else: print("[DEBUG] 没有活跃的工作线程") self.logger.info("✅ 程序已停止") def pause_thread(self): """暂停线程执行""" if hasattr(self, 'worker_thread') and self.worker_thread.is_alive(): self.should_pause = True self.pause_event.clear() # 清除事件,使线程进入等待状态 def resume_thread(self): """恢复线程执行""" if hasattr(self, 'worker_thread') and self.worker_thread.is_alive(): self.should_pause = False self.pause_event.set() # 设置事件,唤醒线程继续执行 if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow() window.ui.show() sys.exit(app.exec())
最新发布
11-01
import pandas as pd import tkinter as tk from tkinter import ttk, messagebox, filedialog import os import json import threading import openpyxl from openpyxl.utils.dataframe import dataframe_to_rows from tkinter.font import Font class ScrollableFrame(ttk.Frame): """自定义可滚动框架实现""" def __init__(self, parent, *args, **kwargs): super().__init__(parent, *args, **kwargs) # 创建Canvas和滚动条 self.canvas = tk.Canvas(self) self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview) self.scrollable_frame = ttk.Frame(self.canvas) # 配置Canvas self.canvas.configure(yscrollcommand=self.scrollbar.set) self.canvas_frame = self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") # 布局 self.canvas.pack(side="left", fill="both", expand=True) self.scrollbar.pack(side="right", fill="y") # 绑定事件 self.scrollable_frame.bind("<Configure>", self.on_frame_configure) self.canvas.bind("<Configure>", self.on_canvas_configure) self.canvas.bind_all("<MouseWheel>", self.on_mousewheel) def on_frame_configure(self, event): """当内部框架大小改变时更新滚动区域""" self.canvas.configure(scrollregion=self.canvas.bbox("all")) def on_canvas_configure(self, event): """当Canvas大小改变时调整内部框架宽度""" self.canvas.itemconfig(self.canvas_frame, width=event.width) def on_mousewheel(self, event): """鼠标滚轮滚动支持""" self.canvas.yview_scroll(int(-1*(event.delta/120)), "units") class ExcelControlPanel: def __init__(self, master): self.master = master master.title("功能点确认系统") master.geometry("1280x800") master.configure(bg="#f0f2f5") # 设置全局样式 self.set_styles() # 加载配置 self.config = self.load_config() # 初始化多列确认配置 self.confirmation_columns = self.config.get("confirmation_columns", []) # 创建界面元素 self.create_widgets() # 初始化数据 self.excel_path = "" self.df = None self.check_vars = {} # 存储每个功能点的复选框变量,key为item_id self.current_sheet = "" self.header_row = 0 # 启用列拖动 self.enable_column_dragging() def set_styles(self): """设置全局样式和字体""" style = ttk.Style() style.theme_use('clam') # 自定义字体 self.title_font = Font(family="Microsoft YaHei", size=16, weight="bold") self.subtitle_font = Font(family="Microsoft YaHei", size=12) self.normal_font = Font(family="Microsoft YaHei", size=10) # 配置样式 style.configure("TFrame", background="#f0f2f5") style.configure("TLabel", font=self.normal_font, background="#f0f2f5", foreground="#333") style.configure("TButton", font=self.normal_font, padding=8) style.configure("Treeview.Heading", font=self.subtitle_font, background="#4a76b5", foreground="white") style.configure("Treeview", font=self.normal_font, rowheight=30, background="white", fieldbackground="white") style.configure("Title.TLabel", font=self.title_font, background="#4a76b5", foreground="white", padding=10) style.configure("Status.TLabel", font=self.normal_font, background="#333", foreground="#fff", padding=5) style.configure("Card.TFrame", background="white", borderwidth=0, relief="solid", padding=10, bordercolor="#e1e4e8", borderradius=8) style.configure("Card.TLabelframe", background="white", borderwidth=1, relief="solid", padding=10, bordercolor="#e1e4e8", borderradius=8) style.configure("Card.TLabelframe.Label", font=self.subtitle_font, foreground="#2c3e50", background="white") # 按钮样式 style.map("Primary.TButton", background=[("active", "#3a66a5"), ("pressed", "#2a5685")], foreground=[("active", "white"), ("pressed", "white")]) style.configure("Primary.TButton", background="#4a76b5", foreground="white", font=self.subtitle_font, borderwidth=0, borderradius=4) style.map("Success.TButton", background=[("active", "#28a745"), ("pressed", "#218838")], foreground=[("active", "white"), ("pressed", "white")]) style.configure("Success.TButton", background="#28a745", foreground="white", font=self.subtitle_font, borderwidth=0, borderradius=4) style.map("Danger.TButton", background=[("active", "#dc3545"), ("pressed", "#c82333")], foreground=[("active", "white"), ("pressed", "white")]) style.configure("Danger.TButton", background="#dc3545", foreground="white", font=self.subtitle_font, borderwidth=0, borderradius=4) # 复选框样式 style.configure("Custom.TCheckbutton", background="white", font=self.normal_font) # 输入框样式 style.configure("Custom.TEntry", fieldbackground="#f8f9fa", bordercolor="#ced4da") def load_config(self): """加载配置文件""" config_path = "excel_config.json" default_config = { "id_col": "No.", "desc_col": "レビュー観点(CHN)", "status_col": "レビュー結果", "sheet_name": "", "header_row": 9, "last_dir": os.getcwd(), "custom_status": "OK", "confirmation_columns": [] } if os.path.exists(config_path): try: with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) # 确保所有键都存在 for key in default_config: if key not in config: config[key] = default_config[key] return config except: return default_config return default_config def save_config(self): """保存配置文件""" config_path = "excel_config.json" with open(config_path, 'w', encoding='utf-8') as f: json.dump(self.config, f, ensure_ascii=False, indent=2) def create_widgets(self): """创建现代化界面元素""" # 主容器 main_container = ttk.Frame(self.master, style="Card.TFrame") main_container.pack(fill="both", expand=True, padx=20, pady=20) # 标题栏 title_frame = ttk.Frame(main_container, style="Title.TFrame") title_frame.pack(fill="x", pady=(0, 20)) # 添加内置图标 try: # 创建一个简单的内置图标 self.icon_img = tk.PhotoImage(width=32, height=32) self.icon_img.put("#4a76b5", (0, 0, 32, 32)) icon_label = ttk.Label(title_frame, image=self.icon_img, background="#4a76b5") icon_label.pack(side="left", padx=(15, 10)) except: pass ttk.Label(title_frame, text="功能点确认系统", style="Title.TLabel").pack(side="left", padx=10) # 主内容区域(卡片式布局) content_frame = ttk.Frame(main_container) content_frame.pack(fill="both", expand=True) # 左侧控制面板(卡片) - 可滚动 control_container = ttk.Frame(content_frame, width=350) control_container.pack(side="left", fill="y", padx=(0, 20)) # 创建自定义可滚动框架 scrollable_frame = ScrollableFrame(control_container, width=350) scrollable_frame.pack(fill="both", expand=True) # 获取内部框架 inner_frame = scrollable_frame.scrollable_frame # 美化控制面板 control_card = ttk.LabelFrame(inner_frame, text="控制面板", style="Card.TLabelframe") control_card.pack(fill="both", expand=True, padx=10, pady=10, ipadx=5, ipady=5) # 文件选择区域 file_frame = ttk.LabelFrame(control_card, text="Excel文件设置") file_frame.pack(fill="x", padx=10, pady=(0, 15)) ttk.Label(file_frame, text="Excel文件路径:").pack(anchor="w", pady=(0, 5), padx=10) path_frame = ttk.Frame(file_frame) path_frame.pack(fill="x", pady=5, padx=10) self.path_entry = ttk.Entry(path_frame, width=30, style="Custom.TEntry") self.path_entry.pack(side="left", fill="x", expand=True, padx=(0, 5)) ttk.Button(path_frame, text="浏览", command=self.browse_file, width=8).pack(side="left") ttk.Button(file_frame, text="加载数据", command=self.load_data, style="Primary.TButton").pack(fill="x", pady=10, padx=10) # Sheet选择区域 sheet_frame = ttk.LabelFrame(control_card, text="工作表设置") sheet_frame.pack(fill="x", padx=10, pady=(0, 15)) ttk.Label(sheet_frame, text="当前Sheet:").pack(anchor="w", padx=10, pady=(10, 0)) self.sheet_var = tk.StringVar(value=self.config.get("sheet_name", "未选择")) sheet_display = ttk.Label(sheet_frame, textvariable=self.sheet_var, font=self.subtitle_font) sheet_display.pack(anchor="w", pady=(0, 10), padx=10) ttk.Button(sheet_frame, text="选择工作表", command=self.select_sheet, style="Primary.TButton").pack(fill="x", padx=10, pady=(0, 10)) # 表头行设置 header_frame = ttk.LabelFrame(control_card, text="表头设置") header_frame.pack(fill="x", padx=10, pady=(0, 15)) ttk.Label(header_frame, text="表头所在行号:").pack(anchor="w", padx=10, pady=(10, 0)) self.header_var = tk.IntVar(value=self.config.get("header_row", 9)) ttk.Entry(header_frame, textvariable=self.header_var, width=10, style="Custom.TEntry").pack(anchor="w", pady=5, padx=10) # 列名配置区域 col_frame = ttk.LabelFrame(control_card, text="列名配置") col_frame.pack(fill="x", padx=10, pady=(0, 15)) # ID列 ttk.Label(col_frame, text="ID列名:").pack(anchor="w", padx=10, pady=(10, 0)) self.id_col_var = tk.StringVar(value=self.config["id_col"]) ttk.Entry(col_frame, textvariable=self.id_col_var, style="Custom.TEntry").pack(fill="x", pady=(0, 10), padx=10) # 功能点列 ttk.Label(col_frame, text="功能点列名:").pack(anchor="w", padx=10, pady=(0, 0)) self.desc_col_var = tk.StringVar(value=self.config["desc_col"]) ttk.Entry(col_frame, textvariable=self.desc_col_var, style="Custom.TEntry").pack(fill="x", pady=(0, 10), padx=10) # 状态列 ttk.Label(col_frame, text="状态列名:").pack(anchor="w", padx=10, pady=(0, 0)) self.status_col_var = tk.StringVar(value=self.config["status_col"]) ttk.Entry(col_frame, textvariable=self.status_col_var, style="Custom.TEntry").pack(fill="x", pady=(0, 10), padx=10) # 自定义状态 ttk.Label(col_frame, text="自定义确认状态:").pack(anchor="w", padx=10, pady=(0, 0)) self.custom_status_var = tk.StringVar(value=self.config.get("custom_status", "OK")) ttk.Entry(col_frame, textvariable=self.custom_status_var, style="Custom.TEntry").pack(fill="x", pady=(0, 10), padx=10) # 多列确认设置 multi_col_frame = ttk.LabelFrame(control_card, text="多列确认设置") multi_col_frame.pack(fill="x", padx=10, pady=(0, 15)) # 列选择器 ttk.Label(multi_col_frame, text="选择要确认的列:").pack(anchor="w", padx=10, pady=(10, 0)) col_selector_frame = ttk.Frame(multi_col_frame) col_selector_frame.pack(fill="x", pady=5, padx=10) self.col_selector = ttk.Combobox(col_selector_frame, state="readonly", width=15, style="Custom.TCombobox") self.col_selector.pack(side="left", fill="x", expand=True, padx=(0, 5)) # 添加/移除按钮 ttk.Button(col_selector_frame, text="添加", command=self.add_confirmation_column, width=8).pack(side="left") # 已选列列表 ttk.Label(multi_col_frame, text="已选确认列 (可拖动调整顺序):").pack(anchor="w", padx=10, pady=(10, 0)) self.selected_cols_listbox = tk.Listbox(multi_col_frame, height=3, font=self.normal_font, bg="#f8f9fa", highlightthickness=0) self.selected_cols_listbox.pack(fill="x", pady=5, padx=10) # 加载已配置的确认列 for col in self.confirmation_columns: self.selected_cols_listbox.insert(tk.END, col) # 移除按钮 remove_btn = ttk.Button(multi_col_frame, text="移除选中列", command=self.remove_confirmation_column) remove_btn.pack(fill="x", pady=(0, 10), padx=10) # 操作按钮区域 btn_frame = ttk.Frame(control_card) btn_frame.pack(fill="x", padx=10, pady=10) ttk.Button(btn_frame, text="保存配置", command=self.save_current_config, style="Success.TButton").pack(fill="x", pady=5) ttk.Button(btn_frame, text="确认选中项", command=self.confirm_selected, style="Primary.TButton").pack(fill="x", pady=5) ttk.Button(btn_frame, text="全选", command=self.select_all, style="Primary.TButton").pack(side="left", fill="x", expand=True, padx=(0, 5), pady=5) ttk.Button(btn_frame, text="取消全选", command=self.deselect_all, style="Danger.TButton").pack(side="left", fill="x", expand=True, padx=(5, 0), pady=5) ttk.Button(btn_frame, text="保存到Excel", command=self.save_to_excel, style="Success.TButton").pack(fill="x", pady=5) # 数据显示区域(卡片) data_card = ttk.LabelFrame(content_frame, text="功能点列表", style="Card.TLabelframe") data_card.pack(side="right", fill="both", expand=True) # 创建带滚动条的表格 columns = ("选择", "ID", "功能点", "状态") self.tree = ttk.Treeview(data_card, columns=columns, show="headings", height=20) vsb = ttk.Scrollbar(data_card, orient="vertical", command=self.tree.yview) hsb = ttk.Scrollbar(data_card, orient="horizontal", command=self.tree.xview) self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) # 添加分割条和详情文本框 paned_window = tk.PanedWindow(data_card, orient=tk.VERTICAL, sashrelief=tk.RAISED, sashwidth=4) paned_window.pack(fill="both", expand=True, padx=5, pady=5) # 将Treeview放入PanedWindow的上部分 tree_frame = ttk.Frame(paned_window) paned_window.add(tree_frame) self.tree.pack(in_=tree_frame, side="top", fill="both", expand=True) vsb.pack(in_=tree_frame, side="right", fill="y", padx=(0, 5), pady=5) hsb.pack(in_=tree_frame, side="bottom", fill="x", padx=5, pady=(0, 5)) # 配置列 col_widths = {"选择": 50, "ID": 120, "功能点": 600, "状态": 120} for col in columns: self.tree.heading(col, text=col, anchor="w") self.tree.column(col, width=col_widths[col], minwidth=50, stretch=tk.YES if col == "功能点" else tk.NO) # 添加标签样式 self.tree.tag_configure("confirmed", background="#d4edda") self.tree.tag_configure("pending", background="#f8d7da") # 绑定列调整事件 self.tree.bind("<Configure>", self.adjust_columns) # 添加详情文本框 detail_frame = ttk.Frame(paned_window, height=100) paned_window.add(detail_frame) ttk.Label(detail_frame, text="功能点详情:").pack(anchor="w", padx=5, pady=5) self.detail_text = tk.Text(detail_frame, wrap="word", font=self.normal_font, height=5) scroll_detail = ttk.Scrollbar(detail_frame, command=self.detail_text.yview) self.detail_text.config(yscrollcommand=scroll_detail.set) self.detail_text.pack(side="left", fill="both", expand=True, padx=5, pady=5) scroll_detail.pack(side="right", fill="y", padx=(0, 5), pady=5) self.detail_text.config(state="disabled") # 绑定Treeview选择事件 self.tree.bind("<<TreeviewSelect>>", self.on_tree_select) # 状态栏 self.status_var = tk.StringVar() self.status_var.set("就绪 - 请选择Excel文件开始") status_bar = ttk.Label(self.master, textvariable=self.status_var, style="Status.TLabel") status_bar.pack(side="bottom", fill="x") def on_tree_select(self, event): """当Treeview中选中行时,显示功能点详情""" selected_items = self.tree.selection() if not selected_items: return item = selected_items[0] # 获取功能点描述,在第三列(索引2) values = self.tree.item(item, "values") if len(values) >= 3: desc = values[2] self.detail_text.config(state="normal") self.detail_text.delete(1.0, tk.END) self.detail_text.insert(tk.END, desc) self.detail_text.config(state="disabled") def enable_column_dragging(self): """启用列拖动功能""" def on_header_click(event): # 记录开始拖动的列 region = self.tree.identify("region", event.x, event.y) if region == "heading": self.drag_start_col = self.tree.identify_column(event.x) def on_header_drag(event): # 处理拖动中的列 if hasattr(self, 'drag_start_col'): region = self.tree.identify("region", event.x, event.y) if region == "heading": end_col = self.tree.identify_column(event.x) if end_col != self.drag_start_col: # 移动列 self.move_column(self.drag_start_col, end_col) self.drag_start_col = end_col def on_header_release(event): # 结束拖动 if hasattr(self, 'drag_start_col'): del self.drag_start_col # 绑定事件 self.tree.bind("<ButtonPress-1>", on_header_click) self.tree.bind("<B1-Motion>", on_header_drag) self.tree.bind("<ButtonRelease-1>", on_header_release) def move_column(self, from_col, to_col): """移动列位置""" # 获取当前列顺序 columns = list(self.tree["columns"]) # 转换为索引 from_idx = int(from_col.replace("#", "")) - 1 to_idx = int(to_col.replace("#", "")) - 1 # 移动列 if from_idx < len(columns) and to_idx < len(columns): col = columns.pop(from_idx) columns.insert(to_idx, col) # 更新列顺序 self.tree["columns"] = columns # 重新配置列 for i, col in enumerate(columns): self.tree.heading(col, text=col) self.tree.column(col, anchor="w") # 调整列宽 self.adjust_columns(None) def adjust_columns(self, event): """根据窗口大小自动调整列宽""" if not self.tree.winfo_exists(): return width = self.tree.winfo_width() if width < 100: # 防止宽度过小 return # 设置列宽比例:选择 5%,ID 15%,功能点 70%,状态 10% self.tree.column("选择", width=int(width * 0.05)) self.tree.column("ID", width=int(width * 0.15)) self.tree.column("功能点", width=int(width * 0.7)) self.tree.column("状态", width=int(width * 0.1)) # 调整多列确认的列宽 if self.confirmation_columns: for col in self.confirmation_columns: self.tree.column(col, width=int(width * 0.1)) def add_confirmation_column(self): """添加确认列""" col = self.col_selector.get() if col and col not in self.confirmation_columns: self.confirmation_columns.append(col) self.selected_cols_listbox.insert(tk.END, col) # 更新Treeview列 self.update_treeview_columns() def update_treeview_columns(self): """更新Treeview列以显示确认列""" # 构建列列表 columns = ["选择", "ID", "功能点", "状态"] + self.confirmation_columns # 重新配置Treeview self.tree.configure(columns=columns) self.tree["show"] = "headings" # 设置列标题 for col in columns: self.tree.heading(col, text=col) # 设置最小宽度和拉伸属性 if col == "功能点": self.tree.column(col, stretch=tk.YES) else: self.tree.column(col, stretch=tk.NO, width=100) # 调整列宽 self.adjust_columns(None) def remove_confirmation_column(self): """移除确认列""" selection = self.selected_cols_listbox.curselection() if selection: index = selection[0] col = self.confirmation_columns.pop(index) self.selected_cols_listbox.delete(index) # 更新Treeview列 self.update_treeview_columns() def browse_file(self): initial_dir = self.config.get("last_dir", os.getcwd()) file_path = filedialog.askopenfilename( initialdir=initial_dir, filetypes=[("Excel文件", "*.xlsx;*.xls")] ) if file_path: self.path_entry.delete(0, tk.END) self.path_entry.insert(0, file_path) # 更新最后访问目录 self.config["last_dir"] = os.path.dirname(file_path) self.save_config() def select_sheet(self): """选择工作表""" file_path = self.path_entry.get() if not file_path or not os.path.exists(file_path): messagebox.showerror("错误", "请先选择有效的Excel文件") return try: # 获取所有sheet名称 xl = pd.ExcelFile(file_path) sheet_names = xl.sheet_names # 创建现代化选择对话框 sheet_dialog = tk.Toplevel(self.master) sheet_dialog.title("选择工作表") sheet_dialog.geometry("400x300") sheet_dialog.transient(self.master) sheet_dialog.grab_set() sheet_dialog.configure(bg="#f5f7fa") ttk.Label(sheet_dialog, text="请选择工作表:", font=self.subtitle_font, background="#f5f7fa").pack(pady=10) # 使用Treeview显示工作表 sheet_tree = ttk.Treeview(sheet_dialog, columns=("名称",), show="headings", height=8) sheet_tree.heading("名称", text="工作表名称") sheet_tree.column("名称", width=350) sheet_tree.pack(fill="both", expand=True, padx=20, pady=5) for name in sheet_names: sheet_tree.insert("", "end", values=(name,)) # 按钮框架 btn_frame = ttk.Frame(sheet_dialog) btn_frame.pack(fill="x", padx=20, pady=10) def on_select(): selected = sheet_tree.selection() if selected: self.current_sheet = sheet_tree.item(selected[0], "values")[0] self.sheet_var.set(self.current_sheet) sheet_dialog.destroy() # 修复按钮布局问题 ttk.Button(btn_frame, text="取消", command=sheet_dialog.destroy).pack(side="right", padx=5) ttk.Button(btn_frame, text="确定", command=on_select, style="Primary.TButton").pack(side="right") except Exception as e: messagebox.showerror("错误", f"读取Excel失败: {str(e)}") def load_data(self): file_path = self.path_entry.get() if not file_path or not os.path.exists(file_path): messagebox.showerror("错误", "无效的文件路径") return # 获取当前配置 id_col = self.id_col_var.get().strip() desc_col = self.desc_col_var.get().strip() status_col = self.status_col_var.get().strip() sheet_name = self.sheet_var.get() or None header_row = self.header_var.get() - 1 # pandas header是0-based索引 # 在状态栏显示加载中 self.status_var.set("正在加载数据...") self.master.update() def load_task(): try: # 读取Excel文件 if sheet_name: self.df = pd.read_excel( file_path, sheet_name=sheet_name, header=header_row ) else: self.df = pd.read_excel( file_path, header=header_row ) self.excel_path = file_path # 检查列是否存在 missing_cols = [] if id_col not in self.df.columns: missing_cols.append(f"ID列 '{id_col}'") if desc_col not in self.df.columns: missing_cols.append(f"功能点列 '{desc_col}'") if missing_cols: # 提供更详细的错误信息,包括可用列名 available_cols = "\n".join(self.df.columns) error_msg = ( f"以下列不存在: {', '.join(missing_cols)}\n\n" f"可用列名:\n{available_cols}\n\n" "请检查表头行设置是否正确(默认为第9行)" ) raise ValueError(error_msg) # 如果状态列不存在,则创建 if status_col not in self.df.columns: self.df[status_col] = "否" # 默认未确认 # 更新列选择器 self.col_selector["values"] = list(self.df.columns) # 清空Treeview self.tree.delete(*self.tree.get_children()) self.check_vars = {} # 添加数据到Treeview for i, row in self.df.iterrows(): status_value = row.get(status_col, "否") # 使用图标表示状态 status_icon = "✓" if status_value in ["是", "Y", "y", "Yes", "yes", "OK", "确认"] else "✗" status_text = f"{status_icon} {status_value}" tag = "confirmed" if status_icon == "✓" else "pending" # 构建行数据 values = [ "", # 选择列 - 留空,我们将使用自定义复选框 row[id_col], row[desc_col], status_text ] # 添加多列确认数据 for col in self.confirmation_columns: if col in row: values.append(row[col]) else: values.append("") # 插入行 item_id = self.tree.insert("", "end", values=values, tags=(tag,)) # 创建复选框变量 var = tk.BooleanVar(value=(status_icon == "✓")) self.check_vars[item_id] = var # 添加复选框到选择列 self.create_checkbox(item_id, var) # 更新状态 self.status_var.set(f"成功加载: {len(self.df)} 条记录") except Exception as e: self.status_var.set("加载失败") messagebox.showerror("加载错误", f"读取Excel失败: {str(e)}") # 在新线程中加载数据 threading.Thread(target=load_task, daemon=True).start() def create_checkbox(self, item_id, var): """为Treeview中的行创建复选框 (修复方法)""" # 创建Frame作为容器 checkbox_frame = tk.Frame(self.tree) # 创建复选框 cb = ttk.Checkbutton( checkbox_frame, variable=var, style="Custom.TCheckbutton", command=lambda: self.toggle_checkbox(item_id, var) ) cb.pack() # 正确嵌入到Treeview单元格 self.tree.set(item_id, column="选择", value="") # 清空单元格文本 # 获取"选择"列的索引 columns = self.tree["columns"] col_index = columns.index("选择") column_id = f"#{col_index+1}" # 列标识从1开始 # 使用正确的方法嵌入控件 self.tree.window_create(item_id, column=column_id, window=checkbox_frame) # 更新复选框状态 self.update_checkbox_state(item_id, var) def toggle_checkbox(self, item_id, var): """切换复选框状态""" # 更新行的状态显示 self.update_checkbox_state(item_id, var) def update_checkbox_state(self, item_id, var): """根据复选框状态更新行状态""" current_values = list(self.tree.item(item_id, "values")) if var.get(): current_values[3] = "✓ 待确认" self.tree.item(item_id, tags=("confirmed",)) else: current_values[3] = "✗ 未确认" self.tree.item(item_id, tags=("pending",)) self.tree.item(item_id, values=current_values) def confirm_selected(self): """确认选中的功能点""" selected_items = [] for item_id in self.tree.get_children(): if self.check_vars[item_id].get(): selected_items.append(item_id) if not selected_items: messagebox.showinfo("提示", "请先选择功能点") return custom_status = self.custom_status_var.get().strip() or "OK" for item_id in selected_items: self.check_vars[item_id].set(True) current_values = list(self.tree.item(item_id, "values")) # 更新状态列 current_values[3] = f"✓ {custom_status}" # 更新多列确认 if self.confirmation_columns and self.df is not None: row_idx = self.tree.index(item_id) for i, col in enumerate(self.confirmation_columns, start=4): if col in self.df.columns: current_values[i] = custom_status self.df.at[row_idx, col] = custom_status # 更新Treeview self.tree.item(item_id, values=tuple(current_values), tags=("confirmed",)) # 更新复选框状态(触发颜色变化) self.update_checkbox_state(item_id, self.check_vars[item_id]) self.status_var.set(f"已确认 {len(selected_items)} 个功能点") # 自动保存 self.auto_save() def select_all(self): """全选功能点""" for item_id in self.tree.get_children(): var = self.check_vars[item_id] var.set(True) self.update_checkbox_state(item_id, var) self.status_var.set("已全选所有功能点") def deselect_all(self): """取消全选功能点""" for item_id in self.tree.get_children(): var = self.check_vars[item_id] var.set(False) self.update_checkbox_state(item_id, var) self.status_var.set("已取消全选所有功能点") def save_current_config(self): """保存当前配置""" self.config["id_col"] = self.id_col_var.get().strip() self.config["desc_col"] = self.desc_col_var.get().strip() self.config["status_col"] = self.status_col_var.get().strip() self.config["sheet_name"] = self.sheet_var.get() self.config["header_row"] = self.header_var.get() self.config["custom_status"] = self.custom_status_var.get().strip() self.config["confirmation_columns"] = self.confirmation_columns self.save_config() messagebox.showinfo("成功", "配置已保存") def auto_save(self): """自动保存功能""" if self.df is None or not self.excel_path: return try: # 获取当前配置 id_col = self.id_col_var.get().strip() desc_col = self.desc_col_var.get().strip() status_col = self.status_col_var.get().strip() # 更新DataFrame中的确认状态 for i, item_id in enumerate(self.tree.get_children()): # 获取Treeview中的状态值(去掉图标) status_value = self.tree.item(item_id, "values")[3] if status_value.startswith(("✓", "✗")): status_value = status_value[2:].strip() self.df.at[i, status_col] = status_value # 保存回Excel - 使用openpyxl直接操作工作簿 wb = openpyxl.load_workbook(self.excel_path) if self.current_sheet in wb.sheetnames: del wb[self.current_sheet] ws = wb.create_sheet(self.current_sheet) # 写入数据 for r, row in enumerate(dataframe_to_rows(self.df, index=False, header=True), 1): ws.append(row) wb.save(self.excel_path) self.status_var.set("数据已自动保存") except Exception as e: self.status_var.set(f"自动保存失败: {str(e)}") def save_to_excel(self): if self.df is None: messagebox.showerror("错误", "没有加载的数据") return try: # 执行保存 self.auto_save() messagebox.showinfo("成功", f"数据已保存到:\n{self.excel_path}") self.status_var.set("数据保存成功") except Exception as e: messagebox.showerror("保存错误", f"写入Excel失败: {str(e)}\n请确保文件未被其他程序打开") def main(): root = tk.Tk() # 设置应用图标 try: # 创建一个简单的蓝色方块作为图标 icon_data = """ R0lGODlhEAAQAIAAAP///wAAACH5BAEAAAAALAAAAAAQABAAAAIOhI+py+0Po5y02ouzPgUAOw== """ icon_img = tk.PhotoImage(data=icon_data) root.tk.call('wm', 'iconphoto', root._w, icon_img) except: pass app = ExcelControlPanel(root) root.mainloop() if __name__ == "__main__": main() 还是会报加载错误 读取Excel失败: ‘Treeview’ object has no attribute ‘window_create’,请更换一种方式进行编辑
08-01
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值