Pygame is a great and simple way to create basic windowed applications. It is therefore a privileged entry point for python learners to get out of the console. However, people often ask for GUI features in pygame: "How can I have a simple button ?" or "How can the user enter their name ?".
The goal of this library is to answer all those questions and provide the easiest possible interface to create, update, control and manipulate what we will call widgets. (Buttons, TextBoxes, Sliders, Switches...)
- KISS: Keep it sweet and simple
- Simple and ugly are not synonyms
- Simple can be powerful
- Simple should be customisable
- No code is better than ugly code
You can install a stable version using pip
pip install graphalama
Or a developer version with
git clone https://gitlab.com/lama-corp/graphalama
cd graphalama
python setup.py install -U
Most simple pygame applications look like this
import pygame
pygame.init()
# Setup of variables, objects...
stop = False
screen = pygame.display.get((400, 400))
# Main loop
while not stop:
for event in pygame.event.get():
if event.type == pygame.QUIT:
stop = True
else:
# Handle inputs, if the user click on some clickable places, buttons...
# Handle the keys pressed to move players, input text...
# It's the logic of the game that depends of inputs
...
# Then you have an internal logic, applying gravity, running AIs, anything that changes itself every frame
# And finally you render everything with some more or less advanced technics
screen.fill((255, 255, 255))
pygame.display.update()
For any widget you need to have at least 3 parts in the code to implement it: creation, handling the input, and rendering it. Some widget can have an internal logic going like clock or timer but we'll come back on that later.
Say you want to add a button in your window. A big green play button. We reduce each step to the minimum.
- Creation :
play_button = Button("PLAY", bg_color=GREEN, function=play)
- Input handling (clicks):
play_button.update(event)
- Rendering:
play_button.render(screen)
A minimal hello world would look like this:
import pygame
from graphalama.widgets import *
pygame.init()
screen = pygame.display.set_mode((800, 500))
# creation
hello_text = SimpleText("Hello World!")
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
# a text doesn't change on input so we don't need to update it
screen.fill((255, 255, 255))
hello_text.render(screen)
pygame.display.flip()
Before knowing how to customise your widgets so they match your style, we have to understand what is a widget. A widget is made of three things:
- a Shadow: optional, it provides a nice depth to our application and accentuate our widgets
- a Shape: defines the shape of the background. It also defines the borders, the size and where you can click/select the widget. To list a few common: Rectangle, Circle, RoundedRect...
- a content: the information of the widget, it can be the widget's own content or a child widget (did you know that we can put any widget inside a Button ?)
Those three layers are mixed together to create the widget. To customise our widgets we can therefore easily change one of those three layers.
First, let's have a look to the common parameters that aren't part of the shadow, shape or content.
With graphalama, as with pygame a position is a couple of integers, (0, 0) being the topleft corner and (x, y)
being the x
-st pixel on the y
-st line of the screen.
Every time we'll need a position as an argument for a function it will be a tuple (x, y)
of integers.
Graphalama has a powerful positioning system for the widgets, whose goal is to be able to design resizable applications without more effort.
A widget's position is defined by two elements: its coordinates pos
and its anchor
. The anchor
defines where the widget is anchored,
that is, where it will stay when the window is resized and where it will be first put on the screen.
For instance, if the anchor is TOPLEFT
, the widget will always stay at the same distance of the top and the left of the screen, no matter how the window is resized.
Its pos
will then be the topleft corner of the widget. This is the default anchor, so we can place our widgets like we would do in most frameworks.
But say, we want a widget to always stay on the right side of the screen (like a "next" button), we just need to set the anchor to RIGHT
and our widget will stay at say 5 pixels from the right border even if we resize the window. Here pos
will be the middle point on the right
of the widget. Here's a small drawing to explain it:
This button was created in a 800x500 window with
Button("Next ->", quit, (770, 250), RoundedRect((200, 100)), bg_color=GREEN, anchor=RIGHT)
If we resize the window, it will always stay 30px from the right edge.
Two conclude, there are four things to remember:
- We can anchor a widget to one or more side of the window by setting the anchor to
RIGHT
,LEFT
,TOP
andBOTTOM
. Those constants are defined ingraphalama.constants
. - We can combine the anchors with the pipe symbol,
|
, so we anchor a widget to the top right corner withanchor=TOP|RIGHT
- Not only the position, but also the size can be controlled with anchors. If
anchor=LEFT|RIGHT
, the distance from the widget to both side will stay constant. Therefore the width will stretch if the window gets bigger and shrink if the window gets smaller. - The
pos
of a widget always reflect the anchor: if the anchor isBOTTOM|RIGHT
thepos
will be the bottom right of the widget. If it isLEFT
,pos
will be the midleft of the widget. However we can always get the topleft of the widget withwidget.topleft
.widget.x
andwidget.y
also work.
That's everything for a very technical part, but it gets very intuitive once we get used to it and saves a lot of time.
Graphalama has a great range of options for coloring widgets. No more of the common monochromatic widgets without transparency!
Usually widgets have three colors:
color
, the foreground color, ie. the text of a buttonbg_color
, the background color, that's how the shape is filledborder_color
, the color of the border
Those three colors accepts different types, and you can pass any of them at any time:
- a RGB or RGBA tuple of integers between 0 and 255, you can find a lot of pre-defined colors in
graphalama.constants
- a
Gradient
from one color to another (both can be RGB or RGBA) - a
MultiGradient
to have a multicolored gradient - a
ImageBrush
to draw an image - a
ImageListBrush
to draw multiple images and swap between them.
You can get the code for this example here.
What actually is a color ?
It is an object with a paint
method that takes a Surface
and colorize it.
Tuples like (R, G, B)
or (R, G, B, A)
are both valid colors, with 0 <= R, G, B, A <= 255
.
They are automatically converted to a Color
object for ease of use. And as expected, they fill the whole area with the same color.
A Gradient(start, end, horizontal)
will fill a shape with a linear interpolation between the two colors.
start
and end
must be two tuples and if horizontal == False
, the gradient will be drawn vertical,
so start
will be at the top of the shape and end
at the bottom.
):
A Multigradient(*colors, positions=None, horizontal=True)
is like a Gradient
but with more than two colors.
Example for an equally spaced blue-yellow-orange-red gradient:
from graphalama.constants import BLUE, YELLOW, ORANGE, RED
MultiGradient(BLUE, YELLOW, ORANGE, RED)
You can also choose where the color points are. Here the orange-red part will take the left half of the gradient whereas the blue-yellow and yellow-orange would take only a fourth. The positions are between 0 and
MultiGradient(BLUE, YELLOW, ORANGE, RED, positions=(0, 1/4, 1/2, 1))
Of course, the number of positions must match the number of colors, but the positions do not have to start at 0 and
end at 1. For instance is the positions are (1/2, 2/3, 1)
, the first half of the surface will be exactly of the first color.
If horizontal == False
, then the gradient is drawn top to bottom and not left to right.
Let's start again with our play button: Button("PLAY", bg_color=GREEN, function=play)
.
The default shape is a Rectangle
, So that's what it would look like:
But you're not limited to rectangles, you can have a rounded rectangle, a circle or even any parametric shape or any custom shape (we'll come back on creating your custom shapes later)
You can get the code for this example here.
There are quite a few things to note here.
First of all, if we don't precise the position of the widgets, the will just stack vertically in the center of the screen.
Then, we can give any shape to a widget. Every widget can have any shape, we just need to pass it as the shape
argument in the object initialisation.
Shapes are found in graphalama.shapes
, they all accept the same parameters :
- First the
size
of the widget, if none is given the widget will just fit its content - Then the arguments for the specific shape, ie. the rounding of the rectangle, the equations of the parametric curve...
- The
border
comes next, which specifies the width of the widget's border - The
padding
is then the space between the content of the widget and the border. - Finally comes
min_size
andmax_size
, those are the minimum and maximum size the widget can take if the window is resized
Finally, a shape object is a container for all the information about the size, border, shape, padding.
If we want to change the size of a widget at runtime, we need to set widget.shape.size
, widget.shape.height
or widget.shape.width
.
Therefore a shape can be set only to ONE widget. Widgets can not share references to the same shape.
The padding is the space in pixels between the content of the widget and its border. It does not adapt on screen resize. The Padding
class is defined in shapes.py
and accepts four constructors.
Padding(padding) -> Padding: Every side has the same size
Padding(horizontal, vertical) -> Padding: left and right have the same size and top and bottom too
Padding(left, top, right, bottom) -> Padding: The four sides have different sizes
Shape
objects also accept integers for the padding. In the case an integer is given, it is the padding for all sides. The default padding is 2 pixels.
There's gonna be some heavy work there, so I wont comment on the current system.
Shadows put a highlight on our widgets, by creating a deeper contrast between the widget and the background.
Shadows are fond in graphalama.shadows
and they are optional: you can pass NoShadow
to any widget and...
it won't show nor calculate any shadow. Otherwise, a shadow accepts 4 optional arguments. Here's the signature
Shadow(dx=2, dy=2, blur=2, strength=100)
The two firsts are the offset between the widget and the shadow,
they shift the shadow by dx
and dy
to the right and down respectively.
Optionally, you can blur the shadow, for a smoother result. If blur is 0 the shadow will have the same shape as the widget with the same sharp borders. If blur is positive, then a Gaussian blur is applied to the shape. Note that this requires pillow. If you don't have pillow installed, it won't blur the shadow, without raising any exception.
The strength is an integer between 0
and 255
, it's how dark the shadow is.
Here's an example from shapes_example, where the widgets are
wid = WidgetList([
Button("Random Shadow", change_shadow, (400, 250), RoundedRect((400, 250), 50, 1, 2),
bg_color=(150, 232, 230), border_color=NICE_BLUE, shadow=NoShadow(), anchor=ALLANCHOR),
Widget((55, 35), RoundedRect(), bg_color=Monokai.PINK, anchor=TOP),
Widget((150, 35), RoundedRect(), bg_color=Monokai.BLUE, shadow=Shadow(5, 5, 0), anchor=TOP),
Widget((245, 35), RoundedRect(), bg_color=Monokai.ORANGE, shadow=Shadow(-5, 5, 10, 200), anchor=TOP),
Widget((340, 35), RoundedRect(), bg_color=Monokai.GREEN, shadow=Shadow(5, 5, 5), anchor=TOP),
Widget((435, 35), RoundedRect(), bg_color=Monokai.YELLOW, shadow=Shadow(0, 0, 10), anchor=TOP),
Widget((530, 35), RoundedRect(), bg_color=Monokai.PURPLE, shadow=Shadow(5, 5, 0, 200), anchor=TOP),
Widget((625, 35), RoundedRect(), bg_color=Monokai.BROWN, shadow=Shadow(-2, -2, 5), anchor=TOP),
Widget((720, 35), RoundedRect(), bg_color=Monokai.BLACK, shadow=Shadow(5, 20, 5), anchor=TOP),
SimpleText("Shadow(dx, dy, blur, strength)", (400, 490), anchor=BOTTOM)
])
There's gonna be some heavy work there, so I wont comment on the current system.
You will often find yourself with a list of widget that you want to render and update.
WidgetList
is a thin wraper around the builtin list
class with the methods
render(screen)
, update(event)
and resize(new_size, old_size)
implemented, so it acts like a widget.
WidgetList
can be contained in other WidgetList
.
A special thank to risson for his help and support. Also, I don't maintain the library actively, but if you open issues I will look into them because I like to see people use what I made. If you open feature requests, I will not implement them except maybe during a lonely rainy Saturday...