diff --git a/.gitignore b/.gitignore index ba0430d..3040d64 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__/ \ No newline at end of file +__pycache__/ +.vscode/ \ No newline at end of file diff --git a/PlotterDialog/ButtonGroup.py b/PlotterDialog/PlotterDialog/ButtonGroup.py similarity index 89% rename from PlotterDialog/ButtonGroup.py rename to PlotterDialog/PlotterDialog/ButtonGroup.py index 8c003de..9ea74d4 100644 --- a/PlotterDialog/ButtonGroup.py +++ b/PlotterDialog/PlotterDialog/ButtonGroup.py @@ -1,6 +1,8 @@ from collections.abc import Callable -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton + +from .FlowLayout import FlowLayout class ButtonGroup(QWidget): @@ -13,7 +15,7 @@ class ButtonGroup(QWidget): ): super().__init__() self.layout = QVBoxLayout() # Создание основного лаяутв - Doplayout = QHBoxLayout() + Doplayout = FlowLayout() label = QLabel(category) self.layout.addWidget(label) @@ -34,7 +36,6 @@ class ButtonGroup(QWidget): Doplayout.addWidget(button) # отрисовывание кнопок Doplayout.setContentsMargins(0, 0, 0, 0) - Doplayout.addStretch(1) self.layout.addLayout(Doplayout) diff --git a/PlotterDialog/PlotterDialog/FlowLayout.py b/PlotterDialog/PlotterDialog/FlowLayout.py new file mode 100644 index 0000000..e756d9d --- /dev/null +++ b/PlotterDialog/PlotterDialog/FlowLayout.py @@ -0,0 +1,147 @@ +############################################################################# +## +## Copyright (C) 2013 Riverbank Computing Limited. +## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +## All rights reserved. +## +## This file is part of the examples of PyQt. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor +## the names of its contributors may be used to endorse or promote +## products derived from this software without specific prior written +## permission. +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## $QT_END_LICENSE$ +## +############################################################################# + + +from PyQt5 import QtCore, QtWidgets + + +class FlowLayout(QtWidgets.QLayout): + def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1): + super(FlowLayout, self).__init__(parent) + self._hspacing = hspacing + self._vspacing = vspacing + self._items = [] + self.setContentsMargins(margin, margin, margin, margin) + + def __del__(self): + del self._items[:] + + def addItem(self, item): + self._items.append(item) + + def horizontalSpacing(self): + if self._hspacing >= 0: + return self._hspacing + else: + return self.smartSpacing(QtWidgets.QStyle.PM_LayoutHorizontalSpacing) + + def verticalSpacing(self): + if self._vspacing >= 0: + return self._vspacing + else: + return self.smartSpacing(QtWidgets.QStyle.PM_LayoutVerticalSpacing) + + def count(self): + return len(self._items) + + def itemAt(self, index): + if 0 <= index < len(self._items): + return self._items[index] + + def takeAt(self, index): + if 0 <= index < len(self._items): + return self._items.pop(index) + + def expandingDirections(self): + return QtCore.Qt.Orientations(0) + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + return self.doLayout(QtCore.QRect(0, 0, width, 0), True) + + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self.doLayout(rect, False) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QtCore.QSize() + for item in self._items: + size = size.expandedTo(item.minimumSize()) + left, top, right, bottom = self.getContentsMargins() + size += QtCore.QSize(left + right, top + bottom) + return size + + def doLayout(self, rect, testonly): + left, top, right, bottom = self.getContentsMargins() + effective = rect.adjusted(+left, +top, -right, -bottom) + x = effective.x() + y = effective.y() + lineheight = 0 + for item in self._items: + widget = item.widget() + hspace = self.horizontalSpacing() + if hspace == -1: + hspace = widget.style().layoutSpacing( + QtWidgets.QSizePolicy.PushButton, + QtWidgets.QSizePolicy.PushButton, + QtCore.Qt.Horizontal, + ) + vspace = self.verticalSpacing() + if vspace == -1: + vspace = widget.style().layoutSpacing( + QtWidgets.QSizePolicy.PushButton, + QtWidgets.QSizePolicy.PushButton, + QtCore.Qt.Vertical, + ) + nextX = x + item.sizeHint().width() + hspace + if nextX - hspace > effective.right() and lineheight > 0: + x = effective.x() + y = y + lineheight + vspace + nextX = x + item.sizeHint().width() + hspace + lineheight = 0 + if not testonly: + item.setGeometry(QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint())) + x = nextX + lineheight = max(lineheight, item.sizeHint().height()) + return y + lineheight - rect.y() + bottom + + def smartSpacing(self, pm): + parent = self.parent() + if parent is None: + return -1 + elif parent.isWidgetType(): + return parent.style().pixelMetric(pm, None, parent) + else: + return parent.spacing() diff --git a/PlotterDialog/GraphRequester.py b/PlotterDialog/PlotterDialog/GraphRequester.py similarity index 92% rename from PlotterDialog/GraphRequester.py rename to PlotterDialog/PlotterDialog/GraphRequester.py index 31165d0..fb3f0fa 100644 --- a/PlotterDialog/GraphRequester.py +++ b/PlotterDialog/PlotterDialog/GraphRequester.py @@ -19,6 +19,10 @@ class FocusNotifyingLineEdit(QLineEdit): class GraphRequester(QWidget): + LineEditGraf: FocusNotifyingLineEdit + LineEditX: FocusNotifyingLineEdit + LineEditY: FocusNotifyingLineEdit + def __init__(self, nomer_grafika=1): super().__init__() layout = QVBoxLayout(self) @@ -45,8 +49,8 @@ class GraphRequester(QWidget): layout_close_and_name.addWidget(Name_Close) layout.addLayout(layout_close_and_name) # Вложения названия и закрыть - layout.addLayout(layout_x) # Вложение layout.addLayout(layout_y) # Вложение + layout.addLayout(layout_x) # Вложение layout.setContentsMargins(0, 0, 0, 0) layout.addStretch(1) diff --git a/PlotterDialog/PlotterDialog.py b/PlotterDialog/PlotterDialog/PlotterDialog.py similarity index 55% rename from PlotterDialog/PlotterDialog.py rename to PlotterDialog/PlotterDialog/PlotterDialog.py index f5cc9d9..15420ab 100644 --- a/PlotterDialog/PlotterDialog.py +++ b/PlotterDialog/PlotterDialog/PlotterDialog.py @@ -1,4 +1,4 @@ -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, QTimer from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, @@ -7,11 +7,19 @@ from PyQt5.QtWidgets import ( QPushButton, QLineEdit, QMessageBox, + QHBoxLayout, + QCheckBox, ) +import numpy as np + from .ButtonGroup import ButtonGroup from .GraphRequester import GraphRequester +from parser.parser import Parser + +from GraphWidget.GraphWidget import GraphWidget + class PlotterDialog(QDialog): focused_line_edit = None @@ -20,10 +28,12 @@ class PlotterDialog(QDialog): self, variable_full_names: dict[str, str], function_full_names: dict[str, str], - # variable_values: dict[str, np.ndarray], + variable_values: dict[str, np.ndarray], ): super().__init__() + self.variable_values = variable_values + self.setWindowTitle("Графопостроитель") layout_boss = QVBoxLayout() # главный лояут @@ -43,6 +53,8 @@ class PlotterDialog(QDialog): self.num_of_input = 0 # инициализация первого графика self.add_input() + QTimer.singleShot(0, self.focus_first_input) + scrollWidget.setLayout(self.inputs_layout) self.scroll.setWidgetResizable(True) @@ -70,6 +82,30 @@ class PlotterDialog(QDialog): ) ) + layout_boss.addSpacing(10) + + buttons_layout = QHBoxLayout() + + buttons_layout.setDirection(QHBoxLayout.RightToLeft) + + submit_button = QPushButton("Построить") + reset_button = QPushButton("Сброс") + + submit_button.clicked.connect(self.plot) + reset_button.clicked.connect(self.reset) + + submit_button.setDefault(True) + + buttons_layout.addWidget(submit_button) + buttons_layout.addWidget(reset_button) + + self.subplots_checkbox = QCheckBox("Рисовать графики на отдельных осях") + buttons_layout.addWidget(self.subplots_checkbox) + + buttons_layout.addStretch(1) + + layout_boss.addLayout(buttons_layout) + self.setLayout(layout_boss) def add_input(self): @@ -102,11 +138,12 @@ class PlotterDialog(QDialog): line_edit = self.focused_line_edit if line_edit is None: - QMessageBox( + dlg = QMessageBox( QMessageBox.Warning, "Ошибка", "Выберите поле ввода выражения", - ).exec() + ) + dlg.exec() return @@ -128,3 +165,65 @@ class PlotterDialog(QDialog): def insert_function(self, name: str): string = f" {name}()" self.insert_string(string, len(string) - 1) # len - 1 for cursor between braces + + def plot(self): + xs, ys, labels = [], [], [] + + for i in range(self.inputs_layout.count()): + graph_requester = self.inputs_layout.itemAt(i).widget() + + if graph_requester is not None: + x_expr = graph_requester.LineEditX.text() + y_expr = graph_requester.LineEditY.text() + label = graph_requester.LineEditGraf.text() + + if len(x_expr) * len(y_expr) == 0: + dlg = QMessageBox( + QMessageBox.Warning, + "Ошибка", + f'График "{label}" не задан', + ) + dlg.exec() + return + + x = Parser(x_expr).evaluate(self.variable_values) + y = Parser(y_expr).evaluate(self.variable_values) + + xs.append(x) + ys.append(y) + labels.append(label) + + mult_subplots = self.subplots_checkbox.isChecked() + + self.graph = GraphWidget(xs, ys, labels, mult_plots=mult_subplots) + + self.graph.show() + + def reset(self): + dlg = QMessageBox( + QMessageBox.Question, + "Очистка", + "Вы уверены, что хотите очистить все введённые выражения?", + buttons=QMessageBox.Yes | QMessageBox.No, + ) + + res = dlg.exec() + + if res != QMessageBox.Yes: + return + + while self.inputs_layout.count() > 0: + widget = self.inputs_layout.takeAt(0).widget() + if widget is not None: + widget.setParent(None) + + self.focused_line_edit = None + + self.num_of_input = 0 + + self.add_input() + + def focus_first_input(self): + first_graph_request = self.inputs_layout.itemAt(0).widget() + if first_graph_request is not None: + first_graph_request.LineEditGraf.setFocus() diff --git a/PlotterDialog/__init__.py b/PlotterDialog/PlotterDialog/__init__.py similarity index 100% rename from PlotterDialog/__init__.py rename to PlotterDialog/PlotterDialog/__init__.py diff --git a/PlotterDialog/PlotterDialog/__main__.py b/PlotterDialog/PlotterDialog/__main__.py new file mode 100644 index 0000000..0cbeda8 --- /dev/null +++ b/PlotterDialog/PlotterDialog/__main__.py @@ -0,0 +1,48 @@ +import sys + +from PyQt5.QtWidgets import QApplication + +import numpy as np + +from .PlotterDialog import PlotterDialog + +app = QApplication(sys.argv) + +variables = [chr(ord("a") + i) for i in range(10)] + +functions = [ + "abs", + "acos", + "acosh", + "acot", + "asin", + "asinh", + "atan", + "avg", + "cos", + "cosh", + "cot", + "exp", + "lg", + "ln", + "log2", + "max", + "min", + "prod", + "sgn", + "sin", + "sinh", + "sqrt", + "sum", + "tanh", + "tan", +] + +dlg = PlotterDialog( + variable_full_names={key: key.upper() for key in variables}, + function_full_names={key: key.upper() for key in functions}, + variable_values={key: np.sort(np.random.random(10)) * 10 for key in variables}, +) +dlg.show() + +sys.exit(app.exec()) diff --git a/PlotterDialog/__main__.py b/PlotterDialog/__main__.py deleted file mode 100644 index d64c03a..0000000 --- a/PlotterDialog/__main__.py +++ /dev/null @@ -1,14 +0,0 @@ -import sys - -from PyQt5.QtWidgets import QApplication - -from .PlotterDialog import PlotterDialog - -app = QApplication(sys.argv) - -PlotterDialog( - variable_full_names={"a": "A", "b": "B", "c": "C"}, - function_full_names={"exp": "экспонента", "ln": "Логарифм", "mod": "модуль"}, -).show() - -sys.exit(app.exec()) diff --git a/PlotterDialog/poetry.lock b/PlotterDialog/poetry.lock index 4d0505b..2b9fc62 100644 --- a/PlotterDialog/poetry.lock +++ b/PlotterDialog/poetry.lock @@ -123,6 +123,25 @@ files = [ {file = "docstring_to_markdown-0.13-py3-none-any.whl", hash = "sha256:aa487059d0883e70e54da25c7b230e918d9e4d40f23d6dfaa2b73e4225b2d7dd"}, ] +[[package]] +name = "graphwidget" +version = "0.1.0" +description = "" +category = "main" +optional = false +python-versions = ">=3.11,<3.13" +files = [] +develop = true + +[package.dependencies] +numpy = "^1.26.0" +pyqt5 = "5.15.2" +pyqt5-qt5 = "5.15.2" + +[package.source] +type = "directory" +url = "../GraphWidget" + [[package]] name = "isort" version = "5.12.0" @@ -308,6 +327,23 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "parser" +version = "0.1.0" +description = "" +category = "main" +optional = false +python-versions = ">=3.11,<3.13" +files = [] +develop = true + +[package.dependencies] +numpy = "^1.26.0" + +[package.source] +type = "directory" +url = "../parser" + [[package]] name = "parso" version = "0.8.3" @@ -492,4 +528,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "cb0cbf2520f4660ecec4008bce226d433786b483e26cbc156fee2085fefbd6e0" +content-hash = "d6fccd0b448db425a584bd320ed7fa1eb0862d8eebca2d11b8150cc761147d4b" diff --git a/PlotterDialog/pyproject.toml b/PlotterDialog/pyproject.toml index 488b523..fdf6d73 100644 --- a/PlotterDialog/pyproject.toml +++ b/PlotterDialog/pyproject.toml @@ -10,6 +10,8 @@ python = ">=3.11,<3.13" numpy = "^1.26.0" pyqt5 = "5.15.2" pyqt5-qt5 = "5.15.2" +parser = { path = "../parser", develop = true } +GraphWidget = { path = "../GraphWidget", develop = true } [tool.poetry.group.dev.dependencies]