# BouncingBallQt.py Copyright (c) Kari Laitinen # http://www.naturalprogramming.com # 2010-11-04 File created. # 2010-11-11 Last modification. # This program shows a ball that moves on the screen. # The ball bounces when it hits a 'wall'. import sys from PyQt4.QtCore import * from PyQt4.QtGui import * import math, random from time import sleep class GraphicalObject : def __init__( self, given_center_point, given_color = Qt.red ) : self.object_center_point = given_center_point self.object_color = given_color # object_velocity specifies the number of pixels the object # will be moved in a single movement operation. self.object_velocity = 4.0 def get_object_position( self ) : return self.object_center_point def set_color( self, new_color ) : self.object_color = new_color def move_right( self ) : self.object_center_point.setX( self.object_center_point.x() + self.object_velocity ) def move_left( self ) : self.object_center_point.setX( self.object_center_point.x() - self.object_velocity ) def move_up( self ) : self.object_center_point.setY( self.object_center_point.y() - self.object_velocity ) def move_down( self ) : self.object_center_point.setY( self.object_center_point.y() + self.object_velocity ) def move_this_object( self, movement_in_direction_x, movement_in_direction_y ) : self.object_center_point.setX( self.object_center_point.x() + movement_in_direction_x ) self.object_center_point.setY( self.object_center_point.y() + movement_in_direction_y ) def move_to_position( self, new_position_x, new_position_y ) : self.object_center_point.setX( self.object_center_point.x() + new_position_x ) self.object_center_point.setY( self.object_center_point.y() + new_position_y ) class Bouncer ( GraphicalObject ) : def __init__( self, given_position, given_color, given_bouncing_area ) : GraphicalObject.__init__( self, given_position, given_color ) self.bouncing_area = given_bouncing_area self.bouncer_radius = 40 # bouncer_direction is an angle in radians. This angle specifies # the direction where the bouncer will be moved next. self.bouncer_direction = random.random() * math.pi * 2 def get_bouncer_radius( self ) : return self.bouncer_radius def shrink( self ) : # The if-construct ensures that the ball does not become # too small. if self.bouncer_radius > 5 : self.bouncer_radius -= 3 def enlarge( self ) : self.bouncer_radius = self.bouncer_radius + 3 def set_radius( self, new_radius ) : if new_radius > 3 : self.bouncer_radius = new_radius def contains_point( self, given_point ) : # Here we use the Pythagorean theorem to calculate the distance # from the given point to the center point of the ball. # See the note at the end of this file. distance_from_given_point_to_ball_center = \ math.sqrt( math.pow( self.object_center_point.x() - given_point.x, 2 ) + math.pow( self.object_center_point.y() - given_point.y, 2 ) ) return distance_from_given_point_to_ball_center <= self.bouncer_radius # The move() method is supposed to be called something like # 25 times a second. def move( self ) : # In the following statement a minus sign is needed when the # y coordinate is calculated. The reason for this is that the # y direction in the graphical coordinate system is 'upside down'. self.object_center_point.setX( self.object_center_point.x() + \ self.object_velocity * math.cos( self.bouncer_direction ) ) self.object_center_point.setY( self.object_center_point.y() - \ self.object_velocity * math.sin( self.bouncer_direction ) ) # Now, after we have moved this bouncer, we start finding out whether # or not it has hit a wall or some other obstacle. If a hit occurs, # a new direction for the bouncer must be calculated. # The following four if constructs must be four separate ifs. # If they are replaced with an if - elif - elif - elif # construct, the program will not work when the bouncer enters # a corner in an angle of 45 degrees (i.e. math.pi / 4). if self.object_center_point.y() - self.bouncer_radius <= \ self.bouncing_area.top() : # The bouncer has hit the northern 'wall' of the bouncing area. self.bouncer_direction = 2 * math.pi - self.bouncer_direction if self.object_center_point.x() - self.bouncer_radius <= \ self.bouncing_area.left() : # The western wall has been reached. self.bouncer_direction = math.pi - self.bouncer_direction if self.object_center_point.y() + self.bouncer_radius >= \ self.bouncing_area.bottom() : # Southern wall has been reached. self.bouncer_direction = 2 * math.pi - self.bouncer_direction if self.object_center_point.x() + self.bouncer_radius >= \ self.bouncing_area.right() : # Eastern wall reached. self.bouncer_direction = math.pi - self.bouncer_direction def draw( self, painter ) : painter.setBrush( self.object_color ) painter.drawEllipse( self.object_center_point.x() - self.bouncer_radius, self.object_center_point.y() - self.bouncer_radius, self.bouncer_radius * 2, self.bouncer_radius * 2 ) class RotatingBouncer ( Bouncer ) : def __init__( self, given_position, given_color, given_bouncing_area ) : Bouncer.__init__( self, given_position, given_color, given_bouncing_area ) self.current_rotation = 0 def move( self ) : Bouncer.move( self ) # run the corresponding upper class method first self.current_rotation = self.current_rotation + 3 if self.current_rotation >= 360 : self.current_rotation = 0 def draw( self, painter ) : Bouncer.draw( self, painter ) # run the upper class draw() first saved_graphics_transform = painter.combinedTransform() # First we move the zero point of the coordinate system into # the center point of the ball. painter.translate( self.object_center_point.x(), self.object_center_point.y() ) # Rotate the coordinate system as much as is the value of # the data field self.current_rotation. painter.rotate( self.current_rotation ) painter.setBrush( Qt.darkGreen ) # Fill one quarter of the ball with black color. painter.drawPie( QRect( - self.bouncer_radius, - self.bouncer_radius, self.bouncer_radius * 2, self.bouncer_radius * 2 ), 0, 90*16 ) # Fill another quarter of the ball with black color. painter.drawPie( QRect( - self.bouncer_radius, - self.bouncer_radius, self.bouncer_radius * 2, self.bouncer_radius * 2 ), 180*16, 90*16 ) # Finally we restore the original coordinate system. painter.setTransform( saved_graphics_transform ) class ExplodingBouncer ( RotatingBouncer ) : BALL_ALIVE_AND_WELL = 0 BALL_EXPLODING = 1 BALL_EXPLODED = 2 def __init__( self, given_position, given_color, given_bouncing_area ) : RotatingBouncer.__init__( self, given_position, given_color, given_bouncing_area ) self.ball_state = ExplodingBouncer.BALL_ALIVE_AND_WELL self.explosion_color_alpha_value = 0 def explode_ball( self ) : self.ball_state = ExplodingBouncer.BALL_EXPLODING self.enlarge() # make the ball somewhat larger in explosion self.enlarge() def move( self ) : # The ball will not move if it is exploding or exploded. if self.ball_state == ExplodingBouncer.BALL_ALIVE_AND_WELL : RotatingBouncer.move( self ) # move the ball with the superclass method def draw( self, painter ) : if self.ball_state == ExplodingBouncer.BALL_ALIVE_AND_WELL : RotatingBouncer.draw( self, painter ) # run upper class draw() first elif self.ball_state == ExplodingBouncer.BALL_EXPLODING : if self.explosion_color_alpha_value > 0xFF : self.ball_state = ExplodingBouncer.BALL_EXPLODED else : # The ball will be 'exploded' by drawing a transparent # yellow ball over the original ball. # As the opaqueness of the yellow color gradually increases, # the ball becomes ultimately completely yellow in # the final stage of the explosion. RotatingBouncer.draw( self, painter ) # draw the original ball first explosion_color = QColor( Qt.yellow ) explosion_color = QColor( explosion_color.red(), explosion_color.green(), explosion_color.blue(), self.explosion_color_alpha_value ) painter.setBrush( explosion_color ) painter.drawEllipse( self.object_center_point.x() - self.bouncer_radius, self.object_center_point.y() - self.bouncer_radius, self.bouncer_radius * 2, self.bouncer_radius * 2 ) self.explosion_color_alpha_value += 4 ; # decrease transparency class AnimationThread ( QThread ) : def __init__( self ) : QThread.__init__( self ) self.thread_must_be_run = True self.mutex = QMutex() # print " thread init done " def stop_this_thread( self ) : self.thread_must_be_run = False def run( self ) : while self.thread_must_be_run == True : self.mutex.lock() # locking may not be necessary # Here we emit a signal whenever we want to update window # contents. The other thread will handle this signal # so that the method process_window_update_request() will # be called. self.emit( SIGNAL( "window_update_request" ) ) self.mutex.unlock() sleep( 0.005 ) #QThread.sleep( 1 ) # An alternative way to sleep. # print " run method ends " class BouncingBallWindow( QWidget ) : def __init__( self, parent=None ) : QWidget.__init__( self, parent ) self.setGeometry( 100, 100, 600, 500 ) self.setWindowTitle( "DEMONSTRATING BOUNCING ACTIVITIES" ) bouncing_area = QRectF( 0, 0, self.width(), self.height() ) self.ball_on_screen = \ ExplodingBouncer( QPointF( self.width() / 2, self.height() / 2 ), Qt.red, bouncing_area ) self.animation_thread = AnimationThread() # The following statement specifies that when the # animation thread emits the window_update_request signal, # the method self.process_window_update_request will be # called automatically. self.connect( self.animation_thread, SIGNAL( "window_update_request" ), self.process_window_update_request ) # After the following method call, the program execution # system will automatically call the run() method of the # AnimationThread class. The run() method is executed # in parallel with "this" thread. self.animation_thread.start() def process_window_update_request( self ) : self.ball_on_screen.move() self.update() def keyPressEvent( self, event ) : if event.key() == Qt.Key_Escape : self.ball_on_screen.explode_ball() def paintEvent( self, event ) : painter = QPainter() painter.begin( self ) self.ball_on_screen.draw( painter ) painter.end() def exit_this_window( self ) : self.animation_thread.stop_this_thread() self.close() #is this necessary??? def main( args ) : this_application = QApplication( args ) application_window = BouncingBallWindow() application_window.show() # The following statement specifies that when the # "lastWindowClosed()" signal is emitted by QApplication, # the Python method exit_this_window is called for the # PictureShowWindow object. this_application.connect( this_application, SIGNAL( "lastWindowClosed()" ), application_window.exit_this_window ) this_application.exec_() if __name__== "__main__" : main( sys.argv )