# -*- coding: utf-8 -*- from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import pyqtSlot import sys import math import datetime INIT_X = 640 INIT_Y = 480 class MyWidget(QtWidgets.QLabel): """Класс для визуализации изображения""" def __init__(self, parent=None): """Конструктор, инициализация всех полей класса""" QtWidgets.QLabel.__init__(self, parent) self.width = INIT_X # Размер виджета по горизонтали self.height = INIT_Y # Размер виджета по вертикали self.resize(self.width, self.height) self.xa = -self.width / 10 # Действительные координаты левой границы виджета self.xb = self.width / 10 # Действительные координаты правой границы виджета self.ya = -self.height / 10 # Действительные координаты нижней границы виджета self.yb = self.height / 10 # Действительные координаты верхней границы виджета self.param = 128 # Значение параметра, используемого при вычислении цвета пикселя self.undo = [] # Список отмены операций для возврата к предыдущему окну self.image = None # Растровое изображение, отображаемое в виджете self.rubberBand = QtWidgets.QRubberBand(QtWidgets.QRubberBand.Rectangle, self) # Виджет - выделение прямоугольной области self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.contextMenuRequested) def color(self, x, y): """Метод возвращает цвет точки по координатам Параметры: (x, y) -- координаты точки на плоскости (действительные числа) Возвращаемое значение: цвет точки (типа int) в модель RGB, от 0x000000 до 0xFFFFFF При вычислении функция может использовать значение параметра self.param """ red = int(128 + self.param * math.sin(x + y)) green = int(128 + (x + y)) blue = int(128 + self.param * math.sin(x - y)) # Вычисляем значения каналов RGB - три целых числа от 0 до 255 red = min(max(red, 0), 255) green = min(max(green, 0), 255) blue = min(max(blue, 0), 255) # Возвращаемое значение - 0xABCDEF, где 0xAB - красный канал, 0xCD - зеленый канал, 0xEF - синий канал return (red * 256 + green) * 256 + blue def update(self): """Метод пересоздает растр и обновляет изображение в виджете""" # Создание списка координат, соответствующих пикселям по оси OX и OY xm = [self.xa + (self.xb - self.xa) * kx / self.width for kx in range(self.width)] ym = [self.yb + (self.ya - self.yb) * ky / self.height for ky in range(self.height)] # Создаем изображение и заполняем его попиксельно self.image = QtGui.QImage(self.width, self.height, QtGui.QImage.Format_RGB32) for i in range(self.width): for j in range(self.height): self.image.setPixel(i, j, self.color(xm[i], ym[j])) # Накладываем надписи координат на изображение painter = QtGui.QPainter() painter.begin(self.image) painter.setPen(QtGui.QColor("white")) painter.drawText(QtCore.QRectF(0, 0, self.width, self.height), QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter, "{:.4g}".format(self.yb)) painter.drawText(QtCore.QRectF(0, 0, self.width, self.height), QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter, "{:.4g}".format(self.ya)) painter.drawText(QtCore.QRectF(0, 0, self.width, self.height), QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, "{:.4g}".format(self.xa)) painter.drawText(QtCore.QRectF(0, 0, self.width, self.height), QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight, "{:.4g}".format(self.xb)) painter.end() # Обновление self.setPixmap(QtGui.QPixmap.fromImage(self.image)) def mousePressEvent(self, event): if event.button() == QtCore.Qt.RightButton: return """Обработка нажатия на мышь""" self.origin = event.pos() # Запоминаем координаты нажатия в поле origin self.rubberBand.setGeometry(QtCore.QRect(self.origin, QtCore.QSize())) self.rubberBand.show() # И отображаем виджет для выделения области def mouseMoveEvent(self, event): """Обработка перемещения мыши после нажатия""" self.rubberBand.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized()) def mouseReleaseEvent(self, event): if event.button() == QtCore.Qt.RightButton: return """Обработка отпускания клавиши мыши""" self.rubberBand.hide() # Скрываем виджет отображения области self.resize_region(event.pos()) # И вызываем метод масштабирования def resize_region(self, pos): """Пересчет координат окна. Старые координаты берутся в self.origin, новые передаются в pos""" x1 = self.origin.x() x2 = pos.x() y1 = self.origin.y() y2 = pos.y() x1, x2 = min(x1, x2), max(x1, x2) y1, y2 = min(y1, y2), max(y1, y2) self.undo.append((self.xa, self.xb, self.ya, self.yb)) # Добавили старые координаты в список undo self.xa, self.xb = self.xa + x1 * (self.xb - self.xa) / self.width, self.xa + x2 * (self.xb - self.xa) / self.width self.yb, self.ya = self.yb + y1 * (self.ya - self.yb) / self.height, self.yb + y2 * (self.ya - self.yb) / self.height self.update() @pyqtSlot() def action_back(self): if self.undo: self.xa, self.xb, self.ya, self.yb = self.undo.pop() self.update() @pyqtSlot() def action_save(self): filename = "fractal_{:%Y-%m-%d_%H-%M-%S}.jpg".format(datetime.datetime.now()) filename, ok_pressed = QtWidgets.QFileDialog.getSaveFileName(self, "Select file to save image", filename, "JPEG files (*.jpg)") if ok_pressed: self.image.save(filename, "JPEG", 100) print("Image saved as", filename) @pyqtSlot() def action_set_param(self): param, ok_pressed = QtWidgets.QInputDialog.getInt(self, "Enter parameter", "", self.param, 32, 256, 1) if ok_pressed: self.param = param self.update() @pyqtSlot(QtCore.QPoint) def contextMenuRequested(self, point): menu = QtWidgets.QMenu() if self.undo: action_back = menu.addAction("Back") action_back.triggered.connect(self.action_back) action_save = menu.addAction("Save image") action_save.triggered.connect(self.action_save) action_set_param = menu.addAction("Set parameter value") action_set_param.triggered.connect(self.action_set_param) menu.exec_(self.mapToGlobal(point)) app = QtWidgets.QApplication(sys.argv) widget = MyWidget() widget.update() widget.show() sys.exit(app.exec_())