Refactored project to remove poetry. Pure pip used instead
This commit is contained in:
3
PyQt-Plotter-Dialog/plotter_dialog/__init__.py
Normal file
3
PyQt-Plotter-Dialog/plotter_dialog/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .dialog import PlotterDialog
|
||||
|
||||
__all__ = ("PlotterDialog",)
|
52
PyQt-Plotter-Dialog/plotter_dialog/__main__.py
Normal file
52
PyQt-Plotter-Dialog/plotter_dialog/__main__.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import sys
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import PlotterDialog
|
||||
|
||||
|
||||
def main():
|
||||
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())
|
||||
|
||||
main()
|
44
PyQt-Plotter-Dialog/plotter_dialog/button_group.py
Normal file
44
PyQt-Plotter-Dialog/plotter_dialog/button_group.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
|
||||
|
||||
from .flow_layout import FlowLayout
|
||||
|
||||
|
||||
class ButtonGroup(QWidget):
|
||||
def __init__(
|
||||
self,
|
||||
category: str,
|
||||
full_names: dict[str, str],
|
||||
buttons_action: Callable[[str], None],
|
||||
parent=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.layout = QVBoxLayout() # Создание основного лаяутв
|
||||
Doplayout = FlowLayout()
|
||||
label = QLabel(category)
|
||||
|
||||
self.layout.addWidget(label)
|
||||
for button_name in full_names:
|
||||
button = QPushButton(button_name, self)
|
||||
|
||||
button.setFixedWidth(80)
|
||||
button.setToolTip(
|
||||
full_names[button_name]
|
||||
) # Создание подскачоки при наведении
|
||||
|
||||
button.clicked.connect(
|
||||
lambda _, name=button_name: buttons_action( # ignore checked state with _
|
||||
name
|
||||
)
|
||||
) # Назначение кнопочке действия
|
||||
|
||||
Doplayout.addWidget(button) # отрисовывание кнопок
|
||||
|
||||
Doplayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.layout.addLayout(Doplayout)
|
||||
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.setLayout(self.layout)
|
229
PyQt-Plotter-Dialog/plotter_dialog/dialog.py
Normal file
229
PyQt-Plotter-Dialog/plotter_dialog/dialog.py
Normal file
@@ -0,0 +1,229 @@
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QScrollArea,
|
||||
QWidget,
|
||||
QPushButton,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QHBoxLayout,
|
||||
QCheckBox,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .button_group import ButtonGroup
|
||||
from .graph_requester import GraphRequester
|
||||
|
||||
from parser import Parser
|
||||
|
||||
from graph_widget import GraphWidget
|
||||
|
||||
|
||||
class PlotterDialog(QDialog):
|
||||
focused_line_edit = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
variable_full_names: dict[str, str],
|
||||
function_full_names: dict[str, str],
|
||||
variable_values: dict[str, np.ndarray],
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.variable_values = variable_values
|
||||
|
||||
self.setWindowTitle("Графопостроитель")
|
||||
|
||||
layout_boss = QVBoxLayout() # главный лояут
|
||||
|
||||
self.scroll = QScrollArea()
|
||||
|
||||
self.scroll.verticalScrollBar().rangeChanged.connect(self.scroll_bottom)
|
||||
|
||||
scrollWidget = QWidget()
|
||||
|
||||
# self.scroll.setFrameShape(QFrame.NoFrame)
|
||||
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
|
||||
self.inputs_layout = QVBoxLayout() # лаяут первой трети
|
||||
self.inputs_layout.addStretch(1)
|
||||
|
||||
self.num_of_input = 0 # инициализация первого графика
|
||||
self.add_input()
|
||||
|
||||
QTimer.singleShot(0, self.focus_first_input)
|
||||
|
||||
scrollWidget.setLayout(self.inputs_layout)
|
||||
|
||||
self.scroll.setWidgetResizable(True)
|
||||
self.scroll.setWidget(scrollWidget)
|
||||
|
||||
layout_boss.addWidget(self.scroll)
|
||||
|
||||
Button_make_fun_button = QPushButton("+")
|
||||
Button_make_fun_button.clicked.connect(self.add_input)
|
||||
Button_make_fun_button.setFixedWidth(80)
|
||||
layout_boss.addWidget(Button_make_fun_button, alignment=Qt.AlignRight)
|
||||
|
||||
layout_boss.addWidget(
|
||||
ButtonGroup(
|
||||
"Переменные",
|
||||
full_names=variable_full_names,
|
||||
buttons_action=self.insert_variable,
|
||||
)
|
||||
)
|
||||
layout_boss.addWidget(
|
||||
ButtonGroup(
|
||||
"Функции",
|
||||
full_names=function_full_names,
|
||||
buttons_action=self.insert_function,
|
||||
)
|
||||
)
|
||||
|
||||
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):
|
||||
self.num_of_input += 1
|
||||
|
||||
graph_requester = GraphRequester(self.num_of_input)
|
||||
|
||||
for line_edit in (graph_requester.LineEditX, graph_requester.LineEditY):
|
||||
line_edit.focused_in.connect(
|
||||
lambda line_edit=line_edit: self.set_focused_line_edit(line_edit)
|
||||
)
|
||||
|
||||
graph_requester.LineEditGraf.focused_in.connect(
|
||||
lambda: self.set_focused_line_edit(None)
|
||||
)
|
||||
|
||||
self.inputs_layout.insertWidget(self.inputs_layout.count() - 1, graph_requester)
|
||||
|
||||
graph_requester.LineEditGraf.setFocus()
|
||||
|
||||
def set_focused_line_edit(self, line_edit: QLineEdit | None):
|
||||
self.focused_line_edit = line_edit
|
||||
|
||||
def scroll_bottom(self):
|
||||
self.scroll.verticalScrollBar().setValue(
|
||||
self.scroll.verticalScrollBar().maximum()
|
||||
)
|
||||
|
||||
def insert_string(self, string: str, string_cursor_padding=-1):
|
||||
line_edit = self.focused_line_edit
|
||||
|
||||
if line_edit is None:
|
||||
dlg = QMessageBox(
|
||||
QMessageBox.Warning,
|
||||
"Ошибка",
|
||||
"Выберите поле ввода выражения",
|
||||
)
|
||||
dlg.exec()
|
||||
|
||||
return
|
||||
|
||||
cusor_pos = line_edit.cursorPosition()
|
||||
text = line_edit.text()
|
||||
|
||||
if string_cursor_padding < 1:
|
||||
string_cursor_padding = len(string)
|
||||
|
||||
line_edit.setText(text[:cusor_pos] + string + text[cusor_pos:])
|
||||
line_edit.setCursorPosition(cusor_pos + string_cursor_padding)
|
||||
|
||||
self.scroll.ensureWidgetVisible(line_edit)
|
||||
line_edit.setFocus()
|
||||
|
||||
def insert_variable(self, name: str):
|
||||
self.insert_string(f" {name} ")
|
||||
|
||||
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()
|
147
PyQt-Plotter-Dialog/plotter_dialog/flow_layout.py
Normal file
147
PyQt-Plotter-Dialog/plotter_dialog/flow_layout.py
Normal file
@@ -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()
|
56
PyQt-Plotter-Dialog/plotter_dialog/graph_requester.py
Normal file
56
PyQt-Plotter-Dialog/plotter_dialog/graph_requester.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from PyQt5.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QLineEdit,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
|
||||
|
||||
class FocusNotifyingLineEdit(QLineEdit):
|
||||
focused_in = pyqtSignal()
|
||||
|
||||
def focusInEvent(self, event):
|
||||
self.focused_in.emit()
|
||||
super(FocusNotifyingLineEdit, self).focusInEvent(event)
|
||||
|
||||
|
||||
class GraphRequester(QWidget):
|
||||
LineEditGraf: FocusNotifyingLineEdit
|
||||
LineEditX: FocusNotifyingLineEdit
|
||||
LineEditY: FocusNotifyingLineEdit
|
||||
|
||||
def __init__(self, nomer_grafika=1):
|
||||
super().__init__()
|
||||
layout = QVBoxLayout(self)
|
||||
layout_x = QHBoxLayout()
|
||||
layout_y = QHBoxLayout()
|
||||
layout_close_and_name = QHBoxLayout()
|
||||
self.LineEditGraf = FocusNotifyingLineEdit(f"график {nomer_grafika}")
|
||||
NameX = QLabel("X")
|
||||
NameY = QLabel("Y")
|
||||
Name_Close = QPushButton("x")
|
||||
|
||||
self.LineEditX = FocusNotifyingLineEdit()
|
||||
self.LineEditY = FocusNotifyingLineEdit()
|
||||
|
||||
layout_x.addWidget(NameX)
|
||||
layout_x.addWidget(self.LineEditX)
|
||||
|
||||
layout_y.addWidget(NameY)
|
||||
layout_y.addWidget(self.LineEditY)
|
||||
|
||||
Name_Close.clicked.connect(lambda: self.setParent(None))
|
||||
|
||||
layout_close_and_name.addWidget(self.LineEditGraf)
|
||||
layout_close_and_name.addWidget(Name_Close)
|
||||
|
||||
layout.addLayout(layout_close_and_name) # Вложения названия и закрыть
|
||||
layout.addLayout(layout_y) # Вложение
|
||||
layout.addLayout(layout_x) # Вложение
|
||||
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addStretch(1)
|
Reference in New Issue
Block a user