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