
Знакомо, правда? Да, да - это "рабочий стол" Windows 3.1, которая вышла в 1992 году. И даже если вы не из того поколения, у которого сейчас свело олдскулы, вы, я думаю, все равно хоть раз в жизни видели эту ОС (хотя бы на картинке) и не остались к ней равнодушны.
В этой статье мы напишем простенький игрушечный оконный псевдо-менеджер в стиле Windows 3.x. Использовать для этого мы будем Python и стандартную библиотеку Tkinter. Выглядеть он будет так:

Целью статьи является не создание визуальной копии 3.x, а упрощенная реализация главной фичи Windows, которая и дала ей название - окошек. Стилизованных под 3.x, разумеется.
Ну что же, поехали!
Как это будет устроено
Структура нашего проекта будет выглядеть так:
├───main.py
│
├───assets
│
└───sources
├───program
│ calc.py
│ notepad.py
│ terminal.py
│
└───window
manager.py
window.py
У нас будет точка входа (main.py), которая запускает всю программу, базовый класс Window
(sources/window.py), который будет отвечать за окна нашей псевдо-windows, и WindowManager
(sources/manager.py), который этими окнами будет управлять.
В придачу у нас идет еще 3 демки, но они необязательны и непосредственно с менеджером никак не связаны (в смысле зависимостей). Вы можете написать и свои приложения - главное, чтобы они были на tkinter, а их главный класс наследовалcя от tk.Frame
Примечание
Если идея понравится общественности, то в отдельной статье я покажу, как можно запихнуть pygame в фрейм tkinter
Пара слов о стилизации и функциональности

При создании интерфейса я старался ориентироваться на оригинал (см. картинку). В нашем проекте также, как в оригинале, используется синий цвет заголовка, рамка вокруг окна с уголками для изменения размера, используются такие же кнопки в заголовке. Но у нас нет полного сворачивания окон (кнопка свернуть только возвращает окно к исходному размеру), отсутствует Program Manager, группы и много чего ещё. Но базовый функционал все-таки присутствует.
Итак, начнем.
Класс Window
Перед тем как приступить к написанию основного класса окна создадим следующий вспомогательный класс:
from enum import IntFlag
class WindowFlags(IntFlag):
"""
Bitmap flags for window options.
'WN' means 'Window Not', so 'WN_DRAGABLE' means 'Window Not Dragable',
'WN_RESIZABLE' means 'Window Not Resizable', and so on.
"""
WN_CONTROLS = 1
WN_DRAGABLE = 2
WN_RESIZABLE = 4
С помощью него мы будем определять и задавать некоторые параметры нашего окна:
WN_CONTROLS - выключает кнопки свернуть/развернуть
WN_DRAGABLE - отключает перетаскивание окна за заголовок (но за углы пока еще можно, это баг)))
WN_RESIZABLE - отключает изменение размеров окна
Возможно, вы уже догадались, что эти параметры задаются битовыми флагами. Так гораздо удобнее их сочетать и использовать - не нужно хранить кучу bool-переменных. Позже вы увидите, как мы будем их использовать (если, конечно, будете внимательно читать код)
Таблица флагов
Флаги | Двоичное | Десятичное |
---|---|---|
WN_CONTROLS | 001 | 1 |
WN_DRAGABLE | 010 | 2 |
WN_RESIZABLE | 100 | 4 |
Теперь перейдем к окну. Импортируем необходимы библиотеки и инициализируем класс:
import tkinter as tk
from PIL import Image, ImageTk
# WindowFlags here
class Window:
"""
A class representing a draggable, resizable window in the style of Windows 3.1.
The window includes a title bar with minimize, maximize and close buttons,
resizable corners, and a content area that can contain child windows.
"""
def __init__(self, parent, title, content, size, flags):
"""Initialize a new window.
Args:
parent: The parent widget (either the main frame or another window's content area)
title: The title of the window
content: The content widget to be placed inside the window
size: A tuple of (x, y, width, height) for the window's initial position and size
flags: Bitmap flags for window options
"""
self.parent = parent
self.title = title
self.content = content
self.size = size
self.flags = flags
# Initialize window state variables
self.is_resizing = False
self.is_maximized = False
# Store the previous size and position for restoration when un-maximizing
self.resize_offset_x = 0
self.resize_offset_y = 0
# Create a list to store child windows references
self.childs = []
self.create_window()
if self.content:
self.set_content(self.content)
Наше окно будет состоять из:
Заголовка с кнопками (
self.title
)Рамки вокруг окна с выделенными углами для перетаскивания (в init не присутствует, мы будем рисовать ее отдельно)
Основного содержимого (
self.content
)И дочерних окон (необязательно) (
self.childs
)
Остальное, думаю, в объяснениях не нуждается. Идём дальше:
Код
def create_window(self):
""" Create the window's visual elements and set up event bindings """
# Unpack the size tuple
x, y, width, height = self.size
# Create the main window frame
self.window_frame = tk.LabelFrame(
self.parent,
relief="flat",
padx=2,
pady=2,
background="#878A8D",
highlightthickness=1,
highlightcolor="#000000",
highlightbackground="#000000"
)
self.window_frame.place(x=x, y=y, width=width, height=height)
# Add resizable corners if the window is resizable
if not self.flags & WindowFlags.WN_RESIZABLE:
self.corner_A = tk.LabelFrame(
self.window_frame,
width=31,
height=31,
bg="#878A8D",
highlightthickness=1,
highlightcolor="#000000",
highlightbackground="#000000",
relief="flat"
)
self.corner_A.place(x=-5, y=-5)
self.corner_B = tk.LabelFrame(
self.window_frame,
width=31,
height=31,
bg="#878A8D",
highlightthickness=1,
bd=1,
highlightcolor="#000000",
highlightbackground="#000000",
relief="flat"
)
self.corner_B.place(relx=1.0, x=5, y=-5, anchor='ne')
self.corner_C = tk.LabelFrame(
self.window_frame,
width=31,
height=31,
bg="#878A8D",
highlightthickness=1,
bd=1,
highlightcolor="#000000",
highlightbackground="#000000",
relief="flat"
)
self.corner_C.place(relx=1.0, x=5, rely=1.0, y=5, anchor='se')
self.corner_D = tk.LabelFrame(
self.window_frame,
width=31,
height=31,
bg="#878A8D",
highlightthickness=1,
bd=1,
highlightcolor="#000000",
highlightbackground="#000000",
relief="flat"
)
self.corner_D.place(rely=1.0, x=-5, y=5, anchor='sw')
# Create the title bar with a close button
self.title_bar = tk.Frame(
self.window_frame,
relief="raised",
borderwidth=1,
background="#000000",
highlightthickness=0,
highlightcolor="#FCFCFC",
highlightbackground="#FCFCFC"
)
self.title_bar.pack(fill="x")
self.close_icon = ImageTk.PhotoImage(
Image.open("./assets/close_button.bmp"))
self.close_button = tk.Button(
self.title_bar,
command=self.show_context_menu,
image=self.close_icon,
width=20,
height=20,
borderwidth=1,
relief="flat",
anchor="center",
bg="#C0C7C8",
activebackground="#C0C7C8",
fg="#000000",
highlightthickness=1,
highlightcolor="#000000",
highlightbackground="#000000"
)
self.close_button.pack(side="left")
self.title_label = tk.Label(
self.title_bar,
text=self.title,
font=("Arial", 10, "bold"),
anchor="center",
background="#000076",
foreground="#FCFCFC"
)
self.title_label.pack(side="left", fill="both", expand=True)
# Add maximize and minimize buttons if the window has controls
if not self.flags & WindowFlags.WN_CONTROLS:
self.maximize_icon = ImageTk.PhotoImage(
Image.open("./assets/maximize_button.bmp"))
self.maximize_button = tk.Button(
self.title_bar,
command=self.maximize_window,
image=self.maximize_icon,
width=20,
height=20,
borderwidth=1,
relief="raised",
anchor="center",
bg="#C0C7C8",
activebackground="#C0C7C8",
fg="#000000",
highlightthickness=1,
highlightcolor="#000000",
highlightbackground="#000000"
)
self.maximize_button.pack(side="right")
self.shrink_icon = ImageTk.PhotoImage(
Image.open("./assets/minimize_button.bmp"))
self.shrink_button = tk.Button(
self.title_bar,
command=self.minimize_window,
image=self.shrink_icon,
width=20,
height=20,
borderwidth=1,
relief="raised",
anchor="center",
bg="#C0C7C8",
activebackground="#C0C7C8",
fg="#000000",
highlightthickness=1,
highlightcolor="#000000",
highlightbackground="#000000"
)
self.shrink_button.pack(side="right")
# Create the content area of the window
self.window_content = tk.Frame(
self.window_frame,
bg="#ffffff",
highlightthickness=1,
highlightcolor="#000000",
highlightbackground="#000000"
)
self.window_content.pack(fill="both", expand=True)
# Bind drag events if the window is draggable
if not self.flags & WindowFlags.WN_DRAGABLE:
self.window_frame.bind("<ButtonPress-1>", self.start_drag)
self.window_frame.bind("<ButtonRelease-1>", self.stop_drag)
self.window_frame.bind("<B1-Motion>", self.drag)
self.title_label.bind("<ButtonPress-1>", self.start_drag)
self.title_label.bind("<ButtonRelease-1>", self.stop_drag)
self.title_label.bind("<B1-Motion>", self.drag)
# Bind resize events if the window is resizable
if not self.flags & WindowFlags.WN_RESIZABLE:
self.corner_A.bind("<ButtonPress-1>", self.start_resize)
self.corner_A.bind("<ButtonRelease-1>", self.stop_resize)
self.corner_A.bind("<B1-Motion>", self.resize)
self.corner_B.bind("<ButtonPress-1>", self.start_resize)
self.corner_B.bind("<ButtonRelease-1>", self.stop_resize)
self.corner_B.bind("<B1-Motion>", self.resize)
self.corner_C.bind("<ButtonPress-1>", self.start_resize)
self.corner_C.bind("<ButtonRelease-1>", self.stop_resize)
self.corner_C.bind("<B1-Motion>", self.resize)
self.corner_D.bind("<ButtonPress-1>", self.start_resize)
self.corner_D.bind("<ButtonRelease-1>", self.stop_resize)
self.corner_D.bind("<B1-Motion>", self.resize)
self.window_frame.bind("<ButtonPress-1>", self.start_resize)
self.window_frame.bind("<ButtonRelease-1>", self.stop_resize)
self.window_frame.bind("<B1-Motion>", self.resize)
# Track window size and position changes
self.window_frame.bind(
"<Configure>", self.track_window_size_and_position)
self.title_label.bind(
"<Configure>", self.track_window_size_and_position)
self.context_menu = tk.Menu(self.window_frame,
tearoff=0,
bg="#C0C0C0",
fg="black",
activebackground="#808080",
activeforeground="white",
font=("MS Sans Serif", 8))
self.context_menu.add_command(label="Close", command=self.close_window)
self.context_menu.add_command(
label="Minimize", command=self.minimize_window)
self.context_menu.add_command(
label="Maximize", command=self.maximize_window)
Это самая большая часть класса. Оно и понятно - здесь мы создаем рамку вокруг окна, углы (если размер можно изменять), панель меню с кнопками и привязываем различные события. В принципе всё понятно, глубоко вникать не будем, пойдём дальше.
Когда я писал статью, я думал, расписывать ли подробно всю геометрию нашего "менеджера окон", и решил, что не стоит. Во-первых, это очень трудоёмко и объёмно, статья бы получилась большой и скучной. Во-вторых, это никому особо не интересно. Если вы новичок в tkinter вас это только запутает, а если вы уже работали с ним, то вы без особого труда разберётесь, как всё это делается.
А дальше у нас:
Метод show_context_menu
. Он отвечает вот за это:

def show_context_menu(self):
"""Show the context menu at the mouse position."""
# Get the position of the close button
x = self.close_button.winfo_rootx()
y = self.close_button.winfo_rooty() + self.close_button.winfo_height()
# Post the context menu at the position of the close button
self.context_menu.tk_popup(x, y)
Затем close_window
. Он уничтожает окно при нажатии на кнопку Close
def close_window(self):
""" Destroy the window and remove it from the display """
self.window_frame.destroy()
Далее у нас идет свертывание (minimize) и развертывание (maximaize) окна. Пока это реализовано так: кнопка "Свернуть" сворачивает окно к исходному состоянию, в котором оно было создано, но не сворачивает полностью, кнопка "Развернуть" разворачивает окно на весь экран.
def maximize_window(self):
"""
Maximize the window to fill the entire parent widget
Stores the previous size and position for restoration when un-maximizing
"""
# If the window is already maximized, do nothing
if self.is_maximized:
return
# Store the current size and position for restoration
x = self.window_frame.winfo_x()
y = self.window_frame.winfo_y()
width = self.window_frame.winfo_width()
height = self.window_frame.winfo_height()
self.previous_size_and_position = (x, y, width, height)
# Get parent dimensions
parent_width = self.parent.winfo_width()
parent_height = self.parent.winfo_height()
# Maximize the window to fill the parent widget
self.window_frame.place(
x=-6, y=-6, width=parent_width + 12, height=parent_height + 13)
self.is_maximized = True
# Update all maximized child windows to fit new size
for child in self.childs:
if child.is_maximized:
content_width = self.window_content.winfo_width()
content_height = self.window_content.winfo_height()
child.window_frame.place(
x=-6, y=-6, width=content_width + 12, height=content_height + 13)
def minimize_window(self):
""" Restore the window to its previous size and position if maximized """
# If the window is not minimized, do nothing
if self.is_maximized:
# Restore to previous size and position
x, y, width, height = self.previous_size_and_position
self.window_frame.place(x=x, y=y, width=width, height=height)
self.is_maximized = False
# Update all child windows that are maximized
for child in self.childs:
if child.is_maximized:
child.maximize_window()
Следующим пунктом у нас идет перемещение окна:
def start_drag(self, event):
""" Begin window dragging operation """
# Bring the window to the front
self.lift(event)
# Store initial mouse position and window position
self.drag_start_x = event.x_root
self.drag_start_y = event.y_root
self.window_start_x = self.window_frame.winfo_x()
self.window_start_y = self.window_frame.winfo_y()
# Prevent conflict with resize operations
self.is_dragging = True
def stop_drag(self, event: tk.Event):
""" End window dragging operation """
self.is_dragging = False
self.drag_start_x = None
self.drag_start_y = None
def drag(self, event: tk.Event):
""" Handle window movement during drag operation """
# Prevent dragging if the window is not being dragged
if not hasattr(self, 'is_dragging') or not self.is_dragging or self.is_maximized:
return
# Calculate the displacement from the start position
deltax = event.x_root - self.drag_start_x
deltay = event.y_root - self.drag_start_y
# Update window position based on initial position plus displacement
new_x = self.window_start_x + deltax
new_y = self.window_start_y + deltay
self.window_frame.place(x=new_x, y=new_y)
Получилось корявенько (особенно if not hasattr(self, 'is_dragging') or not self.is_dragging or self.is_maximized
), ну да ладно. Особо хочу заметить, что start_drag
отвечает еще и за фокус на окне: когда вы нажимаете на заголовок, срабатывает событие drag и self.lift
перемещает окно вверх.
Теперь пришел черед ресайзинга:
Код
def start_resize(self, event):
""" Begin window resizing operation """
self.lift(event)
# Prevent conflict with drag operations
if hasattr(self, 'is_dragging'):
self.is_dragging = False
corner = event.widget
# Store initial window geometry
self.resize_start_x = event.x_root
self.resize_start_y = event.y_root
self.window_start_width = self.window_frame.winfo_width()
self.window_start_height = self.window_frame.winfo_height()
self.window_start_x = self.window_frame.winfo_x()
self.window_start_y = self.window_frame.winfo_y()
# Determine which corner was clicked and set the cursor accordingly
if corner == self.corner_A:
cursor = "top_left_corner"
handle_x = self.window_frame.winfo_width() - 1
handle_y = self.window_frame.winfo_height() - 1
elif corner == self.corner_B:
cursor = "top_right_corner"
handle_x = 0
handle_y = self.window_frame.winfo_height() - 1
elif corner == self.corner_C:
cursor = "bottom_right_corner"
handle_x = 0
handle_y = 0
elif corner == self.corner_D:
cursor = "bottom_left_corner"
handle_x = self.window_frame.winfo_width() - 1
handle_y = 0
else:
cursor = "bottom_left_corner"
handle_x = self.window_frame.winfo_width() - 1
handle_y = 0
# Store the handle position relative to the window and the root window
handle_pos_x_root = handle_x + self.window_frame.winfo_rootx()
handle_pos_y_root = handle_y + self.window_frame.winfo_rooty()
# Store the handle position relative to the window
self.handle_pos_x = handle_x
self.handle_pos_y = handle_y
# Store the handle position relative to the root window
self.handle_pos_x_root = handle_pos_x_root
self.handle_pos_y_root = handle_pos_y_root
# Set the cursor and start resizing
self.cursor = cursor
self.window_frame.config(cursor=cursor)
self.is_resizing = True
# Store the initial mouse position for resizing
self.resize_offset_x = event.x
self.resize_offset_y = event.y
def stop_resize(self, event: tk.Event):
"""
End window resizing operation
Args:
event: The mouse event that triggered the end of resizing
"""
if hasattr(self, 'is_resizing') and self.is_resizing:
# Only stop resizing if we are already resizing (to prevent stopping a resize that never started)
# This can happen if you release the mouse button while not resizing (which can happen accidentally while dragging fast)
# If we don't prevent that we will end up with an inconsistent state that will make the window flicker and behave erratically while dragging or resizing
self.window_frame.config(cursor="")
self.is_resizing = False
def resize(self, event: tk.Event):
"""Handle window resizing during resize operation.
Args:
event: The mouse motion event
Updates window dimensions while maintaining minimum size constraints.
"""
self.lift(event)
if not self.is_resizing:
return
# Calculate the displacement from the start position
deltax = event.x_root - self.resize_start_x
deltay = event.y_root - self.resize_start_y
# Calculate new dimensions based on which corner is being dragged
if self.handle_pos_x == 0:
# Left corners
new_width = max(200, self.window_start_width + deltax)
new_x = self.window_start_x
else:
# Right corners
new_width = max(200, self.window_start_width - deltax)
new_x = self.window_start_x + deltax
if self.handle_pos_y == 0:
# Bottom corners
new_height = max(150, self.window_start_height + deltay)
new_y = self.window_start_y
else:
# Top corners
new_height = max(150, self.window_start_height - deltay)
new_y = self.window_start_y + deltay
# Update window geometry in a single operation
self.window_frame.place(
x=new_x, y=new_y, width=new_width, height=new_height)
Эта самая сложная часть логики класса. Если коротко, то это работает так:
При нажатии на угол, мы определяем угол (A, B, C, D)
Затем фиксируем начальные координаты и размеры
После этого для каждого угла задаем "якорную точку". Например для левого верхнего угла (A) это
handle_x = ширина окна - 1
Затем рассчитываем дельту и определяем новую ширину и позицию.
Условно это можно представить в виде такой таблицы:
Угол
handle_x
handle_y
Изменение параметров
Левый верх
width-1
height-1
width+, height+, x+, y+
Правый верх
0
height-1
width-, height+, y+
Правый низ
0
0
width-, height-
Левый низ
width-1
0
width+, height-, x+
Далее идет метод track_window_size_and_position,
который выполняет две ключевые задачи:
Сохранение текущих координат и размер окна, только если оно не максимизировано, чтобы при восстановлении окна из максимизированного/минимизированного состояния можно было вернуть оригинальные размеры
Синхронизация дочерних окон (если окно содержит дочерние окна, которые были максимизированы, они автоматически подстраиваются под новый размер родительской области)
def track_window_size_and_position(self, event: tk.Event):
"""Store window geometry for restore operations.
Args:
event: The Configure event containing new geometry
Used to remember window size/position before maximize/minimize.
"""
# Track changes in the window's size and position to remember its size before it was maximized or shrinked
# This is used to restore the window to its previous size when it is un-maximized or un-shrinked
x = event.x
y = event.y
width = event.width
height = event.height
if not hasattr(self, 'is_maximized') or not (self.is_maximized):
self.previous_size_and_position = (x, y, width, height)
# Update maximized child windows when parent size changes
if hasattr(self, 'childs'):
for child in self.childs:
if hasattr(child, 'is_maximized') and child.is_maximized:
# Get parent content area dimensions
content_width = self.window_content.winfo_width()
content_height = self.window_content.winfo_height()
# Update child window size to match new parent content area
child.window_frame.place(x=-6, y=-6,
width=content_width + 12,
height=content_height + 13)
Ну и оставшаяся часть:
def lift(self, event: tk.Event):
"""
Raise window above other windows in z-order.
Args:
- event: The event that triggered the raise operation
"""
self.window_frame.lift()
def create_child(self, title, content, size, flags):
"""Create a new window as a child of this window's content area.
Returns:
Window: The newly created child window
"""
child_window = Window(self.window_content, title, content,
size, flags) # Create a new Window instance as a child
# Add the child window to the list of windows
self.childs.append(child_window)
return child_window
def set_content(self, content):
"""
Set the content widget inside the window and ensure it adapts to the window size.
Arguments:
- content: The content widget to be placed inside the window (Must be a class, not an instance)
"""
# We need to destroy the existing content widget before adding a new one
# content must be a class, not an instance
self.content = content(self.window_content)
# then we can place the widget inside the window
self.content.pack(fill="both", expand=True)
Метод lift
"поднимает" окно вверх


Метод create_child
создает дочерние окна

Ну и наконец, метод set_content
устанавливает содержимое окна (дочернее от tk.Frame)
На этом класс Window
заканчивается. Основная часть нашей программы написана!
Класс WindowManager
Это достаточно простой класс и ничего сложного в нем нет:
import tkinter as tk
from .window import Window
class WindowManager:
"""Main window manager class that handles the desktop environment.
Provides the main application window and manages creation of child windows.
"""
def __init__(self, root):
"""Initialize the window manager.
Args:
root: The root Tkinter window
"""
self.root = root
self.root.title("Window Manager")
self.root.attributes("-fullscreen", True)
self.windows = []
self.root.config(cursor="@./assets/cursor_a.cur")
self.main_frame = tk.Frame(self.root, bg="#29A97E")
self.main_frame.pack(fill="both", expand=True)
def create_window(self, title="Window", content=None, size=(50, 50, 210, 200), flags=0):
"""
Create a new top-level window
Args:
title: The title of the window
content: The content widget to be placed inside the window
size: A tuple of (x, y, width, height) for the window's initial position and size
flags: Bitmap flags for window options
Returns:
Window: The newly created window
"""
window = Window(self.main_frame, title, content, size, flags)
self.windows.append(window)
return window
def create_child(self, parent: Window, title="Window", content=None, size=(50, 50, 210, 200), flags=0):
"""
Create a new child window inside the given parent window
Args:
parent: The parent Window instance
Returns:
Window: The newly created child window
"""
child = parent.create_child(
title, content=content, size=size, flags=flags)
self.windows.append(child)
return child
Первым делом мы разворачиваем наше окно на весь экран, загружаем олдскульный курсор и заполняем окно фреймом зеленого цвета.
Примечание
Если вы на Linux, загрузку курсора придется выпилить - файлы .cur там не поддерживаются
Далее идут методы create_window
и create_child
. Они отвечают за добавление нового окна и дочернего окна для уже существующего.
Всё! Давайте протестируем. Для этого в main.py
добавим:
import tkinter as tk
from sources.window.manager import WindowManager
def main():
root = tk.Tk()
app = WindowManager(root)
app.create_window(title="Notepad", content=None,
size=(800, 50, 200, 300))
root.mainloop()
if __name__ == "__main__":
main()
Запустив это, мы увидим следующее (местоположение курсора съехало, но это баг записи):
Демки
Чтобы сделать наш менеджер поинтереснее добавим три программки. У них отсутствует (кроме, разве только, калькулятора) функциональность, но на них хорошо видны возможности нашей игрушки. Итак:
Калькулятор (/sources/program/calc.py)
import tkinter as tk
class Calculator(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.master = master
self.pack()
self.create_widgets()
self.expression = ""
def create_widgets(self):
self.display = tk.Entry(self, font=(
'Arial', 18), borderwidth=2, relief="sunken")
self.display.grid(row=0, column=0, columnspan=4, sticky="nsew")
buttons = [
'7', '8', '9', '/',
'4', '5', '6', '*',
'1', '2', '3', '-',
'0', '.', '=', '+'
]
row_val = 1
col_val = 0
for button in buttons:
tk.Button(self, text=button, font=('Arial', 18), command=lambda b=button: self.button_click(
b)).grid(row=row_val, column=col_val, sticky="nsew")
col_val += 1
if col_val > 3:
col_val = 0
row_val += 1
for i in range(1, 5):
self.grid_rowconfigure(i, weight=1)
for j in range(4):
self.grid_columnconfigure(j, weight=1)
def button_click(self, char):
if char == '=':
try:
result = str(eval(self.expression))
self.display.delete(0, tk.END)
self.display.insert(tk.END, result)
self.expression = result
except Exception as e:
self.display.delete(0, tk.END)
self.display.insert(tk.END, "Ошибка")
self.expression = ""
else:
self.expression += str(char)
self.display.insert(tk.END, char)
Блокнот (/sources/program/notepad.py)
import tkinter as tk
from tkinter import scrolledtext
class Notepad(tk.Frame):
def __init__(self, parent=None):
super().__init__(parent)
self.text_area = scrolledtext.ScrolledText(self, wrap=tk.WORD, width=40, height=10, font=("Fixedsys", 12))
self.text_area.pack(fill="both", expand=True)
Терминал (/sources/program/terminal.py)
В Windows 3.1, конечно, не было терминала, так как он работал поверх dos, но я решил добавить его для большей эффектности))
import tkinter as tk
from tkinter import scrolledtext
class Terminal(tk.Frame):
def __init__(self, parent=None):
super().__init__(parent)
self.text_area = scrolledtext.ScrolledText(
self, wrap=tk.WORD,
width=80,
height=25,
font=("Fixedsys", 10),
bg="black",
fg="white",
insertbackground="white",
blockcursor=True
)
self.text_area.pack(fill="both", expand=True)
self.text_area.bind("<Return>", self.execute_command)
self.text_area.config(insertofftime=500, insertontime=500) # Blinking cursor
self.text_area.focus_set() # Set focus to the text area
def execute_command(self, event):
command = self.text_area.get("insert linestart", "insert lineend")
self.text_area.insert(tk.END, f"\nExecuted: {exec(command)}\n")
Теперь осталось только изменить main.py
import tkinter as tk
from sources.window.manager import WindowManager
from sources.window.window import WindowFlags
from sources.program.notepad import Notepad
from sources.program.terminal import Terminal
from sources.program.calc import Calculator
def main():
root = tk.Tk()
app = WindowManager(root)
app.create_window(title="Notepad", content=Notepad,
size=(800, 50, 200, 300), flags=WindowFlags.WN_DRAGABLE)
app.create_child(app.windows[0], title="Notepad", content=Notepad,)
app.create_window(title="Terminal", content=Terminal,
size=(275, 50, 500, 300), flags=WindowFlags.WN_CONTROLS)
app.create_window(title="Calculator", content=Calculator,
size=(50, 200, 500, 300), flags=WindowFlags.WN_RESIZABLE)
root.mainloop()
if __name__ == "__main__":
main()
Готово!

Заключение
На этом все. Надеюсь идея вам понравилась, а обилие кода не утомило) На самом деле, эта игрушка не несет никакой практической пользы (кроме обучающей, конечно), но работа с этим кодом мне принесла большое удовольствие, ведь всегда приятно самому сделать что-то, чем раньше сам восхищался :)
Весь исходный код можно скачать на моем GitHub: https://github.com/GVCoder09/TkWindowsManager
Спасибо, что уделили время на мою статью (или хотя бы на скроллинг до ее конца)!