LabelPrint/main.py

1253 lines
45 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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 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 <LoggerFileHandler> type.')
if ch and not isinstance(ch, LoggerConsHandler):
raise TypeError('The parameter ch must be <LoggerConsHandler> 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 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.7', '20241204', '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.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.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.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
]
)
# 定义布局
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_file)
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 = CustomLabel()
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()
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 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 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], '<a href=\'%s\'>%s</a>' % (_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_file(self):
filename, _ = QFileDialog.getOpenFileName(self, '打开文件', os.path.dirname(self.last_opened_template[0]), 'BarTender 文档 (*.btw)')
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_())