Tkinter - это та библиотека, с которой на начальных этапах изучения языка python знакомились все, но обходили стороной по разным причинам. Сейчас я предлагаю вернуться назад, немного поностальгировать и открыть для себя в разы больше фич библиотеки.

ВАЖНО! Tkinter - не лучшее решение для создания больших приложений. И по большей части эта статья нацелена на начинающих программистов, которые уже имеют представление о библиотеке и хотят рыть дальше.

Если вы плохо знакомы с Tkinter, вот прекрасный курс, рекомендую >>>

Улучшаем кнопки tkinter.Button

Пройдёмся по параметры кнопок, которые нам пригодятся:

  • bg - фон кнопки (background color)

  • fg - цвет текста кнопки (foreground color)

  • bd - ширина обводи

  • text - сам текст

  • command - функция исполняющаяся при нажатии

  • font - шрифт

  • relief - стиль обводки (tk.GROOVE , tk.SUNKEN , tk.RAISED , tk.RIDGE , tk.FLAT)

  • state - состояние кнопки (tk.ACTIVE , tk.DISABLED)

  • underline - подчёркнутый символ текста (>-1)

  • padx , pady - отступы по горизонтали , вертикали

  • width , height - ширина , высота (!В СТРОЧКАХ)

  • activebackground - фон кнопки при активации

  • activeforeground - цвет текста кнопки при активации

  • cursor - курсор (https://docs.huihoo.com/tkinter/tkinter-reference-a-gui-for-python/cursors.html)

Предлагаю сделать "кнопку ссылку", которая при нажатии будет перекидывать нас на какой-нибудь сайт. Библиотека webbrowser - встроенная, её не нужно устанавливать.

import tkinter as tk 
import webbrowser
FORM = tk.Tk() # Создаём окно
FORM .geometry('500x500') # Задаём размер 
def link(e = None): # !ОБРАТИТЕ ВНИМАНИЕ на e = None
	webbrowser.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ') # Открываем браузер
button = tk.Button(FORM,command = link,padx = 5,pady = 5,text = 'Link',bd = 0, fg = '#fff', bg = '#08f',underline = 0 , activebackground = '#fff', activeforeground = '#fff',cursor = 'hand2') # Инициализация кнопки
button.pack(expand = 1) # Размещение кнопки по центру окна
# Сюда мы ещё добавим код
FORM.mainloop()

Выглядит уже неплохо. Предлагаю добавить смену цвета кнопки при наведении курсором мыши.

def focus_in(e = None):
	button.configure(fg = '#08f') # Задаём кнопке нужные цвета 
	button.configure(bg = '#fff')
def focus_out(e = None):
	button.configure(bg = '#08f')
	button.configure(fg = '#fff')
button.bind('<Enter>', focus_in) # При входе курсора в область кнопки выполняем focus_in
button.bind('<Leave>', focus_out) # При выходе курсора из области кнопки выполняем focus_out_out

Улучшаем окно программы

Начнём с добавления переключения на полноэкранного режима (верхней панели окна не видно) по клавише F11:

def fullscreen(e = None):
	if FORM.attributes('-fullscreen'): # Проверяем режим окна 
		FORM.attributes('-fullscreen',False) # Меняем режим окна
	else:
		FORM.attributes('-fullscreen',True) # Меняем режим окна
FORM.bind('<F11>',fullscreen) # Биндим окно

Теперь нужно сделать так, что бы пользователь не мог не нажать на ту самую ссылку. Давайте сделаем выход из приложения невозможным (возможным только через диспетчер задач (ctrl+shift+esc)).

def on_close(e = None):
	pass # Мы просто ничего не делаем
	# Может быть любой код
  # Можно так же спрашивать пользователя ВЫ ТОЧНО ХОТИТЕ ЗАКРЫТЬ ОКНО ?
FORM.protocol("WM_DELETE_WINDOW", on_close) # Перехватываем событие выхода из приложения

Реализуем ка мы также и "приоритетный" режим, окно будет отображаться поверх остальных. Здесь я рекомендую использовать библиотеку keyboard (pip install keyboard), так как bind tkintera работает только если окно находиться в фокусе. Keyboard же никак не зависит от tkintera и расположения окон.

def topmost(e = None):
	if FORM.attributes('-topmost'): # Проверяем режим окна 
		FORM.attributes('-topmost',False)# Меняем режим окна
	else:
		FORM.attributes('-topmost',True)# Меняем режим окна
keyboard.add_hotkey('ctrl+1',topmost) # Привязываем событие к функции

Проблема окон, которые находятся поверх всех - что бы что-то под ними увидеть, нужно их передвигать, а это иногда бывает не удобно. Да бы избежать неудобств пользователя, мы можем добавить окну 50% прозрачности. Когда пользователь наводит курсор на окно, оно становится непрозрачным.

FORM.attributes('-alpha',0.5) # Задаём изначальное значение прозрачности
def form_focus_in(e = None):
	FORM.attributes('-alpha',1)
def form_focus_out(e = None):
	FORM.attributes('-alpha',0.5)
FORM.bind('<Enter>', form_focus_in) # При входе курсора в область окна выполняем form_focus_in
FORM.bind('<Leave>', form_focus_out)# При входе курсора в область окна выполняем form_focus_out
 

Поэкспериментировав с выше показанным, я думаю, вы в скором времени зададитесь вопросом "Можно ли убрать верхнюю панель у окна ?". Да, можно, но вам придётся самостоятельно реализовывать передвижение и если хотите, изменение размеров окна. Плюсы своего окна начинаются и заканчиваются том, что вы полностью контролируете внешний вид и логику окна. Также есть огромный минус - пока окно открыто, оно не отображается в панели задач, по этому ему желательно давать приоритетный режим, что бы пользователь не потерял окно под другими.

from tkinter import *
# Объявляем основные цвета
BGCL = '#000000'
CANCELCL = '#800000'
CANCELHOVCL = '#400000'
INFOCL = '#000080'
INFOHOVCL = '#000040'
BARCL = '#004000'
# Если число больше min, возвращает minrep, если число больше max, возвращает maxrep
def barrier(val,min = 0, max = None,minrep = None,maxrep = None):
    if minrep is None:minrep = min
    if maxrep is None: maxrep = max
    if not min is None and val < min:return minrep
    elif not max is None and val > max: return maxrep
    else:return val
# Просто пустая функция
def empty(*args,**kwargs):pass
# Класс усовершенстованого окна
class Form(Tk):
    def __init__(self,resizeable = True,exitfunc = empty,onresizefunc = empty):
        Tk.__init__(self)
        self['bg'] = BGCL
        self.resizeable = resizeable 
        self.exitfunc = exitfunc
        self.onresizefunc = onresizefunc
        self.overrideredirect(True) # убираем у окна панельку
        self.wm_attributes('-topmost',True) # приоритетный режим
        self.bar = Frame(self,bg = BARCL) # Создаём свою панельку, как отдельный виджет
        self.bar.place(x = 0 ,y = 0,relwidth = 1,height = 24)
        self.closebtn = Button(self,bg = CANCELCL,fg = BGCL,relief = FLAT,command = self.Exit,bd=0,activebackground = BGCL)
        self.closebtn.place(width =24,height = 24,x = self.winfo_reqwidth()-24)
        self.wrapbtn = Button(self,bg = INFOCL,fg = BGCL,relief = FLAT,command = self.Wrap,bd=0,activebackground = BGCL) 
        self.wrapbtn.place(width =24,height = 24,x = self.winfo_reqwidth()-48)
        # Здесь биндятся функции, передвигающие окно
        self.bar.bind("<ButtonPress-1>", self.StartMove)
        self.bar.bind("<ButtonRelease-1>", self.StopMove)
        self.bar.bind("<B1-Motion>", self.OnMotion)
        # Обеспечиваем hover эффект кнопкам на нашей панельке
        self.closebtn.bind("<Enter>",self.__closebtne)
        self.closebtn.bind("<Leave>",self.__closebtnl)
        self.wrapbtn.bind("<Enter>",self.__wrapbtne)
        self.wrapbtn.bind("<Leave>",self.__wrapbtnl)
        # Запоминаем ширину и высоту окна
        self.width = self.winfo_reqwidth()
        self.height = self.winfo_reqheight()
        # В этом framе создавайте новые виджеты 
        self.content = Frame(self,bg = BGCL,highlightthickness = 0)
        self.content.place(x=0,y = 24, width = self.width,height = self.height-24)
        if resizeable:
          	# создаём кнопку изменения размера
            self.resizebtn = Button(self,bg = BARCL,fg = BGCL,relief = FLAT,bd=0,activebackground = BGCL,text = '=',font = ('Fixedsys',11),cursor = 'tcross')
            self.resizebtn.place(width =12,height = 12,x = self.winfo_reqwidth()-12,y = self.winfo_reqheight()-12)
            # Её hover эффект
            self.resizebtn.bind("<Enter>",self.__resizebtne)
            self.resizebtn.bind("<Leave>",self.__resizebtnl)
            # Здесь биндятся функции, меняющие размер окна
            self.resizebtn.bind("<ButtonPress-1>", self.StartResize)
            self.resizebtn.bind("<ButtonRelease-1>", self.StopResize)
            self.resizebtn.bind("<B1-Motion>", self.OnResize)
        # Событие развёртывания окна (Редкое)
        self.bind('<Expose>',self.Show)
    # функции hover эффектов 
    def __closebtne(self,event = None):
      self.closebtn['bg'] = CANCELHOVCL
    def __closebtnl(self,event = None):
      self.closebtn['bg'] = CANCELCL
    def __resizebtne(self,event = None):
      self.resizebtn['bg'] = BGCL
      self.resizebtn['fg'] = BARCL
    def __resizebtnl(self,event = None):
      self.resizebtn['bg'] = BARCL
      self.resizebtn['fg'] = BGCL
    def __wrapbtne(self,event = None):
      self.wrapbtn['bg'] = INFOHOVCL
    def __wrapbtnl(self,event = None):
      self.wrapbtn['bg'] = INFOCL
    # Передвижение окна
    def StartMove(self, event = None):
        self.dragx = event.x
        self.dragy = event.y
    def StopMove(self, event = None):
        self.dragx = None
        self.dragy = None
    def OnMotion(self, event = None):
        deltax = event.x - self.dragx
        deltay = event.y - self.dragy
        x = self.winfo_x() + deltax
        y = self.winfo_y() + deltay
        self.geometry("+%s+%s" % (x, y))
    # Изменение размера окна
    def StartResize(self, event = None):
        self.resizex = event.x
        self.resizey = event.y
    def StopResize(self, event = None):
        self.resizex = None
        self.resizey = None
    def OnResize(self, event = None):
        deltax = event.x - self.resizex
        deltay = event.y - self.resizey
        x = self.width + deltax
        y = self.height + deltay
        self.width =barrier(x,min=self.minsize()[0])
        self.height = barrier(y,min=self.minsize()[1])
        self.geometry("%sx%s" % (self.width,self.height))
        self.Resize()
    # Функция вызывается после изменения размера, что бы заного разместить все кнопки.
    def Resize(self,event = None):
      # onresizefunc - вы можете передать функцию при инициализации, она будет выполнятся здесь
      self.onresizefunc(self.width,self.height)
      if self.resizeable: self.resizebtn.place_configure(x = self.width-12,y = self.height-12)
      self.closebtn.place_configure(y=0,x = self.width-24)
      self.wrapbtn.place_configure(y=0,x = self.width-48)
      self.content.place_configure(width = self.width,height = self.height-24)
    # Выполняется после нажатия красной кнопки
    def Exit(self,event = None):
      # exitfunc - вы можете передать функцию при инициализации, она будет выполнятся здесь
      self.exitfunc() 
      self.destroy()
    # Сворачивает окно
    def Wrap(self,event = None):
      self.withdraw() # скрытие окна
      self.overrideredirect(False) # возвращаем ему панельку
      self.wm_state('iconic') # сворачиваем
    def Show(self,event = None):
      # Окно уже развёрнуто
      self.overrideredirect(True) #Просто обратно забираем панельку
FORM = Form() # Создаём объект Form (изменённый Tk)
FORM.minsize(200,200) #Задаём минимальный размер
FORM.mainloop()

Вкратце опишу произошедшее выше. Мы создаём класс Form на основе класса Tk, то есть Form это модифицированный Tk. При инициализации объекта мы добавляем ему кнопки закрытия, сворачивания, изменения размера и два фрейма, панелька и контент. Делаем ховер эффект для каждой кнопки. Дальше реализуем перемещение окна. По нажатию на клавишу мыши координаты сохраняются, затем по мере движения мыши в соответствии с сохранёнными координатами меняется положение окна. С изменениями размера система та же, но при изменениях размера меняется также положение кнопок и. т. п.

Результат
Результат

Необычная игра

Скептик скажет "Питон не для игр, а tkinter так уж тем более". И я с этим скептиком от части согласен, tkinter НЕ для игр, для игр лучше pygame, а вот для "десктопных" игр это самое простое и единственное мне известное решение. Под "десктопной" игрой я подразумеваю игру, которая отрисовывается поверх всех окон, прямо на рабочем столе. В этом примере пиксельный человечек прыгает по окнам, это просто пример.

Как работает отрисовка поверх экрана в tkinter? Создаётся полноэкранное белое окно, на нём создаётся белый canvas, на canvase отрисовывается графика и всё белое заменяется прозрачным. Цвет не обязательно должен быть белым. В подобных играх также следует использовать keyboard, а именно функцию is_pressed() для проверки нажатия клавиш.

import tkinter as tk
import keyboard 
FORM = tk.Tk()
def Update(e = None):
  # Ваш игровой цикл 
  FORM.after(int(1000/FPS),Update) 
FPS = 60
CANVAS = tk.Canvas(FORM,bg = 'white',bd = 0,highlightthickness = 0)
CANVAS.place(x=0,y=0,width = FORM.winfo_screenwidth(),height = FORM.winfo_screenheight())
FORM.overrideredirect(True)
FORM.state('zoomed')
FORM.wm_attributes("-topmost", True)
FORM.wm_attributes("-transparentcolor", "white")
FORM.after(int(1000/FPS),Update) 
FORM.mainloop()

Я также напишу статью про создание "десктопной" игры в скором времени.

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


  1. altyshevamaria
    11.08.2022 17:52

    Отличное начало) Хотя и не соглашусь в плане того, что pygame для игр( у меня python ассоциируется в последнее время только с машинным обучением. Но сколько людей, столько и мнений.


    1. iredun
      11.08.2022 23:23

      Если знаешь, что делаешь, то и на pygame можно сделать адекватное.
      Пример https://www.youtube.com/watch?v=JugaoeDyKB4, очень круто человек делает)


    1. BRO_Fedka Автор
      13.08.2022 19:03

      Безусловно, качественные современные игры следует создавать на движках. Создание игр на python это больше челендж и практика навыков, но опять же есть разработчик DaFluffyPotato (есть на YouTube), который делает инди игры для стима на питоне и проблем не испытывает. Я к примеру сейчас делаю браузерную онлайн игру и да бы перенести её на дэсктоп / телефон мне проще написать её на pygame, чем изучать C# / C++ (Движки мне не подходят по тех причинам). И отмечу, что в pygame можно работать с OpenGl, а значит использовать шейдеры. С pygame и с ускорялками по типу Numba работает Standalone Coder (Ютубер).


  1. alexeyohilkov
    11.08.2022 18:46
    +1

    "И по большей части эта статья нацелена на начинающих программистов, которые уже имеют представление о библиотеке и хотят рыть дальше"

    после написания приложения на tkinter и многократных улучшений для него сталкивался с основной проблемой того, что поддержкой основным либ давно никто не занимается.

    pyQt давно перехватил инициативу и большую часть контрибутеров
    поэтому для новичка рекомендовал бы все-таки изучать pyQt, а не tkinter


    1. kai3341
      11.08.2022 20:36

      У tkinter есть одно важное преимущество -- он точно есть везде, где есть python.

      Но убожество мегагалактическое в текущем виде, это да. И проблема не в том, что оно плохо работает, а в том, что production код писать на нём сложно


      1. kAIST
        11.08.2022 21:57

        Не совсем он есть везде. В windows да, он идёт "из коробки", в linux tkinted нужно ставить дополнительно.

        Ну ещё бы назвал плюсом - маленький размер сборки через pyinstaller.


        1. kai3341
          11.08.2022 22:10

          в linux tkinted нужно ставить дополнительно

          Не могу вспомнить, когда я его себе ставил, но он у меня есть. Возможно, он в зависимостях python3-all -- а я не склонен искать себе лишний геморрой

          UPD: хм, действительно, у меня установлен отдельный пакет python3-tk


    1. BRO_Fedka Автор
      13.08.2022 21:06

      Хехе, честно, причина по которой я обычно использую tkinter - я не могу установить нужные инструменты на свою машину.


  1. HemulGM
    11.08.2022 19:27
    +2

    Берём Delphi) Создаем проект Multi-Device Application. У формы вставляем Transparency = True, кладём на форму Viewport3D, задаем ему цвет фона - Null. А теперь помещаем в 3D дизайнер объекты, меши и прочее) Готово. 3D "игра" на рабочем столе)
    Примерно так (https://youtu.be/U802Uik8IzM) простите за мат)

    P.S. у окон можно задать рамку без управляющих кнопок


    1. kai3341
      11.08.2022 20:39

      Delphi -- это такой же мем, как и Гуф


      1. HemulGM
        12.08.2022 08:47

        Тем не менее, возможности и простота Делфи не может не впечатлять.


        1. kai3341
          12.08.2022 17:49

          На старте он поистине прекрасен. Проблемы начинаются, когда ООП заменяется компонентно-ориентированноым программированием и когда формочки залетают на сервер. Насмотрелся в своё время


          1. HemulGM
            12.08.2022 17:52

            "Не правильно" можно использовать любой язык.


    1. BRO_Fedka Автор
      13.08.2022 21:18

      Капееееец красота !


  1. kai3341
    11.08.2022 20:24

    <зануда_mode>

    Нарушен SRP. Да, управление формой через класс -- это вин, с самого начала статьи ожидал этого. Наличие Button в этом классе -- это фейл.

    Рекомендую глянуть на React и их подход -- есть базовые компоненты и есть их композиция в компонентах верхнего уровне, у вышестоящих компонентов своя композиция, и так далее. Ну да, без JSX будет не так удобно UI верстать, но почему бы и да?

    </зануда_mode>

    С учётом развития webasembly и портирования туда python ваши наработки могут лечь в основу питонячьей версии ноды, которая заявляется работающей во всех дырах. Лишь бы был удобный API и работало быстро. Дерзайте

    Можно упороться в EBNF и запилить свой JSX, только с

    from __future__ import braces

    PS: Ну да, есть и такой изврат


    1. kai3341
      11.08.2022 20:56

      И даже что-то похожее есть: https://pypi.org/project/pyreact2/

      Но там всё равно о генерации HTML и конкатенации строк


      1. BRO_Fedka Автор
        13.08.2022 21:16

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


    1. BRO_Fedka Автор
      13.08.2022 21:14

      Ничего не понял, но очень интересно. Я не до конца понял в чём конкретно проблема класса моего окна, если здесь можно было сделать что-то более универсальное, практичное, то как я и сказал в начале статьи "Это по большей части статья для новичков". Я вообще сомневался, стоит ли сюда выкладывать класс окна, так как я сам долго разбирался и упрощал его (модуль с этим окном я написал пол года назад). И под конец напомню о самом главном правиле программиста - " РАБОТАЕТ ? НЕ ТРОГАЙ !"