From 7c396a9a40eeddcb2fa5e2697ac7a57fd8b2198f Mon Sep 17 00:00:00 2001 From: Irony <892768447@qq.com> Date: Tue, 13 Apr 2021 14:40:32 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E5=B1=8F=E5=B9=95=E5=8F=98=E5=8C=96?= =?UTF-8?q?=E7=9B=91=E5=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Demo/README.md | 10 +- Demo/ScreenNotify.py | 58 ++++++++++++ Demo/ScreenShot/ScreenNotify.png | Bin 0 -> 6270 bytes QtRemoteObjects/SyncUi/ClipboardMaster.py | 105 +++++++++++++++++++++ QtRemoteObjects/SyncUi/ClipboardSlave.py | 107 ++++++++++++++++++++++ README.md | 1 + Test/ColumnView.py | 48 ++++++++++ 7 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 Demo/ScreenNotify.py create mode 100644 Demo/ScreenShot/ScreenNotify.png create mode 100644 QtRemoteObjects/SyncUi/ClipboardMaster.py create mode 100644 QtRemoteObjects/SyncUi/ClipboardSlave.py create mode 100644 Test/ColumnView.py diff --git a/Demo/README.md b/Demo/README.md index 31e582c..3bd9406 100644 --- a/Demo/README.md +++ b/Demo/README.md @@ -23,6 +23,7 @@ - [判断信号是否连接](#20判断信号是否连接) - [调用虚拟键盘](#21调用虚拟键盘) - [动态忙碌光标](#22动态忙碌光标) + - [屏幕变动监听](#23屏幕变动监听) ## 1、重启窗口Widget [运行 RestartWindow.py](RestartWindow.py) @@ -223,4 +224,11 @@ PyQt 结合 Opencv 进行人脸检测; 通过定时器不停的修改光标图片来实现动态效果 -![GifCursor](ScreenShot/GifCursor.gif) \ No newline at end of file +![GifCursor](ScreenShot/GifCursor.gif) + +## 23、屏幕变动监听 +[运行 ScreenNotify.py](ScreenNotify.py) + +通过定时器减少不同的变化信号,尽量保证只调用一次槽函数来获取信息 + +![ScreenNotify](ScreenShot/ScreenNotify.png) \ No newline at end of file diff --git a/Demo/ScreenNotify.py b/Demo/ScreenNotify.py new file mode 100644 index 0000000..33c29cb --- /dev/null +++ b/Demo/ScreenNotify.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/4/13 +@author: Irony +@site: https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ScreenNotify +@description: 屏幕、分辨率、DPI变化通知 +""" +import sys + +from PyQt5.QtCore import QTimer, QRect +from PyQt5.QtWidgets import QApplication, QPlainTextEdit, qApp + + +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) # **重要** 保证多次信号尽量少的调用函数 + + # 主要是多屏幕->无屏幕->有屏幕 + qApp.primaryScreenChanged.connect(lambda _: self.m_timer.start(1000)) + # 其它信号最终基本上都会调用该信号 + qApp.primaryScreen().virtualGeometryChanged.connect(lambda _: self.m_timer.start(1000)) + # DPI变化 + qApp.primaryScreen().logicalDotsPerInchChanged.connect(lambda _: self.m_timer.start(1000)) + + def onSolutionChanged(self): + # 获取主屏幕 + screen = qApp.primaryScreen() + if self.m_rect == screen.availableVirtualGeometry(): + return + self.m_rect = screen.availableVirtualGeometry() + # 所有屏幕可用大小 + self.appendPlainText('\navailableVirtualGeometry: {0}'.format(str(screen.availableVirtualGeometry()))) + # 获取所有屏幕 + screens = qApp.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/ScreenNotify.png b/Demo/ScreenShot/ScreenNotify.png new file mode 100644 index 0000000000000000000000000000000000000000..e95330cca5fbf6d60fd5e77cc7cf60b4e7d975da GIT binary patch literal 6270 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 literal 0 HcmV?d00001 diff --git a/QtRemoteObjects/SyncUi/ClipboardMaster.py b/QtRemoteObjects/SyncUi/ClipboardMaster.py new file mode 100644 index 0000000..feefebb --- /dev/null +++ b/QtRemoteObjects/SyncUi/ClipboardMaster.py @@ -0,0 +1,105 @@ +#!/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: +""" +from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, QLoggingCategory, QVariant, QMimeData +from PyQt5.QtRemoteObjects import QRemoteObjectHost +from PyQt5.QtWidgets import QTextBrowser + +__Author__ = 'Irony' +__Copyright__ = 'Copyright (c) 2019 Irony' +__Version__ = 1.0 + +import sys + + +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(1, None, 5, '') + 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 0000000..bfe9b60 --- /dev/null +++ b/QtRemoteObjects/SyncUi/ClipboardSlave.py @@ -0,0 +1,107 @@ +#!/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 + +__Author__ = 'Irony' +__Copyright__ = 'Copyright (c) 2019 Irony' +__Version__ = 1.0 + + +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(1, None, 5, '') + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + w = WindowSlave() + w.show() + sys.exit(app.exec_()) diff --git a/README.md b/README.md index 6405705..e4ac617 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ https://pyqt.site 论坛是专门针对PyQt5学习和提升开设的网站,分 - [判断信号是否连接](Demo/IsSignalConnected.py) - [调用虚拟键盘](Demo/CallVirtualKeyboard.py) - [动态忙碌光标](Demo/GifCursor.py) + - [屏幕变动监听](Demo/ScreenNotify.py) # QQ群 diff --git a/Test/ColumnView.py b/Test/ColumnView.py new file mode 100644 index 0000000..5ce9859 --- /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: +""" + +from PyQt5.QtWidgets import QComboBox, QFileSystemModel, QHBoxLayout, QSpacerItem, QSizePolicy + +__Author__ = 'Irony' +__Copyright__ = 'Copyright (c) 2020' +__Version__ = 'Version 1.0' + + +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 + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + w = PathComboBox() + w.show() + w.addWidget(PathComboBox(w, is_item=True)) + sys.exit(app.exec_()) From 9c6ede1772c1d87696b35afe5c22b88800c3967a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=BDoo=E6=82=BE=E7=B5=94=E2=84=85o=E3=80=82?= <892768447@qq.com> Date: Tue, 13 Apr 2021 15:27:58 +0800 Subject: [PATCH 2/6] Badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e4ac617..ce86710 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Blog](https://img.shields.io/badge/blog-pyqt5-green.svg)](https://pyqt5.com) [![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学习和提升开设的网站,分享大家平时学习中记录的笔记和例子,以及对遇到的问题进行收集整理。 From 58b0ba4d80dd7a67d13f53c2b745dd50eeb9b3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=BDoo=E6=82=BE=E7=B5=94=E2=84=85o=E3=80=82?= <892768447@qq.com> Date: Tue, 13 Apr 2021 15:37:26 +0800 Subject: [PATCH 3/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce86710..30e16ab 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 各种各样的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) From 1e92577e2a2a376a6fb489f6f98be4195879bc1e Mon Sep 17 00:00:00 2001 From: Irony <892768447@qq.com> Date: Thu, 15 Apr 2021 13:19:40 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E8=8E=B7=E5=8F=96=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- QWebEngineView/GetRequestInfo.py | 122 +++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 QWebEngineView/GetRequestInfo.py diff --git a/QWebEngineView/GetRequestInfo.py b/QWebEngineView/GetRequestInfo.py new file mode 100644 index 0000000..0c1eaef --- /dev/null +++ b/QWebEngineView/GetRequestInfo.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2019年9月24日 +@author: Irony +@site: https://pyqt5.com https://github.com/892768447 +@email: 892768447@qq.com +@file: QWebEngineView.BlockAds +@description: 拦截请求 +""" +from PyQt5.QtCore import QUrl +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest +from PyQt5.QtWebEngineCore import QWebEngineUrlSchemeHandler, QWebEngineUrlScheme, \ + QWebEngineUrlRequestInterceptor +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile + +__Author__ = 'Irony' +__Copyright__ = 'Copyright (c) 2019' +__Version__ = 'Version 1.0' + + +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() + if method == b'GET': + self._manager.get(req) + 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: + 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(b'http') + o_https = QWebEngineUrlScheme.schemeByName(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(b'myurl', self.urlSchemeHandler) # for http + de.installUrlSchemeHandler(b'myurls', self.urlSchemeHandler) # for https + + +if __name__ == '__main__': + import sys + import os + import webbrowser + import cgitb + + cgitb.enable(format='text') + from PyQt5.QtWidgets import QApplication + + 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_()) From b81c8fcffee60d03e9577f4afc12264d629d7d93 Mon Sep 17 00:00:00 2001 From: Irony <892768447@qq.com> Date: Thu, 15 Apr 2021 14:19:25 +0800 Subject: [PATCH 5/6] add TODO --- QWebEngineView/GetRequestInfo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/QWebEngineView/GetRequestInfo.py b/QWebEngineView/GetRequestInfo.py index 0c1eaef..91b5105 100644 --- a/QWebEngineView/GetRequestInfo.py +++ b/QWebEngineView/GetRequestInfo.py @@ -45,8 +45,11 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler): 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) @@ -54,6 +57,8 @@ class UrlSchemeHandler(QWebEngineUrlSchemeHandler): 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) From 14a896a22e1f0fd24511c5c7e2241fc8b81709bf Mon Sep 17 00:00:00 2001 From: Irony <892768447@qq.com> Date: Thu, 15 Apr 2021 16:06:36 +0800 Subject: [PATCH 6/6] Image View --- QListView/ImageView.py | 236 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 QListView/ImageView.py diff --git a/QListView/ImageView.py b/QListView/ImageView.py new file mode 100644 index 0000000..2f3aa01 --- /dev/null +++ b/QListView/ImageView.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Created on 2021/4/15 +@author: Irony +@site: https://github.com/PyQt5 +@email: 892768447@qq.com +@file: ImageView +@description: +""" +import os + +from PyQt5.QtCore import QPointF, Qt, QRectF, QSizeF +from PyQt5.QtGui import QStandardItem, QStandardItemModel, QPainter, QColor, QImage, QPixmap +from PyQt5.QtWidgets import 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') + from PyQt5.QtWidgets import QApplication + + 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_())