Rotating graphic is changing sizes at random

Honestly, if I were going to do this, I would use a paintable canvas and draw the entire thing.

Here is an example from sometime ago that does something similar (although I don't think you need to deal with the mouse), perhaps it can provide some insight.

Maybe, I will throw something together later today if I get a chance.

3 Likes

That sounds like a sensible approach.

image
...and by the way, these things are beautiful. I'm definitely going to lose some of my time off this weekend finding uses for this type of slider.

I'm looking forward to seeing the timer component you come up with.

2 Likes

You might loose quite a bit of time fixing the bug I never got around to fixing. :rofl:

4 Likes

I like your solution. very elegant. I hope to try it out when I have a chance!

I gave up on dynamically changing the angle of the "hand" object. I've redone it with a simple bitmap of a line and dynamically changing the rotation of the image.

image

I'd still like to know the issue with the former. IRose, I like your solution, but this keeps it within the K.I.S.S. philosophy (my version: Keep It Simple Steve).

Well if you want it, here is what I threw together. You of course can add more customization if needed or wanted.

repaint script
from java.awt import Color
from java.awt import RadialGradientPaint
from java.awt import MultipleGradientPaint as mgp
from java.awt.geom import Rectangle2D
from java.awt.geom import Ellipse2D
from java.awt.geom import Line2D
from java.awt.geom import Point2D
from java.awt import BasicStroke
from java.awt.geom import Path2D
import math
g = event.graphics


#Watch Body

bodyOutline = Ellipse2D.Float(event.width * .075, event.height * .115, event.width * .85, event.height * .85)
bodyShape = Ellipse2D.Float(event.width * .1 ,event.height * .14 , event.width * .8, event.height * .8)
cX = bodyShape.getX() + bodyShape.getWidth() / 2
cY = bodyShape.getY() + bodyShape.getHeight() / 2
radius = bodyShape.width / 2

#Draw outside
g.setStroke(BasicStroke(1.0))
g.setPaint(Color.BLACK)
g.draw(bodyOutline)

center = Point2D.Float(cX,cY)
focus = Point2D.Float((radius - 10) * math.cos(math.pi / 4) + cX,(radius - 10) * math.sin(math.pi / 4) + cY)
colors = [Color.WHITE,Color.LIGHT_GRAY,Color.GRAY]
bodyPaint = RadialGradientPaint(center,300,focus, [0.0,0.8,1.0],colors,mgp.CycleMethod.NO_CYCLE)
g.setPaint(bodyPaint)
g.fill(bodyOutline)

#Draw Face
g.draw(bodyShape)
g.setPaint(event.source.background)
g.fill(bodyShape)
g.setPaint(Color.DARK_GRAY)
g.draw(bodyShape)

#Set Font
g.setFont(event.source.font)

#Tick Marks
theta = -math.pi/2
for tick in xrange(60):
	
	tickLength = event.source.MinorTickLength if tick % 5 else event.source.MajorTickLength
	#calculate the start and end points for the tick line
	x1 = radius * math.cos(theta) + cX
	y1 = radius * math.sin(theta) + cY
	x2 = (radius - tickLength) * math.cos(theta) + cX
	y2 = (radius - tickLength) * math.sin(theta) + cY
	
	
	tickLine = Line2D.Float(x1,y1,x2,y2)
	if tick % 5:
		g.setColor(event.source.MinorTickColor)
		g.setStroke(BasicStroke(event.source.MinorTickWidth))
	else:
		g.setColor(event.source.MajorTickColor)
		g.setStroke(BasicStroke(event.source.MajorTickWidth))
		
	g.draw(tickLine)
	
	#Draw the tick increment text
	if not tick % 5:
		
		
		fontMetrics = g.getFontMetrics()
		x2 = (radius - tickLength - (fontMetrics.getHeight() /2.0 + fontMetrics.getAscent()/5)) * math.cos(theta) + cX  - (fontMetrics.stringWidth(str(tick)) / 2.0)
		y2 = (radius - tickLength - (fontMetrics.getHeight() /2.0 + fontMetrics.getAscent()/5)) * math.sin(theta) + cY + 4
		g.setColor(event.source.foreground)
		#Uncomment if you would rather it say 60 than 0
		#if tick == 0:
		#	g.drawString("60",x2,y2)
		#else:
		#tab in the next line if you uncomment the above code.
		g.drawString(str(tick),x2,y2)
		
	#increment theta to next angle
	theta += math.radians(360/60)

#Draw the Set-Point hand
g.setPaint(Color.ORANGE)

spNeedle = Path2D.Float()
radPerTick = math.radians(360/60)
value = event.source.Setpoint

spNeedle.moveTo((radius-45) * math.cos(value * radPerTick - math.pi / 2) + cX,(radius-45) * math.sin(value * radPerTick - math.pi / 2) + cY)
spNeedle.lineTo(3 * math.cos(value * radPerTick - math.pi) + cX,3 *math.sin(value * radPerTick - math.pi) + cY)
spNeedle.curveTo(15 * math.cos(value * radPerTick - 5 * math.pi/4) + cX, 15 * math.sin(value * radPerTick - 5 * math.pi/4) + cY,15 * math.cos(value * radPerTick + math.pi/4) + cX,15 * math.sin(value * radPerTick + math.pi/4) + cY, 3 * math.cos(value * radPerTick) + cX, 3 * math.sin(value * radPerTick) + cY)
spNeedle.closePath()
g.fill(spNeedle)


#Draw the Value-Hand
value = event.source.Value
g.setPaint(Color.BLACK)
vNeedle = Path2D.Float()
vNeedle.moveTo(radius * math.cos(value * radPerTick - math.pi/2) + cX, radius * math.sin(value * radPerTick - math.pi/2) + cY)
vNeedle.lineTo(3 * math.cos(value * radPerTick - math.pi) + cX, 3 * math.sin(value * radPerTick - math.pi) + cY)
vNeedle.curveTo(15 * math.cos(value * radPerTick - 5 * math.pi/4) + cX, 15 * math.sin(value * radPerTick - 5 * math.pi/4) + cY, 15 * math.cos(value * radPerTick + math.pi/4) + cX, 15 * math.sin(value * radPerTick + math.pi/4) + cY, 3 * math.cos(value * radPerTick) + cX, 3 * math.sin(value * radPerTick) + cY)
vNeedle.closePath()
g.fill(vNeedle)

g.setPaint(event.source.SetpointColor)

#Draw Set Point Text
fm = g.getFontMetrics()
g.drawString(str(event.source.Setpoint),cX - fm.stringWidth(str(event.source.Setpoint)) / 2.0, cY + 50 )

#Draw Value Text
g.setPaint(event.source.ValueColor)
g.drawString(str(event.source.Value),cX - fm.stringWidth(str(event.source.Value)) / 2.0, cY + 65 )

stopwatch

8 Likes

I use rotating Vision objects frequently. I never have a problem like this when I use a style customizer to drive the rotation. You'd have 60 rows in your rotation table for this situation, but that should be OK.

1 Like

If you only care about integer values, sure that will work, but if you want any higher precision (which @steven.rehnborg seems to from the OP) and it balloons to unworkable. Just a single decimal place and you’re already at 600 entries.

I remember looking into something like this before while working on a different problem. I believe that 60 lines would work even with decimals if the goal is to rotate the hand in one second increments. Nevertheless, the paintable canvas approach is the one I would implement for this usage case.

1 Like

Actually, I don't need anything more precise than seconds. The main concern is minutes in process for the cycle, but fractions (seconds) are recorded. Didn't mean to imply I needed more accuracy.

I will try this out. I'm not familiar with the paintable canvas yet, so I'll be doing some study. Thanks all for the help! I think my issue has technically been resolved even if my question still remains for what happens to the objects in dynamic rotation.

1 Like

have you had any trouble with re-sizing?

No, I didn't have any unforeseen issues with re-sizing. The code doesn't account for changes in aspect ratio, so as long as the height and width are close to the same ratio, you should get decent results. If you get too far off you'll start to see some weird things, like the tick marks not being centered exactly. This is because they are drawn assuming a perfect circle. That can be fixed, I just didn't take the time to do it.

There are also some things which I hard coded based on the original size that I used (such as the position of the labels). The code doesn't account for repositioning those based on the height and width of the template. That is easily done though.

If for instance you change line 114 and 118 to something like this:

#line 114
g.drawString(str(event.source.Setpoint),cX-fm.stringWidth(str(event.source.Setpoint)) / 2.0, cY + event.height / 8)

#line 118
g.drawString(str(event.source.Value),cX - fm.stringWidth(str(event.source.Value)) / 2.0, cY + event.height / 6)

The position of the labels will stay relatively the same.

2 Likes

I was trying to hold aspect ratio, but when I actually checked, it was off a small amount. Changing it manually fixed the issue with the tickmarks. Your code fixed the value location, thanks!
I have one more issue, but I'm going to dig into the code to fix it myself. Gives me a reason to become more familiar with the coding of this.
Thanks!

Comparison shot:
image

1 Like

Something new with the paintable canvas for the timer. Getting this error below when page is opened. Once the Error box is closed, the alarm does not come back unless the page is opened again. Related to the clock setpoint hand because the error is on the line that draws the setpoint hand. An almost identical line for the process variable hand does not error.

spNeedle.moveTo((radius-20) * math.cos(value * radPerTick - math.pi / 2) + cX,(radius-20) * math.sin(value * radPerTick - math.pi / 2) + cY)

The component is paintable before its bindings have finished, and value is still null. You have to handle that case in your repaint event.

2 Likes

An interesting question would be how to handle it. Obviously, wrapping the whole thing in a quick and dirty try, except, pass would get rid of the exception, but that would also get rid of any other exception that could happen for whatever reason down the road. Wouldn't some sort of initial binding verification be better? ...and if so, what is the best practice for accomplishing this?

Just change the value assignment lines to something like this and it will eliviate your issue.

value = event.source.Setpoint if event.source.Setpoint else 0

You might see a slight flash of the needle being drawn at 0 before the binding completes.

1 Like

This makes sense. A null value will evaluate as false and set the hand to the zero [starting] position.

I would test it at the beginning and not paint at all if null.

1 Like
if event.source.Setpoint:
     #entire repaint script