Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Rnreck/cda62c1b7c5417ff9420901a04fcf0f4 to your computer and use it in GitHub Desktop.

Select an option

Save Rnreck/cda62c1b7c5417ff9420901a04fcf0f4 to your computer and use it in GitHub Desktop.

Revisions

  1. @ImN1 ImN1 revised this gist Oct 25, 2023. 1 changed file with 1 addition and 2 deletions.
    3 changes: 1 addition & 2 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -297,8 +297,7 @@ class ViewControls(QtWidgets.QWidget):
    vlyt.addWidget(self.btn_close)
    self.setLayout(vlyt)

    # selfdir = os.path.dirname(__file__)
    selfdir = r'c:\works\pymodule\imnGuis'
    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')))
  2. @ImN1 ImN1 created this gist Oct 25, 2023.
    770 changes: 770 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,770 @@
    #!/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__)
    selfdir = r'c:\works\pymodule\imnGuis'

    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] 格式