Создаем игрушечный оконный менеджер в ретро-стиле Windows 3.x на Python

от автора

Знакомо, правда? Да, да — это «рабочий стол» 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

Пара слов о стилизации и функциональности

Windows 3.11 в DOSBox

Windows 3.11 в DOSBox

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

Эта самая сложная часть логики класса. Если коротко, то это работает так:

  1. При нажатии на угол, мы определяем угол (A, B, C, D)

  2. Затем фиксируем начальные координаты и размеры

  3. После этого для каждого угла задаем «якорную точку». Например для левого верхнего угла (A) это handle_x = ширина окна - 1

  4. Затем рассчитываем дельту и определяем новую ширину и позицию.

    Условно это можно представить в виде такой таблицы:

    Угол

    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

Спасибо, что уделили время на мою статью (или хотя бы на скроллинг до ее конца)!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Ну и (не)большой опросик) Пользовательский интерфейс из какой Windows у вас самый любимый?

0% Windows 1.00
3.08% Windows 2.02
6.15% Windows 3.x (NT 3.x)4
1.54% Windows 951
9.23% Windows 986
10.77% Windows 20007
20% Windows XP13
0% Windows Vista0
4.62% Windows XP (сорри, лишнее, но удалить уже не могу)3
21.54% Windows 714
0% Windows 80
12.31% Windows 108
6.15% Windows 114
4.62% Не-на-ви-жу!3

Проголосовали 65 пользователей. Воздержались 7 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/898484/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *