Hello. Could anyone please look into the curious behaviour I reported last month as seen below? (I'll also ask this on the Qt Interest list as it concerns C++ as well.)
I've included the buggy PyQt program as well as a minimal test of a slider in C++ and PyQt. I note that in both the C++/PyQt minimal examples the bug is reproducible only when there is only a slider + a doubleSpin and not when there is also an integer spin. When there is an integer spin in addition, there is some lagging behind between the widgets (as can be seen visually as well as in the debug output) but it gets through and I'm able to run the whole range using the keyboard, but if there is no integer spin and only the double spin with the slider, the problem surfaces. As it is seen also in C++ I presume it is not a PyQt-only problem, but this is something probably other PyQt programmers also can face or would have faced. I'd like to know what to do to fix this. Thanks! Shriramana. On Mon, Oct 1, 2012 at 9:32 AM, Shriramana Sharma <samj...@gmail.com> wrote: > 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 -- 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_ ()
slider-minimal-test.tar.gz
Description: GNU Zip compressed data
_______________________________________________ PyQt mailing list PyQt@riverbankcomputing.com http://www.riverbankcomputing.com/mailman/listinfo/pyqt