Last active
October 25, 2019 15:20
-
-
Save wmalarski/99814398c9fc8554e526a502a307714d to your computer and use it in GitHub Desktop.
FrameLess resizing widget implementation using PySide2. https://forum.qt.io/topic/66586/make-a-frameless-qwidget-resizable-with-qrubberband/19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
from enum import Enum, IntEnum | |
from typing import List | |
from PySide2.QtCore import QPropertyAnimation, QByteArray, Signal, QRect, Qt | |
from PySide2.QtGui import QResizeEvent, QColor, QMouseEvent | |
from PySide2.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QPushButton, \ | |
QFrame, QToolButton, QHBoxLayout, QVBoxLayout, \ | |
QGraphicsColorizeEffect, QStackedWidget, QButtonGroup, QAbstractButton, QToolBar, QMainWindow, QSizePolicy, \ | |
QDockWidget | |
from tt1.frameless import FrameLess, Edge | |
class AppState(Enum): | |
State1 = "state1" | |
State2 = "state2" | |
class ExtensionState(IntEnum): | |
Settings = 0 | |
EF = 1 | |
ST = 2 | |
class MyWidget(QFrame): | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
self.setFrameStyle(QFrame.Box) | |
self.setStyleSheet("background: blue") | |
layout = QGridLayout(self) | |
self.setLayout(layout) | |
# text over dialog | |
header_label = QLabel(self.tr("I co?")) | |
layout.addWidget(header_label, 0, 0) | |
# project loading | |
load_button = QPushButton(self.tr("Dawaj kurwa ten projekt"), self) | |
layout.addWidget(load_button, 1, 0) | |
class SideBarWidget(QFrame): | |
page_selected = Signal(int) | |
page_deselected = Signal() | |
open_clicked = Signal() | |
def __init__(self, state, parent=None): | |
super().__init__(parent) | |
self.setFrameStyle(QFrame.Box) | |
self.setStyleSheet("background: green") | |
layout = QHBoxLayout(self) | |
layout.setContentsMargins(0, 0, 0, 0) | |
self.setLayout(layout) | |
ver_layout = QVBoxLayout() | |
ver_layout.setContentsMargins(2, 2, 2, 2) | |
layout.addLayout(ver_layout) | |
# Stacked widget | |
self._stacked = QStackedWidget(self) | |
self._stacked_map = {} | |
self._state = state | |
ver_layout.addWidget(self._stacked) | |
# Side pane group | |
button_group = QButtonGroup(self) | |
button_group.setExclusive(False) | |
button_group.buttonClicked.connect(self._on_button_group_button_clicked) | |
button_group.buttonToggled.connect(self._on_button_group_button_toggled) | |
# Menu 1 | |
self._menu1 = QWidget(self) | |
layout_menu1 = QVBoxLayout(self._menu1) | |
layout_menu1.setContentsMargins(0, 0, 0, 0) | |
self._menu1.setLayout(layout_menu1) | |
self._stacked.addWidget(self._menu1) | |
self._stacked_map[AppState.State1] = self._menu1 | |
ef_button = QToolButton(self) | |
ef_button.setText(self.tr("EF")) | |
ef_button.setCheckable(True) | |
button_group.addButton(ef_button, ExtensionState.EF) | |
layout_menu1.addWidget(ef_button) | |
st_button = QToolButton(self) | |
st_button.setText(self.tr("ST")) | |
st_button.setCheckable(True) | |
button_group.addButton(st_button, ExtensionState.ST) | |
layout_menu1.addWidget(st_button) | |
# Menu 2 | |
self._menu2 = QWidget(self) | |
layout_menu2 = QVBoxLayout(self._menu2) | |
layout_menu2.setContentsMargins(0, 0, 0, 0) | |
self._menu2.setLayout(layout_menu2) | |
self._stacked.addWidget(self._menu2) | |
self._stacked_map[AppState.State2] = self._menu2 | |
# Settings button | |
self._settings_button = QToolButton(self) | |
self._settings_button.setText(self.tr("S")) | |
self._settings_button.setCheckable(True) | |
button_group.addButton(self._settings_button, ExtensionState.Settings) | |
ver_layout.addWidget(self._settings_button) | |
# Maximize button | |
self._maximize_button = QToolButton(self) | |
self._maximize_button.setText(">") | |
self._maximize_button.setCheckable(True) | |
self._maximize_button.clicked.connect(self.open_clicked) | |
layout.addWidget(self._maximize_button) | |
# updating state | |
self.state = state | |
@property | |
def state(self) -> AppState: | |
return self._state | |
@state.setter | |
def state(self, state: AppState): | |
self._state = state | |
self._stacked.setCurrentWidget(self._stacked_map[state]) | |
def _on_button_group_button_clicked(self, button: QAbstractButton): | |
if button.isChecked(): | |
for b in button.group().buttons(): | |
if b != button: | |
b.setChecked(False) | |
def _on_button_group_button_toggled(self, button: QAbstractButton, toggled: bool): | |
if toggled: | |
self.page_selected.emit(button.group().id(button)) | |
elif button.group().checkedButton() is None: | |
self.page_deselected.emit() | |
def expand(self, expanded: bool): | |
self._maximize_button.setText("<" if expanded else ">") | |
class ExtensionWidget(QFrame): | |
resize_finished = Signal(QRect) | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
# self.setFrameStyle(QFrame.Box) | |
self.setStyleSheet("background: rgba(255, 0, 0, 100);") | |
self.setMinimumWidth(0) | |
self._frame = FrameLess(self) | |
self._frame.movable_edges = [Edge.Right] | |
self._frame.resize_finished.connect(self.resize_finished) | |
layout = QGridLayout(self) | |
layout.setContentsMargins(0, 0, 0, 0) | |
self.setLayout(layout) | |
self._stacked = QStackedWidget(self) | |
layout.addWidget(self._stacked, 0, 0) | |
self._animation = QPropertyAnimation(self, QByteArray(b"geometry"), parent=parent) | |
def set_animate_visible(self, visible: bool): | |
self._animation.setStartValue(self.geometry()) | |
width = self._frame.geometry.width() if visible else self.minimumWidth() | |
self._animation.setEndValue(QRect(self.x(), self.y(), width, self.height())) | |
self._animation.start() | |
def add_widgets(self, widgets: List[QWidget]): | |
for widget in widgets: | |
self._stacked.addWidget(widget) | |
widget.setParent(self._stacked) | |
@property | |
def current_widget(self) -> QWidget: | |
return self._stacked.currentWidget() | |
@current_widget.setter | |
def current_widget(self, widget: QWidget): | |
self._stacked.setCurrentWidget(widget) | |
class MyToolBar(QToolBar): | |
toggled = Signal(bool) | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
self.setStyleSheet("background: red") | |
self.setMovable(False) | |
self.layout().setContentsMargins(0, 0, 0, 0) | |
open_width = 200 | |
close_width = 50 | |
self._is_open = False | |
self.setMinimumWidth(close_width) | |
self._duration = 250 | |
# close animation | |
self._animation_close = QPropertyAnimation(self, QByteArray(b"minimumWidth"), parent=parent) | |
self._animation_close.setDuration(self._duration) | |
self._animation_close.setStartValue(open_width) | |
self._animation_close.setEndValue(close_width) | |
# open animation | |
self._animation_open = QPropertyAnimation(self, QByteArray(b"minimumWidth"), parent=parent) | |
self._animation_open.setDuration(self._duration) | |
self._animation_open.setStartValue(close_width) | |
self._animation_open.setEndValue(open_width) | |
# extension widget | |
self._ext = ExtensionWidget(parent) | |
self._ext.setVisible(False) | |
def toggle(self): | |
if self._is_open: | |
self._animation_close.start() | |
else: | |
self._animation_open.start() | |
self._is_open = not self._is_open | |
self.toggled.emit(self._is_open) | |
def set_extension_visible(self, visible: bool): | |
self._ext.setVisible(True) | |
self._ext.raise_() | |
self._ext.set_animate_visible(visible) | |
def add_extension_widgets(self, widgets: List[QWidget]): | |
self._ext.add_widgets(widgets) | |
@property | |
def current_extension_widget(self) -> QWidget: | |
return self._ext.current_widget | |
@current_extension_widget.setter | |
def current_extension_widget(self, widget: QWidget): | |
self._ext.current_widget = widget | |
def resizeEvent(self, event: QResizeEvent): | |
super().resizeEvent(event) | |
self._ext.setGeometry(self.x() + self.width(), self.y(), self._ext.width(), event.size().height()) | |
class InnerWidget(QFrame): | |
def __init__(self, text, parent=None): | |
super().__init__(parent) | |
self.setStyleSheet("QFrame{background: rgba(255, 0, 0, 100);}") | |
layout = QVBoxLayout(self) | |
self.setLayout(layout) | |
button = QPushButton(text, self) | |
layout.addWidget(button) | |
self.setMinimumWidth(200) | |
class DockWidget(QDockWidget): | |
def __init__(self, text, parent=None): | |
super().__init__(text, parent) | |
self.setStyleSheet("QWidget{background: purple;}") | |
# self.setAllowedAreas(Qt.RightDockWidgetArea | Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea) | |
button = InnerWidget(text, self) | |
self.setWidget(button) | |
self.setMinimumWidth(200) | |
class MainWindow(QMainWindow): | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
self.setMinimumSize(800, 600) | |
self._widget = MyWidget(self) | |
self.setCentralWidget(self._widget) | |
# Docks | |
self._docks = [DockWidget("dock1", self), DockWidget("dock2", self), DockWidget("dock3", self)] | |
for dock in self._docks: | |
self.addDockWidget(Qt.RightDockWidgetArea, dock) | |
# Tool Bar | |
self._tool_bar = MyToolBar(self) | |
# Side Bar | |
side_bar = SideBarWidget(AppState.State1, self) | |
side_bar.page_selected.connect(self._on_side_bar_page_selected) | |
side_bar.page_deselected.connect(self._on_side_bar_page_deselected) | |
self._tool_bar.addWidget(side_bar) | |
side_bar.open_clicked.connect(self._tool_bar.toggle) | |
self._tool_bar.toggled.connect(side_bar.expand) | |
self.addToolBar(Qt.LeftToolBarArea, self._tool_bar) | |
self.addToolBar(Qt.TopToolBarArea, QToolBar(self)) | |
self._inner1 = InnerWidget("aa", self) | |
self._inner2 = InnerWidget("bb", self) | |
self._inner3 = InnerWidget("cc", self) | |
self._tool_bar.add_extension_widgets([self._inner1, self._inner2, self._inner3]) | |
# Mapping | |
self._page_mapping = { | |
ExtensionState.EF: self._inner1, | |
ExtensionState.ST: self._inner2, | |
ExtensionState.Settings: self._inner3, | |
} | |
def _build_effect(self): | |
effect = QGraphicsColorizeEffect(self) | |
effect.setColor(QColor(0, 0, 0)) | |
effect.setStrength(1.) | |
return effect | |
def _on_side_bar_page_selected(self, page: int): | |
widget = self._page_mapping[ExtensionState(page)] | |
self._tool_bar.current_extension_widget = widget | |
self._tool_bar.set_extension_visible(True) | |
self._widget.setEnabled(False) | |
for dock in self._docks: | |
dock.setEnabled(False) | |
dock.setFeatures(QDockWidget.NoDockWidgetFeatures) | |
dock.setGraphicsEffect(self._build_effect()) | |
self._widget.setGraphicsEffect(self._build_effect()) | |
def _on_side_bar_page_deselected(self): | |
self._tool_bar.set_extension_visible(False) | |
self._widget.setEnabled(True) | |
for dock in self._docks: | |
dock.setEnabled(True) | |
dock.setFeatures(QDockWidget.AllDockWidgetFeatures) | |
dock.setGraphicsEffect(None) | |
self._widget.setGraphicsEffect(None) | |
def main(): | |
# Create the Qt Application | |
app = QApplication(sys.argv) | |
# Create and show the form | |
nav_widget = MainWindow() | |
nav_widget.show() | |
# Run the main Qt loop | |
sys.exit(app.exec_()) | |
if __name__ == '__main__': | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from enum import IntEnum | |
from typing import List | |
from PySide2 import QtWidgets | |
from PySide2.QtCore import QObject, QPoint, Qt, QEvent, QMargins, QRect, Signal, QSize | |
from PySide2.QtGui import QHoverEvent, QMouseEvent | |
from PySide2.QtWidgets import QRubberBand, QWidget, QPushButton, QMainWindow, QSpinBox, QVBoxLayout | |
class Edge(IntEnum): | |
None_ = 0x0 | |
Left = 0x1 | |
Top = 0x2 | |
Right = 0x4 | |
Bottom = 0x8 | |
TopLeft = Top | Left | |
TopRight = Top | Right | |
BottomLeft = Bottom | Left | |
BottomRight = Bottom | Right | |
class FrameLess(QObject): | |
resize_started = Signal(QRect) | |
resize_finished = Signal(QRect) | |
def __init__(self, parent: QWidget): | |
super().__init__(parent) | |
self._cursor_changed = False | |
self._left_button_pressed = False | |
self._border_width = 5 | |
self._drag_pos = QPoint() | |
self._drag_start = False | |
self._limit_size = parent.maximumSize() | |
self._mouse_press_edge = Edge.None_ | |
self._movable_edges = list(Edge) | |
parent.setMouseTracking(True) | |
parent.setWindowFlags(Qt.FramelessWindowHint) | |
parent.setAttribute(Qt.WA_Hover) | |
parent.installEventFilter(self) | |
self._rubber_band = QRubberBand(QRubberBand.Rectangle) | |
@property | |
def geometry(self) -> QRect: | |
return self._rubber_band.geometry() | |
@geometry.setter | |
def geometry(self, geometry): | |
self._rubber_band.setGeometry(geometry) | |
def eventFilter(self, watched: QObject, e: QEvent): | |
if e.type() == QEvent.MouseMove: | |
self._mouse_move(e) | |
elif e.type() == QEvent.HoverMove: | |
self._mouse_hover(e) | |
elif e.type() == QEvent.Leave: | |
self._mouse_leave(e) | |
elif e.type() == QEvent.MouseButtonPress: | |
self._mouse_press(e) | |
elif e.type() == QEvent.MouseButtonRelease: | |
self._mouse_release(e) | |
else: | |
return self.parent().eventFilter(watched, e) | |
return True | |
def _mouse_hover(self, e: QHoverEvent): | |
self._update_cursor_shape(self.parent().mapToParent(e.pos())) | |
def _mouse_leave(self, _: QEvent): | |
if not self._left_button_pressed: | |
self.parent().unsetCursor() | |
def _mouse_press(self, e: QMouseEvent): | |
if e.button() is Qt.LeftButton: | |
self._left_button_pressed = True | |
geometry = self.parent().frameGeometry() | |
self._mouse_press_edge = self._calculate_cursor_position(e.windowPos().toPoint(), geometry) | |
print(e.pos(), geometry, geometry.contains(e.pos()), self._mouse_press_edge) | |
if not (self._mouse_press_edge & Edge.None_): | |
self._rubber_band.setGeometry(self.parent().frameGeometry()) | |
if self.parent().rect().marginsRemoved(QMargins(*([self._border_width] * 4))).contains(e.pos()): | |
self._drag_start = True | |
self._drag_pos = e.pos() | |
self.resize_started.emit(self._rubber_band.geometry()) | |
def _mouse_release(self, e: QMouseEvent): | |
if e.button() is Qt.LeftButton: | |
self._left_button_pressed = False | |
self._drag_start = False | |
self.resize_finished.emit(self._rubber_band.geometry()) | |
def _mouse_move(self, e: QMouseEvent): | |
if self._left_button_pressed and not self.parent().isMaximized(): | |
if self._drag_start: | |
self.parent().move(self.parent().frameGeometry().topLeft() + (e.pos() - self._drag_pos)) | |
if not self._mouse_press_edge & Edge.None_: | |
left = self._rubber_band.frameGeometry().left() | |
top = self._rubber_band.frameGeometry().top() | |
right = self._rubber_band.frameGeometry().right() | |
bottom = self._rubber_band.frameGeometry().bottom() | |
if self._mouse_press_edge & Edge.Top: | |
top = e.windowPos().y() | |
if self._mouse_press_edge & Edge.Bottom: | |
bottom = e.windowPos().y() | |
if self._mouse_press_edge & Edge.Left: | |
left = e.windowPos().x() | |
if self._mouse_press_edge & Edge.Right: | |
right = e.windowPos().x() | |
new_rect = QRect(QPoint(left, top), QPoint(right, bottom)) | |
if new_rect.width() < self.parent().minimumWidth(): | |
left = self.parent().frameGeometry().x() | |
if new_rect.height() < self.parent().minimumHeight(): | |
top = self.parent().frameGeometry().y() | |
if new_rect.width() > self._limit_size.width(): | |
right = self.parent().frameGeometry().x() + self._limit_size.width() | |
if new_rect.height() > self._limit_size.height(): | |
bottom = self.parent().frameGeometry().y() + self._limit_size.height() | |
rect = QRect(QPoint(left, top), QPoint(right, bottom)) | |
self.parent().setGeometry(rect) | |
self._rubber_band.setGeometry(rect) | |
else: | |
self._update_cursor_shape(e.pos()) | |
def _update_cursor_shape(self, pos: QPoint): | |
if self.parent().isFullScreen() or self.parent().isMaximized(): | |
if self._cursor_changed: | |
self.parent().unsetCursor() | |
return | |
if not self._left_button_pressed: | |
geometry = self.parent().frameGeometry() | |
mouse_move = self._calculate_cursor_position(pos, geometry) | |
self._cursor_changed = True | |
if mouse_move == Edge.TopLeft or mouse_move == Edge.BottomRight: | |
self.parent().setCursor(Qt.SizeFDiagCursor) | |
elif mouse_move == Edge.TopRight or mouse_move == Edge.BottomLeft: | |
self.parent().setCursor(Qt.SizeBDiagCursor) | |
elif mouse_move == Edge.Top or mouse_move == Edge.Bottom: | |
self.parent().setCursor(Qt.SizeVerCursor) | |
elif mouse_move == Edge.Left or mouse_move == Edge.Right: | |
self.parent().setCursor(Qt.SizeHorCursor) | |
elif self._cursor_changed: | |
self.parent().unsetCursor() | |
self._cursor_changed = False | |
def _calculate_cursor_position(self, pos: QPoint, rect: QRect): | |
frame_width = self._border_width | |
top = rect.y() + frame_width | |
left = rect.x() + frame_width | |
bottom = rect.y() + rect.height() - frame_width | |
right = rect.x() + rect.width() - frame_width | |
if rect.x() - frame_width <= pos.x() <= left and bottom >= pos.y() >= top: | |
edge = Edge.Left | |
elif right <= pos.x() <= rect.x() + rect.width() and top <= pos.y() <= bottom: | |
edge = Edge.Right | |
elif left <= pos.x() <= right and bottom <= pos.y() <= rect.y() + rect.height(): | |
edge = Edge.Bottom | |
elif left <= pos.x() <= right and rect.y() <= pos.y() <= top: | |
edge = Edge.Top | |
elif left >= pos.x() >= rect.x() and rect.y() + rect.height() >= pos.y() >= bottom: | |
edge = Edge.BottomLeft | |
elif right <= pos.x() <= rect.x() + rect.width() and bottom <= pos.y() <= rect.y() + rect.height(): | |
edge = Edge.BottomRight | |
elif right <= pos.x() <= rect.x() + rect.width() and rect.y() <= pos.y() <= top: | |
edge = Edge.TopRight | |
elif rect.x() <= pos.x() <= left and rect.y() <= pos.y() <= top: | |
edge = Edge.TopLeft | |
else: | |
edge = Edge.None_ | |
print(pos, rect, edge, edge if edge in self._movable_edges else Edge.None_) | |
return edge if edge in self._movable_edges else Edge.None_ | |
@property | |
def border_width(self) -> int: | |
return self._border_width | |
@border_width.setter | |
def border_width(self, border_width: int): | |
self._border_width = border_width | |
@property | |
def movable_edges(self) -> List[Edge]: | |
return self._movable_edges | |
@movable_edges.setter | |
def movable_edges(self, movable_edges: List[Edge]): | |
self._movable_edges = movable_edges | |
@property | |
def limit_size(self) -> QSize: | |
return self._limit_size | |
@limit_size.setter | |
def limit_size(self, limit_size: QSize): | |
self._limit_size = limit_size | |
class MyMainWindow(QMainWindow): | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
self._frame = FrameLess(self) | |
# self._frame.limit_size = QSize(600, 600) | |
widget = QWidget(self) | |
self.setCentralWidget(widget) | |
layout = QVBoxLayout(widget) | |
widget.setLayout(layout) | |
button = QPushButton(self.tr("Click!"), self) | |
button.clicked.connect(self._on_button_clicked) | |
layout.addWidget(button) | |
maximize = QPushButton(self.tr("Maximize!"), self) | |
maximize.clicked.connect(self._on_maximize_clicked) | |
layout.addWidget(maximize) | |
spin = QSpinBox(self) | |
spin.setMinimum(1) | |
spin.setMaximum(25) | |
spin.setValue(self._frame.border_width) | |
spin.valueChanged.connect(self._on_spin_value_changed) | |
layout.addWidget(spin) | |
def _on_button_clicked(self): | |
self.close() | |
def _on_maximize_clicked(self): | |
if self.isMaximized(): | |
self.showNormal() | |
else: | |
self.showMaximized() | |
def _on_spin_value_changed(self, value: int): | |
self._frame.border_width = value | |
def main(): | |
import sys | |
app = QtWidgets.QApplication(sys.argv) | |
widget = MyMainWindow() | |
widget.setMinimumWidth(100) | |
widget.setMinimumHeight(100) | |
widget.show() | |
sys.exit(app.exec_()) | |
if __name__ == "__main__": | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from enum import IntEnum | |
from typing import List | |
from PySide2 import QtWidgets | |
from PySide2.QtCore import QObject, QPoint, Qt, QEvent, QMargins, QRect, Signal, QSize | |
from PySide2.QtGui import QHoverEvent, QMouseEvent | |
from PySide2.QtWidgets import QRubberBand, QWidget, QPushButton, QMainWindow, QSpinBox, QVBoxLayout | |
class Edge(IntEnum): | |
None_ = 0x0 | |
Left = 0x1 | |
Top = 0x2 | |
Right = 0x4 | |
Bottom = 0x8 | |
TopLeft = Top | Left | |
TopRight = Top | Right | |
BottomLeft = Bottom | Left | |
BottomRight = Bottom | Right | |
class FrameLess(QObject): | |
resize_started = Signal(QRect) | |
resize_finished = Signal(QRect) | |
def __init__(self, parent: QWidget): | |
super().__init__(parent) | |
self._cursor_changed = False | |
self._left_button_pressed = False | |
self._border_width = 5 | |
self._drag_pos = QPoint() | |
self._drag_start = False | |
self._limit_size = parent.maximumSize() | |
self._mouse_press_edge = Edge.None_ | |
self._movable_edges = list(Edge) | |
parent.setMouseTracking(True) | |
parent.setWindowFlags(Qt.FramelessWindowHint) | |
parent.setAttribute(Qt.WA_Hover) | |
parent.installEventFilter(self) | |
self._rubber_band = QRubberBand(QRubberBand.Rectangle) | |
@property | |
def geometry(self) -> QRect: | |
return self._rubber_band.geometry() | |
def eventFilter(self, watched: QObject, e: QEvent): | |
if e.type() == QEvent.MouseMove: | |
self._mouse_move(e) | |
elif e.type() == QEvent.HoverMove: | |
self._mouse_hover(e) | |
elif e.type() == QEvent.Leave: | |
self._mouse_leave(e) | |
elif e.type() == QEvent.MouseButtonPress: | |
self._mouse_press(e) | |
elif e.type() == QEvent.MouseButtonRelease: | |
self._mouse_release(e) | |
else: | |
return self.parent().eventFilter(watched, e) | |
return True | |
def _mouse_hover(self, e: QHoverEvent): | |
self._update_cursor_shape(self.parent().mapToGlobal(e.pos())) | |
def _mouse_leave(self, _: QEvent): | |
if not self._left_button_pressed: | |
self.parent().unsetCursor() | |
def _mouse_press(self, e: QMouseEvent): | |
if e.button() is Qt.LeftButton: | |
self._left_button_pressed = True | |
geometry = self.parent().frameGeometry() | |
self._mouse_press_edge = self._calculate_cursor_position(e.globalPos(), geometry) | |
if not (self._mouse_press_edge & Edge.None_): | |
self._rubber_band.setGeometry(self.parent().frameGeometry()) | |
if self.parent().rect().marginsRemoved(QMargins(*([self._border_width] * 4))).contains(e.pos()): | |
self._drag_start = True | |
self._drag_pos = e.pos() | |
self.resize_started.emit(self._rubber_band.geometry()) | |
def _mouse_release(self, e: QMouseEvent): | |
if e.button() is Qt.LeftButton: | |
self._left_button_pressed = False | |
self._drag_start = False | |
self.resize_finished.emit(self._rubber_band.geometry()) | |
def _mouse_move(self, e: QMouseEvent): | |
if self._left_button_pressed and not self.parent().isMaximized(): | |
if self._drag_start: | |
self.parent().move(self.parent().frameGeometry().topLeft() + (e.pos() - self._drag_pos)) | |
if not self._mouse_press_edge & Edge.None_: | |
left = self._rubber_band.frameGeometry().left() | |
top = self._rubber_band.frameGeometry().top() | |
right = self._rubber_band.frameGeometry().right() | |
bottom = self._rubber_band.frameGeometry().bottom() | |
if self._mouse_press_edge & Edge.Top: | |
top = e.globalPos().y() | |
if self._mouse_press_edge & Edge.Bottom: | |
bottom = e.globalPos().y() | |
if self._mouse_press_edge & Edge.Left: | |
left = e.globalPos().x() | |
if self._mouse_press_edge & Edge.Right: | |
right = e.globalPos().x() | |
new_rect = QRect(QPoint(left, top), QPoint(right, bottom)) | |
if new_rect.width() < self.parent().minimumWidth(): | |
left = self.parent().frameGeometry().x() | |
if new_rect.height() < self.parent().minimumHeight(): | |
top = self.parent().frameGeometry().y() | |
if new_rect.width() > self._limit_size.width(): | |
right = self.parent().frameGeometry().x() + self._limit_size.width() | |
if new_rect.height() > self._limit_size.height(): | |
bottom = self.parent().frameGeometry().y() + self._limit_size.height() | |
self.parent().setGeometry(QRect(QPoint(left, top), QPoint(right, bottom))) | |
self._rubber_band.setGeometry(QRect(QPoint(left, top), QPoint(right, bottom))) | |
else: | |
self._update_cursor_shape(e.globalPos()) | |
def _update_cursor_shape(self, pos: QPoint): | |
if self.parent().isFullScreen() or self.parent().isMaximized(): | |
if self._cursor_changed: | |
self.parent().unsetCursor() | |
return | |
if not self._left_button_pressed: | |
mouse_move = self._calculate_cursor_position(pos, self.parent().frameGeometry()) | |
self._cursor_changed = True | |
if mouse_move == Edge.TopLeft or mouse_move == Edge.BottomRight: | |
self.parent().setCursor(Qt.SizeFDiagCursor) | |
elif mouse_move == Edge.TopRight or mouse_move == Edge.BottomLeft: | |
self.parent().setCursor(Qt.SizeBDiagCursor) | |
elif mouse_move == Edge.Top or mouse_move == Edge.Bottom: | |
self.parent().setCursor(Qt.SizeVerCursor) | |
elif mouse_move == Edge.Left or mouse_move == Edge.Right: | |
self.parent().setCursor(Qt.SizeHorCursor) | |
elif self._cursor_changed: | |
self.parent().unsetCursor() | |
self._cursor_changed = False | |
def _calculate_cursor_position(self, pos: QPoint, rect: QRect): | |
frame_width = self._border_width | |
top = rect.y() + frame_width | |
left = rect.x() + frame_width | |
bottom = rect.y() + rect.height() - frame_width | |
right = rect.x() + rect.width() - frame_width | |
if rect.x() - frame_width <= pos.x() <= left and bottom >= pos.y() >= top: | |
edge = Edge.Left | |
elif right <= pos.x() <= rect.x() + rect.width() and top <= pos.y() <= bottom: | |
edge = Edge.Right | |
elif left <= pos.x() <= right and bottom <= pos.y() <= rect.y() + rect.height(): | |
edge = Edge.Bottom | |
elif left <= pos.x() <= right and rect.y() <= pos.y() <= top: | |
edge = Edge.Top | |
elif left >= pos.x() >= rect.x() and rect.y() + rect.height() >= pos.y() >= bottom: | |
edge = Edge.BottomLeft | |
elif right <= pos.x() <= rect.x() + rect.width() and bottom <= pos.y() <= rect.y() + rect.height(): | |
edge = Edge.BottomRight | |
elif right <= pos.x() <= rect.x() + rect.width() and rect.y() <= pos.y() <= top: | |
edge = Edge.TopRight | |
elif rect.x() <= pos.x() <= left and rect.y() <= pos.y() <= top: | |
edge = Edge.TopLeft | |
else: | |
edge = Edge.None_ | |
return edge if edge in self._movable_edges else Edge.None_ | |
@property | |
def border_width(self) -> int: | |
return self._border_width | |
@border_width.setter | |
def border_width(self, border_width: int): | |
self._border_width = border_width | |
@property | |
def movable_edges(self) -> List[Edge]: | |
return self._movable_edges | |
@movable_edges.setter | |
def movable_edges(self, movable_edges: List[Edge]): | |
self._movable_edges = movable_edges | |
@property | |
def limit_size(self) -> QSize: | |
return self._limit_size | |
@limit_size.setter | |
def limit_size(self, limit_size: QSize): | |
self._limit_size = limit_size | |
class MyMainWindow(QMainWindow): | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
self._frame = FrameLess(self) | |
# self._frame.limit_size = QSize(600, 600) | |
widget = QWidget(self) | |
self.setCentralWidget(widget) | |
layout = QVBoxLayout(widget) | |
widget.setLayout(layout) | |
button = QPushButton(self.tr("Click!"), self) | |
button.clicked.connect(self._on_button_clicked) | |
layout.addWidget(button) | |
maximize = QPushButton(self.tr("Maximize!"), self) | |
maximize.clicked.connect(self._on_maximize_clicked) | |
layout.addWidget(maximize) | |
spin = QSpinBox(self) | |
spin.setMinimum(1) | |
spin.setMaximum(25) | |
spin.setValue(self._frame.border_width) | |
spin.valueChanged.connect(self._on_spin_value_changed) | |
layout.addWidget(spin) | |
def _on_button_clicked(self): | |
self.close() | |
def _on_maximize_clicked(self): | |
if self.isMaximized(): | |
self.showNormal() | |
else: | |
self.showMaximized() | |
def _on_spin_value_changed(self, value: int): | |
self._frame.border_width = value | |
def main(): | |
import sys | |
app = QtWidgets.QApplication(sys.argv) | |
widget = MyMainWindow() | |
widget.setMinimumWidth(100) | |
widget.setMinimumHeight(100) | |
widget.show() | |
sys.exit(app.exec_()) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment