Решение
Проектирование взаимодействия пользователя с приложением. Хранение данных
Общая схема интерфейса и взаимодействия пользователей с ним
В хранилище данных требуется одна таблица со структурой:
CREATE TABLE EXPENSES (
ID INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1),
"date" VARCHAR(20),
CATEGORY VARCHAR(50),
AMOUNT NUMERIC(19,2),
DESCRIPTION VARCHAR(256),
CONSTRAINT INTEG_1 PRIMARY KEY (ID)
USING ASCENDING INDEX RDB$PRIMARY1);
Настройка среды разработки
Для этого туториала среда разработки создается на ПК под управлением ОС MS Windows. Можно вести разработку и в других ОС.
Последовательность действий:
- Создать рабочую папку и перейти в нее.
C:\Users\User>cd /d d:\Projects\REDBASE
d:\Projects\REDBASE>md expenses
d:\Projects\REDBASE>cd expenses
d:\Projects\REDBASE\Expenses>
- Создать и активировать виртуальное окружение.
d:\Projects\REDBASE\Expenses>python -m venv .venv
d:\Projects\REDBASE\Expenses>.venv\Scripts\activate
(.venv) d:\Projects\REDBASE\Expenses>
- Установить библиотеку delphifmx.
(.venv) d:\Projects\REDBASE\Expenses>pip install delphifmx
Collecting delphifmx
Obtaining dependency information for delphifmx from https://files.pythonhosted.org/packages/aa/b6/41f433ef9db847a6939153f25168ac5273e67da8f4a44ab3d5f6e403f44f/delphifmx-1.1.0-cp311-cp311-win_amd64.whl.metadata
Using cached delphifmx-1.1.0-cp311-cp311-win_amd64.whl.metadata (5.2 kB)
Using cached delphifmx-1.1.0-cp311-cp311-win_amd64.whl (6.7 MB)
Installing collected packages: delphifmx
Successfully installed delphifmx-1.1.0
(.venv) d:\Projects\REDBASE\Expenses>
Для связи с СУБД РЕД База Данных используется драйвер firebird-driver, который может потребоваться предварительно установить в среду.
(.venv) d:\Projects\REDBASE\Expenses>pip install firebird-driver
В целом, уже можно приступать к разработке. Однако, для работы с БД необходимы клиентские библиотеки СУБД РЕД База Данных. Проще всего установить клиентскую часть при помощи инсталлятора СУБД [^2]. В примере будет использована РЕД База Данных версии 5.0 [^2]: Руководство Администратора СУБД РЕД БазаДанных. Раздел “Установка”
- Запустить среду разработки.
Можно использовать любую привычную среду или инструмент разработки. Ниже будет применяться IDE JetBrains PyCharm
Создается новый проект в рабочей папке:

После этого можно создавать приложение.
Модуль работы с данными
Исходя из результата проектирования взаимодействия пользователя с приложением и хранения данных, уже можно предположить, как будет выглядеть модуль доступа к БД для этого примера. Одно из требований — чтобы основные модули работы с GUI могли обращаться с любой СУБД без переделок кода в модулях. Другими словами, модель данных приложения должна быть независима от используемого типа СУБД. Поэтому, вынесем весь функционал работы с БД в отдельный модуль, где и локализуем всю специфику работы с конкретной СУБД, в нашем случае - РЕД База Данных. Это сделано таким образом: создан специальный класс, остальные модули будут вызывать его методы вместо непосредственного обращения к БД. Интерфейс класса может выглядеть так:
class exmdl_RDB():
def __init__(self):
def __doc__(self):
return "Red Database exmdl\n"
def initialize_db(self,dbpath: str) -> None:
def get_expenses(self) -> list[any]:
def add_expense(self,expense: tuple[any]):
def del_expense(self, _id:int) -> None:
def update_expense(self,expense: tuple[any]):
__datasource = None
def CreateDatasource(*args, **kwargs):
В стандартном методе __init__ выполняется настройка драйвера РЕД Базы Данных, заполняются имена и параметры доступа по умолчанию.
Метод initialize_db(self,dbpath: str) должен проверить наличие в БД таблицы expenses и, в случае ее отсутствия, создать новую – пустую, но с нужной структурой. В противном случае – ничего не делать.
Метод get_expenses(self) -> list[any] должен выдать имеющиеся в таблице записи в виде списка. Для простоты понимания примера он выдает все записи. Может быть, стоило ввести дополнительный параметр, ограничивающий максимальное число записей в списке, тем более, что синтаксис SQL РЕД Базы Данных легко это позволяет реализовать.
Методы add_expense(self,expense: tuple[any]) и update_expense(self,expense: tuple[any]) принимают набор значений полей записи и вносят эти данные в БД – первый в новую запись, второй – в существующую.
Метод del_expense(self, _id:int) удаляет запись, у которой значение поля ID равно указанному в параметре.
Все вместе они позволяют реализовать полный набор CRUD операций.
Определенная в модуле функция CreateDatasource() служит фабрикой для объекта класса exmdl_RDB и обеспечивает единственность его экземпляра.
Полный код реализации модуля находится во включенном исходном тексте dbRDB.py.
Пример реализации некоторых методов приведен ниже.
from firebird.driver import connect, driver_config
_RDB_URL = "inet://redbase/c:\_data\_newdb.fdb"
_user='sysdba'
_password='masterkey'
_charset='utf8'
class exmdl_RDB():
def __init__(self):
driver_config.server_defaults.user.value=_user
driver_config.server_defaults.password.value=_password
self.dbpath = 'expenses.fdb'
def initialize_db(self,dbpath: str) -> None:
if dbpath != None: self.dbpath = _RDB_URL
conn = connect(self.dbpath, charset=_charset)
c = conn.cursor()
c.execute('''
EXECUTE BLOCK AS BEGIN if (not exists(select 1 from rdb$relations where UPPER(rdb$relation_name) = Upper('expenses'))) then execute statement 'create table expenses ( id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "date" varchar(20), category varchar(50), amount NUMERIC(19, 2), description varchar(256) );'; END; ''')
conn.commit()
conn.close()
Использование библиотеки delphifmx
Содержимое библиотеки delphifmx (сокращено)
import delphifmx
dir()
['__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'delphifmx', 'sys']
dir(delphifmx)
['Abort', 'Action', 'ActionList', 'AdapterListView', 'AniIndicator', 'AppearanceListView', 'Application', 'Arc', 'ArcDial', 'BaseBindScopeComponent', 'BaseImageList', 'BaseLinkingBindSource', 'BaseObjectBindSource', 'BaseValueRange', 'BasicAction', 'BasicBindComponent', 'BindComponentDelegate', 'BindingsList', 'BitmapTrackBar', 'Bounds', 'BufferedFileStream', 'BufferedLayout', 'Button', 'BytesStream', 'Calendar', 'CalloutPanel', 'CalloutRectangle', 'CameraComponent', 'CaretRectangle', 'CheckBox', 'CheckColumn', 'Circle', 'Collection', 'ColorBox', 'ColorButton', 'ColorComboBox', 'ColorListBox', 'ColorPanel', 'ColorPicker', 'ColorQuad', 'Column', 'ComboBox',
...
'DialogService', 'Edit', 'Ellipse', 'Expander', 'ExpanderButton', 'FMXBindNavigateAction', 'FMXBindNavigateApplyUpdates', 'FMXBindNavigateCancel', 'FMXBindNavigateCancelUpdates', 'FMXBindNavigateDelete', 'FMXBindNavigateEdit', 'FMXBindNavigateFirst', 'FMXBindNavigateInsert', 'FMXBindNavigateLast', 'FMXBindNavigateNext', 'FMXBindNavigatePost', 'FMXBindNavigatePrior', 'FMXBindNavigateRefresh', 'FileExit', 'FileHideApp', 'FileHideAppOthers', 'FileStream', 'FloatColumn', 'FlowLayout', 'FmxObject', 'Form', 'Frame', 'FramedScrollBox', 'FramedVertScrollBox', 'GetProcessDpiAwareness', 'GlyphColumn', 'GradientEdit', 'Grid', 'GridLayout', 'GridPanelLayout', 'GroupBox', 'HandleStream', 'HintAction', 'HorzScrollBox', 'IDABORT', 'IDCANCEL', 'IDCLOSE', 'IDCONTINUE', 'IDHELP', 'IDIGNORE', 'IDNO', 'IDOK', 'IDRETRY', 'IDTRYAGAIN', 'IDYES', 'Image', 'ImageColumn', 'ImageControl', 'ImageList', 'IndexedProperty', 'IntegerColumn', 'IsDpiAware', 'Label', 'Layout', 'Line', 'LinkControlDelegate', 'LinkControlToField', 'LinkControlToFieldDelegate',
...
список ограничен для наглядности
В модуль вошли адаптеры для всех доступных классов, функций и объектов FMX.
Простейшее (пустое) приложение delphifmx
from delphifmx import *
Application.Initialize()
Application.Title = "Hello Delphi Fmx"
app = Form(Application)
app.SetProps(Caption = "Welcome")
app.Show()
Application.Run()
app.Destroy()
Результат:

Чуть более сложное приложение:
from delphifmx import *
class GUIApp(Form):
def __init__(self, owner):
self.SetProps(Caption="Welcome")
self.lblHello = Label(self)
self.lblHello.SetProps(
Parent=self,
Text="Hello DelphiFMX for Python")
self.lblHello.SetBounds(100, 100, 150, 25)
def main():
Application.Initialize()
Application.Title = "Hello DelphiFMX"
app = GUIApp(Application)
Application.MainForm = app
app.Show()
Application.Run()
app.Destroy()
main()
Дает такую картину:
Посмотрим внимательно на код. Первая строчка импортирует все их модуля delphifmx, что позволяет нам использовать любые классы из модуля.
Затем идет описание класса GUIApp, который расширяет класс Form из библиотеки. Класс Form особенный и отличается тем, что создает окно графического интерфейса, содержащее все остальные компоненты.
При инициализации объекта используется метод SetProps для задания значения свойства Caption. Затем создается новый объект класса Label, владельцем которого назначается объект GUIApp и устанавливаются его свойства Parent и Caption.
Таким образом, в этой части можно добавлять любые компоненты — элементы управления GUI — и задавать им значения свойств прямо в тексте программы на Python.
Владелец (Owner) объекта из delphifmx отвечает за управление его жизненным циклом и за передачу сообщений, а родитель (Parent) – за его расположение и отрисовку.
Функция main создает и инициализирует синглтон Application, который запускает приложение и управляет основным циклом.
Затем создается объект формы app, владельцем которого является Application, и этот объект назначается главной формой приложения (Application.MainForm) – с которой начинается отображение GUI. Следующими строками главная форма отображается и запускается основной цикл приложения. После выхода из него в конце работы, форма освобождает память. Технически функция main является стандартной для таких приложений delphifmx (сравните с простейшим приложением выше).
Если в приложении требуется более одной формы, для каждой из них следует заводить свой класс, а создавать экземпляры можно в main или динамически в модулях форм-родителей.
Создание GUI приложения в WYSIWYG редакторе
Таким путем можно создавать и применять в своих задачах достаточно сложные и изощренные графические формы на основе библиотеки компонентов Delphi. Но как насчет производительности разработчиков? Ведь практически то же делают разработчики с Tkinter и другими Python-библиотеками.
В Delphi можно создавать формы заранее, на этапе проектирования “in design-time”, сохранять их описания в виде файлов на диске и загружать готовый проект формы целиком во время выполнения приложения. Для этого туториала был выбран именно такой подход.
Чтобы выполнить дизайн формы, требуется наличие установленного IDE Delphi или C++Builder. К счастью, пользователям доступен бесплатный вариант IDE редакции Community Edition. Его установщик можно получить на сайте Embarcadero в разделе Free Tools. Для наших целей безразлично, какой язык выбрать — Delphi или C++, поскольку код писать на этом языке не потребуется. Потребуется только редактор форм.
Запустить IDE. Создать новый проект. Для этого в главном меню выбрать:
File -> Multi-Device Application
Выбрать шаблон Blank Application

Откроется редактор форм (закладка Design) с пустой формой. Можно приступать к созданию нужного UI.
В папке с исходниками примера есть подпапка Delphi, в которой находятся готовый Delphi-проект и файлы форм, используемые в примере. Чтобы не тратить много времени, можно просто загрузить готовый проект:
File -> Open Project -> .\Delphi\exp2fm.dproj

Сравните форму с наброском выше, в разделе Проектирование взаимодействия пользователя с приложением. Хранение данных
Создание формы происходило таким образом: в области Palette справа выбирался нужный компонент (проще всего ввести несколько начальных букв в поле поиска), и выбранный компонент перетаскивался в задуманное место на форме. В области Structure слева можно видеть присвоенные компонентам имена (свойство Name). Важно, чтобы в тексте на Python для обращения к компонентам применялись именно эти имена, так как они сохранены в файле, который будет загружаться в момент выполнения.
Общая структура – та же. Есть набор кнопок обращения с данными вверху, только вместо надписей на них появились иконки – результат применения встроенных стилей (подробнее об этом дальше).
Присутствует таблица в виде компонента StringGrid1, а также панель lyEdit с набором полей для редактирования данных. В начале работы эта панель скрыта, а при нажатии одной из кнопок добавления/изменения/удаления она становится видимой и активной до нажатия кнопки Отмена или Сохранить. В StringGrid1 были добавлены колонки для помещения в них значений из БД.
Из элементов управления на панели редактирования следует отметить элемент интерфейса DateEdit1 (компонент типа TDateEdit), позволяющий вводить даты при помощи выбора даты из календаря. Еще появились метки (TLabel) названий полей, режима редактирования, отображения сообщений об ошибках, если они возникнут.
Чтобы в Python появилась возможность реагировать в программе на нажатие кнопок, важно в момент создания формы создать обработчики этих событий. Для создания обработчика, например, для кнопки Добавить (+), нужно сделать по этой кнопке на форме двойной щелчок мыши, и IDE перейдет в режим редактирования кода. Вместо графического вида появится текст кода модуля формы с обработчиком вида:
procedure TmFrm.<имя кнопки>Click(Sender: TObject);
begin
//
end;
Следует вставить внутрь процедуры хотя бы строку комментария, чтобы обработчик не был автоматически удален при сохранении формы на диск. Создайте такие заготовки для каждой кнопки на форме.

Создание формы с большим количеством элементов интерфейса может занять достаточно много времени, поэтому в этот пример не включено подробное описание действий по шагам. В пример включены готовые исходные тексты проекта на Delphi, где можно подробно ознакомиться с параметрами каждого компонента и установленными значениями его свойств.
Итак, дизайн формы готов, обработчики нажатий созданы для каждой кнопки, настал момент экспорта формы для использования в Python.
Следует убедиться в наличии (а при отсутствии - установить) инструмента Export to Python.

По умолчанию он отсутствует, но его можно бесплатно установить из менеджера пакетов GetIt.

При экспорте всего текущего проекта следует указать текст заголовка окна приложения, имя главной формы приложения (выбрать, если форм в приложении несколько) и каталог на диске, куда будут помещены исходные тексты на Python

Теперь можно закрыть IDE Delphi и вернуться в IDE PyCharm.
В каталоге проекта появилась новая папка Delphi с подпапкой PY. В ней находятся экспортированные исходники-заготовки:
- exp2frm.py - приложение
- u2List.py - модуль главной формы
- uAdd.py - модуль второй формы, которая в примере не участвует
- u2List.pyfmx - текстовая запись графического представления спроектированной нами главной формы
Уже можно запустить приложение и посмотреть, как выглядит наша форма

Отлично! Форма запустилась точно в таком виде, как мы ее спроектировали. Можно даже понажимать на кнопки и убедиться, что никакой реакции не происходит. Это и понятно: созданные в режиме проектирования обработчики событий нажатия пока не наполнены кодом. Завершите работу приложения.
Обновленная структура приложения
Теперь в проекте приложения имеются файлы:
- exp2frm.py - приложение
- u2List.py - модуль главной формы
- u2List.pyfmx - текстовое описание графического представления
Перечисленные файлы-заготовки следует скопировать в основной каталог проекта (заготовки пусть остаются в папке Delphi)
В основном каталоге уже должен находиться подготовленный ранее модуль доступа к СУБД
- dbRDB.py,
а также готовые файлы модулей приложения. Они созданы на основе файлов-заготовок, полученных после экспорта из Delphi. В них были внесены изменения и дополнения, которые реализуют логику работы пользователя.
Будет полезно открыть все файлы проекта с расширением .py в IDE и внимательно ознакомиться с их содержимым.
Как и раньше, модуль приложения exp2frm.py служит для запуска главной формы и управления ее циклом сообщений. По сравнению с заготовками выше, в него вручную были внесены некоторые изменения.
##----- exp2frm.py -----
from delphifmx import Application
# импорт класса главной формы приложения
from u2List import mFrm
from dbRDB import exmdl_RDB, CreateDatasource
def main():
# создание уникального экземпляра доступа к СУБД
exmdl = CreateDatasource()
print(exmdl.__doc__())
exmdl.initialize_db("expenses.fdb")
Application.Initialize()
Application.Title = 'Учет расходов'
# Создание экземпляра формы и назначение е главной формой приложения
Application.MainForm = mFrm(Application)
Application.MainForm.Show()
Application.Run()
Application.MainForm.Destroy()
if __name__ == '__main__':
main()
Работа с графическим интерфейсом приложения и вся логика взаимодействия пользователя с приложением реализуется через класс mFrm в модуле главной формы u2List.py
В методе __init__ создаются и инициализируются внутренние переменные класса. По сравнению с первоначально экспортированным вариантом были внесены дополнения
def __init__(self, owner):
...
## Добавленые строки ---------------------------
# Работа со стилями
self.__sm = StyleManager()
try:
self.__sm.SetStyle(StyleStreaming().LoadFromFile(os.path.join(os.getcwd(),"expenses.style")))
except:
pass
self.ds = CreateDatasource() # ссылка на экземпляр доступа к СУБД
self.editMode = None
self.db_id = None
self.gridrow = 0
Добавлен ряд служебных методов, отвечающих за правильное отображение в UI состояния приложения
## Заполнение полей панели редактирования значениями из БД
def panel_prepare(self,fields:dict):
if fields is None: # режим добавления -
# нет словаря со значенями из БД
self.DateEdit1.Text = f'{date.today()}'
self.edCategory.Text = ''
self.edSum.Text = ''
self.edDescr.Text = ''
self.db_id = None
else: # режим модификации -
# значения переданы в словаре
self.DateEdit1.Text = fields['Дата']
self.edCategory.Text = fields['Категория']
self.edSum.Text = fields['Сумма']
self.edDescr.Text = fields['Описание']
self.db_id = fields['ID']
xstr = lambda s: s or ""
if self.editMode.lower() == 'delete':
self.lbID.Text = f"Удалена запись ID:{xstr(self.db_id)}"
elif self.editMode.lower() == 'insert':
self.lbID.Text = f"Запись будет добавлена"
elif self.editMode.lower() == 'update':
self.lbID.Text = f"Изменена запись ID:{xstr(self.db_id)}"
self.lbErrMsg.Text = ''
## оставляет активными только поля панели редактирования
def panel_modal_show(self):
self.lyTopTools.Enabled = False
self.lyGrid.Enabled = False
self.lbStatus.Text = ''
self.lyEdit.Visible = True
if self.editMode == 'delete':
self.lyEdit.Enabled=False
self.lyButtonsModal.Enabled = True
else:
self.lyEdit.Enabled=True
self.ActiveControl=self.DateEdit1
## деактивирует панель редактирования.
## Активизирует остальные элементы UI
def panel_modal_close(self):
self.editMode = ''
self.lyTopTools.Enabled = True
self.lyGrid.Enabled = True
self.lyEdit.Visible = False
def show_status(self, status):
self.lbStatus.Text = status
Отображение данных в таблице
def fill_view(self, expenses: list[any], row:int = 0) -> None:
def cellsclear():
for r in range(0,self.StringGrid1.RowCount):
for c in range(0,self.StringGrid1.ColumnCount):
self.StringGrid1.Cells[c,r] = ''
cellsclear()
rowsNo = self.StringGrid1.RowCount
rowsNo = rowsNo if rowsNo < len(expenses) else len(expenses)
row = rowsNo-1 if row > rowsNo else row
for idx, expense in enumerate(expenses):
y = idx + 1
ex_id, date, category, amount, description = expense
self.StringGrid1.Cells[0,idx] = ex_id
self.StringGrid1.Cells[1,idx] = date
self.StringGrid1.Cells[2,idx] = category
self.StringGrid1.Cells[3,idx] = amount
self.StringGrid1.Cells[4,idx] = description
self.StringGrid1.SelectRow(row)
def get_fields(self, rowno) -> dict:
_result = {}
for c in range(0,self.StringGrid1.ColumnCount):
_result[self.StringGrid1.Columns[c].Header] = self.StringGrid1.Cells[c,rowno]
return _result
Обработка событий на форме
Обработчик нажатия кнопки удаления записи
def BtDelRecClick(self, Sender):
self.editMode = 'Delete'
self.gridrow = self.StringGrid1.Row
self.panel_prepare(self.get_fields(self.gridrow))
self.panel_modal_show()
Обработчики нажатия на кнопки Добавить и Изменить аналогичны этому, но с соответствующим значением self.editMode.
По кнопке Обновить идет обращение к БД для получения набора записей
def btRefreshViewClick(self, Sender):
self.fill_view(self.ds.get_expenses(),self.gridrow)
self.show_status('')
Запись в БД производится только в обработчике кнопки Сохранить
def btCommitClick(self, Sender):
try:
if self.editMode.lower() == 'delete':
self.ds.del_expense(self.db_id)
self.show_status('Запись была удалена')
elif self.editMode.lower() == 'update':
self.ds.update_expense((self.db_id,
self.DateEdit1.Text,
self.edCategory.Text,
self.edSum.Text,
self.edDescr.Text))
self.show_status('Запись была изменена')
elif self.editMode.lower() == 'insert':
self.ds.add_expense((self.db_id,
self.DateEdit1.Text,
self.edCategory.Text,
self.edSum.Text,
self.edDescr.Text))
self.show_status('Запись была добавлена')
else:
raise "неопределенный режим"
except Exception as e:
self.show_status(f'Исключение: {str(e)}')
finally:
self.panel_modal_close()
self.fill_view(self.ds.get_expenses(),self.gridrow)
в то время как кнопка Отменить просто возвращает приложение в предыдущее состояние.
Полный текст кода модуля можно найти в архиве с примером.
Дата последнего изменения: 11.09.2026
Если вы нашли ошибку, пожалуйста, выделите текст и нажмите Ctrl+Enter.