Welcome to part 5 of the Kivy tutorials, where we're making a chatroom application. Leading up to this point, we've built out our GUI to connect to the chat room server, and now we're ready to build out this final page to display messages and send new ones!
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 # We are going to use 1 column and 2 rows 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()
Now we want to build out the ChatPage
. To begin, we want 2 rows, where the first row is most of the page, then the 2nd row is where we'll input text and then have 2 columns, where the first column takes up most of the space and the 2nd column is the submit button. Here's a beautiful paint example:
Cool, so we'll start with:
class ChatPage(GridLayout): def __init__(self, **kwargs): super().__init__(**kwargs) # We are going to use 1 column and 2 rows self.cols = 1 self.rows = 2
Easy enough. The first row is all message history, so as messages come in, we'll update them here. For now, we'll call this a Label
:
# First row is going to be occupied by our scrollable label # We want it be take 90% of app height self.history = Label(height=Window.size[1]*0.9, size_hint_y=None) self.add_widget(self.history)
Here, we just set the size of this label to be 90% of the entire screen's height (Window.size[1] is the y amount, the height), reserving the other 10% for row #2. Now, for row #2:
self.new_message = TextInput(width=Window.size[0]*0.8, size_hint_x=None, multiline=False) self.send = Button(text="Send") self.send.bind(on_press=self.send_message)
Similar logic here. For row 2, the message input takes up 80% of the width of the window, leaving the remainder of the space for our send button. Now, we have to actually add these things to our layout. Recall our main layout is 2 rows and 1 column. Thus, we actually need to add a 2 column layout to row 2. I mentioned this earlier in the series, and here's exactly how and why you might do it!
bottom_line = GridLayout(cols=2) bottom_line.add_widget(self.new_message) bottom_line.add_widget(self.send) self.add_widget(bottom_line)
Easy enough! Finally, since we bound some method called send_message
, we better make that before we can test things up to now:
def send_message(self, _): print("send a message!!!")
Full code for ChatPage
class:
class ChatPage(GridLayout): def __init__(self, **kwargs): super().__init__(**kwargs) # We are going to use 1 column and 2 rows self.cols = 1 self.rows = 2 # First row is going to be occupied by our scrollable label # We want it be take 90% of app height self.history = Label(height=Window.size[1]*0.9, size_hint_y=None) self.add_widget(self.history) # In the second row, we want to have input fields and Send button # Input field should take 80% of window width # We also want to bind button click to send_message method self.new_message = TextInput(width=Window.size[0]*0.8, size_hint_x=None, multiline=False) self.send = Button(text="Send") self.send.bind(on_press=self.send_message) # To be able to add 2 widgets into a layout with just one collumn, we use additional layout, # add widgets there, then add this layout to main layout as second row bottom_line = GridLayout(cols=2) bottom_line.add_widget(self.new_message) bottom_line.add_widget(self.send) self.add_widget(bottom_line) # Gets called when either Send button or Enter key is being pressed # (kivy passes button object here as well, but we don;t care about it) def send_message(self, _): print("send a message!!!")
Running this and joining the server, we see:
Okay so we want to start updating the first row with actual chat messages. We can totally update a regular label, but the problem is what happens when .... we have many messages! Eventually we will overflow this label and we'd rather there be a scroll capability. So, instead of extending a Label
, we will just make our own from the ScrollView
from from kivy.uix.scrollview
from kivy.uix.scrollview import ScrollView
# This class is an improved version of Label # Kivy does not provide scrollable label, so we need to create one class ScrollableLabel(ScrollView): def __init__(self, **kwargs): super().__init__(**kwargs) # ScrollView does not allow us to add more than one widget, so we need to trick it # by creating a layout and placing two widgets inside it # Layout is going to have one collumn and and size_hint_y set to None, # so height wo't default to any size (we are going to set it on our own) self.layout = GridLayout(cols=1, size_hint_y=None) self.add_widget(self.layout) # Now we need two widgets - Label for chat history and 'artificial' widget below # so we can scroll to it every new message and keep new messages visible # We want to enable markup, so we can set colors for example self.chat_history = Label(size_hint_y=None, markup=True) self.scroll_to_point = Label() # We add them to our layout self.layout.add_widget(self.chat_history) self.layout.add_widget(self.scroll_to_point) # Method called externally to add new message to the chat history def update_chat_history(self, message): # First add new line and message itself self.chat_history.text += '\n' + message # Set layout height to whatever height of chat history text is + 15 pixels # (adds a bit of space at teh bottom) # Set chat history label to whatever height of chat history text is # Set width of chat history text to 98 of the label width (adds small margins) self.layout.height = self.chat_history.texture_size[1] + 15 self.chat_history.height = self.chat_history.texture_size[1] self.chat_history.text_size = (self.chat_history.width * 0.98, None) # As we are updating above, text height, so also label and layout height are going to be bigger # than the area we have for this widget. ScrollView is going to add a scroll, but won't # scroll to the botton, nor is there a method that can do that. # That's why we want additional, empty widget below whole text - just to be able to scroll to it, # so scroll to the bottom of the layout self.scroll_to(self.scroll_to_point)
The comments should suffice here. Run the kivy file (with the server still running) to make sure things are still working. The methods for updating wont yet work, since we aren't calling them to yet.
Looks good for me, in the next tutorial we'll finish up this page and hopefully have a nice, pretty, working, chat application!
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 from kivy.core.window import Window from kivy.uix.scrollview import ScrollView 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' # This class is an improved version of Label # Kivy does not provide scrollable label, so we need to create one class ScrollableLabel(ScrollView): def __init__(self, **kwargs): super().__init__(**kwargs) # ScrollView does not allow us to add more than one widget, so we need to trick it # by creating a layout and placing two widgets inside it # Layout is going to have one collumn and and size_hint_y set to None, # so height wo't default to any size (we are going to set it on our own) self.layout = GridLayout(cols=1, size_hint_y=None) self.add_widget(self.layout) # Now we need two wodgets - Label for chat history and 'artificial' widget below # so we can scroll to it every new message and keep new messages visible # We want to enable markup, so we can set colors for example self.chat_history = Label(size_hint_y=None, markup=True) self.scroll_to_point = Label() # We add them to our layout self.layout.add_widget(self.chat_history) self.layout.add_widget(self.scroll_to_point) # Methos called externally to add new message to the chat history def update_chat_history(self, message): # First add new line and message itself self.chat_history.text += '\n' + message # Set layout height to whatever height of chat history text is + 15 pixels # (adds a bit of space at teh bottom) # Set chat history label to whatever height of chat history text is # Set width of chat history text to 98 of the label width (adds small margins) self.layout.height = self.chat_history.texture_size[1] + 15 self.chat_history.height = self.chat_history.texture_size[1] self.chat_history.text_size = (self.chat_history.width * 0.98, None) # As we are updating above, text height, so also label and layout height are going to be bigger # than the area we have for this widget. ScrollView is going to add a scroll, but won't # scroll to the botton, nor there is a method that can do that. # That's why we want additional, empty wodget below whole text - just to be able to scroll to it, # so scroll to the bottom of the layout self.scroll_to(self.scroll_to_point) class ChatPage(GridLayout): def __init__(self, **kwargs): super().__init__(**kwargs) # We are going to use 1 column and 2 rows self.cols = 1 self.rows = 2 # First row is going to be occupied by our scrollable label # We want it be take 90% of app height self.history = ScrollableLabel(height=Window.size[1]*0.9, size_hint_y=None) self.add_widget(self.history) # In the second row, we want to have input fields and Send button # Input field should take 80% of window width # We also want to bind button click to send_message method self.new_message = TextInput(width=Window.size[0]*0.8, size_hint_x=None, multiline=False) self.send = Button(text="Send") self.send.bind(on_press=self.send_message) # To be able to add 2 widgets into a layout with just one collumn, we use additional layout, # add widgets there, then add this layout to main layout as second row bottom_line = GridLayout(cols=2) bottom_line.add_widget(self.new_message) bottom_line.add_widget(self.send) self.add_widget(bottom_line) # Gets called when either Send button or Enter key is being pressed # (kivy passes button object here as well, but we don;t care about it) def send_message(self, _): print("send a message!!!") # 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()