diff --git a/learning_python3.md b/learning_python3.md index ec064c2..9f8d0cb 100644 --- a/learning_python3.md +++ b/learning_python3.md @@ -2491,10 +2491,257 @@ TODO add a countdown timer to the multiple choice game # GUI programming +TODO event loop introduction + ## wxpython helloworld +The absolute most basic way to have a *hello world* GUI program up and running with wxpython is the following. + +```python +import wx + +app = wx.App() + +frame = wx.Frame(None, title="hello world") + +frame.Show() + +app.MainLoop() +``` + +While it works we know better by now. +We should include the `if __name__ == "__main__"` statement. + +```python +import wx + + +if __name__ == "__main__": + app = wx.App() + frame = wx.Frame(None, title="hello world") + frame.Show() + app.MainLoop() +``` + +The instance of `wx.App` is what will actually process our event loop and the `wx.Frame` is the content of our window. +In the latter we will create our button and labels so should create our own class and inherit from it. + +```python +import wx + + +class MainWindow(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="hello world") + self.Show() + + +if __name__ == "__main__": + app = wx.App() + win = MainWindow() + app.MainLoop() +``` + +We can add content to the *frame*, such as labels, input boxes and buttons as follows. +Note the first argument to the `wx.StaticText` creation. +This is *where* we put the label *into*. +It kind of works but we'll encounter a **problem** when we pack more visual **objects** into the same frame. + +```python +import wx + + +class MainWindow(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="hello world") + self.label = wx.StaticText(self, label="this is a label") + self.Show() + + +if __name__ == "__main__": + app = wx.App() + win = MainWindow() + app.MainLoop() +``` + +Let's try to put multiple visual object into the same frame. + +```python +import wx + + +class MainWindow(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="hello world") + self.label = wx.StaticText(self, label="this is a label") + self.input = wx.TextCtrl(self) + self.button = wx.Button(self) + self.Show() + + +if __name__ == "__main__": + app = wx.App() + win = MainWindow() + app.MainLoop() +``` + +You see how they are *stacked* one on top of the other? +We can overcome this problem with [sizers](https://wxpython.org/Phoenix/docs/html/sizers_overview.html). +There are multiple ones we can use but let's dive into a first one called a **box sizer**. + +```python +import wx + + +class MainWindow(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="hello world") + self.panel = wx.Panel(self) + self.box = wx.BoxSizer() + self.label = wx.StaticText(self.panel, label="this is a label") + self.input = wx.TextCtrl(self.panel) + self.button = wx.Button(self.panel, label="I'm clickable!") + self.box.Add(self.label) + self.box.Add(self.input) + self.box.Add(self.button) + self.panel.SetSizer(self.box) + self.Show() + + +if __name__ == "__main__": + app = wx.App() + win = MainWindow() + app.MainLoop() +``` + +This is looking better! +But it requires some explanation though. +Let's break it down. +The `wx.Frame` is your **window** in which you create a `wx.Panel` which is used to draw thing to. +To this panel you're adding three different *objects* (`wx.StaticText`, `wx.TextCtrl` and `wx.Button`) each with or without their own settings (such as a label or not). +Next you add these three objects to the `wx.BoxSizer` and you tell the panel to use this box as sizer. +It probably looks a bit convoluted but this is how most GUI libraries work internally. + +1. You create a frame +2. Within this frame you create a *drawing area* and set some form of automatic layout to it (the `wx.BoxSizer`). +3. You create the visual elements you want and add them one by one to the drawing area. +4. Success + +Now how do we link **user input** to **code actions**? +This is a complicated way of saying *actually do something when I click the damn button*! +For this we'll need to create a function, or better yet a method to the `wx.Frame` objects. +Each time we click the button, that method will be called. +Because it is a *method* it has access to *self* so it can modify *anything* within the scope of the instance. + +```python +import wx +import random + + +class MainWindow(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="hello world") + self.panel = wx.Panel(self) + self.box = wx.BoxSizer() + self.label = wx.StaticText(self.panel, label="this is a label") + self.input = wx.TextCtrl(self.panel) + self.button = wx.Button(self.panel, label="I'm clickable!") + self.button.Bind(wx.EVT_BUTTON, self.set_label_value) + self.box.Add(self.label) + self.box.Add(self.input) + self.box.Add(self.button) + self.panel.SetSizer(self.box) + self.Show() + + def set_label_value(self, event): + number = random.randint(0, 100) + self.label.SetLabel("{}".format(number)) + + +if __name__ == "__main__": + app = wx.App() + win = MainWindow() + app.MainLoop() +``` + +We can use the same *idea* to grab input from the textbox. + +```python +import wx + + +class MainWindow(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="hello world") + self.panel = wx.Panel(self) + self.box = wx.BoxSizer() + self.label = wx.StaticText(self.panel, label="this is a label") + self.input = wx.TextCtrl(self.panel) + self.button = wx.Button(self.panel, label="I'm clickable!") + self.button.Bind(wx.EVT_BUTTON, self.set_label_value) + self.box.Add(self.label) + self.box.Add(self.input) + self.box.Add(self.button) + self.panel.SetSizer(self.box) + self.Show() + + def set_label_value(self, event): + msg = self.input.GetValue() + self.label.SetLabel("{}".format(msg)) + + +if __name__ == "__main__": + app = wx.App() + win = MainWindow() + app.MainLoop() +``` + ## wxpython guess the number +```python +import wx +import random + + +class MainWindow(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="Guess the number") + self.number = random.randint(0, 100) + self.panel = wx.Panel(self) + self.box = wx.BoxSizer(wx.VERTICAL) + self.label = wx.StaticText(self.panel, label="I have a number in mind...") + self.input = wx.TextCtrl(self.panel) + self.button = wx.Button(self.panel, label="I'm clickable!") + self.button.Bind(wx.EVT_BUTTON, self.set_label_value) + self.box.Add(self.label) + self.box.Add(self.input) + self.box.Add(self.button) + self.panel.SetSizer(self.box) + self.Show() + + def set_label_value(self, event): + result = self.input.GetValue() + if result.isdigit(): + status, context = self.evaluate_user_number(int(result)) + self.label.SetLabel(context) + else: + self.label.SetLabel("I need numbers!") + + def evaluate_user_number(self, number): + if number > self.number: + return False, "my number is smaller" + elif number < self.number: + return False, "my number is bigger" + elif number == self.number: + return True, "You win!" + + +if __name__ == "__main__": + app = wx.App() + win = MainWindow() + app.MainLoop() +``` + ## MVC design pattern # Coding challenge - Login generator with GUI