PySide2で他のアプリにオーバーレイするアプリケーションを作る (1)

この記事は[Takumi Akashiro ひとり Advent Calendar 2020](https://qiita.com/advent-calendar/2020/takumi_akashiro)の4日目の記事になります。

# 前置き
はい、[昨日](https://qiita.com/takumi_akashiro/private/c134715bef5106ea1f01 "PySide2で作成したウィンドウを半透過させる - Qiita")に引き続いて、
「別アプリケーションにオーバーレイさせるアプリケーション」に必要な技術を書いていきます。

そもオーバーレイするアプリをには何が必要なん？って話ですよね。

過去にも[Qiitaでオーバーレイフォームを作った方が記事を書いていて](https://qiita.com/kumaS-kumachan/items/ebbc7e9abefc610fb327 "Windowsでオーバーレイフォームを作った - Qiita"
)、そこから引用すると

> 方針
　オーバーレイは、以下のような特徴があります。
　それらを順番に実現します。
　・拡大・縮小とかの枠がない
　・タスクバーに表示されない
　・必要なところは半透明、必要ないところは透明
　・指定の位置にある
　・常に画面の前面にある
　・クリックは透過する

大体自分もこんな感じだと思ってるので、この方針を真似て作っていきます。

## 現状の確認

先に上がった特徴のうち、今の状況は

| 課題 | 備考 |
| :-- | :-- |
|:white_check_mark: 拡大・縮小とかの枠がない|WindowFlagsに「FramelessWindowHint」をセット|
|:white_check_mark: 必要なところは半透明、必要ないところは透明|「WA_TranslucentBackground」を有効にする|
|:white_check_mark: クリックは下のレイヤへ透過する|~~「WA_TranslucentBackground」を有効にする~~<br>「WA_TransparentForMouseEvents」を有効にする|
|:white_large_square: 常に画面の前面にある||
|:white_large_square: タスクバーに表示されない||
|:white_large_square: 指定の位置にある|常に対象アプリのウィンドウに追従させたい|

という感じですね。

--- 

このうち、「指定の位置にある」は下調べした感じ面倒なのはわかったので、
後日に回して、本日は以下の2つを解決したいと思います。

* 常に画面の前面にある
* タスクバーに表示されない


## 常に画面の前面にある

めちゃくちゃ簡単です。
WindowFlagにWindowStaysOnTopHintをセットするだけです。

```python

from PySide2 import QtWidgets
from PySide2 import QtCore, QtGui

class Window(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setWindowTitle("Sample")
        self.setFixedSize(480, 320)

        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)


if __name__ == "__main__":
    app = QtWidgets.QApplication()
    window = Window()
    window.show()
    exit(app.exec_())

```
![04_01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294868/a321cfe8-5f2a-9a0e-bd62-d0293a56d1cb.gif)


できましたね！

## タスクバーに表示されない

タスクバーから表示を消すのは、WindowFlagに「Tool」をセットするだけですので簡単ですね。

ただし、WindowFlagの「FramelessWindowHint」を組み合わせてしまうと、
プロセスを終了する手段がコマンドラインによるプロセスキルか、
タスクマネージャーに頼らざるを得ないため、以下の要素も必要となります。

* タスクトレイに表示される。
* タスクトレイからアプリケーションを終了させる。

そこで役に立つのが、「[QSystemTrayIcon](https://doc.qt.io/qt-5/qsystemtrayicon.html)」です。

QSystemTrayIconはタスクトレイ内のアイコンを扱うクラスです。

余談ですが、QSystemTrayIconのshowMessageメソッドを呼び出すと、以下のようにトースト通知も出せます。
(通常のアプリケーションだとタスクトレイに登録とかしないので、あまり実用性はないですが……)

![04_02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294868/0f579509-d85f-d67a-5f88-1ab8ffbb952c.gif)


アイコンにはコンテキストメニューを追加できるので、アプリケーションの終了処理を登録しておきます。
使ってみると以下のような感じです！

```python
#!python3
# encoding:utf-8

from PySide2 import QtWidgets
from PySide2 import QtCore
from PySide2 import QtGui

class Window(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setWindowFlags(QtCore.Qt.Tool|QtCore.Qt.FramelessWindowHint|QtCore.Qt.WindowStaysOnTopHint)

        # 撮影用に位置を変更
        self.setGeometry(1400, 850, 200, 200)

        label = QtWidgets.QLabel("Frame Less Window Sample", self)
        label.setGeometry(0, 0, 200, 200)
        label.setStyleSheet("background-color: #222; color: #EEE;")

class TaskTray_Icon(QtWidgets.QSystemTrayIcon):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        menu = QtWidgets.QMenu()

        quit_action = menu.addAction("Quit")
        quit_action.triggered.connect(self.__quit)

        # コンテキストメニューに作成したメニューをセット
        self.setContextMenu(menu)

        # NOTE: アイコンには通常はpngなどを指定するが、
        # サンプルなのでPixmapでもモックを作ってお茶を濁す。
        # icon = QtGui.QIcon('icon.png')
        pixmap = QtGui.QPixmap(QtCore.QSize(32, 32))
        pixmap.fill(QtGui.QColor("red"))
        icon = QtGui.QIcon(pixmap)

        self.setIcon(icon)

    def __quit(self):
        QtWidgets.QApplication.quit()


if __name__ == "__main__":
    app = QtWidgets.QApplication()
    
    # トレイアイコンを生成して表示。
    trayicon = TaskTray_Icon()

    trayicon.show()
    
    window = Window()
    window.show()
    exit(app.exec_())
```
![04_03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294868/beee07fd-248d-6033-c885-1163f353b9b3.gif)


## 締め

以上で今日の記事は終了です。
やったことを踏まえると、こんな感じですね。

| 課題 | 備考 |
| :-- | :-- |
|:white_check_mark: 拡大・縮小とかの枠がない|WindowFlagsに「FramelessWindowHint」をセット|
|:white_check_mark: 必要なところは半透明、必要ないところは透明|「WA_TranslucentBackground」を有効にする|
|:white_check_mark: クリックは下のレイヤへ透過する|~~「WA_TranslucentBackground」を有効にする~~<br>「WA_TransparentForMouseEvents」を有効にする|
|:white_check_mark: 常に画面の前面にある|WindowFlagsに「WindowStaysOnTopHint」をセット|
|:white_check_mark: タスクバーに表示されない|WindowFlagsに「Tool」をセット<br>QSystemTrayIconでタスクトレイに登録<br>コンテキストメニューからアプリケーションを終了|
|:white_large_square: 指定の位置にある|常に対象アプリのウィンドウに追従させたい|

まあ、いい感じにオーバーレイウィンドウに近づいてきましたね！

|評価ラベル|ランク(おそらく5段階)|
| --:| :--: |
|おすすめ度|★★|
|難易度|★|
|ニッチ|★★★★|
|汎用性|★★|
|OBSとAEとPS使って<br>説明用Gif作るのめんどい|ScreenToGifを使え|
|ScreenToGif|★★★★★★★★★★|

明日は遂にWinApiとか使いだすので、よろしくお願いします。
では、また明日！