Ввод данных

Qt предоставляет богатый набор виджетов для ввода данных.

  • QLineEdit — ввод одной строки текстовых данных; QTextEdit и QPlainTextEdit — ввод нескольких строк текста, в том числе и форматированного.

  • для ввода чисел и некоторых перечисляемых значений (например, месяц), часто используются так называемые spin box поля: QSpinBox для ввода целых чисел и дискретных перечисляемых значений, QDoubleSpinBox для ввода действительных чисел; QDateTimeEdit, QDateEdit и QTimeEdit для ввода даты и/или времени;

  • также для ввода численных значений можно применять ползунки: QSlider, QScrollBar и QDial;

  • QTableWidget и QTableView могут использоваться и для ввода данных;

Кроме того, для ввода данных и ответа на вопросы можно использовать диалоговые окна, который будут обсуждаться в следующем разделе.

Строка текста. QLineEdit

Для ввода одной строки текста обычно используется QLineEdit. Когда на виджете QLineEdit стоит фокус, в нем мигает курсор и в это время Qt перехватывает большинство нажатий на клавиатуру и интерпретирует их в качестве ввода символов. Кроме символов алфавита QLineEdit принимает и управляющий символы. Например, нажатие на клавишу Enter сигнализирует о завершении редактирования, что приводит к возбуждению сигнала editingFinished.

Вообще говоря, можно управлять тем, будет ли очередной символ принят к вводу или отвергнут. Для этого методом setValidator необходимо настроить QValidator. Qt предлагает ряд встроенных валидаторов. Например, валидатор целых чисел QIntValidator и валидатор действительных чисел QDoubleValidator. Произвольный валидатор можно написать используя регулярные выражения и соответствующий валидатор QRegularExpressionValidator. Кроме того, длину текста можно ограничить методом setMaxLength.

Вне зависимости от источника при изменении текста в текстовом поле излучается сигнал textChanged. Сигнал textEdited излучается только при редактировании текста пользователем, т.е. он игнорирует изменения текста методом setText. Уже упомянутый сигнал editingFinished возбуждается не при каждом изменении текста, а только при завершении редактировании, о чем сигнализирует потеря фокуса виджетом или нажатие на Enter. При этом сигнал не излучится, если во время редактирования текст не изменился или изменился на корректный с точки зрения валидатора.

Также есть приличное количество методов-слотов. Например, метод clear очищает текстовое поле, метод copy копирует его содержимое в буфер обмена, метод paste вставляет в текстовое поле содержимое из текстового поля, selectAll выделяет все содержимое окна.

Отображаемый в этом поле текст необязательно должен совпадать с тем, который находится в буфере ввода. Методом setEchoMode можно настроить другой стиль отображения. На вход он ожидает enum QLineEdit.EchoMode, который принимает значения:

  • QLineEdit.Normal — обычный режим, набираемый текст совпадает с отображаемым;

  • QLineEdit.NoEcho — не отображать ничего;

  • QLineEdit.Password — пароль, вместо каждого символа отображать звездочку;

  • QLineEdit.PasswordEchoOnEdit — как и предыдущий, но отображать текст пока он вводится.

Получить введенный текст можно методом text, а отображаемый текст методом displayText.

../_images/lineedit.png

Код под спойлером ниже воссоздаёт окно выше и демонстрирует большинство описанных выше возможностей виджета QLineEdit.

Код.
import sys
from functools import partial

from PySide6.QtGui import *
from PySide6.QtWidgets import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        widget = QWidget()
        main_layout = QVBoxLayout()
        widget.setLayout(main_layout)
        self.setCentralWidget(widget)

        # edit
        self.edit = QLineEdit()
        self.edit.setClearButtonEnabled(True)

        # buttons row
        clear_button = QPushButton("Clear")
        copy_button = QPushButton("Copy")
        paste_button = QPushButton("Paste")
        select_button = QPushButton("Select all")

        # labels
        self.label = QLabel("")
        self.label.setAlignment(Qt.AlignLeft)

        # checkboxes
        self.is_password = QCheckBox("password")
        self.numbers_only = QCheckBox("numbers only")

        # layout
        main_layout.addWidget(self.edit)

        button_layout = QHBoxLayout()
        button_layout.addWidget(clear_button)
        button_layout.addWidget(copy_button)
        button_layout.addWidget(paste_button)
        button_layout.addWidget(select_button)
        main_layout.addLayout(button_layout)

        main_layout.addWidget(self.label)

        checkbox_layout = QHBoxLayout()
        checkbox_layout.addWidget(self.is_password)
        checkbox_layout.addWidget(self.numbers_only)
        main_layout.addLayout(checkbox_layout)

        # connections
        clear_button.clicked.connect(self.edit.clear)
        copy_button.clicked.connect(self.edit.copy)
        paste_button.clicked.connect(self.edit.paste)
        select_button.clicked.connect(self.edit.selectAll)
        self.edit.editingFinished.connect(self.sync)
        self.is_password.stateChanged.connect(self.update_echo_mode)
        self.numbers_only.stateChanged.connect(self.update_validator)

    def sync(self):
        self.label.setText(self.edit.text())

    def update_echo_mode(self):
        mode = QLineEdit.Password if self.is_password.isChecked() else QLineEdit.Normal
        self.edit.setEchoMode(mode)

    def update_validator(self):
        validator = QIntValidator() if self.numbers_only.isChecked() else None
        validator.setBottom(0)
        self.edit.setValidator(validator)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()

Числа. QSpinBox и его разновидности

Виджет SpinBox в отличие от QLineEdit обычно используется для ввода чисел. Кроме ввода с клавиатуры этот виджет позволяет увеличивать или уменьшать вводимое значение, нажимая на специальные кнопки. Всего есть следующие разновидности спинбоксов:

Все они наследуют от абстрактного базового класса QAbstractSpinBox. Чтобы настроить ввод чисел, необходимо знать про следующие методы:

  • setMinimum и setMaximum — настроить диапазон;

  • setValue — установить значение, в том числе и начальное;

  • setSingleStep — установить шаг.

При изменении значений испускается сигнал. valueChanged

Для QDoubleSpinBox методом setDecimals выставить количество десятичных знаков. QSpinBox можно настроить на выбор перечисляемых значений, перегрузив метод textFromValue. Тогда удобнее использовать сигнал textChanged.

../_images/spinbox.png

Код под спойлером ниже воссоздаёт окно выше и демонстрирует большинство описанных выше возможностей виджета QSpinBox и QDoubleSpinBox.

Код.
import sys

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT

from PySide6.QtCore import *
from PySide6.QtWidgets import *


class MPLGraph(FigureCanvasQTAgg):
    def __init__(self):
        fig, ax = plt.subplots(figsize=(2, 2))
        ax.set_xlim([0, 1])
        ax.set_ylim([0, 1])
        self.x = np.linspace(0, 2 * np.pi, 1000)
        self.line, = ax.plot(self.x, self.x)
        super().__init__(fig)

    def update_power(self, power):
        y = self.x ** power
        self.line.set_ydata(y)
        self.draw()

    def update_linewidth(self, width):
        self.line.set_linewidth(width)
        self.draw()

    def update_color(self, color):
        self.line.set_color(color)
        self.draw()


class PowerSpinBox(QSpinBox):
    def __init__(self):
        super().__init__()
        self.setMinimum(0)
        self.setMaximum(100)
        self.setValue(1)
        self.setPrefix("Power: ")


class RainbowColorSpinBox(QSpinBox):
    colors = ["red",
              "orange",
              "yellow",
              "green",
              "blue",
              "magenta",
              "violet"]

    def __init__(self):
        super().__init__()
        self.setMinimum(0)
        self.setMaximum(6)

    def textFromValue(self, val):
        return self.colors[val]


class LineWidthSpinBox(QDoubleSpinBox):
    def __init__(self):
        super().__init__()
        self.setMinimum(0.1)
        self.setMaximum(10)
        self.setSingleStep(0.1)
        self.setValue(1.)
        self.setSuffix(" px")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(0, 0, 600, 400)
        widget = QWidget()
        layout = QVBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        # widgets
        graph = MPLGraph()

        linewidth_sb = LineWidthSpinBox()
        linewidth_label = QLabel("Line width")
        linewidth_label.setAlignment(Qt.AlignRight)

        power_sb = PowerSpinBox()
        power_label = QLabel("Power")
        power_label.setAlignment(Qt.AlignRight)

        color_sb = RainbowColorSpinBox()
        color_label = QLabel("Color")
        color_label.setAlignment(Qt.AlignRight)


        # layout
        layout.addWidget(graph)

        bottom_row = QHBoxLayout()
        bottom_row.addWidget(linewidth_label)
        bottom_row.addWidget(linewidth_sb)
        bottom_row.addWidget(power_label)
        bottom_row.addWidget(power_sb)
        bottom_row.addWidget(color_label)
        bottom_row.addWidget(color_sb)
        layout.addLayout(bottom_row)

        # connections
        linewidth_sb.valueChanged.connect(graph.update_linewidth)
        power_sb.valueChanged.connect(graph.update_power)
        color_sb.textChanged.connect(graph.update_color)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()

Ползунки. QSlider

В ряде ситуация удобно использовать ползунки, для интерактивного изменения численных значений. Принцип их действия очень похож на QSpinBox: они хранят в себе целое число, при изменении которого испускается сигнал valueChanged, диапазон контролируется методами setMinimum и setMaximum. Для воплощения ползунков есть три следующие виджеты:

../_images/sliders.png

Пример применения этих виджетов приводится в спойлере ниже.

Код.
import sys

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg

from PySide6.QtCore import *
from PySide6.QtWidgets import *


class MPLGraph(FigureCanvasQTAgg):
    def __init__(self):
        fig, self.ax = plt.subplots(figsize=(2, 2), layout="tight")
        self.ax.set_xticks(np.arange(0, 4 * np.pi + 0.0001, np.pi / 2))
        self.ax.set_xticklabels([
            "0",
            r"$\dfrac{\pi}{2}$",
            r"$\pi$",
            r"$\dfrac{3\pi}{2}$",
            r"$2\pi$",
            r"$\dfrac{5\pi}{2}$",
            r"$3\pi$",
            r"$\dfrac{7\pi}{2}$",
            r"$4\pi$"
        ])
        self.ax.grid()

        self.x = np.linspace(0, 4 * np.pi, 1000)
        self.magnitude = 1.
        self.phase = 0.
        self.line, = self.ax.plot(self.x, self.compute_y())
        self.ax.set_xlim([0, 2 * np.pi])
        self.ax.set_ylim([-1, 1])
        super().__init__(fig)

    def compute_y(self):
        return self.magnitude * np.sin(self.x - self.phase)

    def update_plot(self):
        self.line.set_ydata(self.compute_y())
        self.draw()

    def update_horizontal_range(self, scroll_value):
        left_boundary = scroll_value / 100. * 2 * np.pi
        self.ax.set_xlim([left_boundary, left_boundary + 2 * np.pi])
        self.draw()

    def update_magnitude(self, slider_value):
        self.magnitude = slider_value / 100.
        self.update_plot()

    def update_phase(self, dial_value):
        self.phase = dial_value / 100. * np.pi
        self.update_plot()


class RangeScrollBar(QScrollBar):
    def __init__(self):
        super().__init__(Qt.Horizontal)
        self.setMinimum(0)
        self.setMaximum(100)


class HeightSlider(QSlider):
    def __init__(self):
        super().__init__(Qt.Vertical)
        self.setMinimum(0)
        self.setMaximum(100)
        self.setValue(100)


class PhaseDial(QDial):
    def __init__(self):
        super().__init__()
        self.setMinimum(0)
        self.setMaximum(100)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setGeometry(0, 0, 600, 400)
        widget = QWidget()
        layout = QGridLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        # widgets
        graph = MPLGraph()
        range_scroll_bar = RangeScrollBar()
        magnitude_slider = HeightSlider()
        phase_dial = PhaseDial()

        # layout
        layout.addWidget(magnitude_slider, 1, 1)
        layout.addWidget(phase_dial, 2, 1)
        layout.addWidget(graph, 1, 2, 2, 2)
        layout.addWidget(range_scroll_bar, 3, 2, 1, 2)

        # connections
        range_scroll_bar.valueChanged.connect(graph.update_horizontal_range)
        magnitude_slider.valueChanged.connect(graph.update_magnitude)
        phase_dial.valueChanged.connect(graph.update_phase)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()