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_())