Время прочтения: 12 мин.

Часто ли вы задумываетесь над визуальной составляющей, когда работаете над каким-либо учебным проектом /курсовой/ расчетной работой? Здесь обычно более важной деталью является факт сдачи в срок, далее идет правильность выполнения, а на проработку подачи и визуализации результатов времени, как правило, совершенно не остается.

Поэтому я предлагаю рассмотреть одну лабораторную работу по численным методам, реализованную на языке программирования Python, которая долгое время лежала у меня «в столе» и довольно давно появилось желание проработать ее визуально и сделать из этого простенького проекта что-то более привлекательное для пользователя. Также в процессе ознакомлюсь и сравню различные возможности графических библиотек – Tkinter и PyQt5.

Суть задачи

У меня нет цели подробно рассказывать про математическую часть данной работы, поэтому приведу лишь краткое описание задачи лабораторной работы и того в каком виде она находится прямо сейчас.

Название лабораторной работы: «Методы решения нелинейных скалярных уравнений».  Соответственно существуют уравнения специального вида , где x – действительное число,

– нелинейная функция. А решить такое уравнение – означает найти такое x, при котором функция обратится в 0 (если говорить о графическом решении – найти точку пересечения графика функции с осью абсцисс). Для решения данной задачи существует несколько методов, я рассмотрю только 2: метод половинного деления и метод секущих. Чтобы сравнить какой метод выгоднее использовать буду ориентироваться на такие метрики, как количество итераций и невязка, которые отвечают за быстроту и точность вычислений соответственно.

Результат решения задания в рамках консольного приложения С тем как выглядит код на данный момент можно ознакомиться в репозитории в файле Console Application.py.

Результат работы кода

Сначала в отдельном окне изображается график исследуемой функции, затем его требуется закрыть для дальнейшей работы с консолью.

Вычисления корней в консоли производятся по мере ввода начальных значений:

Стоит сразу заметить то, что как видно из графиков у данной функции 3 корня и прежде чем закрыть график придется выбрать какой из них искать. После этого выбора я задавала начальные приближения и границы интервалов методов, что вызывало значительные неудобства, ведь даже для визуальной оценки правильности решения гораздо удобнее видеть график перед глазами и сравнивать результаты вычислений с ним, однако консольное решение таких возможностей не дает. Также в случае опечатки при вводе значений вся программа должна быть запущена с самого начала, что также вызывает массу трудностей и затрат времени.

В связи со всеми этими минусами и неудобствами у меня и возникла навязчивая идея переделать этот код в удобоваримое представление в виде графического интерфейса.

Графическое приложение с использованием Tkinter

Я начала разработку графического приложения с поиска информации о библиотеках, выборе наиболее подходящей и т.д. Для решения таких задач применяются различные библиотеки, но чаще всего можно встретить Tkinter и PyQt. Tkinter более старая и примитивная графическая библиотека, которая обладает всем минимально необходимым функционалом, однако написать с её помощью что-то визуально привлекательное и похожее на современное приложение будет достаточно затруднительно. В то время как PyQt является набором расширений графического фреймворка Qt для Python, что говорит о значительно больших возможностях функционала данной библиотеки.

Начну со случая реализации в рамках библиотеки Tkinter. Здесь пришлось работать исключительно в среде разработки Python и проверять расположение виджетов в окне прогоняя каждый раз код, но обо всем по порядку.

Естественно перед началом проектирования, я мысленно представляла, как именно должно выглядеть окно приложения, поэтому начну с создания главного экрана и размещения в нем полей для ввода функции и промежутков ее построения, а также сопроводительного текста для каждого из полей:

window = Tk()  		# инициализируем главное окно приложения
window.geometry('1000x1000') 		# задаем размеры главного окна приложения
window.title("Методы решения нелинейных скалярных уравнений")
frameUp = Frame(window, relief=GROOVE, height=150) 		# инициализируем структуру для расположения элементов в окне
frameUp.pack(side=TOP, fill=X) 		# фиксируем расположение структуры
Label(frameUp, text="Введите исследуемую функцию:").place(x=2, y=4, width=200, height=25) 		# инициализируем подпись для поля ввода функции
Label(frameUp, text="Начало интервала a:").place(x=260, y=4, width=140, height=25)   		# инициализируем подпись для поля ввода a 
Label(frameUp, text="Конец интервала b:").place(x=420, y=4, width=140, height=25)  		 # инициализируем подпись для поля ввода b
entry = Entry(frameUp, relief=RIDGE, borderwidth=4) 		# ввод значения функции
entry.bind("<Return>", evaluate)  		# передача значения уравнения в функцию построения графика
entry.place(x=6, y=30, width=250, height=25) 		# расположение поля ввода уравнения
strA = StringVar() 			# инициализация поля ввода значения A
strA.set(1)				# значение a по умолчанию	
entryA = Entry(frameUp, relief=RIDGE, borderwidth=4, textvariable=strA) # ввод значения A 
entryA.place(x=280, y=30, width=80, height=25) # расположение поля ввода A
entryA.bind("<Return>", evaluate) # передача значения A в функцию построения графика
strB = StringVar()  		# инициализация поля ввода значения B
strB.set(3)			# значение b по умолчанию
entryB = Entry(frameUp, relief=RIDGE, borderwidth=4, textvariable=strB) # ввод значения B 
entryB.place(x=450, y=30, width=80, height=25) # расположение поля ввода B
entryB.bind("<Return>", evaluate)  		# передача значения B в функцию построения графика

Результат выполнения фрагмента кода:

После этого в главное окно я добавила поле для изображения графика функции и кнопку для его построения

fig = Figure(figsize=(7, 5), facecolor='white')
ax = fig.add_subplot(111)
canvasAgg = FigureCanvasTkAgg(fig, master=window)
canvasAgg.draw()
canvas = canvasAgg.get_tk_widget()
canvas.pack(expand=False, anchor=N)
btn = Button(window, text='Построить график')
btn.bind("<Button-1>", evaluate2)
btn.pack(ipady=2, pady=4, padx=10)

Реализация инициализации функции для корректного построения графиков, которую я добавила в начало кода, представлена в репозитории в файле interface_with_Tkinter.py.

Результат выполнения фрагмента кода:

После ввода функции и нажатия на кнопку построения:

Также программа корректно обрабатывает изменения границ интервалов или значения уравнения и перестраивает график после повторного нажатия на кнопку построения.

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

Также в начале программы инициализируются функции методов нахождения корней уравнения, реализация которых представлена в файле interface_with_Tkinter.py, но оптимизирована для корректной передачи значений в соответствующие поля ввода и вывода, кроме того, я инициализировала функции для очистки полей.

Результат выполнения представленного кода:

Главное окно после передачи всех значений, построения графика и подсчета корней:

Некоторые особенности, которые я заметила в процессе написания этого кода:

  • Сложно было найти информацию по совместному использованию библиотек Tkiter и Matplotlib, чтобы построение графика производилось в главном окне,
  • Очень много времени уходит на подгон расположения элементов в главном окне и постоянную проверку корректности отображения и обработки данных,
  • Сложность с передачей в функции вычисления строки уравнения, которое бы использовалось по умолчанию.

Конечно на данном этапе интерфейс все еще выглядит не идеально, однако максимум, что можно сделать с ним, это подобрать более корректное расположение элементов и по желанию выбрать цветовую палитру, отличную от используемой по умолчанию. Однако, внешне этот интерфейс все равно продолжит выглядеть слишком примитивно и топорно, что естественно не повлияет на работоспособность и корректность решаемой задачи, но является значительным в вопросе визуальной составляющей. А так как тема визуального представления приложения для пользователя выбрана мной ведущей в этой публикации, то стоит рассмотреть еще один вариант реализации задачи, который обещает более широкий спектр возможностей для проектирования.

Графическое приложение с использованием PyQt5

Начиная работу с библиотекой PyQt5 на большинстве сайтов и форумов, связанных с этой темой, я постоянно натыкалась на советы использования дополнительной IDE для разработки, в которой стоит спроектировать весь внешний вид главного окна приложения – Qt Creator (Qt Designer), а основной функционал после форматирования файла можно будет отдельно прописать в привычном IDE для Python. Учитывая мой опыт работы с Tkinter и ужасно неудобного процесса постоянного отслеживания внешнего вида главного окна приложения, я решила, что не стоит пренебрегать данным советом и лучше будет воспользоваться наглядным средством для проектирования всех визуальных элементов рассматриваемой задачи.

Работа с Qt Creator выглядит следующим образом:

Здесь с левой стороны расположены все доступные элементы для создания интерфейса: кнопки, поля, виджеты, контейнеры и многое другое. В центре видно главное окно приложения, однако можно также создавать отдельные виджеты, всплывающие окна в центральном поле данного IDE, с правой стороны представлен контролер объектов и редактор свойств. В первом отображаются все добавленные в главное окно элементы, а во втором можно настроить уникальные стили для каждой детали интерфейса. Итак, вот какое окно получилось в итоге у меня:

Процесс работы с Qt Designer становится интуитивно понятен во время проектирования задачи. Все необходимые виджеты можно спокойно переместить в главное окно, настроить их расположение с помощью Layouts и т.д. Все что касается настройки вида (задание цвета, шрифта текста, заднего фона, градиентов и т.д.) отдельно выбранного фрагмента осуществляется с помощью Change stylesheet после нажатия правой кнопки мыши по интересующему элементу. Qt Designer сохраняет созданный шаблон интерфейса в формате .ui, а для добавления функционала нашему окну в сгенерированном файле кода я дописывала все нужные функции уже в среде обработки Python. Поэтому файл формата .ui я перевела в стандартный формат .py. Это осуществляется с помощью данной команды в терминале:

python –m PyQt5.uic.pyuic –x window_template.ui –o interface_final.py 

Предварительно я импортировала PyQt5 и pyqt5-tools:

pip install PyQt5
pip install pyqt5-tools

После этого в указанной в терминале директории создастся файл .py, который я добавила в проект на Python и продолжила работу над функционалом там.

С тем, как выглядит преобразованный файл в проекте PyCharm можно ознакомиться в репозитории в файле my_interface_from_QtDesigner.py.

Добавление значения уравнения по умолчанию:

self.lineEdit.setText("x ** 3 - 5.9 * x ** 2 + 11.1 * x - 6.7")

А также границы интервала с которых начиналось исследование:

self.lineEdit_2.setText("1")
self.lineEdit_3.setText("3")

Отработка отклика при нажатии на кнопки осуществляется в функции retranslateUi:

self.pushButton_5.clicked.connect(self.half_div)
self.pushButton.clicked.connect(self.clearHaldDivM)
self.pushButton_4.clicked.connect(self.secant)
self.pushButton_6.clicked.connect(self.clearSecant)

В скобках указаны функции, обрабатывающие события нажатия.

Данные функции инициализируются в том же классе, что и функция retranslateUi. С кодом можно ознакомиться в файле interface_with_PyQt5.py

Кроме того, в рамках рассматриваемой задачи я реализовала изображение графика функции внутри главного окна приложения.  Этого можно добиться с помощью добавления классов MplCanvas и TEST:

class MplCanvas(FigureCanvas):      # класс для построения графика в окне интерфейса
    def __init__(self, *args, **kwargs):
        self.fig = Figure()
        super(MplCanvas, self).__init__(self.fig, *args, **kwargs)

    def plot(self, x, y):       # функция для отрисовки графика
        self.fig.clear()        # очистка поля для построения
        self.ax = self.fig.add_subplot(111)
        self.ax.plot(x, y, color="darkmagenta", label="y(x)", linewidth=2)      # оформление графика
        self.ax.set_xlabel("x")
        self.ax.set_ylabel("f(x)")
        self.ax.legend()
        self.ax.grid(color='b', alpha=0.5, linestyle='dashed', linewidth=0.5)
        self.draw()


class TEST(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(TEST, self).__init__()
        self.setupUi(self)
        self.pushButton_2.clicked.connect(self.plot_graph)      # обработка нажатия на кнопку построения графика
        self.canavas = MplCanvas()
        self.canavas.setMinimumSize(300, 300)
        self.toolbar = NavigationToolbar(self.canavas, self)
        self.verticalLayout.addWidget(self.canavas)
        self.verticalLayout.addWidget(self.toolbar)
        self.toolbar.hide()
        self.setStyleSheet('.QWidget {background-image: url(v1.jpg);}')

    def plot_graph(self):           # функция для построения графика
        a = float(self.lineEdit_2.text())           # считывание начальных значений
        b = float(self.lineEdit_3.text())
        mystr = self.lineEdit.text()        # передача исследуемого уравнения в виде строки
        exec('f = lambda x:' + mystr, globals())        # преобразования уравнения из вида строки в функцию f(x)
        X = np.linspace(a, b, 300)
        Y = [f(x) for x in X]
        self.canavas.plot(X, Y)

Таким образом, выполнение данного кода выглядит следующим образом:

Результат обработки после построения графика, задания всех начальных приближений и подсчета корней:

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

Некоторые особенности, которые я заметила в процессе написания этого кода:

  • Сложность встраивания графика в главное окно, далеко не сразу удается найти нужный пример совместного использования PyQt5 и Matplotlib,
  • Очень много информации необходимо обработать для понимания и освоения синтаксиса и принципов PyQt,
  • Из-за усложнения кода возрастает число ошибок в процессе его написания, а время на поиск решения проблемы значительно увеличивается, иногда даже тяжело сформулировать что именно некорректно работает,
  • Сложность во взаимодействии функций между различными классами, всегда приходилось помнить где и какие значения заданы и как их передавать, где правильнее инициализировать ту или иную функцию.

Конечно данный интерфейс выглядит далеко не идеально, можно до бесконечности редактировать шрифты, расположение виджетов, добавление иконок к кнопкам, также отображение графика, однако на данном этапе я считаю свою задачу выполненной. А именно, я на конкретном примере познакомилась с возможностями библиотеки PyQt5, ее совместным использованием с другими библиотеками Python, поработала в Qt Designer.

Подведение итогов

В заключении, хочется сказать, что, пожалуй, самым худшим решением будет реализовывать задачу через консольное приложение из-за всех перечисленных выше недостатков. Что же касается библиотек Python для GUI, и Tkinter, и PyQt, то они полезны для разработки приемлемых графических интерфейсов, но в то же время они различаются с точки зрения адаптивности и функциональности. В большинстве ситуаций лучшим решением является использование PyQt, учитывая преимущества и недостатки как PyQt, так и Tkinter. Поэтому, если у вас есть время и возможность освоить PyQt5, то лучше не начинать с использования Tkinter. Tkinter хорош и подходит для простых задач, но из-за устаревшего вида его дизайна стоит отдавать предпочтение PyQt, который предлагает более общие возможности с точки зрения виджетов и внешнего вида.

Финальные коды программ с использованием библиотек Tkinter и PyQt5 можно посмотреть на GitHub в файлах interface_with_Tkinter.py и interface_with_PyQt5.py соответственно.