431 lines
16 KiB
Python
431 lines
16 KiB
Python
|
|
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
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
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() # Сохраняем начальное состояние
|
|||
|
|
|
|||
|
|
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() # Сохраняем состояние перед началом рисования
|
|||
|
|
|
|||
|
|
def mouseMoveEvent(self, event):
|
|||
|
|
"""Обработка движения мыши"""
|
|||
|
|
if self.drawing:
|
|||
|
|
painter = QPainter(self.image)
|
|||
|
|
|
|||
|
|
if event.buttons() & Qt.RightButton:
|
|||
|
|
# Правая кнопка - ластик (стираем белым цветом)
|
|||
|
|
pen = QPen(Qt.white, self.eraser_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
|
|||
|
|
else:
|
|||
|
|
# Левая кнопка - рисуем
|
|||
|
|
pen = QPen(self.pen_color, self.pen_width, self.pen_style, Qt.RoundCap, Qt.RoundJoin)
|
|||
|
|
|
|||
|
|
painter.setPen(pen)
|
|||
|
|
painter.drawLine(self.last_point, event.pos())
|
|||
|
|
self.last_point = event.pos()
|
|||
|
|
self.update()
|
|||
|
|
|
|||
|
|
def mouseReleaseEvent(self, event):
|
|||
|
|
"""Обработка отпускания кнопки мыши"""
|
|||
|
|
if event.button() == Qt.LeftButton or event.button() == Qt.RightButton:
|
|||
|
|
self.drawing = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
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_())
|