├── cam.png ├── dots.png ├── draw.png ├── grid.png ├── blank.png ├── menu1.png ├── switch.png ├── spacing.png ├── twoitems.png ├── part3.py ├── test.ui ├── .gitignore ├── part1.py ├── part2.py └── README.md /cam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/cam.png -------------------------------------------------------------------------------- /dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/dots.png -------------------------------------------------------------------------------- /draw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/draw.png -------------------------------------------------------------------------------- /grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/grid.png -------------------------------------------------------------------------------- /blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/blank.png -------------------------------------------------------------------------------- /menu1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/menu1.png -------------------------------------------------------------------------------- /switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/switch.png -------------------------------------------------------------------------------- /spacing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/spacing.png -------------------------------------------------------------------------------- /twoitems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taiko2k/GTK4PythonTutorial/HEAD/twoitems.png -------------------------------------------------------------------------------- /part3.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import gi 3 | gi.require_version('Gtk', '4.0') 4 | gi.require_version('Adw', '1') 5 | from gi.repository import Gtk, Adw 6 | 7 | class MyApp(Adw.Application): 8 | def __init__(self, **kwargs): 9 | super().__init__(**kwargs) 10 | self.connect('activate', self.on_activate) 11 | 12 | def on_activate(self, app): 13 | # Create a Builder 14 | builder = Gtk.Builder() 15 | builder.add_from_file("test.ui") 16 | 17 | # Obtain the button widget and connect it to a function 18 | button = builder.get_object("button1") 19 | button.connect("clicked", self.hello) 20 | 21 | # Obtain and show the main window 22 | self.win = builder.get_object("main_window") 23 | self.win.set_application(self) # Application will close once it no longer has active windows attached to it 24 | self.win.present() 25 | 26 | def hello(self, button): 27 | print("Hello") 28 | 29 | app = MyApp(application_id="com.example.GtkApplication") 30 | app.run(sys.argv) -------------------------------------------------------------------------------- /test.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 200 8 | 400 9 | UI XML Test 10 | 11 | 12 | 13 | 14 | 5 15 | 5 16 | 5 17 | 5 18 | vertical 19 | 20 | 21 | start 22 | True 23 | Test button 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /part1.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import gi 3 | 4 | gi.require_version('Gtk', '4.0') 5 | gi.require_version('Adw', '1') 6 | from gi.repository import Gtk, Adw, Gio, GObject 7 | 8 | 9 | class MainWindow(Gtk.ApplicationWindow): 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | 13 | self.set_default_size(600, 250) 14 | self.set_title("MyApp") 15 | 16 | # Main layout containers 17 | self.box1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 18 | self.box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 19 | self.box3 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 20 | 21 | self.set_child(self.box1) # Horizontal box to window 22 | self.box1.append(self.box2) # Put vert box in that box 23 | self.box1.append(self.box3) # And another one, empty for now 24 | 25 | self.grid1 = Gtk.GridView() 26 | self.box3.append(self.grid1) 27 | 28 | fruits = ["Banana", "Apple", "Strawberry", "Pear", "Watermelon", "Blueberry"] 29 | 30 | class Fruit(GObject.Object): 31 | name = GObject.Property(type=str) 32 | def __init__(self, name): 33 | super().__init__() 34 | self.name = name 35 | 36 | self.ls = Gio.ListStore() 37 | 38 | for f in fruits: 39 | self.ls.append(Fruit(f)) 40 | 41 | ss = Gtk.SingleSelection() 42 | ss.set_model(self.ls) 43 | 44 | self.grid1.set_model(ss) 45 | 46 | factory = Gtk.SignalListItemFactory() 47 | def f_setup(fact, item): 48 | label = Gtk.Label(halign=Gtk.Align.START) 49 | label.set_selectable(False) 50 | item.set_child(label) 51 | 52 | factory.connect("setup", f_setup) 53 | 54 | def f_bind(fact, item): 55 | item.get_child().set_label(item.get_item().name) 56 | 57 | factory.connect("bind", f_bind) 58 | 59 | self.grid1.set_factory(factory) 60 | 61 | print(ss.get_selected_item().name) 62 | 63 | def on_selected_items_changed(selection, position, n_items): 64 | selected_item = selection.get_selected_item() 65 | if selected_item is not None: 66 | print(f"Selected item changed to: {selected_item.name}") 67 | 68 | ss.connect("selection-changed", on_selected_items_changed) 69 | 70 | 71 | # Add a button 72 | self.button = Gtk.Button(label="Hello") 73 | self.button.connect('clicked', self.hello) 74 | self.box2.append(self.button) # But button in the first of the two vertical boxes 75 | 76 | # Add a check button 77 | self.check = Gtk.CheckButton(label="And goodbye?") 78 | self.box2.append(self.check) 79 | 80 | # Add a box containing a switch and label 81 | self.switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 82 | self.switch_box.set_spacing(5) 83 | 84 | self.switch = Gtk.Switch() 85 | self.switch.set_active(True) # Let's default it to on 86 | self.switch.connect("state-set", self.switch_switched) # Lets trigger a function on state change 87 | 88 | self.label = Gtk.Label(label="A switch") 89 | 90 | self.switch_box.append(self.switch) 91 | self.switch_box.append(self.label) 92 | self.box2.append(self.switch_box) 93 | 94 | def switch_switched(self, switch, state): 95 | print(f"The switch has been switched {'on' if state else 'off'}") 96 | 97 | def hello(self, button): 98 | print("Hello world") 99 | if self.check.get_active(): 100 | print("Goodbye world!") 101 | self.close() 102 | 103 | 104 | class MyApp(Adw.Application): 105 | def __init__(self, **kwargs): 106 | super().__init__(**kwargs) 107 | self.connect('activate', self.on_activate) 108 | 109 | def on_activate(self, app): 110 | self.win = MainWindow(application=app) 111 | self.win.present() 112 | 113 | 114 | app = MyApp(application_id="com.example.GtkApplication") 115 | app.run(sys.argv) 116 | -------------------------------------------------------------------------------- /part2.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import gi 3 | 4 | gi.require_version('Gtk', '4.0') 5 | gi.require_version('Adw', '1') 6 | from gi.repository import Gtk, Adw, Gio, Gdk, Graphene, GLib 7 | 8 | class Custom(Gtk.Widget): 9 | def __init__(self): 10 | super().__init__() 11 | self.set_size_request(30, 30) 12 | 13 | def do_snapshot(self, s): 14 | #s.save() 15 | print("sn") 16 | red = Gdk.RGBA() 17 | # red.red = 1. 18 | # red.green = 0. 19 | # red.blue = 0. 20 | # red.alpha = 1. 21 | r = Graphene.Rect() 22 | r.init(0, 0, 70, 70) 23 | print(r) 24 | print(r.get_height()) 25 | red.red = 1 26 | red.alpha = 1 27 | print(red.to_string()) 28 | s.append_color(red, r) 29 | #s.restore() 30 | 31 | 32 | def do_measure(self, orientation, for_size): 33 | print("m") 34 | return 50, 50, -1, -1 35 | pass 36 | 37 | class MainWindow(Gtk.ApplicationWindow): 38 | def __init__(self, *args, **kwargs): 39 | super().__init__(*args, **kwargs) 40 | 41 | self.set_default_size(600, 250) 42 | self.set_title("MyApp") 43 | 44 | # Main layout containers 45 | self.box1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 46 | self.box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 47 | self.box3 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 48 | 49 | self.box2.set_spacing(10) 50 | self.box2.set_margin_top(10) 51 | self.box2.set_margin_bottom(10) 52 | self.box2.set_margin_start(10) 53 | self.box2.set_margin_end(10) 54 | 55 | self.set_child(self.box1) # Horizontal box to window 56 | self.box1.append(self.box2) # Put vert box in that box 57 | self.box1.append(self.box3) # And another one, empty for now 58 | 59 | # Add a button 60 | self.button = Gtk.Button(label="Hello") 61 | self.button.connect('clicked', self.hello) 62 | self.box2.append(self.button) # But button in the first of the two vertical boxes 63 | 64 | # Add a check button 65 | self.check = Gtk.CheckButton(label="And goodbye?") 66 | self.box2.append(self.check) 67 | 68 | # Add a box containing a switch and label 69 | self.switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 70 | self.switch_box.set_spacing(5) 71 | 72 | self.switch = Gtk.Switch() 73 | self.switch.set_active(True) # Let's default it to on 74 | self.switch.connect("state-set", self.switch_switched) # Lets trigger a function on state change 75 | 76 | self.label = Gtk.Label(label="A switch") 77 | 78 | self.switch_box.append(self.switch) 79 | self.switch_box.append(self.label) 80 | self.box2.append(self.switch_box) 81 | 82 | self.slider = Gtk.Scale() 83 | self.slider.set_digits(0) # Number of decimal places to use 84 | self.slider.set_range(0, 10) 85 | self.slider.set_draw_value(True) # Show a label with current value 86 | self.slider.set_value(5) # Sets the current value/position 87 | self.slider.connect('value-changed', self.slider_changed) 88 | self.box2.append(self.slider) 89 | 90 | self.header = Gtk.HeaderBar() 91 | self.set_titlebar(self.header) 92 | 93 | self.open_button = Gtk.Button(label="Open") 94 | self.header.pack_start(self.open_button) 95 | self.open_button.set_icon_name("document-open-symbolic") 96 | 97 | self.open_dialog = Gtk.FileChooserNative.new(title="Choose a file", 98 | parent=self, action=Gtk.FileChooserAction.OPEN) 99 | 100 | self.open_dialog.connect("response", self.open_response) 101 | self.open_button.connect("clicked", self.show_open_dialog) 102 | 103 | f = Gtk.FileFilter() 104 | f.set_name("Image files") 105 | f.add_mime_type("image/jpeg") 106 | f.add_mime_type("image/png") 107 | self.open_dialog.add_filter(f) 108 | 109 | 110 | # Create a new "Action" 111 | action = Gio.SimpleAction.new("something", None) 112 | action.connect("activate", self.print_something) 113 | self.add_action(action) # Here the action is being added to the window, but you could add it to the 114 | # application or an "ActionGroup" 115 | 116 | # Create a new menu, containing that action 117 | menu = Gio.Menu.new() 118 | menu.append("Do Something", "win.something") # Or you would do app.grape if you had attached the 119 | # action to the application 120 | 121 | # Create a popover 122 | self.popover = Gtk.PopoverMenu() # Create a new popover menu 123 | self.popover.set_menu_model(menu) 124 | 125 | # Create a menu button 126 | self.hamburger = Gtk.MenuButton() 127 | self.hamburger.set_popover(self.popover) 128 | self.hamburger.set_icon_name("open-menu-symbolic") # Give it a nice icon 129 | 130 | # Add menu button to the header bar 131 | self.header.pack_start(self.hamburger) 132 | 133 | # set app name 134 | GLib.set_application_name("My App") 135 | 136 | # Add an about dialog 137 | action = Gio.SimpleAction.new("about", None) 138 | action.connect("activate", self.show_about) 139 | self.add_action(action) # Here the action is being added to the window, but you could add it to the 140 | menu.append("About", "win.about") 141 | 142 | self.dw = Gtk.DrawingArea() 143 | 144 | # Make it fill the available space (It will stretch with the window) 145 | self.dw.set_hexpand(True) 146 | self.dw.set_vexpand(True) 147 | 148 | # Instead, If we didn't want it to fill the available space but wanted a fixed size 149 | #dw.set_content_width(100) 150 | #dw.set_content_height(100) 151 | 152 | self.dw.set_draw_func(self.draw, None) 153 | self.box3.append(self.dw) 154 | 155 | #evc = Gtk.EventController.key_new() 156 | evk = Gtk.GestureClick.new() 157 | evk.connect("pressed", self.dw_click) # could be "released" 158 | self.dw.add_controller(evk) 159 | 160 | evk = Gtk.EventControllerKey.new() 161 | evk.connect("key-pressed", self.key_press) 162 | self.add_controller(evk) 163 | 164 | self.blobs = [] 165 | 166 | self.cursor_crosshair = Gdk.Cursor.new_from_name("crosshair") 167 | self.dw.set_cursor(self.cursor_crosshair) 168 | 169 | app = self.get_application() 170 | sm = app.get_style_manager() 171 | sm.set_color_scheme(Adw.ColorScheme.PREFER_DARK) 172 | 173 | custom = Custom() 174 | #self.box3.append(custom) 175 | custom.set_hexpand(True) 176 | custom.set_vexpand(True) 177 | 178 | def show_about(self, action, param): 179 | self.about = Gtk.AboutDialog() 180 | self.about.set_transient_for(self) 181 | self.about.set_modal(self) 182 | 183 | self.about.set_authors(["Your Name"]) 184 | self.about.set_copyright("Copyright 2022 Your Full Name") 185 | self.about.set_license_type(Gtk.License.GPL_3_0) 186 | self.about.set_website("http://example.com") 187 | self.about.set_website_label("My Website") 188 | self.about.set_version("1.0") 189 | self.about.set_logo_icon_name("org.example.example") 190 | 191 | self.about.show() 192 | 193 | def key_press(self, event, keyval, keycode, state): 194 | if keyval == Gdk.KEY_q and state & Gdk.ModifierType.CONTROL_MASK: 195 | self.close() 196 | 197 | def show_open_dialog(self, button): 198 | self.open_dialog.show() 199 | 200 | def open_response(self, dialog, response): 201 | if response == Gtk.ResponseType.ACCEPT: 202 | file = dialog.get_file() 203 | filename = file.get_path() 204 | print(filename) 205 | 206 | def dw_click(self, gesture, data, x, y): 207 | self.blobs.append((x, y)) 208 | self.dw.queue_draw() # Force a redraw 209 | 210 | def draw(self, area, c, w, h, data): 211 | # c is a Cairo context 212 | 213 | # Fill background 214 | c.set_source_rgb(0, 0, 0) 215 | c.paint() 216 | 217 | c.set_source_rgb(1, 0, 1) 218 | for x, y in self.blobs: 219 | c.arc(x, y, 10, 0, 2 * 3.1215) 220 | c.fill() 221 | 222 | # Draw a line 223 | c.set_source_rgb(0.5, 0.0, 0.5) 224 | c.set_line_width(3) 225 | c.move_to(10, 10) 226 | c.line_to(w - 10, h - 10) 227 | c.stroke() 228 | 229 | # Draw a rectangle 230 | c.set_source_rgb(0.8, 0.8, 0.0) 231 | c.rectangle(20, 20, 50, 20) 232 | c.fill() 233 | 234 | # Draw some text 235 | c.set_source_rgb(0.1, 0.1, 0.1) 236 | c.select_font_face("Sans") 237 | c.set_font_size(13) 238 | c.move_to(25, 35) 239 | c.show_text("Test") 240 | 241 | 242 | def print_something(self, action, param): 243 | print("Something!") 244 | 245 | def slider_changed(self, slider): 246 | print(int(slider.get_value())) 247 | 248 | def switch_switched(self, switch, state): 249 | print(f"The switch has been switched {'on' if state else 'off'}") 250 | 251 | def hello(self, button): 252 | print("Hello world") 253 | if self.check.get_active(): 254 | print("Goodbye world!") 255 | self.close() 256 | 257 | 258 | class MyApp(Adw.Application): 259 | def __init__(self, **kwargs): 260 | super().__init__(**kwargs) 261 | self.connect('activate', self.on_activate) 262 | 263 | def on_activate(self, app): 264 | self.win = MainWindow(application=app) 265 | self.win.present() 266 | 267 | 268 | app = MyApp(application_id="com.example.GtkApplication") 269 | app.run(sys.argv) 270 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taiko's GTK4 Python tutorial 2 | 3 | Wanna make apps for Linux but not sure how to start with GTK? This guide will hopefully help! 4 | 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. 5 | 6 | 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 7 | without an initial understanding of how to do things. The code examples here should hopefully help. 8 | 9 | How to use this tutorial: You can either follow along or just use it to refer to specific examples. 10 | 11 | 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. 12 | 13 | Topics covered: 14 | 15 | - A basic GTK window 16 | - Widgets: Button, check button, switch, slider 17 | - Layout: Box layout 18 | - Adding a header bar 19 | - Showing an open file dialog 20 | - Adding a menu-button with a menu 21 | - Adding an about dialog 22 | - "Open with" and single instancing 23 | - Custom drawing with Cairo 24 | - Handling mouse input 25 | - Setting the cursor 26 | - Setting dark colour theme 27 | - Spacing and padding 28 | - Selection Grid 29 | - Custom drawing with Snapshot 30 | - Setting the app icon 31 | - UI from graphical designer 32 | 33 | 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. 34 | 35 | It can be helpful to view the [GTK4 Widget Gallery](https://docs.gtk.org/gtk4/visual_index.html) which shows you all the common widgets. 36 | 37 | For Adwaita widgets also see [Adwaita Widget Gallery](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/widget-gallery.html). Though for this tutorial i'll focus on standard GTK4 style widgets. 38 | 39 | 40 | ## A most basic program 41 | 42 | ```python 43 | import gi 44 | gi.require_version('Gtk', '4.0') 45 | from gi.repository import Gtk 46 | 47 | def on_activate(app): 48 | win = Gtk.ApplicationWindow(application=app) 49 | win.present() 50 | 51 | app = Gtk.Application() 52 | app.connect('activate', on_activate) 53 | 54 | app.run(None) 55 | 56 | ``` 57 | 58 | This should display a small blank window. 59 | 60 | ![A blank GTK window](blank.png) 61 | 62 | This is a minimal amount of code to show a window. But we will start off with a better example: 63 | 64 | - Making the code into classes. 'Cause doing it functional style is a little awkward in Python. 65 | - Switching to **Libadwaita**, since many GNOME apps now use its new styling. 66 | - Pass in the app arguments. 67 | - Give the app an application id. 68 | 69 | Here's what we got now: 70 | 71 | ### A better structured basic GTK4 + Adwaita 72 | 73 | 74 | ```python 75 | import sys 76 | import gi 77 | gi.require_version('Gtk', '4.0') 78 | gi.require_version('Adw', '1') 79 | from gi.repository import Gtk, Adw 80 | 81 | 82 | class MainWindow(Gtk.ApplicationWindow): 83 | def __init__(self, *args, **kwargs): 84 | super().__init__(*args, **kwargs) 85 | # Things will go here 86 | 87 | class MyApp(Adw.Application): 88 | def __init__(self, **kwargs): 89 | super().__init__(**kwargs) 90 | self.connect('activate', self.on_activate) 91 | 92 | def on_activate(self, app): 93 | self.win = MainWindow(application=app) 94 | self.win.present() 95 | 96 | app = MyApp(application_id="com.example.GtkApplication") 97 | app.run(sys.argv) 98 | 99 | ``` 100 | 101 | Soo we have an instance of an app class and a window which we extend! We run our app and it makes a window! 102 | 103 | > **Tip:** Don't worry too much if you don't understand the `__init__(self, *args, **kwargs)` stuff for now. 104 | 105 | > **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". 106 | 107 | 108 | ### So! What's next? 109 | 110 | Well, we want to add something to our window. That would likely be a ***layout*** of some sort! 111 | 112 | Most basic layout is a [Box](https://docs.gtk.org/gtk4/class.Box.html). 113 | 114 | Lets add a box to the window! (Where the code comment "*things will go here*" is above) 115 | 116 | ```python 117 | self.box1 = Gtk.Box() 118 | self.set_child(self.box1) 119 | ``` 120 | 121 | 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. 122 | 123 | 124 | ## Add a button! 125 | 126 | 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. 127 | 128 | ```python 129 | self.button = Gtk.Button(label="Hello") 130 | self.box1.append(self.button) 131 | ``` 132 | 133 | Now our app has a button! (The window will be small now) 134 | 135 | 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! 136 | 137 | Here's our MainWindow so far: 138 | 139 | ```python 140 | class MainWindow(Gtk.ApplicationWindow): 141 | def __init__(self, *args, **kwargs): 142 | super().__init__(*args, **kwargs) 143 | self.box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 144 | self.set_child(self.box1) 145 | 146 | self.button = Gtk.Button(label="Hello") 147 | self.box1.append(self.button) 148 | self.button.connect('clicked', self.hello) 149 | 150 | def hello(self, button): 151 | print("Hello world") 152 | ``` 153 | 154 | Cool eh? 155 | 156 | 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: 157 | 158 | ```python 159 | self.box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 160 | ``` 161 | 162 | ## Set some window parameters 163 | 164 | ```python 165 | self.set_default_size(600, 250) 166 | self.set_title("MyApp") 167 | ``` 168 | 169 | ## More boxes 170 | 171 | You'll notice our button is stretched with the window. Let's add two boxes inside that first box we made. 172 | 173 | ```python 174 | self.box1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 175 | self.box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 176 | self.box3 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 177 | 178 | self.button = Gtk.Button(label="Hello") 179 | self.button.connect('clicked', self.hello) 180 | 181 | self.set_child(self.box1) # Horizontal box to window 182 | self.box1.append(self.box2) # Put vert box in that box 183 | self.box1.append(self.box3) # And another one, empty for now 184 | 185 | self.box2.append(self.button) # Put button in the first of the two vertical boxes 186 | ``` 187 | 188 | Now that's more neat! 189 | 190 | ## Add a check button! 191 | 192 | So, we know about a button, next lets add a [Checkbutton](https://docs.gtk.org/gtk4/class.CheckButton.html). 193 | 194 | ```python 195 | ... 196 | self.check = Gtk.CheckButton(label="And goodbye?") 197 | self.box2.append(self.check) 198 | 199 | 200 | def hello(self, button): 201 | print("Hello world") 202 | if self.check.get_active(): 203 | print("Goodbye world!") 204 | self.close() 205 | ``` 206 | 207 | 208 | ![Our window so far](twoitems.png) 209 | 210 | When we click the button, we can check the state of the checkbox! 211 | 212 | ### Extra Tip: Radio Buttons 213 | 214 | 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: 215 | 216 | ```python 217 | radio1 = Gtk.CheckButton(label="test") 218 | radio2 = Gtk.CheckButton(label="test") 219 | radio3 = Gtk.CheckButton(label="test") 220 | radio2.set_group(radio1) 221 | radio3.set_group(radio1) 222 | ``` 223 | 224 | You can handle the toggle signal like this: 225 | 226 | ```python 227 | radio1.connect("toggled", self.radio_toggled) 228 | ``` 229 | 230 | Replace `self.radio_toggled` with your own function. 231 | 232 | 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 233 | the extra parameter in your handler function. 234 | 235 | 236 | ```python 237 | radio1.connect("toggled", self.radio_toggled, "test") 238 | ``` 239 | 240 | (This can apply to other widgets too) 241 | 242 | 243 | ## Add a switch 244 | 245 | For our switch, we'll want to put our switch in a ***Box***, otherwise it'll look all stretched. 246 | 247 | ```python 248 | ... 249 | self.switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 250 | 251 | self.switch = Gtk.Switch() 252 | self.switch.set_active(True) # Let's default it to on 253 | self.switch.connect("state-set", self.switch_switched) # Lets trigger a function 254 | 255 | self.switch_box.append(self.switch) 256 | self.box2.append(self.switch_box) 257 | 258 | def switch_switched(self, switch, state): 259 | print(f"The switch has been switched {'on' if state else 'off'}") 260 | ``` 261 | 262 | Try it out! 263 | 264 | Our switch is looking rather nondescript, so let's add a label to it! 265 | 266 | 267 | ## ...with a Label 268 | 269 | A label is like a basic line of text 270 | 271 | ```python 272 | self.label = Gtk.Label(label="A switch") 273 | self.switch_box.append(self.label) 274 | self.switch_box.set_spacing(5) # Add some spacing 275 | 276 | ``` 277 | 278 | It should look like this now: 279 | 280 | ![Our window including switch and label](switch.png) 281 | 282 | The file `part1.py` is an example of the code so far. 283 | 284 | ## Adding your custom CSS stylesheet 285 | 286 | Did you know you can use **some** CSS rules in GTK? 287 | Lets create a new `style.css` file that we can use to apply properties to our new label: 288 | 289 | ```css 290 | /* Let's create a title class */ 291 | .title { 292 | font-size: 25px; 293 | font-weight: bold; 294 | } 295 | ``` 296 | 297 | Then, we need to load the CSS file in our application; to achieve this, we need a [CssProvider](https://docs.gtk.org/gtk4/class.CssProvider.html). 298 | 299 | ```python 300 | # first, we need to add Gdk to our imports 301 | from gi.repository import Gtk, Gdk 302 | 303 | css_provider = Gtk.CssProvider() 304 | css_provider.load_from_path('style.css') 305 | Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 306 | ``` 307 | 308 | Finally, we can add the `title` class to our `label` 309 | 310 | ```python 311 | self.label.set_css_classes(['title']) 312 | ``` 313 | 314 | 315 | ## Adding a slider (Aka scale) 316 | 317 | Here's an example of adding a [Scale](https://docs.gtk.org/gtk4/ctor.Scale.new.html) with a range from 0 to 10 318 | 319 | ```python 320 | self.slider = Gtk.Scale() 321 | self.slider.set_digits(0) # Number of decimal places to use 322 | self.slider.set_range(0, 10) 323 | self.slider.set_draw_value(True) # Show a label with current value 324 | self.slider.set_value(5) # Sets the current value/position 325 | self.slider.connect('value-changed', self.slider_changed) 326 | self.box2.append(self.slider) 327 | 328 | def slider_changed(self, slider): 329 | print(int(slider.get_value())) 330 | ``` 331 | 332 | ## Adding a button into the header bar 333 | 334 | First we need to make a header bar 335 | 336 | ```python 337 | self.header = Gtk.HeaderBar() 338 | self.set_titlebar(self.header) 339 | ``` 340 | 341 | Simple. 342 | 343 | Now add a button 344 | 345 | ```python 346 | self.open_button = Gtk.Button(label="Open") 347 | self.header.pack_start(self.open_button) 348 | ``` 349 | 350 | We already know how to connect a function to the button, so I've omitted that. 351 | 352 | Done! But... it would look nicer with an icon rather than text. 353 | 354 | ```python 355 | self.open_button.set_icon_name("document-open-symbolic") 356 | ``` 357 | 358 | This will be an icon name from the icon theme. 359 | 360 | For some defaults you can take a look at `/usr/share/icons/Adwaita/scalable/actions`. 361 | 362 | If you were adding a new action icon it would go in `/usr/share/icons/hicolor/scalable/actions` 363 | 364 | > **Help! Todo!** Is this the best way? How do icons work in a development environment? 365 | > 366 | 367 | ## Adding a file chooser 368 | 369 | Here we use [***Gtk.FileDialog***](https://docs.gtk.org/gtk4/class.FileDialog.html) to present an open file dialog to the user. 370 | 371 | ```python 372 | self.open_dialog = Gtk.FileDialog.new() 373 | self.open_dialog.set_title("Select a File") 374 | 375 | def show_open_dialog(self, button): 376 | self.open_dialog.open(self, None, self.open_dialog_open_callback) 377 | 378 | def open_dialog_open_callback(self, dialog, result): 379 | try: 380 | file = dialog.open_finish(result) 381 | if file is not None: 382 | print(f"File path is {file.get_path()}") 383 | # Handle loading file from here 384 | except GLib.Error as error: 385 | print(f"Error opening file: {error.message}") 386 | 387 | ``` 388 | 389 | Adding a filter and setting it as the default: 390 | 391 | ```python 392 | f = Gtk.FileFilter() 393 | f.set_name("Image files") 394 | f.add_mime_type("image/jpeg") 395 | f.add_mime_type("image/png") 396 | 397 | filters = Gio.ListStore.new(Gtk.FileFilter) # Create a ListStore with the type Gtk.FileFilter 398 | filters.append(f) # Add the file filter to the ListStore. You could add more. 399 | 400 | self.open_dialog.set_filters(filters) # Set the filters for the open dialog 401 | self.open_dialog.set_default_filter(f) 402 | ```` 403 | 404 | 405 | ## Adding a button with menu 406 | 407 | For this there are multiple new concepts we need to introduce: 408 | 409 | - The [***MenuButton***](https://docs.gtk.org/gtk4/class.MenuButton.html) widget. 410 | - 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. 411 | - A [***Menu***](https://docs.gtk.org/gio/class.Menu.html). This is an abstract model of a menu. 412 | - [***Actions***](https://docs.gtk.org/gio/class.SimpleAction.html). An abstract action that can be connected to our abstract menu. 413 | 414 | So, we click a MenuButton, which shows a Popover that was generated from a MenuModel that is composed of Actions. 415 | 416 | First make sure `Gio` is added to the list of things we're importing from `gi.repository`: 417 | 418 | ```python 419 | from gi.repository import Gtk, Adw, Gio 420 | ``` 421 | 422 | ```python 423 | # Create a new "Action" 424 | action = Gio.SimpleAction.new("something", None) 425 | action.connect("activate", self.print_something) 426 | self.add_action(action) # Here the action is being added to the window, but you could add it to the 427 | # application or an "ActionGroup" 428 | 429 | # Create a new menu, containing that action 430 | menu = Gio.Menu.new() 431 | menu.append("Do Something", "win.something") # Or you would do app.something if you had attached the 432 | # action to the application 433 | 434 | # Create a popover 435 | self.popover = Gtk.PopoverMenu() # Create a new popover menu 436 | self.popover.set_menu_model(menu) 437 | 438 | # Create a menu button 439 | self.hamburger = Gtk.MenuButton() 440 | self.hamburger.set_popover(self.popover) 441 | self.hamburger.set_icon_name("open-menu-symbolic") # Give it a nice icon 442 | 443 | # Add menu button to the header bar 444 | self.header.pack_start(self.hamburger) 445 | 446 | def print_something(self, action, param): 447 | print("Something!") 448 | 449 | ``` 450 | 451 | ## Add an about dialog 452 | 453 | ```python 454 | from gi.repository import Gtk, Adw, Gio, GLib # Add GLib to imports 455 | ``` 456 | 457 | ```python 458 | # Set app name 459 | GLib.set_application_name("My App") 460 | 461 | # Create an action to run a *show about dialog* function we will create 462 | action = Gio.SimpleAction.new("about", None) 463 | action.connect("activate", self.show_about) 464 | self.add_action(action) 465 | 466 | menu.append("About", "win.about") # Add it to the menu we created in previous section 467 | 468 | def show_about(self, action, param): 469 | self.about = Gtk.AboutDialog() 470 | self.about.set_transient_for(self) # Makes the dialog always appear in from of the parent window 471 | self.about.set_modal(True) # Makes the parent window unresponsive while dialog is showing 472 | 473 | self.about.set_authors(["Your Name"]) 474 | self.about.set_copyright("Copyright 2022 Your Full Name") 475 | self.about.set_license_type(Gtk.License.GPL_3_0) 476 | self.about.set_website("http://example.com") 477 | self.about.set_website_label("My Website") 478 | self.about.set_version("1.0") 479 | self.about.set_logo_icon_name("org.example.example") # The icon will need to be added to appropriate location 480 | # E.g. /usr/share/icons/hicolor/scalable/apps/org.example.example.svg 481 | 482 | self.about.set_visible(True) 483 | 484 | ``` 485 | 486 | ### add about window (better About Dialog) 487 | ```python 488 |          dialog = Adw.AboutWindow(transient_for=app.get_active_window()) 489 |         dialog.set_application_name("App name") 490 |         dialog.set_version("1.0") 491 |         dialog.set_developer_name("Developer") 492 |         dialog.set_license_type(Gtk.License(Gtk.License.GPL_3_0)) 493 |         dialog.set_comments("Adw about Window example") 494 |         dialog.set_website("https://github.com/Tailko2k/GTK4PythonTutorial") 495 |         dialog.set_issue_url("https://github.com/Tailko2k/GTK4PythonTutorial/issues") 496 |         dialog.add_credit_section("Contributors", ["Name1 url"]) 497 |         dialog.set_translator_credits("Name1 url") 498 |         dialog.set_copyright("© 2022 developer") 499 |         dialog.set_developers(["Developer"]) 500 |         dialog.set_application_icon("com.github.devname.appname") # icon must be uploaded in ~/.local/share/icons or /usr/share/icons 501 | 502 | dialog.set_visible(True) 503 | ``` 504 | For further reading on what you can add, see [***AboutDialog***](https://docs.gtk.org/gtk4/class.AboutDialog.html). 505 | 506 | ![A basic menu in headerbar](menu1.png) 507 | 508 | ## "Open with" and single instancing 509 | 510 | > Note that I haven't fully tested the code in this section 511 | 512 | 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 513 | with our application, such as a command line argument, or when they click "Open with" in their file browser etc. 514 | 515 | Also, when the user launches another instance, we may want to determine the behavior of if the file is opened in the original 516 | 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. 517 | 518 | By default, our [GApplication](https://docs.gtk.org/gio/class.Application.html) will maintain the first process as the primary process, and if 519 | a second process is launched, one of two signals will be called on that first primary process, with the 2nd process promptly exiting. 520 | 521 | 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. 522 | 523 | Currently, in our example app, `activate` will launch another identical window. (The app will exit when all windows are closed) 524 | 525 | 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. 526 | 527 | Maintain a single instance: 528 | 529 | ```python 530 | class MyApp(Adw.Application): 531 | def __init__(self, **kwargs): 532 | super().__init__(**kwargs) 533 | self.connect('activate', self.on_activate) 534 | self.win = None # Forgot to add this originally 535 | 536 | def on_activate(self, app): 537 | if not self.win: # added this condition 538 | self.win = MainWindow(application=app) 539 | self.win.present() # if window is already created, this will raise it to the front 540 | ``` 541 | 542 | What about opening files? We need to implement that function: 543 | 544 | ```python 545 | class MyApp(Adw.Application): 546 | def __init__(self, **kwargs): 547 | super().__init__(**kwargs) 548 | self.connect('activate', self.on_activate) 549 | self.connect('open', self.on_open) 550 | self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN) # Need to tell GApplication we can handle this 551 | self.win = None 552 | 553 | def on_activate(self, app): 554 | if not self.win: 555 | self.win = MainWindow(application=app) 556 | self.win.present() 557 | 558 | def on_open(self, app, files, n_files, hint): 559 | self.on_activate(app) # Adding this because window may not have been created yet with this entry point 560 | for file in n_files: 561 | print("File to open: " + file.get_path()) # How you handle it from here is up to you, I guess 562 | 563 | ``` 564 | 565 | Note that an "Open with" option with your application would 566 | require a `.desktop` file that registers a mime type that your application can open, but setting up a desktop 567 | file is outside the scope of this tutorial. 568 | 569 | 570 | ## Custom drawing area using Cairo 571 | 572 | There are two main methods of custom drawing in GTK4, the Cairo way and the Snapshot way. Cairo provides a more high level 573 | drawing API but uses slow software rendering. Snapshot uses a little more low level API but uses much faster hardware accelerated rendering. 574 | 575 | To draw with Cairo we use the [***DrawingArea***](https://docs.gtk.org/gtk4/class.DrawingArea.html) widget. 576 | 577 | ```python 578 | 579 | self.dw = Gtk.DrawingArea() 580 | 581 | # Make it fill the available space (It will stretch with the window) 582 | self.dw.set_hexpand(True) 583 | self.dw.set_vexpand(True) 584 | 585 | # Instead, If we didn't want it to fill the available space but wanted a fixed size 586 | #self.dw.set_content_width(100) 587 | #self.dw.set_content_height(100) 588 | 589 | self.dw.set_draw_func(self.draw, None) 590 | self.box3.append(self.dw) 591 | 592 | def draw(self, area, c, w, h, data): 593 | # c is a Cairo context 594 | 595 | # Fill background with a colour 596 | c.set_source_rgb(0, 0, 0) 597 | c.paint() 598 | 599 | # Draw a line 600 | c.set_source_rgb(0.5, 0.0, 0.5) 601 | c.set_line_width(3) 602 | c.move_to(10, 10) 603 | c.line_to(w - 10, h - 10) 604 | c.stroke() 605 | 606 | # Draw a rectangle 607 | c.set_source_rgb(0.8, 0.8, 0.0) 608 | c.rectangle(20, 20, 50, 20) 609 | c.fill() 610 | 611 | # Draw some text 612 | c.set_source_rgb(0.1, 0.1, 0.1) 613 | c.select_font_face("Sans") 614 | c.set_font_size(13) 615 | c.move_to(25, 35) 616 | c.show_text("Test") 617 | 618 | ``` 619 | 620 | ![A drawing area](draw.png) 621 | 622 | Further resources on Cairo: 623 | 624 | - [PyCairo Visual Documentation](https://seriot.ch/pycairo/) 625 | 626 | Note that Cairo uses software rendering. For accelerated rendering, Gtk Snapshot can be used, see sections further down below. 627 | 628 | ## Input handling in our drawing area 629 | 630 | ### Handling a mouse / touch event 631 | 632 | ```python 633 | ... 634 | evk = Gtk.GestureClick.new() 635 | evk.connect("pressed", self.dw_click) # could be "released" 636 | self.dw.add_controller(evk) 637 | 638 | self.blobs = [] 639 | 640 | def dw_click(self, gesture, data, x, y): 641 | self.blobs.append((x, y)) 642 | self.dw.queue_draw() # Force a redraw 643 | 644 | def draw(self, area, c, w, h, data): 645 | # c is a Cairo context 646 | 647 | # Fill background 648 | c.set_source_rgb(0, 0, 0) 649 | c.paint() 650 | 651 | c.set_source_rgb(1, 0, 1) 652 | for x, y in self.blobs: 653 | c.arc(x, y, 10, 0, 2 * 3.1415926) 654 | c.fill() 655 | ... 656 | 657 | ``` 658 | 659 | ![A drawing area with purple dots where we clicked](dots.png) 660 | 661 | Ref: [GestureClick](https://docs.gtk.org/gtk4/class.GestureClick.html) 662 | 663 | Extra example. If we wanted to listen to other mouse button types: 664 | 665 | ```python 666 | ... 667 | evk.set_button(0) # 0 for all buttons 668 | def dw_click(self, gesture, data, x, y): 669 | button = gesture.get_current_button() 670 | print(button) 671 | ``` 672 | 673 | 674 | See also: [EventControllerMotion](https://docs.gtk.org/gtk4/class.EventControllerMotion.html). Example: 675 | 676 | ```python 677 | evk = Gtk.EventControllerMotion.new() 678 | evk.connect("motion", self.mouse_motion) 679 | self.add_controller(evk) 680 | def mouse_motion(self, motion, x, y): 681 | print(f"Mouse moved to {x}, {y}") 682 | ``` 683 | 684 | See also: [EventControllerKey](https://docs.gtk.org/gtk4/class.EventControllerKey.html) 685 | 686 | ```python 687 | evk = Gtk.EventControllerKey.new() 688 | evk.connect("key-pressed", self.key_press) 689 | self.add_controller(evk) # add to window 690 | def key_press(self, event, keyval, keycode, state): 691 | if keyval == Gdk.KEY_q and state & Gdk.ModifierType.CONTROL_MASK: # Add Gdk to your imports. i.e. from gi.repository import Gdk 692 | self.close() 693 | ``` 694 | 695 | ## Setting the cursor 696 | 697 | We can set a cursor for a widget. 698 | 699 | First we need to import **Gdk**, so we append it to this line like so: 700 | 701 | ```python 702 | from gi.repository import Gtk, Adw, Gio, Gdk 703 | ``` 704 | 705 | Now setting the cursor is easy. 706 | 707 | ```python 708 | self.cursor_crosshair = Gdk.Cursor.new_from_name("crosshair") 709 | self.dw.set_cursor(self.cursor_crosshair) 710 | ``` 711 | 712 | You can find a list of common cursor names [here](https://docs.gtk.org/gdk4/ctor.Cursor.new_from_name.html). 713 | 714 | # Setting a dark color scheme 715 | 716 | We can use: 717 | 718 | ```python 719 | app = self.get_application() 720 | sm = app.get_style_manager() 721 | sm.set_color_scheme(Adw.ColorScheme.PREFER_DARK) 722 | ``` 723 | 724 | See [here](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.0.0/styles-and-appearance.html) for more details. 725 | 726 | 727 | # Spacing and padding 728 | 729 | 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 730 | margin to our **box** layout. 731 | 732 | ```python 733 | self.box2.set_spacing(10) 734 | self.box2.set_margin_top(10) 735 | self.box2.set_margin_bottom(10) 736 | self.box2.set_margin_start(10) 737 | self.box2.set_margin_end(10) 738 | ``` 739 | 740 | ![Spacing and padding](spacing.png) 741 | 742 | # Using GridView 743 | 744 | Here Ill show how to make a [***GridView***](https://docs.gtk.org/gtk4/class.GridView.html). The setup is similar for other wigets like ***ListView*** and ***ColumnsView***. 745 | 746 | ![GridView](grid.png) 747 | 748 | First lets make a GridView and attatch it to our second vert box. 749 | 750 | ```python 751 | 752 | self.grid1 = Gtk.GridView() 753 | self.box3.append(self.grid1) 754 | 755 | fruits = ["Banana", "Apple", "Strawberry", "Pear", "Watermelon", "Blueberry"] 756 | ``` 757 | 758 | That part was easy! But it gets a little more complicated from here. In order for these kinds of widgets to work we need two things, a **model** and a **factory**. 759 | 760 | Lets start with the **model**. The model will hold the basis for the information we want in each item in the grid. 761 | 762 | First we can create an object that will hold the data we want for each item in the list/grid. 763 | 764 | ```python 765 | class Fruit(GObject.Object): 766 | name = GObject.Property(type=str) 767 | def __init__(self, name): 768 | super().__init__() 769 | self.name = name 770 | ``` 771 | 772 | Then we create each object and put them in a ListStore. Then from that ListStore we create a SelectionModel, in this case im using a *SingleSelection*. 773 | 774 | Then we set that selection model as the model for the grid. 775 | 776 | ```python 777 | self.ls = Gio.ListStore() 778 | 779 | for f in fruits: 780 | self.ls.append(Fruit(f)) 781 | 782 | ss = Gtk.SingleSelection() 783 | ss.set_model(self.ls) 784 | 785 | self.grid1.set_model(ss) 786 | ``` 787 | 788 | Next we need a **factory**. The factory is what creates the widgets in the grid for each item in the model. 789 | 790 | ```python 791 | factory = Gtk.SignalListItemFactory() 792 | 793 | def f_setup(fact, item): 794 | label = Gtk.Label(halign=Gtk.Align.START) 795 | label.set_selectable(False) 796 | item.set_child(label) 797 | 798 | factory.connect("setup", f_setup) 799 | 800 | def f_bind(fact, item): 801 | item.get_child().set_label(item.get_item().name) 802 | 803 | factory.connect("bind", f_bind) 804 | 805 | self.grid1.set_factory(factory) 806 | 807 | ``` 808 | 809 | That should then work. 810 | 811 | The above is useful if the displayed data wont change, but if it is to change dynamically we need to "bind" the property so that any changes are synced. Here is a revised bind function using bind_property: 812 | 813 | ```python 814 | def f_bind(fact, item): 815 | fruit = item.get_item() 816 | fruit.bind_property("name", 817 | item.get_child(), "label", 818 | GObject.BindingFlags.SYNC_CREATE) 819 | ``` 820 | 821 | Any changes to the name will automatically update the display. 822 | 823 | To get the selected item in the grid: 824 | 825 | ```python 826 | print(ss.get_selected_item().name) 827 | ``` 828 | 829 | To detect when the selected item has changed: 830 | 831 | ```python 832 | def on_selected_items_changed(selection, position, n_items): 833 | selected_item = selection.get_selected_item() 834 | if selected_item is not None: 835 | print(f"Selected item changed to: {selected_item.name}") 836 | ss.connect("selection-changed", on_selected_items_changed) 837 | ``` 838 | 839 | 840 | 841 | To detect clicks on an item: ***TODO** 842 | 843 | 844 | # Custom drawing with Snapshot 845 | 846 | As mentioned in the Cairo section, Snapshot uses fast hardware accelerated drawing, but it's a little more complicated to 847 | use. Treat this section as more of a general guide of how it works than a tutorial of how you should do things. 848 | 849 | First, we create our own custom widget class which will implement the [***Snapshot***](https://docs.gtk.org/gtk4/class.Snapshot.html) virtual method. 850 | (To implement a virtual method we need to prepend `do_` to the name as it is in the docs.) 851 | 852 | ```python 853 | 854 | class CustomDraw(Gtk.Widget): 855 | def __init__(self): 856 | super().__init__() 857 | 858 | def do_snapshot(self, s): 859 | pass 860 | ``` 861 | 862 | Then it can be added in the same way as any other widget. If we want to manually trigger a redraw we can use 863 | the same `.queue_draw()` method call on it. 864 | 865 | If we want the widget to have a dynamic size we can set the usual `.set_hexpand(True)`/`.set_vexpand(True)`, but if it 866 | is to have a fixed size, you would need to implement the [**Measure**](https://docs.gtk.org/gtk4/vfunc.Widget.measure.html) virtual method. 867 | 868 | 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 869 | could easily create your own helper functions. You can use your imagination! 870 | 871 | Here's some examples: 872 | 873 | ### Draw a solid rectangle 874 | 875 | Here we use: 876 | - [**RGBA Struct**](https://docs.gtk.org/gdk4/struct.RGBA.html) 877 | - [**Rect**](http://ebassi.github.io/graphene/docs/graphene-Rectangle.html) 878 | 879 | ```python 880 | def do_snapshot(self, s): 881 | colour = Gdk.RGBA() 882 | colour.parse("#e80e0e") 883 | 884 | rect = Graphene.Rect().init(10, 10, 40, 60) # Add Graphene to your imports. i.e. from gi.repository import Graphene 885 | 886 | s.append_color(colour, rect) 887 | ``` 888 | 889 | ### Draw a solid rounded rectangle / circle 890 | 891 | This is a little more complicated... 892 | 893 | - [***RoundedRect***](https://docs.gtk.org/gsk4/struct.RoundedRect.html) 894 | 895 | ```python 896 | colour = Gdk.RGBA() 897 | colour.parse("rgb(159, 222, 42)") # another way of parsing 898 | 899 | rect = Graphene.Rect().init(50, 70, 40, 40) 900 | 901 | rounded_rect = Gsk.RoundedRect() # Add Gsk to your imports. i.e. from gi.repository import Gsk 902 | rounded_rect.init_from_rect(rect, radius=20) # A radius of 90 would make a circle 903 | 904 | s.push_rounded_clip(rounded_rect) 905 | s.append_color(colour, rect) 906 | s.pop() # remove the clip 907 | ``` 908 | 909 | ### Outline of rect / rounded rect / circle 910 | 911 | Fairly straightforward, see [append_border](https://docs.gtk.org/gtk4/method.Snapshot.append_border.html). 912 | 913 | ### An Image 914 | 915 | - See [***Texture***](https://docs.gtk.org/gdk4/class.Texture.html). 916 | 917 | ```python 918 | texture = Gdk.Texture.new_from_filename("example.png") 919 | # Warning: For the purposes of demonstration ive shown this declared in our drawing function, 920 | # but of course you would REALLY need to define this somewhere else so that its only called 921 | # once as we don't want to reload/upload the data every draw call. 922 | 923 | # Tip: There are other functions to load image data from in memory pixel data 924 | 925 | rect = Graphene.Rect().__init__(50, 50, texture.get_width(), texture.get_height()) # See warning below 926 | s.append_texture(texture, rect) 927 | 928 | ``` 929 | 930 | Warning: On a HiDPI display the logical and physical measurements may differ in scale, typically by a factor of 2. In most places 931 | we're dealing in logical units but these methods give physical units. So... you might not want to define the size of the rectangle 932 | by the texture. 933 | 934 | ### Text 935 | 936 | Text is drawn using Pango layouts. Pango is quite powerful and really needs a whole tutorial on its own, but here's 937 | a basic example of a single line of text: 938 | 939 | ```python 940 | colour = Gdk.RGBA() 941 | colour.red = 0.0 # Another way of setting colour 942 | colour.green = 0.0 943 | colour.blue = 0.0 944 | colour.alpha = 1.0 945 | 946 | font = Pango.FontDescription.new() 947 | font.set_family("Sans") 948 | font.set_size(12 * Pango.SCALE) # todo how do we follow the window scaling factor? 949 | 950 | context = self.get_pango_context() 951 | layout = Pango.Layout(context) # Add Pango to your imports. i.e. from gi.repository import Pango 952 | layout.set_font_description(font) 953 | layout.set_text("Example text") 954 | 955 | point = Graphene.Point() 956 | point.x = 50 # starting X co-ordinate 957 | point.y = 50 # starting Y co-ordinate 958 | 959 | s.save() 960 | s.translate(point) 961 | s.append_layout(layout, colour) 962 | s.restore() 963 | 964 | ``` 965 | 966 | ## Setting the App Icon 967 | 968 | How to set an icon for your app. 969 | 970 | First make sure you created an application ID as mentioned near the begnning of this tutorial, e.g. `com.github.me.myapp`. 971 | 972 | Then your icon file(s) will go in the `hicolor` theme once you package your app. (hicolor is the base theme that all other themes inherit). 973 | 974 | `/usr/share/icons/hicolor/128x128/apps/com.github.me.myapp.png` for a raster image for example, and/or 975 | `/usr/share/icons/hicolor/scalable/apps/com.github.me.myapp.svg` for vector. 976 | 977 | (Or locally in `~/.local/share/icons/hicolor/...`) 978 | 979 | A single svg is sufficent for GNOME, but other desktop environments may look for PNG's. 980 | 981 | Typically you would store that hicolor directory structure in your project directory as `data/icons/hicolor/...`. When packaging you copy it to the appropriate location on the system. 982 | 983 | Once you make a .desktop file, in it set the icon field to your app id: `Icon=com.github.me.myapp`. The icon will work once the .desktop file and icons are installed to the appropriate locations on your system. 984 | 985 | **Q: OK but how do I programmatically set the icon of my window?** 986 | 987 | **A:** In modern desktop Linux the idea is you don't. Wayland provides no mechanism for a client program to set an icon. How it works is the Wayland client sends your application ID to the window manager, its your window manager which then takes responsibility for picking the icon itself. This is done by referencing the .desktop file, where that application ID corresponds to the name of the desktop file. 988 | 989 | ## UI from Graphical Designer 990 | 991 | It may be faster to mock up a UI in a graghical designer such as [Cambalache](https://flathub.org/apps/ar.xjuan.Cambalache). This will a give you a .ui file which your 992 | GTK application can use to generate its UI. 993 | 994 | In Cambalache try make a window, add some layouts, and a button. Its up to you. Make sure to set an object id for objects you want to reference in your code, 995 | including the main window. When you click export it will generate a .ui XML file. 996 | 997 | ![Cambalache](cam.png) 998 | 999 | For my design I get the XML: 1000 | 1001 | ```xml 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 200 1009 | 400 1010 | UI XML Test 1011 | 1012 | 1013 | 1014 | 1015 | 5 1016 | 5 1017 | 5 1018 | 5 1019 | vertical 1020 | 1021 | 1022 | start 1023 | True 1024 | Test button 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | ``` 1034 | 1035 | Then we can write our app in Python and load the UI using a [Builder](https://docs.gtk.org/gtk4/class.Builder.html). 1036 | 1037 | ```python 1038 | import sys 1039 | import gi 1040 | gi.require_version('Gtk', '4.0') 1041 | gi.require_version('Adw', '1') 1042 | from gi.repository import Gtk, Adw 1043 | 1044 | class MyApp(Adw.Application): 1045 | def __init__(self, **kwargs): 1046 | super().__init__(**kwargs) 1047 | self.connect('activate', self.on_activate) 1048 | 1049 | def on_activate(self, app): 1050 | # Create a Builder 1051 | builder = Gtk.Builder() 1052 | builder.add_from_file("test.ui") 1053 | 1054 | # Obtain the button widget and connect it to a function 1055 | button = builder.get_object("button1") 1056 | button.connect("clicked", self.hello) 1057 | 1058 | # Obtain and show the main window 1059 | self.win = builder.get_object("main_window") 1060 | self.win.set_application(self) # Application will close once it no longer has active windows attached to it 1061 | self.win.present() 1062 | 1063 | def hello(self, button): 1064 | print("Hello") 1065 | 1066 | app = MyApp(application_id="com.example.GtkApplication") 1067 | app.run(sys.argv) 1068 | 1069 | ``` 1070 | 1071 | So in this method we simply obtain the objects defined by our XML using `builder.get_object()` 1072 | 1073 | In the above example I get the button I created and connect it to a function. 1074 | 1075 | ***todo:*** using resoure files 1076 | 1077 | 1078 | ## Todo... 1079 | 1080 | Text box: [Entry](https://docs.gtk.org/gtk4/class.Entry.html) 1081 | 1082 | Number changer: [SpinButton](https://docs.gtk.org/gtk4/class.SpinButton.html) 1083 | 1084 | Picture. 1085 | 1086 | Custom Styles. 1087 | 1088 | 1089 | 1090 | 1091 | --------------------------------------------------------------------------------