{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Ввод данных\n",
"\n",
"`Qt` предоставляет богатый набор виджетов для ввода данных.\n",
"\n",
"- [QLineEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html) --- ввод одной строки текстовых данных; [QTextEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTextEdit.html) и [QPlainTextEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QPlainTextEdit.html) --- ввод нескольких строк текста, в том числе и форматированного.\n",
"- для ввода чисел и некоторых перечисляемых значений (например, месяц), часто используются так называемые `spin box` поля: [QSpinBox](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QSpinBox) для ввода целых чисел и дискретных перечисляемых значений, [QDoubleSpinBox](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDoubleSpinBox.html) для ввода действительных чисел; [QDateTimeEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDateTimeEdit.html), [QDateEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDateEdit.html) и [QTimeEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTimeEdit.html) для ввода даты и/или времени;\n",
"- также для ввода численных значений можно применять ползунки: [QSlider](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSlider.html), [QScrollBar](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QScrollBar.html) и [QDial](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDial.html);\n",
"- [QTableWidget](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableWidget.html) и [QTableView](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTableView.html) могут использоваться и для ввода данных;\n",
"\n",
"Кроме того, для ввода данных и ответа на вопросы можно использовать диалоговые окна, который будут обсуждаться в следующем разделе.\n",
"\n",
"## Строка текста. `QLineEdit`\n",
"\n",
"Для ввода одной строки текста обычно используется [QLineEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html). Когда на виджете `QLineEdit` стоит фокус, в нем мигает курсор и в это время `Qt` перехватывает большинство нажатий на клавиатуру и интерпретирует их в качестве ввода символов. Кроме символов алфавита `QLineEdit` принимает и управляющий символы. Например, нажатие на клавишу `Enter` сигнализирует о завершении редактирования, что приводит к возбуждению сигнала [editingFinished](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.editingFinished). \n",
"\n",
"Вообще говоря, можно управлять тем, будет ли очередной символ принят к вводу или отвергнут. Для этого методом [setValidator](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.setValidator) необходимо настроить [QValidator](https://doc.qt.io/qtforpython/PySide6/QtGui/QValidator.html). `Qt` предлагает ряд встроенных валидаторов. Например, валидатор целых чисел [QIntValidator](https://doc.qt.io/qtforpython/PySide6/QtGui/QIntValidator.html) и валидатор действительных чисел [QDoubleValidator](https://doc.qt.io/qtforpython/PySide6/QtGui/QDoubleValidator.html). Произвольный валидатор можно написать используя регулярные выражения и соответствующий валидатор [QRegularExpressionValidator](https://doc.qt.io/qtforpython/PySide6/QtGui/QRegularExpressionValidator.html). Кроме того, длину текста можно ограничить методом [setMaxLength](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.setMaxLength).\n",
"\n",
"Вне зависимости от источника при изменении текста в текстовом поле излучается сигнал [textChanged](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.textChanged). Сигнал [textEdited](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.textEdited) излучается только при редактировании текста пользователем, т.е. он игнорирует изменения текста методом [setText](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.setText).\n",
"Уже упомянутый сигнал [editingFinished](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.editingFinished) возбуждается не при каждом изменении текста, а только при завершении редактировании, о чем сигнализирует потеря фокуса виджетом или нажатие на `Enter`. При этом сигнал не излучится, если во время редактирования текст не изменился или изменился на корректный с точки зрения валидатора.\n",
"\n",
"Также есть приличное количество методов-слотов. Например, метод [clear](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.clear) очищает текстовое поле, метод [copy](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.copy) копирует его содержимое в буфер обмена, метод [paste](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.paste) вставляет в текстовое поле содержимое из текстового поля, [selectAll](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.selectAll) выделяет все содержимое окна. \n",
"\n",
"Отображаемый в этом поле текст необязательно должен совпадать с тем, который находится в буфере ввода. Методом [setEchoMode](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.setEchoMode) можно настроить другой стиль отображения. На вход он ожидает `enum` [QLineEdit.EchoMode](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.EchoMode), который принимает значения: \n",
"- `QLineEdit.Normal` --- обычный режим, набираемый текст совпадает с отображаемым;\n",
"- `QLineEdit.NoEcho` --- не отображать ничего;\n",
"- `QLineEdit.Password` --- пароль, вместо каждого символа отображать звездочку;\n",
"- `QLineEdit.PasswordEchoOnEdit` --- как и предыдущий, но отображать текст пока он вводится.\n",
"\n",
"Получить введенный текст можно методом [text](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.text), а отображаемый текст методом [displayText](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.displayText).\n",
"\n",
"\n",
"```{figure} ../_static/lecture_specific/qt/lineedit.png\n",
"```\n",
"\n",
"Код под спойлером ниже воссоздаёт окно выше и демонстрирует большинство описанных выше возможностей виджета `QLineEdit`.\n",
"\n",
"\n",
"Код.
\n",
"\n",
"```python\n",
"import sys\n",
"from functools import partial\n",
"\n",
"from PySide6.QtGui import *\n",
"from PySide6.QtWidgets import *\n",
"\n",
"\n",
"class MainWindow(QMainWindow):\n",
" def __init__(self):\n",
" super().__init__()\n",
" widget = QWidget()\n",
" main_layout = QVBoxLayout()\n",
" widget.setLayout(main_layout)\n",
" self.setCentralWidget(widget)\n",
"\n",
" # edit\n",
" self.edit = QLineEdit()\n",
" self.edit.setClearButtonEnabled(True)\n",
"\n",
" # buttons row\n",
" clear_button = QPushButton(\"Clear\")\n",
" copy_button = QPushButton(\"Copy\")\n",
" paste_button = QPushButton(\"Paste\")\n",
" select_button = QPushButton(\"Select all\")\n",
"\n",
" # labels\n",
" self.label = QLabel(\"\")\n",
" self.label.setAlignment(Qt.AlignLeft)\n",
"\n",
" # checkboxes\n",
" self.is_password = QCheckBox(\"password\")\n",
" self.numbers_only = QCheckBox(\"numbers only\")\n",
"\n",
" # layout\n",
" main_layout.addWidget(self.edit)\n",
"\n",
" button_layout = QHBoxLayout()\n",
" button_layout.addWidget(clear_button)\n",
" button_layout.addWidget(copy_button)\n",
" button_layout.addWidget(paste_button)\n",
" button_layout.addWidget(select_button)\n",
" main_layout.addLayout(button_layout)\n",
"\n",
" main_layout.addWidget(self.label)\n",
"\n",
" checkbox_layout = QHBoxLayout()\n",
" checkbox_layout.addWidget(self.is_password)\n",
" checkbox_layout.addWidget(self.numbers_only)\n",
" main_layout.addLayout(checkbox_layout)\n",
"\n",
" # connections\n",
" clear_button.clicked.connect(self.edit.clear)\n",
" copy_button.clicked.connect(self.edit.copy)\n",
" paste_button.clicked.connect(self.edit.paste)\n",
" select_button.clicked.connect(self.edit.selectAll)\n",
" self.edit.editingFinished.connect(self.sync)\n",
" self.is_password.stateChanged.connect(self.update_echo_mode)\n",
" self.numbers_only.stateChanged.connect(self.update_validator)\n",
"\n",
" def sync(self):\n",
" self.label.setText(self.edit.text())\n",
"\n",
" def update_echo_mode(self):\n",
" mode = QLineEdit.Password if self.is_password.isChecked() else QLineEdit.Normal\n",
" self.edit.setEchoMode(mode)\n",
"\n",
" def update_validator(self):\n",
" validator = QIntValidator() if self.numbers_only.isChecked() else None\n",
" validator.setBottom(0)\n",
" self.edit.setValidator(validator)\n",
"\n",
"\n",
"app = QApplication(sys.argv)\n",
"main_window = MainWindow()\n",
"main_window.show()\n",
"app.exec()\n",
"```\n",
" \n",
"\n",
"## Числа. `QSpinBox` и его разновидности\n",
"\n",
"Виджет `SpinBox` в отличие от `QLineEdit` обычно используется для ввода чисел. Кроме ввода с клавиатуры этот виджет позволяет увеличивать или уменьшать вводимое значение, нажимая на специальные кнопки. Всего есть следующие разновидности спинбоксов:\n",
"- [QSpinBox](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSpinBox.html#qspinbox) --- ввод целых чисел и перечисляемых значений;\n",
"- [QDoubleSpinBox](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDoubleSpinBox.html#qdoublespinbox) --- ввод действительных значений;\n",
"- [QDateTimeEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDateTimeEdit.html#qdatetimeedit), [QDateEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDateEdit.html#qdateedit) и [QTimeEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QTimeEdit.html#qtimeedit) --- ввод даты и времени.\n",
"\n",
"Все они наследуют от абстрактного базового класса [QAbstractSpinBox](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QAbstractSpinBox.html). Чтобы настроить ввод чисел, необходимо знать про следующие методы:\n",
"- [setMinimum](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QSpinBox.setMinimum) и [setMaximum](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QSpinBox.setMinimum) --- настроить диапазон;\n",
"- [setValue](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QSpinBox.setValue) --- установить значение, в том числе и начальное;\n",
"- [setSingleStep](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDoubleSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QDoubleSpinBox.setSingleStep) --- установить шаг.\n",
"\n",
"При изменении значений испускается сигнал. [valueChanged](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDoubleSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QDoubleSpinBox.valueChanged)\n",
"\n",
"Для `QDoubleSpinBox` методом [setDecimals](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDoubleSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QDoubleSpinBox.setDecimals) выставить количество десятичных знаков. `QSpinBox` можно настроить на выбор перечисляемых значений, перегрузив метод [textFromValue](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDoubleSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QDoubleSpinBox.textFromValue). Тогда удобнее использовать сигнал [textChanged](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDoubleSpinBox.html#PySide6.QtWidgets.PySide6.QtWidgets.QDoubleSpinBox.textChanged).\n",
"\n",
"```{figure} ../_static/lecture_specific/qt/spinbox.png\n",
"```\n",
"\n",
"Код под спойлером ниже воссоздаёт окно выше и демонстрирует большинство описанных выше возможностей виджета `QSpinBox` и `QDoubleSpinBox`.\n",
"\n",
"\n",
"Код.
\n",
"\n",
"```python\n",
"import sys\n",
"\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT\n",
"\n",
"from PySide6.QtCore import *\n",
"from PySide6.QtWidgets import *\n",
"\n",
"\n",
"class MPLGraph(FigureCanvasQTAgg):\n",
" def __init__(self):\n",
" fig, ax = plt.subplots(figsize=(2, 2))\n",
" ax.set_xlim([0, 1])\n",
" ax.set_ylim([0, 1])\n",
" self.x = np.linspace(0, 2 * np.pi, 1000)\n",
" self.line, = ax.plot(self.x, self.x)\n",
" super().__init__(fig)\n",
"\n",
" def update_power(self, power):\n",
" y = self.x ** power\n",
" self.line.set_ydata(y)\n",
" self.draw()\n",
"\n",
" def update_linewidth(self, width):\n",
" self.line.set_linewidth(width)\n",
" self.draw()\n",
"\n",
" def update_color(self, color):\n",
" self.line.set_color(color)\n",
" self.draw()\n",
"\n",
"\n",
"class PowerSpinBox(QSpinBox):\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.setMinimum(0)\n",
" self.setMaximum(100)\n",
" self.setValue(1)\n",
" self.setPrefix(\"Power: \")\n",
"\n",
"\n",
"class RainbowColorSpinBox(QSpinBox):\n",
" colors = [\"red\",\n",
" \"orange\",\n",
" \"yellow\",\n",
" \"green\",\n",
" \"blue\",\n",
" \"magenta\",\n",
" \"violet\"]\n",
"\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.setMinimum(0)\n",
" self.setMaximum(6)\n",
"\n",
" def textFromValue(self, val):\n",
" return self.colors[val]\n",
"\n",
"\n",
"class LineWidthSpinBox(QDoubleSpinBox):\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.setMinimum(0.1)\n",
" self.setMaximum(10)\n",
" self.setSingleStep(0.1)\n",
" self.setValue(1.)\n",
" self.setSuffix(\" px\")\n",
"\n",
"\n",
"class MainWindow(QMainWindow):\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.setGeometry(0, 0, 600, 400)\n",
" widget = QWidget()\n",
" layout = QVBoxLayout()\n",
" widget.setLayout(layout)\n",
" self.setCentralWidget(widget)\n",
"\n",
" # widgets\n",
" graph = MPLGraph()\n",
"\n",
" linewidth_sb = LineWidthSpinBox()\n",
" linewidth_label = QLabel(\"Line width\")\n",
" linewidth_label.setAlignment(Qt.AlignRight)\n",
"\n",
" power_sb = PowerSpinBox()\n",
" power_label = QLabel(\"Power\")\n",
" power_label.setAlignment(Qt.AlignRight)\n",
"\n",
" color_sb = RainbowColorSpinBox()\n",
" color_label = QLabel(\"Color\")\n",
" color_label.setAlignment(Qt.AlignRight)\n",
"\n",
"\n",
" # layout\n",
" layout.addWidget(graph)\n",
"\n",
" bottom_row = QHBoxLayout()\n",
" bottom_row.addWidget(linewidth_label)\n",
" bottom_row.addWidget(linewidth_sb)\n",
" bottom_row.addWidget(power_label)\n",
" bottom_row.addWidget(power_sb)\n",
" bottom_row.addWidget(color_label)\n",
" bottom_row.addWidget(color_sb)\n",
" layout.addLayout(bottom_row)\n",
"\n",
" # connections\n",
" linewidth_sb.valueChanged.connect(graph.update_linewidth)\n",
" power_sb.valueChanged.connect(graph.update_power)\n",
" color_sb.textChanged.connect(graph.update_color)\n",
"\n",
"\n",
"app = QApplication(sys.argv)\n",
"main_window = MainWindow()\n",
"main_window.show()\n",
"app.exec()\n",
"```\n",
" \n",
"\n",
"\n",
"## Ползунки. `QSlider` \n",
"\n",
"В ряде ситуация удобно использовать ползунки, для интерактивного изменения численных значений. Принцип их действия очень похож на `QSpinBox`: они хранят в себе целое число, при изменении которого испускается сигнал `valueChanged`, диапазон контролируется методами `setMinimum` и `setMaximum`. Для воплощения ползунков есть три следующие виджеты:\n",
"\n",
"- [QSlider](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QSlider.html#qslider);\n",
"- [QDial](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QDial.html);\n",
"- [QScrollBar](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QScrollBar.html).\n",
"\n",
"```{figure} ../_static/lecture_specific/qt/sliders.png\n",
"```\n",
"\n",
"Пример применения этих виджетов приводится в спойлере ниже.\n",
"\n",
"\n",
"Код.
\n",
"\n",
"```python\n",
"import sys\n",
"\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg\n",
"\n",
"from PySide6.QtCore import *\n",
"from PySide6.QtWidgets import *\n",
"\n",
"\n",
"class MPLGraph(FigureCanvasQTAgg):\n",
" def __init__(self):\n",
" fig, self.ax = plt.subplots(figsize=(2, 2), layout=\"tight\")\n",
" self.ax.set_xticks(np.arange(0, 4 * np.pi + 0.0001, np.pi / 2))\n",
" self.ax.set_xticklabels([\n",
" \"0\",\n",
" r\"$\\dfrac{\\pi}{2}$\",\n",
" r\"$\\pi$\",\n",
" r\"$\\dfrac{3\\pi}{2}$\",\n",
" r\"$2\\pi$\",\n",
" r\"$\\dfrac{5\\pi}{2}$\",\n",
" r\"$3\\pi$\",\n",
" r\"$\\dfrac{7\\pi}{2}$\",\n",
" r\"$4\\pi$\"\n",
" ])\n",
" self.ax.grid()\n",
"\n",
" self.x = np.linspace(0, 4 * np.pi, 1000)\n",
" self.magnitude = 1.\n",
" self.phase = 0.\n",
" self.line, = self.ax.plot(self.x, self.compute_y())\n",
" self.ax.set_xlim([0, 2 * np.pi])\n",
" self.ax.set_ylim([-1, 1])\n",
" super().__init__(fig)\n",
"\n",
" def compute_y(self):\n",
" return self.magnitude * np.sin(self.x - self.phase)\n",
"\n",
" def update_plot(self):\n",
" self.line.set_ydata(self.compute_y())\n",
" self.draw()\n",
"\n",
" def update_horizontal_range(self, scroll_value):\n",
" left_boundary = scroll_value / 100. * 2 * np.pi\n",
" self.ax.set_xlim([left_boundary, left_boundary + 2 * np.pi])\n",
" self.draw()\n",
"\n",
" def update_magnitude(self, slider_value):\n",
" self.magnitude = slider_value / 100.\n",
" self.update_plot()\n",
"\n",
" def update_phase(self, dial_value):\n",
" self.phase = dial_value / 100. * np.pi\n",
" self.update_plot()\n",
"\n",
"\n",
"class RangeScrollBar(QScrollBar):\n",
" def __init__(self):\n",
" super().__init__(Qt.Horizontal)\n",
" self.setMinimum(0)\n",
" self.setMaximum(100)\n",
"\n",
"\n",
"class HeightSlider(QSlider):\n",
" def __init__(self):\n",
" super().__init__(Qt.Vertical)\n",
" self.setMinimum(0)\n",
" self.setMaximum(100)\n",
" self.setValue(100)\n",
"\n",
"\n",
"class PhaseDial(QDial):\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.setMinimum(0)\n",
" self.setMaximum(100)\n",
"\n",
"\n",
"class MainWindow(QMainWindow):\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.setGeometry(0, 0, 600, 400)\n",
" widget = QWidget()\n",
" layout = QGridLayout()\n",
" widget.setLayout(layout)\n",
" self.setCentralWidget(widget)\n",
"\n",
" # widgets\n",
" graph = MPLGraph()\n",
" range_scroll_bar = RangeScrollBar()\n",
" magnitude_slider = HeightSlider()\n",
" phase_dial = PhaseDial()\n",
"\n",
" # layout\n",
" layout.addWidget(magnitude_slider, 1, 1)\n",
" layout.addWidget(phase_dial, 2, 1)\n",
" layout.addWidget(graph, 1, 2, 2, 2)\n",
" layout.addWidget(range_scroll_bar, 3, 2, 1, 2)\n",
"\n",
" # connections\n",
" range_scroll_bar.valueChanged.connect(graph.update_horizontal_range)\n",
" magnitude_slider.valueChanged.connect(graph.update_magnitude)\n",
" phase_dial.valueChanged.connect(graph.update_phase)\n",
"\n",
"\n",
"app = QApplication(sys.argv)\n",
"main_window = MainWindow()\n",
"main_window.show()\n",
"app.exec()\n",
"```\n",
" "
]
}
],
"metadata": {
"language_info": {
"name": "python"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}