#PYTHON3 #QT #PYSISE2 #TUTORIAL #STYLE #EVENT #TRAYICON

PySide #13: 視窗的一萬種樣子, QSystemTrayIcon, Frameless Window, MouseEvent

你的視窗不只是你的視窗

DekBan
Bucketing

--

Photo by Alex Block on Unsplash

有時候會覺得系統預設的邊框一直看都看膩了,想要換一下不同的樣貌,但我們也不是真的想要換系統的主題,再說…Mac跟Windows主題要換起來也是挺麻煩的,這時候我們就可以從應用程式的邊框下手了!一起讓我們的UI介面更靈活吧!

🪟 Frameless Window

啊哈!無邊框就是潮

在Qt中要修改外框是不太有機會的,但我們可以讓他消失!並且在Qt中時做這樣的功能是非常方便的!

事實上,Qt Window 有非常多的Flag可以去做設定,可以讓Window產生各種的效果,由於Flag族繁不及備載詳細的內容可以觀看官方Document,這邊我來介紹QFramelessWindowHint。

self._window.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)

這邊利用Window這個flag表示不管有沒有parent,這物件都會是一個視窗物件呈現,並用QFramelessWindowHint來去除邊框。

Frameless Window Demo

但是!人生就是有很多但是。
視窗變成不能移動了!!該怎麼辦咧?這時我們只好去實作QMainWindow的滑鼠事件(MouseEvent)來達成控制視窗的功能。

🖱️ mousePressEvent & mouseMoveEvent

Qt在python中實作Event其實是相對麻煩且有一些條件,且讓我詳細說明。

問題一:QMainWindow 是用uiLoader讀取,無法直接實作Event

因為我們是使用UiLoader來讀取ui檔案,並動態產生QMainWindow,因此沒辦法在我們自己建立的class中直接override QWidget的Event函式。

有一個作法是,將我們的class繼承自QWidget並實作Event,然後用下面這方式來串接QMainWindow,這做法可行,但想當詭異,也會衍生出一些不必要的問題。

self = self._window

應該要做的是使用官方提供的EventFilter,並用 installEventFilter()將其install到QMainWindow之上就完成了。

def setup_ui(self):
# In setup ui & After load .ui file
self._window.installEventFilter(self)
def eventFilter(self, obj, event):
if obj is self._window:
if event.type() == QtCore.QEvent.MouseButtonPress:
self.mousePressEvent(event)
elif event.type() == QtCore.QEvent.MouseMove:
self.mouseMoveEvent(event)
return super(MainWindow, self).eventFilter(obj, event)

問題二:Install 不上去?

嘿丟!就是裝不上去,原因是因為我們的主class並不是一個含有 eventFilter(event)函式的類別,因此我們要將我們的MainWindow繼承於QObject才行,這邊不使用QWidget因為我們的MainWindow實際上並不會真的顯示視窗,那是self._window 的職責。

class MainWindow(QObject):

Implement

再來就是要實作mouseEvent了!
要移動視窗,主要分為『點擊』、『拖曳』兩個動作,所以我們也要實作這兩個函式。

def mousePressEvent(self, event):
self._old_pos = event.globalPos(

def mouseMoveEvent(self, event):
delta_x = int(event.globalPos().x()) - self._old_pos.x()
delta_y = int(event.globalPos().y()) - self._old_pos.y()
self._window.move(
self._window.x() + delta_x, self._window.y() + delta_y)
self._old_pos = event.globalPos()

這樣就完成啦!

mouse event demo

📶 QSystemTrayIcon

除了無邊框的效果,我們對圖形應用程式也有一些要求,一個大概就是在開始列或是Dock中可以顯示Icon的圖片,而另一個呢就是希望可以在狀態列中顯示縮小的圖示,以及一些快速的小功能,甚至是推播一樣的訊息提示。

這時候我們就需要 Qt 為我們準備的QSystemTrayIcon了!

WindowIcon

不過首先,我們先來快速學習一下怎麼修改程式在Dock中的Icon,其實做法有三個,但是Mac就是很傲嬌的只能用其中一種才會有效果。

因此,以下介紹一下這個方法:

Application Icon Demo
# in main.pyapp.setWindowIcon(QIcon('./media/import.svg'))

我們比須對QApplication這個物件做設定,直接設定最上層APP的Icon這樣才能設定Dock中的圖片。

Implement

接下來我們回歸正題,來設定SystemTrayIcon讓程式可以顯示在狀態列。

首先我們要確定我們的作業系統支援System Tray的功能,並且社並一個defualt得Icon來顯示。

from PySide2.QtWidgets import QSystemTrayIcondef __init__(self, parent=None):
self._tray = QSystemTrayIcon(self._window)
if self._tray.isSystemTrayAvailable():
self._tray.setIcon(QIcon('./media/dekban.png'))
else:
self._tray = None

然後我們要設定我們要讓Tray被Trigger時會跑出哪些選項,這個部分需要使用QMenu來實作,這個沒教過可以看這邊 或是 Doc
我們可以實作各種不同的功能,就看需求了,在最後將menu加入到context menu就設定完成囉!

def set_tray(self):
menu = QMenu(self._window)
action_show = menu.addAction("Show/Hide")
action_show.triggered.connect(
lambda: self._window.hide()
if self._window.isVisible() else self._window.show())
action_quit = menu.addAction("Quit")
action_quit.triggered.connect(self._window.close)

self._tray.setContextMenu(menu)

然後我們要在initialize的最後做顯示並輸出SystemTrayIcon。

self._tray.show()

最後,我們要讓SystemTray可以自由更換Icon並且,用通知的方式跳出我們想要的提示訊息。

def set_buttons(self):
"""Setup buttons"""
self._window.send_btn.setText('Send Msg')
self._window.exit_btn.setText('Exit')

self._window.send_btn.setIcon(QIcon('./media/import.svg'))

self._window.send_btn.clicked.connect(self.send_message)
self._window.exit_btn.clicked.connect(self.exit)
def set_icon_combo(self):
"""Setup options in icon select combobox."
self._window.icon_combo.addItem(
QIcon('./media/font.png'), 'font')
self._window.icon_combo.addItem(
QIcon('./media/paint.png'), 'paint')
self._window.icon_combo.addItem(
QIcon('./media/dekban.png'), 'default')
self._window.icon_combo.currentIndexChanged.connect(
self.set_icon)
@QtCore.Slot(int)
def set_icon(self, index):
icon = self._window.icon_combo.itemIcon(index)
self._tray.setIcon(icon)

@QtCore.Slot()
def send_message(self):
title = self._window.title_line.text()
msg = self._window.msg_edit.toPlainText()

self._tray.showMessage(title, msg,
QIcon('./media/dekban.png'))

Result

Demo

Source Code

完整代碼請看 : Window Feature

結論

這一篇實作的東西滿多的,事實上很多東西做法跟C++做法不同,所以在轉換成python的時候也需要特別注意,另外有人會看到有教學說要把Dock的圖形也隱藏,使用Qt::Tool這個WindowFlag,事實上這個在Mac上會有問題的,可以看一下官方的解釋,這應該從系統端去做實作會比較正確。

Qt:Tool

我是夜海中的宅男DekBan,我們下次見,see ya next night.

Next Step : PySide #14:跳出原始的框框, Qdialog, QMessageBox

--

--

DekBan
Bucketing

เด็กบ้าน | 🌃夜裡溜搭的宅男,漂泊於月色鋪成的海