Skip to content

Instantly share code, notes, and snippets.

@ImN1
Last active October 25, 2023 14:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ImN1/149231ac57d637cfc6c67b6d9d89565f to your computer and use it in GitHub Desktop.
Save ImN1/149231ac57d637cfc6c67b6d9d89565f to your computer and use it in GitHub Desktop.
image_viewer.py
#!/usr/bin/env python3
# -*-coding:utf-8 -*-
'''
A picture slideshow viewer widget
'''
import os
import natsort
from PyQt5 import QtWidgets, QtCore, QtGui
# from imnGuis.mQt5Property import Property, PropertyMeta
from imnGuis.mQt5State import StateMachine
# from imnPaths import mPath
from imnPaths.mTree import treeFiles
# selfdir = os.path.dirname(__file__)
regx = r'[^\.]*\.(bmp|gif|jpe?g|pbm|pgm|png|ppm|svg)$'
from functools import partial
# from PySide6 import QtCore, QtGui, QtWidgets, QtStateMachine
# from imnGuis.mQt6State import StateMachine
from imnGuis.mQt5Img import cv2Qim
import pandas as pd
import numpy as np
import pyvips
from imnPaths.mTree import treeFilePaths
from imnArrays.mDict import Namespace
from imnSyntaxs.mVar import empty
from imnEnvs.mCatalog import within
class FloatingButtonWidget(QtWidgets.QPushButton):
'''https://www.deskriders.dev/posts/007-pyqt5-overlay-button-widget/'''
def __init__(self, x:int=0, y:int=0, parent=None):
super().__init__(parent)
self.orginalX = x
self.orginalY = y
self.padding = None
def resizeEvent(self, event):
super().resizeEvent(event)
self.update_position()
def mousePressEvent(self, event):
self.parent().floatingButtonClicked.emit(self)
def set_padding(self, x, y):
self.padding = (x, y)
def update_position(self):
parent_rect = self.parent().rect()
if hasattr(self.parent(), 'viewport'):
parent_rect = self.parent().viewport().rect()
w, h = self.width(), self.height()
if not parent_rect:
self.setGeometry(0, 0, w, h)
return
if self.padding is None:
v = max(parent_rect.width()//50, parent_rect.height()//50)
self.padding = (v, v)
x = self.padding[0] + self.orginalX
y = self.padding[1] + self.orginalY
if hasattr(self.parent(), 'buttonsPosition'):
po = self.parent().buttonsPosition
if po&1:
x = parent_rect.width() - w - x
if (po>>1)&1:
y = parent_rect.height() - h - y
self.setGeometry(x, y, w, h)
class ImageLabel(QtWidgets.QLabel):
# floatingButtonClicked = QtCore.Signal(object)
# dropFilesFinished = QtCore.Signal(object)
floatingButtonClicked = QtCore.pyqtSignal(object)
dropFilesFinished = QtCore.pyqtSignal(object)
regx = r'[^\.]+\.(bmp|gif|jpe?g|pbm|pgm|png|ppm|svg)$'
def __init__(self, parent=None):
super(ImageLabel, self).__init__(parent)
self.setProperty('cssClass', 'imageLabel')
self.images = []
self.current = -1
self.setup_layout()
def setup_layout(self):
'''如果浮动按钮靠右,或靠底,x,y 默认为 0(不含 padding),距离边缘较远的,需要设定x,y'''
self.setMargin(0)
self.setContentsMargins(0, 0, 0, 0)
self.setAcceptDrops(True)
self.setStyleSheet('QLabel{border: 1px solid black;}')
# self.buttonsPosition = QtCore.Qt.Corner.TopRightCorner
self.buttonsPosition = 1 # 0~3 top-left, top-right, bottom-left, bottom-right
self.btn_next = FloatingButtonWidget(parent=self) # parent 是必须的,用于计算位置座标
self.btn_next.setFixedSize(30, 25)
self.btn_next.setContentsMargins(0,0,0,0)
self.btn_next.setStyleSheet('QPushButton {background-color: transparent;border:0}')
self.btn_back = FloatingButtonWidget(parent=self, x=self.btn_next.width())
self.btn_back.setFixedSize(30, 25)
self.btn_back.setContentsMargins(0,0,0,0)
self.btn_back.setStyleSheet('QPushButton {background-color: transparent;border:0}')
self.floatingButtonClicked.connect(self.buttonClick)
self.setText(' Drop Images/Folder Here ')
self.btn_back.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowLeft))
self.btn_next.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_ArrowRight))
def resizeEvent(self, event):
self.updateGeometry()
self.btn_next.update_position()
self.btn_back.update_position()
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
files = tuple(u.toLocalFile() for u in event.mimeData().urls())
ims = []
for x in files:
if os.path.isfile(x) and mPath.regCheckPath(x, self.regx):
ims.append(x)
continue
if os.path.isdir(x):
[ims.append(y) for y in treeFilePaths(x, regx=self.regx)]
self.images = natsort.natsorted(ims, alg=natsort.ns.PATH)
self.dropFilesFinished.emit(self.images)
self.nextim()
return
def buttonClick(self, sender):
if sender==self.btn_back:
self.nextim(reverse=True)
return
if sender==self.btn_next:
self.nextim()
return
def set_current(self, value:int=-1):
self.current = value
def nextim(self, imlist=None, reverse:bool=False):
if imlist is None:
if empty(self.images):
return
imlist = self.images
if reverse:
self.current -= 1
if self.current<0:
self.current = len(imlist) - 1
else:
self.current += 1
if self.current>=len(imlist):
self.current = 0
self.showImage(imlist[self.current])
def showImage(self, im:QtGui.QImage):
if empty(im):
self.clear()
return
w, h = self.width()-2, self.height()-2
if isinstance(im, str):
if not os.path.exists(im):
return
vim = pyvips.Image.thumbnail(im, min(w, h), size='down', crop='all')
im = cv2Qim(vim.numpy())
if im.width()>w or im.height()>h:
im = im.scaled(w, h, QtCore.Qt.KeepAspectRatio, QtCore.Qt.FastTransformation)
self.setPixmap(QtGui.QPixmap.fromImage(im))
class ViewPad(QtWidgets.QWidget):
# floatingButtonClicked = QtCore.Signal(object)
floatingButtonClicked = QtCore.pyqtSignal(object)
def __init__(self, showmode:int=1, parent=None):
super(ViewPad, self).__init__(parent)
# self.parent = parent
self.showmode = showmode
self.setContentsMargins(0,0,0,0)
self.setup_layout()
# self.initSignal()
# self.initStateMachine()
# self.initThread()
def setup_layout(self):
self.setContentsMargins(0,0,0,0)
self.spt = QtWidgets.QSplitter(QtCore.Qt.Orientation.Horizontal)
self.imgLabels = Namespace()
for p in ('left', 'middle', 'right'):
self.imgLabels[p] = ImageLabel(self)
self.imgLabels[p].setContentsMargins(0,0,0,0)
self.spt.addWidget(self.imgLabels[p])
if p=='middle':
self.imgLabels[p].btn_back.hide()
self.imgLabels[p].btn_next.hide()
# # self.spt.setHandleWidth(0) # 控制缝隙距离
hlyt = QtWidgets.QHBoxLayout()
hlyt.setSpacing(0)
hlyt.setContentsMargins(0,0,0,0)
hlyt.addWidget(self.spt)
self.setLayout(hlyt)
self.btn = FloatingButtonWidget(parent=self)
self.btn.setFixedSize(100, 30)
self.btn.set_padding(0, 0)
self.btn.setText('Match')
self.btn.hide()
# self.btn.setStyleSheet("""QPushButton{color: rgba(33,33,33, 0.1);background: rgba(255,255,255, 0.1);opacity: 0.1;}
# QPushButton:hover{color: rgba(0,0,0, 1.0);background: rgba(255,255,255, 1);opacity: 1.0;}""")
self.floatingButtonClicked.connect(partial(print, 'OK'))
def resizeEvent(self, event):
self.updateGeometry()
self.btn.orginalX = (self.width() - self.btn.width()) // 2
self.btn.orginalY = self.height() - (self.height() // 50 + self.btn.height())
self.btn.update_position()
def setup_mode(self, showmode:int):
self._showmode = showmode
class ViewControls(QtWidgets.QWidget):
lang = {
'SlideShowViewer.nextToolTip': ' Next ',
'SlideShowViewer.backToolTip': ' Back ',
'SlideShowViewer.fastToolTip': ' Wait 1s less ',
'SlideShowViewer.slowToolTip': ' Wait 1s more ',
'SlideShowViewer.skipToolTip': ' Skip All & Clear ',
'SlideShowViewer.startToolTip': ' Start slide show ',
'SlideShowViewer.pauseToolTip': ' Pause ',
'SlideShowViewer.resumeToolTip': ' Resume ',
'SlideShowViewer.closeToolTip': ' Close ',
'viewerStartMsg': 'Drag Pictures or Picture Folder Here',
}
def __init__(self, showmode:int=1, parent=None):
super(ViewControls, self).__init__(parent)
self.started = False
self.setFixedWidth(40)
self.setup_layout()
self.initTranslate()
def setup_layout(self):
self.lbl_num = QtWidgets.QLabel()
self.lbl_num.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.btn_single = QtWidgets.QPushButton()
self.btn_dual = QtWidgets.QPushButton()
self.btn_mirror = QtWidgets.QPushButton()
self.btn_triple = QtWidgets.QPushButton()
self.btn_pause = QtWidgets.QPushButton()
self.btn_next = QtWidgets.QPushButton()
self.btn_back = QtWidgets.QPushButton()
self.btn_fast = QtWidgets.QPushButton()
self.btn_slow = QtWidgets.QPushButton()
self.btn_skip = QtWidgets.QPushButton()
self.btn_close = QtWidgets.QPushButton()
vlyt = QtWidgets.QVBoxLayout()
vlyt.setContentsMargins(5, 5, 5, 5)
vlyt.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
vlyt.addWidget(self.btn_single)
vlyt.addWidget(self.btn_dual)
vlyt.addWidget(self.btn_triple)
vlyt.addWidget(self.btn_mirror)
vlyt.addStretch(1)
vlyt.addWidget(self.lbl_num)
vlyt.addWidget(self.btn_pause)
vlyt.addWidget(self.btn_fast)
vlyt.addWidget(self.btn_slow)
vlyt.addWidget(self.btn_next)
vlyt.addWidget(self.btn_back)
vlyt.addWidget(self.btn_skip)
vlyt.addWidget(self.btn_close)
self.setLayout(vlyt)
selfdir = os.path.dirname(__file__)
self.btn_single.setIcon(QtGui.QIcon(os.path.join(selfdir, 'image.svg')))
self.btn_dual.setIcon(QtGui.QIcon(os.path.join(selfdir, 'dual_black.svg')))
self.btn_triple.setIcon(QtGui.QIcon(os.path.join(selfdir, 'tri_black.svg')))
self.btn_mirror.setIcon(QtGui.QIcon(os.path.join(selfdir, 'dual_white.svg')))
self.btn_pause.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaPlay))
self.btn_next.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSkipForward))
self.btn_back.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSkipBackward))
self.btn_fast.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSeekForward))
self.btn_slow.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaSeekBackward))
self.btn_skip.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaStop))
self.btn_close.setIcon(self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_LineEditClearButton))
def initTranslate(self):
self.btn_pause.setToolTip(self.lang['SlideShowViewer.pauseToolTip'])
self.btn_next.setToolTip(self.lang['SlideShowViewer.nextToolTip'])
self.btn_back.setToolTip(self.lang['SlideShowViewer.backToolTip'])
self.btn_fast.setToolTip(self.lang['SlideShowViewer.fastToolTip'])
self.btn_slow.setToolTip(self.lang['SlideShowViewer.slowToolTip'])
self.btn_skip.setToolTip(self.lang['SlideShowViewer.skipToolTip'])
class Viewer(QtWidgets.QWidget, QtCore.QObject):
# imagesChangeSignal = QtCore.Signal()
# pauseSignal = QtCore.Signal()
imagesChangeSignal = QtCore.pyqtSignal()
pauseSignal = QtCore.pyqtSignal()
def __init__(self, showmode:int=1, parent=None):
super(Viewer, self).__init__(parent)
# self._showmode = showmode # useless
self.setProperty('showmode', showmode)
# self._pause = True # useless
# self.setProperty('pause', True) # useless
self.setContentsMargins(0,0,0,0)
self.wait = 1500
self.waitMin = 1000
self.waitMax = 10000
self.timer = QtCore.QTimer()
self.timer.setInterval(self.wait)
self.current = 0
self.total = 0
self.images = None
self.setup_layout()
self.initSignal()
self.initStateMachine()
# self.initThread()
def setup_layout(self):
self.setContentsMargins(0,0,0,0)
self.pad = ViewPad(self)
self.controls = ViewControls()
self.slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
self.slider.setProperty('cssClass', 'circleQSlider')
# self.slider.setStyleSheet(
# # '''
# # QSlider::groove:horizontal {
# # height:10px;
# # border:1px solid black;
# # border-radius: 5px;
# # border-style:inset;
# # }
# # QSlider::handle:horizontal {
# # width:10px;
# # height: 15px;
# # color:#ccc;
# # border:1px solid black;
# # border-radius:5px;
# # border-style:outset;
# # background:qradialgradient(cx:0.3,cy:-0.4,fx:0.3,fy:-0.4,radius:1.35,stop:0 #fff,stop:1 #ccc);
# # }
# # QSlider::handle:horizontal:hover {
# # border-radius: 5px;
# # }
# # '''
# '''
# QSlider::groove:horizontal {
# border-radius: 2px;
# height: 3px;
# margin: 9px;
# background-color: #ccc;
# }
# QSlider::groove:horizontal:hover {
# background-color: #666;
# }
# QSlider::handle:horizontal {
# background-color: #fff;
# border: 2px solid #000;
# height: 14px;
# width: 12px;
# margin: -6px 0;
# border-radius: 7px;
# padding: -6px 0px;
# }
# '''
# )
self.lbl = QtWidgets.QLabel()
self.lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.lbl.adjustSize()
self.lbl.setText('0 / 0')
self.slider.setValue(0) # 数值为下标,显示时 +1
self.slider.setPageStep(1)
hlyt = QtWidgets.QHBoxLayout()
hlyt.addWidget(self.lbl)
hlyt.addWidget(self.slider)
vlyt = QtWidgets.QVBoxLayout()
vlyt.setContentsMargins(0,0,0,0)
vlyt.addWidget(self.pad)
vlyt.addLayout(hlyt)
hlyt_viewer = QtWidgets.QHBoxLayout()
hlyt_viewer.addLayout(vlyt)
hlyt_viewer.addWidget(self.controls)
self.setLayout(hlyt_viewer)
def initTranslate(self):
pass
def initSignal(self):
self.controls.btn_next.clicked.connect(self.nextim)
self.controls.btn_back.clicked.connect(self.nextim)
self.controls.btn_fast.clicked.connect(self.timewait_change)
self.controls.btn_slow.clicked.connect(self.timewait_change)
self.controls.btn_skip.clicked.connect(self.end_show)
# self.btn_close.clicked.connect(self.close)
self.slider.sliderReleased.connect(self.slider_change)
self.slider.valueChanged.connect(self.slider_change)
self.timer.timeout.connect(self.nextim)
# self.slider.sliderPressed.connect(self.timer.stop)
[w.dropFilesFinished.connect(self.add_images) for w in self.pad.imgLabels.values()]
self.imagesChangeSignal.connect(self.images_changed)
def initStateMachine(self):
param = {'states': 2, 'init': 0,} # pause: 0 False, 1 True
param['property'] = (
# (self, b'paused', (False, True)),
(self.controls.btn_pause, 'icon', (
self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaPlay),
self.style().standardIcon(QtWidgets.QStyle.StandardPixmap.SP_MediaPause),
)),
(self.controls.btn_pause, 'toolTip', (
self.controls.lang['SlideShowViewer.pauseToolTip'],
self.controls.lang['SlideShowViewer.resumeToolTip']
)),
)
param['transition'] = (
{self.controls.btn_pause.clicked: 1,}, # self.paused False -> True
{
self.controls.btn_pause.clicked: 0,
self.pauseSignal: 0,
self.slider.sliderPressed: 0,
}, # self.paused True -> False
)
param['connect'] = (
((self.timer.stop,), None), # self.paused True -> False
((self.timer.start,), None), # self.paused False -> True
)
self.pauseState = StateMachine(param=param)
param = {'states': 4, 'init': self.property('showmode'),} # pause: 0 single, 1 dual, 2 triple, 3 mirror
param['property'] = (
(self, 'showmode', (0, 1, 2, 3)),
(self.pad.spt, 'handleWidth', (0, 5, 5, 0)),
(self.controls.btn_single, 'enabled', (False, True, True, True)),
(self.controls.btn_dual, 'enabled', (True, False, True, True)),
(self.controls.btn_triple, 'enabled', (True, True, False, True)),
(self.controls.btn_mirror, 'enabled', (True, True, True, False)),
(self.pad.imgLabels['left'], 'alignment', (
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter,
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter,
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter,
QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignVCenter,)),
(self.pad.imgLabels['right'], 'alignment', (
0,
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter,
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter,
QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignVCenter,)),
(self.pad.imgLabels['middle'], 'alignment', (
0,
0,
QtCore.Qt.AlignmentFlag.AlignCenter|QtCore.Qt.AlignmentFlag.AlignVCenter,
0,)),
(self.pad.imgLabels['right'], 'visible', (False, True, True, True)),
(self.pad.imgLabels['middle'], 'visible', (False, False, True, False)),
)
param['transition'] = ( # key=signal, value=go to state num
{
self.controls.btn_dual.clicked: 1,
self.controls.btn_triple.clicked: 2,
self.controls.btn_mirror.clicked: 3,},
{
self.controls.btn_single.clicked: 0,
self.controls.btn_triple.clicked: 2,
self.controls.btn_mirror.clicked: 3,},
{
self.controls.btn_single.clicked: 0,
self.controls.btn_dual.clicked: 1,
self.controls.btn_mirror.clicked: 3,},
{
self.controls.btn_single.clicked: 0,
self.controls.btn_dual.clicked: 1,
self.controls.btn_triple.clicked: 2,},
)
fun_single = partial(self.pad.spt.setSizes, [1, 0, 0])
fun_dual = partial(self.pad.spt.setSizes, [5, 0, 5])
fun_triple = partial(self.pad.spt.setSizes, [5, 5, 5])
fun_mirror = partial(self.pad.spt.setSizes, [5, 0, 5])
param['connect'] = (
((fun_single,), None),
((fun_dual,), None),
((fun_triple,), None),
((fun_mirror,), None),
)
self.secondPane = StateMachine(param=param)
def keyPressEvent(self, keyevent):
""" Capture key to exit, next image, previous image,
on Escape , Key Right and key left respectively.
"""
key = keyevent.key()
if key == QtCore.Qt.Key_Escape:
self.end_show()
if key == QtCore.Qt.Key_Left:
self.pauseSignal.emit()
self.nextim(True)
if key == QtCore.Qt.Key_Right:
self.pauseSignal.emit()
self.nextim()
if key == 32:
self.controls.btn_pause.click()
def images_changed(self):
if empty(self.images):
self.slider.setRange(0, 100)
self.slider.setValue(0)
self.lbl.setText('0 / 0')
for w in self.pad.imgLabels.values():
w.images = []
w.current = -1
return
def _set_child_images(p):
w = self.pad.imgLabels[p]
if p in self.images.columns:
w.images = self.images[p]
elif (p:=f'paths_{p}') in self.images.columns:
w.images = self.images[p]
self.total = len(self.images)
self.slider.setRange(0, self.total-1)
self.lbl.setText(f'{self.current}/{self.total}')
[_set_child_images(p) for p in self.pad.imgLabels.names()]
def add_images(self, ims):
sender = self.sender()
p = self.pad.imgLabels(sender)
name = f'paths_{p}'
if isinstance(self.images, pd.DataFrame):
if self.images.shape[0]>=len(ims):
self.images[name] = pd.Series(ims)
else:
self.images = self.images.join(pd.Series(ims, name=name), how='outer').reset_index(drop=True)
else:
self.images = pd.DataFrame({name:ims})
self.imagesChangeSignal.emit()
def set_images(self, ims:pd.DataFrame):
self.images = ims
self.imagesChangeSignal.emit()
def slider_change(self):
self.current = self.slider.value()
value = self.current + 1
[w.set_current(self.current) for w in self.pad.imgLabels.values()]
total = self.total
self.lbl.setText(f'{value}/{total}')
QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), f'{value} / {total}', self.slider)
self.showImage()
def set_current(self, value):
self.current = self.slider.value()
def end_show(self):
self.pauseSignal.emit()
[w.clear() for w in self.pad.imgLabels.values()]
self.images = None
self.imagesChangeSignal.emit()
def nextim(self, reverse:bool=False):
if self.images is None:
return
sender = self.sender()
if sender in {self.controls.btn_back, self.controls.btn_next}:
self.pauseSignal.emit()
# self.timer.stop()
if sender==self.controls.btn_back:
reverse = True
if reverse:
self.current -= 1
if self.current<0:
self.current = len(self.images) - 1
else:
self.current += 1
if self.current>=len(self.images):
self.current = 0
self.slider.setValue(self.current)
# self.showImage()
def timewait_change(self):
sender = self.sender()
if sender==self.controls.btn_slow:
self.wait += 1000
if sender==self.controls.btn_fast:
self.wait -= 1000
i = within(self.wait, (self.waitMin, self.waitMax))
self.controls.btn_fast.setEnabled(i>=1)
self.controls.btn_slow.setEnabled(i<=1)
self.wait = (self.waitMin, self.wait, self.waitMax)[i]
self.timer.setInterval(self.wait)
def showImage(self):
if empty(self.images):
return
[w.showImage(w.images[self.current]) for w in self.pad.imgLabels.values() if not empty(w.images)]
# 上面一些不能导入的函数、类,因为多项目通用,所以分在其他文件
# 现抽出来,贴在下面,其他 import 应该没什么用,是以前留下的废代码
# =====================================
# 这个 StateMachine 当初随手写的,没写好,pyqt5 勉强能用,也就懒得改了
# pyqt6不能用,pyqt6 需要多套了一层 QState,而且 namespace 变了
class StateMachine(QtCore.QStateMachine, QtCore.QObject):
def __init__(self, parent=None, param=None):
'''
调用方式例子:\n
param = {'states': 2, 'init': 0,} # states 是状态总数,下标从0开始,init 为初始状态,下标值\n
param['property'] = ( # 属性值 -> 每组各个状态的值,对应下标\n
(self.tray, 'icon', (self._icon(Datas.env_icons['white']), self._icon(Datas.env_icons['green']))),\n
(self.tray.tray_menu_show, 'visible', (False, True)),\n
(self.tray.tray_menu_hide, 'visible', (True, False)),\n
(self, 'visible', (True, False)),\n
)\n
param['transition'] = ( # 每个状态下如何激活其他状态,key激活条件,value是目标状态序号,字典个数不能少于状态数,顺序对应下标\n
{\n
self.tray.dblclick:1, \n
self.tray.tray_menu_hide.triggered:1, \n
self.mainwindowShowHideSignal:1\n
}, # mainWindows show -> hide\n
{\n
self.tray.dblclick:0, \n
self.tray.tray_menu_show.triggered:0, \n
self.mainwindowShowHideSignal:0\n
}, # mainWindows hide -> show\n
)\n
param['connect'] = (\n
状态进出时发出信号,每组对应状态下标,参考上述 if 'connect'...部分\n
结构:每行对应一个状态,二元二维 tuple -> (进入状态要执行的多个方法 [tuple],离开状态要执行的多个方法 [tuple])\n
进入或离开没有任何追加方法则为 None\n
((self.timer.start,), None), # self.paused False -> True\n
((self.timer.stop,), None), # self.paused True -> False\n
)\n
mainShowHideState = StateMachine(self, param) # 定义状态(启用)
'''
# super().__init__(parent=parent)
super(StateMachine, self).__init__(parent=parent)
self.states = [QtCore.QState(self) for _ in range(param['states'])]
# self.states = [QtCore.QState(self) for _ in range(param['states'])]
for i in range(param['states']):
for x in param['property']:
self.states[i].assignProperty(x[0], x[1], x[2][i])
if 'transition' in param and param['transition'][i]:
t = param['transition'][i]
for x in t:
self.states[i].addTransition(x, self.states[t[x]])
if 'connect' in param and param['connect'][i]:
c = param['connect'][i]
if c[0]:
for x in c[0]:
self.states[i].entered.connect(x)
if c[1]:
for x in c[1]:
self.states[i].exited.connect(x)
self.setInitialState(self.states[param['init']])
self.start()
def current(self):
return next((i for i,x in enumerate(self.states) if x.active()), -1)
#====================================
def cv2Qim(cvim): # 输入格式为 opencv(numpy.ndarray)
if len(cvim.shape)==2:
h, w = cvim.shape
bytesPerLine = w
imformat = QtGui.QImage.Format_Grayscale8 # maybe Format_Indexed8 ?
else:
h, w, channels = cvim.shape
bytesPerLine = channels * w
imformat = QtGui.QImage.Format_RGB888
return QtGui.QImage(cvim.data, w, h, bytesPerLine, imformat)
#=======================================
# empty 用于不确定类型的无效值判断,随手写,逻辑不严谨,出错时再改
def empty(s, none2empty:bool=True, false2empty:bool=False, zero2empty:bool=False, white2empty:bool=False):
'''
判断 s 是否为空值、白值、None值、以及嵌套的空数组\n
仅支持较为常用的变量类型,str|bytes|int|float|Nonetype|pandas|numpy.array|一维dict|tuple|set|list等\n
嵌套的字典视为 NOT empty,即二维为空时返回 False\n
'''
if hasattr(s, 'empty'):
return s.empty
if isinstance(s, list):
return not bool(s)
if none2empty and s in {np.nan, None}:
return True
if isinstance(s, (str, bytes)):
if len(s)==0:
return True
if white2empty and s.isspace():
return True
return False
if hasattr(s, 'shape'):
if s.shape[0]==0:
return True
if len(s.shape)>1 and s.shape[1]==0:
return True
return False
if s:
return False
if not false2empty and s is False: return False
if not zero2empty and s==0: return False
if not none2empty and s is None: return False
if isinstance(s, PureIterable) and list(flatten(s)): return False
return True
#==========================================
def within(score, range, included:bool=True):
'''设定最大最小值范围,及对照类别 0,1,2;1 表示在range范围内,included表示范围是否也包含边界值'''
mi, ma = min(range), max(range)
breakpoints = (mi, ma)
left = bisect.bisect_left(breakpoints, score)
right = bisect.bisect(breakpoints, score)
if included:
return left or right
return (left, right)[bool(left and right)]
#========================================
# Namespace 不贴了,很长很乱,实际上就是个加强的 dict,继承自 types.SimpleNamespace
# names() 获取 list(dict.keys()), 而__call__() 可以根据 dict 某个 value 获取首个对应的 key [str]格式,如果 key/value 一一对应可定位 key
#
# treeFilePaths 也不贴了,套了闭包,乱,反正返回就是递归获取所有文件的全路径(不含文件夹,regex 过滤),list[str] 格式
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment