PyQt5 應用在 TeamViewer 下無法使用全屏模式
PyQt5 應用在 TeamViewer 下無法使用全屏模式
問題描述
使用 PyQt5 (版本為 5.7)中的 QtWebEngineView 構建的桌面應用,部署到遠端機器(Windows 7 平臺)上以全屏模式執行時,通過 teamviewer 觀察到遠端桌面沒有變化,但是滑鼠右鍵後會彈出選單選項,與正常開啟的 QtWebEngineView 彈出的選單選項一致。而且直接在遠端機器上執行的應用程式在顯示器上顯示正常,但是無法通過 teamviewer 遠端觀察。
部分程式碼:
view = QtWebEngineView() view.setWindowFlags(Qt.FramelessWindowHint) view.setUrl(QUrl("http://localhost:{}/".format(port))) view.show()
分析問題並解決新產生的問題
推測是 teamviewer 無法捕捉到應用全屏後的介面,使用 Print Screen 按鍵截圖後顯示的圖片也表示該應用的介面無法被捕捉。由於應用的使用場景需要,應用必須全屏,執行時不能漏出 Windows 的工作列。所以嘗試了多種解決方案:
view.showFullScreen()這種方法嘗試過,但很快就發現有問題,並且不僅僅是 PyQt5 構建的應用無法遠端監控,包括直接使用 Qt C++ 的程式碼編譯的程式也無法通過遠端監控。推測是 QWebEngineView 的原因導致的。- 根據目標機器的螢幕解析度,調整應用視窗的大小,比如目標機器的解析度為 1024x768,在高度上減少一個畫素,使應用視窗為 1024x767,然後使用
view.show()
第二種方法是可行的,但它仍然存在一個問題,Windows 桌面底部的工作列會把應用程式的介面擋住。為了讓這個應用程式介面置頂,需要設定視窗的標誌位為 Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
。
然後程式就可以正常運行了。但是新的問題又出現了,使用了 Qt.WindowStaysOnTopHint
標誌位後,程式確實能夠置頂,並且不會被工作列擋住,但是當使用 alt + tab 組合鍵切換程式時,其他程式也會被該應用遮擋住,導致無法顯示。(在實際使用中沒有關係,但是在開發過程中有些頭疼)
為了保持應用視窗的置頂效果,並使切換應用時,PyQt 應用不再置頂,可以定義一個 QWebEngineView 的子類,重寫 changeEvent
方法:
class MyWebView(QWebEngineView):
def __init__(self, app):
super(MyWebView, self).__init__()
self.app = app
def changeEvent(self, event):
if event.type() == QEvent.ActivationChange:
active = self.isActiveWindow()
if active:
self.setWindowFlags( self.windowFlags() | Qt.WindowStaysOnTopHint )
if not self.isVisible():
self.show()
self.activateWindow()
else:
self.setWindowFlags( self.windowFlags() & ~Qt.WindowStaysOnTopHint )
if not self.isVisible():
self.show()
在 changeEvent
中,處理視窗啟用事件,當視窗處於啟用狀態時,在標誌位中增加 Qt.WindowStaysOnTopHint
,否則就去除這個標誌位。當視窗的標誌位改變時,視窗將會處於不可見的狀態(可以列印 self.isVisible()
的值檢視),所以需要再次呼叫 self.show()
方法顯示視窗。
這裡有個很坑的地方,如果不設定視窗的屬性,呼叫 show() 方法後會自動啟用視窗,即 self.isActiveWindow()
會返回 True,首次開啟應用時這是理所應當的。但是當切換幾次後就會發現視窗將會永遠置頂(或者沒法置頂了)。分析一下 changeEvent
的程式碼流程就清楚了。
永遠置頂的情況是:
- 首次開啟應用時,視窗處於 visible、active 狀態;
- 切換應用時,此時將會觸發 changeEvent 事件,呼叫 MyWebView 例項的
changeEvent()
方法,判斷當前視窗是非啟用狀態的,因此修改標誌位為self.windowFlags() & ~Qt.WindowStaysOnTopHint
,修改標誌位後重新顯示視窗,呼叫self.show()
方法,顯示視窗。 - 由於
show()
方法呼叫後,會自動啟用視窗,Qt 的事件迴圈機制導致changeEvent()
方法再次被呼叫,此時視窗處於啟用狀態,將重新修改標誌位為self.windowFlags() | Qt.WindowStaysOnTopHint
,使得切換視窗後,其他程式仍然會被本應用遮擋。
永不置頂的情況是最後不論是哪種狀態,標誌位都會被改成不含 Qt.WindowStaysOnTopHint 的,因此就永遠無法置頂了。
為了解決這個問題,就必須要將 show()
的呼叫和視窗啟用分開,為此需要設定視窗的屬性為:
view.setAttribute( Qt.WA_ShowWithoutActivating )
這樣就可以實現在啟用應用時視窗置頂,切換到其他程式時,應用視窗不置頂(如果其他應用顯示的視窗下,可以看到本應用將堆疊在其他程式下面)。
關閉程式
上述程式碼解決了視窗置頂和顯示其他程式時不置頂的需求,但是當你開啟工作列想要關閉程式的時候,發現程式關不掉了。只好重寫 closeEvent()
方法:
def closeEvent(self, event):
self.activateWindow()
self.destroy()
self.app.quit()
from time import sleep
sleep(1)
sys.exit(0)
當點選視窗關閉按鈕(實際上是沒有的,因為會被設定成無邊框視窗),或在工作列中點選右鍵關閉時,程式將會終止,退出 Python 程序。
其他值得提及的部分
在主函式中,有行程式碼是特意加上去的:
view.settings().setAttribute( QWebEngineSettings.Accelerated2dCanvasEnabled, False)
這是因為遠端機器沒有顯示卡,因此無法使用顯示卡加速功能,但 QWebEngineView 如果預設會啟用 QWebEngineSettings.Accelerated2dCanvasEnabled
這個屬性,導致程式在遠端機器上執行的時候,圖表顯示的功能會出現異常。因此需要顯式關閉該屬性。
小結
Qt 的介面為 Python 桌面應用開發帶來了很多方便,但是由於環境的差異導致程式會出現一些不可名狀的異常。在資料比較少的情況下,自己嘗試摸索出程式碼的執行流程並且給出解決方案其實是挺耗時間的。不過好在最終成功解決了問題。
參考程式碼可在 github 上獲取。