PyQt6写的图片展示程序(针对macOS Retina)

This article is categorized as "Garbage" . It should NEVER be appeared in your search engine's results.


使用chatgpt 4

chatgpt 3.5一直没能成功get到我想要表达的重点在于 Retina,模糊 ,我试了好几个对话都没能拿到好用的代码。


严重的遗留问题

2023年6月,在准备发表这篇笔记前我又跑了一遍程序,突然发现一个很严重的问题:

相比于tkinter展示图片的程序,pyqt部分场景更清晰,部分场景更模糊,懒得分析具体原因了。不想搞了。

并且,目前我试过的所有图片里,pyqt的清晰度永远被preview.app压一头。

为什么会有这篇笔记

最初的原因

因为macOS的Retina屏幕,我发现tkinter展示某些图片莫名其妙的不清晰。

左边:pyqt6实现的Retina效果;右边:preview.app(仍然比pyqt6清晰)
使用tkinter或pyqt5,非常模糊,劣质画面

突然就出问题了

到了即将发这篇笔记的时候,我手痒了,找了2张chrome截图试了下程序,结果发现我的pyqt6不知怎么回事,比tkinter还要糊(当然,tkinter还是比不过preview.app):

懒得再跑程序截图了,两张测试的图片分别是:图片1图片2

代码1:展示1张图片


使用pyqt6,在macOS Retina屏幕下仍然表现良好的代码(这里缩放了图片,让图片显示在左半部分)

懒得让chatgpt帮我写获取屏幕大小的代码了,这里写死了 screen_width = 2560 .

只测试了“高度>宽度“的长图(从pdf里面搞出来的图),没有测试其他尺寸类型的图。

如果需要修改图片位置,则可以修改 view.setAlignment(Qt.AlignmentFlag.AlignLeft) 

如果需要修改屏幕为fullscreen,则可以修改 self.showMaximized() 

chatgpt给出的代码里zoom_percentage还是大了一倍,所以zoom_percentage被我多除了一个2

import sys

from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QImageReader, QPixmap, QIcon
from PyQt6.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QMainWindow, QVBoxLayout, QWidget


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Set window title and icon
        self.setWindowTitle('png Viewer')
        # self.setWindowIcon(QIcon(':/path/to/your/icon.png'))

        # Load PBM image
        image_file = './path/1.png'
        image_reader = QImageReader(image_file)
        image = image_reader.read()

        # Get image size
        image_size = image.size()
        print(f'Image size: {image_size.width()}x{image_size.height()}')

        # Calculate zoom percentage
        screen_width = 2560
        zoom_percentage = (screen_width / 2) / image_size.width() / 2
        #  chatgpt给出的代码里zoom_percentage还是大了一倍,所以zoom_percentage被我多除了一个2
        print(f'Zoom percentage: {zoom_percentage * 100}%')

        # Display image
        scene = QGraphicsScene()
        pixmap = QPixmap.fromImage(image)
        pixmap = pixmap.scaled(image_size * zoom_percentage, Qt.AspectRatioMode.KeepAspectRatio,
                               Qt.TransformationMode.SmoothTransformation)
        scene.addPixmap(pixmap)

        view = QGraphicsView(scene)
        view.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
        view.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
        view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        view.setAlignment(Qt.AlignmentFlag.AlignLeft)

        central_widget = QWidget()
        layout = QVBoxLayout()
        layout.addWidget(view)
        central_widget.setLayout(layout)
        self.setCentralWidget(central_widget)

        # Set the main window to full screen
        self.showMaximized()


if __name__ == '__main__':
    app = QApplication(sys.argv)

    window = MainWindow()
    window.show()

    sys.exit(app.exec())

代码2:展示2张图片

一段能够接受2个参数,分别左右显示图片的pyqt6程序,左边的图尽可能占用左半边屏幕,右边的图尽可能占用右半边屏幕,基于上面的程序略微修改而来:

代码里有个0.95是我加的,因为self.showMaximized()会留出顶部的title框

暂时写死了2张图片的路径

import sys

from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QImageReader, QPixmap, QIcon
from PyQt6.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QMainWindow, QHBoxLayout, QWidget


class MainWindow(QMainWindow):
    def __init__(self, image1_path, image2_path):
        super().__init__()

        # Set window title and icon
        self.setWindowTitle('PBM Viewer')
        # self.setWindowIcon(QIcon(':/path/to/your/icon.png'))

        # Load images
        image1 = QImageReader(image1_path).read()
        image2 = QImageReader(image2_path).read()

        # Calculate zoom percentage
        screen_width = 2560
        zoom_percentage1 = (screen_width / 2) / image1.width() / 2 * 0.95
        zoom_percentage2 = (screen_width / 2) / image2.width() / 2 * 0.95
        # 这个0.95是我加的,因为self.showMaximized()会留出顶部的title框

        # Create scenes
        scene1 = QGraphicsScene()
        pixmap1 = QPixmap.fromImage(image1).scaled(image1.size() * zoom_percentage1, Qt.AspectRatioMode.KeepAspectRatio,
                                                   Qt.TransformationMode.SmoothTransformation)
        scene1.addPixmap(pixmap1)

        scene2 = QGraphicsScene()
        pixmap2 = QPixmap.fromImage(image2).scaled(image2.size() * zoom_percentage2, Qt.AspectRatioMode.KeepAspectRatio,
                                                   Qt.TransformationMode.SmoothTransformation)
        scene2.addPixmap(pixmap2)

        # Create views
        view1 = QGraphicsView(scene1)
        view2 = QGraphicsView(scene2)

        for view in (view1, view2):
            view.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
            view.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
            view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
            view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
            view.setAlignment(Qt.AlignmentFlag.AlignLeft)

        # Set up layout
        central_widget = QWidget()
        layout = QHBoxLayout()
        layout.addWidget(view1)
        layout.addWidget(view2)
        central_widget.setLayout(layout)
        self.setCentralWidget(central_widget)

        # Set the main window to full screen
        self.showMaximized()


if __name__ == '__main__':
    app = QApplication(sys.argv)

    # if len(sys.argv) < 3:
    #     print('Usage: python <script_name> <image1_path> <image2_path>')
    #     sys.exit(1)

    image1_path = "./1.png"
    image2_path = "./2.png"

    window = MainWindow(image1_path, image2_path)
    window.show()

    sys.exit(app.exec())

代码3:动态更新展示的2张图片

假定我们正在运行上面的代码展示图片,但我们另一个进程(可能是python进程,也可能是其他语言写的进程)需要动态更新pyqt程序展示的2张图片。

注意:下面的代码懒得优化了,因为我目前就只会用监控文件的方法来更新图片,所以就暂时这么将就用了

注意:下面的代码是从项目里搬出来的,有些 参数/变量 是写死的,注意!

注意:需要准备这些文件和文件夹:

python运行文件:2个, test_pyqt.py  pyqt_controller.py 

图片文件夹: ./images/ ,写死在代码里了

蓝色过渡图片:浅蓝色的过渡图片 ./images/blue.png 

其他展示图片:初始化的2张图片为 ./images/1.png  ./images/2.png 

监控文件: ./monitor_images.txt 


文件1: test_pyqt.py 

# Save this as test_pyqt.py

import sys

from PyQt6 import QtGui
from PyQt6.QtCore import Qt, QSize
from PyQt6.QtGui import QImageReader, QPixmap, QIcon
from PyQt6.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QMainWindow, QHBoxLayout, QWidget


class MainWindow(QMainWindow):
    def __init__(self, image1_path, image2_path):
        super().__init__()

        # Set window title and icon
        self.setWindowTitle('PBM Viewer')
        # self.setWindowIcon(QIcon(':/path/to/your/icon.png'))

        # Load images
        image1 = QImageReader(image1_path).read()
        image2 = QImageReader(image2_path).read()

        # Calculate zoom percentage
        screen_width = 2560
        self.zoom_percentage1 = (screen_width / 2) / image1.width() / 2 * 0.95
        self.zoom_percentage2 = (screen_width / 2) / image2.width() / 2 * 0.95

        # Create scenes
        scene1 = QGraphicsScene()
        pixmap1 = QPixmap.fromImage(image1).scaled(image1.size() * self.zoom_percentage1, Qt.AspectRatioMode.KeepAspectRatio,
                                                   Qt.TransformationMode.SmoothTransformation)
        scene1.addPixmap(pixmap1)

        scene2 = QGraphicsScene()
        pixmap2 = QPixmap.fromImage(image2).scaled(image2.size() * self.zoom_percentage2, Qt.AspectRatioMode.KeepAspectRatio,
                                                   Qt.TransformationMode.SmoothTransformation)
        scene2.addPixmap(pixmap2)

        # Create views
        self.view1 = QGraphicsView(scene1)
        self.view2 = QGraphicsView(scene2)

        for view in (self.view1, self.view2):
            view.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
            view.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.FullViewportUpdate)
            view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
            view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
            view.setAlignment(Qt.AlignmentFlag.AlignLeft)

        # Set up layout
        central_widget = QWidget()
        layout = QHBoxLayout()
        layout.addWidget(self.view1)
        layout.addWidget(self.view2)
        central_widget.setLayout(layout)
        self.setCentralWidget(central_widget)

        # Set the main window to full screen
        self.showMaximized()

    def update_images(self, image1_path, image2_path):
        # Load images
        image1 = QImageReader(image1_path).read()
        image2 = QImageReader(image2_path).read()

        # Update views
        pixmap1 = QPixmap.fromImage(image1).scaled(image1.size() * self.zoom_percentage1,
                                                   Qt.AspectRatioMode.KeepAspectRatio,
                                                   Qt.TransformationMode.SmoothTransformation)
        pixmap2 = QPixmap.fromImage(image2).scaled(image2.size() * self.zoom_percentage2,
                                                   Qt.AspectRatioMode.KeepAspectRatio,
                                                   Qt.TransformationMode.SmoothTransformation)

        self.view1.scene().clear()
        self.view1.scene().addPixmap(pixmap1)

        self.view2.scene().clear()
        self.view2.scene().addPixmap(pixmap2)


def run(image1_path, image2_path):
    app = QApplication(sys.argv)
    window = MainWindow(image1_path, image2_path)
    window.show()
    return app, window

def start_app(app):
    sys.exit(app.exec())

文件2: pyqt_controller.py 

# Save this as pyqt_controller.py
import test_pyqt
from PyQt6.QtCore import QTimer
import sys


def read_image_paths_from_file(file_path):
    try:
        with open(file_path, 'r') as file:
            image1_path = file.readline().strip()
            image2_path = file.readline().strip()
            return image1_path, image2_path
    except Exception as e:
        print(f'Error reading image paths: {e}')
        return None, None


def update_images_periodically():
    global last_image1_path, last_image2_path, blue

    image1_path, image2_path = read_image_paths_from_file('monitor_images.txt')

    if blue:
        if image1_path and image2_path and (image1_path != last_image1_path) and (image2_path != last_image2_path):
            # print('更新两边blue')
            window.update_images('./images/blue.png', './images/blue.png')
            blue = False
        elif image1_path and image2_path and (image1_path != last_image1_path):
            # print('更新左边blue')
            window.update_images('./images/blue.png', image2_path)
            blue = False
        elif image1_path and image2_path and (image2_path != last_image2_path):
            # print('更新右边blue')
            window.update_images(image1_path, './images/blue.png')
            blue = False
    else:
        if image1_path and image2_path and (image1_path != last_image1_path or image2_path != last_image2_path):
            window.update_images(image1_path, image2_path)
            last_image1_path = image1_path
            last_image2_path = image2_path
            blue = True


if __name__ == "__main__":
    initial_image1_path = './images/image1.png'
    initial_image2_path = './images/image2.png'
    blue = True

    import os

    os.system("echo ''> ./monitor_images.txt")

    app, window = test_pyqt.run(initial_image1_path, initial_image2_path)

    last_image1_path, last_image2_path = initial_image1_path, initial_image2_path

    timer = QTimer()
    timer.timeout.connect(update_images_periodically)
    timer.start(1000)  # Check for updates every 1 second

    test_pyqt.start_app(app)

运行方法:

$ python3 pyqt_controller.py

此时会展示2张初始图片image1.png和image2.png

需要更新图片的时候,另一个进程对 monitor_images.txt 文件进行修改,修改第1行为 新图片a的路径 ,修改第2行为 新图片b的路径 ,比如:

./images/image3.png
./images/image4.png

代码里的pyqt监控函数QTimer()就会监控到文件变化(每隔1秒扫描文件 monitor_images.txt ),如果有更新就会更新。

更新的时候会根据具体情况选择短暂的蓝色幕布(过渡),比如:

 image1-image2 切换到 image3-image4 :左右都有蓝色幕布过渡

 image1-image2 切换到 image3-image2 :只有左边有蓝色幕布过渡

 image1-image2 切换到 image1-image3 :只有右边有蓝色幕布过渡


另一个补充:由于上面的代码是从现有项目里抠出来的,这段代码是新增加的:


Leave a Comment Anonymous comment is allowed / 允许匿名评论