0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

メモ) PyQt6による時計表示(時計ウィジェット)

0
Posted at

やること

 Copilotで、「画面上に常に表示できるオーバーレイ時計(PyQt6製)」のスクリプトを作成する。

結果

 こんな感じ。
スクリーンショット 2026-07-23 221221.png

スクリプト:423行  基本はCopilotで作成したそのまま。コメント部分が修正したところ。
overlay_clock.py
import sys
from datetime import datetime

from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtGui import QAction, QFont, QKeySequence
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMenu,
    QVBoxLayout,
    QWidget,
)


class OverlayClock(QWidget):
    """PyQt6版 オーバレイ時計"""

    def __init__(self):
        super().__init__()

        self.font_size = 48

        self.show_date = True
        self.show_wareki = True
        self.show_weekday = True
        self.show_seconds = True
        self.always_on_top = True

        self.text_color = "white"
        self.background_alpha = 50      # 80(31%) から、20%に変更

        self.drag_pos = None

        self.init_ui()
        self.init_shortcuts()

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_time)
        self.timer.start(1000)  # 200ms->1000msに変更

        self.update_time()

    def init_ui(self):
        """画面初期化"""

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

        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)

        self.label = QLabel()
        self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.setContentsMargins(10, 10, 10, 10)

        self.setLayout(layout)

        self.resize(500, 200)
        self.move(100, 100)

        self.update_style()

    def init_shortcuts(self):
        """キーボードショートカット設定"""

        enlarge = QAction(self)
        enlarge.setShortcut(QKeySequence("Ctrl+Up"))
        enlarge.triggered.connect(self.increase_size)
        self.addAction(enlarge)

        reduce = QAction(self)
        reduce.setShortcut(QKeySequence("Ctrl+Down"))
        reduce.triggered.connect(self.decrease_size)
        self.addAction(reduce)

        date_toggle = QAction(self)
        date_toggle.setShortcut(QKeySequence("Ctrl+D"))
        date_toggle.triggered.connect(self.toggle_date)
        self.addAction(date_toggle)

        top_toggle = QAction(self)
        top_toggle.setShortcut(QKeySequence("Ctrl+T"))
        top_toggle.triggered.connect(self.toggle_topmost)
        self.addAction(top_toggle)

    def update_style(self):
        """表示スタイル更新"""

        font = QFont(
#            "Yu Gothic UI",    # 元 
#            "BIZ UDゴシック",   # OK
            "MSゴシック",
            self.font_size,
            QFont.Weight.Bold,
        )

        self.label.setFont(font)

        self.label.setStyleSheet(
            f"""
            QLabel {{
                color: {self.text_color};
                background-color: rgba(0, 0, 0, {self.background_alpha});
                border-radius: 12px;
                padding: 10px;
            }}
            """
        )

    @staticmethod
    def get_japanese_era(dt: datetime) -> str:
        """和暦文字列を取得する。

        Args:
            dt: 変換対象日時。

        Returns:
            和暦形式の日付文字列。
        """
        year = dt.year

#        if year >= 2019:
        if dt >= datetime(2019, 5, 1):
            era = "令和"
            era_year = year - 2018
#        elif year >= 1989:
        elif dt >= datetime(1989, 1, 8):
            era = "平成"
            era_year = year - 1988
        elif year >= 1926:
            era = "昭和"
            era_year = year - 1925
        else:
            return ""

        year_text = "" if era_year == 1 else str(era_year)

        return f"{era}{year_text}{dt.month}{dt.day}"

    @staticmethod
    def get_weekday_text(dt):
        """曜日文字を取得する"""

        weekdays = (
            "",
            "",
            "",
            "",
            "",
            "",
            "",
        )

        return weekdays[dt.weekday()]

    def update_time(self):
        """時刻表示を更新する"""

        now = datetime.now()

        if self.show_seconds:
            time_text = now.strftime("%H:%M:%S")
        else:
            time_text = now.strftime("%H:%M")

        lines = []

        if self.show_date:
            weekday = self.get_weekday_text(now)

            if self.show_weekday:
                date_text = (
                    f"{now.year:04d}/"
                    f"{now.month:02d}/"
                    f"{now.day:02d}"
                    f"{weekday}"
                )
            else:
                date_text = (
                    f"{now.year:04d}/"
                    f"{now.month:02d}/"
                    f"{now.day:02d}"
                )

            lines.append(date_text)

            if self.show_wareki:
                lines.append(self.get_japanese_era(now))

            lines.append("")    # 1行空け
            lines.append(time_text)
        else:
            lines.append(time_text)

        self.label.setText("\n".join(lines))
        self.adjustSize()

    def increase_size(self):
        """文字サイズを大きくする"""

        self.font_size += 4
        self.update_style()
        self.update_time()

    def decrease_size(self):
        """文字サイズを小さくする"""

        if self.font_size > 12:
            self.font_size -= 4

        self.update_style()
        self.update_time()

    def toggle_date(self)-> None:
        """日付表示を切り替える"""

        self.show_date = not self.show_date
        self.update_time()

    def toggle_wareki(self) -> None:
        """和暦表示を切り替える"""

        self.show_wareki = not self.show_wareki
        self.update_time()

    def toggle_weekday(self) -> None:
        """曜日表示を切り替える"""

        self.show_weekday = not self.show_weekday
        self.update_time()

    def toggle_seconds(self):
        """秒表示を切り替える"""

        self.show_seconds = not self.show_seconds
        self.update_time()

    def toggle_topmost(self):
        """常に最前面表示を切り替える"""

        self.always_on_top = not self.always_on_top

        self.setWindowFlag(
            Qt.WindowType.WindowStaysOnTopHint,
            self.always_on_top,
        )

        self.show()

    def set_background_alpha(self, alpha):
        """背景透明度を設定する"""

        self.background_alpha = alpha
        self.update_style()

    def set_text_color(self, color):
        """文字色を設定する"""

        self.text_color = color
        self.update_style()

    def contextMenuEvent(self, event):
        """右クリックメニューを表示する"""

        menu = QMenu(self)

        date_action = QAction("日付表示", self)
        date_action.setCheckable(True)
        date_action.setChecked(self.show_date)
        date_action.triggered.connect(self.toggle_date)
        menu.addAction(date_action)

        wareki_action = QAction("和暦表示", self)
        wareki_action.setCheckable(True)
        wareki_action.setChecked(self.show_wareki)
        wareki_action.triggered.connect(self.toggle_wareki)
        menu.addAction(wareki_action)

        weekday_action = QAction("曜日表示", self)
        weekday_action.setCheckable(True)
        weekday_action.setChecked(self.show_weekday)
        weekday_action.triggered.connect(self.toggle_weekday)
        menu.addAction(weekday_action)

        seconds_action = QAction("秒表示", self)
        seconds_action.setCheckable(True)
        seconds_action.setChecked(self.show_seconds)
        seconds_action.triggered.connect(self.toggle_seconds)
        menu.addAction(seconds_action)

        menu.addSeparator()

        size_up_action = QAction("文字サイズ +", self)
        size_up_action.triggered.connect(self.increase_size)
        menu.addAction(size_up_action)

        size_down_action = QAction("文字サイズ -", self)
        size_down_action.triggered.connect(self.decrease_size)
        menu.addAction(size_down_action)

        menu.addSeparator()

        opacity_menu = menu.addMenu("背景透明度")

        opacity_items = (
            ("100%", 255),
            ("80%", 200),
            ("60%", 150),
            ("40%", 100),
            ("20%", 50),
            ("ほぼ透明", 20),
        )

        for label, alpha in opacity_items:
            action = QAction(label, self)
            action.triggered.connect(
                lambda checked=False, value=alpha: self.set_background_alpha(
                    value
                )
            )
            opacity_menu.addAction(action)

        color_menu = menu.addMenu("文字色")

        color_items = (
            ("", "white"),
            ("", "red"),
            ("", "yellow"),
            ("", "lime"),
            ("", "cyan"),
            ("", "black"),
        )

        for label, color in color_items:
            action = QAction(label, self)
            action.triggered.connect(
                lambda checked=False, value=color: self.set_text_color(value)
            )
            color_menu.addAction(action)

        menu.addSeparator()

        topmost_action = QAction("常に最前面", self)
        topmost_action.setCheckable(True)
        topmost_action.setChecked(self.always_on_top)
        topmost_action.triggered.connect(self.toggle_topmost)
        menu.addAction(topmost_action)

        menu.addSeparator()

        exit_action = QAction("終了", self)
        exit_action.triggered.connect(QApplication.quit)
        menu.addAction(exit_action)

        menu.exec(event.globalPos())

    def wheelEvent(self, event):
        """マウスホイールで文字サイズ変更"""

        if event.angleDelta().y() > 0:
            self.increase_size()
        else:
            self.decrease_size()

    def mousePressEvent(self, event):
        """ドラッグ開始"""

        if event.button() == Qt.MouseButton.LeftButton:
            self.drag_pos = (
                event.globalPosition().toPoint()
                - self.frameGeometry().topLeft()
            )

    def mouseMoveEvent(self, event):
        """ドラッグ移動"""

#        if event.buttons() & Qt.MouseButton.LeftButton:
        if (
            event.buttons() & Qt.MouseButton.LeftButton
            and self.drag_pos is not None
        ):
            self.move(
                event.globalPosition().toPoint()
                - self.drag_pos
            )

# 追加ここから
    def mouseReleaseEvent(self, event):
        """ドラッグ終了"""
        self.drag_pos = None
# 追加ここまで

    def mouseDoubleClickEvent(self, event):
        """ダブルクリックで日付表示を切り替える"""

        if event.button() == Qt.MouseButton.LeftButton:
            self.toggle_date()

    def keyPressEvent(self, event):
        """キー入力処理"""

        if event.key() == Qt.Key.Key_Escape:
            QApplication.quit()


def main():
    """メイン処理"""

    app = QApplication(sys.argv)

    clock = OverlayClock()
    clock.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

補足

 適切かどうかわからないが、自分が手を加えた辺りのメモ。

フォントの変更

font = QFont(
#            "Yu Gothic UI",    # 元 
#            "BIZ UDゴシック",   # OK
            "MSゴシック",
            self.font_size,
            QFont.Weight.Bold,
        )

Yu Gothic font family - Typography | Microsoft Learn
 Windowsのシステム画面で標準表示フォントとして採用されているゴシック体"Yu Gothic UI"で対案されたが、秒の表示が更新されるたびに、時分の位置が揺らぐのを嫌い、"MSゴシック"に変更。
 ちなみに、"BIZ UDゴシック"でも良さそう。

透明度の変更

       self.background_alpha = 50      # 80(31%) から、20%に変更

 最初31%(=80/255)であったが、下記の様に変更できるようにしたため、20%に変更。

        opacity_items = (
            ("100%", 255),
            ("80%", 200),
            ("60%", 150),
            ("40%", 100),
            ("20%", 50),
            ("ほぼ透明", 20),
        )

更新タイミングの変更

        self.timer.start(1000)  # 200ms->1000msに変更

 最初は200msであったが、秒の更新のみのため、1秒に変更。

年号判定ロジックの微修正

    def get_japanese_era(dt: datetime) -> str:
        """和暦文字列を取得する。

        Args:
            dt: 変換対象日時。

        Returns:
            和暦形式の日付文字列。
        """
        year = dt.year

#        if year >= 2019:
        if dt >= datetime(2019, 5, 1):
            era = "令和"
            era_year = year - 2018
#        elif year >= 1989:
        elif dt >= datetime(1989, 1, 8):
            era = "平成"
            era_year = year - 1988
        elif year >= 1926:
            era = "昭和"
            era_year = year - 1925
        else:
            return ""

        year_text = "" if era_year == 1 else str(era_year)

        return f"{era}{year_text}{dt.month}{dt.day}"

 最初、年だけの判定であったが、令和の判定を日付ベースに変更。昭和以前は未対応。
(昭和の開始日は1926/12/25)

 但し、今回の用途からすれば、あまりこの辺りの変更に意味ないのですが。

 あと、Pythonの型ヒント(Type Hint)辺りはここ以外も複数修正。 Google Python Style Guide に沿ったドキュメンテーション文字列(docstring)も追加。

左クリック押下時の修正

    def mouseMoveEvent(self, event):
        """ドラッグ移動"""

#        if event.buttons() & Qt.MouseButton.LeftButton:
        if (
            event.buttons() & Qt.MouseButton.LeftButton
            and self.drag_pos is not None
        ):
            self.move(
                event.globalPosition().toPoint()
                - self.drag_pos
            )

 drag_pos が None の場合でも move() が実行される可能性があるため、修正。

copilotによる説明

① drag_pos が None のときに move() が呼ばれるのを防ぐ
drag_pos は 左クリックした瞬間にセットされる値です。drag_pos は 左クリックした瞬間にセットされる値です

def mousePressEvent(self, event):
    if event.button() == Qt.MouseButton.LeftButton:
        self.drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()

しかし、
・クリックしていない
・クリックが終わった直後
・何らかの理由で drag_pos が None のまま
こういう状態で mouseMoveEvent が走ると、
None を使って座標計算しようとしてエラーになる可能性があります。

② PyQt のイベントは「押していなくても」頻繁に呼ばれる
mouseMoveEvent はマウスが動くたびに呼ばれるため、
左ボタンを押していない状態でも発火します。
そのため、
「左ボタンが押されている」+「drag_pos がセットされている」
という2条件が必要になります。

③ 実際に PyQt のドラッグ処理の定番パターン
PyQt のドラッグ処理は一般的にこう書きます:

if event.buttons() & Qt.LeftButton and self.drag_pos is not None:
    self.move(event.globalPos() - self.drag_pos)

左クリックリリースイベントの追加

# 追加ここから
    def mouseReleaseEvent(self, event):
        """ドラッグ終了"""
        self.drag_pos = None
# 追加ここまで

 次のドラッグで誤動作する可能性を考慮し追加。

copilotによる説明

  1. drag_pos は「ドラッグ開始位置」を記録する変数
    mousePressEvent でこうセットしています:
self.drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()

つまり、
左クリックした瞬間の位置を記録するための変数です。

  1. ドラッグが終わったら、この値はもう不要
    ドラッグが終わっても drag_pos が残っていると、
    ・左ボタンを押していないのに drag_pos が残っている
    ・次のドラッグ開始前に古い drag_pos が使われる
    ・クリック位置がズレてウィンドウが「飛ぶ」ように動く
    ・まれに変な位置にジャンプする
    という不具合が起きます。

  2. PyQt のドラッグ処理の定番パターン
    PyQt のドラッグ処理は一般的にこう書きます:
     1.押した瞬間に drag_pos をセット
     2.移動中は drag_pos を使って移動
     3.離した瞬間に drag_pos を None に戻す

おまけ

            lines.append("")    # 1行空け
            lines.append(time_text)

 今は最初の提案通り、日付2行+空白1行+時刻1行としている。ここに何か情報を入れようかとテスト時に試した名残。

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?