Python wxPython: wx.Region で円形や複雑な形のウィンドウを作成する方法 (SetShape)

Python

PythonでGUIアプリを作っていると、「ウィンドウは必ず四角くなければいけないのか?」と疑問に思うことはありませんか。スプラッシュスクリーンをロゴの形にしたり、デスクトップマスコットをキャラクターの形にしたりと、デザインに凝ったアプリを作りたい場合もあります。

wxPythonでは、標準の矩形(四角)を超えた、自由な形状の領域を定義するための wx.Region(リージョン)というクラスが用意されています。

この記事では、wx.Region の基本的な使い方から、複数の領域を組み合わせて複雑な形を作る方法、そして wx.Region を使ってウィンドウ自体の形を変更する SetShape メソッドの活用法まで、初心者の方にも分かりやすく解説します。

この記事を読み終える頃には、wx.Region を使って、デスクトップに円形やドーナツ型、あるいはもっと複雑なカスタムシェイプのウィンドウを作成できるようになっているはずです。

wx.Region とは?

まずは wx.Region がどのようなクラスなのか、似ている wx.Rect との違いから理解しましょう。

矩形を超える「任意の領域」を扱うクラス

wx.Region は、画面上の「任意の領域」を表現するためのクラスです。

wx.Rect が (x, y, width, height) だけで定義される、単純な「矩形(長方形)」しか扱えなかったのに対し、wx.Region は**円形、多角形、あるいは複数の領域を組み合わせた複雑な形状(例えば、穴のあいたドーナツ型など)**も一つの「領域」として表現できます。

四角いキャンバスしか扱えなかった wx.Rect に対して、wx.Region は自由な形に切り抜いた画用紙を扱うイメージです。

wx.Region の主な2つの使い道

wx.Region には、主に2つの強力な使い道があります。

  1. クリッピング (SetClippingRegion) wx.PaintDC(描画コンテキスト)と組み合わせて使い、描画する範囲を特定の領域(例:円の内側だけ)に限定します。この領域外にはみ出して描画しようとしても、自動的にカットされます。
  2. ウィンドウシェイプ (SetShape) wx.Framewx.Dialog といったトップレベルウィンドウの「形」そのものを変更します。SetShapewx.Region を渡すと、ウィンドウはその形の通りに切り抜かれます。

この記事では、特にインパクトがあり面白い、2番目の「ウィンドウシェイプ (SetShape)」の使い方をメインに解説していきます。

wx.Region を作成する基本的な方法

SetShape で使う「形」データを作成するには、まず wx.Region オブジェクトを作成する必要があります。いくつかの基本的な作成方法を見ていきましょう。

1. 矩形から作成する (wx.Region(rect))

最もシンプルな方法が、wx.Rect オブジェクト(または x, y, w, h の4つの数値)から wx.Region を作成する方法です。

import wx

# wx.Rect オブジェクトから作成
rect = wx.Rect(10, 10, 100, 100)
region_from_rect = wx.Region(rect)

# 4つの数値から直接作成
region_from_coords = wx.Region(10, 10, 100, 100)

この時点では、まだただの四角い領域です。

2. 円形を作成する

円形ウィンドウを作るための基本です。wx.Region には、円形(または楕円)を簡単に作成するための便利なコンストラクタが用意されています。

方法A: 中心点と半径で指定 wx.Region(centre, radius) の形式で、中心となる wx.Point と半径(整数)を指定します。

import wx

# 中心 (100, 100), 半径 50 の円形領域
center_point = wx.Point(100, 100)
radius = 50
circle_region = wx.Region(center_point, radius)

方法B: 矩形に内接する楕円として指定 wx.Region(rect, wx.ELLIPSE) の形式で、指定した矩形 rect にピッタリ内接する楕円を作成します。rect が正方形であれば、結果は真円になります。

import wx

# (50, 50) から (250, 150) の矩形に内接する「楕円」
ellipse_rect = wx.Rect(50, 50, 200, 100) # 幅200, 高さ100
ellipse_region = wx.Region(ellipse_rect, wx.ELLIPSE)

# (50, 50) から (150, 150) の正方形に内接する「真円」
circle_rect = wx.Rect(50, 50, 100, 100) # 幅100, 高さ100
circle_region_2 = wx.Region(circle_rect, wx.ELLIPSE)

3. 多角形(ポリゴン)から作成する (wx.Region(points))

頂点の wx.Point のリスト(PythonのリストでOK)を渡すことで、その頂点を結んだ多角形の領域を作成できます。三角形、五角形、星形など、自由な形状が作れます。

import wx

# 3つの頂点を結んだ三角形の領域
points = [
    wx.Point(100, 10),  # 上の頂点
    wx.Point(190, 190), # 右下の頂点
    wx.Point(10, 190)   # 左下の頂点
]

triangle_region = wx.Region(points)

wx.Region を組み合わせて複雑な形を作る(領域演算)

wx.Region の真価は、ここから紹介する「領域演算」にあります。複数の wx.Region オブジェクトを足したり、引いたりすることで、単体では作れない複雑な形を生み出せます。

和集合 (Union) – 2つの形を合体させる

region1.Union(region2) は、2つの領域を合体させた、より大きな1つの領域を作成します(region1 オブジェクト自体が変更されます)。

例えば、2つの円を少しずらして Union すれば、雪だるまやミッキーマウスの耳のような形を作ることができます。

import wx
region1 = wx.Region(wx.Point(100, 100), 50) # 円1
region2 = wx.Region(wx.Point(150, 100), 50) # 円2

# region1 に region2 を合体させる
region1.Union(region2)
# これで region1 は2つの円が重なった領域になる

差集合 (Subtract) – 形をくり抜く

region1.Subtract(region2) は、region1 から region2 と重なる部分を取り除いた領域を作成します。

これこそが、この記事で紹介する最も重要なテクニックです。 例えば、大きな円形領域から、それより小さい同心の円形領域を Subtract すれば、「穴のあいたドーナツ型」の領域を作成できます。

import wx
# 大きい円 (半径100)
region_outer = wx.Region(wx.Point(150, 150), 100)
# 小さい円 (半径50)
region_inner = wx.Region(wx.Point(150, 150), 50)

# 大きい円から小さい円を「くり抜く」
region_outer.Subtract(region_inner)
# これで region_outer はドーナツ型の領域になる

積集合 (Intersect) – 重なった部分だけを残す

region1.Intersect(region2) は、2つの領域が**両方とも存在している(重なっている)**部分だけの、新しい領域を作成します。

例えば、2つの円が重なった部分は、レンズ(または葉っぱ)のような形になります。

import wx
region1 = wx.Region(wx.Point(100, 100), 50)
region2 = wx.Region(wx.Point(150, 100), 50)

# region1 と region2 が重なった部分だけを残す
region1.Intersect(region2)
# これで region1 はレンズ型の領域になる

SetShape でウィンドウの形を実際に変更する

wx.Region を作成し、操作する方法がわかったところで、いよいよウィンドウの形を変更する SetShape メソッドを使ってみましょう。

SetShape の基本

wx.Framewx.Dialog といった wx.TopLevelWindow クラス(およびそのサブクラス)は、SetShape(region) というメソッドを持っています。

このメソッドに wx.Region オブジェクトを渡すと、OSのウィンドウマネージャに対し、「このウィンドウの表示領域を、この region の形に切り抜いてください」という指示が送られます。

region の外側の部分は完全に透明になり、マウス操作(クリックやドラッグ)も受け付けなくなります(下のウィンドウに透過します)。

SetShape を使う際の重要な注意点

SetShape は強力ですが、使う上で非常に重要な注意点が2つあります。

  1. wx.NO_BORDER スタイルが最適 SetShape は、OSが標準で描画するウィンドウ枠、つまりタイトルバーやリサイズ用の境界線があると、正常に動作しないか、意図しない表示になることがほとんどです。 SetShape を使う場合は、ウィンドウを作成する際に style=wx.NO_BORDER を指定し、タイトルバーや境界線のない「ボーダーレスウィンドウ」として作成するのが一般的です。
  2. CanSetShape() でのサポート確認 すべてのOSや環境が SetShape をサポートしているとは限りません。安全のため、frame.CanSetShape() メソッド(True / False を返す)を呼び出し、実行環境がこの機能をサポートしているかを確認するのがベストプラクティスです。

実践コード1:円形のウィンドウを作成する

style=wx.NO_BORDER でウィンドウを作成し、SetShape で円形に切り抜くコードです。

最大の問題点: wx.NO_BORDER にすると、タイトルバーが消えるため、ウィンドウをドラッグして移動させたり、閉じるボタンを押したりすることができなくなります。 そのため、以下のコードでは**「ウィンドウの移動処理」と「閉じるボタン」を自前で実装**しています。これは SetShape を使う上でほぼ必須のテクニックです。

import wx

class ShapeFrame(wx.Frame):
    def __init__(self):
        # style=wx.NO_BORDER でボーダーレスウィンドウを作成
        super().__init__(None, title="Shape Frame", style=wx.NO_BORDER)

        self.panel = wx.Panel(self)
        self.panel.SetBackgroundColour(wx.Colour(50, 50, 150)) # 濃い青

        # --- ウィンドウの形を定義 ---
        window_size = (300, 300)
        self.SetSize(window_size)
        
        # ウィンドウサイズに内接する円形リージョンを作成
        # (0, 0) から (300, 300) の矩形に内接する円
        rect_for_ellipse = wx.Rect(0, 0, window_size[0], window_size[1])
        shape_region = wx.Region(rect_for_ellipse, wx.ELLIPSE)

        # SetShape がサポートされているか確認
        if self.CanSetShape():
            self.SetShape(shape_region)
        else:
            print("警告: SetShape はこのプラットフォームでサポートされていません。")

        # --- 自前の閉じるボタン ---
        close_btn = wx.Button(self.panel, label="× 閉じる", pos=(10, 10))
        close_btn.SetForegroundColour(wx.WHITE)
        close_btn.SetBackgroundColour(wx.RED)
        close_btn.Bind(wx.EVT_BUTTON, self.OnClose)

        # --- 自前のウィンドウドラッグ移動処理 ---
        self.panel.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.panel.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.panel.Bind(wx.EVT_MOTION, self.OnMotion)
        self._drag_pos = None

        self.Centre()

    def OnClose(self, event):
        self.Close()

    # --- ウィンドウドラッグ移動のためのイベントハンドラ ---
    def OnLeftDown(self, event):
        # クリック位置を記録
        self._drag_pos = event.GetPosition()
        self.panel.CaptureMouse() # マウスキャプチャ開始

    def OnLeftUp(self, event):
        # マウスキャプチャ解放
        if self.panel.HasCapture():
            self.panel.ReleaseMouse()
        self._drag_pos = None

    def OnMotion(self, event):
        # ドラッグ中 (左ボタンが押されている) かつ _drag_pos が記録されていれば
        if event.Dragging() and event.LeftIsDown() and self._drag_pos:
            # 現在のマウス位置
            current_pos = event.GetPosition()
            # 前回の位置からの差分
            dx = current_pos.x - self._drag_pos.x
            dy = current_pos.y - self._drag_pos.y
            
            # ウィンドウの現在位置に差分を足して、ウィンドウ自体を移動
            current_screen_pos = self.GetScreenPosition()
            new_screen_pos = (current_screen_pos.x + dx, current_screen_pos.y + dy)
            self.SetPosition(new_screen_pos)
            
            # _drag_pos は更新しない (最初のクリック位置からの差分で移動し続ける)
            # ただし、実装によっては _drag_pos = current_pos と更新する方法もある

if __name__ == "__main__":
    app = wx.App()
    frame = ShapeFrame()
    frame.Show()
    app.MainLoop()

このコードを実行すると、濃い青色の円形ウィンドウが表示され、ドラッグで移動したり、左上の赤い「× 閉じる」ボタンで閉じたりできるはずです。

実践コード2:ドーナツ型(穴あき)ウィンドウを作成する

次に、Subtract(差集合)を使って、中央がくり抜かれた「ドーナツ型」のウィンドウを作成します。

import wx

# (ShapeFrame クラスの __init__ メソッドの「ウィンドウの形を定義」部分を
#  以下のように書き換える)

class DonutFrame(wx.Frame):
    def __init__(self):
        # (style=wx.NO_BORDER や Panel の設定は実践コード1と同じ)
        super().__init__(None, title="Donut Frame", style=wx.NO_BORDER)
        self.panel = wx.Panel(self)
        self.panel.SetBackgroundColour(wx.Colour(50, 150, 50)) # 緑色
        
        # --- ウィンドウの形を定義 ---
        window_size = (400, 400)
        self.SetSize(window_size)
        center_point = wx.Point(window_size[0] // 2, window_size[1] // 2)

        # 1. 外側の大きい円 (半径200)
        region_outer = wx.Region(center_point, 200)
        
        # 2. 内側の小さい円 (半径100)
        region_inner = wx.Region(center_point, 100)

        # 3. 差集合 (Subtract) で「穴」をあける
        region_outer.Subtract(region_inner)

        # SetShape がサポートされているか確認
        if self.CanSetShape():
            self.SetShape(region_outer)
        else:
            print("警告: SetShape はこのプラットフォームでサポートされていません。")

        # (自前の閉じるボタンやドラッグ移動処理は実践コード1と同じ)
        close_btn = wx.Button(self.panel, label="× 閉じる", pos=(10, 10))
        # ... (Bind処理なども同様) ...
        # (ここでは簡略化のため省略)
        close_btn.Bind(wx.EVT_BUTTON, self.OnClose)
        self.panel.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.panel.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.panel.Bind(wx.EVT_MOTION, self.OnMotion)
        self._drag_pos = None

        self.Centre()

    def OnClose(self, event):
        self.Close()
    
    # (OnLeftDown, OnLeftUp, OnMotion メソッドも実践コード1と同様に実装)
    def OnLeftDown(self, event):
        self._drag_pos = event.GetPosition()
        self.panel.CaptureMouse()
    def OnLeftUp(self, event):
        if self.panel.HasCapture():
            self.panel.ReleaseMouse()
        self._drag_pos = None
    def OnMotion(self, event):
        if event.Dragging() and event.LeftIsDown() and self._drag_pos:
            current_pos = event.GetPosition()
            dx = current_pos.x - self._drag_pos.x
            dy = current_pos.y - self._drag_pos.y
            current_screen_pos = self.GetScreenPosition()
            new_screen_pos = (current_screen_pos.x + dx, current_screen_pos.y + dy)
            self.SetPosition(new_screen_pos)

# (アプリケーション実行部分も同様)
if __name__ == "__main__":
    app = wx.App()
    frame = DonutFrame()
    frame.Show()
    app.MainLoop()

これを実行すると、中央が透明なドーナツ型のウィンドウが表示されます。透明な部分をクリックすると、背景のデスクトップや他のウィンドウが操作できるはずです。

(補足)wx.Region のもう一つの使い方:クリッピング

SetShape 以外に、wx.Region は「描画領域を限定する(クリッピング)」ためにも使われます。 これは wx.PaintEvent の中で wx.PaintDC(描画コンテキスト)に対して設定します。

dc.SetClippingRegion(region) を呼び出すと、それ以降の dc.Draw... 命令(線や矩形、円を描画する命令)は、指定した region の内側にしか描画されなくなります。

def OnPaint(self, event):
    dc = wx.PaintDC(self) # 描画コンテキスト
    
    # 中心(100, 100), 半径50 の円形リージョンを作成
    clip_region = wx.Region(wx.Point(100, 100), 50)
    
    # 描画領域をこの円形に限定
    dc.SetClippingRegion(clip_region)
    
    # クリッピング設定後、(0, 0) から (200, 200) の
    # 巨大な赤い矩形を描画しようとする
    dc.SetBrush(wx.Brush(wx.RED))
    dc.DrawRectangle(0, 0, 200, 200)
    
    # 結果:実際には、(100, 100)を中心とする半径50の
    # 「円形」に切り抜かれた部分だけが赤く描画される

このように、wx.Region は描画のマスクとしても機能します。

まとめ

今回は、wxPythonで矩形(四角)を超える複雑な領域を扱うための wx.Region クラスについて、その作成方法から応用までを解説しました。

  • wx.Region は、矩形 (wx.Rect) を超える、円形、多角形、複雑な2D領域を扱うためのクラスです。
  • wx.Region は「円形(中心と半径)」や「多角形(頂点のリスト)」から直接作成できます。
  • 領域演算が強力で、Union (和集合)、Intersect (積集合)、そして特に Subtract (差集合) が重要です。
  • Subtract を使うと、領域をくり抜き、「ドーナツ型」のような複雑な形状も作成可能です。
  • wx.FrameSetShape(region) メソッドを使うことで、ウィンドウ自体の形を wx.Region の形に変更できます。
  • SetShape を使う際は、ウィンドウを style=wx.NO_BORDER で作成するのが基本であり、ウィンドウのドラッグ移動や閉じる処理を自前で実装する必要があります。

wx.Region を使いこなすことで、スプラッシュスクリーンやデスクトップウィジェットなど、ユーザーの目を引くデザイン性の高いGUIアプリケーションを作成できます。ぜひ、四角いウィンドウの枠を超えたアプリ開発に挑戦してみてください。

コメント

タイトルとURLをコピーしました