Qt: hide widget with ToolTip flag when window is not visible - qt

I'm creating a video player, below is an MRE implementation of the VideoWidget and a widget to display the video controls, called ControlBar:
import os
from PySide6 import QtMultimedia as qtm
from PySide6 import QtMultimediaWidgets as qtmw
from PySide6 import QtWidgets as qtw
from PySide6 import QtCore as qtc
from PySide6 import QtGui as qtg
class VideoWidget(qtmw.QVideoWidget):
load_finished_signal = qtc.Signal()
_playback_state_changed_signal = qtc.Signal()
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.setFocusPolicy(qtc.Qt.FocusPolicy.NoFocus)
self.setMouseTracking(True)
# In Qt6, a private internal class is used for rendering the video, so
# add `vw.children()[0].setMouseTracking(True)`
self.children()[0].setMouseTracking(True)
self._seeking = False
self._audio_output = qtm.QAudioOutput(qtm.QMediaDevices.defaultAudioOutput())
self._media_player = qtm.QMediaPlayer(self)
self._control_bar = ControlBar(self)
self._hide_control_bar_timer = qtc.QTimer()
self._media_player.setVideoOutput(self)
self._media_player.setAudioOutput(self._audio_output)
self._media_player.mediaStatusChanged.connect(self._video_loaded)
self._media_player.durationChanged.connect(
self._control_bar.update_total_duration
)
self._media_player.positionChanged.connect(self._control_bar.update_progress)
self.videoSink().videoFrameChanged.connect(self._seeked)
self._control_bar.seek_requested_signal.connect(self._seek)
self._control_bar.pause_play_requested_signal.connect(self._change_playback_state)
self._playback_state_changed_signal.connect(self._control_bar.playback_state_changed)
self._hide_control_bar_timer.setSingleShot(True)
self._hide_control_bar_timer.setInterval(1000)
self._hide_control_bar_timer.timeout.connect(self._hide_control_bar)
def _hide_control_bar(self) -> None:
if not self._control_bar.underMouse():
self._control_bar.setVisible(False)
cursor = self.cursor()
cursor.setShape(qtc.Qt.CursorShape.BlankCursor)
self.setCursor(cursor)
def load(self, location: str) -> None:
self._media_player.setSource(qtc.QUrl.fromLocalFile(location))
def _video_loaded(self, location: str) -> None:
self._control_bar.set_filename(
os.path.basename(self._media_player.source().toString())
)
if self._media_player.source():
self._media_player.play()
self.load_finished_signal.emit()
def close_item(self) -> None:
self._media_player.stop()
self._media_player.setSource(qtc.QUrl())
def _screenshot(self, video_frame: qtm.QVideoFrame):
image = video_frame.toImage()
image.save(filename)
def _update_conrol_bar_position(self) -> None:
geometry = self.geometry()
top_left_global = geometry.topLeft()
self._control_bar.update_geometry_to_width(self.width())
self._control_bar.move(
top_left_global.x(),
top_left_global.y() + geometry.height() - self._control_bar.height(),
)
def _change_playback_state(self) -> None:
if (
self._media_player.playbackState()
== qtm.QMediaPlayer.PlaybackState.PlayingState
):
self._media_player.pause()
elif (
self._media_player.playbackState()
== qtm.QMediaPlayer.PlaybackState.PausedState
or self._media_player.playbackState()
== qtm.QMediaPlayer.PlaybackState.StoppedState
):
self._media_player.play()
self._playback_state_changed_signal.emit()
def mouseMoveEvent(self, event: qtg.QMouseEvent) -> None:
if not self.cursor().shape() & qtc.Qt.CursorShape.ArrowCursor:
cursor = self.cursor()
cursor.setShape(qtc.Qt.CursorShape.ArrowCursor)
self.setCursor(cursor)
if (
not self.width() == self._control_bar.width()
or not self.geometry().contains(self._control_bar.geometry())
):
self._update_conrol_bar_position()
if not self._control_bar.isVisible():
self._control_bar.setVisible(True)
self._hide_control_bar_timer.start()
def mousePressEvent(self, event: qtg.QMouseEvent) -> None:
self._change_playback_state()
event.accept()
return
def mouseDoubleClickEvent(self, event: qtg.QMouseEvent) -> None:
F_event = qtg.QKeyEvent(
qtc.QEvent.KeyPress,
qtc.Qt.Key.Key_F,
qtc.Qt.KeyboardModifier.NoModifier,
"F",
)
qtc.QCoreApplication.sendEvent(self, F_event)
super().mouseDoubleClickEvent(event)
def resizeEvent(self, event: qtg.QResizeEvent) -> None:
super().resizeEvent(event)
self._update_conrol_bar_position()
def contextMenuEvent(self, event: qtg.QContextMenuEvent):
""""""
def keyPressEvent(self, event: qtg.QKeyEvent) -> None:
key = event.key()
if (
key == qtc.Qt.Key.Key_Left
or key == qtc.Qt.Key.Key_Right
or key == qtc.Qt.Key.Key_Less
or key == qtc.Qt.Key.Key_Greater
):
factor = (
1
if event.modifiers() == qtc.Qt.KeyboardModifier.ShiftModifier
else 60
if event.modifiers() == qtc.Qt.KeyboardModifier.ControlModifier
else 5
)
if key == qtc.Qt.Key.Key_Left:
self._seek(max(self._media_player.position() - (1000 * factor), 0))
elif key == qtc.Qt.Key.Key_Right:
self._seek(
min(
self._media_player.position() + (1000 * factor),
self._media_player.duration(),
)
)
elif key == qtc.Qt.Key.Key_F5:
self._screenshot(self.videoSink().videoFrame())
elif key == qtc.Qt.Key.Key_Space:
self._change_playback_state()
elif key == qtc.Qt.Key.Key_Down:
self._media_player.audioOutput().setVolume(
max(self._media_player.audioOutput().volume() - 0.1, 0)
)
elif key == qtc.Qt.Key.Key_Up:
self._media_player.audioOutput().setVolume(
min(self._media_player.audioOutput().volume() + 0.1, 1)
)
elif key == qtc.Qt.Key.Key_M:
self._media_player.audioOutput().setMuted(
not self._media_player.audioOutput().isMuted()
)
else:
super().keyPressEvent(event)
return None
event.accept()
return None
"""
- next in directory
- previous in directory
"""
def _seek(self, position: int) -> None:
if self._seeking:
return
self._seeking = True
if self._media_player.isSeekable():
self._media_player.setPosition(position)
else:
self._seeked()
def _seeked(self):
self._seeking = False
def wheelEvent(self, event: qtg.QWheelEvent) -> None:
"volume"
class ControlBar(qtw.QWidget):
seek_requested_signal = qtc.Signal(int)
pause_play_requested_signal = qtc.Signal()
_WRT_HEIGHT = 100
_MAX_FONT_POINT_F = 20
_WRT_WIDTH = 1920 / 2
# Height should be _WRT_HEIGHT if width is _WRT_WIDTH.
_CONTROL_BAR_WIDTH_TO_HEIGHT_RATIO = _WRT_HEIGHT / _WRT_WIDTH
_FONT_POINT_F_RATIO = _MAX_FONT_POINT_F / _WRT_HEIGHT
_WIDGETS_SIZE_RATIOS = {
"filename": (_WRT_WIDTH / _WRT_WIDTH, 40 / _WRT_HEIGHT),
"pause_play": (30 / _WRT_WIDTH, 35 / _WRT_HEIGHT),
"pause": (10 / _WRT_WIDTH, 35 / _WRT_HEIGHT),
"play": (25 / _WRT_WIDTH, 35 / _WRT_HEIGHT),
"left_duration": (0 / _WRT_WIDTH, 40 / _WRT_HEIGHT),
"right_duration": (0 / _WRT_WIDTH, 40 / _WRT_HEIGHT),
"total_progress_bar": (578 / _WRT_WIDTH, 40 / _WRT_HEIGHT),
"completed_progress_bar": (0 / _WRT_WIDTH, 40 / _WRT_HEIGHT),
}
_PADDINGS_RATIOS = {"v_pad": 5 / _WRT_HEIGHT, "h_pad": 10 / _WRT_WIDTH}
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.setWindowFlag(qtc.Qt.WindowType.ToolTip)
self._total_duration_ms = 0
self._completed_duration_ms = 0
self._widget_shapes = {
"filename_rect": qtc.QRect(),
"pause_play_button_rect": qtc.QRect(),
"pause_path": qtg.QPainterPath(),
"play_triangle": qtg.QPolygon(),
"left_duration_rect": qtc.QRect(),
"progress_bar_rect": qtc.QRect(),
"right_duration_rect": qtc.QRect(),
}
self._show_milliseconds = False
self._filename = ""
self._paused = False
def playback_state_changed(self) -> None:
self._paused = not self._paused
def paintEvent(self, event: qtg.QPaintEvent) -> None:
painter = qtg.QPainter(self)
painter.setRenderHint(qtg.QPainter.RenderHint.Antialiasing)
painter.fillRect(0, 0, self.width(), self.height(), qtc.Qt.GlobalColor.black)
pen = qtg.QPen()
pen.setColor(qtc.Qt.GlobalColor.white)
painter.setPen(pen)
painter.setFont(self._get_font())
painter.drawText(
self._widget_shapes["filename_rect"],
qtc.Qt.AlignmentFlag.AlignLeft,
self._filename,
)
if self._paused:
path = qtg.QPainterPath()
path.addPolygon(self._widget_shapes["play_triangle"])
painter.fillPath(path, qtc.Qt.GlobalColor.white)
else:
painter.fillPath(self._widget_shapes["pause_path"], qtc.Qt.GlobalColor.white)
painter.drawText(
self._widget_shapes["left_duration_rect"],
qtc.Qt.AlignmentFlag.AlignCenter,
_format_milliseconds(self._completed_duration_ms, self._show_milliseconds),
)
painter.fillRect(
self._widget_shapes["progress_bar_rect"], qtc.Qt.GlobalColor.gray
)
painter.fillRect(
self._get_completed_progress_bar_rect(), qtc.Qt.GlobalColor.white
)
painter.drawText(
self._widget_shapes["right_duration_rect"],
qtc.Qt.AlignmentFlag.AlignCenter,
_format_milliseconds(self._total_duration_ms, self._show_milliseconds),
)
def set_filename(self, filename: str) -> None:
self._filename = filename
def update_total_duration(self, duration: int) -> None:
self._total_duration_ms = duration
self.update()
def update_progress(self, progress: int) -> None:
self._completed_duration_ms = progress
self.update()
def update_geometry_to_width(self, width: int) -> None:
new_control_bar_height = int(self._CONTROL_BAR_WIDTH_TO_HEIGHT_RATIO * width)
self.setFixedSize(width, min(new_control_bar_height, self._WRT_HEIGHT))
self._update_widgets_geometry()
def _update_widgets_geometry(self) -> None:
total_height = 0
row_1_total_width = 0
row_2_total_width = 0
total_height += self.height() * self._PADDINGS_RATIOS["v_pad"]
row_1_total_width += self.width() * self._PADDINGS_RATIOS["h_pad"]
row_2_total_width += self.width() * self._PADDINGS_RATIOS["h_pad"]
font_metric = qtg.QFontMetrics(self._get_font())
horizontal_advance = font_metric.horizontalAdvance("A")
self._widget_shapes["filename_rect"].setX(row_1_total_width)
self._widget_shapes["filename_rect"].setY(total_height)
self._widget_shapes["filename_rect"].setWidth(
horizontal_advance * len(self._filename)
)
self._widget_shapes["filename_rect"].setHeight(
self.height() * self._WIDGETS_SIZE_RATIOS["filename"][1]
)
total_height += self._widget_shapes["filename_rect"].height()
row_1_total_width += self._widget_shapes["filename_rect"].width()
total_height += self.height() * self._PADDINGS_RATIOS["v_pad"]
row_1_total_width += self.width() * self._PADDINGS_RATIOS["h_pad"]
self._widget_shapes["pause_play_button_rect"].setX(row_2_total_width)
self._widget_shapes["pause_play_button_rect"].setY(total_height)
self._widget_shapes["pause_play_button_rect"].setWidth(
min(30, self.width() * self._WIDGETS_SIZE_RATIOS["pause_play"][0])
)
self._widget_shapes["pause_play_button_rect"].setHeight(
self.height() * self._WIDGETS_SIZE_RATIOS["pause_play"][1]
)
self._widget_shapes["pause_path"] = qtg.QPainterPath()
pause_1 = qtc.QRect(
self._widget_shapes["pause_play_button_rect"].x(),
self._widget_shapes["pause_play_button_rect"].y(),
min(10, self.width() * self._WIDGETS_SIZE_RATIOS["pause"][0]),
self.height() * self._WIDGETS_SIZE_RATIOS["pause"][1],
)
self._widget_shapes["pause_path"].addRect(pause_1)
pause_2 = qtc.QRect(
self._widget_shapes["pause_play_button_rect"].topRight().x(),
self._widget_shapes["pause_play_button_rect"].y(),
-min(10, self.width() * self._WIDGETS_SIZE_RATIOS["pause"][0]),
self.height() * self._WIDGETS_SIZE_RATIOS["pause"][1],
)
self._widget_shapes["pause_path"].addRect(pause_2)
self._widget_shapes["play_triangle"] = qtg.QPolygon()
right = self._widget_shapes["pause_play_button_rect"].center()
right.setX(self._widget_shapes["pause_play_button_rect"].right())
self._widget_shapes["play_triangle"].append(
[
self._widget_shapes["pause_play_button_rect"].topLeft(),
self._widget_shapes["pause_play_button_rect"].bottomLeft(),
right,
]
)
row_2_total_width += self._widget_shapes["pause_play_button_rect"].width()
row_2_total_width += self.width() * self._PADDINGS_RATIOS["h_pad"]
self._widget_shapes["left_duration_rect"].setX(row_2_total_width)
self._widget_shapes["left_duration_rect"].setY(total_height)
self._widget_shapes["left_duration_rect"].setWidth(
horizontal_advance
* len(
_format_milliseconds(
self._completed_duration_ms, self._show_milliseconds
)
)
)
self._widget_shapes["left_duration_rect"].setHeight(
self.height() * self._WIDGETS_SIZE_RATIOS["left_duration"][1]
)
row_2_total_width += self._widget_shapes["left_duration_rect"].width()
row_2_total_width += self.width() * self._PADDINGS_RATIOS["h_pad"]
self._widget_shapes["progress_bar_rect"].setX(row_2_total_width)
self._widget_shapes["progress_bar_rect"].setY(total_height)
self._widget_shapes["progress_bar_rect"].setWidth(
self.width() * self._WIDGETS_SIZE_RATIOS["total_progress_bar"][0]
)
self._widget_shapes["progress_bar_rect"].setHeight(
self.height() * self._WIDGETS_SIZE_RATIOS["total_progress_bar"][1]
)
row_2_total_width += self._widget_shapes["progress_bar_rect"].width()
row_2_total_width += self.width() * self._PADDINGS_RATIOS["h_pad"]
# To adjust the size of the progress bar according to the remaining width
# left after adding all remaining widget's widths.
theoritical_width = row_2_total_width
theoritical_width += horizontal_advance * len(
_format_milliseconds(self._total_duration_ms, self._show_milliseconds)
)
theoritical_width += self.width() * self._PADDINGS_RATIOS["h_pad"]
remaining_width = self.width() - theoritical_width
self._widget_shapes["progress_bar_rect"].setWidth(
self._widget_shapes["progress_bar_rect"].width() + remaining_width
)
row_2_total_width += remaining_width
self._widget_shapes["right_duration_rect"].setX(row_2_total_width)
self._widget_shapes["right_duration_rect"].setY(total_height)
self._widget_shapes["right_duration_rect"].setWidth(
horizontal_advance
* len(
_format_milliseconds(self._total_duration_ms, self._show_milliseconds)
)
)
self._widget_shapes["right_duration_rect"].setHeight(
self.height() * self._WIDGETS_SIZE_RATIOS["right_duration"][1]
)
row_2_total_width += self._widget_shapes["right_duration_rect"].width()
row_2_total_width += self.width() * self._PADDINGS_RATIOS["h_pad"]
def _get_font(self) -> qtg.QFont:
font = qtg.QFont()
font.setPointSizeF(self.height() * self._FONT_POINT_F_RATIO)
return font
def _get_completed_progress_bar_rect(self) -> qtc.QRect:
completed_width = int(
(self._completed_duration_ms / (self._total_duration_ms or 1))
* self._widget_shapes["progress_bar_rect"].width()
)
completed_rect = qtc.QRect(self._widget_shapes["progress_bar_rect"])
completed_rect.setWidth(completed_width)
return completed_rect
def _get_time_from_mouse_press(self, point: qtc.QPoint) -> None:
return int(
(point.x() / self._widget_shapes["progress_bar_rect"].width())
* self._total_duration_ms
)
def mousePressEvent(self, event: qtg.QMouseEvent) -> None:
point = event.pos()
if self._widget_shapes["left_duration_rect"].contains(point):
self._show_milliseconds = not self._show_milliseconds
self._update_widgets_geometry()
elif self._widget_shapes["progress_bar_rect"].contains(point):
self.seek_requested_signal.emit(
self._get_time_from_mouse_press(
point - self._widget_shapes["progress_bar_rect"].topLeft()
)
)
elif self._widget_shapes["pause_play_button_rect"].contains(point):
self.pause_play_requested_signal.emit()
self.update()
event.accept()
return
def mouseMoveEvent(self, event: qtg.QMouseEvent) -> None:
if self._widget_shapes["progress_bar_rect"].contains(event.pos()):
self.seek_requested_signal.emit(
self._get_time_from_mouse_press(
event.pos() - self._widget_shapes["progress_bar_rect"].topLeft()
)
)
event.accept()
return
def _format_milliseconds(milliseconds: int, show_milliseconds: bool = False) -> str:
seconds, milliseconds = divmod(milliseconds, 1000)
minutes, seconds = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
return f"{hours:02}:{minutes:02}:{seconds:02}" + (
f".{milliseconds:03}" if show_milliseconds else ""
)
app = qtw.QApplication()
vw = VideoWidget()
vw.show()
vw.load("video_file")
app.exec()
While this works as expected for the most part, if I alt-tab into another window (effectively hiding the program) with the ControlBar displayed, the bar is visible on top of the other window as well. I'm guessing such is the nature of the Qt.ToolTip window flag. How do I have it "show" or "hide" with the main program?
Also, the _update_widgets_geometry() function's implementation is rather tedious, is there a better way to do this? "This" being the way I layout (set the QRects for) the shapes in the ControlBar.
EDIT:
The code should work "out of the box", just pass a valid video file path to the vw.load() at the end of the code. A screenshot.

Related

(Godot Engine) Compute string as PEMDAS

I've been trying to create a function in GDScript to process and calculate a string using PEMDAS rules. Below is my try on the subject. It can so far only use the MDAS rules:
Is there a better way to achieve such a function?
func _ready() -> void:
### USE CASES ###
print(Compute_String("1+2*3+3=")) # Output = 10
print(Compute_String("1+2*3*3=")) # Output = 19
print(Compute_String("1*2*3+3=")) # Output = 9
print(Compute_String("1+2+3*3=")) # Output = 12
print(Compute_String("5*2+7-3/2=")) # Output = 15.5
print(Compute_String("9+5.5*2.25=")) # Output = 21.375
print(Compute_String("5*2+7-3/2")) # Output = 1.#QNAN (Missing equals)
print(Compute_String("5*2+7-/2=")) # Output = 1.#QNAN (Adjacent operators)
print(Compute_String("*2+7-3/2=")) # Output = 1.#QNAN (Begins with operator)
print(Compute_String("")) # Output = 1.#QNAN (Empty)
print(Compute_String("=")) # Output = 1.#QNAN (Considered as empty)
print(Compute_String("1 +2=")) # Output = 1.#QNAN (Contains space)
print(Compute_String("(1+2)*3=")) # Output = 1.#QNAN (Parentheses not supported)
func Compute_String(_string: String) -> float:
var _result: float = NAN
var _elements: Array = []
if not _string.empty() and _string[_string.length() - 1] == "=":
var _current_element: String = ""
for _count in _string.length():
if _string[_count].is_valid_float() or _string[_count] == ".": _current_element += _string[_count]
else:
if _string[_count - 1].is_valid_float() and (_string[_count + 1].is_valid_float() if _string[_count] != "=" else true):
_elements.append_array([_current_element,_string[_count]]) ; _current_element = ""
else: return NAN
if not _elements.empty():
_elements.resize(_elements.size() - 1)
while _get_operators_count(_elements) != 0:
var _id: Array = [0, 0.0, 0.0]
if "*" in _elements:
_id = _add_adjacent(_elements, "*") ; _remove_adjacent(_elements, _id[0]) ; _elements.insert(_id[0] - 1, _id[1] * _id[2])
elif "/" in _elements:
_id = _add_adjacent(_elements, "/") ; _remove_adjacent(_elements, _id[0]) ; _elements.insert(_id[0] - 1, _id[1] / _id[2])
elif "+" in _elements:
_id = _add_adjacent(_elements, "+") ; _remove_adjacent(_elements, _id[0]) ; _elements.insert(_id[0] - 1, _id[1] + _id[2])
elif "-" in _elements:
_id = _add_adjacent(_elements, "-") ; _remove_adjacent(_elements, _id[0]) ; _elements.insert(_id[0] - 1, _id[1] - _id[2])
else: return NAN
if _elements.size() == 1: _result = _elements[0]
return _result
func _get_operators_count(_elements: Array) -> int:
var _result: int = 0 ; for _element in _elements: if not str(_element).is_valid_float(): _result += 1 ; return _result
func _add_adjacent(_elements: Array, _operator) -> Array:
return [_elements.find(_operator), float(_elements[_elements.find(_operator) - 1]), float(_elements[_elements.find(_operator) + 1])]
func _remove_adjacent(_elements: Array, _operator_idx: int) -> void:
_elements.remove(_operator_idx + 1) ; _elements.remove(_operator_idx) ; _elements.remove(_operator_idx - 1)

Seems impossible to rotate custom QGraphicsItem using QPainter (or any other method.)

Here is the code. It runs. To exhibit this bug. Right-click the ellipse, scale it by click-dragging on the ellipse. Right-click it > "Done editing". Then do the same thing with "Rotate."
I've tried over 10 different permutations of using self.setRotation, self.setTransform, painter.rotate, and so on... The only time it did rotate was when I did self.setTransform(self.transform().rotate(r)) but the result was wrong: scale & rotate in the wrong order or something.
from PyQt5.QtWidgets import QGraphicsItem, QMenu
from PyQt5.QtGui import QTransform, QPen, QPainter, QColor, QBrush
from PyQt5.QtCore import Qt, QPointF, QRectF, QEvent
from math import sqrt
def scaleRect(rect, scale_x, scale_y):
T = QTransform.fromScale(scale_x, scale_y)
return T.mapRect(rect)
def debugPrintTransformMatrix(T):
print(str(T.m11()) + ' ' + str(T.m12()) + ' ' + str(T.m13()))
print(str(T.m21()) + ' ' + str(T.m22()) + ' ' + str(T.m23()))
print(str(T.m31()) + ' ' + str(T.m32()) + ' ' + str(T.m33()))
# Assumes no shearing or stretching.
# Only Rotation, Translation, and Scaling.
def extractTransformScale(T):
# This is math matrix notation transposed (debug print self.sceneTransform() to see)
Sx = sqrt(T.m11()**2 + T.m12()**2)
Sy = sqrt(T.m21()**2 + T.m22()**2)
return Sx, Sy
def extractTransformTranslate(T):
return T.m31(), T.m32()
class Object(QGraphicsItem):
def sceneEvent(self, event):
if event.type() == QEvent.GraphicsSceneMouseMove:
# move, scale, or rotate
if self._mode in ['scale', 'rotate']:
mouse_pos = event.scenePos()
last_pos = event.lastScenePos()
if self._mode == 'scale':
s = self.mouseScalingFactors(mouse_pos, last_pos)
self.setTransform(self.transform().scale(*s))
if self._mode == 'rotate':
r = self.mouseRotationAngle(mouse_pos, last_pos)
self.setRotation(self.rotation() + r)
return True
return super().sceneEvent(event)
def __init__(self):
super().__init__()
self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsSelectable)
self._selectionPen = QPen(Qt.black, 1.0, style=Qt.DotLine, cap=Qt.FlatCap)
self._lastPos = QPointF(0, 0)
self.setPos(self._lastPos)
self._mode = 'neutral'
def setRenderHints(self, painter):
painter.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing)
def boundingRectExtraScale(self):
return (1.2 , 1.2)
def mouseScalingFactors(self, pos, last_pos):
delta = pos - last_pos
return (1.01 ** delta.x(), 1.01 ** delta.y())
def mouseRotationAngle(self, pos, last_pos):
return 1 #TODO
def createDefaultContextMenu(self):
menu = QMenu()
if self._mode == 'neutral':
menu.addAction('Scale').triggered.connect(lambda: self.setMode('scale'))
menu.addAction('Rotate').triggered.connect(lambda: self.setMode('rotate'))
else:
menu.addAction('Done Editing').triggered.connect(lambda: self.setMode('neutral'))
return menu
def contextMenuEvent(self, event):
menu = self.createDefaultContextMenu()
menu.exec(event.screenPos())
def setMode(self, mode):
self._mode = mode
def setSelected(self, selected):
super().setSelected(selected)
self.update()
class ShapedObject(Object):
def __init__(self):
super().__init__()
self._shape = {
'name' : 'ellipse',
'radius': 35
}
self._brush = QBrush(Qt.darkGreen)
self._pen = QPen(Qt.yellow, 3)
def shapeDef(self):
return self._shape
def boundingRect(self):
rect = self.shapeRect()
s = self.boundingRectExtraScale()
return scaleRect(rect, *s)
def shape(self): #TODO QPainterPath shape for collision detection
# Should call self.boundingRectExtraScale()
return super().shape()
def paint(self, painter, option, widget):
self.setRenderHints(painter)
#super().paint(painter, option, widget)
shape = self.shapeDef()
name = shape['name']
painter.setBrush(self._brush)
painter.setPen(self._pen)
painter.save()
# ********** HERE IS THE PROBLEM *************
debugPrintTransformMatrix(painter.transform())
painter.rotate(5)
debugPrintTransformMatrix(painter.transform())
rect = self.shapeRect()
if name == 'ellipse':
painter.drawEllipse(rect)
painter.restore()
def shapeRect(self):
shape = self.shapeDef()
name = shape['name']
if name == 'ellipse':
r = shape['radius']
rect = QRectF(-r, -r, 2*r, 2*r)
return rect
####
import sys
from PyQt5.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsView, QApplication
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QMainWindow()
view = QGraphicsView()
scene = QGraphicsScene()
view.setScene(scene)
window.setCentralWidget(view)
ellipse = ShapedObject()
scene.addItem(ellipse)
window.show()
sys.exit(app.exec_())
Got it to work. It was the order of the operations rotate / scale. Never use an circle to test your rotation code, lol!
from PyQt5.QtWidgets import QGraphicsItem, QMenu, QGraphicsRotation, QGraphicsScale
from PyQt5.QtGui import QTransform, QPen, QPainter, QColor, QBrush, QPainterPath
from PyQt5.QtCore import Qt, QPointF, QRectF, QEvent
from math import sqrt
def scaleRect(rect, scale_x, scale_y):
T = QTransform.fromScale(scale_x, scale_y)
return T.mapRect(rect)
def debugPrintTransformMatrix(T):
print(str(T.m11()) + ' ' + str(T.m12()) + ' ' + str(T.m13()))
print(str(T.m21()) + ' ' + str(T.m22()) + ' ' + str(T.m23()))
print(str(T.m31()) + ' ' + str(T.m32()) + ' ' + str(T.m33()))
# Assumes no shearing or stretching.
# Only Rotation, Translation, and Scaling.
def extractTransformScale(T):
# This is math matrix notation transposed (debug print self.sceneTransform() to see)
Sx = sqrt(T.m11()**2 + T.m12()**2)
Sy = sqrt(T.m21()**2 + T.m22()**2)
return Sx, Sy
def extractTransformTranslate(T):
return T.m31(), T.m32()
class Object(QGraphicsItem):
def sceneEvent(self, event):
if event.type() == QEvent.GraphicsSceneMouseMove:
# move, scale, or rotate
if self._mode in ['scale', 'rotate']:
mouse_pos = event.scenePos()
last_pos = event.lastScenePos()
if self._mode == 'scale':
s = self.mouseScalingFactors(mouse_pos, last_pos)
self.applyScaleTransform(*s)
if self._mode == 'rotate':
r = self.mouseRotationAngle(mouse_pos, last_pos)
self.applyRotateTransform(r)
return True
return super().sceneEvent(event)
def __init__(self):
super().__init__()
self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsSelectable)
self._selectionPen = QPen(Qt.black, 1.0, style=Qt.DotLine, cap=Qt.FlatCap)
self._lastPos = QPointF(0, 0)
self.setPos(self._lastPos)
self._mode = 'neutral'
self._scale = QGraphicsScale()
self._rotate = QGraphicsRotation()
self.setTransformations([self._rotate, self._scale])
def applyRotateTransform(self, angle):
self._rotate.setAngle(self._rotate.angle() + 15)
def applyScaleTransform(self, sx, sy):
self._scale.setXScale(sx * self._scale.xScale())
self._scale.setYScale(sy * self._scale.yScale())
def setRenderHints(self, painter):
painter.setRenderHints(QPainter.SmoothPixmapTransform | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing)
def boundingRectExtraScale(self):
return (1.2 , 1.2)
def mouseScalingFactors(self, pos, last_pos):
delta = pos - last_pos
return (1.01 ** delta.x(), 1.01 ** delta.y())
def mouseRotationAngle(self, pos, last_pos):
return 1 #TODO
def createDefaultContextMenu(self):
menu = QMenu()
if self._mode == 'neutral':
menu.addAction('Scale').triggered.connect(lambda: self.setMode('scale'))
menu.addAction('Rotate').triggered.connect(lambda: self.setMode('rotate'))
else:
menu.addAction('Done Editing').triggered.connect(lambda: self.setMode('neutral'))
return menu
def contextMenuEvent(self, event):
menu = self.createDefaultContextMenu()
menu.exec(event.screenPos())
def setMode(self, mode):
self._mode = mode
def setSelected(self, selected):
super().setSelected(selected)
self.update()
class ShapedObject(Object):
def __init__(self):
super().__init__()
self._shape = {
'name' : 'ellipse',
'radius': 35
}
self._brush = QBrush(Qt.darkGreen)
self._pen = QPen(Qt.yellow, 3)
def shapeDef(self):
return self._shape
def boundingRect(self):
rect = self.shapeRect()
s = self.boundingRectExtraScale()
return scaleRect(rect, *s)
def shape(self): #TODO QPainterPath shape for collision detection
# Should call self.boundingRectExtraScale()
return super().shape()
def paint(self, painter, option, widget):
self.setRenderHints(painter)
#super().paint(painter, option, widget)
shape = self.shapeDef()
name = shape['name']
painter.setBrush(self._brush)
painter.setPen(self._pen)
painter.save()
path = QPainterPath()
if name == 'ellipse':
r = shape['radius']
path.addEllipse(QPointF(0, 0), r, r)
painter.drawPath(path)
painter.restore()
def shapeRect(self):
shape = self.shapeDef()
name = shape['name']
if name == 'ellipse':
r = shape['radius']
rect = QRectF(-r, -r, 2*r, 2*r)
return rect
####
import sys
from PyQt5.QtWidgets import QMainWindow, QGraphicsScene, QGraphicsView, QApplication
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QMainWindow()
view = QGraphicsView()
scene = QGraphicsScene()
view.setScene(scene)
window.setCentralWidget(view)
ellipse = ShapedObject()
scene.addItem(ellipse)
window.show()
sys.exit(app.exec_())

kivy remove_widget not removing

I'm learning about kivy and was playing around with the PONG example. I added a button that appears after a player reaches a given score.
if self.player2.score ==2 or self.player1.score==2:
self.serve_ball(vel=(0, 0))
self.btn= Button()
self.add_widget(self.btn)
self.btn.bind(on_press=self.callback)
It binds to a function callback that resets the score and messages but I've tried several things to remove the button after pressed with no success.
def callback(self, instance):
self.player2.score=0
self.player1.score=0
self.message1=""
self.message2=""
self.serve_ball(vel=(4,0))
self.remove_widget(self.btn)
Any ideas how? Thanks, this is the complete code
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.properties import StringProperty, NumericProperty, ReferenceListProperty, ObjectProperty
from kivy.vector import Vector
from kivy.clock import Clock
from random import randint
class PongPaddle(Widget):
score = NumericProperty(0)
def bounce_ball(self, ball):
if self.collide_widget(ball):
vx, vy = ball.velocity
offset = (ball.center_y - self.center_y) / (self.height / 2)
bounced = Vector(-1 * vx, vy)
vel = bounced * 1.1
ball.velocity = vel.x, vel.y + offset
class PongBall(Widget):
#Vel of ball in x & y axis
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(0)
#referencia para usar como vector
velocity = ReferenceListProperty(velocity_x,velocity_y)
#funcion move 1 paso en intervalos para animar la bola
def move(self):
self.pos = Vector(*self.velocity) + self.pos
class PongGame(Widget):
ball = ObjectProperty(None)
player1 = ObjectProperty(None)
player2 = ObjectProperty(None)
message2 = StringProperty("")
message1 = StringProperty("")
def callback(self, instance):
self.player2.score=0
self.player1.score=0
self.message1=""
self.message2=""
self.serve_ball(vel=(4,0))
self.remove_widget(self.btn)
def serve_ball(self, vel=(4,0)):
self.ball.center = self.center
self.ball.velocity = vel
def btnn(self, instance):
self.remove_widget(self.btn)
def update(self, dt):
self.ball.move()
#bounce of paddles
self.player1.bounce_ball(self.ball)
self.player2.bounce_ball(self.ball)
#bounce ball off bottom or top
if (self.ball.y < self.y) or (self.ball.top > self.top):
self.ball.velocity_y *= -1
#went of to a side to score point?
if self.ball.x < self.x:
self.player2.score += 1
self.serve_ball(vel=(4, 0))
if self.ball.x > self.width:
self.player1.score += 1
self.serve_ball(vel=(-4, 0))
#emilio code
if self.player2.score ==2 or self.player1.score==2:
self.serve_ball(vel=(0, 0))
self.btn= Button()
self.add_widget(self.btn)
self.btn.bind(on_press=self.callback)
if self.player2.score == 2:
self.message2= "player2 won!"
if self.player1.score == 2:
self.message1= "player1 won!"
def on_touch_move(self, touch):
if touch.x < self.width / 3:
self.player1.center_y = touch.y
if touch.x > self.width - self.width / 3:
self.player2.center_y = touch.y
class PongApp(App):
def build(self):
game = PongGame()
game.serve_ball()
Clock.schedule_interval(game.update, 1.0 / 60.0)
return game
if __name__=='__main__':
PongApp().run()`
This is the kv file just in case:
#:kivy 1.0.9
<PongBall>:
size: 50, 50
canvas:
Ellipse:
pos: self.pos
size: self.size
<PongPaddle>:
size: 25, 200
canvas:
Rectangle:
pos:self.pos
size:self.size
<PongGame>:
ball: pong_ball
player1: player_left
player2: player_right
canvas:
Rectangle:
pos: self.center_x - 5, 0
size: 10, self.height
Label:
font_size: 70
center_x: root.width / 4
top: root.top - 50
text: str(root.player1.score)
color: (255,0,255,1)
Label:
font_size: 70
center_x: root.width * 3 / 4
top: root.top - 50
text: str(root.player2.score)
color: (255,0,255,1)
Label:
font_size: 40
center_x: root.width * 3/4
top: root.center_y
text: str(root.message2)
color: (0,255,0,1)
Label:
font_size: 40
center_x: root.width / 4
top: root.center_y
text: str(root.message1)
color: (0,255,0,1)
PongBall:
id: pong_ball
center: self.parent.center
PongPaddle:
id: player_left
x: root.x
center_y: root.center_y
PongPaddle:
id: player_right
x: root.width-self.width
center_y: root.center_y
The update function is called a lot of times and each time your condition is met you create another button. only to remove the last button created when you click at it...
Here is your bug:
if self.player2.score ==2 or self.player1.score==2:
print 'WOO' # see how many times this is gonna be printed
#make sure you don't get here more than once per game :)
self.serve_ball(vel=(0, 0))
self.btn= Button()
self.add_widget(self.btn)
self.btn.bind(on_press=self.callback)

Converting 2D screen coordinates to 3D Coordinates in PyQT

I am using PyQT for one of the first times, and I'm having trouble figuring out where a mouse click is in 3D space. Obviously it is not a perfect 1-to-1 mapping, but let's say that I click on a location (x, y, 0) on my QtGui. How can I transform that mouse click to its 3D location using the camera?
Context: I am trying to have users draw splines in 3-D, and in order to do this, I need to know where the user is clicking for when I render the spline. I'm building an application using PythonOCC. I've attached my code below.
import random
import sys
import IPython
from OCC.Display.qtDisplay import qtViewer3d, get_qt_modules
from OCC.gp import gp_Pnt2d, gp_Pnt
from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeVertex,
BRepBuilderAPI_MakeWire)
from OCC.BRepFill import BRepFill_Filling
from OCC.GeomAbs import GeomAbs_C0
from OCC.GeomAPI import GeomAPI_PointsToBSpline
from OCC.TColgp import TColgp_Array1OfPnt
QtCore, QtGui, QtOpenGL = get_qt_modules()
try:
from OpenGL.GL import (glViewport, glMatrixMode, glOrtho, glLoadIdentity,
GL_PROJECTION, GL_MODELVIEW)
except ImportError:
msg = "for this example, the OpenGL module is required" \
"why not run \"pip install PyOpenGL\"\?"
sys.exit(status=1)
class GLWidget(qtViewer3d):
def __init__(self, parent=None):
super(GLWidget, self).__init__(parent)
self._initialized = False
midnight = QtCore.QTime(0, 0, 0)
random.seed(midnight.secsTo(QtCore.QTime.currentTime()))
self.object = 0
self.xRot = 0
self.yRot = 0
self.zRot = 0
self.image = QtGui.QImage()
self.bubbles = []
self.lastPos = QtCore.QPoint()
self.lines = []
self.current_point = None
self.pts = []
self.shiftHeld = True
self.trolltechGreen = QtGui.QColor.fromCmykF(0.40, 0.0, 1.0, 0.0)
self.trolltechPurple = QtGui.QColor.fromCmykF(0.39, 0.39, 0.0, 0.0)
self.animationTimer = QtCore.QTimer()
self.animationTimer.setSingleShot(False)
self.animationTimer.timeout.connect(self.animate)
self.animationTimer.start(25)
self.setAutoFillBackground(False)
self.setMinimumSize(200, 200)
self.setWindowTitle("Overpainting a Scene")
# parameters for overpainting
self.setAttribute(QtCore.Qt.WA_NoSystemBackground, 0)
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
def setXRotation(self, angle):
if angle != self.xRot:
self.xRot = angle
def setYRotation(self, angle):
if angle != self.yRot:
self.yRot = angle
def setZRotation(self, angle):
if angle != self.zRot:
self.zRot = angle
def mousePressEvent(self, event):
self.lastPos = event.pos()
super(GLWidget, self).mousePressEvent(event)
worldCoords = super(GLWidget, self).mapToGlobal( self.lastPos )
print self.lastPos
if event.buttons() & QtCore.Qt.RightButton and not (event.modifiers() & QtCore.Qt.ShiftModifier):
print 'first'
self.pts.append(gp_Pnt(self.lastPos.x(), self.lastPos.y(), 0.0))
elif event.buttons() & QtCore.Qt.RightButton and (event.modifiers() & QtCore.Qt.ShiftModifier):
print 'second'
curve = self.points_to_bspline(self.pts)
self._display.DisplayShape(curve, update=True)
self.pts = [] #clear it
def mouseMoveEvent(self, event):
dx = event.x() - self.lastPos.x()
dy = event.y() - self.lastPos.y()
"""
if (event.buttons() & QtCore.Qt.LeftButton):
self.setXRotation(self.xRot + 8 * dy)
self.setYRotation(self.yRot + 8 * dx)
elif (event.buttons() & QtCore.Qt.RightButton):
self.setXRotation(self.xRot + 8 * dy)
self.setZRotation(self.zRot + 8 * dx)
"""
self.lastPos = event.pos()
super(GLWidget, self).mouseMoveEvent(event)
def paintGL(self):
if self._inited:
self._display.Context.UpdateCurrentViewer()
def paintEvent(self, event):
if self._inited:
self._display.Context.UpdateCurrentViewer()
self.makeCurrent()
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
if self.context().isValid():
self.swapBuffers()
if self._drawbox:
painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 1))
rect = QtCore.QRect(*self._drawbox)
painter.drawRect(rect)
"""
for bubble in self.bubbles:
if bubble.rect().intersects(QtCore.QRectF(event.rect())):
bubble.drawBubble(painter)
"""
painter.end()
self.doneCurrent()
else:
print('invalid OpenGL context: Qt cannot overpaint viewer')
def showEvent(self, event):
pass
#self.createBubbles(20 - len(self.bubbles))
def sizeHint(self):
return QtCore.QSize(400, 400)
def animate(self):
pass
"""
for bubble in self.bubbles:
bubble.move(self.rect())
self.update()
"""
def setupViewport(self, width, height):
side = min(width, height)
glViewport((width - side) // 2, (height - side) // 2, side, side)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-0.5, +0.5, +0.5, -0.5, 4.0, 15.0)
glMatrixMode(GL_MODELVIEW)
def points_to_bspline(self, pnts):
pts = TColgp_Array1OfPnt(0, len(pnts)-1)
for n, i in enumerate(pnts):
pts.SetValue(n, i)
crv = GeomAPI_PointsToBSpline(pts)
return crv.Curve()
if __name__ == '__main__':
def TestOverPainting():
class AppFrame(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setWindowTitle(self.tr("qtDisplay3d overpainting example"))
self.resize(640, 480)
self.canva = GLWidget(self)
mainLayout = QtGui.QHBoxLayout()
mainLayout.addWidget(self.canva)
mainLayout.setContentsMargins(0, 0, 0, 0)
self.setLayout(mainLayout)
def runTests(self):
self.canva._display.Test()
app = QtGui.QApplication(sys.argv)
frame = AppFrame()
frame.show()
frame.canva.InitDriver()
frame.runTests()
app.exec_()
TestOverPainting()
To answer my own question:
(x, y, z, vx, vy, vz) = self._display.View.ConvertWithProj(self.lastPos.x(), self.lastPos.y())
Gives the entire line of points that x and y can map to, with x, y, and z being one point, and vx, vy, and vz giving the parameterization of the line.

Pygame,shooting bullets using vectors

Ok so all night me and 5 other guys have been working on a project for our pygame module.Our lecturer hasn't given us any programs to reference from he has just throw a bunch of code at us (without commenting) and expecting us to understand it.We only started python since the start of september.We really need some help.
So we have one human player which is controlled by W A S & D and 15 random dots moving around the screen.We need to use vectors(which we have never used) to shoot at the random dots.
Please someone help.
edit code added~
import pygame
class Terminator(pygame.sprite.Sprite):
def __init__(self, screen):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((20, 20))
self.image.fill((0, 0, 0))
pygame.draw.circle(self.image, (255, 0, 0), (10, 10), 10, 0)
self.rect = self.image.get_rect()
self.dx = screen.get_width()/2
self.dy = screen.get_height()/2
self.rect.center = (self.dx, self.dy)
self.speed = 5
self.screen = screen
def update(self):
self.rect.center = (self.dx, self.dy)
def MoveLeft(self):
if self.rect.left < 0:
self.dx += 0
else:
self.dx -= self.speed
def MoveRight(self):
if self.rect.right > self.screen.get_width():
self.dx += 0
else:
self.dx += self.speed
def MoveUp(self):
if self.rect.top <0:
self.dy += 0
else:
self.dy -= self.speed
def MoveDown(self):
if self.rect.bottom > self.screen.get_height():
self.dy += 0
else:
self.dy += self.speed
<code>
humansprite.py
<pre>
import pygame
import random
import math
class Humans(pygame.sprite.Sprite):
def __init__(self, screen):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((16, 16))
self.image.fill((0, 0, 0))
pygame.draw.circle(self.image, (0, 0, 255), (8, 8), 8, 0)
self.rect = self.image.get_rect()
self.dx = random.randrange(0, screen.get_height())
self.dy = random.randrange(0, screen.get_width())
self.screen = screen
self.speed = 1
self.alive = True
def update(self):
self.rect.centerx -= self.dx
self.rect.centery -= self.dy
if self.rect.right < 0:
self.reset()
def reset(self):
self.rect.left = self.screen.get_width()
self.rect.centery = random.randrange(0, self.screen.get_height())
self.dy = random.randrange(-2, 2)
self.dx = random.randrange(1, 4)
<code>
seekandDestory.py
<pre>
import pygame
from TerminatorSprite import Terminator
from humansSprite import Humans
pygame.init()
def checkKeys(myData):
(event, ship) = myData
if event.key == pygame.K_LEFT:
print 'LEFT'
ship.MoveLeft()
if event.key == pygame.K_RIGHT:
print 'RIGHT'
ship.MoveRight()
if event.key == pygame.K_UP:
print 'UP'
ship.MoveUp()
if event.key == pygame.K_DOWN:
print 'DOWN'
ship.MoveDown()
def main():
screen = pygame.display.set_mode((640, 480))
pygame.display.set_caption("Seek and Destroy")
background = pygame.Surface(screen.get_size())
clock = pygame.time.Clock()
screen.blit(background, (0, 0))
terminator = Terminator(screen)
humans = []
for people in range(15):
people = Humans(screen)
humans.append(people)
terminatorGroup = pygame.sprite.Group(terminator)
humanGroup = pygame.sprite.Group(humans)
clock = pygame.time.Clock()
keepGoing = True
pygame.key.set_repeat(10, 10)
while keepGoing:
clock.tick(60)
pygame.mouse.set_visible(False)
for event in pygame.event.get():
if event.type == pygame.QUIT:
keepGoing = False
if event.type == pygame.KEYDOWN:
myData = (event, terminator)
checkKeys(myData)
#if pygame.sprite.spritecollide(terminator, humanGroup, False):
terminatorGroup.clear(screen, background)
terminatorGroup.update()
terminatorGroup.draw(screen)
humanGroup.clear(screen, background)
humanGroup.update()
humanGroup.draw(screen)
pygame.display.flip()
pygame.mouse.set_visible(True)
if __name__ == "__main__":
main()
<code>
I just finished up this set of examples last night! Check out the Lunar Lander example, especially the way I'm handling vectors in lunarlander.py - it provides a class, V, for vector management.
What you'll probably need is a sprite for your guy with a vector representing his speed (moment-to-moment, probably 0) and angle, then a sprite for each random dot with a similar vector and angle. When you shoot, create a new sprite with a vector whose angle matches the current angle your guy is facing and magnitude matches the speed of the bullet. Then, in the update() method, move each sprite (bullet, random-dot). Very similar to what I'm doing in lunarlander.py.
Good luck! If you've just started Python, that's a tough assignment. Happy coding!

Resources