Всіх вітаю! Сьогодні ми будемо створювати редактор електричних схем на Python. Наша програма не буде досконалою, але принаймні зможе будувати прості електричні схеми.
Як саме ця ідея до вас прийшла і як ви її уявляєте?
Дана ідея насправді не прийшла випадково, вона вже була на той момент, і залишалося лише обрати її. Крім цієї ідеї, ще було багато тем, але вони здалися занадто типовими, а звідси й нецікавими.
Спершу варто було витратити деякий час на обмірковування того, що саме ми хочемо побачити в результаті. Головною метою було створення програми для побудови елементарних електричних схем. Чому саме елементарних? Бо схеми, які будуть створюватися за допомогою цієї програми, міститимуть лише три компоненти: джерело напруги, конденсатор та резистор.
Загалом, якщо говорити більш конкретно у контексті електричних схем, то наша програма призначена для створення так званих RC-кіл.
RC-коло — це електрична схема, яка містить лише джерело живлення, резистор і конденсатор.
Наступним кроком ми продумали, як буде виглядати кожне вікно програми та який функціонал матиме. Тому розділимо цю частину питання на три підпункти (відповідно до кількості вікон у програмі).
Перше вікно
Перше вікно ми розглядали як умовне обличчя усієї програми, тому воно повинно мати вступний характер. Тож спершу ми визначилися з тим, що усі елементи у вікні будуть розташовуватися по центру. Далі ми продумали, які саме елементи інтерфейсу будуть у вікні та яке призначення матиме кожний елемент. На цьому моменті ми вже визначилися, що у першому вікні варто створити заголовок із назвою програми, два текстові поля, два випадаючі списки та кнопку.
Останнім кроком варто визначитися з тим, який функціонал буде мати кожен елемент. Отже, перше текстове поле повинно приймати назву файлу, у якому користувач хоче зберегти схему, а друге текстове поле — кількість компонентів, які буде містити майбутня схема. Кнопка, у свою чергу, просто дозволяє перейти до наступного вікна, де ми будемо створювати саму схему.
Друге вікно
Тут варто детальніше розібрати призначення другого текстового поля. Справа у тому, що наша схема буде створюватися таким чином: користувач спочатку вказує кількість компонентів схеми, а вже потім переходить до другого вікна, де з’явиться відповідна до кількості компонентів схеми кількість рядків. У цих рядках якраз і можна буде додавати компоненти схеми. Тому дане текстове поле є дуже важливим, бо від нього залежить схема, що вийде у результаті.
Якщо описувати ідею інтерфейсу вікна конкретно, то у нас буде якась кількість рядків із випадаючими списками, у яких ми будемо обирати тип компонента (джерело напруги, конденсатор або резистор), номінал компонента, розташування на схемі та колір.
Заключною частиною інтерфейсу є кнопки “Show circuit” та “Save circuit”. Перша кнопка показує наступне вікно, де ми можемо побачити схему, яку ми створили, а друга — зберігає схему у файл з назвою, яку ми вказали у текстовому полі першого вікна.
Третє вікно
Третє вікно буде містити найменшу кількість елементів. У даного вікна будуть заголовок, зображення готової схеми і кнопка. Це і все.
Усі елементи будуть розташовуватися по центру.
Головне, що треба знати про функціонал елементів цього вікна, — це те, що кнопка “Back to the editor” дозволяє у будь-який потрібний момент повернутися до редактора (другого вікна) та змінити схему, яка вийшла в результаті.
Які інструменти Ви використовували для реалізації даної програми?
Для реалізацї даної програми використовувалися бібліотеки flet, schemdraw, matplotlib.
Бібліотека flet використовувалася для створення сучасного та гарного інтерфейсу. Також варто сказати, що дана бібліотека є доволі легкою у використанні та має чудову документацію.
Встановлення flet
Windows:
pip install fletMacOs:
pip3 install fletLinux:
pip install fletБібліотека shemdraw має у собі багато функціоналу для креслення електричних схем. Завдяки ній можна креслити не тільки схеми з аналоговими компонентами, а й із цифровими. Крім того, весь процес креслення відбавується дуже легко та зручно.
Встановлення schemdraw
Windows:
pip install shemdrawMacOs:
pip3 install shemdrawLinux:
pip install shemdrawВзагалі-то, бібліотека matplotlib зазвичай використовується для створення графіків та діаграм, але у даному випадку замість графіків чи діаграм ми помістили накреслену схему.
Встановлення matplotlib
Windows:
pip install matplotlibMacOs:
pip3 install matplotlibLinux:
pip install matplotlibЯк створювався головний інтерфейс програми?
Створення деяких елементів у конструкторі класу
Спочатку ми створили деякі елементи інтерфейсу для головного вікна програми у конструкторі __init__.
Створюємо головний клас програми:
class CircuitsEditor:
"""
Головний клас, у якому є всі методи для успішного функціонування програми
"""У консткруторі додаємо усі необхідні елементи інтерфейсу першого вікна:
def __init__(self):
"""
Створюємо елементи для першого (головного) вікна
"""
self.main_title = ft.Text(value="Tesla", style=ft.TextStyle(font_family="Roboto Mono", size=35))
self.circuit_name_field = ft.TextField(hint_text="Circuit name",
width=320, text_align=ft.TextAlign.CENTER)
self.elements_number_field = ft.TextField(hint_text="Elements number",
width=320, text_align=ft.TextAlign.CENTER)
self.component_data = []
self.data_memory = []
self.circuit_background_dropdown = ft.Dropdown(width=320,
hint_text="Style",
options=[
ft.dropdown.Option("default"),
ft.dropdown.Option("dark"),
ft.dropdown.Option("solarizedd"),
ft.dropdown.Option("solarizedl"),
ft.dropdown.Option("onedork"),
ft.dropdown.Option("oceans16"),
ft.dropdown.Option("monokai"),
ft.dropdown.Option("gruvboxl"),
ft.dropdown.Option("grade3"),
ft.dropdown.Option("chesterish")
])
self.fonts_dropdown = ft.Dropdown(width=320,
hint_text="Font",
options=[
ft.dropdown.Option("serif"),
ft.dropdown.Option("Times New Roman"),
ft.dropdown.Option("Courier New")
])
self.windows_switch = ft.AnimatedSwitcher(content=ft.Column(),
transition=ft.AnimatedSwitcherTransition.FADE,
expand=True)Серед елементів інтерфейсу ми додали заголовок вікна:
self.main_title = ft.Text(value="Tesla", style=ft.TextStyle(font_family="Roboto Mono", size=35))Далі додали рядки для введення даних:
self.circuit_name_field = ft.TextField(hint_text="Circuit name",
width=320, text_align=ft.TextAlign.CENTER)
self.elements_number_field = ft.TextField(hint_text="Elements number",
width=320, text_align=ft.TextAlign.CENTER)Текстове поле circuit_name_field буде приймати назву файлу, у якому буде зберігатися створена схема, а поле elements_number_field — кількість компонентів, яку буде містити схема.
Створюємо необхідні списки та додаємо випадний список на головний інтерфейс нашої програми:
self.component_data = []
self.data_memory = []
self.circuit_background_dropdown = ft.Dropdown(width=320,
hint_text="Style",
options=[
ft.dropdown.Option("default"),
ft.dropdown.Option("dark"),
ft.dropdown.Option("solarizedd"),
ft.dropdown.Option("solarizedl"),
ft.dropdown.Option("onedork"),
ft.dropdown.Option("oceans16"),
ft.dropdown.Option("monokai"),
ft.dropdown.Option("gruvboxl"),
ft.dropdown.Option("grade3"),
ft.dropdown.Option("chesterish")
])Випадаючий список circuit_background_dropdown надає користувачу можливість обрати один із запропонованих стилів фону для схеми.
Тепер створюємо випадний список зі шрифтами, які будуть відображатися на схемі. Ми надаємо користувачу можливість самостійно обирати стиль шрифту, який він хоче. Також ми створюємо екземпляр об’єкта windows_switch. Необхідно детальніше описати суть windows_switch та навіщо він був створений. Справа в тому, що наша програма буде мати декілька вікон, а тому важливо зробити перехід між ними плавним та гарним. Отже, windows_switch надає можливість плавно переходити між вікнами.
Ось ця частина коду, яку ми щойно описали:
self.fonts_dropdown = ft.Dropdown(width=320,
hint_text="Font",
options=[
ft.dropdown.Option("serif"),
ft.dropdown.Option("Times New Roman"),
ft.dropdown.Option("Courier New")
])
self.windows_switch = ft.AnimatedSwitcher(content=ft.Column(),
transition=ft.AnimatedSwitcherTransition.FADE,
expand=True)Створення метода для головного вікна
Тепер варто перейти до наступного кроку — створити метод для головного вікна програми та додати попередні елементи до цього вікна.
Ось даний метод у коді:
def main_menu(self, page: ft.Page):
"""
Вікно головного вікна з налаштуваннями для майбутньої схеми
:param page: вікно прорами
:return: None
"""
page.title = "Tesla"
page.theme_mode = page.theme_mode.DARK
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.vertical_alignment = ft.MainAxisAlignment.CENTER
def start_work(event):
page.horizontal_alignment = ft.CrossAxisAlignment.START
page.vertical_alignment = ft.VerticalAlignment.START
self.windows_switch.content = self.circuit_builder(page)
page.add(self.windows_switch)
page.update()
start_work_button = ft.ElevatedButton("Start", on_click=start_work, width=200)
menu_settings = ft.Container(
ft.Column(controls=[self.main_title,
self.circuit_name_field,
self.elements_number_field,
self.circuit_background_dropdown,
self.fonts_dropdown,
start_work_button],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER,
expand=True)
)
self.windows_switch.content = menu_settings
page.add(self.windows_switch)
self.circuit_name_field.update()
self.circuit_name_field.focus()
self.elements_number_field.update()
self.elements_number_field.focus()Отже, для початку ми додали назву програми та встановили темний фон:
page.title = "Tesla"
page.theme_mode = page.theme_mode.DARKДалі відцентрували усі елементи у вікні по горизонталі та вертикалі:
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.vertical_alignment = ft.MainAxisAlignment.CENTERСтворили основну кнопку та функцію, яку ця кнопка буде виконувати:
def start_work(event):
page.horizontal_alignment = ft.CrossAxisAlignment.START
page.vertical_alignment = ft.VerticalAlignment.START
self.windows_switch.content = self.circuit_builder(page)
page.add(self.windows_switch)
page.update()
start_work_button = ft.ElevatedButton("Start", on_click=start_work, width=200)Потрібно розуміти, що рядки у функції start_work(event) застосовуються не до нашого головного вікна, а до наступного вікна, куди ми будемо переходити після натискання кнопки.
Отже, цими рядками ми розташовуємо елементи на новому вікні по лівому краю:
page.horizontal_alignment = ft.CrossAxisAlignment.START
page.vertical_alignment = ft.VerticalAlignment.STARTСаме в цей момент ми переходимо до нового вікна (нове вікно — це метод circuit_builder):
self.windows_switch.content = self.circuit_builder(page)
page.add(self.windows_switch)
page.update()Тобто ми переходимо до вікна, де якраз і буде здійснюватися створення схем.
Тут ми створили саму кнопку, до якої за допомогою параметра on_click прив’язали функцію start_work():
start_work_button = ft.ElevatedButton("Start", on_click=start_work, width=200)Спочатку організовуємо всі елементи інтерфейсу головного вікна у стовпець, а потім поміщаємо цей стовпець у контейнер:
menu_settings = ft.Container(
ft.Column(controls=[self.main_title,
self.circuit_name_field,
self.elements_number_field,
self.circuit_background_dropdown,
self.fonts_dropdown,
start_work_button],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER,
expand=True)
)Тепер додаємо усі ці елементи до нашого головного вікна, тобто додаємо готовий контент до основного вікна:
self.windows_switch.content = menu_settings
page.add(self.windows_switch)Далі оновлюємо елементи інтерфейсу завдяки функції update() та одразу фокусуємо курсор мишки за допомогою focus():
self.elements_number_field.update()
self.elements_number_field.focus()Як реалізовувалося вікно для створення електричних схем?
Для створення вікна з усіма інструментами для проєктування електричних схем створено метод circuit_builder().
Ось повний код даного метода:
def circuit_builder(self, page: ft.Page):
"""
Вікно з панеллю для створення електричних схем
:param page: вікно
:return: full_options
"""
page.title = "Tesla"
page.window_width = 800
page.theme_mode = page.theme_mode.DARK
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.vertical_alignment = ft.MainAxisAlignment.CENTER
options_list = []
elements_number_int = int(self.elements_number_field.value)
main_title = ft.Text(value="Circuit Builder",
style=ft.TextStyle(font_family="Roboto Mono", size=25))
title_container = ft.Container(content=main_title, margin=ft.margin.only(bottom=15),
alignment=ft.alignment.center)
options_list.append(title_container)
component_type = None
component_nominal = None
component_placement = None
component_colour = None
for i in range(elements_number_int):
if i < len(self.data_memory):
saved_data = self.data_memory[i]
component_type = saved_data["element"]
component_nominal = saved_data["nominal"]
component_placement = saved_data["placement"]
component_colour = saved_data["colour"]
element_dropdown = ft.Dropdown(width=150,
hint_text="Component",
value=component_type,
options=[
ft.dropdown.Option("Capacitor"),
ft.dropdown.Option("Resistor"),
ft.dropdown.Option("Source"),
ft.dropdown.Option("Line")
])
nominal_field = ft.TextField(hint_text="Value", value=component_nominal, width=150)
placement_dropdown = ft.Dropdown(width=150,
hint_text="Placement",
value=component_placement,
options=[
ft.dropdown.Option("Up"),
ft.dropdown.Option("Left"),
ft.dropdown.Option("Right"),
ft.dropdown.Option("Down")
])
colours_dropdown = ft.Dropdown(width=150,
hint_text="Colours",
value=component_colour,
options=[
ft.dropdown.Option("Red"),
ft.dropdown.Option("Blue"),
ft.dropdown.Option("Green")
])
component_data_row = {
"element": element_dropdown,
"nominal": nominal_field,
"placement": placement_dropdown,
"colour": colours_dropdown
}
self.component_data.append(component_data_row)
options_row = ft.Row(controls=[
element_dropdown,
nominal_field,
placement_dropdown,
colours_dropdown
], spacing=10, alignment=ft.MainAxisAlignment.CENTER)
options_list.append(options_row)
def show_circuit(event):
self.data_memory = []
schemdraw.theme(self.circuit_background_dropdown.value)
plt.use("Agg")
with draw.Drawing(font=self.fonts_dropdown.value, show=False) as schem:
for row in self.component_data:
component_type = row["element"].value
component_nominal = row["nominal"].value
component_place = row["placement"].value
component_colour = row["colour"].value
component_data_row = {
"element": component_type,
"nominal": component_nominal,
"placement": component_place,
"colour": component_colour
}
self.data_memory.append(component_data_row)
if component_type == "Source":
if component_place.lower() == "up":
component = elm.SourceV().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.SourceV().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.SourceV().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.SourceV().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Capacitor":
if component_place.lower() == "up":
component = elm.Capacitor().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Capacitor().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Capacitor().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Capacitor().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Resistor":
if component_place.lower() == "up":
component = elm.Resistor().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Resistor().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Resistor().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Resistor().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Line":
if component_place.lower() == "up":
component = elm.Line().up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Line().down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Line().left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Line().right()
component.color(component_colour.lower())
schem.add(component)
self.component_data = []
circuit_imagedata = schem.get_imagedata("png")
circuit_base = base64.b64encode(circuit_imagedata)
circuit_string = circuit_base.decode("utf-8")
circuit_image_element = ft.Image(src_base64=circuit_string,
width=300,
fit=ft.ImageFit.CONTAIN,
border_radius=10)
self.windows_switch.content = self.display_circuit(circuit_image=circuit_image_element, page=page)
self.windows_switch.update()
page.update()
def save_circuit(event):
self.data_memory = []
schemdraw.theme(self.circuit_background_dropdown.value)
plt.use("Agg")
with draw.Drawing(file=self.circuit_name_field.value,
font=self.fonts_dropdown.value, show=False) as schem:
for row in self.component_data:
component_type = row["element"].value
component_nominal = row["nominal"].value
component_place = row["placement"].value
component_colour = row["colour"].value
component_data_row = {
"element": row["element"].value,
"nominal": row["nominal"].value,
"placement": row["placement"].value,
"colour": row["colour"].value
}
self.data_memory.append(component_data_row)
if component_type == "Source":
if component_place.lower() == "up":
component = elm.SourceV().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.SourceV().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.SourceV().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.SourceV().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Capacitor":
if component_place.lower() == "up":
component = elm.Capacitor().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Capacitor().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Capacitor().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Capacitor().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Resistor":
if component_place.lower() == "up":
component = elm.Resistor().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Resistor().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Resistor().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Resistor().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Line":
if component_place.lower() == "up":
component = elm.Line().up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Line().down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Line().left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Line().right()
component.color(component_colour.lower())
schem.add(component)
schem.save(self.circuit_name_field.value)
create_circuit_button = ft.ElevatedButton("Show circuit", on_click=show_circuit)
save_circuit_button = ft.ElevatedButton("Save circuit", on_click=save_circuit)
builder_container = ft.Container(ft.Row([
create_circuit_button,
save_circuit_button],
spacing=10, alignment=ft.MainAxisAlignment.CENTER),
margin=ft.margin.only(top=10, bottom=10)
)
page.update()
options_list.append(builder_container)
full_options = ft.Column(controls=options_list, expand=True, scroll=ft.ScrollMode.AUTO)
return full_optionsСпершу знову вказали заголовок новому вікну:
page.title = "Tesla"Вказали точну ширину вікна за замовчуванням:
page.window_width = 800Встановили темний фон для вікна та відцентрували його елементи:
page.theme_mode = page.theme_mode.DARK
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.vertical_alignment = ft.MainAxisAlignment.CENTERСтворили список options_list, який по ходу виконання коду буде вміщувати у собі всі елементи у вікні:
options_list = []Тепер взяли кількість елементів, яку повинна буде містити схема. Це число користувач ввів ще на головному вікні, і воно є дуже важливим в рамках нашої програми, так як саме від нього залежить кількість рядків для створення компонентів:
elements_number_int = int(self.elements_number_field.value)Створили заголовок у вікні, помістили його у контейнер та додали до списку options_list:
main_title = ft.Text(value="Circuit Builder",
style=ft.TextStyle(font_family="Roboto Mono", size=25))
title_container = ft.Container(content=main_title, margin=ft.margin.only(bottom=15),
alignment=ft.alignment.center)
options_list.append(title_container)Далі створили групу змінних, які у подальшому будуть зберігати параметри компенетів схеми:
component_type = None
component_nominal = None
component_placement = None
component_colour = NoneДля кращого розуміння є таблиця, яка детальніше пояснює важливість кожної змінної.
Змінна | Що зберігає? |
|---|---|
| Тип компонента схеми (конденсатор, резистор, генератор). |
| Номінал компонента. |
| Позиція розташування елемента на схемі (праворуч, ліворуч, зверху та знизу). |
| Колір компонента. |
Тут ми створили цикл for, у якому перевіряємо, чи немає нічого у пам’яті нашої програми (if i < len(self.data_memory)), якщо немає, то наступні рядки ігноруються, але якщо ж у data_memory є збережені налаштування схеми, то ми відтворюємо ці налаштування за допомогою подальших рядків.
Тут можна чітко побачити, що кожна змінна відповідає за своє значення, як ми вже розписували у таблиці.
for i in range(elements_number_int):
if i < len(self.data_memory):
saved_data = self.data_memory[i]
component_type = saved_data["element"]
component_nominal = saved_data["nominal"]
component_placement = saved_data["placement"]
component_colour = saved_data["colour"]Далі додали випадні списоки для створення компонентів (назва, номінал, розташування, колір):
element_dropdown = ft.Dropdown(width=150,
hint_text="Component",
value=component_type,
options=[
ft.dropdown.Option("Capacitor"),
ft.dropdown.Option("Resistor"),
ft.dropdown.Option("Source"),
ft.dropdown.Option("Line")
])
nominal_field = ft.TextField(hint_text="Value", value=component_nominal, width=150)
placement_dropdown = ft.Dropdown(width=150,
hint_text="Placement",
value=component_placement,
options=[
ft.dropdown.Option("Up"),
ft.dropdown.Option("Left"),
ft.dropdown.Option("Right"),
ft.dropdown.Option("Down")
])
colours_dropdown = ft.Dropdown(width=150,
hint_text="Colours",
value=component_colour,
options=[
ft.dropdown.Option("Red"),
ft.dropdown.Option("Blue"),
ft.dropdown.Option("Green")
])Важливим моментом у цьому коді є те, що у параметрі value ми вказали змінні попередніх налаштувань (якщо у списку data_memory нічого не було, то випадаючі списки будуть порожніми).
Також ви можете помітити, що, окрім трьох попередніх компонентів, можна буде також додати й лінію (Line), але ми не вважаємо її повноцінним компонентом схеми.
Тут ми створили словник component_data_row, куди записали всі параметри наших компонентів, тобто тепер кожний випадаючий список відповідає його суті. Після цього ми додали цей словник до списку component_data.
Ось цей код нижче:
component_data_row = {
"element": element_dropdown,
"nominal": nominal_field,
"placement": placement_dropdown,
"colour": colours_dropdown
}
self.component_data.append(component_data_row)Далі поміщаємо усі випадаючі списки у вікні в один ряд, який, до того ж, відцентровуємо:
options_row = ft.Row(controls=[
element_dropdown,
nominal_field,
placement_dropdown,
colours_dropdown
], spacing=10, alignment=ft.MainAxisAlignment.CENTER)
options_list.append(options_row)Тепер перейдемо до розбору функції show_circuit(event), де безпосередньо відбувається процес створення схеми за попередніми вказівками користувача.
Спершу ми знову оголосили тут список data_memory, який відповідає за пам’ять програми:
self.data_memory = []Вказали фон, на якому буде наша схема (фон залежить від вподобання користувача):
schemdraw.theme(self.circuit_background_dropdown.value)Далі нам треба десь відображати нашу майбутню схему, а тому ми використовуємо цей рядок для того, щоб створити зображення схеми, при цьому не показуючи його одразу.
У підсумку зображення буде збережене у пам’яті, і ми зможемо показати його тоді, коли саме нам це буде потрібно.
Саме завдяки цьому рядку ми виконуємо дане завдання:
plt.use("Agg")Наступним кроком ми почали створювати схему та взяли усі значення за ключами словника, який знаходився у списку component_data. Варто розуміти, що першочергово значеннями у словнику виступали саме елементи інтерфейсу (випадаючі списки), а тепер ми конвертуємо їх у значення, які користувач для них обрав. Ці вихідні значення одразу записуються у нові змінні.
Надалі ці вихідні значення випадаючих списків будуть використовуватися для присвоєння компонентам схеми типу, номіналу, розташування та кольору.
Ось частина коду, яку ми тільки-но описали:
with draw.Drawing(font=self.fonts_dropdown.value, show=False) as schem:
for row in self.component_data:
component_type = row["element"].value
component_nominal = row["nominal"].value
component_place = row["placement"].value
component_colour = row["colour"].valueДалі нові змінні із вхідними значеннями були записані у словник та додаємо його у пам’ять програми (data_memory). Саме завдяки цьому наша програма запам’ятовує опції, які ми обрали для кожного випадаючого списку:
component_data_row = {
"element": component_type,
"nominal": component_nominal,
"placement": component_place,
"colour": component_colour
}
self.data_memory.append(component_data_row)Маючи усі необхідні вихідні значення завдяки новим змінним, ми нарешті створюємо схему.
Спершу за допомогою змінній component_type ми визначаємо тип компонента, який обрав користувач, через умови if component_type == "[Тип компонента]":.
Далі за допомогою змінної component_place ми визначаємо розташування компонента, обране користувачем, через умови if component_place.lower() == "[розташування компонента]":.
Тут варто звернути увагу, що ми записуємо розташування компонента нижнім регістром, бо саме так це записується у бібліотеці shemdraw.
Ось даний код:
if component_type == "Source":
if component_place.lower() == "up":
component = elm.SourceV().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.SourceV().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.SourceV().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.SourceV().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Capacitor":
if component_place.lower() == "up":
component = elm.Capacitor().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Capacitor().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Capacitor().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Capacitor().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Resistor":
if component_place.lower() == "up":
component = elm.Resistor().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Resistor().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Resistor().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Resistor().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Line":
if component_place.lower() == "up":
component = elm.Line().up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Line().down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Line().left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Line().right()
component.color(component_colour.lower())
schem.add(component)Якщо користувач, наприклад, обрав конденсатор, який повинен розташовуватися зверху, то цим рядком ми сторюємо даний компонент та розташовуємо його зверху через метод up():
component = elm.Capacitor().label(component_nominal).up()А ось таким чином ми задаємо колір компонета, обраний користувачем (зауважте, що назву кольору також треба записувати з маленької літери):
component.color(component_colour.lower())Останнім кроком є додавання вже готового компонета до схеми:
schem.add(component)Знову оголошуємо список component_data:
self.component_data = []Декодували зображення зі схемою:
circuit_imagedata = schem.get_imagedata("png")
circuit_base = base64.b64encode(circuit_imagedata)
circuit_string = circuit_base.decode("utf-8")Важливо розуміти, що circuit_string не зберігає в собі зображення у тому представленні, в якому ми можемо подумати. Саме тому варто створити клас Image для того, щоб відобразити зображення саме як зображення.
З цією метою ми й створили екзампляр об’єкта circuit_image_element:
circuit_image_element = ft.Image(src_base64=circuit_string,
width=300,
fit=ft.ImageFit.CONTAIN,
border_radius=10)Таким чином, ми наче створили холст для нашого зображення та вказали для нього кілька налаштувань. У таблиці внизу це розібрано детальніше.
Параметр | Значення |
|---|---|
| Приймає декодоване зображення. |
| Ширина зображення. |
| Зручно вміщає зображення у холст. |
| Заокруглення кутів холста. |
І фінальним кроком є відображення нашої схеми у новому вікні. Для цього ми переходимо від вікна побудови схеми до останнього вікна, де ми виводимо зображення готової схеми:
self.windows_switch.content = self.display_circuit(circuit_image=circuit_image_element, page=page)
self.windows_switch.update()
page.update()Через те, що у вікні побудови схеми є дві кнопки, відповідно, треба було створити ще одну функцію для кнопки збереження схеми. Тому ми створили функцію save_circuit(event), яка б дозволяла зберігати зображення схеми у форматі PNG.
Ось повний код даної функції:
def save_circuit(event):
self.data_memory = []
schemdraw.theme(self.circuit_background_dropdown.value)
plt.use("Agg")
with draw.Drawing(file=self.circuit_name_field.value,
font=self.fonts_dropdown.value, show=False) as schem:
for row in self.component_data:
component_type = row["element"].value
component_nominal = row["nominal"].value
component_place = row["placement"].value
component_colour = row["colour"].value
component_data_row = {
"element": row["element"].value,
"nominal": row["nominal"].value,
"placement": row["placement"].value,
"colour": row["colour"].value
}
self.data_memory.append(component_data_row)
if component_type == "Source":
if component_place.lower() == "up":
component = elm.SourceV().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.SourceV().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.SourceV().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.SourceV().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Capacitor":
if component_place.lower() == "up":
component = elm.Capacitor().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Capacitor().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Capacitor().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Capacitor().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Resistor":
if component_place.lower() == "up":
component = elm.Resistor().label(component_nominal).up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Resistor().label(component_nominal).down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Resistor().label(component_nominal).left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Resistor().label(component_nominal).right()
component.color(component_colour.lower())
schem.add(component)
elif component_type == "Line":
if component_place.lower() == "up":
component = elm.Line().up()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "down":
component = elm.Line().down()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "left":
component = elm.Line().left()
component.color(component_colour.lower())
schem.add(component)
if component_place.lower() == "right":
component = elm.Line().right()
component.color(component_colour.lower())
schem.add(component)
schem.save(self.circuit_name_field.value)
create_circuit_button = ft.ElevatedButton("Show circuit", on_click=show_circuit)
save_circuit_button = ft.ElevatedButton("Save circuit", on_click=save_circuit)
builder_container = ft.Container(ft.Row([
create_circuit_button,
save_circuit_button],
spacing=10, alignment=ft.MainAxisAlignment.CENTER),
margin=ft.margin.only(top=10, bottom=10)
)
page.update()
options_list.append(builder_container)
full_options = ft.Column(controls=options_list, expand=True, scroll=ft.ScrollMode.AUTO)
return full_optionsНасправді більшість коду у функції save_circuit(event) прямо скопійовано з попередньої функції, а тому деяку частину коду ми пропустимо.
По суті, всередині даної функції єдина річ, яка повинна бути цікавою нам, — це збереження схеми у якості PNG-зображення. Це здійснюється за допомогою настпуного рядка:
schem.save(self.circuit_name_field.value)Варто нагадати, що circuit_name_field — це поле, яке ми створили ще на головному вікні програми. От тепер настав час отримати значення, яке користувач ввів у це поле, щоб використати його як назву файлу.
Також варто розуміти, що наступні рядки коду, які ми будемо розбирати, не відносяться до функції save_circuit(event). Вони входять у метод circuit_builder().
Отже, створюємо кнопки “Show circuit” та “Save circuit”:
create_circuit_button = ft.ElevatedButton("Show circuit", on_click=show_circuit)
save_circuit_button = ft.ElevatedButton("Save circuit", on_click=save_circuit)Далі вкладаємо ці кнопки у контейнер, якому прописуємо кілька додаткових налаштувань щодо відображення цих елементів інтерфейсу:
builder_container = ft.Container(ft.Row([
create_circuit_button,
save_circuit_button],
spacing=10, alignment=ft.MainAxisAlignment.CENTER),
margin=ft.margin.only(top=10, bottom=10)
)
page.update()Потім нарешті додали новостворений контейнер у список options_list, щоб у наступному рядку додати ці елементи в стовпець:
options_list.append(builder_container)
full_options = ft.Column(controls=options_list, expand=True, scroll=ft.ScrollMode.AUTO)
return full_optionsТут було б доречно звернути увагу на параметр scroll. Даний параметр дозволяє встановити можливість переміщатися вгору та вниз по вікну. Даний аспект є важливим для вікна побудови схем, бо дозволяє нам зручно працювати у програмі тоді, коли схема повинна містити дуже багато компонентів. Варто розуміти, що у нашій програмі кількість рядків з випадаючими списками напряму залежить від кількості компонентів схеми, яку користувач збирається розробляти.
Тож, scroll=ft.ScrollMode.AUTO автоматично встановлює скрол, коли кількість рядків з випадаючими списками не вміщається у вікно.
Яким чином було створено відображення схеми після її створення?
Попередньо ми вже декодували зображення та створили для нього полотно, але це ще не все. Задля відображення схеми ми створили третє вікно, яке б показувало саме зображення і при цьому мало мінімальний інтерфейс.
Тож, під цю задачу було створено функцію display_circuit(), яка приймає у якості параметра декодоване зображення схеми та показує його у новому вікні.
Ось повний код даної функції:
def display_circuit(self, page: ft.Page, circuit_image):
"""
Вікно з готовою електричною схемою
:param page: вікно
:param circuit_image: зображення схеми
:return: display_column
"""
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.theme_mode = page.theme_mode.DARK
circuit_title = ft.Text(value="Circuit",
style=ft.TextStyle(font_family="Roboto Mono", size=35))
def back_to_editor(event):
self.windows_switch.content = self.circuit_builder(page)
page.add(self.windows_switch)
page.update()
back_editor_button = ft.ElevatedButton("Back to the editor", on_click=back_to_editor)
buttons_container = ft.Container(
ft.Row([
back_editor_button],
spacing=10,
alignment=ft.MainAxisAlignment.CENTER),
margin=ft.margin.only(top=10, bottom=10)
)
display_column = ft.Column([circuit_title, circuit_image, buttons_container],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
scroll=ft.ScrollMode.AUTO
)
return display_columnВідцентровали елементи та встановили темний фон:
page.vertical_alignment = ft.MainAxisAlignment.CENTER
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.theme_mode = page.theme_mode.DARKСтворили заголовок для вікна:
circuit_title = ft.Text(value="Circuit",
style=ft.TextStyle(font_family="Roboto Mono", size=35))Далі створили кнопку “Back to the editor“, яка б дозволяла повертатися до вікна побудови схем кожен раз, щоб зробити зміни у схемі. У функції back_to_editor() ми повертаємося назад до попереднього вікна.
Ось функція та кнопка:
def back_to_editor(event):
self.windows_switch.content = self.circuit_builder(page)
page.add(self.windows_switch)
page.update()
back_editor_button = ft.ElevatedButton("Back to the editor", on_click=back_to_editor)Поміщаємо кнопку спочатку у ряд, а потім і ряд у контейнер:
buttons_container = ft.Container(
ft.Row([
back_editor_button],
spacing=10,
alignment=ft.MainAxisAlignment.CENTER),
margin=ft.margin.only(top=10, bottom=10)
)У якості фінального кроку ми додаємо усі елементи інтерфейсу вікна у стовпець та повертаємо цей стовпець:
display_column = ft.Column([circuit_title, circuit_image, buttons_container],
alignment=ft.MainAxisAlignment.CENTER,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
scroll=ft.ScrollMode.AUTO
)
return display_columnУ параметрах стовпця ми також відцентрували усі елементи та додали скрол.
Як програма запускається у коді?
Так як ми створювали інтерфейс програми на flet, то і запускати код потрібно за правилами даної бібліотеки — за допомогою функції app(). Проте слід також пам’ятати, що так як ми створили програму за принципами об’єктно-орієнтованого програмування (ООП), то потрібно спочатку створити об’єкт класу, а вже потім викликати методи.
Отже, створюємо функцію run(), в якій і буде запускатися програма:
def run(page: ft.Page):
"""
Створюємо об'єкт класу та викликаємо функцію main_menu(page)
:param page:
:return:
"""
application = CircuitsEditor()
application.main_menu(page)
ft.app(target=run)А вже після цього вказуємо функцію run() як таку, яка буде запускати всю програму.
Що вийшло у результаті? Як виглядає процес від запуску до створення простої схеми у новоствореній програмі?
Головне вікно програми:

Тут ми обираємо назву файлу (якщо хочете згодом зберегти схему), кількість компонентів схеми, фон, на якому буде відображатися схема, та шрифт, яким будуть написані номінали компонентів.
Вікно побудови схем:

Тут обираємо тип компонентів, номінали, розташування на схемі та кольори. Далі натискаємо кнопку “Show circuit”. Тільки от варто зауважити, що для лінії випадаючий список із номіналом не має сенсу, тому цю опцію ми не чіпаємо.
Ось і готова схема:

Тепер ми можемо повернутися назад до другого вікна і зберегти поточну схему:

Тепер спробуємо відкрити:

Ось і весь процес роботи з нашою програмою. Як бачимо, вона коректно працює і виконує своє безпосереднє призначення.
Що ви можете сказати про дану програму у підсумку?
У підсумку ми створили цілком працездатну програму для створення RC-кіл, яка має гарний інтерфейс та може зберігати готові зображення схем у якості файлів. Ми успішно поєднали декілька інструментів та, що найголовніше, чітко розуміли, чому саме вони були нам потрібні.
Насправді, у цю програму буде нескладно додати більше компонентів у подальшому, якщо буде бажання. Єдине — легко буде додати тільки ті компоненти, що мають рівно один вхід і один вихід. Якщо компонент буде мати декілька входів або виходів, то доведеться змінювати інтерфейс креслення електричних схем під нього. Але це вже лише ідеї, бо першочергово наша програма повинна була створювати саме RC-кола, з чим вона чудово впоралася.
Також, можливо, не всім буде зручно спочатку вказувати кількість компонентів схеми, а потім вже безпосередньо реалізовувати саму схему, але з часом до цього можна звикнути. Після деякого часу тестування даної програми, ви вже будете сприймати створення схем як умовну кількість кроків, які вам треба зробити, щоб прийти від точки А до точки Б.