Друкарня від WE.UA

Створення редактора елементарних електричних схем

Зміст

Всіх вітаю! Сьогодні ми будемо створювати редактор електричних схем на Python. Наша програма не буде досконалою, але принаймні зможе будувати прості електричні схеми.

Як саме ця ідея до вас прийшла і як ви її уявляєте?

Дана ідея насправді не прийшла випадково, вона вже була на той момент, і залишалося лише обрати її. Крім цієї ідеї, ще було багато тем, але вони здалися занадто типовими, а звідси й нецікавими.

Спершу варто було витратити деякий час на обмірковування того, що саме ми хочемо побачити в результаті. Головною метою було створення програми для побудови елементарних електричних схем. Чому саме елементарних? Бо схеми, які будуть створюватися за допомогою цієї програми, міститимуть лише три компоненти: джерело напруги, конденсатор та резистор.

Загалом, якщо говорити більш конкретно у контексті електричних схем, то наша програма призначена для створення так званих RC-кіл.
RC-коло — це електрична схема, яка містить лише джерело живлення, резистор і конденсатор.

Наступним кроком ми продумали, як буде виглядати кожне вікно програми та який функціонал матиме. Тому розділимо цю частину питання на три підпункти (відповідно до кількості вікон у програмі).

Перше вікно

Перше вікно ми розглядали як умовне обличчя усієї програми, тому воно повинно мати вступний характер. Тож спершу ми визначилися з тим, що усі елементи у вікні будуть розташовуватися по центру. Далі ми продумали, які саме елементи інтерфейсу будуть у вікні та яке призначення матиме кожний елемент. На цьому моменті ми вже визначилися, що у першому вікні варто створити заголовок із назвою програми, два текстові поля, два випадаючі списки та кнопку.

Останнім кроком варто визначитися з тим, який функціонал буде мати кожен елемент. Отже, перше текстове поле повинно приймати назву файлу, у якому користувач хоче зберегти схему, а друге текстове поле — кількість компонентів, які буде містити майбутня схема. Кнопка, у свою чергу, просто дозволяє перейти до наступного вікна, де ми будемо створювати саму схему.

Друге вікно

Тут варто детальніше розібрати призначення другого текстового поля. Справа у тому, що наша схема буде створюватися таким чином: користувач спочатку вказує кількість компонентів схеми, а вже потім переходить до другого вікна, де з’явиться відповідна до кількості компонентів схеми кількість рядків. У цих рядках якраз і можна буде додавати компоненти схеми. Тому дане текстове поле є дуже важливим, бо від нього залежить схема, що вийде у результаті.

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

Заключною частиною інтерфейсу є кнопки “Show circuit” та “Save circuit”. Перша кнопка показує наступне вікно, де ми можемо побачити схему, яку ми створили, а друга — зберігає схему у файл з назвою, яку ми вказали у текстовому полі першого вікна.

Третє вікно

Третє вікно буде містити найменшу кількість елементів. У даного вікна будуть заголовок, зображення готової схеми і кнопка. Це і все.
Усі елементи будуть розташовуватися по центру.

Головне, що треба знати про функціонал елементів цього вікна, — це те, що кнопка “Back to the editor” дозволяє у будь-який потрібний момент повернутися до редактора (другого вікна) та змінити схему, яка вийшла в результаті.

Які інструменти Ви використовували для реалізації даної програми?

Для реалізацї даної програми використовувалися бібліотеки flet, schemdraw, matplotlib.

Бібліотека flet використовувалася для створення сучасного та гарного інтерфейсу. Також варто сказати, що дана бібліотека є доволі легкою у використанні та має чудову документацію.

Встановлення flet

Windows:

pip install flet

MacOs:

pip3 install flet

Linux:

pip install flet

Бібліотека shemdraw має у собі багато функціоналу для креслення електричних схем. Завдяки ній можна креслити не тільки схеми з аналоговими компонентами, а й із цифровими. Крім того, весь процес креслення відбавується дуже легко та зручно.

Встановлення schemdraw

Windows:

pip install shemdraw

MacOs:

pip3 install shemdraw

Linux:

pip install shemdraw

Взагалі-то, бібліотека matplotlib зазвичай використовується для створення графіків та діаграм, але у даному випадку замість графіків чи діаграм ми помістили накреслену схему.

Встановлення matplotlib

Windows:

pip install matplotlib

MacOs:

pip3 install matplotlib

Linux:

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

Для кращого розуміння є таблиця, яка детальніше пояснює важливість кожної змінної.

Змінна

Що зберігає?

component_type

Тип компонента схеми (конденсатор, резистор, генератор).

component_nominal

Номінал компонента.

component_placement

Позиція розташування елемента на схемі (праворуч, ліворуч, зверху та знизу).

component_colour

Колір компонента.

Тут ми створили цикл 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)

Таким чином, ми наче створили холст для нашого зображення та вказали для нього кілька налаштувань. У таблиці внизу це розібрано детальніше.

Параметр

Значення

src_base64

Приймає декодоване зображення.

width

Ширина зображення.

fit=ft.ImageFit.CONTAIN

Зручно вміщає зображення у холст.

border_radius

Заокруглення кутів холста.

І фінальним кроком є відображення нашої схеми у новому вікні. Для цього ми переходимо від вікна побудови схеми до останнього вікна, де ми виводимо зображення готової схеми:

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”. Тільки от варто зауважити, що для лінії випадаючий список із номіналом не має сенсу, тому цю опцію ми не чіпаємо.

Ось і готова схема:

Ось побудована схема

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

Зберегли схему як common.png

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

Як бачимо, тепер у нас є готова схема у файлі

Ось і весь процес роботи з нашою програмою. Як бачимо, вона коректно працює і виконує своє безпосереднє призначення.

Що ви можете сказати про дану програму у підсумку?

У підсумку ми створили цілком працездатну програму для створення RC-кіл, яка має гарний інтерфейс та може зберігати готові зображення схем у якості файлів. Ми успішно поєднали декілька інструментів та, що найголовніше, чітко розуміли, чому саме вони були нам потрібні.

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

Також, можливо, не всім буде зручно спочатку вказувати кількість компонентів схеми, а потім вже безпосередньо реалізовувати саму схему, але з часом до цього можна звикнути. Після деякого часу тестування даної програми, ви вже будете сприймати створення схем як умовну кількість кроків, які вам треба зробити, щоб прийти від точки А до точки Б.

Статті про вітчизняний бізнес та цікавих людей:

Поділись своїми ідеями в новій публікації.
Ми чекаємо саме на твій довгочит!
Magnifique numérique
Magnifique numérique@nocturnal_reader

Нічний читач

59Довгочити
1.2KПерегляди
20Підписники
Підтримати
На Друкарні з 14 липня 2025

Більше від автора

  • Список різноманітних бібліотек у Python

    Всіх вітаю! Сьогодні хочу поділитися великим списком бібліотек у Python для різного призначення та потреб:https://github.com/vinta/awesome-pythonТут багато цікавих бібліотек зібрано за сферами їхньго призначення.

    Теми цього довгочиту:

    It

Це також може зацікавити:

Коментарі (0)

Підтримайте автора першим.
Напишіть коментар!

Це також може зацікавити: