class KeyboardClickTrigger(BaseTrigger): controller = pynput.keyboard.Controller() TITLE = '單擊鍵盤' INFORMATION = ('點擊設定的按鍵') DIC_DEFAULT = Dict({ **BaseTrigger.DIC_DEFAULT, 'other': { 'keys': { 'class_name': 'HotKeyEdit', 'name': '按鍵設定', 'single_mode': True, 'fixed': [], 'variable': '0' }, } }) def activate(self, logger, *args, **kwargs): try: logger.info(f'【{self.TITLE}】') logger.info(f'點擊{self.keys.value_widget.PRESSED_KEY_STR}') for key in self.keys.value_widget._PRESSED_KEY: self.controller.press(key) self.controller.release(key) logger.info('執行成功。') except: logger.info('執行失敗。') logger.critical('\n' + traceback.format_exc())
class MouseMoveTrigger(BaseTrigger): controller = pynput.mouse.Controller() TITLE = '滑鼠移動' INFORMATION = '將滑鼠移動至指定位置' DIC_DEFAULT = Dict({ **BaseTrigger.DIC_DEFAULT, 'other': { 'pos': { 'class_name': 'PositionBox', 'name': '座標', 'source': 0, 'fixed': [0, 0], 'variable': '0' }, } }) def activate(self, logger, *args, **kwargs): try: sources = super().activate(*args, **kwargs) pyautogui.moveTo(*sources.pos) logger.info(f'【{self.TITLE}】') logger.info(f'將滑鼠移動至{sources.pos}位置') logger.info('執行成功。') except: logger.info('執行失敗。') logger.critical('\n' + traceback.format_exc())
def __init__(self, *args, **kwargs): super().__init__() self.setupUi(self) self.showMaximized() self.dic_scripts = Dict() # 選單 - 檔案 self.action_add_category.triggered.connect(lambda e: self.create_category(None)) self.action_add_script.triggered.connect(lambda e: self.create_script(None)) self.action_save.triggered.connect(self.save_script) self.action_close.triggered.connect(self.closeEvent) # 選單 - 檢視 self.action_file_explorer.triggered.connect(self.switch_file_explorer) self.action_script_explorer.triggered.connect(self.switch_script_explorer) # 選單 - 操作 self.action_start_all.triggered.connect(self.start_all) self.action_stop_all.triggered.connect(self.stop_all) # 畫面修改功能 self.tree_files.itemDoubleClicked.connect(lambda item: item.script.open_editor()) self.tree_files.itemChanged.connect(lambda item: item.script.rename(item.text(0))) self.main_script.currentChanged.connect(self.detect_savable) self.tree_scripts.doubleClicked.connect( lambda: self.tree_scripts.currentItem().parent() and self.main_script.currentWidget() and self.main_script.currentWidget().add_trigger( class_name=self.tree_scripts.currentItem().whatsThis(0) )) self.load_list()
def run_script(self): kwargs = Dict() logger = self.get_logger() for row in range(self.editor.lst_trigger.rowCount()): manager = self.editor.lst_page.widget(row).manager kwargs[str(row)] = manager.activate(logger, **kwargs) logger.info('=============================')
class FileLoadTrigger(BaseTrigger): TITLE = '讀取檔案' NEED_SOURCE = True INFORMATION = ( '檔案類型: 選擇圖檔,返回 PIL.Image 類型\n' ' 選擇文字檔,返回 str 類型\n\n' '檔案路徑: 要讀取的目標檔案路徑\n\n' ) DIC_DEFAULT = Dict({ **BaseTrigger.DIC_DEFAULT, 'other': { 'file_type': { 'class_name': 'ComboBox', 'name': '檔案類型', 'choices': ['圖檔', '文字檔'], 'fixed': 0, 'variable': '0'}, 'file_path': { 'class_name': 'FileEdit', 'name': '檔案路徑', 'method': 'getOpenFileName', 'types': 'All files(*)', 'source': 0, 'fixed': 'data', 'variable': '0', } } }) def activate(self, logger, *args, **kwargs): try: sources = super().activate(*args, **kwargs) logger.info(f'【{self.TITLE}】') logger.info(f'檔案類型為:{self.DIC_DEFAULT.other.file_type.choices[sources.file_type]}') logger.info(f'檔案路徑為:{sources.file_path}') logger.info(f'執行成功。') if sources.file_type == 0: return Image.open(sources.file_path) return sources.file_path.read_text(encoding='utf8') except: logger.info(f'執行失敗。') logger.critical('\n' + traceback.format_exc())
def read_data(self): if self.path.is_dir(): return if not self.path.read_text(encoding='utf8'): self.init_data() return # 讀取資料 self.data = Dict.load_json(self.path, encoding='utf8')
def data(self): data = Dict({ 'class_name': self.__class__.__name__, 'other': { key: { k: getattr(self, key).current.get(k, v) for k, v in value.items() } for key, value in self.DIC_DEFAULT.other.items() } }) return data
class MouseClickTrigger(BaseTrigger): controller = pynput.mouse.Controller() TITLE = '點擊滑鼠' INFORMATION = ('滑鼠按鈕: 選擇左鍵,點擊左鍵\n' ' 選擇中鍵,點擊中鍵\n' ' 選擇右鍵,點擊右鍵\n\n' '點擊次數: 選擇單擊,點擊一次\n' ' 選擇雙擊,點擊兩次') DIC_DEFAULT = Dict({ **BaseTrigger.DIC_DEFAULT, 'other': { 'button': { 'class_name': 'ComboBox', 'name': '滑鼠按鈕', 'choices': ['左鍵', '中鍵', '右鍵'], 'fixed': 0, 'variable': '0' }, 'times': { 'class_name': 'ComboBox', 'name': '點擊次數', 'choices': ['單擊', '雙擊'], 'fixed': 0, 'variable': '0' }, } }) def activate(self, logger, *args, **kwargs): try: sources = super().activate(*args, **kwargs) dic_buttons = dict( enumerate([ pynput.mouse.Button.left, pynput.mouse.Button.middle, pynput.mouse.Button.right, ])) button = dic_buttons[sources.button] count = sources.times + 1 self.controller.click(button, count) logger.info(f'【{self.TITLE}】') logger.info( f'點擊{self.DIC_DEFAULT.other.button.choices[sources.button]}' f'{count}次') logger.info('執行成功。') except: logger.info('執行失敗。') logger.critical('\n' + traceback.format_exc())
class PrintScreenTrigger(BaseTrigger): TITLE = '螢幕截圖' INFORMATION = ( '來源(str): 無\n\n' ) DIC_DEFAULT = Dict({ **BaseTrigger.DIC_DEFAULT, 'other': { } }) def activate(self, logger, *args, **kwargs): try: img = ImageGrab.grab(all_screens=1) logger.info(f'【{self.TITLE}】,執行成功。') return img except: logger.info(f'【{self.TITLE}】,執行失敗。') logger.critical('\n' + traceback.format_exc())
class CopyTrigger(BaseTrigger): controller = pynput.keyboard.Controller() TITLE = '剪貼簿複製' INFORMATION = '' DIC_DEFAULT = Dict({**BaseTrigger.DIC_DEFAULT, 'other': {}}) def activate(self, logger, *args, **kwargs): try: logger.info(f'【{self.TITLE}】') keys = [ pynput.keyboard.Key.ctrl, pynput.keyboard.KeyCode.from_char('c') ] for key in keys: self.controller.press(key) for key in keys: self.controller.release(key) logger.info(f'執行成功。') except: logger.info(f'執行失敗。') logger.critical('\n' + traceback.format_exc())
class MouseScrollTrigger(BaseTrigger): controller = pynput.mouse.Controller() TITLE = '滾動滑鼠' INFORMATION = ('方向: 選擇向上滾動,滾輪向上滾動\n' ' 選擇向下滾動,滾輪向下滾動\n\n' '滾動次數: 輸入要滾動的次數') DIC_DEFAULT = Dict({ **BaseTrigger.DIC_DEFAULT, 'other': { 'forward': { 'class_name': 'ComboBox', 'name': '方向', 'choices': ['向上滾動', '向下滾動'], 'fixed': 0, 'variable': '0' }, 'step': { 'class_name': 'SpinBox', 'name': '滾動次數', 'fixed': 0, 'variable': '0' }, } }) def activate(self, logger, *args, **kwargs): try: sources = super().activate(*args, **kwargs) logger.info(f'【{self.TITLE}】') logger.info( f'{self.DIC_DEFAULT.other.forward.choices[sources.forward]}' f'{sources.step}次') if sources.forward == 0: forward = 1 else: forward = -1 self.controller.scroll(0, forward * sources.step) logger.info('執行成功。') except: logger.info('執行失敗。') logger.critical('\n' + traceback.format_exc())
def data(self) -> Dict: if self.path.is_dir(): return self._data if self.rb_once.isChecked(): category = 'once' if self.rb_while.isChecked(): category = 'while' return Dict({ 'BASIC': { 'activate': bool(self.ccb_activate.currentIndex()), 'start_hotkey': self.le_start_hotkey.PRESSED_KEY_VK, 'stop_hotkey': self.le_stop_hotkey.PRESSED_KEY_VK, 'descript': self.te_descript.toPlainText(), 'category': category }, 'SCRIPT': { 'content': [ self.lst_page.widget(row).manager.data for row in range(self.lst_trigger.rowCount()) ] }, })
class BaseTrigger: TITLE = '' INFORMATION = '' NEED_SOURCE = False DIC_DEFAULT = Dict({ 'other': {}, }) _row = 0 class Trigger(QtWidgets.QWidget, Ui_Form): def __init__(self, manager, data, *args, **kwargs): super().__init__(*args, **kwargs) self.setupUi(self) self.manager = manager self.lbl_title.setText(manager.TITLE) self.lbl_information.setText(manager.INFORMATION) for i, (key, value) in enumerate(data.other.items()): name_widget = QtWidgets.QLabel(f'{value.name}:') value_widget = ArgsWidget(value) value_widget.le_source_variable.right = self self.formLayout_2.setWidget(i, QtWidgets.QFormLayout.LabelRole, name_widget) self.formLayout_2.setWidget(i, QtWidgets.QFormLayout.FieldRole, value_widget) value_widget.sig_current_changed.connect(self.manager.editor.check_saved) setattr(self, key, value_widget) setattr(self.manager, key, value_widget) @property def row(self): return self.manager.row @row.setter def row(self, row): self.manager.row = row @property def data(self): return self.manager.data @property def row(self): return self._row @row.setter def row(self, row): self._row = row self.right.lbl_index.setText(str(row)) @property def data(self): data = Dict({ 'class_name': self.__class__.__name__, 'other': { key: { k: getattr(self, key).current.get(k, v) for k, v in value.items() } for key, value in self.DIC_DEFAULT.other.items() } }) return data @data.setter def data(self, data): for key, value in data.other.items(): value_widget = getattr(self.right, key) value_widget.current = value def __init__(self, editor, data=None, *args, **kwargs): super().__init__(*args, **kwargs) self.editor = editor # 腳本清單 self.header_item = QtWidgets.QTableWidgetItem() self.handle_item = QtWidgets.QWidget() self.name_item = QtWidgets.QTableWidgetItem(self.TITLE) # 腳本清單-操作 h = QtWidgets.QHBoxLayout(self.handle_item) self.pb_remove = QtWidgets.QPushButton('X') self.pb_move_up = QtWidgets.QPushButton('↑') self.pb_move_down = QtWidgets.QPushButton('↓') self.pb_remove.clicked.connect(lambda: self.editor.remove_trigger(self)) self.pb_move_up.clicked.connect(lambda: self.editor.move_up_trigger(self)) self.pb_move_down.clicked.connect(lambda: self.editor.move_down_trigger(self)) h.addWidget(self.pb_remove) h.addWidget(self.pb_move_up) h.addWidget(self.pb_move_down) # 腳本詳細內容 data = data or self.DIC_DEFAULT self.right = self.Trigger(self, data) self.data = data def activate(self, *args, **kwargs): return Dict({ key: getattr(self, key).get_source(kwargs) for key in self.DIC_DEFAULT.other })
def current(self): return Dict({ 'source': self.ccb_source.currentIndex(), 'fixed': self.value_widget.current, 'variable': self.le_source_variable.text() })
def activate(self, *args, **kwargs): return Dict({ key: getattr(self, key).get_source(kwargs) for key in self.DIC_DEFAULT.other })
class FileSaveTrigger(BaseTrigger): TITLE = '儲存檔案' NEED_SOURCE = True INFORMATION = ( '存檔內容: 要儲存的檔案內容\n\n' '存檔路徑: 要儲存的目標檔案路徑\n\n' '檔案類型: 選擇圖檔,返回 PIL.Image 類型\n' ' 選擇文字檔,返回 str 類型\n\n' '複寫: 選擇總是覆蓋,每次執行將覆蓋舊檔\n' ' 選擇忽略,若檔案存在就不會重覆執行存檔動作\n' ' 選擇另存新檔,將在檔案目錄建立新檔儲存\n' ) DIC_DEFAULT = Dict({ **BaseTrigger.DIC_DEFAULT, 'other': { 'write_content': { 'class_name': 'TextEdit', 'name': '存檔內容', 'source': 1, 'fixed': '', 'variable': '0', }, 'file_path': { 'class_name': 'FileEdit', 'name': '檔案路徑', 'method': 'getSaveFileName', 'types': 'All files(*)', 'source': 0, 'fixed': 'data', 'variable': '0', }, 'file_type': { 'class_name': 'ComboBox', 'name': '檔案類型', 'choices': ['圖檔', '文字檔'], 'fixed': 0, 'variable': '0'}, 'over_write': { 'class_name': 'ComboBox', 'name': '覆寫', 'choices': ['總是覆蓋', '忽略', '另存新檔'], 'fixed': 0, 'variable': '0'}, } }) def activate(self, logger, *args, **kwargs): try: logger.info(f'【{self.TITLE}】') sources = super().activate(*args, **kwargs) file_path = Path(sources.file_path) logger.info(f'檔案類型為:{self.DIC_DEFAULT.other.file_type.choices[sources.file_type]}') logger.info(f'複寫設定為:{self.DIC_DEFAULT.other.over_write.choices[sources.over_write]}') if not file_path.exists(): file_path = sources.file_path logger.info(f'檔案路徑為:{file_path}') elif sources.over_write == 0: file_path = sources.file_path logger.info(f'檔案路徑為:{file_path}') elif sources.over_write == 1: logger.info(f'檔案已存在,忽略此步驟') return True elif sources.over_write == 2: cnt = len([f for f in file_path.parent.iterdir() if f.stem in str(f)]) file_path = file_path.parent / f'{file_path.stem} ({cnt}).{file_path.suffix}' logger.info(f'檔案路徑為:{file_path}') if sources.file_type == 0: if isinstance(sources.write_content, Image.Image): sources.write_content.save(file_path) logger.info(f'執行成功。') return True else: logger.info(f'欲存檔內容型別錯誤:{sources.write_content}') return False file_path.write_text( sources.write_content, encoding='utf8') logger.info(f'執行成功。') return True except: logger.info(f'執行失敗。') logger.critical('\n' + traceback.format_exc())
class Editor(QtWidgets.QWidget, Ui_ScriptEditor): sig_reload_log_list = QtCore.pyqtSignal() _data = Dict({ 'BASIC': { 'activate': False, 'start_hotkey': [121], 'stop_hotkey': [123], 'descript': '', 'category': 'once' }, 'SCRIPT': { 'content': [] }, }) def __init__(self, script, path, *args, **kwargs): super().__init__() self.setupUi(self) self.script = script self.path = path self.lst_trigger.itemDoubleClicked.connect( lambda item: self.lst_page.setCurrentIndex(self.lst_trigger. currentRow())) self.lst_trigger.sig_drop_new.connect( lambda class_name: self.add_trigger(class_name=class_name)) self.lst_log.itemDoubleClicked.connect(self.fn_show_log) self.sig_reload_log_list.connect(self.fn_reload_log_list) if not self.path.is_dir(): self.fn_reload_log_list() @property def data(self) -> Dict: if self.path.is_dir(): return self._data if self.rb_once.isChecked(): category = 'once' if self.rb_while.isChecked(): category = 'while' return Dict({ 'BASIC': { 'activate': bool(self.ccb_activate.currentIndex()), 'start_hotkey': self.le_start_hotkey.PRESSED_KEY_VK, 'stop_hotkey': self.le_stop_hotkey.PRESSED_KEY_VK, 'descript': self.te_descript.toPlainText(), 'category': category }, 'SCRIPT': { 'content': [ self.lst_page.widget(row).manager.data for row in range(self.lst_trigger.rowCount()) ] }, }) @data.setter def data(self, data): self._data = data # 修改UI self.ccb_activate.setCurrentIndex(int(data.BASIC.activate)) self.le_start_hotkey.PRESSED_KEY_VK = data.BASIC.start_hotkey self.le_stop_hotkey.PRESSED_KEY_VK = data.BASIC.stop_hotkey self.te_descript.setText(data.BASIC.descript) if data.BASIC.category == 'once': self.rb_once.click() else: self.rb_while.click() for i, d in enumerate(data.SCRIPT.content): self.add_trigger(class_name=d.class_name, data=d, row=i) @property def path(self) -> Path: return self.script.path @path.setter def path(self, path): self.script.path = path @property def tab_text(self): return self.script.tab_text # 腳本內容存讀 def compare_data(self) -> bool: return self._data == self.data def reset_data(self): self.data = self._data def init_data(self): with self.path.open('w', encoding='utf8') as f: json.dump(self.data, f, ensure_ascii=False) def read_data(self): if self.path.is_dir(): return if not self.path.read_text(encoding='utf8'): self.init_data() return # 讀取資料 self.data = Dict.load_json(self.path, encoding='utf8') def save_data(self, path=None): path = path or self.path with path.open('w', encoding='utf8') as f: json.dump(self.data, f, ensure_ascii=False, indent=4) self._data = self.data def check_saved(self): self.script.rename_tab_text(self.script.tab_text) self.script.tree.setText(0, self.script.tab_text) # 腳本設計 def move_up_trigger(self, manager): self.lst_page.removeWidget(manager.right) self.lst_trigger.removeRow(manager.row) self.add_trigger(class_name=manager.__class__.__name__, row=manager.row - 1, data=manager.data) def move_down_trigger(self, manager): self.lst_page.removeWidget(manager.right) self.lst_trigger.removeRow(manager.row) self.add_trigger(class_name=manager.__class__.__name__, row=manager.row + 1, data=manager.data) def remove_trigger(self, manager): self.lst_trigger.removeRow(manager.row) self.lst_page.removeWidget(manager.right) self.reset_index() self.check_saved() def add_trigger(self, class_name, row=None, data=None): Manager = getattr(trigger, class_name, None) if not Manager: return row = row or self.lst_trigger.currentRow() row = 0 if row == -1 else row # 實體化 manager = Manager(self, data) # 清單內容設定 self.lst_trigger.insertRow(row) self.lst_trigger.setVerticalHeaderItem(row, manager.header_item) self.lst_trigger.setCellWidget(row, 0, manager.handle_item) self.lst_trigger.setItem(row, 1, manager.name_item) # 調整高度 self.lst_trigger.verticalHeader().resizeSection(row, 50) # 右側加入 self.lst_page.insertWidget(row, manager.right) # 重新排序 self.reset_index() # 重新排序腳本清單編號、設定清單按鈕是否啟用 def reset_index(self): for row in range(self.lst_trigger.rowCount()): header_item = self.lst_trigger.verticalHeaderItem(row) header_item.setText(str(row)) manager = self.lst_page.widget(row).manager manager.row = row manager.pb_move_up.setDisabled(False) manager.pb_move_down.setDisabled(False) if row == 0: manager.pb_move_up.setDisabled(True) if row + 1 == self.lst_trigger.rowCount(): manager.pb_move_down.setDisabled(True) self.check_saved() def fn_reload_log_list(self): self.lst_log.clear() self.lst_log.addItems( map( lambda p: p.stem, Path(f'log/{self.path.parent.name}/{self.path.stem}').iterdir( ))) def fn_show_log(self, item): log_path = Path( f'log/{self.path.parent.name}/{self.path.stem}/{item.text()}.log') txt = log_path.read_text(encoding='utf8') self.te_log.setText(txt)
class ScreenCheckTrigger(BaseTrigger): TITLE = '圖片定位' INFORMATION = ( '來源圖片: 前置步驟中截圖(開啟檔案)取得的圖片檔。\n' '目標圖片: 將要在來源圖片中定位的目標。\n' '灰階查找: 是否將圖片轉為灰階進行定位。\n' '信心度: 定位結果的信心度設定。' ) DIC_DEFAULT = Dict({ **BaseTrigger.DIC_DEFAULT, 'other': { 'source_img': { 'class_name': 'FileEdit', 'name': '來源圖片', 'method': 'getOpenFileName', 'types': 'All files(*)', 'default': 1, 'fixed': 'data', 'variable': '0', }, 'target_img': { 'class_name': 'FileEdit', 'name': '目標圖片', 'method': 'getOpenFileName', 'types': 'All files(*)', 'default': 1, 'fixed': 'data', 'variable': '0', }, 'grayscale': { 'class_name': 'ComboBox', 'name': '灰階查找', 'choices': ['否', '是'], 'fixed': 0, 'variable': '0', }, 'confidence': { 'class_name': 'DoubleSpinBox', 'name': '信心度', 'fixed': 0, 'variable': '1.0', }, } }) def get_top_left(self, n): _x, _y = pyautogui.position() pyautogui.moveTo(-n * pyautogui.size()[0], -n * pyautogui.size()[1]) x, y = pyautogui.position() pyautogui.moveTo(_x, _y) return x, y def activate(self, logger, *args, **kwargs): try: logger.info(f'【{self.TITLE}】') sources = super().activate(*args, **kwargs) x, y = self.get_top_left( win32api.GetSystemMetrics(win32con.SM_CMONITORS)) logger.info(f'螢幕定位最左上角為:{(x, y)}') pos = pyautogui.locate( sources.target_img, sources.source_img, grayscale=bool(sources.grayscale), confidence=sources.confidence, ) logger.info(f'目標圖片位置定位為:{pos}') pos = pyautogui.center(pos) pos = (pos[0] + x, pos[1] + y) logger.info(f'目標圖片中心點定位為:{pos}') logger.info(f'執行成功。') return pos except: logger.info(f'執行失敗。') logger.critical('\n' + traceback.format_exc())