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.