diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e60ca202..d702b3ce 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,7 @@ # These are supported funding model platforms -custom: https://github.com/PyQt5/PyQt#donate-打赏 +liberapay: Irony +custom: + - https://github.com/PyQt5/PyQt#donate-打赏 + - https://github.com/PyQt5/PyQt/blob/master/Donate/zhifubao.png + - https://github.com/PyQt5/PyQt/blob/master/Donate/weixin.png diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml new file mode 100644 index 00000000..e3084824 --- /dev/null +++ b/.github/workflows/mirror.yml @@ -0,0 +1,15 @@ +name: 'GitHub Actions Mirror' + +on: [push, delete] + +jobs: + mirror_to_gitee: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: pixta-dev/repository-mirroring-action@v1 + with: + target_repo_url: + git@gitee.com:PyQt5/PyQt.git + ssh_private_key: + ${{ secrets.GIT_KEY }} diff --git a/.gitignore b/.gitignore index fb1435fc..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] @@ -24,8 +784,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ parts/ sdist/ var/ diff --git a/Demo/AutoRestart.py b/Demo/AutoRestart.py index 70375954..024d61bb 100644 --- a/Demo/AutoRestart.py +++ b/Demo/AutoRestart.py @@ -1,21 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年3月31日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: AutoRestart @description: -''' +""" -from optparse import OptionParser import os import sys +from optparse import OptionParser -from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QHBoxLayout - +try: + from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QHBoxLayout +except ImportError: + from PySide2.QtWidgets import QApplication, QPushButton, QWidget, QHBoxLayout canRestart = True diff --git a/Demo/BubbleTips.py b/Demo/BubbleTips.py index 8af85c76..6f842a3c 100644 --- a/Demo/BubbleTips.py +++ b/Demo/BubbleTips.py @@ -1,30 +1,31 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月27日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: BubbleTips @description: -''' +""" import sys -from PyQt5.QtCore import QRectF, Qt, QPropertyAnimation, pyqtProperty, \ - QPoint, QParallelAnimationGroup, QEasingCurve -from PyQt5.QtGui import QPainter, QPainterPath, QColor, QPen -from PyQt5.QtWidgets import QLabel, QWidget, QVBoxLayout, QApplication,\ - QLineEdit, QPushButton - - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QRectF, Qt, QPropertyAnimation, pyqtProperty, \ + QPoint, QParallelAnimationGroup, QEasingCurve + from PyQt5.QtGui import QPainter, QPainterPath, QColor, QPen + from PyQt5.QtWidgets import QLabel, QWidget, QVBoxLayout, QApplication, \ + QLineEdit, QPushButton +except ImportError: + from PySide2.QtCore import QRectF, Qt, QPropertyAnimation, Property as pyqtProperty, \ + QPoint, QParallelAnimationGroup, QEasingCurve + from PySide2.QtGui import QPainter, QPainterPath, QColor, QPen + from PySide2.QtWidgets import QLabel, QWidget, QVBoxLayout, QApplication, \ + QLineEdit, QPushButton class BubbleLabel(QWidget): - BackgroundColor = QColor(195, 195, 195) BorderColor = QColor(150, 150, 150) @@ -133,10 +134,10 @@ def setWindowOpacity(self, opacity): opacity = pyqtProperty(float, windowOpacity, setWindowOpacity) -class TestWidget(QWidget): +class Window(QWidget): def __init__(self, *args, **kwargs): - super(TestWidget, self).__init__(*args, **kwargs) + super(Window, self).__init__(*args, **kwargs) layout = QVBoxLayout(self) self.msgEdit = QLineEdit(self, returnPressed=self.onMsgShow) self.msgButton = QPushButton("显示内容", self, clicked=self.onMsgShow) @@ -158,6 +159,6 @@ def onMsgShow(self): if __name__ == "__main__": app = QApplication(sys.argv) - w = TestWidget() + w = Window() w.show() sys.exit(app.exec_()) diff --git a/Demo/CallVirtualKeyboard.py b/Demo/CallVirtualKeyboard.py index b44af512..4afe18ba 100644 --- a/Demo/CallVirtualKeyboard.py +++ b/Demo/CallVirtualKeyboard.py @@ -4,20 +4,19 @@ """ Created on 2019年5月22日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: Demo.CallVirtualKeyboard @description: 调用系统虚拟键盘 """ import glob -from PyQt5.QtCore import QProcess, QSysInfo -from PyQt5.QtWidgets import QWidget, QTextEdit, QVBoxLayout, QPushButton - - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QProcess, QSysInfo + from PyQt5.QtWidgets import QApplication, QWidget, QTextEdit, QVBoxLayout, QPushButton +except ImportError: + from PySide2.QtCore import QProcess, QSysInfo + from PySide2.QtWidgets import QApplication, QWidget, QTextEdit, QVBoxLayout, QPushButton class Window(QWidget): @@ -50,7 +49,7 @@ def _onOpenKeyboard(self): self.resultEdit.append('start osk error: %s' % e) elif kernelType == 'darwin': pass -# elif kernelType=='linux': + # elif kernelType=='linux': else: ret = QProcess.startDetached('florence') self.resultEdit.append('start florence: %s' % ret) @@ -62,7 +61,7 @@ def _onOpenKeyboard(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/Demo/CircleLine.py b/Demo/CircleLine.py index 11a92dc0..83102c7e 100644 --- a/Demo/CircleLine.py +++ b/Demo/CircleLine.py @@ -4,7 +4,7 @@ """ Created on 2019年3月19日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CircleLine @description: @@ -14,13 +14,14 @@ from random import random, randint from time import time -from PyQt5.QtCore import QTimer, Qt -from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPen -from PyQt5.QtWidgets import QWidget - - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' +try: + from PyQt5.QtCore import QTimer, Qt + from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPen + from PyQt5.QtWidgets import QWidget, QApplication +except ImportError: + from PySide2.QtCore import QTimer, Qt + from PySide2.QtGui import QColor, QPainter, QPainterPath, QPen + from PySide2.QtWidgets import QWidget, QApplication # 最小和最大半径、半径阈值和填充圆的百分比 radMin = 10 @@ -60,18 +61,21 @@ circleExpSp = 0.00004 circlePulse = False + # 生成随机整数 a<=x<=b def randint(a, b): return floor(random() * (b - a + 1) + a) + # 生成随机小数 def randRange(a, b): return random() * (b - a) + a + # 生成接近a的随机小数 @@ -88,7 +92,7 @@ def __init__(self, background, width, height): self.radius = hyperRange(radMin, radMax) self.filled = (False if randint( 0, 100) > concentricCircle else 'full') if self.radius < radThreshold else ( - False if randint(0, 100) > concentricCircle else 'concentric') + False if randint(0, 100) > concentricCircle else 'concentric') self.color = colors[randint(0, len(colors) - 1)] self.borderColor = colors[randint(0, len(colors) - 1)] self.opacity = 0.05 @@ -233,29 +237,29 @@ def renderPoints(self, painter, circles): # otherwise we connect them only if the dist is < linkDist if dist < self.linkDist: xi = (1 if circles[i].x < circles[j].x else - - 1) * abs(circles[i].radius * deltax / dist) + 1) * abs(circles[i].radius * deltax / dist) yi = (1 if circles[i].y < circles[j].y else - - 1) * abs(circles[i].radius * deltay / dist) + 1) * abs(circles[i].radius * deltay / dist) xj = (-1 if circles[i].x < circles[j].x else 1) * \ - abs(circles[j].radius * deltax / dist) + abs(circles[j].radius * deltax / dist) yj = (-1 if circles[i].y < circles[j].y else 1) * \ - abs(circles[j].radius * deltay / dist) + abs(circles[j].radius * deltay / dist) path = QPainterPath() path.moveTo(circles[i].x + xi, circles[i].y + yi) path.lineTo(circles[j].x + xj, circles[j].y + yj) -# samecolor = circles[i].color == circles[j].color + # samecolor = circles[i].color == circles[j].color c = QColor(circles[i].borderColor) c.setAlphaF(min(circles[i].opacity, circles[j].opacity) * ((self.linkDist - dist) / self.linkDist)) painter.setPen(QPen(c, ( lineBorder * backgroundMlt if circles[i].background else lineBorder) * ( - (self.linkDist - dist) / self.linkDist))) + (self.linkDist - dist) / self.linkDist))) painter.drawPath(path) if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = CircleLineWindow() w.resize(800, 600) diff --git a/Demo/CustomProperties.py b/Demo/CustomProperties.py index 8a053a2e..43eab24d 100644 --- a/Demo/CustomProperties.py +++ b/Demo/CustomProperties.py @@ -1,25 +1,25 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年4月12日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: 自定义属性测试 @description: -''' +""" from random import randint -from PyQt5.QtCore import pyqtProperty, pyqtSignal -from PyQt5.QtWidgets import QPushButton - - -__version__ = "0.0.1" +try: + from PyQt5.QtCore import pyqtProperty, pyqtSignal + from PyQt5.QtWidgets import QPushButton, QApplication +except ImportError: + from PySide2.QtCore import Property as pyqtProperty, Signal as pyqtSignal + from PySide2.QtWidgets import QPushButton, QApplication class Window(QPushButton): - bgChanged = pyqtSignal(str, str) def __init__(self): @@ -56,7 +56,7 @@ def setTextColor(self, c): if __name__ == "__main__": import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.setStyleSheet( diff --git a/Demo/Data/frameless.ui b/Demo/Data/frameless.ui new file mode 100644 index 00000000..a7919b02 --- /dev/null +++ b/Demo/Data/frameless.ui @@ -0,0 +1,197 @@ + + + FormFrameless + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + Symbola + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 253 + 20 + + + + + + + + + 36 + 36 + + + + + 36 + 36 + + + + + webdings + + + + Minimum + + + 0 + + + + + + + + 36 + 36 + + + + + 36 + 36 + + + + + webdings + + + + Maximum + + + 1 + + + + + + + + 36 + 36 + + + + + 36 + 36 + + + + + webdings + + + + Normal + + + 2 + + + + + + + + 36 + 36 + + + + + 36 + 36 + + + + + webdings + + + + Close + + + r + + + + + + + + + + QFrame::NoFrame + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">frameless window with move and resize</p></body></html> + + + + + + + + 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/EmbedWindow.py b/Demo/EmbedWindow.py index c3360d76..b6ffe04f 100644 --- a/Demo/EmbedWindow.py +++ b/Demo/EmbedWindow.py @@ -4,22 +4,26 @@ """ Created on 2018年3月1日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: EmbedWindow @description: 嵌入外部窗口 """ -__Author__ = 'By: Irony\nQQ: 892768447\nEmail: 892768447@qq.com' -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - -from PyQt5.QtGui import QWindow -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QListWidget,\ - QLabel import win32con import win32gui +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QWindow + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QListWidget, \ + QLabel, QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QWindow + from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QListWidget, \ + QLabel, QApplication + class Window(QWidget): @@ -32,16 +36,22 @@ def __init__(self, *args, **kwargs): layout.addWidget(QPushButton('获取所有可用、可视窗口', self, clicked=self._getWindowList, maximumHeight=30)) + layout.addWidget(QPushButton('释放窗口', clicked=self.releaseWidget, maximumHeight=30)) layout.addWidget( QLabel('双击列表中的项目则进行嵌入目标窗口到下方\n格式为:句柄|父句柄|标题|类名', self, maximumHeight=30)) self.windowList = QListWidget( self, itemDoubleClicked=self.onItemDoubleClicked, maximumHeight=200) layout.addWidget(self.windowList) + def releaseWidget(self): + """释放窗口""" + if self.layout().count() == 5: + self.restore() + self._getWindowList() + def closeEvent(self, event): """窗口关闭""" - if self.layout().count() == 4: - self.restore() + self.releaseWidget() super(Window, self).closeEvent(event) def _getWindowList(self): @@ -55,39 +65,41 @@ def onItemDoubleClicked(self, item): self.windowList.takeItem(self.windowList.indexFromItem(item).row()) hwnd, phwnd, _, _ = item.text().split('|') # 开始嵌入 - - if self.layout().count() == 4: - # 如果数量等于4说明之前已经嵌入了一个窗口,现在需要把它释放出来 - self.restore() + self.releaseWidget() hwnd, phwnd = int(hwnd), int(phwnd) # 嵌入之前的属性 style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE) exstyle = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) - print('save', hwnd, style, exstyle) + wrect = win32gui.GetWindowRect(hwnd)[:2] + win32gui.GetClientRect(hwnd)[2:] + print('save', hwnd, style, exstyle, wrect) widget = QWidget.createWindowContainer(QWindow.fromWinId(hwnd)) widget.hwnd = hwnd # 窗口句柄 widget.phwnd = phwnd # 父窗口句柄 widget.style = style # 窗口样式 widget.exstyle = exstyle # 窗口额外样式 + widget.wrect = wrect # 窗口位置 self.layout().addWidget(widget) + widget.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint) + win32gui.SetParent(hwnd, int(self.winId())) + def restore(self): """归还窗口""" # 有bug,归还后窗口没有了WS_VISIBLE样式,不可见 - widget = self.layout().itemAt(3).widget() - print('restore', widget.hwnd, widget.style, widget.exstyle) - win32gui.SetParent(widget.hwnd, widget.phwnd) # 让它返回它的父窗口 - win32gui.SetWindowLong( - widget.hwnd, win32con.GWL_STYLE, widget.style | win32con.WS_VISIBLE) # 恢复样式 - win32gui.SetWindowLong( - widget.hwnd, win32con.GWL_EXSTYLE, widget.exstyle) # 恢复样式 - win32gui.ShowWindow( - widget.hwnd, win32con.SW_SHOW) # 显示窗口 + widget = self.layout().itemAt(4).widget() + hwnd, phwnd, style, exstyle, wrect = widget.hwnd, widget.phwnd, widget.style, widget.exstyle, widget.wrect + print('restore', hwnd, phwnd, style, exstyle, wrect) widget.close() self.layout().removeWidget(widget) # 从布局中移出 widget.deleteLater() + win32gui.SetParent(hwnd, phwnd) # 让它返回它的父窗口 + win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, style | win32con.WS_VISIBLE) # 恢复样式 + win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, exstyle) # 恢复样式 + win32gui.ShowWindow(hwnd, win32con.SW_SHOW) # 显示窗口 + win32gui.SetWindowPos(hwnd, 0, wrect[0], wrect[1], wrect[2], wrect[3], win32con.SWP_NOACTIVATE) + def _enumWindows(self, hwnd, _): """遍历回调函数""" if hwnd == self.myhwnd: @@ -102,7 +114,10 @@ def _enumWindows(self, hwnd, _): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + import cgitb + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/Demo/FacePoints.py b/Demo/FacePoints.py index 7ebea2b8..98b6165c 100644 --- a/Demo/FacePoints.py +++ b/Demo/FacePoints.py @@ -1,31 +1,33 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月29日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: FacePoints @description: 人脸特征点 -''' -from bz2 import BZ2Decompressor +""" import cgitb import os import sys +from bz2 import BZ2Decompressor -from PyQt5.QtCore import QTimer, QUrl, QFile, QIODevice -from PyQt5.QtGui import QImage, QPixmap -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest -from PyQt5.QtWidgets import QLabel, QMessageBox, QApplication import cv2 # @UnresolvedImport import dlib import numpy - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QTimer, QUrl, QFile, QIODevice + from PyQt5.QtGui import QImage, QPixmap + from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PyQt5.QtWidgets import QLabel, QMessageBox, QApplication +except ImportError: + from PySide2.QtCore import QTimer, QUrl, QFile, QIODevice + from PySide2.QtGui import QImage, QPixmap + from PySide2.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PySide2.QtWidgets import QLabel, QMessageBox, QApplication DOWNSCALE = 4 URL = 'http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2' @@ -163,7 +165,7 @@ def onCapture(self): if __name__ == "__main__": - sys.excepthook = cgitb.enable(1, None, 5, '') + cgitb.enable(format='text') app = QApplication(sys.argv) w = OpencvWidget() w.show() diff --git a/Demo/FollowWindow.py b/Demo/FollowWindow.py index 5e54287c..41ad74c7 100644 --- a/Demo/FollowWindow.py +++ b/Demo/FollowWindow.py @@ -4,23 +4,21 @@ """ Created on 2018年10月22日 @author: Irony -@site: https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: FollowWindow @description: 跟随外部窗口 """ import os -from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton import win32gui - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QTimer + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication +except ImportError: + from PySide2.QtCore import QTimer + from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication class Window(QWidget): @@ -53,7 +51,7 @@ def checkWindow(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + # 先检测是否已有记事本打开 hwnd = win32gui.FindWindow('Notepad', None) print('hwnd', hwnd) diff --git a/Demo/FramelessDialog.py b/Demo/FramelessDialog.py index 4f3d551a..b55bef3f 100644 --- a/Demo/FramelessDialog.py +++ b/Demo/FramelessDialog.py @@ -4,20 +4,22 @@ """ Created on 2019年4月19日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: FramelessDialog @description: 无边框圆角对话框 """ -from PyQt5.QtCore import Qt, QSize, QTimer -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QWidget,\ - QGraphicsDropShadowEffect, QPushButton, QGridLayout, QSpacerItem,\ - QSizePolicy - -__Author__ = "Irony" -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import Qt, QSize, QTimer + from PyQt5.QtWidgets import QDialog, QVBoxLayout, QWidget, \ + QGraphicsDropShadowEffect, QPushButton, QGridLayout, QSpacerItem, \ + QSizePolicy, QApplication +except ImportError: + from PySide2.QtCore import Qt, QSize, QTimer + from PySide2.QtWidgets import QDialog, QVBoxLayout, QWidget, \ + QGraphicsDropShadowEffect, QPushButton, QGridLayout, QSpacerItem, \ + QSizePolicy, QApplication Stylesheet = """ #Custom_Widget { @@ -77,7 +79,7 @@ def sizeHint(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Dialog() w.exec_() diff --git a/Demo/FramelessWindow.py b/Demo/FramelessWindow.py index aef6c261..7e1608e2 100644 --- a/Demo/FramelessWindow.py +++ b/Demo/FramelessWindow.py @@ -1,22 +1,24 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QTextEdit -from Lib.FramelessWindow import FramelessWindow # @UnresolvedImport +""" +Created on 2018年4月30日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: Test +@description: +""" + +try: + from PyQt5.QtGui import QIcon + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QTextEdit, QApplication +except ImportError: + from PySide2.QtGui import QIcon + from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QTextEdit, QApplication -# Created on 2018年4月30日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: Test -# description: -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +from Lib.FramelessWindow import FramelessWindow # @UnresolvedImport class MainWindow(QWidget): @@ -63,7 +65,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) app.setStyleSheet(StyleSheet) w = FramelessWindow() diff --git a/Demo/GifCursor.py b/Demo/GifCursor.py index 2dbdc527..0b1ea8f1 100644 --- a/Demo/GifCursor.py +++ b/Demo/GifCursor.py @@ -4,19 +4,18 @@ """ Created on 2020年3月13日 @author: Irony -@site: https://pyqt.site https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: Demo.GifCursor @description: """ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton -from Demo.Lib.QCursorGif import QCursorGif +try: + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication +except ImportError: + from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2020' -__Version__ = 1.0 +from Lib.QCursorGif import QCursorGif class Window(QWidget, QCursorGif): @@ -38,8 +37,9 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, '') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/Demo/IsSignalConnected.py b/Demo/IsSignalConnected.py index 4dbb19e0..7d534f3f 100644 --- a/Demo/IsSignalConnected.py +++ b/Demo/IsSignalConnected.py @@ -4,7 +4,7 @@ """ Created on 2019年2月24日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: IsSignalConnected @description: 判断信号是否连接 @@ -13,13 +13,6 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QTextBrowser -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 - - class Window(QWidget): def __init__(self, *args, **kwargs): @@ -34,11 +27,13 @@ def __init__(self, *args, **kwargs): def doTest(self): self.retView.append(""" - # button1 clicked 是否连接: %s - # button2 clicked 是否连接: %s + # button1 clicked 是否连接: %s, %s + # button2 clicked 是否连接: %s, %s """ % ( self.isSignalConnected(self.button1, 'clicked()'), - self.isSignalConnected(self.button2, 'clicked()') + self.button1.receivers(self.button1.clicked) > 0, + self.isSignalConnected(self.button2, 'clicked()'), + self.button2.receivers(self.button2.clicked) > 0, )) def isSignalConnected(self, obj, name): @@ -57,6 +52,7 @@ def isSignalConnected(self, obj, name): if __name__ == '__main__': import sys from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/Demo/Lib/Application.py b/Demo/Lib/Application.py index c1216004..eed9376a 100644 --- a/Demo/Lib/Application.py +++ b/Demo/Lib/Application.py @@ -1,35 +1,34 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年3月30日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: 单实例应用.Application @description: -''' +""" + from PyQt5.QtCore import QSharedMemory, pyqtSignal, Qt from PyQt5.QtNetwork import QLocalSocket, QLocalServer from PyQt5.QtWidgets import QApplication -__version__ = "0.0.1" - class SharedApplication(QApplication): - + def __init__(self, *args, **kwargs): super(SharedApplication, self).__init__(*args, **kwargs) self._running = False - key = "SharedApplication" + __version__ + key = "SharedApplication" self._memory = QSharedMemory(key, self) - + isAttached = self._memory.isAttached() print("isAttached", isAttached) if isAttached: # 如果进程附加在共享内存上 detach = self._memory.detach() # 取消进程附加在共享内存上 print("detach", detach) - + if self._memory.create(1) and self._memory.error() != QSharedMemory.AlreadyExists: # 创建共享内存,如果创建失败,则说明已经创建,否则未创建 print("create ok") @@ -37,14 +36,14 @@ def __init__(self, *args, **kwargs): print("create failed") self._running = True del self._memory - + def isRunning(self): return self._running + class QSingleApplication(QApplication): - messageReceived = pyqtSignal(str) - + def __init__(self, *args, **kwargs): super(QSingleApplication, self).__init__(*args, **kwargs) appid = QApplication.applicationFilePath().lower().split("/")[-1] @@ -56,13 +55,13 @@ def __init__(self, *args, **kwargs): self._socketIn = None self._socketOut = None self._running = False - + # 先尝试连接 self._socketOut = QLocalSocket(self) self._socketOut.connectToServer(self._socketName) self._socketOut.error.connect(self.handleError) self._running = self._socketOut.waitForConnected() - + if not self._running: # 程序未运行 self._socketOut.close() del self._socketOut @@ -70,20 +69,20 @@ def __init__(self, *args, **kwargs): self._socketServer.listen(self._socketName) self._socketServer.newConnection.connect(self._onNewConnection) self.aboutToQuit.connect(self.removeServer) - + def handleError(self, message): print("handleError message: ", message) - + def isRunning(self): return self._running - + def activationWindow(self): return self._activationWindow - + def setActivationWindow(self, activationWindow, activateOnMessage=True): self._activationWindow = activationWindow self._activateOnMessage = activateOnMessage - + def activateWindow(self): if not self._activationWindow: return @@ -91,7 +90,7 @@ def activateWindow(self): self._activationWindow.windowState() & ~Qt.WindowMinimized) self._activationWindow.raise_() self._activationWindow.activateWindow() - + def sendMessage(self, message, msecs=5000): if not self._socketOut: return False @@ -99,10 +98,10 @@ def sendMessage(self, message, msecs=5000): message = str(message).encode() self._socketOut.write(message) if not self._socketOut.waitForBytesWritten(msecs): - raise RuntimeError("Bytes not written within %ss" % + raise RuntimeError("Bytes not written within %ss" % (msecs / 1000.)) return True - + def _onNewConnection(self): if self._socketIn: self._socketIn.readyRead.disconnect(self._onReadyRead) @@ -112,7 +111,7 @@ def _onNewConnection(self): self._socketIn.readyRead.connect(self._onReadyRead) if self._activateOnMessage: self.activateWindow() - + def _onReadyRead(self): while 1: message = self._socketIn.readLine() @@ -123,4 +122,4 @@ def _onReadyRead(self): def removeServer(self): self._socketServer.close() - self._socketServer.removeServer(self._socketName) \ No newline at end of file + self._socketServer.removeServer(self._socketName) diff --git a/Demo/Lib/FramelessWindow.py b/Demo/Lib/FramelessWindow.py index 04afccb9..b4fa3e0e 100644 --- a/Demo/Lib/FramelessWindow.py +++ b/Demo/Lib/FramelessWindow.py @@ -1,26 +1,29 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from PyQt5.QtCore import Qt, pyqtSignal, QPoint -from PyQt5.QtGui import QFont, QEnterEvent, QPainter, QColor, QPen -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel,\ - QSpacerItem, QSizePolicy, QPushButton -# Created on 2018年4月30日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: FramelessWindow -# description: -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +""" +Created on 2018年4月30日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: FramelessWindow +@description: +""" +try: + from PyQt5.QtCore import Qt, pyqtSignal, QPoint + from PyQt5.QtGui import QFont, QEnterEvent, QPainter, QColor, QPen + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, \ + QSpacerItem, QSizePolicy, QPushButton +except ImportError: + from PySide2.QtCore import Qt, Signal as pyqtSignal, QPoint + from PySide2.QtGui import QFont, QEnterEvent, QPainter, QColor, QPen + from PySide2.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, \ + QSpacerItem, QSizePolicy, QPushButton -class TitleBar(QWidget): +class TitleBar(QWidget): # 窗口最小化信号 windowMinimumed = pyqtSignal() # 窗口最大化信号 @@ -48,7 +51,7 @@ def __init__(self, *args, **kwargs): layout.setContentsMargins(0, 0, 0, 0) # 窗口图标 self.iconLabel = QLabel(self) -# self.iconLabel.setScaledContents(True) + # self.iconLabel.setScaledContents(True) layout.addWidget(self.iconLabel) # 窗口标题 self.titleLabel = QLabel(self) @@ -138,7 +141,6 @@ def mouseMoveEvent(self, event): class FramelessWindow(QWidget): - # 四周边距 Margins = 5 diff --git a/Demo/Lib/QCursorGif.py b/Demo/Lib/QCursorGif.py index 0ff9959c..707e7479 100644 --- a/Demo/Lib/QCursorGif.py +++ b/Demo/Lib/QCursorGif.py @@ -4,19 +4,20 @@ """ Created on 2020年3月13日 @author: Irony -@site: https://pyqt.site https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: Demo.Lib.QCursorGif @description: """ -from PyQt5.QtCore import QTimer, Qt -from PyQt5.QtGui import QCursor, QPixmap -from PyQt5.QtWidgets import QApplication - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2020' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QTimer, Qt + from PyQt5.QtGui import QCursor, QPixmap + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import QTimer, Qt + from PySide2.QtGui import QCursor, QPixmap + from PySide2.QtWidgets import QApplication class QCursorGif: @@ -55,4 +56,4 @@ def setCursorTimeout(self, timeout): def setOldCursor(self, parent=None): self._oldCursor = (parent.cursor() or Qt.ArrowCursor) if parent else ( - QApplication.instance().overrideCursor() or Qt.ArrowCursor) + QApplication.instance().overrideCursor() or Qt.ArrowCursor) diff --git a/Demo/Lib/UiNotify.py b/Demo/Lib/UiNotify.py index 20d26aa4..38f1ad92 100644 --- a/Demo/Lib/UiNotify.py +++ b/Demo/Lib/UiNotify.py @@ -6,46 +6,50 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore, QtGui, QtWidgets +try: + from PyQt5 import QtCore, QtGui, QtWidgets +except ImportError: + from PySide2 import QtCore, QtGui, QtWidgets + class Ui_NotifyForm(object): def setupUi(self, NotifyForm): NotifyForm.setObjectName("NotifyForm") NotifyForm.resize(300, 200) NotifyForm.setStyleSheet("QWidget#widgetTitle {\n" -" background-color: rgb(76, 169, 106);\n" -"}\n" -"QWidget#widgetBottom {\n" -" border-top-style: solid;\n" -" border-top-width: 2px;\n" -" border-top-color: rgb(185, 218, 201);\n" -"}\n" -"QLabel#labelTitle {\n" -" color: rgb(255, 255, 255);\n" -"}\n" -"QLabel#labelContent {\n" -" padding: 5px;\n" -"}\n" -"QPushButton {\n" -" border: none;\n" -" background: transparent;\n" -"}\n" -"QPushButton#buttonClose {\n" -" font-family: \"webdings\";\n" -" color: rgb(255, 255, 255);\n" -"}\n" -"QPushButton#buttonClose:hover {\n" -" background-color: rgb(212, 64, 39);\n" -"}\n" -"QPushButton#buttonView {\n" -" color: rgb(255, 255, 255);\n" -" border-radius: 5px;\n" -" border: solid 1px rgb(76, 169, 106);\n" -" background-color: rgb(76, 169, 106);\n" -"}\n" -"QPushButton#buttonView:hover {\n" -" color: rgb(0, 0, 0);\n" -"}") + " background-color: rgb(76, 169, 106);\n" + "}\n" + "QWidget#widgetBottom {\n" + " border-top-style: solid;\n" + " border-top-width: 2px;\n" + " border-top-color: rgb(185, 218, 201);\n" + "}\n" + "QLabel#labelTitle {\n" + " color: rgb(255, 255, 255);\n" + "}\n" + "QLabel#labelContent {\n" + " padding: 5px;\n" + "}\n" + "QPushButton {\n" + " border: none;\n" + " background: transparent;\n" + "}\n" + "QPushButton#buttonClose {\n" + " font-family: \"webdings\";\n" + " color: rgb(255, 255, 255);\n" + "}\n" + "QPushButton#buttonClose:hover {\n" + " background-color: rgb(212, 64, 39);\n" + "}\n" + "QPushButton#buttonView {\n" + " color: rgb(255, 255, 255);\n" + " border-radius: 5px;\n" + " border: solid 1px rgb(76, 169, 106);\n" + " background-color: rgb(76, 169, 106);\n" + "}\n" + "QPushButton#buttonView:hover {\n" + " color: rgb(0, 0, 0);\n" + "}") self.verticalLayout = QtWidgets.QVBoxLayout(NotifyForm) self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setSpacing(6) @@ -61,7 +65,8 @@ def setupUi(self, NotifyForm): self.labelTitle.setText("") self.labelTitle.setObjectName("labelTitle") self.horizontalLayout_3.addWidget(self.labelTitle) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_3.addItem(spacerItem) self.buttonClose = QtWidgets.QPushButton(self.widgetTitle) self.buttonClose.setMinimumSize(QtCore.QSize(26, 26)) @@ -80,7 +85,8 @@ def setupUi(self, NotifyForm): self.horizontalLayout.setContentsMargins(0, 5, 5, 5) self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") - spacerItem1 = QtWidgets.QSpacerItem(170, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + spacerItem1 = QtWidgets.QSpacerItem(170, 20, QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem1) self.buttonView = QtWidgets.QPushButton(self.widgetBottom) self.buttonView.setMinimumSize(QtCore.QSize(75, 25)) @@ -102,10 +108,10 @@ def retranslateUi(self, NotifyForm): if __name__ == "__main__": import sys + app = QtWidgets.QApplication(sys.argv) NotifyForm = QtWidgets.QWidget() ui = Ui_NotifyForm() ui.setupUi(NotifyForm) NotifyForm.show() sys.exit(app.exec_()) - 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/Lib/ui_frameless.py b/Demo/Lib/ui_frameless.py new file mode 100644 index 00000000..94bc7b7a --- /dev/null +++ b/Demo/Lib/ui_frameless.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'frameless.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# 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_FormFrameless(object): + def setupUi(self, FormFrameless): + FormFrameless.setObjectName("FormFrameless") + FormFrameless.resize(400, 300) + self.verticalLayout = QtWidgets.QVBoxLayout(FormFrameless) + self.verticalLayout.setContentsMargins(3, 3, 3, 3) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName("verticalLayout") + self.widgetTitleBar = QtWidgets.QWidget(FormFrameless) + font = QtGui.QFont() + font.setFamily("Symbola") + self.widgetTitleBar.setFont(font) + self.widgetTitleBar.setObjectName("widgetTitleBar") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widgetTitleBar) + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") + spacerItem = QtWidgets.QSpacerItem(253, 20, QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.buttonMinimum = QtWidgets.QPushButton(self.widgetTitleBar) + self.buttonMinimum.setMinimumSize(QtCore.QSize(36, 36)) + self.buttonMinimum.setMaximumSize(QtCore.QSize(36, 36)) + font = QtGui.QFont() + font.setFamily("webdings") + self.buttonMinimum.setFont(font) + self.buttonMinimum.setObjectName("buttonMinimum") + self.horizontalLayout.addWidget(self.buttonMinimum) + self.buttonMaximum = QtWidgets.QPushButton(self.widgetTitleBar) + self.buttonMaximum.setMinimumSize(QtCore.QSize(36, 36)) + self.buttonMaximum.setMaximumSize(QtCore.QSize(36, 36)) + font = QtGui.QFont() + font.setFamily("webdings") + self.buttonMaximum.setFont(font) + self.buttonMaximum.setObjectName("buttonMaximum") + self.horizontalLayout.addWidget(self.buttonMaximum) + self.buttonNormal = QtWidgets.QPushButton(self.widgetTitleBar) + self.buttonNormal.setMinimumSize(QtCore.QSize(36, 36)) + self.buttonNormal.setMaximumSize(QtCore.QSize(36, 36)) + font = QtGui.QFont() + font.setFamily("webdings") + self.buttonNormal.setFont(font) + self.buttonNormal.setObjectName("buttonNormal") + self.horizontalLayout.addWidget(self.buttonNormal) + self.buttonClose = QtWidgets.QPushButton(self.widgetTitleBar) + self.buttonClose.setMinimumSize(QtCore.QSize(36, 36)) + self.buttonClose.setMaximumSize(QtCore.QSize(36, 36)) + font = QtGui.QFont() + font.setFamily("webdings") + self.buttonClose.setFont(font) + self.buttonClose.setObjectName("buttonClose") + self.horizontalLayout.addWidget(self.buttonClose) + self.verticalLayout.addWidget(self.widgetTitleBar) + self.textEdit = QtWidgets.QTextEdit(FormFrameless) + self.textEdit.setFrameShape(QtWidgets.QFrame.NoFrame) + self.textEdit.setObjectName("textEdit") + self.verticalLayout.addWidget(self.textEdit) + self.verticalLayout.setStretch(1, 1) + + self.retranslateUi(FormFrameless) + QtCore.QMetaObject.connectSlotsByName(FormFrameless) + + def retranslateUi(self, FormFrameless): + _translate = QtCore.QCoreApplication.translate + FormFrameless.setWindowTitle(_translate("FormFrameless", "Form")) + self.buttonMinimum.setToolTip(_translate("FormFrameless", "Minimum")) + self.buttonMinimum.setText(_translate("FormFrameless", "0")) + self.buttonMaximum.setToolTip(_translate("FormFrameless", "Maximum")) + self.buttonMaximum.setText(_translate("FormFrameless", "1")) + self.buttonNormal.setToolTip(_translate("FormFrameless", "Normal")) + self.buttonNormal.setText(_translate("FormFrameless", "2")) + self.buttonClose.setToolTip(_translate("FormFrameless", "Close")) + self.buttonClose.setText(_translate("FormFrameless", "r")) + self.textEdit.setHtml(_translate("FormFrameless", + "\n" + "\n" + "

frameless window with move and resize

")) + + +if __name__ == "__main__": + import sys + + app = QtWidgets.QApplication(sys.argv) + FormFrameless = QtWidgets.QWidget() + ui = Ui_FormFrameless() + ui.setupUi(FormFrameless) + FormFrameless.show() + sys.exit(app.exec_()) diff --git a/Demo/NativeEvent.py b/Demo/NativeEvent.py index 2ccf1693..17312405 100644 --- a/Demo/NativeEvent.py +++ b/Demo/NativeEvent.py @@ -2,42 +2,42 @@ # -*- coding: utf-8 -*- """Created on 2018年8月2日 author: Irony -site: https://pyqt5.com , https://github.com/892768447 +site: https://pyqt.site , https://github.com/PyQt5 email: 892768447@qq.com file: win无边框调整大小 description: """ -from ctypes.wintypes import POINT import ctypes.wintypes +from ctypes.wintypes import POINT -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QWidget, QPushButton -from PyQt5.QtWinExtras import QtWin import win32api import win32con import win32gui - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QCursor + from PyQt5.QtWidgets import QApplication, QPushButton, QWidget + from PyQt5.QtWinExtras import QtWin +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QCursor + from PySide2.QtWidgets import QApplication, QPushButton, QWidget + from PySide2.QtWinExtras import QtWin class MINMAXINFO(ctypes.Structure): _fields_ = [ - ("ptReserved", POINT), - ("ptMaxSize", POINT), - ("ptMaxPosition", POINT), - ("ptMinTrackSize", POINT), - ("ptMaxTrackSize", POINT), + ("ptReserved", POINT), + ("ptMaxSize", POINT), + ("ptMaxPosition", POINT), + ("ptMinTrackSize", POINT), + ("ptMaxTrackSize", POINT), ] class Window(QWidget): - BorderWidth = 5 def __init__(self, *args, **kwargs): @@ -45,16 +45,15 @@ def __init__(self, *args, **kwargs): # 主屏幕的可用大小(去掉任务栏) self._rect = QApplication.instance().desktop().availableGeometry(self) self.resize(800, 600) - self.setWindowFlags(Qt.Window - | Qt.FramelessWindowHint - | Qt.WindowSystemMenuHint - | Qt.WindowMinimizeButtonHint - | Qt.WindowMaximizeButtonHint - | Qt.WindowCloseButtonHint) + self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | + Qt.WindowSystemMenuHint | + Qt.WindowMinimizeButtonHint | + Qt.WindowMaximizeButtonHint | + Qt.WindowCloseButtonHint) # 增加薄边框 style = win32gui.GetWindowLong(int(self.winId()), win32con.GWL_STYLE) - win32gui.SetWindowLong( - int(self.winId()), win32con.GWL_STYLE, style | win32con.WS_THICKFRAME) + win32gui.SetWindowLong(int(self.winId()), win32con.GWL_STYLE, + style | win32con.WS_THICKFRAME) if QtWin.isCompositionEnabled(): # 加上 Aero 边框阴影 @@ -67,8 +66,9 @@ def nativeEvent(self, eventType, message): if eventType == "windows_generic_MSG": msg = ctypes.wintypes.MSG.from_address(message.__int__()) # 获取鼠标移动经过时的坐标 - x = win32api.LOWORD(msg.lParam) - self.frameGeometry().x() - y = win32api.HIWORD(msg.lParam) - self.frameGeometry().y() + pos = QCursor.pos() + x = pos.x() - self.frameGeometry().x() + y = pos.y() - self.frameGeometry().y() # 判断鼠标位置是否有其它控件 if self.childAt(x, y) != None: return retval, result @@ -77,8 +77,8 @@ def nativeEvent(self, eventType, message): return True, 0 if msg.message == win32con.WM_GETMINMAXINFO: # 当窗口位置改变或者大小改变时会触发该消息 - info = ctypes.cast( - msg.lParam, ctypes.POINTER(MINMAXINFO)).contents + info = ctypes.cast(msg.lParam, + ctypes.POINTER(MINMAXINFO)).contents # 修改最大化的窗口大小为主屏幕的可用大小 info.ptMaxSize.x = self._rect.width() info.ptMaxSize.y = self._rect.height() @@ -121,7 +121,7 @@ def nativeEvent(self, eventType, message): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() btn = QPushButton('exit', w, clicked=app.quit) diff --git a/Demo/NewFramelessWindow.py b/Demo/NewFramelessWindow.py new file mode 100644 index 00000000..75dcc742 --- /dev/null +++ b/Demo/NewFramelessWindow.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2018年4月30日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: NewFramelessWindow +@description: +""" + +import sys + +try: + from PyQt5.QtCore import QEvent, QObject, QPoint, Qt, QTimer + from PyQt5.QtGui import QColor, QMouseEvent, QPainter, QWindow + from PyQt5.QtWidgets import QApplication, QMessageBox, QWidget +except ImportError: + from PySide2.QtCore import QTimer, Qt, QEvent, QObject, QPoint + from PySide2.QtGui import QWindow, QPainter, QColor, QMouseEvent + from PySide2.QtWidgets import QApplication, QWidget, QMessageBox + +from Lib.ui_frameless import Ui_FormFrameless + + +class FramelessObject(QObject): + Margins = 3 # 边缘边距 + TitleHeight = 36 # 标题栏高度 + Widgets = set() # 无边框窗口集合 + + @classmethod + def set_margins(cls, margins): + cls.Margins = margins + + @classmethod + def set_title_height(cls, height): + cls.TitleHeight = height + + @classmethod + def add_widget(cls, widget): + cls.Widgets.add(widget) + + @classmethod + def del_widget(cls, widget): + if widget in cls.Widgets: + cls.Widgets.remove(widget) + + def _get_edges(self, pos, width, height): + """根据坐标获取方向 + :param pos: QPoint + :param width: int + :param height: int + :return: Qt.Edges + """ + edge = 0 + x, y = pos.x(), pos.y() + + if y <= self.Margins: + edge |= Qt.TopEdge + if x <= self.Margins: + edge |= Qt.LeftEdge + if x >= width - self.Margins: + edge |= Qt.RightEdge + if y >= height - self.Margins: + edge |= Qt.BottomEdge + + return edge + + def _get_cursor(self, edges): + """调整鼠标样式 + :param edges: int or None + :return: Qt.CursorShape + """ + if edges == Qt.LeftEdge | Qt.TopEdge or edges == Qt.RightEdge | Qt.BottomEdge: + return Qt.SizeFDiagCursor + elif edges == Qt.RightEdge | Qt.TopEdge or edges == Qt.LeftEdge | Qt.BottomEdge: + return Qt.SizeBDiagCursor + elif edges == Qt.LeftEdge or edges == Qt.RightEdge: + return Qt.SizeHorCursor + elif edges == Qt.TopEdge or edges == Qt.BottomEdge: + return Qt.SizeVerCursor + + return Qt.ArrowCursor + + def is_titlebar(self, pos): + """判断是否是标题栏 + :param pos: QPoint + :return: bool + """ + return pos.y() <= self.TitleHeight + + def moveOrResize(self, window, pos, width, height): + edges = self._get_edges(pos, width, height) + if edges: + if window.windowState() == Qt.WindowNoState: + window.startSystemResize(edges) + else: + if self.is_titlebar(pos): + window.startSystemMove() + # Fixed #172 主动触发一次鼠标释放事件,否则会导致鼠标悬停出问题 + QApplication.instance().postEvent( + window, + QMouseEvent(QEvent.MouseButtonRelease, QPoint(-1, -1), + Qt.LeftButton, Qt.NoButton, Qt.NoModifier)) + + def eventFilter(self, obj, event): + if obj.isWindowType(): + # top window 处理光标样式 + if event.type() == QEvent.MouseMove and obj.windowState( + ) == Qt.WindowNoState: + obj.setCursor( + self._get_cursor( + self._get_edges(event.pos(), obj.width(), + obj.height()))) + elif event.type() == QEvent.TouchUpdate: + self.moveOrResize(obj, event.pos(), obj.width(), obj.height()) + elif obj in self.Widgets and isinstance( + event, QMouseEvent) and event.button() == Qt.LeftButton: + if event.type() == QEvent.MouseButtonDblClick: + # 双击最大化还原 + if self.is_titlebar(event.pos()): + if obj.windowState() == Qt.WindowFullScreen: + pass + elif obj.windowState() == Qt.WindowMaximized: + obj.showNormal() + else: + obj.showMaximized() + elif event.type() == QEvent.MouseButtonPress: + self.moveOrResize(obj.windowHandle(), event.pos(), obj.width(), + obj.height()) + + return False + + +class FramelessWindow(QWidget, Ui_FormFrameless): + + def __init__(self, *args, **kwargs): + super(FramelessWindow, self).__init__(*args, **kwargs) + self.setupUi(self) + # 无边框 + self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + self.setMouseTracking(True) + # 隐藏还原按钮 + self.buttonNormal.setVisible(False) + # 标题栏按钮信号 + self.buttonMinimum.clicked.connect(self.showMinimized) + self.buttonMaximum.clicked.connect(self.showMaximized) + self.buttonNormal.clicked.connect(self.showNormal) + self.buttonClose.clicked.connect(self.close) + self.setStyleSheet('#widgetTitleBar{background: rgb(232, 232, 232);}') + + def showMinimized(self): + flags = self.windowFlags() + if sys.platform == 'darwin': + # fix mac 最小化失效问题 + self.setWindowFlags((self.windowFlags() | Qt.CustomizeWindowHint) & + (~Qt.WindowTitleHint)) + super(FramelessWindow, self).showMinimized() + if sys.platform == 'darwin': + # fix mac 最小化失效问题 + self.setWindowFlags(flags) + self.show() + + def changeEvent(self, event): + """窗口状态改变 + :param event: + """ + super(FramelessWindow, self).changeEvent(event) + # 窗口状态改变时修改标题栏控制按钮 + visible = self.isMaximized() + self.buttonMaximum.setVisible(not visible) + self.buttonNormal.setVisible(visible) + if visible: + self.layout().setContentsMargins(0, 0, 0, 0) + else: + # TODO 与UI文件中的布局边距一致 + m = FramelessObject.Margins + self.layout().setContentsMargins(m, m, m, m) + + def paintEvent(self, event): + # 透明背景但是需要留下一个透明度用于鼠标捕获 + painter = QPainter(self) + painter.fillRect(self.rect(), QColor(255, 255, 255, 1)) + + +if __name__ == '__main__': + import cgitb + import sys + + cgitb.enable(format='text') + + app = QApplication(sys.argv) + if not hasattr(QWindow, 'startSystemMove'): + QWindow.startSystemResize() + # 不支持 + QMessageBox.critical(None, '错误', '当前Qt版本不支持该例子') + QTimer.singleShot(100, app.quit) + else: + # 安装全局事件过滤器 + fo = FramelessObject() + app.installEventFilter(fo) + + w1 = FramelessWindow() + fo.add_widget(w1) + w1.show() + + w2 = FramelessWindow() + fo.add_widget(w2) + w2.show() + sys.exit(app.exec_()) diff --git a/Demo/Notification.py b/Demo/Notification.py index f7a0ab15..889c103f 100644 --- a/Demo/Notification.py +++ b/Demo/Notification.py @@ -4,30 +4,30 @@ """ Created on 2018年9月9日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: Notification @description: """ import base64 -from PyQt5.QtCore import Qt, QRectF, QSize, pyqtSignal, QTimer -from PyQt5.QtGui import QPixmap, QImage, QPainter, QPainterPath,\ - QColor -from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout,\ - QGridLayout, QSpacerItem, QSizePolicy, QGraphicsDropShadowEffect,\ - QListWidget, QListWidgetItem - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import Qt, QRectF, QSize, pyqtSignal, QTimer + from PyQt5.QtGui import QPixmap, QImage, QPainter, QPainterPath, \ + QColor + from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, \ + QGridLayout, QSpacerItem, QSizePolicy, QGraphicsDropShadowEffect, \ + QListWidget, QListWidgetItem, QApplication, QPushButton +except ImportError: + from PySide2.QtCore import Qt, QRectF, QSize, Signal as pyqtSignal, QTimer + from PySide2.QtGui import QPixmap, QImage, QPainter, QPainterPath, \ + QColor + from PySide2.QtWidgets import QWidget, QLabel, QHBoxLayout, \ + QGridLayout, QSpacerItem, QSizePolicy, QGraphicsDropShadowEffect, \ + QListWidget, QListWidgetItem, QApplication, QPushButton class NotificationIcon: - Info, Success, Warning, Error, Close = range(5) Types = { Info: None, @@ -39,10 +39,14 @@ class NotificationIcon: @classmethod def init(cls): - cls.Types[cls.Info] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC5ElEQVRYR8VX0VHbQBB9e/bkN3QQU0FMBSEVYFcQ8xPBJLJ1FWAqOMcaxogfTAWQCiAVRKkgTgfmM4zRZu6QhGzL0p0nDPr17e7bt7tv14RX/uiV48MJgAon+8TiAMRtMFogaqUJxADPwRRzg67kl8+xbWJWANR40iPQSSFgtX/mGQkaDr56V3VAKgGos4s2JXwJoF3naMPvMS+SrpTHs032GwGkdF+DsFMVnJm/oyGGeHico0EjIjpYes+YMyVd6R/flfkpBWCCQ9zaZM2LZDfLMGXsZ5kdI/lYBmINgHHyyLd1mWdBbAFAM/GY7K2WYx1AeB4T6L1N9umbGxZ0qktATaEAdCps48D39oq/LwEw3U5CN92LfczJoewfT7MAywDCaEbAuxeLrh0zz4L+0e4aAJfGy+sP3IMxlH1vpMJoSMCJDXgWtJeJVc6ACs9HBBrYODCJAFdYvAmkPJxnNqMwYht7Bn+T/lGg3z4DGEd3RPhQ54DBvwAOVkeqagRXfTLjh+x7+8sALOtfHLuiYzWOAiLoKbD58mnIGbCmLxUepS6NQmYlUGE0JeCTTXT9JvA9E9sZgO5iIpoyc6/YzcqSwQzgGgBXB7oXpH9klpRSkxY1xW/b7Iu2zk34PILPnazCqEPAtTWA8iZ0HsOu9L0bw4DzCJeNocMGNDpQ3IKO+6NUiJ4ysZNiBv5I3zPnmJmG5oM+wbS+9+qkvGi7NAXGmeUy0ioofa+XA0jH0UaMKpdRWs/adcwMqfV/tenqpqHY/Znt+j2gJi00RUzA201dXaxh9iZdZloJS+9H1otrkbRrD5InFqpPskxEshJQ468CkSmJC+i1HigaaxCAuCljgoDhwPdOjf7rFVxxuJrMkXScjtKc1rOLNpJk6nii5XmYzbngzlZn+RIb40kPJPTBYXUt6VEDJ8Pi6bWpNFb/jFYY6YGpDeKdjBmTKdMcxDGEmP73v2a2Gr/NOycGtglQZ/MPzEqCMLGckJEAAAAASUVORK5CYII='))) - cls.Types[cls.Success] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACZUlEQVRYR8VXS3LTQBDtVsDbcAPMCbB3limkcAKSG4QFdnaYE2BOQLKzxSLJCeAGSUQheSnfwLmB2VJhXmpExpFHI2sk2RWv5FJPv9evP9NieuIfPzE+VSJw8qt3IMDvmahDoDYxt2UAACXMWIIowR5ffn8TJbaBWRE4CXvHAH9RgKXOgQUI48CfXZbZbiTw8Xe/w3d0zkydMkem91IZpyWOJu5sUXS+kEAqt3B+MNOLOuDqDEBLxxFHk7eza5MfIwEJDjhXTYD1s8zinYlEjsCD7FdNI9cJpEq0RFdPR47AMOzLCn69zegz6UgCP+pmfa8RSKudnPNdgCufTOLDxJtdPP7PoA1Cd8HEL5sSUCCD0B0x8bc1f8Bi6sevcgS2VXh6hMOwDz0gsUddNaxWKRjeuKfE/KlJ9Dq4UYH/o/Ns6scj+bgiMAjdayb26xLQwTfVEwg3gRcf6ARq578KuLo7VDc8psCQqwfjr4EfjYvkrAquFJ56UYpdSkAZSmNd1rrg0leOQFELgvA58OJTxVyRaAJORPOpF6UXnFUR5sDiXjs7UqsOMGMRlrWhTkJXpFL3mNrQZhA1lH3F0TiI5FurUQyMpn58VjhkSqQA4Tbw4nSVW6sBU5VXktXSeONlJH3s8jrOVr9RgVSFuNcWfzlh5n3LoKzMAPxxWuiULiQpiR2sZNnCyzIuWUr5Z1Ml0sgdHFZaShVDuR86/0huL3VXtDk/F4e11vKsTHLSCeKx7bYkW80hjLOrV1GhWH0ZrSlyh2MwdZhYfi8oZeYgLBmUiGd8sfVPM6syr2lUSYGaGBuP3QN6rVUwYV/egwAAAABJRU5ErkJggg=='))) - cls.Types[cls.Warning] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACmElEQVRYR8VXTW7TUBD+xjYSXZFukOIsSE9AskNJJMoJmq4r7OYEwAkabhBOkB/Emt4gVIojdpgbpIumEitX6gKB7UHPkauXxLHfc4F6Z3l+vvnmm/fGhAd+6IHzQwvA9cfOITMfAdQAcx1EdVEAM/tEFADsWyaPn57MfdXClABcT1qnzHSWJiwMzrwgoF91vXGRbS6AH59ajd8hDYmoURQo67tgxoij42rv62KX/04Agu44xmciVMokT32YERgGjquvZ1+y4mQCWPUa0/sk3vQlwqssEFsAVrQbU4XKL/ai2+5PPK6waQ4AOsoDnDARh83NdmwBuJq0fQI9L6p+L7rd3+/5gbAToMPI+FbkIzRRc72mbLcGIFE7jGFRIPHddmZrvstJh1X8CHGv6sxHqe1GkPYCoGcqgcoCAPPCdr2DLQC6wqMoPEj7qdqCNKllxs30sLpjYDluDUDGG5XqhY2sal3w4PiD7c7fJnHShMtJR8zpy/8CALiwndnhBgD1/t+XAXkaZAaUVHwnHulg0W6BNEWlAQD8zna8gQB0Ne70iXCm2j55jCUAei1gxvuaO+uXAcDg7zXHSy640iKUAehOEDJFqDmGQkiPLO5Fv+KADXOqvCuIsrPGsIyQdHou22YeRMJgOdHTQTkAfGk7XrLKrWlAvOhcRgBfWiZ3RQti0zxXuUFXCXMuo0TRitfxugjbIxC5RYzI6s9kIGFh+KLOpiW22id5AUuI8IaisFG4kCQg/sFKJgtPLix3KWXGeRETRbQDuCFCV2spTYMm+2FEI1WBbYIRPTeiqFtqLZeDraaD+qrbkpgQAvfl1WsXU0p/RjIjYYhTkNFgcCVlRlRKoAAc+5aF0V//NVPoc2kTLQZKZ8lx/AMXBmMwuXUwOAAAAABJRU5ErkJggg=='))) - cls.Types[cls.Error] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACrklEQVRYR82XW27aQBSG/4PtiNhIpStouoImKwjZAV1B07coWCpZQcgK6kh2lLeSFZSsIOwgdAdkBaUSEBQDpxpjU9vM+EJR03nDzJz/mzm3GcIrD3plfZQCeD47O1ho2jERNRmoE9AQG2BgBGBAwIiZe5Zh3JPjiG+5oxCAEF5q2iWITnMtRhOYu5XF4mr/9naYtSYXYGLbHQCXhYVTEwlom657rVqvBOB2uz71/a+ldq1SYe6ahnEhc4sSYGzbfQKOt915eh0D/ZrrnqS/SwEmrVYXRJ92Jb4OC+C65rrtuN0NgIltNwF837V4zN5Hy3V70e9NgFZrCKJ3CQDmJ9MwDsW36XzeB/AhA/CHqeuN2WxWX2paX2JraHneeynA+Pz8lCqVbxLjV5brimxAEJxqiEA8CjZVBvFy+bl2c9MV9hInoAw85qFpGEeRYQVEQjzMokcQHWxsiPne8jzh6j8AodGfyqNlHpiGcaKAkIk/gChwm2yYuv5W2FqfwLNtN5bAQ2bwySB83zENo50A8/1McaFRAU72XVek+mpk+D/JlIKI/xkee654uCbIhjVAqZIrgSgpLhiCwN4OAEj4vEB2yDybBCjsAol4ZD0nRdMQSRcUCsKUeNSw4o2mKMRGEOamoVx8FXDZKVosDYNMUHXAsBRnppo8RQcbpTgIGEkhykpFjnWxzGhPQYxt2yHgS/oIlKVYTJxImpG482nz+VG1Wh1N84pMCCGa0ULXHwmoJwCYnyzPW5fn/68dh7EgPbrMMl3gz7gro+n/7EoWD7w4a96l1NnJ1Yz5Lt6wCgFEk0r1CIkbiPnC9DxH5aHcd4FYGD5MOqVOg/muslh0/vphkm63k5eXZvA0I6qD+ZCI3jDzLxANiHn1NNvb6+30aVYgwLeeUsgFW1svsPA3Ncq4MHzVeO8AAAAASUVORK5CYII='))) + cls.Types[cls.Info] = QPixmap(QImage.fromData(base64.b64decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC5ElEQVRYR8VX0VHbQBB9e/bkN3QQU0FMBSEVYFcQ8xPBJLJ1FWAqOMcaxogfTAWQCiAVRKkgTgfmM4zRZu6QhGzL0p0nDPr17e7bt7tv14RX/uiV48MJgAon+8TiAMRtMFogaqUJxADPwRRzg67kl8+xbWJWANR40iPQSSFgtX/mGQkaDr56V3VAKgGos4s2JXwJoF3naMPvMS+SrpTHs032GwGkdF+DsFMVnJm/oyGGeHico0EjIjpYes+YMyVd6R/flfkpBWCCQ9zaZM2LZDfLMGXsZ5kdI/lYBmINgHHyyLd1mWdBbAFAM/GY7K2WYx1AeB4T6L1N9umbGxZ0qktATaEAdCps48D39oq/LwEw3U5CN92LfczJoewfT7MAywDCaEbAuxeLrh0zz4L+0e4aAJfGy+sP3IMxlH1vpMJoSMCJDXgWtJeJVc6ACs9HBBrYODCJAFdYvAmkPJxnNqMwYht7Bn+T/lGg3z4DGEd3RPhQ54DBvwAOVkeqagRXfTLjh+x7+8sALOtfHLuiYzWOAiLoKbD58mnIGbCmLxUepS6NQmYlUGE0JeCTTXT9JvA9E9sZgO5iIpoyc6/YzcqSwQzgGgBXB7oXpH9klpRSkxY1xW/b7Iu2zk34PILPnazCqEPAtTWA8iZ0HsOu9L0bw4DzCJeNocMGNDpQ3IKO+6NUiJ4ysZNiBv5I3zPnmJmG5oM+wbS+9+qkvGi7NAXGmeUy0ioofa+XA0jH0UaMKpdRWs/adcwMqfV/tenqpqHY/Znt+j2gJi00RUzA201dXaxh9iZdZloJS+9H1otrkbRrD5InFqpPskxEshJQ468CkSmJC+i1HigaaxCAuCljgoDhwPdOjf7rFVxxuJrMkXScjtKc1rOLNpJk6nii5XmYzbngzlZn+RIb40kPJPTBYXUt6VEDJ8Pi6bWpNFb/jFYY6YGpDeKdjBmTKdMcxDGEmP73v2a2Gr/NOycGtglQZ/MPzEqCMLGckJEAAAAASUVORK5CYII='))) + cls.Types[cls.Success] = QPixmap(QImage.fromData(base64.b64decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACZUlEQVRYR8VXS3LTQBDtVsDbcAPMCbB3limkcAKSG4QFdnaYE2BOQLKzxSLJCeAGSUQheSnfwLmB2VJhXmpExpFHI2sk2RWv5FJPv9evP9NieuIfPzE+VSJw8qt3IMDvmahDoDYxt2UAACXMWIIowR5ffn8TJbaBWRE4CXvHAH9RgKXOgQUI48CfXZbZbiTw8Xe/w3d0zkydMkem91IZpyWOJu5sUXS+kEAqt3B+MNOLOuDqDEBLxxFHk7eza5MfIwEJDjhXTYD1s8zinYlEjsCD7FdNI9cJpEq0RFdPR47AMOzLCn69zegz6UgCP+pmfa8RSKudnPNdgCufTOLDxJtdPP7PoA1Cd8HEL5sSUCCD0B0x8bc1f8Bi6sevcgS2VXh6hMOwDz0gsUddNaxWKRjeuKfE/KlJ9Dq4UYH/o/Ns6scj+bgiMAjdayb26xLQwTfVEwg3gRcf6ARq578KuLo7VDc8psCQqwfjr4EfjYvkrAquFJ56UYpdSkAZSmNd1rrg0leOQFELgvA58OJTxVyRaAJORPOpF6UXnFUR5sDiXjs7UqsOMGMRlrWhTkJXpFL3mNrQZhA1lH3F0TiI5FurUQyMpn58VjhkSqQA4Tbw4nSVW6sBU5VXktXSeONlJH3s8jrOVr9RgVSFuNcWfzlh5n3LoKzMAPxxWuiULiQpiR2sZNnCyzIuWUr5Z1Ml0sgdHFZaShVDuR86/0huL3VXtDk/F4e11vKsTHLSCeKx7bYkW80hjLOrV1GhWH0ZrSlyh2MwdZhYfi8oZeYgLBmUiGd8sfVPM6syr2lUSYGaGBuP3QN6rVUwYV/egwAAAABJRU5ErkJggg=='))) + cls.Types[cls.Warning] = QPixmap(QImage.fromData(base64.b64decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACmElEQVRYR8VXTW7TUBD+xjYSXZFukOIsSE9AskNJJMoJmq4r7OYEwAkabhBOkB/Emt4gVIojdpgbpIumEitX6gKB7UHPkauXxLHfc4F6Z3l+vvnmm/fGhAd+6IHzQwvA9cfOITMfAdQAcx1EdVEAM/tEFADsWyaPn57MfdXClABcT1qnzHSWJiwMzrwgoF91vXGRbS6AH59ajd8hDYmoURQo67tgxoij42rv62KX/04Agu44xmciVMokT32YERgGjquvZ1+y4mQCWPUa0/sk3vQlwqssEFsAVrQbU4XKL/ai2+5PPK6waQ4AOsoDnDARh83NdmwBuJq0fQI9L6p+L7rd3+/5gbAToMPI+FbkIzRRc72mbLcGIFE7jGFRIPHddmZrvstJh1X8CHGv6sxHqe1GkPYCoGcqgcoCAPPCdr2DLQC6wqMoPEj7qdqCNKllxs30sLpjYDluDUDGG5XqhY2sal3w4PiD7c7fJnHShMtJR8zpy/8CALiwndnhBgD1/t+XAXkaZAaUVHwnHulg0W6BNEWlAQD8zna8gQB0Ne70iXCm2j55jCUAei1gxvuaO+uXAcDg7zXHSy640iKUAehOEDJFqDmGQkiPLO5Fv+KADXOqvCuIsrPGsIyQdHou22YeRMJgOdHTQTkAfGk7XrLKrWlAvOhcRgBfWiZ3RQti0zxXuUFXCXMuo0TRitfxugjbIxC5RYzI6s9kIGFh+KLOpiW22id5AUuI8IaisFG4kCQg/sFKJgtPLix3KWXGeRETRbQDuCFCV2spTYMm+2FEI1WBbYIRPTeiqFtqLZeDraaD+qrbkpgQAvfl1WsXU0p/RjIjYYhTkNFgcCVlRlRKoAAc+5aF0V//NVPoc2kTLQZKZ8lx/AMXBmMwuXUwOAAAAABJRU5ErkJggg=='))) + cls.Types[cls.Error] = QPixmap(QImage.fromData(base64.b64decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACrklEQVRYR82XW27aQBSG/4PtiNhIpStouoImKwjZAV1B07coWCpZQcgK6kh2lLeSFZSsIOwgdAdkBaUSEBQDpxpjU9vM+EJR03nDzJz/mzm3GcIrD3plfZQCeD47O1ho2jERNRmoE9AQG2BgBGBAwIiZe5Zh3JPjiG+5oxCAEF5q2iWITnMtRhOYu5XF4mr/9naYtSYXYGLbHQCXhYVTEwlom657rVqvBOB2uz71/a+ldq1SYe6ahnEhc4sSYGzbfQKOt915eh0D/ZrrnqS/SwEmrVYXRJ92Jb4OC+C65rrtuN0NgIltNwF837V4zN5Hy3V70e9NgFZrCKJ3CQDmJ9MwDsW36XzeB/AhA/CHqeuN2WxWX2paX2JraHneeynA+Pz8lCqVbxLjV5brimxAEJxqiEA8CjZVBvFy+bl2c9MV9hInoAw85qFpGEeRYQVEQjzMokcQHWxsiPne8jzh6j8AodGfyqNlHpiGcaKAkIk/gChwm2yYuv5W2FqfwLNtN5bAQ2bwySB83zENo50A8/1McaFRAU72XVek+mpk+D/JlIKI/xkee654uCbIhjVAqZIrgSgpLhiCwN4OAEj4vEB2yDybBCjsAol4ZD0nRdMQSRcUCsKUeNSw4o2mKMRGEOamoVx8FXDZKVosDYNMUHXAsBRnppo8RQcbpTgIGEkhykpFjnWxzGhPQYxt2yHgS/oIlKVYTJxImpG482nz+VG1Wh1N84pMCCGa0ULXHwmoJwCYnyzPW5fn/68dh7EgPbrMMl3gz7gro+n/7EoWD7w4a96l1NnJ1Yz5Lt6wCgFEk0r1CIkbiPnC9DxH5aHcd4FYGD5MOqVOg/muslh0/vphkm63k5eXZvA0I6qD+ZCI3jDzLxANiHn1NNvb6+30aVYgwLeeUsgFW1svsPA3Ncq4MHzVeO8AAAAASUVORK5CYII='))) cls.Types[cls.Close] = QPixmap(QImage.fromData(base64.b64decode( 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAeElEQVQ4T2NkoBAwUqifgboGzJy76AIjE3NCWmL0BWwumzV/qcH/f38XpCfHGcDkUVwAUsDw9+8GBmbmAHRDcMlheAGbQnwGYw0DZA1gp+JwFUgKZyDCDQGpwuIlrGGAHHAUGUCRFygKRIqjkeKERE6+oG5eIMcFAOqSchGwiKKAAAAAAElFTkSuQmCC'))) @@ -52,7 +56,6 @@ def icon(cls, ntype): class NotificationItem(QWidget): - closed = pyqtSignal(QListWidgetItem) def __init__(self, title, message, item, *args, ntype=0, callback=None, **kwargs): @@ -152,7 +155,6 @@ def paintEvent(self, event): class NotificationWindow(QListWidget): - _instance = None def __init__(self, *args, **kwargs): @@ -243,15 +245,18 @@ def error(cls, title, message, callback=None): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.Hook(1, None, 5, sys.stderr, 'text') - from PyQt5.QtWidgets import QApplication, QPushButton + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = QWidget() layout = QHBoxLayout(w) + def callback(): print('回调点击') + layout.addWidget(QPushButton( 'Info', w, clicked=lambda: NotificationWindow.info('提示', '这是一条会自动关闭的消息', callback=callback))) layout.addWidget(QPushButton( @@ -268,9 +273,9 @@ def callback(): callback=callback))) w.show() -# NotificationIcon.init() -# ww = NotificationItem('提示', '

这是提示文案这是提示文案这是提示文案这是提示文案这是提示文案这是提示文案这是提示文案这是提示文案

', None, -# ntype=NotificationIcon.Error) -# ww.bgWidget.setVisible(True) -# ww.show() + # NotificationIcon.init() + # ww = NotificationItem('提示', '

这是提示文案这是提示文案这是提示文案这是提示文案这是提示文案这是提示文案这是提示文案这是提示文案

', None, + # ntype=NotificationIcon.Error) + # ww.bgWidget.setVisible(True) + # ww.show() sys.exit(app.exec_()) diff --git a/Demo/ProbeWindow.py b/Demo/ProbeWindow.py index 7bc4b0b0..e023359c 100644 --- a/Demo/ProbeWindow.py +++ b/Demo/ProbeWindow.py @@ -3,23 +3,22 @@ """ Created on 2018年6月8日 author: Irony -site: https://pyqt5.com , https://github.com/892768447 +site: https://pyqt.site , https://github.com/PyQt5 email: 892768447@qq.com file: ProbeWindow description: 简单探测窗口和放大截图 """ -from PyQt5.QtCore import Qt, QRect -from PyQt5.QtGui import QPainter, QPen, QCursor, QColor -from PyQt5.QtWidgets import QLabel, QWidget, QApplication import win32gui - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import Qt, QRect + from PyQt5.QtGui import QPainter, QPen, QCursor, QColor + from PyQt5.QtWidgets import QLabel, QWidget, QApplication +except ImportError: + from PySide2.QtCore import Qt, QRect + from PySide2.QtGui import QPainter, QPen, QCursor, QColor + from PySide2.QtWidgets import QLabel, QWidget, QApplication class FrameWidget(QWidget): @@ -111,11 +110,12 @@ def paintEvent(self, event): painter.setPen(Qt.white) painter.drawText(self.rect(), Qt.AlignLeft | Qt.AlignBottom, '({}, {})\nRGB: ({}, {}, {})\n{}'.format( - pos.x(), pos.y(), r, g, b, QColor(r, g, b).name())) + pos.x(), pos.y(), r, g, b, QColor(r, g, b).name())) if __name__ == '__main__': import sys + app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(True) w = Label() diff --git a/Demo/QtThreading.py b/Demo/QtThreading.py index 63f36970..d0533850 100644 --- a/Demo/QtThreading.py +++ b/Demo/QtThreading.py @@ -4,7 +4,7 @@ """ Created on 2019年3月8日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: Threading.QtThreading @description: @@ -12,19 +12,15 @@ from threading import Thread from time import sleep -from PyQt5.QtCore import QObject, pyqtSignal, QTimer, Qt -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QProgressBar - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QObject, pyqtSignal, QTimer, Qt + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QProgressBar, QApplication +except ImportError: + from PySide2.QtCore import QObject, Signal as pyqtSignal, QTimer, Qt + from PySide2.QtWidgets import QWidget, QVBoxLayout, QProgressBar, QApplication class _Signals(QObject): - updateProgress = pyqtSignal(int) @@ -63,7 +59,7 @@ def doStart(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/Demo/README.md b/Demo/README.md index 9fc9353e..aa260d63 100644 --- a/Demo/README.md +++ b/Demo/README.md @@ -23,8 +23,12 @@ - [判断信号是否连接](#20判断信号是否连接) - [调用虚拟键盘](#21调用虚拟键盘) - [动态忙碌光标](#22动态忙碌光标) + - [屏幕变动监听](#23屏幕变动监听) + - [无边框窗口](#24无边框窗口) + - [属性绑定](#25属性绑定) ## 1、重启窗口Widget + [运行 RestartWindow.py](RestartWindow.py) 利用类变量对窗口的变量进行引用,防止被回收(导致窗口一闪而过),重启时先显示新窗口后关闭自己 @@ -32,6 +36,7 @@ ![RestartWindow](ScreenShot/RestartWindow.gif) ## 2、简单的窗口贴边隐藏 + [运行 WeltHideWindow.py](WeltHideWindow.py) 1. 大概思路 @@ -50,6 +55,7 @@ ![WeltHideWindow](ScreenShot/WeltHideWindow.gif) ## 3、嵌入外部窗口 + [运行 EmbedWindow.py](EmbedWindow.py) 1. 使用`SetParent`函数设置外部窗口的`parent`为Qt的窗口 @@ -59,8 +65,8 @@ ![EmbedWindow](ScreenShot/EmbedWindow.gif) - ## 4、简单跟随其它窗口 + [运行 FollowWindow.py](FollowWindow.py) 1. 利用win32gui模块获取目标窗口的句柄 @@ -70,8 +76,8 @@ ![FollowWindow](ScreenShot/FollowWindow.gif) - ## 5、简单探测窗口和放大截图 + [运行 ProbeWindow.py](ProbeWindow.py) 1. 利用`win32gui`模块获取鼠标所在位置的窗口大小(未去掉边框)和rgb颜色 @@ -79,8 +85,8 @@ ![ProbeWindow](ScreenShot/ProbeWindow.gif) - ## 6、无边框自定义标题栏窗口 + [运行 FramelessWindow.py](FramelessWindow.py) | [运行 NativeEvent.py](NativeEvent.py) 1. 重写鼠标事件 @@ -98,32 +104,38 @@ ![FramelessWindow](ScreenShot/FramelessWindow.gif) ## 7、右下角弹出框 -[运行 WindowNotify.py](WindowNotify.py) + +[运行 WindowNotify.py](WindowNotify.py) | [查看 notify.ui](Data/notify.ui) ![WindowNotify](ScreenShot/WindowNotify.gif) ## 8、程序重启 + [运行 AutoRestart.py](AutoRestart.py) ![AutoRestart](ScreenShot/AutoRestart.gif) ## 9、自定义属性 + [运行 CustomProperties.py](CustomProperties.py) ![CustomProperties](ScreenShot/CustomProperties.png) ## 10、调用截图DLL + [运行 ScreenShotDll.py](ScreenShotDll.py) ![ScreenShotDll](ScreenShot/ScreenShotDll.gif) ## 11、单实例应用 + [运行 SingleApplication.py](SingleApplication.py) | [运行 SharedMemory.py](SharedMemory.py) 1. QSharedMemory 2. QLocalSocket, QLocalServer ## 12、简单的右下角气泡提示 + [运行 BubbleTips.py](BubbleTips.py) 1. 使用 `QWidget` 包含一个 `QLabel`, 其中 `QWidget` 通过 `paintEvent` 绘制气泡形状 @@ -133,11 +145,13 @@ ![BubbleTips](ScreenShot/BubbleTips.gif) ## 13、右侧消息通知栏 + [运行 Notification.py](Notification.py) ![Notification](ScreenShot/Notification.gif) ## 14、验证码控件 + [运行 VerificationCode.py](VerificationCode.py) 1. 更新为paintEvent方式,采用上下跳动 @@ -147,24 +161,26 @@ ![VerificationCode](ScreenShot/VerificationCode.gif) ## 15、人脸特征点 + [运行 FacePoints.py](FacePoints.py) PyQt 结合 Opencv 进行人脸检测; 由于直接在主线程中进行特征点获取,效率比较低 依赖文件 - + 1. [opencv](https://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv) 2. [numpy](https://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy) 3. [dlib](http://dlib.net/) - 1. [dlib-19.4.0.win32-py2.7.exe](Data/dlib-19.4.0.win32-py2.7.exe) - 2. [dlib-19.4.0.win32-py3.4.exe](Data/dlib-19.4.0.win32-py3.4.exe) - 3. [dlib-19.4.0.win32-py3.5.exe](Data/dlib-19.4.0.win32-py3.5.exe) +1. [dlib-19.4.0.win32-py2.7.exe](Data/dlib-19.4.0.win32-py2.7.exe) +2. [dlib-19.4.0.win32-py3.4.exe](Data/dlib-19.4.0.win32-py3.4.exe) +3. [dlib-19.4.0.win32-py3.5.exe](Data/dlib-19.4.0.win32-py3.5.exe) 4. [shape-predictor-68-face-landmarks.dat.bz2](http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2) ![FacePoints](ScreenShot/FacePoints.png) ## 16、使用Threading + [运行 QtThreading.py](QtThreading.py) 在PyQt中使用Theading线程 @@ -172,14 +188,15 @@ PyQt 结合 Opencv 进行人脸检测; ![QtThreading](ScreenShot/QtThreading.gif) ## 17、背景连线动画 + [运行 CircleLine.py](CircleLine.py) 主要参考 [背景连线动画.html](Data/背景连线动画.html) ![CircleLine](ScreenShot/CircleLine.gif) - ## 18、无边框圆角对话框 + [运行 FramelessDialog.py](FramelessDialog.py) 1. 通过设置 `self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)` 和 `self.setAttribute(Qt.WA_TranslucentBackground, True)` 达到无边框和背景透明 @@ -190,6 +207,7 @@ PyQt 结合 Opencv 进行人脸检测; ![FramelessDialog](ScreenShot/FramelessDialog.png) ## 19、调整窗口显示边框 + [运行 ShowFrameWhenDrag.py](ShowFrameWhenDrag.py) 1. 全局设置是【】在控制面板中->调整Windows的外观和性能->去掉勾选 拖动时显示窗口内容】 @@ -202,13 +220,16 @@ PyQt 结合 Opencv 进行人脸检测; ![ShowFrameWhenDrag](ScreenShot/ShowFrameWhenDrag.gif) ## 20、判断信号是否连接 + [运行 IsSignalConnected.py](IsSignalConnected.py) -通过 `isSignalConnected` 判断是否连接 +1. 通过 `isSignalConnected` 判断是否连接 +2. 通过对象的 `receivers` 获取连接的数量来判断 ![IsSignalConnected](ScreenShot/IsSignalConnected.png) ## 21、调用虚拟键盘 + [运行 CallVirtualKeyboard.py](CallVirtualKeyboard.py) 1. Windows上调用的是`osk.exe` @@ -218,8 +239,35 @@ PyQt 结合 Opencv 进行人脸检测; ![CallVirtualKeyboard2](ScreenShot/CallVirtualKeyboard2.png) ## 22、动态忙碌光标 + [运行 GifCursor.py](GifCursor.py) 通过定时器不停的修改光标图片来实现动态效果 -![GifCursor](ScreenShot/GifCursor.gif) \ No newline at end of file +![GifCursor](ScreenShot/GifCursor.gif) + +## 23、屏幕变动监听 + +[运行 ScreenNotify.py](ScreenNotify.py) + +通过定时器减少不同的变化信号,尽量保证只调用一次槽函数来获取信息 + +![ScreenNotify](ScreenShot/ScreenNotify.png) + +## 24、无边框窗口 + +[运行 NewFramelessWindow.py](NewFramelessWindow.py) + +1. 该方法只针对 `Qt5.15` 以上版本有效 +2. 通过事件过滤器判断边缘设置鼠标样式 +3. 处理点击事件交通过 `QWindow.startSystemMove` 和 `QWindow.startSystemResize` 传递给系统处理 + +![NewFramelessWindow](ScreenShot/NewFramelessWindow.gif) + +## 25、属性绑定 + +[运行 TestSerializeModel.py](TestSerializeModel.py) + +类似:[json数据绑定](../QTreeView#3json数据绑定) + +![TestSerializeModel](ScreenShot/TestSerializeModel.gif) diff --git a/Demo/RestartWindow.py b/Demo/RestartWindow.py index 9eec2242..bd9709b3 100644 --- a/Demo/RestartWindow.py +++ b/Demo/RestartWindow.py @@ -1,26 +1,26 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月17日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: RestartWindow @description: 窗口重启 -''' -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit,\ - QMessageBox +""" - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import pyqtSignal + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, \ + QMessageBox, QApplication +except ImportError: + from PySide2.QtCore import Signal as pyqtSignal + from PySide2.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, \ + QMessageBox, QApplication class RestartWindow(QWidget): - restarted = pyqtSignal(QWidget, str) _Self = None # 很重要,保留窗口引用 @@ -55,7 +55,7 @@ def onRestart(cls, widget, path): if __name__ == "__main__": import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = RestartWindow("test") w.show() diff --git a/Demo/ScreenNotify.py b/Demo/ScreenNotify.py new file mode 100644 index 00000000..f2c9f9c5 --- /dev/null +++ b/Demo/ScreenNotify.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/4/13 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ScreenNotify +@description: 屏幕、分辨率、DPI变化通知 +""" +import sys + +try: + from PyQt5.QtCore import QTimer, QRect + from PyQt5.QtWidgets import QApplication, QPlainTextEdit +except ImportError: + from PySide2.QtCore import QTimer, QRect + from PySide2.QtWidgets import QApplication, QPlainTextEdit + + +class Window(QPlainTextEdit): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + self.appendPlainText('修改分辨率后查看') + # 记录最后一次的值(减少槽调用) + self.m_rect = QRect() + # 使用定时器来延迟触发最后一次变化 + self.m_timer = QTimer(self, timeout=self.onSolutionChanged) + self.m_timer.setSingleShot(True) # **重要** 保证多次信号尽量少的调用函数 + + # 主要是多屏幕->无屏幕->有屏幕 + QApplication.instance().primaryScreenChanged.connect(lambda _: self.m_timer.start(1000)) + # 其它信号最终基本上都会调用该信号 + QApplication.instance().primaryScreen().virtualGeometryChanged.connect( + lambda _: self.m_timer.start(1000)) + # DPI变化 + QApplication.instance().primaryScreen().logicalDotsPerInchChanged.connect( + lambda _: self.m_timer.start(1000)) + + def onSolutionChanged(self): + # 获取主屏幕 + screen = QApplication.instance().primaryScreen() + if self.m_rect == screen.availableVirtualGeometry(): + return + self.m_rect = screen.availableVirtualGeometry() + # 所有屏幕可用大小 + self.appendPlainText('\navailableVirtualGeometry: {0}'.format(str(screen.availableVirtualGeometry()))) + # 获取所有屏幕 + screens = QApplication.instance().screens() + for screen in screens: + self.appendPlainText( + 'screen: {0}, geometry({1}), availableGeometry({2}), logicalDotsPerInch({3}), ' + 'physicalDotsPerInch({4}), refreshRate({5})'.format( + screen.name(), screen.geometry(), screen.availableGeometry(), screen.logicalDotsPerInch(), + screen.physicalDotsPerInch(), screen.refreshRate())) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/Demo/ScreenShot/IsSignalConnected.png b/Demo/ScreenShot/IsSignalConnected.png index e87aaeff..829976f9 100644 Binary files a/Demo/ScreenShot/IsSignalConnected.png and b/Demo/ScreenShot/IsSignalConnected.png differ diff --git a/Demo/ScreenShot/NewFramelessWindow.gif b/Demo/ScreenShot/NewFramelessWindow.gif new file mode 100644 index 00000000..88f4333d Binary files /dev/null and b/Demo/ScreenShot/NewFramelessWindow.gif differ diff --git a/Demo/ScreenShot/ScreenNotify.png b/Demo/ScreenShot/ScreenNotify.png new file mode 100644 index 00000000..e95330cc Binary files /dev/null and b/Demo/ScreenShot/ScreenNotify.png differ 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/ScreenShotDll.py b/Demo/ScreenShotDll.py index 964df60e..0def9e7c 100644 --- a/Demo/ScreenShotDll.py +++ b/Demo/ScreenShotDll.py @@ -1,3 +1,4 @@ from ctypes import CDLL + dll = CDLL('Data/ScreenShot.dll') dll.PrScrn() diff --git a/Demo/SharedMemory.py b/Demo/SharedMemory.py index 0ec5919a..f654ad1e 100644 --- a/Demo/SharedMemory.py +++ b/Demo/SharedMemory.py @@ -1,28 +1,29 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年3月30日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: TestQSharedMemory @description: -''' -from PyQt5.QtWidgets import QWidget +""" +from PyQt5.QtWidgets import QWidget from Lib.Application import SharedApplication # @UnresolvedImport -__version__ = "0.0.1" class Widget(QWidget): - - def __init__(self,*args,**kwargs): - super(Widget, self).__init__(*args,**kwargs) + + def __init__(self, *args, **kwargs): + super(Widget, self).__init__(*args, **kwargs) + if __name__ == "__main__": - import sys,os + import sys, os + print(os.getpid()) app = SharedApplication(sys.argv) if app.isRunning(): @@ -30,4 +31,4 @@ def __init__(self,*args,**kwargs): sys.exit(0) w = Widget() w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/Demo/ShowFrameWhenDrag.py b/Demo/ShowFrameWhenDrag.py index a7c986e6..5f0993fb 100644 --- a/Demo/ShowFrameWhenDrag.py +++ b/Demo/ShowFrameWhenDrag.py @@ -4,20 +4,18 @@ """ Created on 2019年4月23日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ShowFrameWhenDrag @description: 调整窗口显示边框 """ -from ctypes import sizeof, windll, c_int, byref, c_long, c_void_p, c_ulong, c_longlong,\ +from ctypes import sizeof, windll, c_int, byref, c_long, c_void_p, c_ulong, c_longlong, \ c_ulonglong, WINFUNCTYPE, c_uint -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel - - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QApplication +except ImportError: + from PySide2.QtWidgets import QWidget, QVBoxLayout, QLabel, QApplication if sizeof(c_long) == sizeof(c_void_p): WPARAM = c_ulong @@ -82,7 +80,7 @@ def _wndproc(self, hwnd, msg, wparam, lparam): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/Demo/SingleApplication.py b/Demo/SingleApplication.py index d452e167..1f5f74ee 100644 --- a/Demo/SingleApplication.py +++ b/Demo/SingleApplication.py @@ -1,28 +1,29 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年3月30日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: TestQSingleApplication @description: -''' +""" + from PyQt5.QtWidgets import QTextEdit from Lib.Application import QSingleApplication # @UnresolvedImport -__version__ = "0.0.1" - class Widget(QTextEdit): - + def __init__(self, *args, **kwargs): super(Widget, self).__init__(*args, **kwargs) + if __name__ == "__main__": import sys + app = QSingleApplication(sys.argv) if app.isRunning(): app.sendMessage("app is running") 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/VerificationCode.py b/Demo/VerificationCode.py index df25ba7f..9490d8ef 100644 --- a/Demo/VerificationCode.py +++ b/Demo/VerificationCode.py @@ -1,23 +1,25 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年4月5日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: widgets.WidgetCode @description: -''' -from random import sample +""" import string +from random import sample -from PyQt5.QtCore import Qt, qrand, QPointF, QPoint, QBasicTimer -from PyQt5.QtGui import QPainter, QBrush, QPen, QPalette, QFontMetrics -from PyQt5.QtWidgets import QLabel - - -__version__ = "0.0.1" +try: + from PyQt5.QtCore import Qt, qrand, QPointF, QPoint, QBasicTimer + from PyQt5.QtGui import QPainter, QBrush, QPen, QPalette, QFontMetrics, QFontDatabase + from PyQt5.QtWidgets import QLabel, QApplication, QWidget, QHBoxLayout, QLineEdit +except ImportError: + from PySide2.QtCore import Qt, qrand, QPointF, QPoint, QBasicTimer + from PySide2.QtGui import QPainter, QBrush, QPen, QPalette, QFontMetrics, QFontDatabase + from PySide2.QtWidgets import QLabel, QApplication, QWidget, QHBoxLayout, QLineEdit DEF_NOISYPOINTCOUNT = 60 # 噪点数量 COLORLIST = ("black", "gray", "red", "green", "blue", "magenta") @@ -28,8 +30,9 @@ WORDS = list(string.ascii_letters + string.digits) SINETABLE = (0, 38, 71, 92, 100, 92, 71, 38, 0, -38, -71, -92, -100, -92, -71, -38) + class WidgetCode(QLabel): - + def __init__(self, *args, **kwargs): super(WidgetCode, self).__init__(*args, **kwargs) self._sensitive = False # 是否大小写敏感 @@ -47,33 +50,33 @@ def __init__(self, *args, **kwargs): self.step = 0 self.timer = QBasicTimer() self.timer.start(60, self) - + def reset(self): self._code = "".join(sample(WORDS, 4)) # 随机4个字符 self.setText(self._code) - + def check(self, code): return self._code == str(code) if self._sensitive else self._code.lower() == str(code).lower() - + def setSensitive(self, sensitive): self._sensitive = sensitive - -# def setText(self, text): -# text = text if (text and len(text) == 4) else "".join(sample(WORDS, 4)) # 随机4个字符 -# self._code = str(text) -# html = "".join([FONT.format(color=COLORLIST[qrand() % 6], word=t) for t in text]) -# super(WidgetCode, self).setText(HTML.format(html=html)) - + + # def setText(self, text): + # text = text if (text and len(text) == 4) else "".join(sample(WORDS, 4)) # 随机4个字符 + # self._code = str(text) + # html = "".join([FONT.format(color=COLORLIST[qrand() % 6], word=t) for t in text]) + # super(WidgetCode, self).setText(HTML.format(html=html)) + def mouseReleaseEvent(self, event): super(WidgetCode, self).mouseReleaseEvent(event) self.reset() - + def timerEvent(self, event): if event.timerId() == self.timer.timerId(): self.step += 1 return self.update() return super(WidgetCode, self).timerEvent(event) - + def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) @@ -107,21 +110,20 @@ def paintEvent(self, event): painter.drawText(x, y - ((SINETABLE[index] * metrics.height()) / 400), ch) x += metrics.width(ch) + if __name__ == "__main__": import sys - from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout - from PyQt5.QtGui import QFontDatabase - from PyQt5.QtWidgets import QLineEdit + app = QApplication(sys.argv) app.setApplicationName("Validate Code") QFontDatabase.addApplicationFont("Data/itckrist.ttf") w = QWidget() layout = QHBoxLayout(w) - + cwidget = WidgetCode(w, minimumHeight=35, minimumWidth=80) layout.addWidget(cwidget) lineEdit = QLineEdit(w, maxLength=4, placeholderText="请输入验证码并按回车验证", - returnPressed=lambda:print(cwidget.check(lineEdit.text()))) + returnPressed=lambda: print(cwidget.check(lineEdit.text()))) layout.addWidget(lineEdit) w.show() sys.exit(app.exec_()) diff --git a/Demo/WeltHideWindow.py b/Demo/WeltHideWindow.py old mode 100644 new mode 100755 index 73ba7b0d..91a49f9a --- a/Demo/WeltHideWindow.py +++ b/Demo/WeltHideWindow.py @@ -4,32 +4,83 @@ """ Created on 2018年3月1日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: WeltHideWindow @description: 简单的窗口贴边隐藏 """ -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton +import os +import platform +from subprocess import getoutput -__Author__ = 'By: Irony\nQQ: 892768447\nEmail: 892768447@qq.com' -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import ( + QApplication, + QGridLayout, + QMessageBox, + QPushButton, + QSizePolicy, + QSpacerItem, + QWidget, + ) +except ImportError: + from PySide2.QtCore import Qt + 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 - from PyQt5.QtWidgets import QApplication + 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/WindowNotify.py b/Demo/WindowNotify.py index 408ff5e9..490dbd4e 100644 --- a/Demo/WindowNotify.py +++ b/Demo/WindowNotify.py @@ -1,27 +1,27 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年3月30日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: WindowNotify @description: 右下角弹窗 -''' +""" import webbrowser -from PyQt5.QtCore import Qt, QPropertyAnimation, QPoint, QTimer, pyqtSignal -from PyQt5.QtWidgets import QWidget, QPushButton +try: + from PyQt5.QtCore import Qt, QPropertyAnimation, QPoint, QTimer, pyqtSignal + from PyQt5.QtWidgets import QWidget, QPushButton, QApplication, QHBoxLayout +except ImportError: + from PySide2.QtCore import Qt, QPropertyAnimation, QPoint, QTimer, Signal as pyqtSignal + from PySide2.QtWidgets import QWidget, QPushButton, QApplication, QHBoxLayout from Lib.UiNotify import Ui_NotifyForm # @UnresolvedImport -__version__ = "0.0.1" - - class WindowNotify(QWidget, Ui_NotifyForm): - SignalClosed = pyqtSignal() # 弹窗关闭信号 def __init__(self, title="", content="", timeout=5000, *args, **kwargs): @@ -60,10 +60,10 @@ def onView(self): webbrowser.open_new_tab("http://alyl.vip/") def onClose(self): - #点击关闭按钮时 + # 点击关闭按钮时 print("onClose") self.isShow = False - QTimer.singleShot(100, self.closeAnimation)#启动弹回动画 + QTimer.singleShot(100, self.closeAnimation) # 启动弹回动画 def _init(self): # 隐藏任务栏|去掉边框|顶层显示 @@ -112,13 +112,14 @@ def showAnimation(self): print("showAnimation isShow = True") # 显示动画 self.isShow = True - self.animation.stop()#先停止之前的动画,重新开始 + self.animation.stop() # 先停止之前的动画,重新开始 self.animation.setStartValue(self.pos()) self.animation.setEndValue(self._endPos) self.animation.start() # 弹出5秒后,如果没有焦点则弹回去 self._timer.start(self._timeout) -# QTimer.singleShot(self._timeout, self.closeAnimation) + + # QTimer.singleShot(self._timeout, self.closeAnimation) def closeAnimation(self): print("closeAnimation hasFocus", self.hasFocus()) @@ -158,9 +159,10 @@ def leaveEvent(self, event): if self._timeouted: QTimer.singleShot(1000, self.closeAnimation) + if __name__ == "__main__": import sys - from PyQt5.QtWidgets import QApplication, QHBoxLayout + app = QApplication(sys.argv) window = QWidget() 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/Donate/PyQt_Group.png b/Donate/PyQt_Group.png new file mode 100644 index 00000000..4be38ded Binary files /dev/null and b/Donate/PyQt_Group.png differ diff --git a/Donate/PyQt_Guild.png b/Donate/PyQt_Guild.png new file mode 100644 index 00000000..68ffa8e6 Binary files /dev/null and b/Donate/PyQt_Guild.png differ diff --git a/Donate/weixin.png b/Donate/weixin.png index 0e7fe9d3..ac0a7284 100644 Binary files a/Donate/weixin.png and b/Donate/weixin.png differ diff --git a/Donate/weixin2.png b/Donate/weixin2.png new file mode 100644 index 00000000..0e7fe9d3 Binary files /dev/null and b/Donate/weixin2.png differ diff --git a/LICENSE b/LICENSE index 94a9ed02..8000a6fa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,504 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 - Copyright (C) 2007 Free Software Foundation, Inc. + 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 GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, 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 -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + 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. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of +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. - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or + 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 convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. +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 Programs + How to Apply These Terms to Your New Libraries - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. + 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 do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. + 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 program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + 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 program is distributed in the hope that it will be useful, + 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 General Public License for more details. + 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 General Public License - along with this program. If not, see . + 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. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +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/Data/__init__.py b/PyQtGraph/Data/__init__.py index c5a3c6f8..ce54303d 100644 --- a/PyQtGraph/Data/__init__.py +++ b/PyQtGraph/Data/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 -''' +""" @author: wxj @license: (C) Hefei tongzhi electromechanical control technology co.LTD @contact: @@ -8,4 +8,4 @@ @file: __init__.py.py @time: 2019/5/21 18:07 @desc: -''' +""" diff --git a/PyQtGraph/Data/graphAnalysis.py b/PyQtGraph/Data/graphAnalysis.py index 67438538..0f30b3a6 100644 --- a/PyQtGraph/Data/graphAnalysis.py +++ b/PyQtGraph/Data/graphAnalysis.py @@ -8,6 +8,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets + class graph_Form(object): def setupUi(self, Form): Form.setObjectName("Form") @@ -50,4 +51,3 @@ def retranslateUi(self, Form): self.label.setText(_translate("Form", "图形分析")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("Form", "折线图")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("Form", "Tab 2")) - diff --git a/PyQtGraph/Data/graphTest.py b/PyQtGraph/Data/graphTest.py index 22cf8f7c..06ffde6d 100644 --- a/PyQtGraph/Data/graphTest.py +++ b/PyQtGraph/Data/graphTest.py @@ -8,6 +8,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets + class graph_Form(object): def setupUi(self, Form): Form.setObjectName("Form") @@ -51,4 +52,3 @@ def retranslateUi(self, Form): self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("Form", "折线图")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("Form", "Tab 2")) self.pushButton_7.setText(_translate("Form", "分析")) - diff --git a/PyQtGraph/README.md b/PyQtGraph/README.md index ef38e094..c745aaee 100644 --- a/PyQtGraph/README.md +++ b/PyQtGraph/README.md @@ -14,23 +14,28 @@ 6. `pg.PlotWidget()`鼠标获取X轴坐标 ## 目录 + - [鼠标获取X轴坐标](#1鼠标获取X轴坐标) - [禁止右键点击功能、鼠标滚轮,添加滚动条等功能](#2禁止右键点击功能、鼠标滚轮,添加滚动条等功能) ## 1、鼠标获取X轴坐标 + [运行 mouseFlow.py](mouseFlow.py) ![mouseFlow](ScreenShot/mouseFlow.gif) ## 2、禁止右键点击功能、鼠标滚轮,添加滚动条等功能 -[运行 graph1.py](graph1.py) + +[运行 graph1.py](graph1.py) | [查看 graphTest.ui](Data/graphTest.ui) ![mouseFlow](ScreenShot/function.gif) -## 3、不用修改源码,重加载,解决右键保存图片异常;解决自定义坐标轴密集显示;禁止鼠标事件; +## 3、不用修改源码,重加载,解决右键保存图片异常;解决自定义坐标轴密集显示;禁止鼠标事件 + [加载 tools.py](tools.py) -## 4、QScrollArea添加和修改大小例子; -[运行 testGraphAnalysis.py](testGraphAnalysis.py) +## 4、QScrollArea添加和修改大小例子 + +[运行 testGraphAnalysis.py](testGraphAnalysis.py) | [查看 graphAnalysis.ui](Data/graphAnalysis.ui) ![testGraphAnalysis](ScreenShot/GraphAnalysis.gif) diff --git a/PyQtGraph/graph1.py b/PyQtGraph/graph1.py index 521ec41a..b37b8ade 100644 --- a/PyQtGraph/graph1.py +++ b/PyQtGraph/graph1.py @@ -1,19 +1,22 @@ #!/usr/bin/env python # encoding: utf-8 -''' +""" Created on 2019年5月21日 @author: weike32 -@site: https://pyqt5.com ,https://github.com/weike32 +@site: https://pyqt.site ,https://github.com/weike32 @email: 394967319@qq.com @file: CopyContent @description: 禁止右键,添加滑动窗口,点击按钮生成图片,自定义Y轴坐标,背景颜色调整 -''' +""" import sys -from PyQt5.QtWidgets import QDialog, QApplication, QWidget -from qtpy import QtWidgets + import pyqtgraph as pg +from PyQt5.QtGui import QSpacerItem, QSizePolicy +from PyQt5.QtWidgets import QDialog, QApplication, QWidget, QScrollArea, QVBoxLayout + from PyQtGraph.Data.graphTest import graph_Form + class CustomViewBox(pg.ViewBox): def __init__(self, *args, **kwds): pg.ViewBox.__init__(self, *args, **kwds) @@ -27,11 +30,12 @@ def mouseClickEvent(self, ev): def mouseDragEvent(self, ev): pg.ViewBox.mouseDragEvent(self, ev) - def wheelEvent(self,ev, axis=None): + def wheelEvent(self, ev, axis=None): # pg.ViewBox.wheelEvent(self, ev, axis) ev.ignore() -class graphAnalysis(QDialog,graph_Form): + +class graphAnalysis(QDialog, graph_Form): def __init__(self): super(graphAnalysis, self).__init__() self.setupUi(self) @@ -39,35 +43,37 @@ def __init__(self): self.tabWidget.clear() def test(self): - tab1 = QtWidgets.QWidget() - scrollArea = QtWidgets.QScrollArea(tab1) - scrollArea.setMinimumSize(984,550) + tab1 = QWidget() + scrollArea = QScrollArea(tab1) + scrollArea.setMinimumSize(984, 550) scrollArea.setWidgetResizable(True) labelsContainer = QWidget() - labelsContainer.setMinimumSize(0,1500) + labelsContainer.setMinimumSize(0, 1500) scrollArea.setWidget(labelsContainer) - layout = QtWidgets.QVBoxLayout(labelsContainer) + layout = QVBoxLayout(labelsContainer) time = ['2019-04-20 08:09:00', '2019-04-20 08:09:00', '2019-04-20 08:09:00', '2019-04-20 08:09:00'] value = [1.2, 2, 1, 4] xdict = dict(enumerate(time)) ticks = [list(zip(range(4), tuple(time)))] vb = CustomViewBox() - plt = pg.PlotWidget(title="标题这里填写",viewBox=vb) + plt = pg.PlotWidget(title="标题这里填写", viewBox=vb) plt.setBackground(background=None) plt.plot(list(xdict.keys()), value) plt.getPlotItem().getAxis("bottom").setTicks(ticks) - temp = QtWidgets.QWidget() - temp.setMinimumSize(900,300) - temp.setMaximumSize(900,300) - layout1 = QtWidgets.QVBoxLayout(temp) + temp = QWidget() + temp.setMinimumSize(900, 300) + temp.setMaximumSize(900, 300) + layout1 = QVBoxLayout(temp) layout1.addWidget(plt) layout.addWidget(temp) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + spacerItem = QSpacerItem(20, 40, QSizePolicy.Minimum, + QSizePolicy.Expanding) layout.addItem(spacerItem) self.tabWidget.addTab(tab1, '这里tabWidget修改标签') -if __name__ =="__main__": + +if __name__ == "__main__": app = QApplication(sys.argv) w = graphAnalysis() w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/PyQtGraph/mouseFlow.py b/PyQtGraph/mouseFlow.py index 9d9f9fb4..1d3503b7 100644 --- a/PyQtGraph/mouseFlow.py +++ b/PyQtGraph/mouseFlow.py @@ -1,18 +1,20 @@ #!/usr/bin/env python # encoding: utf-8 -''' +""" Created on 2019年5月2日 @author: weike32 -@site: https://pyqt5.com ,https://github.com/weike32 +@site: https://pyqt.site ,https://github.com/weike32 @email: 394967319@qq.com @file: CopyContent @description: 查阅了很多博客,如果有异,可以联系作者邮箱。本Demo仅作学习参考用,保有后续相关权益。 -''' +""" import sys -from PyQt5.QtWidgets import QApplication, QMainWindow -from PyQt5 import QtCore + import numpy as np import pyqtgraph as pg +from PyQt5 import QtCore +from PyQt5.QtWidgets import QApplication, QMainWindow + class Ui_Form(object): def setupUi(self, Form): @@ -22,6 +24,7 @@ def setupUi(self, Form): self.graphicsView.setGeometry(QtCore.QRect(75, 131, 621, 441)) self.graphicsView.setObjectName("graphicsView") + class MyWindow(QMainWindow, Ui_Form): def __init__(self, parent=None): super(MyWindow, self).__init__(parent) @@ -33,12 +36,15 @@ def __init__(self, parent=None): self.graphicsView.addItem(self.label) self.setMouseTracking(True) self.graphicsView.scene().sigMouseMoved.connect(self.onMouseMoved) + def onMouseMoved(self, evt): if self.graphicsView.plotItem.vb.mapSceneToView(evt): - point =self.graphicsView.plotItem.vb.mapSceneToView(evt) + point = self.graphicsView.plotItem.vb.mapSceneToView(evt) self.label.setHtml("

横坐标:{0}

".format(point.x())) + + if __name__ == '__main__': app = QApplication(sys.argv) myWin = MyWindow() myWin.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/PyQtGraph/testGraphAnalysis.py b/PyQtGraph/testGraphAnalysis.py index 7cdbb0bc..23f23a4c 100644 --- a/PyQtGraph/testGraphAnalysis.py +++ b/PyQtGraph/testGraphAnalysis.py @@ -1,18 +1,19 @@ #!/usr/bin/env python # encoding: utf-8 -''' +""" Created on 2019年8月17日 @author: weike32 -@site: https://pyqt5.com ,https://github.com/weike32 +@site: https://pyqt.site ,https://github.com/weike32 @email: 394967319@qq.com @file: CopyContent @description: -''' +""" import sys -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QDialog, QApplication, QWidget -from qtpy import QtWidgets + import pyqtgraph as pg +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QSpacerItem, QSizePolicy +from PyQt5.QtWidgets import QDialog, QApplication, QWidget, QScrollArea, QVBoxLayout from PyQtGraph.Data.graphAnalysis import graph_Form @@ -26,38 +27,41 @@ def __init__(self, *args, **kwds): def mouseClickEvent(self, ev): if ev.button() == pg.QtCore.Qt.RightButton: self.autoRange() + def mouseDragEvent(self, ev): pg.ViewBox.mouseDragEvent(self, ev) - def wheelEvent(self,ev,axis=None): + + def wheelEvent(self, ev, axis=None): ev.ignore() -class graphAnalysis(QDialog,graph_Form): + +class graphAnalysis(QDialog, graph_Form): def __init__(self): super(graphAnalysis, self).__init__() self.setupUi(self) self.pushButton_7.clicked.connect(self.test) self.tabWidget.clear() - def handleChanged(self,item,column): + def handleChanged(self, item, column): count = item.childCount() if item.checkState(column) == Qt.Checked: for index in range(count): - item.child(index).setCheckState(0,Qt.Checked) + item.child(index).setCheckState(0, Qt.Checked) if item.checkState(column) == Qt.Unchecked: for index in range(count): - item.child(index).setCheckState(0,Qt.Unchecked) + item.child(index).setCheckState(0, Qt.Unchecked) def test(self): - tab1 = QtWidgets.QWidget() - scrollArea = QtWidgets.QScrollArea(tab1) - scrollArea.setMinimumSize(650,550) + tab1 = QWidget() + scrollArea = QScrollArea(tab1) + scrollArea.setMinimumSize(650, 550) scrollArea.setWidgetResizable(True) labelsContainer = QWidget() - labelsContainer.setMinimumSize(0,3000+200) + labelsContainer.setMinimumSize(0, 3000 + 200) scrollArea.setWidget(labelsContainer) - layout = QtWidgets.QVBoxLayout(labelsContainer) + layout = QVBoxLayout(labelsContainer) time = ['2019-04-20 08:09:00', '2019-04-20 08:09:00', '2019-04-20 08:09:00', '2019-04-20 08:09:00'] value = [1.2, 2, 1, 4] @@ -65,26 +69,28 @@ def test(self): ticks = [list(zip(range(4), tuple(time)))] for i in range(11): vb1 = CustomViewBox() - plt1 = pg.PlotWidget(title="Basic array plotting%s"%i, viewBox=vb1) + plt1 = pg.PlotWidget(title="Basic array plotting%s" % i, viewBox=vb1) plt1.resize(500, 500) plt1.setBackground(background=None) plt1.plot(list(xdict.keys()), value) plt1.getPlotItem().getAxis("bottom").setTicks(ticks) - temp1 = QtWidgets.QWidget() + temp1 = QWidget() temp1.setMinimumSize(600, 300) temp1.setMaximumSize(600, 300) - layout2 = QtWidgets.QVBoxLayout(temp1) + layout2 = QVBoxLayout(temp1) layout2.addWidget(plt1) layout.addWidget(temp1) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + spacerItem = QSpacerItem(20, 40, QSizePolicy.Minimum, + QSizePolicy.Expanding) layout.addItem(spacerItem) # print(layout.count()) self.tabWidget.addTab(tab1, '12') for i in range(self.tabWidget.count()): self.tabWidget.widget(i) -if __name__ =="__main__": + +if __name__ == "__main__": app = QApplication(sys.argv) w = graphAnalysis() w.show() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/PyQtGraph/tools.py b/PyQtGraph/tools.py index edfb931b..5a867de3 100644 --- a/PyQtGraph/tools.py +++ b/PyQtGraph/tools.py @@ -1,27 +1,31 @@ #!/usr/bin/env python # encoding: utf-8 -''' +""" Created on 2019年5月21日 @author: weike32 -@site: https://pyqt5.com ,https://github.com/weike32 +@site: https://pyqt.site ,https://github.com/weike32 @email: 394967319@qq.com @file: CopyContent @description: 工具类 -''' +""" +import pyqtgraph as pg from pyqtgraph.exporters.ImageExporter import ImageExporter, Exporter from pyqtgraph.parametertree import Parameter -import pyqtgraph as pg -#不用修改源码,重加载,解决右键保存图片异常 + + +# 不用修改源码,重加载,解决右键保存图片异常 def widthChanged(self): sr = self.getSourceRect() ar = float(sr.height()) / sr.width() self.params.param('height').setValue(int(self.params['width'] * ar), blockSignal=self.heightChanged) + def heightChanged(self): sr = self.getSourceRect() ar = float(sr.width()) / sr.height() self.params.param('width').setValue(int(self.params['height'] * ar), blockSignal=self.widthChanged) + def New__init__(self, item): Exporter.__init__(self, item) tr = self.getTargetRect() @@ -42,11 +46,14 @@ def New__init__(self, item): ]) self.params.param('width').sigValueChanged.connect(self.widthChanged) self.params.param('height').sigValueChanged.connect(self.heightChanged) + + ImageExporter.heightChanged = heightChanged ImageExporter.widthChanged = widthChanged ImageExporter.__init__ = New__init__ -#解决自定义坐标轴密集显示 + +# 解决自定义坐标轴密集显示 class MyStringAxis(pg.AxisItem): def __init__(self, xdict, *args, **kwargs): pg.AxisItem.__init__(self, *args, **kwargs) @@ -62,6 +69,8 @@ def tickStrings(self, values, scale, spacing): vstr = "" strings.append(vstr) return strings + + # 禁止鼠标事件 class CustomViewBox(pg.ViewBox): def __init__(self, *args, **kwds): @@ -76,5 +85,5 @@ def mouseClickEvent(self, ev): def mouseDragEvent(self, ev): pg.ViewBox.mouseDragEvent(self, ev) - def wheelEvent(self,ev, axis=None): - pg.ViewBox.wheelEvent(self, ev, axis) \ No newline at end of file + def wheelEvent(self, ev, axis=None): + pg.ViewBox.wheelEvent(self, ev, axis) diff --git a/QAxWidget/README.md b/QAxWidget/README.md index 8811751d..2c8d8cdd 100644 --- a/QAxWidget/README.md +++ b/QAxWidget/README.md @@ -4,10 +4,11 @@ - [显示Word、Excel、PDF文件](#1显示WordExcelPDF文件) ## 1、显示Word、Excel、PDF文件 + [运行 ViewOffice.py](ViewOffice.py) 1. 利用 `Word.Application` 打开Word文件 1. 利用 `Excel.Application` 打开Excel文件 1. 利用 `Adobe PDF Reader` 打开PDF文件(前提先装PDF软件) -![ViewOffice](ScreenShot/ViewOffice.png) \ No newline at end of file +![ViewOffice](ScreenShot/ViewOffice.png) diff --git a/QAxWidget/ViewOffice.py b/QAxWidget/ViewOffice.py index f30d753f..3c331809 100644 --- a/QAxWidget/ViewOffice.py +++ b/QAxWidget/ViewOffice.py @@ -1,24 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年4月6日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ViewOffice @description: -''' +""" + from PyQt5.QAxContainer import QAxWidget -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QFileDialog,\ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QFileDialog, \ QMessageBox -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - - class AxWidget(QWidget): def __init__(self, *args, **kwargs): @@ -68,6 +64,7 @@ def closeEvent(self, event): if __name__ == '__main__': import sys from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = AxWidget() w.show() diff --git a/QCalendarWidget/CalendarQssStyle.py b/QCalendarWidget/CalendarQssStyle.py index b385325e..9d2e0924 100644 --- a/QCalendarWidget/CalendarQssStyle.py +++ b/QCalendarWidget/CalendarQssStyle.py @@ -1,19 +1,23 @@ """ Created on 2018年1月30日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CalendarQssStyle @description: 日历美化样式 """ import sys -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QTextCharFormat, QBrush, QColor -from PyQt5.QtWidgets import QApplication, QCalendarWidget +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QTextCharFormat, QBrush, QColor + from PyQt5.QtWidgets import QApplication, QCalendarWidget +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QTextCharFormat, QBrush, QColor + from PySide2.QtWidgets import QApplication, QCalendarWidget - -StyleSheet = ''' +StyleSheet = """ /*顶部导航区域*/ #qt_calendar_navigationbar { background-color: rgb(0, 188, 212); @@ -101,7 +105,7 @@ outline: 0px;/*去掉选中后的虚线框*/ selection-background-color: rgb(0, 188, 212); /*选中背景颜色*/ } -''' +""" class CalendarWidget(QCalendarWidget): diff --git a/QCalendarWidget/README.md b/QCalendarWidget/README.md index 62bd8815..0a6e8adb 100644 --- a/QCalendarWidget/README.md +++ b/QCalendarWidget/README.md @@ -4,8 +4,9 @@ - [QSS美化日历样式](#1QSS美化日历样式) ## 1、QSS美化日历样式 + [运行 CalendarQssStyle.py](CalendarQssStyle.py) 对日历控件的部分控件进行QSS美化,顶部背景颜色和高度,上下月按钮、月份选择、年选择、菜单 -![CalendarQssStyle](ScreenShot/CalendarQssStyle.gif) \ No newline at end of file +![CalendarQssStyle](ScreenShot/CalendarQssStyle.gif) diff --git a/QColumnView/FileManager.py b/QColumnView/FileManager.py new file mode 100644 index 00000000..810e489a --- /dev/null +++ b/QColumnView/FileManager.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/04/08 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: FileManager.py +@description: +""" + +try: + from PyQt5.QtWidgets import ( + QApplication, + QColumnView, + QFileSystemModel, + QGridLayout, + QMessageBox, + QPushButton, + QSizePolicy, + QSpacerItem, + QWidget, + ) +except Exception: + from PySide2.QtWidgets import ( + QApplication, + QColumnView, + QFileSystemModel, + QGridLayout, + QMessageBox, + QPushButton, + QSizePolicy, + QSpacerItem, + QWidget, + ) + + +class FileManager(QWidget): # type: ignore + def __init__(self, *args, **kwargs): + super(FileManager, self).__init__(*args, **kwargs) + self.resize(800, 600) + self._view = QColumnView(self) + self._btn = QPushButton("确定", self) + layout = QGridLayout(self) + layout.addWidget(self._view, 0, 0, 1, 2) + layout.addItem( + QSpacerItem( + 40, + 20, + QSizePolicy.Expanding, + QSizePolicy.Minimum, + ), + 1, + 0, + ) + layout.addWidget(self._btn, 1, 1, 1, 1) + layout.setRowStretch(1, 0) + self._model = QFileSystemModel(self) + self._model.setRootPath("") # 设置根路径 + self._view.setModel(self._model) # type: ignore + self._btn.clicked.connect(self.onAccept) # type: ignore + + def onAccept(self): + path = self._model.filePath(self._view.currentIndex()) + print("path", path) + if path: + QMessageBox.information(self, "提示", "路径:%s" % path) + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = FileManager() + w.show() + sys.exit(app.exec_()) diff --git a/QColumnView/README.md b/QColumnView/README.md index e69de29b..bea28a78 100644 --- a/QColumnView/README.md +++ b/QColumnView/README.md @@ -0,0 +1,16 @@ +# QColumnView + +- 目录 + - [文件系统浏览器](#1文件系统浏览器) + +## 1、文件系统浏览器 + +[运行 FileManager.py](FileManager.py) + +一个省市区关联的三级联动,数据源在data.json中 + +1. 通过`QFileSystemModel`模型来显示文件系统 +2. 结合`QColumnView`的`setMode`函数显示文件系统模型 +3. 通过`QFileSystemModel.filePath(QColumnView.currentIndex())`获取当前选中的路径 + +![FileManager](ScreenShot/FileManager.png) diff --git a/QColumnView/ScreenShot/FileManager.png b/QColumnView/ScreenShot/FileManager.png new file mode 100644 index 00000000..c69afdce Binary files /dev/null and b/QColumnView/ScreenShot/FileManager.png differ diff --git a/QComboBox/CenterText.py b/QComboBox/CenterText.py new file mode 100644 index 00000000..30eef32a --- /dev/null +++ b/QComboBox/CenterText.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2022/09/03 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CenterText.py +@description: 文字居中对齐 +""" + +import sys + +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import QApplication, QStyle, QVBoxLayout, QWidget +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QApplication, QStyle, QVBoxLayout, QWidget + +from Lib.CtComboBox import CtComboBox + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + layout = QVBoxLayout(self) + + c1 = CtComboBox(self) + c1.addItems(['item-%s' % i for i in range(10)]) + layout.addWidget(c1) + + # 可编辑 + c2 = CtComboBox(self) + c2.setEditable(True) + c2.lineEdit().setAlignment(Qt.AlignCenter) + c2.addItems(['item-%s' % i for i in range(10)]) + layout.addWidget(c2) + + # 带图标 + c3 = CtComboBox(self) + for i in range(10): + c3.addItem(c3.style().standardIcon(QStyle.SP_ComputerIcon), + 'item-%s' % i) + layout.addWidget(c3) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QComboBox/CityLinkage.py b/QComboBox/CityLinkage.py index 603f342d..203888f4 100644 --- a/QComboBox/CityLinkage.py +++ b/QComboBox/CityLinkage.py @@ -3,8 +3,8 @@ """ Created on 2018年1月27日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CityLinkage @description: 下拉联动 @@ -12,16 +12,18 @@ import json import sys -from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp -from PyQt5.QtGui import QStandardItemModel, QStandardItem -from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QComboBox,\ - QLabel, QSpacerItem, QSizePolicy import chardet - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp + from PyQt5.QtGui import QStandardItemModel, QStandardItem + from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QComboBox, \ + QLabel, QSpacerItem, QSizePolicy +except ImportError: + from PySide2.QtCore import Qt, QSortFilterProxyModel, QRegExp + from PySide2.QtGui import QStandardItemModel, QStandardItem + from PySide2.QtWidgets import QWidget, QApplication, QHBoxLayout, QComboBox, \ + QLabel, QSpacerItem, QSizePolicy class SortFilterProxyModel(QSortFilterProxyModel): diff --git a/QComboBox/Lib/CtComboBox.py b/QComboBox/Lib/CtComboBox.py new file mode 100644 index 00000000..3539ff11 --- /dev/null +++ b/QComboBox/Lib/CtComboBox.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2022/09/04 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CtComboBox.py +@description: 文字居中对齐 +""" + +try: + from PyQt5.QtCore import QRect, Qt + from PyQt5.QtGui import QIcon, QPalette + from PyQt5.QtWidgets import QComboBox, QProxyStyle +except ImportError: + from PySide2.QtCore import QRect, Qt + from PySide2.QtGui import QIcon, QPalette + from PySide2.QtWidgets import QComboBox, QProxyStyle + + +class ComboBoxStyle(QProxyStyle): + + def drawControl(self, element, option, painter, widget=None): + if element == QProxyStyle.CE_ComboBoxLabel: + # https://github.com/qt/qtbase/blob/5.15.2/src/widgets/styles/qcommonstyle.cpp#L2200 + editRect = self.subControlRect(QProxyStyle.CC_ComboBox, option, + QProxyStyle.SC_ComboBoxEditField, + widget) + painter.save() + painter.setClipRect(editRect) + if not option.currentIcon.isNull(): + # 绘制图标 + mode = QIcon.Normal if ( + option.state & + QProxyStyle.State_Enabled) else QIcon.Disabled + pixmap = option.currentIcon.pixmap( + widget.window().windowHandle() if widget else None, + option.iconSize, mode) + iconRect = QRect(editRect) + iconRect.setWidth(option.iconSize.width() + 4) + iconRect = self.alignedRect(option.direction, + Qt.AlignLeft | Qt.AlignVCenter, + iconRect.size(), editRect) + if option.editable: + painter.fillRect(iconRect, + option.palette.brush(QPalette.Base)) + self.drawItemPixmap(painter, iconRect, Qt.AlignCenter, pixmap) + + if option.direction == Qt.RightToLeft: + editRect.translate(-4 - option.iconSize.width(), 0) + else: + editRect.translate(option.iconSize.width() + 4, 0) + if option.currentText and not option.editable: + # 考虑右边箭头位置 + arrowRect = self.subControlRect(QProxyStyle.CC_ComboBox, option, + QProxyStyle.SC_ComboBoxArrow, + widget) + editRect.setWidth(editRect.width() + arrowRect.width()) + # 绘制居中文字 + self.drawItemText( + painter, editRect.adjusted(1, 0, -1, 0), + self.visualAlignment(option.direction, Qt.AlignCenter), + option.palette, option.state & QProxyStyle.State_Enabled, + option.currentText) + painter.restore() + return + super(ComboBoxStyle, self).drawControl(element, option, painter, widget) + + +class CtComboBox(QComboBox): + + def __init__(self, *args, **kwargs): + super(CtComboBox, self).__init__(*args, **kwargs) + # 绑定每个元素添加信号,用于设置文本居中 + self.model().rowsInserted.connect(self._onRowsInserted) + self.setStyle(ComboBoxStyle()) + + def _onRowsInserted(self, index, first, last): + if first < 0: + return + for i in range(first, last + 1): + self.view().model().item(i).setTextAlignment(Qt.AlignCenter) diff --git a/QComboBox/Lib/__init__.py b/QComboBox/Lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/QComboBox/README.md b/QComboBox/README.md index 80a20776..aa75ef6c 100644 --- a/QComboBox/README.md +++ b/QComboBox/README.md @@ -2,8 +2,10 @@ - 目录 - [下拉数据关联](#1下拉数据关联) + - [文本居中显示](#2文本居中显示) ## 1、下拉数据关联 + [运行 CityLinkage.py](CityLinkage.py) 一个省市区关联的三级联动,数据源在data.json中 @@ -12,4 +14,13 @@ 2. 并根据唯一编码过滤,为了不影响内容显示,唯一编码的角色为`ToolTipRole` 3. 用`QColumnView`可以实现类似效果 -![CityLinkage](ScreenShot/CityLinkage.gif) \ No newline at end of file +![CityLinkage](ScreenShot/CityLinkage.gif) + +## 2、文本居中显示 + +[运行 CenterText.py](CenterText.py) + +1. 使用`QProxyStyle`对文件居中显示 +2. 新增得item数据使用`setTextAlignment`对齐 + +![CenterText](ScreenShot/CenterText.png) diff --git a/QComboBox/ScreenShot/CenterText.png b/QComboBox/ScreenShot/CenterText.png new file mode 100644 index 00000000..a99d8fad Binary files /dev/null and b/QComboBox/ScreenShot/CenterText.png differ diff --git a/QFileSystemModel/CustomIcon.py b/QFileSystemModel/CustomIcon.py index 9eb15b80..9b322288 100644 --- a/QFileSystemModel/CustomIcon.py +++ b/QFileSystemModel/CustomIcon.py @@ -3,28 +3,29 @@ """ Created on 2018年1月26日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com -@file: FileSystemModel +@file: CustomIcon @description: """ -import sys - -from PyQt5.QtCore import QFileInfo -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QFileSystemModel, QFileIconProvider, QApplication,\ - QTreeView +import sys -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - -# 图标提供类 +try: + from PyQt5.QtCore import QFileInfo + from PyQt5.QtGui import QIcon + from PyQt5.QtWidgets import QFileSystemModel, QFileIconProvider, QApplication, \ + QTreeView +except ImportError: + from PySide2.QtCore import QFileInfo + from PySide2.QtGui import QIcon + from PySide2.QtWidgets import QFileSystemModel, QFileIconProvider, QApplication, \ + QTreeView class FileIconProvider(QFileIconProvider): + """图标提供类""" def __init__(self, *args, **kwargs): super(FileIconProvider, self).__init__(*args, **kwargs) diff --git a/QFileSystemModel/README.md b/QFileSystemModel/README.md index fc76971f..cd0d7e9e 100644 --- a/QFileSystemModel/README.md +++ b/QFileSystemModel/README.md @@ -4,9 +4,10 @@ - [自定义图标](#1自定义图标) ## 1、自定义图标 + [运行 CustomIcon.py](CustomIcon.py) 1. 继承 `QFileIconProvider` 类实现自己的图标提供器 2. 重写 `def icon(self, type_info)` 方法根据文件类型返回对应的图标 -![CustomIcon](ScreenShot/CustomIcon.png) \ No newline at end of file +![CustomIcon](ScreenShot/CustomIcon.png) diff --git a/QFlowLayout/Data/CoverItemWidget.ui b/QFlowLayout/Data/CoverItemWidget.ui new file mode 100644 index 00000000..2f245e13 --- /dev/null +++ b/QFlowLayout/Data/CoverItemWidget.ui @@ -0,0 +1,85 @@ + + + CoverItemWidget + + + + 200 + 256 + + + + + 200 + 256 + + + + + + + + 12 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + 180 + 180 + + + + + + + true + + + Qt::AlignCenter + + + + + + + + 10 + + + + + + + + + + + + + + + + + + + CoverLabel + QLabel +
.CoverLabel
+
+
+ + +
diff --git a/QFlowLayout/Data/CoverLabel.ui b/QFlowLayout/Data/CoverLabel.ui new file mode 100644 index 00000000..2e7e0378 --- /dev/null +++ b/QFlowLayout/Data/CoverLabel.ui @@ -0,0 +1,109 @@ + + + CoverLabel + + + + 0 + 0 + 180 + 180 + + + + PointingHandCursor + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 135 + + + + + + + + #widgetBottom { + background-color: rgba(0, 0, 0, 150); +} + + + + + + + 16 + 16 + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 16 + 16 + + + + + + + + + + + + + + + diff --git a/QFlowLayout/Data/Svg_icon_headset_sm.svg b/QFlowLayout/Data/Svg_icon_headset_sm.svg new file mode 100644 index 00000000..98a1a359 --- /dev/null +++ b/QFlowLayout/Data/Svg_icon_headset_sm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QFlowLayout/Data/Svg_icon_loading.svg b/QFlowLayout/Data/Svg_icon_loading.svg new file mode 100644 index 00000000..0b00b799 --- /dev/null +++ b/QFlowLayout/Data/Svg_icon_loading.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/QFlowLayout/Data/Svg_icon_play_sm.svg b/QFlowLayout/Data/Svg_icon_play_sm.svg new file mode 100644 index 00000000..08a47f20 --- /dev/null +++ b/QFlowLayout/Data/Svg_icon_play_sm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QFlowLayout/HotPlaylist.py b/QFlowLayout/HotPlaylist.py index 2fef1672..6b276c84 100644 --- a/QFlowLayout/HotPlaylist.py +++ b/QFlowLayout/HotPlaylist.py @@ -1,194 +1,47 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' -Created on 2018年2月4日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 @email: 892768447@qq.com -@file: TencentMovieHotPlay_Flow -@description: -''' +@file: HotPlaylist.py +@description: +""" + import os import sys -import webbrowser - -from PyQt5.QtCore import QSize, Qt, QUrl, QTimer, pyqtSignal -from PyQt5.QtGui import QPainter, QFont, QLinearGradient, QGradient, QColor,\ - QBrush, QPaintEvent, QPixmap -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest -from PyQt5.QtSvg import QSvgWidget -from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QLabel,\ - QHBoxLayout, QSpacerItem, QSizePolicy, QScrollArea, QAbstractSlider +from Lib.CoverItemWidget import CoverItemWidget from Lib.flowlayout import FlowLayout # @UnresolvedImport from lxml.etree import HTML # @UnresolvedImport +try: + from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal + from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PyQt5.QtSvg import QSvgWidget + from PyQt5.QtWidgets import (QAbstractSlider, QApplication, QScrollArea, + QWidget) +except ImportError: + from PySide2.QtCore import Qt, QTimer, QUrl, pyqtSignal + from PySide2.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PySide2.QtSvg import QSvgWidget + from PySide2.QtWidgets import (QAbstractSlider, QApplication, QScrollArea, + QWidget) -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - -# offset=0,30,60,90 -Url = "http://v.qq.com/x/list/movie?pay=-1&offset={0}" +# offset=0,35,70,105 +Url = "https://music.163.com/discover/playlist/?order=hot&cat=%E5%85%A8%E9%83%A8&limit=35&offset={0}" -# 播放量图标 -Svg_icon_play_sm = ''' - - -'''.encode() +Agent = b"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50" -Svg_icon_loading = ''' - - - - - - - - - - - - - - - - - -'''.encode() +Referer = b"https://music.163.com/" -# 主演 +# 作者 Actor = '''{title} ''' -class CoverLabel(QLabel): - - def __init__(self, cover_path, cover_title, video_url, *args, **kwargs): - super(CoverLabel, self).__init__(*args, **kwargs) -# super(CoverLabel, self).__init__( -# ''.format(os.path.abspath(cover_path)), *args, **kwargs) - self.setCursor(Qt.PointingHandCursor) - self.setScaledContents(True) - self.setMinimumSize(220, 308) - self.setMaximumSize(220, 308) - self.cover_path = cover_path - self.cover_title = cover_title - self.video_url = video_url - self.setPixmap(QPixmap(cover_path)) - - def setCoverPath(self, path): - self.cover_path = path - - def mouseReleaseEvent(self, event): - super(CoverLabel, self).mouseReleaseEvent(event) - webbrowser.open_new_tab(self.video_url) - - def paintEvent(self, event): - super(CoverLabel, self).paintEvent(event) - if hasattr(self, "cover_title") and self.cover_title != "": - # 底部绘制文字 - painter = QPainter(self) - rect = self.rect() - # 粗略字体高度 - painter.save() - fheight = self.fontMetrics().height() - # 底部矩形框背景渐变颜色 - bottomRectColor = QLinearGradient( - rect.width() / 2, rect.height() - 24 - fheight, - rect.width() / 2, rect.height()) - bottomRectColor.setSpread(QGradient.PadSpread) - bottomRectColor.setColorAt(0, QColor(255, 255, 255, 70)) - bottomRectColor.setColorAt(1, QColor(0, 0, 0, 50)) - # 画半透明渐变矩形框 - painter.setPen(Qt.NoPen) - painter.setBrush(QBrush(bottomRectColor)) - painter.drawRect(rect.x(), rect.height() - 24 - - fheight, rect.width(), 24 + fheight) - painter.restore() - # 距离底部一定高度画文字 - font = self.font() or QFont() - font.setPointSize(8) - painter.setFont(font) - painter.setPen(Qt.white) - rect.setHeight(rect.height() - 12) # 底部减去一定高度 - painter.drawText(rect, Qt.AlignHCenter | - Qt.AlignBottom, self.cover_title) - - -class ItemWidget(QWidget): - - def __init__(self, cover_path, figure_info, figure_title, - figure_score, figure_desc, figure_count, video_url, cover_url, img_path, *args, **kwargs): - super(ItemWidget, self).__init__(*args, **kwargs) - self.setMaximumSize(220, 420) - self.setMaximumSize(220, 420) - self.img_path = img_path - self.cover_url = cover_url - layout = QVBoxLayout(self) - layout.setContentsMargins(10, 20, 10, 0) - # 图片label - self.clabel = CoverLabel(cover_path, figure_info, video_url, self) - layout.addWidget(self.clabel) - - # 片名和分数 - flayout = QHBoxLayout() - flayout.addWidget(QLabel(figure_title, self)) - flayout.addItem(QSpacerItem( - 20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) - flayout.addWidget(QLabel(figure_score, self, styleSheet="color: red;")) - layout.addLayout(flayout) - - # 主演 - layout.addWidget( - QLabel(figure_desc, self, styleSheet="color: #999999;", openExternalLinks=True)) - - # 播放量 - blayout = QHBoxLayout() - count_icon = QSvgWidget(self) - count_icon.setMaximumSize(16, 16) - count_icon.load(Svg_icon_play_sm) - blayout.addWidget(count_icon) - blayout.addWidget( - QLabel(figure_count, self, styleSheet="color: #999999;")) - layout.addLayout(blayout) - - def setCover(self, path): - self.clabel.setCoverPath(path) - self.clabel.setPixmap(QPixmap(path)) -# self.clabel.setText(''.format(os.path.abspath(path))) - - def sizeHint(self): - # 每个item控件的大小 - return QSize(220, 420) - - def event(self, event): - if isinstance(event, QPaintEvent): - if event.rect().height() > 20 and hasattr(self, "clabel"): - if self.clabel.cover_path.find("pic_v.png") > -1: # 封面未加载 - # print("start download img:", self.cover_url) - req = QNetworkRequest(QUrl(self.cover_url)) - # 设置两个自定义属性方便后期reply中处理 - req.setAttribute(QNetworkRequest.User + 1, self) - req.setAttribute(QNetworkRequest.User + 2, self.img_path) - self.parentWidget()._manager.get(req) # 调用父窗口中的下载器下载 - return super(ItemWidget, self).event(event) - - class GridWidget(QWidget): - Page = 0 loadStarted = pyqtSignal(bool) @@ -200,16 +53,18 @@ def __init__(self, *args, **kwargs): self._manager.finished.connect(self.onFinished) def load(self): - if self.Page == -1: + if self.Page == -1 or self.Page > 10: return self.loadStarted.emit(True) # 延迟一秒后调用目的在于显示进度条 QTimer.singleShot(1000, self._load) def _load(self): - print("load url:", Url.format(self.Page * 30)) - url = QUrl(Url.format(self.Page * 30)) - self._manager.get(QNetworkRequest(url)) + print("load url:", Url.format(self.Page * 35)) + url = QUrl(Url.format(self.Page * 35)) + req = QNetworkRequest(url) + req.setRawHeader(b"User-Agent", Agent) + self._manager.get(req) def onFinished(self, reply): # 请求完成后会调用该函数 @@ -229,11 +84,13 @@ def onFinished(self, reply): self.loadStarted.emit(False) def _parseHtml(self, html): + # print(html) # encoding = chardet.detect(html) or {} # html = html.decode(encoding.get("encoding","utf-8")) html = HTML(html) # 查找所有的li list_item - lis = html.xpath("//li[@class='list_item']") + lis = html.xpath("//ul[@id='m-pl-container']/li") + # print(lis) if not lis: self.Page = -1 # 后面没有页面了 return @@ -242,28 +99,30 @@ def _parseHtml(self, html): def _makeItem(self, lis): for li in lis: - a = li.find("a") - video_url = a.get("href") # 视频播放地址 - img = a.find("img") - cover_url = "http:" + img.get("r-lazyload") # 封面图片 - figure_title = img.get("alt") # 电影名 - figure_info = a.find("div/span") - figure_info = "" if figure_info is None else figure_info.text # 影片信息 - figure_score = "".join(li.xpath(".//em/text()")) # 评分 - # 主演 - figure_desc = "主演:" + \ - "".join([Actor.format(**dict(fd.items())) - for fd in li.xpath(".//div[@class='figure_desc']/a")]) + a = li.find('.//div/a') + play_url = "https://music.163.com/" + a.get("href") # 歌单播放地址 + img = li.find(".//div/img") + cover_url = img.get("src") # 封面图片 + playlist_title = a.get("title") # 歌单名 + # 歌手 + author_info = li.xpath(".//p[2]/a")[0] + playlist_author = "".format( + Actor.format(href="https://music.163.com/" + + author_info.get("href"), + title=author_info.get("title"))) # 播放数 - figure_count = ( - li.xpath(".//div[@class='figure_count']/span/text()") or [""])[0] + play_count = (li.xpath(".//div/div/span[2]/text()") or [""])[0] path = "cache/{0}.jpg".format( - os.path.splitext(os.path.basename(video_url))[0]) + os.path.splitext(os.path.basename(cover_url).split('?')[0])[0]) cover_path = "Data/pic_v.png" if os.path.isfile(path): cover_path = path - iwidget = ItemWidget(cover_path, figure_info, figure_title, - figure_score, figure_desc, figure_count, video_url, cover_url, path, self) + + # print(cover_path, playlist_title, + # playlist_author, play_count, play_url, cover_url, path) + iwidget = CoverItemWidget(self, manager=self._manager) + iwidget.init(cover_path, playlist_title, playlist_author, + play_count, play_url, cover_url, path) self._layout.addWidget(iwidget) @@ -283,9 +142,11 @@ def __init__(self, *args, **kwargs): # 连接竖着的滚动条滚动事件 self.verticalScrollBar().actionTriggered.connect(self.onActionTriggered) # 进度条 - self.loadWidget = QSvgWidget( - self, minimumHeight=120, minimumWidth=120, visible=False) - self.loadWidget.load(Svg_icon_loading) + self.loadWidget = QSvgWidget(self, + minimumHeight=120, + minimumWidth=120, + visible=False) + self.loadWidget.load('Data/Svg_icon_loading.svg') def setLoadStarted(self, started): self._loadStart = started @@ -297,7 +158,8 @@ def onActionTriggered(self, action): if action != QAbstractSlider.SliderMove or self._loadStart: return # 使用sliderPosition获取值可以同时满足鼠标滑动和拖动判断 - if self.verticalScrollBar().sliderPosition() == self.verticalScrollBar().maximum(): + if self.verticalScrollBar().sliderPosition() == self.verticalScrollBar( + ).maximum(): # 可以下一页了 self._widget.load() @@ -306,9 +168,7 @@ def resizeEvent(self, event): self.loadWidget.setGeometry( int((self.width() - self.loadWidget.minimumWidth()) / 2), int((self.height() - self.loadWidget.minimumHeight()) / 2), - self.loadWidget.minimumWidth(), - self.loadWidget.minimumHeight() - ) + self.loadWidget.minimumWidth(), self.loadWidget.minimumHeight()) if __name__ == "__main__": diff --git a/QFlowLayout/Lib/CoverItemWidget.py b/QFlowLayout/Lib/CoverItemWidget.py new file mode 100644 index 00000000..5ef3ff00 --- /dev/null +++ b/QFlowLayout/Lib/CoverItemWidget.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CoverItemWidget.py +@description: +""" + +try: + from PyQt5.QtCore import QSize, QUrl + from PyQt5.QtGui import QPaintEvent, QPixmap + from PyQt5.QtNetwork import QNetworkRequest + from PyQt5.QtWidgets import QWidget +except ImportError: + from PySide2.QtCore import QSize, QUrl + from PySide2.QtGui import QPaintEvent, QPixmap + from PySide2.QtNetwork import QNetworkRequest + from PySide2.QtWidgets import QWidget + +from .Ui_CoverItemWidget import Ui_CoverItemWidget # @UnresolvedImport + + +class CoverItemWidget(QWidget, Ui_CoverItemWidget): + + def __init__(self, *args, **kwargs): + self._manager = kwargs.pop('manager', None) + super(CoverItemWidget, self).__init__(*args, **kwargs) + self.setupUi(self) + + def init(self, cover_path, playlist_title, playlist_author, play_count, + play_url, cover_url, img_path): + self.img_path = img_path + self.cover_url = cover_url + # 图片label + self.labelCover.init(cover_path, play_url, play_count) + + # 歌单 + self.labelTitle.setText(playlist_title) + + # 作者 + self.labelAuthor.setText(playlist_author) + + def setCover(self, path): + self.labelCover.setCoverPath(path) + self.labelCover.setPixmap(QPixmap(path)) + + def sizeHint(self): + # 每个item控件的大小 + return QSize(200, 256) + + def event(self, event): + if isinstance(event, QPaintEvent): + if event.rect().height() > 20 and hasattr(self, "labelCover"): + if self.labelCover.cover_path.find("pic_v.png") > -1: # 封面未加载 + # print("start download img:", self.cover_url) + req = QNetworkRequest(QUrl(self.cover_url)) + # 设置两个自定义属性方便后期reply中处理 + req.setAttribute(QNetworkRequest.User + 1, self) + req.setAttribute(QNetworkRequest.User + 2, self.img_path) + if self._manager: + self._manager.get(req) # 调用父窗口中的下载器下载 + return super(CoverItemWidget, self).event(event) diff --git a/QFlowLayout/Lib/CoverLabel.py b/QFlowLayout/Lib/CoverLabel.py new file mode 100644 index 00000000..dbee15af --- /dev/null +++ b/QFlowLayout/Lib/CoverLabel.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CoverLabel.py +@description: +""" +import webbrowser + +try: + from PyQt5.QtGui import QPixmap + from PyQt5.QtWidgets import QLabel +except ImportError: + from PySide2.QtGui import QPixmap + from PySide2.QtWidgets import QLabel + +from .Ui_CoverLabel import Ui_CoverLabel # @UnresolvedImport + + +class CoverLabel(QLabel, Ui_CoverLabel): + + def __init__(self, *args, **kwargs): + super(CoverLabel, self).__init__(*args, **kwargs) + self.setupUi(self) + + def init(self, cover_path, play_url, play_count): + self.cover_path = cover_path + self.play_url = play_url + self.setPixmap(QPixmap(cover_path)) + self.labelHeadset.setPixmap(QPixmap('Data/Svg_icon_headset_sm.svg')) + self.labelPlay.setPixmap(QPixmap('Data/Svg_icon_play_sm.svg')) + self.labelCount.setStyleSheet('color: #999999;') + self.labelCount.setText(play_count) + + def setCoverPath(self, path): + self.cover_path = path + + def mouseReleaseEvent(self, event): + super(CoverLabel, self).mouseReleaseEvent(event) + webbrowser.open_new_tab(self.play_url) diff --git a/QFlowLayout/Lib/Ui_CoverItemWidget.py b/QFlowLayout/Lib/Ui_CoverItemWidget.py new file mode 100644 index 00000000..c048f686 --- /dev/null +++ b/QFlowLayout/Lib/Ui_CoverItemWidget.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CoverItemWidget.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# 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_CoverItemWidget(object): + def setupUi(self, CoverItemWidget): + CoverItemWidget.setObjectName("CoverItemWidget") + CoverItemWidget.setMinimumSize(QtCore.QSize(200, 256)) + CoverItemWidget.setMaximumSize(QtCore.QSize(200, 256)) + CoverItemWidget.setWindowTitle("") + self.verticalLayout = QtWidgets.QVBoxLayout(CoverItemWidget) + self.verticalLayout.setContentsMargins(10, 10, 10, 10) + self.verticalLayout.setSpacing(12) + self.verticalLayout.setObjectName("verticalLayout") + self.labelCover = CoverLabel(CoverItemWidget) + self.labelCover.setMinimumSize(QtCore.QSize(180, 180)) + self.labelCover.setText("") + self.labelCover.setScaledContents(True) + self.labelCover.setAlignment(QtCore.Qt.AlignCenter) + self.labelCover.setObjectName("labelCover") + self.verticalLayout.addWidget(self.labelCover) + self.labelTitle = QtWidgets.QLabel(CoverItemWidget) + font = QtGui.QFont() + font.setPointSize(10) + self.labelTitle.setFont(font) + self.labelTitle.setText("") + self.labelTitle.setObjectName("labelTitle") + self.verticalLayout.addWidget(self.labelTitle) + self.labelAuthor = QtWidgets.QLabel(CoverItemWidget) + self.labelAuthor.setText("") + self.labelAuthor.setObjectName("labelAuthor") + self.verticalLayout.addWidget(self.labelAuthor) + + self.retranslateUi(CoverItemWidget) + QtCore.QMetaObject.connectSlotsByName(CoverItemWidget) + + def retranslateUi(self, CoverItemWidget): + pass +from .CoverLabel import CoverLabel + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + CoverItemWidget = QtWidgets.QWidget() + ui = Ui_CoverItemWidget() + ui.setupUi(CoverItemWidget) + CoverItemWidget.show() + sys.exit(app.exec_()) diff --git a/QFlowLayout/Lib/Ui_CoverLabel.py b/QFlowLayout/Lib/Ui_CoverLabel.py new file mode 100644 index 00000000..86e1526a --- /dev/null +++ b/QFlowLayout/Lib/Ui_CoverLabel.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CoverLabel.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# 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_CoverLabel(object): + def setupUi(self, CoverLabel): + CoverLabel.setObjectName("CoverLabel") + CoverLabel.resize(180, 180) + CoverLabel.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + CoverLabel.setWindowTitle("") + self.verticalLayout = QtWidgets.QVBoxLayout(CoverLabel) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName("verticalLayout") + spacerItem = QtWidgets.QSpacerItem(20, 135, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.widgetBottom = QtWidgets.QWidget(CoverLabel) + self.widgetBottom.setStyleSheet("#widgetBottom {\n" +" background-color: rgba(0, 0, 0, 150);\n" +"}") + self.widgetBottom.setObjectName("widgetBottom") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widgetBottom) + self.horizontalLayout.setObjectName("horizontalLayout") + self.labelHeadset = QtWidgets.QLabel(self.widgetBottom) + self.labelHeadset.setMinimumSize(QtCore.QSize(16, 16)) + self.labelHeadset.setText("") + self.labelHeadset.setObjectName("labelHeadset") + self.horizontalLayout.addWidget(self.labelHeadset) + self.labelCount = QtWidgets.QLabel(self.widgetBottom) + self.labelCount.setText("") + self.labelCount.setObjectName("labelCount") + self.horizontalLayout.addWidget(self.labelCount) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.labelPlay = QtWidgets.QLabel(self.widgetBottom) + self.labelPlay.setMinimumSize(QtCore.QSize(16, 16)) + self.labelPlay.setText("") + self.labelPlay.setObjectName("labelPlay") + self.horizontalLayout.addWidget(self.labelPlay) + self.verticalLayout.addWidget(self.widgetBottom) + + self.retranslateUi(CoverLabel) + QtCore.QMetaObject.connectSlotsByName(CoverLabel) + + def retranslateUi(self, CoverLabel): + pass + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + CoverLabel = QtWidgets.QWidget() + ui = Ui_CoverLabel() + ui.setupUi(CoverLabel) + CoverLabel.show() + sys.exit(app.exec_()) diff --git a/QFlowLayout/Lib/flowlayout.py b/QFlowLayout/Lib/flowlayout.py index ef1d54e3..3cc9a9a3 100644 --- a/QFlowLayout/Lib/flowlayout.py +++ b/QFlowLayout/Lib/flowlayout.py @@ -41,10 +41,14 @@ ## ############################################################################# - -from PyQt5.QtCore import QPoint, QRect, QSize, Qt -from PyQt5.QtWidgets import (QApplication, QLayout, QPushButton, QSizePolicy, - QWidget) +try: + from PyQt5.QtCore import QPoint, QRect, QSize, Qt + from PyQt5.QtWidgets import (QApplication, QLayout, QPushButton, + QSizePolicy, QWidget) +except ImportError: + from PySide2.QtCore import QPoint, QRect, QSize, Qt + from PySide2.QtWidgets import (QApplication, QLayout, QPushButton, + QSizePolicy, QWidget) class Window(QWidget): @@ -152,7 +156,6 @@ def doLayout(self, rect, testOnly): if __name__ == '__main__': - import sys app = QApplication(sys.argv) diff --git a/QFlowLayout/README.md b/QFlowLayout/README.md index dfd85619..21868ec8 100644 --- a/QFlowLayout/README.md +++ b/QFlowLayout/README.md @@ -1,21 +1,22 @@ # QListView - 目录 - - [腾讯视频热播列表](#1腾讯视频热播列表) + - [音乐热歌列表](#1音乐热歌列表) + +## 1、音乐热歌列表 -## 1、腾讯视频热播列表 [运行 HotPlaylist.py](HotPlaylist.py) 简单思路说明: - - 利用`QScrollArea`滚动显示,自定义的`QFlowLayout`做布局来放置自定义的Widget - - `QNetworkAccessManager`异步下载网页和图片 - - `QScrollArea`滚动到底部触发下一页加载 +- 利用`QScrollArea`滚动显示,自定义的`QFlowLayout`做布局来放置自定义的Widget +- `QNetworkAccessManager`异步下载网页和图片 +- `QScrollArea`滚动到底部触发下一页加载 自定义控件说明: - - 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 - - 字体颜色用qss设置 - - 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) +- 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 +- 字体颜色用qss设置 +- 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) -![HotPlaylist](ScreenShot/HotPlaylist.gif) \ No newline at end of file +![HotPlaylist](ScreenShot/HotPlaylist.gif) diff --git a/QFlowLayout/testxpath.py b/QFlowLayout/testxpath.py new file mode 100644 index 00000000..e167f231 --- /dev/null +++ b/QFlowLayout/testxpath.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/21 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: testxpath.py +@description: +""" + +import os + +from lxml.etree import HTML # @UnresolvedImport + +# 作者 +Actor = '''{title} ''' + + +def _makeItem(lis): + for li in lis: + a = li.find('.//div/a') + play_url = "https://music.163.com/" + a.get("href") # 视频播放地址 + img = li.find(".//div/img") + cover_url = img.get("src") # 封面图片 + playlist_title = a.get("title") # 歌单名 + # figure_info = a.find("div/span") + figure_info = "aaa" #if figure_info is None else figure_info.text # 影片信息 + figure_score = "" # 评分 + # 歌手 + figure = li.xpath(".//p[2]/a")[0] + playlist_author = "".format( + Actor.format(href="https://music.163.com/" +figure.get("href"),title=figure.get("title"))) + # 播放数 + play_count = (li.xpath(".//div/div/span[2]/text()") or [""])[0] + path = "cache/{0}.jpg".format( + os.path.splitext(os.path.basename(cover_url).split('?')[0])[0]) + cover_path = "Data/pic_v.png" + if os.path.isfile(path): + cover_path = path + + print(cover_path, playlist_title, playlist_author, + play_count, play_url, cover_url, path) + # iwidget = ItemWidget(cover_path, playlist_title, + # playlist_author, play_count, play_url, + # cover_url, path, self) + # self._layout.addWidget(iwidget) + + +def _parseHtml(html): + html = HTML(html) + # 查找所有的li + lis = html.xpath("//ul[@id='m-pl-container']/li") + # if not lis: + # self.Page = -1 # 后面没有页面了 + # return + # self.Page += 1 + _makeItem(lis) + + +if __name__ == '__main__': + data = open(r'D:\Computer\Desktop\163.html', 'rb').read() + _parseHtml(data) \ No newline at end of file diff --git a/QFont/AwesomeFont.py b/QFont/AwesomeFont.py index 7d8098de..58b1baf2 100644 --- a/QFont/AwesomeFont.py +++ b/QFont/AwesomeFont.py @@ -1,24 +1,25 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年3月30日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: AwesomeFont @description: -''' - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +""" import sys -from PyQt5.QtGui import QFontDatabase, QFont -from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout,\ - QScrollArea, QPushButton +try: + from PyQt5.QtGui import QFontDatabase, QFont + from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, \ + QScrollArea, QPushButton +except ImportError: + from PySide2.QtGui import QFontDatabase, QFont + from PySide2.QtWidgets import QApplication, QWidget, QGridLayout, \ + QScrollArea, QPushButton from Lib.FontAwesome import FontAwesomes # @UnresolvedImport @@ -44,7 +45,7 @@ def __init__(self): minimumHeight=33, font=QFont("FontAwesome", 14)), row, col, 1, 1) - + self.showMaximized() def resizeEvent(self, event): diff --git "a/QFont/Data/\346\217\220\345\217\226\345\255\227\347\254\246/get.py" "b/QFont/Data/\346\217\220\345\217\226\345\255\227\347\254\246/get.py" index 624999c6..216d2e35 100644 --- "a/QFont/Data/\346\217\220\345\217\226\345\255\227\347\254\246/get.py" +++ "b/QFont/Data/\346\217\220\345\217\226\345\255\227\347\254\246/get.py" @@ -1,5 +1,6 @@ # from bs4 import BeautifulSoup import re + cheatsheet = open("cheatsheet.txt", "rb").read().decode() re_fa = re.compile(" fa(.*)") diff --git a/QFont/Lib/FontAwesome.py b/QFont/Lib/FontAwesome.py index 60ca9cae..3527c2ea 100644 --- a/QFont/Lib/FontAwesome.py +++ b/QFont/Lib/FontAwesome.py @@ -1,21 +1,22 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年3月30日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: FontAwesome @description: -''' +""" + + # FontAwesome 版本: 4.7.0 # 字体图标地址: http://fontawesome.io/icons/ # 字体字符地址: http://fontawesome.io/cheatsheet/ class FontAwesomes: - FA = None @classmethod diff --git a/QFont/README.md b/QFont/README.md index 21a12969..28d0cd86 100644 --- a/QFont/README.md +++ b/QFont/README.md @@ -4,8 +4,9 @@ - [加载自定义字体](#1加载自定义字体) ## 1、加载自定义字体 + [运行 AwesomeFont.py](AwesomeFont.py) 通过`QFontDatabase.addApplicationFont`加载字体文件 -![AwesomeFont](ScreenShot/AwesomeFont.png) \ No newline at end of file +![AwesomeFont](ScreenShot/AwesomeFont.png) diff --git a/QGraphicsDropShadowEffect/Lib/AnimationShadowEffect.py b/QGraphicsDropShadowEffect/Lib/AnimationShadowEffect.py index 9cbbd759..4551bd4f 100644 --- a/QGraphicsDropShadowEffect/Lib/AnimationShadowEffect.py +++ b/QGraphicsDropShadowEffect/Lib/AnimationShadowEffect.py @@ -4,20 +4,18 @@ """ Created on 2018年9月25日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: AnimationShadowEffect @description: 边框动画阴影动画 """ -from PyQt5.QtCore import QPropertyAnimation, pyqtProperty -from PyQt5.QtWidgets import QGraphicsDropShadowEffect - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QPropertyAnimation, pyqtProperty + from PyQt5.QtWidgets import QGraphicsDropShadowEffect +except ImportError: + from PySide2.QtCore import QPropertyAnimation, Property as pyqtProperty + from PySide2.QtWidgets import QGraphicsDropShadowEffect class AnimationShadowEffect(QGraphicsDropShadowEffect): diff --git a/QGraphicsDropShadowEffect/README.md b/QGraphicsDropShadowEffect/README.md index 6e5be6fb..20d5d9d3 100644 --- a/QGraphicsDropShadowEffect/README.md +++ b/QGraphicsDropShadowEffect/README.md @@ -4,6 +4,7 @@ - [边框阴影动画](#1边框阴影动画) ## 1、边框阴影动画 + [运行 ShadowEffect.py](ShadowEffect.py) 1. 通过`setGraphicsEffect`设置控件的边框阴影 @@ -11,4 +12,4 @@ 3. 通过`QPropertyAnimation`属性动画不断改变`radius`的值并调用`setBlurRadius`更新半径值 4. 不能对父控件使用 -![ShadowEffect](ScreenShot/ShadowEffect.gif) \ No newline at end of file +![ShadowEffect](ScreenShot/ShadowEffect.gif) diff --git a/QGraphicsDropShadowEffect/ShadowEffect.py b/QGraphicsDropShadowEffect/ShadowEffect.py index 9c03cc6e..b121e852 100644 --- a/QGraphicsDropShadowEffect/ShadowEffect.py +++ b/QGraphicsDropShadowEffect/ShadowEffect.py @@ -4,23 +4,22 @@ """ Created on 2018年9月25日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ShadowEffect @description: """ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton, QLineEdit - -from Lib.AnimationShadowEffect import AnimationShadowEffect # @UnresolvedImport +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPixmap + from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton, QLineEdit, QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPixmap + from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton, QLineEdit, QApplication -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +from Lib.AnimationShadowEffect import AnimationShadowEffect # @UnresolvedImport class Window(QWidget): @@ -64,7 +63,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QGraphicsView/AddQWidget.py b/QGraphicsView/AddQWidget.py index 1689972c..ff8026c9 100644 --- a/QGraphicsView/AddQWidget.py +++ b/QGraphicsView/AddQWidget.py @@ -3,22 +3,22 @@ """ Created on 2017年12月23日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: AddQWidget @description: """ import sys -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QVBoxLayout,\ - QApplication, QGraphicsView, QGraphicsScene - - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QVBoxLayout, \ + QApplication, QGraphicsView, QGraphicsScene +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QVBoxLayout, \ + QApplication, QGraphicsView, QGraphicsScene class ToolTipItem(QWidget): diff --git a/QGraphicsView/Data/bg.jpg b/QGraphicsView/Data/bg.jpg new file mode 100644 index 00000000..db164572 Binary files /dev/null and b/QGraphicsView/Data/bg.jpg differ diff --git a/QGraphicsView/Data/icons/basic.png b/QGraphicsView/Data/icons/basic.png new file mode 100644 index 00000000..7627b244 Binary files /dev/null and b/QGraphicsView/Data/icons/basic.png differ diff --git a/QGraphicsView/Data/icons/business.png b/QGraphicsView/Data/icons/business.png new file mode 100644 index 00000000..479d2e0d Binary files /dev/null and b/QGraphicsView/Data/icons/business.png differ diff --git a/QGraphicsView/Data/icons/cloudService.png b/QGraphicsView/Data/icons/cloudService.png new file mode 100644 index 00000000..3c3579f1 Binary files /dev/null and b/QGraphicsView/Data/icons/cloudService.png differ diff --git a/QGraphicsView/Data/icons/dataBase.png b/QGraphicsView/Data/icons/dataBase.png new file mode 100644 index 00000000..716e0cff Binary files /dev/null and b/QGraphicsView/Data/icons/dataBase.png differ diff --git a/QGraphicsView/Data/icons/engineering.png b/QGraphicsView/Data/icons/engineering.png new file mode 100644 index 00000000..4c68bd45 Binary files /dev/null and b/QGraphicsView/Data/icons/engineering.png differ diff --git a/QGraphicsView/Data/icons/network.png b/QGraphicsView/Data/icons/network.png new file mode 100644 index 00000000..dde507a2 Binary files /dev/null and b/QGraphicsView/Data/icons/network.png differ diff --git a/QGraphicsView/Data/icons/science.png b/QGraphicsView/Data/icons/science.png new file mode 100644 index 00000000..01c37711 Binary files /dev/null and b/QGraphicsView/Data/icons/science.png differ diff --git a/QGraphicsView/Data/icons/softwareEngineering.png b/QGraphicsView/Data/icons/softwareEngineering.png new file mode 100644 index 00000000..51be3221 Binary files /dev/null and b/QGraphicsView/Data/icons/softwareEngineering.png differ diff --git a/QGraphicsView/Data/icons/wireframe.png b/QGraphicsView/Data/icons/wireframe.png new file mode 100644 index 00000000..040e0f42 Binary files /dev/null and b/QGraphicsView/Data/icons/wireframe.png differ diff --git a/QGraphicsView/DragGraphics.py b/QGraphicsView/DragGraphics.py new file mode 100644 index 00000000..2ba56060 --- /dev/null +++ b/QGraphicsView/DragGraphics.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/09 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: DragGraphics.py +@description: +""" + +import json +import os + +try: + from PyQt5.QtCore import QMimeData, Qt + from PyQt5.QtGui import QDrag, QIcon, QPixmap + from PyQt5.QtWidgets import (QApplication, QGraphicsPixmapItem, + QGraphicsScene, QGraphicsView, QHBoxLayout, + QListWidget, QListWidgetItem, QTreeWidget, + QTreeWidgetItem, QWidget) +except ImportError: + from PySide2.QtCore import QMimeData, Qt + from PySide2.QtGui import QDrag, QIcon, QPixmap + from PySide2.QtWidgets import (QApplication, QGraphicsPixmapItem, + QGraphicsScene, QGraphicsView, QHBoxLayout, + QListWidget, QListWidgetItem, QTreeWidget, + QTreeWidgetItem, QWidget) + + +class ListWidget(QListWidget): + + def __init__(self, *args, **kwargs): + super(ListWidget, self).__init__(*args, **kwargs) + self.setDragEnabled(True) + self.setDragDropMode(QListWidget.DragOnly) + self.setDefaultDropAction(Qt.IgnoreAction) + self.setEditTriggers(QListWidget.NoEditTriggers) + self.setResizeMode(QListWidget.Adjust) + self.setViewMode(QListWidget.IconMode) + + def startDrag(self, supportedActions): + items = self.selectedItems() + if not items: + return + # 这里就简单的根据名字提示来传递数据了,实际上可以传递任意数据 + data = QMimeData() + data.setData('application/node-items', + json.dumps([item.toolTip() for item in items]).encode()) + # 这里简单显示第一个缩略图 + pixmap = items[0].icon().pixmap(36, 36) + drag = QDrag(self) + drag.setMimeData(data) + drag.setPixmap(pixmap) + drag.setHotSpot(pixmap.rect().center()) + drag.exec_(supportedActions) + + +class GraphicsView(QGraphicsView): + + def __init__(self, *args, **kwargs): + super(GraphicsView, self).__init__(*args, **kwargs) + self.setAcceptDrops(True) + self._scene = QGraphicsScene(self) # 场景 + self.setScene(self._scene) + + def dragEnterEvent(self, event): + """判断拖入的数据是否支持""" + mimeData = event.mimeData() + if not mimeData.hasFormat('application/node-items'): + event.ignore() + return + + event.acceptProposedAction() + + dragMoveEvent = dragEnterEvent + + def dropEvent(self, event): + """获取拖拽的数据并绘制对于的图形""" + datas = event.mimeData().data('application/node-items') + datas = json.loads(datas.data().decode()) + print('datas:', datas) + + path = os.path.join(os.path.dirname(__file__), 'Data/icons') + for name in datas: + item = QGraphicsPixmapItem(QPixmap(os.path.join(path, name))) + item.setFlags(QGraphicsPixmapItem.ItemIsFocusable | + QGraphicsPixmapItem.ItemIsMovable) + self._scene.addItem(item) + pos = self.mapToScene(event.pos()) + item.moveBy(pos.x(), pos.y()) + + +class DragGraphics(QWidget): + + def __init__(self, *args, **kwargs): + super(DragGraphics, self).__init__(*args, **kwargs) + self.resize(800, 600) + layout = QHBoxLayout(self) + + # 左侧树形控制 + self.treeWidget = QTreeWidget(self) + self.treeWidget.header().setVisible(False) + self.treeWidget.setMaximumWidth(300) + layout.addWidget(self.treeWidget) + + # 右侧图形显示 + self.graphicsView = GraphicsView(self) + layout.addWidget(self.graphicsView) + + self._init_trees() + + def _init_trees(self): + """初始化树形控件中的图形节点列表""" + # 1. 获取所有图标 + path = os.path.join(os.path.dirname(__file__), 'Data/icons') + icons = [os.path.join(path, name) for name in os.listdir(path)] + + # 2. 添加根节点 + for i in range(2): + item = QTreeWidgetItem(self.treeWidget) + item.setText(0, 'View %d' % i) + + # 3. 添加子节点作为容器用于存放图标 + itemc = QTreeWidgetItem(item) + child = ListWidget(self.treeWidget) + self.treeWidget.setItemWidget(itemc, 0, child) + + # 4. 添加图标 + for icon in icons: + item = QListWidgetItem(child) + item.setIcon(QIcon(icon)) + item.setToolTip(os.path.basename(icon)) + + self.treeWidget.expandAll() + + +if __name__ == '__main__': + import cgitb + import sys + + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = DragGraphics() + w.show() + sys.exit(app.exec_()) diff --git a/QGraphicsView/ImageView.py b/QGraphicsView/ImageView.py new file mode 100644 index 00000000..f6789c54 --- /dev/null +++ b/QGraphicsView/ImageView.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/11/12 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ImageView +@description: 图片查看控件,支持移动、放大、缩小 +""" + +import os + +try: + from PyQt5.QtCore import QPointF, Qt, QRectF, QSizeF + from PyQt5.QtGui import QPainter, QColor, QImage, QPixmap + from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsPixmapItem, QGraphicsScene +except ImportError: + from PySide2.QtCore import QPointF, Qt, QRectF, QSizeF + from PySide2.QtGui import QPainter, QColor, QImage, QPixmap + from PySide2.QtWidgets import QApplication, QGraphicsView, QGraphicsPixmapItem, QGraphicsScene + + +class ImageView(QGraphicsView): + """图片查看控件""" + + def __init__(self, *args, **kwargs): + image = kwargs.pop('image', None) + background = kwargs.pop('background', None) + super(ImageView, self).__init__(*args, **kwargs) + self.setCursor(Qt.OpenHandCursor) + self.setBackground(background) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | + QPainter.SmoothPixmapTransform) + self.setCacheMode(self.CacheBackground) + self.setViewportUpdateMode(self.SmartViewportUpdate) + self._item = QGraphicsPixmapItem() # 放置图像 + self._item.setFlags(QGraphicsPixmapItem.ItemIsFocusable | + QGraphicsPixmapItem.ItemIsMovable) + self._scene = QGraphicsScene(self) # 场景 + self.setScene(self._scene) + self._scene.addItem(self._item) + rect = QApplication.instance().desktop().availableGeometry(self) + self.resize(int(rect.width() * 2 / 3), int(rect.height() * 2 / 3)) + + self.pixmap = None + self._delta = 0.1 # 缩放 + self.setPixmap(image) + + def setBackground(self, color): + """设置背景颜色 + :param color: 背景颜色 + :type color: QColor or str or GlobalColor + """ + if isinstance(color, QColor): + self.setBackgroundBrush(color) + elif isinstance(color, (str, Qt.GlobalColor)): + color = QColor(color) + if color.isValid(): + self.setBackgroundBrush(color) + + def setPixmap(self, pixmap, fitIn=True): + """加载图片 + :param pixmap: 图片或者图片路径 + :param fitIn: 是否适应 + :type pixmap: QPixmap or QImage or str + :type fitIn: bool + """ + if isinstance(pixmap, QPixmap): + self.pixmap = pixmap + elif isinstance(pixmap, QImage): + self.pixmap = QPixmap.fromImage(pixmap) + elif isinstance(pixmap, str) and os.path.isfile(pixmap): + self.pixmap = QPixmap(pixmap) + else: + return + self._item.setPixmap(self.pixmap) + self._item.update() + self.setSceneDims() + if fitIn: + self.fitInView(QRectF(self._item.pos(), QSizeF( + self.pixmap.size())), Qt.KeepAspectRatio) + self.update() + + def setSceneDims(self): + if not self.pixmap: + return + self.setSceneRect(QRectF(QPointF(0, 0), QPointF(self.pixmap.width(), self.pixmap.height()))) + + def fitInView(self, rect, flags=Qt.IgnoreAspectRatio): + """剧中适应 + :param rect: 矩形范围 + :param flags: + :return: + """ + if not self.scene() or rect.isNull(): + return + unity = self.transform().mapRect(QRectF(0, 0, 1, 1)) + self.scale(1 / unity.width(), 1 / unity.height()) + viewRect = self.viewport().rect() + sceneRect = self.transform().mapRect(rect) + x_ratio = viewRect.width() / sceneRect.width() + y_ratio = viewRect.height() / sceneRect.height() + if flags == Qt.KeepAspectRatio: + x_ratio = y_ratio = min(x_ratio, y_ratio) + elif flags == Qt.KeepAspectRatioByExpanding: + x_ratio = y_ratio = max(x_ratio, y_ratio) + self.scale(x_ratio, y_ratio) + self.centerOn(rect.center()) + + def wheelEvent(self, event): + if event.angleDelta().y() > 0: + self.zoomIn() + else: + self.zoomOut() + + def zoomIn(self): + """放大""" + self.zoom(1 + self._delta) + + def zoomOut(self): + """缩小""" + self.zoom(1 - self._delta) + + def zoom(self, factor): + """缩放 + :param factor: 缩放的比例因子 + """ + _factor = self.transform().scale( + factor, factor).mapRect(QRectF(0, 0, 1, 1)).width() + if _factor < 0.07 or _factor > 100: + # 防止过大过小 + return + self.scale(factor, factor) + + +if __name__ == '__main__': + import sys + import cgitb + + cgitb.enable(format='text') + + app = QApplication(sys.argv) + w = ImageView(image='Data/bg.jpg', background=Qt.black) + w.show() + sys.exit(app.exec_()) diff --git a/QGraphicsView/README.md b/QGraphicsView/README.md index 733d782b..5db21ca5 100644 --- a/QGraphicsView/README.md +++ b/QGraphicsView/README.md @@ -3,8 +3,11 @@ - 目录 - [绘制世界地图](#1绘制世界地图) - [添加QWidget](#2添加QWidget) + - [图片查看器](#3图片查看器) + - [图标拖拽](#4图标拖拽) ## 1、绘制世界地图 + [运行 WorldMap.py](WorldMap.py) 1. 解析json数据生成 `QPolygonF` @@ -13,8 +16,28 @@ ![WorldMap](ScreenShot/WorldMap.gif) ## 2、添加QWidget + [运行 AddQWidget.py](AddQWidget.py) 通过 `QGraphicsScene.addWidget` 添加自定义QWidget -![AddQWidget](ScreenShot/AddQWidget.png) \ No newline at end of file +![AddQWidget](ScreenShot/AddQWidget.png) + +## 3、图片查看器 + +[运行 ImageView.py](ImageView.py) + +支持放大缩小和移动 + +![ImageView](ScreenShot/ImageView.gif) + +## 3、图标拖拽 + +[运行 DragGraphics.py](DragGraphics.py) + +该示例主要是包含左侧树状图标列表和右侧视图显示,从左侧拖拽到右侧 + +1. 重写`QListWidget`的`startDrag`函数用来封装拖拽数据 +2. 重写`QGraphicsView`的`dragEnterEvent`、`dragMoveEvent`、`dropEvent`函数用来处理拖拽事件 + +![DragGraphics](ScreenShot/DragGraphics.gif) diff --git a/QGraphicsView/ScreenShot/DragGraphics.gif b/QGraphicsView/ScreenShot/DragGraphics.gif new file mode 100644 index 00000000..65f0c4ba Binary files /dev/null and b/QGraphicsView/ScreenShot/DragGraphics.gif differ diff --git a/QGraphicsView/ScreenShot/ImageView.gif b/QGraphicsView/ScreenShot/ImageView.gif new file mode 100644 index 00000000..150634c2 Binary files /dev/null and b/QGraphicsView/ScreenShot/ImageView.gif differ diff --git a/QGraphicsView/WorldMap.py b/QGraphicsView/WorldMap.py index 3efc7b17..32428dfb 100644 --- a/QGraphicsView/WorldMap.py +++ b/QGraphicsView/WorldMap.py @@ -3,8 +3,8 @@ """ Created on 2017年12月17日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: WorldMap @description: @@ -12,19 +12,19 @@ import json import math -from PyQt5.QtCore import Qt, QPointF, QRectF -from PyQt5.QtGui import QColor, QPainter, QPolygonF, QPen, QBrush -from PyQt5.QtOpenGL import QGLFormat -from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPolygonItem - - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt, QPointF, QRectF + from PyQt5.QtGui import QColor, QPainter, QPolygonF, QPen, QBrush + from PyQt5.QtOpenGL import QGLFormat + from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsPolygonItem +except ImportError: + from PySide2.QtCore import Qt, QPointF, QRectF + from PySide2.QtGui import QColor, QPainter, QPolygonF, QPen, QBrush + from PySide2.QtOpenGL import QGLFormat + from PySide2.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsPolygonItem class GraphicsView(QGraphicsView): - # 背景区域颜色 backgroundColor = QColor(31, 31, 47) # 边框颜色 @@ -94,8 +94,8 @@ def __init__(self, *args, **kwargs): AnchorUnderMouse 鼠标当前位置被用作锚点 ''' self.setTransformationAnchor(self.AnchorUnderMouse) -# if QGLFormat.hasOpenGL(): # 如果开启了OpenGL则使用OpenGL Widget -# self.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers))) + # if QGLFormat.hasOpenGL(): # 如果开启了OpenGL则使用OpenGL Widget + # self.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers))) ''' #参考 http://doc.qt.io/qt-5/qgraphicsview.html#ViewportUpdateMode-enum FullViewportUpdate 当场景的任何可见部分改变或重新显示时,QGraphicsView将更新整个视口。 当QGraphicsView花费更多的时间来计算绘制的内容(比如重复更新很多小项目)时,这种方法是最快的。 这是不支持部分更新(如QGLWidget)的视口以及需要禁用滚动优化的视口的首选更新模式。 @@ -157,7 +157,7 @@ def initMap(self): if __name__ == "__main__": import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) print("OpenGL Status:", QGLFormat.hasOpenGL()) view = GraphicsView() diff --git a/QGridLayout/Data/CoverItemWidget.ui b/QGridLayout/Data/CoverItemWidget.ui new file mode 100644 index 00000000..2f245e13 --- /dev/null +++ b/QGridLayout/Data/CoverItemWidget.ui @@ -0,0 +1,85 @@ + + + CoverItemWidget + + + + 200 + 256 + + + + + 200 + 256 + + + + + + + + 12 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + 180 + 180 + + + + + + + true + + + Qt::AlignCenter + + + + + + + + 10 + + + + + + + + + + + + + + + + + + + CoverLabel + QLabel +
.CoverLabel
+
+
+ + +
diff --git a/QGridLayout/Data/CoverLabel.ui b/QGridLayout/Data/CoverLabel.ui new file mode 100644 index 00000000..2e7e0378 --- /dev/null +++ b/QGridLayout/Data/CoverLabel.ui @@ -0,0 +1,109 @@ + + + CoverLabel + + + + 0 + 0 + 180 + 180 + + + + PointingHandCursor + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 135 + + + + + + + + #widgetBottom { + background-color: rgba(0, 0, 0, 150); +} + + + + + + + 16 + 16 + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 16 + 16 + + + + + + + + + + + + + + + diff --git a/QGridLayout/Data/Svg_icon_headset_sm.svg b/QGridLayout/Data/Svg_icon_headset_sm.svg new file mode 100644 index 00000000..98a1a359 --- /dev/null +++ b/QGridLayout/Data/Svg_icon_headset_sm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QGridLayout/Data/Svg_icon_loading.svg b/QGridLayout/Data/Svg_icon_loading.svg new file mode 100644 index 00000000..0b00b799 --- /dev/null +++ b/QGridLayout/Data/Svg_icon_loading.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/QGridLayout/Data/Svg_icon_play_sm.svg b/QGridLayout/Data/Svg_icon_play_sm.svg new file mode 100644 index 00000000..08a47f20 --- /dev/null +++ b/QGridLayout/Data/Svg_icon_play_sm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QGridLayout/HotPlaylist.py b/QGridLayout/HotPlaylist.py index a2022c2c..ff5500ad 100644 --- a/QGridLayout/HotPlaylist.py +++ b/QGridLayout/HotPlaylist.py @@ -1,192 +1,45 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -''' -Created on 2018年2月4日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 @email: 892768447@qq.com -@file: TencentMovieHotPlay -@description: -''' +@file: HotPlaylist.py +@description: +""" + import os import sys -import webbrowser - -from PyQt5.QtCore import QSize, Qt, QUrl, QTimer, pyqtSignal -from PyQt5.QtGui import QPainter, QFont, QLinearGradient, QGradient, QColor,\ - QBrush, QPaintEvent, QPixmap -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest -from PyQt5.QtSvg import QSvgWidget -from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QLabel,\ - QHBoxLayout, QSpacerItem, QSizePolicy, QScrollArea, QGridLayout,\ - QAbstractSlider +from Lib.CoverItemWidget import CoverItemWidget from lxml.etree import HTML # @UnresolvedImport +try: + from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal + from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PyQt5.QtSvg import QSvgWidget + from PyQt5.QtWidgets import (QAbstractSlider, QApplication, QGridLayout, + QScrollArea, QWidget) +except ImportError: + from PySide2.QtCore import Qt, QTimer, QUrl, pyqtSignal + from PySide2.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PySide2.QtSvg import QSvgWidget + from PySide2.QtWidgets import (QAbstractSlider, QApplication, QGridLayout, + QScrollArea, QWidget) -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +# offset=0,35,70,105 +Url = "https://music.163.com/discover/playlist/?order=hot&cat=%E5%85%A8%E9%83%A8&limit=35&offset={0}" -# offset=0,30,60,90 -Url = "http://v.qq.com/x/list/movie?pay=-1&offset={0}" +Agent = b"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50" -# 播放量图标 -Svg_icon_play_sm = ''' - - -'''.encode() +Referer = b"https://music.163.com/" -Svg_icon_loading = ''' - - - - - - - - - - - - - - - - - -'''.encode() - -# 主演 +# 作者 Actor = '''{title} ''' -class CoverLabel(QLabel): - - def __init__(self, cover_path, cover_title, video_url, *args, **kwargs): - super(CoverLabel, self).__init__(*args, **kwargs) - self.setCursor(Qt.PointingHandCursor) - self.setScaledContents(True) - self.setMinimumSize(220, 308) - self.setMaximumSize(220, 308) - self.cover_path = cover_path - self.cover_title = cover_title - self.video_url = video_url - self.setPixmap(QPixmap(cover_path)) - - def setCoverPath(self, path): - self.cover_path = path - - def mouseReleaseEvent(self, event): - super(CoverLabel, self).mouseReleaseEvent(event) - webbrowser.open_new_tab(self.video_url) - - def paintEvent(self, event): - super(CoverLabel, self).paintEvent(event) - if hasattr(self, "cover_title") and self.cover_title != "": - # 底部绘制文字 - painter = QPainter(self) - rect = self.rect() - # 粗略字体高度 - painter.save() - fheight = self.fontMetrics().height() - # 底部矩形框背景渐变颜色 - bottomRectColor = QLinearGradient( - rect.width() / 2, rect.height() - 24 - fheight, - rect.width() / 2, rect.height()) - bottomRectColor.setSpread(QGradient.PadSpread) - bottomRectColor.setColorAt(0, QColor(255, 255, 255, 70)) - bottomRectColor.setColorAt(1, QColor(0, 0, 0, 50)) - # 画半透明渐变矩形框 - painter.setPen(Qt.NoPen) - painter.setBrush(QBrush(bottomRectColor)) - painter.drawRect(rect.x(), rect.height() - 24 - - fheight, rect.width(), 24 + fheight) - painter.restore() - # 距离底部一定高度画文字 - font = self.font() or QFont() - font.setPointSize(8) - painter.setFont(font) - painter.setPen(Qt.white) - rect.setHeight(rect.height() - 12) # 底部减去一定高度 - painter.drawText(rect, Qt.AlignHCenter | - Qt.AlignBottom, self.cover_title) - - -class ItemWidget(QWidget): - - def __init__(self, cover_path, figure_info, figure_title, - figure_score, figure_desc, figure_count, video_url, cover_url, img_path, *args, **kwargs): - super(ItemWidget, self).__init__(*args, **kwargs) - self.setMaximumSize(220, 380) - self.setMaximumSize(220, 380) - self.img_path = img_path - self.cover_url = cover_url - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - # 图片label - self.clabel = CoverLabel(cover_path, figure_info, video_url, self) - layout.addWidget(self.clabel) - - # 片名和分数 - flayout = QHBoxLayout() - flayout.addWidget(QLabel(figure_title, self)) - flayout.addItem(QSpacerItem( - 20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) - flayout.addWidget(QLabel(figure_score, self, styleSheet="color: red;")) - layout.addLayout(flayout) - - # 主演 - layout.addWidget( - QLabel(figure_desc, self, styleSheet="color: #999999;", openExternalLinks=True)) - - # 播放量 - blayout = QHBoxLayout() - count_icon = QSvgWidget(self) - count_icon.setMaximumSize(16, 16) - count_icon.load(Svg_icon_play_sm) - blayout.addWidget(count_icon) - blayout.addWidget( - QLabel(figure_count, self, styleSheet="color: #999999;")) - layout.addLayout(blayout) - - def setCover(self, path): - self.clabel.setCoverPath(path) - self.clabel.setPixmap(QPixmap(path)) -# self.clabel.setText(''.format(os.path.abspath(path))) - - def sizeHint(self): - # 每个item控件的大小 - return QSize(220, 380) - - def event(self, event): - if isinstance(event, QPaintEvent): - if event.rect().height() > 20 and hasattr(self, "clabel"): - if self.clabel.cover_path.find("pic_v.png") > -1: # 封面未加载 - # print("start download img:", self.cover_url) - req = QNetworkRequest(QUrl(self.cover_url)) - # 设置两个自定义属性方便后期reply中处理 - req.setAttribute(QNetworkRequest.User + 1, self) - req.setAttribute(QNetworkRequest.User + 2, self.img_path) - self.parentWidget()._manager.get(req) # 调用父窗口中的下载器下载 - return super(ItemWidget, self).event(event) - - class GridWidget(QWidget): - Page = 0 loadStarted = pyqtSignal(bool) @@ -199,16 +52,18 @@ def __init__(self, *args, **kwargs): self._manager.finished.connect(self.onFinished) def load(self): - if self.Page == -1: + if self.Page == -1 or self.Page > 10: return self.loadStarted.emit(True) # 延迟一秒后调用目的在于显示进度条 QTimer.singleShot(1000, self._load) def _load(self): - print("load url:", Url.format(self.Page * 30)) - url = QUrl(Url.format(self.Page * 30)) - self._manager.get(QNetworkRequest(url)) + print("load url:", Url.format(self.Page * 35)) + url = QUrl(Url.format(self.Page * 35)) + req = QNetworkRequest(url) + req.setRawHeader(b"User-Agent", Agent) + self._manager.get(req) def onFinished(self, reply): # 请求完成后会调用该函数 @@ -232,19 +87,21 @@ def splist(self, src, length): return (src[i:i + length] for i in range(len(src)) if i % length == 0) def _parseHtml(self, html): + # print(html) # encoding = chardet.detect(html) or {} # html = html.decode(encoding.get("encoding","utf-8")) html = HTML(html) # 查找所有的li list_item - lis = html.xpath("//li[@class='list_item']") + lis = html.xpath("//ul[@id='m-pl-container']/li") + # print(lis) if not lis: self.Page = -1 # 后面没有页面了 return - lack_count = self._layout.count() % 30 # 获取布局中上次还缺几个5行*6列的标准 - row_count = int(self._layout.count() / 6) # 行数 + lack_count = self._layout.count() % 35 # 获取布局中上次还缺几个5行*6列的标准 + row_count = int(self._layout.count() / 5) # 行数 print("lack_count:", lack_count) self.Page += 1 # 自增+1 - if lack_count != 0: # 上一次没有满足一行6个,需要补齐 + if lack_count != 0: # 上一次没有满足一行5个,需要补齐 lack_li = lis[:lack_count] lis = lis[lack_count:] self._makeItem(lack_li, row_count) # 补齐 @@ -255,31 +112,34 @@ def _parseHtml(self, html): self._makeItem(lis, row_count) def _makeItem(self, li_s, row_count): - li_s = self.splist(li_s, 6) + li_s = self.splist(li_s, 5) for row, lis in enumerate(li_s): for col, li in enumerate(lis): - a = li.find("a") - video_url = a.get("href") # 视频播放地址 - img = a.find("img") - cover_url = "http:" + img.get("r-lazyload") # 封面图片 - figure_title = img.get("alt") # 电影名 - figure_info = a.find("div/span") - figure_info = "" if figure_info is None else figure_info.text # 影片信息 - figure_score = "".join(li.xpath(".//em/text()")) # 评分 - # 主演 - figure_desc = "主演:" + \ - "".join([Actor.format(**dict(fd.items())) - for fd in li.xpath(".//div[@class='figure_desc']/a")]) + a = li.find('.//div/a') + play_url = "https://music.163.com/" + a.get("href") # 歌单播放地址 + img = li.find(".//div/img") + cover_url = img.get("src") # 封面图片 + playlist_title = a.get("title") # 歌单名 + # 歌手 + author_info = li.xpath(".//p[2]/a")[0] + playlist_author = "".format( + Actor.format(href="https://music.163.com/" + + author_info.get("href"), + title=author_info.get("title"))) # 播放数 - figure_count = ( - li.xpath(".//div[@class='figure_count']/span/text()") or [""])[0] + play_count = (li.xpath(".//div/div/span[2]/text()") or [""])[0] path = "cache/{0}.jpg".format( - os.path.splitext(os.path.basename(video_url))[0]) + os.path.splitext( + os.path.basename(cover_url).split('?')[0])[0]) cover_path = "Data/pic_v.png" if os.path.isfile(path): cover_path = path - iwidget = ItemWidget(cover_path, figure_info, figure_title, - figure_score, figure_desc, figure_count, video_url, cover_url, path, self) + + # print(cover_path, playlist_title, + # playlist_author, play_count, play_url, cover_url, path) + iwidget = CoverItemWidget(self, manager=self._manager) + iwidget.init(cover_path, playlist_title, playlist_author, + play_count, play_url, cover_url, path) self._layout.addWidget(iwidget, row_count + row, col) @@ -299,9 +159,11 @@ def __init__(self, *args, **kwargs): # 连接竖着的滚动条滚动事件 self.verticalScrollBar().actionTriggered.connect(self.onActionTriggered) # 进度条 - self.loadWidget = QSvgWidget( - self, minimumHeight=120, minimumWidth=120, visible=False) - self.loadWidget.load(Svg_icon_loading) + self.loadWidget = QSvgWidget(self, + minimumHeight=120, + minimumWidth=120, + visible=False) + self.loadWidget.load('Data/Svg_icon_loading.svg') def setLoadStarted(self, started): self._loadStart = started @@ -313,7 +175,8 @@ def onActionTriggered(self, action): if action != QAbstractSlider.SliderMove or self._loadStart: return # 使用sliderPosition获取值可以同时满足鼠标滑动和拖动判断 - if self.verticalScrollBar().sliderPosition() == self.verticalScrollBar().maximum(): + if self.verticalScrollBar().sliderPosition() == self.verticalScrollBar( + ).maximum(): # 可以下一页了 self._widget.load() @@ -322,9 +185,7 @@ def resizeEvent(self, event): self.loadWidget.setGeometry( int((self.width() - self.loadWidget.minimumWidth()) / 2), int((self.height() - self.loadWidget.minimumHeight()) / 2), - self.loadWidget.minimumWidth(), - self.loadWidget.minimumHeight() - ) + self.loadWidget.minimumWidth(), self.loadWidget.minimumHeight()) if __name__ == "__main__": diff --git a/QGridLayout/Lib/CoverItemWidget.py b/QGridLayout/Lib/CoverItemWidget.py new file mode 100644 index 00000000..5ef3ff00 --- /dev/null +++ b/QGridLayout/Lib/CoverItemWidget.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CoverItemWidget.py +@description: +""" + +try: + from PyQt5.QtCore import QSize, QUrl + from PyQt5.QtGui import QPaintEvent, QPixmap + from PyQt5.QtNetwork import QNetworkRequest + from PyQt5.QtWidgets import QWidget +except ImportError: + from PySide2.QtCore import QSize, QUrl + from PySide2.QtGui import QPaintEvent, QPixmap + from PySide2.QtNetwork import QNetworkRequest + from PySide2.QtWidgets import QWidget + +from .Ui_CoverItemWidget import Ui_CoverItemWidget # @UnresolvedImport + + +class CoverItemWidget(QWidget, Ui_CoverItemWidget): + + def __init__(self, *args, **kwargs): + self._manager = kwargs.pop('manager', None) + super(CoverItemWidget, self).__init__(*args, **kwargs) + self.setupUi(self) + + def init(self, cover_path, playlist_title, playlist_author, play_count, + play_url, cover_url, img_path): + self.img_path = img_path + self.cover_url = cover_url + # 图片label + self.labelCover.init(cover_path, play_url, play_count) + + # 歌单 + self.labelTitle.setText(playlist_title) + + # 作者 + self.labelAuthor.setText(playlist_author) + + def setCover(self, path): + self.labelCover.setCoverPath(path) + self.labelCover.setPixmap(QPixmap(path)) + + def sizeHint(self): + # 每个item控件的大小 + return QSize(200, 256) + + def event(self, event): + if isinstance(event, QPaintEvent): + if event.rect().height() > 20 and hasattr(self, "labelCover"): + if self.labelCover.cover_path.find("pic_v.png") > -1: # 封面未加载 + # print("start download img:", self.cover_url) + req = QNetworkRequest(QUrl(self.cover_url)) + # 设置两个自定义属性方便后期reply中处理 + req.setAttribute(QNetworkRequest.User + 1, self) + req.setAttribute(QNetworkRequest.User + 2, self.img_path) + if self._manager: + self._manager.get(req) # 调用父窗口中的下载器下载 + return super(CoverItemWidget, self).event(event) diff --git a/QGridLayout/Lib/CoverLabel.py b/QGridLayout/Lib/CoverLabel.py new file mode 100644 index 00000000..dbee15af --- /dev/null +++ b/QGridLayout/Lib/CoverLabel.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CoverLabel.py +@description: +""" +import webbrowser + +try: + from PyQt5.QtGui import QPixmap + from PyQt5.QtWidgets import QLabel +except ImportError: + from PySide2.QtGui import QPixmap + from PySide2.QtWidgets import QLabel + +from .Ui_CoverLabel import Ui_CoverLabel # @UnresolvedImport + + +class CoverLabel(QLabel, Ui_CoverLabel): + + def __init__(self, *args, **kwargs): + super(CoverLabel, self).__init__(*args, **kwargs) + self.setupUi(self) + + def init(self, cover_path, play_url, play_count): + self.cover_path = cover_path + self.play_url = play_url + self.setPixmap(QPixmap(cover_path)) + self.labelHeadset.setPixmap(QPixmap('Data/Svg_icon_headset_sm.svg')) + self.labelPlay.setPixmap(QPixmap('Data/Svg_icon_play_sm.svg')) + self.labelCount.setStyleSheet('color: #999999;') + self.labelCount.setText(play_count) + + def setCoverPath(self, path): + self.cover_path = path + + def mouseReleaseEvent(self, event): + super(CoverLabel, self).mouseReleaseEvent(event) + webbrowser.open_new_tab(self.play_url) diff --git a/QGridLayout/Lib/Ui_CoverItemWidget.py b/QGridLayout/Lib/Ui_CoverItemWidget.py new file mode 100644 index 00000000..c048f686 --- /dev/null +++ b/QGridLayout/Lib/Ui_CoverItemWidget.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CoverItemWidget.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# 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_CoverItemWidget(object): + def setupUi(self, CoverItemWidget): + CoverItemWidget.setObjectName("CoverItemWidget") + CoverItemWidget.setMinimumSize(QtCore.QSize(200, 256)) + CoverItemWidget.setMaximumSize(QtCore.QSize(200, 256)) + CoverItemWidget.setWindowTitle("") + self.verticalLayout = QtWidgets.QVBoxLayout(CoverItemWidget) + self.verticalLayout.setContentsMargins(10, 10, 10, 10) + self.verticalLayout.setSpacing(12) + self.verticalLayout.setObjectName("verticalLayout") + self.labelCover = CoverLabel(CoverItemWidget) + self.labelCover.setMinimumSize(QtCore.QSize(180, 180)) + self.labelCover.setText("") + self.labelCover.setScaledContents(True) + self.labelCover.setAlignment(QtCore.Qt.AlignCenter) + self.labelCover.setObjectName("labelCover") + self.verticalLayout.addWidget(self.labelCover) + self.labelTitle = QtWidgets.QLabel(CoverItemWidget) + font = QtGui.QFont() + font.setPointSize(10) + self.labelTitle.setFont(font) + self.labelTitle.setText("") + self.labelTitle.setObjectName("labelTitle") + self.verticalLayout.addWidget(self.labelTitle) + self.labelAuthor = QtWidgets.QLabel(CoverItemWidget) + self.labelAuthor.setText("") + self.labelAuthor.setObjectName("labelAuthor") + self.verticalLayout.addWidget(self.labelAuthor) + + self.retranslateUi(CoverItemWidget) + QtCore.QMetaObject.connectSlotsByName(CoverItemWidget) + + def retranslateUi(self, CoverItemWidget): + pass +from .CoverLabel import CoverLabel + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + CoverItemWidget = QtWidgets.QWidget() + ui = Ui_CoverItemWidget() + ui.setupUi(CoverItemWidget) + CoverItemWidget.show() + sys.exit(app.exec_()) diff --git a/QGridLayout/Lib/Ui_CoverLabel.py b/QGridLayout/Lib/Ui_CoverLabel.py new file mode 100644 index 00000000..86e1526a --- /dev/null +++ b/QGridLayout/Lib/Ui_CoverLabel.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CoverLabel.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# 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_CoverLabel(object): + def setupUi(self, CoverLabel): + CoverLabel.setObjectName("CoverLabel") + CoverLabel.resize(180, 180) + CoverLabel.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + CoverLabel.setWindowTitle("") + self.verticalLayout = QtWidgets.QVBoxLayout(CoverLabel) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName("verticalLayout") + spacerItem = QtWidgets.QSpacerItem(20, 135, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.widgetBottom = QtWidgets.QWidget(CoverLabel) + self.widgetBottom.setStyleSheet("#widgetBottom {\n" +" background-color: rgba(0, 0, 0, 150);\n" +"}") + self.widgetBottom.setObjectName("widgetBottom") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widgetBottom) + self.horizontalLayout.setObjectName("horizontalLayout") + self.labelHeadset = QtWidgets.QLabel(self.widgetBottom) + self.labelHeadset.setMinimumSize(QtCore.QSize(16, 16)) + self.labelHeadset.setText("") + self.labelHeadset.setObjectName("labelHeadset") + self.horizontalLayout.addWidget(self.labelHeadset) + self.labelCount = QtWidgets.QLabel(self.widgetBottom) + self.labelCount.setText("") + self.labelCount.setObjectName("labelCount") + self.horizontalLayout.addWidget(self.labelCount) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.labelPlay = QtWidgets.QLabel(self.widgetBottom) + self.labelPlay.setMinimumSize(QtCore.QSize(16, 16)) + self.labelPlay.setText("") + self.labelPlay.setObjectName("labelPlay") + self.horizontalLayout.addWidget(self.labelPlay) + self.verticalLayout.addWidget(self.widgetBottom) + + self.retranslateUi(CoverLabel) + QtCore.QMetaObject.connectSlotsByName(CoverLabel) + + def retranslateUi(self, CoverLabel): + pass + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + CoverLabel = QtWidgets.QWidget() + ui = Ui_CoverLabel() + ui.setupUi(CoverLabel) + CoverLabel.show() + sys.exit(app.exec_()) diff --git a/QGridLayout/Lib/__init__.py b/QGridLayout/Lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/QGridLayout/README.md b/QGridLayout/README.md index 10495c6a..106220a6 100644 --- a/QGridLayout/README.md +++ b/QGridLayout/README.md @@ -1,21 +1,22 @@ # QListView - 目录 - - [腾讯视频热播列表](#1腾讯视频热播列表) + - [音乐热歌列表](#1音乐热歌列表) + +## 1、音乐热歌列表 -## 1、腾讯视频热播列表 [运行 HotPlaylist.py](HotPlaylist.py) 简单思路说明: - - 利用`QScrollArea`滚动显示,`QGridLayout`做布局来放置自定义的Widget - - `QNetworkAccessManager`异步下载网页和图片 - - `QScrollArea`滚动到底部触发下一页加载 +- 利用`QScrollArea`滚动显示,`QGridLayout`做布局来放置自定义的Widget +- `QNetworkAccessManager`异步下载网页和图片 +- `QScrollArea`滚动到底部触发下一页加载 自定义控件说明: - - 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 - - 字体颜色用qss设置 - - 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) +- 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 +- 字体颜色用qss设置 +- 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) -![HotPlaylist](ScreenShot/HotPlaylist.gif) \ No newline at end of file +![HotPlaylist](ScreenShot/HotPlaylist.gif) diff --git a/QHBoxLayout/Data/BaseHorizontalLayout.ui b/QHBoxLayout/Data/BaseHorizontalLayout.ui new file mode 100644 index 00000000..0d0410ec --- /dev/null +++ b/QHBoxLayout/Data/BaseHorizontalLayout.ui @@ -0,0 +1,38 @@ + + + BaseHorizontalLayout + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + 通过 右键 -> 布局 -> 水平布局 + + + Qt::AlignCenter + + + + + + + 右侧按钮 + + + + + + + + diff --git a/QHBoxLayout/Data/HorizontalLayoutMargin.ui b/QHBoxLayout/Data/HorizontalLayoutMargin.ui new file mode 100644 index 00000000..89673b34 --- /dev/null +++ b/QHBoxLayout/Data/HorizontalLayoutMargin.ui @@ -0,0 +1,60 @@ + + + HorizontalLayoutMargin + + + + 0 + 0 + 457 + 216 + + + + Form + + + #VerticalLayoutMargin { + background: #5aaadb; +} +#label { + background: #85c440; +} + + + + 20 + + + 20 + + + + + 通过设置Margin和Spacing设置边距 +以及两个控件之间的间隔距离 + + + Qt::AlignCenter + + + + + + + Spcasing 为 20 + + + + + + + Spcasing 为 20 + + + + + + + + diff --git a/QHBoxLayout/Data/HorizontalLayoutStretch.ui b/QHBoxLayout/Data/HorizontalLayoutStretch.ui new file mode 100644 index 00000000..a45f50cb --- /dev/null +++ b/QHBoxLayout/Data/HorizontalLayoutStretch.ui @@ -0,0 +1,62 @@ + + + HorizontalLayoutStretch + + + + 0 + 0 + 400 + 300 + + + + Form + + + #label { + background: #5aaadb; +} +#label_2 { + background: #85c440; +} +#label_3 { + background: #f2b63c; +} + + + + + + 1/6 + + + Qt::AlignCenter + + + + + + + 2/6 + + + Qt::AlignCenter + + + + + + + 3/6 + + + Qt::AlignCenter + + + + + + + + diff --git a/QHBoxLayout/README.md b/QHBoxLayout/README.md index e69de29b..fa074093 100644 --- a/QHBoxLayout/README.md +++ b/QHBoxLayout/README.md @@ -0,0 +1,35 @@ +# QHBoxLayout + +- 目录 + - [水平布局](#1水平布局) + - [边距和间隔](#2边距和间隔) + - [比例分配](#3比例分配) + +## 1、水平布局 + +[查看 BaseHorizontalLayout.ui](Data/BaseHorizontalLayout.ui) + +![BaseHorizontalLayout](ScreenShot/BaseHorizontalLayout.png) + +## 2、边距和间隔 + +[查看 HorizontalLayoutMargin.ui](Data/HorizontalLayoutMargin.ui) + +1. 通过`setContentsMargins(-1, -1, 20, -1)`设置左上右下的边距,-1表示默认值 +2. 通过`setSpacing`设置控件之间的间隔 + +![HorizontalLayoutMargin](ScreenShot/HorizontalLayoutMargin.png) + +## 3、比例分配 + +[查看 HorizontalLayoutStretch.ui](Data/HorizontalLayoutStretch.ui) + +通过`setStretch`设置各个部分的占比 分别为:1/6 2/6 3/6 + +```python +self.horizontalLayout.setStretch(0, 1) +self.horizontalLayout.setStretch(1, 2) +self.horizontalLayout.setStretch(2, 3) +``` + +![HorizontalLayoutStretch](ScreenShot/HorizontalLayoutStretch.png) diff --git a/QHBoxLayout/ScreenShot/BaseHorizontalLayout.png b/QHBoxLayout/ScreenShot/BaseHorizontalLayout.png new file mode 100644 index 00000000..37f0c3f8 Binary files /dev/null and b/QHBoxLayout/ScreenShot/BaseHorizontalLayout.png differ diff --git a/QHBoxLayout/ScreenShot/HorizontalLayoutMargin.png b/QHBoxLayout/ScreenShot/HorizontalLayoutMargin.png new file mode 100644 index 00000000..32dc9ec4 Binary files /dev/null and b/QHBoxLayout/ScreenShot/HorizontalLayoutMargin.png differ diff --git a/QHBoxLayout/ScreenShot/HorizontalLayoutStretch.png b/QHBoxLayout/ScreenShot/HorizontalLayoutStretch.png new file mode 100644 index 00000000..3c568f9a Binary files /dev/null and b/QHBoxLayout/ScreenShot/HorizontalLayoutStretch.png differ diff --git a/QLabel/CircleImage.py b/QLabel/CircleImage.py index 0c805cb0..7b59420e 100644 --- a/QLabel/CircleImage.py +++ b/QLabel/CircleImage.py @@ -3,19 +3,21 @@ """ Created on 2018年1月20日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CircleImage @description: 圆形图片 """ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap, QPainter, QPainterPath -from PyQt5.QtWidgets import QLabel, QWidget, QHBoxLayout -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPixmap, QPainter, QPainterPath + from PyQt5.QtWidgets import QLabel, QWidget, QHBoxLayout, QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPixmap, QPainter, QPainterPath + from PySide2.QtWidgets import QLabel, QWidget, QHBoxLayout, QApplication class Label(QLabel): @@ -67,7 +69,6 @@ def __init__(self, *args, **kwargs): if __name__ == "__main__": import sys - from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = Window() diff --git a/QLabel/ImageRotate.py b/QLabel/ImageRotate.py index f0dd48d6..142299b7 100644 --- a/QLabel/ImageRotate.py +++ b/QLabel/ImageRotate.py @@ -4,22 +4,22 @@ """ Created on 2018年11月19日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: @description: """ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap, QPainter, QImage -from PyQt5.QtWidgets import QWidget, QLabel, QPushButton,\ - QVBoxLayout, QHBoxLayout, QSpacerItem, QSizePolicy - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPixmap, QPainter, QImage + from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, \ + QVBoxLayout, QHBoxLayout, QSpacerItem, QSizePolicy, QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPixmap, QPainter, QImage + from PySide2.QtWidgets import QWidget, QLabel, QPushButton, \ + QVBoxLayout, QHBoxLayout, QSpacerItem, QSizePolicy, QApplication class Window(QWidget): @@ -73,12 +73,12 @@ def doClockwise(self): self.srcImage = image # 替换 self.imageLabel.setPixmap(QPixmap.fromImage(self.srcImage)) -# # 下面这个旋转方法针对90度的倍数,否则图片会变大 -# trans = QTransform() -# trans.rotate(90) -# self.srcImage = self.srcImage.transformed( -# trans, Qt.SmoothTransformation) -# self.imageLabel.setPixmap(QPixmap.fromImage(self.srcImage)) + # # 下面这个旋转方法针对90度的倍数,否则图片会变大 + # trans = QTransform() + # trans.rotate(90) + # self.srcImage = self.srcImage.transformed( + # trans, Qt.SmoothTransformation) + # self.imageLabel.setPixmap(QPixmap.fromImage(self.srcImage)) def doAnticlockwise(self): # 逆时针45度 @@ -96,6 +96,7 @@ def doAnticlockwise(self): self.srcImage = image # 替换 self.imageLabel.setPixmap(QPixmap.fromImage(self.srcImage)) + # # 下面这个旋转方法针对90度的倍数,否则图片会变大 # trans = QTransform() # trans.rotate(90) @@ -106,7 +107,7 @@ def doAnticlockwise(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QLabel/ImageSlipped.py b/QLabel/ImageSlipped.py index dfcac07d..1ea7818b 100644 --- a/QLabel/ImageSlipped.py +++ b/QLabel/ImageSlipped.py @@ -4,20 +4,18 @@ """ Created on 2018年10月18日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ImageSlipped @description: """ -from PyQt5.QtGui import QPixmap, QPainter -from PyQt5.QtWidgets import QWidget - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtGui import QPixmap, QPainter + from PyQt5.QtWidgets import QWidget, QApplication +except ImportError: + from PySide2.QtGui import QPixmap, QPainter + from PySide2.QtWidgets import QWidget, QApplication class SlippedImgWidget(QWidget): @@ -73,7 +71,7 @@ def paintEvent(self, event): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = SlippedImgWidget('Data/bg1.jpg', 'Data/fg1.png') w.show() diff --git a/QLabel/Lib/NinePatch.py b/QLabel/Lib/NinePatch.py index 966903ae..e157f1bf 100644 --- a/QLabel/Lib/NinePatch.py +++ b/QLabel/Lib/NinePatch.py @@ -4,22 +4,19 @@ """ Created on 2018年10月25日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: NinePatch @description: """ from math import fabs -from PyQt5.QtCore import QRect -from PyQt5.QtGui import QImage, QColor, QPainter, qRed, qGreen, qBlue, qAlpha - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QRect + from PyQt5.QtGui import QImage, QColor, QPainter, qRed, qGreen, qBlue, qAlpha +except ImportError: + from PySide2.QtCore import QRect + from PySide2.QtGui import QImage, QColor, QPainter, qRed, qGreen, qBlue, qAlpha class _Exception(Exception): @@ -44,7 +41,8 @@ def __str__(self): class ExceptionIncorrectWidthAndHeight(_Exception): def __str__(self): - return "Input incorrect width width and height. Minimum width = :{imgW} . Minimum height = :{imgH}".format(imgW=self.imgW, imgH=self.imgH) + return "Input incorrect width width and height. Minimum width = :{imgW} . Minimum height = :{imgH}".format( + imgW=self.imgW, imgH=self.imgH) class ExceptionIncorrectHeight(_Exception): @@ -62,11 +60,11 @@ def __str__(self): class NinePatch: def __init__(self, fileName): - self.CachedImage = None # 缓存图片 + self.CachedImage = None # 缓存图片 self.OldWidth = -1 self.OldHeight = -1 self.ResizeDistancesX = [] - self.ResizeDistancesY = [] # [(int,int)]数组 + self.ResizeDistancesY = [] # [(int,int)]数组 self.setImage(fileName) def width(self): @@ -103,7 +101,8 @@ def SetImageSize(self, width, height): for i in range(len(self.ResizeDistancesY)): resizeHeight += self.ResizeDistancesY[i][1] - if (width < (self.Image.width() - 2 - resizeWidth) and height < (self.Image.height() - 2 - resizeHeight)): + if (width < (self.Image.width() - 2 - resizeWidth) and height < ( + self.Image.height() - 2 - resizeHeight)): raise ExceptionIncorrectWidthAndHeight( self.Image.width() - 2, self.Image.height() - 2) @@ -123,7 +122,8 @@ def SetImageSize(self, width, height): @classmethod def GetContentAreaRect(self, width, height): # print("GetContentAreaRect : width:%d height:%d" % (width, height)) - return (QRect(self.ContentArea.x(), self.ContentArea.y(), (width - (self.Image.width() - 2 - self.ContentArea.width())), + return (QRect(self.ContentArea.x(), self.ContentArea.y(), + (width - (self.Image.width() - 2 - self.ContentArea.width())), (height - (self.Image.height() - 2 - self.ContentArea.height())))) def DrawScaledPart(self, oldRect, newRect, painter): @@ -189,7 +189,8 @@ def GetResizeArea(self): for i in range(self.Image.width()): if (self.IsColorBlack(self.Image.pixel(i, j)) and left == 0): left = i - if (left and self.IsColorBlack(self.Image.pixel(i, j)) and not self.IsColorBlack(self.Image.pixel(i + 1, j))): + if (left and self.IsColorBlack(self.Image.pixel(i, j)) and not self.IsColorBlack( + self.Image.pixel(i + 1, j))): right = i left -= 1 # print("ResizeDistancesX.append ", left, " ", right - left) @@ -204,7 +205,8 @@ def GetResizeArea(self): if (self.IsColorBlack(self.Image.pixel(i, j)) and top == 0): top = j - if (top and self.IsColorBlack(self.Image.pixel(i, j)) and not self.IsColorBlack(self.Image.pixel(i, j + 1))): + if (top and self.IsColorBlack(self.Image.pixel(i, j)) and not self.IsColorBlack( + self.Image.pixel(i, j + 1))): bot = j top -= 1 # print("ResizeDistancesY.append ", top, " ", bot - top) @@ -241,10 +243,10 @@ def UpdateCachedImage(self, width, height): # print("after GetFactor: ", width, height, factorX, factorY) lostX = 0.0 lostY = 0.0 - x1 = 0 # for image parts X - y1 = 0 # for image parts Y -# widthResize # width for image parts -# heightResize # height for image parts + x1 = 0 # for image parts X + y1 = 0 # for image parts Y + # widthResize # width for image parts + # heightResize # height for image parts resizeX = 0 resizeY = 0 offsetX = 0 @@ -310,7 +312,8 @@ def UpdateCachedImage(self, width, height): offsetY = 0 for i in range(len(self.ResizeDistancesY)): self.DrawConstPart(QRect(x1 + 1, y1 + 1, widthResize, self.ResizeDistancesY[i][0] - y1), - QRect(x1 + offsetX, y1 + offsetY, widthResize, self.ResizeDistancesY[i][0] - y1), painter) + QRect(x1 + offsetX, y1 + offsetY, widthResize, + self.ResizeDistancesY[i][0] - y1), painter) y1 = self.ResizeDistancesY[i][0] resizeY = round(float(self.ResizeDistancesY[i][1]) * factorY) lostY += resizeY - (float(self.ResizeDistancesY[i][1]) * factorY) @@ -334,7 +337,8 @@ def UpdateCachedImage(self, width, height): offsetX = 0 for i in range(len(self.ResizeDistancesX)): self.DrawConstPart(QRect(x1 + 1, y1 + 1, self.ResizeDistancesX[i][0] - x1, heightResize), - QRect(x1 + offsetX, y1 + offsetY, self.ResizeDistancesX[i][0] - x1, heightResize), painter) + QRect(x1 + offsetX, y1 + offsetY, self.ResizeDistancesX[i][0] - x1, + heightResize), painter) x1 = self.ResizeDistancesX[i][0] resizeX = round(float(self.ResizeDistancesX[i][1]) * factorX) lostX += resizeX - (float(self.ResizeDistancesX[i][1]) * factorX) diff --git a/QLabel/Lib/QtNinePatch/sip/configure.py b/QLabel/Lib/QtNinePatch/sip/configure.py index eae22a59..a3cc255c 100644 --- a/QLabel/Lib/QtNinePatch/sip/configure.py +++ b/QLabel/Lib/QtNinePatch/sip/configure.py @@ -5,9 +5,8 @@ import shutil import PyQt5 -from PyQt5.QtCore import PYQT_CONFIGURATION import sipconfig - +from PyQt5.QtCore import PYQT_CONFIGURATION # 模块名 moduleName = 'QtNinePatch' @@ -35,14 +34,13 @@ '-b', "build/" + build_file, '-I', config.default_sip_dir + '/PyQt5', PYQT_CONFIGURATION.get('sip_flags', ''), - '%s.sip' % moduleName, + '%s.sip' % moduleName, ]) os.makedirs('build', exist_ok=True) print(sip_cmd) os.system(sip_cmd) - # Create the Makefile. makefile = sipconfig.SIPModuleMakefile( config, build_file, dir='build' diff --git a/QLabel/Lib/QtNinePatch2.py b/QLabel/Lib/QtNinePatch2.py index b0a84354..7697cc69 100644 --- a/QLabel/Lib/QtNinePatch2.py +++ b/QLabel/Lib/QtNinePatch2.py @@ -4,22 +4,19 @@ """ Created on 2018年10月25日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QtNinePatch @description: """ from math import floor -from PyQt5.QtCore import Qt, QRect -from PyQt5.QtGui import qAlpha, QPixmap, QPainter - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt, QRect + from PyQt5.QtGui import qAlpha, QPixmap, QPainter +except ImportError: + from PySide2.QtCore import Qt, QRect + from PySide2.QtGui import qAlpha, QPixmap, QPainter class Part: diff --git a/QLabel/Lib/res_rc.py b/QLabel/Lib/res_rc.py index baab0b94..0bebdc4c 100644 --- a/QLabel/Lib/res_rc.py +++ b/QLabel/Lib/res_rc.py @@ -6,7 +6,10 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore +try: + from PyQt5 import QtCore +except ImportError: + from PySide2 import QtCore qt_resource_data = b"\ \x00\x00\x19\xf0\ @@ -462,10 +465,13 @@ rcc_version = 2 qt_resource_struct = qt_resource_struct_v2 + def qInitResources(): QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + def qCleanupResources(): QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + qInitResources() diff --git a/QLabel/Lib/xpmres.py b/QLabel/Lib/xpmres.py index fea8f797..b589c8d1 100644 --- a/QLabel/Lib/xpmres.py +++ b/QLabel/Lib/xpmres.py @@ -1,18 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月23日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: xpmres @description: -''' - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +""" # 这里把转换的xpm数组直接放到py文件中当做一个变量 diff --git a/QLabel/NinePatch.py b/QLabel/NinePatch.py index 936faf7e..f072dc10 100644 --- a/QLabel/NinePatch.py +++ b/QLabel/NinePatch.py @@ -4,23 +4,20 @@ """ Created on 2018年10月25日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: NinePatch @description: """ - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" - import sys -from PyQt5.QtGui import QImage, QPainter -from PyQt5.QtWidgets import QApplication, QLabel, QWidget +try: + from PyQt5.QtGui import QPainter + from PyQt5.QtWidgets import QApplication, QWidget +except ImportError: + from PySide2.QtGui import QPainter + from PySide2.QtWidgets import QApplication, QWidget from Lib.NinePatch import NinePatch @@ -29,7 +26,7 @@ class Label(QWidget): def __init__(self, *args, **kwargs): super(Label, self).__init__(*args, **kwargs) - #.9 格式的图片 + # .9 格式的图片 self.image = NinePatch('Data/skin_aio_friend_bubble_pressed.9.png') def paintEvent(self, event): diff --git a/QLabel/QtNinePatch.py b/QLabel/QtNinePatch.py index 8fbdbc9c..497b7bfa 100644 --- a/QLabel/QtNinePatch.py +++ b/QLabel/QtNinePatch.py @@ -4,22 +4,15 @@ """ Created on 2018年10月25日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: testQtNinePatch @description: """ - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" - import sys - from ctypes import CDLL + from PyQt5.QtGui import QImage from PyQt5.QtWidgets import QApplication, QLabel @@ -32,7 +25,7 @@ class Label(QLabel): def __init__(self, *args, **kwargs): super(Label, self).__init__(*args, **kwargs) - #.9 格式的图片 + # .9 格式的图片 self.image = QImage('Data/skin_aio_friend_bubble_pressed.9.png') def showEvent(self, event): diff --git a/QLabel/QtNinePatch2.py b/QLabel/QtNinePatch2.py index 187b15ce..f8614612 100644 --- a/QLabel/QtNinePatch2.py +++ b/QLabel/QtNinePatch2.py @@ -4,23 +4,20 @@ """ Created on 2018年10月25日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QtNinePatch2 @description: """ - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" - import sys -from PyQt5.QtGui import QImage -from PyQt5.QtWidgets import QApplication, QLabel +try: + from PyQt5.QtGui import QImage + from PyQt5.QtWidgets import QApplication, QLabel +except ImportError: + from PySide2.QtGui import QImage + from PySide2.QtWidgets import QApplication, QLabel from Lib import QtNinePatch2 @@ -29,7 +26,7 @@ class Label(QLabel): def __init__(self, *args, **kwargs): super(Label, self).__init__(*args, **kwargs) - #.9 格式的图片 + # .9 格式的图片 self.image = QImage('Data/skin_aio_friend_bubble_pressed.9.png') def showEvent(self, event): diff --git a/QLabel/README.md b/QLabel/README.md index acfc3d53..b14fca5a 100644 --- a/QLabel/README.md +++ b/QLabel/README.md @@ -8,6 +8,7 @@ - [圆形图片](#5圆形图片) ## 1、图片加载显示 + [运行 ShowImage.py](ShowImage.py) 通过3种方式加载图片文件和显示gif图片 @@ -32,6 +33,7 @@ ![ShowImage](ScreenShot/ShowImage.gif) ## 2、图片旋转 + [运行 ImageRotate.py](ImageRotate.py) 1. 水平翻转 `QImage.mirrored(True, False)` @@ -42,6 +44,7 @@ ![ImageRotate](ScreenShot/ImageRotate.gif) ## 3、仿网页图片错位显示 + [运行 ImageSlipped.py](ImageSlipped.py) 1. 设置`setMouseTracking(True)`开启鼠标跟踪 @@ -51,6 +54,7 @@ ![ImageSlipped](ScreenShot/ImageSlipped.gif) ## 4、显示.9格式图片(气泡) + [运行 NinePatch.py](NinePatch.py) | [运行 QtNinePatch.py](QtNinePatch.py) | [运行 QtNinePatch2.py](QtNinePatch2.py) 什么叫.9.PNG呢,这是安卓开发里面的一种特殊的图片 @@ -64,16 +68,18 @@ 在Github开源库中搜索到两个C++版本的 -1.一个是NinePatchQt https://github.com/Roninsc2/NinePatchQt +1.一个是NinePatchQt -2.一个是QtNinePatch https://github.com/soramimi/QtNinePatch +2.一个是QtNinePatch ### For PyQt + 1、目前针对第一个库在2年前用Python参考源码重新写一个见 `Lib/NinePatch.py` 2、这次针对第二个库用Python编写的见`Lib/QtNinePatch2.py`。用C++编写的pyd版本见`Lib/QtNinePatch`目录 ### 说明 + 1、建议优先使用pyd版本的(后续提供Python3.4 3.5 3.6 3.7 编译好的32为库文件),也可以自行编译,编译步骤见下文。 2、其次可以使用Python写的第二个版本`Lib/QtNinePatch2.py`(个人觉得方便调用) @@ -103,8 +109,9 @@ qt_path = 'D:/soft/Qt/Qt5.5.1/5.5/msvc2010' ![NinePatchImage](ScreenShot/NinePatchImage.gif) ### 5、圆形图片 + [运行 CircleImage.py](CircleImage.py) 使用`QPainter`的`setClipPath`方法结合`QPainterPath`对图片进行裁剪从而实现圆形图片。 -![CircleImage](ScreenShot/CircleImage.png) \ No newline at end of file +![CircleImage](ScreenShot/CircleImage.png) diff --git a/QLabel/ShowImage.py b/QLabel/ShowImage.py index ac3fb9f6..41ca08ea 100644 --- a/QLabel/ShowImage.py +++ b/QLabel/ShowImage.py @@ -1,29 +1,28 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月23日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ShowImage @description: -''' +""" import sys -from PyQt5.QtCore import QResource -from PyQt5.QtGui import QPixmap, QMovie -from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QLabel +try: + from PyQt5.QtCore import QResource + from PyQt5.QtGui import QPixmap, QMovie + from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QLabel +except ImportError: + from PySide2.QtCore import QResource + from PySide2.QtGui import QPixmap, QMovie + from PySide2.QtWidgets import QWidget, QApplication, QHBoxLayout, QLabel -from Lib import res_rc # @UnresolvedImport @UnusedImport from Lib.xpmres import image_head # @UnresolvedImport -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - - class ImageView(QWidget): def __init__(self, *args, **kwargs): diff --git a/QListView/CustomWidgetItem.py b/QListView/CustomWidgetItem.py index 6a8a1be8..3bffcac1 100644 --- a/QListView/CustomWidgetItem.py +++ b/QListView/CustomWidgetItem.py @@ -1,22 +1,26 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from PyQt5.QtCore import QSize -from PyQt5.QtGui import QStandardItemModel, QStandardItem -from PyQt5.QtWidgets import QListView, QWidget, QHBoxLayout, QLineEdit,\ - QPushButton -# Created on 2018年8月4日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: QListView.显示自定义Widget -# description: -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +""" +Created on 2018年8月4日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: QListView.显示自定义Widget +@description: +""" + +try: + from PyQt5.QtCore import QSize + from PyQt5.QtGui import QStandardItemModel, QStandardItem + from PyQt5.QtWidgets import QListView, QWidget, QHBoxLayout, QLineEdit, \ + QPushButton, QApplication +except ImportError: + from PySide2.QtCore import QSize + from PySide2.QtGui import QStandardItemModel, QStandardItem + from PySide2.QtWidgets import QListView, QWidget, QHBoxLayout, QLineEdit, \ + QPushButton, QApplication class CustomWidget(QWidget): @@ -56,7 +60,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = ListView() w.show() diff --git a/QListView/CustomWidgetSortItem.py b/QListView/CustomWidgetSortItem.py index 09fe9036..3a68ef20 100644 --- a/QListView/CustomWidgetSortItem.py +++ b/QListView/CustomWidgetSortItem.py @@ -1,26 +1,29 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from random import choice, randint -import string -from time import time -from PyQt5.QtCore import QSortFilterProxyModel, Qt, QSize -from PyQt5.QtGui import QStandardItem, QStandardItemModel -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QListView,\ - QHBoxLayout, QLineEdit +""" +Created on 2018年8月4日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: QListView.显示自定义Widget并排序 +@description: +""" +import string +from random import choice, randint +from time import time -# Created on 2018年8月4日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: QListView.显示自定义Widget并排序 -# description: -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QSortFilterProxyModel, Qt, QSize + from PyQt5.QtGui import QStandardItem, QStandardItemModel + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QListView, \ + QHBoxLayout, QLineEdit, QApplication +except ImportError: + from PySide2.QtCore import QSortFilterProxyModel, Qt, QSize + from PySide2.QtGui import QStandardItem, QStandardItemModel + from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QListView, \ + QHBoxLayout, QLineEdit, QApplication def randomChar(y): @@ -55,11 +58,11 @@ def lessThan(self, source_left, source_right): leftData = leftData.split('-')[-1] rightData = rightData.split('-')[-1] return leftData < rightData -# elif self.sortOrder() == Qt.AscendingOrder: -# #按照名字升序排序 -# leftData = leftData.split('-')[0] -# rightData = rightData.split('-')[0] -# return leftData < rightData + # elif self.sortOrder() == Qt.AscendingOrder: + # #按照名字升序排序 + # leftData = leftData.split('-')[0] + # rightData = rightData.split('-')[0] + # return leftData < rightData return super(SortFilterProxyModel, self).lessThan(source_left, source_right) @@ -89,7 +92,7 @@ def __init__(self, *args, **kwargs): times = time() + randint(0, 30) # 当前时间随机+ value = '{}-{}'.format(name, times) # 内容用-分开 item = QStandardItem(value) -# item.setData(value, Qt.UserRole + 2) + # item.setData(value, Qt.UserRole + 2) self.dmodel.appendRow(item) # 索引 index = self.fmodel.mapFromSource(item.index()) @@ -109,7 +112,7 @@ def sortByName(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QListView/ImageView.py b/QListView/ImageView.py new file mode 100644 index 00000000..b030806c --- /dev/null +++ b/QListView/ImageView.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/4/15 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ImageView +@description: +""" +import os + +try: + from PyQt5.QtCore import QPointF, Qt, QRectF, QSizeF + from PyQt5.QtGui import QStandardItem, QStandardItemModel, QPainter, QColor, QImage, QPixmap + from PyQt5.QtWidgets import QApplication, QListView, QGraphicsView, QGraphicsPixmapItem, QGraphicsScene +except ImportError: + from PySide2.QtCore import QPointF, Qt, QRectF, QSizeF + from PySide2.QtGui import QStandardItem, QStandardItemModel, QPainter, QColor, QImage, QPixmap + from PySide2.QtWidgets import QApplication, QListView, QGraphicsView, QGraphicsPixmapItem, QGraphicsScene + +ScrollPixel = 40 + + +class BigImageView(QGraphicsView): + """图片查看控件""" + + def __init__(self, *args, **kwargs): + image = kwargs.pop('image', None) + background = kwargs.pop('background', None) + super(BigImageView, self).__init__(*args, **kwargs) + self.setCursor(Qt.OpenHandCursor) + self.setBackground(background) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | + QPainter.SmoothPixmapTransform) + self.setCacheMode(self.CacheBackground) + self.setViewportUpdateMode(self.SmartViewportUpdate) + self._item = QGraphicsPixmapItem() # 放置图像 + self._item.setFlags(QGraphicsPixmapItem.ItemIsFocusable | + QGraphicsPixmapItem.ItemIsMovable) + self._scene = QGraphicsScene(self) # 场景 + self.setScene(self._scene) + self._scene.addItem(self._item) + rect = QApplication.instance().desktop().availableGeometry() + self.resize(int(rect.width() * 2 / 3), int(rect.height() * 2 / 3)) + + self.pixmap = None + self._delta = 0.1 # 缩放 + self.setPixmap(image) + + def setBackground(self, color): + """设置背景颜色 + :param color: 背景颜色 + :type color: QColor or str or GlobalColor + """ + if isinstance(color, QColor): + self.setBackgroundBrush(color) + elif isinstance(color, (str, Qt.GlobalColor)): + color = QColor(color) + if color.isValid(): + self.setBackgroundBrush(color) + + def setPixmap(self, pixmap, fitIn=True): + """加载图片 + :param pixmap: 图片或者图片路径 + :param fitIn: 是否适应 + :type pixmap: QPixmap or QImage or str + :type fitIn: bool + """ + if isinstance(pixmap, QPixmap): + self.pixmap = pixmap + elif isinstance(pixmap, QImage): + self.pixmap = QPixmap.fromImage(pixmap) + elif isinstance(pixmap, str) and os.path.isfile(pixmap): + self.pixmap = QPixmap(pixmap) + else: + return + self._item.setPixmap(self.pixmap) + self._item.update() + self.setSceneDims() + if fitIn: + self.fitInView(QRectF(self._item.pos(), QSizeF( + self.pixmap.size())), Qt.KeepAspectRatio) + self.update() + + def setSceneDims(self): + if not self.pixmap: + return + self.setSceneRect(QRectF(QPointF(0, 0), QPointF(self.pixmap.width(), self.pixmap.height()))) + + def fitInView(self, rect, flags=Qt.IgnoreAspectRatio): + """剧中适应 + :param rect: 矩形范围 + :param flags: + :return: + """ + if not self.scene() or rect.isNull(): + return + unity = self.transform().mapRect(QRectF(0, 0, 1, 1)) + self.scale(1 / unity.width(), 1 / unity.height()) + viewRect = self.viewport().rect() + sceneRect = self.transform().mapRect(rect) + x_ratio = viewRect.width() / sceneRect.width() + y_ratio = viewRect.height() / sceneRect.height() + if flags == Qt.KeepAspectRatio: + x_ratio = y_ratio = min(x_ratio, y_ratio) + elif flags == Qt.KeepAspectRatioByExpanding: + x_ratio = y_ratio = max(x_ratio, y_ratio) + self.scale(x_ratio, y_ratio) + self.centerOn(rect.center()) + + def wheelEvent(self, event): + if event.angleDelta().y() > 0: + self.zoomIn() + else: + self.zoomOut() + + def zoomIn(self): + """放大""" + self.zoom(1 + self._delta) + + def zoomOut(self): + """缩小""" + self.zoom(1 - self._delta) + + def zoom(self, factor): + """缩放 + :param factor: 缩放的比例因子 + """ + _factor = self.transform().scale( + factor, factor).mapRect(QRectF(0, 0, 1, 1)).width() + if _factor < 0.07 or _factor > 100: + # 防止过大过小 + return + self.scale(factor, factor) + + +class ImageView(QListView): + + def __init__(self, *args, **kwargs): + super(ImageView, self).__init__(*args, **kwargs) + self.setFrameShape(self.NoFrame) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setEditTriggers(self.NoEditTriggers) + self.setDropIndicatorShown(True) + self.setDragDropMode(self.DragDrop) + self.setDefaultDropAction(Qt.IgnoreAction) + self.setSelectionMode(self.ExtendedSelection) + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setHorizontalScrollMode(self.ScrollPerPixel) + self.setFlow(self.LeftToRight) + self.setWrapping(True) + self.setResizeMode(self.Adjust) + self.setSpacing(6) + self.setViewMode(self.IconMode) + self.setWordWrap(True) + self.setSelectionRectVisible(True) + self.setContextMenuPolicy(Qt.CustomContextMenu) + # 解决拖动到顶部或者底部自动滚动 + self.setAutoScrollMargin(150) + self.verticalScrollBar().setSingleStep(ScrollPixel) + # 设置model + self.dmodel = QStandardItemModel(self) + self.setModel(self.dmodel) + + # 大图控件 + self.bigView = BigImageView(background='#323232') + + def addItem(self, image): + if isinstance(image, str): + image = QPixmap(image) + # 添加一个item + item = QStandardItem() + # 记录原始图片 + item.setData(image, Qt.UserRole + 1) # 用于双击的时候取出来 + # 缩放成小图并显示 + item.setData(image.scaled(60, 60, Qt.IgnoreAspectRatio, Qt.SmoothTransformation), Qt.DecorationRole) + # 添加item到界面中 + self.dmodel.appendRow(item) + + def count(self): + return self.dmodel.rowCount() + + def setCurrentRow(self, row): + self.setCurrentIndex(self.dmodel.index(row, 0)) + + def currentRow(self): + return self.currentIndex().row() + + def updateGeometries(self): + # 一次滑动20px + super(ImageView, self).updateGeometries() + self.verticalScrollBar().setSingleStep(ScrollPixel) + + def closeEvent(self, event): + # 关闭预览窗口 + self.bigView.close() + super(ImageView, self).closeEvent(event) + + def wheelEvent(self, event): + # 修复滑动bug + if self.flow() == QListView.LeftToRight: + bar = self.horizontalScrollBar() + value = ScrollPixel if event.angleDelta().y() < 0 else (0 - ScrollPixel) + bar.setSliderPosition(bar.value() + value) + else: + super(ImageView, self).wheelEvent(event) + + def mouseDoubleClickEvent(self, event): + # 列表双击,如果有item则进入item处理流程,否则调用打开图片功能 + index = self.indexAt(event.pos()) + if index and index.isValid(): + item = self.dmodel.itemFromIndex(index) + if item: + # 取出原图用来新窗口显示 + image = item.data(Qt.UserRole + 1) + self.bigView.setPixmap(image) + self.bigView.show() + return + super(ImageView, self).mouseDoubleClickEvent(event) + + +if __name__ == '__main__': + import sys + import cgitb + + cgitb.enable(format='text') + + app = QApplication(sys.argv) + w = ImageView() + w.show() + + # 添加模拟图片 + for i in range(3): + for name in os.listdir('ScreenShot'): + w.addItem(os.path.join('ScreenShot', name)) + sys.exit(app.exec_()) diff --git a/QListView/README.md b/QListView/README.md index 95a16ef9..c70c393d 100644 --- a/QListView/README.md +++ b/QListView/README.md @@ -6,6 +6,7 @@ - [自定义角色排序](#3自定义角色排序) ## 1、显示自定义Widget + [运行 CustomWidgetItem.py](CustomWidgetItem.py) 通过设置 `setIndexWidget(QModelIndex, QWidget)` 可以设置自定义 `QWidget` @@ -13,6 +14,7 @@ ![CustomWidgetItem](ScreenShot/CustomWidgetItem.png) ## 2、显示自定义Widget并排序 + [运行 CustomWidgetSortItem.py](CustomWidgetSortItem.py) 1. 对QListView设置代理 `QSortFilterProxyModel` @@ -21,9 +23,11 @@ ![CustomWidgetSortItem](ScreenShot/CustomWidgetSortItem.gif) ## 3、自定义角色排序 + [运行 SortItemByRole.py](SortItemByRole.py) 需求: + 1. 5种分类(唐、宋、元、明、清) 和 未分类 2. 选中唐则按照 唐、宋、元、明、清、未分类排序 3. 选中宋则按照 宋、唐、元、明、清、未分类排序 @@ -31,9 +35,11 @@ 5. 取消排序则恢复到加载时候顺序,如:未分类、唐、唐、明、清、未分类、宋、元、未分类 思路: + 1. 定义`IdRole = Qt.UserRole + 1` 用于恢复默认排序 2. 定义`ClassifyRole = Qt.UserRole + 2` 用于按照分类序号排序 3. 定义5种分类的id + ```python NameDict = { '唐': ['Tang', 0], @@ -50,14 +56,18 @@ 4: '清', } ``` + 4. item设置 `setData(id, IdRole)` 用于恢复默认排序 5. item设置 `setData(cid, ClassifyRole)` 用于标识该item的分类 6. 继承 `QSortFilterProxyModel` 增加 `setSortIndex(self, index)` 方法, 目的在于记录要置顶(不参与排序)的分类ID + ```python def setSortIndex(self, index): self._topIndex = index ``` + 7. 继承 `QSortFilterProxyModel` 重写 `lessThan` 方法, 判断分类ID是否等于要置顶的ID, 如果是则修改为-1, 这样就永远在最前面 + ```python if self.sortRole() == ClassifyRole and \ source_left.column() == self.sortColumn() and \ @@ -76,12 +86,16 @@ return leftIndex < rightIndex ``` + 8. 恢复默认排序 + ```python self.fmodel.setSortRole(IdRole) # 必须设置排序角色为ID self.fmodel.sort(0) # 排序第一列按照ID升序 ``` + 9. 根据分类排序, 这里要注意要先通过 `setSortRole` 设置其它角色再设置目标角色 + ```python self.fmodel.setSortIndex(1) self.fmodel.setSortRole(IdRole) @@ -89,4 +103,4 @@ self.fmodel.sort(0) ``` -![SortItemByRole](ScreenShot/SortItemByRole.gif) \ No newline at end of file +![SortItemByRole](ScreenShot/SortItemByRole.gif) diff --git a/QListView/SortItemByRole.py b/QListView/SortItemByRole.py index 269bd6f8..d4ccd876 100644 --- a/QListView/SortItemByRole.py +++ b/QListView/SortItemByRole.py @@ -4,23 +4,21 @@ """ Created on 2018年12月27日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QListView.SortItemByRole @description: """ from random import choice -from PyQt5.QtCore import QSortFilterProxyModel, Qt -from PyQt5.QtGui import QStandardItem, QStandardItemModel -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QListView, QPushButton - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QSortFilterProxyModel, Qt + from PyQt5.QtGui import QStandardItem, QStandardItemModel + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QListView, QPushButton +except ImportError: + from PySide2.QtCore import QSortFilterProxyModel, Qt + from PySide2.QtGui import QStandardItem, QStandardItemModel + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QListView, QPushButton class SortFilterProxyModel(QSortFilterProxyModel): @@ -72,8 +70,8 @@ def lessThan(self, source_left, source_right): 4: '清', } -IdRole = Qt.UserRole + 1 # 用于恢复排序 -ClassifyRole = Qt.UserRole + 2 # 用于按照分类序号排序 +IdRole = Qt.UserRole + 1 # 用于恢复排序 +ClassifyRole = Qt.UserRole + 2 # 用于按照分类序号排序 class Window(QWidget): @@ -96,8 +94,8 @@ def __init__(self, *args, **kwargs): def restoreSort(self): # 恢复默认排序 - self.fmodel.setSortRole(IdRole) # 必须设置排序角色为ID - self.fmodel.sort(0) # 排序第一列按照ID升序 + self.fmodel.setSortRole(IdRole) # 必须设置排序角色为ID + self.fmodel.sort(0) # 排序第一列按照ID升序 def sortByClassify(self): self.fmodel.setSortIndex(NameDict.get( @@ -141,8 +139,9 @@ def _initItems(self): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, '') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QListWidget/Data/CoverItemWidget.ui b/QListWidget/Data/CoverItemWidget.ui new file mode 100644 index 00000000..2f245e13 --- /dev/null +++ b/QListWidget/Data/CoverItemWidget.ui @@ -0,0 +1,85 @@ + + + CoverItemWidget + + + + 200 + 256 + + + + + 200 + 256 + + + + + + + + 12 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + 180 + 180 + + + + + + + true + + + Qt::AlignCenter + + + + + + + + 10 + + + + + + + + + + + + + + + + + + + CoverLabel + QLabel +
.CoverLabel
+
+
+ + +
diff --git a/QListWidget/Data/CoverLabel.ui b/QListWidget/Data/CoverLabel.ui new file mode 100644 index 00000000..2e7e0378 --- /dev/null +++ b/QListWidget/Data/CoverLabel.ui @@ -0,0 +1,109 @@ + + + CoverLabel + + + + 0 + 0 + 180 + 180 + + + + PointingHandCursor + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 135 + + + + + + + + #widgetBottom { + background-color: rgba(0, 0, 0, 150); +} + + + + + + + 16 + 16 + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 16 + 16 + + + + + + + + + + + + + + + diff --git a/QListWidget/Data/Svg_icon_headset_sm.svg b/QListWidget/Data/Svg_icon_headset_sm.svg new file mode 100644 index 00000000..98a1a359 --- /dev/null +++ b/QListWidget/Data/Svg_icon_headset_sm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QListWidget/Data/Svg_icon_loading.svg b/QListWidget/Data/Svg_icon_loading.svg new file mode 100644 index 00000000..0b00b799 --- /dev/null +++ b/QListWidget/Data/Svg_icon_loading.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/QListWidget/Data/Svg_icon_play_sm.svg b/QListWidget/Data/Svg_icon_play_sm.svg new file mode 100644 index 00000000..08a47f20 --- /dev/null +++ b/QListWidget/Data/Svg_icon_play_sm.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/QListWidget/DeleteCustomItem.py b/QListWidget/DeleteCustomItem.py index a2d49330..ab8516e0 100644 --- a/QListWidget/DeleteCustomItem.py +++ b/QListWidget/DeleteCustomItem.py @@ -4,25 +4,23 @@ """ Created on 2018年11月4日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: 删除Item @description: """ -from PyQt5.QtCore import QSize, pyqtSignal -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLineEdit, QPushButton,\ - QListWidgetItem, QVBoxLayout, QListWidget - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QSize, pyqtSignal + from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLineEdit, QPushButton, \ + QListWidgetItem, QVBoxLayout, QListWidget, QApplication +except ImportError: + from PySide2.QtCore import QSize, Signal as pyqtSignal + from PySide2.QtWidgets import QWidget, QHBoxLayout, QLineEdit, QPushButton, \ + QListWidgetItem, QVBoxLayout, QListWidget, QApplication class ItemWidget(QWidget): - itemDeleted = pyqtSignal(QListWidgetItem) def __init__(self, text, item, *args, **kwargs): @@ -91,8 +89,9 @@ def testData(self): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, 'text') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QListWidget/DragDrop.py b/QListWidget/DragDrop.py index dcc8818c..0df4479e 100644 --- a/QListWidget/DragDrop.py +++ b/QListWidget/DragDrop.py @@ -4,21 +4,20 @@ """ Created on 2018年9月14日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: DragListWidget @description: """ -from PyQt5.QtCore import Qt, QSize, QRect, QPoint -from PyQt5.QtGui import QColor, QPixmap, QDrag, QPainter, QCursor -from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QLabel, QRubberBand - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt, QSize, QRect, QPoint + from PyQt5.QtGui import QColor, QPixmap, QDrag, QPainter, QCursor + from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QLabel, QRubberBand, QApplication +except ImportError: + from PySide2.QtCore import Qt, QSize, QRect, QPoint + from PySide2.QtGui import QColor, QPixmap, QDrag, QPainter, QCursor + from PySide2.QtWidgets import QListWidget, QListWidgetItem, QLabel, QRubberBand, QApplication class DropListWidget(QListWidget): @@ -162,7 +161,7 @@ def initItems(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) app.setStyleSheet("""QListWidget { outline: 0px; diff --git a/QListWidget/FoldWidget.py b/QListWidget/FoldWidget.py index d522c95b..7f75a8d8 100644 --- a/QListWidget/FoldWidget.py +++ b/QListWidget/FoldWidget.py @@ -4,22 +4,17 @@ """ Created on 2019年5月27日 @author: Irony -@site: https://pyqt5.com https://github.com/PyQt5 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: FoldWidget @description: 自定义item折叠控件仿QTreeWidget """ from PyQt5.QtCore import QSize -from PyQt5.QtWidgets import QWidget, QPushButton, QFormLayout,\ +from PyQt5.QtWidgets import QWidget, QPushButton, QFormLayout, \ QLineEdit, QListWidget, QListWidgetItem, QCheckBox -__Author__ = "Irony" -__Copyright__ = "Copyright (c) 2019" -__Version__ = "Version 1.0" - - class CustomWidget(QWidget): def __init__(self, item, *args, **kwargs): @@ -79,8 +74,10 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, '') + + cgitb.enable(format='text') from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) # 通过qss改变按钮的高度 app.setStyleSheet('#testBtn{min-height:40px;}') diff --git a/QListWidget/HotPlaylist.py b/QListWidget/HotPlaylist.py index bcba0d65..e457b5cb 100644 --- a/QListWidget/HotPlaylist.py +++ b/QListWidget/HotPlaylist.py @@ -1,195 +1,45 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -''' -Created on 2018年2月4日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 @email: 892768447@qq.com -@file: TencentMovieHotPlay_ListWidget -@description: -''' +@file: HotPlaylist.py +@description: +""" + import os import sys -import webbrowser - -from PyQt5.QtCore import QSize, Qt, QUrl, QTimer -from PyQt5.QtGui import QPainter, QFont, QLinearGradient, QGradient, QColor,\ - QBrush, QPaintEvent, QPixmap -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest -from PyQt5.QtSvg import QSvgWidget -from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QLabel,\ - QHBoxLayout, QSpacerItem, QSizePolicy, QAbstractSlider,\ - QListWidget, QListWidgetItem +from Lib.CoverItemWidget import CoverItemWidget from lxml.etree import HTML # @UnresolvedImport +try: + from PyQt5.QtCore import QTimer, QUrl + from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PyQt5.QtSvg import QSvgWidget + from PyQt5.QtWidgets import (QAbstractSlider, QApplication, QListWidget, + QListWidgetItem) +except ImportError: + from PySide2.QtCore import QTimer, QUrl + from PySide2.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PySide2.QtSvg import QSvgWidget + from PySide2.QtWidgets import (QAbstractSlider, QApplication, QListWidget, + QListWidgetItem) -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +# offset=0,35,70,105 +Url = "https://music.163.com/discover/playlist/?order=hot&cat=%E5%85%A8%E9%83%A8&limit=35&offset={0}" -# offset=0,30,60,90 -Url = "http://v.qq.com/x/list/movie?pay=-1&offset={0}" +Agent = b"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50" -# 播放量图标 -Svg_icon_play_sm = ''' - - -'''.encode() +Referer = b"https://music.163.com/" -Svg_icon_loading = ''' - - - - - - - - - - - - - - - - - -'''.encode() - -# 主演 +# 作者 Actor = '''{title} ''' -class CoverLabel(QLabel): - - def __init__(self, cover_path, cover_title, video_url, *args, **kwargs): - super(CoverLabel, self).__init__(*args, **kwargs) -# super(CoverLabel, self).__init__( -# ''.format(os.path.abspath(cover_path)), *args, **kwargs) - self.setCursor(Qt.PointingHandCursor) - self.setScaledContents(True) - self.setMinimumSize(220, 308) - self.setMaximumSize(220, 308) - self.cover_path = cover_path - self.cover_title = cover_title - self.video_url = video_url - self.setPixmap(QPixmap(cover_path)) - - def setCoverPath(self, path): - self.cover_path = path - - def mouseReleaseEvent(self, event): - super(CoverLabel, self).mouseReleaseEvent(event) - webbrowser.open_new_tab(self.video_url) - - def paintEvent(self, event): - super(CoverLabel, self).paintEvent(event) - if hasattr(self, "cover_title") and self.cover_title != "": - # 底部绘制文字 - painter = QPainter(self) - rect = self.rect() - # 粗略字体高度 - painter.save() - fheight = self.fontMetrics().height() - # 底部矩形框背景渐变颜色 - bottomRectColor = QLinearGradient( - rect.width() / 2, rect.height() - 24 - fheight, - rect.width() / 2, rect.height()) - bottomRectColor.setSpread(QGradient.PadSpread) - bottomRectColor.setColorAt(0, QColor(255, 255, 255, 70)) - bottomRectColor.setColorAt(1, QColor(0, 0, 0, 50)) - # 画半透明渐变矩形框 - painter.setPen(Qt.NoPen) - painter.setBrush(QBrush(bottomRectColor)) - painter.drawRect(rect.x(), rect.height() - 24 - - fheight, rect.width(), 24 + fheight) - painter.restore() - # 距离底部一定高度画文字 - font = self.font() or QFont() - font.setPointSize(8) - painter.setFont(font) - painter.setPen(Qt.white) - rect.setHeight(rect.height() - 12) # 底部减去一定高度 - painter.drawText(rect, Qt.AlignHCenter | - Qt.AlignBottom, self.cover_title) - - -class ItemWidget(QWidget): - - def __init__(self, cover_path, figure_info, figure_title, - figure_score, figure_desc, figure_count, video_url, cover_url, img_path, manager, *args, **kwargs): - super(ItemWidget, self).__init__(*args, **kwargs) - self.setMaximumSize(220, 420) - self.setMaximumSize(220, 420) - self.img_path = img_path - self.cover_url = cover_url - self._manager = manager - layout = QVBoxLayout(self) - layout.setContentsMargins(10, 20, 10, 0) - # 图片label - self.clabel = CoverLabel(cover_path, figure_info, video_url, self) - layout.addWidget(self.clabel) - - # 片名和分数 - flayout = QHBoxLayout() - flayout.addWidget(QLabel(figure_title, self)) - flayout.addItem(QSpacerItem( - 20, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) - flayout.addWidget(QLabel(figure_score, self, styleSheet="color: red;")) - layout.addLayout(flayout) - - # 主演 - layout.addWidget( - QLabel(figure_desc, self, styleSheet="color: #999999;", openExternalLinks=True)) - - # 播放量 - blayout = QHBoxLayout() - count_icon = QSvgWidget(self) - count_icon.setMaximumSize(16, 16) - count_icon.load(Svg_icon_play_sm) - blayout.addWidget(count_icon) - blayout.addWidget( - QLabel(figure_count, self, styleSheet="color: #999999;")) - layout.addLayout(blayout) - - def setCover(self, path): - self.clabel.setCoverPath(path) - self.clabel.setPixmap(QPixmap(path)) -# self.clabel.setText(''.format(os.path.abspath(path))) - - def sizeHint(self): - # 每个item控件的大小 - return QSize(220, 420) - - def event(self, event): - if isinstance(event, QPaintEvent): - if event.rect().height() > 20 and hasattr(self, "clabel"): - if self.clabel.cover_path.find("pic_v.png") > -1: # 封面未加载 - # print("start download img:", self.cover_url) - req = QNetworkRequest(QUrl(self.cover_url)) - # 设置两个自定义属性方便后期reply中处理 - req.setAttribute(QNetworkRequest.User + 1, self) - req.setAttribute(QNetworkRequest.User + 2, self.img_path) - self._manager.get(req) # 调用父窗口中的下载器下载 - return super(ItemWidget, self).event(event) - - class Window(QListWidget): - Page = 0 def __init__(self, *args, **kwargs): @@ -204,16 +54,18 @@ def __init__(self, *args, **kwargs): # 连接竖着的滚动条滚动事件 self.verticalScrollBar().actionTriggered.connect(self.onActionTriggered) # 进度条 - self.loadWidget = QSvgWidget( - self, minimumHeight=120, minimumWidth=120, visible=False) - self.loadWidget.load(Svg_icon_loading) + self.loadWidget = QSvgWidget(self, + minimumHeight=120, + minimumWidth=120, + visible=False) + self.loadWidget.load('Data/Svg_icon_loading.svg') # 异步网络下载管理器 self._manager = QNetworkAccessManager(self) self._manager.finished.connect(self.onFinished) def load(self): - if self.Page == -1: + if self.Page == -1 or self.Page > 10: return self._loadStart = True self.loadWidget.setVisible(True) @@ -221,9 +73,11 @@ def load(self): QTimer.singleShot(1000, self._load) def _load(self): - print("load url:", Url.format(self.Page * 30)) - url = QUrl(Url.format(self.Page * 30)) - self._manager.get(QNetworkRequest(url)) + print("load url:", Url.format(self.Page * 35)) + url = QUrl(Url.format(self.Page * 35)) + req = QNetworkRequest(url) + req.setRawHeader(b"User-Agent", Agent) + self._manager.get(req) def onFinished(self, reply): # 请求完成后会调用该函数 @@ -244,11 +98,13 @@ def onFinished(self, reply): self.loadWidget.setVisible(False) def _parseHtml(self, html): + # print(html) # encoding = chardet.detect(html) or {} # html = html.decode(encoding.get("encoding","utf-8")) html = HTML(html) # 查找所有的li list_item - lis = html.xpath("//li[@class='list_item']") + lis = html.xpath("//ul[@id='m-pl-container']/li") + # print(lis) if not lis: self.Page = -1 # 后面没有页面了 return @@ -257,28 +113,30 @@ def _parseHtml(self, html): def _makeItem(self, lis): for li in lis: - a = li.find("a") - video_url = a.get("href") # 视频播放地址 - img = a.find("img") - cover_url = "http:" + img.get("r-lazyload") # 封面图片 - figure_title = img.get("alt") # 电影名 - figure_info = a.find("div/span") - figure_info = "" if figure_info is None else figure_info.text # 影片信息 - figure_score = "".join(li.xpath(".//em/text()")) # 评分 - # 主演 - figure_desc = "主演:" + \ - "".join([Actor.format(**dict(fd.items())) - for fd in li.xpath(".//div[@class='figure_desc']/a")]) + a = li.find('.//div/a') + play_url = "https://music.163.com/" + a.get("href") # 歌单播放地址 + img = li.find(".//div/img") + cover_url = img.get("src") # 封面图片 + playlist_title = a.get("title") # 歌单名 + # 歌手 + author_info = li.xpath(".//p[2]/a")[0] + playlist_author = "".format( + Actor.format(href="https://music.163.com/" + + author_info.get("href"), + title=author_info.get("title"))) # 播放数 - figure_count = ( - li.xpath(".//div[@class='figure_count']/span/text()") or [""])[0] + play_count = (li.xpath(".//div/div/span[2]/text()") or [""])[0] path = "cache/{0}.jpg".format( - os.path.splitext(os.path.basename(video_url))[0]) + os.path.splitext(os.path.basename(cover_url).split('?')[0])[0]) cover_path = "Data/pic_v.png" if os.path.isfile(path): cover_path = path - iwidget = ItemWidget(cover_path, figure_info, figure_title, - figure_score, figure_desc, figure_count, video_url, cover_url, path, self._manager, self) + + # print(cover_path, playlist_title, + # playlist_author, play_count, play_url, cover_url, path) + iwidget = CoverItemWidget(self, manager=self._manager) + iwidget.init(cover_path, playlist_title, playlist_author, + play_count, play_url, cover_url, path) item = QListWidgetItem(self) item.setSizeHint(iwidget.sizeHint()) self.setItemWidget(item, iwidget) @@ -289,7 +147,8 @@ def onActionTriggered(self, action): if action != QAbstractSlider.SliderMove or self._loadStart: return # 使用sliderPosition获取值可以同时满足鼠标滑动和拖动判断 - if self.verticalScrollBar().sliderPosition() == self.verticalScrollBar().maximum(): + if self.verticalScrollBar().sliderPosition() == self.verticalScrollBar( + ).maximum(): # 可以下一页了 self.load() @@ -298,9 +157,7 @@ def resizeEvent(self, event): self.loadWidget.setGeometry( int((self.width() - self.loadWidget.minimumWidth()) / 2), int((self.height() - self.loadWidget.minimumHeight()) / 2), - self.loadWidget.minimumWidth(), - self.loadWidget.minimumHeight() - ) + self.loadWidget.minimumWidth(), self.loadWidget.minimumHeight()) if __name__ == "__main__": diff --git a/QListWidget/Lib/CoverItemWidget.py b/QListWidget/Lib/CoverItemWidget.py new file mode 100644 index 00000000..5ef3ff00 --- /dev/null +++ b/QListWidget/Lib/CoverItemWidget.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CoverItemWidget.py +@description: +""" + +try: + from PyQt5.QtCore import QSize, QUrl + from PyQt5.QtGui import QPaintEvent, QPixmap + from PyQt5.QtNetwork import QNetworkRequest + from PyQt5.QtWidgets import QWidget +except ImportError: + from PySide2.QtCore import QSize, QUrl + from PySide2.QtGui import QPaintEvent, QPixmap + from PySide2.QtNetwork import QNetworkRequest + from PySide2.QtWidgets import QWidget + +from .Ui_CoverItemWidget import Ui_CoverItemWidget # @UnresolvedImport + + +class CoverItemWidget(QWidget, Ui_CoverItemWidget): + + def __init__(self, *args, **kwargs): + self._manager = kwargs.pop('manager', None) + super(CoverItemWidget, self).__init__(*args, **kwargs) + self.setupUi(self) + + def init(self, cover_path, playlist_title, playlist_author, play_count, + play_url, cover_url, img_path): + self.img_path = img_path + self.cover_url = cover_url + # 图片label + self.labelCover.init(cover_path, play_url, play_count) + + # 歌单 + self.labelTitle.setText(playlist_title) + + # 作者 + self.labelAuthor.setText(playlist_author) + + def setCover(self, path): + self.labelCover.setCoverPath(path) + self.labelCover.setPixmap(QPixmap(path)) + + def sizeHint(self): + # 每个item控件的大小 + return QSize(200, 256) + + def event(self, event): + if isinstance(event, QPaintEvent): + if event.rect().height() > 20 and hasattr(self, "labelCover"): + if self.labelCover.cover_path.find("pic_v.png") > -1: # 封面未加载 + # print("start download img:", self.cover_url) + req = QNetworkRequest(QUrl(self.cover_url)) + # 设置两个自定义属性方便后期reply中处理 + req.setAttribute(QNetworkRequest.User + 1, self) + req.setAttribute(QNetworkRequest.User + 2, self.img_path) + if self._manager: + self._manager.get(req) # 调用父窗口中的下载器下载 + return super(CoverItemWidget, self).event(event) diff --git a/QListWidget/Lib/CoverLabel.py b/QListWidget/Lib/CoverLabel.py new file mode 100644 index 00000000..dbee15af --- /dev/null +++ b/QListWidget/Lib/CoverLabel.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/22 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CoverLabel.py +@description: +""" +import webbrowser + +try: + from PyQt5.QtGui import QPixmap + from PyQt5.QtWidgets import QLabel +except ImportError: + from PySide2.QtGui import QPixmap + from PySide2.QtWidgets import QLabel + +from .Ui_CoverLabel import Ui_CoverLabel # @UnresolvedImport + + +class CoverLabel(QLabel, Ui_CoverLabel): + + def __init__(self, *args, **kwargs): + super(CoverLabel, self).__init__(*args, **kwargs) + self.setupUi(self) + + def init(self, cover_path, play_url, play_count): + self.cover_path = cover_path + self.play_url = play_url + self.setPixmap(QPixmap(cover_path)) + self.labelHeadset.setPixmap(QPixmap('Data/Svg_icon_headset_sm.svg')) + self.labelPlay.setPixmap(QPixmap('Data/Svg_icon_play_sm.svg')) + self.labelCount.setStyleSheet('color: #999999;') + self.labelCount.setText(play_count) + + def setCoverPath(self, path): + self.cover_path = path + + def mouseReleaseEvent(self, event): + super(CoverLabel, self).mouseReleaseEvent(event) + webbrowser.open_new_tab(self.play_url) diff --git a/QListWidget/Lib/Ui_CoverItemWidget.py b/QListWidget/Lib/Ui_CoverItemWidget.py new file mode 100644 index 00000000..c048f686 --- /dev/null +++ b/QListWidget/Lib/Ui_CoverItemWidget.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CoverItemWidget.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# 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_CoverItemWidget(object): + def setupUi(self, CoverItemWidget): + CoverItemWidget.setObjectName("CoverItemWidget") + CoverItemWidget.setMinimumSize(QtCore.QSize(200, 256)) + CoverItemWidget.setMaximumSize(QtCore.QSize(200, 256)) + CoverItemWidget.setWindowTitle("") + self.verticalLayout = QtWidgets.QVBoxLayout(CoverItemWidget) + self.verticalLayout.setContentsMargins(10, 10, 10, 10) + self.verticalLayout.setSpacing(12) + self.verticalLayout.setObjectName("verticalLayout") + self.labelCover = CoverLabel(CoverItemWidget) + self.labelCover.setMinimumSize(QtCore.QSize(180, 180)) + self.labelCover.setText("") + self.labelCover.setScaledContents(True) + self.labelCover.setAlignment(QtCore.Qt.AlignCenter) + self.labelCover.setObjectName("labelCover") + self.verticalLayout.addWidget(self.labelCover) + self.labelTitle = QtWidgets.QLabel(CoverItemWidget) + font = QtGui.QFont() + font.setPointSize(10) + self.labelTitle.setFont(font) + self.labelTitle.setText("") + self.labelTitle.setObjectName("labelTitle") + self.verticalLayout.addWidget(self.labelTitle) + self.labelAuthor = QtWidgets.QLabel(CoverItemWidget) + self.labelAuthor.setText("") + self.labelAuthor.setObjectName("labelAuthor") + self.verticalLayout.addWidget(self.labelAuthor) + + self.retranslateUi(CoverItemWidget) + QtCore.QMetaObject.connectSlotsByName(CoverItemWidget) + + def retranslateUi(self, CoverItemWidget): + pass +from .CoverLabel import CoverLabel + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + CoverItemWidget = QtWidgets.QWidget() + ui = Ui_CoverItemWidget() + ui.setupUi(CoverItemWidget) + CoverItemWidget.show() + sys.exit(app.exec_()) diff --git a/QListWidget/Lib/Ui_CoverLabel.py b/QListWidget/Lib/Ui_CoverLabel.py new file mode 100644 index 00000000..86e1526a --- /dev/null +++ b/QListWidget/Lib/Ui_CoverLabel.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CoverLabel.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# 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_CoverLabel(object): + def setupUi(self, CoverLabel): + CoverLabel.setObjectName("CoverLabel") + CoverLabel.resize(180, 180) + CoverLabel.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + CoverLabel.setWindowTitle("") + self.verticalLayout = QtWidgets.QVBoxLayout(CoverLabel) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName("verticalLayout") + spacerItem = QtWidgets.QSpacerItem(20, 135, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.widgetBottom = QtWidgets.QWidget(CoverLabel) + self.widgetBottom.setStyleSheet("#widgetBottom {\n" +" background-color: rgba(0, 0, 0, 150);\n" +"}") + self.widgetBottom.setObjectName("widgetBottom") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widgetBottom) + self.horizontalLayout.setObjectName("horizontalLayout") + self.labelHeadset = QtWidgets.QLabel(self.widgetBottom) + self.labelHeadset.setMinimumSize(QtCore.QSize(16, 16)) + self.labelHeadset.setText("") + self.labelHeadset.setObjectName("labelHeadset") + self.horizontalLayout.addWidget(self.labelHeadset) + self.labelCount = QtWidgets.QLabel(self.widgetBottom) + self.labelCount.setText("") + self.labelCount.setObjectName("labelCount") + self.horizontalLayout.addWidget(self.labelCount) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.labelPlay = QtWidgets.QLabel(self.widgetBottom) + self.labelPlay.setMinimumSize(QtCore.QSize(16, 16)) + self.labelPlay.setText("") + self.labelPlay.setObjectName("labelPlay") + self.horizontalLayout.addWidget(self.labelPlay) + self.verticalLayout.addWidget(self.widgetBottom) + + self.retranslateUi(CoverLabel) + QtCore.QMetaObject.connectSlotsByName(CoverLabel) + + def retranslateUi(self, CoverLabel): + pass + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + CoverLabel = QtWidgets.QWidget() + ui = Ui_CoverLabel() + ui.setupUi(CoverLabel) + CoverLabel.show() + sys.exit(app.exec_()) diff --git a/QListWidget/Lib/__init__.py b/QListWidget/Lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/QListWidget/README.md b/QListWidget/README.md index a1292098..faaa1ab7 100644 --- a/QListWidget/README.md +++ b/QListWidget/README.md @@ -3,11 +3,12 @@ - 目录 - [删除自定义Item](#1删除自定义Item) - [自定义可拖拽Item](#2自定义可拖拽Item) - - [腾讯视频热播列表](#3腾讯视频热播列表) + - [音乐热歌列表](#3音乐热歌列表) - [仿折叠控件效果](#4仿折叠控件效果) - [列表常用信号](#5列表常用信号) ## 1、删除自定义Item + [运行 DeleteCustomItem.py](DeleteCustomItem.py) 1. 删除item时先要通过`QListWidget.indexFromItem(item).row()`得到它的行数 @@ -18,24 +19,26 @@ ![CustomWidgetItem](ScreenShot/DeleteCustomItem.gif) ## 2、自定义可拖拽Item + [运行 DragDrop.py](DragDrop.py) ![CustomWidgetSortItem](ScreenShot/DragDrop.gif) -## 3、腾讯视频热播列表 +## 3、音乐热歌列表 + [运行 HotPlaylist.py](HotPlaylist.py) 简单思路说明: - - 利用`QListWidget`设置一些特殊的参数达到可以横向自动显示 - - `QNetworkAccessManager`异步下载网页和图片 - - 滚动到底部触发下一页加载 +- 利用`QListWidget`设置一些特殊的参数达到可以横向自动显示 +- `QNetworkAccessManager`异步下载网页和图片 +- 滚动到底部触发下一页加载 自定义控件说明: - - 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 - - 字体颜色用qss设置 - - 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) +- 主要是多个layout和控件的结合,其中图片`QLabel`为自定义,通过`setPixmap`设置图片,重写`paintEvent`绘制底部渐变矩形框和白色文字 +- 字体颜色用qss设置 +- 图标利用了`QSvgWidget`显示,可以是svg 动画(如圆形加载图) `QListWidget`的参数设置 @@ -46,6 +49,7 @@ ![HotPlaylist](ScreenShot/HotPlaylist.gif) ## 4、仿折叠控件效果 + [运行 FoldWidget.py](FoldWidget.py) 1. 利用`QListWidget`设置Item的自定义控件 @@ -56,8 +60,9 @@ ![FoldWidget](ScreenShot/FoldWidget.gif) ## 5、列表常用信号 + [运行 SignalsExample.py](SignalsExample.py) -根据官网文档 https://doc.qt.io/qt-5/qlistwidget.html#signals 中的信号介绍编写 +根据官网文档 中的信号介绍编写 -![SignalsExample](ScreenShot/SignalsExample.gif) \ No newline at end of file +![SignalsExample](ScreenShot/SignalsExample.gif) diff --git a/QListWidget/SignalsExample.py b/QListWidget/SignalsExample.py index bb17e421..38f20c2b 100644 --- a/QListWidget/SignalsExample.py +++ b/QListWidget/SignalsExample.py @@ -4,20 +4,22 @@ """ Created on 2019年7月3日 @author: Irony -@site: https://pyqt5.com https://github.com/PyQt5 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QListWidget.SignalsExample @description: """ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QColor -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QListWidget, QPlainTextEdit,\ - QListWidgetItem, QAbstractItemView, QListView - -__Author__ = "Irony" -__Copyright__ = "Copyright (c) 2019" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QColor + from PyQt5.QtWidgets import QWidget, QHBoxLayout, QListWidget, QPlainTextEdit, \ + QListWidgetItem, QAbstractItemView, QListView, QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QColor + from PySide2.QtWidgets import QWidget, QHBoxLayout, QListWidget, QPlainTextEdit, \ + QListWidgetItem, QAbstractItemView, QListView, QApplication def formatColor(text, color): @@ -118,7 +120,7 @@ def onItemSelectionChanged(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QMenu/MultiSelect.py b/QMenu/MultiSelect.py index 17bf9bd5..31a78c8a 100644 --- a/QMenu/MultiSelect.py +++ b/QMenu/MultiSelect.py @@ -4,20 +4,16 @@ """ Created on 2018年10月24日 @author: Irony -@site: https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: MultiSelect @description: """ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QMenu,\ - QAction - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QMenu, QAction +except ImportError: + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QMenu, QAction class Window(QWidget): @@ -70,8 +66,9 @@ def _checkAction(self): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, 'text') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.resize(400, 400) diff --git a/QMenu/QQMenu.py b/QMenu/QQMenu.py new file mode 100644 index 00000000..a561eb47 --- /dev/null +++ b/QMenu/QQMenu.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/4/7 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: QQMenu +@description: +""" +import string +from random import choice, randint + +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPixmap, QPainter, QFont, QIcon + from PyQt5.QtWidgets import QLabel, QMenu, QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPixmap, QPainter, QFont, QIcon + from PySide2.QtWidgets import QLabel, QMenu, QApplication + +Style = """ +QMenu { + /* 半透明效果 */ + background-color: rgba(255, 255, 255, 230); + border: none; + border-radius: 4px; +} + +QMenu::item { + border-radius: 4px; + /* 这个距离很麻烦需要根据菜单的长度和图标等因素微调 */ + padding: 8px 48px 8px 36px; /* 36px是文字距离左侧距离*/ + background-color: transparent; +} + +/* 鼠标悬停和按下效果 */ +QMenu::item:selected { + border-radius: 0px; + /* 半透明效果 */ + background-color: rgba(232, 232, 232, 232); +} + +/* 禁用效果 */ +QMenu::item:disabled { + background-color: transparent; +} + +/* 图标距离左侧距离 */ +QMenu::icon { + left: 15px; +} + +/* 分割线效果 */ +QMenu::separator { + height: 1px; + background-color: rgb(232, 236, 243); +} +""" + + +def get_icon(): + # 测试模拟图标 + pixmap = QPixmap(16, 16) + pixmap.fill(Qt.transparent) + painter = QPainter() + painter.begin(pixmap) + painter.setFont(QFont('Webdings', 11)) + painter.setPen(Qt.GlobalColor(randint(4, 18))) + painter.drawText(0, 0, 16, 16, Qt.AlignCenter, + choice(string.ascii_letters)) + painter.end() + return QIcon(pixmap) + + +def about_qt(): + # 关于Qt + QApplication.instance().aboutQt() + + +class Window(QLabel): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + self.resize(400, 400) + self.setAlignment(Qt.AlignCenter) + self.setText('右键弹出菜单') + self.context_menu = QMenu(self) + self.init_menu() + + def contextMenuEvent(self, event): + self.context_menu.exec_(event.globalPos()) + + def init_menu(self): + # 背景透明 + self.context_menu.setAttribute(Qt.WA_TranslucentBackground) + # 无边框、去掉自带阴影 + self.context_menu.setWindowFlags( + self.context_menu.windowFlags() | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint) + + # 模拟菜单项 + for i in range(10): + if i % 2 == 0: + action = self.context_menu.addAction('菜单 %d' % i, about_qt) + action.setEnabled(i % 4) + elif i % 3 == 0: + self.context_menu.addAction(get_icon(), '菜单 %d' % i, about_qt) + if i % 4 == 0: + self.context_menu.addSeparator() + if i % 5 == 0: + # 二级菜单 + # 二级菜单 + menu = QMenu('二级菜单 %d' % i, self.context_menu) + # 背景透明 + menu.setAttribute(Qt.WA_TranslucentBackground) + # 无边框、去掉自带阴影 + menu.setWindowFlags(menu.windowFlags() | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint) + for j in range(3): + menu.addAction(get_icon(), '子菜单 %d' % j) + self.context_menu.addMenu(menu) + + +if __name__ == '__main__': + import sys + import cgitb + + cgitb.enable(format='text') + app = QApplication(sys.argv) + app.setStyleSheet(Style) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QMenu/README.md b/QMenu/README.md index 1c00fea6..f3657b91 100644 --- a/QMenu/README.md +++ b/QMenu/README.md @@ -2,8 +2,10 @@ - 目录 - [菜单设置多选并且不关闭](#1菜单设置多选并且不关闭) + - [仿QQ右键菜单](#2仿QQ右键菜单) ## 1、菜单设置多选并且不关闭 + [运行 MultiSelect.py](MultiSelect.py) 有时候会遇到这种需求:在界面某个位置弹出一个菜单,其中里面的菜单项可以多选(类似配置选项), @@ -35,4 +37,10 @@ def _menu_mouseReleaseEvent(self, event): action.activate(action.Trigger) ``` -![MultiSelect](ScreenShot/MultiSelect.gif) \ No newline at end of file +![MultiSelect](ScreenShot/MultiSelect.gif) + +## 2、仿QQ右键菜单 + +[运行 QQMenu.py](QQMenu.py) + +![QQMenu](ScreenShot/QQMenu.gif) diff --git a/QMenu/ScreenShot/QQMenu.gif b/QMenu/ScreenShot/QQMenu.gif new file mode 100644 index 00000000..e989d9dd Binary files /dev/null and b/QMenu/ScreenShot/QQMenu.gif differ diff --git a/QMessageBox/ChineseText.py b/QMessageBox/ChineseText.py index e1e766df..8ba72778 100644 --- a/QMessageBox/ChineseText.py +++ b/QMessageBox/ChineseText.py @@ -4,19 +4,17 @@ """ Created on 2019年7月10日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ChineseText @description: 修改消息对话框文字汉化 """ import sys -from PyQt5.QtWidgets import QApplication, QMessageBox - - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtWidgets import QApplication, QMessageBox +except ImportError: + from PySide2.QtWidgets import QApplication, QMessageBox TextStyle = """ QMessageBox QPushButton[text="OK"] { diff --git a/QMessageBox/CountDownClose.py b/QMessageBox/CountDownClose.py index 2252ce00..e582bb58 100644 --- a/QMessageBox/CountDownClose.py +++ b/QMessageBox/CountDownClose.py @@ -4,22 +4,19 @@ """ Created on 2018年6月22日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: MessageBox @description: """ from random import randrange -from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QMessageBox - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QTimer + from PyQt5.QtWidgets import QApplication, QMessageBox, QPushButton +except ImportError: + from PySide2.QtCore import QTimer + from PySide2.QtWidgets import QApplication, QMessageBox, QPushButton class MessageBox(QMessageBox): @@ -53,7 +50,7 @@ def doCountDown(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication, QPushButton + app = QApplication(sys.argv) w = QPushButton('点击弹出对话框') w.resize(200, 200) diff --git a/QMessageBox/CustomColorIcon.py b/QMessageBox/CustomColorIcon.py index 53d95c13..e41748c3 100644 --- a/QMessageBox/CustomColorIcon.py +++ b/QMessageBox/CustomColorIcon.py @@ -1,23 +1,21 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月17日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CustomBtnIcon @description: -''' - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +""" import sys -from PyQt5.QtWidgets import QApplication, QMessageBox - +try: + from PyQt5.QtWidgets import QApplication, QMessageBox +except ImportError: + from PySide2.QtWidgets import QApplication, QMessageBox app = QApplication(sys.argv) app.setStyleSheet('''QDialogButtonBox { @@ -62,15 +60,15 @@ } ''') QMessageBox.information(None, "information", "消息", - QMessageBox.Apply | - QMessageBox.Cancel | - QMessageBox.Close | - QMessageBox.Discard | - QMessageBox.Help | - QMessageBox.No | - QMessageBox.Ok | - QMessageBox.Open | - QMessageBox.Reset | - QMessageBox.Save | - QMessageBox.Yes) + QMessageBox.Apply | + QMessageBox.Cancel | + QMessageBox.Close | + QMessageBox.Discard | + QMessageBox.Help | + QMessageBox.No | + QMessageBox.Ok | + QMessageBox.Open | + QMessageBox.Reset | + QMessageBox.Save | + QMessageBox.Yes) sys.exit() diff --git a/QMessageBox/README.md b/QMessageBox/README.md index 84a1e914..9342245c 100644 --- a/QMessageBox/README.md +++ b/QMessageBox/README.md @@ -6,6 +6,7 @@ - [消息框按钮文字汉化](#3消息框按钮文字汉化) ## 1、消息对话框倒计时关闭 + [运行 CountDownClose.py](CountDownClose.py) 1. 通过继承`QMessageBox`实现倒计时关闭的对话框 @@ -14,15 +15,17 @@ ![CountDownClose](ScreenShot/CountDownClose.gif) ## 2、自定义图标等 + [运行 CustomColorIcon.py](CustomColorIcon.py) ![CustomColorIcon](ScreenShot/CustomColorIcon.png) ## 3、消息框按钮文字汉化 + [运行 ChineseText.py](ChineseText.py) 1. 因为Qt5的翻译文件还是沿用旧的Qt4的结构导致部分地方无法翻译 2. 可以通过手动重新编译翻译文件解决问题 3. 这里可以通过QSS特性修改按钮文字,详细见代码 -![ChineseText](ScreenShot/ChineseText.png) \ No newline at end of file +![ChineseText](ScreenShot/ChineseText.png) diff --git a/QMetaObject/CallInThread.py b/QMetaObject/CallInThread.py new file mode 100644 index 00000000..f5fdf824 --- /dev/null +++ b/QMetaObject/CallInThread.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/23 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CallInThread.py +@description: +""" +import time +from datetime import datetime +from threading import Thread + +from PyQt5.QtCore import (Q_ARG, Q_RETURN_ARG, QMetaObject, Qt, QThread, + pyqtSignal, pyqtSlot) +from PyQt5.QtWidgets import QApplication, QHBoxLayout, QTextBrowser, QWidget + + +class ThreadQt(QThread): + + def __init__(self, textBrowser, *args, **kwargs): + super(ThreadQt, self).__init__(*args, **kwargs) + self._textBrowser = textBrowser + + def stop(self): + self.requestInterruption() + + def run(self): + while not self.isInterruptionRequested(): + text = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + # 通过`invokeMethod`直接调用对应的槽函数 + # 1. 获取函数`isReadOnly1`返回值, self.parent() 是Window窗口对象 + # NOTE:注意这里获取返回值要用 `Qt.DirectConnection` 方式 + retValue = QMetaObject.invokeMethod(self.parent(), 'isReadOnly1', + Qt.DirectConnection, + Q_RETURN_ARG(bool)) + # 2. 通过`invokeMethod`队列调用对应控件槽函数`append` + argValue = Q_ARG(str, text + ' readOnly: ' + str(retValue)) + QMetaObject.invokeMethod(self._textBrowser, 'append', + Qt.QueuedConnection, argValue) + self.sleep(1) + + +class ThreadPy(Thread): + + def __init__(self, textBrowser, parent, *args, **kwargs): + super(ThreadPy, self).__init__(*args, **kwargs) + self._running = True + self._textBrowser = textBrowser + self._parent = parent + + def stop(self): + self._running = False + + def run(self): + while self._running: + text = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + # 通过`invokeMethod`队列调用对应控件信号,`self._parent`是Window窗口对象 + QMetaObject.invokeMethod(self._parent, 'appendText', + Qt.QueuedConnection, + Q_ARG(str, text + ' from Signal')) + # 通过`invokeMethod`队列调用对应控件槽函数`append` + QMetaObject.invokeMethod(self._textBrowser, 'append', + Qt.QueuedConnection, + Q_ARG(str, text + ' to Slot')) + time.sleep(1) + + +class Window(QWidget): + + # 更新信号 + appendText = pyqtSignal(str) + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + layout = QHBoxLayout(self) + self.textBrowser1 = QTextBrowser(self) + self.textBrowser2 = QTextBrowser(self) + layout.addWidget(self.textBrowser1) + layout.addWidget(self.textBrowser2) + + self.appendText.connect(self.textBrowser2.append) + + # Qt线程 + self.thread1 = ThreadQt(self.textBrowser1, self) + self.thread1.start() + + # PY线程 + self.thread2 = ThreadPy(self.textBrowser2, self) + self.thread2.start() + + @pyqtSlot(result=bool) + def isReadOnly1(self): + # 线程中直接调用该槽函数获取UI中的内容 + return self.textBrowser1.isReadOnly() + + def closeEvent(self, event): + self.thread1.stop() + self.thread2.stop() + self.thread1.wait() + self.thread2.join() + super(Window, self).closeEvent(event) + + +if __name__ == '__main__': + import cgitb + import sys + + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QMetaObject/README.en.md b/QMetaObject/README.en.md new file mode 100644 index 00000000..e69de29b diff --git a/QMetaObject/README.md b/QMetaObject/README.md new file mode 100644 index 00000000..46cf1ed0 --- /dev/null +++ b/QMetaObject/README.md @@ -0,0 +1,25 @@ +# QMetaObject + +- 目录 + - [在线程中操作UI](#1在线程中操作UI) + +## 1、在线程中操作UI + +[运行 CallInThread.py](CallInThread.py) + +如果想在`QThread`或者`threading.Thread`中不通过信号直接操作UI,则可以使用`QMetaObject.invokeMethod`调用。 + +该函数一般有常用的几种调用方法: + +1. 直接调用槽函数:`QMetaObject.invokeMethod(uiobj, 'slot_method', Qt.QueuedConnection)` +2. 直接调用信号:`QMetaObject.invokeMethod(uiobj, 'signal_method', Qt.QueuedConnection)` +3. 调用信号或槽函数并传递参数:`QMetaObject.invokeMethod(uiobj, 'method', Qt.QueuedConnection, Q_ARG(str, 'text'))` +4. 调用槽函数得到返回值:`QMetaObject.invokeMethod(uiobj, 'slot_method', Qt.DirectConnection, Q_RETURN_ARG(str))` +5. 调用带参数的槽函数得到返回值:`QMetaObject.invokeMethod(uiobj, 'slot_method', Qt.DirectConnection, Q_RETURN_ARG(int), Q_ARG(bool, False))`, 传入bool类型的参数,获取int类型返回值 + +这里需要注意: + +1. 调用函数都是异步队列方式,需要使用`Qt.QueuedConnection` +2. 而要得到返回值则必须使用同步方式, 即`Qt.DirectConnection` + +![CallInThread](ScreenShot/CallInThread.png) diff --git a/QMetaObject/ScreenShot/CallInThread.png b/QMetaObject/ScreenShot/CallInThread.png new file mode 100644 index 00000000..62ed7f62 Binary files /dev/null and b/QMetaObject/ScreenShot/CallInThread.png differ diff --git a/QPainter/Data/qt-logo.png b/QPainter/Data/qt-logo.png new file mode 100644 index 00000000..90e6f905 Binary files /dev/null and b/QPainter/Data/qt-logo.png differ diff --git a/QPainter/Draw.py b/QPainter/Draw.py new file mode 100644 index 00000000..c1e8f808 --- /dev/null +++ b/QPainter/Draw.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2022年12月12日 +@site: https://pyqt.site , https://github.com/PyQt5 +@description: QPainter画图 +""" +import sys +try: + from PyQt5.QtWidgets import QApplication, QWidget, qApp + from PyQt5.QtGui import QPainter, QFont, QColor, QPixmap + from PyQt5.QtCore import Qt, pyqtSignal + from PyQt5.Qt import QPoint, QPolygon +except ImportError: + from PySide2.QtWidgets import QApplication, QWidget, qApp + from PySide2.QtGui import QPainter, QFont, QColor, QPixmap + from PySide2.QtCore import Qt, pyqtSignal + from PySide2.Qt import QPoint, QPolygon + + + +class draw(QWidget): + + + drawsig = pyqtSignal(bool) + + def __init__(self): + super(draw, self).__init__() + self.setWindowTitle("QPainter画图") + self._painter = QPainter() + self.scale = 1.0 + self.pixmap = QPixmap() + self.setMouseTracking(True) + self.setFocusPolicy(Qt.WheelFocus) + self.drawEnable = False + self.points = [] + self.current_points = [] + self.drawsig.connect(self.setDrawEnable) + + + def setDrawEnable(self, enable=False): + self.drawEnable = enable + self.update() + + def mouseMoveEvent(self, ev): + if self.drawEnable: + def in_end_range(curr, first): + return first.x() - 5 <= curr.x() <= first.x() + 5 and first.y() - 5 <= curr.y() <= first.y() + 5 + + if len(self.current_points) > 0 and in_end_range(ev.pos(), self.current_points[0]): + self.current_points.append(self.current_points[0]) + self.points.append(self.current_points) + self.current_points = [] + else: + self.current_points.append(ev.pos()) + elif len(self.current_points) > 0: + self.current_points.append(ev.pos()) + self.points.append(self.current_points) + self.current_points = [] + + self.update() + + def mousePressEvent(self, ev): + if Qt.LeftButton & ev.button(): + self.drawsig.emit(True) + def mouseReleaseEvent(self, ev): + if Qt.LeftButton & ev.button(): + self.drawsig.emit(False) + + def paintEvent(self, ev): + if len(self.points) <= 0 and len(self.current_points) <= 0 : return + p = self._painter + p.begin(self) + p.setRenderHint(QPainter.Antialiasing) + p.setRenderHint(QPainter.HighQualityAntialiasing) + p.setRenderHint(QPainter.SmoothPixmapTransform) + p.scale(self.scale, self.scale) + p.setPen(QColor(0, 0, 0)) + for pts in self.points: + p.drawPolyline(QPolygon(pts)) + if len(self.current_points) > 0: + p.drawPolyline(QPolygon(self.current_points)) + p.end() + + + +if __name__ == '__main__': + app = QApplication(sys.argv) + mainWin = draw() + mainWin.show() + sys.exit(app.exec_()) + diff --git a/QPainter/README.md b/QPainter/README.md index 42512411..29a0050d 100644 --- a/QPainter/README.md +++ b/QPainter/README.md @@ -1 +1,17 @@ -# QPainter \ No newline at end of file +# QPainter + +- 目录 + - [利用QPainter绘制各种图形](#QPainter绘制各种图形) + - [简易画板](#简易画板) + +## 1、QPainter绘制各种图形 + +[运行 StockDialog.py](StockDialog.py) + +![CountDownClose](ScreenShot/StockDialog.gif) + +## 2、简易画板 + +[运行 Draw.py](Draw.py) + +![CustomColorIcon](ScreenShot/Draw.gif) diff --git a/QPainter/ScreenShot/Draw.gif b/QPainter/ScreenShot/Draw.gif new file mode 100644 index 00000000..65f85619 Binary files /dev/null and b/QPainter/ScreenShot/Draw.gif differ diff --git a/QPainter/ScreenShot/StockDialog.gif b/QPainter/ScreenShot/StockDialog.gif new file mode 100644 index 00000000..97d964e7 Binary files /dev/null and b/QPainter/ScreenShot/StockDialog.gif differ diff --git a/QPainter/StockDialog.py b/QPainter/StockDialog.py new file mode 100644 index 00000000..996dbe1f --- /dev/null +++ b/QPainter/StockDialog.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2022年12月12日 +@site: https://pyqt.site , https://github.com/PyQt5 +@description: QPainter绘制各种图形 +""" +import sys + +try: + from PyQt5.QtWidgets import QApplication, QWidget, qApp + from PyQt5.QtGui import QPainter, QFont, QColor, QPixmap + from PyQt5.QtCore import Qt, pyqtSignal, QPointF + from PyQt5.Qt import QPoint, QPolygon, QSplitter, QFrame, QGridLayout, QLabel,\ + QComboBox, QSpinBox, QPalette, QStackedWidget, QVBoxLayout,\ + QPushButton, QColorDialog, QPen,QLinearGradient, QConicalGradient,\ + QRadialGradient, QBrush, QRect, QPainterPath, QFileDialog +except ImportError: + from PySide2.QtWidgets import QApplication, QWidget, qApp + from PySide2.QtGui import QPainter, QFont, QColor, QPixmap + from PySide2.QtCore import Qt, pyqtSignal, QPointF + from PySide2.Qt import QPoint, QPolygon, QSplitter, QFrame, QGridLayout, QLabel,\ + QComboBox, QSpinBox, QPalette, QStackedWidget, QVBoxLayout,\ + QPushButton, QColorDialog, QPen, QLinearGradient, QConicalGradient,\ + QRadialGradient, QBrush, QRect , QPainterPath, QFileDialog + + +class StockDialog(QWidget): + def __init__(self, parent=None): + super(StockDialog, self).__init__(parent) + self.setWindowTitle("QPainter绘制各种图形") + + mainSplitter = QSplitter(Qt.Horizontal) + mainSplitter.setOpaqueResize(True) + frame = QFrame(mainSplitter) + mainLayout = QGridLayout(frame) + mainLayout.setSpacing(6) + + label1 = QLabel("形状:") + label2 = QLabel("画笔线宽:") + label3 = QLabel("画笔颜色:") + label4 = QLabel("画笔风格:") + label5 = QLabel("画笔顶端:") + label6 = QLabel("画笔连接点:") + label7 = QLabel("画刷风格:") + label8 = QLabel("画刷颜色:") + + self.shapeComboBox = QComboBox() + self.shapeComboBox.addItem("Line", "Line") + self.shapeComboBox.addItem("Rectangle", "Rectangle") + self.shapeComboBox.addItem('Rounded Rectangle', 'Rounded Rectangle') + self.shapeComboBox.addItem('Ellipse', 'Ellipse') + self.shapeComboBox.addItem('Pie', 'Pie') + self.shapeComboBox.addItem('Chord', 'Chord') + self.shapeComboBox.addItem('Path', 'Path') + self.shapeComboBox.addItem('Polygon', 'Polygon') + self.shapeComboBox.addItem('Polyline', 'Polyline') + self.shapeComboBox.addItem('Arc', 'Arc') + self.shapeComboBox.addItem('Points', 'Points') + self.shapeComboBox.addItem('Text', 'Text') + self.shapeComboBox.addItem('Pixmap', 'Pixmap') + + self.widthSpinBox = QSpinBox() + self.widthSpinBox.setRange(0, 20) + + self.penColorFrame = QFrame() + self.penColorFrame.setAutoFillBackground(True) + self.penColorFrame.setPalette(QPalette(Qt.blue)) + self.penColorPushButton = QPushButton("更改") + + self.penStyleComboBox = QComboBox() + self.penStyleComboBox.addItem("Solid", Qt.SolidLine) + self.penStyleComboBox.addItem('Dash', Qt.DashLine) + self.penStyleComboBox.addItem('Dot', Qt.DotLine) + self.penStyleComboBox.addItem('Dash Dot', Qt.DashDotLine) + self.penStyleComboBox.addItem('Dash Dot Dot', Qt.DashDotDotLine) + self.penStyleComboBox.addItem('None', Qt.NoPen) + + self.penCapComboBox = QComboBox() + self.penCapComboBox.addItem("Flat", Qt.FlatCap) + self.penCapComboBox.addItem('Square', Qt.SquareCap) + self.penCapComboBox.addItem('Round', Qt.RoundCap) + + self.penJoinComboBox = QComboBox() + self.penJoinComboBox.addItem("Miter", Qt.MiterJoin) + self.penJoinComboBox.addItem('Bebel', Qt.BevelJoin) + self.penJoinComboBox.addItem('Round', Qt.RoundJoin) + + self.brushStyleComboBox = QComboBox() + self.brushStyleComboBox.addItem("Linear Gradient", Qt.LinearGradientPattern) + self.brushStyleComboBox.addItem('Radial Gradient', Qt.RadialGradientPattern) + self.brushStyleComboBox.addItem('Conical Gradient', Qt.ConicalGradientPattern) + self.brushStyleComboBox.addItem('Texture', Qt.TexturePattern) + self.brushStyleComboBox.addItem('Solid', Qt.SolidPattern) + self.brushStyleComboBox.addItem('Horizontal', Qt.HorPattern) + self.brushStyleComboBox.addItem('Vertical', Qt.VerPattern) + self.brushStyleComboBox.addItem('Cross', Qt.CrossPattern) + self.brushStyleComboBox.addItem('Backward Diagonal', Qt.BDiagPattern) + self.brushStyleComboBox.addItem('Forward Diagonal', Qt.FDiagPattern) + self.brushStyleComboBox.addItem('Diagonal Cross', Qt.DiagCrossPattern) + self.brushStyleComboBox.addItem('Dense 1', Qt.Dense1Pattern) + self.brushStyleComboBox.addItem('Dense 2', Qt.Dense2Pattern) + self.brushStyleComboBox.addItem('Dense 3', Qt.Dense3Pattern) + self.brushStyleComboBox.addItem('Dense 4', Qt.Dense4Pattern) + self.brushStyleComboBox.addItem('Dense 5', Qt.Dense5Pattern) + self.brushStyleComboBox.addItem('Dense 6', Qt.Dense6Pattern) + self.brushStyleComboBox.addItem('Dense 7', Qt.Dense7Pattern) + self.brushStyleComboBox.addItem('None', Qt.NoBrush) + + self.brushColorFrame = QFrame() + self.brushColorFrame.setAutoFillBackground(True) + self.brushColorFrame.setPalette(QPalette(Qt.green)) + self.brushColorPushButton = QPushButton("更改") + + labelCol = 0 + contentCol = 1 + + # 建立布局 + mainLayout.addWidget(label1, 1, labelCol) + mainLayout.addWidget(self.shapeComboBox, 1, contentCol) + mainLayout.addWidget(label2, 2, labelCol) + mainLayout.addWidget(self.widthSpinBox, 2, contentCol) + mainLayout.addWidget(label3, 4, labelCol) + mainLayout.addWidget(self.penColorFrame, 4, contentCol) + mainLayout.addWidget(self.penColorPushButton, 4, 3) + mainLayout.addWidget(label4, 6, labelCol) + mainLayout.addWidget(self.penStyleComboBox, 6, contentCol) + mainLayout.addWidget(label5, 8, labelCol) + mainLayout.addWidget(self.penCapComboBox, 8, contentCol) + mainLayout.addWidget(label6, 10, labelCol) + mainLayout.addWidget(self.penJoinComboBox, 10, contentCol) + mainLayout.addWidget(label7, 12, labelCol) + mainLayout.addWidget(self.brushStyleComboBox, 12, contentCol) + mainLayout.addWidget(label8, 14, labelCol) + mainLayout.addWidget(self.brushColorFrame, 14, contentCol) + mainLayout.addWidget(self.brushColorPushButton, 14, 3) + mainSplitter1 = QSplitter(Qt.Horizontal) + mainSplitter1.setOpaqueResize(True) + + stack1 = QStackedWidget() + stack1.setFrameStyle(QFrame.Panel | QFrame.Raised) + self.area = PaintArea() + stack1.addWidget(self.area) + frame1 = QFrame(mainSplitter1) + mainLayout1 = QVBoxLayout(frame1) + mainLayout1.setSpacing(6) + mainLayout1.addWidget(stack1) + + layout = QGridLayout(self) + layout.addWidget(mainSplitter1, 0, 0) + layout.addWidget(mainSplitter, 0, 1) + self.setLayout(layout) + + # 信号和槽函数 + self.shapeComboBox.activated.connect(self.slotShape) + self.widthSpinBox.valueChanged.connect(self.slotPenWidth) + self.penColorPushButton.clicked.connect(self.slotPenColor) + self.penStyleComboBox.activated.connect(self.slotPenStyle) + self.penCapComboBox.activated.connect(self.slotPenCap) + self.penJoinComboBox.activated.connect(self.slotPenJoin) + self.brushStyleComboBox.activated.connect(self.slotBrush) + self.brushColorPushButton.clicked.connect(self.slotBrushColor) + + self.slotShape(self.shapeComboBox.currentIndex()) + self.slotPenWidth(self.widthSpinBox.value()) + self.slotBrush(self.brushStyleComboBox.currentIndex()) + + def slotShape(self, value): + shape = self.area.Shape[value] + self.area.setShape(shape) + + def slotPenWidth(self, value): + color = self.penColorFrame.palette().color(QPalette.Window) + style = Qt.PenStyle(self.penStyleComboBox.itemData(self.penStyleComboBox.currentIndex(), Qt.UserRole)) + cap = Qt.PenCapStyle(self.penCapComboBox.itemData(self.penCapComboBox.currentIndex(), Qt.UserRole)) + join = Qt.PenJoinStyle(self.penJoinComboBox.itemData(self.penJoinComboBox.currentIndex(), Qt.UserRole)) + self.area.setPen(QPen(color, value, style, cap, join)) + + def slotPenStyle(self, value): + self.slotPenWidth(value) + + def slotPenCap(self, value): + self.slotPenWidth(value) + + def slotPenJoin(self, value): + self.slotPenWidth(value) + + def slotPenColor(self): + color = QColorDialog.getColor(Qt.blue) + self.penColorFrame.setPalette(QPalette(color)) + self.area.setPen(QPen(color)) + + def slotBrushColor(self): + color = QColorDialog.getColor(Qt.blue) + self.brushColorFrame.setPalette(QPalette(color)) + self.slotBrush(self.brushStyleComboBox.currentIndex()) + + def slotBrush(self, value): + color = self.brushColorFrame.palette().color(QPalette.Window) + style = Qt.BrushStyle(self.brushStyleComboBox.itemData(value, Qt.UserRole)) + + if (style == Qt.LinearGradientPattern): + linearGradient = QLinearGradient(0, 0, 400, 400) + linearGradient.setColorAt(0.0, Qt.white) + linearGradient.setColorAt(0.2, color) + linearGradient.setColorAt(1.0, Qt.black) + self.area.setBrush(linearGradient) + elif style == Qt.RadialGradientPattern: + radialGradient = QRadialGradient(200, 200, 80, 70, 70); + radialGradient.setColorAt(0.0, Qt.white) + radialGradient.setColorAt(0.2, Qt.green) + radialGradient.setColorAt(1.0, Qt.black) + self.area.setBrush(radialGradient) + elif (style == Qt.ConicalGradientPattern): + conicalGradient = QConicalGradient(200, 200, 30) + conicalGradient.setColorAt(0.0, Qt.white) + conicalGradient.setColorAt(0.2, color) + conicalGradient.setColorAt(1.0, Qt.black) + self.area.setBrush(conicalGradient) + elif (style == Qt.TexturePattern): + self.area.setBrush(QBrush(QPixmap("./Data/qt-logo.png"))) + else: + self.area.setBrush(QBrush(color, style)) + + +class PaintArea(QWidget): + def __init__(self): + super(PaintArea, self).__init__() + self.Shape = ["Line", "Rectangle", 'Rounded Rectangle', "Ellipse", "Pie", 'Chord', + "Path", "Polygon", "Polyline", "Arc", "Points", "Text", "Pixmap"] + self.setPalette(QPalette(Qt.white)) + self.setAutoFillBackground(True) + self.setMinimumSize(500, 500) + self.pen = QPen() + self.brush = QBrush() + + def setShape(self, s): + self.shape = s + self.update() + + def setPen(self, p): + self.pen = p + self.update() + + def setBrush(self, b): + self.brush = b + self.update() + + def paintEvent(self, QPaintEvent): + p = QPainter(self) + p.setPen(self.pen) + p.setBrush(self.brush) + + rect = QRect(50, 100, 300, 200) + points = [QPoint(150, 100), QPoint(300, 150), QPoint(350, 250), QPoint(100, 300)] + startAngle = 30 * 16 + spanAngle = 120 * 16 + + if self.shape == "Line": + p.drawLine(rect.topLeft(), rect.bottomRight()) + elif self.shape == "Rectangle": + p.drawRect(rect) + elif self.shape == 'Rounded Rectangle': + p.drawRoundedRect(rect, 25, 25, Qt.RelativeSize) + elif self.shape == "Ellipse": + p.drawEllipse(rect) + elif self.shape == "Polygon": + p.drawPolygon(QPolygon(points), Qt.WindingFill) + elif self.shape == "Polyline": + p.drawPolyline(QPolygon(points)) + elif self.shape == "Points": + p.drawPoints(QPolygon(points)) + elif self.shape == "Pie": + p.drawPie(rect, startAngle, spanAngle) + elif self.shape == "Arc": + p.drawArc(rect, startAngle, spanAngle) + elif self.shape == "Chord": + p.drawChord(rect, startAngle, spanAngle) + elif self.shape == "Path": + path = QPainterPath() + path.addRect(150, 150, 100, 100) + path.moveTo(100, 100) + path.cubicTo(300, 100, 200, 200, 300, 300) + path.cubicTo(100, 300, 200, 200, 100, 100) + p.drawPath(path) + elif self.shape == "Text": + p.drawText(rect, Qt.AlignCenter, "Hello Qt!") + elif self.shape == "Pixmap": + p.drawPixmap(150, 150, QPixmap("./Data/qt-logo.png")) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + form = StockDialog() + form.show() + app.exec_() + diff --git a/QProcess/GetCmdResult.py b/QProcess/GetCmdResult.py new file mode 100644 index 00000000..f7ea5469 --- /dev/null +++ b/QProcess/GetCmdResult.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/01 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: GetCmdResult.py +@description: +""" + +import sys + +try: + from PyQt5.QtCore import QProcess + from PyQt5.QtWidgets import (QApplication, QLabel, QPushButton, + QTextBrowser, QVBoxLayout, QWidget) +except ImportError: + from PySide2.QtCore import QProcess + from PySide2.QtWidgets import (QApplication, QLabel, QPushButton, + QTextBrowser, QVBoxLayout, QWidget) + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + self.setWindowTitle('执行命令得到结果') + layout = QVBoxLayout(self) + layout.addWidget(QLabel('点击执行 ping www.baidu.com', self)) + + self.buttonRunSync = QPushButton('同步执行', self) + layout.addWidget(self.buttonRunSync) + self.buttonRunSync.clicked.connect(self.run_ping) + + self.buttonRunASync = QPushButton('异步执行', self) + layout.addWidget(self.buttonRunASync) + self.buttonRunASync.clicked.connect(self.run_ping) + + self.resultView = QTextBrowser(self) + layout.addWidget(self.resultView) + + self._pingProcess = None + + def run_ping(self): + sender = self.sender() # 同步或者异步按钮 + self.buttonRunSync.setEnabled(False) + self.buttonRunASync.setEnabled(False) + + if self._pingProcess: + self._pingProcess.terminate() + + self._pingProcess = QProcess(self) + self._pingProcess.setProgram('ping') + if sys.platform.startswith('win'): + self._pingProcess.setArguments(['-n', '5', 'www.baidu.com']) + self._pingProcess.setArguments(['-n', '5', 'www.baidu.com']) + elif sys.platform.startswith('darwin') or sys.platform.startswith( + 'linux'): + self._pingProcess.setArguments(['-c', '5', 'www.baidu.com']) + # 合并输出流和错误流,执行完毕后通过readAll可以一次性读取所有结果 + self._pingProcess.setProcessChannelMode(QProcess.MergedChannels) + self._pingProcess.started.connect(self.on_started) + + if sender == self.buttonRunASync: + # 异步执行 + self._pingProcess.finished.connect(self.on_finished) + self._pingProcess.errorOccurred.connect(self.on_error) + self._pingProcess.start() + elif sender == self.buttonRunSync: + # 同步执行 + self._pingProcess.start() + if self._pingProcess.waitForFinished(): + self.on_finished(self._pingProcess.exitCode(), + self._pingProcess.exitStatus()) + else: + self.resultView.append('ping process read timeout') + self.on_error(self._pingProcess.error()) + + def on_started(self): + self.resultView.append('ping process started') + + def on_finished(self, exitCode, exitStatus): + self.resultView.append( + 'ping process finished, exitCode: %s, exitStatus: %s' % + (exitCode, exitStatus)) + # 读取所有结果 + result = self._pingProcess.readAll().data() + try: + import chardet + encoding = chardet.detect(result) + self.resultView.append(result.decode(encoding['encoding'])) + except Exception: + self.resultView.append(result.decode('utf-8', errors='ignore')) + self._pingProcess.kill() + self._pingProcess = None + self.buttonRunSync.setEnabled(True) + self.buttonRunASync.setEnabled(True) + + def on_error(self, error): + self.resultView.append('ping process error: %s, message: %s' % + (error, self._pingProcess.errorString())) + self._pingProcess.kill() + self._pingProcess = None + self.buttonRunSync.setEnabled(True) + self.buttonRunASync.setEnabled(True) + + +if __name__ == '__main__': + import cgitb + + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QProcess/InteractiveRun.py b/QProcess/InteractiveRun.py new file mode 100644 index 00000000..2c538c1c --- /dev/null +++ b/QProcess/InteractiveRun.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2023/02/01 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: InteractiveRun.py +@description: +""" + +import os +import sys + +try: + import chardet +except ImportError: + print('chardet not found') + +try: + from PyQt5.QtCore import QProcess + from PyQt5.QtWidgets import (QApplication, QLineEdit, QPushButton, + QTextBrowser, QVBoxLayout, QWidget) +except ImportError: + from PySide2.QtCore import QProcess + from PySide2.QtWidgets import (QApplication, QLineEdit, QPushButton, + QTextBrowser, QVBoxLayout, QWidget) + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + layout = QVBoxLayout(self) + + command = 'ping www.baidu.com' + if sys.platform.startswith('win'): + command = 'ping -n 5 www.baidu.com' + elif sys.platform.startswith('darwin') or sys.platform.startswith( + 'linux'): + command = 'ping -c 5 www.baidu.com' + else: + raise RuntimeError('Unsupported platform: %s' % sys.platform) + + self.cmdEdit = QLineEdit(command, self) + layout.addWidget(self.cmdEdit) + + self.buttonRun = QPushButton('执行命令', self) + layout.addWidget(self.buttonRun) + self.buttonRun.clicked.connect(self.run_command) + + self.resultView = QTextBrowser(self) + layout.addWidget(self.resultView) + + self._cmdProcess = None + self._init() + + def closeEvent(self, event): + if self._cmdProcess: + self._cmdProcess.writeData('exit'.encode() + os.linesep.encode()) + self._cmdProcess.waitForFinished() + if self._cmdProcess: + self._cmdProcess.terminate() + super(Window, self).closeEvent(event) + + def _init(self): + if self._cmdProcess: + return + # 打开终端shell + self._cmdProcess = QProcess(self) + self._cmdProcess.setProgram( + 'cmd' if sys.platform.startswith('win') else 'bash') + # 合并输出流和错误流,只从标准输出流读取数据 + self._cmdProcess.setProcessChannelMode(QProcess.MergedChannels) + self._cmdProcess.started.connect(self.on_started) + self._cmdProcess.finished.connect(self.on_finished) + self._cmdProcess.errorOccurred.connect(self.on_error) + self._cmdProcess.readyReadStandardOutput.connect( + self.on_readyReadStandardOutput) + self._cmdProcess.start() + + def run_command(self): + self._init() + command = self.cmdEdit.text().strip() + if not command: + return + command = command.encode(sys.getdefaultencoding()) + os.linesep.encode( + sys.getdefaultencoding()) + self._cmdProcess.writeData(command) + + def on_started(self): + self.resultView.append('ping process started, pid: %s' % + self._cmdProcess.processId()) + + def on_finished(self, exitCode, exitStatus): + print('ping process finished, exitCode: %s, exitStatus: %s' % + (exitCode, exitStatus)) + self._cmdProcess.kill() + self._cmdProcess = None + + def on_error(self, error): + self.resultView.append('ping process error: %s, message: %s' % + (error, self._cmdProcess.errorString())) + self._cmdProcess.kill() + self._cmdProcess = None + + def on_readyReadStandardOutput(self): + # 读取已有结果 + result = self._cmdProcess.readAllStandardOutput().data() + try: + encoding = chardet.detect(result) + self.resultView.append(result.decode(encoding['encoding'])) + except Exception: + self.resultView.append(result.decode('utf-8', errors='ignore')) + + +if __name__ == '__main__': + import cgitb + + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QProcess/README.en.md b/QProcess/README.en.md new file mode 100644 index 00000000..e69de29b diff --git a/QProcess/README.md b/QProcess/README.md new file mode 100644 index 00000000..74bee0b9 --- /dev/null +++ b/QProcess/README.md @@ -0,0 +1,36 @@ +# QProcess + +- 目录 + - [执行命令得到结果](#1执行命令得到结果) + - [交互执行命令](#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()`启动进程,并分为同步和异步两种方式获取输出 + +示例代码为第3种方式: + +1. 通过`setProcessChannelMode(QProcess.MergedChannels)`合并标准输出和错误输出 +2. `waitForFinished`为同步方式,然后调用`readAll`读取所有输出 +3. 也可以绑定`finished`信号,然后通过`readAll`读取所有输出 + +![GetCmdResult](ScreenShot/GetCmdResult.gif) + +## 2、交互执行命令 + +[运行 InteractiveRun.py](InteractiveRun.py) + +`QProcess` 也可以用于交互式执行命令,具体需要如下几步: + +1. 通过`setProcessChannelMode(QProcess.MergedChannels)`合并标准输出和错误输出 +2. 通过`start`启动进程 +3. 通过`readyReadStandardOutput`信号读取进程输出 +4. 通过`writeData`向进程写入数据 + +![InteractiveRun](ScreenShot/InteractiveRun.gif) diff --git a/QProcess/ScreenShot/GetCmdResult.gif b/QProcess/ScreenShot/GetCmdResult.gif new file mode 100644 index 00000000..bdf05960 Binary files /dev/null and b/QProcess/ScreenShot/GetCmdResult.gif differ diff --git a/QProcess/ScreenShot/InteractiveRun.gif b/QProcess/ScreenShot/InteractiveRun.gif new file mode 100644 index 00000000..225144da Binary files /dev/null and b/QProcess/ScreenShot/InteractiveRun.gif differ diff --git a/QProgressBar/ColourfulProgress.py b/QProgressBar/ColourfulProgress.py new file mode 100644 index 00000000..2c629d0c --- /dev/null +++ b/QProgressBar/ColourfulProgress.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2022/02/25 +@author: Irony +@site: https://pyqt.site, https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ColourfulProgress.py +@description: +""" + +try: + from PyQt5.QtCore import QLineF, QRect, QRectF, Qt + from PyQt5.QtGui import QColor, QPainter, QPainterPath, QPen, QTransform + from PyQt5.QtWidgets import (QApplication, QGridLayout, QProgressBar, + QSlider, QStyleOptionProgressBar, QWidget) +except ImportError: + from PySide2.QtCore import QRect, Qt, QRectF, QLineF + from PySide2.QtGui import QColor, QPainter, QPen, QTransform, QPainterPath + from PySide2.QtWidgets import (QApplication, QProgressBar, + QStyleOptionProgressBar, QWidget, QSlider, + QGridLayout) + +from Lib.QStyleAnimation import QProgressStyleAnimation + + +class ColourfulProgress(QProgressBar): + + def __init__(self, *args, **kwargs): + self._color = kwargs.pop('color', QColor(43, 194, 83)) + self._fps = kwargs.pop('fps', 60) + self._lineWidth = kwargs.pop('lineWidth', 50) # 线条宽度 + self._radius = kwargs.pop('radius', None) # None为自动计算圆角 + self._animation = None + super(ColourfulProgress, self).__init__(*args, **kwargs) + self.setColor(self._color) + self.setFps(self._fps) + self.setLineWidth(self._lineWidth) + self.setRadius(self._radius) + + def setColor(self, color): + """ + :type color: QColor + :param color: 颜色 + """ + self._color = QColor(color) if isinstance( + color, (QColor, Qt.GlobalColor)) else QColor(43, 194, 83) + + def setFps(self, fps): + """ + :type fps: int + :param fps: 帧率 + """ + self._fps = max(int(fps), 1) if isinstance(fps, (int, float)) else 60 + + def setLineWidth(self, width): + """ + :type width: int + :param width: 线条宽度 + """ + self._lineWidth = max(int(width), 0) if isinstance(width, + (int, float)) else 50 + + def setRadius(self, radius): + """ + :type radius: int + :param radius: 半径 + """ + self._radius = max(int(radius), 1) if isinstance(radius, + (int, float)) else None + + def paintEvent(self, _): + """ + 重写绘制事件,参考 qfusionstyle.cpp 中的 CE_ProgressBarContents 绘制方法 + """ + option = QStyleOptionProgressBar() + self.initStyleOption(option) + + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + painter.translate(0.5, 0.5) + + vertical = option.orientation == Qt.Vertical # 是否垂直 + inverted = option.invertedAppearance # 是否反转 + # 是否显示动画 + indeterminate = (option.minimum == option.maximum) or ( + option.minimum < option.progress < option.maximum) + rect = option.rect + + if vertical: + rect = QRect(rect.left(), rect.top(), rect.height(), + rect.width()) # 翻转宽度和高度 + m = QTransform.fromTranslate(rect.height(), 0) + m.rotate(90.0) + painter.setTransform(m, True) + + maxWidth = rect.width() + progress = max(option.progress, option.minimum) + totalSteps = max(1, option.maximum - option.minimum) + progressSteps = progress - option.minimum + progressBarWidth = int(progressSteps * maxWidth / totalSteps) + width = progressBarWidth # 已进行的进度宽度 + radius = max(1, (min(width, + self.width() if vertical else self.height()) // + 4) if self._radius is None else self._radius) + + reverse = (not vertical and + option.direction == Qt.RightToLeft) or vertical + if inverted: + reverse = not reverse + + # 绘制范围 + path = QPainterPath() + if not reverse: + progressBar = QRectF(rect.left(), rect.top(), width, rect.height()) + else: + progressBar = QRectF(rect.right() - width, rect.top(), width, + rect.height()) + + # 切割范围 + path.addRoundedRect(progressBar, radius, radius) + painter.setClipPath(path) + + # 绘制背景颜色 + painter.setPen(Qt.NoPen) + painter.setBrush(self._color) + painter.drawRoundedRect(progressBar, radius, radius) + + if not indeterminate: + if self._animation: + self._animation.stop() + self._animation = None + else: + # 叠加颜色覆盖后出现类似线条间隔的效果 + color = self._color.lighter(320) + color.setAlpha(80) + painter.setPen(QPen(color, self._lineWidth)) + + if self._animation: + if self._animation.state() == QProgressStyleAnimation.Stopped: + # FIXME: 最小化后动画会停止 + self._animation.start() + step = int(self._animation.animationStep() % self._lineWidth) + else: + step = 0 + self._animation = QProgressStyleAnimation(self._fps, self) + self._animation.start() + + # 动画斜线绘制 + startX = int(progressBar.left() - rect.height() - self._lineWidth) + endX = int(rect.right() + self._lineWidth) + + if (not inverted and not vertical) or (inverted and vertical): + lines = [ + QLineF(x + step, progressBar.bottom(), + x + rect.height() + step, progressBar.top()) + for x in range(startX, endX, self._lineWidth) + ] + else: + lines = [ + QLineF(x - step, progressBar.bottom(), + x + rect.height() - step, progressBar.top()) + for x in range(startX, endX, self._lineWidth) + ] + painter.drawLines(lines) + + +if __name__ == '__main__': + import cgitb + import sys + + cgitb.enable(format='text') + app = QApplication(sys.argv) + + w = QWidget() + layout = QGridLayout(w) + + w1 = ColourfulProgress(color=QColor('#85c440')) + w1.setMinimumWidth(300) + w1.setMaximumWidth(300) + w1.setRange(0, 100) + layout.addWidget(w1, 0, 0, 1, 1) + + w2 = ColourfulProgress(color=QColor('#f2b63c')) + w2.setMinimumWidth(300) + w2.setMaximumWidth(300) + w2.setInvertedAppearance(True) + w2.setRange(0, 100) + layout.addWidget(w2, 1, 0, 1, 1) + + w3 = ColourfulProgress(color=QColor('#db3a27')) + w3.setMinimumHeight(300) + w3.setMaximumHeight(300) + w3.setOrientation(Qt.Vertical) + w3.setRange(0, 100) + layout.addWidget(w3, 0, 1, 2, 1) + + w4 = ColourfulProgress(color=QColor('#5aaadb')) + w4.setMinimumHeight(300) + w4.setMaximumHeight(300) + w4.setInvertedAppearance(True) + w4.setOrientation(Qt.Vertical) + w4.setRange(0, 100) + layout.addWidget(w4, 0, 2, 2, 1) + + slider = QSlider(Qt.Horizontal) + slider.setRange(0, 100) + slider.valueChanged.connect(w1.setValue) + slider.valueChanged.connect(w2.setValue) + slider.valueChanged.connect(w3.setValue) + slider.valueChanged.connect(w4.setValue) + slider.setValue(50) + layout.addWidget(slider, 2, 0, 1, 3) + + w.show() + + sys.exit(app.exec_()) diff --git a/QProgressBar/Lib/DWaterProgress.py b/QProgressBar/Lib/DWaterProgress.py new file mode 100644 index 00000000..9242ada2 --- /dev/null +++ b/QProgressBar/Lib/DWaterProgress.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/1/1 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: DWaterProgress +@see https://github.com/linuxdeepin/dtkwidget/blob/master/src/widgets/dwaterprogress.cpp +@description: +""" +import math + +try: + from PyQt5.QtCore import pyqtSlot, QTimer, QSizeF, Qt, QRectF, QPointF, QRect, QPoint, QSize + from PyQt5.QtGui import QImage, QColor, QPainter, QLinearGradient, QGradient, QPainterPath, QPixmap, \ + QBrush, QPen + from PyQt5.QtSvg import QSvgRenderer + from PyQt5.QtWidgets import QProgressBar, QGraphicsDropShadowEffect +except ImportError: + from PySide2.QtCore import Slot as pyqtSlot, QTimer, QSizeF, Qt, QRectF, QPointF, QRect, QPoint, QSize + from PySide2.QtGui import QImage, QColor, QPainter, QLinearGradient, QGradient, QPainterPath, QPixmap, \ + QBrush, QPen + from PySide2.QtSvg import QSvgRenderer + from PySide2.QtWidgets import QProgressBar, QGraphicsDropShadowEffect + +WATER_FRONT = """ + + +""" +WATER_BACK = """ + + +""" + + +class Pop: + # https://github.com/linuxdeepin/dtkwidget/blob/master/src/widgets/dwaterprogress.cpp#L36 + + def __init__(self, size, xs, ys, xo=0, yo=0): + self.size = size + self.xSpeed = xs + self.ySpeed = ys + self.xOffset = xo + self.yOffset = yo + + +class DWaterProgress(QProgressBar): + + def __init__(self, *args, **kwargs): + super(DWaterProgress, self).__init__(*args, **kwargs) + self.waterFrontImage = QImage() + self.waterBackImage = QImage() + self.waterFrontSvg = QSvgRenderer(WATER_FRONT.encode()) + self.waterBackSvg = QSvgRenderer(WATER_BACK.encode()) + self.pops = [] + self.initPops() + self.setTextVisible(True) + self.interval = 33 + self.timer = QTimer(self) + self.timer.setInterval(self.interval) + self.timer.timeout.connect(self.onTimerOut) + self.resizePixmap(self.size()) + self.frontXOffset = self.width() + self.backXOffset = 0 + effect = QGraphicsDropShadowEffect(self) + effect.setOffset(0, 6) + effect.setColor(QColor(1, 153, 248, 255 * 5 / 20)) + effect.setBlurRadius(12) + self.setGraphicsEffect(effect) + + def initPops(self): + self.pops = [Pop(7, -1.8, 0.6), Pop(8, 1.2, 1.0), Pop(11, 0.8, 1.6)] + + @pyqtSlot() + def start(self): + self.timer.start() + + @pyqtSlot() + def stop(self): + self.timer.stop() + + def resizePixmap(self, sz): + # https://github.com/linuxdeepin/dtkwidget/blob/master/src/widgets/dwaterprogress.cpp#L192 + # resize water + waterWidth = 500 * sz.width() / 100 + waterHeight = 110 * sz.height() / 100 + waterSize = QSizeF(waterWidth, waterHeight).toSize() + + if self.waterFrontImage.size() != waterSize: + image = QImage(waterWidth, waterHeight, QImage.Format_ARGB32) + image.fill(Qt.transparent) + waterPainter = QPainter(image) + self.waterFrontSvg.render(waterPainter) + self.waterFrontImage = image + + if self.waterBackImage.size() != waterSize: + image = QImage(waterWidth, waterHeight, QImage.Format_ARGB32) + image.fill(Qt.transparent) # partly transparent red-ish background + waterPainter = QPainter(image) + self.waterBackSvg.render(waterPainter) + self.waterBackImage = image + + def onTimerOut(self): + # interval can not be zero, and limit to 1 + self.interval = max(1, self.interval) + # move 60% per second + frontXDeta = 40.0 / (1000.0 / self.interval) + # move 90% per second + backXDeta = 60.0 / (1000.0 / self.interval) + + canvasWidth = int(self.width() * self.devicePixelRatioF()) + self.frontXOffset -= frontXDeta * canvasWidth / 100 + self.backXOffset += backXDeta * canvasWidth / 100 + + if self.frontXOffset > canvasWidth: + self.frontXOffset = canvasWidth + + if self.frontXOffset < - (self.waterFrontImage.width() - canvasWidth): + self.frontXOffset = canvasWidth + + if self.backXOffset > self.waterBackImage.width(): + self.backXOffset = 0 + + # update pop + # move 25% per second default + speed = 25 / (1000.0 / self.interval) # 100 / self.height() + for pop in self.pops: + # yOffset 0 ~ 100 + pop.yOffset += speed * pop.ySpeed + if pop.yOffset < 0: + pass + if pop.yOffset > self.value(): + pop.yOffset = 0 + pop.xOffset = math.sin((pop.yOffset / 100) * 2 * 3.14) * 18 * pop.xSpeed + 50 + self.update() + + def paint(self, painter): + painter.setRenderHint(QPainter.Antialiasing) + + pixelRatio = self.devicePixelRatioF() + rect = QRectF(0, 0, self.width() * pixelRatio, self.height() * pixelRatio) + sz = QSizeF(self.width() * pixelRatio, self.height() * pixelRatio).toSize() + + self.resizePixmap(sz) + + yOffset = rect.toRect().topLeft().y() + (100 - self.value() - 10) * sz.height() / 100 + + # draw water + waterImage = QImage(sz, QImage.Format_ARGB32_Premultiplied) + waterPainter = QPainter() + waterPainter.begin(waterImage) + waterPainter.setRenderHint(QPainter.Antialiasing) + waterPainter.setCompositionMode(QPainter.CompositionMode_Source) + + pointStart = QPointF(sz.width() / 2, 0) + pointEnd = QPointF(sz.width() / 2, sz.height()) + linear = QLinearGradient(pointStart, pointEnd) + startColor = QColor('#1F08FF') + startColor.setAlphaF(1) + endColor = QColor('#50FFF7') + endColor.setAlphaF(0.28) + linear.setColorAt(0, startColor) + linear.setColorAt(1, endColor) + linear.setSpread(QGradient.PadSpread) + waterPainter.setPen(Qt.NoPen) + waterPainter.setBrush(linear) + waterPainter.drawEllipse(waterImage.rect().center(), sz.width() / 2 + 1, sz.height() / 2 + 1) + + waterPainter.setCompositionMode(QPainter.CompositionMode_SourceOver) + waterPainter.drawImage(int(self.backXOffset), yOffset, self.waterBackImage) + waterPainter.drawImage(int(self.backXOffset) - self.waterBackImage.width(), yOffset, + self.waterBackImage) + waterPainter.drawImage(int(self.frontXOffset), yOffset, self.waterFrontImage) + waterPainter.drawImage(int(self.frontXOffset) - self.waterFrontImage.width(), yOffset, + self.waterFrontImage) + + # draw pop + if self.value() > 30: + for pop in self.pops: + popPath = QPainterPath() + popPath.addEllipse(pop.xOffset * sz.width() / 100, (100 - pop.yOffset) * sz.height() / 100, + pop.size * sz.width() / 100, pop.size * sz.height() / 100) + waterPainter.fillPath(popPath, QColor(255, 255, 255, 255 * 0.3)) + + if self.isTextVisible(): + font = waterPainter.font() + rectValue = QRect() + progressText = self.text().strip('%') + + if progressText == '100': + font.setPixelSize(sz.height() * 35 / 100) + waterPainter.setFont(font) + + rectValue.setWidth(sz.width() * 60 / 100) + rectValue.setHeight(sz.height() * 35 / 100) + rectValue.moveCenter(rect.center().toPoint()) + waterPainter.setPen(Qt.white) + waterPainter.drawText(rectValue, Qt.AlignCenter, progressText) + else: + font.setPixelSize(sz.height() * 40 / 100) + waterPainter.setFont(font) + + rectValue.setWidth(sz.width() * 45 / 100) + rectValue.setHeight(sz.height() * 40 / 100) + rectValue.moveCenter(rect.center().toPoint()) + rectValue.moveLeft(rect.left() + rect.width() * 0.45 * 0.5) + + waterPainter.setPen(Qt.white) + waterPainter.drawText(rectValue, Qt.AlignCenter, progressText) + font.setPixelSize(font.pixelSize() / 2) + waterPainter.setFont(font) + rectPerent = QRect(QPoint(rectValue.right(), rectValue.bottom() - rect.height() * 20 / 100), + QPoint(rectValue.right() + rect.width() * 20 / 100, rectValue.bottom())) + + waterPainter.drawText(rectPerent, Qt.AlignCenter, '%') + + waterPainter.end() + + maskPixmap = QPixmap(sz) + maskPixmap.fill(Qt.transparent) + path = QPainterPath() + path.addEllipse(QRectF(0, 0, sz.width(), sz.height())) + maskPainter = QPainter() + maskPainter.begin(maskPixmap) + maskPainter.setRenderHint(QPainter.Antialiasing) + maskPainter.setPen(QPen(Qt.white, 1)) + maskPainter.fillPath(path, QBrush(Qt.white)) + maskPainter.end() + + mode = QPainter.CompositionMode_SourceIn + contentImage = QImage(sz, QImage.Format_ARGB32_Premultiplied) + contentPainter = QPainter() + contentPainter.begin(contentImage) + contentPainter.setCompositionMode(QPainter.CompositionMode_Source) + contentPainter.fillRect(contentImage.rect(), Qt.transparent) + contentPainter.setCompositionMode(QPainter.CompositionMode_SourceOver) + contentPainter.drawImage(0, 0, maskPixmap.toImage()) + contentPainter.setCompositionMode(mode) + contentPainter.drawImage(0, 0, waterImage) + contentPainter.setCompositionMode(QPainter.CompositionMode_DestinationOver) + contentPainter.end() + + contentImage.setDevicePixelRatio(pixelRatio) + painter.drawImage(self.rect(), contentImage) + + def paintEvent(self, event): + painter = QPainter(self) + self.paint(painter) + + def sizeHint(self): + return QSize(100, 100) diff --git a/QProgressBar/Lib/QStyleAnimation.py b/QProgressBar/Lib/QStyleAnimation.py new file mode 100644 index 00000000..c2c91657 --- /dev/null +++ b/QProgressBar/Lib/QStyleAnimation.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2022/02/26 +@author: Irony +@site: https://pyqt.site, https://github.com/PyQt5 +@email: 892768447@qq.com +@file: QStyleAnimation.py +@description: +""" +from enum import IntEnum + +try: + from PyQt5.QtCore import (QAbstractAnimation, QCoreApplication, QEvent, + QTime) +except ImportError: + from PySide2.QtCore import (QAbstractAnimation, QCoreApplication, QEvent, + QTime) + +ScrollBarFadeOutDuration = 200.0 +ScrollBarFadeOutDelay = 450.0 + +StyleAnimationUpdate = 213 + + +class QStyleAnimation(QAbstractAnimation): + FrameRate = IntEnum( + 'FrameRate', + ['DefaultFps', 'SixtyFps', 'ThirtyFps', 'TwentyFps', 'FifteenFps']) + + def __init__(self, *args, **kwargs): + super(QStyleAnimation, self).__init__(*args, **kwargs) + self._delay = 0 + self._duration = -1 + self._startTime = QTime.currentTime() + self._fps = self.FrameRate.ThirtyFps + self._skip = 0 + + def target(self): + return self.parent() + + def duration(self): + return self._duration + + def setDuration(self, duration): + self._duration = duration + + def delay(self): + return self._delay + + def setDelay(self, delay): + self._delay = delay + + def startTime(self): + return self._startTime + + def setStartTime(self, time): + self._startTime = time + + def frameRate(self): + return self._fps + + def setFrameRate(self, fps): + self._fps = fps + + def updateTarget(self): + event = QEvent(QEvent.Type(StyleAnimationUpdate)) + event.setAccepted(False) + QCoreApplication.sendEvent(self.target(), event) + if not event.isAccepted(): + self.stop() + + def start(self): + self._skip = 0 + super(QStyleAnimation, self).start(QAbstractAnimation.KeepWhenStopped) + + def isUpdateNeeded(self): + return self.currentTime() > self._delay + + def updateCurrentTime(self, _): + self._skip += 1 + if self._skip >= self._fps: + self._skip = 0 + if self.parent() and self.isUpdateNeeded(): + self.updateTarget() + + +class QProgressStyleAnimation(QStyleAnimation): + + def __init__(self, speed, *args, **kwargs): + super(QProgressStyleAnimation, self).__init__(*args, **kwargs) + self._speed = speed + self._step = -1 + + def animationStep(self): + return self.currentTime() / (1000.0 / self._speed) + + def progressStep(self, width): + step = self.animationStep() + progress = (step * width / self._speed) % width + if ((step * width / self._speed) % (2 * width)) >= width: + progress = width - progress + return progress + + def speed(self): + return self._speed + + def setSpeed(self, speed): + self._speed = speed + + def isUpdateNeeded(self): + if super(QProgressStyleAnimation, self).isUpdateNeeded(): + current = self.animationStep() + if self._step == -1 or self._step != current: + self._step = current + return True + return False diff --git a/QProgressBar/Lib/WaterRippleProgressBar.py b/QProgressBar/Lib/WaterRippleProgressBar.py index 3673a712..605e56dd 100644 --- a/QProgressBar/Lib/WaterRippleProgressBar.py +++ b/QProgressBar/Lib/WaterRippleProgressBar.py @@ -1,28 +1,28 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Created on 2018年4月1日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: WaterRippleProgressBar -# description: - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +""" +Created on 2018年4月1日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: WaterRippleProgressBar +@description: +""" import math -from PyQt5.QtCore import QTimer, Qt, QRectF, QSize -from PyQt5.QtGui import QPainter, QPainterPath, QColor, QFont -from PyQt5.QtWidgets import QProgressBar +try: + from PyQt5.QtCore import QTimer, Qt, QRectF, QSize + from PyQt5.QtGui import QPainter, QPainterPath, QColor, QFont + from PyQt5.QtWidgets import QProgressBar +except ImportError: + from PySide2.QtCore import QTimer, Qt, QRectF, QSize + from PySide2.QtGui import QPainter, QPainterPath, QColor, QFont + from PySide2.QtWidgets import QProgressBar class WaterRippleProgressBar(QProgressBar): - # 浪高百分比 waterHeight = 1 # 密度 @@ -84,7 +84,7 @@ def paintEvent(self, event): # 正弦曲线公式 y = A * sin(ωx + φ) + k # 当前值所占百分比 percent = 1 - (self.value() - self.minimum()) / \ - (self.maximum() - self.minimum()) + (self.maximum() - self.minimum()) # w表示周期,6为人为定义 w = 6 * self.waterDensity * math.pi / self.width() # A振幅 高度百分比,1/26为人为定义 diff --git a/QProgressBar/MetroCircleProgress.py b/QProgressBar/MetroCircleProgress.py index 3b1fe1f6..58f4f464 100644 --- a/QProgressBar/MetroCircleProgress.py +++ b/QProgressBar/MetroCircleProgress.py @@ -4,27 +4,25 @@ """ Created on 2018年9月日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: MetroCircleProgress @description: """ -from PyQt5.QtCore import QSequentialAnimationGroup, pyqtProperty,\ - QPauseAnimation, QPropertyAnimation, QParallelAnimationGroup,\ - QObject, QSize, Qt, pyqtSignal, QRectF -from PyQt5.QtGui import QPainter, QColor -from PyQt5.QtWidgets import QWidget, QVBoxLayout - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QSequentialAnimationGroup, QPauseAnimation, QPropertyAnimation, \ + QParallelAnimationGroup, QObject, QSize, Qt, QRectF, pyqtSignal, pyqtProperty + from PyQt5.QtGui import QPainter, QColor + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout +except ImportError: + from PySide2.QtCore import QSequentialAnimationGroup, QPauseAnimation, QPropertyAnimation, \ + QParallelAnimationGroup, QObject, QSize, Qt, QRectF, Signal as pyqtSignal, Property as pyqtProperty + from PySide2.QtGui import QPainter, QColor + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout class CircleItem(QObject): - X = 0 # x坐标 Opacity = 1 # 透明度0~1 valueChanged = pyqtSignal() @@ -52,7 +50,6 @@ def qBound(miv, cv, mxv): class MetroCircleProgress(QWidget): - Radius = 5 # 半径 Color = QColor(24, 189, 155) # 圆圈颜色 BackgroundColor = QColor(Qt.transparent) # 背景颜色 @@ -189,7 +186,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QProgressBar/PercentProgressBar.py b/QProgressBar/PercentProgressBar.py index 80f5d6c7..8e469103 100644 --- a/QProgressBar/PercentProgressBar.py +++ b/QProgressBar/PercentProgressBar.py @@ -4,25 +4,23 @@ """ Created on 2018年9月4日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: PercentProgressBar @description: """ -from PyQt5.QtCore import pyqtProperty, QSize, Qt, QRectF, QTimer -from PyQt5.QtGui import QColor, QPainter, QFont -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QSlider - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import pyqtProperty, QSize, Qt, QRectF, QTimer + from PyQt5.QtGui import QColor, QPainter, QFont + from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QSlider +except ImportError: + from PySide2.QtCore import Property as pyqtProperty, QSize, Qt, QRectF, QTimer + from PySide2.QtGui import QColor, QPainter, QFont + from PySide2.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QSlider class PercentProgressBar(QWidget): - MinValue = 0 MaxValue = 100 Value = 0 @@ -300,8 +298,10 @@ def __init__(self, *args, **kwargs): self.staticPercentProgressBar.showFreeArea = True self.staticPercentProgressBar.ShowSmallCircle = True vlayout.addWidget(self.staticPercentProgressBar) - vlayout.addWidget(QSlider(self, minimum=0, maximum=100, orientation=Qt.Horizontal, - valueChanged=self.staticPercentProgressBar.setValue)) + + self.slider = QSlider(self, minimum=0, maximum=100, orientation=Qt.Horizontal) + self.slider.valueChanged.connect(self.staticPercentProgressBar.setValue) + vlayout.addWidget(self.slider) self._timer.start(100) @@ -316,8 +316,9 @@ def updateValue(self): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.Hook(1, None, 5, sys.stderr, 'text') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QProgressBar/README.md b/QProgressBar/README.md index eba728f3..0db222fe 100644 --- a/QProgressBar/README.md +++ b/QProgressBar/README.md @@ -6,8 +6,11 @@ - [百分比进度条](#3百分比进度条) - [Metro进度条](#4Metro进度条) - [水波纹进度条](#5水波纹进度条) + - [圆形水位进度条](#6圆形水位进度条) + - [多彩动画进度条](#7多彩动画进度条) ## 1、常规样式美化 + [运行 SimpleStyle.py](SimpleStyle.py) 主要改变背景颜色、高度、边框、块颜色、边框、圆角 @@ -15,25 +18,45 @@ ![SimpleStyle](ScreenShot/SimpleStyle.gif) ## 2、圆圈进度条 + [运行 RoundProgressBar.py](RoundProgressBar.py) ![RoundProgressBar](ScreenShot/RoundProgressBar.gif) ## 3、百分比进度条 + [运行 PercentProgressBar.py](PercentProgressBar.py) ![PercentProgressBar](ScreenShot/PercentProgressBar.gif) ## 4、Metro进度条 + [运行 MetroCircleProgress.py](MetroCircleProgress.py) ![MetroCircleProgress](ScreenShot/MetroCircleProgress.gif) ## 5、水波纹进度条 + [运行 WaterProgressBar.py](WaterProgressBar.py) 1. 利用正弦函数根据0-width的范围计算y坐标 2. 利用 `QPainterPath` 矩形或者圆形作为背景 3. 用 `QPainterPath` 把y坐标用 `lineTo` 连接起来形成一个U字形+上方波浪的闭合区间 -![WaterProgressBar](ScreenShot/WaterProgressBar.gif) \ No newline at end of file +![WaterProgressBar](ScreenShot/WaterProgressBar.gif) + +## 6、圆形水位进度条 + +[运行 WaterProgress.py](WaterProgress.py) + +参考 + +![WaterProgressBar](ScreenShot/WaterProgress.gif) + +## 7、多彩动画进度条 + +[运行 ColourfulProgress.py](ColourfulProgress.py) + +动画实现参考 qfusionstyle.cpp 中的 CE_ProgressBarContents 绘制方法 + +![ColourfulProgress](ScreenShot/ColourfulProgress.gif) diff --git a/QProgressBar/RoundProgressBar.py b/QProgressBar/RoundProgressBar.py index d406a667..a52cef19 100644 --- a/QProgressBar/RoundProgressBar.py +++ b/QProgressBar/RoundProgressBar.py @@ -4,25 +4,23 @@ """ Created on 2018年9月4日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: 界面美化.圆形进度条.CircleProgressBar @description: """ -from PyQt5.QtCore import QSize, pyqtProperty, QTimer, Qt -from PyQt5.QtGui import QColor, QPainter -from PyQt5.QtWidgets import QWidget, QHBoxLayout - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QSize, pyqtProperty, QTimer, Qt + from PyQt5.QtGui import QColor, QPainter + from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout +except ImportError: + from PySide2.QtCore import QSize, Property as pyqtProperty, QTimer, Qt + from PySide2.QtGui import QColor, QPainter + from PySide2.QtWidgets import QApplication, QWidget, QHBoxLayout class CircleProgressBar(QWidget): - Color = QColor(24, 189, 155) # 圆圈颜色 Clockwise = True # 顺时针还是逆时针 Delta = 36 @@ -105,7 +103,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QProgressBar/ScreenShot/ColourfulProgress.gif b/QProgressBar/ScreenShot/ColourfulProgress.gif new file mode 100644 index 00000000..3489cd96 Binary files /dev/null and b/QProgressBar/ScreenShot/ColourfulProgress.gif differ diff --git a/QProgressBar/ScreenShot/WaterProgress.gif b/QProgressBar/ScreenShot/WaterProgress.gif new file mode 100644 index 00000000..f434c172 Binary files /dev/null and b/QProgressBar/ScreenShot/WaterProgress.gif differ diff --git a/QProgressBar/SimpleStyle.py b/QProgressBar/SimpleStyle.py index 2ad17c8e..b8aa5b73 100644 --- a/QProgressBar/SimpleStyle.py +++ b/QProgressBar/SimpleStyle.py @@ -1,26 +1,26 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月30日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: SimpleStyle @description: -''' -from random import randint -import sys - -from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QProgressBar +""" +import sys +from random import randint -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QTimer + from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QProgressBar +except ImportError: + from PySide2.QtCore import QTimer + from PySide2.QtWidgets import QWidget, QApplication, QVBoxLayout, QProgressBar -StyleSheet = ''' +StyleSheet = """ /*设置红色进度条*/ #RedProgressBar { text-align: center; /*进度值居中*/ @@ -50,7 +50,7 @@ width: 10px; /*区块宽度*/ margin: 0.5px; } -''' +""" class ProgressBar(QProgressBar): diff --git a/QProgressBar/WaterProgress.py b/QProgressBar/WaterProgress.py new file mode 100644 index 00000000..97bbc201 --- /dev/null +++ b/QProgressBar/WaterProgress.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/1/1 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: WaterProgress +@description: +""" +import sys + +try: + from PyQt5.QtCore import QTimer + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout +except ImportError: + from PySide2.QtCore import QTimer + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout + +from Lib.DWaterProgress import DWaterProgress + + +class WaterProgressWindow(QWidget): + + def __init__(self, *args, **kwargs): + super(WaterProgressWindow, self).__init__(*args, **kwargs) + layout = QVBoxLayout(self) + + self.progress = DWaterProgress(self) + self.progress.setFixedSize(100, 100) + self.progress.setValue(0) + self.progress.start() + + layout.addWidget(self.progress) + + self.timer = QTimer(self, timeout=self.updateProgress) + self.timer.start(50) + + def updateProgress(self): + value = self.progress.value() + if value == 100: + self.progress.setValue(0) + else: + self.progress.setValue(value + 1) + + +if __name__ == '__main__': + import cgitb + + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = WaterProgressWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QProgressBar/WaterProgressBar.py b/QProgressBar/WaterProgressBar.py index 5704fe06..30aa6d43 100644 --- a/QProgressBar/WaterProgressBar.py +++ b/QProgressBar/WaterProgressBar.py @@ -1,27 +1,27 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Created on 2018年4月1日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: WaterProgressBar -# description: - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - +""" +Created on 2018年4月1日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: WaterProgressBar +@description: +""" from random import randint -from PyQt5.Qt import QSpinBox -from PyQt5.QtCore import QTimer -from PyQt5.QtGui import QPixmap, QIcon -from PyQt5.QtWidgets import QWidget, QFormLayout, QRadioButton, QPushButton,\ - QColorDialog +try: + from PyQt5.QtCore import QTimer + from PyQt5.QtGui import QPixmap, QIcon + from PyQt5.QtWidgets import QApplication, QWidget, QFormLayout, QRadioButton, QPushButton, QColorDialog, \ + QSpinBox +except ImportError: + from PySide2.QtCore import QTimer + from PySide2.QtGui import QPixmap, QIcon + from PySide2.QtWidgets import QApplication, QWidget, QFormLayout, QRadioButton, QPushButton, QColorDialog, \ + QSpinBox from Lib.WaterRippleProgressBar import WaterRippleProgressBar # @UnresolvedImport @@ -53,11 +53,13 @@ def __init__(self, *args, **kwargs): layout.addWidget( QPushButton('设置随机0-100固定值', self, clicked=self.setRandomValue)) - layout.addRow('振幅(浪高)', - QSpinBox(self, value=1, valueChanged=self.bar.setWaterHeight)) + spb1 = QSpinBox(self, value=1) + spb1.valueChanged.connect(self.bar.setWaterHeight) + layout.addRow('振幅(浪高)', spb1) - layout.addRow('周期(密度)', - QSpinBox(self, value=1, valueChanged=self.bar.setWaterDensity)) + spb2 = QSpinBox(self, value=1) + spb2.valueChanged.connect(self.bar.setWaterDensity) + layout.addRow('周期(密度)', spb2) layout.addWidget(self.bar) @@ -116,9 +118,10 @@ def updateValue(self): value = 0 self.bar.setValue(value) + if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QPropertyAnimation/Data/pointtool.pyx b/QPropertyAnimation/Data/pointtool.pyx index bd47b667..3300cc66 100644 --- a/QPropertyAnimation/Data/pointtool.pyx +++ b/QPropertyAnimation/Data/pointtool.pyx @@ -1,10 +1,8 @@ from libc.math cimport pow - def getDistance(p1, p2): return pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2) - def findClose(points): cdef int plen = len(points) cdef int i = 0 diff --git a/QPropertyAnimation/Data/setup.py b/QPropertyAnimation/Data/setup.py index 09839f6e..3112b44c 100644 --- a/QPropertyAnimation/Data/setup.py +++ b/QPropertyAnimation/Data/setup.py @@ -10,8 +10,9 @@ # ) from distutils.core import setup + from Cython.Build import cythonize setup( ext_modules=cythonize("pointtool.pyx"), -) \ No newline at end of file +) diff --git a/QPropertyAnimation/FadeInOut.py b/QPropertyAnimation/FadeInOut.py index edf30eae..024c65c2 100644 --- a/QPropertyAnimation/FadeInOut.py +++ b/QPropertyAnimation/FadeInOut.py @@ -1,20 +1,22 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from PyQt5.QtCore import QPropertyAnimation -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton -# Created on 2018年6月14日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: FadeInOut -# description: -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +""" +Created on 2018年6月14日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: FadeInOut +@description: +""" + +try: + from PyQt5.QtCore import QPropertyAnimation + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton +except ImportError: + from PySide2.QtCore import QPropertyAnimation + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton class Window(QWidget): @@ -55,7 +57,7 @@ def doClose(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QPropertyAnimation/FlipWidgetAnimation.py b/QPropertyAnimation/FlipWidgetAnimation.py index c41947b9..1adc2648 100644 --- a/QPropertyAnimation/FlipWidgetAnimation.py +++ b/QPropertyAnimation/FlipWidgetAnimation.py @@ -4,21 +4,22 @@ """ Created on 2019年5月15日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: 翻转动画 @description: """ -from PyQt5.QtCore import Qt, pyqtSignal, QTimer -from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QStackedWidget, QLabel - -from Lib.FlipWidget import FlipWidget +try: + from PyQt5.QtCore import Qt, pyqtSignal, QTimer + from PyQt5.QtGui import QPixmap + from PyQt5.QtWidgets import QApplication, QStackedWidget, QLabel +except ImportError: + from PySide2.QtCore import Qt, Signal as pyqtSignal, QTimer + from PySide2.QtGui import QPixmap + from PySide2.QtWidgets import QApplication, QStackedWidget, QLabel -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 +from Lib.FlipWidget import FlipWidget class LoginWidget(QLabel): @@ -97,22 +98,22 @@ def showWidget(self): def jumpLoginWidget(self): # 翻转到登录界面 - self.setWindowOpacity(0) # 类似隐藏,但是保留了任务栏 + self.setWindowOpacity(0) # 类似隐藏,但是保留了任务栏 self.setCurrentWidget(self.loginWidget) # 很重要,一定要先切换过去,不然会导致第一次截图有误 - image1 = self.loginWidget.grab() # 截图1 - image2 = self.settingWidget.grab() # 截图2 - padding = 100 # 扩大边距 @UnusedVariable + image1 = self.loginWidget.grab() # 截图1 + image2 = self.settingWidget.grab() # 截图2 + padding = 100 # 扩大边距 @UnusedVariable self.flipWidget.setGeometry(self.geometry()) # .adjusted(-padding, -padding, padding, padding)) self.flipWidget.updateImages(FlipWidget.Right, image2, image1) def jumpSettingWidget(self): # 翻转到设置界面 - self.setWindowOpacity(0) # 类似隐藏,但是保留了任务栏 + self.setWindowOpacity(0) # 类似隐藏,但是保留了任务栏 self.setCurrentWidget(self.settingWidget) # 很重要,一定要先切换过去,不然会导致第一次截图有误 - image1 = self.loginWidget.grab() # 截图1 - image2 = self.settingWidget.grab() # 截图2 - padding = 100 # 扩大边距 @UnusedVariable + image1 = self.loginWidget.grab() # 截图1 + image2 = self.settingWidget.grab() # 截图2 + padding = 100 # 扩大边距 @UnusedVariable self.flipWidget.setGeometry(self.geometry()) # .adjusted(-padding, -padding, padding, padding)) self.flipWidget.updateImages(FlipWidget.Left, image1, image2) @@ -120,7 +121,7 @@ def jumpSettingWidget(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QPropertyAnimation/Lib/FlipWidget.py b/QPropertyAnimation/Lib/FlipWidget.py index 25ed75c4..6878e632 100644 --- a/QPropertyAnimation/Lib/FlipWidget.py +++ b/QPropertyAnimation/Lib/FlipWidget.py @@ -4,26 +4,27 @@ """ Created on 2019年5月15日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: FlipWidget @description: 动画翻转窗口 """ -from PyQt5.QtCore import pyqtSignal, Qt, QPropertyAnimation, QEasingCurve,\ - pyqtProperty, QPointF -from PyQt5.QtGui import QPainter, QTransform -from PyQt5.QtWidgets import QWidget - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' +try: + from PyQt5.QtCore import pyqtSignal, pyqtProperty, Qt, QPropertyAnimation, QEasingCurve, QPointF + from PyQt5.QtGui import QPainter, QTransform + from PyQt5.QtWidgets import QWidget +except ImportError: + from PySide2.QtCore import Signal as pyqtSignal, Property as pyqtProperty, Qt, QPropertyAnimation, \ + QEasingCurve, QPointF + from PySide2.QtGui import QPainter, QTransform + from PySide2.QtWidgets import QWidget class FlipWidget(QWidget): - - Left = 0 # 从右往左 - Right = 1 # 从左往右 - Scale = 3 # 图片缩放比例 + Left = 0 # 从右往左 + Right = 1 # 从左往右 + Scale = 3 # 图片缩放比例 finished = pyqtSignal() def __init__(self, *args, **kwargs): diff --git a/QPropertyAnimation/Lib/SlidingStackedWidget.py b/QPropertyAnimation/Lib/SlidingStackedWidget.py index 0d35b792..b10bb341 100644 --- a/QPropertyAnimation/Lib/SlidingStackedWidget.py +++ b/QPropertyAnimation/Lib/SlidingStackedWidget.py @@ -4,25 +4,23 @@ """ Created on 2018年11月24日 author: Irony -site: https://pyqt5.com , https://github.com/892768447 +site: https://pyqt.site , https://github.com/PyQt5 email: 892768447@qq.com file: description: 参考 http://qt.shoutwiki.com/wiki/Extending_QStackedWidget_for_sliding_page_animations_in_Qt """ -from PyQt5.QtCore import Qt, pyqtProperty, QEasingCurve, QPoint, \ - QPropertyAnimation, QParallelAnimationGroup, QTimer -from PyQt5.QtWidgets import QStackedWidget - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import Qt, pyqtProperty, QEasingCurve, QPoint, QPropertyAnimation, \ + QParallelAnimationGroup, QTimer + from PyQt5.QtWidgets import QStackedWidget +except ImportError: + from PySide2.QtCore import Qt, Property as pyqtProperty, QEasingCurve, QPoint, QPropertyAnimation, \ + QParallelAnimationGroup, QTimer + from PySide2.QtWidgets import QStackedWidget class SlidingStackedWidget(QStackedWidget): - LEFT2RIGHT, RIGHT2LEFT, TOP2BOTTOM, BOTTOM2TOP, AUTOMATIC = range(5) def __init__(self, *args, **kwargs): @@ -210,7 +208,7 @@ def setCurrentWidget(self, widget): def animationDoneSlot(self): """动画结束处理函数""" # 由于重写了setCurrentIndex方法所以这里要用父类本身的方法 -# self.setCurrentIndex(self._next) + # self.setCurrentIndex(self._next) QStackedWidget.setCurrentIndex(self, self._next) w = self.widget(self._now) w.hide() diff --git a/QPropertyAnimation/Lib/UiImageSlider.py b/QPropertyAnimation/Lib/UiImageSlider.py index a47fb819..78be1306 100644 --- a/QPropertyAnimation/Lib/UiImageSlider.py +++ b/QPropertyAnimation/Lib/UiImageSlider.py @@ -6,7 +6,11 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore, QtGui, QtWidgets +try: + from PyQt5 import QtCore, QtWidgets +except ImportError: + from PySide2 import QtCore, QtWidgets + class Ui_Form(object): def setupUi(self, Form): @@ -89,14 +93,15 @@ def retranslateUi(self, Form): self.pushButtonStart.setText(_translate("Form", "轮播开始")) self.pushButtonStop.setText(_translate("Form", "轮播停止")) + from Lib.SlidingStackedWidget import SlidingStackedWidget if __name__ == "__main__": import sys + app = QtWidgets.QApplication(sys.argv) Form = QtWidgets.QWidget() ui = Ui_Form() ui.setupUi(Form) Form.show() sys.exit(app.exec_()) - diff --git a/QPropertyAnimation/MenuAnimation.py b/QPropertyAnimation/MenuAnimation.py index e8a84c56..4e8c7cdc 100644 --- a/QPropertyAnimation/MenuAnimation.py +++ b/QPropertyAnimation/MenuAnimation.py @@ -4,20 +4,18 @@ """ Created on 2018年8月22日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: MenuAnimation @description: """ -from PyQt5.QtCore import QPropertyAnimation, QEasingCurve, QRect -from PyQt5.QtWidgets import QWidget, QMenu, QApplication - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QPropertyAnimation, QEasingCurve, QRect + from PyQt5.QtWidgets import QWidget, QMenu, QApplication +except ImportError: + from PySide2.QtCore import QPropertyAnimation, QEasingCurve, QRect + from PySide2.QtWidgets import QWidget, QMenu, QApplication class Window(QWidget): @@ -61,7 +59,8 @@ def initMenu(self): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.Hook(1, None, 5, sys.stderr, 'text') + + cgitb.enable(format='text') app = QApplication(sys.argv) w = Window() w.show() diff --git a/QPropertyAnimation/PageSwitching.py b/QPropertyAnimation/PageSwitching.py index 739f4040..9477eb4e 100644 --- a/QPropertyAnimation/PageSwitching.py +++ b/QPropertyAnimation/PageSwitching.py @@ -4,27 +4,25 @@ """ Created on 2018年11月24日 author: Irony -site: https://pyqt5.com , https://github.com/892768447 +site: https://pyqt.site , https://github.com/PyQt5 email: 892768447@qq.com file: PageSwitching description: """ import os -from PyQt5.QtCore import QEasingCurve, Qt -from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QWidget, QLabel +try: + from PyQt5.QtCore import QEasingCurve, Qt + from PyQt5.QtGui import QPixmap + from PyQt5.QtWidgets import QWidget, QLabel, QApplication +except ImportError: + from PySide2.QtCore import QEasingCurve, Qt + from PySide2.QtGui import QPixmap + from PySide2.QtWidgets import QWidget, QLabel, QApplication from Lib.UiImageSlider import Ui_Form # @UnresolvedImport -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - - class ImageSliderWidget(QWidget, Ui_Form): def __init__(self, *args, **kwargs): @@ -76,7 +74,7 @@ def setOrientation(self, checked): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = ImageSliderWidget() w.show() diff --git a/QPropertyAnimation/README.md b/QPropertyAnimation/README.md index 8a1e0ec5..3041e069 100644 --- a/QPropertyAnimation/README.md +++ b/QPropertyAnimation/README.md @@ -9,6 +9,7 @@ - [窗口翻转动画(仿QQ)](#6窗口翻转动画仿QQ) ## 1、窗口淡入淡出 + [运行 FadeInOut.py](FadeInOut.py) 1. 使用`QPropertyAnimation`对窗口的`windowOpacity`透明度属性进行修改 @@ -20,8 +21,9 @@ 1. 绑定动画完成后`finished`信号连接到`close`关闭窗口函数 ![FadeInOut](ScreenShot/FadeInOut.gif) - + ## 2、右键菜单动画 + [运行 MenuAnimation.py](MenuAnimation.py) 1. 使用`QPropertyAnimation`对菜单控件的`geometry`属性进行修改 @@ -30,6 +32,7 @@ ![MenuAnimation](ScreenShot/MenuAnimation.gif) ## 3、点阵特效 + [运行 RlatticeEffect.py](RlatticeEffect.py) 1. emmm,我也不知道这个动画叫啥名字,反正就是仿照网页做的 @@ -45,6 +48,7 @@ 1. PS: pyd是python3.4生成的,删掉pyd也能运行 这部分是js的核心 + ```js // for each point find the 5 closest points for(var i = 0; i < points.length; i++) { @@ -78,6 +82,7 @@ for(var i = 0; i < points.length; i++) { ``` 这部分是py的核心 + ```python def findClose(points): plen = len(points) @@ -105,10 +110,11 @@ def findClose(points): ![RlatticeEffect](ScreenShot/RlatticeEffect.gif) ## 4、页面切换/图片轮播动画 -[运行 PageSwitching.py](PageSwitching.py) + +[运行 PageSwitching.py](PageSwitching.py) | [查看 UiImageSlider.ui](Data/UiImageSlider.ui) 1. 使用`QPropertyAnimation`对`QStackedWidget`中的子控件进行pos位移操作实现动画切换特效 -1. 主要代码参考http://qt.shoutwiki.com/wiki/Extending_QStackedWidget_for_sliding_page_animations_in_Qt +1. 主要代码参考 1. 增加了自动切换函数 函数调用: @@ -121,6 +127,7 @@ def findClose(points): ![PageSwitching](ScreenShot/PageSwitching.gif) ## 5、窗口抖动 + [运行 ShakeWindow.py](ShakeWindow.py) 通过`QPropertyAnimation`对控件的pos属性进行死去活来的修改 @@ -128,6 +135,7 @@ def findClose(points): ![ShakeWindow](ScreenShot/ShakeWindow.gif) ## 6、窗口翻转动画(仿QQ) + [运行 FlipWidgetAnimation.py](FlipWidgetAnimation.py) 1. 用了两个`QLabel`来显示模拟的图片界面,并实现鼠标点击模拟真实的窗口对应位置点击 @@ -136,4 +144,4 @@ def findClose(points): 4. 通过`setWindowOpacity`控制主窗口的显示隐藏(保留任务栏),当然也可以用`hide` 5. 动画窗口`FlipWidget.py`主要实现两张图片的翻转显示,考虑到0-90和90-180之前的情况,以及图片的缩放动画 -![FlipWidgetAnimation](ScreenShot/FlipWidgetAnimation.gif) \ No newline at end of file +![FlipWidgetAnimation](ScreenShot/FlipWidgetAnimation.gif) diff --git a/QPropertyAnimation/RlatticeEffect.py b/QPropertyAnimation/RlatticeEffect.py index 7c443374..a57c37b1 100644 --- a/QPropertyAnimation/RlatticeEffect.py +++ b/QPropertyAnimation/RlatticeEffect.py @@ -4,7 +4,7 @@ """ Created on 2018年11月22日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: RlatticeEffect @description: @@ -12,29 +12,29 @@ from random import random from time import time -from PyQt5.QtCore import QPropertyAnimation, QObject, pyqtProperty, QEasingCurve,\ - Qt, QRectF, pyqtSignal -from PyQt5.QtGui import QColor, QPainterPath, QPainter -from PyQt5.QtWidgets import QWidget - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - +try: + from PyQt5.QtCore import QPropertyAnimation, QObject, QEasingCurve, Qt, QRectF, pyqtSignal, pyqtProperty + from PyQt5.QtGui import QColor, QPainterPath, QPainter + 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.QtWidgets import QApplication, QWidget try: from Lib import pointtool # @UnusedImport @UnresolvedImport + getDistance = pointtool.getDistance findClose = pointtool.findClose except: 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): @@ -66,8 +66,7 @@ def __init__(self, x, y): class Point(QObject): - - valueChanged = pyqtSignal() + valueChanged = pyqtSignal(int) def __init__(self, x, ox, y, oy, *args, **kwargs): super(Point, self).__init__(*args, **kwargs) @@ -90,12 +89,12 @@ def initAnimation(self): # 属性动画 if not hasattr(self, 'xanimation'): self.xanimation = QPropertyAnimation( - self, b'x', self, valueChanged=self.valueChanged.emit, - easingCurve=QEasingCurve.InOutSine) + self, b'x', self, easingCurve=QEasingCurve.InOutSine) + self.xanimation.valueChanged.connect(self.valueChanged.emit) self.yanimation = QPropertyAnimation( - self, b'y', self, valueChanged=self.valueChanged.emit, - easingCurve=QEasingCurve.InOutSine, - finished=self.updateAnimation) + self, b'y', self, easingCurve=QEasingCurve.InOutSine) + self.yanimation.valueChanged.connect(self.valueChanged.emit) + self.yanimation.finished.connect(self.updateAnimation) self.updateAnimation() def updateAnimation(self): @@ -138,6 +137,9 @@ def __init__(self, *args, **kwargs): self.target = Target(self.width() / 2, self.height() / 2) self.initPoints() + def update(self, *args): + super(Window, self).update() + def paintEvent(self, event): super(Window, self).paintEvent(event) painter = QPainter() @@ -220,8 +222,9 @@ def animate(self, painter): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, '') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QPropertyAnimation/ShakeWindow.py b/QPropertyAnimation/ShakeWindow.py index b161ce3b..6c7d9c95 100644 --- a/QPropertyAnimation/ShakeWindow.py +++ b/QPropertyAnimation/ShakeWindow.py @@ -4,18 +4,18 @@ """ Created on 2019年5月8日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ShakeWindow @description: 抖动动画 """ -from PyQt5.QtCore import QPropertyAnimation, QPoint -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QPropertyAnimation, QPoint + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton +except ImportError: + from PySide2.QtCore import QPropertyAnimation, QPoint + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton class Window(QWidget): @@ -66,7 +66,7 @@ def doShakeWindow(self, target): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QProxyStyle/Lib/TabBarStyle.py b/QProxyStyle/Lib/TabBarStyle.py index 12d7c8ab..59406ccf 100644 --- a/QProxyStyle/Lib/TabBarStyle.py +++ b/QProxyStyle/Lib/TabBarStyle.py @@ -4,7 +4,7 @@ """ Created on 2018年12月27日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: TabBarStyle @description: @@ -13,13 +13,6 @@ from PyQt5.QtWidgets import QProxyStyle -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" - - class TabBarStyle(QProxyStyle): def sizeFromContents(self, types, option, size, widget): diff --git a/QProxyStyle/Lib/TabCornerStyle.py b/QProxyStyle/Lib/TabCornerStyle.py new file mode 100644 index 00000000..fe793540 --- /dev/null +++ b/QProxyStyle/Lib/TabCornerStyle.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021年06月23日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TabCornerStyle +@description: +""" + +try: + from PyQt5.QtCore import QRect + from PyQt5.QtWidgets import QProxyStyle, QStyle +except ImportError: + from PySide2.QtCore import QRect + from PySide2.QtWidgets import QProxyStyle, QStyle + + +class TabCornerStyle(QProxyStyle): + + def subElementRect(self, element, option, widget): + try: + rect = super(TabCornerStyle, self).subElementRect(element, option, widget) + if element == QStyle.SE_TabWidgetRightCorner and rect.isValid(): + # 标签tab的矩形范围 + tab_rect = self.subElementRect(QStyle.SE_TabWidgetTabBar, option, widget) + # 内容面板的矩形范围 + panel_rect = self.subElementRect(QStyle.SE_TabWidgetTabPane, option, widget) + ext_height = 2 * self.pixelMetric(QStyle.PM_TabBarBaseHeight, option, widget) + # 修正过后填充的矩形范围 + cor_rect = QRect(tab_rect.x() + tab_rect.width() + ext_height, tab_rect.y() + ext_height, + panel_rect.width() - tab_rect.width() - 2 * ext_height, + tab_rect.height() - 2 * ext_height) + return cor_rect + return rect + except Exception as e: + print(e) + return QRect() diff --git a/QProxyStyle/README.md b/QProxyStyle/README.md index 7d9fe674..2ec8a71c 100644 --- a/QProxyStyle/README.md +++ b/QProxyStyle/README.md @@ -2,12 +2,26 @@ - 目录 - [QTabWidget Tab文字方向](#1qtabwidget-tab文字方向) + - [QTabWidget 角落控件位置](#2qtabwidget-角落控件位置) ## 1、QTabWidget Tab文字方向 + [运行 TabTextDirection.py](TabTextDirection.py) 1. 通过 `app.setStyle(TabBarStyle())` 设置代理样式 2. `sizeFromContents` 转置size 3. `drawControl` 绘制文字 -![TabTextDirection](ScreenShot/TabTextDirection.png) \ No newline at end of file +![TabTextDirection](ScreenShot/TabTextDirection.png) + +## 2、QTabWidget 角落控件位置 + +[运行 TabCornerWidget.py](TabCornerWidget.py) + +1. 通过 `app.setStyle(TabCornerStyle())` 设置代理样式 +2. `setCornerWidget` 设置自定义角落控件 + +原理是通过代理样式中对 `SE_TabWidgetRightCorner` 计算的结果进行校正,使得角落控件占满右边空白位置, +然后再配合自定义控件中使用 `QSpacerItem` 占据右边位置使得 + 号按钮居左,表现效果为 + 号按钮跟随标签的增加和减少 + +![TabCornerStyle](ScreenShot/TabCornerStyle.png) diff --git a/QProxyStyle/ScreenShot/TabCornerStyle.png b/QProxyStyle/ScreenShot/TabCornerStyle.png new file mode 100644 index 00000000..4133f625 Binary files /dev/null and b/QProxyStyle/ScreenShot/TabCornerStyle.png differ diff --git a/QProxyStyle/TabCornerWidget.py b/QProxyStyle/TabCornerWidget.py new file mode 100644 index 00000000..39d71462 --- /dev/null +++ b/QProxyStyle/TabCornerWidget.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021年06月23日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TabCornerWidget +@description: +""" + +try: + from PyQt5.QtCore import pyqtSignal, Qt + from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QSpacerItem, QSizePolicy, \ + QTabWidget +except ImportError: + from PySide2.QtCore import Signal as pyqtSignal, Qt + from PySide2.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QSpacerItem, QSizePolicy, \ + QTabWidget + +from QProxyStyle.Lib.TabCornerStyle import TabCornerStyle + + +class TabCornerWidget(QWidget): + signalTabAdd = pyqtSignal() + + def __init__(self, *args, **kwargs): + super(TabCornerWidget, self).__init__(*args, **kwargs) + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + self.buttonAdd = QPushButton('+', self, toolTip='添加新标签页', clicked=self.signalTabAdd.emit) + layout.addWidget(self.buttonAdd) + layout.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + + def resizeEvent(self, event): + super(TabCornerWidget, self).resizeEvent(event) + # 更新按钮高度 + if hasattr(self, 'buttonAdd'): + self.buttonAdd.setFixedSize(self.height(), self.height()) + + +if __name__ == '__main__': + import sys + + app = QApplication(sys.argv) + app.setStyle(TabCornerStyle()) + + tab1 = QTabWidget() + cor1 = TabCornerWidget(tab1) + cor1.signalTabAdd.connect(lambda: tab1.addTab(QWidget(tab1), 'tab' + str(tab1.count() + 1))) + tab1.setCornerWidget(cor1, Qt.TopRightCorner) + tab1.show() + + tab2 = QTabWidget() + tab2.setTabPosition(QTabWidget.South) # tab 标签方向 + cor2 = TabCornerWidget(tab2) + cor2.signalTabAdd.connect(lambda: tab2.addTab(QWidget(tab2), 'tab' + str(tab2.count() + 1))) + tab2.setCornerWidget(cor2, Qt.BottomRightCorner) + tab2.show() + + for i in range(10): + tab1.addTab(QWidget(tab1), 'tab' + str(i + 1)) + tab2.addTab(QWidget(tab1), 'tab' + str(i + 1)) + + sys.exit(app.exec_()) diff --git a/QProxyStyle/TabTextDirection.py b/QProxyStyle/TabTextDirection.py index 22dc4d86..52ba5687 100644 --- a/QProxyStyle/TabTextDirection.py +++ b/QProxyStyle/TabTextDirection.py @@ -4,7 +4,7 @@ """ Created on 2018年12月27日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: TestTabWidget @description: @@ -14,13 +14,6 @@ from Lib.TabBarStyle import TabBarStyle -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" - - class TabWidget(QTabWidget): def __init__(self, *args, **kwargs): @@ -44,6 +37,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) app.setStyle(TabBarStyle()) w = Window() diff --git a/QPushButton/BottomLineProgress.py b/QPushButton/BottomLineProgress.py index 7a19e59e..00352f54 100644 --- a/QPushButton/BottomLineProgress.py +++ b/QPushButton/BottomLineProgress.py @@ -1,25 +1,26 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年2月1日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: PushButtonLine @description: -''' -from random import randint -import sys - -from PyQt5.QtCore import QTimer, QThread, pyqtSignal -from PyQt5.QtGui import QPainter, QColor, QPen -from PyQt5.QtWidgets import QPushButton, QApplication, QWidget, QVBoxLayout +""" +import sys +from random import randint -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QTimer, QThread, pyqtSignal + from PyQt5.QtGui import QPainter, QColor, QPen + from PyQt5.QtWidgets import QPushButton, QApplication, QWidget, QVBoxLayout +except ImportError: + from PySide2.QtCore import QTimer, QThread, Signal as pyqtSignal + from PySide2.QtGui import QPainter, QColor, QPen + from PySide2.QtWidgets import QPushButton, QApplication, QWidget, QVBoxLayout StyleSheet = ''' PushButtonLine { @@ -32,7 +33,6 @@ class LoadingThread(QThread): - valueChanged = pyqtSignal(float) # 当前值/最大值 def __init__(self, *args, **kwargs): @@ -41,12 +41,13 @@ def __init__(self, *args, **kwargs): def run(self): for i in range(self.totalValue + 1): + if self.isInterruptionRequested(): + break self.valueChanged.emit(i / self.totalValue) QThread.msleep(randint(50, 100)) class PushButtonLine(QPushButton): - lineColor = QColor(0, 150, 136) def __init__(self, *args, **kwargs): @@ -57,6 +58,9 @@ def __init__(self, *args, **kwargs): self._timer = QTimer(self, timeout=self.update) self.clicked.connect(self.start) + def __del__(self): + self.stop() + def paintEvent(self, event): super(PushButtonLine, self).paintEvent(event) if not self._timer.isActive(): @@ -79,14 +83,21 @@ def start(self): self.setText(self._waitText) def stop(self): - self.loadingThread.valueChanged.disconnect(self.setPercent) - self.loadingThread.terminate() - self.loadingThread.deleteLater() - QThread.msleep(100) # 延迟等待deleteLater执行完毕 - del self.loadingThread - self._percent = 0 - self._timer.stop() - self.setText(self._text) + try: + if hasattr(self, "loadingThread"): + if self.loadingThread.isRunning(): + self.loadingThread.requestInterruption() + self.loadingThread.quit() + self.loadingThread.wait(2000) + del self.loadingThread + except RuntimeError: + pass + try: + self._percent = 0 + self._timer.stop() + self.setText(self._text) + except RuntimeError: + pass def setPercent(self, v): self._percent = v 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/FontRotate.py b/QPushButton/FontRotate.py index c9cae277..e8a728a2 100644 --- a/QPushButton/FontRotate.py +++ b/QPushButton/FontRotate.py @@ -1,31 +1,30 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年2月1日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: PushButtonFont @description: -''' - +""" import sys -from PyQt5.QtCore import QPropertyAnimation, Qt, QRectF -from PyQt5.QtGui import QFontDatabase -from PyQt5.QtWidgets import QPushButton, QApplication, QStyleOptionButton,\ - QStylePainter, QStyle - - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QPropertyAnimation, Qt, QRectF + from PyQt5.QtGui import QFontDatabase + from PyQt5.QtWidgets import QPushButton, QApplication, QStyleOptionButton, \ + QStylePainter, QStyle +except ImportError: + from PySide2.QtCore import QPropertyAnimation, Qt, QRectF + from PySide2.QtGui import QFontDatabase + from PySide2.QtWidgets import QPushButton, QApplication, QStyleOptionButton, \ + QStylePainter, QStyle class PushButtonFont(QPushButton): - LoadingText = "\uf110" def __init__(self, *args, **kwargs): diff --git a/QPushButton/NormalStyle.py b/QPushButton/NormalStyle.py index 831064b4..ec36a89f 100644 --- a/QPushButton/NormalStyle.py +++ b/QPushButton/NormalStyle.py @@ -1,23 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月29日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: NormalStyle @description: -''' -import sys -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QPushButton, QApplication +""" +import sys -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtWidgets import QWidget, QHBoxLayout, QPushButton, QApplication +except ImportError: + from PySide2.QtWidgets import QWidget, QHBoxLayout, QPushButton, QApplication -StyleSheet = ''' +StyleSheet = """ /*这里是通用设置,所有按钮都有效,不过后面的可以覆盖这个*/ QPushButton { border: none; /*去掉边框*/ @@ -85,7 +85,7 @@ color: white; /*文字颜色*/ background-color: #9c27b0; } -''' +""" class Window(QWidget): diff --git a/QPushButton/README.md b/QPushButton/README.md index 2328a51e..7467438d 100644 --- a/QPushButton/README.md +++ b/QPushButton/README.md @@ -5,8 +5,11 @@ - [按钮底部线条进度](#2按钮底部线条进度) - [按钮文字旋转进度](#3按钮文字旋转进度) - [按钮常用信号](#4按钮常用信号) + - [旋转动画按钮](#5旋转动画按钮) + - [弹性动画按钮](#6弹性动画按钮) ## 1、普通样式 + [运行 NormalStyle.py](NormalStyle.py) 主要改变背景颜色、鼠标按下颜色、鼠标悬停颜色、圆角、圆形、文字颜色 @@ -14,6 +17,7 @@ ![NormalStyle](ScreenShot/NormalStyle.gif) ## 2、按钮底部线条进度 + [运行 BottomLineProgress.py](BottomLineProgress.py) 在按钮下方画一条线,根据百分值绘制 @@ -21,6 +25,7 @@ ![BottomLineProgress](ScreenShot/BottomLineProgress.gif) ## 3、按钮文字旋转进度 + [运行 FontRotate.py](FontRotate.py) 利用字体,使用FontAwesome字体来显示一个圆形进度条,然后利用旋转动画 @@ -28,9 +33,22 @@ ![FontRotate](ScreenShot/FontRotate.gif) ## 4、按钮常用信号 + [运行 SignalsExample.py](SignalsExample.py) -根据官网文档 https://doc.qt.io/qt-5/qabstractbutton.html#signals 中的信号介绍编写 +根据官网文档 中的信号介绍编写 按钮的点击、按下、释放、选中信号演示 -![SignalsExample](ScreenShot/SignalsExample.gif) \ No newline at end of file +![SignalsExample](ScreenShot/SignalsExample.gif) + +## 5、旋转动画按钮 + +[运行 RotateButton.py](RotateButton.py) + +![RotateButton](ScreenShot/RotateButton.gif) + +## 6、弹性动画按钮 + +[运行 RubberBandButton.py](RubberBandButton.py) + +![RubberBandButton](ScreenShot/RubberBandButton.gif) diff --git a/QPushButton/RotateButton.py b/QPushButton/RotateButton.py new file mode 100644 index 00000000..e9b9f084 --- /dev/null +++ b/QPushButton/RotateButton.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2019年1月2日 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: Widgets.RotateButton +@description: +""" + +import os +import sys + +try: + from PyQt5.QtCore import QPointF, QPropertyAnimation, QRectF, Qt, pyqtProperty + from PyQt5.QtGui import QColor, QImage, QPainter, QPainterPath, QPixmap + from PyQt5.QtWidgets import ( + QGraphicsDropShadowEffect, + QPushButton, + QStyle, + QStyleOptionButton, + QStylePainter, + QApplication, + QWidget, + QHBoxLayout, + ) +except ImportError: + from PySide2.QtCore import QPointF, QPropertyAnimation, QRectF, Qt, pyqtProperty + from PySide2.QtGui import QColor, QImage, QPainter, QPainterPath, QPixmap + from PySide2.QtWidgets import ( + QGraphicsDropShadowEffect, + QPushButton, + QStyle, + QStyleOptionButton, + QStylePainter, + QApplication, + QWidget, + QHBoxLayout, + ) + + +class RotateButton(QPushButton): + + STARTVALUE = 0 # 起始旋转角度 + ENDVALUE = 360 # 结束旋转角度 + DURATION = 540 # 动画完成总时间 + + def __init__(self, *args, **kwargs): + super(RotateButton, self).__init__(*args, **kwargs) + self.setCursor(Qt.PointingHandCursor) + self._angle = 0 # 角度 + self._padding = 10 # 阴影边距 + self._image = "" # 图片路径 + self._shadowColor = QColor(33, 33, 33) # 阴影颜色 + self._pixmap = None # 图片对象 + # 属性动画 + self._animation = QPropertyAnimation(self, b"angle", self) + self._animation.setLoopCount(1) # 只循环一次 + + def paintEvent(self, event): + """绘制事件""" + text = self.text() + option = QStyleOptionButton() + self.initStyleOption(option) + option.text = "" # 不绘制文字 + painter = QStylePainter(self) + painter.setRenderHint(QStylePainter.Antialiasing) + painter.setRenderHint(QStylePainter.HighQualityAntialiasing) + painter.setRenderHint(QStylePainter.SmoothPixmapTransform) + painter.drawControl(QStyle.CE_PushButton, option) + # 变换坐标为正中间 + painter.translate(self.rect().center()) + painter.rotate(self._angle) # 旋转 + + # 绘制图片 + if self._pixmap and not self._pixmap.isNull(): + w = self.width() + h = self.height() + pos = QPointF(-self._pixmap.width() / 2, -self._pixmap.height() / 2) + painter.drawPixmap(pos, self._pixmap) + elif text: + # 在变换坐标后的正中间画文字 + fm = self.fontMetrics() + w = fm.width(text) + h = fm.height() + rect = QRectF(0 - w * 2, 0 - h, w * 2 * 2, h * 2) + painter.drawText(rect, Qt.AlignCenter, text) + else: + super(RotateButton, self).paintEvent(event) + + def enterEvent(self, _): + """鼠标进入事件""" + # 设置阴影 + # 边框阴影效果 + effect = QGraphicsDropShadowEffect(self) + effect.setBlurRadius(self._padding * 2) + effect.setOffset(0, 0) + effect.setColor(self._shadowColor) + self.setGraphicsEffect(effect) + + # 开启旋转动画 + self._animation.stop() + cv = self._animation.currentValue() or self.STARTVALUE + self._animation.setDuration( + self.DURATION if cv == 0 else int(cv / self.ENDVALUE * self.DURATION) + ) + self._animation.setStartValue(cv) + self._animation.setEndValue(self.ENDVALUE) + self._animation.start() + + def leaveEvent(self, _): + """鼠标离开事件""" + # 取消阴影 + self.setGraphicsEffect(None) + + # 旋转动画 + self._animation.stop() + cv = self._animation.currentValue() or self.ENDVALUE + self._animation.setDuration(int(cv / self.ENDVALUE * self.DURATION)) + self._animation.setStartValue(cv) + self._animation.setEndValue(self.STARTVALUE) + self._animation.start() + + def setPixmap(self, path): + if not os.path.exists(path): + self._image = "" + self._pixmap = None + return + self._image = path + size = ( + max( + min(self.width(), self.height()), + min(self.minimumWidth(), self.minimumHeight()), + ) + - self._padding + ) # 需要边距的边框 + radius = int(size / 2) + image = QImage(size, size, QImage.Format_ARGB32_Premultiplied) + image.fill(Qt.transparent) # 填充背景为透明 + pixmap = QPixmap(path).scaled( + size, size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation + ) + # QPainter + painter = QPainter() + painter.begin(image) + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setRenderHint(QPainter.SmoothPixmapTransform, True) + # QPainterPath + path = QPainterPath() + path.addRoundedRect(0, 0, size, size, radius, radius) + # 切割圆 + painter.setClipPath(path) + painter.drawPixmap(0, 0, pixmap) + painter.end() + self._pixmap = QPixmap.fromImage(image) + self.update() + + def pixmap(self): + return self._pixmap + + @pyqtProperty(str) + def image(self): + return self._image + + @image.setter + def image(self, path): + self.setPixmap(path) + + @pyqtProperty(int) + def angle(self): + return self._angle + + @angle.setter + def angle(self, value): + self._angle = value + self.update() + + @pyqtProperty(int) + def padding(self): + return self._padding + + @padding.setter + def padding(self, value): + self._padding = value + + @pyqtProperty(QColor) + def shadowColor(self): + return self._shadowColor + + @shadowColor.setter + def shadowColor(self, color): + self._shadowColor = QColor(color) + + +class TestWindow(QWidget): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + layout = QHBoxLayout(self) + + btn = RotateButton("pyqt.site", self) + btn.setMinimumHeight(96) + btn.setToolTip("旋转按钮") + layout.addWidget(btn) + + btn = RotateButton("", self) + btn.setMinimumHeight(96) + btn.setObjectName("imageLabel1") + btn.setPixmap("./Data/Images/avatar.jpg") + layout.addWidget(btn) + + btn = RotateButton("", self) + btn.setMinimumHeight(96) + btn.setObjectName("imageLabel2") + layout.addWidget(btn) + + +if __name__ == "__main__": + import cgitb + + cgitb.enable(1, None, 5, "text") + + # cd to current dir + os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) + + app = QApplication(sys.argv) + app.setStyleSheet( + """ + RotateButton { + font-size: 48px; + } + #imageLabel1, #imageLabel2 { + background: transparent; + } + #imageLabel2 { + qproperty-image: "./Data/Images/avatar.jpg"; + } + """ + ) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QPushButton/RubberBandButton.py b/QPushButton/RubberBandButton.py new file mode 100644 index 00000000..75af92eb --- /dev/null +++ b/QPushButton/RubberBandButton.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2019年1月3日 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: Widgets.RubberBandButton +@description: +""" + +try: + from PyQt5.QtCore import ( + QEasingCurve, + QParallelAnimationGroup, + QPropertyAnimation, + QRectF, + Qt, + pyqtProperty, + ) + from PyQt5.QtGui import QColor, QPainter + from PyQt5.QtWidgets import ( + QPushButton, + QStyle, + QStyleOptionButton, + QStylePainter, + QWidget, + QApplication, + QHBoxLayout, + ) +except ImportError: + from PySide2.QtCore import ( + QEasingCurve, + QParallelAnimationGroup, + QPropertyAnimation, + QRectF, + Qt, + Property as pyqtProperty, + ) + from PySide2.QtGui import QColor, QPainter + from PySide2.QtWidgets import ( + QPushButton, + QStyle, + QStyleOptionButton, + QApplication, + QStylePainter, + QWidget, + QHBoxLayout, + ) + + +class RubberBandButton(QPushButton): + + def __init__(self, *args, **kwargs): + self._bgcolor = QColor(kwargs.pop("bgcolor", Qt.green)) + super(RubberBandButton, self).__init__(*args, **kwargs) + self.setFlat(True) + self.setCursor(Qt.PointingHandCursor) + self._width = 0 + self._height = 0 + + def paintEvent(self, event): + self._initAnimate() + painter = QStylePainter(self) + painter.setRenderHint(QPainter.Antialiasing, True) + painter.setRenderHint(QPainter.HighQualityAntialiasing, True) + painter.setRenderHint(QPainter.SmoothPixmapTransform, True) + painter.setBrush(QColor(self._bgcolor)) + painter.setPen(QColor(self._bgcolor)) + painter.drawEllipse( + QRectF( + (self.minimumWidth() - self._width) / 2, + (self.minimumHeight() - self._height) / 2, + self._width, + self._height, + ) + ) + # 绘制本身的文字和图标 + options = QStyleOptionButton() + options.initFrom(self) + size = options.rect.size() + size.transpose() + options.rect.setSize(size) + options.features = QStyleOptionButton.Flat + options.text = self.text() + options.icon = self.icon() + options.iconSize = self.iconSize() + painter.drawControl(QStyle.CE_PushButton, options) + event.accept() + + def _initAnimate(self): + if hasattr(self, "_animate"): + return + self._width = self.minimumWidth() * 7 / 8 + self._height = self.minimumHeight() * 7 / 8 + # self._width=175 + # self._height=175 + + # 宽度动画 + wanimate = QPropertyAnimation(self, b"rWidth") + wanimate.setEasingCurve(QEasingCurve.OutElastic) + wanimate.setDuration(700) + wanimate.valueChanged.connect(self.update) + + # 插入宽度线性值 + wanimate.setKeyValueAt(0, self._width) + # wanimate.setKeyValueAt(0.1, 180) + # wanimate.setKeyValueAt(0.2, 185) + # wanimate.setKeyValueAt(0.3, 190) + # wanimate.setKeyValueAt(0.4, 195) + wanimate.setKeyValueAt(0.5, self._width + 6) + # wanimate.setKeyValueAt(0.6, 195) + # wanimate.setKeyValueAt(0.7, 190) + # wanimate.setKeyValueAt(0.8, 185) + # wanimate.setKeyValueAt(0.9, 180) + wanimate.setKeyValueAt(1, self._width) + + # 高度动画 + hanimate = QPropertyAnimation(self, b"rHeight") + hanimate.setEasingCurve(QEasingCurve.OutElastic) + hanimate.setDuration(700) + + # 插入高度线性值 + hanimate.setKeyValueAt(0, self._height) + # hanimate.setKeyValueAt(0.1, 170) + # hanimate.setKeyValueAt(0.3, 165) + hanimate.setKeyValueAt(0.5, self._height - 6) + # hanimate.setKeyValueAt(0.7, 165) + # hanimate.setKeyValueAt(0.9, 170) + hanimate.setKeyValueAt(1, self._height) + + # 设置动画组 + self._animate = QParallelAnimationGroup(self) + self._animate.addAnimation(wanimate) + self._animate.addAnimation(hanimate) + + def enterEvent(self, event): + super(RubberBandButton, self).enterEvent(event) + self._animate.stop() + self._animate.start() + + @pyqtProperty(int) + def rWidth(self): + return self._width + + @rWidth.setter + def rWidth(self, value): + self._width = value + + @pyqtProperty(int) + def rHeight(self): + return self._height + + @rHeight.setter + def rHeight(self, value): + self._height = value + + @pyqtProperty(QColor) + def bgColor(self): + return self._bgcolor + + @bgColor.setter + def bgColor(self, color): + self._bgcolor = QColor(color) + + +class TestWindow(QWidget): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + layout = QHBoxLayout(self) + layout.addWidget(RubberBandButton("d", self, bgcolor="#01847E")) + layout.addWidget(RubberBandButton("v", self, bgcolor="#7FA91F")) + layout.addWidget(RubberBandButton("N", self, bgcolor="#FC8416")) + layout.addWidget(RubberBandButton("U", self, bgcolor="#66D3c0")) + layout.addWidget(RubberBandButton("a", self, bgcolor="#F28195")) + + +if __name__ == "__main__": + import cgitb, sys + + cgitb.enable(1, None, 5, "text") + + app = QApplication(sys.argv) + app.setStyleSheet( + """ + RubberBandButton { + min-width: 100px; + max-width: 100px; + min-height: 100px; + max-height: 100px; + border: none; + color: white; + outline: none; + margin: 4px; + font-family: webdings; + font-size: 60px; + } + """ + ) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QPushButton/ScreenShot/RotateButton.gif b/QPushButton/ScreenShot/RotateButton.gif new file mode 100644 index 00000000..6b1c3aa4 Binary files /dev/null and b/QPushButton/ScreenShot/RotateButton.gif differ diff --git a/QPushButton/ScreenShot/RubberBandButton.gif b/QPushButton/ScreenShot/RubberBandButton.gif new file mode 100644 index 00000000..dcf1e81a Binary files /dev/null and b/QPushButton/ScreenShot/RubberBandButton.gif differ diff --git a/QPushButton/SignalsExample.py b/QPushButton/SignalsExample.py index 0b3cf664..35163e81 100644 --- a/QPushButton/SignalsExample.py +++ b/QPushButton/SignalsExample.py @@ -4,17 +4,16 @@ """ Created on 2019年7月2日 @author: Irony -@site: https://pyqt5.com https://github.com/PyQt5 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QPushButton.SignalsExample @description: 按钮信号例子 """ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QPlainTextEdit - -__Author__ = "Irony" -__Copyright__ = "Copyright (c) 2019" -__Version__ = "Version 1.0" +try: + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QPlainTextEdit +except ImportError: + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QPlainTextEdit class Window(QWidget): @@ -26,14 +25,23 @@ def __init__(self, *args, **kwargs): btn1 = QPushButton('按钮点击信号', self) btn1.setObjectName('ClickBtn') btn1.clicked.connect(self.onClicked) - layout.addWidget(btn1) - layout.addWidget(QPushButton( - '按钮按下信号', self, objectName='PressBtn', pressed=self.onPressed)) - layout.addWidget(QPushButton( - '按钮释放信号', self, objectName='ReleaseBtn', released=self.onReleased)) - layout.addWidget(QPushButton( - '按钮选中信号', self, checkable=True, objectName='ToggleBtn', toggled=self.onToggled)) + + btn2 = QPushButton('按钮按下信号', self) + btn2.setObjectName('PressBtn') + btn2.pressed.connect(self.onPressed) + layout.addWidget(btn2) + + btn3 = QPushButton('按钮释放信号', self) + btn3.setObjectName('ReleaseBtn') + btn3.released.connect(self.onReleased) + layout.addWidget(btn3) + + btn4 = QPushButton('按钮释放信号', self) + btn4.setObjectName('ToggleBtn') + btn4.setCheckable(True) + btn4.toggled.connect(self.onToggled) + layout.addWidget(btn4) self.resultView = QPlainTextEdit(self) self.resultView.setReadOnly(True) @@ -58,7 +66,7 @@ def onToggled(self, checked): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QScrollArea/Lib/SettingUi.py b/QScrollArea/Lib/SettingUi.py index a9bfdffc..d25f3ef2 100644 --- a/QScrollArea/Lib/SettingUi.py +++ b/QScrollArea/Lib/SettingUi.py @@ -6,7 +6,11 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore, QtGui, QtWidgets +try: + from PyQt5 import QtCore, QtWidgets +except ImportError: + from PySide2 import QtCore, QtWidgets + class Ui_Setting(object): def setupUi(self, Setting): @@ -151,7 +155,8 @@ def setupUi(self, Setting): self.comboBox.addItem("") self.comboBox.addItem("") self.horizontalLayout_2.addWidget(self.comboBox) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacerItem) self.formLayout_9.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_2) self.checkBox_30 = QtWidgets.QCheckBox(self.widget_2) @@ -171,7 +176,8 @@ def setupUi(self, Setting): self.pushButton_4 = QtWidgets.QPushButton(self.widget_2) self.pushButton_4.setObjectName("pushButton_4") self.horizontalLayout_3.addWidget(self.pushButton_4) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_3.addItem(spacerItem1) self.formLayout_9.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_3) self.verticalLayout.addWidget(self.widget_2) @@ -469,7 +475,8 @@ def retranslateUi(self, Setting): self.checkBox_23.setText(_translate("Setting", "启用一声问候消息")) self.checkBox_24.setText(_translate("Setting", "启用设备连接提醒")) self.right5.setText(_translate("Setting", "当插入安卓设备时,提示安装或者更新QQ手机版")) - self.label_3.setText(_translate("Setting", "

您可以设置是否在屏幕右下角收到来自QQ空间的通知,进入设置

")) + self.label_3.setText(_translate("Setting", + "

您可以设置是否在屏幕右下角收到来自QQ空间的通知,进入设置

")) self.label_4.setText(_translate("Setting", "好友上线提醒")) self.radioButton.setText(_translate("Setting", "关闭好友上线提醒")) self.radioButton_2.setText(_translate("Setting", "全部好友上线提醒")) @@ -485,10 +492,10 @@ def retranslateUi(self, Setting): if __name__ == "__main__": import sys + app = QtWidgets.QApplication(sys.argv) Setting = QtWidgets.QWidget() ui = Ui_Setting() ui.setupUi(Setting) Setting.show() sys.exit(app.exec_()) - diff --git a/QScrollArea/QQSettingPanel.py b/QScrollArea/QQSettingPanel.py index 6dccd7c0..2a5236aa 100644 --- a/QScrollArea/QQSettingPanel.py +++ b/QScrollArea/QQSettingPanel.py @@ -1,22 +1,21 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from PyQt5.QtWidgets import QWidget - -from Lib.SettingUi import Ui_Setting # @UnresolvedImport +""" +Created on 2018年3月28日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: QQSettingPanel +@description: +""" -# Created on 2018年3月28日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: QQSettingPanel -# description: +try: + from PyQt5.QtWidgets import QApplication, QWidget +except ImportError: + from PySide2.QtWidgets import QApplication, QWidget -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +from Lib.SettingUi import Ui_Setting # @UnresolvedImport class Window(QWidget, Ui_Setting): @@ -59,9 +58,9 @@ def onItemClicked(self, item): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) - app.setStyleSheet(open("Data/style.qss", "rb").read().decode("utf-8")) + app.setStyleSheet(open('Data/style.qss', 'rb').read().decode('utf-8')) w = Window() w.show() sys.exit(app.exec_()) diff --git a/QScrollArea/README.md b/QScrollArea/README.md index 65ff31b1..52294bd5 100644 --- a/QScrollArea/README.md +++ b/QScrollArea/README.md @@ -4,7 +4,8 @@ - [仿QQ设置面板](#1仿QQ设置面板) ## 1、仿QQ设置面板 -[运行 QQSettingPanel.py](QQSettingPanel.py) + +[运行 QQSettingPanel.py](QQSettingPanel.py) | [查看 setting.ui](Data/setting.ui) 1. 左侧为`QListWidget`,右侧使用`QScrollArea`设置`QVBoxLayout`,然后依次往里面添加QWidget 2. 右侧添加`QWidget`的时候有两种方案 @@ -12,9 +13,10 @@ 2. 左侧list添加item时给定右侧对应的widget变量值 相关事件: + 1. 绑定左侧`QListWidget`的`itemClicked`的到该item的索引 2. 绑定右侧滚动条的`valueChanged`事件得到pos 注意:当`itemClicked`时定位滚动条的值时,需要设置一个标志位用来避免`valueChanged`重复调用item的定位 -![QQSettingPanel](ScreenShot/QQSettingPanel.gif) \ No newline at end of file +![QQSettingPanel](ScreenShot/QQSettingPanel.gif) diff --git a/QScrollBar/README.md b/QScrollBar/README.md index 1de1b8b0..6445e59a 100644 --- a/QScrollBar/README.md +++ b/QScrollBar/README.md @@ -4,6 +4,7 @@ - [滚动条样式美化](#1滚动条样式美化) ## 1、滚动条样式美化 + [运行 StyleScrollBar.py](StyleScrollBar.py) 使用QSS和图片对滚动条进行美化(horizontal 横向、vertical 纵向) diff --git a/QScrollBar/StyleScrollBar.py b/QScrollBar/StyleScrollBar.py index 96c591d2..f5d4d347 100644 --- a/QScrollBar/StyleScrollBar.py +++ b/QScrollBar/StyleScrollBar.py @@ -1,22 +1,22 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月20日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ScrollBar @description: -''' -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QTextEdit, QApplication +""" import chardet - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import QTextEdit, QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QTextEdit, QApplication class Window(QTextEdit): @@ -38,6 +38,7 @@ def __init__(self, parent=None): if __name__ == "__main__": import sys + app = QApplication(sys.argv) app.setApplicationName("滚动条样式") app.setApplicationDisplayName("滚动条样式") diff --git a/QSerialPort/Lib/UiSerialPort.py b/QSerialPort/Lib/UiSerialPort.py index 0da8c351..934e2156 100644 --- a/QSerialPort/Lib/UiSerialPort.py +++ b/QSerialPort/Lib/UiSerialPort.py @@ -6,19 +6,20 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtWidgets + class Ui_FormSerialPort(object): def setupUi(self, FormSerialPort): FormSerialPort.setObjectName("FormSerialPort") FormSerialPort.resize(721, 597) FormSerialPort.setStyleSheet("#labelStatus {\n" -" border-radius: 13px;\n" -" background-color: gray;\n" -"}\n" -"#labelStatus[isOn=\"true\"] {\n" -" background-color: green;\n" -"}") + " border-radius: 13px;\n" + " background-color: gray;\n" + "}\n" + "#labelStatus[isOn=\"true\"] {\n" + " background-color: green;\n" + "}") self.gridLayout = QtWidgets.QGridLayout(FormSerialPort) self.gridLayout.setObjectName("gridLayout") self.groupBox = QtWidgets.QGroupBox(FormSerialPort) @@ -86,7 +87,8 @@ def setupUi(self, FormSerialPort): self.labelStatus.setProperty("isOn", False) self.labelStatus.setObjectName("labelStatus") self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.labelStatus) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Expanding) self.formLayout.setItem(6, QtWidgets.QFormLayout.FieldRole, spacerItem) self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1) self.textBrowser = QtWidgets.QTextBrowser(FormSerialPort) @@ -166,10 +168,10 @@ def retranslateUi(self, FormSerialPort): if __name__ == "__main__": import sys + app = QtWidgets.QApplication(sys.argv) FormSerialPort = QtWidgets.QWidget() ui = Ui_FormSerialPort() ui.setupUi(FormSerialPort) FormSerialPort.show() sys.exit(app.exec_()) - diff --git a/QSerialPort/README.md b/QSerialPort/README.md index ab9916bb..00c3de0b 100644 --- a/QSerialPort/README.md +++ b/QSerialPort/README.md @@ -4,9 +4,10 @@ - [串口调试小助手](#1串口调试小助手) ## 1、串口调试小助手 -[运行 SerialDebugAssistant.py](SerialDebugAssistant.py) -用`QSerialPort`写了个类似串口调试小助手的工具, 这个类的官方资料: http://doc.qt.io/qt-5/qserialport.html +[运行 SerialDebugAssistant.py](SerialDebugAssistant.py) | [查看 UiSerialPort.ui](Data/UiSerialPort.ui) + +用`QSerialPort`写了个类似串口调试小助手的工具, 这个类的官方资料: 1. 通过`QSerialPortInfo.availablePorts()` 获取所有可用的串口 1. `QSerialPort.setPortName` 设置串口名 @@ -16,5 +17,4 @@ 1. `QSerialPort.setStopBits` 设置停止位 1. `QSerialPort.setFlowControl` 设置流控制 - -![SerialDebugAssistant](ScreenShot/SerialDebugAssistant.gif) \ No newline at end of file +![SerialDebugAssistant](ScreenShot/SerialDebugAssistant.gif) diff --git a/QSerialPort/SerialDebugAssistant.py b/QSerialPort/SerialDebugAssistant.py index 0f87638f..5f5c8e41 100644 --- a/QSerialPort/SerialDebugAssistant.py +++ b/QSerialPort/SerialDebugAssistant.py @@ -4,7 +4,7 @@ """ Created on 2018年11月6日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: SerialDebugAssistant @description: 串口调试小助手 @@ -16,13 +16,6 @@ from Lib.UiSerialPort import Ui_FormSerialPort # @UnresolvedImport -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - - class Window(QWidget, Ui_FormSerialPort): def __init__(self, *args, **kwargs): @@ -51,7 +44,7 @@ def on_buttonConnect_clicked(self): QMessageBox.critical(self, '错误', '没有选择串口') return port = self._ports[name] -# self._serial.setPort(port) + # self._serial.setPort(port) # 根据名字设置串口(也可以用上面的函数) self._serial.setPortName(port.systemLocation()) # 设置波特率 @@ -139,8 +132,10 @@ def closeEvent(self, event): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, '') + + cgitb.enable(format='text') from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QSlider/ClickJumpSlider.py b/QSlider/ClickJumpSlider.py index 3e7f00a3..d12f97a5 100644 --- a/QSlider/ClickJumpSlider.py +++ b/QSlider/ClickJumpSlider.py @@ -4,21 +4,20 @@ """ Created on 2018年11月5日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ClickJumpSlider @description: """ -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QSlider, QStyleOptionSlider, QStyle, QWidget,\ - QFormLayout, QLabel - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import QApplication, QSlider, QStyleOptionSlider, QStyle, QWidget, QFormLayout, \ + QLabel +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QApplication, QSlider, QStyleOptionSlider, QStyle, QWidget, QFormLayout, \ + QLabel class ClickJumpSlider(QSlider): @@ -55,31 +54,34 @@ def __init__(self, *args, **kwargs): layout = QFormLayout(self) self.label1 = QLabel('0', self) - layout.addRow(self.label1, ClickJumpSlider( - Qt.Horizontal, valueChanged=lambda v: self.label1.setText(str(v)))) + self.slider1 = ClickJumpSlider(Qt.Horizontal) + self.slider1.valueChanged.connect(lambda v: self.label1.setText(str(v))) + layout.addRow(self.label1, self.slider1) # 横向-反向显示 self.label2 = QLabel('0', self) - layout.addRow(self.label2, ClickJumpSlider( - Qt.Horizontal, invertedAppearance=True, - valueChanged=lambda v: self.label2.setText(str(v)))) + self.slider2 = ClickJumpSlider(Qt.Horizontal, invertedAppearance=True) + self.slider2.valueChanged.connect(lambda v: self.label2.setText(str(v))) + layout.addRow(self.label2, self.slider2) self.label3 = QLabel('0', self) - layout.addRow(self.label3, ClickJumpSlider( - Qt.Vertical, minimumHeight=200, valueChanged=lambda v: self.label3.setText(str(v)))) + self.slider3 = ClickJumpSlider(Qt.Vertical, minimumHeight=200) + self.slider3.valueChanged.connect(lambda v: self.label3.setText(str(v))) + layout.addRow(self.label3, self.slider3) # 纵向反向显示 self.label4 = QLabel('0', self) - layout.addRow(self.label4, ClickJumpSlider( - Qt.Vertical, invertedAppearance=True, - minimumHeight=200, valueChanged=lambda v: self.label4.setText(str(v)))) + self.slider4 = ClickJumpSlider(Qt.Vertical, invertedAppearance=True, minimumHeight=200) + self.slider4.valueChanged.connect(lambda v: self.label4.setText(str(v))) + layout.addRow(self.label4, self.slider4) if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, '') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = DemoWindow() w.show() diff --git a/QSlider/LfSlider.py b/QSlider/LfSlider.py new file mode 100644 index 00000000..2c900cfa --- /dev/null +++ b/QSlider/LfSlider.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/4/9 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: LfSlider +@description: 降低值变化频率 +""" +from datetime import datetime + +try: + from PyQt5.QtCore import pyqtSignal, QTimer, Qt + from PyQt5.QtWidgets import QApplication, QSlider, QWidget, QVBoxLayout, QPlainTextEdit, QHBoxLayout, \ + QGroupBox +except ImportError: + from PySide2.QtCore import Signal as pyqtSignal, QTimer, Qt + from PySide2.QtWidgets import QApplication, QSlider, QWidget, QVBoxLayout, QPlainTextEdit, QHBoxLayout, \ + QGroupBox + + +class LfSlider(QSlider): + valueChanged = pyqtSignal(int) + + def __init__(self, *args, **kwargs): + delay = kwargs.pop('delay', 500) + super(LfSlider, self).__init__(*args, **kwargs) + self.lastValue = self.value() + self.uTimer = QTimer(self) + self.uTimer.timeout.connect(self.onValueChanged) + self.uTimer.start(delay) + + def onValueChanged(self): + if self.lastValue != self.value(): + self.lastValue = self.value() + self.valueChanged.emit(self.lastValue) + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + layout = QHBoxLayout(self) + + # 左侧原始 + left_group = QGroupBox('原始QSlider', self) + left_layout = QVBoxLayout(left_group) + self.leftLabel = QPlainTextEdit(self) + left_layout.addWidget(self.leftLabel) + + self.leftSlider = QSlider(Qt.Horizontal, self) + self.leftSlider.valueChanged.connect(self.onLeftChanged) + left_layout.addWidget(self.leftSlider) + + layout.addWidget(left_group) + + # 右侧低频率变化 + right_group = QGroupBox('LfSlider', self) + right_layout = QVBoxLayout(right_group) + self.rightLabel = QPlainTextEdit(self) + right_layout.addWidget(self.rightLabel) + + self.rightSlider = LfSlider(Qt.Horizontal, self) + self.rightSlider.valueChanged.connect(self.onRightChanged) + right_layout.addWidget(self.rightSlider) + + layout.addWidget(right_group) + + def onLeftChanged(self, value): + self.leftLabel.appendPlainText(datetime.now().strftime("[%H:%M:%S.%f] ") + str(value)) + + def onRightChanged(self, value): + self.rightLabel.appendPlainText(datetime.now().strftime("[%H:%M:%S.%f] ") + str(value)) + + +if __name__ == '__main__': + import sys + import cgitb + + cgitb.enable(format='text') + + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QSlider/PaintQSlider.py b/QSlider/PaintQSlider.py index 0d154e8b..d72a7574 100644 --- a/QSlider/PaintQSlider.py +++ b/QSlider/PaintQSlider.py @@ -4,24 +4,18 @@ """ Created on 2018年5月15日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: PaintQSlider @description: """ + from PyQt5.QtCore import Qt, QRect, QPointF from PyQt5.QtGui import QPainter, QColor -from PyQt5.QtWidgets import QSlider, QWidget, QVBoxLayout, QProxyStyle, QStyle,\ +from PyQt5.QtWidgets import QSlider, QWidget, QVBoxLayout, QProxyStyle, QStyle, \ QStyleOptionSlider -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" - - class SliderStyle(QProxyStyle): def subControlRect(self, control, option, subControl, widget=None): @@ -124,6 +118,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.setStyleSheet('QWidget {background: gray;}') diff --git a/QSlider/QssQSlider.py b/QSlider/QssQSlider.py index af65e491..7b3a0f33 100644 --- a/QSlider/QssQSlider.py +++ b/QSlider/QssQSlider.py @@ -4,20 +4,18 @@ """ Created on 2018年5月15日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QssQSlider @description: 通过QSS美化QSlider """ -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QSlider - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QSlider +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QSlider StyleSheet = """ QWidget { @@ -76,7 +74,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) app.setStyleSheet(StyleSheet) w = Window() diff --git a/QSlider/README.md b/QSlider/README.md index 488c2930..ee33d3be 100644 --- a/QSlider/README.md +++ b/QSlider/README.md @@ -3,8 +3,10 @@ - 目录 - [滑动条点击定位](#1滑动条点击定位) - [双层圆环样式](#2双层圆环样式) + - [低频率值变化](#3低频率值变化) ## 1、滑动条点击定位 + [运行 ClickJumpSlider.py](ClickJumpSlider.py) 1. `QSlider`对鼠标点击然后跳转到该位置的支持不是很好,通过重写鼠标点击事件`mousePressEvent`来达到效果 @@ -40,7 +42,16 @@ def mousePressEvent(self, event): ![ClickJumpSlider](ScreenShot/ClickJumpSlider.gif) ## 2、双层圆环样式 + [运行 QssQSlider.py](QssQSlider.py) | [运行 PaintQSlider.py](PaintQSlider.py) ![QssQSlider](ScreenShot/QssQSlider.gif) -![PaintQSlider](ScreenShot/PaintQSlider.gif) \ No newline at end of file +![PaintQSlider](ScreenShot/PaintQSlider.gif) + +## 3、低频率值变化 + +[运行 LfSlider.py](LfSlider.py) + +覆盖了`valueChanged`信号,通过使用定时器来延迟发送值变化,如果无法覆盖信号则可以自定义一个新的信号 + +![LfSlider](ScreenShot/LfSlider.gif) diff --git a/QSlider/ScreenShot/LfSlider.gif b/QSlider/ScreenShot/LfSlider.gif new file mode 100644 index 00000000..035f3118 Binary files /dev/null and b/QSlider/ScreenShot/LfSlider.gif differ diff --git a/QSplashScreen/Data/splash.gif b/QSplashScreen/Data/splash.gif new file mode 100644 index 00000000..8553388e Binary files /dev/null and b/QSplashScreen/Data/splash.gif differ diff --git a/QSplashScreen/GifSplashScreen.py b/QSplashScreen/GifSplashScreen.py new file mode 100644 index 00000000..d409dd04 --- /dev/null +++ b/QSplashScreen/GifSplashScreen.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/6/11 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: +@description: +""" + +from time import sleep + +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QMovie + from PyQt5.QtWidgets import QApplication, QSplashScreen, QWidget +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QMovie + from PySide2.QtWidgets import QApplication, QSplashScreen, QWidget + + +class GifSplashScreen(QSplashScreen): + + def __init__(self, *args, **kwargs): + super(GifSplashScreen, self).__init__(*args, **kwargs) + self.movie = QMovie('Data/splash.gif') + self.movie.frameChanged.connect(self.onFrameChanged) + self.movie.start() + + def onFrameChanged(self, _): + self.setPixmap(self.movie.currentPixmap()) + + def finish(self, widget): + self.movie.stop() + super(GifSplashScreen, self).finish(widget) + + +class BusyWindow(QWidget): + + def __init__(self, *args, **kwargs): + super(BusyWindow, self).__init__(*args, **kwargs) + # 模拟耗时操作,一般来说耗时的加载数据应该放到线程 + for i in range(5): + sleep(1) + splash.showMessage('加载进度: %d' % i, Qt.AlignHCenter | Qt.AlignBottom, Qt.white) + QApplication.instance().processEvents() + + splash.showMessage('初始化完成', Qt.AlignHCenter | Qt.AlignBottom, Qt.white) + splash.finish(self) + + +if __name__ == '__main__': + import sys + import cgitb + + cgitb.enable(format='text') + + app = QApplication(sys.argv) + + global splash + splash = GifSplashScreen() + splash.show() + + w = BusyWindow() + w.show() + + # 测试二 + # def createWindow(): + # app.w = QWidget() + # # 模拟初始5秒后再显示 + # splash.showMessage('等待界面显示', Qt.AlignHCenter | Qt.AlignBottom, Qt.white) + # QTimer.singleShot(3000, lambda: ( + # splash.showMessage('初始化完成', Qt.AlignHCenter | Qt.AlignBottom, Qt.white), app.w.show(), + # splash.finish(app.w))) + + # 模拟耗时5秒。但是不能用sleep + # 可以使用子线程加载耗时的数据 + # 主线程中循环设置UI可以配合QApplication.instance().processEvents() + # QTimer.singleShot(3000, createWindow) + + splash.showMessage('等待创建界面', Qt.AlignHCenter | Qt.AlignBottom, Qt.white) + + sys.exit(app.exec_()) diff --git a/QSplashScreen/README.en.md b/QSplashScreen/README.en.md new file mode 100644 index 00000000..e69de29b diff --git a/QSplashScreen/README.md b/QSplashScreen/README.md new file mode 100644 index 00000000..6c16f8bb --- /dev/null +++ b/QSplashScreen/README.md @@ -0,0 +1,12 @@ +# QSplashScreen + +- 目录 + - [启动画面动画](#1启动画面动画) + +## 1、启动画面动画 + +[运行 GifSplashScreen.py](GifSplashScreen.py) + +结合 `QMovie` 的 `frameChanged` 信号 不停地设置新的pixmap图片 + +![GifSplashScreen](ScreenShot/GifSplashScreen.gif) diff --git a/QSplashScreen/ScreenShot/GifSplashScreen.gif b/QSplashScreen/ScreenShot/GifSplashScreen.gif new file mode 100644 index 00000000..0c226350 Binary files /dev/null and b/QSplashScreen/ScreenShot/GifSplashScreen.gif differ diff --git a/QSplitter/README.md b/QSplitter/README.md index f52a7ca3..e7844300 100644 --- a/QSplitter/README.md +++ b/QSplitter/README.md @@ -4,6 +4,7 @@ - [分割窗口的分割条重绘](#1分割窗口的分割条重绘) ## 1、分割窗口的分割条重绘 + [运行 RewriteHandle.py](RewriteHandle.py) 1. 原理在于`QSplitter`在创建分割条的时候会调用`createHandle`函数 @@ -11,4 +12,4 @@ 1. 通过`QSplitterHandle`的`paintEvent`实现绘制其它形状, 1. 重写`mousePressEvent`和`mouseMoveEvent`来实现鼠标的其它事件 -![RewriteHandle](ScreenShot/RewriteHandle.gif) \ No newline at end of file +![RewriteHandle](ScreenShot/RewriteHandle.gif) diff --git a/QSplitter/RewriteHandle.py b/QSplitter/RewriteHandle.py index 241f6c28..d69ab630 100644 --- a/QSplitter/RewriteHandle.py +++ b/QSplitter/RewriteHandle.py @@ -4,25 +4,23 @@ """ Created on 2018年3月21日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: Splitter @description: """ import sys -from PyQt5.QtCore import Qt, QPointF, pyqtSignal -from PyQt5.QtGui import QPainter, QPolygonF -from PyQt5.QtWidgets import QTextEdit, QListWidget,\ - QTreeWidget, QSplitter, QApplication, QMainWindow, QSplitterHandle - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0"\ - +try: + from PyQt5.QtCore import Qt, QPointF, pyqtSignal + from PyQt5.QtGui import QPainter, QPolygonF + from PyQt5.QtWidgets import QTextEdit, QListWidget, \ + QTreeWidget, QSplitter, QApplication, QMainWindow, QSplitterHandle +except ImportError: + from PySide2.QtCore import Qt, QPointF, Signal as pyqtSignal + from PySide2.QtGui import QPainter, QPolygonF + from PySide2.QtWidgets import QTextEdit, QListWidget, \ + QTreeWidget, QSplitter, QApplication, QMainWindow, QSplitterHandle class SplitterHandle(QSplitterHandle): @@ -49,7 +47,7 @@ def mouseMoveEvent(self, event): else: # 设置默认的鼠标样式并可以移动 self.setCursor(Qt.SplitHCursor if self.orientation() - == Qt.Horizontal else Qt.SplitVCursor) + == Qt.Horizontal else Qt.SplitVCursor) super(SplitterHandle, self).mouseMoveEvent(event) def paintEvent(self, event): diff --git a/QStackedWidget/LeftTabStacked.py b/QStackedWidget/LeftTabStacked.py index cc5d761e..6c8257d0 100644 --- a/QStackedWidget/LeftTabStacked.py +++ b/QStackedWidget/LeftTabStacked.py @@ -1,24 +1,27 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from random import randint -from PyQt5.QtCore import Qt, QSize -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QWidget, QListWidget, QStackedWidget, QHBoxLayout,\ - QListWidgetItem, QLabel +""" +Created on 2018年5月29日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: LeftTabWidget +@description: +""" +from random import randint -# Created on 2018年5月29日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: LeftTabWidget -# description: -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import Qt, QSize + from PyQt5.QtGui import QIcon + from PyQt5.QtWidgets import QApplication, QWidget, QListWidget, QStackedWidget, QHBoxLayout, \ + QListWidgetItem, QLabel +except ImportError: + from PySide2.QtCore import Qt, QSize + from PySide2.QtGui import QIcon + from PySide2.QtWidgets import QApplication, QWidget, QListWidget, QStackedWidget, QHBoxLayout, \ + QListWidgetItem, QLabel class LeftTabWidget(QWidget): @@ -26,7 +29,7 @@ class LeftTabWidget(QWidget): def __init__(self, *args, **kwargs): super(LeftTabWidget, self).__init__(*args, **kwargs) self.resize(800, 600) - #左右布局(左边一个QListWidget + 右边QStackedWidget) + # 左右布局(左边一个QListWidget + 右边QStackedWidget) layout = QHBoxLayout(self, spacing=0) layout.setContentsMargins(0, 0, 0, 0) # 左侧列表 @@ -102,7 +105,7 @@ def initUi(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) app.setStyleSheet(Stylesheet) w = LeftTabWidget() diff --git a/QStackedWidget/README.md b/QStackedWidget/README.md index 2329fc3e..58996c68 100644 --- a/QStackedWidget/README.md +++ b/QStackedWidget/README.md @@ -4,6 +4,7 @@ - [左侧选项卡](#1左侧选项卡) ## 1、左侧选项卡 + [运行 LeftTabStacked.py](LeftTabStacked.py) 本来使用`QTabWidget`可以实现多标签页面,但是当标签在左侧时会出现文字方向不对的问题, @@ -14,7 +15,7 @@ 2. 右侧添加`QWidget`的时候有两种方案 1. 左侧list根据序号来索引,右侧添加widget时给定带序号的变量名,如widget_0,widget_1,widget_2之类的,这样可以直接根据`QListWidget`的序号关联起来 2. 左侧list添加item时给定右侧对应的widget变量值 - -PS: 用设计设的做法 : https://www.jianshu.com/p/dac62b5c225c + +PS: 用设计设的做法 : ![LeftTabStacked](ScreenShot/LeftTabStacked.gif) diff --git a/QSystemTrayIcon/MinimizeToTray.py b/QSystemTrayIcon/MinimizeToTray.py index 24382152..f4325838 100755 --- a/QSystemTrayIcon/MinimizeToTray.py +++ b/QSystemTrayIcon/MinimizeToTray.py @@ -1,11 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import sys -from PyQt5.QtWidgets import ( - QApplication, QMainWindow, - QLabel, QGridLayout, QWidget, - QCheckBox, QSystemTrayIcon, - QSpacerItem, QSizePolicy, QMenu, QAction, QStyle, qApp) -from PyQt5.QtCore import QSize +try: + from PyQt5.QtCore import QSize + from PyQt5.QtWidgets import ( + QApplication, QMainWindow, + QLabel, QGridLayout, QWidget, + QCheckBox, QSystemTrayIcon, + QSpacerItem, QSizePolicy, QMenu, QAction, QStyle) +except ImportError: + from PySide2.QtCore import QSize + from PySide2.QtWidgets import ( + QApplication, QMainWindow, + QLabel, QGridLayout, QWidget, + QCheckBox, QSystemTrayIcon, + QSpacerItem, QSizePolicy, QMenu, QAction, QStyle) class MainWindow(QMainWindow): @@ -21,14 +32,14 @@ def __init__(self): # Be sure to call the super class method QMainWindow.__init__(self) - self.setMinimumSize(QSize(480, 80)) # Set sizes + self.setMinimumSize(QSize(480, 80)) # Set sizes self.setWindowTitle("System Tray Application") # Set a title # Create a central widget central_widget = QWidget(self) # Set the central widget self.setCentralWidget(central_widget) - grid_layout = QGridLayout(self) # Create a QGridLayout + grid_layout = QGridLayout(self) # Create a QGridLayout # Set the layout into the central widget central_widget.setLayout(grid_layout) grid_layout.addWidget( @@ -56,7 +67,7 @@ def __init__(self): hide_action = QAction("Hide", self) show_action.triggered.connect(self.show) hide_action.triggered.connect(self.hide) - quit_action.triggered.connect(qApp.quit) + quit_action.triggered.connect(QApplication.instance().quit) tray_menu = QMenu() tray_menu.addAction(show_action) tray_menu.addAction(hide_action) @@ -82,4 +93,4 @@ def closeEvent(self, event): app = QApplication(sys.argv) mw = MainWindow() mw.show() - sys.exit(app.exec()) + sys.exit(app.exec_()) diff --git a/QSystemTrayIcon/README.md b/QSystemTrayIcon/README.md index 6e14cf6a..268c3036 100755 --- a/QSystemTrayIcon/README.md +++ b/QSystemTrayIcon/README.md @@ -2,6 +2,7 @@ - 目录 - [最小化到系统托盘](#1最小化到系统托盘) + - [系统托盘闪烁](#2系统托盘闪烁) ## 1、最小化到系统托盘 @@ -9,9 +10,12 @@ 选择 Minimize to Tray 在关闭窗口时最小化到系统托盘。 -> Reference: https://evileg.com/en/post/68/ +> Reference: ![MinimizeToTray](ScreenShot/MinimizeToTray.gif) +## 2、系统托盘闪烁 +[运行 TrayNotify.py](TrayNotify.py) +通过定时器设置不同图标来实现闪烁。 diff --git a/QSystemTrayIcon/TrayNotify.py b/QSystemTrayIcon/TrayNotify.py new file mode 100644 index 00000000..914a064d --- /dev/null +++ b/QSystemTrayIcon/TrayNotify.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2021年12月09日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TrayNotify +@description: 托盘闪烁 +""" + +try: + from PyQt5.QtCore import QTimer + from PyQt5.QtWidgets import (QApplication, QHBoxLayout, QPushButton, + QStyle, QSystemTrayIcon, QWidget) +except ImportError: + from PySide2.QtCore import QTimer + from PySide2.QtWidgets import (QApplication, QHBoxLayout, QPushButton, + QStyle, QSystemTrayIcon, QWidget) + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + layout = QHBoxLayout(self) + layout.addWidget(QPushButton('开始闪烁', self, clicked=self.start_flash)) + layout.addWidget(QPushButton('停止闪烁', self, clicked=self.stop_flash)) + # 创建托盘图标 + self.tray_icon = QSystemTrayIcon(self) + self.tray_icon.setIcon(self.style().standardIcon( + QStyle.SP_ComputerIcon)) + self.tray_icon.show() + # 图标闪烁定时器 + self.tray_visible = True + self.flash_timer = QTimer(self, timeout=self.flash_icon) + + def closeEvent(self, event): + self.stop_flash() + self.tray_icon.hide() + super(Window, self).closeEvent(event) + + def start_flash(self): + """开始闪烁""" + if not self.flash_timer.isActive(): + self.flash_timer.start(500) + + def stop_flash(self): + """停止闪烁后需要显示图标""" + if self.flash_timer.isActive(): + self.flash_timer.stop() + self.tray_icon.setIcon(self.style().standardIcon( + QStyle.SP_ComputerIcon)) + + def flash_icon(self): + """根据当前图标是否可见切换图标""" + if self.tray_visible: + self.tray_icon.setIcon(self.style().standardIcon( + QStyle.SP_TrashIcon)) + else: + self.tray_icon.setIcon(self.style().standardIcon( + QStyle.SP_ComputerIcon)) + self.tray_visible = not self.tray_visible + + +if __name__ == '__main__': + import sys + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QTabWidget/HideCloseButton.py b/QTabWidget/HideCloseButton.py new file mode 100644 index 00000000..cd41597d --- /dev/null +++ b/QTabWidget/HideCloseButton.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/12 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: HideCloseButton.py +@description: +""" + +from PyQt5.QtWidgets import QApplication, QLabel, QTabBar, QTabWidget + + +class HideCloseButton(QTabWidget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.resize(800, 600) + self.setTabsClosable(True) + self.tabCloseRequested.connect(self.onCloseTab) + + # add tabs + self.addTab(QLabel("Home", self), "Home") + for i in range(10): + title = "Tab {}".format(i) + self.addTab(QLabel(title, self), title) + + # hide first tab's close button + btn = self.tabBar().tabButton(0, QTabBar.RightSide) + btn.close() + self.tabBar().setTabButton(0, QTabBar.RightSide, None) + + def onCloseTab(self, index): + w = self.widget(index) + if w: + w.close() + self.removeTab(index) + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = HideCloseButton() + w.show() + sys.exit(app.exec_()) diff --git a/QTabWidget/README.md b/QTabWidget/README.md index e69de29b..4ca37677 100644 --- a/QTabWidget/README.md +++ b/QTabWidget/README.md @@ -0,0 +1,16 @@ +# QTabWidget + +- 目录 + - [隐藏指定Tab关闭按钮](#1隐藏指定Tab关闭按钮) + +## 1、隐藏指定Tab关闭按钮 + +[运行 HideCloseButton.py](HideCloseButton.py) + +```python +btn = self.tabBar().tabButton(0, QTabBar.RightSide) +btn.close() +self.tabBar().setTabButton(0, QTabBar.RightSide, None) +``` + +![HideCloseButton](ScreenShot/HideCloseButton.png) diff --git a/QTabWidget/ScreenShot/HideCloseButton.png b/QTabWidget/ScreenShot/HideCloseButton.png new file mode 100644 index 00000000..dddd2fcb Binary files /dev/null and b/QTabWidget/ScreenShot/HideCloseButton.png differ diff --git a/QTableView/CopyContent.py b/QTableView/CopyContent.py index b5c82943..abeabe43 100644 --- a/QTableView/CopyContent.py +++ b/QTableView/CopyContent.py @@ -1,22 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年4月6日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CopyContent @description: -''' -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QStandardItemModel, QStandardItem -from PyQt5.QtWidgets import QTableView, QApplication, QAction, QMessageBox +""" - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QStandardItemModel, QStandardItem + from PyQt5.QtWidgets import QTableView, QApplication, QAction, QMessageBox +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QStandardItemModel, QStandardItem + from PySide2.QtWidgets import QTableView, QApplication, QAction, QMessageBox class TableView(QTableView): @@ -98,8 +99,10 @@ def initData(self): self.myModel.setItem( row, col, QStandardItem("row: {row},col: {col}".format(row=row + 1, col=col + 1))) + if __name__ == "__main__": import sys + app = QApplication(sys.argv) app.setApplicationName("TableView") w = TableView() diff --git a/QTableView/README.md b/QTableView/README.md index 1d79c7a7..92dbf3c7 100644 --- a/QTableView/README.md +++ b/QTableView/README.md @@ -4,6 +4,7 @@ - [表格内容复制](#1表格内容复制) ## 1、表格内容复制 + [运行 CopyContent.py](CopyContent.py) 1. 通过构造一个和选中区域一样的空数组,然后对数组进行填充形成表格 @@ -11,4 +12,3 @@ 1. 把字符串复制到剪切板中 ![CopyContent1](ScreenShot/CopyContent1.png) ![CopyContent2](ScreenShot/CopyContent2.png) - diff --git a/QTableWidget/Lib/TableWidget.py b/QTableWidget/Lib/TableWidget.py deleted file mode 100644 index 75e61f43..00000000 --- a/QTableWidget/Lib/TableWidget.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -''' -@author: wxj -@license: (C) Hefei tongzhi electromechanical control technology co.LTD -@contact: -@software: garner -@file: table.py -@time: 2019/4/11 21:26 -@desc: -''' -from PyQt5 import QtWidgets -from PyQt5.QtWidgets import * -from PyQt5.QtGui import * -from PyQt5.QtCore import * -import sys - -class MyTable(QTableWidget): - def __init__(self,parent=None): - super(MyTable, self).__init__(parent) - self.setWindowTitle("我是一个表格") - self.setWindowIcon(QIcon("male.png")) - self.resize(920, 240) - self.setColumnCount(6) - self.setRowCount(2) - #设置表格有两行五列。 - self.setColumnWidth(0, 200) - self.setColumnWidth(4, 200) - self.setRowHeight(0, 100) - #设置第一行高度为100px,第一列宽度为200px。 - - self.table() - - def table(self): - self.setItem(0,0,QTableWidgetItem(" 你的名字")) - self.setItem(0,1,QTableWidgetItem("性别")) - self.setItem(0,2,QTableWidgetItem("出生日期")) - self.setItem(0,3, QTableWidgetItem("职业")) - self.setItem(0,4, QTableWidgetItem("收入")) - self.setItem(0, 5, QTableWidgetItem("进度条")) - #添加表格的文字内容. - self.setHorizontalHeaderLabels(["第一行", "第二行", "第三行", "第四行", "第五行","第六行"]) - self.setVerticalHeaderLabels(["第一列", "第二列"]) - #设置表头 - lbp = QLabel() - lbp.setPixmap(QPixmap("Male.png")) - self.setCellWidget(1,1,lbp) - #在表中添加一张图片 - twi = QTableWidgetItem(" 新海诚") - twi.setFont(QFont("Times", 10, )) - self.setItem(1,0,twi) - - #添加一个自己设置了大小和类型的文字。 - dte = QDateTimeEdit() - dte.setDateTime(QDateTime.currentDateTime()) - dte.setDisplayFormat("yyyy/MM/dd") - dte.setCalendarPopup(True) - self.setCellWidget(1,2,dte) - #添加一个弹出的日期选择,设置默认值为当前日期,显示格式为年月日。 - cbw = QComboBox() - cbw.addItem("医生") - cbw.addItem("老师") - cbw.addItem("律师") - self.setCellWidget(1,3,cbw) - #添加了一个下拉选择框 - sb = QSpinBox() - sb.setRange(1000,10000) - sb.setValue(5000)#设置最开始显示的数字 - sb.setDisplayIntegerBase(10)#这个是显示数字的进制,默认是十进制。 - sb.setSuffix("元")#设置后辍 - sb.setPrefix("RMB: ")#设置前辍 - sb.setSingleStep(100) - self.setCellWidget(1,4,sb) - # 添加一个进度条 - - self.progressBar = QtWidgets.QProgressBar(self) - self.progressBar.setProperty("value", 0) - self.progressBar.setObjectName("progressBar") - self.setCellWidget(1, 5, self.progressBar) - self.step = 0 - self.timer = QTimer() - self.timer.setInterval(1000) - self.timer.start() - # 信号连接到槽 - self.timer.timeout.connect(self.onTimerOut) - self.count=0 - def onTimerOut(self): # 重写timerEvent - self.count +=1 - if self.count >= 100: # value >= 100时,停止计时器 - self.timer.stop() - print("结束") - # self.progressBar.setValue(self.step) - else: - print(self.count) - self.progressBar.setValue(self.count) - # return - # self.step += 1 - - - -if __name__ == '__main__': - app = QApplication(sys.argv) - myTable = MyTable() - myTable.show() - app.exit(app.exec_()) diff --git a/QTableWidget/Lib/mainui.py b/QTableWidget/Lib/mainui.py index d5f7041a..c8f46651 100644 --- a/QTableWidget/Lib/mainui.py +++ b/QTableWidget/Lib/mainui.py @@ -6,7 +6,11 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore, QtGui, QtWidgets +try: + from PyQt5 import QtCore, QtWidgets +except ImportError: + from PySide2 import QtCore, QtWidgets + class Ui_Form(object): def setupUi(self, Form): @@ -102,10 +106,10 @@ def retranslateUi(self, Form): if __name__ == "__main__": import sys + app = QtWidgets.QApplication(sys.argv) Form = QtWidgets.QWidget() ui = Ui_Form() ui.setupUi(Form) Form.show() sys.exit(app.exec_()) - diff --git a/QTableWidget/README.md b/QTableWidget/README.md index 634f4451..74db3b16 100644 --- a/QTableWidget/README.md +++ b/QTableWidget/README.md @@ -5,13 +5,15 @@ - [表格嵌入日历,下拉框,进度条,按钮](#2表格嵌入) ## 1、Sqlalchemy动态拼接字段查询显示表格 -[运行 SqlQuery.py](SqlQuery.py) + +[运行 SqlQuery.py](SqlQuery.py) | [查看 mainui.ui](Data/mainui.ui) 通过判断界面中选择的条件对`Sqlalchemy`的`model`进行字段拼接从而实现按条件查询 ![SqlQuery](ScreenShot/SqlQuery.png) ## 2、TableWidget嵌入部件 + [运行 TableWidget.py](TableWidget.py) 点击开始按钮,进度条开始 ![嵌入小部件](ScreenShot/table.png) diff --git a/QTableWidget/SqlQuery.py b/QTableWidget/SqlQuery.py index 0e417f75..c40581e0 100644 --- a/QTableWidget/SqlQuery.py +++ b/QTableWidget/SqlQuery.py @@ -4,26 +4,27 @@ """ Created on 2018年5月15日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: SqlQuery @description: """ -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import QWidget, QMessageBox, QTableWidgetItem + +try: + from PyQt5.QtCore import pyqtSlot + from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox, QTableWidgetItem +except ImportError: + from PySide2.QtCore import Slot as pyqtSlot + from PySide2.QtWidgets import QApplication, QWidget, QMessageBox, QTableWidgetItem + from sqlalchemy.engine import create_engine from sqlalchemy.ext.declarative.api import declarative_base from sqlalchemy.orm.session import sessionmaker from sqlalchemy.sql.expression import and_ from sqlalchemy.sql.schema import Column from sqlalchemy.sql.sqltypes import Integer, Text -from Lib.mainui import Ui_Form -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +from Lib.mainui import Ui_Form # engine = create_engine('mysql+mysqldb://root@localhost:3306/tourist?charset=utf8') engine = create_engine('sqlite:///Data/data.sqlite3', echo=True) # echo 表示开启命令显示 @@ -31,7 +32,6 @@ class Tourist(Base): - __tablename__ = 'tourist' id = Column(Integer, primary_key=True) @@ -141,8 +141,9 @@ def applyPort(self): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.Hook(1, None, 5, sys.stderr, 'text') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QTableWidget/TableWidget.py b/QTableWidget/TableWidget.py index bae38af4..a052566b 100644 --- a/QTableWidget/TableWidget.py +++ b/QTableWidget/TableWidget.py @@ -1,75 +1,83 @@ #!/usr/bin/env python # encoding: utf-8 -''' +""" Created on 2017年4月21日 @author: weike32 -@site: https://pyqt5.com ,https://github.com/weike32 +@site: https://pyqt.site ,https://github.com/weike32 @email: 394967319@qq.com @file: CopyContent @description: 查阅了很多博客,如果有异,可以联系作者邮箱。本Demo仅作学习参考用,保有后续相关权益。 -''' -from PyQt5 import QtWidgets -from PyQt5.QtWidgets import * -from PyQt5.QtGui import * -from PyQt5.QtCore import * +""" import sys +try: + from PyQt5 import QtWidgets + from PyQt5.QtCore import * + from PyQt5.QtGui import * + from PyQt5.QtWidgets import * +except ImportError: + from PySide2 import QtWidgets + from PySide2.QtCore import * + from PySide2.QtGui import * + from PySide2.QtWidgets import * + + class MyTable(QTableWidget): - def __init__(self,parent=None): + def __init__(self, parent=None): super(MyTable, self).__init__(parent) self.setWindowTitle("我是一个表格") self.setWindowIcon(QIcon("male.png")) self.resize(920, 240) self.setColumnCount(6) self.setRowCount(2) - #设置表格有两行五列。 + # 设置表格有两行五列。 self.setColumnWidth(0, 200) self.setColumnWidth(4, 200) self.setRowHeight(0, 100) - #设置第一行高度为100px,第一列宽度为200px。 + # 设置第一行高度为100px,第一列宽度为200px。 self.table() def table(self): - self.setItem(0,0,QTableWidgetItem("你的名字")) - self.setItem(0,1,QTableWidgetItem("性别")) - self.setItem(0,2,QTableWidgetItem("出生日期")) - self.setItem(0,3, QTableWidgetItem("职业")) - self.setItem(0,4, QTableWidgetItem("收入")) + self.setItem(0, 0, QTableWidgetItem("你的名字")) + self.setItem(0, 1, QTableWidgetItem("性别")) + self.setItem(0, 2, QTableWidgetItem("出生日期")) + self.setItem(0, 3, QTableWidgetItem("职业")) + self.setItem(0, 4, QTableWidgetItem("收入")) self.setItem(0, 5, QTableWidgetItem("进度条")) - #添加表格的文字内容. - self.setHorizontalHeaderLabels(["第一行", "第二行", "第三行", "第四行", "第五行","第六行"]) + # 添加表格的文字内容. + self.setHorizontalHeaderLabels(["第一行", "第二行", "第三行", "第四行", "第五行", "第六行"]) self.setVerticalHeaderLabels(["第一列", "第二列"]) - #设置表头 + # 设置表头 lbp = QLabel() lbp.setPixmap(QPixmap("youPicture.png")) - self.setCellWidget(1,1,lbp) - #在表中添加一张图片 + self.setCellWidget(1, 1, lbp) + # 在表中添加一张图片 twi = QTableWidgetItem("Graph") twi.setFont(QFont("Times", 10, )) - self.setItem(1,0,twi) + self.setItem(1, 0, twi) - #添加一个自己设置了大小和类型的文字。 + # 添加一个自己设置了大小和类型的文字。 dte = QDateTimeEdit() dte.setDateTime(QDateTime.currentDateTime()) dte.setDisplayFormat("yyyy/MM/dd") dte.setCalendarPopup(True) - self.setCellWidget(1,2,dte) - #添加一个弹出的日期选择,设置默认值为当前日期,显示格式为年月日。 + self.setCellWidget(1, 2, dte) + # 添加一个弹出的日期选择,设置默认值为当前日期,显示格式为年月日。 cbw = QComboBox() cbw.addItem("医生") cbw.addItem("老师") cbw.addItem("律师") - self.setCellWidget(1,3,cbw) - #添加了一个下拉选择框 + self.setCellWidget(1, 3, cbw) + # 添加了一个下拉选择框 sb = QSpinBox() - sb.setRange(1000,10000) - sb.setValue(5000)#设置最开始显示的数字 - sb.setDisplayIntegerBase(10)#这个是显示数字的进制,默认是十进制。 - sb.setSuffix("元")#设置后辍 - sb.setPrefix("RMB: ")#设置前辍 + sb.setRange(1000, 10000) + sb.setValue(5000) # 设置最开始显示的数字 + sb.setDisplayIntegerBase(10) # 这个是显示数字的进制,默认是十进制。 + sb.setSuffix("元") # 设置后辍 + sb.setPrefix("RMB: ") # 设置前辍 sb.setSingleStep(100) - self.setCellWidget(1,4,sb) + self.setCellWidget(1, 4, sb) # 添加一个进度条 self.progressBar = QtWidgets.QProgressBar(self) @@ -82,9 +90,10 @@ def table(self): self.timer.start() # 信号连接到槽 self.timer.timeout.connect(self.onTimerOut) - self.count=0 + self.count = 0 + def onTimerOut(self): # 重写timerEvent - self.count +=1 + self.count += 1 if self.count >= 100: # value >= 100时,停止计时器 self.timer.stop() print("结束") @@ -96,7 +105,6 @@ def onTimerOut(self): # 重写timerEvent # self.step += 1 - if __name__ == '__main__': app = QApplication(sys.argv) myTable = MyTable() diff --git a/QTextBrowser/DynamicRes.py b/QTextBrowser/DynamicRes.py new file mode 100644 index 00000000..2c229fe5 --- /dev/null +++ b/QTextBrowser/DynamicRes.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/6/3 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: DynamicRes +@description: +""" +from threading import Thread + +import requests + +try: + from PyQt5.QtCore import QUrl, QByteArray + from PyQt5.QtGui import QImage, QTextDocument + from PyQt5.QtWidgets import QApplication, QTextBrowser, QWidget, QVBoxLayout, QPushButton +except ImportError: + from PySide2.QtCore import QUrl, QByteArray + from PySide2.QtGui import QImage, QTextDocument + from PySide2.QtWidgets import QApplication, QTextBrowser, QWidget, QVBoxLayout, QPushButton + + +class TextBrowser(QTextBrowser): + NetImages = {} + + def __init__(self, *args, **kwargs): + super(TextBrowser, self).__init__(*args, **kwargs) + self.setOpenLinks(False) # 禁止打开URL + + def downloadImage(self, url): + try: + self.NetImages[url] = [QByteArray(requests.get(url.toString()).content), 1] + print('下载完成', url) + except Exception as e: + print('下载失败', url, e) + self.NetImages[url] = [QByteArray(), 1] + + def loadResource(self, rtype, url): + ret = super(TextBrowser, self).loadResource(rtype, url) + # 加载图片资源 + if rtype == QTextDocument.ImageResource: + if ret: + return ret + if url.toString().startswith('irony'): # 自定义的协议头 + print('加载本地', '../Donate/zhifubao.png', url) + return QImage( + '../Donate/zhifubao.png') # 或者 QByteArray(open('../Donate/zhifubao.png', 'rb').read()) + elif url.toString().startswith('http'): # 加载网络图片 + img, status = self.NetImages.get(url, [None, None]) + if url not in self.NetImages or status is None: + # 子线程下载 + self.NetImages[url] = [None, 1] + print('download ', url) + Thread(target=self.downloadImage, args=(url,), daemon=True).start() + elif img: + return img + return ret + + def mouseDoubleClickEvent(self, event): + # 双击图片得到图片的URL,也可以用来放大显示 + super(TextBrowser, self).mouseDoubleClickEvent(event) + url = self.anchorAt(event.pos()) + if url: + print('url:', url, self.document().resource(QTextDocument.ImageResource, QUrl(url))) + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + layout = QVBoxLayout(self) + + self.textBrowser = TextBrowser(self) + self.downButton = QPushButton('加载网络图片', self) + + layout.addWidget(self.textBrowser) + layout.addWidget(self.downButton) + + # 加载本地图片 + img = QImage('../Donate/weixin.png') + # 第二个参数为任意唯一的url类似于qrc方式 + self.textBrowser.document().addResource(QTextDocument.ImageResource, + QUrl('dynamic:/images/weixin.png'), img) + + # 设置html + # 需要注意里面的图片地址 + self.textBrowser.setHtml( + '

' # 方式一直接加载本地图片 + '

' # 方式二通过addResource添加资源 + '

' # 方式三定义自定义的协议头通过loadResource动态加载 + '

') + + +if __name__ == '__main__': + import sys + import cgitb + + cgitb.enable(format='text') + + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QTextBrowser/README.md b/QTextBrowser/README.md index e69de29b..dfe873bf 100644 --- a/QTextBrowser/README.md +++ b/QTextBrowser/README.md @@ -0,0 +1,15 @@ +# QTextBrowser + +- 目录 + - [动态加载图片](#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) 函数 + +1、通过 `self.textBrowser.document().addResource(QTextDocument.ImageResource, QUrl('dynamic:/images/weixin.png'), img)` 向文档中注册新的资源索引,类似QRC +2、通过重载 `loadResource` 函数可以监听到所有的资源加载,然后动态返回内容 + +![DynamicRes](ScreenShot/DynamicRes.gif) diff --git a/QTextBrowser/ScreenShot/DynamicRes.gif b/QTextBrowser/ScreenShot/DynamicRes.gif new file mode 100644 index 00000000..9c923b58 Binary files /dev/null and b/QTextBrowser/ScreenShot/DynamicRes.gif differ diff --git a/QTextEdit/HighlightText.py b/QTextEdit/HighlightText.py index d1c8c485..ca7398fd 100644 --- a/QTextEdit/HighlightText.py +++ b/QTextEdit/HighlightText.py @@ -1,7 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2019年5月22日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: +@description: +""" + import sys -from PyQt5.QtGui import QTextCharFormat, QTextDocument, QTextCursor -from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, - QToolBar, QLineEdit, QPushButton, QColorDialog, QHBoxLayout, QWidget) + +try: + from PyQt5.QtCore import QRegExp + from PyQt5.QtGui import QTextCharFormat, QTextCursor + from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, + QToolBar, QLineEdit, QPushButton, QColorDialog, QHBoxLayout, QWidget) +except ImportError: + from PySide2.QtCore import QRegExp + from PySide2.QtGui import QTextCharFormat, QTextCursor + from PySide2.QtWidgets import (QApplication, QMainWindow, QTextEdit, + QToolBar, QLineEdit, QPushButton, QColorDialog, QHBoxLayout, QWidget) class TextEdit(QMainWindow): @@ -22,30 +42,46 @@ def __init__(self, parent=None): tb = QToolBar(self) tb.addWidget(widget) + self.addToolBar(tb) def setText(self, text): self.textEdit.setPlainText(text) - def mergeFormatOnWordOrSelection(self, format): - cursor = self.textEdit.textCursor() - if not cursor.hasSelection(): - cursor.select(QTextCursor.WordUnderCursor) - cursor.mergeCharFormat(format) - self.textEdit.mergeCurrentCharFormat(format) - def highlight(self): text = self.findText.text() # 输入框中的文字 if not text: return + col = QColorDialog.getColor(self.textEdit.textColor(), self) if not col.isValid(): return + + # 恢复默认的颜色 + cursor = self.textEdit.textCursor() + cursor.select(QTextCursor.Document) + cursor.setCharFormat(QTextCharFormat()) + cursor.clearSelection() + self.textEdit.setTextCursor(cursor) + + # 文字颜色 fmt = QTextCharFormat() fmt.setForeground(col) - # 先把光标移动到开头 + + # 正则 + expression = QRegExp(text) self.textEdit.moveCursor(QTextCursor.Start) - while self.textEdit.find(text, QTextDocument.FindWholeWords): # 查找所有文字 - self.mergeFormatOnWordOrSelection(fmt) + cursor = self.textEdit.textCursor() + + # 循环查找设置颜色 + pos = 0 + index = expression.indexIn(self.textEdit.toPlainText(), pos) + while index >= 0: + cursor.setPosition(index) + cursor.movePosition(QTextCursor.Right, + QTextCursor.KeepAnchor, len(text)) + cursor.mergeCharFormat(fmt) + pos = index + expression.matchedLength() + index = expression.indexIn(self.textEdit.toPlainText(), pos) if __name__ == '__main__': diff --git a/QTextEdit/README.md b/QTextEdit/README.md index 98bec9d8..f6a4a672 100644 --- a/QTextEdit/README.md +++ b/QTextEdit/README.md @@ -4,8 +4,9 @@ - [文本查找高亮](#1文本查找高亮) ## 1、文本查找高亮 + [运行 HighlightText.py](HighlightText.py) 主要用到`mergeCurrentCharFormat`函数 -![HighlightText](ScreenShot/HighlightText.gif) \ No newline at end of file +![HighlightText](ScreenShot/HighlightText.gif) diff --git a/QThread/InheritQThread.py b/QThread/InheritQThread.py index ea108018..051f5f6b 100644 --- a/QThread/InheritQThread.py +++ b/QThread/InheritQThread.py @@ -4,27 +4,28 @@ """ Created on 2018年3月9日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: InheritQThread @description: 继承QThread """ -from PyQt5.QtCore import QThread, pyqtSignal -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QProgressBar, QPushButton - -__Author__ = 'By: Irony\nQQ: 892768447\nEmail: 892768447@qq.com' -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QThread, pyqtSignal + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QProgressBar, QPushButton +except ImportError: + from PySide2.QtCore import QThread, Signal as pyqtSignal + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QProgressBar, QPushButton class Worker(QThread): - valueChanged = pyqtSignal(int) # 值变化信号 def run(self): - print('thread id', int(QThread.currentThreadId())) + print('thread id', QThread.currentThread()) for i in range(1, 101): + if self.isInterruptionRequested(): + break print('value', i) self.valueChanged.emit(i) QThread.sleep(1) @@ -41,7 +42,7 @@ def __init__(self, *args, **kwargs): layout.addWidget(QPushButton('开启线程', self, clicked=self.onStart)) # 当前线程id - print('main id', int(QThread.currentThreadId())) + print('main id', QThread.currentThread()) # 子线程 self._thread = Worker(self) @@ -49,21 +50,26 @@ def __init__(self, *args, **kwargs): self._thread.valueChanged.connect(self.progressBar.setValue) def onStart(self): - print('main id', int(QThread.currentThreadId())) - self._thread.start() # 启动线程 + if not self._thread.isRunning(): + print('main id', QThread.currentThread()) + self._thread.start() # 启动线程 def closeEvent(self, event): if self._thread.isRunning(): + self._thread.requestInterruption() self._thread.quit() + self._thread.wait() # 强制 # self._thread.terminate() - del self._thread + self._thread.deleteLater() super(Window, self).closeEvent(event) if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + import cgitb + + cgitb.enable(format='text') app = QApplication(sys.argv) w = Window() w.show() diff --git a/QThread/QuitThread.py b/QThread/QuitThread.py new file mode 100644 index 00000000..35b7a8e9 --- /dev/null +++ b/QThread/QuitThread.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/11/27 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: QuitThread +@description: +""" + +import sys +from time import time + +try: + from PyQt5.QtCore import QThread, QCoreApplication, QTimer +except ImportError: + from PySide2.QtCore import QThread, QCoreApplication, QTimer + + +class Thread(QThread): + + def run(self): + print('thread id', QThread.currentThread()) + i = 0 + while i < 101 and not self.isInterruptionRequested(): + print('value', i, time()) + i += 1 + QThread.msleep(500) + print('thread quit') + + +if __name__ == '__main__': + app = QCoreApplication(sys.argv) + t = Thread() + t.finished.connect(app.quit) + t.start() + # 3秒后退出 + print('will quit 3s latter') + QTimer.singleShot(3000, t.requestInterruption) + sys.exit(app.exec_()) diff --git a/QThread/README.md b/QThread/README.md index ef1ee88c..e1068ded 100644 --- a/QThread/README.md +++ b/QThread/README.md @@ -2,21 +2,25 @@ - 目录 - [继承QThread](#1继承QThread) - - [moveToThread](#2moveToThread) + - [moveToThread](#2movetothread) - [线程挂起恢复](#3线程挂起恢复) - [线程休眠唤醒](#4线程休眠唤醒) + - [线程退出](#5线程退出) ## 1、继承QThread + [运行 InheritQThread.py](InheritQThread.py) ![InheritQThread](ScreenShot/InheritQThread.png) ## 2、moveToThread + [运行 moveToThread.py](moveToThread.py) ![moveToThread](ScreenShot/InheritQThread.png) ## 3、线程挂起恢复 + [运行 SuspendThread.py](SuspendThread.py) 注意,这里只是简单演示,在应用这些代码时要小心 @@ -30,8 +34,17 @@ ![SuspendThread](ScreenShot/SuspendThread.gif) ## 4、线程休眠唤醒 + [运行 WakeupThread.py](WakeupThread.py) 使用 `QWaitCondition` 的 `wait` 和 `wakeAll` 方法 -![WakeupThread](ScreenShot/WakeupThread.gif) \ No newline at end of file +![WakeupThread](ScreenShot/WakeupThread.gif) + +## 5、线程退出 + +[运行 QuitThread.py](QuitThread.py) + +`isInterruptionRequested` 和 `requestInterruption` 函数作为退出标识调用 + +![QuitThread](ScreenShot/QuitThread.jpg) diff --git a/QThread/ScreenShot/QuitThread.jpg b/QThread/ScreenShot/QuitThread.jpg new file mode 100644 index 00000000..2c96dee2 Binary files /dev/null and b/QThread/ScreenShot/QuitThread.jpg differ diff --git a/QThread/SuspendThread.py b/QThread/SuspendThread.py index 707324e6..32c36018 100644 --- a/QThread/SuspendThread.py +++ b/QThread/SuspendThread.py @@ -1,26 +1,24 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- + +""" +Created on 2018年3月13日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: +@description: +""" + import ctypes +import win32con from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtWidgets import QWidget, QVBoxLayout, QProgressBar, QPushButton -import win32con from win32process import SuspendThread, ResumeThread -# Created on 2018年3月13日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: 多线程使用.a -# description: -__Author__ = 'By: Irony\nQQ: 892768447\nEmail: 892768447@qq.com' -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - - class Worker(QThread): - valueChanged = pyqtSignal(int) # 值变化信号 handle = -1 @@ -109,8 +107,10 @@ def closeEvent(self, event): if __name__ == '__main__': import sys import os + print('pid', os.getpid()) from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QThread/WakeupThread.py b/QThread/WakeupThread.py index 2f11a806..e9350371 100644 --- a/QThread/WakeupThread.py +++ b/QThread/WakeupThread.py @@ -4,7 +4,7 @@ """ Created on 2018年11月11日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: @description: @@ -13,15 +13,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QProgressBar -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - - class Thread(QThread): - valueChange = pyqtSignal(int) def __init__(self, *args, **kwargs): @@ -75,8 +67,10 @@ def doWake(self): if __name__ == '__main__': import sys import cgitb - sys.excepthook = cgitb.enable(1, None, 5, '') + + cgitb.enable(format='text') from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QThread/moveToThread.py b/QThread/moveToThread.py index ea1602de..77214df1 100644 --- a/QThread/moveToThread.py +++ b/QThread/moveToThread.py @@ -4,27 +4,28 @@ """ Created on 2018年3月9日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: moveToThread @description: moveToThread """ -from PyQt5.QtCore import QObject, pyqtSignal, QThread -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QProgressBar, QPushButton - -__Author__ = 'By: Irony\nQQ: 892768447\nEmail: 892768447@qq.com' -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +try: + from PyQt5.QtCore import QObject, pyqtSignal, QThread + from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QProgressBar, QPushButton +except ImportError: + from PySide2.QtCore import QObject, Signal as pyqtSignal, QThread + from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QProgressBar, QPushButton class Worker(QObject): - valueChanged = pyqtSignal(int) # 值变化信号 def run(self): - print('thread id', int(QThread.currentThreadId())) + print('thread id', ) for i in range(1, 101): + if QThread.currentThread().isInterruptionRequested(): + break print('value', i) self.valueChanged.emit(i) QThread.sleep(1) @@ -41,33 +42,38 @@ def __init__(self, *args, **kwargs): layout.addWidget(QPushButton('开启线程', self, clicked=self.onStart)) # 当前线程id - print('main id', int(QThread.currentThreadId())) + print('main id', QThread.currentThread()) # 启动线程更新进度条值 self._thread = QThread(self) self._worker = Worker() self._worker.moveToThread(self._thread) # 移动到线程中执行 self._thread.finished.connect(self._worker.deleteLater) + self._thread.started.connect(self._worker.run) self._worker.valueChanged.connect(self.progressBar.setValue) def onStart(self): - print('main id', int(QThread.currentThreadId())) - self._thread.started.connect(self._worker.run) - self._thread.start() # 启动线程 + if not self._thread.isRunning(): + print('main id', QThread.currentThread()) + self._thread.start() # 启动线程 def closeEvent(self, event): if self._thread.isRunning(): + self._thread.requestInterruption() self._thread.quit() + self._thread.wait() # 强制 # self._thread.terminate() - del self._thread - del self._worker + self._thread.deleteLater() super(Window, self).closeEvent(event) if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + import cgitb + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QTreeView/Data/serializewidget.ui b/QTreeView/Data/serializewidget.ui new file mode 100644 index 00000000..9204a9e3 --- /dev/null +++ b/QTreeView/Data/serializewidget.ui @@ -0,0 +1,271 @@ + + + SerializeWidget + + + + 0 + 0 + 800 + 600 + + + + TestSerialize + + + + + + Qt::Horizontal + + + false + + + + + 800 + 16777215 + + + + Json View + + + + + + + + + + + + + Widget View + + + + + + 0 + + + + Input + + + + + + + + + CheckBox + + + + + + + RadioButton + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Edit + + + + + + + + + + + + + Correlation + + + + + + Qt::Horizontal + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 0 + + + Qt::Vertical + + + + + + + Qt::Vertical + + + + + + + + View + + + + + + ListView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + TreeView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + TableView + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + + + + + + + + + + + + + diff --git a/QTreeView/Lib/qjsonmodel.py b/QTreeView/Lib/qjsonmodel.py new file mode 100644 index 00000000..336ddbd6 --- /dev/null +++ b/QTreeView/Lib/qjsonmodel.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/05 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: qjsonmodel.py +@description: +""" + +import json +from functools import reduce +from typing import Any, List, Union + +try: + from PyQt5.QtCore import QObject, Qt + from PyQt5.QtGui import QStandardItem, QStandardItemModel +except ImportError: + from PySide2.QtCore import QObject, Qt + from PySide2.QtGui import QStandardItem, QStandardItemModel + + +class QJsonItem(QStandardItem): + Sep = "/" + EditRole = Qt.EditRole + KeyRole = Qt.UserRole + 1 + ValueRole = Qt.UserRole + 2 + DatasRole = Qt.UserRole + 3 + PathRole = Qt.UserRole + 4 + + def __init__( + self, + *args, + key: Union["QJsonItem", None] = None, # 上一级key item + value: Union["QJsonItem", None] = None, # 当前value item + editAble: bool = True, + role: int = Qt.UserRole + 1, + ): + super().__init__(*args) + self.setInfo(key, value, None, editAble, role) + + def clear(self): + for _ in range(self.rowCount()): + self.removeRow(0) + + def setInfo( + self, + key: Union["QJsonItem", None] = None, + value: Union["QJsonItem", None] = None, + datas: Any = None, + editAble: bool = True, + role: int = Qt.UserRole + 1, + edit=None, + ): + self.setEditable(editAble) + self._role = role + self.keyItem = key + self.valueItem = value + self.edit = edit + self.datas = datas + + @property + def keyItem(self) -> Union["QJsonItem", None]: + # 上一级key item + item = self.data(self.KeyRole) + if not isinstance(item, QJsonItem): + return None + return item + + @keyItem.setter + def keyItem(self, value: "QJsonItem") -> None: + self.setData(value, self.KeyRole) + + @property + def valueItem(self) -> "QJsonItem": + # 当前value item + return self.data(self.ValueRole) + + @valueItem.setter + def valueItem(self, value: "QJsonItem") -> None: + self.setData(value, self.ValueRole) + + @property + def edit(self) -> Any: + # 设置角色为EditRole的值 + return self.data(self.EditRole) + + @edit.setter + def edit(self, value: Any) -> None: + if value is None: + return + self.setData(value, self.EditRole) + + @property + def datas(self) -> Any: + return self.data(self.DatasRole) + + @datas.setter + def datas(self, value: Any) -> None: + # key item中的原始数据设置 + self.clear() + self.setData(value, self.DatasRole) + if self._role == QJsonItem.ValueRole: + if not isinstance(value, (list, tuple, dict)): + self.edit = value + return + self.__loadData(value) + + @property + def type(self) -> Any: + return type(self.edit) + + @property + def role(self) -> int: + return self._role + + @property + def path(self) -> str: + item = self.keyItem if self._role == self.ValueRole else self + paths = [] + + while item: + paths.append(item.text()) + item = item.keyItem + + paths.reverse() + return QJsonItem.Sep.join(paths) + + def data(self, role: int = Qt.DisplayRole) -> Any: + if role == self.PathRole: + return self.path + + return super().data(role) + + def setData(self, data: Any, role: int = Qt.EditRole): + super().setData(data, role) + + def __loadData(self, data: Any) -> None: + # 数组类型的key是索引, 不允许修改 + isArray = isinstance(data, (list, tuple)) + keyEditAble = not isArray + datas = ( + data.items() + if isinstance(data, dict) + else enumerate(data) + if isArray + else [] + ) + + for key, value in datas: + itemKey = QJsonItem(str(key)) + itemValue = QJsonItem() + + # 先添加items + self.appendRow([itemKey, itemValue]) + + itemKey.setInfo(self, itemValue, value, keyEditAble, edit=key) + # key对应的后面的空列不允许修改 + valueEditAble = not isinstance(value, (list, tuple, dict)) + itemValue.setInfo(itemKey, None, value, valueEditAble, QJsonItem.ValueRole) + + def updateValue(self, value: Any) -> bool: + itemValue: Union[QJsonItem, None] = self.valueItem + if itemValue is None: + return False + if itemValue.datas == value: + return True + + self.datas = value + # key对应的后面的空列不允许修改 + valueEditAble = not isinstance(value, (list, tuple, dict)) + itemValue.setInfo(self, None, value, valueEditAble, QJsonItem.ValueRole) + + return True + + def toObject(self) -> Any: + datas = self.datas + typ = type(datas) + + if typ is dict: + return { + self.child(i, 0).text(): self.child(i, 1).toObject() + for i in range(self.rowCount()) + } + elif typ is list: + return [self.child(i, 0).toObject() for i in range(self.rowCount())] + elif self.valueItem: + return self.valueItem.toObject() + + return self.edit + + +class QJsonModel(QStandardItemModel): + def __init__( + self, + parent: QObject = None, + data: dict = None, + ) -> None: + super().__init__(parent) + self.setHorizontalHeaderLabels(["Key", "Value"]) + self.loadData(data) + + def loadFile( + self, file: str, encoding: str = "utf-8", errors: str = "ignore" + ) -> bool: + with open(file, "rb") as f: + return self.loadJson(f.read().decode(encoding=encoding, errors=errors)) + + def loadJson(self, string: str) -> bool: + return self.loadData(json.loads(string)) + + def loadData(self, data: dict, force: bool = False) -> bool: + if isinstance(data, dict): + if force: + self.clear() + self.__loadData(data) + return True + + return False + + def horizontalHeaderLabels(self) -> List[str]: + return [self.horizontalHeaderItem(i).text() for i in range(self.columnCount())] + + def clear(self): + headers = self.horizontalHeaderLabels() + super().clear() + self.setHorizontalHeaderLabels(headers) + + def findPath( + self, + path: str, + flags=Qt.MatchFixedString + | Qt.MatchCaseSensitive + | Qt.MatchWrap + | Qt.MatchRecursive, + ) -> Union[QJsonItem, None]: + indexes = self.match(self.index(0, 0), QJsonItem.PathRole, path, -1, flags) + indexes = [index for index in indexes if index.isValid()] + return self.itemFromIndex(indexes[0]) if indexes else None # type: ignore + + def updateValue( + self, path: str, value: Any, item: Union[QJsonItem, None] = None + ) -> bool: + item = item or self.findPath(path) + if item is None: + keys = path.split(QJsonItem.Sep) + self.__loadData(reduce(lambda val, key: {key: val}, reversed(keys), value)) + return True + + return item.updateValue(value) + + def __findItem( + self, key: str, parent=None + ) -> Union[QStandardItem, "QJsonItem", None]: + parent = parent or self.invisibleRootItem() + for row in range(parent.rowCount()): + item = parent.child(row) + if item.text() == key: + return item + + return None + + def __createItem(self, key: str, value: Any, parent=None): + parent = parent or self.invisibleRootItem() + + itemKey = QJsonItem(str(key)) + itemValue = QJsonItem() + parent.appendRow([itemKey, itemValue]) + + itemKey.setInfo(parent, itemValue, value, edit=key) + # key对应的后面的空列不允许修改 + valueEditAble = not isinstance(value, (list, tuple, dict)) + itemValue.setInfo(itemKey, None, value, valueEditAble, QJsonItem.ValueRole) + + def __loadData(self, data: Any, parent=None): + if not isinstance(data, dict): # 更新值 + itemKey = parent + if itemKey: + itemKey.updateValue(data) + return + + parent = parent or self.invisibleRootItem() + + for key, value in data.items(): + key = str(key) + itemKey = self.__findItem(key, parent) + if not itemKey: + self.__createItem(key, value, parent) + else: + self.__loadData(value, itemKey) + + def toDict(self) -> dict: + item = self.invisibleRootItem() + + return { + item.child(i).text(): item.child(i).toObject() + for i in range(item.rowCount()) + } + + def toJson(self, ensure_ascii=False, indent=None, **kwargs) -> str: + return json.dumps( + self.toDict(), ensure_ascii=ensure_ascii, indent=indent, **kwargs + ) diff --git a/QTreeView/Lib/qmodelmapper.py b/QTreeView/Lib/qmodelmapper.py new file mode 100644 index 00000000..9d1dc854 --- /dev/null +++ b/QTreeView/Lib/qmodelmapper.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/08 +@file: qmodelmapper.py +@description: +""" + +from copy import deepcopy +from typing import Any, List, Union + +from Lib.qjsonmodel import QJsonModel + +try: + from PyQt5.QtCore import ( + QDateTime, + QMetaProperty, + QModelIndex, + QObject, + Qt, + QTimer, + ) + from PyQt5.QtCore import pyqtSignal as Signal + from PyQt5.QtWidgets import QWidget +except ImportError: + from PySide2.QtCore import ( + QDateTime, + QMetaProperty, + QModelIndex, + QObject, + Qt, + QTimer, + Signal, + ) + from PySide2.QtWidgets import QWidget + + +class QModelMapper(QObject): + Debug = False + valueChanged = Signal() + Signal = ( + "dateTimeChanged", + "currentTextChanged", + "valueChanged", + "toggled", + "textChanged", + ) + Props = ( + "dateTime", + "html", + "plainText", + "currentText", + "checked", + "value", + "text", + ) + + def __init__(self, *args, **kwargs): + self._delay = kwargs.pop("delay", 50) + data = kwargs.pop("data", {}) + super().__init__(*args, **kwargs) + self._old = deepcopy(data) + self._widgetkey = {} + self._keywidget = {} + self._timer = QTimer(self) + self._timer.setSingleShot(True) + self._timer.timeout.connect(self.valueChanged.emit) + self._model = QJsonModel(self.parent(), data=data) + self._model.dataChanged.connect(self.onItemDataChanged) + + def bind(self, widget: QWidget, key: str, default: Any = None, prop: str = ""): + if widget not in self._widgetkey: + self._widgetkey[widget] = { + "key": key, + "prop": self.getProperty(widget, prop), + } + self._setValue(widget, key, default) + + # record all widgets for key + if key not in self._keywidget: + self._keywidget[key] = set() + self._keywidget[key].add(widget) + + for signal in self.Signal: + signal = getattr(widget, signal, None) + if signal: + signal.connect(self._setData) + break + + def isModify(self) -> bool: + return self._model.toDict() != self._old + + def getModel(self) -> QJsonModel: + return self._model + + def getData(self) -> dict: + return self._model.toDict() + + def getJson(self, ensure_ascii=False, indent=None, **kwargs) -> str: + return self._model.toJson(ensure_ascii=ensure_ascii, indent=indent, **kwargs) + + def setData(self, data: dict, force: bool = False): + if force: + self._old = deepcopy(data) + else: + self._old.update(data) + + if force: + self._model.blockSignals(True) + self._model.loadData(data, force) + if force: + self._model.blockSignals(False) + + def getProperty( + self, widget: QWidget, prop: str = "" + ) -> Union[QMetaProperty, None]: + qmo = widget.metaObject() + props = [prop] if prop else self.Props + widgetProps = ( + qmo.property(i) + for i in range( + QWidget.staticMetaObject.propertyCount(), qmo.propertyCount() + ) + ) + widgetProps = [ + p for p in widgetProps if p and p.isReadable() and p.isWritable() + ] + + rets = [p for prop in props for p in widgetProps if p.name() == prop] + return rets[0] if rets else None + + def _getDefault(self, widget: QWidget, default: Any = None): + if default: + return default + + prop: Union[QMetaProperty, None] = self._widgetkey[widget]["prop"] + if prop is None: + return None + + value = prop.read(widget) + if value is not None: + if prop.name() == "dateTime": + return value.toString("yyyy-MM-dd HH:mm:ss") + elif prop.name() == "html" and len(widget.property("plainText")) == 0: + return "" + return value + + return None + + def _setValue( + self, + widget: QWidget, + key: str, + default: Any = None, + updated: bool = False, + block=False, + ): + created = False + value: Any = default + + if not updated: + item = self._model.findPath(key) + if item is None: + value = self._getDefault(widget, default) + created = self._model.updateValue(key, value, item) + else: + # value from model + if item.valueItem: + value = item.valueItem.edit + created = default != value + block = True + if value is None: + return + + if block: + widget.blockSignals(True) + + prop: Union[QMetaProperty, None] = self._widgetkey[widget]["prop"] + if prop is not None: + if prop.name() == "dateTime" and isinstance(value, str): + value = QDateTime.fromString(value, Qt.ISODate) + prop.write(widget, value) + if created: + self._timer.start(self._delay) + + if block: + widget.blockSignals(False) + + def _setData(self, *args, **kwargs): + sender = self.sender() + info = self._widgetkey.get(sender, {}) + key = info.get("key", None) + prop: Union[QMetaProperty, None] = info.get("prop", None) + if key is None or prop is None: + return + + value = prop.read(sender) + if value is not None: + if prop.name() == "dateTime": + value = value.toString("yyyy-MM-dd HH:mm:ss") + elif prop.name() == "html" and len(sender.property("plainText")) == 0: + value = "" + + if value is None: + return + + self._timer.start(self._delay) + + # 更新关联的widget + for widget in self._keywidget.get(key, []): + if widget != sender: + if self.Debug: + print(f"view to view({widget}) = {value}") + self._setValue(widget, key, value, updated=True, block=True) + + # 更新关联的model + if self.Debug: + print(f"view({sender}) to model = {value}") + if not self._model.updateValue(key, value): + return + + def onItemDataChanged( + self, topLeft: QModelIndex, bottomRight: QModelIndex, roles: List[int] + ): + item = self._model.itemFromIndex(topLeft) + if not item or Qt.EditRole not in roles or item.valueItem: + return + + path = getattr(item, "path", None) + if not path: + return + + # 更新关联的widget + value = item.edit + for widget in self._keywidget.get(path, []): + if self.Debug: + print(f"model to view({widget}) = {value}") + self._setValue(widget, path, value, updated=True, block=True) diff --git a/QTreeView/Lib/serializewidget.py b/QTreeView/Lib/serializewidget.py new file mode 100644 index 00000000..fcd98edb --- /dev/null +++ b/QTreeView/Lib/serializewidget.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- + +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +try: + from PyQt5 import QtCore, QtGui, QtWidgets +except ImportError: + from PySide2 import QtCore, QtGui, QtWidgets + + +class Ui_SerializeWidget(object): + def setupUi(self, SerializeWidget): + SerializeWidget.setObjectName("SerializeWidget") + SerializeWidget.resize(800, 600) + self.verticalLayout = QtWidgets.QVBoxLayout(SerializeWidget) + self.verticalLayout.setObjectName("verticalLayout") + self.splitter = QtWidgets.QSplitter(SerializeWidget) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setChildrenCollapsible(False) + self.splitter.setObjectName("splitter") + self.groupBox = QtWidgets.QGroupBox(self.splitter) + self.groupBox.setMaximumSize(QtCore.QSize(800, 16777215)) + self.groupBox.setObjectName("groupBox") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.groupBox) + self.horizontalLayout.setObjectName("horizontalLayout") + self.treeJsonView = QtWidgets.QTreeView(self.groupBox) + self.treeJsonView.setObjectName("treeJsonView") + self.horizontalLayout.addWidget(self.treeJsonView) + self.editJsonView = QtWidgets.QTextEdit(self.groupBox) + self.editJsonView.setObjectName("editJsonView") + self.horizontalLayout.addWidget(self.editJsonView) + self.groupBox_2 = QtWidgets.QGroupBox(self.splitter) + self.groupBox_2.setObjectName("groupBox_2") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox_2) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.tabWidget = QtWidgets.QTabWidget(self.groupBox_2) + self.tabWidget.setObjectName("tabWidget") + self.tab = QtWidgets.QWidget() + self.tab.setObjectName("tab") + self.gridLayout = QtWidgets.QGridLayout(self.tab) + self.gridLayout.setObjectName("gridLayout") + self.doubleSpinBox = QtWidgets.QDoubleSpinBox(self.tab) + self.doubleSpinBox.setObjectName("doubleSpinBox") + self.gridLayout.addWidget(self.doubleSpinBox, 2, 1, 1, 1) + self.checkBox = QtWidgets.QCheckBox(self.tab) + self.checkBox.setObjectName("checkBox") + self.gridLayout.addWidget(self.checkBox, 0, 1, 1, 1) + self.radioButton = QtWidgets.QRadioButton(self.tab) + self.radioButton.setObjectName("radioButton") + self.gridLayout.addWidget(self.radioButton, 0, 0, 1, 1) + self.spinBox = QtWidgets.QSpinBox(self.tab) + self.spinBox.setObjectName("spinBox") + self.gridLayout.addWidget(self.spinBox, 2, 0, 1, 1) + self.lineEdit = QtWidgets.QLineEdit(self.tab) + self.lineEdit.setObjectName("lineEdit") + self.gridLayout.addWidget(self.lineEdit, 5, 0, 1, 2) + self.dateTimeEdit = QtWidgets.QDateTimeEdit(self.tab) + self.dateTimeEdit.setObjectName("dateTimeEdit") + self.gridLayout.addWidget(self.dateTimeEdit, 4, 0, 1, 2) + self.comboBox = QtWidgets.QComboBox(self.tab) + self.comboBox.setObjectName("comboBox") + self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1) + self.timeEdit = QtWidgets.QTimeEdit(self.tab) + self.timeEdit.setObjectName("timeEdit") + self.gridLayout.addWidget(self.timeEdit, 3, 0, 1, 1) + self.dateEdit = QtWidgets.QDateEdit(self.tab) + self.dateEdit.setObjectName("dateEdit") + self.gridLayout.addWidget(self.dateEdit, 3, 1, 1, 1) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem, 0, 3, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem1, 6, 0, 1, 1) + self.tabWidget.addTab(self.tab, "") + self.tab_2 = QtWidgets.QWidget() + self.tab_2.setObjectName("tab_2") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.tab_2) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.plainTextEdit = QtWidgets.QPlainTextEdit(self.tab_2) + self.plainTextEdit.setObjectName("plainTextEdit") + self.verticalLayout_4.addWidget(self.plainTextEdit) + self.textEdit = QtWidgets.QTextEdit(self.tab_2) + self.textEdit.setObjectName("textEdit") + self.verticalLayout_4.addWidget(self.textEdit) + self.tabWidget.addTab(self.tab_2, "") + self.tab_3 = QtWidgets.QWidget() + self.tab_3.setObjectName("tab_3") + self.gridLayout_2 = QtWidgets.QGridLayout(self.tab_3) + self.gridLayout_2.setObjectName("gridLayout_2") + self.horizontalSlider = QtWidgets.QSlider(self.tab_3) + self.horizontalSlider.setOrientation(QtCore.Qt.Horizontal) + self.horizontalSlider.setObjectName("horizontalSlider") + self.gridLayout_2.addWidget(self.horizontalSlider, 1, 0, 1, 1) + self.spinBox_2 = QtWidgets.QSpinBox(self.tab_3) + self.spinBox_2.setObjectName("spinBox_2") + self.gridLayout_2.addWidget(self.spinBox_2, 0, 0, 1, 1) + spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout_2.addItem(spacerItem2, 2, 0, 1, 1) + self.progressBar = QtWidgets.QProgressBar(self.tab_3) + self.progressBar.setProperty("value", 0) + self.progressBar.setOrientation(QtCore.Qt.Vertical) + self.progressBar.setObjectName("progressBar") + self.gridLayout_2.addWidget(self.progressBar, 0, 1, 3, 1) + self.verticalSlider = QtWidgets.QSlider(self.tab_3) + self.verticalSlider.setOrientation(QtCore.Qt.Vertical) + self.verticalSlider.setObjectName("verticalSlider") + self.gridLayout_2.addWidget(self.verticalSlider, 0, 2, 3, 1) + self.tabWidget.addTab(self.tab_3, "") + self.tab_4 = QtWidgets.QWidget() + self.tab_4.setObjectName("tab_4") + self.gridLayout_3 = QtWidgets.QGridLayout(self.tab_4) + self.gridLayout_3.setObjectName("gridLayout_3") + self.groupBox_3 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_3.setObjectName("groupBox_3") + self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.groupBox_3) + self.verticalLayout_6.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_6.setObjectName("verticalLayout_6") + self.listView = QtWidgets.QListView(self.groupBox_3) + self.listView.setObjectName("listView") + self.verticalLayout_6.addWidget(self.listView) + self.gridLayout_3.addWidget(self.groupBox_3, 0, 0, 1, 1) + self.groupBox_4 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_4.setObjectName("groupBox_4") + self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.groupBox_4) + self.verticalLayout_7.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_7.setObjectName("verticalLayout_7") + self.treeView = QtWidgets.QTreeView(self.groupBox_4) + self.treeView.setObjectName("treeView") + self.verticalLayout_7.addWidget(self.treeView) + self.gridLayout_3.addWidget(self.groupBox_4, 0, 1, 1, 1) + self.groupBox_5 = QtWidgets.QGroupBox(self.tab_4) + self.groupBox_5.setObjectName("groupBox_5") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.groupBox_5) + self.verticalLayout_5.setContentsMargins(2, 2, 2, 2) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.tableView = QtWidgets.QTableView(self.groupBox_5) + self.tableView.setObjectName("tableView") + self.verticalLayout_5.addWidget(self.tableView) + self.gridLayout_3.addWidget(self.groupBox_5, 1, 0, 1, 2) + self.tabWidget.addTab(self.tab_4, "") + self.verticalLayout_3.addWidget(self.tabWidget) + self.verticalLayout.addWidget(self.splitter) + + self.retranslateUi(SerializeWidget) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(SerializeWidget) + + def retranslateUi(self, SerializeWidget): + _translate = QtCore.QCoreApplication.translate + SerializeWidget.setWindowTitle(_translate("SerializeWidget", "TestSerialize")) + self.groupBox.setTitle(_translate("SerializeWidget", "Json View")) + self.groupBox_2.setTitle(_translate("SerializeWidget", "Widget View")) + self.checkBox.setText(_translate("SerializeWidget", "CheckBox")) + self.radioButton.setText(_translate("SerializeWidget", "RadioButton")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("SerializeWidget", "Input")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("SerializeWidget", "Edit")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), _translate("SerializeWidget", "Correlation")) + self.groupBox_3.setTitle(_translate("SerializeWidget", "ListView")) + self.groupBox_4.setTitle(_translate("SerializeWidget", "TreeView")) + self.groupBox_5.setTitle(_translate("SerializeWidget", "TableView")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), _translate("SerializeWidget", "View")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + SerializeWidget = QtWidgets.QWidget() + ui = Ui_SerializeWidget() + ui.setupUi(SerializeWidget) + SerializeWidget.show() + sys.exit(app.exec_()) diff --git a/QTreeView/README.md b/QTreeView/README.md index e69de29b..79327648 100644 --- a/QTreeView/README.md +++ b/QTreeView/README.md @@ -0,0 +1,24 @@ +# QTreeView + +- 目录 + - [通过json数据生成树形结构](#1通过json数据生成树形结构) + - [json树形结构查询和修改](#2json树形结构查询和修改) + - [json数据绑定](#3json数据绑定) + +## 1、通过json数据生成树形结构 + +[运行 TestJsonModel.py](TestJsonModel.py) + +![TestJsonModel](ScreenShot/TestJsonModel.gif) + +## 2、json树形结构查询和修改 + +[运行 TestModelModify.py](TestModelModify.py) + +![TestModelModify](ScreenShot/TestModelModify.gif) + +## 3、json数据绑定 + +[运行 TestSerializeModel.py](TestSerializeModel.py) + +![TestSerializeModel](ScreenShot/TestSerializeModel.gif) diff --git a/QTreeView/ScreenShot/TestJsonModel.gif b/QTreeView/ScreenShot/TestJsonModel.gif new file mode 100644 index 00000000..d91b7176 Binary files /dev/null and b/QTreeView/ScreenShot/TestJsonModel.gif differ diff --git a/QTreeView/ScreenShot/TestModelModify.gif b/QTreeView/ScreenShot/TestModelModify.gif new file mode 100644 index 00000000..834dc413 Binary files /dev/null and b/QTreeView/ScreenShot/TestModelModify.gif differ diff --git a/QTreeView/ScreenShot/TestSerializeModel.gif b/QTreeView/ScreenShot/TestSerializeModel.gif new file mode 100644 index 00000000..ca54f485 Binary files /dev/null and b/QTreeView/ScreenShot/TestSerializeModel.gif differ diff --git a/QTreeView/TestJsonModel.py b/QTreeView/TestJsonModel.py new file mode 100644 index 00000000..febf6fe7 --- /dev/null +++ b/QTreeView/TestJsonModel.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/05 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TestJsonModel.py +@description: +""" + +import sys + +from Lib.qjsonmodel import QJsonItem, QJsonModel + +try: + from PyQt5.QtCore import QModelIndex, QSortFilterProxyModel, Qt + from PyQt5.QtWidgets import ( + QAction, + QApplication, + QCheckBox, + QGridLayout, + QLineEdit, + QPlainTextEdit, + QTreeView, + QWidget, + ) +except ImportError: + from PySide2.QtCore import QModelIndex, QSortFilterProxyModel, Qt + from PySide2.QtWidgets import ( + QAction, + QApplication, + QCheckBox, + QGridLayout, + QLineEdit, + QPlainTextEdit, + QTreeView, + QWidget, + ) + + +class TestWindow(QWidget): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.resize(800, 600) + layout = QGridLayout(self) + self.filterPath = QCheckBox("Path Filter?", self) + self.filterEdit = QLineEdit(self) + self.treeView = QTreeView(self) + self.widgetEdit = QPlainTextEdit(self) + layout.addWidget(self.filterPath, 0, 0, 1, 1) + layout.addWidget(self.filterEdit, 0, 1, 1, 1) + layout.addWidget(self.treeView, 1, 0, 1, 2) + layout.addWidget(self.widgetEdit, 0, 2, 2, 1) + + self.filterPath.toggled.connect(self.onFilterPathToggled) + self.filterEdit.setPlaceholderText("过滤条件") + self.filterEdit.textChanged.connect(self.onFilterTextChanged) + + self.model = QJsonModel(self) + self.model.itemChanged.connect(self.onItemChanged) + self.model.rowsRemoved.connect(self.onRowsRemoved) + self.treeView.clicked.connect(self.onItemChanged) + action = QAction("Delete", self.treeView) + action.triggered.connect(self.deleteItem) + self.treeView.setContextMenuPolicy(Qt.ActionsContextMenu) + self.treeView.addAction(action) + + # setup model data + self.setupModel() + + # filter model + self.fmodel = QSortFilterProxyModel(self) + self.fmodel.setSourceModel(self.model) + self.fmodel.setRecursiveFilteringEnabled(True) + self.treeView.setModel(self.fmodel) + self.treeView.expandAll() + + def setupModel(self): + data = { + "name": "Irony", + "age": 33, + "address": { + "country": "China", + "city": "Chengdu", + }, + "phone": [ + {"type": "home", "number": "123456789"}, + {"type": "fax", "number": "6987654321"}, + ], + "marriage": False, + "salary": 6666.6, + "skills": [ + "C++", + "Python", + "Java", + "JavaScript", + "Shell", + "Android", + ], + "others": [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + } + self.model.blockSignals(True) + self.model.loadData(data) + self.model.blockSignals(False) + + self.treeView.expandAll() + self.doExport() + + def doExport(self): + self.widgetEdit.setPlainText(self.model.toJson(ensure_ascii=False, indent=4)) + + def deleteItem(self, _): + indexes = self.treeView.selectedIndexes() + if not indexes: + return + + index = indexes[0] + print(index.row(), index.column(), index.parent()) + self.model.removeRow(index.row(), index.parent()) + + def onRowsRemoved(self, parent, first, last): + print("onRowsRemoved", parent, first, last) + self.doExport() + + def onItemChanged(self, item): + if self.sender() == self.model: + self.doExport() + + if isinstance(item, QModelIndex): + item = self.model.itemFromIndex(item) + + if not item: + return + keyInfo = "" + if item.keyItem: + keyInfo = f"row={item.keyItem.row()}, col={item.keyItem.column()}, text={item.keyItem.text()}" + print( + f"row={item.row()}, col={item.column()}, text={item.text()}, value={str(item.data(Qt.EditRole))}, key=({keyInfo})\n\trowCount={item.rowCount()}\n\tpath={item.path}" + ) + + def onFilterPathToggled(self, checked): + if checked: + self.fmodel.setFilterRole(QJsonItem.PathRole) + else: + self.fmodel.setFilterRole(Qt.DisplayRole) + + def onFilterTextChanged(self, text): + self.fmodel.setFilterFixedString(text) + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QTreeView/TestModelModify.py b/QTreeView/TestModelModify.py new file mode 100644 index 00000000..b62b0976 --- /dev/null +++ b/QTreeView/TestModelModify.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/05 +@author: Irony +@site: https://pyqt.site | https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TestModelModify.py +@description: +""" + +import json +import sys + +from Lib.qjsonmodel import QJsonItem, QJsonModel + +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import ( + QApplication, + QFormLayout, + QHBoxLayout, + QLineEdit, + QPlainTextEdit, + QPushButton, + QTreeView, + QWidget, + ) +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import ( + QApplication, + QFormLayout, + QHBoxLayout, + QLineEdit, + QPlainTextEdit, + QPushButton, + QTreeView, + QWidget, + ) + + +class TestWindow(QWidget): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + layout = QHBoxLayout(self) + self.treeView = QTreeView(self) + self.widgetEdit = QWidget(self) + layout.addWidget(self.treeView) + layout.addWidget(self.widgetEdit) + + layout = QFormLayout(self.widgetEdit) + layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop) + layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) + self.editPath = QLineEdit(f"address{QJsonItem.Sep}city", self.widgetEdit) + self.editPath.setPlaceholderText( + f"请输入查询内容,支持路径查询,比如:aaa{QJsonItem.Sep}bbb{QJsonItem.Sep}ccc" + ) + layout.addRow("Path:", self.editPath) + self.editValue = QLineEdit("SiChuan", self.widgetEdit) + self.editValue.setPlaceholderText("请输入修改值,只支持路径匹配") + layout.addRow("Value:", self.editValue) + self.buttonQuery = QPushButton("查询", self.widgetEdit) + self.buttonModify = QPushButton("修改", self.widgetEdit) + self.buttonExport = QPushButton("获取Json", self.widgetEdit) + layout.addRow("", self.buttonQuery) + layout.addRow("", self.buttonModify) + layout.addRow("", self.buttonExport) + self.editText = QPlainTextEdit(self.widgetEdit) + self.editText.setReadOnly(True) + layout.addRow("", self.editText) + self.buttonQuery.clicked.connect(self.doQuery) + self.buttonModify.clicked.connect(self.doModify) + self.buttonExport.clicked.connect(self.doExport) + + self.model = QJsonModel(self) + self.model.itemChanged.connect(self.onItemChanged) + self.treeView.setModel(self.model) + + self.setupModel() + + def doQuery(self): + self.editText.clear() + path = self.editPath.text().strip() + if not path: + return + + item = self.model.findPath(path) + + if item: + self.editText.appendPlainText( + str( + { + "path": item.path, + "text": item.text(), + "value": item.valueItem.edit, + } + ) + ) + + def doModify(self): + self.editText.clear() + path = self.editPath.text().strip() + value = self.editValue.text().strip() + if not path or not value: + return + + try: + value = json.loads(value) + except Exception: + pass + + ret = self.model.updateValue(path, value) + self.editText.setPlainText(f"change ret: {ret}") + + def doExport(self): + self.editText.setPlainText(self.model.toJson(ensure_ascii=False, indent=4)) + + def setupModel(self): + data = { + "name": "Irony", + "age": 33, + "address": { + "country": "China", + "city": "Chengdu", + }, + "phone": [ + {"type": "home", "number": "123456789"}, + {"type": "fax", "number": "6987654321"}, + ], + "marriage": False, + "salary": 6666.6, + "skills": [ + "C++", + "Python", + "Java", + "JavaScript", + "Shell", + "Android", + ], + "others": [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + } + self.model.blockSignals(True) + self.model.loadData(data) + self.model.blockSignals(False) + + self.treeView.expandAll() + self.doExport() + + def onItemChanged(self, item): + self.doExport() + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QTreeView/TestSerializeModel.py b/QTreeView/TestSerializeModel.py new file mode 100644 index 00000000..20b0b62b --- /dev/null +++ b/QTreeView/TestSerializeModel.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2025/08/08 +@file: TestSerializeModel.py +@description: +""" + +import json +import sys +from datetime import datetime + +from Lib.qmodelmapper import QModelMapper +from Lib.serializewidget import Ui_SerializeWidget + +try: + from PyQt5.QtCore import QRegExp, Qt + from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat + from PyQt5.QtWidgets import QApplication, QWidget +except ImportError: + from PySide2.QtCore import QRegExp, Qt + from PySide2.QtGui import QSyntaxHighlighter, QTextCharFormat + from PySide2.QtWidgets import QApplication, QWidget + + +class HighlightingRule: + def __init__(self, pattern, color): + self.pattern = QRegExp(pattern) + self.format = QTextCharFormat() + self.format.setForeground(color) + + +class JsonHighlighter(QSyntaxHighlighter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._rules = [ + # numbers + HighlightingRule(QRegExp('([-0-9.]+)(?!([^"]*"[\\s]*\\:))'), Qt.darkRed), + # key + HighlightingRule(QRegExp('("[^"]*")\\s*\\:'), Qt.darkBlue), + # value + HighlightingRule(QRegExp(':+(?:[: []*)("[^"]*")'), Qt.darkGreen), + ] + + def highlightBlock(self, text: str) -> None: + for rule in self._rules: + index = rule.pattern.indexIn(text) + while index >= 0: + length = rule.pattern.matchedLength() + self.setFormat(index, length, rule.format) + index = rule.pattern.indexIn(text, index + length) + + +class TestWindow(QWidget, Ui_SerializeWidget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setupUi(self) + self.resize(1200, 600) + + # json view + self.highlighter = JsonHighlighter(self.editJsonView.document()) + self.editJsonView.textChanged.connect(self.onJsonChanged) + + # serialize model + QModelMapper.Debug = True + self.mapper = QModelMapper(self) + self.mapper.setData( + { + "input": { + "radioButton": True, + "checkBox": False, + }, + "name": "Irony", + } + ) + self.mapper.valueChanged.connect(self.onModelChanged) + self.treeJsonView.setModel(self.mapper.getModel()) + self.treeJsonView.expandAll() + + # comboBox + self.comboBox.addItems([f"Item {i}" for i in range(10)]) + + self.mapper.bind(self.radioButton, "input/radioButton") + self.mapper.bind(self.checkBox, "input/checkBox", True) + self.mapper.bind(self.comboBox, "input/comboBox") + self.mapper.bind(self.spinBox, "age") + self.mapper.bind(self.doubleSpinBox, "money") + self.mapper.bind(self.lineEdit, "name") + + # date and time + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.mapper.bind(self.timeEdit, "input/time", now) + self.mapper.bind(self.dateEdit, "input/date", now) + self.mapper.bind(self.dateTimeEdit, "input/dateTime", now) + + # edit + self.mapper.bind(self.plainTextEdit, "desc/desc") + self.mapper.bind(self.textEdit, "desc/text") + + # Correlation + self.mapper.bind(self.spinBox_2, "correlation/slider") + self.mapper.bind(self.horizontalSlider, "correlation/slider") + self.mapper.bind(self.progressBar, "correlation/progress") + self.mapper.bind(self.verticalSlider, "correlation/progress") + self.treeJsonView.expandAll() + + def onModelChanged(self): + data = self.mapper.getJson(indent=2) + self.editJsonView.blockSignals(True) + self.editJsonView.setPlainText(data) + self.editJsonView.blockSignals(False) + + def onJsonChanged(self): + text = self.editJsonView.toPlainText().strip() + try: + data = json.loads(text) + self.mapper.setData(data) + except Exception: + pass + + +if __name__ == "__main__": + import cgitb + import sys + + cgitb.enable(format="text") + app = QApplication(sys.argv) + w = TestWindow() + w.show() + sys.exit(app.exec_()) diff --git a/QTreeWidget/Lib/testTree.py b/QTreeWidget/Lib/testTree.py index 647f0055..2745b729 100644 --- a/QTreeWidget/Lib/testTree.py +++ b/QTreeWidget/Lib/testTree.py @@ -6,7 +6,8 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtWidgets + class Ui_Form(object): def setupUi(self, Form): @@ -48,10 +49,10 @@ def retranslateUi(self, Form): if __name__ == "__main__": import sys + app = QtWidgets.QApplication(sys.argv) Form = QtWidgets.QWidget() ui = Ui_Form() ui.setupUi(Form) Form.show() sys.exit(app.exec_()) - diff --git a/QTreeWidget/ParentNodeForbid.py b/QTreeWidget/ParentNodeForbid.py index 18759dd2..1a139e7b 100644 --- a/QTreeWidget/ParentNodeForbid.py +++ b/QTreeWidget/ParentNodeForbid.py @@ -4,19 +4,20 @@ """ Created on 2019年11月8日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QTreeWidget.ParentNodeForbid @description: 父节点不可选中 """ -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QStyledItemDelegate,\ - QStyle - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' -__Version__ = 1.0 +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem, QStyledItemDelegate, \ + QStyle +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem, QStyledItemDelegate, \ + QStyle class NoColorItemDelegate(QStyledItemDelegate): @@ -84,8 +85,9 @@ def onItemPressed(self, item, column): if __name__ == '__main__': import sys import cgitb - cgitb.enable(1, None, 5, '') - from PyQt5.QtWidgets import QApplication + + cgitb.enable(format='text') + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QTreeWidget/ParsingJson.py b/QTreeWidget/ParsingJson.py index dbbc088c..c32480ce 100644 --- a/QTreeWidget/ParsingJson.py +++ b/QTreeWidget/ParsingJson.py @@ -4,7 +4,7 @@ """ Created on 2018年4月8日 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ParsingJson @description: @@ -12,18 +12,18 @@ import json import webbrowser -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QWidget,\ - QLabel, QSpacerItem, QSizePolicy, QHBoxLayout import chardet - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QIcon + from PyQt5.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem, QWidget, \ + QLabel, QSpacerItem, QSizePolicy, QHBoxLayout +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QIcon + from PySide2.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem, QWidget, \ + PySide2, QSpacerItem, QSizePolicy, QHBoxLayout class ItemWidget(QWidget): @@ -94,7 +94,7 @@ def loadData(self, path): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) app.setStyleSheet("""QTreeView { outline: 0px; diff --git a/QTreeWidget/README.md b/QTreeWidget/README.md index a1afc7d0..1ecaf373 100644 --- a/QTreeWidget/README.md +++ b/QTreeWidget/README.md @@ -6,24 +6,26 @@ - [禁止父节点/禁止父节点](#3禁止父节点) ## 1、通过json数据生成树形结构 + [运行 ParsingJson.py](ParsingJson.py) 解析每一层json数据中的list - ![ParsingJson](ScreenShot/ParsingJson.png) ## 2、点击父节点全选/取消全选子节点 -[运行 testTreeWidget.py](testTreeWidget.py) + +[运行 testTreeWidget.py](testTreeWidget.py) | [查看 testTree.ui](Data/testTree.ui) 点击父节点全选/取消全选子节点 ![testTreeWidget](ScreenShot/allSelectNode.png) ## 3、禁止父节点 + [运行 ParentNodeForbid.py](ParentNodeForbid.py) 1. 父节点通过设置`pitem1.setFlags(pitem1.flags() & ~Qt.ItemIsSelectable)`为不可选 2. 完全禁用点击等需要重写`mousePressEvent`事件并结合item的标志来判断 -![ParentNodeForbid](ScreenShot/ParentNodeForbid.gif) \ No newline at end of file +![ParentNodeForbid](ScreenShot/ParentNodeForbid.gif) diff --git a/QTreeWidget/testTreeWidget.py b/QTreeWidget/testTreeWidget.py index 39aecad0..3bdb9568 100644 --- a/QTreeWidget/testTreeWidget.py +++ b/QTreeWidget/testTreeWidget.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # encoding: utf-8 -''' +""" Created on 2017年4月20日 @author: weike32 -@site: https://pyqt5.com , https://github.com/weike32 +@site: https://pyqt.site , https://github.com/weike32 @email: 394967319@qq.com @file: CopyContent @description: -''' +""" import sys from PyQt5.QtCore import Qt diff --git a/QVBoxLayout/Data/BaseVerticalLayout.ui b/QVBoxLayout/Data/BaseVerticalLayout.ui new file mode 100644 index 00000000..77c5b148 --- /dev/null +++ b/QVBoxLayout/Data/BaseVerticalLayout.ui @@ -0,0 +1,38 @@ + + + BaseVerticalLayout + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + 通过 右键 -> 布局 -> 垂直布局 + + + Qt::AlignCenter + + + + + + + 底部按钮 + + + + + + + + diff --git a/QVBoxLayout/Data/VerticalLayoutMargin.ui b/QVBoxLayout/Data/VerticalLayoutMargin.ui new file mode 100644 index 00000000..6045d809 --- /dev/null +++ b/QVBoxLayout/Data/VerticalLayoutMargin.ui @@ -0,0 +1,62 @@ + + + VerticalLayoutMargin + + + + 0 + 0 + 400 + 300 + + + + Form + + + #VerticalLayoutMargin { + background: #5aaadb; +} +#label { + background: #85c440; +} + + + + 50 + + + 20 + + + 20 + + + + + 通过设置Margin和Spacing设置边距以及两个控件之间的间隔距离 + + + Qt::AlignCenter + + + + + + + Spcasing 为 50 + + + + + + + Spcasing 为 50 + + + + + + + + diff --git a/QVBoxLayout/Data/VerticalLayoutStretch.ui b/QVBoxLayout/Data/VerticalLayoutStretch.ui new file mode 100644 index 00000000..bf198c9f --- /dev/null +++ b/QVBoxLayout/Data/VerticalLayoutStretch.ui @@ -0,0 +1,62 @@ + + + VerticalLayoutStretch + + + + 0 + 0 + 400 + 300 + + + + Form + + + #label { + background: #5aaadb; +} +#label_2 { + background: #85c440; +} +#label_3 { + background: #f2b63c; +} + + + + + + 通过设置Stretch设置每部分的占比(1,2,3) 1/6 + + + Qt::AlignCenter + + + + + + + 2/6 + + + Qt::AlignCenter + + + + + + + 3/6 + + + Qt::AlignCenter + + + + + + + + diff --git a/QVBoxLayout/README.md b/QVBoxLayout/README.md index e69de29b..8d9c67c5 100644 --- a/QVBoxLayout/README.md +++ b/QVBoxLayout/README.md @@ -0,0 +1,35 @@ +# QVBoxLayout + +- 目录 + - [垂直布局](#1垂直布局) + - [边距和间隔](#2边距和间隔) + - [比例分配](#3比例分配) + +## 1、垂直布局 + +[查看 BaseVerticalLayout.ui](Data/BaseVerticalLayout.ui) + +![BaseVerticalLayout](ScreenShot/BaseVerticalLayout.png) + +## 2、边距和间隔 + +[查看 VerticalLayoutMargin.ui](Data/VerticalLayoutMargin.ui) + +1. 通过`setContentsMargins(20, 20, -1, -1)`设置左上右下的边距,-1表示默认值 +2. 通过`setSpacing`设置控件之间的间隔 + +![VerticalLayoutMargin](ScreenShot/VerticalLayoutMargin.png) + +## 3、比例分配 + +[查看 VerticalLayoutStretch.ui](Data/VerticalLayoutStretch.ui) + +通过`setStretch`设置各个部分的占比 分别为:1/6 2/6 3/6 + +```python +self.verticalLayout.setStretch(0, 1) +self.verticalLayout.setStretch(1, 2) +self.verticalLayout.setStretch(2, 3) +``` + +![VerticalLayoutStretch](ScreenShot/VerticalLayoutStretch.png) diff --git a/QVBoxLayout/ScreenShot/BaseVerticalLayout.png b/QVBoxLayout/ScreenShot/BaseVerticalLayout.png new file mode 100644 index 00000000..944a73a7 Binary files /dev/null and b/QVBoxLayout/ScreenShot/BaseVerticalLayout.png differ diff --git a/QVBoxLayout/ScreenShot/VerticalLayoutMargin.png b/QVBoxLayout/ScreenShot/VerticalLayoutMargin.png new file mode 100644 index 00000000..2208490c Binary files /dev/null and b/QVBoxLayout/ScreenShot/VerticalLayoutMargin.png differ diff --git a/QVBoxLayout/ScreenShot/VerticalLayoutStretch.png b/QVBoxLayout/ScreenShot/VerticalLayoutStretch.png new file mode 100644 index 00000000..ff357323 Binary files /dev/null and b/QVBoxLayout/ScreenShot/VerticalLayoutStretch.png differ diff --git a/QWebChannel/CallEachWithJs.py b/QWebChannel/CallEachWithJs.py new file mode 100644 index 00000000..238b0625 --- /dev/null +++ b/QWebChannel/CallEachWithJs.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2021/12/15 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CallEachWithJs.py +@description: 与JS之间的互相调用 +""" + +import os + +from PyQt5.QtCore import QUrl, pyqtSlot +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtWidgets import (QApplication, QLineEdit, QPushButton, QVBoxLayout, + QWidget) + +from Lib.WebChannelObject import WebChannelObject + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + self.m_obj = WebChannelObject(self) + # 注册该窗口,可以访问该窗口的属性,槽函数,信号 + # https://doc.qt.io/qt-5/qwidget.html#properties + # https://doc.qt.io/qt-5/qwidget.html#signals + # https://doc.qt.io/qt-5/qwidget.html#public-slots + self.m_obj.registerObject('qtwindow', self) + self.m_obj.start() + + layout = QVBoxLayout(self) + self.editTitle = QLineEdit(self, placeholderText='输入标题') + layout.addWidget(self.editTitle) + layout.addWidget(QPushButton('修改标题', self, clicked=self.onChangeTitle)) + + QDesktopServices.openUrl( + QUrl.fromLocalFile( + os.path.join(os.path.dirname(sys.argv[0] or __file__), + 'Data/CallEachWithJs.html'))) + + def onChangeTitle(self): + self.setWindowTitle(self.editTitle.text()) + + # ------- 把非槽函数通过pyqtSlot重新暴露 ------- + @pyqtSlot(int, int) + def resize(self, width, height): + super().resize(width, height) + + +if __name__ == '__main__': + import cgitb + import sys + + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QWebChannel/Data/CallEachWithJs.html b/QWebChannel/Data/CallEachWithJs.html new file mode 100644 index 00000000..fb5752be --- /dev/null +++ b/QWebChannel/Data/CallEachWithJs.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + 输出:


+ + +

+ + +

+ + +

+ + +

+ + + +

+ + + + + \ No newline at end of file diff --git a/QWebChannel/Data/qwebchannel.js b/QWebChannel/Data/qwebchannel.js new file mode 100644 index 00000000..32ad51eb --- /dev/null +++ b/QWebChannel/Data/qwebchannel.js @@ -0,0 +1,448 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +"use strict"; + +var QWebChannelMessageTypes = { + signal: 1, + propertyUpdate: 2, + init: 3, + idle: 4, + debug: 5, + invokeMethod: 6, + connectToSignal: 7, + disconnectFromSignal: 8, + setProperty: 9, + response: 10, +}; + +var QWebChannel = function(transport, initCallback) +{ + if (typeof transport !== "object" || typeof transport.send !== "function") { + console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + + " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); + return; + } + + var channel = this; + this.transport = transport; + + this.send = function(data) + { + if (typeof(data) !== "string") { + data = JSON.stringify(data); + } + channel.transport.send(data); + } + + this.transport.onmessage = function(message) + { + var data = message.data; + if (typeof data === "string") { + data = JSON.parse(data); + } + switch (data.type) { + case QWebChannelMessageTypes.signal: + channel.handleSignal(data); + break; + case QWebChannelMessageTypes.response: + channel.handleResponse(data); + break; + case QWebChannelMessageTypes.propertyUpdate: + channel.handlePropertyUpdate(data); + break; + default: + console.error("invalid message received:", message.data); + break; + } + } + + this.execCallbacks = {}; + this.execId = 0; + this.exec = function(data, callback) + { + if (!callback) { + // if no callback is given, send directly + channel.send(data); + return; + } + if (channel.execId === Number.MAX_VALUE) { + // wrap + channel.execId = Number.MIN_VALUE; + } + if (data.hasOwnProperty("id")) { + console.error("Cannot exec message with property id: " + JSON.stringify(data)); + return; + } + data.id = channel.execId++; + channel.execCallbacks[data.id] = callback; + channel.send(data); + }; + + this.objects = {}; + + this.handleSignal = function(message) + { + var object = channel.objects[message.object]; + if (object) { + object.signalEmitted(message.signal, message.args); + } else { + console.warn("Unhandled signal: " + message.object + "::" + message.signal); + } + } + + this.handleResponse = function(message) + { + if (!message.hasOwnProperty("id")) { + console.error("Invalid response message received: ", JSON.stringify(message)); + return; + } + channel.execCallbacks[message.id](message.data); + delete channel.execCallbacks[message.id]; + } + + this.handlePropertyUpdate = function(message) + { + message.data.forEach(data => { + var object = channel.objects[data.object]; + if (object) { + object.propertyUpdate(data.signals, data.properties); + } else { + console.warn("Unhandled property update: " + data.object + "::" + data.signal); + } + }); + channel.exec({type: QWebChannelMessageTypes.idle}); + } + + this.debug = function(message) + { + channel.send({type: QWebChannelMessageTypes.debug, data: message}); + }; + + channel.exec({type: QWebChannelMessageTypes.init}, function(data) { + for (const objectName of Object.keys(data)) { + new QObject(objectName, data[objectName], channel); + } + + // now unwrap properties, which might reference other registered objects + for (const objectName of Object.keys(channel.objects)) { + channel.objects[objectName].unwrapProperties(); + } + + if (initCallback) { + initCallback(channel); + } + channel.exec({type: QWebChannelMessageTypes.idle}); + }); +}; + +function QObject(name, data, webChannel) +{ + this.__id__ = name; + webChannel.objects[name] = this; + + // List of callbacks that get invoked upon signal emission + this.__objectSignals__ = {}; + + // Cache of all properties, updated when a notify signal is emitted + this.__propertyCache__ = {}; + + var object = this; + + // ---------------------------------------------------------------------- + + this.unwrapQObject = function(response) + { + if (response instanceof Array) { + // support list of objects + return response.map(qobj => object.unwrapQObject(qobj)) + } + if (!(response instanceof Object)) + return response; + + if (!response["__QObject*__"] || response.id === undefined) { + var jObj = {}; + for (const propName of Object.keys(response)) { + jObj[propName] = object.unwrapQObject(response[propName]); + } + return jObj; + } + + var objectId = response.id; + if (webChannel.objects[objectId]) + return webChannel.objects[objectId]; + + if (!response.data) { + console.error("Cannot unwrap unknown QObject " + objectId + " without data."); + return; + } + + var qObject = new QObject( objectId, response.data, webChannel ); + qObject.destroyed.connect(function() { + if (webChannel.objects[objectId] === qObject) { + delete webChannel.objects[objectId]; + // reset the now deleted QObject to an empty {} object + // just assigning {} though would not have the desired effect, but the + // below also ensures all external references will see the empty map + // NOTE: this detour is necessary to workaround QTBUG-40021 + Object.keys(qObject).forEach(name => delete qObject[name]); + } + }); + // here we are already initialized, and thus must directly unwrap the properties + qObject.unwrapProperties(); + return qObject; + } + + this.unwrapProperties = function() + { + for (const propertyIdx of Object.keys(object.__propertyCache__)) { + object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); + } + } + + function addSignal(signalData, isPropertyNotifySignal) + { + var signalName = signalData[0]; + var signalIndex = signalData[1]; + object[signalName] = { + connect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to connect to signal " + signalName); + return; + } + + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + object.__objectSignals__[signalIndex].push(callback); + + // only required for "pure" signals, handled separately for properties in propertyUpdate + if (isPropertyNotifySignal) + return; + + // also note that we always get notified about the destroyed signal + if (signalName === "destroyed" || signalName === "destroyed()" || signalName === "destroyed(QObject*)") + return; + + // and otherwise we only need to be connected only once + if (object.__objectSignals__[signalIndex].length == 1) { + webChannel.exec({ + type: QWebChannelMessageTypes.connectToSignal, + object: object.__id__, + signal: signalIndex + }); + } + }, + disconnect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to disconnect from signal " + signalName); + return; + } + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + var idx = object.__objectSignals__[signalIndex].indexOf(callback); + if (idx === -1) { + console.error("Cannot find connection of signal " + signalName + " to " + callback.name); + return; + } + object.__objectSignals__[signalIndex].splice(idx, 1); + if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: QWebChannelMessageTypes.disconnectFromSignal, + object: object.__id__, + signal: signalIndex + }); + } + } + }; + } + + /** + * Invokes all callbacks for the given signalname. Also works for property notify callbacks. + */ + function invokeSignalCallbacks(signalName, signalArgs) + { + var connections = object.__objectSignals__[signalName]; + if (connections) { + connections.forEach(function(callback) { + callback.apply(callback, signalArgs); + }); + } + } + + this.propertyUpdate = function(signals, propertyMap) + { + // update property cache + for (const propertyIndex of Object.keys(propertyMap)) { + var propertyValue = propertyMap[propertyIndex]; + object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue); + } + + for (const signalName of Object.keys(signals)) { + // Invoke all callbacks, as signalEmitted() does not. This ensures the + // property cache is updated before the callbacks are invoked. + invokeSignalCallbacks(signalName, signals[signalName]); + } + } + + this.signalEmitted = function(signalName, signalArgs) + { + invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs)); + } + + function addMethod(methodData) + { + var methodName = methodData[0]; + var methodIdx = methodData[1]; + + // Fully specified methods are invoked by id, others by name for host-side overload resolution + var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName + + object[methodName] = function() { + var args = []; + var callback; + var errCallback; + for (var i = 0; i < arguments.length; ++i) { + var argument = arguments[i]; + if (typeof argument === "function") + callback = argument; + else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined) + args.push({ + "id": argument.__id__ + }); + else + args.push(argument); + } + + var result; + // during test, webChannel.exec synchronously calls the callback + // therefore, the promise must be constucted before calling + // webChannel.exec to ensure the callback is set up + if (!callback && (typeof(Promise) === 'function')) { + result = new Promise(function(resolve, reject) { + callback = resolve; + errCallback = reject; + }); + } + + webChannel.exec({ + "type": QWebChannelMessageTypes.invokeMethod, + "object": object.__id__, + "method": invokedMethod, + "args": args + }, function(response) { + if (response !== undefined) { + var result = object.unwrapQObject(response); + if (callback) { + (callback)(result); + } + } else if (errCallback) { + (errCallback)(); + } + }); + + return result; + }; + } + + function bindGetterSetter(propertyInfo) + { + var propertyIndex = propertyInfo[0]; + var propertyName = propertyInfo[1]; + var notifySignalData = propertyInfo[2]; + // initialize property cache with current value + // NOTE: if this is an object, it is not directly unwrapped as it might + // reference other QObject that we do not know yet + object.__propertyCache__[propertyIndex] = propertyInfo[3]; + + if (notifySignalData) { + if (notifySignalData[0] === 1) { + // signal name is optimized away, reconstruct the actual name + notifySignalData[0] = propertyName + "Changed"; + } + addSignal(notifySignalData, true); + } + + Object.defineProperty(object, propertyName, { + configurable: true, + get: function () { + var propertyValue = object.__propertyCache__[propertyIndex]; + if (propertyValue === undefined) { + // This shouldn't happen + console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); + } + + return propertyValue; + }, + set: function(value) { + if (value === undefined) { + console.warn("Property setter for " + propertyName + " called with undefined value!"); + return; + } + object.__propertyCache__[propertyIndex] = value; + var valueToSend = value; + if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined) + valueToSend = { "id": valueToSend.__id__ }; + webChannel.exec({ + "type": QWebChannelMessageTypes.setProperty, + "object": object.__id__, + "property": propertyIndex, + "value": valueToSend + }); + } + }); + + } + + // ---------------------------------------------------------------------- + + data.methods.forEach(addMethod); + + data.properties.forEach(bindGetterSetter); + + data.signals.forEach(function(signal) { addSignal(signal, false); }); + + Object.assign(object, data.enums); +} + +//required for use with nodejs +if (typeof module === 'object') { + module.exports = { + QWebChannel: QWebChannel + }; +} diff --git a/QWebChannel/Lib/WebChannelObject.py b/QWebChannel/Lib/WebChannelObject.py new file mode 100644 index 00000000..7fe5e9ed --- /dev/null +++ b/QWebChannel/Lib/WebChannelObject.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2021/12/15 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: WebChannelObject.py +@description: 交互对象,需要继承QObject并暴露接口 +""" + +from PyQt5.QtCore import (QJsonDocument, QJsonParseError, QObject, + pyqtProperty, pyqtSlot) +from PyQt5.QtNetwork import QHostAddress +from PyQt5.QtWebChannel import QWebChannel, QWebChannelAbstractTransport +from PyQt5.QtWebSockets import QWebSocketServer + + +class WebSocketTransport(QWebChannelAbstractTransport): + + def __init__(self, socket, *args, **kwargs): + super(WebSocketTransport, self).__init__(*args, **kwargs) + self.m_socket = socket + self.m_socket.textMessageReceived.connect(self.textMessageReceived) + self.m_socket.disconnected.connect(self.deleteLater) + + def sendMessage(self, message): + print('sendMessage:', message) + self.m_socket.sendTextMessage( + QJsonDocument(message).toJson(QJsonDocument.Compact).data().decode( + 'utf-8', errors='ignore')) + + def textMessageReceived(self, message): + print('textMessageReceived:', message) + error = QJsonParseError() + json = QJsonDocument.fromJson(message.encode('utf-8', errors='ignore'), + error) + if error.error: + print('Failed to parse message:{}, Error is:{}'.format( + message, error.errorString())) + return + if not json.isObject(): + print('Received JSON message that is not an object:{}'.format( + message)) + return + self.messageReceived.emit(json.object(), self) + + +class WebChannelObject(QObject): + + def __init__(self, *args, **kwargs): + super(WebChannelObject, self).__init__(*args, **kwargs) + # 内部属性供外部调用 + self._intValue = 0 + self._floatValue = 0.0 + self._boolValue = False + self._strValue = '' + # 设置数组或者字典有一定问题 + # self._listValue = [] + # self._mapValue = {} + + # webchannel对象 + self.m_webchannel = QWebChannel(self) + # 这里默认注册自己,这里使用了类名作为名称 + self.registerObject(self.__class__.__name__, self) + # websocket服务 + self.m_clients = {} + self.m_server = QWebSocketServer(self.__class__.__name__, + QWebSocketServer.NonSecureMode, self) + + def registerObject(self, name, obj): + """注册对象 + @param name: 名称 + @type name: str + @param obj: 对象 + @type obj: QObject + """ + self.m_webchannel.registerObject(name, obj) + + def registerObjects(self, objects): + """注册多个对象 + @param objects: 对象列表 + @type objects: list + """ + for name, obj in objects: + self.registerObject(name, obj) + + def deregisterObject(self, obj): + """注销对象 + @param obj: 对象 + @type obj: QObject + """ + self.m_webchannel.deregisterObject(obj) + + def deregisterObjects(self, objects): + """注销多个对象 + @param objects: 对象列表 + @type objects: list + """ + for obj in objects: + self.deregisterObject(obj) + + def start(self, port=12345): + """启动服务 + @param port: 端口 + @type port: int + """ + if not self.m_server.listen(QHostAddress.Any, port): + raise Exception( + 'Failed to create WebSocket server on port {}'.format(port)) + + print('WebSocket server listening on port {}'.format(port)) + # 新连接信号 + self.m_server.newConnection.connect(self._handleNewConnection) + + def stop(self): + """停止服务""" + self.m_server.close() + + def _handleNewConnection(self): + """新连接""" + socket = self.m_server.nextPendingConnection() + print('New WebSocket connection from {}'.format( + socket.peerAddress().toString())) + # 连接关闭信号 + socket.disconnected.connect(self._handleDisconnected) + transport = WebSocketTransport(socket) + self.m_clients[socket] = transport + self.m_webchannel.connectTo(transport) + + def _handleDisconnected(self): + """连接关闭""" + socket = self.sender() + print('WebSocket connection from {} closed'.format( + socket.peerAddress())) + if socket in self.m_clients: + self.m_clients.pop(socket) + socket.deleteLater() + + # ------- 下面是注册属性的方法 ------- + + @pyqtProperty(int) + def intValue(self): + return self._intValue + + @intValue.setter + def intValue(self, value): + self._intValue = value + + @pyqtProperty(float) + def floatValue(self): + return self._floatValue + + @floatValue.setter + def floatValue(self, value): + self._floatValue = value + + @pyqtProperty(bool) + def boolValue(self): + return self._boolValue + + @boolValue.setter + def boolValue(self, value): + self._boolValue = value + + @pyqtProperty(str) + def strValue(self): + return self._strValue + + @strValue.setter + def strValue(self, value): + self._strValue = value + + # @pyqtProperty(list) + # def listValue(self): + # return self._listValue + + # @listValue.setter + # def listValue(self, value): + # self._listValue = value + + # @pyqtProperty(dict) + # def mapValue(self): + # return self._mapValue + + # @mapValue.setter + # def mapValue(self, value): + # self._mapValue = value + + # ------- 下面是注册函数的方法 ------- + # ------- 如果有返回值一定要注明 result=返回类型 ------- + + @pyqtSlot(int, int, result=int) + def testAdd(self, a, b): + return a + b diff --git a/QWebChannel/Lib/__init__.py b/QWebChannel/Lib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/QWebChannel/README.en.md b/QWebChannel/README.en.md new file mode 100644 index 00000000..e69de29b diff --git a/QWebChannel/README.md b/QWebChannel/README.md new file mode 100644 index 00000000..4bcdb07e --- /dev/null +++ b/QWebChannel/README.md @@ -0,0 +1,13 @@ +# QWebChannel + +- 目录 + - [和Js互相调用](#1和Js互相调用) + +## 1、和Js互相调用 + +[运行 CallEachWithJs.py](CallEachWithJs.py) + +通过`qwebchannel.js`和`QWebChannel.registerObject`通过中间件`WebSocket`进行对象和Javascript的交互(类似于json rpc) +该方法类似与`QWebEngineView`中的例子,同时该demo也适用与nodejs。 + +![CallEachWithJs](ScreenShot/CallEachWithJs.gif) diff --git a/QWebChannel/ScreenShot/CallEachWithJs.gif b/QWebChannel/ScreenShot/CallEachWithJs.gif new file mode 100644 index 00000000..8ff3a88e Binary files /dev/null and b/QWebChannel/ScreenShot/CallEachWithJs.gif differ diff --git a/QWebEngineView/BlockRequest.py b/QWebEngineView/BlockRequest.py index e46744e4..27b86d78 100644 --- a/QWebEngineView/BlockRequest.py +++ b/QWebEngineView/BlockRequest.py @@ -4,19 +4,22 @@ """ Created on 2019年9月24日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com -@file: QWebEngineView.BlockAds +@file: BlockRequest @description: 拦截请求 """ -from PyQt5.QtCore import QUrl -from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' -__Version__ = 'Version 1.0' +try: + from PyQt5.QtCore import QUrl + from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor + from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import QUrl + from PySide2.QtWebEngineCore import QWebEngineUrlRequestInterceptor + from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile + from PySide2.QtWidgets import QApplication class RequestInterceptor(QWebEngineUrlRequestInterceptor): @@ -39,7 +42,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QWebEngineView/BlockRequestData.py b/QWebEngineView/BlockRequestData.py index 00da4fb8..eea7a024 100644 --- a/QWebEngineView/BlockRequestData.py +++ b/QWebEngineView/BlockRequestData.py @@ -4,20 +4,24 @@ """ Created on 2020年2月18日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com -@file: QWebEngineView.BlockRequestData +@file: BlockRequestData @description: 拦截请求内容 """ -from PyQt5.QtCore import QUrl, QFile, QIODevice -from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler,\ - QWebEngineUrlRequestInterceptor, QWebEngineUrlScheme # @UnresolvedImport -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' -__Version__ = 'Version 1.0' +try: + from PyQt5.QtCore import QUrl, QFile, QIODevice, QByteArray + from PyQt5.QtWidgets import QApplication + from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler, \ + QWebEngineUrlRequestInterceptor, QWebEngineUrlScheme # @UnresolvedImport + from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile +except ImportError: + from PySide2.QtCore import QUrl, QFile, QIODevice, QByteArray + from PySide2.QtWidgets import QApplication + from PySide2.QtWebEngineCore import QWebEngineUrlSchemeHandler, \ + QWebEngineUrlRequestInterceptor, QWebEngineUrlScheme # @UnresolvedImport + from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile # 自定义url协议头 @@ -30,6 +34,7 @@ def requestStarted(self, job): file.open(QIODevice.ReadOnly) job.reply(b'image/png', file) + # 请求拦截器 @@ -50,8 +55,8 @@ def __init__(self, *args, **kwargs): self.resize(800, 600) # 首先获取默认的url协议 - h1 = QWebEngineUrlScheme.schemeByName(b'http') - h2 = QWebEngineUrlScheme.schemeByName(b'https') + h1 = QWebEngineUrlScheme.schemeByName(QByteArray(b'http')) + h2 = QWebEngineUrlScheme.schemeByName(QByteArray(b'https')) # 这里需要修改增加本地文件和跨域支持 CorsEnabled = 0x80 # 5.14才增加 @@ -69,12 +74,12 @@ def __init__(self, *args, **kwargs): # 安装url拦截器和自定义url协议处理 de = QWebEngineProfile.defaultProfile() # @UndefinedVariable de.setRequestInterceptor(RequestInterceptor(self)) - de.installUrlSchemeHandler(b'myurl', UrlSchemeHandler(self)) + de.installUrlSchemeHandler(QByteArray(b'myurl'), UrlSchemeHandler(self)) if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QWebEngineView/GetCookie.py b/QWebEngineView/GetCookie.py index 81aa2592..f74eae8b 100644 --- a/QWebEngineView/GetCookie.py +++ b/QWebEngineView/GetCookie.py @@ -1,28 +1,28 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月10日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: GetCookie @description: -''' -import sys - -from PyQt5.QtCore import QUrl, QByteArray -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile -from PyQt5.QtWidgets import QApplication, QTextEdit +""" +import sys -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QUrl, QByteArray + from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile + from PyQt5.QtWidgets import QApplication, QTextEdit +except ImportError: + from PySide2.QtCore import QUrl, QByteArray + from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile + from PySide2.QtWidgets import QApplication, QTextEdit class WebEngineView(QWebEngineView): - DomainCookies = {} # 存放domain的key-value PathCookies = {} # 存放domain+path的key-value @@ -63,9 +63,9 @@ def onLoadFinished(self): "AllPathCookies: " + self.bytestostr(self.getAllPathCookies())) self.cookieView.append('') - print("*****pyqt5.com cookie:", self.getDomainCookies(".pyqt5.com")) - print("*****pyqt5.com / path cookie:", - self.getPathCookies(".pyqt5.com/")) + print("*****pyqt.site cookie:", self.getDomainCookies(".pyqt.site")) + print("*****pyqt.site / path cookie:", + self.getPathCookies(".pyqt.site/")) def getAllDomainCookies(self): return self.DomainCookies @@ -80,9 +80,9 @@ def getPathCookies(self, dpath): return self.PathCookies.get(dpath, {}) def onCookieAdd(self, cookie): - ''' + """ :param cookie: QNetworkCookie - ''' + """ domain = cookie.domain() path = cookie.path() name = cookie.name().data() @@ -98,6 +98,8 @@ def onCookieAdd(self, cookie): _cookie[name] = value else: self.PathCookies[domain_path] = {name: value} + + # print("add cookie:", cookie.domain(), # cookie.path(), cookie.name(), cookie.value()) @@ -106,5 +108,5 @@ def onCookieAdd(self, cookie): app = QApplication(sys.argv) w = WebEngineView() w.show() - w.load(QUrl("https://pyqt5.com/")) + w.load(QUrl("https://pyqt.site/")) sys.exit(app.exec_()) diff --git a/QWebEngineView/GetRequestInfo.py b/QWebEngineView/GetRequestInfo.py new file mode 100644 index 00000000..a86d810a --- /dev/null +++ b/QWebEngineView/GetRequestInfo.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2019年9月24日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: QWebEngineView.BlockAds +@description: 拦截请求 +""" + +try: + from PyQt5.QtCore import QUrl, QByteArray + from PyQt5.QtWidgets import QApplication + from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler, QWebEngineUrlScheme, \ + QWebEngineUrlRequestInterceptor + from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile +except ImportError: + from PySide2.QtCore import QUrl, QByteArray + from PySide2.QtWidgets import QApplication + from PySide2.QtNetwork import QNetworkAccessManager, QNetworkRequest + from PySide2.QtWebEngineCore import QWebEngineUrlSchemeHandler, QWebEngineUrlScheme, \ + QWebEngineUrlRequestInterceptor + from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile + + +class UrlSchemeHandler(QWebEngineUrlSchemeHandler): + AttrType = QNetworkRequest.User + 1 + + def __init__(self, *args, **kwargs): + super(UrlSchemeHandler, self).__init__(*args, **kwargs) + self._manager = QNetworkAccessManager(self) + self._manager.finished.connect(self.onFinished) + + def requestStarted(self, request): + # 拦截 + # request.fail(QWebEngineUrlRequestJob.RequestDenied) + # print('initiator:', request.initiator()) + print('requestMethod:', request.requestMethod()) + print('requestHeaders:', request.requestHeaders()) + url = request.requestUrl() + if url.scheme().startswith('myurl'): + url.setScheme(url.scheme().replace('myurl', 'http')) + print('requestUrl:', url) + + # 构造真实请求 + req = QNetworkRequest(url) + req.setAttribute(self.AttrType, request) # 记录 + for headerName, headerValue in request.requestHeaders().items(): + req.setRawHeader(headerName, headerValue) + method = request.requestMethod() + + # TODO: 这里需要把浏览器内部的cookie获取出来重新设置 + if method == b'GET': + self._manager.get(req) + # TODO: 这里貌似没法得到POST的数据,ajax的请求貌似也有问题 + elif method == b'POST': + self._manager.post(req) + + def onFinished(self, reply): + req = reply.request() # 获取请求 + o_req = req.attribute(self.AttrType, None) + if o_req: + # Notice: 这里可以对数据做修改再返回 + # TODO: 可能还存在 QNetworkAccessManager 与浏览器之间的 cookie 同步问题 + o_req.reply(req.header(QNetworkRequest.ContentTypeHeader) or b'text/html', reply) + o_req.destroyed.connect(reply.deleteLater) + + +# 把所有请求重定向到myurl +class RequestInterceptor(QWebEngineUrlRequestInterceptor): + + def interceptRequest(self, info): + url = info.requestUrl() + if url.scheme() == 'http': + # 重定向 + url.setScheme('myurl') + info.redirect(url) + elif url.scheme() == 'https': + # 重定向 + url.setScheme('myurls') + info.redirect(url) + + +class Window(QWebEngineView): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + self.resize(800, 600) + profile = QWebEngineProfile.defaultProfile() + + # 首先获取默认的url协议 + o_http = QWebEngineUrlScheme.schemeByName(QByteArray(b'http')) + o_https = QWebEngineUrlScheme.schemeByName(QByteArray(b'https')) + print('scheme:', o_http, o_https) + + # 这里需要修改增加本地文件和跨域支持 + CorsEnabled = 0x80 # 5.14才增加 + o_http.setFlags(o_http.flags() | + QWebEngineUrlScheme.SecureScheme | + QWebEngineUrlScheme.LocalScheme | + QWebEngineUrlScheme.LocalAccessAllowed | + CorsEnabled) + o_https.setFlags(o_https.flags() | + QWebEngineUrlScheme.SecureScheme | + QWebEngineUrlScheme.LocalScheme | + QWebEngineUrlScheme.LocalAccessAllowed | + CorsEnabled) + + # 安装url拦截器和自定义url协议处理 + de = QWebEngineProfile.defaultProfile() # @UndefinedVariable + de.setRequestInterceptor(RequestInterceptor(self)) + self.urlSchemeHandler = UrlSchemeHandler(self) + de.installUrlSchemeHandler(QByteArray(b'myurl'), self.urlSchemeHandler) # for http + de.installUrlSchemeHandler(QByteArray(b'myurls'), self.urlSchemeHandler) # for https + + +if __name__ == '__main__': + import sys + import os + import webbrowser + import cgitb + + cgitb.enable(format='text') + + app = QApplication(sys.argv) + # 开启F12 控制台功能,需要单独通过浏览器打开这个页面 + # 这里可以做个保护, 发布软件,启动时把这个环境变量删掉。防止他人通过环境变量开启 + os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = '9966' + # 打开调试页面 + webbrowser.open_new_tab('http://127.0.0.1:9966/') + + w = Window() + w.show() + w.load(QUrl('https://pyqt.site/')) + sys.exit(app.exec_()) diff --git a/QWebEngineView/JsSignals.py b/QWebEngineView/JsSignals.py index 725ea901..aac94019 100644 --- a/QWebEngineView/JsSignals.py +++ b/QWebEngineView/JsSignals.py @@ -4,7 +4,7 @@ """ Created on 2019年4月27日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QWebEngineView.JsSignals @description: @@ -12,18 +12,19 @@ import os from time import time -from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal -from PyQt5.QtWebChannel import QWebChannel -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings -from PyQt5.QtWidgets import QMessageBox, QWidget, QVBoxLayout, QPushButton - - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' +try: + from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal + from PyQt5.QtWebChannel import QWebChannel + from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings + from PyQt5.QtWidgets import QMessageBox, QWidget, QVBoxLayout, QPushButton +except ImportError: + from PySide2.QtCore import QUrl, Slot as pyqtSlot, Signal as pyqtSignal + from PySide2.QtWebChannel import QWebChannel + from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings + from PySide2.QtWidgets import QMessageBox, QWidget, QVBoxLayout, QPushButton class WebEngineView(QWebEngineView): - customSignal = pyqtSignal(str) def __init__(self, *args, **kwargs): @@ -38,13 +39,13 @@ def __init__(self, *args, **kwargs): # START #####以下代码可能是在5.6 QWebEngineView刚出来时的bug,必须在每次加载页面的时候手动注入 #### 也有可能是跳转页面后就失效了,需要手动注入,有没有修复具体未测试 -# self.page().loadStarted.connect(self.onLoadStart) -# self._script = open('Data/qwebchannel.js', 'rb').read().decode() + # self.page().loadStarted.connect(self.onLoadStart) + # self._script = open('Data/qwebchannel.js', 'rb').read().decode() -# def onLoadStart(self): -# self.page().runJavaScript(self._script) + # def onLoadStart(self): + # self.page().runJavaScript(self._script) - # END ########################### + # END ########################### # 注意pyqtSlot用于把该函数暴露给js可以调用 @pyqtSlot(str) @@ -58,16 +59,16 @@ def sendCustomSignal(self): @pyqtSlot(str) @pyqtSlot(QUrl) def load(self, url): - ''' - eg: load("https://pyqt5.com/") + """ + eg: load("https://pyqt.site/") :param url: 网址 - ''' + """ return super(WebEngineView, self).load(QUrl(url)) def initSettings(self): - ''' + """ eg: 初始化设置 - ''' + """ # 获取浏览器默认设置 settings = QWebEngineSettings.globalSettings() # 设置默认编码utf8 @@ -133,9 +134,10 @@ def __init__(self, *args, **kwargs): os.path.abspath('Data/JsSignals.html'))) -if __name__ == "__main__": +if __name__ == '__main__': from PyQt5.QtWidgets import QApplication import sys + # 开启F12 控制台功能,需要单独通过浏览器打开这个页面 # 这里可以做个保护, 发布软件,启动时把这个环境变量删掉。防止他人通过环境变量开启 os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = '9966' diff --git a/QWebEngineView/README.md b/QWebEngineView/README.md index 533876a3..4a26699a 100644 --- a/QWebEngineView/README.md +++ b/QWebEngineView/README.md @@ -7,8 +7,10 @@ - [同网站不同用户](#4同网站不同用户) - [拦截请求](#5拦截请求) - [拦截请求内容](#6拦截请求内容) + - [设置Cookie](#7设置Cookie) ## 1、获取Cookie + [运行 GetCookie.py](GetCookie.py) 通过`QWebEngineProfile`中得到的`cookieStore`并绑定它的`cookieAdded`信号来得到Cookie @@ -16,6 +18,7 @@ ![GetCookie](ScreenShot/GetCookie.png) ## 2、和Js交互操作 + [运行 JsSignals.py](JsSignals.py) 通过`qwebchannel.js`和`QWebChannel.registerObject`进行Python对象和Javascript的交互 @@ -25,6 +28,7 @@ ![JsSignals](ScreenShot/JsSignals.gif) ## 3、网页整体截图 + [运行 ScreenShotPage.py](ScreenShotPage.py) 1. 方式1:目前通过不完美方法(先调整`QWebEngineView`的大小为`QWebEnginePage`的内容大小,等待一定时间后截图再还原大小) @@ -33,6 +37,7 @@ ![ScreenShotPage](ScreenShot/ScreenShotPage.gif) ## 4、同网站不同用户 + [运行 SiteDiffUser.py](SiteDiffUser.py) 原理是为每个`QWebEngineView`创建一个`QWebEnginePage`,且使用独立的`QWebEngineProfile`,并配置`persistentStoragePath`不同路径 @@ -40,6 +45,7 @@ ![SiteDiffUser](ScreenShot/SiteDiffUser.gif) ## 5、拦截请求 + [运行 BlockRequest.py](BlockRequest.py) 通过`QWebEngineUrlRequestInterceptor`中的`interceptRequest`方法对每个请求做拦截过滤 @@ -47,9 +53,19 @@ ![BlockRequest](ScreenShot/BlockRequest.gif) ## 6、拦截请求内容 + [运行 BlockRequestData.py](BlockRequestData.py) 这里用了一个投巧的办法,原理是先通过`QWebEngineUrlRequestInterceptor`中的`interceptRequest`方法对每个请求做拦截过滤, 发现目标url后重定向到`QWebEngineUrlSchemeHandler`实现的自定义协议头返回数据 -![BlockRequestData](ScreenShot/BlockRequestData.png) \ No newline at end of file +![BlockRequestData](ScreenShot/BlockRequestData.png) + +## 7、设置Cookie + +[运行 SetCookies.py](SetCookies.py) + +通过`QWebEngineProfile`中得到的`cookieStore`来添加`QNetworkCookie`对象实现, +需要注意的是httpOnly=true时,通过js无法获取 + +![SetCookies](ScreenShot/SetCookies.png) diff --git a/QWebEngineView/ScreenShot/SetCookies.png b/QWebEngineView/ScreenShot/SetCookies.png new file mode 100644 index 00000000..01c98c1d Binary files /dev/null and b/QWebEngineView/ScreenShot/SetCookies.png differ diff --git a/QWebEngineView/ScreenShotPage.py b/QWebEngineView/ScreenShotPage.py index ad828219..bef64383 100644 --- a/QWebEngineView/ScreenShotPage.py +++ b/QWebEngineView/ScreenShotPage.py @@ -4,7 +4,7 @@ """ Created on 2019年7月8日 @author: Irony -@site: https://pyqt5.com https://github.com/PyQt5 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ScreenShotPage @description: 网页整体截图 @@ -14,18 +14,22 @@ import os import sys -from PyQt5.QtCore import QUrl, Qt, pyqtSlot, QSize, QTimer -from PyQt5.QtGui import QImage, QPainter, QIcon, QPixmap -from PyQt5.QtWebChannel import QWebChannel -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings -from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QPushButton,\ - QGroupBox, QLineEdit, QHBoxLayout, QListWidget, QListWidgetItem,\ - QProgressDialog - - -__Author__ = "Irony" -__Copyright__ = "Copyright (c) 2019" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QUrl, Qt, pyqtSlot, QSize, QTimer, QPoint + from PyQt5.QtGui import QImage, QPainter, QIcon, QPixmap + from PyQt5.QtWebChannel import QWebChannel + from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings + from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QPushButton, \ + QGroupBox, QLineEdit, QHBoxLayout, QListWidget, QListWidgetItem, \ + QProgressDialog +except ImportError: + from PySide2.QtCore import QUrl, Qt, Slot as pyqtSlot, QSize, QTimer, QPoint + from PySide2.QtGui import QImage, QPainter, QIcon, QPixmap + from PySide2.QtWebChannel import QWebChannel + from PySide2.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings + from PySide2.QtWidgets import QWidget, QApplication, QVBoxLayout, QPushButton, \ + QGroupBox, QLineEdit, QHBoxLayout, QListWidget, QListWidgetItem, \ + QProgressDialog # 对部分内容进行截图 CODE = """ @@ -116,8 +120,8 @@ def onLoadFinished(self, finished): open('Data/qwebchannel.js', 'rb').read().decode()) page.runJavaScript( open('Data/jquery.js', 'rb').read().decode()) -# page.runJavaScript( -# open('Data/promise-7.0.4.min.js', 'rb').read().decode()) + # page.runJavaScript( + # open('Data/promise-7.0.4.min.js', 'rb').read().decode()) page.runJavaScript( open('Data/html2canvas.min.js', 'rb').read().decode()) page.runJavaScript(CreateBridge) @@ -143,7 +147,7 @@ def doScreenShot(): painter.setRenderHint(QPainter.TextAntialiasing, True) painter.setRenderHint(QPainter.SmoothPixmapTransform, True) - self.webView.render(painter) + self.webView.render(painter, QPoint()) painter.end() self.webView.resize(oldSize) @@ -181,11 +185,11 @@ def saveImage(self, image): item.setData(Qt.UserRole + 1, image) -if __name__ == "__main__": +if __name__ == '__main__': # 开启F12 控制台功能,需要单独通过浏览器打开这个页面 # 这里可以做个保护, 发布软件,启动时把这个环境变量删掉。防止他人通过环境变量开启 os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = '9966' - sys.excepthook = cgitb.enable(1, None, 5, '') + cgitb.enable(format='text') app = QApplication(sys.argv) w = Window() w.show() diff --git a/QWebEngineView/SetCookies.py b/QWebEngineView/SetCookies.py new file mode 100644 index 00000000..8a858378 --- /dev/null +++ b/QWebEngineView/SetCookies.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Created on 2022/05/12 +@author: Irony +@site: https://pyqt.site https://github.com/PyQt5 +@email: 892768447@qq.com +@file: SetCookies.py +@description: 主动设置Cookie +""" + +try: + from PyQt5.QtCore import QDateTime, Qt, QUrl + from PyQt5.QtWebEngineWidgets import QWebEngineProfile, QWebEngineView + from PyQt5.QtWidgets import QApplication + from PyQt5.QtNetwork import QNetworkCookie +except ImportError: + from PySide2.QtCore import QDateTime, Qt, QUrl + from PySide2.QtWebEngineWidgets import QWebEngineProfile, QWebEngineView + from PySide2.QtWidgets import QApplication + from PySide2.QtNetwork import QNetworkCookie + +cookies = [{ + "domain": "pyqt.site", + "expirationDate": 1714906174.734258, + "hostOnly": True, + "httpOnly": False, + "name": "snexid", + "path": "/", + "sameSite": None, + "secure": False, + "session": False, + "storeId": None, + "value": "1-22-333-4444-55555" +}, { + "domain": "pyqt.site", + "expirationDate": 1714906174.734258, + "hostOnly": True, + "httpOnly": True, + "name": "testonly", + "path": "/", + "secure": True, + "value": "testonly" +}, { + "domain": "pyqt.site", + "hostOnly": True, + "httpOnly": False, + "name": "test", + "path": "/", + "secure": False, + "value": "test" +}] + + +class Window(QWebEngineView): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + # global + # self.cookieStore = QWebEngineProfile.defaultProfile().cookieStore() + + # current + self.cookieStore = self.page().profile().cookieStore() + self.initCookies() + self.loadProgress.connect(self.onLoadProgress) + self.load(QUrl('https://pyqt.site/')) + + def onLoadProgress(self, progress): + if progress == 100: + # 测试获取cookie + self.page().runJavaScript('alert(document.cookie);') + + def initCookies(self): + for cookie in cookies: + qcookie = QNetworkCookie() + qcookie.setName(cookie.get('name', '').encode()) + qcookie.setValue(cookie.get('value', '').encode()) + qcookie.setDomain(cookie.get('domain', '')) + qcookie.setPath(cookie.get('path', '')) + qcookie.setExpirationDate( + QDateTime.fromString(str(cookie.get('expirationDate', 0)), + Qt.ISODate)) + qcookie.setHttpOnly(cookie.get('httpOnly', False)) + qcookie.setSecure(cookie.get('secure', False)) + # 注意可以设置具体的url + self.cookieStore.setCookie(qcookie, QUrl()) + + +if __name__ == '__main__': + import cgitb + import sys + + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QWebEngineView/SiteDiffUser.py b/QWebEngineView/SiteDiffUser.py index 89d096a0..3b30b642 100644 --- a/QWebEngineView/SiteDiffUser.py +++ b/QWebEngineView/SiteDiffUser.py @@ -1,23 +1,26 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - """ Created on 2019年8月23日 @author: Irony -@site: https://pyqt5.com https://github.com/PyQt5 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QWebEngineView.SiteDiffUser @description: 同个网站不同用户 """ -from PyQt5.QtCore import QUrl -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage,\ - QWebEngineProfile -from PyQt5.QtWidgets import QTabWidget +import os -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' -__Version__ = 'Version 1.0' +try: + from PyQt5.QtCore import QUrl + from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineProfile, + QWebEngineView) + from PyQt5.QtWidgets import QApplication, QTabWidget +except ImportError: + from PySide2.QtCore import QUrl + from PySide2.QtWebEngineWidgets import (QWebEnginePage, QWebEngineProfile, + QWebEngineView) + from PySide2.QtWidgets import QApplication, QTabWidget class Window(QTabWidget): @@ -28,7 +31,9 @@ def __init__(self, *args, **kwargs): # 用户1 self.webView1 = QWebEngineView(self) profile1 = QWebEngineProfile('storage1', self.webView1) - profile1.setPersistentStoragePath('Tmp/Storage1') + # 要设置绝对路径,否则会出现部分问题 + # 比如 reCaptcha 不能加载,见 https://github.com/PyQt5/PyQt/issues/125 + profile1.setPersistentStoragePath(os.path.abspath('Tmp/Storage1')) print(profile1.cookieStore()) # 如果要清除cookie # cookieStore = profile1.cookieStore() @@ -41,7 +46,7 @@ def __init__(self, *args, **kwargs): # 用户2 self.webView2 = QWebEngineView(self) profile2 = QWebEngineProfile('storage2', self.webView2) - profile2.setPersistentStoragePath('Tmp/Storage2') + profile2.setPersistentStoragePath(os.path.abspath('Tmp/Storage2')) print(profile2.cookieStore()) page2 = QWebEnginePage(profile2, self.webView2) self.webView2.setPage(page2) @@ -53,7 +58,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QWebView/BlockRequest.py b/QWebView/BlockRequest.py index 3db7d0d1..dd50c6ec 100644 --- a/QWebView/BlockRequest.py +++ b/QWebView/BlockRequest.py @@ -4,21 +4,16 @@ """ Created on 2019年9月24日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QWebView.BlockAds @description: 拦截请求 """ -from PyQt5.QtCore import QUrl, QBuffer, QByteArray +from PyQt5.QtCore import QUrl, QBuffer from PyQt5.QtNetwork import QNetworkAccessManager from PyQt5.QtWebKitWidgets import QWebView -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' -__Version__ = 'Version 1.0' - - class RequestInterceptor(QNetworkAccessManager): def createRequest(self, op, originalReq, outgoingData): @@ -55,6 +50,7 @@ def __init__(self, *args, **kwargs): if __name__ == '__main__': import sys from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QWebView/DreamTree.py b/QWebView/DreamTree.py index 8263b01a..071e2f1e 100644 --- a/QWebView/DreamTree.py +++ b/QWebView/DreamTree.py @@ -1,14 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年4月6日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: DreamTree @description: -''' +""" import sys @@ -17,12 +17,8 @@ from PyQt5.QtWebKitWidgets import QWebView from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout -from Lib import data_rc # @UnusedImport @UnresolvedImport # from PyQt5.QtWebKit import QWebSettings -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" # 要实现透明的webview,需要先用一个QWidget作为父控件 diff --git a/QWebView/GetCookie.py b/QWebView/GetCookie.py index 090b4f6e..1420837e 100644 --- a/QWebView/GetCookie.py +++ b/QWebView/GetCookie.py @@ -1,14 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月10日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: GetCookie @description: -''' +""" import cgitb import sys @@ -17,11 +17,6 @@ from PyQt5.QtWidgets import QApplication, QTextEdit -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - - class WebView(QWebView): def __init__(self, *args, **kwargs): @@ -52,7 +47,7 @@ def onLoadFinished(self): allCookies = self.page().networkAccessManager().cookieJar().allCookies() print("allCookies:", allCookies) for cookie in allCookies: - # if cookie.domain() == ".pyqt5.com": + # if cookie.domain() == ".pyqt.site": self.cookieView.append( "domain: " + self.bytestostr(cookie.domain())) self.cookieView.append("path: " + self.bytestostr(cookie.path())) @@ -68,9 +63,9 @@ def onLoadFinished(self): if __name__ == "__main__": - sys.excepthook = cgitb.enable(1, None, 5, '') + cgitb.enable(format='text') app = QApplication(sys.argv) w = WebView() w.show() - w.load(QUrl("https://pyqt5.com/")) + w.load(QUrl("https://pyqt.site/")) sys.exit(app.exec_()) diff --git a/QWebView/JsSignals.py b/QWebView/JsSignals.py index 878e61b6..6a3d7a55 100644 --- a/QWebView/JsSignals.py +++ b/QWebView/JsSignals.py @@ -4,7 +4,7 @@ """ Created on 2019年4月27日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QWebEngineView.JsSignals @description: @@ -18,12 +18,7 @@ from PyQt5.QtWidgets import QMessageBox, QWidget, QVBoxLayout, QPushButton -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' - - class WebView(QWebView): - customSignal = pyqtSignal(str) def __init__(self, *args, **kwargs): @@ -50,7 +45,7 @@ def sendCustomSignal(self): @pyqtSlot(QUrl) def load(self, url): ''' - eg: load("https://pyqt5.com/") + eg: load("https://pyqt.site/") :param url: 网址 ''' return super(WebView, self).load(QUrl(url)) @@ -85,6 +80,7 @@ def __init__(self, *args, **kwargs): if __name__ == "__main__": from PyQt5.QtWidgets import QApplication import sys + app = QApplication(sys.argv) w = Window() w.show() diff --git a/QWebView/Lib/data_rc.py b/QWebView/Lib/data_rc.py index 59b9117a..17c0cc24 100644 --- a/QWebView/Lib/data_rc.py +++ b/QWebView/Lib/data_rc.py @@ -6216,10 +6216,13 @@ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ " + def qInitResources(): QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + def qCleanupResources(): QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + qInitResources() diff --git a/QWebView/PlayFlash.py b/QWebView/PlayFlash.py index dc992f57..15da61d0 100644 --- a/QWebView/PlayFlash.py +++ b/QWebView/PlayFlash.py @@ -4,7 +4,7 @@ """ Created on 2019年9月18日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QWebView.PlayFlash @description: 播放Flash @@ -19,11 +19,6 @@ from PyQt5.QtWidgets import QApplication -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 - - class Window(QWebView): def __init__(self, *args, **kwargs): @@ -53,5 +48,6 @@ def handleSslErrors(self, reply, errors): app = QApplication(sys.argv) w = Window() w.show() - w.load(QUrl('https://www.17sucai.com/preview/8825/2013-07-07/%E4%B8%80%E6%AC%BE%E4%BA%BA%E5%BD%A2%E5%8A%A8%E4%BD%9C%E6%98%BE%E7%A4%BA%E7%9A%84flash%E6%97%B6%E9%97%B4/index.html')) + w.load(QUrl( + 'https://www.17sucai.com/preview/8825/2013-07-07/%E4%B8%80%E6%AC%BE%E4%BA%BA%E5%BD%A2%E5%8A%A8%E4%BD%9C%E6%98%BE%E7%A4%BA%E7%9A%84flash%E6%97%B6%E9%97%B4/index.html')) sys.exit(app.exec_()) diff --git a/QWebView/README.md b/QWebView/README.md index 557b5faa..f9b509e2 100644 --- a/QWebView/README.md +++ b/QWebView/README.md @@ -9,6 +9,7 @@ - [拦截请求](#6拦截请求) ## 1、梦幻树 + [运行 DreamTree.py](DreamTree.py) 在桌面上显示透明html效果,使用`QWebkit`加载html实现,采用窗口背景透明和穿透方式 @@ -16,6 +17,7 @@ ![DreamTree](ScreenShot/DreamTree.png) ## 2、获取Cookie + [运行 GetCookie.py](GetCookie.py) 从`page()`中得到`QNetworkAccessManager`,在从中得到`QNetworkCookieJar`, @@ -24,6 +26,7 @@ ![GetCookie](ScreenShot/GetCookie.png) ## 3、和Js交互操作 + [运行 JsSignals.py](JsSignals.py) 通过`QWebFrame`的`addToJavaScriptWindowObject`函数提供进行Python对象和Javascript的交互 @@ -33,6 +36,7 @@ ![JsSignals](ScreenShot/JsSignals.gif) ## 4、网页整体截图 + [运行 ScreenShotPage.py](ScreenShotPage.py) 1. 方式1:原理是通过`QWebView.QWebPage.QWebFrame`得到内容的高度,然后设置`QWebPage.setViewportSize`的大小, @@ -42,6 +46,7 @@ ![ScreenShotPage](ScreenShot/ScreenShotPage.gif) ## 5、播放Flash + [运行 PlayFlash.py](PlayFlash.py) 1. 重点在于设置 `os.environ['QTWEBKIT_PLUGIN_PATH'] = os.path.abspath('Data')` ,非常重要,设置为NPSWF32.dll文件所在目录 @@ -50,8 +55,9 @@ ![PlayFlash](ScreenShot/PlayFlash.gif) ## 6、拦截请求 + [运行 BlockRequest.py](BlockRequest.py) 通过`QNetworkAccessManager`中的`createRequest`方法对每个请求做拦截过滤 -![BlockRequest](ScreenShot/BlockRequest.png) \ No newline at end of file +![BlockRequest](ScreenShot/BlockRequest.png) diff --git a/QWebView/ScreenShotPage.py b/QWebView/ScreenShotPage.py index 5569a545..6a00fc9a 100644 --- a/QWebView/ScreenShotPage.py +++ b/QWebView/ScreenShotPage.py @@ -4,7 +4,7 @@ """ Created on 2019年7月8日 @author: Irony -@site: https://pyqt5.com https://github.com/PyQt5 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ScreenShotPage @description: 网页整体截图 @@ -17,15 +17,10 @@ from PyQt5.QtGui import QImage, QPainter, QIcon, QPixmap from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebView -from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QPushButton,\ - QGroupBox, QLineEdit, QHBoxLayout, QListWidget, QListWidgetItem,\ +from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QPushButton, \ + QGroupBox, QLineEdit, QHBoxLayout, QListWidget, QListWidgetItem, \ QProgressDialog - -__Author__ = "Irony" -__Copyright__ = "Copyright (c) 2019" -__Version__ = "Version 1.0" - # 对部分内容进行截图 CODE = """ var el = $("%s"); @@ -80,7 +75,7 @@ def __init__(self, *args, **kwargs): QWebSettings.DeveloperExtrasEnabled, True) self.webView.loadStarted.connect(self.onLoadStarted) self.webView.loadFinished.connect(self.onLoadFinished) - self.webView.load(QUrl("https://pyqt5.com/")) + self.webView.load(QUrl("https://pyqt.site/")) # 暴露接口和加载完成后执行jquery等一些库文件 self.webView.page().mainFrame().javaScriptWindowObjectCleared.connect( @@ -165,7 +160,7 @@ def saveImage(self, image): if __name__ == "__main__": - sys.excepthook = cgitb.enable(1, None, 5, '') + cgitb.enable(format='text') app = QApplication(sys.argv) w = Window() w.show() diff --git a/QWidget/Lib/CustomPaintWidget.py b/QWidget/Lib/CustomPaintWidget.py index 4d5d4ed6..f006d0a6 100644 --- a/QWidget/Lib/CustomPaintWidget.py +++ b/QWidget/Lib/CustomPaintWidget.py @@ -1,21 +1,21 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月10日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CustomPaintWidget @description: -''' -from PyQt5.QtGui import QPainter -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QStyleOption, QStyle +""" - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtGui import QPainter + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QStyleOption, QStyle +except ImportError: + from PySide2.QtGui import QPainter + from PySide2.QtWidgets import QWidget, QVBoxLayout, QLabel, QStyleOption, QStyle class CustomPaintWidget(QWidget): diff --git a/QWidget/Lib/CustomWidget.py b/QWidget/Lib/CustomWidget.py index 11edcfb3..0231fc94 100644 --- a/QWidget/Lib/CustomWidget.py +++ b/QWidget/Lib/CustomWidget.py @@ -1,20 +1,19 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月10日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CustomWidget @description: -''' -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel +""" - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel +except ImportError: + from PySide2.QtWidgets import QWidget, QVBoxLayout, QLabel class CustomWidget(QWidget): @@ -22,4 +21,4 @@ class CustomWidget(QWidget): def __init__(self, *args, **kwargs): super(CustomWidget, self).__init__(*args, **kwargs) layout = QVBoxLayout(self) - layout.addWidget(QLabel("我是自定义CustomWidget", self)) \ No newline at end of file + layout.addWidget(QLabel("我是自定义CustomWidget", self)) diff --git a/QWidget/README.md b/QWidget/README.md index d5b6c1c9..0d86518e 100644 --- a/QWidget/README.md +++ b/QWidget/README.md @@ -4,9 +4,10 @@ - [样式表测试](#1样式表测试) ## 1、样式表测试 + [运行 WidgetStyle.py](WidgetStyle.py) 1. 一种是重写 `paintEvent` 2. 设置 `Qt.WA_StyledBackground` 后可以通过QSS增加背景等 -![WidgetStyle](ScreenShot/WidgetStyle.png) \ No newline at end of file +![WidgetStyle](ScreenShot/WidgetStyle.png) diff --git a/QWidget/WidgetStyle.py b/QWidget/WidgetStyle.py index 945407c6..842a29a6 100644 --- a/QWidget/WidgetStyle.py +++ b/QWidget/WidgetStyle.py @@ -1,28 +1,28 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月10日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: test @description: -''' +""" + import sys -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout +try: + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QWidget, QApplication, QHBoxLayout from Lib.CustomPaintWidget import CustomPaintWidget # @UnresolvedImport from Lib.CustomWidget import CustomWidget # @UnresolvedImport -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - - class Window(QWidget): def __init__(self, *args, **kwargs): @@ -31,9 +31,9 @@ def __init__(self, *args, **kwargs): layout.addWidget(CustomPaintWidget(self)) layout.addWidget(CustomWidget(self)) # 注意 - w = CustomWidget(self) - w.setAttribute(Qt.WA_StyledBackground) # 很重要 - layout.addWidget(w) + wc = CustomWidget(self) + wc.setAttribute(Qt.WA_StyledBackground) # 很重要 + layout.addWidget(wc) if __name__ == "__main__": diff --git a/QtChart/AreaChart.py b/QtChart/AreaChart.py index 9e2d022f..80b88682 100644 --- a/QtChart/AreaChart.py +++ b/QtChart/AreaChart.py @@ -4,14 +4,27 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: AreaChart @description: 区域图表 """ -from PyQt5.QtChart import QChartView, QChart, QLineSeries, QAreaSeries -from PyQt5.QtCore import QPointF -from PyQt5.QtGui import QColor, QGradient, QLinearGradient, QPainter, QPen + +try: + from PyQt5.QtChart import QChartView, QChart, QLineSeries, QAreaSeries + from PyQt5.QtCore import QPointF + from PyQt5.QtGui import QColor, QGradient, QLinearGradient, QPainter, QPen + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import QPointF + from PySide2.QtGui import QColor, QGradient, QLinearGradient, QPainter, QPen + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QLineSeries = QtCharts.QLineSeries + QAreaSeries = QtCharts.QAreaSeries class Window(QChartView): @@ -42,9 +55,9 @@ def getSeries(self): # 添加数据 series0 << QPointF(1, 5) << QPointF(3, 7) << QPointF(7, 6) << QPointF(9, 7) \ - << QPointF(12, 6) << QPointF(16, 7) << QPointF(18, 5) + << QPointF(12, 6) << QPointF(16, 7) << QPointF(18, 5) series1 << QPointF(1, 3) << QPointF(3, 4) << QPointF(7, 3) << QPointF(8, 2) \ - << QPointF(12, 3) << QPointF(16, 4) << QPointF(18, 3) + << QPointF(12, 3) << QPointF(16, 4) << QPointF(18, 3) # 创建区域图 series = QAreaSeries(series0, series1) @@ -67,7 +80,6 @@ def getSeries(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = Window() diff --git a/QtChart/BarChart.py b/QtChart/BarChart.py index 73bd762b..fadcbdb0 100644 --- a/QtChart/BarChart.py +++ b/QtChart/BarChart.py @@ -4,14 +4,28 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: BarChart @description: 柱状图表 """ -from PyQt5.QtChart import QChartView, QChart, QBarSet, QBarSeries, QBarCategoryAxis -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPainter + +try: + from PyQt5.QtChart import QChartView, QChart, QBarSet, QBarSeries, QBarCategoryAxis + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPainter + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPainter + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QBarSet = QtCharts.QBarSet + QBarSeries = QtCharts.QBarSeries + QBarCategoryAxis = QtCharts.QBarCategoryAxis class Window(QChartView): @@ -72,7 +86,6 @@ def getSeries(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = Window() diff --git a/QtChart/BarStack.py b/QtChart/BarStack.py index 6267f422..f0ea25cc 100644 --- a/QtChart/BarStack.py +++ b/QtChart/BarStack.py @@ -1,27 +1,36 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月28日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: charts.bar.BarStack @description: like http://echarts.baidu.com/demo.html#bar-stack -''' +""" import sys from random import randint -from PyQt5.QtChart import QChartView, QChart, QBarSeries, QBarSet, QBarCategoryAxis -from PyQt5.QtCore import Qt, QPointF, QRectF, QPoint -from PyQt5.QtGui import QPainter, QPen -from PyQt5.QtWidgets import QApplication, QGraphicsLineItem, QWidget, \ - QHBoxLayout, QLabel, QVBoxLayout, QGraphicsProxyWidget - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtChart import QChartView, QChart, QBarSeries, QBarSet, QBarCategoryAxis + from PyQt5.QtCore import Qt, QPointF, QRectF, QPoint + from PyQt5.QtGui import QPainter, QPen + from PyQt5.QtWidgets import QApplication, QGraphicsLineItem, QWidget, \ + QHBoxLayout, QLabel, QVBoxLayout, QGraphicsProxyWidget +except ImportError: + from PySide2.QtCore import Qt, QPointF, QRectF, QPoint + from PySide2.QtGui import QPainter, QPen + from PySide2.QtWidgets import QApplication, QGraphicsLineItem, QWidget, \ + QHBoxLayout, QLabel, QVBoxLayout, QGraphicsProxyWidget + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QBarSeries = QtCharts.QBarSeries + QBarSet = QtCharts.QBarSet + QBarCategoryAxis = QtCharts.QBarCategoryAxis class ToolTipItem(QWidget): @@ -44,7 +53,6 @@ def setText(self, text): class ToolTipWidget(QWidget): - Cache = {} def __init__(self, *args, **kwargs): @@ -134,7 +142,7 @@ def mouseMoveEvent(self, event): serie = self._chart.series()[0] bars = [(bar, bar.at(index)) for bar in serie.barSets() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] -# print(bars) + # print(bars) if bars: right_top = self._chart.mapToPosition( QPointF(self.max_x, self.max_y)) @@ -153,10 +161,10 @@ def mouseMoveEvent(self, event): t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ - pos.x() - 20 < t_width else pos.x() + pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ - pos.y() - 20 < t_height else pos.y() + pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show( title, bars, QPoint(x, y)) else: diff --git a/QtChart/ChartThemes.py b/QtChart/ChartThemes.py index 38704012..059e7f15 100644 --- a/QtChart/ChartThemes.py +++ b/QtChart/ChartThemes.py @@ -4,7 +4,7 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ChartThemes @description: 图表主题动画等 @@ -29,13 +29,30 @@ import random -from PyQt5.QtChart import (QAreaSeries, QBarSet, QChart, QChartView, - QLineSeries, QPieSeries, QScatterSeries, QSplineSeries, - QStackedBarSeries) -from PyQt5.QtCore import pyqtSlot, QPointF, Qt -from PyQt5.QtGui import QColor, QPainter, QPalette -from PyQt5.QtWidgets import (QCheckBox, QComboBox, QGridLayout, QHBoxLayout, - QLabel, QSizePolicy, QWidget) +try: + from PyQt5.QtChart import (QAreaSeries, QBarSet, QChart, QChartView, + QLineSeries, QPieSeries, QScatterSeries, QSplineSeries, + QStackedBarSeries) + from PyQt5.QtCore import pyqtSlot, QPointF, Qt + from PyQt5.QtGui import QColor, QPainter, QPalette + from PyQt5.QtWidgets import QApplication, QMainWindow, QCheckBox, QComboBox, QGridLayout, QHBoxLayout, \ + QLabel, QSizePolicy, QWidget +except ImportError: + from PySide2.QtCore import Slot as pyqtSlot, QPointF, Qt + from PySide2.QtGui import QColor, QPainter, QPalette + from PySide2.QtWidgets import QApplication, QMainWindow, QCheckBox, QComboBox, QGridLayout, QHBoxLayout, \ + QLabel, QSizePolicy, QWidget + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QAreaSeries = QtCharts.QAreaSeries + QBarSet = QtCharts.QBarSet + QLineSeries = QtCharts.QLineSeries + QPieSeries = QtCharts.QPieSeries + QScatterSeries = QtCharts.QScatterSeries + QSplineSeries = QtCharts.QSplineSeries + QStackedBarSeries = QtCharts.QStackedBarSeries class ThemeWidget(QWidget): @@ -238,7 +255,7 @@ def createPieChart(self): series = QPieSeries(chart) for value, label in data_list: slice = series.append(label, value.y()) - if len(series) == 1: + if series.count() == 1: slice.setLabelVisible() slice.setExploded() @@ -290,7 +307,7 @@ def updateUI(self): if self.m_charts[0].chart().theme() != theme: for chartView in self.m_charts: - chartView.chart().setTheme(theme) + chartView.chart().setTheme(QChart.ChartTheme(theme)) pal = self.window().palette() @@ -349,8 +366,6 @@ def updateUI(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication, QMainWindow - app = QApplication(sys.argv) window = QMainWindow() diff --git a/QtChart/CpuLineChart.py b/QtChart/CpuLineChart.py new file mode 100644 index 00000000..17b521e5 --- /dev/null +++ b/QtChart/CpuLineChart.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/5/13 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: CpuLineChart +@description: +""" + +import sys + +from PyQt5.QtChart import QChartView, QChart, QSplineSeries, QDateTimeAxis, QValueAxis +from PyQt5.QtCore import Qt, QTimer, QDateTime, QPointF +from PyQt5.QtGui import QPainter, QPen, QColor +from PyQt5.QtWidgets import QApplication +from psutil import cpu_percent + + +class CpuLineChart(QChart): + + def __init__(self, *args, **kwargs): + super(CpuLineChart, self).__init__(*args, **kwargs) + self.m_count = 10 + # 隐藏图例 + self.legend().hide() + self.m_series = QSplineSeries(self) + # 设置画笔 + self.m_series.setPen(QPen(QColor('#3B8CFF'), 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) + self.addSeries(self.m_series) + # x轴 + self.m_axisX = QDateTimeAxis(self) + self.m_axisX.setTickCount(self.m_count + 1) # 设置刻度数量 + self.m_axisX.setFormat('hh:mm:ss') # 设置时间显示格式 + now = QDateTime.currentDateTime() # 前10秒到现在 + self.m_axisX.setRange(now.addSecs(-self.m_count), now) + self.addAxis(self.m_axisX, Qt.AlignBottom) + self.m_series.attachAxis(self.m_axisX) + # y轴 + self.m_axisY = QValueAxis(self) + self.m_axisY.setLabelFormat('%d') # 设置文本格式 + self.m_axisY.setMinorTickCount(4) # 设置小刻度线的数目 + self.m_axisY.setTickCount(self.m_count + 1) + self.m_axisY.setRange(0, 100) + self.addAxis(self.m_axisY, Qt.AlignLeft) + self.m_series.attachAxis(self.m_axisY) + + # 填充11个初始点,注意x轴 需要转为秒的时间戳 + self.m_series.append( + [QPointF(now.addSecs(-i).toMSecsSinceEpoch(), 0) for i in range(self.m_count, -1, -1)]) + + # 定时器获取数据 + self.m_timer = QTimer() + self.m_timer.timeout.connect(self.update_data) + self.m_timer.start(1000) + + def update_data(self): + value = cpu_percent() + now = QDateTime.currentDateTime() + self.m_axisX.setRange(now.addSecs(-self.m_count), now) # 重新调整x轴的时间范围 + # 获取原来的所有点,去掉第一个并追加新的一个 + points = self.m_series.pointsVector() + points.pop(0) + points.append(QPointF(now.toMSecsSinceEpoch(), value)) + # 替换法速度更快 + self.m_series.replace(points) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + chart = CpuLineChart() + chart.setTitle('cpu') + # chart.setAnimationOptions(QChart.SeriesAnimations) + + view = QChartView(chart) + view.setRenderHint(QPainter.Antialiasing) # 抗锯齿 + view.resize(800, 600) + view.show() + sys.exit(app.exec_()) diff --git a/QtChart/CustomXYaxis.py b/QtChart/CustomXYaxis.py index 9ab84fbe..014ab6bc 100644 --- a/QtChart/CustomXYaxis.py +++ b/QtChart/CustomXYaxis.py @@ -1,22 +1,30 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月19日 -@author: Irony."[讽刺] -@site: http://alyl.vip, http://orzorz.vip, http://coding.net/u/892768447, http://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: CustomXYaxis @description: -''' +""" import random import sys -from PyQt5.QtChart import QChartView, QLineSeries, QChart, QCategoryAxis -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout - -__version__ = "0.0.1" +try: + from PyQt5.QtChart import QChartView, QLineSeries, QChart, QCategoryAxis + from PyQt5.QtCore import Qt + from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QApplication, QWidget, QHBoxLayout + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QLineSeries = QtCharts.QLineSeries + QCategoryAxis = QtCharts.QCategoryAxis m_listCount = 3 m_valueMax = 10 diff --git a/QtChart/DynamicSpline.py b/QtChart/DynamicSpline.py index 3ed3b85a..b6bed0e0 100644 --- a/QtChart/DynamicSpline.py +++ b/QtChart/DynamicSpline.py @@ -10,12 +10,21 @@ """ import sys -from PyQt5.QtChart import QChartView, QChart, QSplineSeries, QValueAxis -from PyQt5.QtCore import Qt, QTimer, QRandomGenerator -from PyQt5.QtGui import QPainter, QPen -from PyQt5.QtWidgets import QApplication +try: + from PyQt5.QtChart import QChartView, QChart, QSplineSeries, QValueAxis + from PyQt5.QtCore import Qt, QTimer, QRandomGenerator + from PyQt5.QtGui import QPainter, QPen + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import Qt, QTimer, QRandomGenerator + from PySide2.QtGui import QPainter, QPen + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts -__version__ = "0.0.1" + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QSplineSeries = QtCharts.QSplineSeries + QValueAxis = QtCharts.QValueAxis class DynamicSpline(QChart): diff --git a/QtChart/HorizontalBarChart.py b/QtChart/HorizontalBarChart.py index 1f4000da..6018cefb 100644 --- a/QtChart/HorizontalBarChart.py +++ b/QtChart/HorizontalBarChart.py @@ -4,14 +4,28 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: HorizontalBarChart @description: 横向柱状图表 """ -from PyQt5.QtChart import QChartView, QChart, QBarSet, QHorizontalBarSeries, QBarCategoryAxis -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPainter + +try: + from PyQt5.QtChart import QChartView, QChart, QBarSet, QHorizontalBarSeries, QBarCategoryAxis + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPainter + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPainter + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QBarSet = QtCharts.QBarSet + QHorizontalBarSeries = QtCharts.QHorizontalBarSeries + QBarCategoryAxis = QtCharts.QBarCategoryAxis class Window(QChartView): @@ -72,7 +86,6 @@ def getSeries(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = Window() diff --git a/QtChart/HorizontalPercentBarChart.py b/QtChart/HorizontalPercentBarChart.py index 6f807e21..785efc9f 100644 --- a/QtChart/HorizontalPercentBarChart.py +++ b/QtChart/HorizontalPercentBarChart.py @@ -4,14 +4,26 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: HorizontalPercentBarChart @description: 横向百分比柱状图表 """ -from PyQt5.QtChart import QChartView, QChart, QBarSet, QHorizontalPercentBarSeries, QBarCategoryAxis -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPainter + +try: + from PyQt5.QtChart import QChartView, QChart, QBarSet, QHorizontalPercentBarSeries, QBarCategoryAxis + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPainter +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPainter + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QBarSet = QtCharts.QBarSet + QHorizontalPercentBarSeries = QtCharts.QHorizontalPercentBarSeries + QBarCategoryAxis = QtCharts.QBarCategoryAxis class Window(QChartView): diff --git a/QtChart/LineChart.py b/QtChart/LineChart.py index aa2709c4..9386a99c 100644 --- a/QtChart/LineChart.py +++ b/QtChart/LineChart.py @@ -1,27 +1,33 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月19日 -@author: Irony."[讽刺] -@site: http://alyl.vip, http://orzorz.vip, http://coding.net/u/892768447, http://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: LineChart @description: -''' +""" import sys -from PyQt5.QtChart import QChartView, QLineSeries, QChart -from PyQt5.QtGui import QPainter -from PyQt5.QtWidgets import QApplication +try: + from PyQt5.QtChart import QChartView, QLineSeries, QChart + from PyQt5.QtGui import QPainter + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtGui import QPainter + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts -__version__ = "0.0.1" + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QLineSeries = QtCharts.QLineSeries - -if __name__ == "__main__": +if __name__ == '__main__': app = QApplication(sys.argv) chart = QChart() - chart.setTitle("Line Chart 1") + chart.setTitle('Line Chart 1') series = QLineSeries(chart) series.append(0, 6) series.append(2, 4) diff --git a/QtChart/LineStack.py b/QtChart/LineStack.py index 7a06c813..fb03eaf1 100644 --- a/QtChart/LineStack.py +++ b/QtChart/LineStack.py @@ -1,27 +1,36 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月28日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: charts.line.LineStack @description: like http://echarts.baidu.com/demo.html#line-stack -''' +""" import sys -from PyQt5.QtChart import QChartView, QChart, QLineSeries, QLegend, \ - QCategoryAxis -from PyQt5.QtCore import Qt, QPointF, QRectF, QPoint -from PyQt5.QtGui import QPainter, QPen -from PyQt5.QtWidgets import QApplication, QGraphicsLineItem, QWidget, \ - QHBoxLayout, QLabel, QVBoxLayout, QGraphicsProxyWidget - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtChart import QChartView, QChart, QLineSeries, QLegend, \ + QCategoryAxis + from PyQt5.QtCore import Qt, QPointF, QRectF, QPoint + from PyQt5.QtGui import QPainter, QPen + from PyQt5.QtWidgets import QApplication, QGraphicsLineItem, QWidget, \ + QHBoxLayout, QLabel, QVBoxLayout, QGraphicsProxyWidget +except ImportError: + from PySide2.QtCore import Qt, QPointF, QRectF, QPoint + from PySide2.QtGui import QPainter, QPen + from PySide2.QtWidgets import QApplication, QGraphicsLineItem, QWidget, \ + QHBoxLayout, QLabel, QVBoxLayout, QGraphicsProxyWidget + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QLineSeries = QtCharts.QLineSeries + QLegend = QtCharts.QLegend + QCategoryAxis = QtCharts.QCategoryAxis class ToolTipItem(QWidget): @@ -44,7 +53,6 @@ def setText(self, text): class ToolTipWidget(QWidget): - Cache = {} def __init__(self, *args, **kwargs): @@ -130,7 +138,7 @@ def resizeEvent(self, event): self.point_bottom = self._chart.mapToPosition( QPointF(self.min_x, self.min_y)) self.step_x = (self.max_x - self.min_x) / \ - (self._chart.axisX().tickCount() - 1) + (self._chart.axisX().tickCount() - 1) def mouseMoveEvent(self, event): super(ChartView, self).mouseMoveEvent(event) @@ -158,10 +166,10 @@ def mouseMoveEvent(self, event): t_height = self.toolTipWidget.height() # 如果鼠标位置离右侧的距离小于tip宽度 x = pos.x() - t_width if self.width() - \ - pos.x() - 20 < t_width else pos.x() + pos.x() - 20 < t_width else pos.x() # 如果鼠标位置离底部的高度小于tip高度 y = pos.y() - t_height if self.height() - \ - pos.y() - 20 < t_height else pos.y() + pos.y() - 20 < t_height else pos.y() self.toolTipWidget.show( title, points, QPoint(x, y)) else: @@ -173,7 +181,7 @@ def handleMarkerClicked(self): if not marker: return visible = not marker.series().isVisible() -# # 隐藏或显示series + # # 隐藏或显示series marker.series().setVisible(visible) marker.setVisible(True) # 要保证marker一直显示 # 透明度 diff --git a/QtChart/PercentBarChart.py b/QtChart/PercentBarChart.py index 1cd4d066..dcbf8837 100644 --- a/QtChart/PercentBarChart.py +++ b/QtChart/PercentBarChart.py @@ -4,14 +4,28 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: PercentBarChart @description: 百分比柱状图表 """ -from PyQt5.QtChart import QChartView, QChart, QBarSet, QPercentBarSeries, QBarCategoryAxis -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPainter + +try: + from PyQt5.QtChart import QChartView, QChart, QBarSet, QPercentBarSeries, QBarCategoryAxis + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPainter + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPainter + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QBarSet = QtCharts.QBarSet + QPercentBarSeries = QtCharts.QPercentBarSeries + QBarCategoryAxis = QtCharts.QBarCategoryAxis class Window(QChartView): @@ -72,7 +86,6 @@ def getSeries(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = Window() diff --git a/QtChart/PieChart.py b/QtChart/PieChart.py index f3bb7a0e..57fa5bde 100644 --- a/QtChart/PieChart.py +++ b/QtChart/PieChart.py @@ -4,13 +4,24 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: PieChart @description: 饼状图表 """ -from PyQt5.QtChart import QChartView, QChart, QPieSeries -from PyQt5.QtGui import QPainter, QColor + +try: + from PyQt5.QtChart import QChartView, QChart, QPieSeries + from PyQt5.QtGui import QPainter, QColor + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtGui import QPainter, QColor + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QPieSeries = QtCharts.QPieSeries class Window(QChartView): @@ -47,7 +58,6 @@ def getSeries(self): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = Window() diff --git a/QtChart/README.md b/QtChart/README.md index 8330a7a3..80f9a1ff 100644 --- a/QtChart/README.md +++ b/QtChart/README.md @@ -16,13 +16,16 @@ - [横向百分比柱状图表](#13横向百分比柱状图表) - [散点图表](#14散点图表) - [图表主题动画](#15图表主题动画) + - [CPU动态折线图](#16CPU动态折线图) ## 1、折线图 + [运行 LineChart.py](LineChart.py) ![LineChart](ScreenShot/LineChart.png) ## 2、折线堆叠图 + [运行 LineStack.py](LineStack.py) 仿照 [line-stack](http://echarts.baidu.com/demo.html#line-stack) @@ -30,6 +33,7 @@ ![LineStack](ScreenShot/LineStack.gif) ## 3、柱状堆叠图 + [运行 BarStack.py](BarStack.py) 仿照 [bar-stack](http://echarts.baidu.com/demo.html#bar-stack) @@ -37,61 +41,81 @@ ![BarStack](ScreenShot/BarStack.gif) ## 4、LineChart自定义xy轴 + [运行 CustomXYaxis.py](CustomXYaxis.py) ![CustomXYaxis](ScreenShot/CustomXYaxis.png) ## 5、ToolTip提示 -[运行 ToolTip.py](ToolTip.py) | [运行 ToolTip2.py](ToolTip2.py) + +[运行 ToolTip.py](ToolTip.py) | [运行 ToolTip2.py](ToolTip2.py) ![ToolTip](ScreenShot/ToolTip.gif) ![ToolTip2](ScreenShot/ToolTip2.gif) ## 6、动态曲线图 + [运行 DynamicSpline.py](DynamicSpline.py) ![DynamicSpline](ScreenShot/DynamicSplineChart.gif) ## 7、区域图表 + [运行 AreaChart.py](AreaChart.py) ![AreaChart](ScreenShot/AreaChart.png) ## 8、柱状图表 + [运行 BarChart.py](BarChart.py) ![BarChart](ScreenShot/BarChart.png) ## 9、饼状图表 + [运行 PieChart.py](PieChart.py) ![PieChart](ScreenShot/PieChart.png) ## 10、样条图表 + [运行 SplineChart.py](SplineChart.py) ![SplineChart](ScreenShot/SplineChart.png) ## 11、百分比柱状图表 + [运行 PercentBarChart.py](PercentBarChart.py) ![PercentBarChart](ScreenShot/PercentBarChart.png) ## 12、横向柱状图表 + [运行 HorizontalBarChart.py](HorizontalBarChart.py) ![HorizontalBarChart](ScreenShot/HorizontalBarChart.png) ## 13、横向百分比柱状图表 + [运行 HorizontalPercentBarChart.py](HorizontalPercentBarChart.py) ![HorizontalPercentBarChart](ScreenShot/HorizontalPercentBarChart.png) ## 14、散点图表 + [运行 ScatterChart.py](ScatterChart.py) ![ScatterChart](ScreenShot/ScatterChart.png) ## 15、图表主题动画 + [运行 ChartThemes.py](ChartThemes.py) -![ChartThemes](ScreenShot/ChartThemes.gif) \ No newline at end of file +![ChartThemes](ScreenShot/ChartThemes.gif) + +## 16、CPU动态折线图 + +[运行 CpuLineChart.py](CpuLineChart.py) + +通过设置x轴的时间范围并替换y点达到动态移动效果 + +![CpuLineChart](ScreenShot/CpuLineChart.png) diff --git a/QtChart/ScatterChart.py b/QtChart/ScatterChart.py index e6d7b0a6..d523e83c 100644 --- a/QtChart/ScatterChart.py +++ b/QtChart/ScatterChart.py @@ -4,16 +4,27 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ScatterChart @description: 散点图表 """ import random -from PyQt5.QtChart import QChartView, QChart, QScatterSeries -from PyQt5.QtCore import QPointF -from PyQt5.QtGui import QPainter +try: + from PyQt5.QtChart import QChartView, QChart, QScatterSeries + from PyQt5.QtCore import QPointF + from PyQt5.QtGui import QPainter + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import QPointF + from PySide2.QtGui import QPainter + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QScatterSeries = QtCharts.QScatterSeries class Window(QChartView): @@ -70,7 +81,6 @@ def generateRandomData(self, listCount, valueMax, valueCount): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = Window() diff --git a/QtChart/ScreenShot/CpuLineChart.png b/QtChart/ScreenShot/CpuLineChart.png new file mode 100644 index 00000000..06cb3b2c Binary files /dev/null and b/QtChart/ScreenShot/CpuLineChart.png differ diff --git a/QtChart/SplineChart.py b/QtChart/SplineChart.py index 69a54361..9fe2eaf8 100644 --- a/QtChart/SplineChart.py +++ b/QtChart/SplineChart.py @@ -4,14 +4,26 @@ """ Created on 2019/10/2 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: SplineChart @description: 样条图表 """ -from PyQt5.QtChart import QChartView, QChart, QSplineSeries -from PyQt5.QtCore import QPointF -from PyQt5.QtGui import QPainter + +try: + from PyQt5.QtChart import QChartView, QChart, QSplineSeries + from PyQt5.QtCore import QPointF + from PyQt5.QtGui import QPainter + from PyQt5.QtWidgets import QApplication +except ImportError: + from PySide2.QtCore import QPointF + from PySide2.QtGui import QPainter + from PySide2.QtWidgets import QApplication + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QSplineSeries = QtCharts.QSplineSeries class Window(QChartView): @@ -49,7 +61,6 @@ def getSeries(self, chart): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) w = Window() diff --git a/QtChart/ToolTip.py b/QtChart/ToolTip.py index dc34a596..397254b1 100644 --- a/QtChart/ToolTip.py +++ b/QtChart/ToolTip.py @@ -1,27 +1,34 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月23日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ToolTip @description: -''' +""" import sys -from PyQt5.QtChart import QChartView, QChart, QLineSeries -from PyQt5.QtCore import Qt, QRectF, QPoint, QPointF -from PyQt5.QtGui import QPainter, QCursor -from PyQt5.QtWidgets import QApplication, QGraphicsProxyWidget, QLabel, \ - QWidget, QHBoxLayout, QVBoxLayout, QToolTip, QGraphicsLineItem - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - -''' +try: + from PyQt5.QtChart import QChartView, QChart, QLineSeries + from PyQt5.QtCore import Qt, QRectF, QPoint, QPointF + from PyQt5.QtGui import QPainter, QCursor + from PyQt5.QtWidgets import QApplication, QGraphicsProxyWidget, QLabel, \ + QWidget, QHBoxLayout, QVBoxLayout, QToolTip, QGraphicsLineItem +except ImportError: + from PySide2.QtCore import Qt, QRectF, QPoint, QPointF + from PySide2.QtGui import QPainter, QCursor + from PySide2.QtWidgets import QApplication, QGraphicsProxyWidget, QLabel, \ + QWidget, QHBoxLayout, QVBoxLayout, QToolTip, QGraphicsLineItem + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QLineSeries = QtCharts.QLineSeries + +""" class CircleWidget(QGraphicsProxyWidget): def __init__(self, color, *args, **kwargs): @@ -57,7 +64,7 @@ def show(self, pos): self.setGeometry(pos.x(), pos.y(), self.size().width(), self.size().height()) super(GraphicsWidget, self).show() -''' +""" class ToolTipItem(QWidget): @@ -161,7 +168,8 @@ def mouseMoveEvent(self, event): # print(x, pos_x, index, index * self.step_x + self.min_x) # 得到在坐标系中的所有series的类型和点 points = [(serie, serie.at(index)) - for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] + for serie in self._chart.series() if + self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] if points: # 永远在轴上的黑线条 self.lineItem.setLine(pos_x.x(), self.point_top.y(), diff --git a/QtChart/ToolTip2.py b/QtChart/ToolTip2.py index 349473ed..3a8ca3df 100644 --- a/QtChart/ToolTip2.py +++ b/QtChart/ToolTip2.py @@ -1,27 +1,34 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月23日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ToolTip2 @description: -''' +""" import sys -from PyQt5.QtChart import QChartView, QChart, QLineSeries -from PyQt5.QtCore import Qt, QRectF, QPoint, QPointF -from PyQt5.QtGui import QPainter, QCursor -from PyQt5.QtWidgets import QApplication, QGraphicsProxyWidget, QLabel, \ - QWidget, QHBoxLayout, QVBoxLayout, QToolTip, QGraphicsLineItem - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - -''' +try: + from PyQt5.QtChart import QChartView, QChart, QLineSeries + from PyQt5.QtCore import Qt, QRectF, QPoint, QPointF + from PyQt5.QtGui import QPainter, QCursor + from PyQt5.QtWidgets import QApplication, QGraphicsProxyWidget, QLabel, \ + QWidget, QHBoxLayout, QVBoxLayout, QToolTip, QGraphicsLineItem +except ImportError: + from PySide2.QtCore import Qt, QRectF, QPoint, QPointF + from PySide2.QtGui import QPainter, QCursor + from PySide2.QtWidgets import QApplication, QGraphicsProxyWidget, QLabel, \ + QWidget, QHBoxLayout, QVBoxLayout, QToolTip, QGraphicsLineItem + from PySide2.QtCharts import QtCharts + + QChartView = QtCharts.QChartView + QChart = QtCharts.QChart + QLineSeries = QtCharts.QLineSeries + +""" class CircleWidget(QGraphicsProxyWidget): def __init__(self, color, *args, **kwargs): @@ -57,7 +64,7 @@ def show(self, pos): self.setGeometry(pos.x(), pos.y(), self.size().width(), self.size().height()) super(GraphicsWidget, self).show() -''' +""" class ToolTipItem(QWidget): @@ -159,7 +166,8 @@ def mouseMoveEvent(self, event): # print(x, pos_x, index, index * self.step_x + self.min_x) # 得到在坐标系中的所有series的类型和点 points = [(serie, serie.at(index)) - for serie in self._chart.series() if self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] + for serie in self._chart.series() if + self.min_x <= x <= self.max_x and self.min_y <= y <= self.max_y] if points: # 跟随鼠标的黑线条 self.lineItem.setLine(event.pos().x(), self.point_top.y(), diff --git a/QtDataVisualization/BarsVisualization.py b/QtDataVisualization/BarsVisualization.py index 6808b09a..7fe583dd 100644 --- a/QtDataVisualization/BarsVisualization.py +++ b/QtDataVisualization/BarsVisualization.py @@ -4,7 +4,7 @@ """ Created on 2019/10/4 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: BarsVisualization @description: 柱状图 diff --git a/QtDataVisualization/MagneticOfSun.py b/QtDataVisualization/MagneticOfSun.py index d4562f56..da9fa0d1 100644 --- a/QtDataVisualization/MagneticOfSun.py +++ b/QtDataVisualization/MagneticOfSun.py @@ -4,7 +4,7 @@ """ Created on 2019/10/4 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: MagneticOfSun @description: diff --git a/QtDataVisualization/README.md b/QtDataVisualization/README.md index 9366e669..bcd8a75c 100644 --- a/QtDataVisualization/README.md +++ b/QtDataVisualization/README.md @@ -6,16 +6,19 @@ - [余弦波3D](#3余弦波3D) ## 1、柱状图3D + [运行 BarsVisualization.py](BarsVisualization.py) ![BarsVisualization](ScreenShot/BarsVisualization.gif) ## 2、太阳磁场线 + [运行 MagneticOfSun.py](MagneticOfSun.py) ![MagneticOfSun](ScreenShot/MagneticOfSun.gif) ## 3、余弦波3D + [运行 ScatterVisualization.py](ScatterVisualization.py) -![ScatterVisualization](ScreenShot/ScatterVisualization.gif) \ No newline at end of file +![ScatterVisualization](ScreenShot/ScatterVisualization.gif) diff --git a/QtDataVisualization/ScatterVisualization.py b/QtDataVisualization/ScatterVisualization.py index eff7fe0c..69eeeb82 100644 --- a/QtDataVisualization/ScatterVisualization.py +++ b/QtDataVisualization/ScatterVisualization.py @@ -4,7 +4,7 @@ """ Created on 2019/10/4 @author: Irony -@site: https://pyqt5.com , https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ScatterVisualization @description: diff --git a/QtQuick/FlatStyle.py b/QtQuick/FlatStyle.py index c4e42b92..e11e984d 100644 --- a/QtQuick/FlatStyle.py +++ b/QtQuick/FlatStyle.py @@ -4,7 +4,7 @@ """ Created on 2019年2月2日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QtQuick.FlatStyle @description: @@ -12,14 +12,14 @@ import os import sys -from PyQt5.QtCore import QCoreApplication, Qt, QUrl -from PyQt5.QtQml import QQmlApplicationEngine -from PyQt5.QtWidgets import QApplication, QMessageBox - - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' - +try: + from PyQt5.QtCore import QCoreApplication, Qt, QUrl + from PyQt5.QtQml import QQmlApplicationEngine + from PyQt5.QtWidgets import QApplication, QMessageBox +except ImportError: + from PySide2.QtCore import QCoreApplication, Qt, QUrl + from PySide2.QtQml import QQmlApplicationEngine + from PySide2.QtWidgets import QApplication, QMessageBox if __name__ == '__main__': try: @@ -32,7 +32,8 @@ app = QApplication(sys.argv) engine = QQmlApplicationEngine() engine.objectCreated.connect( - lambda obj, _: (QMessageBox.critical(None, '错误', '运行失败,可能是当前PyQt版本不支持'), engine.quit) if not obj else 0) + lambda obj, _: ( + QMessageBox.critical(None, '错误', '运行失败,可能是当前PyQt版本不支持'), engine.quit) if not obj else 0) engine.addImportPath('imports') engine.load(QUrl('flatstyle.qml')) diff --git a/QtQuick/README.md b/QtQuick/README.md index 253ed416..879091c9 100644 --- a/QtQuick/README.md +++ b/QtQuick/README.md @@ -5,11 +5,13 @@ - [QML与Python交互](#2QML与Python交互) ## 1、Flat样式 + [运行 FlatStyle.py](FlatStyle.py) ![FlatStyle](ScreenShot/FlatStyle.gif) ## 2、QML与Python交互 + [运行 Signals.py](Signals.py) 交互的办法有很多种,由于主要界面功能都是有QML来实现,Python只是作为辅助提供部分功能。 @@ -18,6 +20,7 @@ 1. 通过 `engine.rootContext().setContextProperty('_Window', w)` 注册提供一个Python对象 2. Python对象中被访问的方法前面使用装饰器 `@pyqtSlot`,比如: `@pyqtSlot(int)` 或者 `@pyqtSlot(str, result=str) # 可以获取返回值` 。 3. QML中的信号或者Python对象中的信号都可以互相绑定对方的槽函数 + ```js Component.onCompleted: { // 绑定信号槽到python中的函数 @@ -27,5 +30,4 @@ Component.onCompleted: { } ``` - ![Signals](ScreenShot/Signals.gif) diff --git a/QtQuick/Signals.py b/QtQuick/Signals.py index 3ea5b812..7735c944 100644 --- a/QtQuick/Signals.py +++ b/QtQuick/Signals.py @@ -4,23 +4,25 @@ """ Created on 2019年9月18日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QtQuick.Signals @description: 信号槽 """ -from time import time import sys +from time import time -from PyQt5.QtCore import QCoreApplication, Qt, pyqtSlot, pyqtSignal, QTimer -from PyQt5.QtQml import QQmlApplicationEngine -from PyQt5.QtWidgets import QApplication, QMessageBox, QWidget, QVBoxLayout,\ - QPushButton, QTextBrowser - - -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019' +try: + from PyQt5.QtCore import QCoreApplication, Qt, pyqtSlot, pyqtSignal, QTimer + from PyQt5.QtQml import QQmlApplicationEngine + from PyQt5.QtWidgets import QApplication, QMessageBox, QWidget, QVBoxLayout, \ + QPushButton, QTextBrowser +except ImportError: + from PySide2.QtCore import QCoreApplication, Qt, Slot as pyqtSlot, Signal as pyqtSignal, QTimer + from PySide2.QtQml import QQmlApplicationEngine + from PySide2.QtWidgets import QApplication, QMessageBox, QWidget, QVBoxLayout, \ + QPushButton, QTextBrowser QML = """import QtQuick 2.0 import QtQuick.Controls 1.6 @@ -84,7 +86,6 @@ class Window(QWidget): - # 定义一个时间信号 timerSignal = pyqtSignal(str) diff --git a/QtRemoteObjects/README.md b/QtRemoteObjects/README.md index ce6e3c53..87e22996 100644 --- a/QtRemoteObjects/README.md +++ b/QtRemoteObjects/README.md @@ -6,11 +6,13 @@ - [简单界面数据同步](#3简单界面数据同步) ## 1、modelview + [运行 modelviewserver.py](modelview/modelviewserver.py) | [运行 modelviewclient.py](modelview/modelviewclient.py) 官方关于QTreeView/QStandardItemModel的同步model例子 ## 2、simpleswitch + [运行 directconnectdynamicserver.py](simpleswitch/directconnectdynamicserver.py) | [运行 directconnectdynamicclient.py](simpleswitch/directconnectdynamicclient.py) [运行 registryconnecteddynamicserver.py](simpleswitch/registryconnecteddynamicserver.py) | [运行 registryconnecteddynamicclient.py](simpleswitch/registryconnecteddynamicclient.py) @@ -18,8 +20,9 @@ 官方关于简单的信号槽、属性访问测试例子 ## 3、简单界面数据同步 + [运行 WindowMaster.py](SyncUi/WindowMaster.py) | [运行 WindowSlave.py](SyncUi/WindowSlave.py) 绑定信号槽同步双方数据,属性方法测试没通过,详细注释在代码中 -![SyncUi](ScreenShot/SyncUi.gif) \ No newline at end of file +![SyncUi](ScreenShot/SyncUi.gif) diff --git a/QtRemoteObjects/SyncUi/ClipboardMaster.py b/QtRemoteObjects/SyncUi/ClipboardMaster.py new file mode 100644 index 00000000..50aea697 --- /dev/null +++ b/QtRemoteObjects/SyncUi/ClipboardMaster.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/7/31 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ClipboardMaster +@description: +""" +import sys + +from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, QLoggingCategory, QVariant, QMimeData +from PyQt5.QtRemoteObjects import QRemoteObjectHost +from PyQt5.QtWidgets import QTextBrowser + + +class WindowMaster(QTextBrowser): + SignalUpdateMimeData = pyqtSignal( + bool, QVariant, # color + bool, QVariant, # html + bool, QVariant, # image + bool, QVariant, # text + bool, QVariant, # urls + ) + + def __init__(self, *args, **kwargs): + super(WindowMaster, self).__init__(*args, **kwargs) + # 监听剪切板 + clipboard = QApplication.clipboard() + clipboard.dataChanged.connect(self.on_data_changed) + # 开启节点 + host = QRemoteObjectHost(QUrl('tcp://0.0.0.0:' + sys.argv[1]), parent=self) + host.enableRemoting(self, 'WindowMaster') + self.append('开启节点完成') + + def on_data_changed(self): + # 服务端剪贴板变化后发送到客户端 + clipboard = QApplication.clipboard() + clipboard.blockSignals(True) + mime_data = clipboard.mimeData() + self.SignalUpdateMimeData.emit( + mime_data.hasColor(), mime_data.colorData(), + mime_data.hasHtml(), mime_data.html(), + mime_data.hasImage(), mime_data.imageData(), + mime_data.hasText(), mime_data.text(), + mime_data.hasUrls(), mime_data.urls(), + ) + clipboard.blockSignals(False) + + @pyqtSlot( + bool, QVariant, # color + bool, QVariant, # html + bool, QVariant, # image + bool, QVariant, # text + bool, QVariant, # urls + bool, QVariant # files + ) + def updateMimeData(self, + hasColor, color, + hasHtml, html, + hasImage, image, + hasText, text, + hasUrls, urls, + hasFiles, files, + ): + # 客户端剪切板同步到服务端 + self.append('收到客户端发送的剪贴板') + clipboard = QApplication.clipboard() + clipboard.blockSignals(True) + data = QMimeData() + if hasColor: + data.setColorData(color) + if hasHtml: + data.setHtml(html) + if hasImage: + data.setImageData(image) + if hasText: + data.setText(text) + # if hasUrls: + # data.setUrls(urls) + if hasFiles: + data.setData('') + clipboard.setMimeData(data) + clipboard.blockSignals(False) + + +if __name__ == '__main__': + import cgitb + + cgitb.enable(format='text') + from PyQt5.QtWidgets import QApplication + + QLoggingCategory.setFilterRules('qt.remoteobjects.debug=true\n' + 'qt.remoteobjects.warning=true') + + app = QApplication(sys.argv) + w = WindowMaster() + w.show() + sys.exit(app.exec_()) diff --git a/QtRemoteObjects/SyncUi/ClipboardSlave.py b/QtRemoteObjects/SyncUi/ClipboardSlave.py new file mode 100644 index 00000000..2e271b1c --- /dev/null +++ b/QtRemoteObjects/SyncUi/ClipboardSlave.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/7/31 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ClipboardSlave +@description: +""" +from PyQt5.QtCore import QUrl, pyqtSignal, QVariant, QMimeData +from PyQt5.QtRemoteObjects import QRemoteObjectNode, QRemoteObjectReplica +from PyQt5.QtWidgets import QTextBrowser + + +class WindowSlave(QTextBrowser): + SignalUpdateMimeData = pyqtSignal( + bool, QVariant, # color + bool, QVariant, # html + bool, QVariant, # image + bool, QVariant, # text + bool, QVariant, # urls + bool, QVariant, # files + ) + + def __init__(self, *args, **kwargs): + super(WindowSlave, self).__init__(*args, **kwargs) + # 监听剪切板 + clipboard = QApplication.clipboard() + clipboard.dataChanged.connect(self.on_data_changed) + # 加入Master节点 + node = QRemoteObjectNode(parent=self) + node.connectToNode(QUrl('tcp://{}:{}'.format(sys.argv[1], sys.argv[2]))) + # 获取WindowMaster对象 + self.windowMaster = node.acquireDynamic('WindowMaster') + # 初始化成功后才能去绑定信号等 + self.windowMaster.initialized.connect(self.onInitialized) + # 状态改变 https://doc.qt.io/qt-5/qremoteobjectreplica.html#State-enum + self.windowMaster.stateChanged.connect(self.onStateChanged) + + def onStateChanged(self, newState, oldState): + if newState == QRemoteObjectReplica.Suspect: + self.append('连接丢失') + + def onInitialized(self): + self.SignalUpdateMimeData.connect(self.windowMaster.updateMimeData) + self.windowMaster.SignalUpdateMimeData.connect(self.updateMimeData) + self.append('绑定信号槽完成') + + def on_data_changed(self): + # 客户端剪贴板变化后发送到远程 + print('on_data_changed') + clipboard = QApplication.clipboard() + clipboard.blockSignals(True) + mime_data = clipboard.mimeData() + files = mime_data.data('text/uri-list') + self.SignalUpdateMimeData.emit( + mime_data.hasColor(), mime_data.colorData(), + mime_data.hasHtml(), mime_data.html(), + mime_data.hasImage(), mime_data.imageData(), + mime_data.hasText(), mime_data.text(), + mime_data.hasUrls(), mime_data.urls(), + True if files else False, files, + ) + clipboard.blockSignals(False) + + def updateMimeData(self, + hasColor, color, + hasHtml, html, + hasImage, image, + hasText, text, + hasUrls, urls + ): + # 远程的剪贴板同步到客户端 + clipboard = QApplication.clipboard() + clipboard.blockSignals(True) + data = QMimeData() + if hasColor: + data.setColorData(color) + if hasHtml: + data.setHtml(html) + if hasImage: + data.setImageData(image) + if hasText: + data.setText(text) + if hasUrls: + data.setUrls(urls) + clipboard.setMimeData(data) + clipboard.blockSignals(False) + + +if __name__ == '__main__': + import sys + import cgitb + + cgitb.enable(format='text') + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + w = WindowSlave() + w.show() + sys.exit(app.exec_()) diff --git a/QtRemoteObjects/SyncUi/WindowMaster.py b/QtRemoteObjects/SyncUi/WindowMaster.py index 22ba0bc8..d93e6561 100644 --- a/QtRemoteObjects/SyncUi/WindowMaster.py +++ b/QtRemoteObjects/SyncUi/WindowMaster.py @@ -4,24 +4,18 @@ """ Created on 2019年8月7日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QtRemoteObjects.SyncUi.WindowMaster @description: 主窗口 """ from PyQt5.QtCore import QUrl, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtRemoteObjects import QRemoteObjectHost -from PyQt5.QtWidgets import QWidget, QLineEdit, QVBoxLayout, QCheckBox,\ +from PyQt5.QtWidgets import QWidget, QLineEdit, QVBoxLayout, QCheckBox, \ QProgressBar -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 - - class WindowMaster(QWidget): - # 输入框内容变化信号 editValueChanged = pyqtSignal(str) # 勾选框变化信号 @@ -84,6 +78,7 @@ def updateCheck(self, checked): if __name__ == '__main__': import sys from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = WindowMaster() w.show() diff --git a/QtRemoteObjects/SyncUi/WindowSlave.py b/QtRemoteObjects/SyncUi/WindowSlave.py index 44cfc147..dc3ea26a 100644 --- a/QtRemoteObjects/SyncUi/WindowSlave.py +++ b/QtRemoteObjects/SyncUi/WindowSlave.py @@ -4,22 +4,17 @@ """ Created on 2019年8月7日 @author: Irony -@site: https://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: QtRemoteObjects.SyncUi.WindowSlave @description: 备窗口 """ from PyQt5.QtCore import QUrl from PyQt5.QtRemoteObjects import QRemoteObjectNode, QRemoteObjectReplica -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QCheckBox,\ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QCheckBox, \ QProgressBar, QMessageBox -__Author__ = 'Irony' -__Copyright__ = 'Copyright (c) 2019 Irony' -__Version__ = 1.0 - - class WindowSlave(QWidget): def __init__(self, *args, **kwargs): @@ -65,13 +60,14 @@ def onInitialized(self): # Master进度条同步到Slave self.windowMaster.progressValueChanged.connect( self.progressBar.setValue) - + print('绑定信号槽完成') if __name__ == '__main__': import sys from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = WindowSlave() w.show() diff --git a/QtRemoteObjects/modelview/modelviewclient.py b/QtRemoteObjects/modelview/modelviewclient.py index 3ae7e6a3..1dd65f9f 100644 --- a/QtRemoteObjects/modelview/modelviewclient.py +++ b/QtRemoteObjects/modelview/modelviewclient.py @@ -52,15 +52,22 @@ import sys -from PyQt5.QtCore import QLoggingCategory, QUrl -from PyQt5.QtRemoteObjects import QRemoteObjectNode -from PyQt5.QtWidgets import QApplication, QTreeView +try: + from PyQt5.QtCore import QLoggingCategory, QUrl + from PyQt5.QtRemoteObjects import QRemoteObjectNode + from PyQt5.QtWidgets import QApplication, QTreeView +except ImportError: + from PySide2.QtCore import QUrl + from PySide2.QtRemoteObjects import QRemoteObjectNode + from PySide2.QtWidgets import QApplication, QTreeView - -QLoggingCategory.setFilterRules('qt.remoteobjects.debug=false\n' - 'qt.remoteobjects.warning=false\n' - 'qt.remoteobjects.models.debug=false\n' - 'qt.remoteobjects.models.debug=false') +try: + QLoggingCategory.setFilterRules('qt.remoteobjects.debug=false\n' + 'qt.remoteobjects.warning=false\n' + 'qt.remoteobjects.models.debug=false\n' + 'qt.remoteobjects.models.debug=false') +except NameError: + pass app = QApplication(sys.argv) @@ -73,6 +80,6 @@ model = node.acquireModel('RemoteModel') view.setModel(model) -view.show(); +view.show() sys.exit(app.exec_()) diff --git a/QtRemoteObjects/modelview/modelviewserver.py b/QtRemoteObjects/modelview/modelviewserver.py index 34cf04ce..e2af99c1 100644 --- a/QtRemoteObjects/modelview/modelviewserver.py +++ b/QtRemoteObjects/modelview/modelviewserver.py @@ -53,11 +53,16 @@ import sys -from PyQt5.QtCore import (pyqtSlot, QLoggingCategory, QModelIndex, QObject, Qt, - QTimer, QUrl) -from PyQt5.QtGui import QColor, QStandardItem, QStandardItemModel -from PyQt5.QtRemoteObjects import QRemoteObjectHost, QRemoteObjectRegistryHost -from PyQt5.QtWidgets import QApplication, QTreeView +try: + from PyQt5.QtCore import pyqtSlot, QLoggingCategory, QModelIndex, QObject, Qt, QTimer, QUrl + from PyQt5.QtGui import QColor, QStandardItem, QStandardItemModel + from PyQt5.QtRemoteObjects import QRemoteObjectHost, QRemoteObjectRegistryHost + from PyQt5.QtWidgets import QApplication, QTreeView +except ImportError: + from PySide2.QtCore import Slot as pyqtSlot, QModelIndex, QObject, Qt, QTimer, QUrl + from PySide2.QtGui import QColor, QStandardItem, QStandardItemModel + from PySide2.QtRemoteObjects import QRemoteObjectHost, QRemoteObjectRegistryHost + from PySide2.QtWidgets import QApplication, QTreeView class TimerHandler(QObject): @@ -71,7 +76,7 @@ def __init__(self, model, parent=None): def changeData(self): for i in range(10, 50): self._model.setData(self._model.index(i, 1), QColor(Qt.blue), - Qt.BackgroundRole) + Qt.BackgroundRole) @pyqtSlot() def insertData(self): @@ -79,9 +84,9 @@ def insertData(self): for i in range(2, 11): self._model.setData(self._model.index(i, 1), QColor(Qt.green), - Qt.BackgroundRole) + Qt.BackgroundRole) self._model.setData(self._model.index(i, 1), "InsertedRow", - Qt.DisplayRole) + Qt.DisplayRole) @pyqtSlot() def removeData(self): @@ -108,7 +113,7 @@ def addChild(numChildren, nestingLevel): for i in range(numChildren): child = QStandardItem( - "Child num {}, nesting level {}".format(i + 1, nestingLevel)) + "Child num {}, nesting level {}".format(i + 1, nestingLevel)) if i == 0: child.appendRow(addChild(numChildren, nestingLevel - 1)) @@ -120,14 +125,17 @@ def addChild(numChildren, nestingLevel): if __name__ == '__main__': - QLoggingCategory.setFilterRules('qt.remoteobjects.debug=false\n' - 'qt.remoteobjects.warning=false') + try: + QLoggingCategory.setFilterRules('qt.remoteobjects.debug=false\n' + 'qt.remoteobjects.warning=false') + except NameError: + pass app = QApplication(sys.argv) sourceModel = QStandardItemModel() sourceModel.setHorizontalHeaderLabels( - ["First Column with spacing", "Second Column with spacing"]) + ["First Column with spacing", "Second Column with spacing"]) for i in range(10000): firstItem = QStandardItem("FancyTextNumber {}".format(i)) diff --git a/QtRemoteObjects/simpleswitch/directconnectdynamicclient.py b/QtRemoteObjects/simpleswitch/directconnectdynamicclient.py index 27fcb9f1..abee8480 100644 --- a/QtRemoteObjects/simpleswitch/directconnectdynamicclient.py +++ b/QtRemoteObjects/simpleswitch/directconnectdynamicclient.py @@ -58,7 +58,6 @@ class DynamicClient(QObject): - # This signal is connected with server_slot() slot of the source object and # echoes back the switch state received from the source. echoSwitchState = pyqtSignal(bool) @@ -92,7 +91,6 @@ def initConnection(self): if __name__ == '__main__': - app = QCoreApplication(sys.argv) # Create the remote object node. diff --git a/QtRemoteObjects/simpleswitch/directconnectdynamicserver.py b/QtRemoteObjects/simpleswitch/directconnectdynamicserver.py index b7b0d05e..256d63bd 100644 --- a/QtRemoteObjects/simpleswitch/directconnectdynamicserver.py +++ b/QtRemoteObjects/simpleswitch/directconnectdynamicserver.py @@ -54,7 +54,7 @@ import sys from PyQt5.QtCore import (pyqtProperty, pyqtSignal, pyqtSlot, QCoreApplication, - QObject, QTimer, QUrl) + QObject, QTimer, QUrl) from PyQt5.QtRemoteObjects import QRemoteObjectHost @@ -98,7 +98,7 @@ def _set_currState(self, value): # The property exposed to a remote client. currState = pyqtProperty(bool, fget=_get_currState, fset=_set_currState, - notify=currStateChanged) + notify=currStateChanged) # The slot exposed to a remote client. @pyqtSlot(bool) @@ -115,7 +115,6 @@ def _timeout(self): if __name__ == '__main__': - app = QCoreApplication(sys.argv) # Create the simple switch. diff --git a/QtRemoteObjects/simpleswitch/registryconnecteddynamicclient.py b/QtRemoteObjects/simpleswitch/registryconnecteddynamicclient.py index f8368787..56b1d381 100644 --- a/QtRemoteObjects/simpleswitch/registryconnecteddynamicclient.py +++ b/QtRemoteObjects/simpleswitch/registryconnecteddynamicclient.py @@ -58,7 +58,6 @@ class DynamicClient(QObject): - # This signal is connected with server_slot() slot of the source object and # echoes back the switch state received from the source. echoSwitchState = pyqtSignal(bool) @@ -92,7 +91,6 @@ def initConnection(self): if __name__ == '__main__': - app = QCoreApplication(sys.argv) # Create the remote object node. diff --git a/QtRemoteObjects/simpleswitch/registryconnecteddynamicserver.py b/QtRemoteObjects/simpleswitch/registryconnecteddynamicserver.py index 7745f947..c7760b4a 100644 --- a/QtRemoteObjects/simpleswitch/registryconnecteddynamicserver.py +++ b/QtRemoteObjects/simpleswitch/registryconnecteddynamicserver.py @@ -54,7 +54,7 @@ import sys from PyQt5.QtCore import (pyqtProperty, pyqtSignal, pyqtSlot, QCoreApplication, - QObject, QTimer, QUrl) + QObject, QTimer, QUrl) from PyQt5.QtRemoteObjects import QRemoteObjectHost, QRemoteObjectRegistryHost @@ -98,7 +98,7 @@ def _set_currState(self, value): # The property exposed to a remote client. currState = pyqtProperty(bool, fget=_get_currState, fset=_set_currState, - notify=currStateChanged) + notify=currStateChanged) # The slot exposed to a remote client. @pyqtSlot(bool) @@ -115,7 +115,6 @@ def _timeout(self): if __name__ == '__main__': - app = QCoreApplication(sys.argv) # Create the simple switch. diff --git a/QtWinExtras/README.en.md b/QtWinExtras/README.en.md new file mode 100644 index 00000000..e69de29b diff --git a/QtWinExtras/README.md b/QtWinExtras/README.md new file mode 100644 index 00000000..e1775b13 --- /dev/null +++ b/QtWinExtras/README.md @@ -0,0 +1,21 @@ +# QtWinExtras + +- 目录 + - [任务栏进度条](#1任务栏进度条) + - [任务栏缩略图工具按钮](#2任务栏缩略图工具按钮) + +## 1、任务栏进度条 + +[运行 TaskbarProgress.py](TaskbarProgress.py) + +`QWinTaskbarProgress`类似和`QProgressBar`一样的操作 + +![TaskbarProgress](ScreenShot/TaskbarProgress.gif) + +## 2、任务栏缩略图工具按钮 + +[运行 ThumbnailToolBar.py](ThumbnailToolBar.py) + +`QWinThumbnailToolBar`和`QWinThumbnailToolButton`的组合实现音乐播放器的播放、上下一曲按钮 + +![ThumbnailToolBar](ScreenShot/ThumbnailToolBar.gif) diff --git a/QtWinExtras/ScreenShot/TaskbarProgress.gif b/QtWinExtras/ScreenShot/TaskbarProgress.gif new file mode 100644 index 00000000..5193d963 Binary files /dev/null and b/QtWinExtras/ScreenShot/TaskbarProgress.gif differ diff --git a/QtWinExtras/ScreenShot/ThumbnailToolBar.gif b/QtWinExtras/ScreenShot/ThumbnailToolBar.gif new file mode 100644 index 00000000..eae88c3e Binary files /dev/null and b/QtWinExtras/ScreenShot/ThumbnailToolBar.gif differ diff --git a/QtWinExtras/TaskbarProgress.py b/QtWinExtras/TaskbarProgress.py new file mode 100644 index 00000000..96b3a0a3 --- /dev/null +++ b/QtWinExtras/TaskbarProgress.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/7/1 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: TaskbarProgress +@description: +""" + +import cgitb +import sys + +try: + from PyQt5.QtCore import QTimer + from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QSpinBox, QPushButton, QLabel + from PyQt5.QtWinExtras import QWinTaskbarButton +except ImportError: + from PySide2.QtCore import QTimer + from PySide2.QtWidgets import QWidget, QApplication, QGridLayout, QSpinBox, QPushButton, QLabel + from PySide2.QtWinExtras import QWinTaskbarButton + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + # 获取任务栏按钮 + self.taskButton = QWinTaskbarButton(self) + # 获取任务栏进度条 + self.taskProgress = self.taskButton.progress() + # 定时器模拟进度 + self.timerProgress = QTimer(self) + self.timerProgress.timeout.connect(self.update_progress) + + self.setup_ui() + + def showEvent(self, event): + super(Window, self).showEvent(event) + if not self.taskButton.window(): + # 必须等窗口显示后设置才有效,或者通过软件流程在适当的时候设置也可以 + self.taskButton.setWindow(self.windowHandle()) + self.taskProgress.show() + + def closeEvent(self, event): + self.timerProgress.stop() + super(Window, self).closeEvent(event) + + def setup_ui(self): + layout = QGridLayout(self) + + # 设置最新小值和最大值 + self.spinBoxMin = QSpinBox(self) + self.spinBoxMax = QSpinBox(self) + self.spinBoxMax.setMaximum(100) + self.spinBoxMax.setValue(100) + layout.addWidget(self.spinBoxMin, 0, 0) + layout.addWidget(self.spinBoxMax, 0, 1) + layout.addWidget(QPushButton('设置范围值', self, clicked=self.set_range), 0, 2) + + # 设置当前值 + self.spinBoxCur = QSpinBox(self) + self.spinBoxCur.setMaximum(100) + self.spinBoxCur.setValue(50) + layout.addWidget(self.spinBoxCur, 0, 3) + layout.addWidget(QPushButton('设置当前值', self, clicked=self.set_current_value), 0, 4) + + # 功能按钮 + layout.addWidget(QPushButton('隐藏', self, clicked=self.set_show_hide), 1, 0) + layout.addWidget(QPushButton('暂停', self, clicked=self.set_pause_resume), 1, 1) + layout.addWidget(QPushButton('重置', self, clicked=self.set_reset), 1, 2) + layout.addWidget(QPushButton('停止', self, clicked=self.set_stop), 1, 3) + layout.addWidget(QPushButton('不可见', self, clicked=self.set_visible), 1, 4) + + # 模拟进度 + layout.addWidget(QPushButton('模拟进度动画', self, clicked=self.start_progress), 2, 0, 1, 5) + + # 状态 + layout.addWidget(QLabel('暂停信号 :', self), 3, 0) + self.labelPause = QLabel(self) + layout.addWidget(self.labelPause, 3, 1) + self.taskProgress.pausedChanged.connect(lambda v: self.labelPause.setText(str(v))) + + layout.addWidget(QLabel('停止信号 :', self), 4, 0) + self.labelStop = QLabel(self) + layout.addWidget(self.labelStop, 4, 1) + self.taskProgress.stoppedChanged.connect(lambda v: self.labelStop.setText(str(v))) + + layout.addWidget(QLabel('值改变信号:', self), 5, 0) + self.labelValue = QLabel(self) + layout.addWidget(self.labelValue, 5, 1) + self.taskProgress.valueChanged.connect(lambda v: self.labelValue.setText(str(v))) + + layout.addWidget(QLabel('可见度信号:', self), 6, 0) + self.labelVisible = QLabel(self) + layout.addWidget(self.labelVisible, 6, 1) + self.taskProgress.visibilityChanged.connect(lambda v: self.labelVisible.setText(str(v))) + + def set_range(self): + # 设置进度条范围值 + vmin = min(self.spinBoxMin.value(), self.spinBoxMax.value()) + vmax = max(self.spinBoxMin.value(), self.spinBoxMax.value()) + self.taskProgress.setRange(vmin, vmax) + + def set_current_value(self): + # 设置进度条当前值 + self.taskProgress.setValue(self.spinBoxCur.value()) + + def set_show_hide(self): + # 显示/隐藏 + visible = self.taskProgress.isVisible() + # 也可以使用self.taskProgress.setVisible + if visible: + self.taskProgress.hide() + self.sender().setText('显示') + else: + self.taskProgress.show() + self.sender().setText('隐藏') + + def set_pause_resume(self): + # 暂停/恢复 + paused = self.taskProgress.isPaused() + # 也可以使用self.taskProgress.setPaused + if paused: + self.taskProgress.resume() + self.timerProgress.start(100) + self.sender().setText('暂停') + else: + self.taskProgress.pause() + self.timerProgress.stop() + self.sender().setText('恢复') + + def set_reset(self): + # 重置 + self.taskProgress.reset() + paused = self.taskProgress.isPaused() + if not paused: + self.timerProgress.stop() + self.timerProgress.start(100) + + def set_stop(self): + # 停止 + self.timerProgress.stop() + self.taskProgress.stop() + self.setEnabled(False) + + def set_visible(self): + # 可见/不可见 + visible = self.taskProgress.isVisible() + self.taskProgress.setVisible(not visible) + self.sender().setText('可见' if visible else '不可见') + + def start_progress(self): + # 模拟进度 + self.timerProgress.start(100) + self.sender().setEnabled(False) + + def update_progress(self): + value = self.taskProgress.value() + value += 1 + if value > self.taskProgress.maximum(): + value = 0 + self.taskProgress.setValue(value) + + +if __name__ == '__main__': + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/QtWinExtras/ThumbnailToolBar.py b/QtWinExtras/ThumbnailToolBar.py new file mode 100644 index 00000000..8aab7b75 --- /dev/null +++ b/QtWinExtras/ThumbnailToolBar.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/7/3 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ThumbnailToolBar +@description: +""" + +import cgitb +import sys + +try: + from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QStyle, QVBoxLayout + from PyQt5.QtWinExtras import QWinThumbnailToolBar, QWinThumbnailToolButton +except ImportError: + from PySide2.QtWidgets import QWidget, QApplication, QLabel, QStyle, QVBoxLayout + from PySide2.QtWinExtras import QWinThumbnailToolBar, QWinThumbnailToolButton + + +class Window(QWidget): + + def __init__(self, *args, **kwargs): + super(Window, self).__init__(*args, **kwargs) + self.countPrev = 0 + self.countNext = 0 + self.init_ui() + + def init_ui(self): + layout = QVBoxLayout(self) + self.labelPrev = QLabel(self) + self.labelControl = QLabel('暂停播放', self) + self.labelNext = QLabel(self) + layout.addWidget(self.labelPrev) + layout.addWidget(self.labelControl) + layout.addWidget(self.labelNext) + + # 任务栏缩略图工具条 + self.toolBar = QWinThumbnailToolBar(self) + # 上一首,播放/暂停,下一首按钮 + self.toolBtnPrev = QWinThumbnailToolButton(self.toolBar) + self.toolBtnPrev.setToolTip('上一首') + self.toolBtnPrev.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipBackward)) + self.toolBtnPrev.clicked.connect(self.set_prev) + self.toolBar.addButton(self.toolBtnPrev) + + self.toolBtnControl = QWinThumbnailToolButton(self.toolBar) + self.toolBtnControl.setToolTip('播放') + self.toolBtnControl.setProperty('status', 0) + self.toolBtnControl.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) + self.toolBtnControl.clicked.connect(self.set_control) + self.toolBar.addButton(self.toolBtnControl) + + self.toolBtnNext = QWinThumbnailToolButton(self.toolBar) + self.toolBtnNext.setToolTip('下一首') + self.toolBtnNext.setIcon(self.style().standardIcon(QStyle.SP_MediaSkipForward)) + self.toolBtnNext.clicked.connect(self.set_next) + self.toolBar.addButton(self.toolBtnNext) + + def set_prev(self): + self.countPrev += 1 + self.labelPrev.setText('点击上一首按钮: %d 次' % self.countPrev) + + def set_next(self): + self.countNext += 1 + self.labelNext.setText('点击下一首按钮: %d 次' % self.countNext) + + def set_control(self): + if self.toolBtnControl.property('status') == 0: + self.labelControl.setText('正在播放') + self.toolBtnControl.setProperty('status', 1) + self.toolBtnControl.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) + else: + self.labelControl.setText('暂停播放') + self.toolBtnControl.setProperty('status', 0) + self.toolBtnControl.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) + + def showEvent(self, event): + super(Window, self).showEvent(event) + if not self.toolBar.window(): + # 必须等窗口显示后设置才有效,或者通过软件流程在适当的时候设置也可以 + self.toolBar.setWindow(self.windowHandle()) + + +if __name__ == '__main__': + cgitb.enable(format='text') + app = QApplication(sys.argv) + w = Window() + w.show() + sys.exit(app.exec_()) diff --git a/README.md b/README.md index d038768b..3f3685da 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # 各种各样的PyQt测试和例子 -[![Blog](https://img.shields.io/badge/blog-pyqt5-green.svg)](https://pyqt5.com) +[![Blog](https://img.shields.io/badge/blog-pyqt-green.svg)](https://pyqt.site) [![codebeat badge](https://codebeat.co/badges/d23d0dc8-aef3-43d2-96aa-e3215b2c9861)](https://codebeat.co/projects/github-com-pyqt5-pyqt-master) +[![Badge](https://img.shields.io/badge/link-996.icu-%23FF4D5B.svg?style=flat-square)](https://996.icu/#/zh_CN) +[![LICENSE](https://img.shields.io/badge/license-Anti%20996-blue.svg?style=flat-square)](https://github.com/996icu/996.ICU/blob/master/LICENSE) -https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分享大家平时学习中记录的笔记和例子,以及对遇到的问题进行收集整理。 +[https://pyqt.site](https://pyqt.site) 论坛是专门针对PyQt5学习和提升开设的网站,分享大家平时学习中记录的笔记和例子,以及对遇到的问题进行收集整理。 [![GitHub watchers](https://img.shields.io/github/watchers/PyQt5/PyQt.svg?style=social&label=Watch)](https://github.com/PyQt5/PyQt) [![GitHub stars](https://img.shields.io/github/stars/PyQt5/PyQt.svg?style=social)](https://github.com/PyQt5/PyQt) @@ -11,24 +13,36 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 如果您觉得这里的东西对您有帮助,别忘了帮忙点一颗:star:小星星:star: -## 微信博客小程序 +[客户端下载](https://github.com/PyQt5/PyQtClient/releases) | [自定义控件](https://github.com/PyQt5/CustomWidgets) - +## QQ群 -[客户端下载](https://github.com/PyQt5/PyQtClient/releases) + or -[自定义控件](https://github.com/PyQt5/CustomWidgets) +       [PyQt 学习](https://jq.qq.com/?_wv=1027&k=5QVVEdF) + +       [PyQt 频道](https://pd.qq.com/s/157c1hiay) + +## 状态 + +![Alt](https://repobeats.axiom.co/api/embed/12e69289ec9d9a7037d2c31aaaadd228dd99638f.svg "Repobeats analytics image") ## 目录 - Layouts - [QVBoxLayout](QVBoxLayout) + - [垂直布局](QVBoxLayout#1垂直布局) + - [边距和间隔](QVBoxLayout#2边距和间隔) + - [比例分配](QVBoxLayout#3比例分配) - [QHBoxLayout](QHBoxLayout) + - [水平布局](QHBoxLayout#1水平布局) + - [边距和间隔](QHBoxLayout#2边距和间隔) + - [比例分配](QHBoxLayout#3比例分配) - [QGridLayout](QGridLayout) - - [腾讯视频热播列表](QGridLayout/HotPlaylist.py) + - [音乐热歌列表](QGridLayout/HotPlaylist.py) - [QFormLayout](QFormLayout) - [QFlowLayout](QFlowLayout) - - [腾讯视频热播列表](QFlowLayout/HotPlaylist.py) + - [音乐热歌列表](QFlowLayout/HotPlaylist.py) - Spacers - [Horizontal Spacer](QSpacerItem) @@ -40,6 +54,8 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [按钮底部线条进度](QPushButton/BottomLineProgress.py) - [按钮文字旋转进度](QPushButton/FontRotate.py) - [按钮常用信号](QPushButton/SignalsExample.py) + - [旋转动画按钮](QPushButton/RotateButton.py) + - [弹性动画按钮](QPushButton/RubberBandButton.py) - [QToolButton](QToolButton) - [QRadioButton](QRadioButton) - [QCheckBox](QCheckBox) @@ -50,16 +66,20 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [显示自定义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 - [QListWidget](QListWidget) - [删除自定义Item](QListWidget/DeleteCustomItem.py) - [自定义可拖拽Item](QListWidget/DragDrop.py) - - [腾讯视频热播列表](QListWidget/HotPlaylist.py) + - [音乐热歌列表](QListWidget/HotPlaylist.py) - [仿折叠控件效果](QListWidget/FoldWidget.py) - [列表常用信号](QListWidget/SignalsExample.py) - [在item中添加图标](Test/partner_625781186/13.combo_listwidget) @@ -89,6 +109,7 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - Input Widgets - [QComboBox](QComboBox) - [下拉数据关联](QComboBox/CityLinkage.py) + - [文本居中显示](QComboBox/CenterText.py) - [QFontComboBox](QFontComboBox) - [QLineEdit](QLineEdit) - [QTextEdit](QTextEdit) @@ -104,6 +125,7 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [QSlider](QSlider) - [滑动条点击定位](QSlider/ClickJumpSlider.py) - [双层圆环样式](QSlider/QssQSlider.py) + - [低频率值变化](QSlider/LfSlider.py) - Display Widgets - [QLabel](QLabel) @@ -113,9 +135,12 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [显示.9格式图片(气泡)](QLabel/NinePatch.py) - [圆形图片](QLabel/CircleImage.py) - [QTextBrowser](QTextBrowser) + - [动态加载图片](QTextBrowser/DynamicRes.py) - [QGraphicsView](QGraphicsView) - [绘制世界地图](QGraphicsView/WorldMap.py) - [添加QWidget](QGraphicsView/AddQWidget.py) + - [图片查看器](QGraphicsView/ImageView.py) + - [图标拖拽](QGraphicsView/DragGraphics.py) - [QCalendarWidget](QCalendarWidget) - [QSS美化日历样式](QCalendarWidget/CalendarQssStyle.py) - [QLCDNumber](QLCDNumber) @@ -125,6 +150,10 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [百分比进度条](QProgressBar/PercentProgressBar.py) - [Metro进度条](QProgressBar/MetroCircleProgress.py) - [水波纹进度条](QProgressBar/WaterProgressBar.py) + - [圆形水位进度条](QProgressBar/WaterProgress.py) + - [多彩动画进度条](QProgressBar/ColourfulProgress.py) + - [QSplashScreen](QSplashScreen) + - [启动画面动画](QSplashScreen/GifSplashScreen.py) - [QOpenGLWidget](QOpenGLWidget) - [QWebView](QWebView) - [梦幻树](QWebView/DreamTree.py) @@ -140,14 +169,18 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [同网站不同用户](QWebEngineView/SiteDiffUser.py) - [拦截请求](QWebEngineView/BlockRequest.py) - [拦截请求内容](QWebEngineView/BlockRequestData.py) + - [设置Cookie](QWebEngineView/SetCookies.py) - [浏览器下载文件](Test/partner_625781186/6.QWebEngineView下载文件) - [打印网页](Test/partner_625781186/17_打印预览qwebengineview) + - [QWebChannel](QWebChannel) + - [和Js互相调用](QWebChannel/CallEachWithJs.py) - [QThread](QThread) - [继承QThread](QThread/InheritQThread.py) - [moveToThread](QThread/moveToThread.py) - [线程挂起恢复](QThread/SuspendThread.py) - [线程休眠唤醒](QThread/WakeupThread.py) + - [线程退出](QThread/QuitThread.py) - [QtQuick](QtQuick) - [Flat样式](QtQuick/FlatStyle.py) @@ -169,6 +202,7 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [横向百分比柱状图表](QtChart/HorizontalPercentBarChart.py) - [散点图表](QtChart/ScatterChart.py) - [图表主题动画](QtChart/ChartThemes.py) + - [CPU动态折线图](QtChart/CpuLineChart.py) - [QtDataVisualization](QtDataVisualization) - [柱状图3D](QtDataVisualization/BarsVisualization.py) @@ -196,12 +230,19 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [simpleswitch](QtRemoteObjects/simpleswitch) - [QPainter](QPainter) + - [QPainter绘制各种图形](QPainter/StockDialog.py) + - [简易画板](QPainter/Draw.py) + +- [QtWinExtras](QtWinExtras) + - [任务栏进度条](QtWinExtras/TaskbarProgress.py) + - [任务栏缩略图工具按钮](QtWinExtras/ThumbnailToolBar.py) - Others - [QFont](QFont) - [加载自定义字体](QFont/AwesomeFont.py) - [QMenu](QMenu) - [菜单设置多选并且不关闭](QMenu/MultiSelect.py) + - [仿QQ右键菜单](QMenu/QQMenu.py) - [悬停菜单](Test/partner_625781186/5.hoverMenu) - [QAxWidget](QAxWidget) - [显示Word、Excel、PDF文件](QAxWidget/ViewOffice.py) @@ -209,8 +250,12 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [分割窗口的分割条重绘](QSplitter/RewriteHandle.py) - [QSerialPort](QSerialPort) - [串口调试小助手](QSerialPort/SerialDebugAssistant.py) + - [QProcess](QProcess) + - [执行命令得到结果](QProcess/GetCmdResult.py) + - [交互执行命令](QProcess/InteractiveRun.py) - [QProxyStyle](QProxyStyle) - [Tab文字方向](QProxyStyle/TabTextDirection.py) + - [Tab角落控件位置占满](QProxyStyle/TabCornerWidget.py) - [QMessageBox](QMessageBox) - [消息对话框倒计时关闭](QMessageBox/CountDownClose.py) - [自定义图标等](QMessageBox/CustomColorIcon.py) @@ -221,6 +266,10 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [边框阴影动画](QGraphicsDropShadowEffect/ShadowEffect.py) - [QSystemTrayIcon](QSystemTrayIcon) - [最小化到系统托盘](QSystemTrayIcon/MinimizeToTray.py) + - [QSystemTrayIcon](QSystemTrayIcon) + - [系统托盘闪烁](QSystemTrayIcon/TrayNotify.py) + - [QMetaObject](QMetaObject) + - [在线程中操作UI](QMetaObject/CallInThread.py) - [Demo](Demo) - [重启窗口Widget](Demo/RestartWindow.py) @@ -245,16 +294,16 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [判断信号是否连接](Demo/IsSignalConnected.py) - [调用虚拟键盘](Demo/CallVirtualKeyboard.py) - [动态忙碌光标](Demo/GifCursor.py) + - [屏幕变动监听](Demo/ScreenNotify.py) + - [无边框窗口](Demo/NewFramelessWindow.py) + - [属性绑定](Demo/TestSerializeModel.py) -# QQ群 - -[PyQt 学习](https://jq.qq.com/?_wv=1027&k=5QVVEdF) +## 其它项目 +[一些Qt写的三方APP](https://github.com/PyQt5/3rd-Apps) -# [Donate-打赏](Donate) +## [Donate-打赏](Donate) 感谢所有捐助者的鼓励,[这里](https://github.com/PyQt5/thanks) 列出了捐助者名单(由于一些收款渠道无法知道对方是谁,如有遗漏请联系我修改) -or - -[一些Qt写的三方APP](https://github.com/PyQt5/3rd-Apps) +or diff --git a/Test/ButtomZoom.py b/Test/ButtomZoom.py index 0a2b482e..ab61e773 100644 --- a/Test/ButtomZoom.py +++ b/Test/ButtomZoom.py @@ -4,21 +4,17 @@ """ Created on 2018年10月30日 @author: Irony -@site: http://pyqt5.com https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ButtomZoom @description: """ -from PyQt5.QtCore import QPropertyAnimation, QRect -from PyQt5.QtWidgets import QPushButton, QWidget, QHBoxLayout, QSpacerItem,\ - QSizePolicy - - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import QPropertyAnimation, QRect + from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QHBoxLayout, QSpacerItem, QSizePolicy +except ImportError: + from PySide2.QtCore import QPropertyAnimation, QRect + from PySide2.QtWidgets import QApplication, QPushButton, QWidget, QHBoxLayout, QSpacerItem, QSizePolicy class ZoomButton(QPushButton): @@ -70,11 +66,11 @@ def mousePressEvent(self, event): super(ZoomButton, self).mousePressEvent(event) -class TestWindow(QWidget): +class Window(QWidget): # 测试窗口 def __init__(self, *args, **kwargs): - super(TestWindow, self).__init__(*args, **kwargs) + super(Window, self).__init__(*args, **kwargs) # 1. 加入布局中 layout = QHBoxLayout(self) layout.addSpacerItem(QSpacerItem( @@ -89,7 +85,7 @@ def __init__(self, *args, **kwargs): # 以下两个方法需要重写 def showEvent(self, event): - super(TestWindow, self).showEvent(event) + super(Window, self).showEvent(event) # 更新按钮的位置 self.button1.updatePos() # 针对不在控件中的按钮 @@ -98,7 +94,7 @@ def showEvent(self, event): self.button2.updatePos() def resizeEvent(self, event): - super(TestWindow, self).resizeEvent(event) + super(Window, self).resizeEvent(event) # 更新按钮的位置 self.button1.updatePos() # 针对不在控件中的按钮 @@ -109,7 +105,7 @@ def resizeEvent(self, event): if __name__ == '__main__': import sys - from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) app.setStyleSheet("""QPushButton { border: none; @@ -120,6 +116,6 @@ def resizeEvent(self, event): min-height: 40px; background-color: white; }""") - w = TestWindow() + w = Window() w.show() sys.exit(app.exec_()) diff --git "a/Test/C++\344\270\255\344\277\256\346\224\271PyQt\345\257\271\350\261\241/Test.py" "b/Test/C++\344\270\255\344\277\256\346\224\271PyQt\345\257\271\350\261\241/Test.py" index f8be27f9..1431c2fd 100644 --- "a/Test/C++\344\270\255\344\277\256\346\224\271PyQt\345\257\271\350\261\241/Test.py" +++ "b/Test/C++\344\270\255\344\277\256\346\224\271PyQt\345\257\271\350\261\241/Test.py" @@ -1,33 +1,34 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2020年3月2日 @author: Irony -@site: https://pyqt.site https://github.com/PyQt5 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: @description: 冷色调 -''' +""" import sys from ctypes import CDLL from time import time -# For PyQt5 try: - from PyQt5 import sip -except: - import sip -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QImage, QPixmap -from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QSlider, QVBoxLayout - -# For PySide2 -# import shiboken2 -# from PySide2.QtCore import Qt -# from PySide2.QtGui import QImage, QPixmap -# from PySide2.QtWidgets import QApplication, QWidget, QLabel, QSlider, QVBoxLayout + # For PyQt5 + try: + from PyQt5 import sip + except ImportError: + import sip + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QImage, QPixmap + from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QSlider, QVBoxLayout +except ImportError: + # For PySide2 + import shiboken2 + from PySide2.QtCore import Qt + from PySide2.QtGui import QImage, QPixmap + from PySide2.QtWidgets import QApplication, QWidget, QLabel, QSlider, QVBoxLayout class Window(QWidget): @@ -52,10 +53,12 @@ def __init__(self, *args, **kwargs): def doChange(self, value): t = time() img = self.srcImg.copy() # 复制一份 - # For PyQt5 - self.dll.cold(sip.unwrapinstance(img), value) - # For PySide2 - # self.dll.cold(shiboken2.getCppPointer(img)[0], value) + if 'sip' in sys.modules: + # For PyQt5 + self.dll.cold(sip.unwrapinstance(img), value) + elif 'shiboken2' in sys.modules: + # For PySide2 + self.dll.cold(shiboken2.getCppPointer(img)[0], value) self.imgLabel.setPixmap(QPixmap.fromImage(img).scaledToWidth(800, Qt.SmoothTransformation)) print('use time:', time() - t) diff --git "a/Test/C++\344\270\255\344\277\256\346\224\271PyQt\345\257\271\350\261\241/src.jpg" "b/Test/C++\344\270\255\344\277\256\346\224\271PyQt\345\257\271\350\261\241/src.jpg" index 2b113b08..c455f6ac 100644 Binary files "a/Test/C++\344\270\255\344\277\256\346\224\271PyQt\345\257\271\350\261\241/src.jpg" and "b/Test/C++\344\270\255\344\277\256\346\224\271PyQt\345\257\271\350\261\241/src.jpg" differ diff --git a/Test/ChartView/ChartView.py b/Test/ChartView/ChartView.py index a23d7491..2880867a 100644 --- a/Test/ChartView/ChartView.py +++ b/Test/ChartView/ChartView.py @@ -1,24 +1,21 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月18日 -@author: Irony."[讽刺] -@site: http://alyl.vip, http://orzorz.vip, http://coding.net/u/892768447, http://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ChartView @description: -''' +""" import json import os +import chardet from PyQt5.QtChart import QChart, QChartView, QLineSeries, QCategoryAxis from PyQt5.QtCore import QMargins, Qt, QEasingCurve from PyQt5.QtGui import QColor, QBrush, QFont, QPainter, QPen, QPixmap -import chardet - - -__version__ = "0.0.1" # QEasingCurve 类型枚举 EasingCurve = dict( @@ -55,8 +52,8 @@ def initUi(self, file): data = data.decode(encoding.get("encoding") or "utf-8") self.__analysis(json.loads(data)) -# def onSeriesHoverd(self, point, state): -# print(point, state) + # def onSeriesHoverd(self, point, state): + # print(point, state) def mouseMoveEvent(self, event): super(ChartView, self).mouseMoveEvent(event) @@ -89,8 +86,8 @@ def __getColor(self, color=None, default=Qt.white): return QColor(color) def __getPen(self, pen=None, default=QPen( - Qt.white, 1, Qt.SolidLine, - Qt.SquareCap, Qt.BevelJoin)): + Qt.white, 1, Qt.SolidLine, + Qt.SquareCap, Qt.BevelJoin)): ''' :param pen: pen json ''' @@ -112,13 +109,14 @@ def __getAlignment(self, alignment): return getattr(Qt, "Align" + alignment.capitalize()) except: return Qt.AlignTop -# if alignment == "left": -# return Qt.AlignLeft -# if alignment == "right": -# return Qt.AlignRight -# if alignment == "bottom": -# return Qt.AlignBottom -# return Qt.AlignTop + + # if alignment == "left": + # return Qt.AlignLeft + # if alignment == "right": + # return Qt.AlignRight + # if alignment == "bottom": + # return Qt.AlignBottom + # return Qt.AlignTop def __setTitle(self, title=None): ''' diff --git a/Test/ChartView/ChatWidget.py b/Test/ChartView/ChatWidget.py index fedfa9ce..a5f6622d 100644 --- a/Test/ChartView/ChatWidget.py +++ b/Test/ChartView/ChatWidget.py @@ -1,33 +1,30 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月20日 -@author: Irony."[讽刺] -@site: http://alyl.vip, http://orzorz.vip, http://coding.net/u/892768447, http://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: ChatWidget @description: -''' -from collections import OrderedDict +""" import json import os import sys +from collections import OrderedDict +import chardet from PyQt5.Qsci import QsciScintilla, QsciLexerJSON from PyQt5.QtChart import QChartView from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QColor, QFont, QFontMetrics, QKeySequence, QMovie -from PyQt5.QtWidgets import QSplitter, QTreeWidget, QApplication, QWidget,\ +from PyQt5.QtWidgets import QSplitter, QTreeWidget, QApplication, QWidget, \ QVBoxLayout, QPushButton, QTreeWidgetItem, QMessageBox, QShortcut, QLabel -import chardet from ChartView import ChartView # @UnresolvedImport -__version__ = "0.0.1" - - class LoadingWidget(QLabel): def __init__(self, *args, **kwargs): @@ -54,7 +51,6 @@ def closeEvent(self, event): class ClassifyWidget(QTreeWidget): - fileSelected = pyqtSignal(str) def __init__(self, *args, **kwargs): @@ -135,7 +131,6 @@ def init(self): class CodeWidget(QWidget): - runSignal = pyqtSignal(str) def __init__(self, *args, **kwargs): diff --git a/Test/ColumnView.py b/Test/ColumnView.py new file mode 100644 index 00000000..da5e3745 --- /dev/null +++ b/Test/ColumnView.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2020/9/14 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ColumnView +@description: +""" + +try: + from PyQt5.QtWidgets import QApplication, QComboBox, QFileSystemModel, QHBoxLayout, QSpacerItem, \ + QSizePolicy +except ImportError: + from PyQt5.QtWidgets import QApplication, QComboBox, QFileSystemModel, QHBoxLayout, QSpacerItem, \ + QSizePolicy + + +class PathComboBox(QComboBox): + + def __init__(self, *args, is_item=False, **kwargs): + super(PathComboBox, self).__init__(*args, **kwargs) + self.is_item = is_item + if not self.is_item: + self.setEditable(True) + layout = QHBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 23) + layout.addItem(QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)) + else: + self.f_model = QFileSystemModel(self) + self.f_model.setRootPath('') + self.setModel(self.f_model) + + def addWidget(self, widget): + self.layout().insertWidget(self.layout().count() - 1, widget) + + +if __name__ == '__main__': + import sys + + app = QApplication(sys.argv) + w = PathComboBox() + w.show() + w.addWidget(PathComboBox(w, is_item=True)) + sys.exit(app.exec_()) diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/.suo" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/.suo" deleted file mode 100644 index 9fa59224..00000000 Binary files "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/.suo" and /dev/null differ diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/Browse.VC.db" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/Browse.VC.db" deleted file mode 100644 index 08395773..00000000 Binary files "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/Browse.VC.db" and /dev/null differ diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/ipch/AutoPCH/7828e06aa30c0d3c/PYDEXT.ipch" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/ipch/AutoPCH/7828e06aa30c0d3c/PYDEXT.ipch" deleted file mode 100644 index b59817e5..00000000 Binary files "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/ipch/AutoPCH/7828e06aa30c0d3c/PYDEXT.ipch" and /dev/null differ diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/ipch/AutoPCH/d5916c660b4c4bac/PYDEXT.ipch" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/ipch/AutoPCH/d5916c660b4c4bac/PYDEXT.ipch" deleted file mode 100644 index 76f9a76e..00000000 Binary files "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/.vs/pydext/v15/ipch/AutoPCH/d5916c660b4c4bac/PYDEXT.ipch" and /dev/null differ diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj" deleted file mode 100644 index 6184a2a5..00000000 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj" +++ /dev/null @@ -1,203 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - {023C9F4B-FECC-430A-81D2-7044FAFF15D0} - pydext - 3.5 - 10.0.16299.0 - - - - DynamicLibrary - true - v141 - Unicode - - - DynamicLibrary - false - v141 - true - Unicode - - - DynamicLibrary - true - v141 - Unicode - - - DynamicLibrary - false - v141 - true - Unicode - - - RegistryView.Registry32 - RegistryView.Registry64 - $(PythonVersion)-32 - $(PythonVersion) - $([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Python\PythonCore\$(PythonTag)\InstallPath', null, null, $(RegistryView))) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Python\PythonCore\$(PythonTag)\InstallPath', null, null, $(RegistryView))) - $([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Python\PythonCore\$(PythonTag)\InstallPath', 'ExecutablePath', null, $(RegistryView))) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Python\PythonCore\$(PythonTag)\InstallPath', 'ExecutablePath', null, $(RegistryView))) - $(PythonHome)python.exe - $([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Python\PythonCore\$(PythonTag)\InstalledFeatures', 'dev', null, $(RegistryView))) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Python\PythonCore\$(PythonTag)\InstalledFeatures', 'dev', null, $(RegistryView))) - $([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Python\PythonCore\$(PythonTag)\InstalledFeatures', 'core_pdb', null, $(RegistryView))) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Python\PythonCore\$(PythonTag)\InstalledFeatures', 'core_pdb', null, $(RegistryView))) - $([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Python\PythonCore\$(PythonTag)\InstalledFeatures', 'core_d', null, $(RegistryView))) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Python\PythonCore\$(PythonTag)\InstalledFeatures', 'core_d', null, $(RegistryView))) - _d - $([System.IO.Path]::GetDirectoryName($(PythonExe)))\python$(PythonDebugSuffix).exe - $(PythonExe) - - - - WindowsLocalDebugger - PythonDebugLaunchProvider - - - - - - - - - - - - - - - - - - - - pydext$(PythonDebugSuffix) - .pyd - $(PythonDExe) - -i -c "print('>>> import pydext'); import pydext" - PYTHONPATH=$(OutDir) - $(DefaultDebuggerFlavor) - - - pydext - .pyd - $(PythonExe) - -i -c "print('>>> import pydext'); import pydext" - PYTHONPATH=$(OutDir) - $(DefaultDebuggerFlavor) - - - pydext$(PythonDebugSuffix) - .pyd - $(PythonDExe) - -i -c "print('>>> import pydext'); import pydext" - PYTHONPATH=$(OutDir) - $(DefaultDebuggerFlavor) - - - pydext - .pyd - $(PythonExe) - -i -c "print('>>> import pydext'); import pydext" - PYTHONPATH=$(OutDir) - $(DefaultDebuggerFlavor) - - - - Level3 - Disabled - MultithreadedDLL - D:\soft\Python35\include;$(PythonHome)Include;%(AdditionalIncludeDirectories) - true - - - true - $(PythonHome)libs;%(AdditionalLibraryDirectories) - - - - - Level3 - Disabled - MultithreadedDLL - $(PythonHome)Include;%(AdditionalIncludeDirectories) - true - - - true - $(PythonHome)libs;%(AdditionalLibraryDirectories) - - - - - Level3 - MaxSpeed - true - true - Multithreaded - D:\soft\Python35\include;$(PythonHome)Include;%(AdditionalIncludeDirectories) - true - - - true - true - true - libucrt.lib;%(IgnoreSpecificDefaultLibraries) - ucrt.lib;%(AdditionalDependencies) - $(PythonHome)libs;%(AdditionalLibraryDirectories) - - - - - Level3 - MaxSpeed - true - true - Multithreaded - $(PythonHome)Include;%(AdditionalIncludeDirectories) - true - - - true - true - true - libucrt.lib;%(IgnoreSpecificDefaultLibraries) - ucrt.lib;%(AdditionalDependencies) - $(PythonHome)libs;%(AdditionalLibraryDirectories) - - - - - - - - - - - - - - \ No newline at end of file diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj.filters" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj.filters" deleted file mode 100644 index 2b201335..00000000 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj.filters" +++ /dev/null @@ -1,22 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - Source Files - - - \ No newline at end of file diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj.user" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj.user" deleted file mode 100644 index be250787..00000000 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/pydext.vcxproj.user" +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/setup.py" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/setup.py" index 03ce03ee..233046e9 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/setup.py" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/setup.py" @@ -1,8 +1,8 @@ from distutils.core import setup, Extension - -module = Extension('pydext', sources = ['pydext.c']) - -setup (name = 'pydext', - version = '1.0.0', - description = 'This is pydext', - ext_modules = [module]) \ No newline at end of file + +module = Extension('pydext', sources=['pydext.c']) + +setup(name='pydext', + version='1.0.0', + description='This is pydext', + ext_modules=[module]) diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/test.py" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/test.py" index 3a3d8703..13062296 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/test.py" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/pydext/test.py" @@ -1,8 +1,8 @@ import sys sys.path.insert(0, - './build/lib.{0}-{1}.{2}'.format(sys.platform, sys.version_info.major, sys.version_info.minor)) - + './build/lib.{0}-{1}.{2}'.format(sys.platform, sys.version_info.major, + sys.version_info.minor)) import pydext diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/test.py" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/test.py" index 38c6394e..ecf23016 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/test.py" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/pydext/test.py" @@ -1,4 +1,5 @@ import sys + sys.path.insert(0, './Release') import pydext diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/CalSpecSpea.pyx" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/CalSpecSpea.pyx" index 669ea114..bd6e390f 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/CalSpecSpea.pyx" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/CalSpecSpea.pyx" @@ -20,8 +20,8 @@ def calspecaccel(np.ndarray[double, ndim=1, mode="c"] acc, int length, double dt cdef np.ndarray[double, ndim=1] Period = np.arange(0.0, maxPeriod + periodStep, periodStep) # 10.0 + 0.02, 0.02 Period[0] = 0.001 # 调用CalSpecSpeaLib.cpp定义的函数对数组进行处理 - cal_spec_accel( np.PyArray_DATA(acc), length, dt, maxPeriod, periodStep, dampRatio, - np.PyArray_DATA(Period), np.PyArray_DATA(Fre), - np.PyArray_DATA(MAcc), np.PyArray_DATA(MVel), + cal_spec_accel( np.PyArray_DATA(acc), length, dt, maxPeriod, periodStep, dampRatio, + np.PyArray_DATA(Period), np.PyArray_DATA(Fre), + np.PyArray_DATA(MAcc), np.PyArray_DATA(MVel), np.PyArray_DATA(MDis), numt) return Period, Fre, MAcc, MVel, MDis \ No newline at end of file diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/setup.py" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/setup.py" index eb42160a..0b689bf2 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/setup.py" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/setup.py" @@ -1,11 +1,10 @@ from distutils.core import setup, Extension -from Cython.Distutils import build_ext import numpy - +from Cython.Distutils import build_ext setup( cmdclass={'build_ext': build_ext}, ext_modules=[Extension("CalSpecSpea", sources=[ - "CalSpecSpea.pyx", "CalSpecSpeaLib.cpp"], language="c++", include_dirs=[numpy.get_include()])] + "CalSpecSpea.pyx", "CalSpecSpeaLib.cpp"], language="c++", include_dirs=[numpy.get_include()])] ) diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/test.py" "b/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/test.py" index c0781379..9ea580c5 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/test.py" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/pyx\345\222\214c++/test.py" @@ -4,7 +4,6 @@ sys.path.append( './build/lib.{0}-{1}.{2}'.format(sys.platform, sys.version_info.major, sys.version_info.minor)) - from CalSpecSpea import calspecaccel import matplotlib.pyplot as plt import numpy as np diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/pydmod.py" "b/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/pydmod.py" index 810a5bfb..50ed74bb 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/pydmod.py" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/pydmod.py" @@ -1,18 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Created on 2018年5月6日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: -# description: - -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 +""" +Created on 2018年5月6日 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 +@email: 892768447@qq.com +@file: +@description: +""" def sum(x, y): diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/setup.py" "b/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/setup.py" index d3cc302f..b18bc64b 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/setup.py" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/setup.py" @@ -2,7 +2,6 @@ from Cython.Distutils import build_ext - setup( cmdclass={'build_ext': build_ext}, ext_modules=[Extension("pydmod", sources=["pydmod.py"])] diff --git "a/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/test.py" "b/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/test.py" index 3c451454..387314a5 100644 --- "a/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/test.py" +++ "b/Test/C\345\222\214C++\346\211\251\345\261\225/py\350\275\254pyd/test.py" @@ -1,7 +1,10 @@ import sys + sys.path.insert(0, - './build/lib.{0}-{1}.{2}'.format(sys.platform, sys.version_info.major, sys.version_info.minor)) + './build/lib.{0}-{1}.{2}'.format(sys.platform, sys.version_info.major, + sys.version_info.minor)) import pydmod + print(pydmod) print(pydmod.sum(1, 5)) diff --git a/Test/Network/README.md b/Test/Network/README.md index b6bbec5c..2aeccb1c 100644 --- a/Test/Network/README.md +++ b/Test/Network/README.md @@ -1,43 +1,46 @@ # 网络 ## [1、控制小车](控制小车/) + 通过TCP连接树莓派控制小车的简单例子 需求: - - 通过TCP连接到树莓派控制小车前后左右 - - 前进:0-100, 发送命令为F:2 - - 后退:0-100, 发送命令为B:2 - - 向左:32-42, 发送命令为L:2 - - 向右:42-52, 发送命令为R:2 +- 通过TCP连接到树莓派控制小车前后左右 +- 前进:0-100, 发送命令为F:2 +- 后退:0-100, 发送命令为B:2 +- 向左:32-42, 发送命令为L:2 +- 向右:42-52, 发送命令为R:2 注意: - - 这里只用了UI文件做界面,并没有转换为python代码 - - server.py只是做个本地echo服务器用来测试命令是否正常,依赖`tornado`库,可以通过`pip install tornado`来安装 - - 另外需要做粘包处理,以(\n)作为粘包符 - - 由于wifi能力不行,发送图片要尽量小 +- 这里只用了UI文件做界面,并没有转换为python代码 +- server.py只是做个本地echo服务器用来测试命令是否正常,依赖`tornado`库,可以通过`pip install tornado`来安装 +- 另外需要做粘包处理,以(\n)作为粘包符 +- 由于wifi能力不行,发送图片要尽量小 说明: - - `QTcpSocket.connected` 服务连接成功后触发该信号 - - `QTcpSocket.disconnected` 服务器丢失连接触发该信号 - - `QTcpSocket.readyRead` 服务器返回数据触发该信号 - - `QTcpSocket.error` 连接报错触发该信号(连接超时、服务器断开等等) +- `QTcpSocket.connected` 服务连接成功后触发该信号 +- `QTcpSocket.disconnected` 服务器丢失连接触发该信号 +- `QTcpSocket.readyRead` 服务器返回数据触发该信号 +- `QTcpSocket.error` 连接报错触发该信号(连接超时、服务器断开等等) 目前暂未修复接收图片异,原因在于`readyRead`中没有判断数据长度进行多次接收(类似粘包处理) ![截图](控制小车/ScreenShot/控制小车.png) ## [2、窗口配合异步Http](窗口配合异步Http/) + `asyncio`结合PyQt例子 1. 依赖库: - 1. `quamash`(对QT事件循环的封装替换):https://github.com/harvimt/quamash - 2. `asyncio`:https://docs.python.org/3/library/asyncio.html - 3. `aiohttp`:https://aiohttp.readthedocs.io/en/stable/ + 1. `quamash`(对QT事件循环的封装替换): + 2. `asyncio`: + 3. `aiohttp`: 2. 在创建`QApplication`后随即设置替换事件循环loop + ```python app = QApplication(sys.argv) loop = QEventLoop(app) @@ -67,4 +70,4 @@ Window  →→  initSession(初始化session) 添加到界面  ←←  _doDownloadImage(对单张图片进行下载) -![截图](窗口配合异步Http/ScreenShot/窗口配合异步Http.gif) \ No newline at end of file +![截图](窗口配合异步Http/ScreenShot/窗口配合异步Http.gif) diff --git "a/Test/Network/\346\216\247\345\210\266\345\260\217\350\275\246/server.py" "b/Test/Network/\346\216\247\345\210\266\345\260\217\350\275\246/server.py" index 2a642614..485aefb7 100644 --- "a/Test/Network/\346\216\247\345\210\266\345\260\217\350\275\246/server.py" +++ "b/Test/Network/\346\216\247\345\210\266\345\260\217\350\275\246/server.py" @@ -10,26 +10,11 @@ from tornado.options import options, define from tornado.tcpserver import TCPServer - try: import RPi.GPIO as GPIO # @UnusedImport @UnresolvedImport except: pass - -# Created on 2018年4月18日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: server -# description: -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - - define("port", default=8888, help="TCP port to listen on") logger = logging.getLogger(__name__) @@ -41,7 +26,6 @@ class EchoServer(TCPServer): - IMAGE = None def __init__(self, cap, *args, **kwargs): @@ -104,7 +88,7 @@ def handle_stream(self, stream, address): while True: try: data = yield stream.read_until(b"\n") -# logger.info("Received bytes: %s", data) + # logger.info("Received bytes: %s", data) if not data.endswith(b"\n"): data = data + b"\n" if data == b'getimage\n' and self.cap and self.cap.isOpened(): diff --git "a/Test/Network/\346\216\247\345\210\266\345\260\217\350\275\246/\346\216\247\345\210\266\345\260\217\350\275\246.py" "b/Test/Network/\346\216\247\345\210\266\345\260\217\350\275\246/\346\216\247\345\210\266\345\260\217\350\275\246.py" index e4b184e0..6e5b4310 100644 --- "a/Test/Network/\346\216\247\345\210\266\345\260\217\350\275\246/\346\216\247\345\210\266\345\260\217\350\275\246.py" +++ "b/Test/Network/\346\216\247\345\210\266\345\260\217\350\275\246/\346\216\247\345\210\266\345\260\217\350\275\246.py" @@ -7,21 +7,7 @@ from PyQt5.QtWidgets import QWidget -# Created on 2018年4月18日 -# author: Irony -# site: https://pyqt5.com , https://github.com/892768447 -# email: 892768447@qq.com -# file: ControlCar -# description: -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = 'Copyright (c) 2018 Irony' -__Version__ = 1.0 - - class ControlCar(QWidget): - HOST = '127.0.0.1' PORT = 8888 @@ -156,6 +142,7 @@ def sendData(self, ver, data): if __name__ == '__main__': import sys from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) w = ControlCar() w.show() diff --git "a/Test/Network/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http/requirements.txt" "b/Test/Network/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http/requirements.txt" new file mode 100644 index 00000000..a17d8ae6 --- /dev/null +++ "b/Test/Network/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http/requirements.txt" @@ -0,0 +1 @@ +quamash \ No newline at end of file diff --git "a/Test/Network/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http.py" "b/Test/Network/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http.py" index 4885b420..b3d63f93 100644 --- "a/Test/Network/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http.py" +++ "b/Test/Network/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http/\347\252\227\345\217\243\351\205\215\345\220\210\345\274\202\346\255\245Http.py" @@ -4,26 +4,34 @@ """ Created on 2018年10月24日 @author: Irony -@site: https://github.com/892768447 +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: AsyncioUiClient @description: """ import asyncio +import os -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap, QMovie -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton,\ - QApplication, QListWidget, QListWidgetItem, QLabel, QMessageBox import aiohttp -from quamash import QEventLoop +try: + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QPixmap, QMovie + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication, QListWidget, QListWidgetItem, \ + QLabel, QMessageBox +except ImportError: + from PySide2.QtCore import Qt + from PySide2.QtGui import QPixmap, QMovie + from PySide2.QtWidgets import QWidget, QVBoxLayout, QPushButton, QApplication, QListWidget, \ + QListWidgetItem, \ + QLabel, QMessageBox + + os.environ['QUAMASH_QTIMPL'] = 'PySide2' + from PySide2 import QtGui + + setattr(QtGui, 'QApplication', QApplication) -__Author__ = """By: Irony -QQ: 892768447 -Email: 892768447@qq.com""" -__Copyright__ = "Copyright (c) 2018 Irony" -__Version__ = "Version 1.0" +from quamash import QEventLoop Url = 'https://www.doutula.com/api/search?keyword=%E6%9C%80%E6%96%B0%E8%A1%A8%E6%83%85&mime=0&page={}' Headers = { @@ -67,6 +75,7 @@ async def _initSession(): # 初始化session self.session = aiohttp.ClientSession(loop=loop) print(self.session) + asyncio.ensure_future(_initSession(), loop=loop) async def _doDownloadImage(self, url): @@ -131,8 +140,9 @@ def closeEvent(self, event): import sys import cgitb import os + os.makedirs('tmp', exist_ok=True) - sys.excepthook = cgitb.enable(1, None, 5, 'text') + cgitb.enable(format='text') app = QApplication(sys.argv) loop = QEventLoop(app) asyncio.set_event_loop(loop) 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/HotKey.py" "b/Test/\345\205\250\345\261\200\347\203\255\351\224\256/HotKey.py" index 0d2d2409..b9b2f286 100644 --- "a/Test/\345\205\250\345\261\200\347\203\255\351\224\256/HotKey.py" +++ "b/Test/\345\205\250\345\261\200\347\203\255\351\224\256/HotKey.py" @@ -1,29 +1,27 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年12月11日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: HotKey @description: -''' +""" import sys -from PyQt5.QtCore import pyqtSignal, Qt -from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QTextBrowser, QPushButton,\ - QMessageBox import keyboard - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +try: + from PyQt5.QtCore import pyqtSignal, Qt + from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QTextBrowser, QPushButton, QMessageBox +except ImportError: + from PySide2.QtCore import Signal as pyqtSignal, Qt + from PySide2.QtWidgets import QWidget, QApplication, QVBoxLayout, QTextBrowser, QPushButton, QMessageBox class Window(QWidget): - dialogShow = pyqtSignal() def __init__(self, *args, **kwargs): @@ -51,7 +49,8 @@ def __init__(self, *args, **kwargs): 'ctrl+alt+del', lambda: self.logView.append('😏😏我知道你按了任务管理器😏😏')) # 这个函数类似while True,由于这里有界面GUI的loop事件,可以达到类似的效果 -# keyboard.wait()#Block forever, like `while True`.== + + # keyboard.wait()#Block forever, like `while True`.== def onShow(self): """显示""" diff --git "a/Test/\345\205\250\345\261\200\347\203\255\351\224\256/README.md" "b/Test/\345\205\250\345\261\200\347\203\255\351\224\256/README.md" index f6db5d27..af9c9420 100644 --- "a/Test/\345\205\250\345\261\200\347\203\255\351\224\256/README.md" +++ "b/Test/\345\205\250\345\261\200\347\203\255\351\224\256/README.md" @@ -2,12 +2,13 @@ pip install keyboard -https://github.com/892768447/keyboard + * keyboard - * 该模块使用全局低级钩子的方式hook键盘来处理,对系统有一定的影响 - * 有反映说弹出对话框假死,这里粗略解决下使用信号槽的方式来弹出对话框 - * 该模块里使用了每次产生一个子线程来回调函数 + * 该模块使用全局低级钩子的方式hook键盘来处理,对系统有一定的影响 + * 有反映说弹出对话框假死,这里粗略解决下使用信号槽的方式来弹出对话框 + * 该模块里使用了每次产生一个子线程来回调函数 + ``` def call_later(fn, args=(), delay=0.001): """ @@ -20,4 +21,5 @@ def call_later(fn, args=(), delay=0.001): ``` # 截图 -![截图](ScreenShot/1.gif) \ No newline at end of file + +![截图](ScreenShot/1.gif) diff --git "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/README.md" "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/README.md" index 1adc33fb..300bc484 100644 --- "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/README.md" +++ "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/README.md" @@ -1,6 +1,6 @@ # 自动更新 - - dist/mylibs1.zip 为版本一的文件 - - dist/mylibs2.zip 为版本二的文件 +- dist/mylibs1.zip 为版本一的文件 +- dist/mylibs2.zip 为版本二的文件 -运行演示后,再次演示。需要把mylibs1.zip中的文件解压出来替换 \ No newline at end of file +运行演示后,再次演示。需要把mylibs1.zip中的文件解压出来替换 diff --git "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/mylibs/testlibs.py" "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/mylibs/testlibs.py" index 431226ea..6cd81036 100644 --- "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/mylibs/testlibs.py" +++ "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/mylibs/testlibs.py" @@ -1,18 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年5月7日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: 自动更新.mylibs.testlibs @description: -''' - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +""" def version(): diff --git "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/setup.py" "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/setup.py" index 6e994728..7bca2a0d 100644 --- "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/setup.py" +++ "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/setup.py" @@ -1,13 +1,11 @@ -from distutils.core import setup import glob import os import py_compile import site import sys +from distutils.core import setup from zipfile import ZipFile, ZIP_DEFLATED -import py2exe # @UnusedImport - sys.argv.append("py2exe") sitepackages = site.getsitepackages()[1] diff --git "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/test.py" "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/test.py" index b70643a7..dd7a40a9 100644 --- "a/Test/\350\207\252\345\212\250\346\233\264\346\226\260/test.py" +++ "b/Test/\350\207\252\345\212\250\346\233\264\346\226\260/test.py" @@ -1,26 +1,22 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2017年5月7日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: 自动更新.test @description: -''' +""" import sys + sys.path.append("mylibs") import os from time import sleep from zipfile import ZipFile -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2017 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - - def update(): # 更新 from mylibs import testlibs # @UnresolvedImport @@ -50,6 +46,7 @@ def main(): os.startfile(sys.executable) # 重启 sys.exit() # 退出本身 + if __name__ == "__main__": main() input("press any key exit") diff --git "a/Test/\350\207\252\345\256\232\344\271\211import/IronyImporter.py" "b/Test/\350\207\252\345\256\232\344\271\211import/IronyImporter.py" index 1a67eef0..d2b203f1 100644 --- "a/Test/\350\207\252\345\256\232\344\271\211import/IronyImporter.py" +++ "b/Test/\350\207\252\345\256\232\344\271\211import/IronyImporter.py" @@ -1,14 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月28日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: IronyImporter @description: -''' +""" import base64 import os import sys @@ -16,11 +16,6 @@ import xxtea # @UnresolvedImport - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" - KEY = base64.b85decode("HF5^hbNbOVOKM=(SB`7h") diff --git "a/Test/\350\207\252\345\256\232\344\271\211import/README.md" "b/Test/\350\207\252\345\256\232\344\271\211import/README.md" index 4ee4369a..a339481d 100644 --- "a/Test/\350\207\252\345\256\232\344\271\211import/README.md" +++ "b/Test/\350\207\252\345\256\232\344\271\211import/README.md" @@ -1,14 +1,17 @@ # 自定义import + 需要Python3.5.2(或者自行编译xxtea) 简单的了解了下import的原理 # 测试过程 - - 1.在src中编写一个test.py - - 2.通过build.py 利用xxtea加密src/test.py 到当前目录的test.irony文件 - - 3.运行main.py 进行测试 + +- 1.在src中编写一个test.py +- 2.通过build.py 利用xxtea加密src/test.py 到当前目录的test.irony文件 +- 3.运行main.py 进行测试 # 截图 + test.py ![test.py](ScreenShot/1.png) @@ -19,4 +22,4 @@ test.irony main.py -![main.py](ScreenShot/3.png) \ No newline at end of file +![main.py](ScreenShot/3.png) diff --git "a/Test/\350\207\252\345\256\232\344\271\211import/build.py" "b/Test/\350\207\252\345\256\232\344\271\211import/build.py" index 86aeba8b..21eeb3a1 100644 --- "a/Test/\350\207\252\345\256\232\344\271\211import/build.py" +++ "b/Test/\350\207\252\345\256\232\344\271\211import/build.py" @@ -1,23 +1,19 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月28日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: build @description: -''' +""" -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" import base64 import xxtea # @UnresolvedImport - KEY = base64.b85decode("HF5^hbNbOVOKM=(SB`7h") with open("src/test.py", "rb") as fi: diff --git "a/Test/\350\207\252\345\256\232\344\271\211import/main.py" "b/Test/\350\207\252\345\256\232\344\271\211import/main.py" index cccc6cbb..47ebb2e2 100644 --- "a/Test/\350\207\252\345\256\232\344\271\211import/main.py" +++ "b/Test/\350\207\252\345\256\232\344\271\211import/main.py" @@ -1,25 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月28日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: main @description: -''' - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +""" # 首先要引入importer -import IronyImporter # @UnresolvedImport @UnusedImport - # 测试开始 import test + print(test) print(dir(test)) print(test.test(1, 5)) # @UndefinedVariable diff --git "a/Test/\350\207\252\345\256\232\344\271\211import/src/test.py" "b/Test/\350\207\252\345\256\232\344\271\211import/src/test.py" index 07d5d45e..4049409f 100644 --- "a/Test/\350\207\252\345\256\232\344\271\211import/src/test.py" +++ "b/Test/\350\207\252\345\256\232\344\271\211import/src/test.py" @@ -1,18 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -''' +""" Created on 2018年1月28日 -@author: Irony."[讽刺] -@site: https://pyqt5.com , https://github.com/892768447 +@author: Irony +@site: https://pyqt.site , https://github.com/PyQt5 @email: 892768447@qq.com @file: test @description: -''' - -__Author__ = "By: Irony.\"[讽刺]\nQQ: 892768447\nEmail: 892768447@qq.com" -__Copyright__ = "Copyright (c) 2018 Irony.\"[讽刺]" -__Version__ = "Version 1.0" +""" def test(a, b): 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