Всех с наступающими праздниками! Надеюсь, каждый отдохнёт и восполнит силы за праздничные дни, а не будет зависать за очередными багами/фичами/обновлениями!

Помню, лет так 12 назад, когда я был ещё школьником, у всех моих знакомых стояла windows XP. И в преддверии нового года у нас была традиция, скачать на каком-нибудь сайте новогоднюю ёлочку, которая запускается отдельной программой и просто на рабочем столе (либо на любом другом окне, если её открыть поверх окон) играет гифка с этой ёлочкой. Мелочь, но к новогоднему настроению она давала в те года +100 очков.

Если раньше такую штуку приходилось искать, где скачать, то теперь пришло время сделать всё самому.

Приступим к написанию своей версии "ёлочки"

Создание окна

Главная идея приложения такая: в правом рабочего стола должна отображаться анимированная картинка, должна быть возможность перетянуть в любую точку экрана эту картинку, а также приложение не должно отображаться в панеле задач.

Для этого первым делом установим нужные библиотеки:

  • screeninfo - нужна для получения информации о мониторе

  • pygame - нужна для создания окна и отрисовки графики

  • pypiwin32 - понадобится в будущем для изменения отображения окна

P.s. большая часть объяснений будет представлена в коде

# run.py
import pygame
from screeninfo import get_monitors
import os

# получаем информацию о мониторах
monitors = get_monitors()
# получаем данные о разрешении
screen_width = monitors[0].width
screen_height = monitors[0].height
print(screen_width, screen_height) # в моём случае вывод: 1920 1080

# -> создаем окно PyGame
pygame.init()
# сразу укажем, что окно должно открываться в левом верхнем углу экрана
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (0,0)
# размер окна - максимальный по нашим параметрам,
# pygame.NOFRAME - означает, что окно должно открываться без рамок
screen = pygame.display.set_mode([screen_width, screen_height], pygame.NOFRAME)
# Clock нужен быть для того, чтобы ограничить fps программы
# ограничение fps необходимо для того, чтобы сделать анимацию картинок более простой
Clock = pygame.time.Clock()
running = True

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # закрашиваем окно в черный
    screen.fill((0,0,0))
    # обновляем экран
    pygame.display.update()
    # ограничиваем fps
    Clock.tick(30)

Результатом выполнения этого кода станет черный экран. Первая часть программы выполнена.

Делаем прозрачный фон и убираем иконку из панели задач

Следующим шагом необходимо сделать фон программы полностью прозрачным, т.е. должны отображаться картинки, но при этом вокруг картинок должна быть воз��ожность кликать по элементам рабочего стола (или другого окна).

Для этого нам поможет библиотека pypiwin32. Модифицируем немного код главного файла:

# run.py
import pygame
from screeninfo import get_monitors
import os
import win32api
import win32con
import win32gui

# получаем информацию о мониторах
monitors = get_monitors()
# получаем данные о разрешении
screen_width = monitors[0].width
screen_height = monitors[0].height

# -> создаем окно PyGame
pygame.init()
# сразу укажем, что окно должно открываться в левом верхнем углу экрана
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (0,0)
# размер окна - максимальный по нашим параметрам,
# pygame.NOFRAME - означает, что окно должно открываться без рамок
screen = pygame.display.set_mode([screen_width, screen_height], pygame.NOFRAME)
# Clock нужен быть для того, чтобы ограничить fps программы
# ограничение fps необходимо для того, чтобы сделать анимацию картинок более простой
Clock = pygame.time.Clock()
running = True

# -> делаем прозрачный фон
# для этого определим цвет, который будет меняться на прозрачный
fuchsia = (255, 0, 128)
# получаем окно pygame
hwnd = pygame.display.get_wm_info()["window"]
# указываем параметры, какой цвет в программе должен меняться на прозрачный
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE,
                       win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) | win32con.WS_EX_LAYERED)
win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(*fuchsia), 0, win32con.LWA_COLORKEY)
# -> убираем с панели задач иконку программы
# для этого возьмем текущее окно, которое получили в hwnd = pygame.display.get_wm_info()["window"]
# указываем параметры для того, чтобы скрыть иконки
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE,win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)| win32con.WS_EX_TOOLWINDOW)


# -> Главный цикл программы
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # закрашиваем окно в тот цвет, который будет становиться прозрачным
    screen.fill(fuchsia)
    # обновляем экран
    pygame.display.update()
    # ограничиваем fps
    Clock.tick(30)

Выполнив теперь код, можно увидеть, что фон стал прозрачным (так как программа запущена, но ничего нет), и иконки программы в панеделе задач нет.

Начинаем работу с анимацией

Для того, чтобы отображать анимации понадобятся гифки. Но так как pygame сам по себе не отображает гифки, то необходимо каждую гифку преобразовать в набор картинок. Я скачал несколько гиф и преобразовал их в набор картинок при помощи бесплатных онлайн сервисов. Однако, картинки получились с фоном. Что будет выглядеть не очень красиво на рабочем, так как ёлочка будет отображаться на белом квадрате. Это легко изменить, открыв gimp и убрав у каждой картинки фон (сделав его прозрачным и сохранив в формате png).

Также каждую картинку я выровнял так, чтобы все они были 500х500 размером и сама картинка находилась в правом нижнем углу. В итоге для каждой картинки должна получиться папка с набором спрайтов:

Пример картинок для отрисовки
Пример картинок для отрисовки

Теперь поместим все разложенные гифки в проект в таком порядке:

Порядок хранения картинок в проекте
Порядок хранения картинок в проекте

Следующим этапом создадим класс для работы с выводом картинок (создадим его в модуле gif_animate.py):

Класс для работы с анимациями
# gif_animate.py
import pygame
class GIFAnimate:
    def __init__(self):
        # позиции картинки на экране
        self.x, self.y = 0, 0
        # список всех гифок в программе
        self.gifs_paths = [
            ["gifs/gif_1/0.png", "gifs/gif_1/1.png", "gifs/gif_1/2.png", "gifs/gif_1/3.png"],
            ["gifs/gif_2/0.png", "gifs/gif_2/1.png", "gifs/gif_2/2.png", "gifs/gif_2/3.png"],
            ["gifs/gif_3/0.png", "gifs/gif_3/1.png", "gifs/gif_3/2.png", "gifs/gif_3/3.png", "gifs/gif_3/4.png", "gifs/gif_3/5.png"],
            ["gifs/gif_4/0.png", "gifs/gif_4/1.png", "gifs/gif_4/2.png", "gifs/gif_4/3.png"],
        ]
        # список загруженых картинок
        self.gifs = []
        # текущий индекс гифки
        self.current_gif = 0
        # индекс картинки в текущей гифке
        self.current_index = 0
        # заргужаем все картинки сразу в мапять
        self.pre_load_images()
    def pre_load_images(self):
        # предзагрузка всех изображений
        for gif_paths in self.gifs_paths:
            loaded_images = []
            for path in gif_paths:
                loaded_images.append(pygame.image.load(path))
            self.gifs.append(loaded_images)
    def show_next_image(self, display, fps, current_step):
        # display - экран для отрисовки
        # fps - сколько кадров в секунду поддерживает приложение
        # current_step - какой кадр сейчас проигрывается
        # менять картинку необходимо каждый fps//len(self.gifs[self.current_gif]) шаг
        step = fps//len(self.gifs[self.current_gif])
        # проверяем, если сейчас кадр (fps+current_step)%step == 0, то меняем картинку
        if (fps+current_step)%step == 0:
            self.current_index += 1
        # если индекс новой картинки выходит за количество картинок, то
        # новый индекс картинки равен 0
        if self.current_index >= len(self.gifs[self.current_gif]):
            self.current_index = 0
        # отрисовываем на экране картинку
        display.blit(self.gifs[self.current_gif][self.current_index], (self.x, self.y))
    def change_gif(self, index):
        # если крутим колесиком мыши, то надо делать сдвиг по картинке
        # назад или вперед, для этого просто сохраняем индекс текущей гифки
        self.current_gif += index
        if self.current_gif >= len(self.gifs):
            self.current_gif = 0
        elif self.current_gif < 0:
            self.current_gif = len(self.gifs) - 1

Далее изменим главный файл run.py, добавив в него строчки создания класса анимаций:

# run.py
from gif_animate import GIFAnimate

# создаем класс работы с анимациями и указываем начальную позицию анимации
gif_anim = GIFAnimate(100, 100)

Теперь поменяем основной pygame цикл, добавив следующий события:

  • Нажата правая кнопка мышки - закрыть программу

  • Колесико вверх - следующая анимация

  • Колесико вниз - предыдущая анимация

  • Зажат ЛКМ - перетягиваем картинку

Новый главный цикл PyGame
# run.py
# количество fps
FPS = 30
# текущий фрейм
current_frame = 0
# начальная позиция мышки
start_mouse_pos = [500, 500]
# была ли зажата мышка
mouse_pressed = False
# -> Главный цикл программы
while running:
    # считаем индекс текущего фрейма
    if current_frame > FPS: current_frame = 0
    current_frame += 1

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        # обработка ивента с перетягиванием картинки
        # если нажали ЛКМ на картинке, то считаем, что начали перетягивать картинку
        # и при этом запоминаем текущую позицию мышки
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            mouse_pressed = True
            start_mouse_pos = pygame.mouse.get_pos()
        # если отпустили ЛКМ, то считаем, что перетягивание закончилось
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
            mouse_pressed = False
        # правая кнопка мыши - выход из приложения
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 3:
            running = False
        # колесико мыши - меняем анимацию
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 4:
            gif_anim.change_gif(1)
        # колесико в обратную сторону
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 5:
            gif_anim.change_gif(-1)
        # если мышка зажата, то смотрим разницу между предыдущей позицией мышки и текущей
        if mouse_pressed:
            current_pos = pygame.mouse.get_pos()
            delta_x, delta_y = start_mouse_pos[0] - current_pos[0], start_mouse_pos[1] - current_pos[1]
            start_mouse_pos = current_pos
            # передвигаем картинку на разницу между позициями мышки
            gif_anim.x -= delta_x
            gif_anim.y -= delta_y
    # закрасили окно бесцветным
    screen.fill(fuchsia)
    # нарисовали следующий кадр
    gif_anim.show_next_image(screen, FPS, current_frame)
    # pygame.draw.circle(screen, (0, 0, 255), (250, 250), 75)
    pygame.display.update()
    # os.environ['SDL_VIDEO_WINDOW_POS'] = "%i,%i" % (screen_width - width + i, screen_height - height + i)
    Clock.tick(FPS)

pygame.quit()

Посмотрим на весь файл run.py

# run.py
import pygame
from screeninfo import get_monitors
import os
import win32api
import win32con
import win32gui
from gif_animate import GIFAnimate
# получаем информацию о мониторах
monitors = get_monitors()
# получаем данные о разрешении
screen_width = monitors[0].width
screen_height = monitors[0].height
# создаем класс работы с анимациями и указываем начальную позицию анимации
gif_anim = GIFAnimate(100, 100)
# -> создаем окно PyGame
pygame.init()
# сразу укажем, что окно должно открываться в левом верхнем углу экрана
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (0,0)
# размер окна - максимальный по нашим параметрам,
# pygame.NOFRAME - означает, что окно должно открываться без рамок
screen = pygame.display.set_mode([screen_width, screen_height], pygame.NOFRAME)
# -> делаем прозрачный фон
# для этого определим цвет, который будет меняться на прозрачный
fuchsia = (255, 0, 128)
# получаем окно pygame
hwnd = pygame.display.get_wm_info()["window"]
# указываем параметры, какой цвет в программе должен меняться на прозрачный
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE,
                       win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) | win32con.WS_EX_LAYERED)
win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(*fuchsia), 0, win32con.LWA_COLORKEY)
# -> убираем с панели задач иконку программы
# для этого возьмем текущее окно, которое получили в hwnd = pygame.display.get_wm_info()["window"]
# указываем параметры для того, чтобы скрыть иконки
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE,win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)| win32con.WS_EX_TOOLWINDOW)
# Clock нужен быть для того, чтобы ограничить fps программы
# ограничение fps необходимо для того, чтобы сделать анимацию картинок более простой
Clock = pygame.time.Clock()
running = True
# количество fps
FPS = 30
# текущий фрейм
current_frame = 0
# начальная позиция мышки
start_mouse_pos = [500, 500]
# была ли зажата мышка
mouse_pressed = False
# -> Главный цикл программы
while running:
    # считаем индекс текущего фрейма
    if current_frame > FPS: current_frame = 0
    current_frame += 1
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        # обработка ивента с перетягиванием картинки
        # если нажали ЛКМ на картинке, то считаем, что начали перетягивать картинку
        # и при этом запоминаем текущую позицию мышки
        if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
            mouse_pressed = True
            start_mouse_pos = pygame.mouse.get_pos()
        # если отпустили ЛКМ, то считаем, что перетягивание закончилось
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
            mouse_pressed = False
        # правая кнопка мыши - выход из приложения
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 3:
            running = False
        # колесико мыши - меняем анимацию
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 4:
            gif_anim.change_gif(1)
        # колесико в обратную сторону
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 5:
            gif_anim.change_gif(-1)
        # если мышка зажата, то смотрим разницу между предыдущей позицией мышки и текущей
        if mouse_pressed:
            current_pos = pygame.mouse.get_pos()
            delta_x, delta_y = start_mouse_pos[0] - current_pos[0], start_mouse_pos[1] - current_pos[1]
            start_mouse_pos = current_pos
            # передвигаем картинку на разницу между позициями мышки
            gif_anim.x -= delta_x
            gif_anim.y -= delta_y
    # закрасили окно бесцветным
    screen.fill(fuchsia)
    # нарисовали следующий кадр
    gif_anim.show_next_image(screen, FPS, current_frame)
    pygame.display.update()
    Clock.tick(FPS)
pygame.quit()

Заключение

Файлы в проекте лежат в следующей структуре:

Проверим работоспособность программы:

Запустив программу, вы можете поставить себе на рабочий стол одну из гифок, она будет отображаться снизу всех окон и без иконки в панеле задач, поэтому не будет мешать работать, но на рабочем столе придаст немного позитива и новогоднего настроения.
Всех еще раз с наступающими праздниками!

P.s. ссылка на гитхаб