{ "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 }