Build desktop app using Python and HTML

Building desktop apps can be a daunting task if you are coming from the web development world and have little knowledge in Python.

Luckily though, you can build desktop apps using Python, HTML, Javascript, and Tailwindcss. Let me show you how.

Introduction

Well, getting started with this journey is pretty easy. With some underlying programming knowledge, a little of python skills and Chatgpt as your best friend, you can build a desktop app with less hassle.

First and foremost, allow me to introduce you to an important python package called `python eel` https://github.com/python-eel/Eel , tailwindcss https://tailwindcss.com/ and alphinejs https://alpinejs.dev/ . Those are teh `accessories` what we shall use to creat our desktop app.

Setting up the environment

  1. Create a folder called `desktop`

  2. Open the folder in vscode. https://code.visualstudio.com/download

  3. Install Python eel (I am assuming you installed python already) by running `pip install eel`

  4. Create `web` directory

  5. Download Tailwindcss here and save it to the `web` Directory .

  6. Similarly, Download Alphinejs here

  7. in the root directory, that is in desktop folder, create main.py file.

  8. Inside the web directory create index.html

Coding

Lets begin by setting up index.html

<!-- index.html -->
<html>
<head>
    <script type="text/javascript" src="/eel.js"></script>
    <link rel="stylesheet" href="./tailwind.css">
    <script defer src="./alphine.js" ></script>
</head>
<body>
 Destop application 
</body>
</html>

In the above code, you can see there is reference to '/eel.js' yet We did not create. What the hell do you think is going on here?

Well, no need to worry, eel will inject automatically the `eel.js` to take care of its own stuff under the hood.

Now lets go ahead and setup main.py

import eel

eel.init('web') # Tell eel to use web dir as frontend

#expose a function to be accesed from frontend use @eel expose like so:

@eel.expose
def getAll():
    eel.update_Alphine_Table_data(db.selectAll()) # Notice eel. here, tis function has been exposed by js

# Now launch the application
if __name__ == '__main__':
    eel.start('index.html', size=(1920, 1080))

Now let me show you how you can expose and use eel in the frontend.Let head back to our index.html and create a script tag

<script type="text/javascript">

function fetchDataFromServer() {
        // Fetch or compute data from the server
        eel.getAll() // Here we are calling a function from main.py
 }

// to expose your own function
eel.expose(exposedFaromJS)
function exposedFaromJS() {
   alert("You exposed me, didn't you?")
}

</script>

Until the ladies and gentlemen that is a quick intro to python eel and alphinejs

Pos Example

Well, while learning myself, I created a simple POS system and I am going to share the example with you

<!-- index.html -->
<html>
<head>
    <script type="text/javascript" src="/eel.js"></script>
    <link rel="stylesheet" href="./style.css">
    <script defer src="./alphine.js" ></script>
</head>
<body class=" w-full relative h-screen w-full flex items-center justify-center" x-data="menu">

<div class="w-64 fixed left-0 h-full  top-0 bg-green-100 z-50">
    <h4 class="px-4 text-2xl font-bold text-green-500 py-2 mb-10 mt-10">MAIN MENU</h4>

    <ul class="flex flex-col gap-2">
        <li x-on:click="switchPage('/')" :class="page==='/' ? 'text-green-500' : 'text-gray-500'" class="w-full px-4 py-2 cursor-pointer">Home</li>
        <li x-on:click="switchPage('products')" :class="page==='products' ? 'text-green-500' : 'text-gray-500'" class="w-full px-4 py-2 cursor-pointer">Products</li>
        <li x-on:click="switchPage('sales')" :class="page==='sales' ? 'text-green-500' : 'text-gray-500'" class="w-full px-4 py-2 cursor-pointer">Sales</li>
        <li x-on:click="switchPage('orders')" :class="page==='orders' ? 'text-green-500' : 'text-gray-500'" class="w-full px-4 py-2 cursor-pointer">Orders</li>
        <li x-on:click="switchPage('analytics')" :class="page==='analytics' ? 'text-green-500' : 'text-gray-500'" class="w-full px-4 py-2 cursor-pointer">Analytics</li>
    </ul>
</div>


    <!-- <div id="result"></div> -->

<div class="w-1/5  border flex flex-wrap justify-between items-center px-4 rounded-xl shadow" style="height: 400px;" x-show="page==='/'">



    <button class="px-10 py-2 text-xl bg-green-500 rounded text-white shadow-md" x-on:click="page='sales'">Sales</button>
    <button class="px-10 py-2 text-xl bg-red-500 rounded text-white shadow-md" x-on:click="page='products'">Products</button>
    <button class="px-10 py-2 text-xl bg-indigo-500 rounded text-white shadow-md" x-on:click="page='orders'">Orders</button>
    <button class="px-10 py-2 text-xl bg-blue-500 rounded text-white shadow-md" x-on:click="page='analytics'">Analytics</button>
</div>


<div class="products w-full h-screen bg-gray-50"  x-show="page==='products'">
    <div class="container  px-4 lg:px-20 mx-auto">
        <h4 class="py-8  text-4xl text-teal-500 font-semibold text-center w-full ">Products</h4>
        <div class="flex gap-10 items-center">
            <div class="w-1/4">
               <div class="w-full" >
                   <div class="px-2 py-3 flex flex-col gap-3 w-full">
                       <label>Barcode</label>
                        <div id="result"></div>
                       <input type="number"

                       x-model="form.barcode"
                       class=" barcode px-3 py-2 border shadow w-full" />
                   </div>
                   <div class="px-2 py-3 flex flex-col gap-3 w-full">
                       <label>Product Name</label>
                       <input type="text"
                       x-model="form.name" class="product-name px-3 py-2 border shadow w-full" />
                   </div>
                   <div class="px-2 py-3 flex flex-col gap-3 w-full">
                       <label>Product Price</label>
                       <input
                       x-model="form.price"
                        type="text" class="product-price px-3 py-2 border shadow w-full" />
                   </div>
                   <div class="px-2 py-3 flex flex-col gap-3 w-full">
                       <label>Product Quantity</label>
                       <input 
                       x-model="form.quantity"
                       type="number" class="product-quantity px-3 py-2 border shadow w-full" />
                   </div>
                <div class="px-2 py-3 flex flex-col gap-3 w-full">
                    <button  class="px-4 py-2 text-center bg-green-500 text-center" x-on:click="submitData()">Submit</button>
                </div>

            </div>
            </div>
            <div class="w-3/4">
              <div class="w-full flex flex-col bg-white  overflow-y-auto" style="height: 600px;">

                <table>
                    <thead>

                        <tr>
                            <th class="text-left" x-text="tableData.length">#</th>
                            <th class="text-left">Barcode</th>
                            <th class="text-left">Name</th>
                            <th class="text-left">Quantity</th>
                            <th class="text-left">Price</th>
                            <th class="text-left">Actions</th>
                        </tr>
                    </thead>

                    <tbody>

                        <template x-for="i in tableData">
                            <tr >
                                <td class="text-left py-2 px-1" x-text="i[0]"></td>
                                <td class="text-left py-2 px-1" x-text="i[1]"></td>
                                <td class="text-left py-2 px-1" x-text="i[2]"></td>
                                <td class="text-left py-2 px-1" x-text="i[3]"></td>
                                <td class="text-left py-2 px-1" x-text="i[4]"></td>
                                <td class="text-left py-2 px-1">
                                    <div class="flex gap-4">
                                        <span class="text-blue-500">
                                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34"></path><polygon points="18 2 22 6 12 16 8 16 8 12 18 2"></polygon></svg>
                                        </span>
                                        <span class="text-green-500">
                                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="20.5" r="1"/><circle cx="18" cy="20.5" r="1"/><path d="M2.5 2.5h3l2.7 12.4a2 2 0 0 0 2 1.6h7.7a2 2 0 0 0 2-1.6l1.6-8.4H7.1"/></svg>
                                        </span>
                                        <span class="text-red-500">
                                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
                                        </span>
                                    </div>
                                </td>
                            </tr>
                        </template>

                    </tbody>
                </table>
              </div>
            </div>
       </div>
    </div>


</div>

<div class="products w-full h-screen bg-gray-50"  x-show="page==='sales'">
    <div class="container px-4 lg:px-20 mx-auto ">
        <h4 class="py-8  text-4xl text-teal-500 font-semibold text-center w-full ">Sales</h4>
        <div class="flex gap-4 items-center">
            <div class="w-1/3 p-4">
                <div class="input">
                    <input id="" type="text" x-model="cart_code"
                    @keydown.enter ="addToCart()" 
                    class="w-full border shadow px-3 py-2" placeholder="scan code">
                </div>
               <table class="table w-full bg-white cart">
                   <thead>
                       <tr>
                           <th class="text-left  px-3 py-2">#</th>
                           <th class="text-left  px-3 py-2">Name</th>
                           <th class="text-left  px-3 py-2">Quantity</th>
                           <th class="text-left  px-3 py-2">Price</th>
                           <th class="text-left  px-3 py-2">Total</th>
                       </tr>
                   </thead>

                   <tbody>
                    <template x-for="cartItem in cart_items">
                        <tr>
                            <td class="text-left px-3 py-2" x-text="cartItem[0]"></td>
                            <td class="text-left px-3 py-2" x-text="cartItem[2]">Name</td>
                            <td class="text-left px-3 py-2">
                                <div class="flex gap-3 items-center">
                                    <span class="h-6 w-6 flex items-center justify-center shadow border">
                                        <svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>
                                    </span>
                                    <span>
                                        1
                                    </span>
                                    <span class="h-6 w-6 flex items-center justify-center  shadow border">
                                        <svg class="h-4 w-4 "  xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
                                    </span>
                                </div>
                            </td>
                            <td class="text-left px-3 py-2" x-text="cartItem[4]">100</td>
                            <td class="text-left px-3 py-2" x-text="1 *cartItem[4]">100</td>
                        </tr>
                    </template>

                   </tbody>
               </table>
               <div class="flex justify-between mt-4" x-show="cart_items.length > 0">
                <button class="bg-red-500 text-white px-4 py-1.5 rounded shadow">Pay</button>
                <button class="bg-green-500 text-white px-4 py-1.5 rounded shadow" @click="printReceipt">Print Receipt</button>
               </div>
            </div>
            <div class="w-2/3 flex flex-col">
              <div class="flex items-center gap-4">
                <input type="search" class="px-4 py-2 border bg-white" placeholder="Search by code"/>
              </div>

               <table class="table w-full mt-6 bg-white">
                <thead>
                    <tr>
                        <th class="text-left" x-text="tableData.length">#</th>
                        <th class="text-left">Barcode</th>
                        <th class="text-left">Name</th>
                        <th class="text-left">Quantity</th>
                        <th class="text-left">Price</th>
                        <th class="text-left">Actions</th>
                    </tr>
                </thead>


                    <tr>
                        <td class="text-left py-2 px-1">1</td>
                        <td class="text-left py-2 px-1">12345678</td>
                        <td class="text-left py-2 px-1">Ream paper</td>
                        <td class="text-left py-2 px-1">100</td>
                        <td class="text-left py-2 px-1">600</td>
                        <td class="text-left py-2 px-1">
                            <div class="flex gap-4">
                                <span class="text-blue-500 hidden">
                                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34"></path><polygon points="18 2 22 6 12 16 8 16 8 12 18 2"></polygon></svg>
                                </span>
                                <button class="flex items-center gap-3">
                                    <span class="text-green-500">
                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="20.5" r="1"/><circle cx="18" cy="20.5" r="1"/><path d="M2.5 2.5h3l2.7 12.4a2 2 0 0 0 2 1.6h7.7a2 2 0 0 0 2-1.6l1.6-8.4H7.1"/></svg>
                                    </span>
                                    <span>Add</span>
                                </button>

                                <span class="text-red-500 hidden">
                                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
                                </span>
                            </div>
                        </td>
                    </tr>
                </tbody>
            </table>
            </div>
       </div>
    </div>
</div>

<div class="order" x-show="page==='orders'">
    <h4>Orders page will be here</h4>
</div>
<div class="analytics" x-show="page==='analytics'">
    <h4>Analytics page will be here</h4>
</div>


<script type="text/javascript">






function fetchDataFromServer() {
        // Fetch or compute data from the server
        eel.getAll()
 }
 function updateTableData(tableData) {
        // console.log(  Alpine.store('tableData'))
        Alpine.store('tableData', tableData);
}

function TestUpdate(tData) {
    console.log(Alpine.store('tableData'))
}

    function listenForCustomEvent(that) {
        window.addEventListener('custom-event', (event) => {
            // Assuming event.detail contains the updated data
          that.tableData = event.detail;

        });
    }


eel.expose(startListeningPython);
function startListeningPython() {
    eel.startListening()
    eel.start_barcode_listener();
    alert("Start")
}

eel.expose(stopListeningPython);
function stopListeningPython() {
    eel.stopListening()
    eel.start_barcode_listener();
    alert("Stop")
}

async  function updateCart(that) {
 const product = await   eel.getOneByBarcode(that.cart_code)();
 that.cart_items.push(product)
 that.cart_code = '';
 alert(that.cart_items.length)
}

document.addEventListener('alpine:init', () => {
        Alpine.data('menu', () => ({
            init() {
                this.updateTableData();
                this.listenForCustomEvent();
                // this.$watch('cart_code', value => alert(value))
            },
            updateTableData() {
                this.tableData = fetchDataFromServer();
            },
            listenForCustomEvent() {
                listenForCustomEvent(this);
            },
             addToCart() {
                updateCart(this)

            },
            printReceipt() {
                eel.printReceipt(this.cart_items)
            },

             page: '/',
             tableData:[],
             cart_items:[],
             cart_code:'',
             form: {
                  barcode:'',
                  name:"",
                  price:'',
                  quantity:''
             },

             submitData() {

                eel.submitForm(this.form.barcode, this.form.name, this.form.price,this.form.quantity)

             },

             switchPage(page) {
                // if(page==='products') {
                //     startListeningPython()
                // } else {
                //     stopListeningPython()
                //     page='products'
                // }

                this.page = page
            }
        }))
    })

    eel.expose(display_barcode_result);
    function display_barcode_result(result) {

        document.getElementById('result').value = result;
    }

    eel.expose(update_Alphine_Table_data)
    function  update_Alphine_Table_data(items) {
        let event = new CustomEvent('custom-event', { detail: items });
        window.dispatchEvent(event);
    }


    eel.start_barcode_listener();
</script>

</body>
</html>
import eel
import keyboard
import barcode
import controller.printer as printer



import sqlite3

class Database:
    def __init__(self,db_name="products"):
        self.db_name="products"
        self.con=sqlite3.connect(db_name +'.db')
        self.cursor = self.con.cursor()
        print("Connection initialized")

    def createTable(self,tablename):
        self.tablename=tablename
        query = """ CREATE TABLE IF NOT EXISTS PRODUCTS (
        id INTEGER PRIMARY KEY,
        code TEXT,
        name TEXT,
        quantity INTEGER,
        price INTEGER
        ) """
        self.cursor.execute(query)
        self.con.commit()
    def insertRecords(self,barcode, name, price, quantity):
        query = "INSERT INTO products (code, name, quantity, price) VALUES (?,?, ?, ?)"
        data = (barcode, name, quantity, price)
        self.cursor.execute(query, data)
        self.con.commit()
        # self.con.close()

    def insertMany(self):
        records = [(2,
            '123456900', 'staple pins', 10, 50),
            (3,
            '12345678001', 'staple pins', 10, 50)]
        query = """ INSERT INTO products VALUES(?,?,?,?,?)"""
        self.cursor.executemany(query,records)
        self.con.commit()
        # self.con.close()

    def selectAll(self):
        query ="""SELECT * FROM products"""
        self.cursor.execute(query)
        return (self.cursor.fetchall())

    def getProductByBarcode(self, barcode):
        query = "SELECT * FROM products WHERE code = ?"
        self.cursor.execute(query, (barcode,))
        return self.cursor.fetchone()



db = Database('pos2')
db.createTable('products')
# db.insertRecords()
# print("All", db.selectAll())


eel.init('web')
scannedText = ''
stopListening = False



@eel.expose
def getAll():
    eel.update_Alphine_Table_data(db.selectAll()) 


@eel.expose 
def printReceipt(items):
    # text_to_print = """BORATECHLIFE LIMITED.\n
    # Printing  ksh 10, \n
    # Photocopy  ksh 10.\n\n\n
    # """

    text_to_print = """ BORATECHLIFE LIMTED \n"""
    for i in items:
        text_to_print +=  f"""ID. {i[0]} \t {i[2]} \t {i[3]} \t {i[4]}.\n"""


    print(text_to_print)
    text_to_print +="\n\n\n"
    printer.printSomething(text_to_print = text_to_print)






@eel.expose
def getOneByBarcode(barcode):
    product = db.getProductByBarcode(barcode)
    print("Product",product)
    return product

@eel.expose
def submitForm(barcode, name, price, quantity):
    db.insertRecords(barcode=barcode, name=name, price=price, quantity=quantity)
    print(db.selectAll())
    eel.update_Alphine_Table_data(db.selectAll()) 


@eel.expose
def startListening():
    global stopListening
    stopListening = False

@eel.expose   
def stopListening():
    global stopListening
    stopListening = True




@eel.expose
def start_barcode_listener():
    global stopListening
    global barcode_data
    global scannedText
    barcode_data=''
    while True:
        if(stopListening):
            break

        event = keyboard.read_event(suppress=True)
        if event.event_type == keyboard.KEY_DOWN:
            if(event.name =='enter'):
                print("Barcode scaanning ended", barcode_data)
                scannedText = barcode_data
                barcode_data=''
                eel.display_barcode_result(scannedText)  # Call JavaScript function to display result
                stopListening = True
                print("Barcode scaanning ended",scannedText)

            else:
                barcode_data+=event.name



if __name__ == '__main__':
    eel.start('index.html', size=(1920, 1080))

printer.py

# import win32print

# def print_rongta(text, printer_name):
#     printer_handle = win32print.OpenPrinter(printer_name)

#     try:
#         hprinter = win32print.GetPrinter(printer_handle, 2)['pPrinterName']

#         # Send ESC/POS commands for setting font size and printing text
#         esc_pos_commands = b'\x1B\x40'  # Initialize printer
#         esc_pos_commands += b'\x1B\x21\x08'  # Set font size to double width and height
#         esc_pos_commands += text.encode('cp437')  # Encode text using CP437 character set
#         esc_pos_commands += b'\n\n\n'  # Print additional lines
#         esc_pos_commands += b'\x1D\x56\x00'  # Paper cut command

#         win32print.StartDocPrinter(printer_handle, 1, (hprinter, None, "RAW"))
#         win32print.StartPagePrinter(printer_handle)

#         win32print.WritePrinter(printer_handle, esc_pos_commands)

#         win32print.EndPagePrinter(printer_handle)
#         win32print.EndDocPrinter(printer_handle)

#     finally:
#         win32print.ClosePrinter(printer_handle)

# if __name__ == "__main__":
#     printer_name = "RONGTA 80mm Series Printer"  # Replace with your printer's name
#     text_to_print = """BORATECHLIFE LIMITED. \n 
#     Printing  ksh 10, \n 
#     Photocopy  ksh 10.\n \n"""

#     print_rongta(text_to_print, printer_name)



import win32print

def print_rongta(text, printer_name):
    printer_handle = win32print.OpenPrinter(printer_name)

    try:
        hprinter = win32print.GetPrinter(printer_handle, 2)['pPrinterName']

        # Send ESC/POS commands for setting font size and printing text
        esc_pos_commands = b'\x1B\x40'  # Initialize printer
        esc_pos_commands += b'\x1B\x21\x08'  # Set font size to double width and height

        # Split the text into multiple lines and encode each line
        lines = text.split('\n')
        for line in lines:
            centered_line = line.center(40)  # Center the line within 40 characters
            esc_pos_commands += centered_line.encode('cp437') + b'\n'  # Encode centered line and add newline

        esc_pos_commands += b'\x1D\x56\x00'  # Paper cut command

        win32print.StartDocPrinter(printer_handle, 1, (hprinter, None, "RAW"))
        win32print.StartPagePrinter(printer_handle)

        win32print.WritePrinter(printer_handle, esc_pos_commands)

        win32print.EndPagePrinter(printer_handle)
        win32print.EndDocPrinter(printer_handle)

    finally:
        win32print.ClosePrinter(printer_handle)


def printSomething(text_to_print): 

    printer_name = "RONGTA 80mm Series Printer"  # Replace with your printer's name


    print_rongta(text_to_print, printer_name)

That is the example I created. I hope it will help you understand the concept behind alphine, html, python and Tailwindcss. To see A working version here is:https://www.linkedin.com/posts/kiprono-denis-138562185_milestone-1-ai-pos-desktop-app-mvp12092023-activity-7107350309663875073-_3or?utm_source=share&utm_medium=member_desktop

To get help developing Desktop apps from me. You can as well find me on fiverr here