Hello. With my work on a simple Cubic Bezier investigation application in PyQt (attached, obviously under GPL), I ran into a curious behaviour of the slider/spin (for the time along the curve) when controlled by the keyboard.
Steps: 1. Let the focus be either on the slider (on the left) or the spin (below it). (The default slider/spin value is 0.50.) 3. Press up-arrow key to increase the slider/spin value. Observation: The value will not increase past 0.56. Steps: 4. Press down-arrow key to decrease the slider/spin value until 0.30. 5. Press down-arrow once more. Observation: The value jumps down from 0.30 to 0.28 even though the precision is set at 0.01. Step: 6. Press up-arrow. Observation: The value will now not rise above 0.28. Step: 7. Adjust the slider position using the mouse. Observation: The value can change to any value in its full range from 0.00 to 1.00. Step: 8. Adjust the slider using the mouse to go beyond 0.60. 9. Press down-arrow to decrease the value until 0.59. 10. Press down-arrow once more. Observation: 11. The value jumps down to 0.56. 12. It will no longer go above 0.56 using the keyboard (as before). Query: My sliderMoved, spinChanged slots are straightforward, and just convert the integer slider value to the spin and update the bezier widget accordingly. In which case, I do not understand what it is I am doing wrong in my programming. However, I wrote a minimal test where the behaviour is not seen. Any guidance is appreciated. Thanks! -- Shriramana Sharma
#! /usr/bin/env python3 from PyQt4 . QtCore import * from PyQt4 . QtGui import * from math import sqrt # float->string formatting function def str_three_decimals ( a ) : return str ( int ( a * 1000 + 0.5 ) / 1000 ) # mathematical function def quadraticroots ( a, b, c ) : if a == 0 : if b != 0 : return [ - c / b ] else : return [] # no valid equation so no roots det = b * b - 4 * a * c if det < 0 : return [] # only real roots will be returned if det == 0 : return [ - b / ( 2 * a ) ] # one root return [ ( - b - sqrt ( det ) ) / ( 2 * a ), ( - b + sqrt ( det ) ) / ( 2 * a ) ] # bezier analysis functions def pointForTime ( t, p1, c1, c2, p2 ) : if t <= 0 : return p1 if t >= 1 : return p2 return p1 + ( c1 - p1 ) * 3 * t + ( c2 - c1 * 2 + p1 ) * 3 * t * t + ( p2 - c2 * 3 + c1 * 3 - p1 ) * t * t * t def dirForTime ( t, p1, c1, c2, p2 ) : if t < 0 : t = 0 if t > 1 : t = 1 return ( c1 - p1 ) * 3 + ( c2 - c1 * 2 + p1 ) * 6 * t + ( p2 - c2 * 3 + c1 * 3 - p1 ) * 3 * t * t def accelForTime ( t, p1, c1, c2, p2 ) : if t < 0 : t = 0 if t > 1 : t = 1 return ( c2 - c1 * 2 + p1 ) * 6 + ( p2 - c2 * 3 + c1 * 3 - p1 ) * 6 * t def timesOfCusp ( p1, c1, c2, p2 ) : a = c1 - p1 b = c2 - c1 - a c = p2 - c2 - a - b * 2 # dir = ( c * t * t + b * 2 * t + a ) * 3 # at cusp, dir vector becomes null i.e. both x and y components are zero # qA = c ; qB = 2 * b ; qC = a rootsX = quadraticroots ( c . x (), b . x () * 2, a . x () ) rootsY = quadraticroots ( c . y (), b . y () * 2, a . y () ) cusps = [] for x in rootsX : if x in rootsY : cusps += [ x ] return cusps def timesOfInflection ( p1, c1, c2, p2 ) : # algorithm from http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html a = c1 - p1 b = c2 - c1 - a c = p2 - c2 - a - b * 2 qA = ( b . x () * c . y () - b . y () * c . x () ) qB = ( a . x () * c . y () - a . y () * c . x () ) qC = ( a . x () * b . y () - a . y () * b . x () ) roots = quadraticroots ( qA, qB, qC ) cusps = timesOfCusp ( p1, c1, c2, p2 ) validroots = [] for x in roots : if x > 0 and x < 1 and x not in cusps : validroots . append ( x ) return validroots # widgets class BezierWidget ( QWidget ) : def __init__ ( self, parent = None ) : super ( BezierWidget, self ) . __init__ ( parent ) self . setFixedSize ( 400, 400 ) self . setMouseTracking ( True ) self . p1 = QPointF ( 100, 150 ) self . c1 = QPointF ( 166, 250 ) self . c2 = QPointF ( 234, 250 ) self . p2 = QPointF ( 300, 150 ) self . bezTime = 0.5 self . timePoint = pointForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 ) self . timeDir = dirForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 ) self . timeAccel = accelForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 ) self . calcInflectionAndCusp () self . tweaking = False self . diamond = QPainterPath () self . diamond . moveTo ( 4, 0 ) self . diamond . lineTo ( 0, 4 ) self . diamond . lineTo ( -4, 0 ) self . diamond . lineTo ( 0, -4 ) self . diamond . closeSubpath () def paintEvent ( self, event ) : painter = QPainter ( self ) painter . setRenderHint ( QPainter . Antialiasing ) painter . translate ( 0, 400 ) painter . scale ( 1, -1 ) palette = QApplication . palette () bezierPen = QPen ( palette . text (), 1 ) handlePen = QPen ( palette . highlight (), 2, Qt . DashLine ) chandlePen = QPen ( palette . highlightedText (), 0.5, Qt . DashLine ) handle1 = QPainterPath () handle1 . moveTo ( self . p1 ) ; handle1 . lineTo ( self . c1 ) painter . strokePath ( handle1, handlePen ) handle2 = QPainterPath () handle2 . moveTo ( self . p2 ) ; handle2 . lineTo ( self . c2 ) painter . strokePath ( handle2, handlePen ) chandle1 = QPainterPath () chandle1 . moveTo ( self . p1 ) ; chandle1 . lineTo ( self . c2 ) painter . strokePath ( chandle1, chandlePen ) chandle2 = QPainterPath () chandle2 . moveTo ( self . p2 ) ; chandle2 . lineTo ( self . c1 ) painter . strokePath ( chandle2, chandlePen ) bezier = QPainterPath () bezier . moveTo ( self . p1 ) bezier . cubicTo ( self . c1, self . c2, self . p2 ) painter . strokePath ( bezier, bezierPen ) for pt in ( self . p1, self . c1, self . c2, self . p2 ) : painter . save () painter . translate ( pt ) painter . fillPath ( self . diamond, palette . highlightedText () ) painter . restore () for i in range ( 9 ) : pt = pointForTime ( ( i + 1 ) / 10, self . p1, self . c1, self . c2, self . p2 ) painter . save () painter . translate ( pt ) painter . scale ( 0.5, 0.5 ) painter . fillPath ( self . diamond, palette . highlightedText () ) painter . restore () self . timePoint = pointForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 ) self . timeDir = dirForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 ) self . timeAccel = accelForTime ( self . bezTime, self . p1, self . c1, self . c2, self . p2 ) dirLine = QPainterPath () dirLine . lineTo ( self . timeDir / 10 ) accelLine = QPainterPath () accelLine . lineTo ( self . timeAccel / 10 ) painter . save () painter . translate ( self . timePoint ) painter . strokePath ( dirLine, QPen ( palette . highlight (), 2 ) ) painter . strokePath ( accelLine, QPen ( palette . linkVisited (), 2 ) ) painter . scale ( 1.5, 1.5 ) painter . fillPath ( self . diamond, palette . highlightedText () ) painter . restore () for i in range ( len ( self . inflectionTimes ) ) : painter . save () pt = pointForTime ( self . inflectionTimes [ i ], self . p1, self . c1, self . c2, self . p2 ) painter . translate ( pt ) painter . fillPath ( self . diamond, palette . linkVisited () ) painter . restore () def mousePressEvent ( self, event ) : pt = event . posF () pt . setY ( 399 - pt . y () ) offset = 5 hotspot = QRectF ( - offset, - offset, offset * 2, offset * 2 ) for x in ( self . p1, self . c1, self . c2, self . p2 ) : if hotspot . translated ( x ) . contains ( pt ) : self . tweaking = True if x is self . p1 : self . tweakedPoint = "p1" elif x is self . c1 : self . tweakedPoint = "c1" elif x is self . c2 : self . tweakedPoint = "c2" elif x is self . p2 : self . tweakedPoint = "p2" return def mouseMoveEvent ( self, event ) : pt = event . posF () if pt . x () < 0 : pt . setX ( 0 ) if pt . x () > 399 : pt . setX ( 399 ) if pt . y () < 0 : pt . setY ( 0 ) if pt . y () > 399 : pt . setY ( 399 ) ptInt = pt . toPoint () QToolTip . showText ( self . mapToGlobal ( ptInt ), "%d,%d" % ( ptInt . x (), 399 - ptInt . y () ), self ) if self . tweaking : pt . setY ( 399 - pt . y () ) if self . tweakedPoint == "p1" : self . p1 = pt elif self . tweakedPoint == "c1" : self . c1 = pt elif self . tweakedPoint == "c2" : self . c2 = pt elif self . tweakedPoint == "p2" : self . p2 = pt self . repaint () self . emit ( SIGNAL ( "pointsModifiedInWidget ()" ) ) def mouseReleaseEvent ( self, event ) : self . tweaking = False def calcInflectionAndCusp ( self ) : self . cuspTimes = timesOfCusp ( self . p1, self . c1, self . c2, self . p2 ) self . inflectionTimes = timesOfInflection ( self . p1, self . c1, self . c2, self . p2 ) self . inflectionDirs = [ dirForTime ( t, self . p1, self . c1, self . c2, self . p2 ) for t in self . inflectionTimes ] self . inflectionAccels = [ accelForTime ( t, self . p1, self . c1, self . c2, self . p2 ) for t in self . inflectionTimes ] class MainWindow ( QWidget ) : def __init__ ( self, parent = None ) : super ( MainWindow, self ) . __init__ ( parent ) self . setWindowTitle ( "Cubic Bezier Sandbox" ) self . timeSlider = QSlider () self . timeSlider . setMinimum ( 0 ) self . timeSlider . setMaximum ( 100 ) self . timeSlider . setValue ( 50 ) self . timeSpin = QDoubleSpinBox () self . timeSpin . setDecimals ( 2 ) self . timeSpin . setMinimum ( 0 ) self . timeSpin . setMaximum ( 1 ) self . timeSpin . setSingleStep ( 0.01 ) self . timeSpin . setValue ( 0.5 ) self . lhsLayout = QVBoxLayout () self . lhsLayout . addWidget ( self . timeSlider, 0, Qt . AlignHCenter ) self . lhsLayout . addWidget ( self . timeSpin ) self . bezierWidget = BezierWidget () self . p1Label = QLabel ( "p1" ) self . c1Label = QLabel ( "c1" ) self . c2Label = QLabel ( "c2" ) self . p2Label = QLabel ( "p2" ) self . p1xSpin = QSpinBox () self . p1ySpin = QSpinBox () self . c1xSpin = QSpinBox () self . c1ySpin = QSpinBox () self . c2xSpin = QSpinBox () self . c2ySpin = QSpinBox () self . p2xSpin = QSpinBox () self . p2ySpin = QSpinBox () self . xyFieldsGrid = QGridLayout () xyFieldsGridItems = ( ( self . p1Label, self . p1xSpin, self . p1ySpin ), ( self . c1Label, self . c1xSpin, self . c1ySpin ), ( self . c2Label, self . c2xSpin, self . c2ySpin ), ( self . p2Label, self . p2xSpin, self . p2ySpin ) ) for i in range ( 4 ) : for j in range ( 3 ) : self . xyFieldsGrid . addWidget ( xyFieldsGridItems [ i ] [ j ], i, j ) if j > 0 : # isinstance ( xyFieldsGridItems [ i ] [ j ], QSpinBox ) would be pedantically correct xyFieldsGridItems [ i ] [ j ] . setMaximum ( 399 ) self . inflectTimeLabel = QLabel ( "Inflection times" ) self . inflectTime1Label = QLabel () self . inflectTime2Label = QLabel () self . inflDirLabel = QLabel ( "Inflection dirs" ) self . inflDirValLabels = [ [ QLabel (), QLabel () ], [ QLabel (), QLabel () ] ] self . inflAccelLabel = QLabel ( "Inflection accels" ) self . inflAccelValLabels = [ [ QLabel (), QLabel () ], [ QLabel (), QLabel () ] ] self . timePointLabel = QLabel ( "Point for time" ) self . timePointXLabel = QLabel () self . timePointYLabel = QLabel () self . timeDirLabel = QLabel ( "Velocity for time" ) self . timeDirXLabel = QLabel () self . timeDirYLabel = QLabel () self . timeAccelLabel = QLabel ( "Acceleration for time" ) self . timeAccelXLabel = QLabel () self . timeAccelYLabel = QLabel () self . otherFieldsGrid = QGridLayout () a = self . otherFieldsGrid . addWidget a ( self . inflectTimeLabel, 0, 0, 1, 2 ) a ( self . inflectTime1Label, 1, 0 ) a ( self . inflectTime2Label, 1, 1 ) a ( self . inflDirLabel, 2, 0, 1, 2 ) for i in range ( 2 ) : for j in range ( 2 ) : a ( self . inflDirValLabels [ i ] [ j ], 3 + i, j ) a ( self . inflAccelLabel, 5, 0, 1, 2 ) for i in range ( 2 ) : for j in range ( 2 ) : a ( self . inflAccelValLabels [ i ] [ j ], 6 + i, j ) a ( self . timePointLabel, 8, 0, 1, 2 ) a ( self . timePointXLabel, 9, 0 ) a ( self . timePointYLabel, 9, 1 ) a ( self . timeDirLabel, 10, 0, 1, 2 ) a ( self . timeDirXLabel, 11, 0 ) a ( self . timeDirYLabel, 11, 1 ) a ( self . timeAccelLabel, 12, 0, 1, 2 ) a ( self . timeAccelXLabel, 13, 0 ) a ( self . timeAccelYLabel, 13, 1 ) self . rhsLayout = QVBoxLayout () self . rhsLayout . addLayout ( self . xyFieldsGrid ) self . rhsLayout . addLayout ( self . otherFieldsGrid ) self . rhsLayout . addStretch () self . mainLayout = QHBoxLayout ( self ) self . mainLayout . addLayout ( self . lhsLayout ) self . mainLayout . addWidget ( self . bezierWidget ) self . mainLayout . addLayout ( self . rhsLayout ) QObject . connect ( self . timeSlider, SIGNAL ( "valueChanged ( int )" ), self . sliderMoved ) QObject . connect ( self . timeSpin, SIGNAL ( "valueChanged ( double )" ), self . spinChanged ) QObject . connect ( self . bezierWidget, SIGNAL ( "pointsModifiedInWidget ()" ), self . updateXYFields ) QObject . connect ( self . bezierWidget, SIGNAL ( "pointsModifiedInWidget ()" ), self . updateAnalysisFields ) self . updateXYFields () self . updateAnalysisFields () for x in self . children () : if isinstance ( x, QSpinBox ) : QObject . connect ( x, SIGNAL ( "valueChanged ( int )" ), self . updateBezierWidgetData ) QObject . connect ( x, SIGNAL ( "valueChanged ( int )" ), self . bezierWidget . repaint ) QObject . connect ( x, SIGNAL ( "valueChanged ( int )" ), self . updateAnalysisFields ) # note that updateAnalysisFields takes the data from the bezier widget so it should also come after updateBezierWidgetData def sliderMoved ( self, timeScaled ) : self . timeSpin . setValue ( timeScaled / 100 ) self . bezierWidget . bezTime = timeScaled / 100 self . bezierWidget . repaint () self . updateAnalysisFields () def spinChanged ( self, time ) : self . timeSlider . setValue ( time * 100 ) self . bezierWidget . bezTime = time self . bezierWidget . repaint () self . updateAnalysisFields () def updateXYFields ( self ) : bezierSpinMap = ( ( self . bezierWidget . p1, self . p1xSpin, self . p1ySpin ), ( self . bezierWidget . c1, self . c1xSpin, self . c1ySpin ), ( self . bezierWidget . c2, self . c2xSpin, self . c2ySpin ), ( self . bezierWidget . p2, self . p2xSpin, self . p2ySpin ) ) for mapitem in bezierSpinMap : mapitem [ 1 ] . setValue ( mapitem [ 0 ] . x () ) mapitem [ 2 ] . setValue ( mapitem [ 0 ] . y () ) def updateBezierWidgetData ( self ) : bw = self . bezierWidget bezierSpinMap = ( ( bw . p1, self . p1xSpin, self . p1ySpin ), ( bw . c1, self . c1xSpin, self . c1ySpin ), ( bw . c2, self . c2xSpin, self . c2ySpin ), ( bw . p2, self . p2xSpin, self . p2ySpin ) ) for mapitem in bezierSpinMap : mapitem [ 0 ] . setX ( mapitem [ 1 ] . value () ) mapitem [ 0 ] . setY ( mapitem [ 2 ] . value () ) bw . calcInflectionAndCusp () def updateAnalysisFields ( self ) : bw = self . bezierWidget if len ( bw . inflectionTimes ) == 0 : self . inflectTime1Label . setText ( "-" ) self . inflectTime2Label . setText ( "-" ) for row in self . inflDirValLabels : for x in row : x . setText ( "-" ) for row in self . inflAccelValLabels : for x in row : x . setText ( "-" ) elif len ( bw . inflectionTimes ) == 1 : self . inflectTime1Label . setText ( str_three_decimals ( bw . inflectionTimes [ 0 ] ) ) self . inflectTime2Label . setText ( "-" ) d = self . inflDirValLabels d [ 0 ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionDirs [ 0 ] . x () ) ) d [ 0 ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionDirs [ 0 ] . y () ) ) d [ 1 ] [ 0 ] . setText ( "-" ) d [ 1 ] [ 1 ] . setText ( "-" ) a = self . inflAccelValLabels a [ 0 ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionAccels [ 0 ] . x () ) ) a [ 0 ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionAccels [ 0 ] . y () ) ) a [ 1 ] [ 0 ] . setText ( "-" ) a [ 1 ] [ 1 ] . setText ( "-" ) elif len ( bw . inflectionTimes ) == 2 : self . inflectTime1Label . setText ( str_three_decimals ( bw . inflectionTimes [ 0 ] ) ) self . inflectTime2Label . setText ( str_three_decimals ( bw . inflectionTimes [ 1 ] ) ) for i in ( 0, 1 ) : self . inflDirValLabels [ i ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionDirs [ i ] . x () ) ) self . inflDirValLabels [ i ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionDirs [ i ] . y () ) ) self . inflAccelValLabels [ i ] [ 0 ] . setText ( str_three_decimals ( bw . inflectionAccels [ i ] . x () ) ) self . inflAccelValLabels [ i ] [ 1 ] . setText ( str_three_decimals ( bw . inflectionAccels [ i ] . y () ) ) for ( v, x, y ) in ( ( bw . timePoint, self . timePointXLabel, self . timePointYLabel ), ( bw . timeDir , self . timeDirXLabel , self . timeDirYLabel ), ( bw . timeAccel, self . timeAccelXLabel, self . timeAccelYLabel ) ) : x . setText ( str_three_decimals ( v . x () ) ) y . setText ( str_three_decimals ( v . y () ) ) app = QApplication ( [] ) mainWindow = MainWindow () mainWindow . show () app . exec_ ()
#! /usr/bin/env python3 from PyQt4 . QtCore import * from PyQt4 . QtGui import * class MainWindow ( QWidget ) : def __init__ ( self, parent = None ) : super ( MainWindow, self ) . __init__ ( parent ) self . slider = QSlider () self . slider . setMinimum ( 0 ) self . slider . setMaximum ( 100 ) self . slider . setValue ( 50 ) self . spin = QSpinBox () self . spin . setMinimum ( 0 ) self . spin . setMaximum ( 100 ) self . spin . setValue ( 50 ) self . dblSpin = QDoubleSpinBox () self . dblSpin . setDecimals ( 2 ) self . dblSpin . setMinimum ( 0 ) self . dblSpin . setMaximum ( 1 ) self . dblSpin . setSingleStep ( 0.01 ) self . dblSpin . setValue ( 0.5 ) self . layout = QVBoxLayout () self . layout . addWidget ( self . slider, 0, Qt . AlignHCenter ) self . layout . addWidget ( self . spin ) self . layout . addWidget ( self . dblSpin ) self . setLayout ( self . layout ) QObject . connect ( self . slider, SIGNAL ( "valueChanged ( int )" ), self . sliderMoved ) QObject . connect ( self . spin, SIGNAL ( "valueChanged ( int )" ), self . spinChanged ) QObject . connect ( self . dblSpin, SIGNAL ( "valueChanged ( double )" ), self . dblSpinChanged ) def sliderMoved ( self, scaledTime ) : self . spin . setValue ( scaledTime ) self . dblSpin . setValue ( scaledTime / 100 ) def spinChanged ( self, scaledTime ) : self . slider . setValue ( scaledTime ) self . dblSpin . setValue ( scaledTime / 100 ) def dblSpinChanged ( self, time ) : self . slider . setValue ( time * 100 ) self . spin . setValue ( time * 100 ) app = QApplication ( [] ) window = MainWindow () window . show () app . exec_ ()
_______________________________________________ PyQt mailing list PyQt@riverbankcomputing.com http://www.riverbankcomputing.com/mailman/listinfo/pyqt