mirror of
https://github.com/Taiko2k/GTK4PythonTutorial.git
synced 2026-01-07 20:00:09 +01:00
567 lines
16 KiB
Markdown
567 lines
16 KiB
Markdown
# Taiko's GTK4 Python tutorial
|
|
|
|
Wanna make apps for Linux but not sure how to start with GTK? This guide will hopefully help!
|
|
The intent is to show you how to do common things with basic code examples so that you can get up and running making your own GTK app quickly.
|
|
|
|
Prerequisite: You have learnt the basics of Python. Ideally have some idea of how classes work.
|
|
|
|
Topics covered:
|
|
|
|
- A basic GTK window
|
|
- Widgets: Button, check button, switch, slider
|
|
- Layout: Box layout
|
|
- Adding a custom header bar
|
|
- Showing an open file dialog
|
|
- Adding a menu button with a menu
|
|
- Custom drawing with Cairo
|
|
- Handling mouse input
|
|
- Setting the cursor
|
|
|
|
For beginners, I suggest walking through each example and try to understand what each line is doing.
|
|
|
|
Note that some code examples in this guide require completing previous topics to work.
|
|
|
|
## A most basic program
|
|
|
|
```python
|
|
import gi
|
|
gi.require_version('Gtk', '4.0')
|
|
from gi.repository import Gtk
|
|
|
|
def on_activate(app):
|
|
win = Gtk.ApplicationWindow(application=app)
|
|
win.present()
|
|
|
|
app = Gtk.Application()
|
|
app.connect('activate', on_activate)
|
|
|
|
app.run(None)
|
|
|
|
```
|
|
|
|
This should display a small blank window.
|
|
|
|

|
|
|
|
This is a minimal amount of code to show a window. But we will start off with a better example:
|
|
|
|
- Making the code into classes. 'Cause doing it functional style is a little awkward in Python.
|
|
- Switching to **Libawaita**, since many GNOME apps now use its new styling.
|
|
- Pass in the app arguments.
|
|
- Give the app an application id.
|
|
|
|
Here's what we got now:
|
|
|
|
### A better structured basic GTK4 + Adwaita
|
|
|
|
|
|
```python
|
|
import sys
|
|
import gi
|
|
gi.require_version('Gtk', '4.0')
|
|
gi.require_version('Adw', '1')
|
|
from gi.repository import Gtk, Adw
|
|
|
|
|
|
class MainWindow(Gtk.ApplicationWindow):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Things will go here
|
|
|
|
class MyApp(Adw.Application):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.connect('activate', self.on_activate)
|
|
|
|
def on_activate(self, app):
|
|
self.win = MainWindow(application=app)
|
|
self.win.present()
|
|
|
|
app = MyApp(application_id="com.example.GtkApplication")
|
|
app.run(sys.argv)
|
|
|
|
```
|
|
|
|
Soo we have an instance of an app class and a window which we extend! We run our app and it makes a window!
|
|
|
|
> **Tip:** Don't worry too much if you don't understand the `__init__(self, *args, **kwargs)` stuff for now.
|
|
|
|
> **Tip:** For a serious app, you'll need to think of your own application id. It should be the reverse of a domain or page you control. If you don't have your own domain you can do like "com.github.me.myproject".
|
|
|
|
|
|
### So! Whats next?
|
|
|
|
Well, we want to add something to our window. That would likely be a ***layout*** of some sort!
|
|
|
|
Most basic layout is a [Box](https://docs.gtk.org/gtk4/class.Box.html).
|
|
|
|
Lets add a box to the window! (Where the code comment "*things will go here*" is above)
|
|
|
|
```python
|
|
self.box1 = Gtk.Box()
|
|
self.set_child(self.box1)
|
|
```
|
|
|
|
We make a new box, and attach it to the window. Simple. If you run the app now you'll see no difference, because there's nothing in the layout yet either.
|
|
|
|
|
|
## Add a button!
|
|
|
|
One of the most basic widgets is a [Button](https://docs.gtk.org/gtk4/class.Button.html). Let's make one and add it to the layout.
|
|
|
|
```python
|
|
self.button = Gtk.Button(label="Hello")
|
|
self.box1.append(self.button)
|
|
```
|
|
|
|
Now our app has a button! (The window will be small now)
|
|
|
|
But it does nothing when we click it. Let's connect it to a function! Make a new method that prints hello world, and we connect it!
|
|
|
|
Here's our MainWindow so far:
|
|
|
|
```python
|
|
class MainWindow(Gtk.ApplicationWindow):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
self.set_child(self.box1)
|
|
|
|
self.button = Gtk.Button(label="Hello")
|
|
self.box1.append(self.button)
|
|
self.button.connect('clicked', self.hello)
|
|
|
|
def hello(self, button):
|
|
print("Hello world")
|
|
```
|
|
|
|
Cool eh?
|
|
|
|
By the way the ***Box*** layout lays out widgets in like a vertical or horizontal order. We should set the orientation of the box. See the change:
|
|
|
|
```python
|
|
self.box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
```
|
|
|
|
## Set some window parameters
|
|
|
|
```python
|
|
self.set_default_size(600, 250)
|
|
self.set_title("MyApp")
|
|
```
|
|
|
|
## More boxes
|
|
|
|
You'll notice our button is stretched with the window. Let's add two boxes inside that first box we made.
|
|
|
|
```python
|
|
self.box1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
self.box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
self.box3 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
|
|
self.button = Gtk.Button(label="Hello")
|
|
self.button.connect('clicked', self.hello)
|
|
|
|
self.set_child(self.box1) # Horizontal box to window
|
|
self.box1.append(self.box2) # Put vert box in that box
|
|
self.box1.append(self.box3) # And another one, empty for now
|
|
|
|
self.box2.append(self.button) # But button in the first of the two vertial boxes
|
|
```
|
|
|
|
Now that's more neat!
|
|
|
|
## Add a check button!
|
|
|
|
So, we know about a button, next lets add a [Checkbutton](https://docs.gtk.org/gtk4/class.CheckButton.html).
|
|
|
|
```python
|
|
...
|
|
self.check = Gtk.CheckButton(label="And goodbye?")
|
|
self.box2.append(self.check)
|
|
|
|
|
|
def hello(self, button):
|
|
print("Hello world")
|
|
if self.check.get_active():
|
|
print("Goodbye world!")
|
|
self.close()
|
|
```
|
|
|
|
|
|

|
|
|
|
When we click the button, we can check the state of the checkbox!
|
|
|
|
### Extra Tip: Radio Buttons
|
|
|
|
Check buttons can be turned into radio buttons by adding them to a group. You can do it using the `.set_group` method like this:
|
|
|
|
```python
|
|
radio1 = Gtk.CheckButton(label="test")
|
|
radio2 = Gtk.CheckButton(label="test")
|
|
radio3 = Gtk.CheckButton(label="test")
|
|
radio2.set_group(radio1)
|
|
radio3.set_group(radio1)
|
|
```
|
|
|
|
You can handle the toggle signal like this:
|
|
|
|
```python
|
|
radio1.connect("toggled", self.radio_toggled)
|
|
```
|
|
|
|
When connecting a signal its helpful to pass additional paramiters like as follows. This way you can have one functon handle events from multiple widgets. Just forget to handle the extra paramiter in your handler function.
|
|
|
|
|
|
```python
|
|
radio1.connect("toggled", self.radio_toggled, "test")
|
|
```
|
|
|
|
(This can apply to other widgets too)
|
|
|
|
|
|
## Add a switch
|
|
|
|
For our switch, we'll want to put our switch in a ***Box***, otherwise it'll look all stretched.
|
|
|
|
```python
|
|
...
|
|
self.switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
|
|
self.switch = Gtk.Switch()
|
|
self.switch.set_active(True) # Let's default it to on
|
|
self.switch.connect("state-set", self.switch_switched) # Lets trigger a function
|
|
|
|
self.switch_box.append(self.switch)
|
|
self.box2.append(self.switch_box)
|
|
|
|
def switch_switched(self, switch, state):
|
|
print(f"The switch has been switched {'on' if state else 'off'}")
|
|
```
|
|
|
|
Try it out!
|
|
|
|
Our switch is looking rather nondescript, so lets add a label to it!
|
|
|
|
|
|
## ...with a Label
|
|
|
|
A label is like a basic line of text
|
|
|
|
```python
|
|
self.label = Gtk.Label(label="A switch")
|
|
self.switch_box.append(self.switch)
|
|
self.switch_box.set_spacing(5) # Add some spacing
|
|
|
|
```
|
|
|
|
It should look like this now:
|
|
|
|

|
|
|
|
The file `part1.py` is an example of the code so far.
|
|
|
|
## Adding a slider (Aka scale)
|
|
|
|
Here's an example of adding a [Scale](https://docs.gtk.org/gtk4/ctor.Scale.new.html) with a range from 0 - 10
|
|
|
|
```python
|
|
self.slider = Gtk.Scale()
|
|
self.slider.set_digits(0) # Number of decimal places to use
|
|
self.slider.set_range(0, 10)
|
|
self.slider.set_draw_value(True) # Show a label with current value
|
|
self.slider.set_value(5) # Sets the current value/position
|
|
self.slider.connect('value-changed', self.slider_changed)
|
|
self.box2.append(self.slider)
|
|
|
|
def slider_changed(self, slider):
|
|
print(int(slider.get_value()))
|
|
```
|
|
|
|
## Adding a button into the header bar
|
|
|
|
First we need to make a header bar
|
|
|
|
```python
|
|
self.header = Gtk.HeaderBar()
|
|
self.set_titlebar(self.header)
|
|
```
|
|
|
|
Simple.
|
|
|
|
Now add a button
|
|
|
|
```python
|
|
self.open_button = Gtk.Button(label="Open")
|
|
self.header.pack_start(self.open_button)
|
|
```
|
|
|
|
We already know how to connect a function to the button, so i've omitted that.
|
|
|
|
Done! But... it would look nicer with an icon rather than text.
|
|
|
|
```python
|
|
self.open_button.set_icon_name("document-open-symbolic")
|
|
```
|
|
|
|
This will be an icon name from the icon theme.
|
|
|
|
For some defaults you can take a look at `/usr/share/icons/Adwaita/scalable/actions`.
|
|
|
|
If you were adding a new action icon it would go in `/usr/share/icons/hicolor/scalable/actions`
|
|
|
|
> **Help! Todo!** Is this the best way? How do icons work in a development environment?
|
|
|
|
## Open file dialog
|
|
|
|
Let's make that open button actually show an open file dialog
|
|
|
|
```python
|
|
self.open_dialog = Gtk.FileChooserNative.new(title="Choose a file",
|
|
parent=self, action=Gtk.FileChooserAction.OPEN)
|
|
|
|
self.open_dialog.connect("response", self.open_response)
|
|
self.open_button.connect("clicked", self.show_open_dialog)
|
|
|
|
def show_open_dialog(self, button):
|
|
self.open_dialog.show()
|
|
|
|
def open_response(self, dialog, response):
|
|
if response == Gtk.ResponseType.ACCEPT:
|
|
file = dialog.get_file()
|
|
filename = file.get_path()
|
|
print(filename) # Here you could handle opening or saving the file
|
|
|
|
```
|
|
|
|
The action type can also be **SAVE** and **SELECT_FOLDER**
|
|
|
|
If you wanted to restrict the file types shown, you could add a filter. For example:
|
|
|
|
```python
|
|
f = Gtk.FileFilter()
|
|
f.set_name("Image files")
|
|
f.add_mime_type("image/jpeg")
|
|
f.add_mime_type("image/png")
|
|
self.open_dialog.add_filter(f)
|
|
```
|
|
|
|
## Adding a button with menu
|
|
|
|
For this there are multiple new concepts we need to introduce:
|
|
|
|
- The [***MenuButton***](https://docs.gtk.org/gtk4/class.MenuButton.html) widget.
|
|
- The [***Popover***](https://docs.gtk.org/gtk4/class.Popover.html), but here we will use a [***PopoverMenu***](https://docs.gtk.org/gtk4/class.PopoverMenu.html) which is built using an abstract menu model.
|
|
- A [***Menu***](https://docs.gtk.org/gio/class.Menu.html). This is an abstract model of a menu.
|
|
- [***Actions***](https://docs.gtk.org/gio/class.SimpleAction.html). An abstract action that can be connected to our abstract menu.
|
|
|
|
So, we click a MenuButton, which shows a Popover that was generated from a MenuModel that is composed of Actions.
|
|
|
|
```python
|
|
# Create a new "Action"
|
|
action = Gio.SimpleAction.new("something", None)
|
|
action.connect("activate", self.print_something)
|
|
self.add_action(action) # Here the action is being added to the window, but you could add it to the
|
|
# application or an "ActionGroup"
|
|
|
|
# Create a new menu, containing that action
|
|
menu = Gio.Menu.new()
|
|
menu.append("Do Something", "win.something") # Or you would do app.grape if you had attached the
|
|
# action to the application
|
|
|
|
# Create a popover
|
|
self.popover = Gtk.PopoverMenu() # Create a new popover menu
|
|
self.popover.set_menu_model(menu)
|
|
|
|
# Create a menu button
|
|
self.hamburger = Gtk.MenuButton()
|
|
self.hamburger.set_popover(self.popover)
|
|
self.hamburger.set_icon_name("open-menu-symbolic") # Give it a nice icon
|
|
|
|
# Add menu button to the header bar
|
|
self.header.pack_start(self.hamburger)
|
|
|
|
def print_something(self, action, param):
|
|
print("Something!")
|
|
|
|
```
|
|
|
|

|
|
|
|
## Custom drawing area using Cairo
|
|
|
|
Here we use the [***DrawingArea***](https://docs.gtk.org/gtk4/class.DrawingArea.html) widget.
|
|
|
|
```python
|
|
|
|
dw = Gtk.DrawingArea()
|
|
|
|
# Make it fill the available space (It will stretch with the window)
|
|
dw.set_hexpand(True)
|
|
dw.set_vexpand(True)
|
|
|
|
# Instead, If we didn't want it to fill the available space but wanted a fixed size
|
|
#dw.set_content_width(100)
|
|
#dw.set_content_height(100)
|
|
|
|
dw.set_draw_func(self.draw, None)
|
|
self.box3.append(dw)
|
|
|
|
def draw(self, area, c, w, h, data):
|
|
# c is a Cairo context
|
|
|
|
# Fill background with a colour
|
|
c.set_source_rgb(0, 0, 0)
|
|
c.paint()
|
|
|
|
# Draw a line
|
|
c.set_source_rgb(0.5, 0.0, 0.5)
|
|
c.set_line_width(3)
|
|
c.move_to(10, 10)
|
|
c.line_to(w - 10, h - 10)
|
|
c.stroke()
|
|
|
|
# Draw a rectangle
|
|
c.set_source_rgb(0.8, 0.8, 0.0)
|
|
c.rectangle(20, 20, 50, 20)
|
|
c.fill()
|
|
|
|
# Draw some text
|
|
c.set_source_rgb(0.1, 0.1, 0.1)
|
|
c.select_font_face("Sans")
|
|
c.set_font_size(13)
|
|
c.move_to(25, 35)
|
|
c.show_text("Test")
|
|
|
|
```
|
|
|
|

|
|
|
|
Further resources on Cairo:
|
|
|
|
- [PyCairo Visual Documentation](https://seriot.ch/pycairo/)
|
|
|
|
Note that Cairo uses software rendering. For accelerated rendering, Gtk Snapshot can be used (todo)
|
|
|
|
## Input handling in our drawing area
|
|
|
|
### Handling a mouse / touch event
|
|
|
|
```python
|
|
...
|
|
evk = Gtk.GestureClick.new()
|
|
evk.connect("pressed", self.dw_click) # could be "released"
|
|
self.dw.add_controller(evk)
|
|
|
|
self.blobs = []
|
|
|
|
def dw_click(self, gesture, data, x, y):
|
|
self.blobs.append((x, y))
|
|
self.dw.queue_draw() # Force a redraw
|
|
|
|
def draw(self, area, c, w, h, data):
|
|
# c is a Cairo context
|
|
|
|
# Fill background
|
|
c.set_source_rgb(0, 0, 0)
|
|
c.paint()
|
|
|
|
c.set_source_rgb(1, 0, 1)
|
|
for x, y in self.blobs:
|
|
c.arc(x, y, 10, 0, 2 * 3.1415926)
|
|
c.fill()
|
|
...
|
|
|
|
```
|
|
|
|

|
|
|
|
Ref: [GestureClick](https://docs.gtk.org/gtk4/class.GestureClick.html)
|
|
|
|
Extra example. If we wanted to listen to other mouse button types:
|
|
|
|
```python
|
|
...
|
|
evk.set_button(0) # 0 for all buttons
|
|
def dw_click(self, gesture, data, x, y):
|
|
button = gesture.get_current_button()
|
|
print(button)
|
|
```
|
|
|
|
|
|
See also: [EventControllerMotion](https://docs.gtk.org/gtk4/class.EventControllerMotion.html). Example:
|
|
|
|
```python
|
|
evk = Gtk.EventControllerMotion.new()
|
|
evk.connect("motion", self.mouse_motion)
|
|
def mouse_motion(self, motion, x, y):
|
|
print(f"Mouse moved to {x}, {y}")
|
|
```
|
|
|
|
See also: [EventControllerKey](https://docs.gtk.org/gtk4/class.EventControllerKey.html)
|
|
|
|
```python
|
|
evk = Gtk.EventControllerKey.new()
|
|
evk.connect("key-pressed", self.key_press)
|
|
self.add_controller(evk) # add to window
|
|
def key_press(self, event, keyval, keycode, state):
|
|
if keyval == Gdk.KEY_q and state & Gdk.ModifierType.CONTROL_MASK:
|
|
self.close()
|
|
```
|
|
|
|
## Setting the cursor
|
|
|
|
We can set a cursor for a widget.
|
|
|
|
First we need to import **Gdk**, so we append it to this line like so:
|
|
|
|
```python
|
|
from gi.repository import Gtk, Adw, Gio, Gdk
|
|
```
|
|
|
|
Now setting the cursor is easy.
|
|
|
|
```python
|
|
self.cursor_crosshair = Gdk.Cursor.new_from_name("crosshair")
|
|
self.dw.set_cursor(self.cursor_crosshair)
|
|
```
|
|
|
|
You can find a list of common cursor names [here](https://docs.gtk.org/gdk4/ctor.Cursor.new_from_name.html).
|
|
|
|
## Setting a dark color scheme
|
|
|
|
We can use:
|
|
|
|
```python
|
|
app = self.get_application()
|
|
sm = app.get_style_manager()
|
|
sm.set_color_scheme(Adw.ColorScheme.PREFER_DARK)
|
|
```
|
|
|
|
See [here](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.0.0/styles-and-appearance.html) for more details.
|
|
|
|
## Further reading
|
|
|
|
- [GTK4 Widget Gallery](https://docs.gtk.org/gtk4/visual_index.html)
|
|
|
|
## Todo...
|
|
|
|
Text box: [Entry](https://docs.gtk.org/gtk4/class.Entry.html)
|
|
|
|
Number changer: [SpinButton](https://docs.gtk.org/gtk4/class.SpinButton.html)
|
|
|
|
Picture.
|
|
|
|
Layout and spacing.
|
|
|
|
Snapshot drawing.
|
|
|
|
UI from XML.
|
|
|
|
Custom Styles.
|
|
|
|
|
|
|
|
|