Система столкновения группы фигур со стеной при увеличении. PyQT5
Возникла проблема, которую не могу решить. Задача такая: графический редактор, в котором создаются 3 фигуры на выбор. Меняется цвет фигур, фигуры могут двигаться до конца окна и изменять свой размер.
Далее нужно было реализовать группу фигур (должна работать как единый объект: двигаться, изменять размер и т.д) - группа создаётся путём выделения объектов через ЛКМ + Ctrl и нажатием кнопки "Группировать". Так же можно просто выделять по одному объекту нажатием ЛКМ. Вне группы тоже можно выделить несколько объектов, но они будут независимы друг от друга.
Проблема в том, что когда две группы фигур объединяешь в третью группу, то они работают независимо(но не для движения фигур. Если элементы одной группы уже достигли минимума, а другой нет - то вторая группа будет уменьшаться до тех пор, пока может)
Возможно я неправильно подошёл к реализации группы как единого обьекта, так как совсем недавно начал программировать на QT.
Спасибо заранее за помощь. Снизу весь код.
import sys, random, math
from PyQt5 import QtGui, QtCore, uic
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QColorDialog, QFileDialog, QMessageBox
from PyQt5.QtGui import QPainter, QPainterPath, QBrush, QPen, QColor, QPolygon
from PyQt5.QtCore import Qt, QPoint, pyqtSignal, QRect, QMargins
from design import Ui_MainWindow
import logging
import xml.etree.ElementTree as ET
COLOR_SELECTED = Qt.red
COLOR_BORDER = Qt.gray
INITIAL_SIZE = 50
INITIAL_RADIUS = INITIAL_SIZE // 2
STEP_CHANGE_SIZE = 10 // 2
class Shape():
_linked_widget = None
_is_current = False
def __init__(self, point, color, length=INITIAL_SIZE, activate=False):
self._rect = QRect(0, 0, length, length)
self._rect.moveCenter(point)
self._activate = activate
self._color = color
self.canmove = True
@property
def rect(self):
return self._rect
@classmethod
def get_linked_widget(cls):
return cls._linked_widget
@classmethod
def set_linked_widget(cls, widget):
cls._linked_widget = widget
widget.clicked.connect(lambda: widget.window().selectShape(cls))
@classmethod
def set_is_current(cls, status):
if cls.get_linked_widget():
color = 'yellow' if status else 'none'
cls.get_linked_widget().setStyleSheet("background-color: " + color)
@property
def color(self):
return COLOR_SELECTED if self._activate else self._color
@color.setter
def color(self, color):
self._color = color
def draw(self, painter):
pass
def paint(self, painter):
if not painter.isActive():
return
painter.save()
painter.setPen(QPen(COLOR_BORDER, 0, Qt.SolidLine))
painter.setBrush(QBrush(self.color, Qt.SolidPattern))
self.draw(painter)
painter.restore()
def changeFlag(self):
self._activate = not self._activate
logger.info("Activated" if self._activate else "Deactivated")
def getStatus(self):
return self._activate
def deactivate(self):
self._activate = False
def isSelected(self, point):
pass
def is_inner_canvas(self, canvas: QRect):
return self._rect.united(canvas) == canvas
def addMargins(self, size_margins):
return QMargins(size_margins,
size_margins,
size_margins,
size_margins)
def is_valid_size(self, shape_copy: QRect):
if shape_copy.width() < 10 and shape_copy.height() < 10:
return False
return True
def move_inplace(self, canvas: QRect, dx, dy):
old_rect = self._rect
self._rect = self._rect.translated(dx, dy)
if not self.is_inner_canvas(canvas):
self._rect = old_rect
return False
return True
def changesize(self, canvas: QRect, dsize):
old_rect = self._rect
self._rect = self._rect + self.addMargins(dsize)
if (not self.is_inner_canvas(canvas)) or (not self.is_valid_size(self._rect + self.addMargins(dsize))):
self._rect = old_rect
return False
print(self._rect)
return True
def save(self) -> ET:
element = ET.Element(self.__class__.__name__)
element.set('color', self.color.name())
rect = ET.SubElement(element, 'rect')
rect.set('left', str(self.rect.x()))
rect.set('top', str(self.rect.y()))
rect.set('width', str(self.rect.width()))
rect.set('height', str(self.rect.height()))
return element
### CLASS CIRCLE ###=====
class CCircle(Shape):
def draw(self, painter):
painter.drawEllipse(self._rect)
def isSelected(self, point):
d = self._rect.center() - point
return (d.x() ** 2 + d.y() ** 2) <= ((self._rect.width() // 2) ** 2)
### CLASS RECTANGLE ###
class Rectangle(Shape):
def draw(self, painter):
painter.drawRect(self._rect)
def isSelected(self, point):
return self._rect.contains(point)
### CLASS TRIANGLE ###
class Triangle(Shape):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._rect.setHeight(int(round(self._rect.width() * math.sqrt(3) / 2)))
self._poligon = QPolygon([
QPoint(self._rect.center().x(), self._rect.top()),
self._rect.bottomRight(),
self._rect.bottomLeft()
])
def draw(self, painter):
painter.drawPolygon(self._poligon)
def isSelected(self, point):
return self._poligon.containsPoint(point, Qt.WindingFill)
def move_inplace(self, canvas, dx, dy):
if super().move_inplace(canvas, dx, dy):
self._poligon.translate(dx, dy)
def changesize(self, canvas: QRect, dsize):
if super().changesize(canvas, dsize):
self._poligon = QPolygon([
QPoint(self._rect.center().x(), self._rect.top()),
self._rect.bottomRight(),
self._rect.bottomLeft()
])
return True
return False
def save(self) -> ET:
element = super().save()
polygon = ET.SubElement(element, 'polygon')
points = ET.SubElement(polygon, 'points')
points.set('count_points', '3')
for point in self._poligon:
el_point = ET.SubElement(points, 'point')
el_point.set('x', str(point.x()))
el_point.set('y', str(point.y()))
return element
### CLASS GROUP ###
class Group(Shape):
def __init__(self):
self._childrens = []
self._rect = None
self._activate = False
self._color = Qt.black
self.canmove = True
def updateRect(self) -> QRect:
self._rect = QRect(self._childrens[0].rect.x(),
self._childrens[0].rect.y(),
self._childrens[0].rect.width(),
self._childrens[0].rect.height())
if self._childrens:
for child in self._childrens:
self._rect = child.rect.united(self._rect)
def draw(self, painter):
painter.drawRect(self._rect)
for elem in self._childrens:
elem.draw(painter)
def deactivate(self):
for elem in self._childrens:
elem._activate = False
self._activate = False
def changeFlag(self):
for elem in self._childrens:
elem._activate = not elem._activate
self._activate = not self._activate
def addChild(self, child):
self._childrens.append(child)
self.updateRect()
def isSelected(self, point):
for shape in self._childrens:
if shape.isSelected(point):
return True
return False
def move_inplace(self, canvas: QRect, dx, dy):
if super(Group, self).move_inplace(canvas, dx, dy):
for elem in self._childrens:
elem.move_inplace(canvas, dx, dy)
def changesize(self, canvas: QRect, dsize) -> Shape:
for elem in self._childrens:
old_rect = elem._rect
elem._rect = elem._rect + self.addMargins()
if not elem.changesize(canvas, dsize):
self.canmove = False
else: self.canmove = True
self.updateRect()
return True
def save(self) -> ET:
element = super().save()
group = ET.SubElement(element, 'group')
group_elements = ET.SubElement(group, 'group_elements')
group_elements.set('group_elements', f'{len(self._childrens)}')
for element in group_elements:
element = ET.SubElement(element, 'element')
element.set('123', '123')
return element
### MY STORAGE ###
class Storage:
arr = []
def __len__(self):
return len(self.arr)
def __getitem__(self, item) -> Shape:
return self.arr[item]
def __setitem__(self, item, value):
self.arr[item] = value
return 0
def addItem(self, item):
self.arr.append(item)
def delItem(self, index):
self.arr.pop(index)
def getItem(self, index):
return self.arr[index]
def insertItem(self, item, index):
self.arr.insert(index, item)
def deact_all(self):
for i in self.arr:
i.deactivate()
def deleteAllActive(self):
for i in range(len(self.arr) - 1, -1, -1):
if self.arr[i]._activate:
self.arr.remove(self.arr[i])
def getActiveItems(self):
for i in self.arr:
if i.getStatus():
yield i
def save(self, filename):
root = ET.Element('storage')
items = ET.SubElement(root, 'items')
count_items = 0
for elem in self:
items.append(elem.save())
count_items += 1
items.set('count_elements', str(count_items))
print(ET.indent(root, space=' '))
result = ET.tostring(root, encoding='utf-8')
with open(filename, 'wb') as f:
f.write(result)
class Window(QMainWindow):
MOVE_KEYS = [87, 65, 83, 68]
CHANGE_SIZE_KEYS = [61, 45]
STEP_MOVE = 5
HEIGHT_HEADER = 80
MINIMUM_WIDTH = 750
MINIMUM_HEIGHT = HEIGHT_HEADER + 100
INITIAL_COLOR = QColor(Qt.black)
def __init__(self):
super().__init__()
self._currentColor = None
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.window_width = self.size().width()
self.window_height = self.size().height()
self.storage = Storage()
CCircle.set_linked_widget(self.ui.circlebutton)
Rectangle.set_linked_widget(self.ui.rectanlebutton)
Triangle.set_linked_widget(self.ui.trianglebutton)
self.ui.colorButton.clicked.connect(self.changeColor)
self.ui.saveButton.clicked.connect(self.saveToFile)
self.active_figure_class = CCircle
self.active_figure_class.set_is_current(True)
self.currentColor = self.INITIAL_COLOR
self.ui.groupButton.clicked.connect(self.groupElements)
@property
def currentColor(self):
return self._currentColor
@currentColor.setter
def currentColor(self, color: QColor):
if color != self._currentColor:
for elem in self.storage.getActiveItems():
elem.color = color
self.storage.deact_all()
self._currentColor = color
self.ui.colorButton.setStyleSheet(f'background: {color.name()}')
def resizeEvent(self, a0):
minimal_height = self.MINIMUM_HEIGHT
minimal_width = self.MINIMUM_WIDTH
for elem in self.storage:
minimal_height = max(minimal_height, elem.rect.bottomRight().y())
minimal_width = max(minimal_width, elem.rect.bottomRight().x())
self.setMinimumSize(minimal_width, minimal_height)
@property
def canvasrect(self):
return QRect(0, self.HEIGHT_HEADER, self.width(), self.height() - self.HEIGHT_HEADER)
def check(self, point):
for elem in reversed(self.storage):
if elem.isSelected(point):
elem.changeFlag()
break
else:
shape = self.active_figure_class(point, self.currentColor)
if shape.is_inner_canvas(self.canvasrect):
self.storage.addItem(shape)
def changeColor(self):
color = QColorDialog.getColor(self.currentColor, self, 'Выберите цвет')
if color.isValid():
self.currentColor = color
def selectShape(self, shape):
if shape is not self.active_figure_class:
self.active_figure_class.set_is_current(False)
self.active_figure_class = shape
self.active_figure_class.set_is_current(True)
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
for shapes in self.storage:
shapes.paint(painter)
def mousePressEvent(self, event):
modifier = QApplication.keyboardModifiers()
if not modifier == Qt.ControlModifier:
self.storage.deact_all()
point = event.pos()
self.check(point)
self.ui.groupButton.setEnabled(sum(1 for _ in self.storage.getActiveItems()) > 1)
self.update()
def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete:
self.storage.deleteAllActive()
elif event.key() in self.MOVE_KEYS:
dx, dy = [
(0, -self.STEP_MOVE), # Qt.Key_W
(-self.STEP_MOVE, 0), # Qt.Key_A
(0, self.STEP_MOVE), # Qt.Key_S
(self.STEP_MOVE, 0) # Qt.Key_D
][self.MOVE_KEYS.index(event.key())]
for shape in self.storage.getActiveItems():
shape.move_inplace(self.canvasrect, dx, dy)
elif event.key() in self.CHANGE_SIZE_KEYS:
dsize = [STEP_CHANGE_SIZE, -STEP_CHANGE_SIZE][self.CHANGE_SIZE_KEYS.index(event.key())]
for shape in self.storage.getActiveItems():
shape.changesize(self.canvasrect, dsize)
self.update()
def groupElements(self):
group = Group()
for elem in self.storage:
if elem.getStatus():
group.addChild(elem)
self.storage.deleteAllActive()
self.storage.deact_all()
self.storage.addItem(group)
print(group._childrens)
print(self.storage.arr)
self.update()
def saveToFile(self):
filename, _ = QFileDialog.getSaveFileName(self, 'Сохранение фигур', filter='*.xml')
if filename:
msg = QMessageBox(self)
msg.setWindowTitle("Сохранение файла")
try:
self.storage.save(filename)
except BaseException as e:
msg.setText("Ошибка сохранения")
msg.setIcon(QMessageBox.Critical)
else:
msg.setText(f"Файл '{filename}' успешно сохранен")
finally:
msg.exec_()
def my_excepthook(type, value, tback):
QtWidgets.QMessageBox.critical(
window, "CRITICAL ERROR", str(value),
QtWidgets.QMessageBox.Cancel
)
sys.__excepthook__(type, value, tback)
sys.excepthook = my_excepthook
if __name__ == "__main__":
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
App = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(App.exec())