本文为记录摘抄自 ZooTool 工具集中的 ListViewLayout ,该布局可以根据父 UI 组件窗口的宽度,调整每一行所能容纳的最大的元素个数。类似flowLayout,但比flowLayout省资源。
示例代码如下:
# -*- coding:utf-8 -*-
# !/usr/bin/env python3
# Author: Mirror
# File: flowLayout
# Time: 2022-12-10 21:43
# Update: 2022-12-12 22:43
# Environment:PyCharm
# Blog: www.mirrorcg.com
# Description: listViewLayout布局示例
# =========================================
import os
import glob
import tempfile
import copy
import json
import zipfile
import imghdr
import sys
from functools import partial
from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtWidgets import *
if sys.version_info[0] == 3:
string_types = str
else:
string_types = bytes
INFOASSET = "assetType" # key for the .zooinfo dict and file (as json)
INFOCREATORS = "creators" # key for the .zooinfo dict and file (as json)
INFOWEBSITES = "websites" # key for the .zooinfo dict and file (as json)
INFOTAGS = "tags" # key for the .zooinfo dict and file (as json)
INFODESCRIPTION = "description" # key for the .zooinfo dict and file (as json)
INFOSAVE = "saved" # key for the .zooinfo dict and file (as json)
INFOANIM = "animation" # key for the .zooinfo dict and file (as json)
VERSIONKEY = "version"
GENERICVERSIONNO = "1.0.0" # the version number of the generic file format
ASSETTYPES = ["Not Specified", "Hero Model", "Prop Model", "Background", "Background Lights", "Scene", "IBL", "Lights",
"Shaders", "Animation", "Camera"]
ZOOINFOSUFFIX = "zooInfo" # TODO use new constant in zoosceneconstants
QTSUPPORTEDIMAGES = ('bmp', 'gif', 'jpg', 'jpeg', 'mng', 'png', 'pbm', 'pgm', 'ppm', 'tiff', 'xbm', 'xpm', 'svg', 'tga')
DEPENDENCY_FOLDER = "fileDependencies" # the prefix of the dependency folder for file dependencies.
ZOO_THUMBNAIL = "thumbnail" # the name of thumbnail images minus the file extension which can be png or jpg etc
ZOOSCENE_EXT = "zooScene" # main suffix for zooScene files
PREFERENCE_FOLDER = "preferences"
INTERFACE_FOLDER = "interface"
preference = None
DIRECTORYNAMEDEPENDENCIES = "fileDependencies"
def dpiScale(value):
"""根据当前 DPI 按值调整大小
:param value: 默认2K int型
:return value: 当前显示器的的大小 int型
"""
DEFAULT_DPI = 96
mult = max(1, float(QApplication.desktop().logicalDpiY()) / float(DEFAULT_DPI))
return value * mult
def getTempDir():
"""返回所有操作系统上的临时目录路径"""
return os.environ.get('TEMP', os.environ.get('TMP', os.environ.get('TMPDIR', '/tmp')))
def getFilesNoExt(directory, fileNoExtension):
"""给定一个没有扩展名的文件,找到文件名,对于查找扩展名未知的缩略图很有用,即可能是.jpg或.png
:param directory: 用于搜索和返回文件名的目录 str
:param fileNoExtension: 要查找的文件的名称 str
:return fileList: 与名称匹配的文件列表,可以是空的或多个 list(str)
"""
fileList = list()
os.chdir(directory)
for fileName in glob.glob("{}.*".format(fileNoExtension)):
fileList.append(fileName)
# TODO: Probably shouldn't be using glob, return the current path to the directory
tempDir = getTempDir()
os.chdir(tempDir)
return fileList
def getImageNoExtension(directory, nameNoExtension):
"""返回目录中具有名称(不带文件扩展名)的图像列表,有助于查找“缩略图.png或缩略图.jpg”,返回找到的文件的列表
:param directory: 用于搜索和返回文件名的目录 str
:param nameNoExtension: 没有扩展名的文件名 str
:return imagePathList: 与名称NoExtension匹配的图像列表 list
"""
imagePathList = list()
fileList = getFilesNoExt(directory, nameNoExtension)
if not fileList:
return
for file in fileList:
filename, file_extension = os.path.splitext(file)
file_extension = file_extension.replace(".", "")
if file_extension in QTSUPPORTEDIMAGES:
imagePathList.append(file)
return imagePathList
def thumbnails(directory, fileListNoExt):
"""从不带扩展名的 zooScene 列表中返回缩略图路径的列表
:return thumbPathList: 每个 zooScene 文件一个缩略图路径列表(无扩展名)
:rtype thumbFullPathList: 基础字符串列表
"""
thumbFullPathList = list()
for i, zooSceneName in enumerate(fileListNoExt):
dependFolder = "_".join([zooSceneName, DEPENDENCY_FOLDER])
dependFolderPath = os.path.join(directory, dependFolder)
if not os.path.isdir(dependFolderPath):
thumbFullPathList.append(None)
continue
imageList = getImageNoExtension(dependFolderPath, ZOO_THUMBNAIL)
if imageList: # image list is only thumbnail, take the first image, could be jpg or png
thumbFullPathList.append(os.path.join(dependFolderPath, imageList[0]))
continue
thumbFullPathList.append(None)
return thumbFullPathList
def filesByExtension(directory, extensionList):
"""列出给定文件扩展名列表的给定目录中的所有文件,扩展名应该没有句号,即 [“zooScene”, “json”, “jpg”]
Return Example:
["soft_sunsetSides.zooScene", "sun_redHarsh.zooScene", "sun_warmGlow.zooScene"]
:param directory: 用于搜索和返回文件名的目录 str
:param extensionList: A list of extensions to search ["zooScene", "json", "jpg"]
:type extensionList: list of basestring
:return fileList: A list of files returned that are in the directory and match the extension list
:rtype fileList: list()str
"""
fileList = list()
if not os.path.isdir(directory): # check if directory actually exists
return fileList # emptyList and directory
for ext in extensionList:
for filePath in glob.glob(os.path.join(directory, "*.{}".format(ext))):
fileList.append(os.path.basename(filePath))
# TODO: Probably shouldn't be using glob, return the current path to the directory
tempDir = getTempDir()
os.chdir(tempDir)
return fileList
def saveJson(data, filepath, **kws):
"""此过程将给定数据保存到 json 文件
:param kws: Json Dumps arguments , see standard python docs
"""
try:
with open(filepath, 'w') as f:
json.dump(data, f, **kws)
except IOError:
print("Data not saved to file {}".format(filepath))
return False
print("------->> file correctly saved to : {0}".format(filepath))
return True
def ensureFolderExists(path, permissions=0o775, placeHolder=False):
"""如果该文件夹不存在,则将创建一个。由于版本控制错误而构建的函数与未提交的空文件夹,此文件夹可以生成占位符文件。
:param path: the folderpath to check or create
:type path: str
:param permissions: folder permissions mode
:type permissions: int
:param placeHolder: if True create a placeholder text file
:type placeHolder: bool
:raise OSError: raise OSError if the creation of the folder fails
"""
if not os.path.exists(path):
try:
print("Creating folder {} [{}]".format(path, permissions))
os.makedirs(path, permissions)
if placeHolder:
placePath = os.path.join(path, "placeholder")
if not os.path.exists(placePath):
with open(placePath, "wt") as fh:
fh.write("Automatically Generated placeHolder file.")
fh.write("The reason why this file exists is due to source control system's which do not "
"handle empty folders.")
except OSError as e:
# more less work if network race conditions(joy!)
raise
def loadJson(filePath):
"""
此过程加载并返回 json 文件的数据
:return type{dict}: the content of the file
"""
# load our file
try:
with loadFile(filePath) as f:
data = json.load(f)
except Exception as er:
print("file ({}) not loaded".format(filePath))
raise er
# return the files data
return data
def getFileDependenciesList(zooSceneFullPath, ignoreThumbnail=False):
"""检索依赖目录 目录名依赖项中所有文件的列表,文件没有完整路径,因此也会返回目录路径,文件是 [“文件名.abc”, “文件名.zooInfo”] 等
:param zooSceneFullPath: the full path to the file usually .zooscene but can be any extension
:type zooSceneFullPath: str
:param ignoreThumbnail: ignores the files called thumbnail.* useful for renaming
:type ignoreThumbnail: str
:return fileDependencyList: list of short name files found in the subdirectory DIRECTORYNAMEDEPENDENCIES
:rtype fileDependencyList: list
:return fullDirPath: the full path of the sub directory DIRECTORYNAMEDEPENDENCIES
:rtype fullDirPath: str
"""
fileDependencyList = list()
zooSceneFileName = os.path.basename(zooSceneFullPath)
directoryPath = os.path.dirname(zooSceneFullPath)
fileNameNoExt = os.path.splitext(zooSceneFileName)[0]
newDirectoryName = "_".join([fileNameNoExt, DIRECTORYNAMEDEPENDENCIES])
fullDirPath = os.path.join(directoryPath, newDirectoryName)
if not os.path.exists(fullDirPath): # doesn't already exist
return fileDependencyList, "" # return empty as directory doesn't exist
os.chdir(fullDirPath)
for fileName in glob.glob("{}.*".format(fileNameNoExt)):
fileDependencyList.append(fileName)
if not ignoreThumbnail:
for fileName in glob.glob("thumbnail.*".format(fileNameNoExt)):
fileDependencyList.append(fileName)
# return os.chdir to the temp dir as it gets in the way with file permissions for later renames!
os.chdir(tempfile.gettempdir())
return fileDependencyList, fullDirPath
def getSingleFileFromZooScene(zooSceneFullPath, fileExtension):
"""返回文件的名称(如果存在),扩展名为 zooSceneFullPath 中的 .abc
获取与 .zooScene 文件关联的子目录中的所有文件,并筛选文件类型,支持返回一个文件,第一个文件它发现不适合纹理
:param zooSceneFullPath: the full path of the .zooScene file to be saved
:type zooSceneFullPath: str
:param fileExtension: the file extension to find no ".", so alembic is "abc"
:type fileExtension: str
:return extFileName: the filename (no directory) of the extension given > for example "myFileName.abc"
:rtype extFileName: str
"""
extFileName = ""
fileList, directory = getFileDependenciesList(zooSceneFullPath)
if not directory:
return extFileName
for fileName in fileList: # cycle through the files and find if a match with the extension
if fileName.endswith(fileExtension):
return os.path.join(directory, fileName)
return extFileName
def loadFile(filepath):
if filepath.endswith(".zip"):
with zipfile.ZipFile(filepath, 'r') as f:
yield f
return
elif ".zip" in filepath:
# load from zipfile
zippath, relativefilePath = filepath.split(".zip")
zipPath = zippath + ".zip"
with zipfile.ZipFile(zipPath, 'r') as zip:
path = relativefilePath.replace("\\", "/").lstrip("/")
for i in iter(zip.namelist()):
if path == i:
yield zip.open(i)
break
return
with open(filepath) as f:
yield f
def createTagInfoDict(assetType, creator, website, tags, description, saveInfo, animInfo):
"""为zooInfo 文件创建一个字典
:param assetType: the information about asset type, model, scene, lights, shaders, etc
:type assetType: str
:param creator: the information about creator/s
:type creator: str
:param website: the information about the creators website links
:type website: str
:param tags: the tag information
:type tags: str
:param description: the full description
:type description: str
:param saveInfo: the file information saved as a list ["alembic", "generic lights"] etc
:type saveInfo: list
:param animInfo: the animation information of the file "0 100" or "" or None if none
:type animInfo: str
:return zooInfoDict: the dict containing all information including the file version number
:rtype zooInfoDict: str
"""
zooInfoDict = {INFOASSET: assetType,
INFOCREATORS: creator,
INFOWEBSITES: website,
INFOTAGS: tags,
INFODESCRIPTION: description,
INFOSAVE: saveInfo,
INFOANIM: animInfo,
VERSIONKEY: GENERICVERSIONNO}
return zooInfoDict
def getZooInfoFromFile(zooSceneFullPath, message=True):
"""从 .zooScene 获取其他文件,例如从磁盘上的文件中获取 .zooInfo
:param zooSceneFullPath: the full path of the zooScene file, this will save out as another file zooInfo
:type zooSceneFullPath: str
:return zooInfoDict: the dictionary with all info information, if None the file wasn't found
:rtype zooInfoDict: dict
:return fileFound: was the zooInfo file found?
:rtype fileFound: bool
"""
zooInfoFileFullPath = getSingleFileFromZooScene(zooSceneFullPath, ZOOINFOSUFFIX)
if not os.path.exists(zooInfoFileFullPath): # doesn't exist
if message:
om2.MGlobal.displayWarning("ZooInfo File Not Found")
fileFound = False
return createTagInfoDict(ASSETTYPES[0], "", "", "", "", "", ""), fileFound # return the empty dict as no file found
fileFound = True
return loadJson(zooInfoFileFullPath), fileFound # returns zooInfoDict
def createEmptyInfoDict():
"""创建一个空的 .zooInfo 字典"""
infoDict = {u"assetType": u"",
u"animation": None,
u"description": u"",
u"version": u"1.0.0",
u"tags": u"",
u"creators": u"",
u"saved": str(list()),
u"websites": u""
}
return infoDict
def infoDictionaries(zooSceneNameList, directory):
""" 返回每个 .zooScene 文件的信息字典列表。这些词典包含作者、标签、描述等信息
:return infoDictList: 每个 .zooScene 文件的信息字典列表
:rtype infoDictList: list of dict
"""
infoDictList = list()
if not zooSceneNameList:
return dict()
for zooSceneFile in zooSceneNameList:
lightPresetFullPath = os.path.join(directory, zooSceneFile)
zooInfoDict, fileFound = getZooInfoFromFile(lightPresetFullPath, message=False)
if fileFound:
infoDictList.append(zooInfoDict)
else:
infoDictList.append(createEmptyInfoDict())
return infoDictList
def isImage(path):
try:
return imghdr.what(path) is not None or path.split(os.extsep)[-1] in QTSUPPORTEDIMAGES
except IOError:
return False
class ThumbnailDelegate(QStyledItemDelegate):
def __init__(self, parent):
super(ThumbnailDelegate, self).__init__(parent)
def sizeHint(self, option, index):
return index.model().itemFromIndex(index).sizeHint()
def paint(self, painter, options, index):
index.model().itemFromIndex(index).paint(painter, options, index)
class ThumbListView(QListView):
contextMenuRequested = Signal(list, object)
requestZoom = Signal(object, float)
# QModelIndex, Treeitem
requestDoubleClick = Signal(object, object)
zoomAmount = 1
defaultMinIconSize = 20
defaultMaxIconSize = 512
defaultIconSize = QSize(256, 256)
stateChanged = Signal()
WheelEvent = 1
EnterEvent = 2
CalcInitialEvent = 3
CalcEvent = 4
VerticalSliderReleasedEvent = 5
def __init__(self, parent=None, delegate=None, iconSize=defaultIconSize, uniformItems=False):
super(ThumbListView, self).__init__(parent=parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._contextMenu)
self.autoScale = True
self.defaultSize = None
self.initialIconSize = None
self.columnQueue = 0
self.columnOffset = 1
self.zoomable = True
self._iconSize = QSize()
self.setIconSize(iconSize or self.defaultIconSize)
self.initUi()
self.maxColumns = 8
self.setVerticalScrollBar(QScrollBar(self))
self.connections()
self._delegate = delegate(self) if delegate is not None else ThumbnailDelegate(self)
self.setItemDelegate(self._delegate)
self.setUpdatesEnabled(True)
self._uniformItemSizes = uniformItems
self.setUniformItemSizes(uniformItems)
def initUi(self):
self.setMouseTracking(True)
self.setSelectionRectVisible(True)
self.setViewMode(QListView.IconMode)
self.setResizeMode(QListView.Adjust)
self.setSelectionMode(QListView.SingleSelection)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setDragEnabled(False)
self.setAcceptDrops(False)
def _contextMenu(self, position):
model = self.model()
if model is None:
return
menu = QMenu(self)
selectionModel = self.selectionModel()
selection = [model.itemFromIndex(index) for index in selectionModel.selectedIndexes()]
self.contextMenuRequested.emit(selection, menu)
menu.exec_(self.viewport().mapToGlobal(position))
def setUniformItemSizes(self, enable):
self._uniformItemSizes = enable
if self.model():
self.model().setUniformItemSizes(enable)
def setIconSize(self, size):
self._iconSize = size
super(ThumbListView, self).setIconSize(size)
def connections(self):
self.verticalScrollBar().sliderMoved.connect(self.verticalSliderMoved)
self.verticalScrollBar().sliderReleased.connect(self.verticalSliderReleased)
self.clicked.connect(lambda: self.stateChanged.emit())
self.activated.connect(lambda: self.stateChanged.emit())
# self.entered.connect(lambda: self.stateChanged.emit())
def wheelEvent(self, event):
""" 重写以处理缩放 listview.
:type event: :class:`QEvent`
"""
modifiers = event.modifiers()
if self.zoomable and modifiers == Qt.ControlModifier:
if event.delta() > 0:
self.columnOffset -= 1
else:
self.columnOffset += 1
self.columnOffset = min(self.columnOffset, self.maxColumns)
size = self.widgetSize()
index = self.indexAt(event.pos())
# if its an invalid index, find closest instead
if not index.isValid():
index = self.closestIndex(event.pos())
self.calcResize(size)
QTimer.singleShot(0, lambda: self.scrollTo(index))
event.accept()
self.stateChanged.emit()
return
super(ThumbListView, self).wheelEvent(event)
self.loadVisibleIcons()
self.stateChanged.emit()
def state(self):
""" Returns useful settings to copy from one list view behaviour to another
:return:
:rtype:
"""
selectedIndex = self.selectionModel().currentIndex().row()
ret = {"sliderPos": self.verticalScrollBar().value(),
"sliderMin": self.verticalScrollBar().minimum(),
"sliderMax": self.verticalScrollBar().maximum(),
"selected": selectedIndex,
"columns": self.columnOffset,
"zoomAmount": self.zoomAmount,
"iconSize": self._iconSize,
"initialIconSize": self.initialIconSize,
"defaultSize": self.defaultSize,
"fixedSize": self.parentWidget().minimumSize()
}
return ret
def setState(self, state):
""" Set the state of the listview with the new settings provided from ThumbListView.state()
:param state:
:type state:
:return:
:rtype:
"""
self.columnOffset = state['columns']
self._iconSize = state['iconSize']
self.zoomAmount = state['zoomAmount']
self.defaultSize = state['defaultSize']
self.initialIconSize = state['initialIconSize']
fixedSize = state['fixedSize']
if fixedSize.width() != 0:
self.parentWidget().setFixedWidth(fixedSize.width())
if fixedSize.height() != 0:
self.parentWidget().setFixedHeight(fixedSize.height())
self.calcResize(self.widgetSize())
self.verticalScrollBar().setMinimum(state['sliderMin'])
self.verticalScrollBar().setMaximum(state['sliderMax'])
self.verticalScrollBar().setValue(state['sliderPos'])
self.loadVisibleIcons()
if state['selected'] != -1:
QTimer.singleShot(0, lambda: self.delaySelect(state['selected']))
def delaySelect(self, sel):
""" Select index
:param sel:
:type sel:
:return:
:rtype:
"""
autoScroll = self.hasAutoScroll()
self.setAutoScroll(False)
self.selectionModel().setCurrentIndex(self.model().index(sel, 0),
QItemSelectionModel.ClearAndSelect)
self.setAutoScroll(autoScroll)
def closestIndex(self, pos):
""" Get closest index based on pos
:param pos:
:type pos:
:return:
:rtype:
"""
maxDist = -1
closest = None
for index in self.visibleItems():
c = self.visualRect(index)
if c.top() <= pos.y() <= c.bottom(): # Only choose the ones from the same row
dist = pos.x() - c.center().x()
if maxDist == -1 or dist < maxDist:
closest = index
maxDist = dist
return closest
def loadVisibleIcons(self):
""" Loads visible icons in the view if they have not been loaded yet
:return:
:rtype:
"""
for index in self.visibleItems(pre=5, post=5):
try:
treeItem = self.model().items[index.row()]
item = treeItem.item()
if not item.iconLoaded():
self.model().threadPool.start(item.iconThread)
except IndexError:
pass # this should possibly be handled
def filter(self, text, tag=None):
filterList = self.model().filterList(text, tag)
self.loadVisibleIcons()
def verticalSliderReleased(self):
self.stateChanged.emit()
def verticalSliderMoved(self, pos):
""" On vertical slider moved, reload the visible icons
:return:
:rtype:
"""
self.loadVisibleIcons()
def model(self):
""" 获取 ListView model
:return:
:rtype: FileViewModel
"""
return super(ThumbListView, self).model()
def visibleItems(self, pre=0, post=0):
""" 获取可见项。如果要在开头和结尾添加额外的索引,请将 extra 设置为 1 或更多。它只会返回有效索引。
:param pre: Add extra items behind the currently visible items
:type pre: int
:param post: Add extra items after the currently visible items
:type post: int
:return: List of indices that are visible plus the pre and post. It only returns valid indices
:rtype: list of QModelIndex
"""
firstIndex = self.indexAt(QPoint(0, 0))
viewportRect = self.viewport().rect()
items = list()
after = post
i = -pre # We want to check indices behind the firstIndex
while True:
sibling = firstIndex.sibling(firstIndex.row() + i, 0)
# The index is visually shown in the viewport?
if sibling.isValid() and viewportRect.intersects(self.visualRect(sibling)):
items.append(sibling)
elif sibling.isValid() and i < 0: # If it's indices behind the firstIndex
items.append(sibling)
elif sibling.isValid() and after > 0: # We want some extra indices at the end so keep going
after -= 1
items.append(sibling)
elif i < 0 or self.isIndexHidden(sibling): # We want to keep going even if the behind siblings are invalid,
# Or hidden by search
i += 1
continue
else:
break # Either it is invalid (reached the end) or we've gone outside the screen
i += 1
return items
def scrollTo(self, index, hint=None):
""" 覆盖默认的scrollTo并使用我们自己的
:param index:
:type index:
:return:
:rtype:
"""
if hint is None:
itemRect = self.rectForIndex(index)
vbar = self.verticalScrollBar()
mPos = self.mapFromGlobal(QCursor().pos()) # Pos of mouse in listview widget
newPos = (float(itemRect.y()) / float(self.contentsSize().height())) * \
(float(self.contentsSize().height())) - \
mPos.y() + \
(itemRect.height() * 0.5) # maybe better if this is the mousePosition relative to the item instead
vbar.setValue(newPos)
else:
super(ThumbListView, self).scrollTo(index, hint)
self.loadVisibleIcons()
def widgetSize(self):
""" 不带垂直滚动条的大小
:return:
:rtype:
"""
size = QSize(self.size())
size -= QSize(self.verticalScrollBar().size().width(), 0)
return size
def _calculateZoom(self, delta):
""" 在通过 'setZoomAmount' 将其应用于图标之前计算缩放系数
:param delta: the delta value
:type delta: float
:return: the converted delta to zoom factor
:rtype: float
"""
inFactor = 1.15
outFactor = 1 / inFactor
if delta > 0:
zoomFactor = inFactor
else:
zoomFactor = outFactor
self.zoomAmount = zoomFactor
return zoomFactor
def setZoomAmount(self, value):
""" 通过采用视图图标大小()*值来设置此视图的缩放量
:param value:
:type value:
:return:
:rtype:
"""
currentSize = QSize(self.initialIconSize)
newSize = currentSize.width() * value
if newSize < self.defaultMinIconSize:
return
currentSize.setWidth(newSize)
currentSize.setHeight(newSize)
self.requestZoom.emit(newSize, self.zoomAmount)
self.setIconSize(currentSize)
def mouseDoubleClickEvent(self, event):
if event.button() == Qt.LeftButton:
index = self.currentIndex()
model = self.model()
if model is not None:
item = model.itemFromIndex(index)
model.doubleClickEvent(index, item)
self.requestDoubleClick.emit(index, item)
def resizeEvent(self, event):
self.calcResize(event.size())
super(ThumbListView, self).resizeEvent(event)
self.loadVisibleIcons()
def setColumns(self, col):
""" 根据小组件的当前大小设置列数
:param col:
:type col:
:return:
:rtype:
"""
self.setIconSize(self.defaultIconSize)
# Sets columns on next calcResize
self.columnQueue = col
# Change the default size, but only if it has been already set
if self.defaultSize is not None:
self.defaultSize = self.widgetSize()
self.calcResize(self.defaultSize)
def calcResize(self, size):
""" 计算项目的大小调整
:param size:
:type size:
:return:
:rtype:
"""
if self.defaultSize is None:
# Initialize values
self.defaultSize = size
self.initialIconSize = QSize(self._iconSize)
self.stateChanged.emit()
QTimer.singleShot(0, self.refresh)
return
# Exclude the verticalScrollbar space
if not self.verticalScrollBar().isVisible():
size -= QSize(self.verticalScrollBar().size().width(), 0)
size -= QSize(dpiScale(2), 0)
# Calculate the number of columns
iconWidth = (self.initialIconSize * self.zoomAmount).width() + self.spacing()
calcColumns = int(size.width() / iconWidth) # calculate number of columns and round it down
columns = calcColumns + self.columnOffset
# setColumn() was run so set the columns by changing the columnOffset
if self.columnQueue != 0:
diff = self.columnQueue - calcColumns
columns = self.columnQueue
self.columnOffset = diff
self.columnQueue = 0
columns = max(columns, 1) # atleast 1 column
# Set columnOffset maximum and minimum
# Calculate the new ratio to resize the icons to
ratio = float(iconWidth * columns) / float(size.width())
self.setZoomAmount(1 / ratio)
self.stateChanged.emit()
def refresh(self):
""" 刷新,以便图标正确显示
:return:
:rtype:
"""
QCoreApplication.processEvents()
size = self.size() - QSize(self.verticalScrollBar().size().width(), 0)
self.resizeEvent(QResizeEvent(size, size))
self.model().layoutChanged.emit()
if not self.updatesEnabled():
self.setUpdatesEnabled(True)
def setModel(self, model):
model.setUniformItemSizes(self._uniformItemSizes)
return super(ThumbListView, self).setModel(model)
class ThumbnailViewWidget(QWidget):
"""用于查看缩略图的主要小部件,它运行在自定义QStandardItemModel上"""
requestSelectionChanged = Signal(object, object)
def __init__(self, parent=None, listDelegate=None, listView=ThumbListView, columns=None, iconSize=None,
fixedWidth=None, fixedHeight=None, uniformIcons=False):
"""
:param parent: the parent widget
:type parent: QWidget
:param listDelegate:
:type listDelegate:
:param listView:
:type listView:
:param columns: The number of square image columns, will vary for non-square images, overrides iconSize
:type columns: int
:param iconSize: Set the icon size in pixels, will be overridden by columns
:type iconSize: QSize
:param fixedWidth: The fixed width of the widget in pixels, dpi handled
:type fixedWidth: int
:param fixedHeight: the fixed height of the widget in pixels, dpi handled
:type fixedHeight: int
:param uniformIcons: Will keep the icons square, images will be clipped if True, non square if False
:type uniformIcons: bool
"""
super(ThumbnailViewWidget, self).__init__(parent=parent)
self.qModel = None
self.listDelegate = listDelegate
self.listViewClass = listView # type: type(ThumbListView)
self.uniformIcons = uniformIcons
self.initUi()
self.autoResizeItems = True
self.pagination = True
if iconSize is not None:
self.setIconSize(iconSize)
if columns:
self.setColumns(columns)
if fixedHeight:
self.setFixedHeight(dpiScale(fixedHeight))
if fixedWidth:
self.setFixedWidth(dpiScale(fixedWidth))
def initUi(self):
layout = QVBoxLayout(self)
self.setLayout(layout)
self.listView = self.listViewClass(parent=self, delegate=self.listDelegate, uniformItems=self.uniformIcons)
layout.addWidget(self.listView)
self.listView.verticalScrollBar().valueChanged.connect(self.paginationLoadNextItems)
self.listView.contextMenuRequested.connect(self.contextMenu)
layout.setContentsMargins(0, 0, 0, 0)
self.listView.setSpacing(dpiScale(0))
def setModel(self, model):
self.listView.setModel(model)
model.refreshRequested.connect(self.listView.loadVisibleIcons)
model.view = self
self.qModel = model
if self.listView.selectionModel():
self.listView.selectionModel().selectionChanged.connect(self.onSelectionChanged)
def refreshListView(self):
""" 刷新列表视图,确保图标正确调整大小"""
self.listView.refresh()
def invisibleRootItem(self):
if self.qModel:
return self.qModel.invisibleRootItem()
def iconSize(self):
return self.listView.iconSize()
def setIconSize(self, size):
if size == self.listView.iconSize():
return
maxSize = self.listView.defaultMaxIconSize
minSize = self.listView.defaultMinIconSize
width = size.width()
height = size.height()
if width > maxSize or height > maxSize:
size = QSize(maxSize, maxSize)
elif width < minSize or height < minSize:
size = QSize(minSize, minSize)
self.listView.setIconSize(size)
def filter(self, text, tag=None):
self.listView.filter(text, tag)
def setColumns(self, col):
""" 将列重置为默认值"""
self.listView.setColumns(col)
def setIconMinMax(self, size):
""" 设置最小和最大图标大小
:param size: 图标大小的最小值和最大值
:type size: tuple(int, int)
"""
minSize = size[0]
maxSize = size[1]
self.listView.defaultMinIconSize = minSize
self.listView.defaultMaxIconSize = maxSize
currentSize = self.listView.iconSize()
width = currentSize.width()
height = currentSize.height()
if width > maxSize or height > maxSize:
size = QSize(maxSize, maxSize)
self.listView.setIconSize(size)
elif width < minSize or height > minSize:
size = QSize(minSize, minSize)
self.listView.setIconSize(size)
def paginationLoadNextItems(self):
"""当垂直滑块达到最大值时调用模型 loadData 方法的简单方法,对加载模型的下一页数据很有用。"""
if not self.pagination:
return
model = self.listView.model()
if model is None:
return
vbar = self.listView.verticalScrollBar()
sliderMax = vbar.maximum()
sliderPos = vbar.sliderPosition()
if sliderPos == sliderMax:
indexes = self.listView.selectionModel().selection().indexes()
model.loadData()
# Reselect selection
self.listView.setAutoScroll(False)
[self.listView.selectionModel().setCurrentIndex(index, QItemSelectionModel.SelectCurrent) for index
in indexes]
self.listView.setAutoScroll(True)
def contextMenu(self, items, menu):
pass
def onSelectionChanged(self):
index = self.listView.currentIndex()
model = self.listView.model()
if model is not None:
item = model.itemFromIndex(index)
model.onSelectionChanged(index, item)
self.requestSelectionChanged.emit(index, item)
class SettingObject(dict):
"""设置类,用于封装给定设置的 json 数据"""
def __init__(self, root, relativePath=None, **kwargs):
relativePath = relativePath
if not relativePath.suffix:
relativePath = relativePath.with_suffix(".json")
kwargs["relativePath"] = relativePath
kwargs["root"] = root
super(SettingObject, self).__init__(**kwargs)
def rootPath(self):
if self.root:
return self.root
return
def path(self):
return self.root / self["relativePath"]
def isValid(self):
if self.root is None:
return False
elif (self.root / self.relativePath).exists():
return True
return False
def __repr__(self):
return "<{}> root: {}, path: {}".format(self.__class__.__name__, self.root, self.relativePath)
def __cmp__(self, other):
return self.name == other and self.version == other.version
def __getattr__(self, item):
try:
return self[item]
except KeyError:
return super(SettingObject, self).__getattribute__(item)
def __setattr__(self, key, value):
self[key] = value
def save(self, indent=False, sort=False):
"""将文件作为 json 保存到磁盘
:param indent: 如果为 True,则很好地格式化 json(缩进 = 2)
:type indent: bool
:return fullPath: 保存的 .json 文件的完整路径
:rtype fullPath: str
"""
root = self.root
if not root:
return
fullPath = root / self.relativePath
ensureFolderExists(str(fullPath.parent))
output = copy.deepcopy(self)
del output["root"]
del output["relativePath"]
exts = fullPath.suffix
if not exts:
fullPath = fullPath.with_suffix("json")
if not indent:
saveJson(output, str(fullPath), sort_keys=sort)
else:
saveJson(output, str(fullPath), indent=2, sort_keys=sort)
return self.path()
class ItemSignals(QObject):
"""
定义正在运行的工作线程中可用的信号。
支持的信号包括:
完成:无数据
错误:`tuple` (exctype, value, traceback.format_exc() )
处理结果:`object` 从处理中返回的数据,任何东西
处理进度:`int` 指示进度百分比
"""
updated = Signal(object)
finished = Signal()
error = Signal(tuple)
result = Signal(object)
progress = Signal(int)
class TreeItem(QStandardItem):
backgroundColor = QColor(70, 70, 80)
backgroundColorSelected = QColor(50, 180, 240)
backgroundColorHover = QColor(50, 180, 150)
textColorSelected = QColor(255, 255, 255)
textColor = QColor(255, 255, 255)
textBGColor = QColor(0, 0, 0)
backgroundBrush = QBrush(backgroundColor)
backgroundColorSelectedBrush = QBrush(backgroundColorSelected)
backgroundColorHoverBrush = QBrush(backgroundColorHover)
borderColorSelected = QColor(0, 0, 0)
borderColorHover = QColor(0, 0, 0)
borderColor = QColor(0, 0, 0)
backgroundColorIcon = QColor(50, 50, 50)
def __init__(self, item, parent=None, themePref=None, squareIcon=False):
super(TreeItem, self).__init__(parent=parent)
self.currentTheme = ""
self.padding = 0
self.textHeight = 11
self.borderWidth = 1
self.textPaddingH = 7
self.textPaddingV = 2
self.showText = True
self._item = item
self._pixmap = None
self.iconSize = QSize(256, 256)
self.loaderThread = ThreadedIcon(item.iconPath)
self.setEditable(False)
self.aspectRatio = Qt.KeepAspectRatioByExpanding
self.themePref = themePref
self.squareIcon = squareIcon
self.setBorderWidth(1)
if themePref is not None:
self.updateTheme()
def updateTheme(self):
self.currentTheme = self.themePref.currentTheme()
self.textHeight = self.themePref.DEFAULT_FONTSIZE
self.borderWidth = self.themePref.ONE_PIXEL
self.textPaddingH = dpiScale(7)
self.textPaddingV = dpiScale(3)
self.backgroundColorSelected = QColor(*self.themePref.BROWSER_SELECTED_COLOR)
self.backgroundColor = QColor(*self.themePref.BROWSER_BG_COLOR)
self.backgroundBrush = QBrush(self.backgroundColor)
self.backgroundColorHover = self.backgroundColor
self.backgroundColorHoverBrush = QBrush(self.backgroundColorHover)
self.backgroundColorSelectedBrush = QBrush(self.backgroundColorSelected)
self.backgroundColorIcon = QColor(*self.themePref.BROWSER_ICON_BG_COLOR)
self.borderColor = QColor(self.backgroundColor)
self.textColorSelected = QColor(*self.themePref.TBL_TREE_ACT_TEXT_COLOR)
self.borderColorSelected = QColor(*self.themePref.BROWSER_SELECTED_COLOR)
self.borderColorHover = QColor(*self.themePref.BROWSER_BORDER_HOVER_COLOR)
self.textColor = QColor(*self.themePref.BROWSER_TEXT_COLOR)
self.textBGColor = QColor(*self.themePref.BROWSER_TEXT_BG_COLOR)
def item(self):
return self._item
def applyFromImage(self, image):
pixmap = QPixmap()
pixmap = pixmap.fromImage(image)
self._pixmap = pixmap
self.model().dataChanged.emit(self.index(), self.index())
def setIconPath(self, iconPath):
self._item.iconPath = iconPath
self._pixmap = QPixmap(iconPath)
def pixmap(self):
"""
:return:
:rtype: QPixmap
"""
if not self._pixmap.isNull():
return self._pixmap
elif not os.path.exists(self._item.iconPath):
return QPixmap()
return self._pixmap
def toolTip(self):
return self._item.description()
def isEditable(self, *args, **kwargs):
return False
def sizeHint(self):
# todo: could be done better
sizeHint = self.model().view.iconSize()
if self._pixmap:
pxSize = self._pixmap.rect().size()
else:
pxSize = QSize(1, 1)
size = min(sizeHint.height(), sizeHint.width())
pxSize = QSize(128, 128) if pxSize == QSize(0, 0) else pxSize
if self.squareIcon:
aspectRatio = 1
else:
aspectRatio = float(pxSize.width()) / float(pxSize.height())
if aspectRatio <= 1:
sizeHint.setWidth(size * aspectRatio)
else:
sizeHint.setWidth(size * aspectRatio)
sizeHint.setHeight(size + 1)
if self.showText:
sizeHint.setHeight(sizeHint.height() + self.textHeight + self.textPaddingV * 2)
return sizeHint
def font(self, index):
return QFont("Roboto")
def textAlignment(self, index):
return Qt.AlignLeft | Qt.AlignBottom
def iconAlignment(self, index):
return Qt.AlignLeft | Qt.AlignVCenter
def removeRow(self, item):
if item.loaderThread.isRunning:
item.loaderThread.wait()
return super(TreeItem, self).removeRow(item)
def removeRows(self, items):
for item in items:
if item.loaderThread.isRunning:
item.loaderThread.wait()
return super(TreeItem, self).removeRows(items)
def isSelected(self, option):
return option.state & QStyle.State_Selected
def isMouseOver(self, option):
return option.state & QStyle.State_MouseOver
def setBorderWidth(self, width):
self.borderWidth = dpiScale(width)
def paint(self, painter, option, index):
painter.save()
self._paintBackground(painter, option, index)
if self.showText:
self._paintText(painter, option, index)
if self._pixmap is not None:
self._paintIcon(painter, option, index)
painter.restore()
def _paintIcon(self, painter, option, index):
"""
:param painter:
:type painter: QPainter
:param option:
:type option:
:param index:
:type index:
:return:
:rtype:
"""
rect = self.iconRect(option)
pixmap = self.pixmap() # type: QPixmap
if pixmap.isNull():
return
pixmap = pixmap.scaled(
rect.width() - self.borderWidth * 2,
rect.height() - self.borderWidth * 2,
self.aspectRatio,
Qt.SmoothTransformation,
)
pixmapRect = QRect(rect)
pixmapRect.setWidth(pixmap.width())
pixmapRect.setHeight(pixmap.height())
aspectRatio = float(pixmap.width()) / float(pixmap.height())
iconAlign = self.iconAlignment(None)
if iconAlign & Qt.AlignHCenter == Qt.AlignHCenter:
x = float(rect.width() - pixmap.width()) * 0.5
elif iconAlign & Qt.AlignLeft == Qt.AlignLeft:
x = 0
else: # todo: set the rest of the flags
x = float(rect.width() - pixmap.width()) * 0.5
print("Flags not set for TreeItem._paintIcon()! x-Value")
if iconAlign & Qt.AlignVCenter == Qt.AlignVCenter:
y = float(rect.height() - pixmap.height()) * 0.5
else: # todo: set the rest of the flags
y = float(rect.height() - pixmap.height()) * 0.5
print("Flags not set for TreeItem._paintIcon() y-Value! ")
x += self.borderWidth
pixmapRect.translate(x, y)
# Hacky fixes to visuals =[
if self.squareIcon:
clippedRect = QRect(pixmapRect)
clippedRect.setWidth(clippedRect.width() - 1)
if clippedRect.height() <= clippedRect.width(): # Wide icons
translate = (clippedRect.width() - clippedRect.height()) / 2
clippedRect.setWidth(clippedRect.height() - 1)
pixmapRect.translate(-translate, 0)
else: # Tall Icons
translate = (clippedRect.height() - clippedRect.width()) / 2
clippedRect.setHeight(clippedRect.width() + 2)
clippedRect.setWidth(clippedRect.width())
clippedRect.translate(0, translate)
painter.setClipRect(clippedRect)
else:
if aspectRatio > 1:
pixmapRect.setWidth(pixmapRect.width())
elif aspectRatio >= 1:
pixmapRect.setWidth(pixmapRect.width() - 1)
painter.drawPixmap(pixmapRect, pixmap)
def _paintText(self, painter, option, index):
isSelected = self.isSelected(option)
isMouseOver = self.isMouseOver(option)
text = self._item.name
color = self.textColorSelected if isSelected else self.textColor
rect = QRect(option.rect)
width = rect.width() - self.textPaddingH * 2
height = rect.height()
padding = self.padding
x, y = padding, padding
rect.translate(x + self.textPaddingH, y + self.textPaddingV)
rect.setWidth(width - padding)
rect.setHeight(height - padding - self.textPaddingV)
font = self.font(index)
font.setPixelSize(self.textHeight)
align = self.textAlignment(index)
metrics = QFontMetricsF(font)
textWidth = metrics.width(text)
# does the text fit? if not cut off the text
if textWidth > rect.width() - padding:
text = metrics.elidedText(text, Qt.ElideRight, rect.width())
if isSelected:
textBgColor = self.borderColorSelected
else:
textBgColor = self.textBGColor
textBg = QRect(option.rect)
textBg.setTop(textBg.top() + textBg.height() - (self.textHeight + self.textPaddingV * 2))
textBg.translate(max(1, self.borderWidth * 0.5), max(1, self.borderWidth * 0.5) - 2)
textBg.setWidth(textBg.width() - self.borderWidth * 2 - 1)
textBg.setHeight(self.textHeight + self.textPaddingV)
painter.setBrush(textBgColor)
painter.setPen(textBgColor)
painter.drawRect(textBg)
pen = QPen(color)
painter.setPen(pen)
painter.setFont(font)
painter.drawText(rect, align, text)
def _paintBackground(self, painter, option, index):
isSelected = self.isSelected(option)
isMouseOver = self.isMouseOver(option)
if isSelected:
brush = self.backgroundColorSelectedBrush
if isMouseOver:
borderColor = self.borderColorHover
else:
borderColor = self.borderColorSelected
elif isMouseOver:
brush = self.backgroundColorHoverBrush
borderColor = self.borderColorHover
else:
brush = self.backgroundBrush
borderColor = self.borderColor
pen = QPen(borderColor)
pen.setJoinStyle(Qt.MiterJoin)
pen.setWidth(self.borderWidth)
painter.setPen(pen)
rect = QRect(option.rect)
rect.setWidth(rect.width() - self.borderWidth)
rect.setHeight(rect.height() - self.borderWidth)
rect.translate(self.borderWidth * 0.5, self.borderWidth * 0.5)
painter.setBrush(brush)
painter.drawRect(rect)
# Icon background
iconPen = QPen(self.backgroundColorIcon)
iconPen.setWidth(0)
iconRect = QRect(rect)
painter.setBrush(QBrush(self.backgroundColorIcon))
iconRect.setHeight(iconRect.height() - (self.textHeight + self.textPaddingV * 2))
iconRect.translate(max(1, self.borderWidth * 0.5), max(1, self.borderWidth * 0.5))
iconRect.setWidth(iconRect.width() - self.borderWidth * 2)
painter.setPen(iconPen)
painter.drawRect(iconRect)
def iconRect(self, option):
padding = self.padding
rect = option.rect
width = rect.width() - padding
height = rect.height() - padding
# deal with the text #
if self.showText:
height -= self.textHeight + self.textPaddingV * 2
rect.setWidth(width)
rect.setHeight(height)
x = padding + (float(width - rect.width()) * 0.5)
y = padding + (float(height - rect.height()) * 0.5)
rect.translate(x, y)
return rect
class ThreadedIcon(QRunnable):
def __init__(self, iconPath, width=None, height=None, *args, **kwargs):
super(ThreadedIcon, self).__init__(*args, **kwargs)
self.signals = ItemSignals()
# Add the callback to our kwargs
kwargs['progress_callback'] = self.signals.progress
self._path = iconPath
self.width = width
self.height = height
self.placeHolderImage = QImage(50, 50, QImage.Format_ARGB32)
self.placeHolderImage.fill(qRgb(96, 96, 96))
self._finished = False
def finished(self, state):
self._finished = state
self.signals.finished.emit()
def isFinished(self):
return self._finished
@Slot()
def run(self):
if not self._path or self._finished:
return
self.signals.updated.emit(self.placeHolderImage)
try:
image = QImage(self._path)
except Exception as er:
self.signals.error.emit((er,))
self.finished(True)
return
self.signals.updated.emit(image)
self.finished(True)
class BaseItem(object):
def __init__(self, name=None, description=None, iconPath=None):
self.name = name or ""
self.iconPath = iconPath or ""
self._description = description or ""
self.metadata = {}
self.user = ""
self.iconThread = None # type: ThreadedIcon
def description(self):
return self._description
def iconLoaded(self):
if self.iconThread is not None and self.iconThread.isFinished():
return True
return False
def tags(self):
return self.metadata.get("metadata", {}).get("tags", [])
def hasTag(self, tag):
for i in self.tags:
if tag in i:
return True
return False
def hasAnyTags(self, tags):
for i in tags:
if self.hasTag(i):
return True
return False
def serialize(self):
return {"metadata": {"time": "",
"version": "",
"user": "",
"name": self.name,
"application": {"name": "",
"version": ""},
"description": "",
"tags": []},
}
class FileViewModel(QStandardItemModel):
parentClosed = Signal(bool)
doubleClicked = Signal(str)
selectionChanged = Signal(str)
refreshRequested = Signal()
def __init__(self, view, directory=None, extensions=None, chunkCount=20, uniformIcons=False):
""" 覆盖基本模型以处理缩略图视图小部件数据的加载,例如。特定文件/图像
此类是附加到缩略图视图的缩略图模型的最基本形式,给定并填充有“png”、“jpg”或“jpeg”图像的目录。
工具提示也是从文件名生成的,可以进一步覆盖此类,以便在子目录(如 .zooScene 文件)中加载自定义图像。
:param view: The viewer to assign this model data?
:type view: thumbwidget.ThumbListView
:param directory: The directory full path where the .zooScenes live
:type directory: str
:param chunkCount: The number of images to load at a time into the ThumbnailView widget
:type chunkCount: int
:param extensions: The image file extensions to override, otherwise will be ["png", "jpg", "jpeg"]
:type extensions: list of basestring
:param uniformIcons: False keeps the images non-square. True the icons will be square, clipped on longest axis
:type uniformIcons: bool
"""
super(FileViewModel, self).__init__(parent=view)
self.view = view
self.extensions = extensions
self.chunkCount = chunkCount
self.currentFilesList = [] # the files loaded in the viewer, empty while initializing
self.infoDictList = None # usually used for generating tooltips
self.toolTipList = None # list of tooltips to match each images
self.currentImage = None # the current image name selected (no image highlighted on creation)
self.fileNameList = None # each file's name with an extension
self.fileNameListNoExt = None # each file's name without an extension, for display
self.directory = None
self.themePref = None
self.threadPool = QThreadPool.globalInstance()
self.lastFilter = ""
self.uniformIcons = uniformIcons
self.dynamicLoading = True # Dynamic icon loading
# load the images
if directory is not None:
self.setDirectory(directory, True)
def setDirectory(self, directory, refresh=True):
"""用于设置或更改目录"""
self.directory = directory
if refresh:
self.refreshList()
def refreshList(self):
""" 如果内容已修改,则刷新图标列表,不更改根目录 """
self.view.setUpdatesEnabled(False)
self.clear()
self.refreshModelData()
self.loadData()
self.view.setUpdatesEnabled(True)
def _thumbnails(self, extensions=None):
""" 获取文件缩略图的完整路径名
在这种情况下,缩略图是文件(图像)本身
["C:/aPath/image01.jpg", "C:/aPath/image02.jpg"]
如果您需要从其他位置获取缩略图,即 .zooScene 或 .hdr 文件的缩略图,请覆盖此设置
:type extensions: 文件扩展名列表或设置为“None”以使用所有图像
:return: List of paths to image files
:rtype: list of basestring
"""
results = []
for i in self.fileNameList:
fullPath = os.path.join(self.directory, i)
if extensions is None and isImage(fullPath):
results.append(fullPath)
else:
# todo: do extensions
pass
return results
def _infoDictionaries(self):
"""重写基类,返回每个 .zooScene 文件的信息字典列表。这些词典包含作者、标签、描述等信息
:return infoDictList: A list of info dictionaries for each .zooScene file
:rtype infoDictList: list(dict)
"""
return infoDictionaries(self.fileNameList, self.directory)
def _toolTips(self):
"""重写基类将_toolTips生成为列表,由 self.infoDictList 构造而成"""
self.toolTipList = list()
for i, thumb in enumerate(self.currentFilesList):
self.toolTipList.append(self._createToolTip(self.infoDictList[i], self.fileNameList[i]))
def _createToolTip(self, infoDictSingle, fileName):
"""重写基类,从单个 zooScene 文件创建单个工具提示
:param zooScenePath: The full path to the zooScene image
:type zooScenePath: str
:param infoDictSingle: The information dictionary with tag author and description information
:type infoDictSingle: str
:return toolTip: The tooltip description
:rtype toolTip: str
"""
if not infoDictSingle: # dict is none so return file path as tooltip
return os.path.join(self.directory, fileName)
# breaks on some unicode so convert to plain text
filepath = "{} ".format(os.path.join(self.directory, fileName))
info = ""
website = ""
toolTip = ("{}{}{} ".format(info, filepath, website))
return toolTip
def clear(self):
"""清除模型中的图像和数据,通常在刷新时使用"""
# remove any threads that haven't started yet
self.threadPool.clear()
while not self.threadPool.waitForDone():
continue
# clear any items, this is necessary to get python to GC alloc memory
self.items = []
self.loadedCount = 0
self.currentFilesList = []
super(FileViewModel, self).clear()
def fileList(self):
"""更新self.fileNameList ,可以覆盖
"""
if not self.extensions:
self.extensions = ["png", "jpg", "jpeg"]
if not isinstance(self.extensions, list):
raise ValueError("Extensions must be list, \"{}\" type given \"{}\" ".format(type(self.extensions),
self.extensions))
self.fileNameList = filesByExtension(self.directory, self.extensions)
def filterList(self, text, tag=None):
""" 返回在搜索中显示或隐藏的行的整数列表
:param text:
:type text:
:param tag:
:type tag: 字符串或列表
:return:
:rtype:
"""
filterList = list()
if isinstance(tag, string_types):
tag = [tag]
for i in range(len(self.infoDictList)):
for t in tag:
if t is None or t == "filename":
for j, fileName in enumerate(self.fileNameListNoExt):
if text.lower() in fileName.lower():
filterList.append(j)
else:
if text.lower() in self.infoDictList[i][t.lower()].lower():
filterList.append(i)
for i in range(len(self.fileNameListNoExt)):
self.view.listView.setRowHidden(i, i not in filterList) # Show row if not in filterList
self.lastFilter = text
return filterList
def refreshModelData(self):
"""Needs to create/recreate the thumbnail lists, tooltips and infoDict data
"""
self.fileList() # updates self.fileNameList
self.fileNameListNoExt = [os.path.splitext(preset)[0] for preset in self.fileNameList]
self.currentFilesList = self._thumbnails()
self.infoDictList = self._infoDictionaries()
self._toolTips() # generates self.toolTipList
self.refreshRequested.emit()
def lazyLoadFilter(self):
"""Breaks up the lists self.currentFilesList, self.fileNameList, self.toolTipList for lazy loading.
Can be overridden, usually to choose if the display names should have extensions or not
Default is no extensions on display names
:return filesToLoad: The file name fullpath list (with extensions)
:rtype filesToLoad: list of basestring
:return namesToLoad: The name of the item, will be the label under the image, usually extension removed
:rtype namesToLoad: list of basestring
:return tooltipToLoad: The toolTip list to load
:rtype tooltipToLoad: list of basestring
"""
if len(self.currentFilesList) < self.loadedCount:
filesToLoad = self.currentFilesList
namesToLoad = self.fileNameListNoExt
tooltipToLoad = self.toolTipList
else:
filesToLoad = self.currentFilesList[self.loadedCount: self.loadedCount + self.chunkCount]
namesToLoad = self.fileNameListNoExt[self.loadedCount: self.loadedCount + self.chunkCount]
tooltipToLoad = self.toolTipList[self.loadedCount: self.loadedCount + self.chunkCount]
return filesToLoad, namesToLoad, tooltipToLoad
def loadData(self):
""" Overridden method that prepares the images for loading and viewing.
Is filtered first via self.lazyLoadFilter()
From base class documentation:
Lazy loading happens either on first class initialization and any time the vertical bar hits the max value,
we then grab the current the new file chunk by files[self.loadedCount: loadedCount + self.chunkCount] that
way we are only loading a small amount at a time.
Since this is an example of how to use the method , you can approach it in any way you wish but for each item you
add you must initialize a item.BaseItem() or custom subclass and a item.treeItem or subclass which handles the
qt side of the data per item
"""
if self.lastFilter != "": # Don't load any new data if theres a search going on
return
filesToLoad, namesToLoad, tooltipToLoad = self.lazyLoadFilter()
# Load the files
for i, f in enumerate(filesToLoad):
if f is None:
f = r"C:\Users\mirror\Documents\zoo_preferences\assets\model_assets\scene_lightSetup_empty_fileDependencies\thumbnail.jpg"
workerThread = ThreadedIcon(iconPath=f)
# create an item for the image type
it = BaseItem(name=namesToLoad[i], iconPath=f, description=tooltipToLoad[i])
qitem = self.createItem(item=it)
workerThread.signals.updated.connect(partial(self.setItemIconFromImage, qitem))
self.parentClosed.connect(workerThread.finished)
it.iconThread = workerThread
if self.dynamicLoading is False:
self.threadPool.start(workerThread)
self.loadedCount += 1
if len(filesToLoad) > 0:
pass
def createItem(self, item):
"""自定义包装器 创建 ::类项的方法。TreeItem',将其添加到模型项和类 appendRow()
:param item:
:type item: ::class:`baseItem`
:return:
:rtype: ::class:`items.TreeItem`
"""
tItem = TreeItem(item=item, themePref=self.themePref, squareIcon=self.uniformIcons)
self.items.append(tItem)
self.appendRow(tItem)
return tItem
def setUniformItemSizes(self, enabled):
self.uniformIcons = enabled
for it in self.items:
it.squareIcon = enabled
def setItemIconFromImage(self, item, image):
"""由线程调用的自定义方法
:param item:
:type item: :class:`TreeItem`
:param image: The Loaded QImage
:type image: QImage
"""
item.applyFromImage(image)
def doubleClickEvent(self, modelIndex, item):
"""双击项时由列表视图调用
:param modelIndex:
:type modelIndex: QModelIndex
:param item:
:type item: TreeItem
:return self.currentImage: The current image with it's name and file extension
:rtype self.currentImage: str
"""
self.currentImage = item._item.name
self.doubleClicked.emit(self.currentImage)
return self.currentImage
def onSelectionChanged(self, modelIndex, item):
"""在更改项目时由列表视图调用,例如左键单击或右键单击
:param modelIndex:
:type modelIndex: QModelIndex
:param item:
:type item: TreeItem
"""
try: # can error while renaming files and the change no longer exists so ignore if so
self.currentImage = item._item.name
self.selectionChanged.emit(self.currentImage)
return self.currentImage
except AttributeError:
pass
def closeEvent(self, event):
"""关闭模型
:param event:
:type event:
"""
if self.qModel is not None:
self.qModel.clear()
self.qModel.parentClosed.emit(True)
super(FileViewModel, self).closeEvent(event)
class ZooSceneViewerModel(FileViewModel):
# Emit signals
parentClosed = Signal(bool)
doubleClicked = Signal(str)
selectionChanged = Signal(str)
def __init__(self, view, directory="", chunkCount=20, uniformIcons=False):
"""从缩略图视图小组件的目录中加载 .zooscene 模型数据,拉取依赖目录中的缩略图,并从 file.zooInfo 文件生成工具提示。
另请参阅类 FileViewModel() 以获取更多功能和文档。其可以进一步覆盖此类以在子目录中加载自定义图像,例如Skydomes或控件
使用 .zooScene 标签和缩略图信息。
:param view: 查看器分配此模型数据
:type view: qtWidget?
:param directory: The directory full path where the .zooScenes live
:type directory: str
:param chunkCount: The number of images to load at a time into the ThumbnailView widget
:type chunkCount: int
"""
super(ZooSceneViewerModel, self).__init__(view, directory=directory, chunkCount=chunkCount,
uniformIcons=uniformIcons)
def _thumbnails(self):
"""重写基类,从每个文件的子目录“fileName_fileDependencies”返回缩略图路径的列表:
["C:/aPath/scene01_fileDependencies/thumbnail.jpg", "C:/aPath/scene02_fileDependencies/thumbnail.jpg"]
:return 当前文件列表 list(str):
:return 缩略图列表 list(str):
"""
return ["C:/aPath/scene01_fileDependencies/thumbnail.jpg",
"C:/aPath/scene02_fileDependencies/thumbnail.jpg"] # thumbnails(self.directory, self.fileNameListNoExt)
def fileList(self):
"""
重写基类 生成文件列表:
[“scene01.zooScene”, “scene02.zooScene”]
"""
self.fileNameList = ["scene01.zooScene", "scene02.zooScene"] # filesByExtension(self.directory, [ZOOSCENE_EXT])
if __name__ == "__main__":
app = QApplication(sys.argv)
thumbViewer = ThumbnailViewWidget(parent=None, columns=3, fixedHeight=382, uniformIcons=True)
thumbModel = ZooSceneViewerModel(thumbViewer, directory=r"C:\Users\mirror\Documents\zoo_preferences\assets\model_assets",
chunkCount=200,
uniformIcons=True)
thumbViewer.setModel(thumbModel)
thumbViewer.show()
sys.exit(app.exec_())
示例结果: