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 @@

## 2、简单的窗口贴边隐藏
+
[运行 WeltHideWindow.py](WeltHideWindow.py)
1. 大概思路
@@ -52,6 +55,7 @@

## 3、嵌入外部窗口
+
[运行 EmbedWindow.py](EmbedWindow.py)
1. 使用`SetParent`函数设置外部窗口的`parent`为Qt的窗口
@@ -61,8 +65,8 @@

-
## 4、简单跟随其它窗口
+
[运行 FollowWindow.py](FollowWindow.py)
1. 利用win32gui模块获取目标窗口的句柄
@@ -72,8 +76,8 @@

-
## 5、简单探测窗口和放大截图
+
[运行 ProbeWindow.py](ProbeWindow.py)
1. 利用`win32gui`模块获取鼠标所在位置的窗口大小(未去掉边框)和rgb颜色
@@ -81,8 +85,8 @@

-
## 6、无边框自定义标题栏窗口
+
[运行 FramelessWindow.py](FramelessWindow.py) | [运行 NativeEvent.py](NativeEvent.py)
1. 重写鼠标事件
@@ -100,32 +104,38 @@

## 7、右下角弹出框
+
[运行 WindowNotify.py](WindowNotify.py) | [查看 notify.ui](Data/notify.ui)

## 8、程序重启
+
[运行 AutoRestart.py](AutoRestart.py)

## 9、自定义属性
+
[运行 CustomProperties.py](CustomProperties.py)

## 10、调用截图DLL
+
[运行 ScreenShotDll.py](ScreenShotDll.py)

## 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 @@

## 13、右侧消息通知栏
+
[运行 Notification.py](Notification.py)

## 14、验证码控件
+
[运行 VerificationCode.py](VerificationCode.py)
1. 更新为paintEvent方式,采用上下跳动
@@ -149,24 +161,26 @@

## 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)

## 16、使用Threading
+
[运行 QtThreading.py](QtThreading.py)
在PyQt中使用Theading线程
@@ -174,14 +188,15 @@ PyQt 结合 Opencv 进行人脸检测;

## 17、背景连线动画
+
[运行 CircleLine.py](CircleLine.py)
主要参考 [背景连线动画.html](Data/背景连线动画.html)

-
## 18、无边框圆角对话框
+
[运行 FramelessDialog.py](FramelessDialog.py)
1. 通过设置 `self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)` 和 `self.setAttribute(Qt.WA_TranslucentBackground, True)` 达到无边框和背景透明
@@ -192,6 +207,7 @@ PyQt 结合 Opencv 进行人脸检测;

## 19、调整窗口显示边框
+
[运行 ShowFrameWhenDrag.py](ShowFrameWhenDrag.py)
1. 全局设置是【】在控制面板中->调整Windows的外观和性能->去掉勾选 拖动时显示窗口内容】
@@ -204,6 +220,7 @@ PyQt 结合 Opencv 进行人脸检测;

## 20、判断信号是否连接
+
[运行 IsSignalConnected.py](IsSignalConnected.py)
1. 通过 `isSignalConnected` 判断是否连接
@@ -212,6 +229,7 @@ PyQt 结合 Opencv 进行人脸检测;

## 21、调用虚拟键盘
+
[运行 CallVirtualKeyboard.py](CallVirtualKeyboard.py)
1. Windows上调用的是`osk.exe`
@@ -221,6 +239,7 @@ PyQt 结合 Opencv 进行人脸检测;

## 22、动态忙碌光标
+
[运行 GifCursor.py](GifCursor.py)
通过定时器不停的修改光标图片来实现动态效果
@@ -228,6 +247,7 @@ PyQt 结合 Opencv 进行人脸检测;

## 23、屏幕变动监听
+
[运行 ScreenNotify.py](ScreenNotify.py)
通过定时器减少不同的变化信号,尽量保证只调用一次槽函数来获取信息
@@ -235,10 +255,19 @@ PyQt 结合 Opencv 进行人脸检测;

## 24、无边框窗口
+
[运行 NewFramelessWindow.py](NewFramelessWindow.py)
1. 该方法只针对 `Qt5.15` 以上版本有效
2. 通过事件过滤器判断边缘设置鼠标样式
3. 处理点击事件交通过 `QWindow.startSystemMove` 和 `QWindow.startSystemResize` 传递给系统处理
-
\ No newline at end of file
+
+
+## 25、属性绑定
+
+[运行 TestSerializeModel.py](TestSerializeModel.py)
+
+类似:[json数据绑定](../QTreeView#3json数据绑定)
+
+
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)

## 2、禁止右键点击功能、鼠标滚轮,添加滚动条等功能
+
[运行 graph1.py](graph1.py) | [查看 graphTest.ui](Data/graphTest.ui)

-## 3、不用修改源码,重加载,解决右键保存图片异常;解决自定义坐标轴密集显示;禁止鼠标事件;
+## 3、不用修改源码,重加载,解决右键保存图片异常;解决自定义坐标轴密集显示;禁止鼠标事件
+
[加载 tools.py](tools.py)
-## 4、QScrollArea添加和修改大小例子;
+## 4、QScrollArea添加和修改大小例子
+
[运行 testGraphAnalysis.py](testGraphAnalysis.py) | [查看 graphAnalysis.ui](Data/graphAnalysis.ui)

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软件)
-
\ No newline at end of file
+
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美化,顶部背景颜色和高度,上下月按钮、月份选择、年选择、菜单
-
\ No newline at end of file
+
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())`获取当前选中的路径
+
+
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`对齐
-
\ No newline at end of file
+
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)` 方法根据文件类型返回对应的图标
-
\ No newline at end of file
+
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 动画(如圆形加载图)
-
\ No newline at end of file
+
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`加载字体文件
-
\ No newline at end of file
+
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. 不能对父控件使用
-
\ No newline at end of file
+
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 @@

## 2、添加QWidget
+
[运行 AddQWidget.py](AddQWidget.py)
通过 `QGraphicsScene.addWidget` 添加自定义QWidget
@@ -22,6 +24,7 @@

## 3、图片查看器
+
[运行 ImageView.py](ImageView.py)
支持放大缩小和移动
@@ -29,6 +32,7 @@

## 3、图标拖拽
+
[运行 DragGraphics.py](DragGraphics.py)
该示例主要是包含左侧树状图标列表和右侧视图显示,从左侧拖拽到右侧
@@ -36,4 +40,4 @@
1. 重写`QListWidget`的`startDrag`函数用来封装拖拽数据
2. 重写`QGraphicsView`的`dragEnterEvent`、`dragMoveEvent`、`dropEvent`函数用来处理拖拽事件
-
\ No newline at end of file
+
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 动画(如圆形加载图)
-
\ No newline at end of file
+
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)

## 2、边距和间隔
+
[查看 HorizontalLayoutMargin.ui](Data/HorizontalLayoutMargin.ui)
1. 通过`setContentsMargins(-1, -1, 20, -1)`设置左上右下的边距,-1表示默认值
@@ -19,6 +21,7 @@

## 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)
```
-
\ No newline at end of file
+
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 @@

## 2、图片旋转
+
[运行 ImageRotate.py](ImageRotate.py)
1. 水平翻转 `QImage.mirrored(True, False)`
@@ -42,6 +44,7 @@

## 3、仿网页图片错位显示
+
[运行 ImageSlipped.py](ImageSlipped.py)
1. 设置`setMouseTracking(True)`开启鼠标跟踪
@@ -51,6 +54,7 @@

## 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'

### 5、圆形图片
+
[运行 CircleImage.py](CircleImage.py)
使用`QPainter`的`setClipPath`方法结合`QPainterPath`对图片进行裁剪从而实现圆形图片。
-
\ No newline at end of file
+
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 @@

## 2、显示自定义Widget并排序
+
[运行 CustomWidgetSortItem.py](CustomWidgetSortItem.py)
1. 对QListView设置代理 `QSortFilterProxyModel`
@@ -21,9 +23,11 @@

## 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)
```
-
\ No newline at end of file
+
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 @@

## 2、自定义可拖拽Item
+
[运行 DragDrop.py](DragDrop.py)

## 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 @@

## 4、仿折叠控件效果
+
[运行 FoldWidget.py](FoldWidget.py)
1. 利用`QListWidget`设置Item的自定义控件
@@ -56,8 +60,9 @@

## 5、列表常用信号
+
[运行 SignalsExample.py](SignalsExample.py)
-根据官网文档 https://doc.qt.io/qt-5/qlistwidget.html#signals 中的信号介绍编写
+根据官网文档 中的信号介绍编写
-
\ No newline at end of file
+
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):

## 2、仿QQ右键菜单
+
[运行 QQMenu.py](QQMenu.py)
-
\ No newline at end of file
+
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 @@

## 2、自定义图标等
+
[运行 CustomColorIcon.py](CustomColorIcon.py)

## 3、消息框按钮文字汉化
+
[运行 ChineseText.py](ChineseText.py)
1. 因为Qt5的翻译文件还是沿用旧的Qt4的结构导致部分地方无法翻译
2. 可以通过手动重新编译翻译文件解决问题
3. 这里可以通过QSS特性修改按钮文字,详细见代码
-
\ No newline at end of file
+
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`
-
\ No newline at end of file
+
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)

## 2、简易画板
+
[运行 Draw.py](Draw.py)
-
\ No newline at end of file
+
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 @@

## 2、交互执行命令
+
[运行 InteractiveRun.py](InteractiveRun.py)
`QProcess` 也可以用于交互式执行命令,具体需要如下几步:
@@ -30,4 +33,4 @@
3. 通过`readyReadStandardOutput`信号读取进程输出
4. 通过`writeData`向进程写入数据
-
\ No newline at end of file
+
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 @@

## 2、圆圈进度条
+
[运行 RoundProgressBar.py](RoundProgressBar.py)

## 3、百分比进度条
+
[运行 PercentProgressBar.py](PercentProgressBar.py)

## 4、Metro进度条
+
[运行 MetroCircleProgress.py](MetroCircleProgress.py)

## 5、水波纹进度条
+
[运行 WaterProgressBar.py](WaterProgressBar.py)
1. 利用正弦函数根据0-width的范围计算y坐标
@@ -41,13 +46,15 @@

## 6、圆形水位进度条
+
[运行 WaterProgress.py](WaterProgress.py)
-参考 https://github.com/linuxdeepin/dtkwidget/blob/master/src/widgets/dwaterprogress.cpp
+参考

## 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`关闭窗口函数

-
+
## 2、右键菜单动画
+
[运行 MenuAnimation.py](MenuAnimation.py)
1. 使用`QPropertyAnimation`对菜单控件的`geometry`属性进行修改
@@ -30,6 +32,7 @@

## 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):

## 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):

## 5、窗口抖动
+
[运行 ShakeWindow.py](ShakeWindow.py)
通过`QPropertyAnimation`对控件的pos属性进行死去活来的修改
@@ -128,6 +135,7 @@ def findClose(points):

## 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之前的情况,以及图片的缩放动画
-
\ No newline at end of file
+
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 @@

## 2、QTabWidget 角落控件位置
+
[运行 TabCornerWidget.py](TabCornerWidget.py)
1. 通过 `app.setStyle(TabCornerStyle())` 设置代理样式
@@ -22,4 +24,4 @@
原理是通过代理样式中对 `SE_TabWidgetRightCorner` 计算的结果进行校正,使得角落控件占满右边空白位置,
然后再配合自定义控件中使用 `QSpacerItem` 占据右边位置使得 + 号按钮居左,表现效果为 + 号按钮跟随标签的增加和减少
-
\ No newline at end of file
+
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 @@

## 2、按钮底部线条进度
+
[运行 BottomLineProgress.py](BottomLineProgress.py)
在按钮下方画一条线,根据百分值绘制
@@ -21,6 +25,7 @@

## 3、按钮文字旋转进度
+
[运行 FontRotate.py](FontRotate.py)
利用字体,使用FontAwesome字体来显示一个圆形进度条,然后利用旋转动画
@@ -28,9 +33,22 @@

## 4、按钮常用信号
+
[运行 SignalsExample.py](SignalsExample.py)
-根据官网文档 https://doc.qt.io/qt-5/qabstractbutton.html#signals 中的信号介绍编写
+根据官网文档 中的信号介绍编写
按钮的点击、按下、释放、选中信号演示
-
\ No newline at end of file
+
+
+## 5、旋转动画按钮
+
+[运行 RotateButton.py](RotateButton.py)
+
+
+
+## 6、弹性动画按钮
+
+[运行 RubberBandButton.py](RubberBandButton.py)
+
+
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的定位
-
\ No newline at end of file
+
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` 设置流控制
-
-
\ No newline at end of file
+
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):

## 2、双层圆环样式
+
[运行 QssQSlider.py](QssQSlider.py) | [运行 PaintQSlider.py](PaintQSlider.py)


## 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图片
-
\ No newline at end of file
+
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`来实现鼠标的其它事件
-
\ No newline at end of file
+
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: 用设计设的做法 :

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:

@@ -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)
+```
+
+
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. 把字符串复制到剪切板中
 
-
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 @@

## 2、TableWidget嵌入部件
+
[运行 TableWidget.py](TableWidget.py)
点击开始按钮,进度条开始

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` 函数可以监听到所有的资源加载,然后动态返回内容
-
\ No newline at end of file
+
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`函数
-
\ No newline at end of file
+
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)

## 2、moveToThread
+
[运行 moveToThread.py](moveToThread.py)

## 3、线程挂起恢复
+
[运行 SuspendThread.py](SuspendThread.py)
注意,这里只是简单演示,在应用这些代码时要小心
@@ -31,6 +34,7 @@

## 4、线程休眠唤醒
+
[运行 WakeupThread.py](WakeupThread.py)
使用 `QWaitCondition` 的 `wait` 和 `wakeAll` 方法
@@ -38,8 +42,9 @@

## 5、线程退出
+
[运行 QuitThread.py](QuitThread.py)
`isInterruptionRequested` 和 `requestInterruption` 函数作为退出标识调用
-
\ No newline at end of file
+
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)
+
+
+
+## 2、json树形结构查询和修改
+
+[运行 TestModelModify.py](TestModelModify.py)
+
+
+
+## 3、json数据绑定
+
+[运行 TestSerializeModel.py](TestSerializeModel.py)
+
+
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
-

## 2、点击父节点全选/取消全选子节点
+
[运行 testTreeWidget.py](testTreeWidget.py) | [查看 testTree.ui](Data/testTree.ui)
点击父节点全选/取消全选子节点
@@ -21,9 +22,10 @@

## 3、禁止父节点
+
[运行 ParentNodeForbid.py](ParentNodeForbid.py)
1. 父节点通过设置`pitem1.setFlags(pitem1.flags() & ~Qt.ItemIsSelectable)`为不可选
2. 完全禁用点击等需要重写`mousePressEvent`事件并结合item的标志来判断
-
\ No newline at end of file
+
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)

## 2、边距和间隔
+
[查看 VerticalLayoutMargin.ui](Data/VerticalLayoutMargin.ui)
1. 通过`setContentsMargins(20, 20, -1, -1)`设置左上右下的边距,-1表示默认值
@@ -19,6 +21,7 @@

## 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)
```
-
\ No newline at end of file
+
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。
-
\ No newline at end of file
+
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 @@

## 2、和Js交互操作
+
[运行 JsSignals.py](JsSignals.py)
通过`qwebchannel.js`和`QWebChannel.registerObject`进行Python对象和Javascript的交互
@@ -26,6 +28,7 @@

## 3、网页整体截图
+
[运行 ScreenShotPage.py](ScreenShotPage.py)
1. 方式1:目前通过不完美方法(先调整`QWebEngineView`的大小为`QWebEnginePage`的内容大小,等待一定时间后截图再还原大小)
@@ -34,6 +37,7 @@

## 4、同网站不同用户
+
[运行 SiteDiffUser.py](SiteDiffUser.py)
原理是为每个`QWebEngineView`创建一个`QWebEnginePage`,且使用独立的`QWebEngineProfile`,并配置`persistentStoragePath`不同路径
@@ -41,6 +45,7 @@

## 5、拦截请求
+
[运行 BlockRequest.py](BlockRequest.py)
通过`QWebEngineUrlRequestInterceptor`中的`interceptRequest`方法对每个请求做拦截过滤
@@ -48,6 +53,7 @@

## 6、拦截请求内容
+
[运行 BlockRequestData.py](BlockRequestData.py)
这里用了一个投巧的办法,原理是先通过`QWebEngineUrlRequestInterceptor`中的`interceptRequest`方法对每个请求做拦截过滤,
@@ -56,9 +62,10 @@

## 7、设置Cookie
+
[运行 SetCookies.py](SetCookies.py)
通过`QWebEngineProfile`中得到的`cookieStore`来添加`QNetworkCookie`对象实现,
需要注意的是httpOnly=true时,通过js无法获取
-
\ No newline at end of file
+
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 @@

## 2、获取Cookie
+
[运行 GetCookie.py](GetCookie.py)
从`page()`中得到`QNetworkAccessManager`,在从中得到`QNetworkCookieJar`,
@@ -24,6 +26,7 @@

## 3、和Js交互操作
+
[运行 JsSignals.py](JsSignals.py)
通过`QWebFrame`的`addToJavaScriptWindowObject`函数提供进行Python对象和Javascript的交互
@@ -33,6 +36,7 @@

## 4、网页整体截图
+
[运行 ScreenShotPage.py](ScreenShotPage.py)
1. 方式1:原理是通过`QWebView.QWebPage.QWebFrame`得到内容的高度,然后设置`QWebPage.setViewportSize`的大小,
@@ -42,6 +46,7 @@

## 5、播放Flash
+
[运行 PlayFlash.py](PlayFlash.py)
1. 重点在于设置 `os.environ['QTWEBKIT_PLUGIN_PATH'] = os.path.abspath('Data')` ,非常重要,设置为NPSWF32.dll文件所在目录
@@ -50,8 +55,9 @@

## 6、拦截请求
+
[运行 BlockRequest.py](BlockRequest.py)
通过`QNetworkAccessManager`中的`createRequest`方法对每个请求做拦截过滤
-
\ No newline at end of file
+
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增加背景等
-
\ No newline at end of file
+
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)

## 2、折线堆叠图
+
[运行 LineStack.py](LineStack.py)
仿照 [line-stack](http://echarts.baidu.com/demo.html#line-stack)
@@ -31,6 +33,7 @@

## 3、柱状堆叠图
+
[运行 BarStack.py](BarStack.py)
仿照 [bar-stack](http://echarts.baidu.com/demo.html#bar-stack)
@@ -38,68 +41,81 @@

## 4、LineChart自定义xy轴
+
[运行 CustomXYaxis.py](CustomXYaxis.py)

## 5、ToolTip提示
-[运行 ToolTip.py](ToolTip.py) | [运行 ToolTip2.py](ToolTip2.py)
+
+[运行 ToolTip.py](ToolTip.py) | [运行 ToolTip2.py](ToolTip2.py)
 
## 6、动态曲线图
+
[运行 DynamicSpline.py](DynamicSpline.py)

## 7、区域图表
+
[运行 AreaChart.py](AreaChart.py)

## 8、柱状图表
+
[运行 BarChart.py](BarChart.py)

## 9、饼状图表
+
[运行 PieChart.py](PieChart.py)

## 10、样条图表
+
[运行 SplineChart.py](SplineChart.py)

## 11、百分比柱状图表
+
[运行 PercentBarChart.py](PercentBarChart.py)

## 12、横向柱状图表
+
[运行 HorizontalBarChart.py](HorizontalBarChart.py)

## 13、横向百分比柱状图表
+
[运行 HorizontalPercentBarChart.py](HorizontalPercentBarChart.py)

## 14、散点图表
+
[运行 ScatterChart.py](ScatterChart.py)

## 15、图表主题动画
+
[运行 ChartThemes.py](ChartThemes.py)

## 16、CPU动态折线图
+
[运行 CpuLineChart.py](CpuLineChart.py)
通过设置x轴的时间范围并替换y点达到动态移动效果
-
\ No newline at end of file
+
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)

## 2、太阳磁场线
+
[运行 MagneticOfSun.py](MagneticOfSun.py)

## 3、余弦波3D
+
[运行 ScatterVisualization.py](ScatterVisualization.py)
-
\ No newline at end of file
+
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)

## 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: {
}
```
-

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)
绑定信号槽同步双方数据,属性方法测试没通过,详细注释在代码中
-
\ No newline at end of file
+
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 @@

## 2、任务栏缩略图工具按钮
+
[运行 ThumbnailToolBar.py](ThumbnailToolBar.py)
`QWinThumbnailToolBar`和`QWinThumbnailToolButton`的组合实现音乐播放器的播放、上下一曲按钮
-
\ No newline at end of file
+
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)
## 状态
+

## 目录
@@ -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`中没有判断数据长度进行多次接收(类似粘包处理)

## [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(对单张图片进行下载)
-
\ No newline at end of file
+
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):
```
# 截图
-
\ No newline at end of file
+
+
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

@@ -19,4 +22,4 @@ test.irony
main.py
-
\ No newline at end of file
+
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