diff --git a/.gitignore b/.gitignore index 9f409dbe..6688f9a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,763 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,qt,qtcreator,visualstudiocode,visualstudio,macos,linux,windows,cmake +# Edit at https://www.toptal.com/developers/gitignore?templates=python,qt,qtcreator,visualstudiocode,visualstudio,macos,linux,windows,cmake + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + +### CMake Patch ### +# External projects +*-prefix/ + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ + +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### Qt ### +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so.* +*.dll +*.dylib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database + +# QtCreator local machine specific files for imported projects +*creator.user* + +*_qmlcache.qrc + +### QtCreator ### +# gitignore for Qt Creator like IDE for pure C/C++ project without Qt +# +# Reference: http://doc.qt.io/qtcreator/creator-project-generic.html + + + +# Qt Creator autogenerated files + + +# A listing of all the files included in the project +*.files + +# Include directories +*.includes + +# Project configuration settings like predefined Macros +*.config + +# Qt Creator settings +*.creator + +# User project settings +*.creator.user* + +# Qt Creator backups + +# Flags for Clang Code Model +*.cxxflags +*.cflags + + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +*.code-workspace + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs + +# JetBrains Rider +*.sln.iml + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/python,qt,qtcreator,visualstudiocode,visualstudio,macos,linux,windows,cmake + # Byte-compiled / optimized / DLL files __pycache__/ *.py[co] diff --git a/Demo/Data/serializewidget.ui b/Demo/Data/serializewidget.ui new file mode 100644 index 00000000..c165780e --- /dev/null +++ b/Demo/Data/serializewidget.ui @@ -0,0 +1,268 @@ + + + SerializeWidget + + + + 0 + 0 + 800 + 600 + + + + TestSerialize + + + + + + Qt::Horizontal + + + false + + + + + 500 + 16777215 + + + + Json View + + + + + + + + + + Widget View + + + + + + 0 + + + + Input + + + + + + + + + CheckBox + + + + + + + RadioButton + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Edit + + + + + + + + + + + + + Correlation + + + + + + Qt::Horizontal + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 0 + + + Qt::Vertical + + + + + + + Qt::Vertical + + + + + + + + View + + + + + + ListView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + TreeView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + TableView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Lib/qpropmapper.py b/Demo/Lib/qpropmapper.py new file mode 100644 index 00000000..9064f108 --- /dev/null +++ b/Demo/Lib/qpropmapper.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/05 +@file: qpropertymapper.py +@description: +""" + +from typing import Any, Union + +from box import Box + +try: + from PyQt5.QtCore import ( + QDateTime, + QMetaProperty, + QObject, + Qt, + ) + from PyQt5.QtCore import ( + pyqtSignal as Signal, + ) + from PyQt5.QtWidgets import QWidget +except ImportError: + from PySide2.QtCore import ( + QDateTime, + QMetaProperty, + QObject, + Qt, + Signal, + ) + from PySide2.QtWidgets import QWidget + + +class DictBox(Box): + def keys(self, dotted: bool = False): + if not dotted: + return super().keys() + + if not self._box_config["box_dots"]: + raise Exception( + "Cannot return dotted keys as this Box does not have `box_dots` enabled" + ) + + keys = set() + for key, value in self.items(): + added = False + if isinstance(key, str): + if isinstance(value, Box): + for sub_key in value.keys(dotted=True): + keys.add(f"{key}.{sub_key}") + added = True + if not added: + keys.add(key) + return sorted(keys, key=lambda x: str(x)) + + +class QPropertyMapper(QObject): + Verbose = False + propertyChanged = Signal(int, str, object) + Signal = ( + "dateTimeChanged", + "currentTextChanged", + "valueChanged", + "toggled", + "textChanged", + ) + Props = ( + "dateTime", + "html", + "plainText", + "currentText", + "checked", + "value", + "text", + ) + + def __init__(self, *args, **kwargs): + data = kwargs.pop("data", {}) + super().__init__(*args, **kwargs) + self._widgetKey = {} + self._keyWidget = {} + self.propertyChanged.connect(self.onPropertyChanged) + self.loadData(data, clear=False) + + def __enter__(self): + self.__log("[__enter__]: block signals") + self.blockSignals(True) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.__log("[__exit__]: unblock signals") + self.blockSignals(False) + + def event(self, ev) -> bool: + if ev.type() == ev.DynamicPropertyChange: + name = bytes(ev.propertyName()).decode() + value = self.property(name) + self.propertyChanged.emit(value is not None, name, value) + return super().event(ev) + + def clear(self): + self.__log("[clear]: clear datas") + with self: + keys = self.dynamicPropertyNames().copy() + for key in keys: + self.setProperty(bytes(key).decode(), None) + # clear binds + + def loadData(self, data: dict, clear: bool = False): + self.__log("[loadData]: load data") + if clear: + self.clear() + data = DictBox(data, default_box=True, box_dots=True) + for key, value in data.items(True): + self.setProperty(str(key), value) + + def toDict(self, raw=True) -> dict: + data = DictBox(default_box=True, box_dots=True) + for key in self.dynamicPropertyNames(): + key = bytes(key).decode() + try: + data[key] = self.property(key) + except Exception as e: + self.__log(f"[toDict]: {e}") + if raw: + return data.to_dict() + return data + + def toJson(self, indent=None, **kwargs) -> str: + return self.toDict(False).to_json(indent=indent, **kwargs) + + def getProperty( + self, widget: QWidget, prop: str = "" + ) -> Union[QMetaProperty, None]: + """获取控件的对应属性 + + Args: + widget (QWidget): 控件对象 + prop (str, optional): 指定的属性. 默认为 "" + + Returns: + Union[QMetaProperty, None]: 返回属性对象 + """ + qmo = widget.metaObject() + props = [prop] if prop else self.Props + + for prop in props: + idx = qmo.indexOfProperty(prop) + if idx > -1: + p = qmo.property(idx) + if p.isReadable() and p.isWritable(): + self.__log( + f"[getProperty]: get prop: {prop} of widget: {self.__widgetInfo(widget)}" + ) + return p + + return None + + def bind(self, widget: QWidget, key: str, default: Any = None, prop: str = ""): + # 1. 记录widget和key + if widget not in self._widgetKey: + self.__log( + f"[bind]: bind key: {key} to widget: {self.__widgetInfo(widget)}" + ) + self._widgetKey[widget] = { + "key": key, + "prop": self.getProperty(widget, prop), + } + # 2. 设置控件的默认值 + with self: + self.__setValue(widget, key, default) + + # 3. 记录所有关联的widget + if key not in self._keyWidget: + self._keyWidget[key] = set() + self._keyWidget[key].add(widget) + + # 4. 绑定widget的相关信号 + for signal in self.Signal: + signal = getattr(widget, signal, None) + if signal: + self.__log( + f"[bind]: connect key: {key}, signal: {signal} of widget: {self.__widgetInfo(widget)}" + ) + signal.connect(self.__setData) + break + + def __log(self, *args): + if self.Verbose: + print(*args) + + def __widgetInfo(self, widget: QWidget) -> Union[str, None]: + try: + return f"<{widget.__class__.__name__}(name={widget.objectName()}) {hex(id(widget))}>" + except Exception: + return None + + def __getDefault(self, widget: QWidget, default: Any = None): + """获取控件的默认值 + + Args: + widget (QWidget): 控件对象 + default (Any, optional): 默认值. 默认为None + + Returns: + Any: 默认值 + """ + if default: + return default + + # 1. 从记录表中获取窗口的属性对象 + prop: Union[QMetaProperty, None] = self._widgetKey[widget]["prop"] + if prop is None: + return None + + # 2. 获取属性值并进行一些转换 + value = prop.read(widget) + if value is not None: + if prop.name() == "dateTime": + return value.toString("yyyy-MM-dd HH:mm:ss") + elif prop.name() == "html" and len(widget.property("plainText")) == 0: + return "" + return value + + return None + + def __setValue( + self, + widget: QWidget, + key: str, + default: Any = None, + updated: bool = False, + ): + value: Any = default + + # 1. 首次同步widget属性值到mapper + if not updated: + value = self.__getDefault(widget, default) + self.__log( + f"[__setValue]: setProperty key: {key}, value: {value} of widget: {self.__widgetInfo(widget)}" + ) + self.setProperty(key, value) + + if value is None: + return + + # 2. 设置widget的属性值 + prop: Union[QMetaProperty, None] = self._widgetKey[widget]["prop"] + if prop is not None: + if prop.name() == "dateTime" and isinstance(value, str): + value = QDateTime.fromString(value, Qt.ISODate) + self.__log( + f"[__setValue]: update key: {key}, value: {value} of widget: {self.__widgetInfo(widget)}" + ) + prop.write(widget, value) + + def __setData(self, *args, **kwargs): + """控件值发生变化时,更新关联控件以及设置mapper数据""" + + # 1. 获取发送者widget关联的key和prop + sender = self.sender() + info = self._widgetKey.get(sender, {}) + key = info.get("key", None) + prop: Union[QMetaProperty, None] = info.get("prop", None) + if key is None or prop is None: + self.__log( + f"[__setData]: sender {self.__widgetInfo(sender)} has no key or prop" + ) + return + + # 2. 读取发送者widget的属性值 + value = prop.read(sender) + if value is not None: + if prop.name() == "dateTime": + value = value.toString("yyyy-MM-dd HH:mm:ss") + elif prop.name() == "html" and len(sender.property("plainText")) == 0: + value = "" + + if value is None: + return + + # 更新关联的key + self.__log( + f"[__setData]: update key: {key}, value: {value} from widget: {self.__widgetInfo(sender)}" + ) + self.setProperty(key, value) + + def onPropertyChanged(self, op: int, key: str, value: Any): + """mapper属性值发生变化时,更新关联的widget + + Args: + op (int): 操作类型(1=添加,0=删除) + key (str): 属性名字 + value (Any): 属性值 + """ + if op != 1: + return + + # 1. 获取mapper中key对应的值 + value = self.property(key) + if value is None: + return + + # 更新关联的widget + for widget in self._keyWidget.get(key, []): + self.__log( + f"[onPropertyChanged]: set key: {key}, value: {value} of widget: {self.__widgetInfo(widget)}" + ) + try: + self.__setValue(widget, key, value, updated=True) + except Exception as e: + self.__log(f"[onPropertyChanged]: __setValue failed: {e}") diff --git a/Demo/Lib/serializewidget.py b/Demo/Lib/serializewidget.py new file mode 100644 index 00000000..10730198 --- /dev/null +++ b/Demo/Lib/serializewidget.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'serializewidget.ui' +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +try: + from PyQt5 import QtCore, QtGui, QtWidgets +except ImportError: + from PySide2 import QtCore, QtGui, QtWidgets + + +class Ui_SerializeWidget(object): + def setupUi(self, SerializeWidget): + SerializeWidget.setObjectName("SerializeWidget") + SerializeWidget.resize(800, 600) + self.verticalLayout = QtWidgets.QVBoxLayout(SerializeWidget) + self.verticalLayout.setObjectName("verticalLayout") + self.splitter = QtWidgets.QSplitter(SerializeWidget) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setChildrenCollapsible(False) + self.splitter.setObjectName("splitter") + self.groupBox = QtWidgets.QGroupBox(self.splitter) + self.groupBox.setMaximumSize(QtCore.QSize(500, 16777215)) + self.groupBox.setObjectName("groupBox") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.groupBox) + self.horizontalLayout.setObjectName("horizontalLayout") + self.editJsonView = QtWidgets.QTextEdit(self.groupBox) + self.editJsonView.setObjectName("editJsonView") + self.horizontalLayout.addWidget(self.editJsonView) + self.groupBox_2 = QtWidgets.QGroupBox(self.splitter) + self.groupBox_2.setObjectName("groupBox_2") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox_2) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.tabWidget = QtWidgets.QTabWidget(self.groupBox_2) + self.tabWidget.setObjectName("tabWidget") + self.tab = QtWidgets.QWidget() + self.tab.setObjectName("tab") + self.gridLayout = QtWidgets.QGridLayout(self.tab) + self.gridLayout.setObjectName("gridLayout") + self.doubleSpinBox = QtWidgets.QDoubleSpinBox(self.tab) + self.doubleSpinBox.setObjectName("doubleSpinBox") + self.gridLayout.addWidget(self.doubleSpinBox, 2, 1, 1, 1) + self.checkBox = QtWidgets.QCheckBox(self.tab) + self.checkBox.setObjectName("checkBox") + self.gridLayout.addWidget(self.checkBox, 0, 1, 1, 1) + self.radioButton = QtWidgets.QRadioButton(self.tab) + self.radioButton.setObjectName("radioButton") + self.gridLayout.addWidget(self.radioButton, 0, 0, 1, 1) + self.spinBox = QtWidgets.QSpinBox(self.tab) + self.spinBox.setObjectName("spinBox") + self.gridLayout.addWidget(self.spinBox, 2, 0, 1, 1) + self.lineEdit = QtWidgets.QLineEdit(self.tab) + self.lineEdit.setObjectName("lineEdit") + self.gridLayout.addWidget(self.lineEdit, 5, 0, 1, 2) + self.dateTimeEdit = QtWidgets.QDateTimeEdit(self.tab) + self.dateTimeEdit.setObjectName("dateTimeEdit") + self.gridLayout.addWidget(self.dateTimeEdit, 4, 0, 1, 2) + self.comboBox = QtWidgets.QComboBox(self.tab) + self.comboBox.setObjectName("comboBox") + self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1) + self.timeEdit = QtWidgets.QTimeEdit(self.tab) + self.timeEdit.setObjectName("timeEdit") + self.gridLayout.addWidget(self.timeEdit, 3, 0, 1, 1) + self.dateEdit = QtWidgets.QDateEdit(self.tab) + self.dateEdit.setObjectName("dateEdit") + self.gridLayout.addWidget(self.dateEdit, 3, 1, 1, 1) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem, 0, 3, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem1, 6, 0, 1, 1) + self.tabWidget.addTab(self.tab, "") + self.tab_2 = QtWidgets.QWidget() + self.tab_2.setObjectName("tab_2") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.tab_2) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.plainTextEdit = QtWidgets.QPlainTextEdit(self.tab_2) + self.plainTextEdit.setObjectName("plainTextEdit") + self.verticalLayout_4.addWidget(self.plainTextEdit) + self.textEdit = QtWidgets.QTextEdit(self.tab_2) + self.textEdit.setObjectName("textEdit") + self.verticalLayout_4.addWidget(self.textEdit) + self.tabWidget.addTab(self.tab_2, "") + self.tab_3 = QtWidgets.QWidget() + self.tab_3.setObjectName("tab_3") + self.gridLayout_2 = QtWidgets.QGridLayout(self.tab_3) + self.gridLayout_2.setObjectName("gridLayout_2") + self.horizontalSlider = QtWidgets.QSlider(self.tab_3) + self.horizontalSlider.setOrientation(QtCore.Qt.Horizontal) + self.horizontalSlider.setObjectName("horizontalSlider") + self.gridLayout_2.addWidget(self.horizontalSlider, 1, 0, 1, 1) + self.spinBox_2 = QtWidgets.QSpinBox(self.tab_3) + self.spinBox_2.setObjectName("spinBox_2") + self.gridLayout_2.addWidget(self.spinBox_2, 0, 0, 1, 1) + spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_2.addItem(spacerItem2, 2, 0, 1, 1) + self.progressBar = QtWidgets.QProgressBar(self.tab_3) + self.progressBar.setProperty("value", 0) + self.progressBar.setOrientation(QtCore.Qt.Vertical) + self.progressBar.setObjectName("progressBar") + self.gridLayout_2.addWidget(self.progressBar, 0, 1, 3, 1) + self.verticalSlider = QtWidgets.QSlider(self.tab_3) + self.verticalSlider.setOrientation(QtCore.Qt.Vertical) + self.verticalSlider.setObjectName("verticalSlider") + self.gridLayout_2.addWidget(self.verticalSlider, 0, 2, 3, 1) + self.tabWidget.addTab(self.tab_3, "") + self.tab_4 = QtWidgets.QWidget() + self.tab_4.setObjectName("tab_4") + self.gridLayout_3 = QtWidgets.QGridLayout(self.tab_4) + self.gridLayout_3.setObjectName("gridLayout_3") + self.groupBox_3 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_3.setObjectName("groupBox_3") + self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.groupBox_3) + self.verticalLayout_6.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_6.setObjectName("verticalLayout_6") + self.listView = QtWidgets.QListView(self.groupBox_3) + self.listView.setObjectName("listView") + self.verticalLayout_6.addWidget(self.listView) + self.gridLayout_3.addWidget(self.groupBox_3, 0, 0, 1, 1) + self.groupBox_4 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_4.setObjectName("groupBox_4") + self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.groupBox_4) + self.verticalLayout_7.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_7.setObjectName("verticalLayout_7") + self.treeView = QtWidgets.QTreeView(self.groupBox_4) + self.treeView.setObjectName("treeView") + self.verticalLayout_7.addWidget(self.treeView) + self.gridLayout_3.addWidget(self.groupBox_4, 0, 1, 1, 1) + self.groupBox_5 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_5.setObjectName("groupBox_5") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.groupBox_5) + self.verticalLayout_5.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.tableView = QtWidgets.QTableView(self.groupBox_5) + self.tableView.setObjectName("tableView") + self.verticalLayout_5.addWidget(self.tableView) + self.gridLayout_3.addWidget(self.groupBox_5, 1, 0, 1, 2) + self.tabWidget.addTab(self.tab_4, "") + self.verticalLayout_3.addWidget(self.tabWidget) + self.verticalLayout.addWidget(self.splitter) + + self.retranslateUi(SerializeWidget) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(SerializeWidget) + + def retranslateUi(self, SerializeWidget): + _translate = QtCore.QCoreApplication.translate + SerializeWidget.setWindowTitle(_translate("SerializeWidget", "TestSerialize")) + self.groupBox.setTitle(_translate("SerializeWidget", "Json View")) + self.groupBox_2.setTitle(_translate("SerializeWidget", "Widget View")) + self.checkBox.setText(_translate("SerializeWidget", "CheckBox")) + self.radioButton.setText(_translate("SerializeWidget", "RadioButton")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("SerializeWidget", "Input")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("SerializeWidget", "Edit")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("SerializeWidget", "Correlation")) + self.groupBox_3.setTitle(_translate("SerializeWidget", "ListView")) + self.groupBox_4.setTitle(_translate("SerializeWidget", "TreeView")) + self.groupBox_5.setTitle(_translate("SerializeWidget", "TableView")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), _translate("SerializeWidget", "View")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + SerializeWidget = QtWidgets.QWidget() + ui = Ui_SerializeWidget() + ui.setupUi(SerializeWidget) + SerializeWidget.show() + sys.exit(app.exec_()) diff --git a/Demo/README.md b/Demo/README.md index 175dd969..aa260d63 100644 --- a/Demo/README.md +++ b/Demo/README.md @@ -25,8 +25,10 @@ - [动态忙碌光标](#22动态忙碌光标) - [屏幕变动监听](#23屏幕变动监听) - [无边框窗口](#24无边框窗口) + - [属性绑定](#25属性绑定) ## 1、重启窗口Widget + [运行 RestartWindow.py](RestartWindow.py) 利用类变量对窗口的变量进行引用,防止被回收(导致窗口一闪而过),重启时先显示新窗口后关闭自己 @@ -34,6 +36,7 @@ ![RestartWindow](ScreenShot/RestartWindow.gif) ## 2、简单的窗口贴边隐藏 + [运行 WeltHideWindow.py](WeltHideWindow.py) 1. 大概思路 @@ -52,6 +55,7 @@ ![WeltHideWindow](ScreenShot/WeltHideWindow.gif) ## 3、嵌入外部窗口 + [运行 EmbedWindow.py](EmbedWindow.py) 1. 使用`SetParent`函数设置外部窗口的`parent`为Qt的窗口 @@ -61,8 +65,8 @@ ![EmbedWindow](ScreenShot/EmbedWindow.gif) - ## 4、简单跟随其它窗口 + [运行 FollowWindow.py](FollowWindow.py) 1. 利用win32gui模块获取目标窗口的句柄 @@ -72,8 +76,8 @@ ![FollowWindow](ScreenShot/FollowWindow.gif) - ## 5、简单探测窗口和放大截图 + [运行 ProbeWindow.py](ProbeWindow.py) 1. 利用`win32gui`模块获取鼠标所在位置的窗口大小(未去掉边框)和rgb颜色 @@ -81,8 +85,8 @@ ![ProbeWindow](ScreenShot/ProbeWindow.gif) - ## 6、无边框自定义标题栏窗口 + [运行 FramelessWindow.py](FramelessWindow.py) | [运行 NativeEvent.py](NativeEvent.py) 1. 重写鼠标事件 @@ -100,32 +104,38 @@ ![FramelessWindow](ScreenShot/FramelessWindow.gif) ## 7、右下角弹出框 + [运行 WindowNotify.py](WindowNotify.py) | [查看 notify.ui](Data/notify.ui) ![WindowNotify](ScreenShot/WindowNotify.gif) ## 8、程序重启 + [运行 AutoRestart.py](AutoRestart.py) ![AutoRestart](ScreenShot/AutoRestart.gif) ## 9、自定义属性 + [运行 CustomProperties.py](CustomProperties.py) ![CustomProperties](ScreenShot/CustomProperties.png) ## 10、调用截图DLL + [运行 ScreenShotDll.py](ScreenShotDll.py) ![ScreenShotDll](ScreenShot/ScreenShotDll.gif) ## 11、单实例应用 + [运行 SingleApplication.py](SingleApplication.py) | [运行 SharedMemory.py](SharedMemory.py) 1. QSharedMemory 2. QLocalSocket, QLocalServer ## 12、简单的右下角气泡提示 + [运行 BubbleTips.py](BubbleTips.py) 1. 使用 `QWidget` 包含一个 `QLabel`, 其中 `QWidget` 通过 `paintEvent` 绘制气泡形状 @@ -135,11 +145,13 @@ ![BubbleTips](ScreenShot/BubbleTips.gif) ## 13、右侧消息通知栏 + [运行 Notification.py](Notification.py) ![Notification](ScreenShot/Notification.gif) ## 14、验证码控件 + [运行 VerificationCode.py](VerificationCode.py) 1. 更新为paintEvent方式,采用上下跳动 @@ -149,24 +161,26 @@ ![VerificationCode](ScreenShot/VerificationCode.gif) ## 15、人脸特征点 + [运行 FacePoints.py](FacePoints.py) PyQt 结合 Opencv 进行人脸检测; 由于直接在主线程中进行特征点获取,效率比较低 依赖文件 - + 1. [opencv](https://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv) 2. [numpy](https://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy) 3. [dlib](http://dlib.net/) - 1. [dlib-19.4.0.win32-py2.7.exe](Data/dlib-19.4.0.win32-py2.7.exe) - 2. [dlib-19.4.0.win32-py3.4.exe](Data/dlib-19.4.0.win32-py3.4.exe) - 3. [dlib-19.4.0.win32-py3.5.exe](Data/dlib-19.4.0.win32-py3.5.exe) +1. [dlib-19.4.0.win32-py2.7.exe](Data/dlib-19.4.0.win32-py2.7.exe) +2. [dlib-19.4.0.win32-py3.4.exe](Data/dlib-19.4.0.win32-py3.4.exe) +3. [dlib-19.4.0.win32-py3.5.exe](Data/dlib-19.4.0.win32-py3.5.exe) 4. [shape-predictor-68-face-landmarks.dat.bz2](http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2) ![FacePoints](ScreenShot/FacePoints.png) ## 16、使用Threading + [运行 QtThreading.py](QtThreading.py) 在PyQt中使用Theading线程 @@ -174,14 +188,15 @@ PyQt 结合 Opencv 进行人脸检测; ![QtThreading](ScreenShot/QtThreading.gif) ## 17、背景连线动画 + [运行 CircleLine.py](CircleLine.py) 主要参考 [背景连线动画.html](Data/背景连线动画.html) ![CircleLine](ScreenShot/CircleLine.gif) - ## 18、无边框圆角对话框 + [运行 FramelessDialog.py](FramelessDialog.py) 1. 通过设置 `self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)` 和 `self.setAttribute(Qt.WA_TranslucentBackground, True)` 达到无边框和背景透明 @@ -192,6 +207,7 @@ PyQt 结合 Opencv 进行人脸检测; ![FramelessDialog](ScreenShot/FramelessDialog.png) ## 19、调整窗口显示边框 + [运行 ShowFrameWhenDrag.py](ShowFrameWhenDrag.py) 1. 全局设置是【】在控制面板中->调整Windows的外观和性能->去掉勾选 拖动时显示窗口内容】 @@ -204,6 +220,7 @@ PyQt 结合 Opencv 进行人脸检测; ![ShowFrameWhenDrag](ScreenShot/ShowFrameWhenDrag.gif) ## 20、判断信号是否连接 + [运行 IsSignalConnected.py](IsSignalConnected.py) 1. 通过 `isSignalConnected` 判断是否连接 @@ -212,6 +229,7 @@ PyQt 结合 Opencv 进行人脸检测; ![IsSignalConnected](ScreenShot/IsSignalConnected.png) ## 21、调用虚拟键盘 + [运行 CallVirtualKeyboard.py](CallVirtualKeyboard.py) 1. Windows上调用的是`osk.exe` @@ -221,6 +239,7 @@ PyQt 结合 Opencv 进行人脸检测; ![CallVirtualKeyboard2](ScreenShot/CallVirtualKeyboard2.png) ## 22、动态忙碌光标 + [运行 GifCursor.py](GifCursor.py) 通过定时器不停的修改光标图片来实现动态效果 @@ -228,6 +247,7 @@ PyQt 结合 Opencv 进行人脸检测; ![GifCursor](ScreenShot/GifCursor.gif) ## 23、屏幕变动监听 + [运行 ScreenNotify.py](ScreenNotify.py) 通过定时器减少不同的变化信号,尽量保证只调用一次槽函数来获取信息 @@ -235,10 +255,19 @@ PyQt 结合 Opencv 进行人脸检测; ![ScreenNotify](ScreenShot/ScreenNotify.png) ## 24、无边框窗口 + [运行 NewFramelessWindow.py](NewFramelessWindow.py) 1. 该方法只针对 `Qt5.15` 以上版本有效 2. 通过事件过滤器判断边缘设置鼠标样式 3. 处理点击事件交通过 `QWindow.startSystemMove` 和 `QWindow.startSystemResize` 传递给系统处理 -![NewFramelessWindow](ScreenShot/NewFramelessWindow.gif) \ No newline at end of file +![NewFramelessWindow](ScreenShot/NewFramelessWindow.gif) + +## 25、属性绑定 + +[运行 TestSerializeModel.py](TestSerializeModel.py) + +类似:[json数据绑定](../QTreeView#3json数据绑定) + +![TestSerializeModel](ScreenShot/TestSerializeModel.gif) diff --git a/Demo/ScreenShot/TestSerializeModel.gif b/Demo/ScreenShot/TestSerializeModel.gif new file mode 100644 index 00000000..7b220a19 Binary files /dev/null and b/Demo/ScreenShot/TestSerializeModel.gif differ diff --git a/Demo/TestSerializeModel.py b/Demo/TestSerializeModel.py new file mode 100644 index 00000000..4483c4a9 --- /dev/null +++ b/Demo/TestSerializeModel.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/08 +@file: TestSerializeModel.py +@description: +""" + +import json +import sys +from datetime import datetime + +from Lib.qpropmapper import QPropertyMapper +from Lib.serializewidget import Ui_SerializeWidget + +try: + from PyQt5.QtCore import QRegExp, Qt + from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat + from PyQt5.QtWidgets import QApplication, QWidget +except ImportError: + from PySide2.QtCore import QRegExp, Qt + from PySide2.QtGui import QSyntaxHighlighter, QTextCharFormat + from PySide2.QtWidgets import QApplication, QWidget + + +class HighlightingRule: + def __init__(self, pattern, color): + self.pattern = QRegExp(pattern) + self.format = QTextCharFormat() + self.format.setForeground(color) + + +class JsonHighlighter(QSyntaxHighlighter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._rules = [ + # numbers + HighlightingRule(QRegExp('([-0-9.]+)(?!([^"]*"[\\s]*\\:))'), Qt.darkRed), + # key + HighlightingRule(QRegExp('("[^"]*")\\s*\\:'), Qt.darkBlue), + # value + HighlightingRule(QRegExp(':+(?:[: []*)("[^"]*")'), Qt.darkGreen), + ] + + def highlightBlock(self, text: str) -> None: + for rule in self._rules: + index = rule.pattern.indexIn(text) + while index >= 0: + length = rule.pattern.matchedLength() + self.setFormat(index, length, rule.format) + index = rule.pattern.indexIn(text, index + length) + + +class TestWindow(QWidget, Ui_SerializeWidget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setupUi(self) + self.resize(1200, 600) + + # json view + self.highlighter = JsonHighlighter(self.editJsonView.document()) + self.editJsonView.textChanged.connect(self.onJsonChanged) + + # serialize model + QPropertyMapper.Verbose = True + self.mapper = QPropertyMapper(self) + self.mapper.propertyChanged.connect(self.onPropertyChanged) + self.mapper.loadData( + { + "input": { + "radioButton": True, + "checkBox": False, + }, + "name": "Irony", + } + ) + + # comboBox + self.comboBox.addItems([f"Item {i}" for i in range(10)]) + + self.mapper.bind(self.radioButton, "input.radioButton") + self.mapper.bind(self.checkBox, "input.checkBox", True) + self.mapper.bind(self.comboBox, "input.comboBox") + self.mapper.bind(self.spinBox, "age") + self.mapper.bind(self.doubleSpinBox, "money") + self.mapper.bind(self.lineEdit, "name") + + # date and time + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.mapper.bind(self.timeEdit, "input.time", now) + self.mapper.bind(self.dateEdit, "input.date", now) + self.mapper.bind(self.dateTimeEdit, "input.dateTime", now) + + # edit + self.mapper.bind(self.plainTextEdit, "desc.desc") + self.mapper.bind(self.textEdit, "desc.text") + + # Correlation + self.mapper.bind(self.spinBox_2, "correlation.slider") + self.mapper.bind(self.horizontalSlider, "correlation.slider") + self.mapper.bind(self.progressBar, "correlation.progress") + self.mapper.bind(self.verticalSlider, "correlation.progress") + + # get new dict + self.onPropertyChanged() + + def onPropertyChanged(self, *args, **kwargs): + data = self.mapper.toJson(indent=2) + self.editJsonView.blockSignals(True) + self.editJsonView.setPlainText(data) + self.editJsonView.blockSignals(False) + + def onJsonChanged(self): + text = self.editJsonView.toPlainText().strip() + try: + data = json.loads(text) + self.mapper.loadData(data) + except Exception: + pass + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/Demo/WeltHideWindow.py b/Demo/WeltHideWindow.py old mode 100644 new mode 100755 index 5faa20bd..91a49f9a --- a/Demo/WeltHideWindow.py +++ b/Demo/WeltHideWindow.py @@ -10,26 +10,77 @@ @description: 简单的窗口贴边隐藏 """ +import os +import platform +from subprocess import getoutput + try: from PyQt5.QtCore import Qt - from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication + from PyQt5.QtWidgets import ( + QApplication, + QGridLayout, + QMessageBox, + QPushButton, + QSizePolicy, + QSpacerItem, + QWidget, + ) except ImportError: from PySide2.QtCore import Qt - from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication + from PySide2.QtWidgets import ( + QApplication, + QGridLayout, + QMessageBox, + QPushButton, + QSizePolicy, + QSpacerItem, + QWidget, + ) -class WeltHideWindow(QWidget): +def IsSupport(): + """判断是否支持""" + if platform.system() == "Linux": + name = os.environ.get("XDG_SESSION_DESKTOP", "") + os.environ.get( + "XDG_CURRENT_DESKTOP", "" + ) + if name.lower().find("gnome") != -1: + print("gnome desktop") + return False + + wid = getoutput("xprop -root _NET_SUPPORTING_WM_CHECK").split(" # ")[-1] + print("wid:", wid) + if wid: + name = getoutput("xprop -id %s _NET_WM_NAME" % wid) + print("name:", name) + if name.lower().find("gnome") != -1: + print("gnome desktop") + return False + return True + + +class WeltHideWindow(QWidget): def __init__(self, *args, **kwargs): super(WeltHideWindow, self).__init__(*args, **kwargs) self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint) - self.resize(800, 600) + self.resize(400, 300) self._width = QApplication.desktop().availableGeometry(self).width() - layout = QVBoxLayout(self) - layout.addWidget(QPushButton("关闭窗口", self, clicked=self.close)) + layout = QGridLayout(self) + layout.addItem( + QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, 0 + ) + self.closeBtn = QPushButton("X", self) + layout.addWidget(self.closeBtn, 0, 1) + layout.addItem( + QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding), 1, 0 + ) + self.closeBtn.clicked.connect(self.close) + self.closeBtn.setMinimumSize(24, 24) + self.closeBtn.setMaximumSize(24, 24) def mousePressEvent(self, event): - '''鼠标按下事件,需要记录下坐标self._pos 和 是否可移动self._canMove''' + """鼠标按下事件,需要记录下坐标self._pos 和 是否可移动self._canMove""" super(WeltHideWindow, self).mousePressEvent(event) if event.button() == Qt.LeftButton: self._pos = event.globalPos() - self.pos() @@ -37,13 +88,13 @@ def mousePressEvent(self, event): self._canMove = not self.isMaximized() or not self.isFullScreen() def mouseMoveEvent(self, event): - '''鼠标移动事件,动态调整窗口位置''' + """鼠标移动事件,动态调整窗口位置""" super(WeltHideWindow, self).mouseMoveEvent(event) if event.buttons() == Qt.LeftButton and self._canMove: self.move(event.globalPos() - self._pos) def mouseReleaseEvent(self, event): - '''鼠标弹起事件,这个时候需要判断窗口的左边是否符合贴到左边,顶部,右边一半''' + """鼠标弹起事件,这个时候需要判断窗口的左边是否符合贴到左边,顶部,右边一半""" super(WeltHideWindow, self).mouseReleaseEvent(event) self._canMove = False pos = self.pos() @@ -60,7 +111,7 @@ def mouseReleaseEvent(self, event): return self.move(self._width - 1, y) def enterEvent(self, event): - '''鼠标进入窗口事件,用于弹出显示窗口''' + """鼠标进入窗口事件,用于弹出显示窗口""" super(WeltHideWindow, self).enterEvent(event) pos = self.pos() x = pos.x() @@ -73,7 +124,7 @@ def enterEvent(self, event): return self.move(self._width - self.width(), y) def leaveEvent(self, event): - '''鼠标离开事件,如果原先窗口已经隐藏,并暂时显示,此时离开后需要再次隐藏''' + """鼠标离开事件,如果原先窗口已经隐藏,并暂时显示,此时离开后需要再次隐藏""" super(WeltHideWindow, self).leaveEvent(event) pos = self.pos() x = pos.x() @@ -86,10 +137,18 @@ def leaveEvent(self, event): return self.move(self._width - 1, y) -if __name__ == '__main__': +if __name__ == "__main__": import sys app = QApplication(sys.argv) - w = WeltHideWindow() - w.show() - sys.exit(app.exec_()) + if not IsSupport(): + QMessageBox.warning( + None, + "Warning", + "当前桌面不支持此功能", + ) + app.quit() + else: + w = WeltHideWindow() + w.show() + sys.exit(app.exec_()) diff --git a/Demo/requirements.txt b/Demo/requirements.txt index afed57a9..04346aa8 100644 --- a/Demo/requirements.txt +++ b/Demo/requirements.txt @@ -1,3 +1,4 @@ pywin32 numpy dlib +python-box diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8000a6fa --- /dev/null +++ b/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random + Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/PyQtGraph/README.md b/PyQtGraph/README.md index 3ac9f29d..c745aaee 100644 --- a/PyQtGraph/README.md +++ b/PyQtGraph/README.md @@ -14,23 +14,28 @@ 6. `pg.PlotWidget()`鼠标获取X轴坐标 ## 目录 + - [鼠标获取X轴坐标](#1鼠标获取X轴坐标) - [禁止右键点击功能、鼠标滚轮,添加滚动条等功能](#2禁止右键点击功能、鼠标滚轮,添加滚动条等功能) ## 1、鼠标获取X轴坐标 + [运行 mouseFlow.py](mouseFlow.py) ![mouseFlow](ScreenShot/mouseFlow.gif) ## 2、禁止右键点击功能、鼠标滚轮,添加滚动条等功能 + [运行 graph1.py](graph1.py) | [查看 graphTest.ui](Data/graphTest.ui) ![mouseFlow](ScreenShot/function.gif) -## 3、不用修改源码,重加载,解决右键保存图片异常;解决自定义坐标轴密集显示;禁止鼠标事件; +## 3、不用修改源码,重加载,解决右键保存图片异常;解决自定义坐标轴密集显示;禁止鼠标事件 + [加载 tools.py](tools.py) -## 4、QScrollArea添加和修改大小例子; +## 4、QScrollArea添加和修改大小例子 + [运行 testGraphAnalysis.py](testGraphAnalysis.py) | [查看 graphAnalysis.ui](Data/graphAnalysis.ui) ![testGraphAnalysis](ScreenShot/GraphAnalysis.gif) diff --git a/QAxWidget/README.md b/QAxWidget/README.md index 8811751d..2c8d8cdd 100644 --- a/QAxWidget/README.md +++ b/QAxWidget/README.md @@ -4,10 +4,11 @@ - [显示Word、Excel、PDF文件](#1显示WordExcelPDF文件) ## 1、显示Word、Excel、PDF文件 + [运行 ViewOffice.py](ViewOffice.py) 1. 利用 `Word.Application` 打开Word文件 1. 利用 `Excel.Application` 打开Excel文件 1. 利用 `Adobe PDF Reader` 打开PDF文件(前提先装PDF软件) -![ViewOffice](ScreenShot/ViewOffice.png) \ No newline at end of file +![ViewOffice](ScreenShot/ViewOffice.png) diff --git a/QCalendarWidget/README.md b/QCalendarWidget/README.md index 62bd8815..0a6e8adb 100644 --- a/QCalendarWidget/README.md +++ b/QCalendarWidget/README.md @@ -4,8 +4,9 @@ - [QSS美化日历样式](#1QSS美化日历样式) ## 1、QSS美化日历样式 + [运行 CalendarQssStyle.py](CalendarQssStyle.py) 对日历控件的部分控件进行QSS美化,顶部背景颜色和高度,上下月按钮、月份选择、年选择、菜单 -![CalendarQssStyle](ScreenShot/CalendarQssStyle.gif) \ No newline at end of file +![CalendarQssStyle](ScreenShot/CalendarQssStyle.gif) diff --git a/QColumnView/FileManager.py b/QColumnView/FileManager.py new file mode 100644 index 00000000..810e489a --- /dev/null +++ b/QColumnView/FileManager.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/04/08 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: FileManager.py +@description: +""" + +try: + from PyQt5.QtWidgets import ( + QApplication, + QColumnView, + QFileSystemModel, + QGridLayout, + QMessageBox, + QPushButton, + QSizePolicy, + QSpacerItem, + QWidget, + ) +except Exception: + from PySide2.QtWidgets import ( + QApplication, + QColumnView, + QFileSystemModel, + QGridLayout, + QMessageBox, + QPushButton, + QSizePolicy, + QSpacerItem, + QWidget, + ) + + +class FileManager(QWidget): # type: ignore + def __init__(self, *args, **kwargs): + super(FileManager, self).__init__(*args, **kwargs) + self.resize(800, 600) + self._view = QColumnView(self) + self._btn = QPushButton("确定", self) + layout = QGridLayout(self) + layout.addWidget(self._view, 0, 0, 1, 2) + layout.addItem( + QSpacerItem( + 40, + 20, + QSizePolicy.Expanding, + QSizePolicy.Minimum, + ), + 1, + 0, + ) + layout.addWidget(self._btn, 1, 1, 1, 1) + layout.setRowStretch(1, 0) + self._model = QFileSystemModel(self) + self._model.setRootPath("") # 设置根路径 + self._view.setModel(self._model) # type: ignore + self._btn.clicked.connect(self.onAccept) # type: ignore + + def onAccept(self): + path = self._model.filePath(self._view.currentIndex()) + print("path", path) + if path: + QMessageBox.information(self, "提示", "路径:%s" % path) + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = FileManager() + w.show() + sys.exit(app.exec_()) diff --git a/QColumnView/README.md b/QColumnView/README.md index e69de29b..bea28a78 100644 --- a/QColumnView/README.md +++ b/QColumnView/README.md @@ -0,0 +1,16 @@ +# QColumnView + +- 目录 + - [文件系统浏览器](#1文件系统浏览器) + +## 1、文件系统浏览器 + +[运行 FileManager.py](FileManager.py) + +一个省市区关联的三级联动,数据源在data.json中 + +1. 通过`QFileSystemModel`模型来显示文件系统 +2. 结合`QColumnView`的`setMode`函数显示文件系统模型 +3. 通过`QFileSystemModel.filePath(QColumnView.currentIndex())`获取当前选中的路径 + +![FileManager](ScreenShot/FileManager.png) diff --git a/QColumnView/ScreenShot/FileManager.png b/QColumnView/ScreenShot/FileManager.png new file mode 100644 index 00000000..c69afdce Binary files /dev/null and b/QColumnView/ScreenShot/FileManager.png differ diff --git a/QComboBox/README.md b/QComboBox/README.md index 8a914d23..aa75ef6c 100644 --- a/QComboBox/README.md +++ b/QComboBox/README.md @@ -23,4 +23,4 @@ 1. 使用`QProxyStyle`对文件居中显示 2. 新增得item数据使用`setTextAlignment`对齐 -![CenterText](ScreenShot/CenterText.png) \ No newline at end of file +![CenterText](ScreenShot/CenterText.png) diff --git a/QFileSystemModel/README.md b/QFileSystemModel/README.md index fc76971f..cd0d7e9e 100644 --- a/QFileSystemModel/README.md +++ b/QFileSystemModel/README.md @@ -4,9 +4,10 @@ - [自定义图标](#1自定义图标) ## 1、自定义图标 + [运行 CustomIcon.py](CustomIcon.py) 1. 继承 `QFileIconProvider` 类实现自己的图标提供器 2. 重写 `def icon(self, type_info)` 方法根据文件类型返回对应的图标 -![CustomIcon](ScreenShot/CustomIcon.png) \ No newline at end of file +![CustomIcon](ScreenShot/CustomIcon.png) diff --git a/QFlowLayout/README.md b/QFlowLayout/README.md index 9fa52ee6..21868ec8 100644 --- a/QFlowLayout/README.md +++ b/QFlowLayout/README.md @@ -4,18 +4,19 @@ - [音乐热歌列表](#1音乐热歌列表) ## 1、音乐热歌列表 + [运行 HotPlaylist.py](HotPlaylist.py) 简单思路说明: - - 利用`QScrollArea`滚动显示,自定义的`QFlowLayout`做布局来放置自定义的Widget - - `QNetworkAccessManager`异步下载网页和图片 - - `QScrollArea`滚动到底部触发下一页加载 +- 利用`QScrollArea`滚动显示,自定义的`QFlowLayout`做布局来放置自定义的Widget +- `QNetworkAccessManager`异步下载网页和图片 +- `QScrollArea`滚动到底部触发下一页加载 自定义控件说明: - - 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 - - 字体颜色用qss设置 - - 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) +- 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 +- 字体颜色用qss设置 +- 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) -![HotPlaylist](ScreenShot/HotPlaylist.gif) \ No newline at end of file +![HotPlaylist](ScreenShot/HotPlaylist.gif) diff --git a/QFont/README.md b/QFont/README.md index 21a12969..28d0cd86 100644 --- a/QFont/README.md +++ b/QFont/README.md @@ -4,8 +4,9 @@ - [加载自定义字体](#1加载自定义字体) ## 1、加载自定义字体 + [运行 AwesomeFont.py](AwesomeFont.py) 通过`QFontDatabase.addApplicationFont`加载字体文件 -![AwesomeFont](ScreenShot/AwesomeFont.png) \ No newline at end of file +![AwesomeFont](ScreenShot/AwesomeFont.png) diff --git a/QGraphicsDropShadowEffect/README.md b/QGraphicsDropShadowEffect/README.md index 6e5be6fb..20d5d9d3 100644 --- a/QGraphicsDropShadowEffect/README.md +++ b/QGraphicsDropShadowEffect/README.md @@ -4,6 +4,7 @@ - [边框阴影动画](#1边框阴影动画) ## 1、边框阴影动画 + [运行 ShadowEffect.py](ShadowEffect.py) 1. 通过`setGraphicsEffect`设置控件的边框阴影 @@ -11,4 +12,4 @@ 3. 通过`QPropertyAnimation`属性动画不断改变`radius`的值并调用`setBlurRadius`更新半径值 4. 不能对父控件使用 -![ShadowEffect](ScreenShot/ShadowEffect.gif) \ No newline at end of file +![ShadowEffect](ScreenShot/ShadowEffect.gif) diff --git a/QGraphicsView/README.md b/QGraphicsView/README.md index ef66543d..5db21ca5 100644 --- a/QGraphicsView/README.md +++ b/QGraphicsView/README.md @@ -7,6 +7,7 @@ - [图标拖拽](#4图标拖拽) ## 1、绘制世界地图 + [运行 WorldMap.py](WorldMap.py) 1. 解析json数据生成 `QPolygonF` @@ -15,6 +16,7 @@ ![WorldMap](ScreenShot/WorldMap.gif) ## 2、添加QWidget + [运行 AddQWidget.py](AddQWidget.py) 通过 `QGraphicsScene.addWidget` 添加自定义QWidget @@ -22,6 +24,7 @@ ![AddQWidget](ScreenShot/AddQWidget.png) ## 3、图片查看器 + [运行 ImageView.py](ImageView.py) 支持放大缩小和移动 @@ -29,6 +32,7 @@ ![ImageView](ScreenShot/ImageView.gif) ## 3、图标拖拽 + [运行 DragGraphics.py](DragGraphics.py) 该示例主要是包含左侧树状图标列表和右侧视图显示,从左侧拖拽到右侧 @@ -36,4 +40,4 @@ 1. 重写`QListWidget`的`startDrag`函数用来封装拖拽数据 2. 重写`QGraphicsView`的`dragEnterEvent`、`dragMoveEvent`、`dropEvent`函数用来处理拖拽事件 -![DragGraphics](ScreenShot/DragGraphics.gif) \ No newline at end of file +![DragGraphics](ScreenShot/DragGraphics.gif) diff --git a/QGridLayout/README.md b/QGridLayout/README.md index 8c851274..106220a6 100644 --- a/QGridLayout/README.md +++ b/QGridLayout/README.md @@ -4,18 +4,19 @@ - [音乐热歌列表](#1音乐热歌列表) ## 1、音乐热歌列表 + [运行 HotPlaylist.py](HotPlaylist.py) 简单思路说明: - - 利用`QScrollArea`滚动显示,`QGridLayout`做布局来放置自定义的Widget - - `QNetworkAccessManager`异步下载网页和图片 - - `QScrollArea`滚动到底部触发下一页加载 +- 利用`QScrollArea`滚动显示,`QGridLayout`做布局来放置自定义的Widget +- `QNetworkAccessManager`异步下载网页和图片 +- `QScrollArea`滚动到底部触发下一页加载 自定义控件说明: - - 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 - - 字体颜色用qss设置 - - 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) +- 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 +- 字体颜色用qss设置 +- 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) -![HotPlaylist](ScreenShot/HotPlaylist.gif) \ No newline at end of file +![HotPlaylist](ScreenShot/HotPlaylist.gif) diff --git a/QHBoxLayout/README.md b/QHBoxLayout/README.md index a20a92b5..fa074093 100644 --- a/QHBoxLayout/README.md +++ b/QHBoxLayout/README.md @@ -6,11 +6,13 @@ - [比例分配](#3比例分配) ## 1、水平布局 + [查看 BaseHorizontalLayout.ui](Data/BaseHorizontalLayout.ui) ![BaseHorizontalLayout](ScreenShot/BaseHorizontalLayout.png) ## 2、边距和间隔 + [查看 HorizontalLayoutMargin.ui](Data/HorizontalLayoutMargin.ui) 1. 通过`setContentsMargins(-1, -1, 20, -1)`设置左上右下的边距,-1表示默认值 @@ -19,6 +21,7 @@ ![HorizontalLayoutMargin](ScreenShot/HorizontalLayoutMargin.png) ## 3、比例分配 + [查看 HorizontalLayoutStretch.ui](Data/HorizontalLayoutStretch.ui) 通过`setStretch`设置各个部分的占比 分别为:1/6 2/6 3/6 @@ -29,4 +32,4 @@ self.horizontalLayout.setStretch(1, 2) self.horizontalLayout.setStretch(2, 3) ``` -![HorizontalLayoutStretch](ScreenShot/HorizontalLayoutStretch.png) \ No newline at end of file +![HorizontalLayoutStretch](ScreenShot/HorizontalLayoutStretch.png) diff --git a/QLabel/README.md b/QLabel/README.md index acfc3d53..b14fca5a 100644 --- a/QLabel/README.md +++ b/QLabel/README.md @@ -8,6 +8,7 @@ - [圆形图片](#5圆形图片) ## 1、图片加载显示 + [运行 ShowImage.py](ShowImage.py) 通过3种方式加载图片文件和显示gif图片 @@ -32,6 +33,7 @@ ![ShowImage](ScreenShot/ShowImage.gif) ## 2、图片旋转 + [运行 ImageRotate.py](ImageRotate.py) 1. 水平翻转 `QImage.mirrored(True, False)` @@ -42,6 +44,7 @@ ![ImageRotate](ScreenShot/ImageRotate.gif) ## 3、仿网页图片错位显示 + [运行 ImageSlipped.py](ImageSlipped.py) 1. 设置`setMouseTracking(True)`开启鼠标跟踪 @@ -51,6 +54,7 @@ ![ImageSlipped](ScreenShot/ImageSlipped.gif) ## 4、显示.9格式图片(气泡) + [运行 NinePatch.py](NinePatch.py) | [运行 QtNinePatch.py](QtNinePatch.py) | [运行 QtNinePatch2.py](QtNinePatch2.py) 什么叫.9.PNG呢,这是安卓开发里面的一种特殊的图片 @@ -64,16 +68,18 @@ 在Github开源库中搜索到两个C++版本的 -1.一个是NinePatchQt https://github.com/Roninsc2/NinePatchQt +1.一个是NinePatchQt -2.一个是QtNinePatch https://github.com/soramimi/QtNinePatch +2.一个是QtNinePatch ### For PyQt + 1、目前针对第一个库在2年前用Python参考源码重新写一个见 `Lib/NinePatch.py` 2、这次针对第二个库用Python编写的见`Lib/QtNinePatch2.py`。用C++编写的pyd版本见`Lib/QtNinePatch`目录 ### 说明 + 1、建议优先使用pyd版本的(后续提供Python3.4 3.5 3.6 3.7 编译好的32为库文件),也可以自行编译,编译步骤见下文。 2、其次可以使用Python写的第二个版本`Lib/QtNinePatch2.py`(个人觉得方便调用) @@ -103,8 +109,9 @@ qt_path = 'D:/soft/Qt/Qt5.5.1/5.5/msvc2010' ![NinePatchImage](ScreenShot/NinePatchImage.gif) ### 5、圆形图片 + [运行 CircleImage.py](CircleImage.py) 使用`QPainter`的`setClipPath`方法结合`QPainterPath`对图片进行裁剪从而实现圆形图片。 -![CircleImage](ScreenShot/CircleImage.png) \ No newline at end of file +![CircleImage](ScreenShot/CircleImage.png) diff --git a/QListView/README.md b/QListView/README.md index 95a16ef9..c70c393d 100644 --- a/QListView/README.md +++ b/QListView/README.md @@ -6,6 +6,7 @@ - [自定义角色排序](#3自定义角色排序) ## 1、显示自定义Widget + [运行 CustomWidgetItem.py](CustomWidgetItem.py) 通过设置 `setIndexWidget(QModelIndex, QWidget)` 可以设置自定义 `QWidget` @@ -13,6 +14,7 @@ ![CustomWidgetItem](ScreenShot/CustomWidgetItem.png) ## 2、显示自定义Widget并排序 + [运行 CustomWidgetSortItem.py](CustomWidgetSortItem.py) 1. 对QListView设置代理 `QSortFilterProxyModel` @@ -21,9 +23,11 @@ ![CustomWidgetSortItem](ScreenShot/CustomWidgetSortItem.gif) ## 3、自定义角色排序 + [运行 SortItemByRole.py](SortItemByRole.py) 需求: + 1. 5种分类(唐、宋、元、明、清) 和 未分类 2. 选中唐则按照 唐、宋、元、明、清、未分类排序 3. 选中宋则按照 宋、唐、元、明、清、未分类排序 @@ -31,9 +35,11 @@ 5. 取消排序则恢复到加载时候顺序,如:未分类、唐、唐、明、清、未分类、宋、元、未分类 思路: + 1. 定义`IdRole = Qt.UserRole + 1` 用于恢复默认排序 2. 定义`ClassifyRole = Qt.UserRole + 2` 用于按照分类序号排序 3. 定义5种分类的id + ```python NameDict = { '唐': ['Tang', 0], @@ -50,14 +56,18 @@ 4: '清', } ``` + 4. item设置 `setData(id, IdRole)` 用于恢复默认排序 5. item设置 `setData(cid, ClassifyRole)` 用于标识该item的分类 6. 继承 `QSortFilterProxyModel` 增加 `setSortIndex(self, index)` 方法, 目的在于记录要置顶(不参与排序)的分类ID + ```python def setSortIndex(self, index): self._topIndex = index ``` + 7. 继承 `QSortFilterProxyModel` 重写 `lessThan` 方法, 判断分类ID是否等于要置顶的ID, 如果是则修改为-1, 这样就永远在最前面 + ```python if self.sortRole() == ClassifyRole and \ source_left.column() == self.sortColumn() and \ @@ -76,12 +86,16 @@ return leftIndex < rightIndex ``` + 8. 恢复默认排序 + ```python self.fmodel.setSortRole(IdRole) # 必须设置排序角色为ID self.fmodel.sort(0) # 排序第一列按照ID升序 ``` + 9. 根据分类排序, 这里要注意要先通过 `setSortRole` 设置其它角色再设置目标角色 + ```python self.fmodel.setSortIndex(1) self.fmodel.setSortRole(IdRole) @@ -89,4 +103,4 @@ self.fmodel.sort(0) ``` -![SortItemByRole](ScreenShot/SortItemByRole.gif) \ No newline at end of file +![SortItemByRole](ScreenShot/SortItemByRole.gif) diff --git a/QListWidget/README.md b/QListWidget/README.md index 69610a6f..faaa1ab7 100644 --- a/QListWidget/README.md +++ b/QListWidget/README.md @@ -8,6 +8,7 @@ - [列表常用信号](#5列表常用信号) ## 1、删除自定义Item + [运行 DeleteCustomItem.py](DeleteCustomItem.py) 1. 删除item时先要通过`QListWidget.indexFromItem(item).row()`得到它的行数 @@ -18,24 +19,26 @@ ![CustomWidgetItem](ScreenShot/DeleteCustomItem.gif) ## 2、自定义可拖拽Item + [运行 DragDrop.py](DragDrop.py) ![CustomWidgetSortItem](ScreenShot/DragDrop.gif) ## 3、音乐热歌列表 + [运行 HotPlaylist.py](HotPlaylist.py) 简单思路说明: - - 利用`QListWidget`设置一些特殊的参数达到可以横向自动显示 - - `QNetworkAccessManager`异步下载网页和图片 - - 滚动到底部触发下一页加载 +- 利用`QListWidget`设置一些特殊的参数达到可以横向自动显示 +- `QNetworkAccessManager`异步下载网页和图片 +- 滚动到底部触发下一页加载 自定义控件说明: - - 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 - - 字体颜色用qss设置 - - 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) +- 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 +- 字体颜色用qss设置 +- 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) `QListWidget`的参数设置 @@ -46,6 +49,7 @@ ![HotPlaylist](ScreenShot/HotPlaylist.gif) ## 4、仿折叠控件效果 + [运行 FoldWidget.py](FoldWidget.py) 1. 利用`QListWidget`设置Item的自定义控件 @@ -56,8 +60,9 @@ ![FoldWidget](ScreenShot/FoldWidget.gif) ## 5、列表常用信号 + [运行 SignalsExample.py](SignalsExample.py) -根据官网文档 https://doc.qt.io/qt-5/qlistwidget.html#signals 中的信号介绍编写 +根据官网文档 中的信号介绍编写 -![SignalsExample](ScreenShot/SignalsExample.gif) \ No newline at end of file +![SignalsExample](ScreenShot/SignalsExample.gif) diff --git a/QMenu/README.md b/QMenu/README.md index 33271cab..f3657b91 100644 --- a/QMenu/README.md +++ b/QMenu/README.md @@ -5,6 +5,7 @@ - [仿QQ右键菜单](#2仿QQ右键菜单) ## 1、菜单设置多选并且不关闭 + [运行 MultiSelect.py](MultiSelect.py) 有时候会遇到这种需求:在界面某个位置弹出一个菜单,其中里面的菜单项可以多选(类似配置选项), @@ -39,6 +40,7 @@ def _menu_mouseReleaseEvent(self, event): ![MultiSelect](ScreenShot/MultiSelect.gif) ## 2、仿QQ右键菜单 + [运行 QQMenu.py](QQMenu.py) -![QQMenu](ScreenShot/QQMenu.gif) \ No newline at end of file +![QQMenu](ScreenShot/QQMenu.gif) diff --git a/QMessageBox/README.md b/QMessageBox/README.md index 84a1e914..9342245c 100644 --- a/QMessageBox/README.md +++ b/QMessageBox/README.md @@ -6,6 +6,7 @@ - [消息框按钮文字汉化](#3消息框按钮文字汉化) ## 1、消息对话框倒计时关闭 + [运行 CountDownClose.py](CountDownClose.py) 1. 通过继承`QMessageBox`实现倒计时关闭的对话框 @@ -14,15 +15,17 @@ ![CountDownClose](ScreenShot/CountDownClose.gif) ## 2、自定义图标等 + [运行 CustomColorIcon.py](CustomColorIcon.py) ![CustomColorIcon](ScreenShot/CustomColorIcon.png) ## 3、消息框按钮文字汉化 + [运行 ChineseText.py](ChineseText.py) 1. 因为Qt5的翻译文件还是沿用旧的Qt4的结构导致部分地方无法翻译 2. 可以通过手动重新编译翻译文件解决问题 3. 这里可以通过QSS特性修改按钮文字,详细见代码 -![ChineseText](ScreenShot/ChineseText.png) \ No newline at end of file +![ChineseText](ScreenShot/ChineseText.png) diff --git a/QMetaObject/README.md b/QMetaObject/README.md index 06f2560d..46cf1ed0 100644 --- a/QMetaObject/README.md +++ b/QMetaObject/README.md @@ -4,6 +4,7 @@ - [在线程中操作UI](#1在线程中操作UI) ## 1、在线程中操作UI + [运行 CallInThread.py](CallInThread.py) 如果想在`QThread`或者`threading.Thread`中不通过信号直接操作UI,则可以使用`QMetaObject.invokeMethod`调用。 @@ -21,4 +22,4 @@ 1. 调用函数都是异步队列方式,需要使用`Qt.QueuedConnection` 2. 而要得到返回值则必须使用同步方式, 即`Qt.DirectConnection` -![CallInThread](ScreenShot/CallInThread.png) \ No newline at end of file +![CallInThread](ScreenShot/CallInThread.png) diff --git a/QPainter/README.md b/QPainter/README.md index 81206483..29a0050d 100644 --- a/QPainter/README.md +++ b/QPainter/README.md @@ -1,16 +1,17 @@ # QPainter - - 目录 - [利用QPainter绘制各种图形](#QPainter绘制各种图形) - [简易画板](#简易画板) ## 1、QPainter绘制各种图形 + [运行 StockDialog.py](StockDialog.py) ![CountDownClose](ScreenShot/StockDialog.gif) ## 2、简易画板 + [运行 Draw.py](Draw.py) -![CustomColorIcon](ScreenShot/Draw.gif) \ No newline at end of file +![CustomColorIcon](ScreenShot/Draw.gif) diff --git a/QProcess/README.md b/QProcess/README.md index 0136bd7b..74bee0b9 100644 --- a/QProcess/README.md +++ b/QProcess/README.md @@ -5,9 +5,11 @@ - [交互执行命令](#2交互执行命令) ## 1、执行命令得到结果 + [运行 GetCmdResult.py](GetCmdResult.py) `QProcess` 常用执行命令方式有以下几种: + 1. `QProcess.execute('ping', ['www.baidu.com'])`:同步执行,返回值为进程退出码 2. `QProcess.startDetached('ping', ['www.baidu.com'], '工作路径')`:返回值为是否启动成功,该命令一般用于启动某个程序后就不管了 3. 通过构造`QProcess`对象,然后通过`QProcess.start()`启动进程,并分为同步和异步两种方式获取输出 @@ -21,6 +23,7 @@ ![GetCmdResult](ScreenShot/GetCmdResult.gif) ## 2、交互执行命令 + [运行 InteractiveRun.py](InteractiveRun.py) `QProcess` 也可以用于交互式执行命令,具体需要如下几步: @@ -30,4 +33,4 @@ 3. 通过`readyReadStandardOutput`信号读取进程输出 4. 通过`writeData`向进程写入数据 -![InteractiveRun](ScreenShot/InteractiveRun.gif) \ No newline at end of file +![InteractiveRun](ScreenShot/InteractiveRun.gif) diff --git a/QProgressBar/README.md b/QProgressBar/README.md index b7197dc8..0db222fe 100644 --- a/QProgressBar/README.md +++ b/QProgressBar/README.md @@ -10,6 +10,7 @@ - [多彩动画进度条](#7多彩动画进度条) ## 1、常规样式美化 + [运行 SimpleStyle.py](SimpleStyle.py) 主要改变背景颜色、高度、边框、块颜色、边框、圆角 @@ -17,21 +18,25 @@ ![SimpleStyle](ScreenShot/SimpleStyle.gif) ## 2、圆圈进度条 + [运行 RoundProgressBar.py](RoundProgressBar.py) ![RoundProgressBar](ScreenShot/RoundProgressBar.gif) ## 3、百分比进度条 + [运行 PercentProgressBar.py](PercentProgressBar.py) ![PercentProgressBar](ScreenShot/PercentProgressBar.gif) ## 4、Metro进度条 + [运行 MetroCircleProgress.py](MetroCircleProgress.py) ![MetroCircleProgress](ScreenShot/MetroCircleProgress.gif) ## 5、水波纹进度条 + [运行 WaterProgressBar.py](WaterProgressBar.py) 1. 利用正弦函数根据0-width的范围计算y坐标 @@ -41,13 +46,15 @@ ![WaterProgressBar](ScreenShot/WaterProgressBar.gif) ## 6、圆形水位进度条 + [运行 WaterProgress.py](WaterProgress.py) -参考 https://github.com/linuxdeepin/dtkwidget/blob/master/src/widgets/dwaterprogress.cpp +参考 ![WaterProgressBar](ScreenShot/WaterProgress.gif) ## 7、多彩动画进度条 + [运行 ColourfulProgress.py](ColourfulProgress.py) 动画实现参考 qfusionstyle.cpp 中的 CE_ProgressBarContents 绘制方法 diff --git a/QPropertyAnimation/README.md b/QPropertyAnimation/README.md index 25ecc4df..3041e069 100644 --- a/QPropertyAnimation/README.md +++ b/QPropertyAnimation/README.md @@ -9,6 +9,7 @@ - [窗口翻转动画(仿QQ)](#6窗口翻转动画仿QQ) ## 1、窗口淡入淡出 + [运行 FadeInOut.py](FadeInOut.py) 1. 使用`QPropertyAnimation`对窗口的`windowOpacity`透明度属性进行修改 @@ -20,8 +21,9 @@ 1. 绑定动画完成后`finished`信号连接到`close`关闭窗口函数 ![FadeInOut](ScreenShot/FadeInOut.gif) - + ## 2、右键菜单动画 + [运行 MenuAnimation.py](MenuAnimation.py) 1. 使用`QPropertyAnimation`对菜单控件的`geometry`属性进行修改 @@ -30,6 +32,7 @@ ![MenuAnimation](ScreenShot/MenuAnimation.gif) ## 3、点阵特效 + [运行 RlatticeEffect.py](RlatticeEffect.py) 1. emmm,我也不知道这个动画叫啥名字,反正就是仿照网页做的 @@ -45,6 +48,7 @@ 1. PS: pyd是python3.4生成的,删掉pyd也能运行 这部分是js的核心 + ```js // for each point find the 5 closest points for(var i = 0; i < points.length; i++) { @@ -78,6 +82,7 @@ for(var i = 0; i < points.length; i++) { ``` 这部分是py的核心 + ```python def findClose(points): plen = len(points) @@ -105,10 +110,11 @@ def findClose(points): ![RlatticeEffect](ScreenShot/RlatticeEffect.gif) ## 4、页面切换/图片轮播动画 + [运行 PageSwitching.py](PageSwitching.py) | [查看 UiImageSlider.ui](Data/UiImageSlider.ui) 1. 使用`QPropertyAnimation`对`QStackedWidget`中的子控件进行pos位移操作实现动画切换特效 -1. 主要代码参考http://qt.shoutwiki.com/wiki/Extending_QStackedWidget_for_sliding_page_animations_in_Qt +1. 主要代码参考 1. 增加了自动切换函数 函数调用: @@ -121,6 +127,7 @@ def findClose(points): ![PageSwitching](ScreenShot/PageSwitching.gif) ## 5、窗口抖动 + [运行 ShakeWindow.py](ShakeWindow.py) 通过`QPropertyAnimation`对控件的pos属性进行死去活来的修改 @@ -128,6 +135,7 @@ def findClose(points): ![ShakeWindow](ScreenShot/ShakeWindow.gif) ## 6、窗口翻转动画(仿QQ) + [运行 FlipWidgetAnimation.py](FlipWidgetAnimation.py) 1. 用了两个`QLabel`来显示模拟的图片界面,并实现鼠标点击模拟真实的窗口对应位置点击 @@ -136,4 +144,4 @@ def findClose(points): 4. 通过`setWindowOpacity`控制主窗口的显示隐藏(保留任务栏),当然也可以用`hide` 5. 动画窗口`FlipWidget.py`主要实现两张图片的翻转显示,考虑到0-90和90-180之前的情况,以及图片的缩放动画 -![FlipWidgetAnimation](ScreenShot/FlipWidgetAnimation.gif) \ No newline at end of file +![FlipWidgetAnimation](ScreenShot/FlipWidgetAnimation.gif) diff --git a/QPropertyAnimation/RlatticeEffect.py b/QPropertyAnimation/RlatticeEffect.py index a57c37b1..405ca095 100644 --- a/QPropertyAnimation/RlatticeEffect.py +++ b/QPropertyAnimation/RlatticeEffect.py @@ -7,19 +7,29 @@ @site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: RlatticeEffect -@description: +@description: """ + from random import random from time import time try: - from PyQt5.QtCore import QPropertyAnimation, QObject, QEasingCurve, Qt, QRectF, pyqtSignal, pyqtProperty - from PyQt5.QtGui import QColor, QPainterPath, QPainter + from PyQt5.QtCore import ( + QEasingCurve, + QObject, + QPropertyAnimation, + QRectF, + Qt, + pyqtProperty, + pyqtSignal, + ) + from PyQt5.QtGui import QColor, QPainter, QPainterPath from PyQt5.QtWidgets import QApplication, QWidget except ImportError: - from PySide2.QtCore import QPropertyAnimation, QObject, QEasingCurve, Qt, QRectF, Signal as pyqtSignal, \ - Property as pyqtProperty - from PySide2.QtGui import QColor, QPainterPath, QPainter + from PySide2.QtCore import Property as pyqtProperty + from PySide2.QtCore import QEasingCurve, QObject, QPropertyAnimation, QRectF, Qt + from PySide2.QtCore import Signal as pyqtSignal + from PySide2.QtGui import QColor, QPainter, QPainterPath from PySide2.QtWidgets import QApplication, QWidget try: @@ -27,14 +37,12 @@ getDistance = pointtool.getDistance findClose = pointtool.findClose -except: +except Exception: import math - def getDistance(p1, p2): return math.pow(p1.x - p2.x, 2) + math.pow(p1.y - p2.y, 2) - def findClose(points): plen = len(points) for i in range(plen): @@ -59,13 +67,12 @@ def findClose(points): class Target: - def __init__(self, x, y): self.x = x self.y = y -class Point(QObject): +class Point(QObject): # type: ignore valueChanged = pyqtSignal(int) def __init__(self, x, ox, y, oy, *args, **kwargs): @@ -87,12 +94,14 @@ def __init__(self, x, ox, y, oy, *args, **kwargs): def initAnimation(self): # 属性动画 - if not hasattr(self, 'xanimation'): + if not hasattr(self, "xanimation"): self.xanimation = QPropertyAnimation( - self, b'x', self, easingCurve=QEasingCurve.InOutSine) + self, b"x", self, easingCurve=QEasingCurve.InOutSine + ) self.xanimation.valueChanged.connect(self.valueChanged.emit) self.yanimation = QPropertyAnimation( - self, b'y', self, easingCurve=QEasingCurve.InOutSine) + self, b"y", self, easingCurve=QEasingCurve.InOutSine + ) self.yanimation.valueChanged.connect(self.valueChanged.emit) self.yanimation.finished.connect(self.updateAnimation) self.updateAnimation() @@ -100,7 +109,7 @@ def initAnimation(self): def updateAnimation(self): self.xanimation.stop() self.yanimation.stop() - duration = (1 + random()) * 1000 + duration = int((1 + random()) * 1000) self.xanimation.setDuration(duration) self.yanimation.setDuration(duration) self.xanimation.setStartValue(self.__x) @@ -111,24 +120,23 @@ def updateAnimation(self): self.yanimation.start() @pyqtProperty(float) - def x(self): + def x(self): # type: ignore return self._x - @x.setter + @x.setter # type: ignore def x(self, x): self._x = x @pyqtProperty(float) - def y(self): + def y(self): # type: ignore return self._y - @y.setter + @y.setter # type: ignore def y(self, y): self._y = y -class Window(QWidget): - +class Window(QWidget): # type: ignore def __init__(self, *args, **kwargs): super(Window, self).__init__(*args, **kwargs) self.setMouseTracking(True) @@ -211,19 +219,22 @@ def animate(self, painter): painter.save() painter.setPen(Qt.NoPen) painter.setBrush(p.circleColor) - painter.drawRoundedRect(QRectF( - p.x - p.radius, p.y - p.radius, 2 * p.radius, 2 * p.radius), p.radius, p.radius) + painter.drawRoundedRect( + QRectF(p.x - p.radius, p.y - p.radius, 2 * p.radius, 2 * p.radius), + p.radius, + p.radius, + ) painter.restore() # 开启动画 p.initAnimation() -if __name__ == '__main__': - import sys +if __name__ == "__main__": import cgitb + import sys - cgitb.enable(format='text') + cgitb.enable(format="text") app = QApplication(sys.argv) w = Window() diff --git a/QProxyStyle/README.md b/QProxyStyle/README.md index d64072bf..2ec8a71c 100644 --- a/QProxyStyle/README.md +++ b/QProxyStyle/README.md @@ -5,6 +5,7 @@ - [QTabWidget 角落控件位置](#2qtabwidget-角落控件位置) ## 1、QTabWidget Tab文字方向 + [运行 TabTextDirection.py](TabTextDirection.py) 1. 通过 `app.setStyle(TabBarStyle())` 设置代理样式 @@ -14,6 +15,7 @@ ![TabTextDirection](ScreenShot/TabTextDirection.png) ## 2、QTabWidget 角落控件位置 + [运行 TabCornerWidget.py](TabCornerWidget.py) 1. 通过 `app.setStyle(TabCornerStyle())` 设置代理样式 @@ -22,4 +24,4 @@ 原理是通过代理样式中对 `SE_TabWidgetRightCorner` 计算的结果进行校正,使得角落控件占满右边空白位置, 然后再配合自定义控件中使用 `QSpacerItem` 占据右边位置使得 + 号按钮居左,表现效果为 + 号按钮跟随标签的增加和减少 -![TabCornerStyle](ScreenShot/TabCornerStyle.png) \ No newline at end of file +![TabCornerStyle](ScreenShot/TabCornerStyle.png) diff --git a/QPushButton/Data/Images/avatar.jpg b/QPushButton/Data/Images/avatar.jpg new file mode 100644 index 00000000..32856da1 Binary files /dev/null and b/QPushButton/Data/Images/avatar.jpg differ diff --git a/QPushButton/README.md b/QPushButton/README.md index 2328a51e..7467438d 100644 --- a/QPushButton/README.md +++ b/QPushButton/README.md @@ -5,8 +5,11 @@ - [按钮底部线条进度](#2按钮底部线条进度) - [按钮文字旋转进度](#3按钮文字旋转进度) - [按钮常用信号](#4按钮常用信号) + - [旋转动画按钮](#5旋转动画按钮) + - [弹性动画按钮](#6弹性动画按钮) ## 1、普通样式 + [运行 NormalStyle.py](NormalStyle.py) 主要改变背景颜色、鼠标按下颜色、鼠标悬停颜色、圆角、圆形、文字颜色 @@ -14,6 +17,7 @@ ![NormalStyle](ScreenShot/NormalStyle.gif) ## 2、按钮底部线条进度 + [运行 BottomLineProgress.py](BottomLineProgress.py) 在按钮下方画一条线,根据百分值绘制 @@ -21,6 +25,7 @@ ![BottomLineProgress](ScreenShot/BottomLineProgress.gif) ## 3、按钮文字旋转进度 + [运行 FontRotate.py](FontRotate.py) 利用字体,使用FontAwesome字体来显示一个圆形进度条,然后利用旋转动画 @@ -28,9 +33,22 @@ ![FontRotate](ScreenShot/FontRotate.gif) ## 4、按钮常用信号 + [运行 SignalsExample.py](SignalsExample.py) -根据官网文档 https://doc.qt.io/qt-5/qabstractbutton.html#signals 中的信号介绍编写 +根据官网文档 中的信号介绍编写 按钮的点击、按下、释放、选中信号演示 -![SignalsExample](ScreenShot/SignalsExample.gif) \ No newline at end of file +![SignalsExample](ScreenShot/SignalsExample.gif) + +## 5、旋转动画按钮 + +[运行 RotateButton.py](RotateButton.py) + +![RotateButton](ScreenShot/RotateButton.gif) + +## 6、弹性动画按钮 + +[运行 RubberBandButton.py](RubberBandButton.py) + +![RubberBandButton](ScreenShot/RubberBandButton.gif) diff --git a/QPushButton/RotateButton.py b/QPushButton/RotateButton.py new file mode 100644 index 00000000..e9b9f084 --- /dev/null +++ b/QPushButton/RotateButton.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2019年1月2日 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: Widgets.RotateButton +@description: +""" + +import os +import sys + +try: + from PyQt5.QtCore import QPointF, QPropertyAnimation, QRectF, Qt, pyqtProperty + from PyQt5.QtGui import QColor, QImage, QPainter, QPainterPath, QPixmap + from PyQt5.QtWidgets import ( + QGraphicsDropShadowEffect, + QPushButton, + QStyle, + QStyleOptionButton, + QStylePainter, + QApplication, + QWidget, + QHBoxLayout, + ) +except ImportError: + from PySide2.QtCore import QPointF, QPropertyAnimation, QRectF, Qt, pyqtProperty + from PySide2.QtGui import QColor, QImage, QPainter, QPainterPath, QPixmap + from PySide2.QtWidgets import ( + QGraphicsDropShadowEffect, + QPushButton, + QStyle, + QStyleOptionButton, + QStylePainter, + QApplication, + QWidget, + QHBoxLayout, + ) + + +class RotateButton(QPushButton): + + STARTVALUE = 0 # 起始旋转角度 + ENDVALUE = 360 # 结束旋转角度 + DURATION = 540 # 动画完成总时间 + + def __init__(self, *args, **kwargs): + super(RotateButton, self).__init__(*args, **kwargs) + self.setCursor(Qt.PointingHandCursor) + self._angle = 0 # 角度 + self._padding = 10 # 阴影边距 + self._image = "" # 图片路径 + self._shadowColor = QColor(33, 33, 33) # 阴影颜色 + self._pixmap = None # 图片对象 + # 属性动画 + self._animation = QPropertyAnimation(self, b"angle", self) + self._animation.setLoopCount(1) # 只循环一次 + + def paintEvent(self, event): + """绘制事件""" + text = self.text() + option = QStyleOptionButton() + self.initStyleOption(option) + option.text = "" # 不绘制文字 + painter = QStylePainter(self) + painter.setRenderHint(QStylePainter.Antialiasing) + painter.setRenderHint(QStylePainter.HighQualityAntialiasing) + painter.setRenderHint(QStylePainter.SmoothPixmapTransform) + painter.drawControl(QStyle.CE_PushButton, option) + # 变换坐标为正中间 + painter.translate(self.rect().center()) + painter.rotate(self._angle) # 旋转 + + # 绘制图片 + if self._pixmap and not self._pixmap.isNull(): + w = self.width() + h = self.height() + pos = QPointF(-self._pixmap.width() / 2, -self._pixmap.height() / 2) + painter.drawPixmap(pos, self._pixmap) + elif text: + # 在变换坐标后的正中间画文字 + fm = self.fontMetrics() + w = fm.width(text) + h = fm.height() + rect = QRectF(0 - w * 2, 0 - h, w * 2 * 2, h * 2) + painter.drawText(rect, Qt.AlignCenter, text) + else: + super(RotateButton, self).paintEvent(event) + + def enterEvent(self, _): + """鼠标进入事件""" + # 设置阴影 + # 边框阴影效果 + effect = QGraphicsDropShadowEffect(self) + effect.setBlurRadius(self._padding * 2) + effect.setOffset(0, 0) + effect.setColor(self._shadowColor) + self.setGraphicsEffect(effect) + + # 开启旋转动画 + self._animation.stop() + cv = self._animation.currentValue() or self.STARTVALUE + self._animation.setDuration( + self.DURATION if cv == 0 else int(cv / self.ENDVALUE * self.DURATION) + ) + self._animation.setStartValue(cv) + self._animation.setEndValue(self.ENDVALUE) + self._animation.start() + + def leaveEvent(self, _): + """鼠标离开事件""" + # 取消阴影 + self.setGraphicsEffect(None) + + # 旋转动画 + self._animation.stop() + cv = self._animation.currentValue() or self.ENDVALUE + self._animation.setDuration(int(cv / self.ENDVALUE * self.DURATION)) + self._animation.setStartValue(cv) + self._animation.setEndValue(self.STARTVALUE) + self._animation.start() + + def setPixmap(self, path): + if not os.path.exists(path): + self._image = "" + self._pixmap = None + return + self._image = path + size = ( + max( + min(self.width(), self.height()), + min(self.minimumWidth(), self.minimumHeight()), + ) + - self._padding + ) # 需要边距的边框 + radius = int(size / 2) + image = QImage(size, size, QImage.Format_ARGB32_Premultiplied) + image.fill(Qt.transparent) # 填充背景为透明 + pixmap = QPixmap(path).scaled( + size, size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation + ) + # QPainter + painter = QPainter() + painter.begin(image) + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setRenderHint(QPainter.SmoothPixmapTransform, True) + # QPainterPath + path = QPainterPath() + path.addRoundedRect(0, 0, size, size, radius, radius) + # 切割圆 + painter.setClipPath(path) + painter.drawPixmap(0, 0, pixmap) + painter.end() + self._pixmap = QPixmap.fromImage(image) + self.update() + + def pixmap(self): + return self._pixmap + + @pyqtProperty(str) + def image(self): + return self._image + + @image.setter + def image(self, path): + self.setPixmap(path) + + @pyqtProperty(int) + def angle(self): + return self._angle + + @angle.setter + def angle(self, value): + self._angle = value + self.update() + + @pyqtProperty(int) + def padding(self): + return self._padding + + @padding.setter + def padding(self, value): + self._padding = value + + @pyqtProperty(QColor) + def shadowColor(self): + return self._shadowColor + + @shadowColor.setter + def shadowColor(self, color): + self._shadowColor = QColor(color) + + +class TestWindow(QWidget): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + layout = QHBoxLayout(self) + + btn = RotateButton("pyqt.site", self) + btn.setMinimumHeight(96) + btn.setToolTip("旋转按钮") + layout.addWidget(btn) + + btn = RotateButton("", self) + btn.setMinimumHeight(96) + btn.setObjectName("imageLabel1") + btn.setPixmap("./Data/Images/avatar.jpg") + layout.addWidget(btn) + + btn = RotateButton("", self) + btn.setMinimumHeight(96) + btn.setObjectName("imageLabel2") + layout.addWidget(btn) + + +if __name__ == "__main__": + import cgitb + + cgitb.enable(1, None, 5, "text") + + # cd to current dir + os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) + + app = QApplication(sys.argv) + app.setStyleSheet( + """ + RotateButton { + font-size: 48px; + } + #imageLabel1, #imageLabel2 { + background: transparent; + } + #imageLabel2 { + qproperty-image: "./Data/Images/avatar.jpg"; + } + """ + ) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QPushButton/RubberBandButton.py b/QPushButton/RubberBandButton.py new file mode 100644 index 00000000..75af92eb --- /dev/null +++ b/QPushButton/RubberBandButton.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2019年1月3日 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: Widgets.RubberBandButton +@description: +""" + +try: + from PyQt5.QtCore import ( + QEasingCurve, + QParallelAnimationGroup, + QPropertyAnimation, + QRectF, + Qt, + pyqtProperty, + ) + from PyQt5.QtGui import QColor, QPainter + from PyQt5.QtWidgets import ( + QPushButton, + QStyle, + QStyleOptionButton, + QStylePainter, + QWidget, + QApplication, + QHBoxLayout, + ) +except ImportError: + from PySide2.QtCore import ( + QEasingCurve, + QParallelAnimationGroup, + QPropertyAnimation, + QRectF, + Qt, + Property as pyqtProperty, + ) + from PySide2.QtGui import QColor, QPainter + from PySide2.QtWidgets import ( + QPushButton, + QStyle, + QStyleOptionButton, + QApplication, + QStylePainter, + QWidget, + QHBoxLayout, + ) + + +class RubberBandButton(QPushButton): + + def __init__(self, *args, **kwargs): + self._bgcolor = QColor(kwargs.pop("bgcolor", Qt.green)) + super(RubberBandButton, self).__init__(*args, **kwargs) + self.setFlat(True) + self.setCursor(Qt.PointingHandCursor) + self._width = 0 + self._height = 0 + + def paintEvent(self, event): + self._initAnimate() + painter = QStylePainter(self) + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setRenderHint(QPainter.HighQualityAntialiasing, True) + painter.setRenderHint(QPainter.SmoothPixmapTransform, True) + painter.setBrush(QColor(self._bgcolor)) + painter.setPen(QColor(self._bgcolor)) + painter.drawEllipse( + QRectF( + (self.minimumWidth() - self._width) / 2, + (self.minimumHeight() - self._height) / 2, + self._width, + self._height, + ) + ) + # 绘制本身的文字和图标 + options = QStyleOptionButton() + options.initFrom(self) + size = options.rect.size() + size.transpose() + options.rect.setSize(size) + options.features = QStyleOptionButton.Flat + options.text = self.text() + options.icon = self.icon() + options.iconSize = self.iconSize() + painter.drawControl(QStyle.CE_PushButton, options) + event.accept() + + def _initAnimate(self): + if hasattr(self, "_animate"): + return + self._width = self.minimumWidth() * 7 / 8 + self._height = self.minimumHeight() * 7 / 8 + # self._width=175 + # self._height=175 + + # 宽度动画 + wanimate = QPropertyAnimation(self, b"rWidth") + wanimate.setEasingCurve(QEasingCurve.OutElastic) + wanimate.setDuration(700) + wanimate.valueChanged.connect(self.update) + + # 插入宽度线性值 + wanimate.setKeyValueAt(0, self._width) + # wanimate.setKeyValueAt(0.1, 180) + # wanimate.setKeyValueAt(0.2, 185) + # wanimate.setKeyValueAt(0.3, 190) + # wanimate.setKeyValueAt(0.4, 195) + wanimate.setKeyValueAt(0.5, self._width + 6) + # wanimate.setKeyValueAt(0.6, 195) + # wanimate.setKeyValueAt(0.7, 190) + # wanimate.setKeyValueAt(0.8, 185) + # wanimate.setKeyValueAt(0.9, 180) + wanimate.setKeyValueAt(1, self._width) + + # 高度动画 + hanimate = QPropertyAnimation(self, b"rHeight") + hanimate.setEasingCurve(QEasingCurve.OutElastic) + hanimate.setDuration(700) + + # 插入高度线性值 + hanimate.setKeyValueAt(0, self._height) + # hanimate.setKeyValueAt(0.1, 170) + # hanimate.setKeyValueAt(0.3, 165) + hanimate.setKeyValueAt(0.5, self._height - 6) + # hanimate.setKeyValueAt(0.7, 165) + # hanimate.setKeyValueAt(0.9, 170) + hanimate.setKeyValueAt(1, self._height) + + # 设置动画组 + self._animate = QParallelAnimationGroup(self) + self._animate.addAnimation(wanimate) + self._animate.addAnimation(hanimate) + + def enterEvent(self, event): + super(RubberBandButton, self).enterEvent(event) + self._animate.stop() + self._animate.start() + + @pyqtProperty(int) + def rWidth(self): + return self._width + + @rWidth.setter + def rWidth(self, value): + self._width = value + + @pyqtProperty(int) + def rHeight(self): + return self._height + + @rHeight.setter + def rHeight(self, value): + self._height = value + + @pyqtProperty(QColor) + def bgColor(self): + return self._bgcolor + + @bgColor.setter + def bgColor(self, color): + self._bgcolor = QColor(color) + + +class TestWindow(QWidget): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + layout = QHBoxLayout(self) + layout.addWidget(RubberBandButton("d", self, bgcolor="#01847E")) + layout.addWidget(RubberBandButton("v", self, bgcolor="#7FA91F")) + layout.addWidget(RubberBandButton("N", self, bgcolor="#FC8416")) + layout.addWidget(RubberBandButton("U", self, bgcolor="#66D3c0")) + layout.addWidget(RubberBandButton("a", self, bgcolor="#F28195")) + + +if __name__ == "__main__": + import cgitb, sys + + cgitb.enable(1, None, 5, "text") + + app = QApplication(sys.argv) + app.setStyleSheet( + """ + RubberBandButton { + min-width: 100px; + max-width: 100px; + min-height: 100px; + max-height: 100px; + border: none; + color: white; + outline: none; + margin: 4px; + font-family: webdings; + font-size: 60px; + } + """ + ) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QPushButton/ScreenShot/RotateButton.gif b/QPushButton/ScreenShot/RotateButton.gif new file mode 100644 index 00000000..6b1c3aa4 Binary files /dev/null and b/QPushButton/ScreenShot/RotateButton.gif differ diff --git a/QPushButton/ScreenShot/RubberBandButton.gif b/QPushButton/ScreenShot/RubberBandButton.gif new file mode 100644 index 00000000..dcf1e81a Binary files /dev/null and b/QPushButton/ScreenShot/RubberBandButton.gif differ diff --git a/QScrollArea/README.md b/QScrollArea/README.md index 81073095..52294bd5 100644 --- a/QScrollArea/README.md +++ b/QScrollArea/README.md @@ -4,6 +4,7 @@ - [仿QQ设置面板](#1仿QQ设置面板) ## 1、仿QQ设置面板 + [运行 QQSettingPanel.py](QQSettingPanel.py) | [查看 setting.ui](Data/setting.ui) 1. 左侧为`QListWidget`,右侧使用`QScrollArea`设置`QVBoxLayout`,然后依次往里面添加QWidget @@ -12,9 +13,10 @@ 2. 左侧list添加item时给定右侧对应的widget变量值 相关事件: + 1. 绑定左侧`QListWidget`的`itemClicked`的到该item的索引 2. 绑定右侧滚动条的`valueChanged`事件得到pos 注意:当`itemClicked`时定位滚动条的值时,需要设置一个标志位用来避免`valueChanged`重复调用item的定位 -![QQSettingPanel](ScreenShot/QQSettingPanel.gif) \ No newline at end of file +![QQSettingPanel](ScreenShot/QQSettingPanel.gif) diff --git a/QScrollBar/README.md b/QScrollBar/README.md index 1de1b8b0..6445e59a 100644 --- a/QScrollBar/README.md +++ b/QScrollBar/README.md @@ -4,6 +4,7 @@ - [滚动条样式美化](#1滚动条样式美化) ## 1、滚动条样式美化 + [运行 StyleScrollBar.py](StyleScrollBar.py) 使用QSS和图片对滚动条进行美化(horizontal 横向、vertical 纵向) diff --git a/QSerialPort/README.md b/QSerialPort/README.md index a0418b79..00c3de0b 100644 --- a/QSerialPort/README.md +++ b/QSerialPort/README.md @@ -4,9 +4,10 @@ - [串口调试小助手](#1串口调试小助手) ## 1、串口调试小助手 + [运行 SerialDebugAssistant.py](SerialDebugAssistant.py) | [查看 UiSerialPort.ui](Data/UiSerialPort.ui) -用`QSerialPort`写了个类似串口调试小助手的工具, 这个类的官方资料: http://doc.qt.io/qt-5/qserialport.html +用`QSerialPort`写了个类似串口调试小助手的工具, 这个类的官方资料: 1. 通过`QSerialPortInfo.availablePorts()` 获取所有可用的串口 1. `QSerialPort.setPortName` 设置串口名 @@ -16,5 +17,4 @@ 1. `QSerialPort.setStopBits` 设置停止位 1. `QSerialPort.setFlowControl` 设置流控制 - -![SerialDebugAssistant](ScreenShot/SerialDebugAssistant.gif) \ No newline at end of file +![SerialDebugAssistant](ScreenShot/SerialDebugAssistant.gif) diff --git a/QSlider/README.md b/QSlider/README.md index 1dc8803a..ee33d3be 100644 --- a/QSlider/README.md +++ b/QSlider/README.md @@ -6,6 +6,7 @@ - [低频率值变化](#3低频率值变化) ## 1、滑动条点击定位 + [运行 ClickJumpSlider.py](ClickJumpSlider.py) 1. `QSlider`对鼠标点击然后跳转到该位置的支持不是很好,通过重写鼠标点击事件`mousePressEvent`来达到效果 @@ -41,13 +42,15 @@ def mousePressEvent(self, event): ![ClickJumpSlider](ScreenShot/ClickJumpSlider.gif) ## 2、双层圆环样式 + [运行 QssQSlider.py](QssQSlider.py) | [运行 PaintQSlider.py](PaintQSlider.py) ![QssQSlider](ScreenShot/QssQSlider.gif) ![PaintQSlider](ScreenShot/PaintQSlider.gif) ## 3、低频率值变化 -[运行 LfSlider.py](LfSlider.py) + +[运行 LfSlider.py](LfSlider.py) 覆盖了`valueChanged`信号,通过使用定时器来延迟发送值变化,如果无法覆盖信号则可以自定义一个新的信号 diff --git a/QSplashScreen/README.md b/QSplashScreen/README.md index 51da420e..6c16f8bb 100644 --- a/QSplashScreen/README.md +++ b/QSplashScreen/README.md @@ -4,8 +4,9 @@ - [启动画面动画](#1启动画面动画) ## 1、启动画面动画 + [运行 GifSplashScreen.py](GifSplashScreen.py) 结合 `QMovie` 的 `frameChanged` 信号 不停地设置新的pixmap图片 -![GifSplashScreen](ScreenShot/GifSplashScreen.gif) \ No newline at end of file +![GifSplashScreen](ScreenShot/GifSplashScreen.gif) diff --git a/QSplitter/README.md b/QSplitter/README.md index f52a7ca3..e7844300 100644 --- a/QSplitter/README.md +++ b/QSplitter/README.md @@ -4,6 +4,7 @@ - [分割窗口的分割条重绘](#1分割窗口的分割条重绘) ## 1、分割窗口的分割条重绘 + [运行 RewriteHandle.py](RewriteHandle.py) 1. 原理在于`QSplitter`在创建分割条的时候会调用`createHandle`函数 @@ -11,4 +12,4 @@ 1. 通过`QSplitterHandle`的`paintEvent`实现绘制其它形状, 1. 重写`mousePressEvent`和`mouseMoveEvent`来实现鼠标的其它事件 -![RewriteHandle](ScreenShot/RewriteHandle.gif) \ No newline at end of file +![RewriteHandle](ScreenShot/RewriteHandle.gif) diff --git a/QStackedWidget/README.md b/QStackedWidget/README.md index 2329fc3e..58996c68 100644 --- a/QStackedWidget/README.md +++ b/QStackedWidget/README.md @@ -4,6 +4,7 @@ - [左侧选项卡](#1左侧选项卡) ## 1、左侧选项卡 + [运行 LeftTabStacked.py](LeftTabStacked.py) 本来使用`QTabWidget`可以实现多标签页面,但是当标签在左侧时会出现文字方向不对的问题, @@ -14,7 +15,7 @@ 2. 右侧添加`QWidget`的时候有两种方案 1. 左侧list根据序号来索引,右侧添加widget时给定带序号的变量名,如widget_0,widget_1,widget_2之类的,这样可以直接根据`QListWidget`的序号关联起来 2. 左侧list添加item时给定右侧对应的widget变量值 - -PS: 用设计设的做法 : https://www.jianshu.com/p/dac62b5c225c + +PS: 用设计设的做法 : ![LeftTabStacked](ScreenShot/LeftTabStacked.gif) diff --git a/QSystemTrayIcon/README.md b/QSystemTrayIcon/README.md index b333ea50..268c3036 100755 --- a/QSystemTrayIcon/README.md +++ b/QSystemTrayIcon/README.md @@ -10,7 +10,7 @@ 选择 Minimize to Tray 在关闭窗口时最小化到系统托盘。 -> Reference: https://evileg.com/en/post/68/ +> Reference: ![MinimizeToTray](ScreenShot/MinimizeToTray.gif) @@ -18,4 +18,4 @@ [运行 TrayNotify.py](TrayNotify.py) -通过定时器设置不同图标来实现闪烁。 \ No newline at end of file +通过定时器设置不同图标来实现闪烁。 diff --git a/QTabWidget/HideCloseButton.py b/QTabWidget/HideCloseButton.py new file mode 100644 index 00000000..cd41597d --- /dev/null +++ b/QTabWidget/HideCloseButton.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/12 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: HideCloseButton.py +@description: +""" + +from PyQt5.QtWidgets import QApplication, QLabel, QTabBar, QTabWidget + + +class HideCloseButton(QTabWidget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.resize(800, 600) + self.setTabsClosable(True) + self.tabCloseRequested.connect(self.onCloseTab) + + # add tabs + self.addTab(QLabel("Home", self), "Home") + for i in range(10): + title = "Tab {}".format(i) + self.addTab(QLabel(title, self), title) + + # hide first tab's close button + btn = self.tabBar().tabButton(0, QTabBar.RightSide) + btn.close() + self.tabBar().setTabButton(0, QTabBar.RightSide, None) + + def onCloseTab(self, index): + w = self.widget(index) + if w: + w.close() + self.removeTab(index) + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = HideCloseButton() + w.show() + sys.exit(app.exec_()) diff --git a/QTabWidget/README.md b/QTabWidget/README.md index e69de29b..4ca37677 100644 --- a/QTabWidget/README.md +++ b/QTabWidget/README.md @@ -0,0 +1,16 @@ +# QTabWidget + +- 目录 + - [隐藏指定Tab关闭按钮](#1隐藏指定Tab关闭按钮) + +## 1、隐藏指定Tab关闭按钮 + +[运行 HideCloseButton.py](HideCloseButton.py) + +```python +btn = self.tabBar().tabButton(0, QTabBar.RightSide) +btn.close() +self.tabBar().setTabButton(0, QTabBar.RightSide, None) +``` + +![HideCloseButton](ScreenShot/HideCloseButton.png) diff --git a/QTabWidget/ScreenShot/HideCloseButton.png b/QTabWidget/ScreenShot/HideCloseButton.png new file mode 100644 index 00000000..dddd2fcb Binary files /dev/null and b/QTabWidget/ScreenShot/HideCloseButton.png differ diff --git a/QTableView/README.md b/QTableView/README.md index 1d79c7a7..92dbf3c7 100644 --- a/QTableView/README.md +++ b/QTableView/README.md @@ -4,6 +4,7 @@ - [表格内容复制](#1表格内容复制) ## 1、表格内容复制 + [运行 CopyContent.py](CopyContent.py) 1. 通过构造一个和选中区域一样的空数组,然后对数组进行填充形成表格 @@ -11,4 +12,3 @@ 1. 把字符串复制到剪切板中 ![CopyContent1](ScreenShot/CopyContent1.png) ![CopyContent2](ScreenShot/CopyContent2.png) - diff --git a/QTableWidget/README.md b/QTableWidget/README.md index 7ed80337..74db3b16 100644 --- a/QTableWidget/README.md +++ b/QTableWidget/README.md @@ -5,6 +5,7 @@ - [表格嵌入日历,下拉框,进度条,按钮](#2表格嵌入) ## 1、Sqlalchemy动态拼接字段查询显示表格 + [运行 SqlQuery.py](SqlQuery.py) | [查看 mainui.ui](Data/mainui.ui) 通过判断界面中选择的条件对`Sqlalchemy`的`model`进行字段拼接从而实现按条件查询 @@ -12,6 +13,7 @@ ![SqlQuery](ScreenShot/SqlQuery.png) ## 2、TableWidget嵌入部件 + [运行 TableWidget.py](TableWidget.py) 点击开始按钮,进度条开始 ![嵌入小部件](ScreenShot/table.png) diff --git a/QTextBrowser/README.md b/QTextBrowser/README.md index dee5984c..dfe873bf 100644 --- a/QTextBrowser/README.md +++ b/QTextBrowser/README.md @@ -4,6 +4,7 @@ - [动态加载图片](#1动态加载图片) ## 1、动态加载图片 + [运行 DynamicRes.py](DynamicRes.py) 动态加载资源有多种方式,这里主要介绍 [addResource](https://doc.qt.io/qt-5/qtextdocument.html#addResource) 和 [loadResource](https://doc.qt.io/qt-5/qtextbrowser.html#loadResource) 函数 @@ -11,4 +12,4 @@ 1、通过 `self.textBrowser.document().addResource(QTextDocument.ImageResource, QUrl('dynamic:/images/weixin.png'), img)` 向文档中注册新的资源索引,类似QRC 2、通过重载 `loadResource` 函数可以监听到所有的资源加载,然后动态返回内容 -![DynamicRes](ScreenShot/DynamicRes.gif) \ No newline at end of file +![DynamicRes](ScreenShot/DynamicRes.gif) diff --git a/QTextEdit/README.md b/QTextEdit/README.md index 98bec9d8..f6a4a672 100644 --- a/QTextEdit/README.md +++ b/QTextEdit/README.md @@ -4,8 +4,9 @@ - [文本查找高亮](#1文本查找高亮) ## 1、文本查找高亮 + [运行 HighlightText.py](HighlightText.py) 主要用到`mergeCurrentCharFormat`函数 -![HighlightText](ScreenShot/HighlightText.gif) \ No newline at end of file +![HighlightText](ScreenShot/HighlightText.gif) diff --git a/QThread/README.md b/QThread/README.md index b9661c21..e1068ded 100644 --- a/QThread/README.md +++ b/QThread/README.md @@ -2,22 +2,25 @@ - 目录 - [继承QThread](#1继承QThread) - - [moveToThread](#2moveToThread) + - [moveToThread](#2movetothread) - [线程挂起恢复](#3线程挂起恢复) - [线程休眠唤醒](#4线程休眠唤醒) - [线程退出](#5线程退出) ## 1、继承QThread + [运行 InheritQThread.py](InheritQThread.py) ![InheritQThread](ScreenShot/InheritQThread.png) ## 2、moveToThread + [运行 moveToThread.py](moveToThread.py) ![moveToThread](ScreenShot/InheritQThread.png) ## 3、线程挂起恢复 + [运行 SuspendThread.py](SuspendThread.py) 注意,这里只是简单演示,在应用这些代码时要小心 @@ -31,6 +34,7 @@ ![SuspendThread](ScreenShot/SuspendThread.gif) ## 4、线程休眠唤醒 + [运行 WakeupThread.py](WakeupThread.py) 使用 `QWaitCondition` 的 `wait` 和 `wakeAll` 方法 @@ -38,8 +42,9 @@ ![WakeupThread](ScreenShot/WakeupThread.gif) ## 5、线程退出 + [运行 QuitThread.py](QuitThread.py) `isInterruptionRequested` 和 `requestInterruption` 函数作为退出标识调用 -![QuitThread](ScreenShot/QuitThread.jpg) \ No newline at end of file +![QuitThread](ScreenShot/QuitThread.jpg) diff --git a/QTreeView/Data/serializewidget.ui b/QTreeView/Data/serializewidget.ui new file mode 100644 index 00000000..9204a9e3 --- /dev/null +++ b/QTreeView/Data/serializewidget.ui @@ -0,0 +1,271 @@ + + + SerializeWidget + + + + 0 + 0 + 800 + 600 + + + + TestSerialize + + + + + + Qt::Horizontal + + + false + + + + + 800 + 16777215 + + + + Json View + + + + + + + + + + + + + Widget View + + + + + + 0 + + + + Input + + + + + + + + + CheckBox + + + + + + + RadioButton + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Edit + + + + + + + + + + + + + Correlation + + + + + + Qt::Horizontal + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 0 + + + Qt::Vertical + + + + + + + Qt::Vertical + + + + + + + + View + + + + + + ListView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + TreeView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + TableView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + + + + + + + + + + diff --git a/QTreeView/Lib/qjsonmodel.py b/QTreeView/Lib/qjsonmodel.py new file mode 100644 index 00000000..336ddbd6 --- /dev/null +++ b/QTreeView/Lib/qjsonmodel.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/05 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: qjsonmodel.py +@description: +""" + +import json +from functools import reduce +from typing import Any, List, Union + +try: + from PyQt5.QtCore import QObject, Qt + from PyQt5.QtGui import QStandardItem, QStandardItemModel +except ImportError: + from PySide2.QtCore import QObject, Qt + from PySide2.QtGui import QStandardItem, QStandardItemModel + + +class QJsonItem(QStandardItem): + Sep = "/" + EditRole = Qt.EditRole + KeyRole = Qt.UserRole + 1 + ValueRole = Qt.UserRole + 2 + DatasRole = Qt.UserRole + 3 + PathRole = Qt.UserRole + 4 + + def __init__( + self, + *args, + key: Union["QJsonItem", None] = None, # 上一级key item + value: Union["QJsonItem", None] = None, # 当前value item + editAble: bool = True, + role: int = Qt.UserRole + 1, + ): + super().__init__(*args) + self.setInfo(key, value, None, editAble, role) + + def clear(self): + for _ in range(self.rowCount()): + self.removeRow(0) + + def setInfo( + self, + key: Union["QJsonItem", None] = None, + value: Union["QJsonItem", None] = None, + datas: Any = None, + editAble: bool = True, + role: int = Qt.UserRole + 1, + edit=None, + ): + self.setEditable(editAble) + self._role = role + self.keyItem = key + self.valueItem = value + self.edit = edit + self.datas = datas + + @property + def keyItem(self) -> Union["QJsonItem", None]: + # 上一级key item + item = self.data(self.KeyRole) + if not isinstance(item, QJsonItem): + return None + return item + + @keyItem.setter + def keyItem(self, value: "QJsonItem") -> None: + self.setData(value, self.KeyRole) + + @property + def valueItem(self) -> "QJsonItem": + # 当前value item + return self.data(self.ValueRole) + + @valueItem.setter + def valueItem(self, value: "QJsonItem") -> None: + self.setData(value, self.ValueRole) + + @property + def edit(self) -> Any: + # 设置角色为EditRole的值 + return self.data(self.EditRole) + + @edit.setter + def edit(self, value: Any) -> None: + if value is None: + return + self.setData(value, self.EditRole) + + @property + def datas(self) -> Any: + return self.data(self.DatasRole) + + @datas.setter + def datas(self, value: Any) -> None: + # key item中的原始数据设置 + self.clear() + self.setData(value, self.DatasRole) + if self._role == QJsonItem.ValueRole: + if not isinstance(value, (list, tuple, dict)): + self.edit = value + return + self.__loadData(value) + + @property + def type(self) -> Any: + return type(self.edit) + + @property + def role(self) -> int: + return self._role + + @property + def path(self) -> str: + item = self.keyItem if self._role == self.ValueRole else self + paths = [] + + while item: + paths.append(item.text()) + item = item.keyItem + + paths.reverse() + return QJsonItem.Sep.join(paths) + + def data(self, role: int = Qt.DisplayRole) -> Any: + if role == self.PathRole: + return self.path + + return super().data(role) + + def setData(self, data: Any, role: int = Qt.EditRole): + super().setData(data, role) + + def __loadData(self, data: Any) -> None: + # 数组类型的key是索引, 不允许修改 + isArray = isinstance(data, (list, tuple)) + keyEditAble = not isArray + datas = ( + data.items() + if isinstance(data, dict) + else enumerate(data) + if isArray + else [] + ) + + for key, value in datas: + itemKey = QJsonItem(str(key)) + itemValue = QJsonItem() + + # 先添加items + self.appendRow([itemKey, itemValue]) + + itemKey.setInfo(self, itemValue, value, keyEditAble, edit=key) + # key对应的后面的空列不允许修改 + valueEditAble = not isinstance(value, (list, tuple, dict)) + itemValue.setInfo(itemKey, None, value, valueEditAble, QJsonItem.ValueRole) + + def updateValue(self, value: Any) -> bool: + itemValue: Union[QJsonItem, None] = self.valueItem + if itemValue is None: + return False + if itemValue.datas == value: + return True + + self.datas = value + # key对应的后面的空列不允许修改 + valueEditAble = not isinstance(value, (list, tuple, dict)) + itemValue.setInfo(self, None, value, valueEditAble, QJsonItem.ValueRole) + + return True + + def toObject(self) -> Any: + datas = self.datas + typ = type(datas) + + if typ is dict: + return { + self.child(i, 0).text(): self.child(i, 1).toObject() + for i in range(self.rowCount()) + } + elif typ is list: + return [self.child(i, 0).toObject() for i in range(self.rowCount())] + elif self.valueItem: + return self.valueItem.toObject() + + return self.edit + + +class QJsonModel(QStandardItemModel): + def __init__( + self, + parent: QObject = None, + data: dict = None, + ) -> None: + super().__init__(parent) + self.setHorizontalHeaderLabels(["Key", "Value"]) + self.loadData(data) + + def loadFile( + self, file: str, encoding: str = "utf-8", errors: str = "ignore" + ) -> bool: + with open(file, "rb") as f: + return self.loadJson(f.read().decode(encoding=encoding, errors=errors)) + + def loadJson(self, string: str) -> bool: + return self.loadData(json.loads(string)) + + def loadData(self, data: dict, force: bool = False) -> bool: + if isinstance(data, dict): + if force: + self.clear() + self.__loadData(data) + return True + + return False + + def horizontalHeaderLabels(self) -> List[str]: + return [self.horizontalHeaderItem(i).text() for i in range(self.columnCount())] + + def clear(self): + headers = self.horizontalHeaderLabels() + super().clear() + self.setHorizontalHeaderLabels(headers) + + def findPath( + self, + path: str, + flags=Qt.MatchFixedString + | Qt.MatchCaseSensitive + | Qt.MatchWrap + | Qt.MatchRecursive, + ) -> Union[QJsonItem, None]: + indexes = self.match(self.index(0, 0), QJsonItem.PathRole, path, -1, flags) + indexes = [index for index in indexes if index.isValid()] + return self.itemFromIndex(indexes[0]) if indexes else None # type: ignore + + def updateValue( + self, path: str, value: Any, item: Union[QJsonItem, None] = None + ) -> bool: + item = item or self.findPath(path) + if item is None: + keys = path.split(QJsonItem.Sep) + self.__loadData(reduce(lambda val, key: {key: val}, reversed(keys), value)) + return True + + return item.updateValue(value) + + def __findItem( + self, key: str, parent=None + ) -> Union[QStandardItem, "QJsonItem", None]: + parent = parent or self.invisibleRootItem() + for row in range(parent.rowCount()): + item = parent.child(row) + if item.text() == key: + return item + + return None + + def __createItem(self, key: str, value: Any, parent=None): + parent = parent or self.invisibleRootItem() + + itemKey = QJsonItem(str(key)) + itemValue = QJsonItem() + parent.appendRow([itemKey, itemValue]) + + itemKey.setInfo(parent, itemValue, value, edit=key) + # key对应的后面的空列不允许修改 + valueEditAble = not isinstance(value, (list, tuple, dict)) + itemValue.setInfo(itemKey, None, value, valueEditAble, QJsonItem.ValueRole) + + def __loadData(self, data: Any, parent=None): + if not isinstance(data, dict): # 更新值 + itemKey = parent + if itemKey: + itemKey.updateValue(data) + return + + parent = parent or self.invisibleRootItem() + + for key, value in data.items(): + key = str(key) + itemKey = self.__findItem(key, parent) + if not itemKey: + self.__createItem(key, value, parent) + else: + self.__loadData(value, itemKey) + + def toDict(self) -> dict: + item = self.invisibleRootItem() + + return { + item.child(i).text(): item.child(i).toObject() + for i in range(item.rowCount()) + } + + def toJson(self, ensure_ascii=False, indent=None, **kwargs) -> str: + return json.dumps( + self.toDict(), ensure_ascii=ensure_ascii, indent=indent, **kwargs + ) diff --git a/QTreeView/Lib/qmodelmapper.py b/QTreeView/Lib/qmodelmapper.py new file mode 100644 index 00000000..9d1dc854 --- /dev/null +++ b/QTreeView/Lib/qmodelmapper.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/08 +@file: qmodelmapper.py +@description: +""" + +from copy import deepcopy +from typing import Any, List, Union + +from Lib.qjsonmodel import QJsonModel + +try: + from PyQt5.QtCore import ( + QDateTime, + QMetaProperty, + QModelIndex, + QObject, + Qt, + QTimer, + ) + from PyQt5.QtCore import pyqtSignal as Signal + from PyQt5.QtWidgets import QWidget +except ImportError: + from PySide2.QtCore import ( + QDateTime, + QMetaProperty, + QModelIndex, + QObject, + Qt, + QTimer, + Signal, + ) + from PySide2.QtWidgets import QWidget + + +class QModelMapper(QObject): + Debug = False + valueChanged = Signal() + Signal = ( + "dateTimeChanged", + "currentTextChanged", + "valueChanged", + "toggled", + "textChanged", + ) + Props = ( + "dateTime", + "html", + "plainText", + "currentText", + "checked", + "value", + "text", + ) + + def __init__(self, *args, **kwargs): + self._delay = kwargs.pop("delay", 50) + data = kwargs.pop("data", {}) + super().__init__(*args, **kwargs) + self._old = deepcopy(data) + self._widgetkey = {} + self._keywidget = {} + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.timeout.connect(self.valueChanged.emit) + self._model = QJsonModel(self.parent(), data=data) + self._model.dataChanged.connect(self.onItemDataChanged) + + def bind(self, widget: QWidget, key: str, default: Any = None, prop: str = ""): + if widget not in self._widgetkey: + self._widgetkey[widget] = { + "key": key, + "prop": self.getProperty(widget, prop), + } + self._setValue(widget, key, default) + + # record all widgets for key + if key not in self._keywidget: + self._keywidget[key] = set() + self._keywidget[key].add(widget) + + for signal in self.Signal: + signal = getattr(widget, signal, None) + if signal: + signal.connect(self._setData) + break + + def isModify(self) -> bool: + return self._model.toDict() != self._old + + def getModel(self) -> QJsonModel: + return self._model + + def getData(self) -> dict: + return self._model.toDict() + + def getJson(self, ensure_ascii=False, indent=None, **kwargs) -> str: + return self._model.toJson(ensure_ascii=ensure_ascii, indent=indent, **kwargs) + + def setData(self, data: dict, force: bool = False): + if force: + self._old = deepcopy(data) + else: + self._old.update(data) + + if force: + self._model.blockSignals(True) + self._model.loadData(data, force) + if force: + self._model.blockSignals(False) + + def getProperty( + self, widget: QWidget, prop: str = "" + ) -> Union[QMetaProperty, None]: + qmo = widget.metaObject() + props = [prop] if prop else self.Props + widgetProps = ( + qmo.property(i) + for i in range( + QWidget.staticMetaObject.propertyCount(), qmo.propertyCount() + ) + ) + widgetProps = [ + p for p in widgetProps if p and p.isReadable() and p.isWritable() + ] + + rets = [p for prop in props for p in widgetProps if p.name() == prop] + return rets[0] if rets else None + + def _getDefault(self, widget: QWidget, default: Any = None): + if default: + return default + + prop: Union[QMetaProperty, None] = self._widgetkey[widget]["prop"] + if prop is None: + return None + + value = prop.read(widget) + if value is not None: + if prop.name() == "dateTime": + return value.toString("yyyy-MM-dd HH:mm:ss") + elif prop.name() == "html" and len(widget.property("plainText")) == 0: + return "" + return value + + return None + + def _setValue( + self, + widget: QWidget, + key: str, + default: Any = None, + updated: bool = False, + block=False, + ): + created = False + value: Any = default + + if not updated: + item = self._model.findPath(key) + if item is None: + value = self._getDefault(widget, default) + created = self._model.updateValue(key, value, item) + else: + # value from model + if item.valueItem: + value = item.valueItem.edit + created = default != value + block = True + if value is None: + return + + if block: + widget.blockSignals(True) + + prop: Union[QMetaProperty, None] = self._widgetkey[widget]["prop"] + if prop is not None: + if prop.name() == "dateTime" and isinstance(value, str): + value = QDateTime.fromString(value, Qt.ISODate) + prop.write(widget, value) + if created: + self._timer.start(self._delay) + + if block: + widget.blockSignals(False) + + def _setData(self, *args, **kwargs): + sender = self.sender() + info = self._widgetkey.get(sender, {}) + key = info.get("key", None) + prop: Union[QMetaProperty, None] = info.get("prop", None) + if key is None or prop is None: + return + + value = prop.read(sender) + if value is not None: + if prop.name() == "dateTime": + value = value.toString("yyyy-MM-dd HH:mm:ss") + elif prop.name() == "html" and len(sender.property("plainText")) == 0: + value = "" + + if value is None: + return + + self._timer.start(self._delay) + + # 更新关联的widget + for widget in self._keywidget.get(key, []): + if widget != sender: + if self.Debug: + print(f"view to view({widget}) = {value}") + self._setValue(widget, key, value, updated=True, block=True) + + # 更新关联的model + if self.Debug: + print(f"view({sender}) to model = {value}") + if not self._model.updateValue(key, value): + return + + def onItemDataChanged( + self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: List[int] + ): + item = self._model.itemFromIndex(topLeft) + if not item or Qt.EditRole not in roles or item.valueItem: + return + + path = getattr(item, "path", None) + if not path: + return + + # 更新关联的widget + value = item.edit + for widget in self._keywidget.get(path, []): + if self.Debug: + print(f"model to view({widget}) = {value}") + self._setValue(widget, path, value, updated=True, block=True) diff --git a/QTreeView/Lib/serializewidget.py b/QTreeView/Lib/serializewidget.py new file mode 100644 index 00000000..fcd98edb --- /dev/null +++ b/QTreeView/Lib/serializewidget.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- + +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +try: + from PyQt5 import QtCore, QtGui, QtWidgets +except ImportError: + from PySide2 import QtCore, QtGui, QtWidgets + + +class Ui_SerializeWidget(object): + def setupUi(self, SerializeWidget): + SerializeWidget.setObjectName("SerializeWidget") + SerializeWidget.resize(800, 600) + self.verticalLayout = QtWidgets.QVBoxLayout(SerializeWidget) + self.verticalLayout.setObjectName("verticalLayout") + self.splitter = QtWidgets.QSplitter(SerializeWidget) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setChildrenCollapsible(False) + self.splitter.setObjectName("splitter") + self.groupBox = QtWidgets.QGroupBox(self.splitter) + self.groupBox.setMaximumSize(QtCore.QSize(800, 16777215)) + self.groupBox.setObjectName("groupBox") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.groupBox) + self.horizontalLayout.setObjectName("horizontalLayout") + self.treeJsonView = QtWidgets.QTreeView(self.groupBox) + self.treeJsonView.setObjectName("treeJsonView") + self.horizontalLayout.addWidget(self.treeJsonView) + self.editJsonView = QtWidgets.QTextEdit(self.groupBox) + self.editJsonView.setObjectName("editJsonView") + self.horizontalLayout.addWidget(self.editJsonView) + self.groupBox_2 = QtWidgets.QGroupBox(self.splitter) + self.groupBox_2.setObjectName("groupBox_2") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox_2) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.tabWidget = QtWidgets.QTabWidget(self.groupBox_2) + self.tabWidget.setObjectName("tabWidget") + self.tab = QtWidgets.QWidget() + self.tab.setObjectName("tab") + self.gridLayout = QtWidgets.QGridLayout(self.tab) + self.gridLayout.setObjectName("gridLayout") + self.doubleSpinBox = QtWidgets.QDoubleSpinBox(self.tab) + self.doubleSpinBox.setObjectName("doubleSpinBox") + self.gridLayout.addWidget(self.doubleSpinBox, 2, 1, 1, 1) + self.checkBox = QtWidgets.QCheckBox(self.tab) + self.checkBox.setObjectName("checkBox") + self.gridLayout.addWidget(self.checkBox, 0, 1, 1, 1) + self.radioButton = QtWidgets.QRadioButton(self.tab) + self.radioButton.setObjectName("radioButton") + self.gridLayout.addWidget(self.radioButton, 0, 0, 1, 1) + self.spinBox = QtWidgets.QSpinBox(self.tab) + self.spinBox.setObjectName("spinBox") + self.gridLayout.addWidget(self.spinBox, 2, 0, 1, 1) + self.lineEdit = QtWidgets.QLineEdit(self.tab) + self.lineEdit.setObjectName("lineEdit") + self.gridLayout.addWidget(self.lineEdit, 5, 0, 1, 2) + self.dateTimeEdit = QtWidgets.QDateTimeEdit(self.tab) + self.dateTimeEdit.setObjectName("dateTimeEdit") + self.gridLayout.addWidget(self.dateTimeEdit, 4, 0, 1, 2) + self.comboBox = QtWidgets.QComboBox(self.tab) + self.comboBox.setObjectName("comboBox") + self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1) + self.timeEdit = QtWidgets.QTimeEdit(self.tab) + self.timeEdit.setObjectName("timeEdit") + self.gridLayout.addWidget(self.timeEdit, 3, 0, 1, 1) + self.dateEdit = QtWidgets.QDateEdit(self.tab) + self.dateEdit.setObjectName("dateEdit") + self.gridLayout.addWidget(self.dateEdit, 3, 1, 1, 1) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem, 0, 3, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem1, 6, 0, 1, 1) + self.tabWidget.addTab(self.tab, "") + self.tab_2 = QtWidgets.QWidget() + self.tab_2.setObjectName("tab_2") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.tab_2) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.plainTextEdit = QtWidgets.QPlainTextEdit(self.tab_2) + self.plainTextEdit.setObjectName("plainTextEdit") + self.verticalLayout_4.addWidget(self.plainTextEdit) + self.textEdit = QtWidgets.QTextEdit(self.tab_2) + self.textEdit.setObjectName("textEdit") + self.verticalLayout_4.addWidget(self.textEdit) + self.tabWidget.addTab(self.tab_2, "") + self.tab_3 = QtWidgets.QWidget() + self.tab_3.setObjectName("tab_3") + self.gridLayout_2 = QtWidgets.QGridLayout(self.tab_3) + self.gridLayout_2.setObjectName("gridLayout_2") + self.horizontalSlider = QtWidgets.QSlider(self.tab_3) + self.horizontalSlider.setOrientation(QtCore.Qt.Horizontal) + self.horizontalSlider.setObjectName("horizontalSlider") + self.gridLayout_2.addWidget(self.horizontalSlider, 1, 0, 1, 1) + self.spinBox_2 = QtWidgets.QSpinBox(self.tab_3) + self.spinBox_2.setObjectName("spinBox_2") + self.gridLayout_2.addWidget(self.spinBox_2, 0, 0, 1, 1) + spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_2.addItem(spacerItem2, 2, 0, 1, 1) + self.progressBar = QtWidgets.QProgressBar(self.tab_3) + self.progressBar.setProperty("value", 0) + self.progressBar.setOrientation(QtCore.Qt.Vertical) + self.progressBar.setObjectName("progressBar") + self.gridLayout_2.addWidget(self.progressBar, 0, 1, 3, 1) + self.verticalSlider = QtWidgets.QSlider(self.tab_3) + self.verticalSlider.setOrientation(QtCore.Qt.Vertical) + self.verticalSlider.setObjectName("verticalSlider") + self.gridLayout_2.addWidget(self.verticalSlider, 0, 2, 3, 1) + self.tabWidget.addTab(self.tab_3, "") + self.tab_4 = QtWidgets.QWidget() + self.tab_4.setObjectName("tab_4") + self.gridLayout_3 = QtWidgets.QGridLayout(self.tab_4) + self.gridLayout_3.setObjectName("gridLayout_3") + self.groupBox_3 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_3.setObjectName("groupBox_3") + self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.groupBox_3) + self.verticalLayout_6.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_6.setObjectName("verticalLayout_6") + self.listView = QtWidgets.QListView(self.groupBox_3) + self.listView.setObjectName("listView") + self.verticalLayout_6.addWidget(self.listView) + self.gridLayout_3.addWidget(self.groupBox_3, 0, 0, 1, 1) + self.groupBox_4 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_4.setObjectName("groupBox_4") + self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.groupBox_4) + self.verticalLayout_7.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_7.setObjectName("verticalLayout_7") + self.treeView = QtWidgets.QTreeView(self.groupBox_4) + self.treeView.setObjectName("treeView") + self.verticalLayout_7.addWidget(self.treeView) + self.gridLayout_3.addWidget(self.groupBox_4, 0, 1, 1, 1) + self.groupBox_5 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_5.setObjectName("groupBox_5") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.groupBox_5) + self.verticalLayout_5.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.tableView = QtWidgets.QTableView(self.groupBox_5) + self.tableView.setObjectName("tableView") + self.verticalLayout_5.addWidget(self.tableView) + self.gridLayout_3.addWidget(self.groupBox_5, 1, 0, 1, 2) + self.tabWidget.addTab(self.tab_4, "") + self.verticalLayout_3.addWidget(self.tabWidget) + self.verticalLayout.addWidget(self.splitter) + + self.retranslateUi(SerializeWidget) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(SerializeWidget) + + def retranslateUi(self, SerializeWidget): + _translate = QtCore.QCoreApplication.translate + SerializeWidget.setWindowTitle(_translate("SerializeWidget", "TestSerialize")) + self.groupBox.setTitle(_translate("SerializeWidget", "Json View")) + self.groupBox_2.setTitle(_translate("SerializeWidget", "Widget View")) + self.checkBox.setText(_translate("SerializeWidget", "CheckBox")) + self.radioButton.setText(_translate("SerializeWidget", "RadioButton")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("SerializeWidget", "Input")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("SerializeWidget", "Edit")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("SerializeWidget", "Correlation")) + self.groupBox_3.setTitle(_translate("SerializeWidget", "ListView")) + self.groupBox_4.setTitle(_translate("SerializeWidget", "TreeView")) + self.groupBox_5.setTitle(_translate("SerializeWidget", "TableView")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), _translate("SerializeWidget", "View")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + SerializeWidget = QtWidgets.QWidget() + ui = Ui_SerializeWidget() + ui.setupUi(SerializeWidget) + SerializeWidget.show() + sys.exit(app.exec_()) diff --git a/QTreeView/README.md b/QTreeView/README.md index e69de29b..79327648 100644 --- a/QTreeView/README.md +++ b/QTreeView/README.md @@ -0,0 +1,24 @@ +# QTreeView + +- 目录 + - [通过json数据生成树形结构](#1通过json数据生成树形结构) + - [json树形结构查询和修改](#2json树形结构查询和修改) + - [json数据绑定](#3json数据绑定) + +## 1、通过json数据生成树形结构 + +[运行 TestJsonModel.py](TestJsonModel.py) + +![TestJsonModel](ScreenShot/TestJsonModel.gif) + +## 2、json树形结构查询和修改 + +[运行 TestModelModify.py](TestModelModify.py) + +![TestModelModify](ScreenShot/TestModelModify.gif) + +## 3、json数据绑定 + +[运行 TestSerializeModel.py](TestSerializeModel.py) + +![TestSerializeModel](ScreenShot/TestSerializeModel.gif) diff --git a/QTreeView/ScreenShot/TestJsonModel.gif b/QTreeView/ScreenShot/TestJsonModel.gif new file mode 100644 index 00000000..d91b7176 Binary files /dev/null and b/QTreeView/ScreenShot/TestJsonModel.gif differ diff --git a/QTreeView/ScreenShot/TestModelModify.gif b/QTreeView/ScreenShot/TestModelModify.gif new file mode 100644 index 00000000..834dc413 Binary files /dev/null and b/QTreeView/ScreenShot/TestModelModify.gif differ diff --git a/QTreeView/ScreenShot/TestSerializeModel.gif b/QTreeView/ScreenShot/TestSerializeModel.gif new file mode 100644 index 00000000..ca54f485 Binary files /dev/null and b/QTreeView/ScreenShot/TestSerializeModel.gif differ diff --git a/QTreeView/TestJsonModel.py b/QTreeView/TestJsonModel.py new file mode 100644 index 00000000..febf6fe7 --- /dev/null +++ b/QTreeView/TestJsonModel.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/05 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TestJsonModel.py +@description: +""" + +import sys + +from Lib.qjsonmodel import QJsonItem, QJsonModel + +try: + from PyQt5.QtCore import QModelIndex, QSortFilterProxyModel, Qt + from PyQt5.QtWidgets import ( + QAction, + QApplication, + QCheckBox, + QGridLayout, + QLineEdit, + QPlainTextEdit, + QTreeView, + QWidget, + ) +except ImportError: + from PySide2.QtCore import QModelIndex, QSortFilterProxyModel, Qt + from PySide2.QtWidgets import ( + QAction, + QApplication, + QCheckBox, + QGridLayout, + QLineEdit, + QPlainTextEdit, + QTreeView, + QWidget, + ) + + +class TestWindow(QWidget): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.resize(800, 600) + layout = QGridLayout(self) + self.filterPath = QCheckBox("Path Filter?", self) + self.filterEdit = QLineEdit(self) + self.treeView = QTreeView(self) + self.widgetEdit = QPlainTextEdit(self) + layout.addWidget(self.filterPath, 0, 0, 1, 1) + layout.addWidget(self.filterEdit, 0, 1, 1, 1) + layout.addWidget(self.treeView, 1, 0, 1, 2) + layout.addWidget(self.widgetEdit, 0, 2, 2, 1) + + self.filterPath.toggled.connect(self.onFilterPathToggled) + self.filterEdit.setPlaceholderText("过滤条件") + self.filterEdit.textChanged.connect(self.onFilterTextChanged) + + self.model = QJsonModel(self) + self.model.itemChanged.connect(self.onItemChanged) + self.model.rowsRemoved.connect(self.onRowsRemoved) + self.treeView.clicked.connect(self.onItemChanged) + action = QAction("Delete", self.treeView) + action.triggered.connect(self.deleteItem) + self.treeView.setContextMenuPolicy(Qt.ActionsContextMenu) + self.treeView.addAction(action) + + # setup model data + self.setupModel() + + # filter model + self.fmodel = QSortFilterProxyModel(self) + self.fmodel.setSourceModel(self.model) + self.fmodel.setRecursiveFilteringEnabled(True) + self.treeView.setModel(self.fmodel) + self.treeView.expandAll() + + def setupModel(self): + data = { + "name": "Irony", + "age": 33, + "address": { + "country": "China", + "city": "Chengdu", + }, + "phone": [ + {"type": "home", "number": "123456789"}, + {"type": "fax", "number": "6987654321"}, + ], + "marriage": False, + "salary": 6666.6, + "skills": [ + "C++", + "Python", + "Java", + "JavaScript", + "Shell", + "Android", + ], + "others": [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + } + self.model.blockSignals(True) + self.model.loadData(data) + self.model.blockSignals(False) + + self.treeView.expandAll() + self.doExport() + + def doExport(self): + self.widgetEdit.setPlainText(self.model.toJson(ensure_ascii=False, indent=4)) + + def deleteItem(self, _): + indexes = self.treeView.selectedIndexes() + if not indexes: + return + + index = indexes[0] + print(index.row(), index.column(), index.parent()) + self.model.removeRow(index.row(), index.parent()) + + def onRowsRemoved(self, parent, first, last): + print("onRowsRemoved", parent, first, last) + self.doExport() + + def onItemChanged(self, item): + if self.sender() == self.model: + self.doExport() + + if isinstance(item, QModelIndex): + item = self.model.itemFromIndex(item) + + if not item: + return + keyInfo = "" + if item.keyItem: + keyInfo = f"row={item.keyItem.row()}, col={item.keyItem.column()}, text={item.keyItem.text()}" + print( + f"row={item.row()}, col={item.column()}, text={item.text()}, value={str(item.data(Qt.EditRole))}, key=({keyInfo})\n\trowCount={item.rowCount()}\n\tpath={item.path}" + ) + + def onFilterPathToggled(self, checked): + if checked: + self.fmodel.setFilterRole(QJsonItem.PathRole) + else: + self.fmodel.setFilterRole(Qt.DisplayRole) + + def onFilterTextChanged(self, text): + self.fmodel.setFilterFixedString(text) + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QTreeView/TestModelModify.py b/QTreeView/TestModelModify.py new file mode 100644 index 00000000..b62b0976 --- /dev/null +++ b/QTreeView/TestModelModify.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/05 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TestModelModify.py +@description: +""" + +import json +import sys + +from Lib.qjsonmodel import QJsonItem, QJsonModel + +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import ( + QApplication, + QFormLayout, + QHBoxLayout, + QLineEdit, + QPlainTextEdit, + QPushButton, + QTreeView, + QWidget, + ) +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import ( + QApplication, + QFormLayout, + QHBoxLayout, + QLineEdit, + QPlainTextEdit, + QPushButton, + QTreeView, + QWidget, + ) + + +class TestWindow(QWidget): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + layout = QHBoxLayout(self) + self.treeView = QTreeView(self) + self.widgetEdit = QWidget(self) + layout.addWidget(self.treeView) + layout.addWidget(self.widgetEdit) + + layout = QFormLayout(self.widgetEdit) + layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop) + layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) + self.editPath = QLineEdit(f"address{QJsonItem.Sep}city", self.widgetEdit) + self.editPath.setPlaceholderText( + f"请输入查询内容,支持路径查询,比如:aaa{QJsonItem.Sep}bbb{QJsonItem.Sep}ccc" + ) + layout.addRow("Path:", self.editPath) + self.editValue = QLineEdit("SiChuan", self.widgetEdit) + self.editValue.setPlaceholderText("请输入修改值,只支持路径匹配") + layout.addRow("Value:", self.editValue) + self.buttonQuery = QPushButton("查询", self.widgetEdit) + self.buttonModify = QPushButton("修改", self.widgetEdit) + self.buttonExport = QPushButton("获取Json", self.widgetEdit) + layout.addRow("", self.buttonQuery) + layout.addRow("", self.buttonModify) + layout.addRow("", self.buttonExport) + self.editText = QPlainTextEdit(self.widgetEdit) + self.editText.setReadOnly(True) + layout.addRow("", self.editText) + self.buttonQuery.clicked.connect(self.doQuery) + self.buttonModify.clicked.connect(self.doModify) + self.buttonExport.clicked.connect(self.doExport) + + self.model = QJsonModel(self) + self.model.itemChanged.connect(self.onItemChanged) + self.treeView.setModel(self.model) + + self.setupModel() + + def doQuery(self): + self.editText.clear() + path = self.editPath.text().strip() + if not path: + return + + item = self.model.findPath(path) + + if item: + self.editText.appendPlainText( + str( + { + "path": item.path, + "text": item.text(), + "value": item.valueItem.edit, + } + ) + ) + + def doModify(self): + self.editText.clear() + path = self.editPath.text().strip() + value = self.editValue.text().strip() + if not path or not value: + return + + try: + value = json.loads(value) + except Exception: + pass + + ret = self.model.updateValue(path, value) + self.editText.setPlainText(f"change ret: {ret}") + + def doExport(self): + self.editText.setPlainText(self.model.toJson(ensure_ascii=False, indent=4)) + + def setupModel(self): + data = { + "name": "Irony", + "age": 33, + "address": { + "country": "China", + "city": "Chengdu", + }, + "phone": [ + {"type": "home", "number": "123456789"}, + {"type": "fax", "number": "6987654321"}, + ], + "marriage": False, + "salary": 6666.6, + "skills": [ + "C++", + "Python", + "Java", + "JavaScript", + "Shell", + "Android", + ], + "others": [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + } + self.model.blockSignals(True) + self.model.loadData(data) + self.model.blockSignals(False) + + self.treeView.expandAll() + self.doExport() + + def onItemChanged(self, item): + self.doExport() + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QTreeView/TestSerializeModel.py b/QTreeView/TestSerializeModel.py new file mode 100644 index 00000000..20b0b62b --- /dev/null +++ b/QTreeView/TestSerializeModel.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/08 +@file: TestSerializeModel.py +@description: +""" + +import json +import sys +from datetime import datetime + +from Lib.qmodelmapper import QModelMapper +from Lib.serializewidget import Ui_SerializeWidget + +try: + from PyQt5.QtCore import QRegExp, Qt + from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat + from PyQt5.QtWidgets import QApplication, QWidget +except ImportError: + from PySide2.QtCore import QRegExp, Qt + from PySide2.QtGui import QSyntaxHighlighter, QTextCharFormat + from PySide2.QtWidgets import QApplication, QWidget + + +class HighlightingRule: + def __init__(self, pattern, color): + self.pattern = QRegExp(pattern) + self.format = QTextCharFormat() + self.format.setForeground(color) + + +class JsonHighlighter(QSyntaxHighlighter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._rules = [ + # numbers + HighlightingRule(QRegExp('([-0-9.]+)(?!([^"]*"[\\s]*\\:))'), Qt.darkRed), + # key + HighlightingRule(QRegExp('("[^"]*")\\s*\\:'), Qt.darkBlue), + # value + HighlightingRule(QRegExp(':+(?:[: []*)("[^"]*")'), Qt.darkGreen), + ] + + def highlightBlock(self, text: str) -> None: + for rule in self._rules: + index = rule.pattern.indexIn(text) + while index >= 0: + length = rule.pattern.matchedLength() + self.setFormat(index, length, rule.format) + index = rule.pattern.indexIn(text, index + length) + + +class TestWindow(QWidget, Ui_SerializeWidget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setupUi(self) + self.resize(1200, 600) + + # json view + self.highlighter = JsonHighlighter(self.editJsonView.document()) + self.editJsonView.textChanged.connect(self.onJsonChanged) + + # serialize model + QModelMapper.Debug = True + self.mapper = QModelMapper(self) + self.mapper.setData( + { + "input": { + "radioButton": True, + "checkBox": False, + }, + "name": "Irony", + } + ) + self.mapper.valueChanged.connect(self.onModelChanged) + self.treeJsonView.setModel(self.mapper.getModel()) + self.treeJsonView.expandAll() + + # comboBox + self.comboBox.addItems([f"Item {i}" for i in range(10)]) + + self.mapper.bind(self.radioButton, "input/radioButton") + self.mapper.bind(self.checkBox, "input/checkBox", True) + self.mapper.bind(self.comboBox, "input/comboBox") + self.mapper.bind(self.spinBox, "age") + self.mapper.bind(self.doubleSpinBox, "money") + self.mapper.bind(self.lineEdit, "name") + + # date and time + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.mapper.bind(self.timeEdit, "input/time", now) + self.mapper.bind(self.dateEdit, "input/date", now) + self.mapper.bind(self.dateTimeEdit, "input/dateTime", now) + + # edit + self.mapper.bind(self.plainTextEdit, "desc/desc") + self.mapper.bind(self.textEdit, "desc/text") + + # Correlation + self.mapper.bind(self.spinBox_2, "correlation/slider") + self.mapper.bind(self.horizontalSlider, "correlation/slider") + self.mapper.bind(self.progressBar, "correlation/progress") + self.mapper.bind(self.verticalSlider, "correlation/progress") + self.treeJsonView.expandAll() + + def onModelChanged(self): + data = self.mapper.getJson(indent=2) + self.editJsonView.blockSignals(True) + self.editJsonView.setPlainText(data) + self.editJsonView.blockSignals(False) + + def onJsonChanged(self): + text = self.editJsonView.toPlainText().strip() + try: + data = json.loads(text) + self.mapper.setData(data) + except Exception: + pass + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QTreeWidget/README.md b/QTreeWidget/README.md index 5194650a..1ecaf373 100644 --- a/QTreeWidget/README.md +++ b/QTreeWidget/README.md @@ -6,14 +6,15 @@ - [禁止父节点/禁止父节点](#3禁止父节点) ## 1、通过json数据生成树形结构 + [运行 ParsingJson.py](ParsingJson.py) 解析每一层json数据中的list - ![ParsingJson](ScreenShot/ParsingJson.png) ## 2、点击父节点全选/取消全选子节点 + [运行 testTreeWidget.py](testTreeWidget.py) | [查看 testTree.ui](Data/testTree.ui) 点击父节点全选/取消全选子节点 @@ -21,9 +22,10 @@ ![testTreeWidget](ScreenShot/allSelectNode.png) ## 3、禁止父节点 + [运行 ParentNodeForbid.py](ParentNodeForbid.py) 1. 父节点通过设置`pitem1.setFlags(pitem1.flags() & ~Qt.ItemIsSelectable)`为不可选 2. 完全禁用点击等需要重写`mousePressEvent`事件并结合item的标志来判断 -![ParentNodeForbid](ScreenShot/ParentNodeForbid.gif) \ No newline at end of file +![ParentNodeForbid](ScreenShot/ParentNodeForbid.gif) diff --git a/QVBoxLayout/README.md b/QVBoxLayout/README.md index 00679522..8d9c67c5 100644 --- a/QVBoxLayout/README.md +++ b/QVBoxLayout/README.md @@ -6,11 +6,13 @@ - [比例分配](#3比例分配) ## 1、垂直布局 + [查看 BaseVerticalLayout.ui](Data/BaseVerticalLayout.ui) ![BaseVerticalLayout](ScreenShot/BaseVerticalLayout.png) ## 2、边距和间隔 + [查看 VerticalLayoutMargin.ui](Data/VerticalLayoutMargin.ui) 1. 通过`setContentsMargins(20, 20, -1, -1)`设置左上右下的边距,-1表示默认值 @@ -19,6 +21,7 @@ ![VerticalLayoutMargin](ScreenShot/VerticalLayoutMargin.png) ## 3、比例分配 + [查看 VerticalLayoutStretch.ui](Data/VerticalLayoutStretch.ui) 通过`setStretch`设置各个部分的占比 分别为:1/6 2/6 3/6 @@ -29,4 +32,4 @@ self.verticalLayout.setStretch(1, 2) self.verticalLayout.setStretch(2, 3) ``` -![VerticalLayoutStretch](ScreenShot/VerticalLayoutStretch.png) \ No newline at end of file +![VerticalLayoutStretch](ScreenShot/VerticalLayoutStretch.png) diff --git a/QWebChannel/README.md b/QWebChannel/README.md index 51618de7..4bcdb07e 100644 --- a/QWebChannel/README.md +++ b/QWebChannel/README.md @@ -4,9 +4,10 @@ - [和Js互相调用](#1和Js互相调用) ## 1、和Js互相调用 + [运行 CallEachWithJs.py](CallEachWithJs.py) 通过`qwebchannel.js`和`QWebChannel.registerObject`通过中间件`WebSocket`进行对象和Javascript的交互(类似于json rpc) 该方法类似与`QWebEngineView`中的例子,同时该demo也适用与nodejs。 -![CallEachWithJs](ScreenShot/CallEachWithJs.gif) \ No newline at end of file +![CallEachWithJs](ScreenShot/CallEachWithJs.gif) diff --git a/QWebEngineView/README.md b/QWebEngineView/README.md index c916f999..4a26699a 100644 --- a/QWebEngineView/README.md +++ b/QWebEngineView/README.md @@ -10,6 +10,7 @@ - [设置Cookie](#7设置Cookie) ## 1、获取Cookie + [运行 GetCookie.py](GetCookie.py) 通过`QWebEngineProfile`中得到的`cookieStore`并绑定它的`cookieAdded`信号来得到Cookie @@ -17,6 +18,7 @@ ![GetCookie](ScreenShot/GetCookie.png) ## 2、和Js交互操作 + [运行 JsSignals.py](JsSignals.py) 通过`qwebchannel.js`和`QWebChannel.registerObject`进行Python对象和Javascript的交互 @@ -26,6 +28,7 @@ ![JsSignals](ScreenShot/JsSignals.gif) ## 3、网页整体截图 + [运行 ScreenShotPage.py](ScreenShotPage.py) 1. 方式1:目前通过不完美方法(先调整`QWebEngineView`的大小为`QWebEnginePage`的内容大小,等待一定时间后截图再还原大小) @@ -34,6 +37,7 @@ ![ScreenShotPage](ScreenShot/ScreenShotPage.gif) ## 4、同网站不同用户 + [运行 SiteDiffUser.py](SiteDiffUser.py) 原理是为每个`QWebEngineView`创建一个`QWebEnginePage`,且使用独立的`QWebEngineProfile`,并配置`persistentStoragePath`不同路径 @@ -41,6 +45,7 @@ ![SiteDiffUser](ScreenShot/SiteDiffUser.gif) ## 5、拦截请求 + [运行 BlockRequest.py](BlockRequest.py) 通过`QWebEngineUrlRequestInterceptor`中的`interceptRequest`方法对每个请求做拦截过滤 @@ -48,6 +53,7 @@ ![BlockRequest](ScreenShot/BlockRequest.gif) ## 6、拦截请求内容 + [运行 BlockRequestData.py](BlockRequestData.py) 这里用了一个投巧的办法,原理是先通过`QWebEngineUrlRequestInterceptor`中的`interceptRequest`方法对每个请求做拦截过滤, @@ -56,9 +62,10 @@ ![BlockRequestData](ScreenShot/BlockRequestData.png) ## 7、设置Cookie + [运行 SetCookies.py](SetCookies.py) 通过`QWebEngineProfile`中得到的`cookieStore`来添加`QNetworkCookie`对象实现, 需要注意的是httpOnly=true时,通过js无法获取 -![SetCookies](ScreenShot/SetCookies.png) \ No newline at end of file +![SetCookies](ScreenShot/SetCookies.png) diff --git a/QWebView/README.md b/QWebView/README.md index 557b5faa..f9b509e2 100644 --- a/QWebView/README.md +++ b/QWebView/README.md @@ -9,6 +9,7 @@ - [拦截请求](#6拦截请求) ## 1、梦幻树 + [运行 DreamTree.py](DreamTree.py) 在桌面上显示透明html效果,使用`QWebkit`加载html实现,采用窗口背景透明和穿透方式 @@ -16,6 +17,7 @@ ![DreamTree](ScreenShot/DreamTree.png) ## 2、获取Cookie + [运行 GetCookie.py](GetCookie.py) 从`page()`中得到`QNetworkAccessManager`,在从中得到`QNetworkCookieJar`, @@ -24,6 +26,7 @@ ![GetCookie](ScreenShot/GetCookie.png) ## 3、和Js交互操作 + [运行 JsSignals.py](JsSignals.py) 通过`QWebFrame`的`addToJavaScriptWindowObject`函数提供进行Python对象和Javascript的交互 @@ -33,6 +36,7 @@ ![JsSignals](ScreenShot/JsSignals.gif) ## 4、网页整体截图 + [运行 ScreenShotPage.py](ScreenShotPage.py) 1. 方式1:原理是通过`QWebView.QWebPage.QWebFrame`得到内容的高度,然后设置`QWebPage.setViewportSize`的大小, @@ -42,6 +46,7 @@ ![ScreenShotPage](ScreenShot/ScreenShotPage.gif) ## 5、播放Flash + [运行 PlayFlash.py](PlayFlash.py) 1. 重点在于设置 `os.environ['QTWEBKIT_PLUGIN_PATH'] = os.path.abspath('Data')` ,非常重要,设置为NPSWF32.dll文件所在目录 @@ -50,8 +55,9 @@ ![PlayFlash](ScreenShot/PlayFlash.gif) ## 6、拦截请求 + [运行 BlockRequest.py](BlockRequest.py) 通过`QNetworkAccessManager`中的`createRequest`方法对每个请求做拦截过滤 -![BlockRequest](ScreenShot/BlockRequest.png) \ No newline at end of file +![BlockRequest](ScreenShot/BlockRequest.png) diff --git a/QWidget/README.md b/QWidget/README.md index d5b6c1c9..0d86518e 100644 --- a/QWidget/README.md +++ b/QWidget/README.md @@ -4,9 +4,10 @@ - [样式表测试](#1样式表测试) ## 1、样式表测试 + [运行 WidgetStyle.py](WidgetStyle.py) 1. 一种是重写 `paintEvent` 2. 设置 `Qt.WA_StyledBackground` 后可以通过QSS增加背景等 -![WidgetStyle](ScreenShot/WidgetStyle.png) \ No newline at end of file +![WidgetStyle](ScreenShot/WidgetStyle.png) diff --git a/QtChart/README.md b/QtChart/README.md index 62271e5c..80f9a1ff 100644 --- a/QtChart/README.md +++ b/QtChart/README.md @@ -19,11 +19,13 @@ - [CPU动态折线图](#16CPU动态折线图) ## 1、折线图 + [运行 LineChart.py](LineChart.py) ![LineChart](ScreenShot/LineChart.png) ## 2、折线堆叠图 + [运行 LineStack.py](LineStack.py) 仿照 [line-stack](http://echarts.baidu.com/demo.html#line-stack) @@ -31,6 +33,7 @@ ![LineStack](ScreenShot/LineStack.gif) ## 3、柱状堆叠图 + [运行 BarStack.py](BarStack.py) 仿照 [bar-stack](http://echarts.baidu.com/demo.html#bar-stack) @@ -38,68 +41,81 @@ ![BarStack](ScreenShot/BarStack.gif) ## 4、LineChart自定义xy轴 + [运行 CustomXYaxis.py](CustomXYaxis.py) ![CustomXYaxis](ScreenShot/CustomXYaxis.png) ## 5、ToolTip提示 -[运行 ToolTip.py](ToolTip.py) | [运行 ToolTip2.py](ToolTip2.py) + +[运行 ToolTip.py](ToolTip.py) | [运行 ToolTip2.py](ToolTip2.py) ![ToolTip](ScreenShot/ToolTip.gif) ![ToolTip2](ScreenShot/ToolTip2.gif) ## 6、动态曲线图 + [运行 DynamicSpline.py](DynamicSpline.py) ![DynamicSpline](ScreenShot/DynamicSplineChart.gif) ## 7、区域图表 + [运行 AreaChart.py](AreaChart.py) ![AreaChart](ScreenShot/AreaChart.png) ## 8、柱状图表 + [运行 BarChart.py](BarChart.py) ![BarChart](ScreenShot/BarChart.png) ## 9、饼状图表 + [运行 PieChart.py](PieChart.py) ![PieChart](ScreenShot/PieChart.png) ## 10、样条图表 + [运行 SplineChart.py](SplineChart.py) ![SplineChart](ScreenShot/SplineChart.png) ## 11、百分比柱状图表 + [运行 PercentBarChart.py](PercentBarChart.py) ![PercentBarChart](ScreenShot/PercentBarChart.png) ## 12、横向柱状图表 + [运行 HorizontalBarChart.py](HorizontalBarChart.py) ![HorizontalBarChart](ScreenShot/HorizontalBarChart.png) ## 13、横向百分比柱状图表 + [运行 HorizontalPercentBarChart.py](HorizontalPercentBarChart.py) ![HorizontalPercentBarChart](ScreenShot/HorizontalPercentBarChart.png) ## 14、散点图表 + [运行 ScatterChart.py](ScatterChart.py) ![ScatterChart](ScreenShot/ScatterChart.png) ## 15、图表主题动画 + [运行 ChartThemes.py](ChartThemes.py) ![ChartThemes](ScreenShot/ChartThemes.gif) ## 16、CPU动态折线图 + [运行 CpuLineChart.py](CpuLineChart.py) 通过设置x轴的时间范围并替换y点达到动态移动效果 -![CpuLineChart](ScreenShot/CpuLineChart.png) \ No newline at end of file +![CpuLineChart](ScreenShot/CpuLineChart.png) diff --git a/QtDataVisualization/README.md b/QtDataVisualization/README.md index 9366e669..bcd8a75c 100644 --- a/QtDataVisualization/README.md +++ b/QtDataVisualization/README.md @@ -6,16 +6,19 @@ - [余弦波3D](#3余弦波3D) ## 1、柱状图3D + [运行 BarsVisualization.py](BarsVisualization.py) ![BarsVisualization](ScreenShot/BarsVisualization.gif) ## 2、太阳磁场线 + [运行 MagneticOfSun.py](MagneticOfSun.py) ![MagneticOfSun](ScreenShot/MagneticOfSun.gif) ## 3、余弦波3D + [运行 ScatterVisualization.py](ScatterVisualization.py) -![ScatterVisualization](ScreenShot/ScatterVisualization.gif) \ No newline at end of file +![ScatterVisualization](ScreenShot/ScatterVisualization.gif) diff --git a/QtQuick/README.md b/QtQuick/README.md index 253ed416..879091c9 100644 --- a/QtQuick/README.md +++ b/QtQuick/README.md @@ -5,11 +5,13 @@ - [QML与Python交互](#2QML与Python交互) ## 1、Flat样式 + [运行 FlatStyle.py](FlatStyle.py) ![FlatStyle](ScreenShot/FlatStyle.gif) ## 2、QML与Python交互 + [运行 Signals.py](Signals.py) 交互的办法有很多种,由于主要界面功能都是有QML来实现,Python只是作为辅助提供部分功能。 @@ -18,6 +20,7 @@ 1. 通过 `engine.rootContext().setContextProperty('_Window', w)` 注册提供一个Python对象 2. Python对象中被访问的方法前面使用装饰器 `@pyqtSlot`,比如: `@pyqtSlot(int)` 或者 `@pyqtSlot(str, result=str) # 可以获取返回值` 。 3. QML中的信号或者Python对象中的信号都可以互相绑定对方的槽函数 + ```js Component.onCompleted: { // 绑定信号槽到python中的函数 @@ -27,5 +30,4 @@ Component.onCompleted: { } ``` - ![Signals](ScreenShot/Signals.gif) diff --git a/QtRemoteObjects/README.md b/QtRemoteObjects/README.md index ce6e3c53..87e22996 100644 --- a/QtRemoteObjects/README.md +++ b/QtRemoteObjects/README.md @@ -6,11 +6,13 @@ - [简单界面数据同步](#3简单界面数据同步) ## 1、modelview + [运行 modelviewserver.py](modelview/modelviewserver.py) | [运行 modelviewclient.py](modelview/modelviewclient.py) 官方关于QTreeView/QStandardItemModel的同步model例子 ## 2、simpleswitch + [运行 directconnectdynamicserver.py](simpleswitch/directconnectdynamicserver.py) | [运行 directconnectdynamicclient.py](simpleswitch/directconnectdynamicclient.py) [运行 registryconnecteddynamicserver.py](simpleswitch/registryconnecteddynamicserver.py) | [运行 registryconnecteddynamicclient.py](simpleswitch/registryconnecteddynamicclient.py) @@ -18,8 +20,9 @@ 官方关于简单的信号槽、属性访问测试例子 ## 3、简单界面数据同步 + [运行 WindowMaster.py](SyncUi/WindowMaster.py) | [运行 WindowSlave.py](SyncUi/WindowSlave.py) 绑定信号槽同步双方数据,属性方法测试没通过,详细注释在代码中 -![SyncUi](ScreenShot/SyncUi.gif) \ No newline at end of file +![SyncUi](ScreenShot/SyncUi.gif) diff --git a/QtWinExtras/README.md b/QtWinExtras/README.md index c560e34e..e1775b13 100644 --- a/QtWinExtras/README.md +++ b/QtWinExtras/README.md @@ -5,6 +5,7 @@ - [任务栏缩略图工具按钮](#2任务栏缩略图工具按钮) ## 1、任务栏进度条 + [运行 TaskbarProgress.py](TaskbarProgress.py) `QWinTaskbarProgress`类似和`QProgressBar`一样的操作 @@ -12,8 +13,9 @@ ![TaskbarProgress](ScreenShot/TaskbarProgress.gif) ## 2、任务栏缩略图工具按钮 + [运行 ThumbnailToolBar.py](ThumbnailToolBar.py) `QWinThumbnailToolBar`和`QWinThumbnailToolButton`的组合实现音乐播放器的播放、上下一曲按钮 -![ThumbnailToolBar](ScreenShot/ThumbnailToolBar.gif) \ No newline at end of file +![ThumbnailToolBar](ScreenShot/ThumbnailToolBar.gif) diff --git a/README.md b/README.md index 2c6817b4..3f3685da 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ or        [PyQt 学习](https://jq.qq.com/?_wv=1027&k=5QVVEdF) -         +        [PyQt 频道](https://pd.qq.com/s/157c1hiay) ## 状态 + ![Alt](https://repobeats.axiom.co/api/embed/12e69289ec9d9a7037d2c31aaaadd228dd99638f.svg "Repobeats analytics image") ## 目录 @@ -53,6 +54,8 @@ - [按钮底部线条进度](QPushButton/BottomLineProgress.py) - [按钮文字旋转进度](QPushButton/FontRotate.py) - [按钮常用信号](QPushButton/SignalsExample.py) + - [旋转动画按钮](QPushButton/RotateButton.py) + - [弹性动画按钮](QPushButton/RubberBandButton.py) - [QToolButton](QToolButton) - [QRadioButton](QRadioButton) - [QCheckBox](QCheckBox) @@ -63,9 +66,13 @@ - [显示自定义Widget并排序](QListView/CustomWidgetSortItem.py) - [自定义角色排序](QListView/SortItemByRole.py) - [QTreeView](QTreeView) + - [通过json数据生成树形结构](QTreeView/TestJsonModel.py) + - [json树形结构查询和修改](QTreeView/TestModelModify.py) + - [json数据绑定](QTreeView/TestSerializeModel.py) - [QTableView](QTableView) - [表格内容复制](QTableView/CopyContent.py) - [QColumnView](QColumnView) + - [文件系统浏览器](QColumnView/FileManager.py) - [QUndoView](QUndoView) - Item Widgets @@ -289,11 +296,11 @@ - [动态忙碌光标](Demo/GifCursor.py) - [屏幕变动监听](Demo/ScreenNotify.py) - [无边框窗口](Demo/NewFramelessWindow.py) - + - [属性绑定](Demo/TestSerializeModel.py) ## 其它项目 -[一些Qt写的三方APP](https://github.com/PyQt5/3rd-Apps) +[一些Qt写的三方APP](https://github.com/PyQt5/3rd-Apps) ## [Donate-打赏](Donate) diff --git a/Test/Network/README.md b/Test/Network/README.md index b6bbec5c..2aeccb1c 100644 --- a/Test/Network/README.md +++ b/Test/Network/README.md @@ -1,43 +1,46 @@ # 网络 ## [1、控制小车](控制小车/) + 通过TCP连接树莓派控制小车的简单例子 需求: - - 通过TCP连接到树莓派控制小车前后左右 - - 前进:0-100, 发送命令为F:2 - - 后退:0-100, 发送命令为B:2 - - 向左:32-42, 发送命令为L:2 - - 向右:42-52, 发送命令为R:2 +- 通过TCP连接到树莓派控制小车前后左右 +- 前进:0-100, 发送命令为F:2 +- 后退:0-100, 发送命令为B:2 +- 向左:32-42, 发送命令为L:2 +- 向右:42-52, 发送命令为R:2 注意: - - 这里只用了UI文件做界面,并没有转换为python代码 - - server.py只是做个本地echo服务器用来测试命令是否正常,依赖`tornado`库,可以通过`pip install tornado`来安装 - - 另外需要做粘包处理,以(\n)作为粘包符 - - 由于wifi能力不行,发送图片要尽量小 +- 这里只用了UI文件做界面,并没有转换为python代码 +- server.py只是做个本地echo服务器用来测试命令是否正常,依赖`tornado`库,可以通过`pip install tornado`来安装 +- 另外需要做粘包处理,以(\n)作为粘包符 +- 由于wifi能力不行,发送图片要尽量小 说明: - - `QTcpSocket.connected` 服务连接成功后触发该信号 - - `QTcpSocket.disconnected` 服务器丢失连接触发该信号 - - `QTcpSocket.readyRead` 服务器返回数据触发该信号 - - `QTcpSocket.error` 连接报错触发该信号(连接超时、服务器断开等等) +- `QTcpSocket.connected` 服务连接成功后触发该信号 +- `QTcpSocket.disconnected` 服务器丢失连接触发该信号 +- `QTcpSocket.readyRead` 服务器返回数据触发该信号 +- `QTcpSocket.error` 连接报错触发该信号(连接超时、服务器断开等等) 目前暂未修复接收图片异,原因在于`readyRead`中没有判断数据长度进行多次接收(类似粘包处理) ![截图](控制小车/ScreenShot/控制小车.png) ## [2、窗口配合异步Http](窗口配合异步Http/) + `asyncio`结合PyQt例子 1. 依赖库: - 1. `quamash`(对QT事件循环的封装替换):https://github.com/harvimt/quamash - 2. `asyncio`:https://docs.python.org/3/library/asyncio.html - 3. `aiohttp`:https://aiohttp.readthedocs.io/en/stable/ + 1. `quamash`(对QT事件循环的封装替换): + 2. `asyncio`: + 3. `aiohttp`: 2. 在创建`QApplication`后随即设置替换事件循环loop + ```python app = QApplication(sys.argv) loop = QEventLoop(app) @@ -67,4 +70,4 @@ Window  →→  initSession(初始化session) 添加到界面  ←←  _doDownloadImage(对单张图片进行下载) -![截图](窗口配合异步Http/ScreenShot/窗口配合异步Http.gif) \ No newline at end of file +![截图](窗口配合异步Http/ScreenShot/窗口配合异步Http.gif) diff --git a/Test/WigglyWidget/CMakeLists.txt b/Test/WigglyWidget/CMakeLists.txt new file mode 100644 index 00000000..41beb600 --- /dev/null +++ b/Test/WigglyWidget/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.14) + +project(WigglyWidget LANGUAGES CXX) + +add_subdirectory(LibWigglyWidget) +add_subdirectory(TestWigglyWidget) +add_subdirectory(PyQtWrapper) +add_subdirectory(PySideWrapper) diff --git a/Test/WigglyWidget/LibWigglyWidget/CMakeLists.txt b/Test/WigglyWidget/LibWigglyWidget/CMakeLists.txt new file mode 100644 index 00000000..d6478270 --- /dev/null +++ b/Test/WigglyWidget/LibWigglyWidget/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.14) + +project(LibWigglyWidget LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 配置安装目录 +set(CMAKE_INSTALL_DIR "${CMAKE_SOURCE_DIR}/dist") + +# 静态库 +set(BUILD_SHARED_LIBS OFF) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) + +add_library(LibWigglyWidget wigglywidget.cpp wigglywidget.h) + +target_link_libraries(LibWigglyWidget PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) + +if(BUILD_SHARED_LIBS) + target_compile_definitions(LibWigglyWidget PRIVATE LIBWIGGLYWIDGET_LIBRARY) +endif() + +# 配置生成的文件安装目录 +install( + TARGETS ${PROJECT_NAME} + RUNTIME DESTINATION "${CMAKE_INSTALL_DIR}/bin" + LIBRARY DESTINATION "${CMAKE_INSTALL_DIR}/lib" + ARCHIVE DESTINATION "${CMAKE_INSTALL_DIR}/lib") + +install(FILES wigglywidget.h DESTINATION "${CMAKE_INSTALL_DIR}/include") diff --git a/Test/WigglyWidget/LibWigglyWidget/wigglywidget.cpp b/Test/WigglyWidget/LibWigglyWidget/wigglywidget.cpp new file mode 100644 index 00000000..0433fc15 --- /dev/null +++ b/Test/WigglyWidget/LibWigglyWidget/wigglywidget.cpp @@ -0,0 +1,50 @@ +#include "wigglywidget.h" + +#include +#include +#include + +WigglyWidget::WigglyWidget(QWidget *parent) : QWidget(parent), step(0) { + setBackgroundRole(QPalette::Midlight); + setAutoFillBackground(true); + + QFont newFont = font(); + newFont.setPointSize(newFont.pointSize() + 20); + setFont(newFont); + + timer.start(60, this); +} + +void WigglyWidget::setText(const QString &newText) { text = newText; } + +void WigglyWidget::paintEvent(QPaintEvent * /* event */) + +{ + static constexpr int sineTable[16] = {0, 38, 71, 92, 100, 92, 71, 38, + 0, -38, -71, -92, -100, -92, -71, -38}; + + QFontMetrics metrics(font()); + int x = (width() - metrics.horizontalAdvance(text)) / 2; + int y = (height() + metrics.ascent() - metrics.descent()) / 2; + QColor color; + + QPainter painter(this); + + for (int i = 0; i < text.size(); ++i) { + int index = (step + i) % 16; + color.setHsv((15 - index) * 16, 255, 191); + painter.setPen(color); + painter.drawText(x, y - ((sineTable[index] * metrics.height()) / 400), + QString(text[i])); + x += metrics.horizontalAdvance(text[i]); + } +} + +void WigglyWidget::timerEvent(QTimerEvent *event) { + if (event->timerId() == timer.timerId()) { + ++step; + update(); + } else { + QWidget::timerEvent(event); + } +} diff --git a/Test/WigglyWidget/LibWigglyWidget/wigglywidget.h b/Test/WigglyWidget/LibWigglyWidget/wigglywidget.h new file mode 100644 index 00000000..3df215e0 --- /dev/null +++ b/Test/WigglyWidget/LibWigglyWidget/wigglywidget.h @@ -0,0 +1,36 @@ +#ifndef WIGGLYWIDGET_H +#define WIGGLYWIDGET_H + +#include +#include + +#ifdef Q_OS_WIN +#include + +#if defined(WIGGLYWIDGET_LIBRARY) +#define WIGGLYWIDGET_EXPORT Q_DECL_EXPORT +#else +#define WIGGLYWIDGET_EXPORT +#endif +#endif + +class WIGGLYWIDGET_EXPORT WigglyWidget : public QWidget { + Q_OBJECT + +public: + WigglyWidget(QWidget *parent = nullptr); + +public slots: + void setText(const QString &newText); + +protected: + virtual void paintEvent(QPaintEvent *event) override; + virtual void timerEvent(QTimerEvent *event) override; + +private: + QBasicTimer timer; + QString text; + int step; +}; + +#endif // WIGGLYWIDGET_H diff --git a/Test/WigglyWidget/PyQtWrapper/CMakeLists.txt b/Test/WigglyWidget/PyQtWrapper/CMakeLists.txt new file mode 100644 index 00000000..8218c322 --- /dev/null +++ b/Test/WigglyWidget/PyQtWrapper/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.14) + +project(PyQtWrapper) diff --git a/Test/WigglyWidget/PyQtWrapper/TestWigglyWidget.py b/Test/WigglyWidget/PyQtWrapper/TestWigglyWidget.py new file mode 100644 index 00000000..ac1177ae --- /dev/null +++ b/Test/WigglyWidget/PyQtWrapper/TestWigglyWidget.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2024/04/26 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TestWigglyWidget.py +@description: +""" + +import os +import sys + +sys.path.append( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "build/WigglyWidget") +) + +from PyQt5.QtWidgets import QApplication, QLineEdit, QVBoxLayout, QWidget +from WigglyWidget import WigglyWidget + + +class TestWigglyWidget(QWidget): + def __init__(self, *args, **kwargs): + super(TestWigglyWidget, self).__init__(*args, **kwargs) + self._layout = QVBoxLayout(self) + self._lineEdit = QLineEdit(self) + self._wigglyWidget = WigglyWidget(self) + self._layout.addWidget(self._lineEdit) + self._layout.addWidget(self._wigglyWidget) + + self._lineEdit.textChanged.connect(self._wigglyWidget.setText) + self._lineEdit.setText("pyqt.site") + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWigglyWidget() + w.show() + w.resize(800, 600) + if not hasattr(app, "exec_"): + app.exec_ = app.exec + sys.exit(app.exec_()) diff --git a/Test/WigglyWidget/PyQtWrapper/pyproject.toml b/Test/WigglyWidget/PyQtWrapper/pyproject.toml new file mode 100644 index 00000000..fb837510 --- /dev/null +++ b/Test/WigglyWidget/PyQtWrapper/pyproject.toml @@ -0,0 +1,47 @@ +# Specify sip v6 as the build system for the package. +[build-system] +requires = ["sip >=5.3, <7", "PyQt-builder >=1.9, <2"] +build-backend = "sipbuild.api" + +# Specify the PEP 621 metadata for the project. +[project] +name = "WigglyWidget" +version = "0.1.0" +description = "Python bindings for the WigglyWidget library" +urls.homepage = "https://github.com/PyQt5/PyQt" +dependencies = ["PyQt5 (>=5.15.0, <6.0.0)"] + +[[project.authors]] +name = "Irony" +email = "892768447@qq.com" + +# Specify a PyQt-based project. +[tool.sip] +project-factory = "pyqtbuild:PyQtProject" + +# Specify the PEP 566 metadata for the project. +[tool.sip.metadata] +name = "WigglyWidget" +summary = "Python bindings for the WigglyWidget library" +home-page = "https://github.com/PyQt5/PyQt" +author = "Irony" +author-email = "892768447@qq.com" +requires-dist = "PyQt5 (>=5.15.0, <6.0.0)" + +# Configure the project. +[tool.sip.project] +tag-prefix = "WigglyWidget" +sip-include-dirs = [ + "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/PyQt5/bindings", + "/usr/local/lib64/python3.6/site-packages/PyQt5/bindings", + "C:/soft/Python311/Lib/site-packages/PyQt5/bindings", + "C:/soft/Python/lib/site-packages/PyQt6/bindings" +] + +# Configure the building of the fib bindings. +[tool.sip.bindings.WigglyWidget] +qmake-QT = ["core", "gui", "widgets"] +headers = ["wigglywidget.h"] +include-dirs = ["../dist/include"] +libraries = ["LibWigglyWidget"] +library-dirs = ["../dist/lib"] diff --git a/Test/WigglyWidget/PyQtWrapper/requirements.txt b/Test/WigglyWidget/PyQtWrapper/requirements.txt new file mode 100644 index 00000000..4d4e9f19 --- /dev/null +++ b/Test/WigglyWidget/PyQtWrapper/requirements.txt @@ -0,0 +1,6 @@ +PyQt5 +PyQt6 +sip +PyQt5-sip +PyQt6-sip +PyQt-builder diff --git a/Test/WigglyWidget/PyQtWrapper/sip/WigglyWidget/WigglyWidget.sip b/Test/WigglyWidget/PyQtWrapper/sip/WigglyWidget/WigglyWidget.sip new file mode 100644 index 00000000..6fe00065 --- /dev/null +++ b/Test/WigglyWidget/PyQtWrapper/sip/WigglyWidget/WigglyWidget.sip @@ -0,0 +1,16 @@ +class WigglyWidget : QWidget +{ +%TypeHeaderCode +#include "wigglywidget.h" +%End + +public: + WigglyWidget(QWidget *parent /TransferThis/ = 0); + +public slots: + void setText(const QString &newText); + +protected: + virtual void paintEvent(QPaintEvent *); + virtual void timerEvent(QTimerEvent *); +}; diff --git a/Test/WigglyWidget/PyQtWrapper/sip/WigglyWidget/WigglyWidgetmod.sip b/Test/WigglyWidget/PyQtWrapper/sip/WigglyWidget/WigglyWidgetmod.sip new file mode 100644 index 00000000..927d003f --- /dev/null +++ b/Test/WigglyWidget/PyQtWrapper/sip/WigglyWidget/WigglyWidgetmod.sip @@ -0,0 +1,9 @@ +%Module(name=WigglyWidget, keyword_arguments="Optional", use_limited_api=True) + + +%Import QtCore/QtCoremod.sip +%Import QtWidgets/QtWidgetsmod.sip + +%DefaultSupertype sip.simplewrapper + +%Include WigglyWidget.sip diff --git a/Test/WigglyWidget/PySideWrapper/CMakeLists.txt b/Test/WigglyWidget/PySideWrapper/CMakeLists.txt new file mode 100644 index 00000000..99122055 --- /dev/null +++ b/Test/WigglyWidget/PySideWrapper/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.14) +cmake_policy(VERSION 3.14) + +# Enable policy to not use RPATH settings for install_name on macOS. +if(POLICY CMP0068) + cmake_policy(SET CMP0068 NEW) +endif() + +# Enable policy to run automoc on generated files. +if(POLICY CMP0071) + cmake_policy(SET CMP0071 NEW) +endif() + +project(PySideWrapper) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(BINDINGS_HEADER_FILE "${CMAKE_CURRENT_SOURCE_DIR}/bindings.h") +set(BINDINGS_TYPESYSTEM_FILE "${CMAKE_CURRENT_SOURCE_DIR}/bindings.xml") +set(BINDINGS_OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dist") +set(BINDINGS_INCLUDE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../dist/include/") + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/bindings.txt.in" + "${CMAKE_CURRENT_SOURCE_DIR}/bindings.txt") diff --git a/Test/WigglyWidget/PySideWrapper/TestWigglyWidget.py b/Test/WigglyWidget/PySideWrapper/TestWigglyWidget.py new file mode 100644 index 00000000..d2a851c9 --- /dev/null +++ b/Test/WigglyWidget/PySideWrapper/TestWigglyWidget.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2024/04/26 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TestWigglyWidget.py +@description: +""" + +import sys + +from PySide2.QtWidgets import QApplication, QLineEdit, QVBoxLayout, QWidget +from WigglyWidget import WigglyWidget + + +class TestWigglyWidget(QWidget): + def __init__(self, *args, **kwargs): + super(TestWigglyWidget, self).__init__(*args, **kwargs) + self._layout = QVBoxLayout(self) + self._lineEdit = QLineEdit(self) + self._wigglyWidget = WigglyWidget(self) + self._layout.addWidget(self._lineEdit) + self._layout.addWidget(self._wigglyWidget) + + self._lineEdit.textChanged.connect(self._wigglyWidget.setText) + self._lineEdit.setText("pyqt.site") + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWigglyWidget() + w.show() + sys.exit(app.exec_()) diff --git a/Test/WigglyWidget/PySideWrapper/bindings.h b/Test/WigglyWidget/PySideWrapper/bindings.h new file mode 100644 index 00000000..0634a4b1 --- /dev/null +++ b/Test/WigglyWidget/PySideWrapper/bindings.h @@ -0,0 +1,4 @@ +#ifndef BINDINGS_H +#define BINDINGS_H +#include "wigglywidget.h" +#endif // BINDINGS_H diff --git a/Test/WigglyWidget/PySideWrapper/bindings.txt.in b/Test/WigglyWidget/PySideWrapper/bindings.txt.in new file mode 100644 index 00000000..d1bb8a4b --- /dev/null +++ b/Test/WigglyWidget/PySideWrapper/bindings.txt.in @@ -0,0 +1,13 @@ +[generator-project] +generator-set = shiboken +header-file = ${BINDINGS_HEADER_FILE} +typesystem-file = ${BINDINGS_TYPESYSTEM_FILE} +output-directory = ${BINDINGS_OUTPUT_DIR} +include-path = ${BINDINGS_INCLUDE_PATH} +framework-include-paths = +typesystem-paths = ${BINDINGS_TYPESYSTEM_PATH} + +enable-parent-ctor-heuristic +enable-pyside-extensions +enable-return-value-heuristic +use-isnull-as-nb_nonzero diff --git a/Test/WigglyWidget/PySideWrapper/bindings.xml b/Test/WigglyWidget/PySideWrapper/bindings.xml new file mode 100644 index 00000000..c3a31296 --- /dev/null +++ b/Test/WigglyWidget/PySideWrapper/bindings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Test/WigglyWidget/PySideWrapper/generated.bat b/Test/WigglyWidget/PySideWrapper/generated.bat new file mode 100644 index 00000000..e69de29b diff --git a/Test/WigglyWidget/PySideWrapper/generated.sh b/Test/WigglyWidget/PySideWrapper/generated.sh new file mode 100644 index 00000000..e69de29b diff --git a/Test/WigglyWidget/PySideWrapper/requirements.txt b/Test/WigglyWidget/PySideWrapper/requirements.txt new file mode 100644 index 00000000..fa259e98 --- /dev/null +++ b/Test/WigglyWidget/PySideWrapper/requirements.txt @@ -0,0 +1,9 @@ +PySide2==5.15.2.1 + +https://download.qt.io/official_releases/QtForPython/shiboken2-generator/shiboken2_generator-5.15.2.1-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310-none-win_amd64.whl; platform_machine == "x86_64" and platform_system == "Windows" + +https://download.qt.io/official_releases/QtForPython/shiboken2-generator/shiboken2_generator-5.15.2.1-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310-none-win32.whl; platform_machine == "i686" and platform_system == "Windows" + +https://download.qt.io/official_releases/QtForPython/shiboken2-generator/shiboken2_generator-5.15.2.1-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310-abi3-manylinux1_x86_64.whl; platform_machine == "x86_64" and platform_system == "Linux" + +https://download.qt.io/official_releases/QtForPython/shiboken2-generator/shiboken2_generator-5.15.2.1-5.15.2-cp35.cp36.cp37.cp38.cp39.cp310-abi3-macosx_10_13_intel.whl; platform_machine == "x86_64" and platform_system == "Darwin" diff --git a/Test/WigglyWidget/README.md b/Test/WigglyWidget/README.md new file mode 100644 index 00000000..bfd71091 --- /dev/null +++ b/Test/WigglyWidget/README.md @@ -0,0 +1,11 @@ +# WigglyWidget + +## Build + +Windows + +1. 使用 `QtCreator` 打开项目 `CMakeLists.txt`,勾选对应的Qt版本。 +2. 在 `QtCreator` 中通过 `项目`->`构建`->`构建步骤`->`详情` 里勾选 `all` 和 `install` +3. 进入 `PyQtWrapper` 目录,打开`vs cmd`,运行 `python -m pip install -r requirements.txt` +4. `sip-build --verbose --tracing --qmake=你的Qt目录下的qmake.exe路径`,等待编译完成 +5. `python TestWigglyWidget.py` 进行测试 diff --git a/Test/WigglyWidget/TestWigglyWidget/CMakeLists.txt b/Test/WigglyWidget/TestWigglyWidget/CMakeLists.txt new file mode 100644 index 00000000..9ab36f94 --- /dev/null +++ b/Test/WigglyWidget/TestWigglyWidget/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.5) + +project( + TestWigglyWidget + VERSION 0.1 + LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 配置安装目录 +set(CMAKE_INSTALL_DIR "${CMAKE_SOURCE_DIR}/dist") + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) + +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../LibWigglyWidget") + +set(PROJECT_SOURCES main.cpp) + +if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) + qt_add_executable(${PROJECT_NAME} MANUAL_FINALIZATION ${PROJECT_SOURCES}) + # Define target properties for Android with Qt 6 as: set_property(TARGET + # TestWigglyWidget APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR + # ${CMAKE_CURRENT_SOURCE_DIR}/android) For more information, see + # https://doc.qt.io/qt-6/qt-add-executable.html#target-creation +else() + if(ANDROID) + add_library(${PROJECT_NAME} SHARED ${PROJECT_SOURCES}) + # Define properties for Android with Qt 5 after find_package() calls as: + # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") + else() + add_executable(${PROJECT_NAME} ${PROJECT_SOURCES}) + endif() +endif() + +target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Widgets + LibWigglyWidget) + +# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. If +# you are developing for iOS or macOS you should consider setting an explicit, +# fixed bundle identifier manually though. +if(${QT_VERSION} VERSION_LESS 6.1.0) + set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER + com.example.TestWigglyWidget) +endif() +set_target_properties( + ${PROJECT_NAME} + PROPERTIES ${BUNDLE_ID_OPTION} MACOSX_BUNDLE_BUNDLE_VERSION + ${PROJECT_VERSION} MACOSX_BUNDLE_SHORT_VERSION_STRING + ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} MACOSX_BUNDLE + TRUE WIN32_EXECUTABLE + TRUE) + +if(QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(${PROJECT_NAME}) +endif() diff --git a/Test/WigglyWidget/TestWigglyWidget/main.cpp b/Test/WigglyWidget/TestWigglyWidget/main.cpp new file mode 100644 index 00000000..a74498e1 --- /dev/null +++ b/Test/WigglyWidget/TestWigglyWidget/main.cpp @@ -0,0 +1,11 @@ +#include + +#include "wigglywidget.h" + +int main(int argc, char *argv[]) { + QApplication a(argc, argv); + WigglyWidget w; + w.setText("pyqt.site"); + w.show(); + return a.exec(); +} diff --git "a/Test/\345\205\250\345\261\200\347\203\255\351\224\256/README.md" "b/Test/\345\205\250\345\261\200\347\203\255\351\224\256/README.md" index f6db5d27..af9c9420 100644 --- "a/Test/\345\205\250\345\261\200\347\203\255\351\224\256/README.md" +++ "b/Test/\345\205\250\345\261\200\347\203\255\351\224\256/README.md" @@ -2,12 +2,13 @@ pip install keyboard -https://github.com/892768447/keyboard + * keyboard - * 该模块使用全局低级钩子的方式hook键盘来处理,对系统有一定的影响 - * 有反映说弹出对话框假死,这里粗略解决下使用信号槽的方式来弹出对话框 - * 该模块里使用了每次产生一个子线程来回调函数 + * 该模块使用全局低级钩子的方式hook键盘来处理,对系统有一定的影响 + * 有反映说弹出对话框假死,这里粗略解决下使用信号槽的方式来弹出对话框 + * 该模块里使用了每次产生一个子线程来回调函数 + ``` def call_later(fn, args=(), delay=0.001): """ @@ -20,4 +21,5 @@ def call_later(fn, args=(), delay=0.001): ``` # 截图 -![截图](ScreenShot/1.gif) \ No newline at end of file + +![截图](ScreenShot/1.gif) diff --git "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/README.md" "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/README.md" index 1adc33fb..300bc484 100644 --- "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/README.md" +++ "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/README.md" @@ -1,6 +1,6 @@ # 自动更新 - - dist/mylibs1.zip 为版本一的文件 - - dist/mylibs2.zip 为版本二的文件 +- dist/mylibs1.zip 为版本一的文件 +- dist/mylibs2.zip 为版本二的文件 -运行演示后,再次演示。需要把mylibs1.zip中的文件解压出来替换 \ No newline at end of file +运行演示后,再次演示。需要把mylibs1.zip中的文件解压出来替换 diff --git "a/Test/\350\207\252\345\256\232\344\271\211import/README.md" "b/Test/\350\207\252\345\256\232\344\271\211import/README.md" index 4ee4369a..a339481d 100644 --- "a/Test/\350\207\252\345\256\232\344\271\211import/README.md" +++ "b/Test/\350\207\252\345\256\232\344\271\211import/README.md" @@ -1,14 +1,17 @@ # 自定义import + 需要Python3.5.2(或者自行编译xxtea) 简单的了解了下import的原理 # 测试过程 - - 1.在src中编写一个test.py - - 2.通过build.py 利用xxtea加密src/test.py 到当前目录的test.irony文件 - - 3.运行main.py 进行测试 + +- 1.在src中编写一个test.py +- 2.通过build.py 利用xxtea加密src/test.py 到当前目录的test.irony文件 +- 3.运行main.py 进行测试 # 截图 + test.py ![test.py](ScreenShot/1.png) @@ -19,4 +22,4 @@ test.irony main.py -![main.py](ScreenShot/3.png) \ No newline at end of file +![main.py](ScreenShot/3.png) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..e873f8cf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "PyQt" + +[tool.ruff.lint] +ignore = ["E402", "E501"] + +[tool.pyright] +reportArgumentType = false +reportOperatorIssue = false +reportAttributeAccessIssue = false +reportCallIssue = false +reportIncompatibleMethodOverride = false