Всех с наступающими праздниками! Надеюсь, каждый отдохнёт и восполнит силы за праздничные дни, а не будет зависать за очередными багами/фичами/обновлениями!
Помню, лет так 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. ссылка на гитхаб