How to use WebSockets with Python and GTK+

Hey there! I’m here to present you an amazing trip to the native apps with WebSockets!

In this current case we will be using these technologies:

  • Python
  • GTK+
  • Libsoup
  • Websocket messaging

If you’re wondering how to build a native GTK+ chat app, let’s dive with me!

Theory

WebSockets is a protocol designed to provide a consistent connection between the client and a server where both can send data to each other. It’s like a wire over the Internet or a tube, distinct of regular HTTP, where the client only can send data to the server and it gets a response immediately.

Benefits of this approach are quite obvious: the server can push the data to the client when it comes to server from another client or generated somewhere on the backend. And the client doesn’t need to refresh the page or do something to fetch new data, it just comes to the client by itself.

More information about WebSockets you can find on the websocket.org.

What I want to show you in this article it is how to connect to a WebSocket server and send or receive data from it in a native GTK+ application.

I will code my app in elementary OS using GNOME Builder, but the same logic should work anywhere where GTK+ and libsoup are available and all code could be easily adapted even for macOS.

UI

The app logic is quite simple yet representative. The UI will consist of two screens (Gtk.Grid) placed in Gtk.Stack. The first screen contains widgets required to connect to the server such as Gtk.Entry for server address, button to initiate a connection and a spinner to show the state.

First page

The second screen is more complex. Purpose of this is to enter the message, send it to the server and show the server’s response in Gtk.TextView. Thus we need Gtk.Grid to place widgets. Gtk.Entry and Gtk.Button the same way we did it on the first page. And to display log we need Gtk.TextView placed within Gtk.ScrolledWindow in case there will be much more logs than the window can contain and the scrolling will be as much useful feature as it could :)

Second page

Connection page

Let’s start building! Create a new project in Builder, as the language, we will use Python 3.6+.

New project

IDE will generate a base app structure with all we need, and all we need is just two files: window.ui and window.py.

Firstly, we need a correct UI for our app, so open window.ui. file and replace the “hello world” Label with Gtk.Stack. We will need two pages.

Stack properties

Name the first screen “connection”, and place onto it Gtk.Grid with 3 rows and columns. In the central cell place Gtk.Box with 4 rows:

  1. Gtk.Label with text “Connection”
  2. Gtk.Entry. Set its id to “host_entry”
  3. Gtk.Button with id “connect_btn”
  4. Gtk.Spinner. Call it whatever you like, mine is just a “spinner” :)

That’s enough for the “Connection” page.

Chat page

Moving further. The second screen is a bit more complex. Again, we need Gtk.Grid to put all the widgets we need. But for now, it will be a grid with just 2 rows and 2 columns.

First row with two widgets in its own cells:

  • Gtk.Entry with id “message_entry”
  • Gtk.Button with id “send_btn”

On the second row place Gtk.ScrolledWindow, no id required because we don’t need to call its method. Inside this ScrolledWindow put a new Gtk.TextView widget. Set its id to “log_view”.

To make it looks more convenient, set width = 2 in the container’s props inside attribute editor for ScrolledWindow.

ScrolledWindow packing

Logic

Ok, out UI is ready enough to build some logic for it, finally :)

Open window.py and add class vars to connect with widgets from the .ui file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Stack with pages
screens = Gtk.Template.Child()

# Connection page widgets
host_entry = Gtk.Template.Child()
connect_btn = Gtk.Template.Child()
disconnect_btn = Gtk.Template.Child()
spinner = Gtk.Template.Child()

# Chat page widgets
send_btn = Gtk.Template.Child()
message_entry = Gtk.Template.Child()
log_view = Gtk.Template.Child()

# Declare some class-vars
self.session = None
self.connection = None

Well, now we are able to call methods of these widgets. But also, we have to handle its signals. Let’s connect “connect_btn”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def __init__(self, **kwargs):
    # Some app logic

    self.connect_btn.connect('clicked', self.on_connect_clicked)

def on_connect_clicked(self, widget):
    # Check if there is a text inside the host_entry
    # and grab focus if its empty
    if not self.host_entry.get_text():
        self.host_entry.grab_focus()
        return

    # Show user we are trying to connect
    self.spinner.start()

    # Init libsoup Session, create message and start connection
    self.session = Soup.Session()
    msg = Soup.Message.new("GET", self.host_entry.get_text())
    self.session.websocket_connect_async(msg, None, None, None, self.on_connection)

The code above is quite straightforward:

As a callback for connection function, we put on_connection method, which takes the result of this operation and finish the process.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def on_connection(self, session, result):
    try:
        # Finish connection process
        self.connection = session.websocket_connect_finish(result)
        
        # If we here, the connection went correctly
        # Connect `message` handler to the connection
        self.connection.connect('message', self.on_message)
        
        # Show to user all is OK and switch to the Chat page
        self.screens.set_visible_child_name('chat')
        self.disconnect_btn.set_visible(True)
    
    except Exception as e:
        # In case something goes wrong just print an exception
        print(e)
        self.session = None
    
    finally:
        # Don't forget to show a user that process is finished.
        self.spinner.stop()

We are almost done! All we need to do more is to handle click event from the “send_btn” and display messages in the “log_view”.

1
2
3
4
5
def on_message(self, connection, msg_type, message):
    msg = f'<b>RECEIVED:</b> {message.get_data().decode()}\n'
    self.buffer.insert_markup(self.buffer.get_start_iter(),
                              msg,
                              len(msg))

Could it be more simple than that? :wink: Just to display a message in a more readable way we add some Pango markup and use insert_markup method of Gtk.TextBuffer to… insert markup!

Last, but not least required thing is a handler for “send_btn” signal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def on_send_clicked(self, widget):
    # Get the message from the Entry
    msg = self.message_entry.get_text()

    # If there is no message - grab focus.
    if not msg:
        self.message_entry.grab_focus()
        return

    # Send a text to the server.
    self.connection.send_text(msg)

For our simple case, all we need is just the send_text method. But libsoup could do a lot of different stuff, such as send_binary or emit a signal if something goes wrong.

Spacebeam

Conclusion

Simple yet descriptive example of how to start building your GTK+ app with WebSockets inside. Maybe you will create a new chat app?

Stay tuned if you want to read more! Or go to the docs from the Links section.

Anyway, good luck!

Source code: https://github.com/amka/pygobject-gtk-websocket-app

Docs: