Creating Buttons in PyGame - button

I am making a program for an automated timetable and I have managed to create a title but I would like to add a log in and sign up button that goes onto a new page. I would also like to add a return button on these new pages. I was using programmingpixels.com to help but I still cannot do what I would like to do. I am new into using PyGame so I may not have done an efficient code as I could have done and there may be quite a few errors. My title screen was previously working but when I tried to add these buttons it is blank and won't let me exit my screen. Any help would be great. Thank You.
import pygame
import pygame.freetype
from pygame.sprite import Sprite
from pygame.rect import Rect
from enum import Enum
PINK = (250, 100, 100)
WHITE = (255, 255, 255)
BLACK = (0,0,0)
def create_surface_with_text(text, font_size, text_rgb, bg_rgb):
font = pygame.freetype.SysFont("Arial", font_size, bold=True)
surface, _ = font.render(text=text, fgcolor=text_rgb, bgcolor=bg_rgb)
return surface.convert_alpha()
class UIElement(Sprite):
def __init__(self, center_position, text, font_size, bg_rgb, text_rgb, action=None):
self.mouse_over = False
# what happens when the mouse is not over the element
default_image = create_surface_with_text(
text=text, font_size=font_size, text_rgb=text_rgb, bg_rgb=bg_rgb
)
# what happens when the mouse is over the element
highlighted_image = create_surface_with_text(
text=text, font_size=font_size * 1.1, text_rgb=text_rgb, bg_rgb=bg_rgb
)
self.images = [default_image, highlighted_image]
self.rects = [
default_image.get_rect(center=center_position),
highlighted_image.get_rect(center=center_position),
]
self.action = action
super().__init__()
#property
def image(self):
return self.images[1] if self.mouse_over else self.images[0]
#property
def rect(self):
return self.rects[1] if self.mouse_over else self.rects[0]
def update(self, mouse_pos, mouse_up):
if self.rect.collidepoint(mouse_pos):
self.mouse_over = True
else:
self.mouse_over = False
def draw(self, surface):
surface.blit(self.image, self.rect)
def main():
pygame.init()
screen = pygame.display.set_mode((800, 600))
game_state = GameState.LOGIN
while True:
if game_state == GameState.LOGIN:
game_state = log_in(screen)
if game_state == GameState.SIGNUP:
game_state = sign_up(screen)
if game_state == GameState.RETURN:
game_state = title_screen(screen)
if game_state == GameState.QUIT:
pygame.quit()
return
def title_screen(screen):
login_btn = UIElement(
center_position=(400,300),
font_size=30,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Log In",
action=GameState.LOGIN,
)
signup_btn = UIElement(
center_position=(400,200),
font_size=30,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Log In",
action=GameState.LOGIN,
)
uielement = UIElement(
center_position=(400, 100),
font_size=40,
bg_rgb=PINK,
text_rgb=BLACK,
text="Welcome to the Automated Timetable Program",
action=GameState.QUIT,
)
buttons = [login_btn, signup_btn]
while True:
mouse_up = False
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
mouse_up = True
elif event.type == pygame.QUIT:
pygame.quit()
sys.exitIO
screen.fill(PINK)
for button in buttons:
ui_action = button.update(pygame.mouse.get_pos(),mouse_up)
if ui_action is not None:
return ui_action
button.draw(screen)
pygame.display.flip()
def log_in(screen):
return_btn = UIElement(
center_position=(140, 570),
font_size=20,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Return to main menu",
action=GameState.TITLE,
)
while True:
mouse_up = False
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
mouse_up = True
screen.fill(PINK)
ui_action = return_btn.update(pygame.mouse.get_pos(),mouse_up)
if ui_action is not None:
return ui_action
return_btn.draw(screen)
pygame.display.flip()
class GameState(Enum):
LOGIN = -1
SIGNUP = 0
RETURN = 1
QUIT = 2
if __name__ == "__main__":
main()

For starters GameState is missing TITLE value.
class GameState(Enum):
# ...
TITLE = 3
Adding this makes the code run.
The log_in() function does not handle the window being closed. You must handle the pygame.QUIT event, in every event loop. For example:
def log_in( screen ):
# ...
while True:
mouse_up = False
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
pygame.event.post( pygame.event.Event( pygame.QUIT ) ) # re-send the quit event to the next loop
return GameState.QUIT
elif ( event.type == pygame.MOUSEBUTTONUP and event.button == 1 ):
mouse_up = True # Mouse button 1 weas released
ui_action = return_btn.update( pygame.mouse.get_pos(), mouse_up )
if ui_action is not None:
print( "log_in() - returning action" )
return ui_action
screen.fill(PINK)
return_btn.draw(screen)
pygame.display.flip()
The UIElement.update() looks like it should return the self.action when the mouse button is released over the control. However, in the existing code, nothing is ever returned. Probably it needs to be something like this:
class UIElement( Sprite ):
# ...
def update(self, mouse_pos, mouse_up):
""" Track the mouse, setting the self.mouse_over. Also check
if the mouse-button was clicked while over this control
returning the pre-defined self.action, if so. """
result = None # No click => no action
if self.rect.collidepoint(mouse_pos):
self.mouse_over = True
if ( mouse_up ):
result = self.action # Mouse was clicked on element, add action
else:
self.mouse_over = False
return result
After these changes, your script runs OK before dropping into an outer loop when the button is clicked. The outer loop also does not handle exiting properly, but probably it's just the same set of changes over again.
It would be better to only have one user-input processing loop. having these separate event-loops is causing the same problem in multiple locations. Work out a way to have a single event processing function, then adapt your UI code to use it. This will make your code easier to write and debug going forward.
Ref: All Code
import pygame
import pygame.freetype
from pygame.sprite import Sprite
from pygame.rect import Rect
from enum import Enum
PINK = (250, 100, 100)
WHITE = (255, 255, 255)
BLACK = (0,0,0)
def create_surface_with_text(text, font_size, text_rgb, bg_rgb):
font = pygame.freetype.SysFont("Arial", font_size, bold=True)
surface, _ = font.render(text=text, fgcolor=text_rgb, bgcolor=bg_rgb)
return surface.convert_alpha()
class UIElement(Sprite):
def __init__(self, center_position, text, font_size, bg_rgb, text_rgb, action=None):
self.mouse_over = False
# what happens when the mouse is not over the element
default_image = create_surface_with_text(
text=text, font_size=font_size, text_rgb=text_rgb, bg_rgb=bg_rgb
)
# what happens when the mouse is over the element
highlighted_image = create_surface_with_text(
text=text, font_size=font_size * 1.1, text_rgb=text_rgb, bg_rgb=bg_rgb
)
self.images = [default_image, highlighted_image]
self.rects = [
default_image.get_rect(center=center_position),
highlighted_image.get_rect(center=center_position),
]
self.action = action
super().__init__()
#property
def image(self):
return self.images[1] if self.mouse_over else self.images[0]
#property
def rect(self):
return self.rects[1] if self.mouse_over else self.rects[0]
def update(self, mouse_pos, mouse_up):
""" Track the mouse, setting the self.mouse_over. Also check
if the mouse-button was clicked while over this control
returning the pre-defined self.action, if so. """
result = None # No click => no action
if self.rect.collidepoint(mouse_pos):
self.mouse_over = True
if ( mouse_up ):
result = self.action # Mouse was clicked on element, add action
else:
self.mouse_over = False
return result
def draw(self, surface):
surface.blit(self.image, self.rect)
def main():
pygame.init()
screen = pygame.display.set_mode((800, 600))
game_state = GameState.LOGIN
while True:
if game_state == GameState.LOGIN:
game_state = log_in(screen)
if game_state == GameState.SIGNUP:
game_state = sign_up(screen)
if game_state == GameState.RETURN:
game_state = title_screen(screen)
if game_state == GameState.QUIT:
pygame.quit()
return
def title_screen(screen):
login_btn = UIElement(
center_position=(400,300),
font_size=30,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Log In",
action=GameState.LOGIN,
)
signup_btn = UIElement(
center_position=(400,200),
font_size=30,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Log In",
action=GameState.LOGIN,
)
uielement = UIElement(
center_position=(400, 100),
font_size=40,
bg_rgb=PINK,
text_rgb=BLACK,
text="Welcome to the Automated Timetable Program",
action=GameState.QUIT,
)
buttons = [login_btn, signup_btn]
while True:
mouse_up = False
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP and event.button == 1:
mouse_up = True
elif event.type == pygame.QUIT:
pygame.quit()
sys.exitIO
screen.fill(PINK)
for button in buttons:
ui_action = button.update(pygame.mouse.get_pos(),mouse_up)
if ui_action is not None:
return ui_action
button.draw(screen)
pygame.display.flip()
def log_in(screen):
return_btn = UIElement(
center_position=(140, 570),
font_size=20,
bg_rgb=WHITE,
text_rgb=BLACK,
text="Return to main menu",
action=GameState.TITLE,
)
while True:
mouse_up = False
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
pygame.event.post( pygame.event.Event( pygame.QUIT ) ) # re-send the quit event to the next loop
return GameState.QUIT
elif ( event.type == pygame.MOUSEBUTTONUP and event.button == 1 ):
mouse_up = True # Mouse button 1 weas released
ui_action = return_btn.update( pygame.mouse.get_pos(), mouse_up )
if ui_action is not None:
print( "log_in() - returning action" )
return ui_action
screen.fill(PINK)
return_btn.draw(screen)
pygame.display.flip()
class GameState(Enum):
LOGIN = -1
SIGNUP = 0
RETURN = 1
QUIT = 2
TITLE=3
if __name__ == "__main__":
main()

Related

The buttons in my pygame menu only work on hold and not on push [duplicate]

This question already has answers here:
Pygame mouse clicking detection
(4 answers)
Closed 5 days ago.
i am working through a pygame code and am in the process of creating a second menu sequentially, however unlike the first menu the second one differs in that it only works when holding down on the position of the button.
i am wondering if anyone can help differentiate what makes the two work and if there is a fix for it.
import random
import time
import pygame
pygame.init()
WIDTH = 500
HEIGHT = 500
screen = pygame.display.set_mode([WIDTH, HEIGHT])
fps = 60
timer = pygame.time.Clock()
main_menu = False
font = pygame.font.Font('freesansbold.ttf', 24)
menu_command = 0
p1score = 0
p2score = 0
class Button:
def __init__(self, txt, pos):
self.text = txt
self.pos = pos
self.button = pygame.rect.Rect((self.pos[0], self.pos[1]), (200, 40))
def draw(self):
pygame.draw.rect(screen, 'light gray', self.button, 0, 5)
pygame.draw.rect(screen, 'dark gray', self.button, 5, 5)
text = font.render(self.text, True, 'black')
screen.blit(text, (self.pos[0] + 15, self.pos[1] + 7))
def check_clicked(self):
if self.button.collidepoint(pygame.mouse.get_pos()) and pygame.mouse.get_pressed()[0]:
return True
else:
return False
def draw_game():
button = Button('Main Menu', (120, 450))
button.draw()
menu = button.check_clicked()
return menu
def draw_menu():
command = -1
pygame.draw.rect(screen, 'black', [100, 100, 300, 300])
#exit menu button
menu_button = Button('Exit Menu', (120, 350))
btn1 = Button('Local Match', (120, 180))
btn2 = Button('Online Match', (120, 240))
btn3 = Button('Settings', (120, 300))
menu_button.draw()
btn1.draw()
btn2.draw()
btn3.draw()
if menu_button.check_clicked():
command = 0
if btn1.check_clicked():
command = 1
if btn2.check_clicked():
command = 2
if btn3.check_clicked():
command = 3
return command
def turndecide():
time.sleep(0.1)
firstturn = -1
p1button = Button('player 1', (120, 120))
p2button = Button('player 2', (120, 180))
ranbutton = Button('Random', (120, 240))
firstturntext = font.render(f'please select who will take the first turn', True, 'black')
screen.blit(firstturntext, (20, 20))
p1button.draw()
p2button.draw()
ranbutton.draw()
if p1button.check_clicked():
firstturn = 1
if p2button.check_clicked():
firstturn = 2
if ranbutton.check_clicked():
firstturn = random.randint(1, 2)
return firstturn
def localgame():
screen.fill('white')
turn = turndecide()
if turn > 0:
screen.fill('red')
outputtext = font.render(f'player {turn} will move first', True, 'black')
screen.blit(outputtext, (20, 20))
run = True
while run:
screen.fill('white')
timer.tick(fps)
if main_menu:
menu_command = draw_menu()
if menu_command != -1:
main_menu = False
else:
main_menu = draw_game()
if menu_command > 0:
if menu_command == 1:
localgame()
if menu_command == 2:
onlinegame()
if menu_command == 3:
settings()
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.display.flip()
pygame.quit()
i tried adding a while loop to ensure the code does not move backward as it does
def localgame():
screen.fill('white')
turn = turndecide()
testvariable = True
while testvariable:
if turn > 0:
screen.fill('red')
outputtext = font.render(f'player {turn} will move first', True, 'black')
screen.blit(outputtext, (20, 20))
but this causes the program to crash.
In your check_clicked function, it will return True only if the user is actively clicking on the button. Whether or not you show the menu is, therefore, also linked to whether or not the user is actively clicking/holding the button.
Instead, each time the button is clicked, toggle the menu option. Only toggle the menu again once the player releases the mouse and presses it again. This avoids the menu being toggled really quickly constantly while holding the button down.
Perhaps, have a can_click variable that is set to False every time a click is registered and reset to True when the mouse is released.

How to swap the position of the 2 buttons by dragging in PyQt5 [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I am trying to make small program in Python,using PyQt5.The program will as have many button has arranged neatly.When dragging one of the buttons to another button, you can swap their positions while the buttons are still arranged neatly.I tried many ways, but in the end the buttons couldn’t be arranged neatly.What should I do to achieve this effect
Sounds like a job for QEventFilter.
Simple version:
from PyQt5.QtCore import QEvent, QObject
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QPushButton
class Filter(QObject):
def __init__(self, parent = None):
super().__init__(parent)
self._pressed = None
self._drag = False
def setDragMode(self, active):
""" When drag mode activated buttons are not clickable (mouse events are filtered) """
self._drag = active
def setLayout(self, layout):
self._layout = layout
for i in range(layout.count()):
item = layout.itemAt(i)
if item.widget():
item.widget().installEventFilter(self)
def eventFilter(self, obj, event):
if not self._drag:
return False
if event.type() == QEvent.MouseButtonPress:
if isinstance(obj, QPushButton):
self._pressed = obj
return True
elif event.type() == QEvent.MouseMove:
button = self._pressed
layout = self._layout
if button is None:
return True
pos = obj.mapToParent(event.pos())
index = layout.indexOf(button)
geometry = button.geometry()
if pos.x() < geometry.topLeft().x():
if index - 1 >= 0:
prev = layout.itemAt(index - 1).widget().geometry()
if prev.topRight().x() > pos.x():
layout.insertWidget(index - 1, button)
elif pos.x() > geometry.topRight().x():
if index + 1 < layout.count():
next_ = layout.itemAt(index + 1).widget().geometry()
if next_.topLeft().x() < pos.x():
layout.insertWidget(index + 1, button)
return True
elif event.type() == QEvent.MouseButtonRelease:
self._pressed = None
return True
return False
if __name__ == "__main__":
app = QApplication([])
widget = QWidget()
layout = QHBoxLayout()
buttons = [QPushButton("button {}".format(i)) for i in range(5)]
for button in buttons:
layout.addWidget(button)
widget.setLayout(layout)
widget.show()
filter_ = Filter()
filter_.setLayout(layout)
filter_.setDragMode(True)
app.exec_()
More intuitive and interactive version:
from PyQt5.QtCore import QEvent, QObject
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QPushButton
class Overlay(QWidget):
def __init__(self, parent = None):
super().__init__(parent)
self._widget = None
self._pressPos = None
self._topLeftPos = None
self._movePos = None
self._pixmap = None
def setDragWidget(self, widget, pressPos, topLeftPos):
self._widget = widget
self._pressPos = pressPos
self._movePos = pressPos
self._topLeftPos = topLeftPos
self._pixmap = widget.grab()
def mouseMoved(self, pos):
self._movePos = pos
self.update()
def mouseReleased(self):
self._pixmap = None
self.update()
def paintEvent(self, event):
if self._pixmap is None:
return
painter = QPainter(self)
painter.drawPixmap(self._topLeftPos + self._movePos - self._pressPos, self._pixmap)
def swap_widgets(layout, index1, index2):
if index1 == index2:
return
widget1 = layout.itemAt(index1).widget()
widget2 = layout.itemAt(index2).widget()
layout.insertWidget(index1, widget2)
layout.insertWidget(index2, widget1)
class Filter(QObject):
def __init__(self, parent = None):
super().__init__(parent)
self._pressed = None
self._drag = False
self._overlay = None
def setDragMode(self, active):
""" When drag mode activated buttons are not clickable (mouse events are filtered) """
self._drag = active
def setLayout(self, layout):
self._layout = layout
for i in range(layout.count()):
item = layout.itemAt(i)
if item.widget():
item.widget().installEventFilter(self)
def eventFilter(self, obj, event):
if not self._drag:
return False
if event.type() == QEvent.MouseButtonPress:
if isinstance(obj, QPushButton):
self._pressed = obj
parent = obj.parent()
overlay = Overlay(parent)
overlay.setGeometry(parent.rect())
pressPos = obj.mapToParent(event.pos())
overlay.setDragWidget(obj, pressPos, obj.geometry().topLeft())
overlay.show()
self._overlay = overlay
return True
elif event.type() == QEvent.MouseMove:
button = self._pressed
layout = self._layout
if button is None:
return True
pos = obj.mapToParent(event.pos())
overlay = self._overlay
if overlay is not None:
overlay.mouseMoved(pos)
return True
elif event.type() == QEvent.MouseButtonRelease:
overlay = self._overlay
button = self._pressed
layout = self._layout
if overlay is not None:
overlay.mouseReleased()
overlay.hide()
pos = obj.mapToParent(event.pos())
index1 = layout.indexOf(button)
for index2 in range(layout.count()):
widget = layout.itemAt(index2).widget()
if widget is None:
continue
if widget.geometry().contains(pos):
swap_widgets(layout, index1, index2)
break
self._pressed = None
return True
return False
if __name__ == "__main__":
app = QApplication([])
widget = QWidget()
layout = QHBoxLayout()
buttons = [QPushButton("button {}".format(i)) for i in range(5)]
for button in buttons:
layout.addWidget(button)
widget.setLayout(layout)
widget.show()
filter_ = Filter()
filter_.setLayout(layout)
filter_.setDragMode(True)
app.exec_()

I want to show a pygame button, which is inherited from pygame.sprite.Sprite, but it is not getting blitted on the display

All
I am building a game with the PYGame library.
I am struggeling with this piece of code, where I want to schow a button. The button is inherited from the pygame.sprite.Sprite class.
I have searched around but I could not find any example wiht a button generated from the pygame.sprite.Sprite class.
#!/usr/bin/env python
import os, sys
import pygame
import numpy
# initialize the pygame module
pygame.init();
if not pygame.font: logging.warning(' Fonts disabled');
class Button(pygame.sprite.Sprite):
def __init__(self, gameplayCode, gameplayLbl, gameplaycolorRGB):
# Call the parent class (Sprite) constructor
super().__init__();
self.gameplayCode = gameplayCode;
self.gameplayLbl = gameplayLbl;
self.gameplaycolorRGB = gameplaycolorRGB;
self.buttondims = self.width, self.height = 190, 60;
self.smallText = pygame.font.SysFont('comicsansms',15);
# calculating a lichter color, needs to be used when hoovering over button
self.color = numpy.array(gameplaycolorRGB);
self.white = numpy.array(pygame.color.THECOLORS['white']);
self.vector = self.white - self.color;
self.gameplaycolorRGBFaded = self.color + self.vector *0.6;
def setCords(self,x,y):
self.textSurf = self.smallText.render(self.gameplayLbl, 1,
pygame.color.THECOLORS['black']);
self.image = pygame.Surface((self.width, self.height));
self.image.fill(self.gameplaycolorRGB);
self.rect = self.image.get_rect();
self.rect.topleft = x,y;
self.rect.center = (x+(x/2),y+(y/2));
def pressed(self,mouse):
if mouse.get_pos()[0] > self.rect.topleft[0]:
if mouse.get_pos()[1] > self.rect.topleft[1]:
if mouse.get_pos()[0] < self.rect.bottomright[0]:
if mouse.get_pos()[1] < self.rect.bottomright[1]:
if mouse.get_pressed()[0] == 1:
return True;
else:
return False;
else:
return False;
else:
return False;
else:
return False;
else:
return False;
def getGamePlayCode(self):
return self.gameplayCode;
def getGamePlayLbl(self):
return self.gameplayLbl;
def getGamePlayColorRGB(self):
return self.gameplaycolorRGB;
def getGamePlayColorRGBFaded(self):
return self.gameplaycolorRGBFaded;
def getButtonWidth(self):
return self.buttondims[0];
def getButtonHeight(self):
return self.buttondims[1];
def getButtonDims(self):
return self.buttondims;
button=Button('CODE','LABEL',pygame.color.THECOLORS['darkturquoise']);
os.environ['SDL_VIDEO_CENTERED'] = '1';
display_size = display_width, display_height = 1300,600;
gameDisplay = pygame.display.set_mode(display_size);
display_xcenter = gameDisplay.get_width()/2;
display_ycenter = gameDisplay.get_height()/2;
# create a background
background = pygame.display.get_surface();
background.fill(pygame.color.THECOLORS['white']);
# put background on the surface
backgroundPos = xcoord, ycoord = 0,0;
gameDisplay.blit(background, backgroundPos);
pygame.display.update();
title='Testing to show a button which is inherited form
pygame.sprite.Sprite. When pressing the button code must react. When
hoovering color must be brighter.';
textSurface = pygame.font.SysFont('comicsansms',15).render(title, True,
pygame.color.THECOLORS['black']);
textRect = textSurface.get_rect();
gameDisplay.blit(textSurface, textRect);
pygame.display.update();
clock = pygame.time.Clock();
FPS = 60;
game_loop = True;
button.setCords(display_xcenter,display_ycenter);
while game_loop:
mouse = pygame.mouse;
for event in pygame.event.get():
if event.type == pygame.QUIT:
print('Quiting');
game_loop = False;
if button.pressed(mouse):
print('Gameplay pressed');
pygame.display.update();
clock.tick(FPS);
# ending the pygame module
pygame.quit();
I want to blit the button, react on the pressed method, when hoovering over the button the color must be brighter.
Any input is highly appreciated.
In the beginning my game didnot had any classes. Now I am rebuilding the game with the use of classes.
Kind Regards
Olivier
-A Python beginner-
Pygame sprites should usually be added to a sprite group (there are several different group types). Then you can update and draw all sprites in your main loop by calling group.update() and group.draw(gameDisplay).
# Create a sprite group and add the button.
buttons = pygame.sprite.Group(button)
# You could also add it afterwards.
# buttons.add(button)
while game_loop:
mouse = pygame.mouse
for event in pygame.event.get():
if event.type == pygame.QUIT:
print('Quiting')
game_loop = False
if button.pressed(mouse):
print('Gameplay pressed')
# This will call the `update` methods of all sprites in
# this group (not necessary here).
buttons.update()
# The display is usually filled each frame (or blit a background image).
gameDisplay.fill(pygame.color.THECOLORS['white'])
# This will draw each sprite `self.image` at the `self.rect` coords.
buttons.draw(gameDisplay)
pygame.display.update()
clock.tick(FPS)

QTableView does not show values in column after setItemDelegateForColumn

I haveve implemented a table model (inherited QAbstractTableModel) and added it on a QTableView.
I wanted to add delegate to use custom edit widgets for my data items (for example, use a QSlider for some double values). Alas, after I've added a delegate, the table view does not shows values in this column anymore.
Can anyone tell me, how to fix it?
Here's the code:
# -*- coding: utf-8 -*-
import os, sys
from enum import IntEnum, unique
from collections import OrderedDict
from PyQt5 import QtCore, QtGui, QtWidgets
#unique
class BGModelCols(IntEnum):
alpha = 0
alpha_for_nans = 1
color_map = 2
is_boolean = 3
x_size_px = 4
y_size_px = 5
is_visible = 6
class ScanRadarSimulator(QtCore.QObject):
backgrounds_supported = ["height", "elevation", "visibility", "closing_angles", "land_rcs"]
#property
def background_names(self):
return [self.tr("Land height"), self.tr("Elevation"), self.tr("Visibility"), self.tr("Closing angles"),\
self.tr("Land RCS")]
def __init__(self, parent=None):
super().__init__(parent)
class BackgroundTableModel(QtCore.QAbstractTableModel):
def __init__(self, radar_simulator, parent=None):
super().__init__(parent)
self._radar_simulator = radar_simulator
self._background_names = self._radar_simulator.background_names
assert isinstance(self._radar_simulator, ScanRadarSimulator)
self.column_names = {BGModelCols.alpha: self.tr("α-channel"),
BGModelCols.alpha_for_nans: self.tr("α-channel for NANs"),
BGModelCols.color_map: self.tr("Color map"),
BGModelCols.is_boolean: self.tr("Is boolean mask"),
BGModelCols.x_size_px: self.tr("X pixels"),
BGModelCols.y_size_px: self.tr("Y pixels"),
BGModelCols.is_visible: self.tr("Is visible")}
self._background_items = OrderedDict()
for bg_id in radar_simulator.backgrounds_supported:
bg_dict = {BGModelCols.alpha: 0.7,
BGModelCols.alpha_for_nans: 0.0,
BGModelCols.color_map: "jet",
BGModelCols.is_boolean: False,
BGModelCols.x_size_px: 4000,
BGModelCols.y_size_px: 4000,
BGModelCols.is_visible: False}
self._background_items[bg_id] = bg_dict
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._radar_simulator.backgrounds_supported)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(BGModelCols)
def flags(self, index=QtCore.QModelIndex()):
if index.isValid():
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole):
if not index.isValid():
return QtCore.QVariant()
row, col = index.row(), index.column()
print("DATA", row, col)
col_item = BGModelCols(col)
if role == QtCore.Qt.DisplayRole:
return str(self._background_items[radar_simulator.backgrounds_supported[row]][col_item])
elif role == QtCore.Qt.EditRole:
return self._background_items[radar_simulator.backgrounds_supported[row]][col_item]
else:
return QtCore.QVariant()
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid():
return False
if role == QtCore.Qt.EditRole:
row, col = index.row(), index.column()
col_item = BGModelCols(col)
self._background_items[radar_simulator.backgrounds_supported[row]][col_item] = value
self.dataChanged.emit(index, index)
return True
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.column_names[BGModelCols(section)]
elif orientation == QtCore.Qt.Vertical:
return self._background_names[section]
else:
return QtCore.QVariant()
class AlphaChannelDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def createEditor(self, parent, option, index):
print("CREATING EDITOR")
slider = QtWidgets.QSlider(parent)
slider.setMinimum(0)
slider.setMaximum(100)
slider.setOrientation(QtCore.Qt.Horizontal)
return slider
def setModelData(self, editor, model, index):
# row, col = index.row(), index.column()
# col_item = BGModelCols(col)
# model._background_items[model._radar_simulator.backgrounds_supported[row]][col_item] = editor.value() / 100.
print("setModelData")
model.setData(index, editor.value() / 100., QtCore.Qt.EditRole)
def setEditorData(self, editor, index):
print("setEditorData")
row, col = index.row(), index.column()
col_item = BGModelCols(col)
val = int(index.model()._background_items[index.model()._radar_simulator.backgrounds_supported[row]][col_item] * 100)
editor.setValue(val)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def paint(self, painter, option, index):
QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.setStyle("fusion")
radar_simulator = ScanRadarSimulator()
table_view = QtWidgets.QTableView()
alpha_delegate = AlphaChannelDelegate(table_view)
table_view.setItemDelegateForColumn(int(BGModelCols.alpha), alpha_delegate)
table_view.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers)
table_view.setModel(BackgroundTableModel(radar_simulator))
table_view.show()
sys.exit(app.exec_())
Well, it will work if comment out the paint method.
^_^

how to override qscrollbar onclick default behaviour

When you click on QScrollBar's "page control area" ('c' on the image), it will scroll one page. What I want is to make it scroll to the full, just like when you choose "Scroll here" context menu item.
That's quite an interesting question. To find out an answer, we need to take a look at QScrollBar source and find out two things:
How to determine which part of the scroll bar has been clicked;
How to trigger "Scroll Here" behavior.
The answer to the first question lies in QScrollBar::mousePressEvent implementation. It turns out that QStyle::hitTestComplexControl does just what we need. What for the second question, just search "Scroll here" and you'll see that QScrollBarPrivate::pixelPosToRangeValue is used to convert event position to slider value. Unfortunately, we don't have access to functions of this private class, so we're forced to reimplement it. Now let's apply gained knowledge and implement new behavior in a subclass:
import sys
from PyQt4 import QtCore, QtGui
class ModifiedScrollBar(QtGui.QScrollBar):
def __init__(self, parent = None):
super(ModifiedScrollBar, self).__init__(parent)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
opt = QtGui.QStyleOptionSlider()
self.initStyleOption(opt)
control = self.style().hitTestComplexControl(QtGui.QStyle.CC_ScrollBar, opt,
event.pos(), self)
if (control == QtGui.QStyle.SC_ScrollBarAddPage or
control == QtGui.QStyle.SC_ScrollBarSubPage):
# scroll here
gr = self.style().subControlRect(QtGui.QStyle.CC_ScrollBar, opt,
QtGui.QStyle.SC_ScrollBarGroove, self)
sr = self.style().subControlRect(QtGui.QStyle.CC_ScrollBar, opt,
QtGui.QStyle.SC_ScrollBarSlider, self)
if self.orientation() == QtCore.Qt.Horizontal:
pos = event.pos().x()
sliderLength = sr.width()
sliderMin = gr.x()
sliderMax = gr.right() - sliderLength + 1
if (self.layoutDirection() == QtCore.Qt.RightToLeft):
opt.upsideDown = not opt.upsideDown
else:
pos = event.pos().y()
sliderLength = sr.height()
sliderMin = gr.y()
sliderMax = gr.bottom() - sliderLength + 1
self.setValue(QtGui.QStyle.sliderValueFromPosition(
self.minimum(), self.maximum(), pos - sliderMin,
sliderMax - sliderMin, opt.upsideDown))
return
return super(ModifiedScrollBar, self).mousePressEvent(event)
def main():
app = QtGui.QApplication(sys.argv)
edit = QtGui.QTextEdit()
#uncomment for testing horizontal scrollbar
#edit.setLineWrapMode(QtGui.QTextEdit.NoWrap)
edit.setPlainText("Lorem ipsum...")
edit.setVerticalScrollBar(ModifiedScrollBar())
edit.setHorizontalScrollBar(ModifiedScrollBar())
edit.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Works for me, python 3.4 pyqt 5.4.
pixelPosToRangeValue is taken from qt source
def wrapEF(ef):
w = QObject()
w.eventFilter = ef
return w
def sbEventFilter(s, e):
q = s
if (e.type() == QEvent.MouseButtonPress and e.button() == Qt.LeftButton
or e.type() == QEvent.MouseButtonDblClick):
#pixelPosToRangeValue(pos)
opt = QStyleOptionSlider()
q.initStyleOption(opt)
gr = q.style().subControlRect(QStyle.CC_ScrollBar, opt,
QStyle.SC_ScrollBarGroove, q)
sr = q.style().subControlRect(QStyle.CC_ScrollBar, opt,
QStyle.SC_ScrollBarSlider, q)
if q.orientation() == Qt.Horizontal:
sliderLength = sr.width()
sliderMin = gr.x()
sliderMax = gr.right() - sliderLength + 1
if q.layoutDirection() == Qt.RightToLeft:
opt.upsideDown = not opt.upsideDown
dt = sr.width()/2
pos = e.pos().x()
else:
sliderLength = sr.height()
sliderMin = gr.y()
sliderMax = gr.bottom() - sliderLength + 1
dt = sr.height()/2
pos = e.pos().y()
r = QStyle.sliderValueFromPosition(q.minimum(), q.maximum(),
pos - sliderMin - dt,
sliderMax - sliderMin, opt.upsideDown)
#pixelPosToRangeValue,
q.setValue(r)
return q.eventFilter(s, e)
self.scrollBarEF = wrapEF(sbEventFilter)
self.hscrollbar.installEventFilter(self.scrollBarEF)
self.vscrollbar.installEventFilter(self.scrollBarEF)

Resources