Painting with PYQT - MouseEvent uses wrong coordinates - python

My Problem is: The coordinates where my mouse is and where the painting starts is wrong.
The Canvas starts on the top left corner (0,0) but the label are on somewhere at 250,500(Because ive done this with the designer)
so if draw i have to draw outside the label to get something in the canvas :( I didnt find the problem.
Here is my Code:
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtGui import QIcon, QImage, QPen, QPainter, QColor
from PyQt5.QtCore import Qt
from ui.main import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent = None):
super().__init__(parent)
self.main = Ui_MainWindow()
self.main.setupUi(self)
path = "G:/SoftwareDevelopement/SignInPrototyp/signin.png"
self.main.labelSign.setPixmap(QtGui.QPixmap(path))
#self.main.labelSign.move(250,410)
self.drawing = False
self.brushSize = 2
self.brushColor = Qt.black
self.brushStyle = Qt.SolidLine
self.brushCap = Qt.RoundCap
self.brushJoin = Qt.RoundJoin
self.last_x, self.last_y = None, None
def mouseMoveEvent(self, e):
print(e.x)
print(e.y)
if self.last_x is None: # First event.
self.last_x = e.x()
self.last_y = e.y()
return # Ignore the first time.
painter = QtGui.QPainter(self.main.labelSign.pixmap())
painter.setPen(QPen(self.brushColor, self.brushSize, self.brushStyle, self.brushCap, self.brushJoin))
painter.drawLine(self.last_x, self.last_y, e.x(), e.y())
painter.end()
self.update()
# Update the origin for next time.
self.last_x = e.x()
self.last_y = e.y()
def mouseReleaseEvent(self, e):
self.last_x = None
self.last_y = None
# Open and Exit main Window
if __name__ == "__main__":
global app
global window
# SCALE TO ALL RESOLUTIONS! 1
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
# AND THIS
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
window = MainWindow()
window.raise_()
window.showMinimized()
#window.show()
window.showNormal()
window.activateWindow()
sys.exit(app.exec_())
######################!
This dont work, i have to draw on the left top corner to get inside the white

Point 0,0 of window is at top left corner of window, point 0,0 of label is at top left corner of label, you use window coordinates to draw on label (you overriden window event not label event), this two coordinate systems are not equal and to go from one to another you need to translate coordinates. For example like this:
offset = self.main.labelSign.pos()
painter.drawLine(self.last_x - offset.x(), self.last_y - offset.y(), e.x() - offset.x(), e.y() - offset.y())
You can make your drawable label self contained by creating class that inherits from QLabel and overrides mouseMoveEvent and mouseReleaseEvent, this way you dont need to translate coordinates.

so i got after long trial and error and solution:
painter = QtGui.QPainter(self.main.labelSign.pixmap())
painter_map = self.main.labelSign.geometry()
painter.setWindow(painter_map
simple remap.

Related

Draw centered circle and implement an object (bitmp) that moves along the circle

I created a new UI window and drew a circle. I would like to have a small figure (think small stick figure) move along the perimeter of the circle at a certain rate. The rate of movement is probably easy enough to change. How do I have an object move along the perimeter of the circle?
from PyQt5 import QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow
import sys
from PyQt5.QtGui import QPainter, QPen, QBrush
from PyQt5.QtCore import Qt
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.title = "Drawing Ellipse"
self.top = 200
self.left = 200
self.width = 750
self.height = 750
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QtGui.QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawEllipse(200,200, 400,400)
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
First of all, there are some issues with your code:
the circle is not correctly centered: if a centered circle positioned at (200, 200) has a 400 diameter, the window should have a width and height equal to 800; but this is not a good choice, as the window could be resized (by the user or the OS), and if you want to ensure that it is actually centered, you need to adapt the rect used for the painting to the current size;
no painting should ever happen in a QMainWindow unless absolutely required: a Qt main window uses a central widget to show its main contents, and if you want to do any "main" custom painting, it should happen in that central widget; there are very few cases for which a paintEvent should be implemented in a QMainWindow, but they are very only for specific and very advanced scenarios;
a QMainWindow has an internal layout that takes into account possible features such as a menu bar, a status bar, and possibly some amount of toolbars and dock widgets, so you cannot rely on a fixed geometry for the painting, as you risk that the painting would be partially hidden by other objects;
existing class properties should never, ever be overwritten; I'm specifically referring to self.width and self.height, which are existing dynamic properties of any QWidget subclass, and should never be overwritten; I know that there are some tutorials that use that pattern (I've already warned some bloggers about this), but you should not follow those suggestions;
Considering the above, the solution is to create a QWidget subclass, and implement the paintEvent of that class. Then, using a QVariantAnimation allows to create a smooth animation that can be used to correctly position the moving image on the perimeter.
In the following example I'm using a QPainterPath with a simple ellipse, which allows me to get the position using the pointAtPercent() function.
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtGui import QIcon, QPainter, QPen, QPixmap, QPainterPath
from PyQt5.QtCore import Qt, QRectF, QPoint, QPointF, QVariantAnimation
class CirclingWidget(QWidget):
offset = QPoint()
def __init__(self, diameter=1, relative=True, sprite=None,
anglesPerSecond=360, clockwise=True, offset=None, alignment=None):
super().__init__()
self.diameter = diameter
self.relative = relative
self.clockwise = clockwise
self.setSprite(sprite, offset or alignment)
self.animation = QVariantAnimation(startValue=0., endValue=1.,
duration=360000 / anglesPerSecond, loopCount=-1)
self.animation.valueChanged.connect(self.update)
def setSprite(self, sprite, arg):
if isinstance(sprite, str):
sprite = QPixmap(sprite)
if isinstance(sprite, QPixmap) and not sprite.isNull():
self.sprite = sprite
if isinstance(arg, Qt.AlignmentFlag):
self.setAlignment(arg)
else:
self.setOffset(arg)
else:
self.sprite = None
self.update()
def setAlignment(self, alignment=None):
if self.sprite:
x = y = 0
if alignment is not None:
if alignment & Qt.AlignHCenter:
x = -self.sprite.width() / 2
elif alignment & Qt.AlignRight:
x = -self.sprite.width()
if alignment & Qt.AlignVCenter:
y = -self.sprite.height() / 2
elif alignment & Qt.AlignBottom:
y = -self.sprite.height()
self.offset = QPointF(x, y)
self.update()
def setOffset(self, offset=None):
if self.sprite:
x = y = 0
if isinstance(offset, int):
x = y = offset
elif isinstance(offset, float):
x = self.sprite.width() * offset
y = self.sprite.height() * offset
elif isinstance(offset, (QPoint, QPointF)):
x = offset.x()
y = offset.y()
elif isinstance(offset, (tuple, list)):
x, y = offset
self.offset = QPointF(x, y)
self.update()
def setAnglesPerSecond(self, ratio):
self.animation.setDuration(360000 / ratio)
def setClockwise(self, clockwise=True):
if self.clockwise != clockwise:
self.clockwise = clockwise
self.animation.setCurrentTime(
self.animation.duration() - self.animation.currentTime())
self.update()
def start(self, angle=0):
self.animation.start()
# the angle is considered clockwise, starting from 12h
start = (angle - 90) / 360 % 1
self.animation.setCurrentTime(start * self.animation.duration())
self.update()
def stop(self):
self.animation.stop()
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHints(painter.Antialiasing)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
size = self.diameter
if self.relative:
size *= min(self.width(), self.height())
rect = QRectF(0, 0, size, size)
rect.moveCenter(QRectF(self.rect()).center())
path = QPainterPath()
path.addEllipse(rect)
painter.drawPath(path)
if self.sprite:
pos = self.animation.currentValue()
if not self.clockwise:
pos = 1 - pos
pos = path.pointAtPercent(pos)
painter.drawPixmap(pos + self.offset, self.sprite)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.title = "Drawing Ellipse"
self.top = 200
self.left = 200
self._width = 800
self._height = 800
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self._width, self._height)
self.circleWidget = CirclingWidget(.5, sprite='image.png',
anglesPerSecond=60, alignment=Qt.AlignCenter)
self.setCentralWidget(self.circleWidget)
self.circleWidget.start(90)
self.show()
if __name__ == '__main__':
import sys
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
Finally, avoid mixed import styles. The following lines might lead to confusion:
from PyQt5 import QtGui
from PyQt5.QtGui import QPainter, QPen, QBrush
You either import QtGui, or you import the individual classes of that submodule.

Is there any way to make mouse events completely ignore windows in PyQt5?

I have tried to use setAttribute(Qt.Qt.WA_TransparentForMouseEvents),but mouse also can't pierce through Qtwindow.
I want make mouse event penetrate Qtwindow,like I have clicked mouse's right button at a Qtwindow which is located in Windows10 Desktop,then it will trigger win10 contextmenu.
Would a transparent window suit your needs?
from PyQt5 import QtCore, QtWidgets, QtGui
class Overlay(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
layout = QtWidgets.QHBoxLayout(self)
label = QtWidgets.QLabel('Transparent and propagating')
label.setFont(QtGui.QFont('Arial', 26))
label.setStyleSheet("background-color : white")
layout.addWidget(label)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
form = Overlay()
app.exec_()
I tried to figure out a way to directly transmit clicks to the desktop. The closest related question gave me some ideas, but ultimately I was not able to get it working, the clicks never reach the desktop. Maybe you can still get some ideas from this:
from PyQt5 import QtWidgets, QtGui
import win32api, win32con
from ctypes import windll
class Overlay(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
label = QtWidgets.QLabel('Click to Desktop')
label.setFont(QtGui.QFont('Arial', 26))
label.setStyleSheet("background-color : white")
layout.addWidget(label)
# make window partially transparent to see where you are clicking
self.setWindowOpacity(0.5)
# get handle to desktop as described in linked question
hProgman = windll.User32.FindWindowW("Progman", 0)
hFolder = windll.User32.FindWindowExW(hProgman, 0, "SHELLDLL_DefView", 0)
self.desktop = windll.User32.FindWindowExW(hFolder, 0, "SysListView32", 0)
self.show()
def mousePressEvent(self, event):
# catch mouse event to route it to desktop
x = event.globalPos().x()
y = event.globalPos().y()
lParam = win32api.MAKELONG(x, y)
# left click on desktop (left button down + up, => should be replaced by event.button() pseudo switch case once working)
windll.User32.SendInput(self.desktop, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, lParam)
windll.User32.SendInput(self.desktop, win32con.WM_LBUTTONUP, 0, lParam)
# display position for debugging (position gets displayed, but nothing gets clicked)
print(f'clicked on desktop at position {x} and {y}')
if __name__ == '__main__':
app = QtWidgets.QApplication([])
form = Overlay()
app.exec_()
class main(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.Popup|Qt.WindowDoesNotAcceptFocus|Qt.WindowTransparentForInput)
self.setAttribute(Qt.WA_AlwaysStackOnTop, True)

2D clickable surface in Pyside2

I'm writing a simple GUI interface for a project using PySide2.
I'm using the typical MVC design pattern, for the sake of clarity I will just post the code of my GUI (without controller and support methods ecc...)
Here's the code:
from PySide2.QtWidgets import *
from PySide2.QtWidgets import QSizePolicy
from PySide2.QtGui import QIcon
from PySide2.QtWidgets import (QPushButton, QMainWindow)
class myView(QMainWindow):
def __init__(self, parent=None):
"""View initializer."""
#Creates blank view of a given size
super().__init__()
# Set some main window's properties
self.setWindowTitle('8D.me')
self.setFixedSize(800, 500) # Block user resize of the window
self.setIcon()
self.generalLayout = QHBoxLayout() #Layout generale
self.button = QPushButton('test3',self)
self.button.setSizePolicy(
QSizePolicy.Preferred,
QSizePolicy.Expanding)
self.generalLayout.addWidget(QPushButton('test2',self),1)
self.generalLayout.addWidget(self.button,3)
# Set the central widget
self._centralWidget = QWidget(self) #creates a QWidget object to play the role of a central widget. Remember that since your GUI class inherits from QMainWindow, you need a central widget. This object will be the parent for the rest of the GUI component.
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
# Insert methods for creating/adding elements to the default view.
# Mehods....
def setIcon(self):
appIcon = QIcon('logo')
self.setWindowIcon(appIcon)
#Insert here the public methods called by the Controller to update the view...
My GUI right no is pretty simple and looks like this:
What I would like to do is change the test 3 button and insert a 2D clickable surface.
More in details, I would like to be able to click anywhere on this surface and get the position of the mouse click.
Basically I would like to create a 2D xy axis and retrieve the coordinates of my mouse click, something like this:
And then if I click at position (1,1) I wll print something like "You clicked at (1,1) on the axis", pretty simple.
I looked around for examples, tutorials and documentation, but I didn't find any proper tool to create what I wanted.
Is there any class inside the PySide2 package that could help me?
If you took literally that your goal is to get the X-Y plane in your image then a possible solution is to use a QGraphicsView:
import math
import sys
from PySide2.QtCore import Signal, QPointF
from PySide2.QtGui import QColor, QPainterPath
from PySide2.QtWidgets import (
QApplication,
QGraphicsScene,
QGraphicsView,
QHBoxLayout,
QMainWindow,
QPushButton,
QWidget,
)
class GraphicsScene(QGraphicsScene):
clicked = Signal(QPointF)
def drawBackground(self, painter, rect):
l = min(rect.width(), rect.height()) / 30
x_left = QPointF(rect.left(), 0)
x_right = QPointF(rect.right(), 0)
painter.drawLine(x_left, x_right)
right_triangle = QPainterPath()
right_triangle.lineTo(-0.5 * math.sqrt(3) * l, 0.5 * l)
right_triangle.lineTo(-0.5 * math.sqrt(3) * l, -0.5 * l)
right_triangle.closeSubpath()
right_triangle.translate(x_right)
painter.setBrush(QColor("black"))
painter.drawPath(right_triangle)
y_top = QPointF(0, rect.top())
y_bottom = QPointF(0, rect.bottom())
painter.drawLine(y_top, y_bottom)
top_triangle = QPainterPath()
top_triangle.lineTo(.5*l, -0.5 * math.sqrt(3) * l)
top_triangle.lineTo(-.5*l, -0.5 * math.sqrt(3) * l)
top_triangle.closeSubpath()
top_triangle.translate(y_bottom)
painter.setBrush(QColor("black"))
painter.drawPath(top_triangle)
def mousePressEvent(self, event):
sp = event.scenePos()
self.clicked.emit(sp)
super().mousePressEvent(event)
class MyView(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("8D.me")
self.setFixedSize(800, 500)
self.btn = QPushButton("test2")
self.view = QGraphicsView()
self.view.scale(1, -1)
self.scene = GraphicsScene()
self.view.setScene(self.scene)
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QHBoxLayout(central_widget)
layout.addWidget(self.btn)
layout.addWidget(self.view)
self.scene.clicked.connect(self.handle_clicked)
def handle_clicked(self, p):
print("clicked", p.x(), p.y())
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyView()
w.show()
sys.exit(app.exec_())

how to open a QDialog widget on the center position of the main window

Im looking for a way to open a QDialog widget on the center position of the main window.
I have set the position of the mainwindow to center.
centerPoint = qtw.QDesktopWidget().availableGeometry().center()
qtRectangle.moveCenter(centerPoint)
to folow the dialog widget to the postion of the main window
i have set it to
msgb.move(self.pos().x(), self.pos().y())
the dialog window follows the positon of the main window , but it opens on the top left side of the main window, how can I change its position to the center of the main window ?
#!/usr/bin/env python
"""
startscreen
base window remit to specific tests
"""
import os
import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
class Startscreen(qtw.QWidget):
'''
remit to one of three tests if widgets toggled/clicked
hide its self after
'''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# your code will go here
# interface
# position
qtRectangle = self.frameGeometry()
centerPoint = qtw.QDesktopWidget().availableGeometry().center()
qtRectangle.moveCenter(centerPoint)
self.move(qtRectangle.topLeft())
# size
self.resize(700, 410)
# frame title
self.setWindowTitle("Lambda")
# heading
heading_label = qtw.QLabel("Lambda Version 1.0")
heading_label.setAlignment(qtc.Qt.AlignHCenter)
# active user
activeuser_label = qtw.QLabel('Benutzer: ' + os.getlogin())
activeuser_label.setStyleSheet("background-color: rgb(234, 246, 22)")
activeuser_label.setAlignment(qtc.Qt.AlignRight | qtc.Qt.AlignTop)
# groubox for widget positioning
self.groupbox = qtw.QGroupBox(self)
# groupbox.setAlignment(qtc.Qt.AlignHCenter)
# layout and widgets
vlayout = qtw.QVBoxLayout()
vlayout.setAlignment(qtc.Qt.AlignHCenter)
self.particlesize_radiobutton = qtw.QRadioButton("test1")
vlayout.addWidget(self.particlesize_radiobutton)
self.dimensionalchange_radiobutton = qtw.QRadioButton("test2")
vlayout.addWidget(self.dimensionalchange_radiobutton)
self.dimensionalchangecook_radiobutton = qtw.QRadioButton("test3")
vlayout.addWidget(self.dimensionalchangecook_radiobutton)
self.select_button = qtw.QPushButton('select')
vlayout.addWidget(self.select_button)
self.groupbox.setLayout(vlayout)
# mainlayout
main_layout = qtw.QFormLayout()
main_layout.addRow(activeuser_label)
main_layout.addRow(heading_label)
main_layout.setVerticalSpacing(40)
main_layout.addRow(self.groupbox)
self.setLayout(main_layout)
# functionality
self.select_button.clicked.connect(self.open_box)
self.show()
def open_box(self):
msgb = qtw.QMessageBox()
msgb.setWindowTitle("title")
msgb.setText("hier sthet was")
msgb.move(self.pos().x(), self.pos().y())
run = msgb.exec_()
# msgb = qtw.QMessageBox()
# msgb.addButton()
# if x open new windwo
#
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = Startscreen()
sys.exit(app.exec_())
A widget has a position relative to its parent, and if it does not have a parent then it will be relative to the screen. And in the case of msgb it belongs to the second case so you will have to convert the coordinate of the center of the window to global coordinates (that is to say with respect to the screen). Even doing the above it will not be centered because the position is with respect to the topleft, that is, the msgb topleft will be in the center of the screen which is not desirable so you have to also take into account the size of the msgb. And the size of the msgb before and after it is displayed is different so with a QTimer it will be enough:
def open_box(self):
msgb = qtw.QMessageBox()
msgb.setWindowTitle("title")
msgb.setText("hier sthet was")
qtc.QTimer.singleShot(
0,
lambda: msgb.move(
self.mapToGlobal(self.rect().center() - msgb.rect().center())
),
)
run = msgb.exec_()

Custom Titlebar with frame in PyQt5

I'm working on an opensource markdown supported minimal note taking application for Windows/Linux. I'm trying to remove the title bar and add my own buttons. I want something like, a title bar with only two custom buttons as shown in the figure
Currently I have this:
I've tried modifying the window flags:
With not window flags, the window is both re-sizable and movable. But no custom buttons.
Using self.setWindowFlags(QtCore.Qt.FramelessWindowHint), the window has no borders, but cant move or resize the window
Using self.setWindowFlags(QtCore.Qt.CustomizeWindowHint), the window is resizable but cannot move and also cant get rid of the white part at the top of the window.
Any help appreciated. You can find the project on GitHub here.
Thanks..
This is my python code:
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, uic
import sys
import os
import markdown2 # https://github.com/trentm/python-markdown2
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QFont
simpleUiForm = uic.loadUiType("Simple.ui")[0]
class SimpleWindow(QtWidgets.QMainWindow, simpleUiForm):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.markdown = markdown2.Markdown()
self.css = open(os.path.join("css", "default.css")).read()
self.editNote.setPlainText("")
#self.noteView = QtWebEngineWidgets.QWebEngineView(self)
self.installEventFilter(self)
self.displayNote.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
#self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate:
print("widget window has gained focus")
self.editNote.show()
self.displayNote.hide()
elif event.type() == QtCore.QEvent.WindowDeactivate:
print("widget window has lost focus")
note = self.editNote.toPlainText()
htmlNote = self.getStyledPage(note)
# print(note)
self.editNote.hide()
self.displayNote.show()
# print(htmlNote)
self.displayNote.setHtml(htmlNote)
elif event.type() == QtCore.QEvent.FocusIn:
print("widget has gained keyboard focus")
elif event.type() == QtCore.QEvent.FocusOut:
print("widget has lost keyboard focus")
return False
The UI file is created in the following hierarchy
Here are the steps you just gotta follow:
Have your MainWindow, be it a QMainWindow, or QWidget, or whatever [widget] you want to inherit.
Set its flag, self.setWindowFlags(Qt.FramelessWindowHint)
Implement your own moving around.
Implement your own buttons (close, max, min)
Implement your own resize.
Here is a small example with move around, and buttons implemented. You should still have to implement the resize using the same logic.
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addStretch(-1)
self.setMinimumSize(800,400)
self.setWindowFlags(Qt.FramelessWindowHint)
self.pressing = False
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
print(self.parent.width())
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("My Own Bar")
btn_size = 35
self.btn_close = QPushButton("x")
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(btn_size,btn_size)
self.btn_close.setStyleSheet("background-color: red;")
self.btn_min = QPushButton("-")
self.btn_min.clicked.connect(self.btn_min_clicked)
self.btn_min.setFixedSize(btn_size, btn_size)
self.btn_min.setStyleSheet("background-color: gray;")
self.btn_max = QPushButton("+")
self.btn_max.clicked.connect(self.btn_max_clicked)
self.btn_max.setFixedSize(btn_size, btn_size)
self.btn_max.setStyleSheet("background-color: gray;")
self.title.setFixedHeight(35)
self.title.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_min)
self.layout.addWidget(self.btn_max)
self.layout.addWidget(self.btn_close)
self.title.setStyleSheet("""
background-color: black;
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
def btn_max_clicked(self):
self.parent.showMaximized()
def btn_min_clicked(self):
self.parent.showMinimized()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
Here are some tips:
Option 1:
Have a QGridLayout with widget in each corner and side(e.g. left, top-left, menubar, top-right, right, bottom-right, bottom and bottom left)
With the approach (1) you would know when you are clicking in each border, you just got to define each one size and add each one on their place.
When you click on each one treat them in their respective ways, for example, if you click in the left one and drag to the left, you gotta resize it larger and at the same time move it to the left so it will appear to be stopped at the right place and grow width.
Apply this reasoning to each edge, each one behaving in the way it has to.
Option 2:
Instead of having a QGridLayout you can detect in which place you are clicking by the click pos.
Verify if the x of the click is smaller than the x of the moving pos to know if it's moving left or right and where it's being clicked.
The calculation is made in the same way of the Option1
Option 3:
Probably there are other ways, but those are the ones I just thought of. For example using the CustomizeWindowHint you said you are able to resize, so you just would have to implement what I gave you as example. BEAUTIFUL!
Tips:
Be careful with the localPos(inside own widget), globalPos(related to your screen). For example: If you click in the very left of your left widget its 'x' will be zero, if you click in the very left of the middle(content)it will be also zero, although if you mapToGlobal you will having different values according to the pos of the screen.
Pay attention when resizing, or moving, when you have to add width or subtract, or just move, or both, I'd recommend you to draw on a paper and figure out how the logic of resizing works before implementing it out of blue.
GOOD LUCK :D
While the accepted answer can be considered valid, it has some issues.
using setGeometry() is not appropriate (and the reason for using it was wrong) since it doesn't consider possible frame margins set by the style;
the position computation is unnecessarily complex;
resizing the title bar to the total width is wrong, since it doesn't consider the buttons and can also cause recursion problems in certain situations (like not setting the minimum size of the main window); also, if the title is too big, it makes impossible to resize the main window;
buttons should not accept focus;
setting a layout creates a restraint for the "main widget" or layout, so the title should not be added, but the contents margins of the widget should be used instead;
I revised the code to provide a better base for the main window, simplify the moving code, and add other features like the Qt windowTitle() property support, standard QStyle icons for buttons (instead of text), and proper maximize/normal button icons. Note that the title label is not added to the layout.
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.titleBar = MyBar(self)
self.setContentsMargins(0, self.titleBar.height(), 0, 0)
self.resize(640, self.titleBar.height() + 480)
def changeEvent(self, event):
if event.type() == event.WindowStateChange:
self.titleBar.windowStateChanged(self.windowState())
def resizeEvent(self, event):
self.titleBar.resize(self.width(), self.titleBar.height())
class MyBar(QWidget):
clickPos = None
def __init__(self, parent):
super(MyBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.Shadow)
# alternatively:
# palette = self.palette()
# palette.setColor(palette.Window, Qt.black)
# palette.setColor(palette.WindowText, Qt.white)
# self.setPalette(palette)
layout = QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.addStretch()
self.title = QLabel("My Own Bar", self, alignment=Qt.AlignCenter)
# if setPalette() was used above, this is not required
self.title.setForegroundRole(QPalette.Light)
style = self.style()
ref_size = self.fontMetrics().height()
ref_size += style.pixelMetric(style.PM_ButtonMargin) * 2
self.setMaximumHeight(ref_size + 2)
btn_size = QSize(ref_size, ref_size)
for target in ('min', 'normal', 'max', 'close'):
btn = QToolButton(self, focusPolicy=Qt.NoFocus)
layout.addWidget(btn)
btn.setFixedSize(btn_size)
iconType = getattr(style,
'SP_TitleBar{}Button'.format(target.capitalize()))
btn.setIcon(style.standardIcon(iconType))
if target == 'close':
colorNormal = 'red'
colorHover = 'orangered'
else:
colorNormal = 'palette(mid)'
colorHover = 'palette(light)'
btn.setStyleSheet('''
QToolButton {{
background-color: {};
}}
QToolButton:hover {{
background-color: {}
}}
'''.format(colorNormal, colorHover))
signal = getattr(self, target + 'Clicked')
btn.clicked.connect(signal)
setattr(self, target + 'Button', btn)
self.normalButton.hide()
self.updateTitle(parent.windowTitle())
parent.windowTitleChanged.connect(self.updateTitle)
def updateTitle(self, title=None):
if title is None:
title = self.window().windowTitle()
width = self.title.width()
width -= self.style().pixelMetric(QStyle.PM_LayoutHorizontalSpacing) * 2
self.title.setText(self.fontMetrics().elidedText(
title, Qt.ElideRight, width))
def windowStateChanged(self, state):
self.normalButton.setVisible(state == Qt.WindowMaximized)
self.maxButton.setVisible(state != Qt.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clickPos = event.windowPos().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
self.window().move(event.globalPos() - self.clickPos)
def mouseReleaseEvent(self, QMouseEvent):
self.clickPos = None
def closeClicked(self):
self.window().close()
def maxClicked(self):
self.window().showMaximized()
def normalClicked(self):
self.window().showNormal()
def minClicked(self):
self.window().showMinimized()
def resizeEvent(self, event):
self.title.resize(self.minButton.x(), self.height())
self.updateTitle()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
layout = QVBoxLayout(mw)
widget = QTextEdit()
layout.addWidget(widget)
mw.show()
mw.setWindowTitle('My custom window with a very, very long title')
sys.exit(app.exec_())
This is for the people who are going to implement custom title bar in PyQt6 or PySide6
The below changes should be done in the answer given by #musicamante
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# self.clickPos = event.windowPos().toPoint()
self.clickPos = event.scenePosition().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
# self.window().move(event.globalPos() - self.clickPos)
self.window().move(event.globalPosition().toPoint() - self.clickPos)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
# sys.exit(app.exec_())
sys.exit(app.exec())
References:
QMouseEvent.globalPosition(),
QMouseEvent.scenePosition()
This method of moving Windows with Custom Widget doesn't work with WAYLAND. If anybody has a solution for that please post it here for future reference
Working functions for WAYLAND and PyQT6/PySide6 :
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._move()
return super().mousePressEvent(event)
def _move(self):
window = self.window().windowHandle()
window.startSystemMove()
Please check.

Categories