Files
sibuti/visuapPart1/secondLabVisualProg/main.py
2025-12-11 15:39:41 +03:00

447 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QComboBox, QSlider, QFileDialog,
QMessageBox, QColorDialog, QAction, QToolBar, QStatusBar
)
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import (
QImage, QPainter, QPen, QColor, QPixmap, QIcon, QPainterPath
)
class Canvas(QWidget):
"""Холст для рисования"""
def __init__(self, parent=None):
super().__init__(parent)
self.setMinimumSize(800, 600)
# Создаём изображение для рисования
self.image = QImage(800, 600, QImage.Format_RGB32)
self.image.fill(Qt.white)
# Параметры рисования
self.drawing = False
self.last_point = QPoint()
self.pen_color = QColor(Qt.black)
self.pen_width = 3
self.pen_style = Qt.SolidLine
self.eraser_width = 20
# История для undo/redo
self.undo_stack = []
self.redo_stack = []
self.save_state() # Сохраняем начальное состояние
# Путь для текущего штриха (для корректной работы пунктирных стилей)
self.current_path = None
def save_state(self):
"""Сохранить текущее состояние для undo"""
self.undo_stack.append(self.image.copy())
self.redo_stack.clear() # Очищаем redo при новом действии
# Ограничиваем размер истории
if len(self.undo_stack) > 50:
self.undo_stack.pop(0)
def undo(self):
"""Отменить последнее действие"""
if len(self.undo_stack) > 1:
# Сохраняем текущее состояние в redo
self.redo_stack.append(self.undo_stack.pop())
# Восстанавливаем предыдущее состояние
self.image = self.undo_stack[-1].copy()
self.update()
return True
return False
def redo(self):
"""Повторить отменённое действие"""
if self.redo_stack:
state = self.redo_stack.pop()
self.undo_stack.append(state)
self.image = state.copy()
self.update()
return True
return False
def clear_canvas(self):
"""Очистить холст"""
self.save_state()
self.image.fill(Qt.white)
self.update()
def new_image(self, width=800, height=600):
"""Создать новое изображение"""
self.image = QImage(width, height, QImage.Format_RGB32)
self.image.fill(Qt.white)
self.undo_stack.clear()
self.redo_stack.clear()
self.save_state()
self.setMinimumSize(width, height)
self.update()
def load_image(self, file_path):
"""Загрузить изображение из файла"""
loaded_image = QImage(file_path)
if loaded_image.isNull():
return False
self.image = loaded_image.convertToFormat(QImage.Format_RGB32)
self.setMinimumSize(self.image.width(), self.image.height())
self.undo_stack.clear()
self.redo_stack.clear()
self.save_state()
self.update()
return True
def save_image(self, file_path):
"""Сохранить изображение в файл"""
return self.image.save(file_path)
def set_pen_color(self, color):
"""Установить цвет кисти"""
self.pen_color = color
def set_pen_width(self, width):
"""Установить толщину кисти"""
self.pen_width = width
def set_pen_style(self, style):
"""Установить стиль линии"""
self.pen_style = style
def set_eraser_width(self, width):
"""Установить размер ластика"""
self.eraser_width = width
def paintEvent(self, event):
"""Отрисовка холста"""
painter = QPainter(self)
painter.drawImage(0, 0, self.image)
def mousePressEvent(self, event):
"""Обработка нажатия кнопки мыши"""
if event.button() == Qt.LeftButton or event.button() == Qt.RightButton:
self.drawing = True
self.last_point = event.pos()
self.save_state() # Сохраняем состояние перед началом рисования
# Создаём новый путь для штриха
self.current_path = QPainterPath()
self.current_path.moveTo(event.pos())
def mouseMoveEvent(self, event):
"""Обработка движения мыши"""
if self.drawing:
if event.buttons() & Qt.RightButton:
# Правая кнопка - ластик (стираем белым цветом)
# Ластик рисуем линиями напрямую
painter = QPainter(self.image)
pen = QPen(Qt.white, self.eraser_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
painter.setPen(pen)
painter.drawLine(self.last_point, event.pos())
self.last_point = event.pos()
else:
# Левая кнопка - рисуем с использованием пути
self.current_path.lineTo(event.pos())
# Восстанавливаем сохранённое состояние и перерисовываем весь путь
if len(self.undo_stack) > 0:
self.image = self.undo_stack[-1].copy()
painter = QPainter(self.image)
pen = QPen(self.pen_color, self.pen_width, self.pen_style, Qt.RoundCap, Qt.RoundJoin)
painter.setPen(pen)
painter.drawPath(self.current_path)
self.update()
def mouseReleaseEvent(self, event):
"""Обработка отпускания кнопки мыши"""
if event.button() == Qt.LeftButton or event.button() == Qt.RightButton:
self.drawing = False
self.current_path = None
class MainWindow(QMainWindow):
"""Главное окно графического редактора"""
def __init__(self):
super().__init__()
self.setWindowTitle("Лабораторная работа №2 - Графический редактор")
self.setMinimumSize(1000, 750)
# Создаём холст
self.canvas = Canvas()
# Центральный виджет с холстом
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.addWidget(self.canvas)
# Создаём меню
self.create_menu()
# Создаём панель инструментов
self.create_toolbar()
# Создаём статусбар
self.statusBar = QStatusBar()
self.setStatusBar(self.statusBar)
self.statusBar.showMessage("Готово")
def create_menu(self):
"""Создание меню"""
menubar = self.menuBar()
# Меню "Файл"
file_menu = menubar.addMenu("Файл")
new_action = QAction("Создать", self)
new_action.setShortcut("Ctrl+N")
new_action.triggered.connect(self.new_file)
file_menu.addAction(new_action)
open_action = QAction("Открыть", self)
open_action.setShortcut("Ctrl+O")
open_action.triggered.connect(self.open_file)
file_menu.addAction(open_action)
save_action = QAction("Сохранить", self)
save_action.setShortcut("Ctrl+S")
save_action.triggered.connect(self.save_file)
file_menu.addAction(save_action)
save_as_action = QAction("Сохранить как...", self)
save_as_action.setShortcut("Ctrl+Shift+S")
save_as_action.triggered.connect(self.save_file_as)
file_menu.addAction(save_as_action)
file_menu.addSeparator()
exit_action = QAction("Выход", self)
exit_action.setShortcut("Ctrl+Q")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# Меню "Редактирование"
edit_menu = menubar.addMenu("Редактирование")
undo_action = QAction("Отменить", self)
undo_action.setShortcut("Ctrl+Z")
undo_action.triggered.connect(self.undo)
edit_menu.addAction(undo_action)
redo_action = QAction("Повторить", self)
redo_action.setShortcut("Ctrl+Y")
redo_action.triggered.connect(self.redo)
edit_menu.addAction(redo_action)
edit_menu.addSeparator()
clear_action = QAction("Очистить", self)
clear_action.setShortcut("Ctrl+Delete")
clear_action.triggered.connect(self.clear_canvas)
edit_menu.addAction(clear_action)
# Меню "Инструменты"
tools_menu = menubar.addMenu("Инструменты")
color_action = QAction("Выбрать цвет...", self)
color_action.triggered.connect(self.choose_color)
tools_menu.addAction(color_action)
def create_toolbar(self):
"""Создание панели инструментов"""
toolbar = QToolBar("Инструменты")
toolbar.setMovable(False)
self.addToolBar(toolbar)
# Кнопки файловых операций
btn_new = QPushButton("Создать")
btn_new.clicked.connect(self.new_file)
toolbar.addWidget(btn_new)
btn_open = QPushButton("Открыть")
btn_open.clicked.connect(self.open_file)
toolbar.addWidget(btn_open)
btn_save = QPushButton("Сохранить")
btn_save.clicked.connect(self.save_file)
toolbar.addWidget(btn_save)
toolbar.addSeparator()
# Кнопки undo/redo
btn_undo = QPushButton("↶ Отменить")
btn_undo.clicked.connect(self.undo)
toolbar.addWidget(btn_undo)
btn_redo = QPushButton("↷ Повторить")
btn_redo.clicked.connect(self.redo)
toolbar.addWidget(btn_redo)
toolbar.addSeparator()
# Выбор цвета
toolbar.addWidget(QLabel("Цвет: "))
self.color_btn = QPushButton()
self.color_btn.setFixedSize(30, 30)
self.color_btn.setStyleSheet("background-color: black;")
self.color_btn.clicked.connect(self.choose_color)
toolbar.addWidget(self.color_btn)
toolbar.addSeparator()
# Толщина линии (TrackBar - QSlider)
toolbar.addWidget(QLabel("Толщина: "))
self.width_slider = QSlider(Qt.Horizontal)
self.width_slider.setMinimum(1)
self.width_slider.setMaximum(50)
self.width_slider.setValue(3)
self.width_slider.setFixedWidth(100)
self.width_slider.valueChanged.connect(self.change_pen_width)
toolbar.addWidget(self.width_slider)
self.width_label = QLabel("3")
toolbar.addWidget(self.width_label)
toolbar.addSeparator()
# Стиль линии (ComboBox)
toolbar.addWidget(QLabel("Стиль: "))
self.style_combo = QComboBox()
self.style_combo.addItem("Сплошная", Qt.SolidLine)
self.style_combo.addItem("Штриховая", Qt.DashLine)
self.style_combo.addItem("Пунктирная", Qt.DotLine)
self.style_combo.addItem("Штрих-пунктир", Qt.DashDotLine)
self.style_combo.addItem("Штрих-две точки", Qt.DashDotDotLine)
self.style_combo.currentIndexChanged.connect(self.change_pen_style)
toolbar.addWidget(self.style_combo)
toolbar.addSeparator()
# Размер ластика
toolbar.addWidget(QLabel("Ластик: "))
self.eraser_slider = QSlider(Qt.Horizontal)
self.eraser_slider.setMinimum(5)
self.eraser_slider.setMaximum(100)
self.eraser_slider.setValue(20)
self.eraser_slider.setFixedWidth(100)
self.eraser_slider.valueChanged.connect(self.change_eraser_width)
toolbar.addWidget(self.eraser_slider)
self.eraser_label = QLabel("20")
toolbar.addWidget(self.eraser_label)
toolbar.addSeparator()
# Кнопка очистки
btn_clear = QPushButton("Очистить")
btn_clear.clicked.connect(self.clear_canvas)
toolbar.addWidget(btn_clear)
def new_file(self):
"""Создать новое изображение"""
reply = QMessageBox.question(
self, "Новый файл",
"Создать новое изображение? Несохранённые изменения будут потеряны.",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.canvas.new_image()
self.current_file = None
self.statusBar.showMessage("Создано новое изображение")
def open_file(self):
"""Открыть изображение"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Открыть изображение", "",
"Изображения (*.png *.jpg *.jpeg *.bmp *.gif);;Все файлы (*)"
)
if file_path:
if self.canvas.load_image(file_path):
self.current_file = file_path
self.statusBar.showMessage(f"Открыто: {file_path}")
else:
QMessageBox.critical(self, "Ошибка", "Не удалось открыть изображение")
def save_file(self):
"""Сохранить изображение"""
if hasattr(self, 'current_file') and self.current_file:
if self.canvas.save_image(self.current_file):
self.statusBar.showMessage(f"Сохранено: {self.current_file}")
else:
QMessageBox.critical(self, "Ошибка", "Не удалось сохранить изображение")
else:
self.save_file_as()
def save_file_as(self):
"""Сохранить изображение как..."""
file_path, _ = QFileDialog.getSaveFileName(
self, "Сохранить изображение", "",
"PNG (*.png);;JPEG (*.jpg *.jpeg);;BMP (*.bmp);;Все файлы (*)"
)
if file_path:
if self.canvas.save_image(file_path):
self.current_file = file_path
self.statusBar.showMessage(f"Сохранено: {file_path}")
else:
QMessageBox.critical(self, "Ошибка", "Не удалось сохранить изображение")
def undo(self):
"""Отменить действие"""
if self.canvas.undo():
self.statusBar.showMessage("Отменено")
else:
self.statusBar.showMessage("Нечего отменять")
def redo(self):
"""Повторить действие"""
if self.canvas.redo():
self.statusBar.showMessage("Повторено")
else:
self.statusBar.showMessage("Нечего повторять")
def clear_canvas(self):
"""Очистить холст"""
reply = QMessageBox.question(
self, "Очистить",
"Очистить холст?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
self.canvas.clear_canvas()
self.statusBar.showMessage("Холст очищен")
def choose_color(self):
"""Выбрать цвет кисти"""
color = QColorDialog.getColor(self.canvas.pen_color, self, "Выберите цвет")
if color.isValid():
self.canvas.set_pen_color(color)
self.color_btn.setStyleSheet(f"background-color: {color.name()};")
self.statusBar.showMessage(f"Выбран цвет: {color.name()}")
def change_pen_width(self, value):
"""Изменить толщину кисти"""
self.canvas.set_pen_width(value)
self.width_label.setText(str(value))
def change_pen_style(self, index):
"""Изменить стиль линии"""
style = self.style_combo.itemData(index)
self.canvas.set_pen_style(style)
def change_eraser_width(self, value):
"""Изменить размер ластика"""
self.canvas.set_eraser_width(value)
self.eraser_label.setText(str(value))
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())