wxpythonで画面いっぱいに雪を降らす

冬になっても家にこもってると冬を感じられないので冬を感じるために画面に雪を降らせてあげたいと思います。
wxpythonで、と書いてはいますがwin32apiを使用しているのでwin32apiが使えればどのライブラリでも変わらないと思います。

# 成果物
![snow2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323266/9182330b-0f30-6a05-fc46-b95a71a9762a.gif)

# 環境
windows 10
python 3.6.10
wxPython 4.1.0

# 方針

雪を画面に降らすためには最前面、透明、クリックされない、フォーカスされないウィンドウを作成します。
その後、雪を描画します。


## ウィンドウの作成
STAY_ON_TOPを使用して最前面でウィンドウを作成します

```python
wx.Frame.__init__(self,parent=None, title="Snow Layer",style=wx.STAY_ON_TOP)
```
透明でクリックされない、フォーカスされないなどの設定がwxpythonではできないようなので、win32apiで使用するwindow Handleを取得します。

```python
hwnd = self.GetHandle()
```
現在のウィンドウの状況を取得し、WS_EX_LAYERED(透過設定が可能になる)、WS_DISABLED(フォーカス、クリックなどのユーザー入力を受け付けない)を設定します。
その後、レイヤーウィンドウ設定でカラーコードを指定して透過色を設定しています。今回は黒が透過色となっています。

```python
extendedStyleSettings = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, extendedStyleSettings | win32con.WS_EX_LAYERED | win32con.WS_DISABLED)
win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(0,0,0), 0, win32con.LWA_COLORKEY)
```
UpdateLayeredWindowAttributesを使えば半透明な部分の作成もできそうですが、大変そうなのと今回は必要ないので使っていません。
やってみた方がいたらコメントで教えていただけると私が喜びます。

## 雪の描画

ウィンドウが作成出来たら後は雪を描くだけです。

雪の情報を保持するSnowクラスを作成しました。

```python
class Snow:
  def __init__(self,x,y):
    self.beforex=self.x=x
    self.beforey=self.y=y
    self.baseX=x
    self.size=random.randint(1,3)
  def update(self):
    self.beforex=self.x
    self.beforey=self.y
    self.x=self.baseX+2*math.sin(self.y*0.1)
    self.y+=1
```
Snowクラスは前回の座標と現在の座標を持っています。updateで座標を更新しy座標に応じて少し左右に揺れるようになっています。

タイマーイベントにて描画処理を16msごとに呼び出しています、また画面がちらつくのでリフレッシュは無効にしています。

```python
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.onTimer)
self.timer.Start(16)
  def onTimer(self,event):
    self.Refresh(False)
```

描画処理本体では以下の処理を行います。

* ダブルバッファリングして裏面への描画設定
* ランダムに画面上部に雪を発生
* すべての雪に対して前回座標の雪を黒(透過色)で塗りつぶして現在の座標に描画
* 画面外に出た雪を削除

どのように描くのが良さそうなのかわからなかったのであんまりいい書き方ではないのかもしれない。
普通こうするんだよっての教えていただけるとやはり喜びます。

```python
  def OnPaint(self, evt):
    dc=wx.PaintDC(self)
    dc=wx.BufferedDC(dc)
    if not random.randint(0,3):
      self.snowPoints.append(Snow(random.randint(0, self.GetSize()[0]),0))
    for snow in self.snowPoints:
      dc.SetPen(wx.BLACK_PEN)
      dc.SetBrush(wx.BLACK_BRUSH)
      dc.DrawCircle(snow.beforex,snow.beforey,snow.size)
      snow.update()
      dc.SetPen(wx.WHITE_PEN)
      dc.SetBrush(wx.WHITE_BRUSH)
      dc.DrawCircle(snow.x,snow.y,snow.size)
      if snow.y>self.GetSize()[1]:
        self.snowPoints.remove(snow)
```

# 最終的なコード
pythonwで実行するとコンソールを消しても生き残るようになりますが、終了の方法がタスクマネージャからプロセスを止めるとかになるので気を付けてください。

```python
import win32con
import win32gui
import win32api
import wx
import random
import math

class Snow:
  def __init__(self,x,y):
    self.beforex=self.x=x
    self.beforey=self.y=y
    self.baseX=x
    self.size=random.randint(1,3)
  def update(self):
    self.beforex=self.x
    self.beforey=self.y
    self.x=self.baseX+2*math.sin(self.y*0.1)
    self.y+=1



class AppFrame( wx.Frame ):
  def __init__(self):
    wx.Frame.__init__(self,parent=None, title="Snow Layer",style= wx.STAY_ON_TOP)

    hwnd = self.GetHandle()
    extendedStyleSettings = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
    win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, extendedStyleSettings | win32con.WS_EX_LAYERED | win32con.WS_DISABLED)
    win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(0,0,0), 0, win32con.LWA_COLORKEY)
    self.Maximize()
    self.snowPoints=[]
    self.Bind(wx.EVT_PAINT,self.OnPaint)
    self.timer = wx.Timer(self)
    self.Bind(wx.EVT_TIMER, self.onTimer)
    self.timer.Start(10)
    

  def OnPaint(self, evt):
    dc=wx.PaintDC(self)
    dc=wx.BufferedDC(dc)
    if not random.randint(0,3):
      self.snowPoints.append(Snow(random.randint(0, self.GetSize()[0]),0))
    for snow in self.snowPoints:
      dc.SetPen(wx.BLACK_PEN)
      dc.SetBrush(wx.BLACK_BRUSH)
      dc.DrawCircle(snow.beforex,snow.beforey,snow.size)
      snow.update()
      dc.SetPen(wx.WHITE_PEN)
      dc.SetBrush(wx.WHITE_BRUSH)
      dc.DrawCircle(snow.x,snow.y,snow.size)
      if snow.y>self.GetSize()[1]:
        self.snowPoints.remove(snow)
  def onTimer(self,event):
    self.Refresh(False)


app = wx.App(False)
AppFrame().Show()
app.MainLoop()

```