import os import re import io import sys import threading import clr import time import json import queue import ctypes import shutil import sqlite3 import logging import tempfile import subprocess import pywintypes import hashlib import win32com.client import win32print from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QComboBox, QCheckBox, QLineEdit, QAction, QMenu, \ QMessageBox, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog, QDialog, QTableWidget, QTableWidgetItem, QSizePolicy from PyQt5.QtCore import Qt, QCoreApplication, QTimer, QPropertyAnimation from PyQt5.QtGui import QPixmap, QIntValidator, QFocusEvent from threading import RLock from flask import Flask, request, send_from_directory flask_app = Flask(__name__) def _flask_thread(): try: flask_app.run(host='127.0.0.1', port=7746) except: pass flask_thread = threading.Thread(target=_flask_thread) sys.path.append(os.path.dirname(__file__)) clr.AddReference('BarTender') try: import BarTender except Exception: pass def killThread(thread): import ctypes try: if thread.is_alive(): return ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread.ident), ctypes.py_object(BaseException)) == 1 else: return True except BaseException: pass def cp(src: str, dst: str): """ Support file or directory. """ return shutil.copytree(src, dst) and True if os.path.isdir(src) else shutil.copy(src, dst) and True def mv(src: str, dst: str): """ Support file or directory. """ return shutil.move(src, dst) and True def rm(src: str): """ Support file or directory. """ shutil.rmtree(src) if os.path.isdir(src) else os.remove(src) return True def rename(path: str, filename: str): """ Support file or directory. """ if filename.find('/') != -1: raise Exception('File name, catalog name or scroll grammar incorrect grammar.') return os.rename(path, os.path.dirname(path) + '/' + filename) or True def exists(path: str): """ Support file or directory. """ return os.path.exists(path) def mkdirs(path: str): """ Support multi-level directory. """ return os.makedirs(path) def _fd(f): return f.fileno() if hasattr(f, 'fileno') else f if os.name == 'nt': import msvcrt from ctypes import (sizeof, c_ulong, c_void_p, c_int64, Structure, Union, POINTER, windll, byref) from ctypes.wintypes import BOOL, DWORD, HANDLE LOCK_SH = 0x0 LOCK_NB = 0x1 LOCK_EX = 0x2 LOCK_UN = 0x9 if sizeof(c_ulong) != sizeof(c_void_p): ULONG_PTR = c_int64 else: ULONG_PTR = c_ulong PVOID = c_void_p class _OFFSET(Structure): _fields_ = [ ('Offset', DWORD), ('OffsetHigh', DWORD) ] class _OFFSET_UNION(Union): _fields_ = [ ('_offset', _OFFSET), ('Pointer', PVOID) ] _anonymous_ = ['_offset'] class OVERLAPPED(Structure): _fields_ = [ ('Internal', ULONG_PTR), ('InternalHigh', ULONG_PTR), ('_offset_union', _OFFSET_UNION), ('hEvent', HANDLE) ] _anonymous_ = ['_offset_union'] LPOVERLAPPED = POINTER(OVERLAPPED) LockFileEx = windll.kernel32.LockFileEx LockFileEx.restype = BOOL LockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, DWORD, LPOVERLAPPED] UnlockFileEx = windll.kernel32.UnlockFileEx UnlockFileEx.restype = BOOL UnlockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, LPOVERLAPPED] def flock(f, flags): hfile = msvcrt.get_osfhandle(_fd(f)) overlapped = OVERLAPPED() if flags == LOCK_UN: ret = UnlockFileEx( hfile, 0, 0, 0xFFFF0000, byref(overlapped) ) else: ret = LockFileEx( hfile, flags, 0, 0, 0xFFFF0000, byref(overlapped) ) return bool(ret) else: try: import fcntl LOCK_SH = fcntl.LOCK_SH LOCK_NB = fcntl.LOCK_NB LOCK_EX = fcntl.LOCK_EX LOCK_UN = fcntl.LOCK_UN except (ImportError, AttributeError): LOCK_EX = LOCK_SH = LOCK_NB = 0 def flock(f, flags): return flags == LOCK_UN else: def flock(f, flags): return fcntl.flock(_fd(f), flags) == 0 def getJson(file, data=None): if os.path.exists(file): try: return json.loads(open(file=file, mode='r', encoding='utf-8').read()) except json.decoder.JSONDecodeError: return data return data def putJson(file, data=None): with open(file=file, mode='w', encoding='utf-8') as f: flock(f, LOCK_EX) res = f.write(json.dumps(data, indent=4, ensure_ascii=True)) flock(f, LOCK_UN) return res class ShellResult(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.code = self.stdout = self.stderr = None def __setattr__(self, key, value): pass def __getitem__(self, item): try: return super().__getitem__(item) except Exception: return None def __getattr__(self, item): try: return super().__getitem__(item) except Exception: return None def exec_shell(command='', stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=0) -> ShellResult: stdout = stdout or subprocess.DEVNULL stderr = stderr or subprocess.DEVNULL try: result = subprocess.run(command, shell=True, stdout=stdout, stderr=stderr, timeout=timeout or None) if result.stdout: stdout_text = result.stdout.decode('utf-8') else: stdout_text = '' if result.stderr: stderr_text = result.stderr.decode('utf-8') else: stderr_text = '' return ShellResult({ 'code': result.returncode, 'stdout': stdout_text, 'stderr': stderr_text }) except subprocess.TimeoutExpired: return ShellResult({ 'code': 1, 'stdout': '', 'stderr': 'Execution timeout.' }) class SQLite3: __conn__ = None __curr__ = None __data__ = None def __init__(self, *args, **kwargs): if args or kwargs: self.connect(*args, **kwargs) def _check_connection(self): if self.__curr__ is None: raise Exception('Connection does not exist.') @staticmethod def _sqlite_dict_factory(cursor, row): d = {} for idx, col in enumerate(cursor.description): d[col[0]] = row[idx] return d def connect(self, database): if self.__conn__: raise Exception('Already Connect.') self.__conn__ = sqlite3.connect(database, timeout=10, check_same_thread=False) self.__conn__.row_factory = self._sqlite_dict_factory self.__curr__ = self.__conn__.cursor() return self def execute(self, *args, **kwargs): self._check_connection() self.__curr__.execute(*args, **kwargs) self.__conn__.commit() return self def rownums(self): self._check_connection() return self.__curr__.rowcount def fetchall(self): self._check_connection() return self.__curr__.fetchall() def fetchone(self): self._check_connection() return self.__curr__.fetchone() def close(self): if self.__curr__ or self.__conn__: self.__curr__.close() self.__conn__.close() self.__curr__ = None self.__conn__ = None return True class Ditto: def __init__(self, db_file, rlock=None, class_name='Ditto-Default', limit_time=None, limit_rows=None): self.rLock = rlock or RLock() if limit_time is not None and not limit_time >= 0: raise Exception('Range error.') if limit_rows is not None and not limit_rows >= 0: raise Exception('Range error.') self.class_name = class_name self.limit_rows = limit_rows self.limit_time = limit_time try: self.database = SQLite3(db_file) self.init_database() except sqlite3.DatabaseError: self.database.close() and os.remove(db_file) raise Exception('The database was damaged and has been reset.') def init_database(self): with self.rLock: self.database.execute(''' CREATE TABLE IF NOT EXISTS "ditto" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "class" TEXT(64) COLLATE NOCASE, "value" TEXT(256) COLLATE NOCASE, "time" integer(12) ); ''') def fresh(self): with self.rLock: if self.limit_time is not None: self.database.execute(''' DELETE FROM ditto WHERE class='%s' AND time < %s; ''' % (self.class_name, (int(time.time()) - self.limit_time))) if self.limit_rows is not None: self.database.execute(''' DELETE FROM ditto WHERE id IN (SELECT id FROM (SELECT id FROM ditto WHERE class='%s' ORDER BY time DESC LIMIT %s,100)); ''' % (self.class_name, self.limit_rows)) def count(self): with self.rLock: self.fresh() self.database.execute(''' SELECT COUNT(1) AS rows FROM ditto WHERE class='%s'; ''' % (self.class_name,)) return self.database.fetchone()['rows'] def clear(self): with self.rLock: self.fresh() self.database.execute(''' DELETE FROM ditto WHERE class='%s'; ''' % (self.class_name,)) return self.database.rownums() def query(self, value): with self.rLock: self.fresh() self.database.execute(''' SELECT COUNT(1) AS rows FROM ditto WHERE class='%s' AND value='%s'; ''' % (self.class_name, value)) return self.database.fetchone()['rows'] def addit(self, value): with self.rLock: if self.query(value) != 0: return False self.database.execute(''' INSERT INTO ditto (\"class\", \"value\", \"time\") VALUES ('%s', '%s', '%s'); ''' % (self.class_name, value, int(time.time()))) self.fresh() return True class LoggerFileHandler: def __init__(self, log_file: str, mode: str = 'a', level: str = None, fmt: str = None): self.log, self.mod = log_file, mode self.lev, self.fmt = level, fmt self.sty = '%' class LoggerConsHandler: def __init__(self, level: str = None, fmt: str = None): self.lev, self.fmt = level, fmt self.sty = '%' class Logger: logger = None levels = { 'CRITICAL': logging.CRITICAL, 'FATAL': logging.FATAL, 'ERROR': logging.ERROR, 'WARNING': logging.WARNING, 'WARN': logging.WARN, 'INFO': logging.INFO, 'DEBUG': logging.DEBUG, 'NOTSET': logging.NOTSET, 'D': logging.DEBUG, 'I': logging.INFO, 'W': logging.WARNING, 'E': logging.ERROR, 'F': logging.FATAL } default_format = '{asctime} - {name} - {levelname[0]}: {message}' default_format_style = '{' handler_list = [] def __init__( self, name: str = 'default', default_level='DEBUG', fh: LoggerFileHandler = None, ch: LoggerConsHandler = None, add_default_handler=False ): ch = LoggerConsHandler() if add_default_handler and not ch else ch if fh and not isinstance(fh, LoggerFileHandler): raise TypeError('The parameter fh must be type.') if ch and not isinstance(ch, LoggerConsHandler): raise TypeError('The parameter ch must be type.') self.logger = logging.getLogger(name) self.logger.setLevel(self.levels[default_level]) if fh: fhandler = logging.FileHandler(filename=fh.log, mode=fh.mod, encoding='utf-8') self.handler_list.append(fhandler) fhandler.setLevel(self.levels[fh.lev or default_level]) fh.fmt = fh.fmt or self.default_format fh.sty = '{' if '%' not in fh.fmt else '%' fhandler.setFormatter(logging.Formatter(fmt=fh.fmt, style=fh.sty)) self.logger.addHandler(fhandler) if ch: chandler = logging.StreamHandler() self.handler_list.append(chandler) chandler.setLevel(self.levels[ch.lev or default_level]) ch.fmt = ch.fmt or self.default_format ch.sty = '{' if '%' not in ch.fmt else '%' chandler.setFormatter(logging.Formatter(fmt=ch.fmt, style=ch.sty)) self.logger.addHandler(chandler) self.d = self.logger.debug self.i = self.logger.info self.w = self.logger.warning self.e = self.logger.error self.f = self.logger.fatal self.c = self.logger.critical class Setting(dict): def __init__(self, setting_file: str, setting_default: dict): self.data_file = os.path.abspath(setting_file) super().__init__(getJson(self.data_file, setting_default)) def __getitem__(self, item): if item in self: return super().__getitem__(item) else: return None def __setitem__(self, key, value): super().__setitem__(key, value) putJson(self.data_file, self) class BarTenderPrint: def __init__(self, logger: Logger): self.logger = logger self.label = None self.print = None self.sheet = None self.bt_app = BarTender.Application() self.bt_app.Visible = False self.bt_format = None self.printer_list = [printer[2] for printer in win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL)][::-1] self.printer_current_index = 0 self.data_sources_cache = None self.primary_ds_dict = {} self.default_ds_name = 'A' self.current_ds_name = self.default_ds_name self.logger.i('%s%s' % ('Init', '')) def set_label(self, data: str): self.label = os.path.abspath(data) self.logger.i('%s%s' % ('Open the label template ', self.label)) for _ in range(2): if (self.bt_format is not None) == 1: self.bt_format.Close(BarTender.BtSaveOptions.btDoNotSaveChanges) self.bt_format = self.bt_app.Formats.Open(self.label, False, '') self.data_sources_cache = None self.primary_ds_dict = {} self.current_ds_name = self.default_ds_name self.set_print(self.printer_current_index or 0) self.set_sheet(self.sheet or 1) def set_print(self, data: int): self.printer_current_index = data self.print = self.printer_list[self.printer_current_index] self.logger.i('%s%s' % ('Printer set to ', self.print)) if (self.bt_format is not None) == 1: self.bt_format.PrintSetup.Printer = self.print def set_sheet(self, data: int): self.sheet = int(data) self.logger.i('%s%s' % ('Number of copies ', self.sheet)) if (self.bt_format is not None) == 1: self.bt_format.PrintSetup.IdenticalCopiesOfLabel = self.sheet def set_current_ds_name(self, name: str): self.current_ds_name = str(name) self.logger.i('%s%s' % ('Set current data source name ', name)) def get_current_ds_name(self): return self.current_ds_name def set_data_source(self, name, value): if name not in self.primary_ds_dict.keys(): self.primary_ds_dict[name] = self.bt_format.GetNamedSubStringValue(name) self.logger.i('%s%s' % ('Set data source ', '%s=%s' % (name, value))) self.bt_format.SetNamedSubStringValue(name, value) def get_data_source(self): if self.data_sources_cache: return self.data_sources_cache else: data = {} delimiter = ['=>', '::'] for ds in [i for i in self.bt_format.NamedSubStrings.GetAll(delimiter[0], delimiter[1]).split(delimiter[1]) if i != '']: ds_list = ds.split(delimiter[0]) data[ds_list[0]] = ds_list[1] self.data_sources_cache = data return data def start_printing(self, content: str): for k, v in {self.current_ds_name: content}.items(): self.set_data_source(k, v) if (self.bt_format is not None) == 1: self.logger.i('%s%s' % ('Printing ', 'normal')) self.bt_format.PrintOut(False, False) def start_printing_template(self): for k, v in self.primary_ds_dict.items(): self.set_data_source(k, v) if (self.bt_format is not None) == 1: self.logger.i('%s%s' % ('Printing ', 'template')) self.bt_format.PrintOut(False, False) def generate_preview(self): path = os.path.abspath( os.path.join(tempfile.gettempdir(), '%s%s.png' % ('preview_', int(round(time.time() * 1000))))) if (self.bt_format is not None) == 1: self.logger.i('%s%s' % ('Generate preview to ', path)) self.bt_format.ExportToFile(path, 'PNG', BarTender.BtColors.btColors24Bit, BarTender.BtResolution.btResolutionPrinter, BarTender.BtSaveOptions.btDoNotSaveChanges) return path def quit(self): try: self.bt_format is not None and self.bt_format.Close(BarTender.BtSaveOptions.btDoNotSaveChanges) self.bt_app is not None and self.bt_app.Quit(BarTender.BtSaveOptions.btDoNotSaveChanges) except Exception: pass self.bt_format = None self.bt_app = None self.logger.i('%s%s' % ('Quit', '')) class ScanVerify: def __init__(self, logger, rule_file): self.logger = logger self.checker_list = [['无', '.*']] self.checker_list.extend(getJson(os.path.abspath(rule_file), [])) self.checker_current_index = 0 def set_check(self, data: int): self.checker_current_index = data self.logger.i('%s%s' % ('Checker set to ', self.checker_list[self.checker_current_index][0])) def verify(self, content: str): return bool(re.match(self.checker_list[self.checker_current_index][1], content)) class CustomLineEdit(QLineEdit): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet('font-size: 12px; font-family: \'Microsoft YaHei\'; color: #000000;') def contextMenuEvent(self, event): menu = QMenu(self) action_c = menu.addAction('复制') action_p = menu.addAction('粘贴') action_c.triggered.connect(self.copy) action_p.triggered.connect(self.paste) menu.popup(self.mapToGlobal(event.pos())) def focusInEvent(self, event: QFocusEvent): super().focusInEvent(event) self.focusChanged(event) def focusOutEvent(self, event: QFocusEvent): super().focusOutEvent(event) self.focusChanged(event) def focusChanged(self, event: QFocusEvent): pass class CustomLineEditNoPopup(QLineEdit): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet('font-size: 12px; font-family: \'Microsoft YaHei\'; color: #656565;') def contextMenuEvent(self, event): pass class CustomPushButton(QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet('font-size: 12px; font-family: \'Microsoft YaHei\'; color: #0C0C0C;') class CustomLabel(QLabel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet('font-size: 12px; font-family: \'Microsoft YaHei\'; color: #0C0C0C; border: 1px solid #0C0C0C;') class CustomImageLabel(QLabel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet('font-size: 12px; font-family: \'Microsoft YaHei\'; color: #0C0C0C; border: 2px dashed #999999;') class CustomText(QLabel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet( 'font-size: 12px; font-family: \'Microsoft YaHei\'; color: #F00000;') class CustomComboBox(QComboBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet('font-size: 12px; font-family: \'Microsoft YaHei\'; color: #0C0C0C;') class CustomCheckBox(QCheckBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet('font-size: 12px; font-family: \'Microsoft YaHei\'; color: #0C0C0C;') class CustomHBoxLayout(QHBoxLayout): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setAlignment(Qt.AlignTop | Qt.AlignLeft) class CustomVBoxLayout(QVBoxLayout): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setAlignment(Qt.AlignTop | Qt.AlignLeft) class MainWindow(QMainWindow): def __init__(self, logger: Logger): super().__init__() self.app_name = '标签打印' self.app_version = ('1.0.9', '20241215', 'zhaoyafan', 'zhaoyafan@foxmail.com', 'https://www.fanscloud.net/') self.logger = logger self.toast = ToastNotification() self.setting = Setting( os.path.abspath(os.path.join(tempfile.gettempdir(), '%s.config' % self.md5(__file__)[:16])), {}) self.bt = BarTenderPrint(logger=self.logger) self.sv = ScanVerify(logger=self.logger, rule_file=os.path.abspath(os.path.join(os.path.dirname(__file__), 'rules.json'))) self.ditto = Ditto( db_file=os.path.abspath(os.path.join(tempfile.gettempdir(), '%s.db' % self.md5(__file__)[:16])), class_name='LabelPrint', limit_time=7776000, limit_rows=100000 ) self.open_file_extension = {'class': 'BarTender 文档 (*.btw)', 'extension': '.btw'} self.fast_print_data = getJson(os.path.abspath(os.path.join(os.path.dirname(__file__), 'fast-print.json')), []) self.last_fast_print_i = None self.last_fast_print_action_shortcut = 'F10' self.last_fast_print_action = QAction('', self) self.last_fast_print_action.setShortcut(self.last_fast_print_action_shortcut) self.last_opened_template = ['', ''] self.input_convert_letter = 0 self.input_scan_prohibited_enter = 0 self.task_queue = queue.Queue() self.task_timer = QTimer(self) self.task_timer.timeout.connect(self.task_execute) self.task_timer.start(100) self.ui_timer = QTimer(self) self.ui_timer.timeout.connect(self.update_ui) self.ui_timer.start(1500) self.capslock_state_timer = QTimer(self) self.capslock_state_timer.timeout.connect(self.update_capslock_state) self.setWindowTitle(self.app_name) self.setGeometry(0, 0, 600, 400) self.setAcceptDrops(True) self.menubar = self.menuBar() filemenu = self.menubar.addMenu('设置') self.blockRepeatAction = QAction('禁止重复打印', self, checkable=True) self.clearRecordAction = QAction('清空打印记录', self) filemenu.addActions( [ self.blockRepeatAction, self.clearRecordAction ] ) moremenu = self.menubar.addMenu('更多') self.designStateAction = QAction('编辑模式', self, checkable=True) self.createShortAction = QAction('发送快捷方式到桌面', self) self.aboutWindowAction = QAction('关于', self) self.blockRepeatAction.triggered.connect(self.blockRepeatActionFunction) self.clearRecordAction.triggered.connect(self.clearRecordActionFunction) self.designStateAction.triggered.connect(self.designStateActionFunction) self.createShortAction.triggered.connect(self.createShortActionFunction) self.aboutWindowAction.triggered.connect(self.aboutWindowActionFunction) moremenu.addActions( [ self.designStateAction, self.createShortAction, self.aboutWindowAction ] ) fastmenu = None for i in range(len(self.fast_print_data)): if (fastmenu is None) == 1: fastmenu = self.menubar.addMenu('快捷打印') self.menubar.addAction(self.last_fast_print_action) self.last_fast_print_action.triggered.connect(self.on_execute_fast_print_last) data = self.fast_print_data[i] act = QAction(str(data['name']), self) act.triggered.connect(lambda checked, index=i: self.on_execute_fast_print(index)) fastmenu.addAction(act) # 定义布局 m_layout_0 = CustomVBoxLayout() l_layout_0 = CustomVBoxLayout() l_layout_1 = CustomHBoxLayout() l_layout_2 = CustomVBoxLayout() r_layout_0 = CustomVBoxLayout() r_layout_1 = CustomHBoxLayout() r_layout_2 = CustomHBoxLayout() r_layout_3 = CustomVBoxLayout() l_layout_0.addLayout(l_layout_1) l_layout_0.addLayout(l_layout_2) r_layout_0.addLayout(r_layout_1) r_layout_0.addLayout(r_layout_2) r_layout_0.addLayout(r_layout_3) m_layout_0.addLayout(l_layout_0) m_layout_0.addLayout(r_layout_0) # Select file self.butSTP = CustomPushButton('选择文件') self.butSTP.setFixedSize(82, 27) self.butSTP.clicked.connect(self.on_open_template) l_layout_1.addWidget(self.butSTP) # Template name self.edtNTP = CustomLineEditNoPopup('') self.edtNTP.setReadOnly(True) self.edtNTP.setFixedSize(280, 27) l_layout_1.addWidget(self.edtNTP) # Test printing self.butTPT = CustomPushButton('测试打印') self.butTPT.setFixedSize(82, 27) self.butTPT.clicked.connect(self.on_test_print) l_layout_1.addWidget(self.butTPT) # Print preview self.labPRV = CustomImageLabel() self.labPRV.setAlignment(Qt.AlignCenter) self.labPRV.setFixedSize(455, 256) l_layout_2.addWidget(self.labPRV) # Printer self.labNPT = CustomLabel('打印机选择') self.labNPT.setFixedSize(82, 27) r_layout_1.addWidget(self.labNPT) # Select printer self.slcSPT = CustomComboBox() self.slcSPT.addItems(self.bt.printer_list) self.slcSPT.setFixedSize(213, 27) self.slcSPT.currentIndexChanged.connect(self.on_printer_changed) r_layout_1.addWidget(self.slcSPT) # Number of copies self.labPQT = CustomLabel('打印份数') self.labPQT.setFixedSize(82, 27) r_layout_1.addWidget(self.labPQT) self.edtNQT = CustomLineEditNoPopup('1') self.edtNQT.setFixedSize(60, 27) self.edtNQT.textChanged.connect(self.on_copies_editing_changed) validator = QIntValidator(1, 999) self.edtNQT.setValidator(validator) r_layout_1.addWidget(self.edtNQT) # Input verification self.labNVR = CustomLabel('扫描验证') self.labNVR.setFixedSize(82, 27) r_layout_2.addWidget(self.labNVR) # Select input verification self.slcSVR = CustomComboBox() self.slcSVR.addItems([data[0] for data in self.sv.checker_list]) self.slcSVR.setFixedSize(213, 27) self.slcSVR.currentIndexChanged.connect(self.on_checker_changed) r_layout_2.addWidget(self.slcSVR) # Convert letter self.chkCVL = CustomCheckBox('强制大写') self.chkCVL.setFixedSize(72, 27) self.chkCVL.stateChanged.connect(self.on_convert_letter_changed) r_layout_2.addWidget(self.chkCVL) # Disable enter self.chkDET = CustomCheckBox('禁用回车') self.chkDET.setFixedSize(72, 27) self.chkDET.stateChanged.connect(self.on_prohibit_enter_changed) r_layout_2.addWidget(self.chkDET) # Scan self.edtSCN = CustomLineEdit('') self.edtSCN.setStyleSheet( 'QLineEdit {font-size: 28px; font-family: \'Roboto Mono\',Consolas,\'Microsoft YaHei\'; color: #000000; background-color: #FFFFCC;}') self.edtSCN.setFixedSize(455, 45) self.edtSCN.focusChanged = self.on_print_scan_focus_change r_layout_3.addWidget(self.edtSCN) # Print button self.butSPT = CustomPushButton('打印') self.butSPT.setStyleSheet('font-size: 18px; font-family: \'Microsoft YaHei\'; color: #0C0C0C;') self.butSPT.setFixedSize(455, 45) self.butSPT.clicked.connect(self.on_start_printing) self.edtSCN.returnPressed.connect(self.on_print_scan_enter) r_layout_3.addWidget(self.butSPT) # CapsLock State self.TxtCAP = CustomText('') self.TxtCAP.setFixedSize(205, 15) r_layout_3.addWidget(self.TxtCAP) # 显示界面 central_widget = QWidget() central_widget.setLayout(m_layout_0) self.setCentralWidget(central_widget) self.setFixedSize(self.minimumSizeHint()) screen_rect = QApplication.desktop().availableGeometry() window_rect = self.geometry() self.move(int((screen_rect.width() - window_rect.width()) * 0.5), int((screen_rect.height() - window_rect.height()) * 0.5)) self.edtSCN.setFocus() self.show() self.load_setting() len(sys.argv) == 2 and os.path.isfile(sys.argv[1]) and os.path.basename(sys.argv[1]).lower().endswith(self.open_file_extension['extension']) and self.on_open_template_file(sys.argv[1]) def dragEnterEvent(self, event): if (event.mimeData().hasUrls() and len(event.mimeData().urls()) == 1) == 1: file = event.mimeData().urls()[0].toLocalFile() event.accept() if os.path.isfile(file) and os.path.basename(file).lower().endswith(self.open_file_extension['extension']) else event.ignore() return None event.ignore() def dropEvent(self, event): self.on_open_template_file(event.mimeData().urls()[0].toLocalFile()) def closeEvent(self, event): self.bt.quit() killThread(flask_thread) event.accept() def task_push(self, func, args): self.task_queue.put([func, args]) def task_execute(self): while True: if not self.task_queue.empty(): task = self.task_queue.get() try: task[0](*task[1]) except: pass self.task_queue.task_done() else: break def update_capslock_state(self): if (ctypes.windll.user32.GetKeyState(0x14) != 0) == 1: self.TxtCAP.setText('大写锁定已开启') else: self.TxtCAP.setText('') def update_ui(self): try: self.bt.bt_app.IsPrinting and print('') self.bt.bt_format and self.bt.bt_format.PrintSetup.Printer or print('') except Exception: self.close() def load_setting(self): # load blockRepeat self.blockRepeatAction.setChecked(bool(self.setting['blockRepeat'])) # load printer self.slcSPT.setCurrentIndex( next((index for index, item in enumerate(self.bt.printer_list) if item == self.setting['printer']), 0)) # load checker self.slcSVR.setCurrentIndex(self.setting['checker'] or 0) # load printCopies self.edtNQT.setText(str(self.setting['printCopies'] or 1)) # load template tempfile_last = self.setting['template'] tempfile_last and os.path.exists(tempfile_last) and self.load_template(tempfile_last) def on_execute_fast_print(self, action_index): self.last_fast_print_i = action_index self.last_fast_print_action.setText('%s (%s)' % (self.fast_print_data[action_index]['name'], self.last_fast_print_action_shortcut)) execute_file = os.path.join(os.path.dirname(__file__), self.fast_print_data[action_index]['executable']) if (os.path.exists(execute_file)) == 0: self.toast.show_toast('无法找到脚本') return None result = exec_shell(command=execute_file, timeout=3).stdout.strip() if (result != '') == 1: self.toast.show_toast('获取数据成功') self.on_start_printing(content=result) else: self.toast.show_toast('获取数据失败') def on_execute_fast_print_last(self): self.last_fast_print_i is not None and self.on_execute_fast_print(self.last_fast_print_i) def blockRepeatActionFunction(self, checked): self.setting['blockRepeat'] = bool(checked) def clearRecordActionFunction(self): count = self.ditto.clear() if (count > 0) == 1: self.toast.show_toast('已成功清空%s条打印记录' % (count,)) def designStateActionFunction(self, checked): if (not self.bt.bt_app) == 1: return None if (checked is True) == 1: t1 = '1.在编辑模式中您可以动态对模板进行修改;' t2 = '2.保存模板将会覆盖原文件永久生效;' t3 = '3.修改完成后手动关闭编辑模式;' QMessageBox.information(self, '提示', '%s\n%s\n%s' % (t1, t2, t3)) self.bt.bt_app.Visible = bool(1) else: self.bt.bt_app.Visible = bool(0) filename = self.last_opened_template[0] if (filename == '' or not os.path.exists(filename)) == 1: return None last_md5 = self.last_opened_template[1] temp_md5 = self.md5(open(self.last_opened_template[0], 'rb')) QMessageBox.information(self, '提示', '模板文件已被修改:\n\n%s' % filename) if (last_md5 != temp_md5) == 1 else None self.last_opened_template[1] = temp_md5 def createShortActionFunction(self): target = '%s.exe' % (os.path.splitext(__file__)[0],) shortcut_path = self.path_expandvars('%%USERPROFILE%%\\Desktop\\%s.lnk' % (self.app_name,)) if (os.path.exists(target)) == 1: self.create_shortcut(target, shortcut_path, 1) QMessageBox.information(self, '提示', '%s' % ('已成功发送快捷方式到桌面',)) def aboutWindowActionFunction(self): _c = self.app_version if (not _c) == 0: QMessageBox.information(self, '关于', "" 'version: %s, build: %s, author: %s, email: %s, site: %s' % ( _c[0], _c[1], _c[2], _c[3], '%s' % (_c[4], _c[4]))) @staticmethod def md5(input_data): if isinstance(input_data, bytes): return hashlib.md5(input_data).hexdigest() if isinstance(input_data, str): return hashlib.md5(bytes(input_data, encoding='utf-8')).hexdigest() md5_object = hashlib.md5() while True: data = input_data.read(io.DEFAULT_BUFFER_SIZE) if data: md5_object.update(data) else: return md5_object.hexdigest() @staticmethod def path_expandvars(path): resolve = os.path.expandvars(path) if (re.search('%[A-Za-z0-9_]+%', resolve)) is not None: raise Exception('Variable analysis failed') return resolve @staticmethod def create_shortcut(target, shortcut_path, run_as_admin): shell = win32com.client.Dispatch('WScript.Shell') try: os.remove(shortcut_path) except FileNotFoundError: pass shortcut = shell.CreateShortCut(shortcut_path) shortcut.TargetPath = target if shortcut_path.endswith('.lnk'): shortcut.Arguments = '' shortcut.WorkingDirectory = os.path.dirname(target) if target.startswith('\\\\'): shortcut.WorkingDirectory = '' shortcut.save() if shortcut_path.endswith('.lnk') and run_as_admin: with open(shortcut_path, 'r+b') as f: if os.path.isfile(shortcut_path): f.seek(21, 0) f.write(b'\x22\x00') def set_preview(self, preview_image: str): if (preview_image and os.path.exists(preview_image)) == 1: pixmap = QPixmap(preview_image) pixmap = pixmap.scaled(self.labPRV.size(), aspectRatioMode=Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation) self.labPRV.setPixmap(pixmap) if (tempfile.gettempdir() in preview_image) == 1: rm(preview_image) def set_template_name(self, name): if (isinstance(name, str)) == 1: self.edtNTP.setText(name) def set_current_data_source_name(self, name): name = name or self.bt.default_ds_name self.bt.set_current_ds_name(name) self.setting['current_data_source_name'] = name def get_current_data_source_name(self): return self.setting['current_data_source_name'] or self.bt.default_ds_name def load_template(self, file): try: if (file != '' and os.path.exists(file)) == 1: self.bt.set_label(file) self.set_template_name(os.path.splitext(os.path.basename(file))[0]) self.set_preview(self.bt.generate_preview()) self.set_current_data_source_name(self.get_current_data_source_name()) self.last_opened_template[0] = file self.last_opened_template[1] = self.md5(open(file, 'rb')) self.toast.show_toast('模板加载成功') return True except pywintypes.com_error as e: t = str(e.args[1]) m = str(e.args[2][2] if isinstance(e.args[2], tuple) else e.args[2]) if (len(t) > 5) == 1: m = '%s\n\n%s' % (t, m) t = '错误' QMessageBox.critical(self, t, m) except AttributeError: QMessageBox.critical(self, '错误', '调用服务失败') except Exception as e: QMessageBox.critical(self, '错误', '%s' % e) def on_open_template(self): self.on_open_template_file() def on_open_template_file(self, file=''): if (file != '') == 1: filename = file else: filename, _ = QFileDialog.getOpenFileName(self, '打开文件', os.path.dirname(self.last_opened_template[0]), self.open_file_extension['class']) if (filename and os.path.exists(filename)) == 1: if (self.load_template(filename)) == 1: self.setting['template'] = filename self.set_current_data_source_name('') if self.bt.default_ds_name not in self.bt.get_data_source().keys(): DataWindow(self).exec() def on_test_print(self): try: if (self.bt.bt_format is not None) == 1: self.bt.start_printing_template() self.set_preview(self.bt.generate_preview()) return None self.toast.show_toast('请先加载模板文件') except pywintypes.com_error as e: t = str(e.args[1]) m = str(e.args[2][2] if isinstance(e.args[2], tuple) else e.args[2]) if (len(t) > 5) == 1: m = '%s\n\n%s' % (t, m) t = '错误' QMessageBox.critical(self, t, m) except AttributeError: QMessageBox.critical(self, '错误', '调用服务失败') except Exception as e: QMessageBox.critical(self, '错误', '%s' % e) def on_printer_changed(self, index): try: self.bt.set_print(index) self.set_preview(self.bt.generate_preview()) self.setting['printer'] = str(self.bt.print) except pywintypes.com_error as e: t = str(e.args[1]) m = str(e.args[2][2] if isinstance(e.args[2], tuple) else e.args[2]) if (len(t) > 5) == 1: m = '%s\n\n%s' % (t, m) t = '错误' QMessageBox.critical(self, t, m) except AttributeError: QMessageBox.critical(self, '错误', '调用服务失败') except Exception as e: QMessageBox.critical(self, '错误', '%s' % e) def on_checker_changed(self, index): try: self.sv.set_check(index) self.setting['checker'] = index except pywintypes.com_error as e: t = str(e.args[1]) m = str(e.args[2][2] if isinstance(e.args[2], tuple) else e.args[2]) if (len(t) > 5) == 1: m = '%s\n\n%s' % (t, m) t = '错误' QMessageBox.critical(self, t, m) except AttributeError: QMessageBox.critical(self, '错误', '调用服务失败') except Exception as e: QMessageBox.critical(self, '错误', '%s' % e) def on_copies_editing_changed(self, text): try: if (text == '' or int(text) == 0) == 1: text = '1' self.edtNQT.setText(text) else: copies = int(text) self.bt.set_sheet(copies) self.setting['printCopies'] = copies except pywintypes.com_error as e: t = str(e.args[1]) m = str(e.args[2][2] if isinstance(e.args[2], tuple) else e.args[2]) if (len(t) > 5) == 1: m = '%s\n\n%s' % (t, m) t = '错误' QMessageBox.critical(self, t, m) except AttributeError: QMessageBox.critical(self, '错误', '调用服务失败') except Exception as e: QMessageBox.critical(self, '错误', '%s' % e) def on_convert_letter_changed(self, checked): if (checked == 2) == 1: self.input_convert_letter = 1 self.toast.show_toast('强制大写开启') else: self.input_convert_letter = 0 self.toast.show_toast('强制大写关闭') def on_prohibit_enter_changed(self, checked): if (checked == 2) == 1: self.input_scan_prohibited_enter = 1 self.toast.show_toast('禁用回车开启') else: self.input_scan_prohibited_enter = 0 self.toast.show_toast('禁用回车关闭') def on_print_scan_enter(self): self.input_scan_prohibited_enter or self.on_start_printing() def on_print_scan_focus_change(self, event: QFocusEvent): if (event.gotFocus()) == 1: self.capslock_state_timer.start(250) else: self.capslock_state_timer.stop() def on_start_printing(self, content=None): edit = self.edtSCN if (self.input_convert_letter == 1) == 1: text = edit.text().strip().upper() else: text = edit.text().strip() if (not content) == 0: text = str(content).strip() edit.setText(text) if (self.bt.bt_format is None) == 1: self.toast.show_toast('未加载模板文件') edit.selectAll() return None if (text == '') == 1: self.toast.show_toast('输入的内容为空') edit.selectAll() return None if (not self.sv.verify(text)) == 1: self.toast.show_toast('输入的内容不符合验证规则') edit.selectAll() return None if (not self.ditto.addit(text) and self.setting['blockRepeat']) == 1: QMessageBox.critical(self, '错误', '重复打印:\n\n%s' % (text,)) edit.selectAll() return None try: self.bt.start_printing(text) self.set_preview(self.bt.generate_preview()) edit.clear() except pywintypes.com_error as e: t = str(e.args[1]) m = str(e.args[2][2] if isinstance(e.args[2], tuple) else e.args[2]) if (len(t) > 5) == 1: m = '%s\n\n%s' % (t, m) t = '错误' QMessageBox.critical(self, t, m) except AttributeError: QMessageBox.critical(self, '错误', '调用服务失败') except Exception as e: QMessageBox.critical(self, '错误', '%s' % e) class DataWindow(QDialog): def __init__(self, mainwindow): super().__init__() self.ds_list = [] self.mainwindow = mainwindow self.setGeometry(0, 0, 205, 170) self.setWindowTitle('数据源名选择') self.tableWidget = QTableWidget(self) self.tableWidget.setFixedSize(205, 170) self.tableWidget.setRowCount(0) self.tableWidget.setColumnCount(2) self.tableWidget.setHorizontalHeaderLabels(['名称', '选择']) layout = QVBoxLayout() layout.addWidget(self.tableWidget) self.setLayout(layout) self.tableWidget.verticalHeader().hide() self.tableWidget.setColumnWidth(0, 120) self.tableWidget.setColumnWidth(1, 60) # 显示界面 screen_rect = QApplication.desktop().availableGeometry() window_rect = self.geometry() self.setFixedSize(self.minimumSizeHint()) self.move(int((screen_rect.width() - window_rect.width()) * 0.5), int((screen_rect.height() - window_rect.height()) * 0.5)) self.show() self.list_update() def list_update(self): try: ds_list = self.mainwindow.bt.get_data_source().keys() except AttributeError: self.close() return None if (self.ds_list == ds_list) == 1: return None self.ds_list = ds_list self.tableWidget.setRowCount(0) for row, ds in enumerate(ds_list): self.tableWidget.insertRow(row) self.tableWidget.setItem(row, 0, QTableWidgetItem('%s' % ds)) selectButton = QPushButton('选择', self) selectButton.clicked.connect(lambda _, ds=ds: self.set_ds(ds)) cell_widget = QWidget() cell_layout = QHBoxLayout(cell_widget) cell_layout.addWidget(selectButton) cell_layout.setAlignment(selectButton, Qt.AlignHCenter) cell_layout.setContentsMargins(0, 0, 0, 0) self.tableWidget.setCellWidget(row, 1, cell_widget) for row in range(self.tableWidget.rowCount()): for col in range(self.tableWidget.columnCount()): item = self.tableWidget.item(row, col) item and item.setFlags(item.flags() & ~Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsEnabled) def set_ds(self, ds): self.mainwindow.set_current_data_source_name(ds) self.close() def closeEvent(self, event): event.accept() def keyPressEvent(self, event): pass class ToastNotification(QDialog): def __init__(self): super().__init__() self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowType.Tool) layout = QVBoxLayout() layout.setContentsMargins(5, 705, 5, 5) self.label = QLabel("") self.label.setStyleSheet("font-family: 'Microsoft YaHei'; font-size: 24px; color: #ffffff;") layout.addWidget(self.label) self.setLayout(layout) self.setAttribute(Qt.WA_TranslucentBackground) self.setStyleSheet("background-color: rgba(65, 65, 65, 180); border-radius: 5px; padding: 10px 16px 10px 16px") self.adjustSize() self.timer = QTimer(self) self.timer.setInterval(2000) self.timer.timeout.connect(self.close) self.fade_animation = QPropertyAnimation(self, b"windowOpacity") self.fade_animation.setDuration(500) def show_toast(self, message): self.close() self.label.setText(message) self.timer.isActive() and self.timer.stop() self.timer.start() self.adjustSize() self.fade_animation.setStartValue(0.0) self.fade_animation.setEndValue(1.0) self.show() self.fade_animation.start() def closeEvent(self, event): if self.timer.isActive(): self.timer.stop() super().closeEvent(event) if __name__ == '__main__': if (os.path.basename(__file__).lower().endswith('.int')) == 1: QCoreApplication.addLibraryPath( os.path.abspath(os.path.join(os.path.dirname(__file__), 'site-packages/PyQt5/Qt5/plugins'))) f_lock = open(file=os.path.abspath(os.path.join(tempfile.gettempdir(), '%s.lock' % hashlib.md5( bytes(__file__, encoding='utf-8')).hexdigest()[:16])), mode='w', encoding='utf-8') if (not flock(f_lock, LOCK_EX | LOCK_NB)) == 1: app = QApplication(sys.argv) msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setText('程序已经在运行中') msg.setWindowTitle('提示') msg.setStandardButtons(QMessageBox.Ok) msg.exec_() app.exit(1) sys.exit(1) try: win32com.client.Dispatch('BarTender.Application') except pywintypes.com_error: app = QApplication(sys.argv) msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText('初始化失败,请检查BarTender是否已安装。') msg.setWindowTitle('错误') msg.setStandardButtons(QMessageBox.Ok) msg.exec_() app.exit(1) sys.exit(1) app = QApplication(sys.argv) window_main = MainWindow(logger=Logger(name='main', fh=None, ch=LoggerConsHandler())) @flask_app.route('/print', methods=['POST']) def api_print(): content = request.json['data'] if (isinstance(content, str) is True) == 1: threading.Thread(target=window_main.task_push, args=[window_main.on_start_printing, [content]]).start() return 'Printing task sent' else: return 'Error' @flask_app.route('/', methods=['GET']) def index(): return send_from_directory(os.path.dirname(__file__), 'index.html') flask_thread.start() sys.exit(app.exec_())