**Создание оконного приложения на Python с использовением Kivy/KivyMD** *** **Установка Kivy/KivyMD на Windows** *** Для установки нам потребуется всего две `команды`: ``` pip install kivy ``` ``` pip install kivymd ``` *** **Установка Kivy/KivyMD на MacOS** *** Для установки `Kivy` на `Mac`, нам нужно установить версию языка `Python` от `3.6` до `3.9`, так как начиная с версии `3.10`, Kivy перестает работать на `MacOS`. Эта проблема затрагивает только пользователей `MacOS`, с `Windows` же таких проблем нет. *** ``` pip install kivy ``` ``` pip install kivymd ``` *** **Скачать** Python любой версии можно с [официального сайта](https://www.python.org/downloads/release/python-390/). *** **В чем же отличие Kivy от KivyMD?** *** `Kivy:` * **Базовый фреймворк**: Kivy - основа, которая предоставляет все основные инструменты для создания UI, обработки событий, работы с графикой, анимацией и т.д. * **Собственный стиль**: Kivy имеет свой собственный, довольно минималистичный стиль виджетов. * **Гибкость**: Kivy предоставляет большую гибкость в настройке внешнего вида приложения. Вы можете создавать свои собственные виджеты и стилизовать их как угодно. *** `KivyMD:` * **Надстройка над Kivy**: KivyMD - это коллекция виджетов, созданная поверх Kivy, которая реализует [Material Design](https://m3.material.io/) от Google. * **Современный внешний вид**: Виджеты KivyMD соответствуют гайдлайнам Material Design, что обеспечивает современный и привлекательный внешний вид приложения. * **Готовые компоненты**: KivyMD предоставляет широкий набор готовых к использованию компонентов Material Design, что упрощает разработку. * **Меньше гибкости**: KivyMD предлагает меньше возможностей для кастомизации, чем Kivy, так как он следует определенным правилам Material Design. *** `Вывод:` * Если вам нужна максимальная гибкость в настройке дизайна, и вы готовы создавать свой собственный стиль, выбирайте `Kivy`. * Если вам нужен современный дизайн в стиле Material Design, и вы хотите использовать готовые компоненты для ускорения разработки, выбирайте `KivyMD`. *** **Перейдем к созданию приложеяния нa KivyMD** *** **Напишем обычное окно с тектом.** Для начала нам нужно импортировать все необходимые для нас ресурсы, для создания приложения. *** * импортируем класс `MDApp`, который является основой для приложений KivyMD. ``` from kivymd.app import MDApp ``` *** * импортируем класс `MDLabel` для создания текстовой метки в стиле Material Design. ``` from kivymd.uix.label import MDLabel ``` *** **Создание класса приложения** * создаем класс `MyMDApp`, наследуемый от `MDApp`. ``` class MyMDApp(MDApp): ``` *** **Метод** `build()` * определяет содержимое окна приложения ``` def build(self): ``` * создаем объект `MDLabel` с текстом, выровненным по центру, и возвращаем его. ``` return MDLabel(text="Любой другой текст", halign="center") ``` *** **Запуск приложения** * стандартная проверка для запуска скрипта. ``` if __name__ == '__main__': ``` * создаем экземпляр класса `MyMDApp` и запускаем приложение. ``` MyMDApp().run() ``` *** **Полный код выглядит таким образом:** ``` from kivymd.app import MDApp from kivymd.uix.label import MDLabel class MyMDApp(MDApp): def build(self): return MDLabel(text="Любой другой текст", halign="center") if __name__ == '__main__': MyMDApp().run() ``` ![](./img/1.png) *** Совсем забыл сказать, что в Kivy мы используем ООП (Объектно Ориентированное Программирование). Если вы с ним не знакомы, советую прочитать познавательную [статью](https://proglib.io/p/python-oop) на эту тему. *** **Думаю суть уловили, буду приступать к своей работе.** Импорт главного окна приложения ``` from kivymd.app import MDApp ``` Импорт бокслояута для добавления в него виджетов ``` from kivy.uix.boxlayout import BoxLayout ``` Импорт для изображения ``` from kivy.uix.image import Image ``` *** * главный класс ``` class SearchBoxLayout(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) self.orientation = 'vertical' self.padding = [50, 0, 50, 50] ``` `def __init__(self, **kwargs):` # Инициализация класса `super().__init__(**kwargs)` # Вызов конструктора родительского класса BoxLayout `self.orientation = 'vertical'` # Установка ориентации элементов на вертикальную `self.padding = [50, 0, 50, 50]` # Отступы вокруг элементов *** * добавление изображения ``` self.image = Image( source='net.jpg', size_hint_y=0.3 ) self.add_widget(self.image) ``` `self.image = Image()` # Создание объекта Image `source='net.jpg',` # Путь к изображению `size_hint_y=0.3` # Относительный размер изображения по вертикали `self.add_widget(self.image)` # Добавление изображения в бокслояут **Основной код выглядит так:** ``` from kivymd.app import MDApp from kivy.uix.boxlayout import BoxLayout from kivy.uix.image import Image class SearchBoxLayout(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) self.orientation = 'vertical' self.padding = [50, 0, 50, 50] # Импорт фотографии self.image = Image( source='net.jpg', size_hint_y=0.3 ) self.add_widget(self.image) class MyApp(MDApp): def build(self): return SearchBoxLayout() if __name__ == '__main__': MyApp().run() ``` ![](./img/2.png) *** * создание таблицы и импорт данных из `json` это создание обычной таблицы ``` self.data_table = MDDataTable( size_hint=(0.9, 0.9), rows_num=10, column_data=[ ("Название", dp(30)), ("Год", dp(20)), ("Цена", dp(20)), ("Цвет", dp(20)), ("Повреждения", dp(30)), ("Дата СТО", dp(30)) ], row_data=[ ("Toyota Trueno AE86", "1995", "15000", "white", "True", "19/04/2024"), ("Toyota Supra A80", "1996", "30000", "black", "False", "09/02/2024"), ("Nissal Skyline R34", "1996", "25000", "orange", "False", "10/07/2024"), ("Nissan Silvia S15", "1999", "20000", "gray", "False", "20/03/2024"), ("Toyota Camry 3.5", "2020", "20000", "blue", "False", "19/04/2024"), ("Audi RS 6", "2016", "30000", "lightblue", "True", "19/04/2024"), ("Трактор LOVOL TE354 HT", "2024", "66666", "brown", "False", "19/04/2024"), ("BMW M5 F90", "2019", "30000", "black", "True", "19/04/2024"), ("BMW E36", "2006", "35000", "red", "False", "19/04/2024"), ("Daewoo Matiz", "2010", "20000", "pink", "True", "19/04/2024"), ] ) ``` *** Это создание таблицы с `json` ``` # Прокрутка для таблицы self.scrollview = ScrollView(size_hint_y=0.7)# Создание прокручиваемого контейнера для таблицы self.add_widget(self.scrollview)# Добавление прокрутки в основной бокслояут # Создание таблицы self.data_table = MDDataTable( size_hint=(1, 1), use_pagination=True, rows_num=10, column_data=[ ("Название", dp(30)), ("Год", dp(15)), ("Цена", dp(15)), ("Цвет", dp(15)), ("Повреждения", dp(25)), ("Дата СТО", dp(30)) ], # Инициализируем row_data пустым списком, данные будут загружены из JSON row_data=[] ) self.scrollview.add_widget(self.data_table) # Загрузка данных из JSON файла self.load_data_from_json('package.json') def load_data_from_json(self, json_path): # Загрузка данных из JSON файла и обновление таблицы with open(json_path, 'r', encoding='utf-8') as json_file: data = json.load(json_file) # Предполагаем, что данные в JSON файле имеют формат списка словарей row_data = [(d['Название'], d['Год'], d['Цена'], d['Цвет'], d['Повреждения'], d['Дата СТО']) for d in data] self.data_table.row_data = row_data self.original_row_data = row_data self.original_row_data = self.data_table.row_data ``` **json файл:** ``` [ { "Название": "Toyota Trueno AE86", "Год": 1995, "Цена": 15000, "Цвет": "white", "Повреждения": "True", "Дата СТО": "19/04/2024", }, ] ``` *** **Строка поиска и кнопки фильтрации** ``` # Панель поиска и фильтрации search_filter_bar = BoxLayout(size_hint_y=None, height=dp(40), spacing=10) # Создание бокслояута для панели поиска и фильтрации self.add_widget(search_filter_bar) # Добавление панели в основной бокслояут # Строка поиска self.search_field = MDTextField( # Создание строки ввода hint_text="Search...", # Текст подсказки size_hint_x=0.7 # Относительный размер строки ввода по горизонтали ) self.search_field.bind(text=self.filter_table) # Привязка функции filter_table к изменению текста в строке поиска search_filter_bar.add_widget(self.search_field) # Добавление строки поиска на панель # Кнопки фильтрации self.asc_button = MDIconButton(icon="arrow-up-drop-circle") # Создание кнопки с иконкой для сортировки по возрастанию self.asc_button.bind(on_release=self.sort_ascending) # Привязка функции sort_ascending к нажатию на кнопку search_filter_bar.add_widget(self.asc_button) # Добавление кнопки на панель self.desc_button = MDIconButton(icon="arrow-down-drop-circle") # Создание кнопки с иконкой для сортировки по убыванию self.desc_button.bind(on_release=self.sort_descending) # Привязка функции sort_descending к нажатию на кнопку search_filter_bar.add_widget(self.desc_button) # Добавление кнопки на панель ``` *** **Выпадающий список с фильтрацией** ``` # Выпадающий список для выбора машины self.spinner_car = Spinner( # Создание выпадающего списка для выбора машины text='Выберите машину', # Начальный текст values=('Toyota Trueno AE86', 'Toyota Supra A80', 'Nissan Skyline R34', 'Nissan Silvia S15', 'Toyota Camry 3.5', 'Audi RS 6', 'Трактор LOVOL TE354 HT', 'BMW M5 F90', 'BMW E36', 'Daewoo Matiz'), # Список значений size_hint=(None, None), # Отключение автоматического расчета размера size=(dp(200), dp(44)), # Фиксированный размер pos_hint={'center_x': 0.5} # Позиционирование по центру по горизонтали ) self.spinner_car.bind(text=self.filter_table_by_name) # Привязка функции filter_table_by_name к изменению значения в списке search_filter_bar.add_widget(self.spinner_car) # Добавление списка на панель # Выпадающий список для выбора ценового диапазона self.spinner_price = Spinner( # Создание выпадающего списка для выбора ценового диапазона text='Выберите цену', # Начальный текст values=('10000 - 20000', '21000 - 30000', '31000 - 70000'), # Список значений size_hint=(None, None), # Отключение автоматического расчета размера size=(dp(200), dp(44)), # Фиксированный размер pos_hint={'center_x': 0.5} # Позиционирование по центру по горизонтали ) self.spinner_price.bind(text=self.filter_table_by_price) # Привязка функции filter_table_by_price к изменению значения в списке search_filter_bar.add_widget(self.spinner_price) # Добавление списка на панель ``` *** **Методы для всех фильтрациий и поисков (писать в конец кода)** ``` def filter_table(self, instance, text): # Фильтрация таблицы по поиску filtered_data = [] for row in self.original_row_data: if text.lower() in row[0].lower(): filtered_data.append(row) self.data_table.row_data = filtered_data def filter_table_by_name(self, spinner, text): # Фильтрация таблицы по названию машины if text != 'Выберите машину': filtered_data = [row for row in self.original_row_data if row[0] == text] self.data_table.row_data = filtered_data def filter_table_by_price(self, spinner, text): # Фильтрация таблицы по ценовому диапазону if text != 'Выберите цену': price_range = text.split(' - ') min_price, max_price = int(price_range[0]), int(price_range[1]) filtered_data = [row for row in self.original_row_data if min_price <= int(row[2]) <= max_price] self.data_table.row_data = filtered_data def sort_ascending(self, instance): # Сортировка таблицы по цене в порядке возрастания self.data_table.row_data = sorted(self.original_row_data, key=lambda row: int(row[2])) def sort_descending(self, instance): # Сортировка таблицы по цене в порядке убывания self.data_table.row_data = sorted(self.original_row_data, key=lambda row: int(row[2]), reverse=True) ``` *** Метод `filter_table(self, instance, text):`** - `filtered_data = []`: создаёт пустой список для хранения отфильтрованных данных. - Цикл `for row in self.original_row_data:`: - Проходит по каждой строке `row` в исходных данных. - `if text.lower() in row[0].lower():`: - Проверяет, содержится ли текст поиска (`text`) в названии автомобиля (`row[0]`), игнорируя регистр. - Если содержится, добавляет строку в `filtered_data`. - `self.data_table.row_data = filtered_data`: - Обновляет данные в таблице, отображая только отфильтрованные строки. Метод `filter_table_by_name(self, spinner, text):` - Фильтрует таблицу по названию машины, выбранному в выпадающем списке. - `if text != 'Выберите машину':`: проверяет, выбрана ли какая-то машина. - `filtered_data = [row for row in self.original_row_data if row[0] == text]`: - Создаёт список, содержащий только строки, где название машины совпадает с выбранным. - `self.data_table.row_data = filtered_data`: - Обновляет таблицу отфильтрованными данными. Метод `filter_table_by_price(self, spinner, text):` - Фильтрует таблицу по ценовому диапазону, выбранному в выпадающем списке. - `if text != 'Выберите цену':`: проверяет, выбран ли ценовой диапазон. - `price_range = text.split(' - ')`: разделяет текст диапазона на минимальную и максимальную цену. - `min_price, max_price = int(price_range[0]), int(price_range[1])`: преобразует цены в числа. - `filtered_data = [row for row in self.original_row_data if min_price <= int(row[2]) <= max_price]`:- Создаёт список, содержащий строки, где цена находится в заданном диапазоне. - `self.data_table.row_data = filtered_data`:- Обновляет таблицу отфильтрованными данными. Метод `sort_ascending(self, instance):` - Сортирует таблицу по цене в порядке возрастания. - `self.data_table.row_data = sorted(self.original_row_data, key=lambda row: int(row[2])):`: - Сортирует строки - `self.original_row_data` по цене (`row[2]`), преобразуя её в число. Метод `sort_descending(self, instance):` - Сортирует таблицу по цене в порядке убывания. - `self.data_table.row_data = sorted(self.original_row_data, key=lambda row: int(row[2]), reverse=True):`: - Сортирует строки - `self.original_row_data` по цене (`row[2]`), преобразуя её в число, с обратным порядком (`reverse=True`). *** **Замена таблицы на разметку с изображениями и текстом** ``` self.spinner_price.bind(text=self.filter_items) search_filter_bar.add_widget(self.spinner_price) # Прокрутка для таблицы self.scrollview = ScrollView() self.add_widget(self.scrollview) # Сетчатая разметка для хранения элементов автомобиля self.car_grid = GridLayout(cols=1, spacing=10, size_hint_y=None) self.car_grid.bind(minimum_height=self.car_grid.setter('height')) self.scrollview.add_widget(self.car_grid) # Загрузка данных из JSON self.car_data = self.load_data_from_json('package.json') self.original_car_data = self.car_data.copy() self.populate_car_grid() self.spinner_car.values = list({car['Название'] for car in self.car_data}) def load_data_from_json(self, filename): with open(filename, 'r', encoding='utf-8') as f: data = json.load(f) return data def populate_car_grid(self): # Заполнение сетки виджетов self.car_grid.clear_widgets() for car in self.car_data: car_item = CarItem(car) self.car_grid.add_widget(car_item) ``` *** **`self.spinner_price.bind(text=self.filter_items)`:** - Эта строка связывает *изменение текста* в выпадающем списке `self.spinner_price` с методом `self.filter_items`. - То есть, когда пользователь выбирает новое значение в `self.spinner_price`, Kivy автоматически вызывает `self.filter_items`. **`search_filter_bar.add_widget(self.spinner_price)`:** - Добавляет выпадающий список `self.spinner_price` в контейнер `search_filter_bar`. - `search_filter_bar` - это, скорее всего, `BoxLayout`, или другой контейнер, который хранит элементы панели поиска и фильтрации. **`self.scrollview = ScrollView()`:** - Создаёт экземпляр класса `ScrollView`. - `ScrollView` - это контейнер, который позволяет прокручивать содержимое, если оно не помещается в видимой области. **`self.add_widget(self.scrollview)`:** - Добавляет `self.scrollview` в главный контейнер экрана. - То есть, теперь `self.scrollview` будет отображаться на экране, и все его дочерние элементы тоже. **`self.car_grid = GridLayout(cols=1, spacing=10, size_hint_y=None)`:** - Создаёт экземпляр класса `GridLayout`. - `cols=1`: сетка будет иметь одну колонку. - `spacing=10`: расстояние между элементами сетки будет 10 пикселей. - `size_hint_y=None`: сетка не будет масштабироваться по вертикали, её высота будет определяться содержимым. **`self.car_grid.bind(minimum_height=self.car_grid.setter('height'))`:** - Эта строка гарантирует, что высота сетки `self.car_grid` будет всегда достаточной для отображения всех элементов. **`self.scrollview.add_widget(self.car_grid)`:** - Добавляет сетку `self.car_grid` в `self.scrollview`. - Теперь содержимое `self.car_grid` можно будет прокручивать, если оно не помещается в видимой области. **`self.car_data = self.load_data_from_json('package.json')`:** - Вызывает метод `self.load_data_from_json`, передавая ему имя файла `'package.json'`. - Метод загружает данные из JSON файла и возвращает их. - Результат сохраняется в `self.car_data`. **`self.original_car_data = self.car_data.copy()`:** - Создает копию данных из `self.car_data` и сохраняет ее в `self.original_car_data`. - Это делается для того, чтобы можно было фильтровать и сортировать данные, не изменяя исходный набор. **`self.populate_car_grid()`:** - Вызывает метод `self.populate_car_grid`, который отвечает за заполнение сетки `self.car_grid` виджетами, представляющими автомобили. - Предполагается, что у вас есть отдельный класс (например, `CarItem`) для создания виджетов, отображающих информацию об автомобиле. **`self.spinner_car.values = list({car['Название'] for car in self.car_data})`:** - Эта строка задает значения для выпадающего списка `self.spinner_car`, который, вероятно, используется для фильтрации по названию машины. - `{car['Название'] for car in self.car_data}`: создает множество уникальных названий машин из `self.car_data`. - `list(...)`: преобразует множество в список, так как `spinner.values` ожидает список значений. *** `json file` ``` [ { "Название": "Toyota Trueno AE86", "Год": 1995, "Цена": 15000, "Цвет": "white", "Повреждения": "True", "Дата СТО": "19/04/2024", "image": "tru.jpg" }, ] ``` *** **все импорты, которые нам понадобились:** ``` from kivymd.app import MDApp # Импорт главного окна приложения from kivy.uix.boxlayout import BoxLayout # Импорт бокслояута для добавления в него виджетов from kivy.uix.gridlayout import GridLayout # Импорт гридлояута для добавления элементов в виде сетки from kivy.uix.spinner import Spinner # Импорт для выпадающего списка from kivymd.uix.textfield import MDTextField # Импорт для таблицы from kivymd.uix.button import MDIconButton # Импорт для иконок кнопок from kivy.uix.image import Image # Импорт для изменения размера from kivy.uix.label import Label # Импорт Label для отображения текста from kivy.uix.scrollview import ScrollView # Импорт для прокрутки таблицы from kivy.metrics import dp # Импорт для изменения размера from kivy.utils import get_color_from_hex # Импорт для использования шестнадцатеричных цветовых кодов import json ```