mirror of
https://github.com/Taiko2k/GTK4PythonTutorial.git
synced 2026-01-07 20:00:09 +01:00
818 lines
26 KiB
Markdown
818 lines
26 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 some common things with basic code examples so that you can get up and running making your own GTK app quickly.
|
|
|
|
Ultimately you want to be able to refer to the official documentation [here](https://docs.gtk.org/gtk4/) yourself. But I find it can be hard getting started
|
|
without an initial understanding of how to do things. The code examples here should hopefully help.
|
|
|
|
How to use this tutorial: You can either follow along or just use it to refer to specific examples.
|
|
|
|
Prerequisites: You have learnt the basics of Python. Ideally have some idea of how classes work. You will also need the following packages installed on your system: GTK4, PyGObject and Libadwaita.
|
|
|
|
Topics covered:
|
|
|
|
- A basic GTK window
|
|
- Widgets: Button, check button, switch, slider
|
|
- Layout: Box layout
|
|
- Adding a header bar
|
|
- Showing an open file dialog
|
|
- Adding a menu-button with a menu
|
|
- Adding an about dialog
|
|
- "Open with" and single instancing
|
|
- Custom drawing with Cairo
|
|
- Handling mouse input
|
|
- Setting the cursor
|
|
- Setting dark colour theme
|
|
- Spacing and padding
|
|
- Custom drawing with Snapshot
|
|
|
|
For beginners, I suggest walking through each example and try to understand what each line is doing. I also recommend taking a look at the docs for each widget.
|
|
|
|
Helpful is the [GTK4 Widget Gallery](https://docs.gtk.org/gtk4/visual_index.html) which shows you all the common widgets.
|
|
|
|
|
|
## 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! What's 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) # Put 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 it's helpful to pass additional parameters like as follows. This way you can have one function handle events from multiple widgets. Just don't forget to handle
|
|
the extra parameter 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 let's 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.label)
|
|
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 to 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.
|
|
|
|
First make sure `Gio` is added to the list of things we're importing from `gi.repository`:
|
|
|
|
```python
|
|
from gi.repository import Gtk, Adw, Gio
|
|
```
|
|
|
|
```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.something 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!")
|
|
|
|
```
|
|
|
|
## Add an about dialog
|
|
|
|
```python
|
|
from gi.repository import Gtk, Adw, Gio, GLib # Add GLib to imports
|
|
```
|
|
|
|
```python
|
|
# Set app name
|
|
GLib.set_application_name("My App")
|
|
|
|
# Create an action to run a *show about dialog* function we will create
|
|
action = Gio.SimpleAction.new("about", None)
|
|
action.connect("activate", self.show_about)
|
|
self.add_action(action)
|
|
|
|
menu.append("About", "win.about") # Add it to the menu we created in previous section
|
|
|
|
def show_about(self, action, param):
|
|
self.about = Gtk.AboutDialog()
|
|
self.about.set_transient_for(self) # Makes the dialog always appear in from of the parent window
|
|
self.about.set_modal(self) # Makes the parent window unresponsive while dialog is showing
|
|
|
|
self.about.set_authors(["Your Name"])
|
|
self.about.set_copyright("Copyright 2022 Your Full Name")
|
|
self.about.set_license_type(Gtk.License.GPL_3_0)
|
|
self.about.set_website("http://example.com")
|
|
self.about.set_website_label("My Website")
|
|
self.about.set_version("1.0")
|
|
self.about.set_logo_icon_name("org.example.example") # The icon will need to be added to appropriate location
|
|
# E.g. /usr/share/icons/hicolor/scalable/apps/org.example.example.svg
|
|
|
|
self.about.show()
|
|
|
|
```
|
|
|
|
For further reading on what you can add, see [***AboutDialog***](https://docs.gtk.org/gtk4/class.AboutDialog.html).
|
|
|
|

|
|
|
|
## "Open with" and single instancing
|
|
|
|
> Note that I haven't fully tested the code in this section
|
|
|
|
We already covered how to open a file with an explicit dialog box, but there are other ways users might want to open a file
|
|
with our application, such as a command line argument, or when they click "Open with" in their file browser etc.
|
|
|
|
Also, when the user launches another instance, we may want to determine the behavior of if the file is opened in the original
|
|
window or a new one. Fortunately, GTK handles most of the hard work for us, but there are some things we need to do if we want handle file opening.
|
|
|
|
By default, our [GApplication](https://docs.gtk.org/gio/class.Application.html) will maintain the first process as the primary process, and if
|
|
a second process is launched, one of two signals will be called on that first primary process, with the 2nd process promptly exiting.
|
|
|
|
Those two signals are two possible entry points to our app; `activate` which we already implemented, and `open` which we haven't yet implemented. The open function handles the opening of files.
|
|
|
|
Currently, in our example app, `activate` will launch another identical window. (The app will exit when all windows are closed)
|
|
|
|
So what if we wanted only one window open at a time? Just detect if we have already opened a window and return from the activate function if we have.
|
|
|
|
Maintain a single instance:
|
|
|
|
```python
|
|
class MyApp(Adw.Application):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.connect('activate', self.on_activate)
|
|
self.win = None # Forgot to add this originally
|
|
|
|
def on_activate(self, app):
|
|
if not self.win: # added this condition
|
|
self.win = MainWindow(application=app)
|
|
self.win.present() # if window is already created, this will raise it to the front
|
|
```
|
|
|
|
What about opening files? We need to implement that function:
|
|
|
|
```python
|
|
class MyApp(Adw.Application):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.connect('activate', self.on_activate)
|
|
self.connect('open', self.on_open)
|
|
self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN) # Need to tell GApplication we can handle this
|
|
self.win = None
|
|
|
|
def on_activate(self, app):
|
|
if not self.win:
|
|
self.win = MainWindow(application=app)
|
|
self.win.present()
|
|
|
|
def on_open(self, app, files, n_files, hint):
|
|
self.on_activate(app) # Adding this because window may not have been created yet with this entry point
|
|
for file in n_files:
|
|
print("File to open: " + file.get_path()) # How you handle it from here is up to you, I guess
|
|
|
|
```
|
|
|
|
Note that an "Open with" option with your application would
|
|
require a `.desktop` file that registers a mime type that your application can open, but setting up a desktop
|
|
file is outside the scope of this tutorial.
|
|
|
|
|
|
## Custom drawing area using Cairo
|
|
|
|
There are two main methods of custom drawing in GTK4, the Cairo way and the Snapshot way. Cairo provides a more high level
|
|
drawing API but uses slow software rendering. Snapshot uses a little more low level API but uses much faster hardware accelerated rendering.
|
|
|
|
To draw with Cairo we use the [***DrawingArea***](https://docs.gtk.org/gtk4/class.DrawingArea.html) widget.
|
|
|
|
```python
|
|
|
|
self.dw = Gtk.DrawingArea()
|
|
|
|
# Make it fill the available space (It will stretch with the window)
|
|
self.dw.set_hexpand(True)
|
|
self.dw.set_vexpand(True)
|
|
|
|
# Instead, If we didn't want it to fill the available space but wanted a fixed size
|
|
#self.dw.set_content_width(100)
|
|
#self.dw.set_content_height(100)
|
|
|
|
self.dw.set_draw_func(self.draw, None)
|
|
self.box3.append(self.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)
|
|
self.add_controller(evk)
|
|
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: # Add Gdk to your imports. i.e. from gi import Gdk
|
|
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.
|
|
|
|
|
|
# Spacing and padding
|
|
|
|
For a better look we can add spacing to our **layout**. We can also add a margin to any widget, here I've added a
|
|
margin to our **box** layout.
|
|
|
|
```python
|
|
self.box2.set_spacing(10)
|
|
self.box2.set_margin_top(10)
|
|
self.box2.set_margin_bottom(10)
|
|
self.box2.set_margin_start(10)
|
|
self.box2.set_margin_end(10)
|
|
```
|
|
|
|

|
|
|
|
# Custom drawing with Snapshot
|
|
|
|
As mentioned in the Cairo section, Snapshot uses fast hardware accelerated drawing, but it's a little more complicated to
|
|
use. Treat this section as more of a general guide of how it works than a tutorial of how you should do things.
|
|
|
|
First, we create our own custom widget class which will implement the [***Snapshot***](https://docs.gtk.org/gtk4/class.Snapshot.html) virtual method.
|
|
(To implement a virtual method we need to prepend `do_` to the name as it is in the docs.)
|
|
|
|
```python
|
|
|
|
class CustomDraw(Gtk.Widget):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
def do_snapshot(self, s):
|
|
pass
|
|
```
|
|
|
|
Then it can be added in the same way as any other widget. If we want to manually trigger a redraw we can use
|
|
the same `.queue_draw()` method call on it.
|
|
|
|
If we want the widget to have a dynamic size we can set the usual `.set_hexpand(True)`/`.set_vexpand(True)`, but if it
|
|
is to have a fixed size, you would need to implement the [**Measure**](https://docs.gtk.org/gtk4/vfunc.Widget.measure.html) virtual method.
|
|
|
|
Have a read of the [***snapshot***](https://docs.gtk.org/gtk4/class.Snapshot.html) docs. It's a little more complex, but once you know what you're doing you
|
|
could easily create your own helper functions. You can use your imagination!
|
|
|
|
Here's some examples:
|
|
|
|
### Draw a solid rectangle
|
|
|
|
Here we use:
|
|
- [**RGBA Struct**](https://docs.gtk.org/gdk4/struct.RGBA.html)
|
|
- [**Rect**](http://ebassi.github.io/graphene/docs/graphene-Rectangle.html)
|
|
|
|
```python
|
|
def do_snapshot(self, s):
|
|
colour = Gdk.RGBA()
|
|
colour.parse("#e80e0e")
|
|
|
|
rect = Graphene.Rect().__init__(10, 10, 40, 60) # Add Graphene to your imports. i.e. from gi import Graphene
|
|
|
|
s.append_color(colour, rect)
|
|
```
|
|
|
|
### Draw a solid rounded rectangle / circle
|
|
|
|
This is a little more complicated...
|
|
|
|
- [***RoundedRect***](https://docs.gtk.org/gsk4/struct.RoundedRect.html)
|
|
|
|
```python
|
|
colour = Gdk.RGBA()
|
|
colour.parse("rgb(159, 222, 42)") # another way of parsing
|
|
|
|
rect = Graphene.Rect().init(50, 70, 40, 40)
|
|
|
|
rounded_rect = Gsk.RoundedRect() # Add Gsk to your imports. i.e. from gi import Gsk
|
|
rounded_rect.init_from_rect(rect, radius=20) # A radius of 90 would make a circle
|
|
|
|
s.push_rounded_clip(rounded_rect)
|
|
s.append_color(colour, rect)
|
|
s.pop() # remove the clip
|
|
```
|
|
|
|
### Outline of rect / rounded rect / circle
|
|
|
|
Fairly straightforward, see [append_border](https://docs.gtk.org/gtk4/method.Snapshot.append_border.html).
|
|
|
|
### An Image
|
|
|
|
- See [***Texture***](https://docs.gtk.org/gdk4/class.Texture.html).
|
|
|
|
```python
|
|
texture = Gdk.Texture.new_from_filename("example.png")
|
|
# Warning: For the purposes of demonstration ive shown this declared in our drawing function,
|
|
# but of course you would REALLY need to define this somewhere else so that its only called
|
|
# once as we don't want to reload/upload the data every draw call.
|
|
|
|
# Tip: There are other functions to load image data from in memory pixel data
|
|
|
|
rect = Graphene.Rect().__init__(50, 50, texture.get_width(), texture.get_height()) # See warning below
|
|
s.append_texture(texture, rect)
|
|
|
|
```
|
|
|
|
Warning: On a HiDPI display the logical and physical measurements may differ in scale, typically by a factor of 2. In most places
|
|
we're dealing in logical units but these methods give physical units. So... you might not want to define the size of the rectangle
|
|
by the texture.
|
|
|
|
### Text
|
|
|
|
Text is drawn using Pango layouts. Pango is quite powerful and really needs a whole tutorial on its own, but here's
|
|
a basic example of a single line of text:
|
|
|
|
```python
|
|
colour = Gdk.RGBA()
|
|
colour.red = 0.0 # Another way of setting colour
|
|
colour.green = 0.0
|
|
colour.blue = 0.0
|
|
colour.alpha = 1.0
|
|
|
|
font = Pango.FontDescription.new()
|
|
font.set_family("Sans")
|
|
font.set_size(12 * Pango.SCALE) # todo how do we follow the window scaling factor?
|
|
|
|
context = self.get_pango_context()
|
|
layout = Pango.Layout(context) # Add Pango to your imports. i.e. from gi import Pango
|
|
layout.set_font_description(font)
|
|
layout.set_text("Example text")
|
|
|
|
point = Graphene.Point()
|
|
point.x = 50 # starting X co-ordinate
|
|
point.y = 50 # starting Y co-ordinate
|
|
|
|
s.save()
|
|
s.translate(point)
|
|
s.append_layout(layout, colour)
|
|
s.restore()
|
|
|
|
```
|
|
|
|
## Todo...
|
|
|
|
Text box: [Entry](https://docs.gtk.org/gtk4/class.Entry.html)
|
|
|
|
Number changer: [SpinButton](https://docs.gtk.org/gtk4/class.SpinButton.html)
|
|
|
|
Picture.
|
|
|
|
UI from XML.
|
|
|
|
Custom Styles.
|
|
|
|
|
|
|
|
|