[prev in list] [next in list] [prev in thread] [next in thread]
List: pykde
Subject: Re: [PyQt] Curious behaviour of slider/spin when controlled by keyboard
From: Shriramana Sharma <samjnaa () gmail ! com>
Date: 2012-11-15 17:53:27
Message-ID: CAH-HCWVScpCX0S4FLyKGimnccbmUw-fAdSUuJt=_jik3ogxYPQ () mail ! gmail ! com
[Download RAW message or body]
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 <samjnaa@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
["bezierview.py" (text/x-python)]
#! /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" (application/x-gzip)]
_______________________________________________
PyQt mailing list PyQt@riverbankcomputing.com
http://www.riverbankcomputing.com/mailman/listinfo/pyqt
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic