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

Помню, лет так 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. ссылка на гитхаб

Комментарии (5)


  1. DarkPreacher
    27.12.2021 08:36
    +2

    Я себе что-то похожее пару лет назад делал, с помощью Rainmeter. Моргает огоньками, прячется при наведении мыши, чтобы не мешалась. Вот

    картинка


  1. slagovskiy
    27.12.2021 11:31

    А сами картинки почему не выложены?


    1. daniilgorbenko Автор
      27.12.2021 13:17

      Извиняюсь, залил картинки)


  1. lastsector
    27.12.2021 13:05

    А гифки где на github?


    1. daniilgorbenko Автор
      27.12.2021 13:17

      Залил