Делаем своё приложение для ведения бюджета
easy

Делаем своё приложение для ведения бюджета

Базовая версия

Сегодня начнём делать собственное приложение для ведения бюджета — с графическим интерфейсом, базой данных и кросс-платформенностью. Для этого нам понадобится освежить знания про базы данных:

  • База данных — это способ хранения данных в одном месте.
  • Внутри базы могут храниться разные данные: фото, текст, музыка, числа, код, ссылки, цены и что угодно ещё.
  • Когда говорят про базы данных, чаще всего имеют в виду табличные базы данных — те, где информация хранится в таблицах.
  • Пример табличной базы — MySQL. Она многое умеет, к ней написано много документации и правил, поэтому начинают обычно с неё.
  • Чтобы управлять данными в базе, используют специальный язык запросов к базе — SQL.
  • У Python есть поддержка встраиваемой базы данных SQLite — она тоже поддерживает SQL, но для неё не нужно разворачивать и настраивать отдельную систему управления.

Ещё нам понадобится графический интерфейс. Для начала сделаем всё на простой библиотеке Tkinter, а потом, может, перейдём и на PySimpleGUI. Такой интерфейс нам нужен для того, чтобы управлять бюджетом можно было с помощью полей ввода и кнопок, а не из командной строки.

Всё будем писать на Python, поэтому его тоже нужно установить и настроить, если ещё не.

Как установить Python на компьютер и начать на нём писать

Логика проекта

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

Что программа будет уметь на старте:

  • запоминать введённые покупки, их стоимость и комментарии к ним;
  • удалять записи;
  • искать по названию покупки;
  • изменять выбранные записи;
  • закрывать окно и спрашивать подтверждение выхода.

Это будет наша минимальная рабочая версия. Потом мы будем доводить её до ума.

Делаем своё приложение для ведения бюджета
Общий вид готовой программы

Подключаем и настраиваем базу данных

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

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

👉 Если не знаете, что такое «класс» в программировании, вот вам простое объяснение.

В классе у нас будет два особых метода — конструктор и деструктор.

Конструктор выполняется в момент создания объекта. В нём мы пропишем название файла с базой данных и создадим таблицу, если её ещё нет.

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

Все остальные методы — это просто обёртки SQL-команд. То есть смотрите: чтобы работать с базой данных, нужно давать ей особые команды. У них есть какой-то свой синтаксис. Нам не очень хочется каждый раз вспоминать этот синтаксис, поэтому мы «завернём» эти команды в более понятные нам.

Получается, что эти методы — это как бы переводчик с нашего языка на язык базы данных. Мы скажем База_данных.запомнить_трату(такую-то), а уже метод внутри этого объекта сам сформулирует правильный запрос, проверит подключение к базе, запишет строчку в базу, проверит эту строчку и т. д. 

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

# подключаем библиотеку для работы с базой данных
import sqlite3    
# создаём класс для работы с базой данных
class DB:                        
    # конструктор класса
    def __init__(self):           
        # соединяемся с файлом базы данных
        self.conn = sqlite3.connect("mybooks.db")  
        # создаём курсор для виртуального управления базой данных
        self.cur = self.conn.cursor()    
        # если нужной нам таблицы в базе нет — создаём её
        self.cur.execute(             
            "CREATE TABLE IF NOT EXISTS buy (id INTEGER PRIMARY KEY, product TEXT, price TEXT, comment TEXT)") 
        # сохраняем сделанные изменения в базе
        self.conn.commit()  

    # деструктор класса
    def __del__(self):        
        # отключаемся от базы при завершении работы
        self.conn.close()   
   
    # просмотр всех записей
    def view(self):        
        # выбираем все записи о покупках
        self.cur.execute("SELECT * FROM buy") 
        # собираем все найденные записи в колонку со строками
        rows = self.cur.fetchall()  
        # возвращаем сроки с записями расходов
        return rows

    # добавляем новую запись
    def insert(self, product, price, comment):  
        # формируем запрос с добавлением новой записи в БД
        self.cur.execute("INSERT INTO buy VALUES (NULL,?,?,?)", (product, price, comment,)) 
        # сохраняем изменения
        self.conn.commit()
        

    # обновляем информацию о покупке
    def update(self, id, product, price):   
        # формируем запрос на обновление записи в БД
        self.cur.execute("UPDATE buy SET product=?, price=? WHERE id=?", (product, price, id,))
        # сохраняем изменения 
        self.conn.commit()

    # удаляем запись
    def delete(self, id):                   
        # формируем запрос на удаление выделенной записи по внутреннему порядковому номеру
        self.cur.execute("DELETE FROM buy WHERE id=?", (id,))
        # сохраняем изменения
        self.conn.commit()

    # ищем запись по названию покупки
    def search(self, product="", price=""):  
        # формируем запрос на поиск по точному совпадению
        self.cur.execute("SELECT * FROM buy WHERE product=?", (product,))
        # формируем полученные строки и возвращаем их как ответ
        rows = self.cur.fetchall()
        return rows

# создаём экземпляр базы данных на основе класса
db = DB()

Создаём интерфейс

Мы хотим нарисовать графический интерфейс — то есть окно с кнопками, чтобы управлять базой не текстом, а понятными человеку движениями и кликами. Для создания графического интерфейса есть много библиотек, мы воспользуемся Tkinter. 

В библиотеке Tkinter используется виртуальная сетка: каждую кнопку, поле ввода или что-то ещё можно расставить по этой сетке, а дальше библиотека сама посчитает координаты. За это отвечают параметры row и column, вы их увидите ниже в коде.

Единственное, где мы укажем явные размеры, — это блок с общим списком расходов. Это нужно для того, чтобы сетка не размещала всё максимально близко друг к другу:

list1 = Listbox(window, height=25, width=65)

Мы подписали каждый элемент в коде, чтобы было проще разобраться, что за что отвечает:

# создаём надписи для полей ввода и размещаем их по сетке
l1 = Label(window, text="Название") 
l1.grid(row=0, column=0) 

l2 = Label(window, text="Стоимость")
l2.grid(row=0, column=2)

l3 = Label(window, text="Комментарий")
l3.grid(row=1, column=0)

# создаём поле ввода названия покупки, говорим, что это будут строковые переменные и размещаем их тоже по сетке
product_text = StringVar()
e1 = Entry(window, textvariable=product_text)
e1.grid(row=0, column=1)

# то же самое для комментариев и цен
price_text = StringVar() 
e2 = Entry(window, textvariable=price_text)
e2.grid(row=0, column=3)

comment_text = StringVar() 
e3 = Entry(window, textvariable=comment_text)
e3.grid(row=1, column=1)

# создаём список, где появятся наши покупки, и сразу определяем его размеры в окне
list1 = Listbox(window, height=25, width=65) 
list1.grid(row=2, column=0, rowspan=6, columnspan=2) 

# на всякий случай добавим сбоку скролл, чтобы можно было быстро прокручивать длинные списки
sb1 = Scrollbar(window) 
sb1.grid(row=2, column=2, rowspan=6)

# привязываем скролл к списку
list1.configure(yscrollcommand=sb1.set) 
sb1.configure(command=list1.yview)

# создаём кнопки действий и привязываем их к своим функциям
# кнопки размещаем тоже по сетке
b1 = Button(window, text="Посмотреть все", width=12, command=print('view_command')) 
b1.grid(row=2, column=3) #size of the button

b2 = Button(window, text="Поиск", width=12, command=print('search_command'))
b2.grid(row=3, column=3)

b3 = Button(window, text="Добавить", width=12, command=print('add_command'))
b3.grid(row=4, column=3)

b4 = Button(window, text="Обновить", width=12, command=print('update_command'))
b4.grid(row=5, column=3)

b5 = Button(window, text="Удалить", width=12, command=print('delete_command'))
b5.grid(row=6, column=3)

b6 = Button(window, text="Закрыть", width=12, command=print('on_closing'))
b6.grid(row=7, column=3)
Делаем своё приложение для ведения бюджета

Пишем логику работы

Сейчас у нас есть только интерфейс, который не умеет ничего. Даже при нажатии на кнопки мы получим только сообщение о том, какую функцию мы бы запустили, но самих функций у нас ещё нет. 

Добавим внутреннюю логику в программу, а чтобы было понятнее, как всё устроено, разберём подробно функцию добавления новой записи.

Чтобы добавить новую запись о расходах, в программе должно появиться такое:

  1. С помощью метода .get() считываем, что написано в полях ввода в интерфейсе.
  2. Отправляем и сохраняем их в базе.
  3. Обновляем общий список покупок.

Запишем это в виде кода:

# обработчик нажатия на кнопку «Добавить»
def add_command():         
    # добавляем запись в БД
    db.insert(product_text.get(), price_text.get(), comment_text.get()) 
    # обновляем общий список в приложении
    view_command()

В конце мы добавили функцию обновления списка, которой у нас ещё нет в коде, — view_command(). Её задача — очистить список и заполнить его новой информацией из базы данных. Добавим её в код:

# обработчик нажатия на кнопку «Посмотреть всё»
def view_command():         
    # очищаем список в приложении
    list1.delete(0, END)    
    # проходим все записи в БД
    for row in db.view():   
        # и сразу добавляем их на экран
        list1.insert(END, row) 

Нам осталось заменить команду print в обработчике действий кнопки на название нашей функции, чтобы кнопка работала как нужно:

b3 = Button(window, text="Добавить", width=12, command=add_command)

С этого момента мы можем добавлять новые записи о расходах, а программа будет сразу запоминать их в свою базу данных:

Делаем своё приложение для ведения бюджета

Для работы остальных кнопок нам понадобится функция, которая определит, какую именно запись в списке мы выделили. Ещё мы сразу будем отправлять выделенные данные в соответствующие поля ввода, чтобы было видно, что мы хотим изменить или удалить:

# заполняем поля ввода значениями выделенной позиции в общем списке
def get_selected_row(event): 
    # будем обращаться к глобальной переменной
    global selected_tuple
    # получаем позицию выделенной записи в списке
    index = list1.curselection()[0] #this is the id of the selected tuple
    # получаем значение выделенной записи
    selected_tuple = list1.get(index) 
    # удаляем то, что было раньше в поле ввода
    e1.delete(0, END)                
    # и добавляем туда текущее значение названия покупки
    e1.insert(END, selected_tuple[1]) 
    # делаем то же самое с другими полями
    e2.delete(0, END)
    e2.insert(END, selected_tuple[2]) 
    e3.delete(0, END)
    e3.insert(END, selected_tuple[3]) 
# привязываем выбор любого элемента списка к запуску функции выбора
list1.bind('<<ListboxSelect>>', get_selected_row)

Теперь у нас всё готово к тому, чтобы добавить жизнь во все остальные кнопки и привязать их к реальным функциям. Главное — не забудьте поменять вызовы в кнопках с print() на названия новых функций:

# обработчик нажатия на кнопку «Поиск»
def search_command():       
    # очищаем список в приложении
    list1.delete(0, END)   
    # находим все записи по названию покупки
    for row in db.search(product_text.get()):
        # и добавляем их в список в приложение
        list1.insert(END, row) 

# обработчик нажатия на кнопку «Удалить»
def delete_command(): 
    # удаляем запись из базы данных по индексу выделенного элемента
    db.delete(selected_tuple[0]) 
    # обновляем общий список расходов в приложении
    view_command()

# обработчик нажатия на кнопку «Обновить»
def update_command():
    # обновляем данные в БД о выделенной записи
    db.update(selected_tuple[0], product_text.get(), price_text.get()) 
    # обновляем общий список расходов в приложении
view_command()
Делаем своё приложение для ведения бюджета
Теперь работают все кнопки в программе. Красота

Добавляем вопрос при выходе

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

Для этого добавим в код функцию и системное сообщение: при закрытии окна будет появляться сообщение с подтверждением. Если пользователь нажимает на «OK» — значит, всё в порядке и можно закрывать окно и освобождать память. В параметрах последней кнопки тоже заменим вывод текста на настоящую команду on_closing:

b6 = Button(window, text="Закрыть", width=12, command=on_closing)

# обрабатываем закрытие окна
def on_closing(): 
    # показываем диалоговое окно с кнопкой
    if messagebox.askokcancel("", "Закрыть программу?"): 
        # удаляем окно и освобождаем память
        window.destroy()
        
# сообщаем системе о том, что делать, когда окно закрывается
window.protocol("WM_DELETE_WINDOW", on_closing)
Делаем своё приложение для ведения бюджета

# подключаем библиотеку для работы с базой данных
import sqlite3                   
# подключаем графическую библиотеку для создания интерфейсов
from tkinter import *            
from tkinter import messagebox


# создаём класс для работы с базой данных
class DB:                        
    # конструктор класса
    def __init__(self):           
        # соединяемся с файлом базы данных
        self.conn = sqlite3.connect("mybooks.db")  
        # создаём курсор для виртуального управления базой данных
        self.cur = self.conn.cursor()    
        # если нужной нам таблицы в базе нет — создаём её
        self.cur.execute(             
            "CREATE TABLE IF NOT EXISTS buy (id INTEGER PRIMARY KEY, product TEXT, price TEXT, comment TEXT)") 
        # сохраняем сделанные изменения в базе
        self.conn.commit()  

    # деструктор класса
    def __del__(self):        
        # отключаемся от базы при завершении работы
        self.conn.close()   
   
    # просмотр всех записей
    def view(self):        
        # выбираем все записи о покупках
        self.cur.execute("SELECT * FROM buy") 
        # собираем все найденные записи в колонку со строками
        rows = self.cur.fetchall()  
        # возвращаем сроки с записями расходов
        return rows

    # добавляем новую запись
    def insert(self, product, price, comment):  
        # формируем запрос с добавлением новой записи в БД
        self.cur.execute("INSERT INTO buy VALUES (NULL,?,?,?)", (product, price, comment,)) 
        # сохраняем изменения
        self.conn.commit()
        

    # обновляем информацию о покупке
    def update(self, id, product, price):    
        # формируем запрос на обновление записи в БД
        self.cur.execute("UPDATE buy SET product=?, price=? WHERE id=?", (product, price, id,))
        # сохраняем изменения 
        self.conn.commit()

    # удаляем запись
    def delete(self, id):                  
        # формируем запрос на удаление выделенной записи по внутреннему порядковому номеру
        self.cur.execute("DELETE FROM buy WHERE id=?", (id,))
        # сохраняем изменения
        self.conn.commit()

    # ищем запись по названию покупки
    def search(self, product="", price=""):  
        # формируем запрос на поиск по точному совпадению
        self.cur.execute("SELECT * FROM buy WHERE product=?", (product,))
        # формируем полученные строки и возвращаем их как ответ
        rows = self.cur.fetchall()
        return rows

# создаём новый экземпляр базы данных на основе класса
db = DB()  

# заполняем поля ввода значениями выделенной позиции в общем списке
def get_selected_row(event): 
    # будем обращаться к глобальной переменной
    global selected_tuple
    # получаем позицию выделенной записи в списке
    index = list1.curselection()[0] #this is the id of the selected tuple
    # получаем значение выделенной записи
    selected_tuple = list1.get(index) 
    # удаляем то, что было раньше в поле ввода
    e1.delete(0, END)                
    # и добавляем туда текущее значение названия покупки
    e1.insert(END, selected_tuple[1]) 
    # делаем то же самое с другими полями
    e2.delete(0, END)
    e2.insert(END, selected_tuple[2]) 
    e3.delete(0, END)
    e3.insert(END, selected_tuple[3]) 

# обработчик нажатия на кнопку «Посмотреть всё»
def view_command():         
    # очищаем список в приложении
    list1.delete(0, END)    
    # проходим все записи в БД
    for row in db.view():   
        # и сразу добавляем их на экран
        list1.insert(END, row)  

# обработчик нажатия на кнопку «Поиск»
def search_command():       
    # очищаем список в приложении
    list1.delete(0, END)   
    # находим все записи по названию покупки
    for row in db.search(product_text.get()):
        # и добавляем их в список в приложение
        list1.insert(END, row) 

# обработчик нажатия на кнопку «Добавить»
def add_command():         
    # добавляем запись в БД
    db.insert(product_text.get(), price_text.get(), comment_text.get()) 
    # обновляем общий список в приложении
    view_command()

# обработчик нажатия на кнопку «Удалить»
def delete_command(): 
    # удаляем запись из базы данных по индексу выделенного элемента
    db.delete(selected_tuple[0]) 
    # обновляем общий список расходов в приложении
    view_command()

# обработчик нажатия на кнопку «Обновить»
def update_command():
    # обновляем данные в БД о выделенной записи
    db.update(selected_tuple[0], product_text.get(), price_text.get()) 
    # обновляем общий список расходов в приложении
    view_command()

# подключаем графическую библиотеку
window = Tk() 
# заголовок окна
window.title("Бюджет 0.1") 

# обрабатываем закрытие окна
def on_closing(): 
    # показываем диалоговое окно с кнопкой
    if messagebox.askokcancel("", "Закрыть программу?"): 
        # удаляем окно и освобождаем память
        window.destroy()
        
# сообщаем системе о том, что делать, когда окно закрывается
window.protocol("WM_DELETE_WINDOW", on_closing)  

# создаём надписи для полей ввода и размещаем их по сетке
l1 = Label(window, text="Название") 
l1.grid(row=0, column=0) 

l2 = Label(window, text="Стоимость")
l2.grid(row=0, column=2)

l3 = Label(window, text="Комментарий")
l3.grid(row=1, column=0)

# создаём поле ввода названия покупки, говорим, что это будут строковые переменные и размещаем их тоже по сетке
product_text = StringVar()
e1 = Entry(window, textvariable=product_text)
e1.grid(row=0, column=1)

# то же самое для комментариев и цен
price_text = StringVar() 
e2 = Entry(window, textvariable=price_text)
e2.grid(row=0, column=3)

comment_text = StringVar() 
e3 = Entry(window, textvariable=comment_text)
e3.grid(row=1, column=1)

# создаём список, где появятся наши покупки, и сразу определяем его размеры в окне
list1 = Listbox(window, height=25, width=65) 
list1.grid(row=2, column=0, rowspan=6, columnspan=2) 

# на всякий случай добавим сбоку скролл, чтобы можно было быстро прокручивать длинные списки
sb1 = Scrollbar(window) 
sb1.grid(row=2, column=2, rowspan=6)

# привязываем скролл к списку
list1.configure(yscrollcommand=sb1.set) 
sb1.configure(command=list1.yview)

# привязываем выбор любого элемента списка к запуску функции выбора
list1.bind('<<ListboxSelect>>', get_selected_row)

# создаём кнопки действий и привязываем их к своим функциям
# кнопки размещаем тоже по сетке
b1 = Button(window, text="Посмотреть все", width=12, command=view_command) 
b1.grid(row=2, column=3) #size of the button

b2 = Button(window, text="Поиск", width=12, command=search_command)
b2.grid(row=3, column=3)

b3 = Button(window, text="Добавить", width=12, command=add_command)
b3.grid(row=4, column=3)

b4 = Button(window, text="Обновить", width=12, command=update_command)
b4.grid(row=5, column=3)

b5 = Button(window, text="Удалить", width=12, command=delete_command)
b5.grid(row=6, column=3)

b6 = Button(window, text="Закрыть", width=12, command=on_closing)
b6.grid(row=7, column=3)

# обновляем общий список расходов
view_command()

# пусть окно работает всё время до закрытия
window.mainloop() 

Что дальше

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

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

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

Любишь Python? Зарабатывай на нём!
Изучите самый модный язык программирования и станьте крутым бэкенд-разработчиком. Старт — бесплатно.
Попробовать бесплатно
Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём! Любишь Python? Зарабатывай на нём!
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
easy