do all class functions need to be called to be used - python-3.x

I'm new to this so forgive me.
In the follow code on_draw(), on_update(), on_key_press(), and on_key_release() are never called but I know the code works so they do something, but why does it work?
I was under the impression that you had to do something like Mygame.on_update() to use a class function.
class Player(arcade.Sprite):
""" Player class """
def __init__(self, image, scale):
""" Set up the player """
# Call the parent init
super().__init__(image, scale)
# Create a variable to hold our speed. 'angle' is created by the parent
self.speed = 0
def update(self):
# Convert angle in degrees to radians.
angle_rad = math.radians(self.angle)
# Rotate the ship
self.angle += self.change_angle
# Use math to find our change based on our speed and angle
self.center_x += -self.speed * math.sin(angle_rad)
self.center_y += self.speed * math.cos(angle_rad)
class MyGame(arcade.Window):
"""
Main application class.
"""
def __init__(self, width, height, title):
"""
Initializer
"""
# Call the parent class initializer
super().__init__(width, height, title)
# Set the working directory (where we expect to find files) to the same
# directory this .py file is in. You can leave this out of your own
# code, but it is needed to easily run the examples using "python -m"
# as mentioned at the top of this program.
file_path = os.path.dirname(os.path.abspath(__file__))
os.chdir(file_path)
# Variables that will hold sprite lists
self.player_list = None
# Set up the player info
self.player_sprite = None
# Set the background color
arcade.set_background_color(arcade.color.BLACK)
def setup(self):
""" Set up the game and initialize the variables. """
# Sprite lists
self.player_list = arcade.SpriteList()
# Set up the player
self.player_sprite = Player(":resources:images/space_shooter/playerShip1_orange.png",
SPRITE_SCALING)
self.player_sprite.center_x = SCREEN_WIDTH / 2
self.player_sprite.center_y = SCREEN_HEIGHT / 2
self.player_list.append(self.player_sprite)
def on_draw(self):
"""
Render the screen.
"""
# This command has to happen before we start drawing
self.clear()
# Draw all the sprites.
self.player_list.draw()
def on_update(self, delta_time):
""" Movement and game logic """
# Call update on all sprites (The sprites don't do much in this
# example though.)
self.player_list.update()
def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed. """
# Forward/back
if key == arcade.key.UP:
self.player_sprite.speed = MOVEMENT_SPEED
elif key == arcade.key.DOWN:
self.player_sprite.speed = -MOVEMENT_SPEED
# Rotate left/right
elif key == arcade.key.LEFT:
self.player_sprite.change_angle = ANGLE_SPEED
elif key == arcade.key.RIGHT:
self.player_sprite.change_angle = -ANGLE_SPEED
def on_key_release(self, key, modifiers):
"""Called when the user releases a key. """
if key == arcade.key.UP or key == arcade.key.DOWN:
self.player_sprite.speed = 0
elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
self.player_sprite.change_angle = 0
def main():
""" Main function """
window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
window.setup()
arcade.run()
if __name__ == "__main__":
main()

Related

Kivy Infinite Looping ScrollView

I am trying to create a ScrollView in Kivy with widgets that loop infinitely like in this demo I found online:
I have tried to remove widgets outside the ScrollView's bounds and add them back to the top or bottom, depending on their original location. However, this made the whole app flicker and glitch and ultimately did not work. If anyone has a method of achieving this, it would be greatly appreciated!
I created a custom class based on kivy's Stencilview and RelativeLayout.
It's used pretty much like kivy.uix.recycleview and supports looping. It's somewhat unoptimized but works pretty well. No x-axis scrolling (yet). Should be capable of processing an infinite amount of data without any performance loss. (Tested with 10.000.000 data entries => no lag)).
please don't quote me on anything inside on_touch_down, on_touch_move, on_touch_up, or any function called inside it (the only exception is scroll_y) cause I have no idea how they work.
from functools import partial
from kivy.app import App
from kivy.clock import Clock
from kivy.compat import iteritems
from kivy.lang import Builder
from kivy.metrics import sp
from kivy.properties import DictProperty, ListProperty, NumericProperty, BooleanProperty, ObjectProperty
from kivy.uix.label import Label
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.stencilview import StencilView
from kivy.uix.widget import Widget
class LoopEntry(Widget):
data_index = NumericProperty(0)
data = DictProperty(None, allow_none=True)
hidden = BooleanProperty(False)
def is_hidden(self):
"""
:return:
"""
return self.hidden
def hide(self):
"""
:return:
"""
self.opacity = 0
self.hidden = True
def show(self):
"""
:return:
"""
self.opacity = 1
self.hidden = False
def update(self, data):
"""
overwrite this function if values other than attributes are needed
:param data:
:return:
"""
assert isinstance(data, dict)
# assign data
self.data = data
# apply values
for key, value in iteritems(data):
setattr(self, key, value)
class LoopContainer(RelativeLayout, StencilView):
pass
class LoopContainerDebug(RelativeLayout):
pass
class LoopScrollView(RelativeLayout):
"""
Main data source. Contains the data that needs to be exchanged.
Data can be manipulated without a whole redraw. Might cause unwanted behaviour
like blank lines and incorrect orders if not properly updated.
Setting this value causes a complete refresh.
"""
data = ListProperty()
"""
Children height. All children need to be the same height else
unwanted behaviour might occur.
Altering this value causes a complete refresh.
"""
children_height = NumericProperty(44)
"""
Amount of widgets added to the minimum widgets. (Very) Big numbers may cause lag.
Altering this value causes a complete refresh
"""
protection_amount = NumericProperty(4)
"""
viewclass is used to set the class type the widgets should be
future version might support manual adding of widgets
Altering this value causes a complete refresh.
"""
viewclass = ObjectProperty(LoopEntry)
"""
controls looping behaviour.
"""
loop = BooleanProperty(True)
"""
debugging option. shows hidden entries. not possible to switch
while running (yet)
"""
debug = BooleanProperty(False)
"""
scroll timeout. If the mouse has not been moved 'scroll_distance' within this time, dispatch the touch to children
in milliseconds
"""
scroll_timeout = NumericProperty(200)
"""
touch distance. Distance mouse needs to be moved
in pixel
"""
scroll_distance = NumericProperty('20dp')
def __init__(self, **kwargs):
"""
:param kwargs:
"""
# minimum widgets controls min/max amount of widgets on screen. Readonly.
self.__minimum_widgets = 0
# controls overscroll blocking if loop is disabled
self.__overscroll_block_y = "free"
# container
_kwargs = {
"size_hint": (None, None),
"size": (0, 0)
}
self.container = LoopContainer(**_kwargs) if not kwargs.get('debug', False) else LoopContainerDebug(**_kwargs)
# init super values
super(LoopScrollView, self).__init__(**kwargs)
# add container
self.add_widget(self.container)
# create widgets
self.__create_widgets()
# no idea
self._drag_touch = None
def on_pos(self, widget, value) -> None:
"""
:param widget:
:param value:
:return:
"""
def on_size(self, widget, value) -> None:
"""
:param widget:
:param value:
:return:
"""
# set container size
self.container.size = self.size
# recreate widgets
self.__create_widgets()
def on_data(self, widget, value) -> None:
"""
called if new data is set. Forces complete refresh. Use with care.
:param widget: widget event belongs to
:param value: event value
:return: None
"""
self.__create_widgets()
def on_protection_amount(self, widget, value) -> None:
"""
Forces complete refresh. Use with care.
:param widget: widget event belongs to
:param value: event value
:return: None
"""
self.__create_widgets()
def on_viewclass(self, widget, value) -> None:
"""
sets the viewclass used to entries
Forces complete refresh
:param widget: widget
:param value: value
:return: None
"""
self.__create_widgets()
def on_children_height(self, widget, value) -> None:
"""
Changes the children height.
Forces complete refresh.
:param widget: widget
:param value: value
:return: None
"""
self.__create_widgets()
def __create_widgets(self) -> None:
"""
clear all widgets and recreate
:return: None
"""
# remove all widgets
self.container.clear_widgets()
# calculate the minimum amount of required widgets
self.minimum_widgets = round(self.height / self.children_height) + self.protection_amount
# adding entries to the stencil view in reversed order to start with the smallest value (index 0) at top
for entry in range(self.minimum_widgets, 0, -1):
# create widget instance
_tmp_entry = self.viewclass(
size_hint=(1, None),
height=self.children_height,
pos=(0, self.height - self.children_height * entry)
)
# add to container
self.container.add_widget(_tmp_entry)
# refresh all widgets from given index and apply data values
self.__refresh_from_index(0)
def __refresh_from_index(self, index=0) -> None:
"""
refreshes widgets from given index
:param index: index to start with (very top entry)
:return: None
"""
# return if data is empty
if not self.data:
return
# reset widget positions to prevent weird behaviour
self.__reset_widget_positions(brute=True)
# reduce overhead. Slightly.
_data_length = len(self.data)
# loop through children and set values from given index
for child in self.container.children:
# if the current index exceeds the lengths and looping is disabled hide the widget
if index >= _data_length and not self.loop:
# Note : I dislike direct changing of values -_-
if not child.is_hidden():
child.hide() # hide child
child.data_index = index
else:
_normalized_index = index % _data_length
# get the new data value for the widget
_data_value = self.data[_normalized_index]
child.update(_data_value)
child.data_index = _normalized_index
if child.is_hidden():
child.show()
# increase index
index += 1
def __reset_widget_positions(self, brute=False) -> None:
"""
resets widget positions. Does not take into account values or
value positions.
If brute is True positions will be reset forcefully meaning data may mix up.
If brute is False positions will be scrolled meaning values will remain ordered.
:param brute: boolean
:return: None
"""
if brute:
# forcefully reset children
for child in self.container.children:
child.y = self.height - (child.height * (self.get_child_index(child) + 1))
else:
# get top child
_top_child = self.container.children[0]
# loop until child's y value matches the top threshold
while _top_child.y != self.height - _top_child.height:
# scroll by up to ensure proper order
self.scroll_y(1)
def __trigger_overscroll(self, entry: (LoopEntry, None), state):
"""
:param entry:
:param state:
:return:
"""
# trigger overscroll for down
if state == "bottom" and entry is not None:
# reset child to a proper spot
while entry.y != 0:
# scroll in the fastest direction
self.scroll_y(1 if entry.y < 0 else -1)
# set overscroll AFTER scrolling
self.__overscroll_block_y = "bottom"
# for up
elif state == "top" and entry is not None:
# reset child to proper spot
while entry.y != self.height - entry.height:
self.scroll_y(1 if entry.y < self.height - entry.height else 1)
# set overscroll AFTER scrolling
self.__overscroll_block_y = "top"
# reset else
else:
# free scrolling
self.__overscroll_block_y = "free"
def __update_entry(self, entry: LoopEntry, direction) -> None:
"""
:param entry:
:param direction:
:return:
"""
# get data length
_data_length = len(self.data)
# check direction
if direction == "down":
# get new index
_data_index = entry.data_index + self.minimum_widgets
if self.loop:
# normalize data index
_normalized_data_index = _data_index % _data_length
# update entry
entry.update(self.data[_normalized_data_index])
# set data index
entry.data_index = _normalized_data_index
# show entry
if entry.is_hidden():
entry.show()
else:
# if loop is disabled and data index exceeds either direction
if _data_index >= _data_length or _data_index < 0:
# hide children
if not entry.is_hidden():
entry.hide()
else:
# update entry from data index
entry.update(self.data[_data_index])
# show entry
if entry.is_hidden():
entry.show()
# set data index
entry.data_index = _data_index
elif direction == "up":
# get new data index
_data_index = entry.data_index - self.minimum_widgets
# if looping is enabled
if self.loop:
# normalize index
_normalized_data_index = _data_index % _data_length
# update entry
entry.update(self.data[_normalized_data_index])
# set data index
entry.data_index = _normalized_data_index
# show entry
if entry.is_hidden():
entry.show()
else:
if _data_index < 0 or _data_index >= _data_length:
# hide children
if not entry.is_hidden():
entry.hide()
else:
# update entry from data index
entry.update(self.data[_data_index])
# show entry
if entry.is_hidden():
entry.show()
# set data index
entry.data_index = _data_index
else:
# error
raise Exception
def get_child_index(self, child) -> (int, None):
"""
returns the index if the child exists in list else None
:param child: child instance
:return: int,None
"""
return self.container.children.index(child) if child in self.container.children else None
def scroll_y(self, delta_y) -> None:
"""
scroll by given amount
:param delta_y: delta value in pixels
:return: None
"""
# set highest and lowest children (needed for rotation)
_highest, _lowest = self.container.children[0], self.container.children[0]
# round delta y
delta_y = round(delta_y)
# get data length
data_length = len(self.data)
# control var
_free_block = True
# loop through children
for child in self.container.children:
# increase/decrease children y position
child.y += delta_y
# update highest and lowest children
_highest = child if child.y > _highest.y else _highest
_lowest = child if child.y < _lowest.y else _lowest
# check for loop condition
if not self.loop:
# if current child's index exceeds or evens data length and is higher than given
# threshold trigger overscroll event for bottom
if child.data_index >= data_length - 1 and child.y >= 0:
self.__trigger_overscroll(child, 'bottom')
_free_block = False
# if current child's index is smaller or evens 0 and is higher than the given
# threshold trigger overscroll event for top
elif child.data_index <= 0 and child.y <= self.height - child.height:
self.__trigger_overscroll(child, 'top')
_free_block = False
# unblock if block is free to be unblocked
if _free_block:
self.__trigger_overscroll(None, 'reset')
# check if swap is needed
if _highest.y > self.height + _highest.height and delta_y > 0:
# set new y for highest if it exceeds max height
_highest.y = _lowest.y - _highest.height
self.__update_entry(_highest, direction="down")
elif _lowest.y < 0 - _lowest.height - _lowest.height and delta_y < 0:
_lowest.y = _highest.y + _highest.height
self.__update_entry(_lowest, direction="up")
def _get_uid(self, prefix='sv'):
return '{0}.{1}'.format(prefix, self.uid)
def on_touch_down(self, touch):
x, y = touch.pos
if not self.collide_point(x, y):
touch.ud[self._get_uid('svavoid')] = True
return super(LoopScrollView, self).on_touch_down(touch)
if self._drag_touch or ('button' in touch.profile and touch.button.startswith('scroll')):
return super(LoopScrollView, self).on_touch_down(touch)
# no mouse scrolling, so the user is going to drag with this touch.
self._drag_touch = touch
uid = self._get_uid()
touch.grab(self)
touch.ud[uid] = {
'mode': 'unknown',
'dx': 0,
'dy': 0
}
Clock.schedule_once(self._change_touch_mode, self.scroll_timeout / 1000.)
return True
def on_touch_move(self, touch):
if self._get_uid('svavoid') in touch.ud or self._drag_touch is not touch:
return super(LoopScrollView, self).on_touch_move(touch) or self._get_uid() in touch.ud
if touch.grab_current is not self:
return True
uid = self._get_uid()
ud = touch.ud[uid]
mode = ud['mode']
if mode == 'unknown':
ud['dx'] += abs(touch.dx)
ud['dy'] += abs(touch.dy)
if ud['dx'] > sp(self.scroll_distance):
mode = 'drag'
if ud['dy'] > sp(self.scroll_distance):
mode = 'drag'
ud['mode'] = mode
if mode == 'drag':
if (touch.dy > 0 and self.__overscroll_block_y == "bottom" or
touch.dy < 0 and self.__overscroll_block_y == "top"):
pass
else:
self.scroll_y(touch.dy)
return True
def on_touch_up(self, touch):
if self._get_uid('svavoid') in touch.ud:
return super(LoopScrollView, self).on_touch_up(touch)
if self._drag_touch and self in [x() for x in touch.grab_list]:
touch.ungrab(self)
self._drag_touch = None
ud = touch.ud[self._get_uid()]
if ud['mode'] == 'unknown':
super(LoopScrollView, self).on_touch_down(touch)
Clock.schedule_once(partial(self._do_touch_up, touch), .1)
else:
if self._drag_touch is not touch:
super(LoopScrollView, self).on_touch_up(touch)
return self._get_uid() in touch.ud
def _do_touch_up(self, touch, *largs):
super(LoopScrollView, self).on_touch_up(touch)
# don't forget about grab event!
for x in touch.grab_list[:]:
touch.grab_list.remove(x)
x = x()
if not x:
continue
touch.grab_current = x
super(LoopScrollView, self).on_touch_up(touch)
touch.grab_current = None
def _change_touch_mode(self, *largs):
if not self._drag_touch:
return
uid = self._get_uid()
touch = self._drag_touch
ud = touch.ud[uid]
if ud['mode'] != 'unknown':
return
touch.ungrab(self)
self._drag_touch = None
touch.push()
touch.apply_transform_2d(self.parent.to_widget)
super(LoopScrollView, self).on_touch_down(touch)
touch.pop()
return
# ------------------ Showcase ------------------ #
from kivy.uix.button import Button
class LoopLabel(LoopEntry, Label):
pass
class LoopButton(LoopEntry, Button):
pass
__style = ("""
<LoopLabel>:
color : 1,1,1
text: "test"
canvas:
Color:
rgb : 1,1,1
Line:
rectangle: (*self.pos,self.width ,self.height )
<LoopContainer,LoopContainerDebug>:
canvas:
Color:
rgb : 1,0,0
Line:
rectangle: (0+1,0+1,self.width - 1,self.height -1 )
width:5
""")
class InfiniteScrollingScrollView(App):
def build(self):
root = RelativeLayout()
root.bind(on_touch_down=lambda x, y: print("-" * 10))
sv = LoopScrollView(
size_hint=(0.5, 0.5), pos_hint={'center': (0.5, 0.5)}, viewclass=LoopLabel, debug=False
)
sv.data = [{'text': str(x)} for x in range(10000000)]
root.add_widget(sv)
return root
if __name__ == "__main__":
Builder.load_string(__style)
InfiniteScrollingScrollView().run()
i think this helpful but not the Answer
and this will help https://youtu.be/l8Imtec4ReQ in minute 59:30
from kivy.app import App
from kivy.uix.label import Label
from kivy.lang.builder import Builder
kv=Builder.load_string('''
ScrollView:
do_scroll_x:False
scroll_type:['bars', 'content']
bar_width:'20dp'
GridLayout:
id:g
cols:1
size_hint:(1,None)
height:self.minimum_height
''')
class a(App):
def build(self):
return kv
def on_start(self):
for i in range(100):self.root.ids.g.add_widget(Label(text=str(i),size_hint=(1, None),size=(1, 20)))
a().run()

How To drag an image object in WxPython GUI?

In below code, I have image shaped transparent window and a image inside of it, I would like to move the image(screw photo)by mouse. I wrote a bind function for that screw image but it does not move? what might be the problem?
As it can be seen I added images and bind functions. Is there a missing logic?
import wx
from wx import *
import wx.lib.statbmp as sb
from io import StringIO
# Create a .png image with something drawn on a white background
# and put the path to it here.
IMAGE_PATH = './wood.png'
class ShapedFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None, -1, "Shaped Window",
style = wx.FRAME_SHAPED | wx.SIMPLE_BORDER | wx.STAY_ON_TOP)
self.hasShape = False
self.delta = wx.Point(0,0)
# Load the image
image = wx.Image(IMAGE_PATH, wx.BITMAP_TYPE_PNG)
image.SetMaskColour(255,255,255)
image.SetMask(True)
self.bmp = wx.Bitmap(image)
self.SetClientSize((self.bmp.GetWidth(), self.bmp.GetHeight()))
dc = wx.ClientDC(self)
dc.DrawBitmap(self.bmp, 0,0, True)
self.SetWindowShape()
self.Bind(wx.EVT_RIGHT_UP, self.OnExit)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_WINDOW_CREATE, self.SetWindowShape)
panel = MyPanel(parent=self)
def SetWindowShape(self, evt=None):
r = wx.Region(self.bmp)
self.hasShape = self.SetShape(r)
def OnDoubleClick(self, evt):
if self.hasShape:
self.SetShape(wx.Region())
self.hasShape = False
else:
self.SetWindowShape()
def OnPaint(self, evt):
dc = wx.PaintDC(self)
dc.DrawBitmap(self.bmp, 0,0, True)
def OnExit(self, evt):
self.Close()
def OnLeftDown(self, evt):
self.CaptureMouse()
pos = self.ClientToScreen(evt.GetPosition())
origin = self.GetPosition()
self.delta = wx.Point(pos.x - origin.x, pos.y - origin.y)
def OnMouseMove(self, evt):
if evt.Dragging() and evt.LeftIsDown():
pos = self.ClientToScreen(evt.GetPosition())
newPos = (pos.x - self.delta.x, pos.y - self.delta.y)
self.Move(newPos)
def OnLeftUp(self, evt):
if self.HasCapture():
self.ReleaseMouse()
class MyPanel(wx.Panel):
# A panel is a window on which controls are placed. (e.g. buttons and text boxes)
# wx.Panel class is usually put inside a wxFrame object. This class is also inherited from wxWindow class.
def __init__(self,parent):
super().__init__(parent=parent)
MyImage(self)
class MyImage(wx.StaticBitmap):
def __init__(self,parent):
super().__init__(parent=parent)
jpg1 = wx.Image('./Images/screwsmall.png', wx.BITMAP_TYPE_ANY).ConvertToBitmap()
# bitmap upper left corner is in the position tuple (x, y) = (5, 5)
self.myImage = wx.StaticBitmap(parent, -1, jpg1, (10 + jpg1.GetWidth(), 5), (jpg1.GetWidth(), jpg1.GetHeight()))
self.myImage.Bind(wx.EVT_MOTION, self.OnMouseMove)
self.myImage.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
def OnMouseMove(self, evt):
if evt.Dragging() and evt.LeftIsDown():
pos = self.ClientToScreen(evt.GetPosition())
newPos = (pos.x - self.delta.x, pos.y - self.delta.y)
self.Move(newPos)
def OnLeftUp(self, evt):
if self.HasCapture():
self.ReleaseMouse()
def OnLeftDown(self, evt):
self.CaptureMouse()
pos = self.ClientToScreen(evt.GetPosition())
origin = self.GetPosition()
self.delta = wx.Point(pos.x - origin.x, pos.y - origin.y)
if __name__ == '__main__':
app = wx.App()
ShapedFrame().Show()
app.MainLoop()
Visual output of my code you can use different shapes in local directory. To install wxpython for 3.x you can check this link https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-16.04/ download your version for ubuntu and use pip install command.
I am going to put my answer. As it can be seen in the #RolfofSaxony comment, there is a drag image demo inside the WXPYTHON tar file. In that DragImage.py file there are two different classes do the dragging job. I modified those functions and wrote my own two class. You can use these classes in your code as a component. My code is working and tested.
class DragShape:
def __init__(self, bmp):
self.bmp = bmp
self.pos = (0,0)
self.shown = True
self.text = None
self.fullscreen = False
def HitTest(self, pt):
rect = self.GetRect()
return rect.Contains(pt)
def GetRect(self):
return wx.Rect(self.pos[0], self.pos[1],
self.bmp.GetWidth(), self.bmp.GetHeight())
def Draw(self, dc, op = wx.COPY):
if self.bmp.IsOk():
memDC = wx.MemoryDC()
memDC.SelectObject(self.bmp)
dc.Blit(self.pos[0], self.pos[1],
self.bmp.GetWidth(), self.bmp.GetHeight(),
memDC, 0, 0, op, True)
return True
else:
return False
#----------------------------------------------------------------------
class DragCanvas(wx.ScrolledWindow):
def __init__(self, parent, ID):
wx.ScrolledWindow.__init__(self, parent, ID)
self.shapes = []
self.dragImage = None
self.dragShape = None
self.hiliteShape = None
self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))
bmp = images.TheKid.GetBitmap()
shape = DragShape(bmp)
shape.pos = (200, 5)
self.shapes.append(shape)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
self.Bind(wx.EVT_MOTION, self.OnMotion)
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
# We're not doing anything here, but you might have reason to.
# for example, if you were dragging something, you might elect to
# 'drop it' when the cursor left the window.
def OnLeaveWindow(self, evt):
pass
# Go through our list of shapes and draw them in whatever place they are.
def DrawShapes(self, dc):
for shape in self.shapes:
if shape.shown:
shape.Draw(dc)
# This is actually a sophisticated 'hit test', but in this
# case we're also determining which shape, if any, was 'hit'.
def FindShape(self, pt):
for shape in self.shapes:
if shape.HitTest(pt):
return shape
return None
# Fired whenever a paint event occurs
def OnPaint(self, evt):
dc = wx.PaintDC(self)
self.PrepareDC(dc)
self.DrawShapes(dc)
# print('OnPaint')
# Left mouse button is down.
def OnLeftDown(self, evt):
# Did the mouse go down on one of our shapes?
shape = self.FindShape(evt.GetPosition())
# If a shape was 'hit', then set that as the shape we're going to
# drag around. Get our start position. Dragging has not yet started.
# That will happen once the mouse moves, OR the mouse is released.
if shape:
self.dragShape = shape
self.dragStartPos = evt.GetPosition()
# Left mouse button up.
def OnLeftUp(self, evt):
if not self.dragImage or not self.dragShape:
self.dragImage = None
self.dragShape = None
return
# Hide the image, end dragging, and nuke out the drag image.
self.dragImage.Hide()
self.dragImage.EndDrag()
self.dragImage = None
if self.hiliteShape:
self.RefreshRect(self.hiliteShape.GetRect())
self.hiliteShape = None
# reposition and draw the shape
# Note by jmg 11/28/03
# Here's the original:
#
# self.dragShape.pos = self.dragShape.pos + evt.GetPosition() - self.dragStartPos
#
# So if there are any problems associated with this, use that as
# a starting place in your investigation. I've tried to simulate the
# wx.Point __add__ method here -- it won't work for tuples as we
# have now from the various methods
#
# There must be a better way to do this :-)
#
self.dragShape.pos = (
self.dragShape.pos[0] + evt.GetPosition()[0] - self.dragStartPos[0],
self.dragShape.pos[1] + evt.GetPosition()[1] - self.dragStartPos[1]
)
self.dragShape.shown = True
self.RefreshRect(self.dragShape.GetRect())
self.dragShape = None
# The mouse is moving
def OnMotion(self, evt):
# Ignore mouse movement if we're not dragging.
if not self.dragShape or not evt.Dragging() or not evt.LeftIsDown():
return
# if we have a shape, but haven't started dragging yet
if self.dragShape and not self.dragImage:
# only start the drag after having moved a couple pixels
tolerance = 2
pt = evt.GetPosition()
dx = abs(pt.x - self.dragStartPos.x)
dy = abs(pt.y - self.dragStartPos.y)
if dx <= tolerance and dy <= tolerance:
return
# refresh the area of the window where the shape was so it
# will get erased.
self.dragShape.shown = False
self.RefreshRect(self.dragShape.GetRect(), True)
self.Update()
item = self.dragShape.text if self.dragShape.text else self.dragShape.bmp
self.dragImage = wx.DragImage(item,
wx.Cursor(wx.CURSOR_HAND))
hotspot = self.dragStartPos - self.dragShape.pos
self.dragImage.BeginDrag(hotspot, self, self.dragShape.fullscreen)
self.dragImage.Move(pt)
self.dragImage.Show()
# if we have shape and image then move it, posibly highlighting another shape.
elif self.dragShape and self.dragImage:
onShape = self.FindShape(evt.GetPosition())
unhiliteOld = False
hiliteNew = False
# figure out what to hilite and what to unhilite
if self.hiliteShape:
if onShape is None or self.hiliteShape is not onShape:
unhiliteOld = True
if onShape and onShape is not self.hiliteShape and onShape.shown:
hiliteNew = True
# if needed, hide the drag image so we can update the window
if unhiliteOld or hiliteNew:
self.dragImage.Hide()
if unhiliteOld:
dc = wx.ClientDC(self)
self.hiliteShape.Draw(dc)
self.hiliteShape = None
if hiliteNew:
dc = wx.ClientDC(self)
self.hiliteShape = onShape
self.hiliteShape.Draw(dc, wx.INVERT)
# now move it and show it again if needed
self.dragImage.Move(evt.GetPosition())
if unhiliteOld or hiliteNew:
self.dragImage.Show()

How to resize a window from the edges after adding the property QtCore.Qt.FramelessWindowHint

Good night.
I have seen some programs with new borderless designs and still you can make use of resizing.
At the moment I know that to remove the borders of a pyqt program we use:
QtCore.Qt.FramelessWindowHint
And that to change the size of a window use QSizeGrip.
But how can we resize a window without borders?
This is the code that I use to remove the border of a window but after that I have not found information on how to do it in pyqt5.
I hope you can help me with an example of how to solve this problem
from PyQt5.QtWidgets import QMainWindow,QApplication
from PyQt5 import QtCore
class Main(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
app = QApplication([])
m = Main()
m.show()
m.resize(800,600)
app.exec_()
If you use a QMainWindow you can add a QStatusBar (which automatically adds a QSizeGrip) just by calling statusBar():
This function creates and returns an empty status bar if the status bar does not exist.
Otherwise, you can manually add grips, and their interaction is done automatically based on their position. In the following example I'm adding 4 grips, one for each corner, and then I move them each time the window is resized.
class Main(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.gripSize = 16
self.grips = []
for i in range(4):
grip = QSizeGrip(self)
grip.resize(self.gripSize, self.gripSize)
self.grips.append(grip)
def resizeEvent(self, event):
QMainWindow.resizeEvent(self, event)
rect = self.rect()
# top left grip doesn't need to be moved...
# top right
self.grips[1].move(rect.right() - self.gripSize, 0)
# bottom right
self.grips[2].move(
rect.right() - self.gripSize, rect.bottom() - self.gripSize)
# bottom left
self.grips[3].move(0, rect.bottom() - self.gripSize)
UPDATE
Based on comments, also side-resizing is required. To do so a good solution is to create a custom widget that behaves similarly to QSizeGrip, but for vertical/horizontal resizing only.
For better implementation I changed the code above, used a gripSize to construct an "inner" rectangle and, based on it, change the geometry of all widgets, for both corners and sides.
Here you can see the "outer" rectangle and the "inner" rectangle used for geometry computations:
Then you can create all geometries, for QSizeGrip widgets (in light blue):
And for custom side widgets:
from PyQt5 import QtCore, QtGui, QtWidgets
class SideGrip(QtWidgets.QWidget):
def __init__(self, parent, edge):
QtWidgets.QWidget.__init__(self, parent)
if edge == QtCore.Qt.LeftEdge:
self.setCursor(QtCore.Qt.SizeHorCursor)
self.resizeFunc = self.resizeLeft
elif edge == QtCore.Qt.TopEdge:
self.setCursor(QtCore.Qt.SizeVerCursor)
self.resizeFunc = self.resizeTop
elif edge == QtCore.Qt.RightEdge:
self.setCursor(QtCore.Qt.SizeHorCursor)
self.resizeFunc = self.resizeRight
else:
self.setCursor(QtCore.Qt.SizeVerCursor)
self.resizeFunc = self.resizeBottom
self.mousePos = None
def resizeLeft(self, delta):
window = self.window()
width = max(window.minimumWidth(), window.width() - delta.x())
geo = window.geometry()
geo.setLeft(geo.right() - width)
window.setGeometry(geo)
def resizeTop(self, delta):
window = self.window()
height = max(window.minimumHeight(), window.height() - delta.y())
geo = window.geometry()
geo.setTop(geo.bottom() - height)
window.setGeometry(geo)
def resizeRight(self, delta):
window = self.window()
width = max(window.minimumWidth(), window.width() + delta.x())
window.resize(width, window.height())
def resizeBottom(self, delta):
window = self.window()
height = max(window.minimumHeight(), window.height() + delta.y())
window.resize(window.width(), height)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.mousePos = event.pos()
def mouseMoveEvent(self, event):
if self.mousePos is not None:
delta = event.pos() - self.mousePos
self.resizeFunc(delta)
def mouseReleaseEvent(self, event):
self.mousePos = None
class Main(QtWidgets.QMainWindow):
_gripSize = 8
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.sideGrips = [
SideGrip(self, QtCore.Qt.LeftEdge),
SideGrip(self, QtCore.Qt.TopEdge),
SideGrip(self, QtCore.Qt.RightEdge),
SideGrip(self, QtCore.Qt.BottomEdge),
]
# corner grips should be "on top" of everything, otherwise the side grips
# will take precedence on mouse events, so we are adding them *after*;
# alternatively, widget.raise_() can be used
self.cornerGrips = [QtWidgets.QSizeGrip(self) for i in range(4)]
#property
def gripSize(self):
return self._gripSize
def setGripSize(self, size):
if size == self._gripSize:
return
self._gripSize = max(2, size)
self.updateGrips()
def updateGrips(self):
self.setContentsMargins(*[self.gripSize] * 4)
outRect = self.rect()
# an "inner" rect used for reference to set the geometries of size grips
inRect = outRect.adjusted(self.gripSize, self.gripSize,
-self.gripSize, -self.gripSize)
# top left
self.cornerGrips[0].setGeometry(
QtCore.QRect(outRect.topLeft(), inRect.topLeft()))
# top right
self.cornerGrips[1].setGeometry(
QtCore.QRect(outRect.topRight(), inRect.topRight()).normalized())
# bottom right
self.cornerGrips[2].setGeometry(
QtCore.QRect(inRect.bottomRight(), outRect.bottomRight()))
# bottom left
self.cornerGrips[3].setGeometry(
QtCore.QRect(outRect.bottomLeft(), inRect.bottomLeft()).normalized())
# left edge
self.sideGrips[0].setGeometry(
0, inRect.top(), self.gripSize, inRect.height())
# top edge
self.sideGrips[1].setGeometry(
inRect.left(), 0, inRect.width(), self.gripSize)
# right edge
self.sideGrips[2].setGeometry(
inRect.left() + inRect.width(),
inRect.top(), self.gripSize, inRect.height())
# bottom edge
self.sideGrips[3].setGeometry(
self.gripSize, inRect.top() + inRect.height(),
inRect.width(), self.gripSize)
def resizeEvent(self, event):
QtWidgets.QMainWindow.resizeEvent(self, event)
self.updateGrips()
app = QtWidgets.QApplication([])
m = Main()
m.show()
m.resize(240, 160)
app.exec_()
to hide the QSizeGrip on the corners where they shouldn't be showing, you can just change the background color of the QSizeGrip to camouflage them to the background. add this to each of the corners of musicamante's answer:
self.cornerGrips[0].setStyleSheet("""
background-color: transparent;
""")

Memory game using pygame

I am making a memory game for an assignment. I can get all the tiles to be covered and make them reveal themselves upon being clicked on but I cannot cover them after they have been clicked on or match them. I dont know how to match the tiles also.
class Tile:
surface = None
border_size = 3
border_color = pygame.Color('black')
# An object in this class represents a Dot that moves
#classmethod
def set_surface(cls,game_surface):
cls.surface = game_surface
# instance method
def __init__(self,x , y, image, cover):
self.image = image
self.cover = cover
self.covered = True
self.time_cover = None
# self.timer = pygame.time.get_ticks()
width = self.image.get_width()
height = self.image.get_height()
self.rect = pygame.Rect(x, y, width, height)
def draw(self):
pygame.draw.rect(Tile.surface,Tile.border_color,self.rect,Tile.border_size)
Tile.surface.blit(self.image,self.rect)
if self.covered:
Tile.surface.blit(self.cover, self.rect)
def select(self, position):
valid_click = False
if self.rect.collidepoint(position):
if self.covered:
valid_click = True
self.expose_tile()
self.time_cover = pygame.time.get_ticks() + 2000
self.update()
else:
valid_click = False
return valid_click
def update(self):
if not self.covered and self.time_cover >= 2000:
self.covered = True
return self.covered
def expose_tile(self):
# if a tile is clicked this method will show the pic ture underneath that tile
self.covered = False
def __eq__ (self, other_tile):
pass
When you call update() in the main application loop, then revealed tiles will be covered after 2 seconds.
But you can add a cover_tile method, too:
class Tile:
# [...]
def cover_tile(self):
self.covered = True
If matching tiles share the same image (self.image), then matching tiles can be identified by comparing the images. e.g.:
(In the following tileA and tileB are instances of Tile)
if tielA.image != tileB.image:
tileA.cover_tile()
tileB.cover_tile()
else
print("matching")

Is there an equivalent of Toastr for PyQt?

I am working on my first PyQt project and I would like to come up with a way to provide the user with success or error messages when they complete tasks. With Javascript in the past, I used Toastr and I was curious if there is anything like it for Python applications. I considered using the QDialog class in PyQt, but I would rather not have separate windows as popups if possible since even modeless dialog windows would be distracting for the user.
UPDATE: I've updated the code, making it possible to show desktop-wise notifications (see below).
Implementing a desktop-aware toaster like widget is not impossible, but presents some issues that are platform dependent. On the other hand, a client-side one is easier.
I've created a small class that is able to show a notification based on the top level window of the current widget, with the possibility to set the message text, the icon, and if the notification is user-closable. I also added a nice opacity animation, that is common in such systems.
Its main use is based on a static method, similarly to what QMessageBox does, but it can also be implemented in a similar fashion by adding other features.
UPDATE
I realized that making a desktop-wise notification is not that hard (but some care is required for cross-platform development, I'll leave that up to the programmer).
The following is the updated code that allows using None as a parent for the class, making the notification a desktop widget instead of a child widget of an existing Qt one. If you're reading this and you're not interested in such a feature, just check the editing history for the original (and slightly simpler) code.
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class QToaster(QtWidgets.QFrame):
closed = QtCore.pyqtSignal()
def __init__(self, *args, **kwargs):
super(QToaster, self).__init__(*args, **kwargs)
QtWidgets.QHBoxLayout(self)
self.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
QtWidgets.QSizePolicy.Maximum)
self.setStyleSheet('''
QToaster {
border: 1px solid black;
border-radius: 4px;
background: palette(window);
}
''')
# alternatively:
# self.setAutoFillBackground(True)
# self.setFrameShape(self.Box)
self.timer = QtCore.QTimer(singleShot=True, timeout=self.hide)
if self.parent():
self.opacityEffect = QtWidgets.QGraphicsOpacityEffect(opacity=0)
self.setGraphicsEffect(self.opacityEffect)
self.opacityAni = QtCore.QPropertyAnimation(self.opacityEffect, b'opacity')
# we have a parent, install an eventFilter so that when it's resized
# the notification will be correctly moved to the right corner
self.parent().installEventFilter(self)
else:
# there's no parent, use the window opacity property, assuming that
# the window manager supports it; if it doesn't, this won'd do
# anything (besides making the hiding a bit longer by half a second)
self.opacityAni = QtCore.QPropertyAnimation(self, b'windowOpacity')
self.opacityAni.setStartValue(0.)
self.opacityAni.setEndValue(1.)
self.opacityAni.setDuration(100)
self.opacityAni.finished.connect(self.checkClosed)
self.corner = QtCore.Qt.TopLeftCorner
self.margin = 10
def checkClosed(self):
# if we have been fading out, we're closing the notification
if self.opacityAni.direction() == self.opacityAni.Backward:
self.close()
def restore(self):
# this is a "helper function", that can be called from mouseEnterEvent
# and when the parent widget is resized. We will not close the
# notification if the mouse is in or the parent is resized
self.timer.stop()
# also, stop the animation if it's fading out...
self.opacityAni.stop()
# ...and restore the opacity
if self.parent():
self.opacityEffect.setOpacity(1)
else:
self.setWindowOpacity(1)
def hide(self):
# start hiding
self.opacityAni.setDirection(self.opacityAni.Backward)
self.opacityAni.setDuration(500)
self.opacityAni.start()
def eventFilter(self, source, event):
if source == self.parent() and event.type() == QtCore.QEvent.Resize:
self.opacityAni.stop()
parentRect = self.parent().rect()
geo = self.geometry()
if self.corner == QtCore.Qt.TopLeftCorner:
geo.moveTopLeft(
parentRect.topLeft() + QtCore.QPoint(self.margin, self.margin))
elif self.corner == QtCore.Qt.TopRightCorner:
geo.moveTopRight(
parentRect.topRight() + QtCore.QPoint(-self.margin, self.margin))
elif self.corner == QtCore.Qt.BottomRightCorner:
geo.moveBottomRight(
parentRect.bottomRight() + QtCore.QPoint(-self.margin, -self.margin))
else:
geo.moveBottomLeft(
parentRect.bottomLeft() + QtCore.QPoint(self.margin, -self.margin))
self.setGeometry(geo)
self.restore()
self.timer.start()
return super(QToaster, self).eventFilter(source, event)
def enterEvent(self, event):
self.restore()
def leaveEvent(self, event):
self.timer.start()
def closeEvent(self, event):
# we don't need the notification anymore, delete it!
self.deleteLater()
def resizeEvent(self, event):
super(QToaster, self).resizeEvent(event)
# if you don't set a stylesheet, you don't need any of the following!
if not self.parent():
# there's no parent, so we need to update the mask
path = QtGui.QPainterPath()
path.addRoundedRect(QtCore.QRectF(self.rect()).translated(-.5, -.5), 4, 4)
self.setMask(QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon()))
else:
self.clearMask()
#staticmethod
def showMessage(parent, message,
icon=QtWidgets.QStyle.SP_MessageBoxInformation,
corner=QtCore.Qt.TopLeftCorner, margin=10, closable=True,
timeout=5000, desktop=False, parentWindow=True):
if parent and parentWindow:
parent = parent.window()
if not parent or desktop:
self = QToaster(None)
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint |
QtCore.Qt.BypassWindowManagerHint)
# This is a dirty hack!
# parentless objects are garbage collected, so the widget will be
# deleted as soon as the function that calls it returns, but if an
# object is referenced to *any* other object it will not, at least
# for PyQt (I didn't test it to a deeper level)
self.__self = self
currentScreen = QtWidgets.QApplication.primaryScreen()
if parent and parent.window().geometry().size().isValid():
# the notification is to be shown on the desktop, but there is a
# parent that is (theoretically) visible and mapped, we'll try to
# use its geometry as a reference to guess which desktop shows
# most of its area; if the parent is not a top level window, use
# that as a reference
reference = parent.window().geometry()
else:
# the parent has not been mapped yet, let's use the cursor as a
# reference for the screen
reference = QtCore.QRect(
QtGui.QCursor.pos() - QtCore.QPoint(1, 1),
QtCore.QSize(3, 3))
maxArea = 0
for screen in QtWidgets.QApplication.screens():
intersected = screen.geometry().intersected(reference)
area = intersected.width() * intersected.height()
if area > maxArea:
maxArea = area
currentScreen = screen
parentRect = currentScreen.availableGeometry()
else:
self = QToaster(parent)
parentRect = parent.rect()
self.timer.setInterval(timeout)
# use Qt standard icon pixmaps; see:
# https://doc.qt.io/qt-5/qstyle.html#StandardPixmap-enum
if isinstance(icon, QtWidgets.QStyle.StandardPixmap):
labelIcon = QtWidgets.QLabel()
self.layout().addWidget(labelIcon)
icon = self.style().standardIcon(icon)
size = self.style().pixelMetric(QtWidgets.QStyle.PM_SmallIconSize)
labelIcon.setPixmap(icon.pixmap(size))
self.label = QtWidgets.QLabel(message)
self.layout().addWidget(self.label)
if closable:
self.closeButton = QtWidgets.QToolButton()
self.layout().addWidget(self.closeButton)
closeIcon = self.style().standardIcon(
QtWidgets.QStyle.SP_TitleBarCloseButton)
self.closeButton.setIcon(closeIcon)
self.closeButton.setAutoRaise(True)
self.closeButton.clicked.connect(self.close)
self.timer.start()
# raise the widget and adjust its size to the minimum
self.raise_()
self.adjustSize()
self.corner = corner
self.margin = margin
geo = self.geometry()
# now the widget should have the correct size hints, let's move it to the
# right place
if corner == QtCore.Qt.TopLeftCorner:
geo.moveTopLeft(
parentRect.topLeft() + QtCore.QPoint(margin, margin))
elif corner == QtCore.Qt.TopRightCorner:
geo.moveTopRight(
parentRect.topRight() + QtCore.QPoint(-margin, margin))
elif corner == QtCore.Qt.BottomRightCorner:
geo.moveBottomRight(
parentRect.bottomRight() + QtCore.QPoint(-margin, -margin))
else:
geo.moveBottomLeft(
parentRect.bottomLeft() + QtCore.QPoint(margin, -margin))
self.setGeometry(geo)
self.show()
self.opacityAni.start()
class W(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QVBoxLayout(self)
toasterLayout = QtWidgets.QHBoxLayout()
layout.addLayout(toasterLayout)
self.textEdit = QtWidgets.QLineEdit('Ciao!')
toasterLayout.addWidget(self.textEdit)
self.cornerCombo = QtWidgets.QComboBox()
toasterLayout.addWidget(self.cornerCombo)
for pos in ('TopLeft', 'TopRight', 'BottomRight', 'BottomLeft'):
corner = getattr(QtCore.Qt, '{}Corner'.format(pos))
self.cornerCombo.addItem(pos, corner)
self.windowBtn = QtWidgets.QPushButton('Show window toaster')
toasterLayout.addWidget(self.windowBtn)
self.windowBtn.clicked.connect(self.showToaster)
self.screenBtn = QtWidgets.QPushButton('Show desktop toaster')
toasterLayout.addWidget(self.screenBtn)
self.screenBtn.clicked.connect(self.showToaster)
# a random widget for the window
layout.addWidget(QtWidgets.QTableView())
def showToaster(self):
if self.sender() == self.windowBtn:
parent = self
desktop = False
else:
parent = None
desktop = True
corner = QtCore.Qt.Corner(self.cornerCombo.currentData())
QToaster.showMessage(
parent, self.textEdit.text(), corner=corner, desktop=desktop)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = W()
w.show()
sys.exit(app.exec_())
Try it:
import sys
from PyQt5.QtCore import (QRectF, Qt, QPropertyAnimation, pyqtProperty,
QPoint, QParallelAnimationGroup, QEasingCurve)
from PyQt5.QtGui import QPainter, QPainterPath, QColor, QPen
from PyQt5.QtWidgets import (QLabel, QWidget, QVBoxLayout, QApplication,
QLineEdit, QPushButton)
class BubbleLabel(QWidget):
BackgroundColor = QColor(195, 195, 195)
BorderColor = QColor(150, 150, 150)
def __init__(self, *args, **kwargs):
text = kwargs.pop("text", "")
super(BubbleLabel, self).__init__(*args, **kwargs)
self.setWindowFlags(
Qt.Window | Qt.Tool | Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint | Qt.X11BypassWindowManagerHint)
# Set minimum width and height
self.setMinimumWidth(200)
self.setMinimumHeight(58)
self.setAttribute(Qt.WA_TranslucentBackground, True)
layout = QVBoxLayout(self)
# Top left and bottom right margins (16 below because triangles are included)
layout.setContentsMargins(8, 8, 8, 16)
self.label = QLabel(self)
layout.addWidget(self.label)
self.setText(text)
# Get screen height and width
self._desktop = QApplication.instance().desktop()
def setText(self, text):
self.label.setText(text)
def text(self):
return self.label.text()
def stop(self):
self.hide()
self.animationGroup.stop()
self.close()
def show(self):
super(BubbleLabel, self).show()
# Window start position
startPos = QPoint(
self._desktop.screenGeometry().width() - self.width() - 100,
self._desktop.availableGeometry().height() - self.height())
endPos = QPoint(
self._desktop.screenGeometry().width() - self.width() - 100,
self._desktop.availableGeometry().height() - self.height() * 3 - 5)
self.move(startPos)
# Initialization animation
self.initAnimation(startPos, endPos)
def initAnimation(self, startPos, endPos):
# Transparency animation
opacityAnimation = QPropertyAnimation(self, b"opacity")
opacityAnimation.setStartValue(1.0)
opacityAnimation.setEndValue(0.0)
# Set the animation curve
opacityAnimation.setEasingCurve(QEasingCurve.InQuad)
opacityAnimation.setDuration(4000)
# Moving up animation
moveAnimation = QPropertyAnimation(self, b"pos")
moveAnimation.setStartValue(startPos)
moveAnimation.setEndValue(endPos)
moveAnimation.setEasingCurve(QEasingCurve.InQuad)
moveAnimation.setDuration(5000)
# Parallel animation group (the purpose is to make the two animations above simultaneously)
self.animationGroup = QParallelAnimationGroup(self)
self.animationGroup.addAnimation(opacityAnimation)
self.animationGroup.addAnimation(moveAnimation)
# Close window at the end of the animation
self.animationGroup.finished.connect(self.close)
self.animationGroup.start()
def paintEvent(self, event):
super(BubbleLabel, self).paintEvent(event)
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) # Antialiasing
rectPath = QPainterPath() # Rounded Rectangle
triPath = QPainterPath() # Bottom triangle
height = self.height() - 8 # Offset up 8
rectPath.addRoundedRect(QRectF(0, 0, self.width(), height), 5, 5)
x = self.width() / 5 * 4
triPath.moveTo(x, height) # Move to the bottom horizontal line 4/5
# Draw triangle
triPath.lineTo(x + 6, height + 8)
triPath.lineTo(x + 12, height)
rectPath.addPath(triPath) # Add a triangle to the previous rectangle
# Border brush
painter.setPen(QPen(self.BorderColor, 1, Qt.SolidLine,
Qt.RoundCap, Qt.RoundJoin))
# Background brush
painter.setBrush(self.BackgroundColor)
# Draw shape
painter.drawPath(rectPath)
# Draw a line on the bottom of the triangle to ensure the same color as the background
painter.setPen(QPen(self.BackgroundColor, 1,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.drawLine(x, height, x + 12, height)
def windowOpacity(self):
return super(BubbleLabel, self).windowOpacity()
def setWindowOpacity(self, opacity):
super(BubbleLabel, self).setWindowOpacity(opacity)
# Since the opacity property is not in QWidget, you need to redefine one
opacity = pyqtProperty(float, windowOpacity, setWindowOpacity)
class TestWidget(QWidget):
def __init__(self, *args, **kwargs):
super(TestWidget, self).__init__(*args, **kwargs)
layout = QVBoxLayout(self)
self.msgEdit = QLineEdit(self, returnPressed=self.onMsgShow)
self.msgButton = QPushButton("Display content", self, clicked=self.onMsgShow)
layout.addWidget(self.msgEdit)
layout.addWidget(self.msgButton)
def onMsgShow(self):
msg = self.msgEdit.text().strip()
if not msg:
return
if hasattr(self, "_blabel"):
self._blabel.stop()
self._blabel.deleteLater()
del self._blabel
self._blabel = BubbleLabel()
self._blabel.setText(msg)
self._blabel.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = TestWidget()
w.show()
sys.exit(app.exec_())
There is nothing like that even in Qt 6.
Anyways, you said "but I would rather not have separate windows as popups if possible since even modeless dialog windows would be distracting for the user.".
Yes, there are two things necessary for the toast, and there is a solution.
Should not be a separated window - Qt.SubWindow
self.setWindowFlags(Qt.SubWindow)
Should ignore the mouse event, be unable to focus - Qt.WA_TransparentForMouseEvents
self.setAttribute(Qt.WA_TransparentForMouseEvents, True)
Based on those rules, i made the toast that user can set the text, font, color(text or background) of it.
Here is my repo if you want to check the detail: https://github.com/yjg30737/pyqt-toast

Resources