pyxllib 0.3.197__py3-none-any.whl → 0.3.200__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. pyxllib/__init__.py +21 -21
  2. pyxllib/algo/__init__.py +8 -8
  3. pyxllib/algo/disjoint.py +54 -54
  4. pyxllib/algo/geo.py +541 -541
  5. pyxllib/algo/intervals.py +964 -964
  6. pyxllib/algo/matcher.py +389 -389
  7. pyxllib/algo/newbie.py +166 -166
  8. pyxllib/algo/pupil.py +629 -629
  9. pyxllib/algo/shapelylib.py +67 -67
  10. pyxllib/algo/specialist.py +241 -241
  11. pyxllib/algo/stat.py +494 -494
  12. pyxllib/algo/treelib.py +149 -149
  13. pyxllib/algo/unitlib.py +66 -66
  14. pyxllib/autogui/__init__.py +5 -5
  15. pyxllib/autogui/activewin.py +246 -246
  16. pyxllib/autogui/all.py +9 -9
  17. pyxllib/autogui/autogui.py +852 -852
  18. pyxllib/autogui/uiautolib.py +362 -362
  19. pyxllib/autogui/virtualkey.py +102 -102
  20. pyxllib/autogui/wechat.py +827 -827
  21. pyxllib/autogui/wechat_msg.py +421 -421
  22. pyxllib/autogui/wxautolib.py +84 -84
  23. pyxllib/cv/__init__.py +5 -5
  24. pyxllib/cv/expert.py +267 -267
  25. pyxllib/cv/imfile.py +159 -159
  26. pyxllib/cv/imhash.py +39 -39
  27. pyxllib/cv/pupil.py +9 -9
  28. pyxllib/cv/rgbfmt.py +1525 -1525
  29. pyxllib/cv/slidercaptcha.py +137 -137
  30. pyxllib/cv/trackbartools.py +251 -251
  31. pyxllib/cv/xlcvlib.py +1040 -1040
  32. pyxllib/cv/xlpillib.py +423 -423
  33. pyxllib/data/echarts.py +240 -240
  34. pyxllib/data/jsonlib.py +89 -89
  35. pyxllib/data/oss.py +72 -72
  36. pyxllib/data/pglib.py +1127 -1127
  37. pyxllib/data/sqlite.py +568 -568
  38. pyxllib/data/sqllib.py +297 -297
  39. pyxllib/ext/JLineViewer.py +505 -505
  40. pyxllib/ext/__init__.py +6 -6
  41. pyxllib/ext/demolib.py +246 -246
  42. pyxllib/ext/drissionlib.py +277 -277
  43. pyxllib/ext/kq5034lib.py +12 -12
  44. pyxllib/ext/old.py +663 -663
  45. pyxllib/ext/qt.py +449 -449
  46. pyxllib/ext/robustprocfile.py +497 -497
  47. pyxllib/ext/seleniumlib.py +76 -76
  48. pyxllib/ext/tk.py +173 -173
  49. pyxllib/ext/unixlib.py +827 -827
  50. pyxllib/ext/utools.py +351 -351
  51. pyxllib/ext/webhook.py +124 -119
  52. pyxllib/ext/win32lib.py +40 -40
  53. pyxllib/ext/wjxlib.py +88 -88
  54. pyxllib/ext/wpsapi.py +124 -124
  55. pyxllib/ext/xlwork.py +9 -9
  56. pyxllib/ext/yuquelib.py +1105 -1105
  57. pyxllib/file/__init__.py +17 -17
  58. pyxllib/file/docxlib.py +761 -761
  59. pyxllib/file/gitlib.py +309 -309
  60. pyxllib/file/libreoffice.py +165 -165
  61. pyxllib/file/movielib.py +148 -148
  62. pyxllib/file/newbie.py +10 -10
  63. pyxllib/file/onenotelib.py +1469 -1469
  64. pyxllib/file/packlib/__init__.py +330 -330
  65. pyxllib/file/packlib/zipfile.py +2441 -2441
  66. pyxllib/file/pdflib.py +426 -426
  67. pyxllib/file/pupil.py +185 -185
  68. pyxllib/file/specialist/__init__.py +685 -685
  69. pyxllib/file/specialist/dirlib.py +799 -799
  70. pyxllib/file/specialist/download.py +193 -193
  71. pyxllib/file/specialist/filelib.py +2829 -2829
  72. pyxllib/file/xlsxlib.py +3131 -3131
  73. pyxllib/file/xlsyncfile.py +341 -341
  74. pyxllib/prog/__init__.py +5 -5
  75. pyxllib/prog/cachetools.py +64 -64
  76. pyxllib/prog/deprecatedlib.py +233 -233
  77. pyxllib/prog/filelock.py +42 -42
  78. pyxllib/prog/ipyexec.py +253 -253
  79. pyxllib/prog/multiprogs.py +940 -940
  80. pyxllib/prog/newbie.py +451 -451
  81. pyxllib/prog/pupil.py +1197 -1197
  82. pyxllib/prog/sitepackages.py +33 -33
  83. pyxllib/prog/specialist/__init__.py +391 -391
  84. pyxllib/prog/specialist/bc.py +203 -203
  85. pyxllib/prog/specialist/browser.py +497 -497
  86. pyxllib/prog/specialist/common.py +347 -347
  87. pyxllib/prog/specialist/datetime.py +198 -198
  88. pyxllib/prog/specialist/tictoc.py +240 -240
  89. pyxllib/prog/specialist/xllog.py +180 -180
  90. pyxllib/prog/xlosenv.py +108 -108
  91. pyxllib/stdlib/__init__.py +17 -17
  92. pyxllib/stdlib/tablepyxl/__init__.py +10 -10
  93. pyxllib/stdlib/tablepyxl/style.py +303 -303
  94. pyxllib/stdlib/tablepyxl/tablepyxl.py +130 -130
  95. pyxllib/text/__init__.py +8 -8
  96. pyxllib/text/ahocorasick.py +39 -39
  97. pyxllib/text/airscript.js +744 -744
  98. pyxllib/text/charclasslib.py +121 -121
  99. pyxllib/text/jiebalib.py +267 -267
  100. pyxllib/text/jinjalib.py +32 -32
  101. pyxllib/text/jsa_ai_prompt.md +271 -271
  102. pyxllib/text/jscode.py +922 -922
  103. pyxllib/text/latex/__init__.py +158 -158
  104. pyxllib/text/levenshtein.py +303 -303
  105. pyxllib/text/nestenv.py +1215 -1215
  106. pyxllib/text/newbie.py +300 -300
  107. pyxllib/text/pupil/__init__.py +8 -8
  108. pyxllib/text/pupil/common.py +1121 -1121
  109. pyxllib/text/pupil/xlalign.py +326 -326
  110. pyxllib/text/pycode.py +47 -47
  111. pyxllib/text/specialist/__init__.py +8 -8
  112. pyxllib/text/specialist/common.py +112 -112
  113. pyxllib/text/specialist/ptag.py +186 -186
  114. pyxllib/text/spellchecker.py +172 -172
  115. pyxllib/text/templates/echart_base.html +10 -10
  116. pyxllib/text/templates/highlight_code.html +16 -16
  117. pyxllib/text/templates/latex_editor.html +102 -102
  118. pyxllib/text/vbacode.py +17 -17
  119. pyxllib/text/xmllib.py +747 -747
  120. pyxllib/xl.py +42 -39
  121. pyxllib/xlcv.py +17 -17
  122. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/METADATA +1 -1
  123. pyxllib-0.3.200.dist-info/RECORD +126 -0
  124. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/licenses/LICENSE +190 -190
  125. pyxllib-0.3.197.dist-info/RECORD +0 -126
  126. {pyxllib-0.3.197.dist-info → pyxllib-0.3.200.dist-info}/WHEEL +0 -0
pyxllib/ext/qt.py CHANGED
@@ -1,449 +1,449 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # @Author : 陈坤泽
4
- # @Email : 877362867@qq.com
5
- # @Date : 2021/05/26 17:24
6
-
7
- import os.path as osp
8
- import sys
9
- import time
10
- from datetime import datetime, timedelta
11
-
12
- from PyQt5 import QtWidgets
13
- from PyQt5.QtCore import QTimer, Qt, QThread, pyqtSignal, QEventLoop
14
- from PyQt5.QtWidgets import QApplication, QMainWindow, QFrame, QInputDialog, QMessageBox, QVBoxLayout, \
15
- QTextEdit, QSizePolicy, QLabel, QProgressBar, QDialog
16
- from PyQt5.QtGui import QTextOption
17
-
18
- from pyxllib.prog.newbie import CvtType
19
-
20
- here = osp.dirname(osp.abspath(__file__))
21
-
22
-
23
- class QHLine(QFrame):
24
- """ https://stackoverflow.com/questions/5671354/how-to-programmatically-make-a-horizontal-line-in-qt """
25
-
26
- def __init__(self):
27
- super(QHLine, self).__init__()
28
- self.setFrameShape(QFrame.HLine)
29
- self.setFrameShadow(QFrame.Sunken)
30
-
31
-
32
- class QVLine(QFrame):
33
- def __init__(self):
34
- super(QVLine, self).__init__()
35
- self.setFrameShape(QFrame.VLine)
36
- self.setFrameShadow(QFrame.Sunken)
37
-
38
-
39
- class XlLineEdit(QtWidgets.QLineEdit):
40
- correctChanged = pyqtSignal(object) # 符合数值类型的修改
41
- wrongChanged = pyqtSignal(str) # 不符合数值类型的修改
42
-
43
- def __init__(self, text=None, parent=None, *, valcvt=None):
44
- """
45
- :param valcvt: 数值类型转换器
46
- """
47
- super().__init__(str(text), parent)
48
-
49
- def check():
50
- # TODO 目前是强制重置样式,可以考虑怎么保留原样式基础上修改属性值
51
- s = self.text()
52
- try:
53
- if valcvt:
54
- s = valcvt(s)
55
- self.setStyleSheet('')
56
- self.setToolTip('')
57
- self.correctChanged.emit(s)
58
- except ValueError:
59
- self.setStyleSheet('background-color: lightpink;')
60
- self.setToolTip(f'输入数据不是{valcvt}类型')
61
- self.wrongChanged.emit(s)
62
-
63
- self.textChanged.connect(check)
64
- if text:
65
- check()
66
-
67
- # self.setStyleSheet(self.styleSheet() + 'qproperty-cursorPosition: 0;')
68
- self.setStyleSheet(self.styleSheet())
69
-
70
-
71
- class XlComboBox(QtWidgets.QComboBox):
72
- # 这个控件一般没有类型检查,但在支持填入自定义值时,是存在类型错误问题的
73
- # 但在工程上还是依然写了 correctChanged,方便下游任务统一接口
74
- correctChanged = pyqtSignal(object) # 符合数值类型的修改
75
- wrongChanged = pyqtSignal(str) # 不符合数值类型的修改
76
-
77
- def __init__(self, parent=None, *, text=None, items=None, valcvt=None, editable=False):
78
- """
79
- """
80
- # 1 基础设置
81
- super().__init__(parent)
82
- self.reset_items(items)
83
- self.editable = editable
84
- if self.editable:
85
- self.setEditable(True)
86
-
87
- # 2 检查功能
88
- def check(s):
89
- try:
90
- if valcvt:
91
- s = valcvt(s)
92
-
93
- if self.editable: # 支持自定义值
94
- self.setStyleSheet('')
95
- self.setToolTip('')
96
- self.correctChanged.emit(s)
97
- elif s not in self.items_set: # 不支持自定义值,但出现自定义值
98
- self.setStyleSheet('background-color: yellow;')
99
- self.setToolTip(f'不在清单里的非法值')
100
- self.wrongChanged.emit(s)
101
- else: # 不支持自定义值,且目前值在清单中
102
- self.setEditable(False)
103
- self.setStyleSheet('')
104
- self.setToolTip('')
105
- self.correctChanged.emit(s)
106
- except ValueError:
107
- self.setStyleSheet('background-color: lightpink;')
108
- self.setToolTip(f'输入数据不是{valcvt}类型')
109
- self.wrongChanged.emit(s)
110
-
111
- self.currentTextChanged.connect(check)
112
- # self.wrongChanged.connect(lambda s: print('非法值:', s)) # 可以监控非法值
113
-
114
- # 3 是否有预设值
115
- if text:
116
- self.setText(text)
117
-
118
- # 4 补充格式
119
- # self.setStyleSheet(self.styleSheet() + 'qproperty-cursorPosition: 0;')
120
- self.setStyleSheet(self.styleSheet())
121
-
122
- def setText(self, text):
123
- text = str(text)
124
- if text not in self.items and not self.editable:
125
- # 虽然不支持editable,但是出现了意外值,需要强制升级为可编辑
126
- self.setEditable(True)
127
- self.setCurrentText(text)
128
-
129
- def reset_items(self, items):
130
- # 1 存储配置清单
131
- self.clear()
132
- self.raw_items = items # noqa
133
- self.items = [str(x) for x in items if x is not None] # noqa
134
- self.items_set = set(self.items) # noqa 便于判断是否存在的集合类型
135
- self.addItems(self.items)
136
-
137
- # 2 画出 元素值、分隔符
138
- cnt = 0
139
- for i in range(len(items)):
140
- if items[i] is None:
141
- self.insertSeparator(i - cnt) # 不过这个分割符没那么显眼
142
- cnt += 1
143
-
144
-
145
- def get_input_widget(items=None, cur_value=None, *, parent=None, valcvt=None,
146
- n_widget=1, enabled=True,
147
- correct_changed=None):
148
- """ 根据items参数情况,智能判断生成对应的widget
149
-
150
- :param items:
151
- None, 普通的文本编辑框
152
- 普通数组,下拉框 (可以用None元素表示分隔符)
153
- list,除了列表中枚举值,也支持自定义输入其他值
154
- tuple,只能用列表中的枚举值
155
- 多级嵌套数组,多级下拉框 (未实装) (一般都是不可改的,并且同类型的数据)
156
- [('福建', [('龙岩', ['连城', '长汀', ...], ...)]), ('北京', ...)]
157
- 这种情况会返回多个widget
158
- :param cur_value: 当前显示的文本值
159
- :param valcvt: 数值类型转换函数,非法时抛出ValueError
160
- 很多输入框是传入文本,有时需要转为int、float、list等类型
161
- 支持输入常见类型转换的字符串名称,比如int、float
162
- :param correct_changed: 文本改变时的回调函数,一般用于数值有效性检查
163
- :param n_widget: 配合items为嵌套数组使用,需要指定嵌套层数
164
- 此时cur_value、cvt、enabled、text_changed等系列值可以传入n_widget长度的list
165
- :param enabled: 是否可编辑
166
- """
167
- if n_widget > 1:
168
- raise NotImplementedError
169
-
170
- # 1 封装类型检查功能
171
- if isinstance(valcvt, str):
172
- cvtfunc = CvtType.factory(valcvt)
173
- else:
174
- cvtfunc = valcvt
175
-
176
- # 2 正式生成控件
177
- if isinstance(items, (list, tuple)):
178
- # 带有 items 的字段支持候选下拉菜单
179
- w = XlComboBox(parent, text=cur_value, items=items, valcvt=cvtfunc, editable=isinstance(items, list))
180
- elif items is None:
181
- # 普通填充框
182
- w = XlLineEdit(cur_value, parent=parent, valcvt=cvtfunc)
183
- else:
184
- raise ValueError(f'{type(items)}')
185
-
186
- # 3 通用配置
187
- if callable(correct_changed):
188
- w.correctChanged.connect(correct_changed)
189
- if not enabled:
190
- w.setEnabled(enabled)
191
-
192
- return w
193
-
194
-
195
- def __other():
196
- pass
197
-
198
-
199
- def main_qapp(window):
200
- """ 执行Qt应用 """
201
- app = QApplication(sys.argv)
202
- window.show() # 展示窗口
203
- sys.exit(app.exec_())
204
-
205
-
206
- def qt_clipboard_monitor(func=None, verbose=1, *, cooldown=0.5):
207
- """ qt实现的剪切板监控器
208
-
209
- :param cooldown: cd,冷切时间,防止短时间内因为重复操作响应剪切板,重复执行功能
210
-
211
- 感觉这个组件还有很多可以扩展的,比如设置可以退出的快捷键
212
- """
213
- import pyperclip
214
-
215
- last_response = time.time()
216
-
217
- if func is None:
218
- func = lambda s: s
219
-
220
- def on_clipboard_change():
221
- # 1 数据内容一样则跳过不处理,表示很可能是该函数调用pyperclip.copy(s)产生的重复响应
222
- nonlocal last_response
223
- s0 = pyperclip.paste()
224
- s0 = s0.replace('\r\n', '\n')
225
-
226
- cur_time = time.time()
227
-
228
- if cur_time - last_response < cooldown:
229
- return
230
- last_response = cur_time
231
-
232
- # 2 处理函数
233
- s1 = func(s0)
234
- if s1 != s0:
235
- if verbose:
236
- print('【处理前】', time.strftime('%H:%M:%S'))
237
- print(s0)
238
- print('【处理后】')
239
- print(s1)
240
- print()
241
- pyperclip.copy(s1)
242
-
243
- app = QApplication([])
244
- clipboard = app.clipboard()
245
- clipboard.dataChanged.connect(on_clipboard_change)
246
- app.exec_()
247
-
248
-
249
- class XlThreadWorker(QThread):
250
- result = pyqtSignal(object) # 运行结果信号
251
- error = pyqtSignal(Exception) # 错误信号
252
- progress = pyqtSignal(int) # 进度信号
253
-
254
- def __init__(self, func, *args, use_progress=False, **kwargs):
255
- super().__init__()
256
- self.func = func
257
- self.args = args
258
- self.kwargs = kwargs
259
- if use_progress:
260
- self.kwargs['progress_callback'] = lambda v: self.progress.emit(v)
261
-
262
- def run(self):
263
- try:
264
- result = self.func(*self.args, **self.kwargs)
265
- self.result.emit(result) # Emit the result when done
266
- except Exception as e:
267
- self.error.emit(e)
268
-
269
-
270
- class WaitDialog(QDialog):
271
-
272
- def __init__(self, parent=None, text='', title='正在执行任务...', delay_seconds=5):
273
- super().__init__(parent)
274
- self.base_text = text
275
- self.setWindowTitle(title)
276
-
277
- self.timer = QTimer()
278
- self.timer.timeout.connect(self.update_text)
279
- self.start_time = None
280
- self.result = None
281
- self.worker = None
282
- self.error = None
283
-
284
- self.delay_milliseconds = delay_seconds * 1000 # 延迟弹窗
285
- self.is_running = False
286
-
287
- self.layout = QVBoxLayout() # 布局
288
- self.label = QLabel(self.base_text) # 标签
289
- self.layout.addWidget(self.label)
290
- self.pbar = QProgressBar() # 进度条
291
- self.layout.addWidget(self.pbar)
292
- self.setLayout(self.layout)
293
-
294
- def handle_result(self, result):
295
- self.result = result
296
- self.is_running = False
297
-
298
- def handle_error(self, error):
299
- self.error = error
300
- self.label.setText(f"{self.base_text}\n运行出现错误: {error}")
301
- self.is_running = False
302
-
303
- def handle_progress(self, progress):
304
- self.pbar.setValue(progress)
305
-
306
- def update_text(self):
307
- elapsed_time = int((datetime.now() - self.start_time).total_seconds()) + self.delay_milliseconds // 1000
308
- self.label.setText(f"{self.base_text}\n已运行 {elapsed_time} 秒")
309
-
310
- def run(self, func, *args, **kwargs):
311
- """
312
- def func():
313
- ... # 程序功能
314
-
315
- msg = WaitDialog().run(func) # 开一个等待窗口等程序运行
316
- """
317
- self.worker = XlThreadWorker(func, *args, **kwargs)
318
- self.worker.result.connect(self.handle_result)
319
- self.worker.error.connect(self.handle_error)
320
- self.is_running = True
321
- self.worker.start()
322
-
323
- QTimer.singleShot(self.delay_milliseconds, self.check_and_show)
324
-
325
- # 阻塞主线程,直到子线程完成
326
- while self.is_running:
327
- QApplication.processEvents() # 刷新UI,保持其响应性
328
- time.sleep(0.1) # 等待一段时间,以减少CPU使用率
329
-
330
- self.timer.stop()
331
- self.accept()
332
-
333
- return self.result
334
-
335
- def run_with_progress(self, func, *args, **kwargs):
336
- """
337
- def func(progress_callback):
338
- progress_callback(50) # 可以在运行中设置进度,进度值为0~100
339
- ... # 其他功能
340
-
341
- msg = WaitDialog().run_with_progress(func) # 运行完获得返回值
342
- """
343
- self.worker = XlThreadWorker(func, *args, use_progress=True, **kwargs)
344
- self.worker.result.connect(self.handle_result)
345
- self.worker.error.connect(self.handle_error)
346
- self.worker.progress.connect(self.handle_progress)
347
- self.is_running = True
348
- self.worker.start()
349
-
350
- QTimer.singleShot(self.delay_milliseconds, self.check_and_show)
351
-
352
- # 阻塞主线程,直到子线程完成
353
- while self.is_running:
354
- QApplication.processEvents() # 刷新UI,保持其响应性
355
- time.sleep(0.1) # 等待一段时间,以减少CPU使用率
356
-
357
- self.timer.stop()
358
- self.accept()
359
-
360
- return self.result
361
-
362
- def start_timer(self):
363
- self.start_time = datetime.now()
364
- self.timer.start(1000)
365
-
366
- def check_and_show(self):
367
- if self.is_running:
368
- self.show()
369
- self.start_timer()
370
-
371
- def __enter__(self):
372
- """ with写法比较简洁,但不太推荐这种使用方法,这样并不工程化
373
- 这样会把要运行的功能变成主线程,这个提示窗口会被挂起
374
-
375
- 这里功能设计上也比较简单些,不考虑写的很完善强大了。
376
- """
377
- self.show()
378
- QApplication.processEvents()
379
- return self
380
-
381
- def __exit__(self, exc_type, exc_val, exc_tb):
382
- self.accept()
383
-
384
-
385
- class CustomMessageBox(QMessageBox):
386
- def __init__(self, icon, title, text, copyable):
387
- super().__init__(icon, title, "")
388
- self.init_ui(title, text, copyable)
389
-
390
- def init_ui(self, title, text, copyable=False):
391
- layout = QVBoxLayout()
392
-
393
- if copyable:
394
- widget = QTextEdit()
395
- widget.setText(text)
396
- widget.setReadOnly(True)
397
- widget.setWordWrapMode(QTextOption.WrapAnywhere)
398
- widget.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
399
- widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
400
- widget.document().documentLayout().documentSizeChanged.connect(
401
- lambda: widget.setMinimumHeight(min(widget.document().size().height(), 700))
402
- )
403
- widget.setMinimumHeight(100)
404
- widget.setMaximumHeight(700)
405
- else:
406
- widget = QLabel()
407
- widget.setText(text)
408
- widget.setWordWrap(True)
409
-
410
- min_width = max(len(title) * 15, 600)
411
- widget.setMinimumWidth(min_width)
412
-
413
- layout.addWidget(widget)
414
- self.layout().addLayout(layout, 1, 1)
415
-
416
-
417
- def show_message_box(text, title=None, icon=None, detail=None,
418
- buttons=QMessageBox.Ok | QMessageBox.Cancel,
419
- default_button=QMessageBox.Ok, copyable=False):
420
- """ 显示一个提示框
421
-
422
- :param text: 提示框的文本内容
423
- :param title: 提示框的标题,默认值为 "提示"
424
- :param icon: 提示框的图标,默认值为 QMessageBox.NoIcon
425
- 注意Information、Warning、Critical等都会附带一个提示音
426
- 而Question是不带提示音的,默认的NoIcon也是不带提示音的
427
- :param detail: 提示框的详细信息,默认值为 None
428
- :param buttons: 提示框的按钮,默认值为 QMessageBox.Ok
429
- :param copyable: 消息窗中的文本是否可复制
430
-
431
- :return: 选择的按钮
432
-
433
- 实现上,本来应该依据setMinimumWidth可以搞定的事,但不知道为什么就是会有bug问题,总之最后问gpt靠实现一个类来解决了
434
-
435
- """
436
- if title is None:
437
- title = "提示"
438
-
439
- if icon is None:
440
- icon = QMessageBox.NoIcon
441
-
442
- msg_box = CustomMessageBox(icon, title, text, copyable)
443
-
444
- if detail is not None:
445
- msg_box.setDetailedText(detail)
446
- msg_box.setStandardButtons(buttons)
447
- msg_box.setDefaultButton(default_button)
448
-
449
- return msg_box.exec_()
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # @Author : 陈坤泽
4
+ # @Email : 877362867@qq.com
5
+ # @Date : 2021/05/26 17:24
6
+
7
+ import os.path as osp
8
+ import sys
9
+ import time
10
+ from datetime import datetime, timedelta
11
+
12
+ from PyQt5 import QtWidgets
13
+ from PyQt5.QtCore import QTimer, Qt, QThread, pyqtSignal, QEventLoop
14
+ from PyQt5.QtWidgets import QApplication, QMainWindow, QFrame, QInputDialog, QMessageBox, QVBoxLayout, \
15
+ QTextEdit, QSizePolicy, QLabel, QProgressBar, QDialog
16
+ from PyQt5.QtGui import QTextOption
17
+
18
+ from pyxllib.prog.newbie import CvtType
19
+
20
+ here = osp.dirname(osp.abspath(__file__))
21
+
22
+
23
+ class QHLine(QFrame):
24
+ """ https://stackoverflow.com/questions/5671354/how-to-programmatically-make-a-horizontal-line-in-qt """
25
+
26
+ def __init__(self):
27
+ super(QHLine, self).__init__()
28
+ self.setFrameShape(QFrame.HLine)
29
+ self.setFrameShadow(QFrame.Sunken)
30
+
31
+
32
+ class QVLine(QFrame):
33
+ def __init__(self):
34
+ super(QVLine, self).__init__()
35
+ self.setFrameShape(QFrame.VLine)
36
+ self.setFrameShadow(QFrame.Sunken)
37
+
38
+
39
+ class XlLineEdit(QtWidgets.QLineEdit):
40
+ correctChanged = pyqtSignal(object) # 符合数值类型的修改
41
+ wrongChanged = pyqtSignal(str) # 不符合数值类型的修改
42
+
43
+ def __init__(self, text=None, parent=None, *, valcvt=None):
44
+ """
45
+ :param valcvt: 数值类型转换器
46
+ """
47
+ super().__init__(str(text), parent)
48
+
49
+ def check():
50
+ # TODO 目前是强制重置样式,可以考虑怎么保留原样式基础上修改属性值
51
+ s = self.text()
52
+ try:
53
+ if valcvt:
54
+ s = valcvt(s)
55
+ self.setStyleSheet('')
56
+ self.setToolTip('')
57
+ self.correctChanged.emit(s)
58
+ except ValueError:
59
+ self.setStyleSheet('background-color: lightpink;')
60
+ self.setToolTip(f'输入数据不是{valcvt}类型')
61
+ self.wrongChanged.emit(s)
62
+
63
+ self.textChanged.connect(check)
64
+ if text:
65
+ check()
66
+
67
+ # self.setStyleSheet(self.styleSheet() + 'qproperty-cursorPosition: 0;')
68
+ self.setStyleSheet(self.styleSheet())
69
+
70
+
71
+ class XlComboBox(QtWidgets.QComboBox):
72
+ # 这个控件一般没有类型检查,但在支持填入自定义值时,是存在类型错误问题的
73
+ # 但在工程上还是依然写了 correctChanged,方便下游任务统一接口
74
+ correctChanged = pyqtSignal(object) # 符合数值类型的修改
75
+ wrongChanged = pyqtSignal(str) # 不符合数值类型的修改
76
+
77
+ def __init__(self, parent=None, *, text=None, items=None, valcvt=None, editable=False):
78
+ """
79
+ """
80
+ # 1 基础设置
81
+ super().__init__(parent)
82
+ self.reset_items(items)
83
+ self.editable = editable
84
+ if self.editable:
85
+ self.setEditable(True)
86
+
87
+ # 2 检查功能
88
+ def check(s):
89
+ try:
90
+ if valcvt:
91
+ s = valcvt(s)
92
+
93
+ if self.editable: # 支持自定义值
94
+ self.setStyleSheet('')
95
+ self.setToolTip('')
96
+ self.correctChanged.emit(s)
97
+ elif s not in self.items_set: # 不支持自定义值,但出现自定义值
98
+ self.setStyleSheet('background-color: yellow;')
99
+ self.setToolTip(f'不在清单里的非法值')
100
+ self.wrongChanged.emit(s)
101
+ else: # 不支持自定义值,且目前值在清单中
102
+ self.setEditable(False)
103
+ self.setStyleSheet('')
104
+ self.setToolTip('')
105
+ self.correctChanged.emit(s)
106
+ except ValueError:
107
+ self.setStyleSheet('background-color: lightpink;')
108
+ self.setToolTip(f'输入数据不是{valcvt}类型')
109
+ self.wrongChanged.emit(s)
110
+
111
+ self.currentTextChanged.connect(check)
112
+ # self.wrongChanged.connect(lambda s: print('非法值:', s)) # 可以监控非法值
113
+
114
+ # 3 是否有预设值
115
+ if text:
116
+ self.setText(text)
117
+
118
+ # 4 补充格式
119
+ # self.setStyleSheet(self.styleSheet() + 'qproperty-cursorPosition: 0;')
120
+ self.setStyleSheet(self.styleSheet())
121
+
122
+ def setText(self, text):
123
+ text = str(text)
124
+ if text not in self.items and not self.editable:
125
+ # 虽然不支持editable,但是出现了意外值,需要强制升级为可编辑
126
+ self.setEditable(True)
127
+ self.setCurrentText(text)
128
+
129
+ def reset_items(self, items):
130
+ # 1 存储配置清单
131
+ self.clear()
132
+ self.raw_items = items # noqa
133
+ self.items = [str(x) for x in items if x is not None] # noqa
134
+ self.items_set = set(self.items) # noqa 便于判断是否存在的集合类型
135
+ self.addItems(self.items)
136
+
137
+ # 2 画出 元素值、分隔符
138
+ cnt = 0
139
+ for i in range(len(items)):
140
+ if items[i] is None:
141
+ self.insertSeparator(i - cnt) # 不过这个分割符没那么显眼
142
+ cnt += 1
143
+
144
+
145
+ def get_input_widget(items=None, cur_value=None, *, parent=None, valcvt=None,
146
+ n_widget=1, enabled=True,
147
+ correct_changed=None):
148
+ """ 根据items参数情况,智能判断生成对应的widget
149
+
150
+ :param items:
151
+ None, 普通的文本编辑框
152
+ 普通数组,下拉框 (可以用None元素表示分隔符)
153
+ list,除了列表中枚举值,也支持自定义输入其他值
154
+ tuple,只能用列表中的枚举值
155
+ 多级嵌套数组,多级下拉框 (未实装) (一般都是不可改的,并且同类型的数据)
156
+ [('福建', [('龙岩', ['连城', '长汀', ...], ...)]), ('北京', ...)]
157
+ 这种情况会返回多个widget
158
+ :param cur_value: 当前显示的文本值
159
+ :param valcvt: 数值类型转换函数,非法时抛出ValueError
160
+ 很多输入框是传入文本,有时需要转为int、float、list等类型
161
+ 支持输入常见类型转换的字符串名称,比如int、float
162
+ :param correct_changed: 文本改变时的回调函数,一般用于数值有效性检查
163
+ :param n_widget: 配合items为嵌套数组使用,需要指定嵌套层数
164
+ 此时cur_value、cvt、enabled、text_changed等系列值可以传入n_widget长度的list
165
+ :param enabled: 是否可编辑
166
+ """
167
+ if n_widget > 1:
168
+ raise NotImplementedError
169
+
170
+ # 1 封装类型检查功能
171
+ if isinstance(valcvt, str):
172
+ cvtfunc = CvtType.factory(valcvt)
173
+ else:
174
+ cvtfunc = valcvt
175
+
176
+ # 2 正式生成控件
177
+ if isinstance(items, (list, tuple)):
178
+ # 带有 items 的字段支持候选下拉菜单
179
+ w = XlComboBox(parent, text=cur_value, items=items, valcvt=cvtfunc, editable=isinstance(items, list))
180
+ elif items is None:
181
+ # 普通填充框
182
+ w = XlLineEdit(cur_value, parent=parent, valcvt=cvtfunc)
183
+ else:
184
+ raise ValueError(f'{type(items)}')
185
+
186
+ # 3 通用配置
187
+ if callable(correct_changed):
188
+ w.correctChanged.connect(correct_changed)
189
+ if not enabled:
190
+ w.setEnabled(enabled)
191
+
192
+ return w
193
+
194
+
195
+ def __other():
196
+ pass
197
+
198
+
199
+ def main_qapp(window):
200
+ """ 执行Qt应用 """
201
+ app = QApplication(sys.argv)
202
+ window.show() # 展示窗口
203
+ sys.exit(app.exec_())
204
+
205
+
206
+ def qt_clipboard_monitor(func=None, verbose=1, *, cooldown=0.5):
207
+ """ qt实现的剪切板监控器
208
+
209
+ :param cooldown: cd,冷切时间,防止短时间内因为重复操作响应剪切板,重复执行功能
210
+
211
+ 感觉这个组件还有很多可以扩展的,比如设置可以退出的快捷键
212
+ """
213
+ import pyperclip
214
+
215
+ last_response = time.time()
216
+
217
+ if func is None:
218
+ func = lambda s: s
219
+
220
+ def on_clipboard_change():
221
+ # 1 数据内容一样则跳过不处理,表示很可能是该函数调用pyperclip.copy(s)产生的重复响应
222
+ nonlocal last_response
223
+ s0 = pyperclip.paste()
224
+ s0 = s0.replace('\r\n', '\n')
225
+
226
+ cur_time = time.time()
227
+
228
+ if cur_time - last_response < cooldown:
229
+ return
230
+ last_response = cur_time
231
+
232
+ # 2 处理函数
233
+ s1 = func(s0)
234
+ if s1 != s0:
235
+ if verbose:
236
+ print('【处理前】', time.strftime('%H:%M:%S'))
237
+ print(s0)
238
+ print('【处理后】')
239
+ print(s1)
240
+ print()
241
+ pyperclip.copy(s1)
242
+
243
+ app = QApplication([])
244
+ clipboard = app.clipboard()
245
+ clipboard.dataChanged.connect(on_clipboard_change)
246
+ app.exec_()
247
+
248
+
249
+ class XlThreadWorker(QThread):
250
+ result = pyqtSignal(object) # 运行结果信号
251
+ error = pyqtSignal(Exception) # 错误信号
252
+ progress = pyqtSignal(int) # 进度信号
253
+
254
+ def __init__(self, func, *args, use_progress=False, **kwargs):
255
+ super().__init__()
256
+ self.func = func
257
+ self.args = args
258
+ self.kwargs = kwargs
259
+ if use_progress:
260
+ self.kwargs['progress_callback'] = lambda v: self.progress.emit(v)
261
+
262
+ def run(self):
263
+ try:
264
+ result = self.func(*self.args, **self.kwargs)
265
+ self.result.emit(result) # Emit the result when done
266
+ except Exception as e:
267
+ self.error.emit(e)
268
+
269
+
270
+ class WaitDialog(QDialog):
271
+
272
+ def __init__(self, parent=None, text='', title='正在执行任务...', delay_seconds=5):
273
+ super().__init__(parent)
274
+ self.base_text = text
275
+ self.setWindowTitle(title)
276
+
277
+ self.timer = QTimer()
278
+ self.timer.timeout.connect(self.update_text)
279
+ self.start_time = None
280
+ self.result = None
281
+ self.worker = None
282
+ self.error = None
283
+
284
+ self.delay_milliseconds = delay_seconds * 1000 # 延迟弹窗
285
+ self.is_running = False
286
+
287
+ self.layout = QVBoxLayout() # 布局
288
+ self.label = QLabel(self.base_text) # 标签
289
+ self.layout.addWidget(self.label)
290
+ self.pbar = QProgressBar() # 进度条
291
+ self.layout.addWidget(self.pbar)
292
+ self.setLayout(self.layout)
293
+
294
+ def handle_result(self, result):
295
+ self.result = result
296
+ self.is_running = False
297
+
298
+ def handle_error(self, error):
299
+ self.error = error
300
+ self.label.setText(f"{self.base_text}\n运行出现错误: {error}")
301
+ self.is_running = False
302
+
303
+ def handle_progress(self, progress):
304
+ self.pbar.setValue(progress)
305
+
306
+ def update_text(self):
307
+ elapsed_time = int((datetime.now() - self.start_time).total_seconds()) + self.delay_milliseconds // 1000
308
+ self.label.setText(f"{self.base_text}\n已运行 {elapsed_time} 秒")
309
+
310
+ def run(self, func, *args, **kwargs):
311
+ """
312
+ def func():
313
+ ... # 程序功能
314
+
315
+ msg = WaitDialog().run(func) # 开一个等待窗口等程序运行
316
+ """
317
+ self.worker = XlThreadWorker(func, *args, **kwargs)
318
+ self.worker.result.connect(self.handle_result)
319
+ self.worker.error.connect(self.handle_error)
320
+ self.is_running = True
321
+ self.worker.start()
322
+
323
+ QTimer.singleShot(self.delay_milliseconds, self.check_and_show)
324
+
325
+ # 阻塞主线程,直到子线程完成
326
+ while self.is_running:
327
+ QApplication.processEvents() # 刷新UI,保持其响应性
328
+ time.sleep(0.1) # 等待一段时间,以减少CPU使用率
329
+
330
+ self.timer.stop()
331
+ self.accept()
332
+
333
+ return self.result
334
+
335
+ def run_with_progress(self, func, *args, **kwargs):
336
+ """
337
+ def func(progress_callback):
338
+ progress_callback(50) # 可以在运行中设置进度,进度值为0~100
339
+ ... # 其他功能
340
+
341
+ msg = WaitDialog().run_with_progress(func) # 运行完获得返回值
342
+ """
343
+ self.worker = XlThreadWorker(func, *args, use_progress=True, **kwargs)
344
+ self.worker.result.connect(self.handle_result)
345
+ self.worker.error.connect(self.handle_error)
346
+ self.worker.progress.connect(self.handle_progress)
347
+ self.is_running = True
348
+ self.worker.start()
349
+
350
+ QTimer.singleShot(self.delay_milliseconds, self.check_and_show)
351
+
352
+ # 阻塞主线程,直到子线程完成
353
+ while self.is_running:
354
+ QApplication.processEvents() # 刷新UI,保持其响应性
355
+ time.sleep(0.1) # 等待一段时间,以减少CPU使用率
356
+
357
+ self.timer.stop()
358
+ self.accept()
359
+
360
+ return self.result
361
+
362
+ def start_timer(self):
363
+ self.start_time = datetime.now()
364
+ self.timer.start(1000)
365
+
366
+ def check_and_show(self):
367
+ if self.is_running:
368
+ self.show()
369
+ self.start_timer()
370
+
371
+ def __enter__(self):
372
+ """ with写法比较简洁,但不太推荐这种使用方法,这样并不工程化
373
+ 这样会把要运行的功能变成主线程,这个提示窗口会被挂起
374
+
375
+ 这里功能设计上也比较简单些,不考虑写的很完善强大了。
376
+ """
377
+ self.show()
378
+ QApplication.processEvents()
379
+ return self
380
+
381
+ def __exit__(self, exc_type, exc_val, exc_tb):
382
+ self.accept()
383
+
384
+
385
+ class CustomMessageBox(QMessageBox):
386
+ def __init__(self, icon, title, text, copyable):
387
+ super().__init__(icon, title, "")
388
+ self.init_ui(title, text, copyable)
389
+
390
+ def init_ui(self, title, text, copyable=False):
391
+ layout = QVBoxLayout()
392
+
393
+ if copyable:
394
+ widget = QTextEdit()
395
+ widget.setText(text)
396
+ widget.setReadOnly(True)
397
+ widget.setWordWrapMode(QTextOption.WrapAnywhere)
398
+ widget.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
399
+ widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
400
+ widget.document().documentLayout().documentSizeChanged.connect(
401
+ lambda: widget.setMinimumHeight(min(widget.document().size().height(), 700))
402
+ )
403
+ widget.setMinimumHeight(100)
404
+ widget.setMaximumHeight(700)
405
+ else:
406
+ widget = QLabel()
407
+ widget.setText(text)
408
+ widget.setWordWrap(True)
409
+
410
+ min_width = max(len(title) * 15, 600)
411
+ widget.setMinimumWidth(min_width)
412
+
413
+ layout.addWidget(widget)
414
+ self.layout().addLayout(layout, 1, 1)
415
+
416
+
417
+ def show_message_box(text, title=None, icon=None, detail=None,
418
+ buttons=QMessageBox.Ok | QMessageBox.Cancel,
419
+ default_button=QMessageBox.Ok, copyable=False):
420
+ """ 显示一个提示框
421
+
422
+ :param text: 提示框的文本内容
423
+ :param title: 提示框的标题,默认值为 "提示"
424
+ :param icon: 提示框的图标,默认值为 QMessageBox.NoIcon
425
+ 注意Information、Warning、Critical等都会附带一个提示音
426
+ 而Question是不带提示音的,默认的NoIcon也是不带提示音的
427
+ :param detail: 提示框的详细信息,默认值为 None
428
+ :param buttons: 提示框的按钮,默认值为 QMessageBox.Ok
429
+ :param copyable: 消息窗中的文本是否可复制
430
+
431
+ :return: 选择的按钮
432
+
433
+ 实现上,本来应该依据setMinimumWidth可以搞定的事,但不知道为什么就是会有bug问题,总之最后问gpt靠实现一个类来解决了
434
+
435
+ """
436
+ if title is None:
437
+ title = "提示"
438
+
439
+ if icon is None:
440
+ icon = QMessageBox.NoIcon
441
+
442
+ msg_box = CustomMessageBox(icon, title, text, copyable)
443
+
444
+ if detail is not None:
445
+ msg_box.setDetailedText(detail)
446
+ msg_box.setStandardButtons(buttons)
447
+ msg_box.setDefaultButton(default_button)
448
+
449
+ return msg_box.exec_()