如何在辅助显示器上全屏显示图像?
Posted
技术标签:
【中文标题】如何在辅助显示器上全屏显示图像?【英文标题】:How to display image on secondary monitor in full screen? 【发布时间】:2019-11-19 05:09:49 【问题描述】:如何使用 PyQt5/PySide 或任何其他 Python 库以全屏模式在辅助显示器上显示所需的图像?过去,我使用帧缓冲图像查看器(Fbi 和 Fbi improved)。但是,这种方法需要我使用 Linux。我更喜欢在 Windows 中工作,最好使用 Python 找到解决方案。
动机/背景
我正在研究基于 DLP 投影的 3D 打印流程。当我使用 HDMI 将 DLP 投影仪连接到我的 Windows PC 时,它显示为第二台显示器。我只想将这个辅助显示器 (DLP) 用于显示我想要的 3D 打印过程的图案图像(png、bmp 或 svg)。我想使用 Python 以编程方式控制正在显示的图像。 这是https://3dprinting.stackexchange.com/questions/1217/how-to-display-images-on-dlp-using-hdmi-for-3d-printing 的后续问题
部分解决方案和问题
下面的代码是一种可能的解决方案,但我不确定它是正确的还是最有效的方法。我发现了两种使用 PyQt5 的方法:1)使用闪屏,2)使用 QLabel。我的代码面临以下问题:
光标按预期隐藏,但是如果我不小心在辅助屏幕上单击鼠标,则启动屏幕会关闭。 如果我使用 QLabel 方法,我会看到出现白屏,然后我的图像被加载。从出现白屏到显示实际图像之间存在约 0.5-1 秒的明显延迟。 如果图像以高频率显示(例如:每 1 秒),则此代码无法正常工作。例如,在代码中将 total_loops=1 更改为 total_loops=25。使用启动画面方法时,我看到启动画面出现在主屏幕上,然后它移动到辅助屏幕。使用 QLabel 方法时,我看到的只是出现白屏,并且仅显示图像的最后一次迭代。此外,QLabel 的窗口在主屏幕上变为活动状态,并且在任务栏中可见。 如果我想显示视频而不是图像,我该如何处理?对于3D打印应用,解决方案需要满足以下要求:
辅助屏幕是 DLP 投影仪,它不应包含任何与操作系统相关的窗口/任务栏/等... 辅助屏幕上不应出现光标/鼠标或其他应用程序 图片/视频需要全屏显示 在副屏显示或更新图像时,主屏应无干扰。例如,辅助屏幕中的图像窗口不应将焦点从主屏幕中的当前活动窗口移开import time
start_time = time.time()
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QSplashScreen
from PyQt5.QtGui import QPixmap, QCursor
from PyQt5.QtCore import Qt
import os
app = QApplication(sys.argv)
total_loops = 1
for i in range(total_loops):
# https://doc.qt.io/qtforpython/index.html
# https://www.riverbankcomputing.com/static/Docs/PyQt5/module_index.html
s = app.screens()[1] # Get the secondary screen
# Display info about secondary screen
print('Screen Name: Size: x Available geometry x '.format(s.name(), s.size().width(), s.size().height(), s.availableGeometry().width(), s.availableGeometry().height()))
# Hide cursor from appearing on screen
app.setOverrideCursor(QCursor(Qt.BlankCursor)) # https://forum.qt.io/topic/49877/hide-cursor
# Select desired image to be displayed
pixmap = QPixmap('test.png')
# Splash screen approach
# https://doc.qt.io/qtforpython/PySide2/QtWidgets/QSplashScreen.html?highlight=windowflags
splash = QSplashScreen(pixmap) # Set the splash screen to desired image
splash.show() # Show the splash screen
splash.windowHandle().setScreen(s) # Set splash screen to secondary monitor https://***.com/a/30597458/4988010
splash.showFullScreen() # Show in splash screen in full screen mode
# # Qlabel apporach
# l = QLabel()
# l.setPixmap(pixmap)
# l.move(1920,0)
# l.show()
# l.windowHandle().setScreen(s) # https://***.com/a/30597458/4988010
# l.showFullScreen()
time.sleep(0.5)
end_time = time.time()
print('Execution time: ', end_time-start_time )
sys.exit(app.exec_())
【问题讨论】:
你使用的for循环不可能工作,因为它会阻塞qt的事件处理。time.sleep
也将被阻止。在循环内不断地重新创建窗口是一个非常糟糕的主意,而且完全没有必要。使用QSplashSctreen
也没什么意义,它不太适合您的用例。我建议您摆脱 for 循环,最初专注于全屏显示单个图像,该图像不会响应用户交互。 (与其他应用程序、任务栏等相关的问题对于 SO 来说是题外话,因为它们与编程无关,本身)。
有很多window attributes和window flags可以用来控制qt窗口的行为。平台之间的确切行为通常可能有很大差异,因此您可能需要进行一些试验。 (PS:你瞄准的基本设计和kiosk mode很像)。
【参考方案1】:
下面的代码是我的问题的一种可能的解决方案。我的解决方案假设 Qt 仅用于全屏显示图像,而不用于其余逻辑。因此,我不得不run the QT app in a secondary thread。这是因为在我运行函数app.exec_()
的那一刻,Qt 将持续运行一个事件循环,从而阻止我的其余不依赖于 Qt 的 Python 逻辑。我的理解是不建议在主线程之外运行QApplication
,因此我欢迎更有经验的用户发布更好的方法。
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QObject, pyqtSignal
import sys
import time
import threading
def main():
print('Step 1')
print(' Some logic here without QT')
print('Step 2')
print(' Launch QT app to run in background')
myapp = myImageDisplayApp()
print('Step 3')
print(' Continue some logic while QT running in background')
time.sleep(2)
print('Step 4')
print(' Update the displayed image in the QT app running in background')
myapp.emit_image_update('qt_test_static_1.png')
time.sleep(2)
print('Step 5')
print(' Update displayed image again')
myapp.emit_image_update('qt_test_static_2.png')
time.sleep(2)
class myImageDisplayApp (QObject):
# Define the custom signal
# https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html#the-pyqtslot-decorator
signal_update_image = pyqtSignal(str)
def __init__ (self):
super().__init__()
# Setup the seperate thread
# https://***.com/a/37694109/4988010
self.thread = threading.Thread(target=self.run_app_widget_in_background)
self.thread.daemon = True
self.thread.start()
def run_app_widget_in_background(self):
self.app = QApplication(sys.argv)
self.my_bg_qt_app = qtAppWidget(main_thread_object=self)
self.app.exec_()
def emit_image_update(self, pattern_file=None):
print('emit_image_update signal')
self.signal_update_image.emit(pattern_file)
class qtAppWidget (QLabel):
def __init__ (self, main_thread_object):
super().__init__()
# Connect the singal to slot
main_thread_object.signal_update_image.connect(self.updateImage)
self.setupGUI()
def setupGUI(self):
self.app = QApplication.instance()
# Get avaliable screens/monitors
# https://doc.qt.io/qt-5/qscreen.html
# Get info on selected screen
self.selected_screen = 0 # Select the desired monitor/screen
self.screens_available = self.app.screens()
self.screen = self.screens_available[self.selected_screen]
self.screen_width = self.screen.size().width()
self.screen_height = self.screen.size().height()
# Create a black image for init
self.pixmap = QPixmap(self.screen_width, self.screen_height)
self.pixmap.fill(QColor('black'))
# Create QLabel object
self.app_widget = QLabel()
# Varioius flags that can be applied to make displayed window frameless, fullscreen, etc...
# https://doc.qt.io/qt-5/qt.html#WindowType-enum
# https://doc.qt.io/qt-5/qt.html#WidgetAttribute-enum
self.app_widget.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus | Qt.WindowStaysOnTopHint)
# Hide mouse cursor
self.app_widget.setCursor(Qt.BlankCursor)
self.app_widget.setGeometry(0, 0, self.screen_width, self.screen_height) # Set the size of Qlabel to size of the screen
self.app_widget.setWindowTitle('myImageDisplayApp')
self.app_widget.setAlignment(Qt.AlignLeft | Qt.AlignTop) #https://doc.qt.io/qt-5/qt.html#AlignmentFlag-enum
self.app_widget.setPixmap(self.pixmap)
self.app_widget.show()
# Set the screen on which widget is on
self.app_widget.windowHandle().setScreen(self.screen)
# Make full screen
self.app_widget.showFullScreen()
def updateImage(self, pattern_file=None):
print('Pattern file given: ', pattern_file)
self.app_widget.clear() # Clear all existing content of the QLabel
self.pixmap = QPixmap(pattern_file) # Update pixmap with desired image
self.app_widget.setPixmap(self.pixmap) # Show desired image on Qlabel
if __name__ == "__main__":
main()
我还要感谢 @ekhumoro 向我指出 QWidget 属性/标志。
【讨论】:
为什么不直接启动一个新线程并在上面做你的工作,而 qt 事件循环在主线程上启动。【参考方案2】:您不应该在主线程之外运行 GUI,因为 Qt 不保证它可以正常工作,如 the docs 所示。您必须在另一个线程中执行其他繁重的任务,而不是在另一个线程中执行 GUI。
你必须改变你对经典顺序逻辑的方法,但你必须使用面向事件的编程,在事件之前采取行动,在 Qt 通过信号的情况下。
综合以上情况,解决办法是:
import sys
import time
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QThread, QTimer
from PyQt5.QtGui import QColor, QPixmap
from PyQt5.QtWidgets import QApplication, QLabel, QWidget
class TaskManager(QObject):
task3Finished = pyqtSignal()
task4Finished = pyqtSignal()
@pyqtSlot()
def task3(self):
print("Step 3")
print(" Continue some logic while QT running in background")
time.sleep(2)
self.task3Finished.emit()
@pyqtSlot()
def task4(self):
print("Step 4")
print(" Update the displayed image in the QT app running in background")
time.sleep(2)
self.task4Finished.emit()
class qtAppWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setupGUI()
def setupGUI(self):
self.app = QApplication.instance()
# Get avaliable screens/monitors
# https://doc.qt.io/qt-5/qscreen.html
# Get info on selected screen
self.selected_screen = 0 # Select the desired monitor/screen
self.screens_available = self.app.screens()
self.screen = self.screens_available[self.selected_screen]
self.screen_width = self.screen.size().width()
self.screen_height = self.screen.size().height()
# Create a black image for init
self.pixmap = QPixmap(self.screen_width, self.screen_height)
self.pixmap.fill(QColor("black"))
# Create QLabel object
self.app_widget = QLabel()
# Varioius flags that can be applied to make displayed window frameless, fullscreen, etc...
# https://doc.qt.io/qt-5/qt.html#WindowType-enum
# https://doc.qt.io/qt-5/qt.html#WidgetAttribute-enum
self.app_widget.setWindowFlags(
Qt.FramelessWindowHint
| Qt.WindowDoesNotAcceptFocus
| Qt.WindowStaysOnTopHint
)
# Hide mouse cursor
self.app_widget.setCursor(Qt.BlankCursor)
self.app_widget.setGeometry(
0, 0, self.screen_width, self.screen_height
) # Set the size of Qlabel to size of the screen
self.app_widget.setWindowTitle("myImageDisplayApp")
self.app_widget.setAlignment(
Qt.AlignLeft | Qt.AlignTop
) # https://doc.qt.io/qt-5/qt.html#AlignmentFlag-enum
self.app_widget.setPixmap(self.pixmap)
self.app_widget.show()
# Set the screen on which widget is on
self.app_widget.windowHandle().setScreen(self.screen)
# Make full screen
self.app_widget.show()
@pyqtSlot()
def on_task3_finished(self):
pixmap = QPixmap("qt_test_static_1.png")
self.app_widget.setPixmap(pixmap)
@pyqtSlot()
def on_task4_finished(self):
pixmap = QPixmap("qt_test_static_2.png")
self.app_widget.setPixmap(pixmap)
# quit application after to 2 secons
QTimer.singleShot(2 * 1000, QApplication.quit)
def main(args):
print("Step 1")
print(" Some logic here without QT")
print("Step 2")
print(" Launch QT app to run")
app = QApplication(args)
myapp = qtAppWidget()
thread = QThread()
thread.start()
manager = TaskManager()
# move the QObject to the other thread
manager.moveToThread(thread)
manager.task3Finished.connect(myapp.on_task3_finished)
manager.task3Finished.connect(manager.task4)
manager.task4Finished.connect(myapp.on_task4_finished)
# start task
QTimer.singleShot(0, manager.task3)
ret = app.exec_()
thread.quit()
thread.wait()
del thread, app
return ret
if __name__ == "__main__":
sys.exit(main(sys.argv))
【讨论】:
以上是关于如何在辅助显示器上全屏显示图像?的主要内容,如果未能解决你的问题,请参考以下文章
如何强制在 UIWebView 中播放的视频在 iPad 上全屏显示?