Программирование собственного менеджера окон на Python
Обзор решений для оконного менеджера в Python
Как создать многодокументный интерфейс с вложенными окнами?
Наиболее эффективное решение: PyQt5 и QMdiArea
Библиотека PyQt5 предоставляет готовый компонент QMdiArea, предназначенный для управления несколькими дочерними окнами внутри одного родительского. Этот подход широко используется в приложениях с многодокументной архитектурой (MDI), таких как редакторы кода, среды разработки и графические редакторы.
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QMdiArea, QMdiSubWindow, QTextEdit
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('MDI Приложение')
self.setGeometry(100, 100, 800, 600)
mdi = QMdiArea()
self.setCentralWidget(mdi)
# Создание первого дочернего окна
sub1 = QMdiSubWindow()
sub1.setWidget(QTextEdit('Текст окна 1'))
sub1.setWindowTitle('Окно 1')
mdi.addSubWindow(sub1)
# Создание второго дочернего окна
sub2 = QMdiSubWindow()
sub2.setWidget(QTextEdit('Текст окна 2'))
sub2.setWindowTitle('Окно 2')
mdi.addSubWindow(sub2)
sub1.show()
sub2.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Python window manager (оконный менеджер в python)
В примере создаётся главное окно с QMdiArea в центре. Каждое дочернее окно - экземпляр QMdiSubWindow, в которое помещается произвольный виджет (QTextEdit). Все окна можно перемещать, изменять размеры, сворачивать и разворачивать стандартными средствами операционной системы.
Типичные ошибки и их решение
- Проблема: Дочерние окна не отображаются. Решение: Убедитесь, что для каждого QMdiSubWindow вызван show() после добавления в QMdiArea.
- Проблема: Окна не реагируют на события клавиатуры. Решение: Проверьте, что фокус передаётся дочернему виджету с помощью setFocus() или через установку активного окна setActiveSubWindow().
- Проблема: При закрытии главного окна дочерние окна не сохраняют состояние. Решение: Реализуйте обработчик closeEvent и сохраняйте необходимые данные перед закрытием.
Цели использования:
- Организация рабочего пространства приложения с множеством независимых панелей.
- Реализация редакторов документов, где каждый файл открывается в отдельном окне.
- Создание инструментов для разработки с возможностью группировки окон по проектам.
Как реализовать плавающие окна с перетаскиванием в tkinter?
Для приложений на основе tkinter можно организовать управление несколькими окнами Toplevel, которые ведут себя как независимые окна рабочего стола. Однако встроенной поддержки MDI нет, поэтому позиционирование и перетаскивание реализуется вручную.
import tkinter as tk
class FloatingWindow:
def __init__(self, master, title='Окно', width=300, height=200):
self.master = master
self.window = tk.Toplevel(master)
self.window.title(title)
self.window.geometry(f'{width}x{height}+100+100')
# Захват мыши для перетаскивания
self.window.bind('', self.start_move)
self.window.bind('', self.do_move)
self.window.protocol('WM_DELETE_WINDOW', self.close)
self._drag_data = {'x': 0, 'y': 0}
def start_move(self, event):
self._drag_data['x'] = event.x
self._drag_data['y'] = event.y
def do_move(self, event):
x = self.window.winfo_x() + event.x - self._drag_data['x']
y = self.window.winfo_y() + event.y - self._drag_data['y']
self.window.geometry(f'+{x}+{y}')
def close(self):
self.window.destroy()
root = tk.Tk()
root.withdraw() # скрыть главное окно
# Создание двух плавающих окон
f1 = FloatingWindow(root, 'Окно 1')
f2 = FloatingWindow(root, 'Окно 2')
root.mainloop()
Python графический редактор (графический редактор на python)
Класс FloatingWindow создаёт окно Toplevel и привязывает обработчики событий мыши для перетаскивания. Главное окно Tk скрывается, чтобы не мешать. Окна можно перемещать и закрывать.
Возможные проблемы
- Проблема: Перетаскивание работает некорректно при смещении мыши за границы окна. Решение: Использовать winfo_rootx() и winfo_rooty() для получения абсолютных координат окна на экране.
- Проблема: Окна не реагируют на сворачивание/разворачивание. Решение: tkinter не поддерживает сворачивание окон в панель задач - для этого требуется внешняя библиотека или использование системных вызовов.
Случаи использования:
- Создание инструментов с плавающими панелями (инспекторы, палитры).
- Быстрое прототипирование оконного интерфейса без внешних зависимостей.
Как написать оконный менеджер для X11 на Python?
Для работы с оконной системой X11 на низком уровне используется библиотека python-xlib. Она позволяет управлять окнами других приложений, перехватывать события и управлять фокусом, что необходимо для создания собственного оконного менеджера.
import Xlib
from Xlib.display import Display
from Xlib import X
from Xlib.protocol import event
display = Display()
root = display.screen().root
# Изменяем корневое окно для захвата событий
root.change_attributes(event_mask=X.SubstructureRedirectMask | X.SubstructureNotifyMask | X.ButtonPressMask)
# Обработчик событий
while True:
ev = display.next_event()
if ev.type == X.ConfigureRequest:
# Запрос на изменение размера или позиции окна
ev.window.configure(
width=ev.width,
height=ev.height,
x=ev.x,
y=ev.y,
border_width=ev.border_width,
)
display.flush()
elif ev.type == X.MapRequest:
# Запрос на отображение окна
ev.window.map()
display.flush()
Этот минимальный пример перехватывает запросы на отображение (MapRequest) и изменение геометрии (ConfigureRequest) окон. Такой код является основой для мозаичного или плавающего оконного менеджера. Для полноценной работы потребуется обработка фокуса, украшений окон и управления рабочими столами.
Типичные ошибки
- Проблема: При запуске программа зависает или не реагирует. Решение: Убедиться, что на системе не запущен другой оконный менеджер - политика X11 позволяет только одному менеджеру захватывать SubstructureRedirectMask.
- Проблема: Окна не отображаются или отображаются некорректно. Решение: Проверить, что все окна приложения явно вызывают map() после обработки MapRequest.
Когда применимо:
- Создание кастомного рабочего окружения для Linux с особыми требованиями (например, киоск-режим, мозаичные раскладки).
- Изучение принципов работы оконных менеджеров на низком уровне.
Расширенные примеры оконного менеджера
Пример 1: Кастомный мозаичный менеджер на PyQt5
Реализация менеджера, который автоматически распределяет дочерние окна по сетке внутри QMdiArea.
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QMdiArea, QMdiSubWindow, QTextEdit
from PyQt5.QtCore import QTimer
class MosaicManager(QMainWindow):
def __init__(self):
super().__init__()
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
self.setWindowTitle('Мозаичный менеджер')
self.setGeometry(100, 100, 800, 600)
# Добавляем несколько окон
for i in range(6):
sub = QMdiSubWindow()
sub.setWidget(QTextEdit(f'Окно {i+1}'))
sub.setWindowTitle(f'Окно {i+1}')
self.mdi.addSubWindow(sub)
sub.show()
# Таймер для перераспределения
self.timer = QTimer()
self.timer.timeout.connect(self.tile_windows)
self.timer.start(2000) # каждые 2 секунды
def tile_windows(self):
windows = self.mdi.subWindowList()
if not windows:
return
n = len(windows)
cols = int(n ** 0.5) or 1
rows = (n + cols - 1) // cols
width = self.mdi.width() // cols
height = self.mdi.height() // rows
for i, win in enumerate(windows):
row, col = divmod(i, cols)
win.move(col * width, row * height)
win.resize(width, height)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MosaicManager()
window.show()
sys.exit(app.exec_())
Результат: каждые 2 секунды дочерние окна автоматически выстраиваются в сетку, заполняя все пространство QMdiArea.
Пример 2: Управление окнами с привязкой к границам (tkinter)
Реализация, при которой окна прилипают к краям экрана или друг к другу.
import tkinter as tk
class SnapWindow:
SNAP_DISTANCE = 20
def __init__(self, master, title='Snap', width=200, height=150):
self.master = master
self.win = tk.Toplevel(master)
self.win.title(title)
self.win.geometry(f'{width}x{height}+200+200')
self.win.bind('', self.start_move)
self.win.bind('', self.do_move)
self.win.protocol('WM_DELETE_WINDOW', self.close)
self._start_x = 0
self._start_y = 0
def start_move(self, event):
self._start_x = event.x
self._start_y = event.y
def do_move(self, event):
x = self.win.winfo_x() + event.x - self._start_x
y = self.win.winfo_y() + event.y - self._start_y
# Прилипание к границам
screen_w = self.win.winfo_screenwidth()
screen_h = self.win.winfo_screenheight()
win_w = self.win.winfo_width()
win_h = self.win.winfo_height()
if abs(x) < self.SNAP_DISTANCE:
x = 0
elif abs(x + win_w - screen_w) < self.SNAP_DISTANCE:
x = screen_w - win_w
if abs(y) < self.SNAP_DISTANCE:
y = 0
elif abs(y + win_h - screen_h) < self.SNAP_DISTANCE:
y = screen_h - win_h
self.win.geometry(f'+{x}+{y}')
def close(self):
self.win.destroy()
root = tk.Tk()
root.withdraw()
win1 = SnapWindow(root, 'Окно A')
win2 = SnapWindow(root, 'Окно B', width=250)
root.mainloop()
При перетаскивании окна автоматически прилипают к краям экрана. Расстояние прилипания задаётся константой SNAP_DISTANCE.
Пример 3: Минимальный мозаичный менеджер на python-xlib
Менеджер, размещающий все окна по сетке без перекрытия.
import Xlib
from Xlib.display import Display
from Xlib import X
display = Display()
screen = display.screen()
root = screen.root
root.change_attributes(event_mask=X.SubstructureRedirectMask | X.SubstructureNotifyMask)
window_list = []
cols, rows = 3, 2
tile_width = screen.width_in_pixels // cols
tile_height = screen.height_in_pixels // rows
def tile_window(win, idx):
col = idx % cols
row = idx // cols
win.configure(x=col * tile_width, y=row * tile_height,
width=tile_width, height=tile_height, border_width=2)
display.flush()
while True:
ev = display.next_event()
if ev.type == X.MapRequest:
ev.window.map()
window_list.append(ev.window)
tile_window(ev.window, len(window_list) - 1)
elif ev.type == X.DestroyNotify:
if ev.window in window_list:
window_list.remove(ev.window)
# Переразметка оставшихся окон
for i, w in enumerate(window_list):
tile_window(w, i)
Результат: каждое новое окно размещается в следующей ячейке сетки 3x2. При закрытии окна оставшиеся перераспределяются.