In this part of the Kivy tutorials, we're going to cover how to schedule tasks to run (either just once, or repeatedly), as well as actually connect to our chat server.
Code up to this point:
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
# to use buttons:
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
import socket_client
kivy.require("1.10.1")
class ConnectPage(GridLayout):
# runs on initialization
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 2 # used for our grid
with open("prev_details.txt","r") as f:
d = f.read().split(",")
prev_ip = d[0]
prev_port = d[1]
prev_username = d[2]
self.add_widget(Label(text='IP:')) # widget #1, top left
self.ip = TextInput(text=prev_ip, multiline=False) # defining self.ip...
self.add_widget(self.ip) # widget #2, top right
self.add_widget(Label(text='Port:'))
self.port = TextInput(text=prev_port, multiline=False)
self.add_widget(self.port)
self.add_widget(Label(text='Username:'))
self.username = TextInput(text=prev_username, multiline=False)
self.add_widget(self.username)
# add our button.
self.join = Button(text="Join")
self.join.bind(on_press=self.join_button)
self.add_widget(Label()) # just take up the spot.
self.add_widget(self.join)
def join_button(self, instance):
port = self.port.text
ip = self.ip.text
username = self.username.text
with open("prev_details.txt","w") as f:
f.write(f"{ip},{port},{username}")
#print(f"Joining {ip}:{port} as {username}")
# Create info string, update InfoPage with a message and show it
info = f"Joining {ip}:{port} as {username}"
chat_app.info_page.update_info(info)
chat_app.screen_manager.current = 'Info'
# Simple information/error page
class InfoPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Just one column
self.cols = 1
# And one label with bigger font and centered text
self.message = Label(halign="center", valign="middle", font_size=30)
# By default every widget returns it's side as [100, 100], it gets finally resized,
# but we have to listen for size change to get a new one
# more: https://github.com/kivy/kivy/issues/1044
self.message.bind(width=self.update_text_width)
# Add text widget to the layout
self.add_widget(self.message)
# Called with a message, to update message text in widget
def update_info(self, message):
self.message.text = message
# Called on label width update, so we can set text width properly - to 90% of label width
def update_text_width(self, *_):
self.message.text_size = (self.message.width * 0.9, None)
class EpicApp(App):
def build(self):
# We are going to use screen manager, so we can add multiple screens
# and switch between them
self.screen_manager = ScreenManager()
# Initial, connection screen (we use passed in name to activate screen)
# First create a page, then a new screen, add page to screen and screen to screen manager
self.connect_page = ConnectPage()
screen = Screen(name='Connect')
screen.add_widget(self.connect_page)
self.screen_manager.add_widget(screen)
# Info page
self.info_page = InfoPage()
screen = Screen(name='Info')
screen.add_widget(self.info_page)
self.screen_manager.add_widget(screen)
return self.screen_manager
if __name__ == "__main__":
chat_app = EpicApp()
chat_app.run()
After we've reached the "connecting to..." bit, we want to actually run the connection and then either show some sort of error or connect and start chatting! We're going to schedule the actual connection 1 second after the user clicks join. To do this, we need to use the Kivy clock:
from kivy.clock import Clock
Then, in our join_button method, we can schedule another method, which we'll do at the very end with:
Clock.schedule_once(self.connect, 1)
The full join_button method is now:
def join_button(self, instance):
port = self.port.text
ip = self.ip.text
username = self.username.text
with open("prev_details.txt","w") as f:
f.write(f"{ip},{port},{username}")
#print(f"Joining {ip}:{port} as {username}")
# Create info string, update InfoPage with a message and show it
info = f"Joining {ip}:{port} as {username}"
chat_app.info_page.update_info(info)
chat_app.screen_manager.current = 'Info'
Clock.schedule_once(self.connect, 1)
Now, we need to create this connect method, which we can just add under our join_button method.
# Connects to the server
# (second parameter is the time after which this function had been called,
# we don't care about it, but kivy sends it, so we have to receive it)
def connect(self, _):
# Get information for sockets client
port = int(self.port.text)
ip = self.ip.text
username = self.username.text
if not socket_client.connect(ip, port, username, show_error):
return
So this will actually connect for us, but we actually want to see messages and stuff. Upon a connection, we need to change the page again to some sort of chatroom window.
# Create chat page and activate it
chat_app.create_chat_page()
chat_app.screen_manager.current = 'Chat'
Our full connect method is now:
# Connects to the server
# (second parameter is the time after which this function had been called,
# we don't care about it, but kivy sends it, so we have to receive it)
def connect(self, _):
# Get information for sockets client
port = int(self.port.text)
ip = self.ip.text
username = self.username.text
if not socket_client.connect(ip, port, username, show_error):
return
# Create chat page and activate it
chat_app.create_chat_page()
chat_app.screen_manager.current = 'Chat'
Okay, now we need to create this chat_page method. But where? We want this in our EpicApp class. Now, you might be wondering why we're not just making yet another screen, like we did with the other two.
This chatroom page is the "final" page in the pipeline of pages, and you can almost think of it as its own app entirely. As soon as we land on this page, we want our own initialization and listening for messages...etc.
So, we'll add a new method called create_chat_page to our EpicApp:
def create_chat_page(self):
self.chat_page = ChatPage()
screen = Screen(name='Chat')
screen.add_widget(self.chat_page)
self.screen_manager.add_widget(screen)
Making the full EpicApp class:
class EpicApp(App):
def build(self):
# We are going to use screen manager, so we can add multiple screens
# and switch between them
self.screen_manager = ScreenManager()
# Initial, connection screen (we use passed in name to activate screen)
# First create a page, then a new screen, add page to screen and screen to screen manager
self.connect_page = ConnectPage()
screen = Screen(name='Connect')
screen.add_widget(self.connect_page)
self.screen_manager.add_widget(screen)
# Info page
self.info_page = InfoPage()
screen = Screen(name='Info')
screen.add_widget(self.info_page)
self.screen_manager.add_widget(screen)
return self.screen_manager
# We cannot create chat screen with other but screens, as it;s init method will start listening
# for incoming connections, but at this stage connection is not being made yet, so we
# call this method later
def create_chat_page(self):
self.chat_page = ChatPage()
screen = Screen(name='Chat')
screen.add_widget(self.chat_page)
self.screen_manager.add_widget(screen)
Now we need this ChatPage class, which will wind up being its own tutorial next, but, for now, let's just throw a label in there to test the code!
class ChatPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 1
self.add_widget(Label(text='Fancy stuff here to come!!!', font_size=30))
Run our socket_server.py first, since we will need a server to connect to. Then, run the kivy code.
NameError: name 'show_error' is not defined
Oh, right, I never defined that. Let's do that real quick. This will just be a standalone function:
# Error callback function, used by sockets client
# Updates info page with an error message, shows message and schedules exit in 10 seconds
# time.sleep() won't work here - will block Kivy and page with error message won't show up
def show_error(message):
chat_app.info_page.update_info(message)
chat_app.screen_manager.current = 'Info'
Clock.schedule_once(sys.exit, 10)
Full code up to this point:
import kivy
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
# to use buttons:
from kivy.uix.button import Button
from kivy.uix.screenmanager import ScreenManager, Screen
import socket_client
from kivy.clock import Clock
kivy.require("1.10.1")
class ConnectPage(GridLayout):
# runs on initialization
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 2 # used for our grid
with open("prev_details.txt","r") as f:
d = f.read().split(",")
prev_ip = d[0]
prev_port = d[1]
prev_username = d[2]
self.add_widget(Label(text='IP:')) # widget #1, top left
self.ip = TextInput(text=prev_ip, multiline=False) # defining self.ip...
self.add_widget(self.ip) # widget #2, top right
self.add_widget(Label(text='Port:'))
self.port = TextInput(text=prev_port, multiline=False)
self.add_widget(self.port)
self.add_widget(Label(text='Username:'))
self.username = TextInput(text=prev_username, multiline=False)
self.add_widget(self.username)
# add our button.
self.join = Button(text="Join")
self.join.bind(on_press=self.join_button)
self.add_widget(Label()) # just take up the spot.
self.add_widget(self.join)
def join_button(self, instance):
port = self.port.text
ip = self.ip.text
username = self.username.text
with open("prev_details.txt","w") as f:
f.write(f"{ip},{port},{username}")
#print(f"Joining {ip}:{port} as {username}")
# Create info string, update InfoPage with a message and show it
info = f"Joining {ip}:{port} as {username}"
chat_app.info_page.update_info(info)
chat_app.screen_manager.current = 'Info'
Clock.schedule_once(self.connect, 1)
# Connects to the server
# (second parameter is the time after which this function had been called,
# we don't care about it, but kivy sends it, so we have to receive it)
def connect(self, _):
# Get information for sockets client
port = int(self.port.text)
ip = self.ip.text
username = self.username.text
if not socket_client.connect(ip, port, username, show_error):
return
# Create chat page and activate it
chat_app.create_chat_page()
chat_app.screen_manager.current = 'Chat'
class ChatPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.cols = 1
self.add_widget(Label(text='Fancy stuff here to come!!!', font_size=30))
# Simple information/error page
class InfoPage(GridLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Just one column
self.cols = 1
# And one label with bigger font and centered text
self.message = Label(halign="center", valign="middle", font_size=30)
# By default every widget returns it's side as [100, 100], it gets finally resized,
# but we have to listen for size change to get a new one
# more: https://github.com/kivy/kivy/issues/1044
self.message.bind(width=self.update_text_width)
# Add text widget to the layout
self.add_widget(self.message)
# Called with a message, to update message text in widget
def update_info(self, message):
self.message.text = message
# Called on label width update, so we can set text width properly - to 90% of label width
def update_text_width(self, *_):
self.message.text_size = (self.message.width * 0.9, None)
class EpicApp(App):
def build(self):
# We are going to use screen manager, so we can add multiple screens
# and switch between them
self.screen_manager = ScreenManager()
# Initial, connection screen (we use passed in name to activate screen)
# First create a page, then a new screen, add page to screen and screen to screen manager
self.connect_page = ConnectPage()
screen = Screen(name='Connect')
screen.add_widget(self.connect_page)
self.screen_manager.add_widget(screen)
# Info page
self.info_page = InfoPage()
screen = Screen(name='Info')
screen.add_widget(self.info_page)
self.screen_manager.add_widget(screen)
return self.screen_manager
# We cannot create chat screen with other screens, as it;s init method will start listening
# for incoming connections, but at this stage connection is not being made yet, so we
# call this method later
def create_chat_page(self):
self.chat_page = ChatPage()
screen = Screen(name='Chat')
screen.add_widget(self.chat_page)
self.screen_manager.add_widget(screen)
# Error callback function, used by sockets client
# Updates info page with an error message, shows message and schedules exit in 10 seconds
# time.sleep() won't work here - will block Kivy and page with error message won't show up
def show_error(message):
chat_app.info_page.update_info(message)
chat_app.screen_manager.current = 'Info'
Clock.schedule_once(sys.exit, 10)
if __name__ == "__main__":
chat_app = EpicApp()
chat_app.run()
End result:
Okay, things are coming together! You should also see on your server-side that you have: Accepted new connection from 127.0.0.1:50490, username: sentdex, or something like that, since we're actually connected and now we just need to handle for sending and receiving information.