본문 바로가기
기타/Vibe Coding

[파이썬] 컴퓨터에 눈이 와요! Snowing

by ㅇ달빛천사ㅇ 2025. 12. 29.
728x90

개발 동기

To, 블로그씨에서 트리 만들기에 관한 글을 쓰면서 컴퓨터에서 사용할 수 있는 트리 위젯을 만들었습니다. 위젯의 기능 상 항상 화면 맨 위에 위치해 있어야했는데 구글 Gemini를 통해 파이썬으로 해당 기능을 구현할 수 있는 것을 알게되었습니다.

그래서 이번에는 카톡의 눈 오는 배경화면에서 아이디어를 얻어 컴퓨터 화면에 눈이 오게 할 수 있을까?

궁금해져 이번 코딩을 하게되었습니다.


주요 기능

  • 컴퓨터의 전체 화면에 눈이 오는 듯한 효과 연출
  • 작업 표시줄의 '숨겨진 아이콘'에서 해당 위젯을 마우스 오른쪽 클릭하여 종료가능

 

 
눈 오는 컴퓨터 화면, 유튜브에서 검은 화면 재생해서 잘 보이게 해 보았어요.

 

 

작업표시줄에서 숨김 아이콘을 표시하고 Snowing 종료 아이콘을 마우스 우클릭 하시면 종료 버튼이 보입니다.

트러블 슈팅

  • 눈송이가 비정상적으로 길쭉하거나 크게 늘어져서 내림
    • canvas.coords로 눈송이 위치를 재설정할 때 좌표 값이 어긋나서 발생하는 현상
      • 작업에 방해되지 않도록 눈송이 크기를 아주 작고 일정하게(2~4픽셀) 고정
        • size = random.randint(2, 4)로 설정하여 시야를 가리지 않는 아주 작은 입자로만 구성
      • 화면 아래로 사라진 눈송이가 다시 위에서 나타날 때 모양이 깨지지 않도록 코드를 수정
        • coords 함수 사용 시 4개의 좌표(x1, y1, x2, y2)를 모두 정확히 넣어 눈이 재생성될 때도 원래의 동그란 모양을 유지하도록 함
        • 눈이 너무 휙휙 지나가지 않도록 속도를 0.7 ~ 2.0 사이로 낮춰서 훨씬 차분한 분위기를 냄
  • 실행 창 없이 화면을 투명하게 덮고 있어 '종료 버튼'이 보이지 않음
    • pystray 라이브러리를 이용하여 프로그램이 방해되지 않도록 종료 버튼을 트레이 아이콘(시계 옆 아이콘)에 넣어 우클릭으로 종료하게 만듦

코드

# 코드 실행 전 라이브러리 설치
# pip install pystray Pillow

import tkinter as tk
import random
import threading
from PIL import Image, ImageDraw
import pystray
from pystray import MenuItem as item
import sys

class Snowfall:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Snowing")
        
        self.screen_width = self.root.winfo_screenwidth()
        self.screen_height = self.root.winfo_screenheight()
        
        self.root.overrideredirect(True)
        self.root.attributes("-topmost", True)
        self.root.attributes("-transparentcolor", "black")
        self.root.geometry(f"{self.screen_width}x{self.screen_height}+0+0")

        try:
            import ctypes
            GWL_EXSTYLE = -20
            WS_EX_LAYERED = 0x00080000
            WS_EX_TRANSPARENT = 0x00000020
            hwnd = ctypes.windll.user32.GetParent(self.root.winfo_id())
            style = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
            ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style | WS_EX_LAYERED | WS_EX_TRANSPARENT)
        except:
            pass

        self.canvas = tk.Canvas(self.root, width=self.screen_width, height=self.screen_height, 
                               bg='black', highlightthickness=0)
        self.canvas.pack()

        self.root.bind("<Escape>", lambda e: self.quit_window())

        self.snowflakes = []
        # 눈송이 개수를 80개로 조절 (방해되지 않는 선에서 적당히)
        for _ in range(80):
            self.add_snowflake(initial=True)

        self.animate()
        
        self.tray_thread = threading.Thread(target=self.setup_tray, daemon=True)
        self.tray_thread.start()

        self.root.mainloop()

    def add_snowflake(self, initial=False):
        # 눈송이 크기를 2~4픽셀 사이로 아주 작게 제한
        size = random.randint(2, 4)
        x = random.randint(0, self.screen_width)
        # 처음 생성 시에는 화면 전체에 뿌리고, 재생성 시에는 화면 위쪽에서 생성
        y = random.randint(0, self.screen_height) if initial else random.randint(-50, -10)
        
        speed = random.uniform(0.7, 2.0) # 눈 속도도 조금 더 차분하게 조절
        
        flake = self.canvas.create_oval(x, y, x + size, y + size, fill="white", outline="white")
        self.snowflakes.append({'id': flake, 'speed': speed, 'size': size})

    def animate(self):
        for flake_data in self.snowflakes:
            flake_id = flake_data['id']
            speed = flake_data['speed']
            size = flake_data['size']
            
            # 아래로 이동
            self.canvas.move(flake_id, 0, speed)
            pos = self.canvas.coords(flake_id)

            # 화면 아래로 완전히 사라지면 위로 재배치
            if pos[1] > self.screen_height:
                new_x = random.randint(0, self.screen_width)
                new_y = random.randint(-20, -5)
                # 좌표를 다시 설정할 때 크기(size)가 유지되도록 x+size, y+size를 정확히 지정
                self.canvas.coords(flake_id, new_x, new_y, new_x + size, new_y + size)
                
        self.root.after(30, self.animate)

    def create_image(self):
        image = Image.new('RGB', (64, 64), color=(255, 255, 255))
        dc = ImageDraw.Draw(image)
        dc.rectangle((16, 16, 48, 48), fill=(0, 150, 255))
        return image

    def setup_tray(self):
        menu = (item('종료(Exit)', self.quit_window),)
        self.icon = pystray.Icon("Snowfall", self.create_image(), "Snowing", menu)
        self.icon.run()

    def quit_window(self):
        if hasattr(self, 'icon'):
            self.icon.stop()
        self.root.quit()
        sys.exit()

if __name__ == "__main__":
    Snowfall()

실행 파일

onefile 배포 실행 파일과 onedir 배포 실행 파일 중 하나만 다운받아 사용하면 됩니다.

onefile배포와 onedir배포의 차이

onefile에는 실행 파일(.exe)하나만 다운 받으면 되고

onedir배포 파일은 zip파일을 다운받아 압축 해제 후, 그 안의 실행 파일(.exe)을 실행하여 사용합니다.

속도는 프로그램 용량에 따라 다를 수 있지만 onedir 배포 파일이 더 빠를 수 있습니다.

 

CodingWithGemini/Snowing/dist at main · MinjuKang727/CodingWithGemini

Contribute to MinjuKang727/CodingWithGemini development by creating an account on GitHub.

github.com

 

 

위 링크 클릭 후, 오른쪽 위 'Download raw file' 클릭하시면 다운받아집니다.​
728x90


Top