Python Forum
[PyGame] Text stacking on top of one another
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Text stacking on top of one another
#1
I've been playing around a bit with pygame and image buttons. When creating a button with the button class, I can't seem to get button text correct. It stacks on top of one another in all buttons. Each time I call the class should it not be a new instance of that class? Therefor should not the text be unique to that class. What am I missing here? Thanks for any incite.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import pygame
 
pygame.init()
pygame.font.init()
 
# Setup pygame screen
screen_size = (800, 600)
screen = pygame.display.set_mode(screen_size)
pygame.display.set_caption('Pygame Button')
 
# Setup some colors
screen_bg = 'ivory2'
 
# Set framerate
fps = 60
framerate = pygame.time.Clock()
 
# Load and define button images. Three states normal, hover, and pressed
normal = pygame.image.load('normal.png')
hover = pygame.image.load('hover.png')
pressed = pygame.image.load('pressed.png')
 
# change cursor on hover
hand = pygame.SYSTEM_CURSOR_HAND
 
# Create Button class
class Button:
    def __init__(self, image, pos, callback, text='Default Text'):
        '''
            Create a animated button from images
            self.callback is for a funtion for the button to do - set to None
        '''
        self.image = image
        self.rect = self.image.get_rect(topleft=pos)
        self.text = text
        self.callback = callback
        self.default()
 
    def default(self):
        font = pygame.font.SysFont('verdana', 16)
        self.text_surf = font.render(self.text, True, 'cyan')
        self.text_rect = self.text_surf.get_rect()
        # self.text_rect.center = self.rect.center
        self.text_rect.center = (65,20)
        self.image.blit(self.text_surf, self.text_rect.center)
 
 
btns = []
col = 100
for i in range(3):
    btns.append(Button(normal, (col, 200), None, text=f'Button {i}'))
    col += 200
 
# Set a variabe to True and enter loop
running = True
while running:
 
    # Fill screen with background color
    screen.fill(screen_bg)
 
    # Start event loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
 
    # Get mouse button from pygame
    left, middle, right = pygame.mouse.get_pressed()
 
    # Blit button to screen
    for btn in btns:
        screen.blit(btn.image, btn.rect)
 
    # Set framerate and update
    framerate.tick(fps)
    pygame.display.update()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags
Download my project scripts


Reply
#2
Line 44 you are setting all instances of that class text to a specified location regardless of where that instance actually is located.
Recommended Tutorials:
Reply
#3
You are blitting the text on the image. This changes the image. All your buttons share the same image.

You could make a new image for each button, or you could have your button draw itself, first blitting the image then blitting the text over the image.

In this example the button blits the text on the button surface. To allow changing the text the button must keep an unmodified copy of the image so it can create a new surface that has both image and text.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import pygame
  
pygame.init()
pygame.font.init()
  
screen_size = (800, 600)
screen = pygame.display.set_mode(screen_size)
normal = pygame.image.load("ttt_x.png")
   
class Button:
    instances = []
    default_font = pygame.font.SysFont('verdana', 16)
 
    def __init__(self, window, image, fg="black", **kwargs):
        super().__init__(**kwargs)
        self.window = window
        self.image = image
        self.font = self.default_font
        self.fg = fg
        self.text = ""
        self.instances.append(self)
        self.callback = None
 
    @classmethod
    def button_press(cls, point):
        for button in cls.instances:
            if button.click(point):
                return True
        return False
 
    @classmethod
    def draw_all(cls):
        for button in cls.instances:
            button.draw()
 
    def connect(self, func):
        self.callback = func
 
    def click(self, point):
        if self.callback and self.rect.collidepoint(point):
            self.callback()
            return True
        return False
 
    def draw(self):
        self.window.blit(self.surface, self.pos)
 
    @property
    def pos(self):
        return self.rect.x, self.rect.y
 
    @pos.setter
    def pos(self, xy):
        self.rect.x, self.rect.y = xy
 
    @property
    def text(self):
        return self._text
 
    @text.setter
    def text(self, text):
        self._text = text
        text = self.font.render(self._text, True, self.fg)
        text_rect = text.get_rect()
        x = (self.rect.width - text_rect.width) // 2
        y = (self.rect.height - text_rect.height) // 2
        self.surface = self.image.copy()
        self.surface.blit(text, (x, y))
        self.rect = self.surface.get_rect()
 
class CounterButton(Button):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.count = 0
        self.increment()
        self.connect(self.increment)
 
    def increment(self):
        self.count += 1
        self.text = f"Button {self.count}"
 
 
btns = []
for i in range(3):
    button = CounterButton(screen, normal)
    button.pos = 100+200*i, 200
  
# Set a variabe to True and enter loop
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
 
        elif event.type == pygame.MOUSEBUTTONUP:
            Button.button_press(pygame.mouse.get_pos())
 
    screen.fill("black")
    Button.draw_all()
    pygame.display.update()
In this example the text is drawn on top of the image. Setting the button text does not change the image. We no longer have to keep a "clean" image because the text is blitted on the window, not the image. The image is never modified.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import pygame
  
pygame.init()
pygame.font.init()
  
screen_size = (800, 600)
screen = pygame.display.set_mode(screen_size)
normal = pygame.image.load("ttt_x.png")
   
class Button:
    instances = []
    default_font = pygame.font.SysFont('verdana', 16)
 
    def __init__(self, window, image, fg="black", **kwargs):
        super().__init__(**kwargs)
        self.window = window
        self.image = image
        self.rect = self.image.get_rect()
        self.font = self.default_font
        self.fg = fg
        self.text = ""
        self.instances.append(self)
        self.callback = None
 
    @classmethod
    def button_press(cls, point):
        for button in cls.instances:
            if button.click(point):
                return True
        return False
 
    @classmethod
    def draw_all(cls):
        for button in cls.instances:
            button.draw()
 
    def connect(self, func):
        self.callback = func
 
    def click(self, point):
        if self.callback and self.rect.collidepoint(point):
            self.callback()
            return True
        return False
 
    def draw(self):
        self.window.blit(self.image, self.pos)
        self.window.blit(self.text_surface, self.text_rect)
 
    @property
    def pos(self):
        return self.rect.x, self.rect.y
 
    @pos.setter
    def pos(self, xy):
        self.rect.x, self.rect.y = xy
        if self.text_rect:
            self.text_rect.center = self.rect.center
 
    @property
    def text(self):
        return self._text
 
    @text.setter
    def text(self, text):
        self._text = text
        self.text_surface = self.font.render(self._text, True, self.fg)
        self.text_rect = self.text_surface.get_rect()
        self.text_rect.center = self.rect.center
        self.draw()
 
 
class CounterButton(Button):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.count = 0
        self.increment()
        self.connect(self.increment)
 
    def increment(self):
        self.count += 1
        self.text = f"Button {self.count}"
 
 
btns = []
for i in range(3):
    button = CounterButton(screen, normal)
    button.pos = 100+200*i, 200
  
# Set a variabe to True and enter loop
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
 
        elif event.type == pygame.MOUSEBUTTONUP:
            Button.button_press(pygame.mouse.get_pos())
 
    screen.fill("black")
    Button.draw_all()
    pygame.display.update()
Reply
#4
I always try to avoid global. I keep refactoring my code to the smallest part. I always go for flexibility.
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import pygame
from pygame.sprite import Group, Sprite
from itertools import count
 
class ButtonGroup:
    def __init__(self):
        self.buttons = Group()
        self.text = Group()
 
    def add(self, *buttons):
        for button in buttons:
            self.buttons.add(button)
            self.text.add(button.text)
  
    def draw(self, surface):
        self.buttons.draw(surface)
        self.text.draw(surface)
 
    def on_event(self, event):
        if event.type == pygame.MOUSEMOTION:
            for button in self.buttons:
                button.collide(event.pos)
        elif event.type == pygame.MOUSEBUTTONDOWN:
            for button in self.buttons:
                if event.button == 1:
                    button.mouse_down()
        elif event.type == pygame.MOUSEBUTTONUP:
            for button in self.buttons:
                if event.button == 1:
                    button.mouse_up()
 
class Pen:
    def __init__(self, font, color):
        self.font = font
        self.color = color
 
    def write(self, text):
        return Text(pen, text)
 
    def render(self, text):
        return self.font.render(text, 1, self.color)
 
class Text(Sprite):
    def __init__(self, pen, text):
        super().__init__()
        self.pen = pen
        self.text = text
        self.image = pen.render(text)
        self.rect = self.image.get_rect()
 
class ButtonImages:
    def __init__(self, normal, press, hover):
        self.normal = normal
        self.press = press
        self.hover = hover
 
class Callback:
    def __init__(self, callback, user_data=None):
        self.callback = callback
        self.user_data = user_data
 
    def call(self, widget):
        self.callback(widget, self)
 
class Button(Sprite):
    def __init__(self, images, text, callback, position, anchor):
        super().__init__()
        self.callback = callback
        self.images = images
        self.image = images.normal
        self.rect = self.image.get_rect()
        setattr(self.rect, anchor, position)
        self.text = text
        self.text.rect.center = self.rect.center
        self.is_hovering = False
        self.is_press = False
 
    def collide(self, mpos):
        hovering = self.rect.collidepoint(mpos)
        if hovering is not self.is_hovering:
            self.update_image(hovering)
            self.is_hovering = hovering
 
    def mouse_up(self):
        if self.is_hovering:
            self.is_press = False
            self.update_image(self.is_hovering)
            self.callback.call(self)
  
    def mouse_down(self):
        if self.is_hovering:
            self.is_press = True
            self.update_image(self.is_hovering)
        else:
            self.is_press = False
 
    def update_image(self, hovering):
        if hovering:
            if self.is_press:
                self.image = self.images.press
            else:
                self.image = self.images.hover
        else:
            self.image = self.images.normal
 
class QuickWindow:
    def __init__(self, caption, size, fps=60, flags=0):
        # Basic Pygame Setup
        pygame.display.set_caption(caption)
        self.surface = pygame.display.set_mode(size, flags)
        self.rect = self.surface.get_rect()
        self.clock = pygame.time.Clock()
        self.running = False
        self.delta = 0
        self.fps = fps
 
        # Variables
        self.create_buttons()
 
    def create_buttons(self):
        # Blending colors
        hcolor = pygame.Color("navy").lerp("white", 0.15)
        pcolor =  pygame.Color("navy").lerp("white", 0.2)
        # Build surface images
        surfaces = []
        for color in ["navy", pcolor, hcolor]:
            surface = pygame.Surface((120, 30))
            surface.fill(color)
            surfaces.append(surface)
 
        images = ButtonImages(*surfaces)
        # Build pen
        font = pygame.font.Font(None, 24)
        pen = Pen(font, "white")
        numb = count(20, 40)
        self.buttons = ButtonGroup()
        for n in range(1, 6):
            self.buttons.add(
                Button(images,
                    Text(pen, "Button " + str(n)),
                    Callback(button_click),
                    (20, next(numb)), "topleft")
            )
 
    def on_draw(self):
        self.surface.fill("gray30")
        self.buttons.draw(self.surface)
 
    def on_event(self, event):
        if event.type == pygame.QUIT:
            self.running = False
        else:
            self.buttons.on_event(event)
 
    def main_loop(self):
        self.running = True
        while self.running:
            for event in pygame.event.get():
                self.on_event(event)
 
            self.on_draw()
            pygame.display.flip()
            self.delta = self.clock.tick(self.fps)
 
def button_click(button, callback):
    print(button.text.text)
 
if __name__ == "__main__":
    pygame.init()
    window = QuickWindow("Pygame Window", (400, 300))
    window.main_loop()
    pygame.quit()
menator01 likes this post
99 percent of computer problems exists between chair and keyboard.
Reply
#5
Thanks for the examples all. @Windspar I got the images and mouse cursor change to work with your example.
Now to break down the code and study it.

Thanks again all.
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags
Download my project scripts


Reply
#6
Anchor in my code. Is using pygame rect position. "topleft", "midleft", "center", so on.
Here a more functional example.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import pygame
from pygame.sprite import Group, Sprite
from itertools import count
 
class ButtonGroup:
    def __init__(self):
        self.buttons = Group()
        self.text = Group()
 
    def add(self, *buttons):
        for button in buttons:
            self.buttons.add(button)
            self.text.add(button.text)
 
    def draw(self, surface):
        self.buttons.draw(surface)
        self.text.draw(surface)
 
    def on_event(self, event):
        if event.type == pygame.MOUSEMOTION:
            for button in self.buttons:
                button.collide(event.pos)
        elif event.type == pygame.MOUSEBUTTONDOWN:
            for button in self.buttons:
                if event.button == 1:
                    button.mouse_down()
        elif event.type == pygame.MOUSEBUTTONUP:
            for button in self.buttons:
                if event.button == 1:
                    button.mouse_up()
 
class Pen:
    def __init__(self, font, color):
        self.font = font
        self.color = color
 
    def write(self, text):
        return Text(pen, text)
 
    def render(self, text):
        return self.font.render(text, 1, self.color)
 
class Text(Sprite):
    def __init__(self, pen, text):
        super().__init__()
        self.pen = pen
        self.text = text
        self.image = pen.render(text)
        self.rect = self.image.get_rect()
 
    def set_text(self, text, anchor):
        pos = getattr(self.rect, anchor)
        self.text = text
        self.image = self.pen.render(text)
        self.rect = self.image.get_rect()
        setattr(self.rect, anchor, pos)
 
class ButtonImages:
    def __init__(self, normal, press, hover):
        self.normal = normal
        self.press = press
        self.hover = hover
 
class Callback:
    def __init__(self, callback, user_data=None):
        self.callback = callback
        self.user_data = user_data
 
    def call(self, widget):
        self.callback(widget, self)
 
class Button(Sprite):
    def __init__(self, images, text, callback, position, anchor):
        super().__init__()
        self.callback = callback
        self.images = images
        self.image = images.normal
        self.rect = self.image.get_rect()
        setattr(self.rect, anchor, position)
        self.text = text
        self.text.rect.center = self.rect.center
        self.is_hovering = False
        self.is_press = False
 
    def collide(self, mpos):
        hovering = self.rect.collidepoint(mpos)
        if hovering is not self.is_hovering:
            self.update_image(hovering)
            self.is_hovering = hovering
 
    def mouse_up(self):
        if self.is_hovering:
            self.is_press = False
            self.update_image(self.is_hovering)
            self.callback.call(self)
 
    def mouse_down(self):
        if self.is_hovering:
            self.is_press = True
            self.update_image(self.is_hovering)
        else:
            self.is_press = False
 
    def update_image(self, hovering):
        if hovering:
            if self.is_press:
                self.image = self.images.press
            else:
                self.image = self.images.hover
        else:
            self.image = self.images.normal
 
class ButtonManager:
    def __init__(self, window, color, mix_color, size):
        self.window = window
        self.color_buttons = ButtonGroup()
        self.create(color, mix_color, size)
 
        # Shortcuts.
        # Link button on_event and draw methods from ButtonGroup.
        self.draw = self.color_buttons.draw
        self.on_event = self.color_buttons.on_event
 
    def create(self, color, mix_color, size):
        # Build Pen
        font = pygame.font.Font(None, 24)
        pen = Pen(font, "white")
 
        # Creating Images
        hcolor = pygame.Color(color).lerp(mix_color, 0.15)
        pcolor =  pygame.Color(color).lerp(mix_color, 0.2)
        surfaces = []
        for color in [color, pcolor, hcolor]:
            surface = pygame.Surface(size)
            surface.fill(color)
            surfaces.append(surface)
 
        images = ButtonImages(*surfaces)
 
        # Data for buttons
        colors = ("blue", "darkred", "darkgreen", "dodgerblue", "orange", "gray30")
        num = count(20, 40)
 
        # Create and add Button to group.
        for color in colors:
            y = next(num)
            self.color_buttons.add(
                Button(images,
                    Text(pen, color),
                    Callback(self.push_color_button, color),
                    (20, y), "topleft")
            )
 
    def push_color_button(self, button, callback):
        self.window.background = callback.user_data
 
class QuickWindow:
    def __init__(self, caption, size, fps=60, flags=0):
        # Basic Pygame Setup
        pygame.display.set_caption(caption)
        self.surface = pygame.display.set_mode(size, flags)
        self.rect = self.surface.get_rect()
        self.clock = pygame.time.Clock()
        self.running = False
        self.delta = 0
        self.fps = fps
 
        # Variables
        self.background = 'gray30'
        self.buttons = ButtonManager(self, "navy", "white", (140, 30))
 
    def on_draw(self):
        self.surface.fill(self.background)
        self.buttons.draw(self.surface)
 
    def on_event(self, event):
        if event.type == pygame.QUIT:
            self.running = False
        else:
            self.buttons.on_event(event)
 
    def main_loop(self):
        self.running = True
        while self.running:
            for event in pygame.event.get():
                self.on_event(event)
 
            self.on_draw()
            pygame.display.flip()
            self.delta = self.clock.tick(self.fps)
 
if __name__ == "__main__":
    pygame.init()
    window = QuickWindow("Pygame Window", (400, 300))
    window.main_loop()
    pygame.quit()
99 percent of computer problems exists between chair and keyboard.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020