I'm trying to create a circular button with an image.
So far I've been able to create a circular button and a button with an image background but I haven't been able to combine the two.
Here is my current code:
import sys
import PyQt5.QtWidgets
class Window(PyQt5.QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# setting title
self.setWindowTitle("Python ")
# setting geometry
self.setGeometry(100, 100, 600, 400)
# calling method
self.UiComponents()
# showing all the widgets
self.show()
# method for widgets
def UiComponents(self):
button = PyQt5.QtWidgets.QPushButton("CLICK", self)
button.setGeometry(200, 150, 100, 100)
# Background img and circular
button.setStyleSheet('''border-radius : 5;
border : 2px solid black
background-image : url(image.png);
''')
# adding action to a button
button.clicked.connect(self.clickme)
# action method
def clickme(self):
print("pressed")
App = PyQt5.QtWidgets.QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
Qt documentation has a section exactly about this topic:
When styling a QPushButton, it is often desirable to use an image as the button graphic. It is common to try the background-image property, but this has a number of drawbacks: For instance, the background will often appear hidden behind the button decoration, because it is not considered a background. In addition, if the button is resized, the entire background will be stretched or tiled, which does not always look good.
It is better to use the border-image property, as it will always display the image, regardless of the background (you can combine it with a background if it has alpha values in it), and it has special settings to deal with button resizing.
So, you must ensure that you're using a square (not rectangular) image with the circle margins right at the edges, and use border-image instead of background-image (which tiles the image if it's smaller than the button size); note that you should not set any border in the stylesheet.
button.setStyleSheet('''
border-image: url(image.png);
''')
You obviously need to always use the same value for both the height and the width of the button, possibly by using setFixedSize(), otherwise if you add the button to a layout it will not be circular any more.
Related
I've got a small example of the GUI I'm working on in Qt6, that has a problem switching palette colors (to switch from dark to light theme). When I apply my changes to QPalette to change the text color, they only work when the window is inactive. Weirdly, if I remove the font-family specification from the stylesheet then the color change works properly. This all works fine in Qt5 without any messing around.
On load, the GUI looks fine
After clicking the "Change Theme" button, it looks fine except that the text color setting that I change using Palette does not work (it's still black)
If I click on my desktop or a different window to make my GUI inactive, it then shows the correct text color (red)
Light - Working, Dark - Broken, Dark - Working
Any workaround suggestions (that make color and font both always work correctly) are welcome, but I'd love to know what I'm actually doing wrong here, and why it used to work in Qt5 and doesn't in Qt6! Thanks!
from PyQt6 import QtWidgets
from PyQt6.QtGui import QPalette, QColor, QFont
APP = QtWidgets.QApplication([])
class UiMain(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setObjectName("MainWindow")
central_widget = QtWidgets.QWidget(self)
self.setCentralWidget(central_widget)
tabs = QtWidgets.QTabWidget(central_widget)
vertical_layout_26 = QtWidgets.QVBoxLayout(central_widget)
vertical_layout_26.addWidget(tabs)
search_tab = QtWidgets.QWidget()
tabs.addTab(search_tab, "")
tabs.setTabText(tabs.indexOf(search_tab), "Search")
filter_group_box = QtWidgets.QGroupBox(search_tab)
filter_group_box.setTitle("Filters")
self.theme_btn = QtWidgets.QPushButton()
self.theme_btn.setText("Change Theme")
searchbar_layout = QtWidgets.QHBoxLayout(search_tab)
searchbar_layout.addWidget(QtWidgets.QLabel("asdf"))
searchbar_layout.addWidget(filter_group_box)
searchbar_layout.addWidget(self.theme_btn)
class View(UiMain):
def __init__(self):
super().__init__()
self.theme_btn.clicked.connect(self.change_theme) # noqa
# Create Palettes
self.light_palette = QPalette()
self.dark_palette = QPalette()
self.dark_palette.setColor(QPalette.ColorRole.WindowText, QColor("red"))
# # This didn't help
# self.dark_palette.setColor(QPalette.ColorGroup.Active, QPalette.ColorRole.WindowText, QColor("red"))
# Create Stylesheets
self.style_light = """
* {font-family: 'Noto Sans';} /* REMOVING THIS LINE AVOIDS THE ISSUE, BUT THEN FONTS ARE WRONG INITIALLY */
QMainWindow {background-color: white;}
"""
self.style_dark = """
* {font-family: 'Noto Sans';}
QMainWindow {background-color: gray;}
"""
# Set initial theme
self.dark = False
APP.setPalette(self.light_palette)
APP.setStyleSheet(self.style_light)
self.show()
def change_theme(self):
"""Allow user to switch between dark and light theme"""
if self.dark:
self.dark = False
APP.setPalette(self.light_palette)
APP.setStyleSheet(self.style_light)
else:
self.dark = True
APP.setPalette(self.dark_palette)
APP.setStyleSheet(self.style_dark)
if __name__ == '__main__':
gui = View()
APP.exec()
I ran into this as well using PySide6 (occurs both on Windows and Linux).
In my case, the issue was that changing the palette using QApplication.setPalette after I had done anything with stylesheets resulted in the stylesheet not being properly applied to existing windows / objects or their children. Deleting the window and creating a new one worked as intended.
The issue (in my case) can be seen using something like the following. The application's palette would show my text color from my palette, but the window's palette shows the default black text color.
# Change theme
my_app.setStyleSheet(new_stylesheet)
my_app.setStyle(new_style)
my_app.setPalette(new_palette)
# self is a QMainWindow that was open when the theme was changed
print(QApplication.palette().color(QPalette.Text).name())
print(self.palette().color(QPalette.Text).name())
I do not know if this is a bug or not, but I was able to work around it without creating a new window instance by manually (and recursively) applying the palette to my existing window and its children using something like the following function
def change_palette_recursive(root: QWidget, palette: QPalette):
root.setPalette(palette)
for child in root.children():
if isinstance(child, QWidget):
change_palette_recursive(child, palette)
This should be called after changing the theme. It likely needs to be called on each open window (unless it is a child of another open window).
change_theme()
change_palette_recursive(existing_window, QApplication.palette())
I would generally consider this sort of thing bad form, but it is the only thing I have found (so far) that works.
I create a pyqt window without any title bar and transparent. Also added blue border for my window but when on running the app I can't see any blue border for the window.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import Qt
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
# this will hide the title bar
self.setWindowFlag(Qt.FramelessWindowHint)
self.setStyleSheet("border : 3px solid blue;")
self.setWindowOpacity(0.01)
# setting the geometry of window
self.setGeometry(100, 100, 400, 300)
self.showFullScreen()
# create pyqt5 app
App = QApplication(sys.argv)
# create the instance of our Window
window = Window()
window.show()
# start the app
sys.exit(App.exec())
How can I show the border for my window?
You can use the TranslucentBackground attribute and paint the border/background in paintEvent.
class Window(QMainWindow):
def __init__(self):
super().__init__()
# this will hide the title bar
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
# setting the geometry of window
self.setGeometry(100, 100, 400, 300)
self.showFullScreen()
def paintEvent(self, event):
qp = QPainter(self)
qp.setPen(QPen(Qt.blue, 6))
qp.drawRect(self.rect())
qp.setOpacity(0.01)
qp.setPen(Qt.NoPen)
qp.setBrush(self.palette().window())
qp.drawRect(self.rect())
The main problem with the original code is that the whole window gets almost transparent (having an alpha channel value of 0.01); this makes any content of the window as much as transparent, including the border, which becomes practically invisible unless the desktop background (or underlying windows) have enough contrast with the color set.
While the proposed solution works as expected and properly answer the OP question, there's another possibility that doesn't directly involve overriding paintEvent().
The issue of setting the border in the stylesheet and setting the WA_TranslucentBackground attribute is that only explicit opaque parts of the window are visible (child widgets, and any other painting implemented in paintEvent()): the background is automatically ignored, and, normally, the border along with it[1].
A possibility is to add a child widget to the main one and set the border for that widget only using a proper style sheet selector:
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
borderWidget = QWidget(objectName='borderWidget')
self.setCentralWidget(borderWidget)
bgd = self.palette().color(QPalette.Window)
bgd.setAlphaF(.01)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: {bgd};
}}
'''.format(bgd=bgd.name(bgd.HexArgb)))
self.showFullScreen()
Note: in the code above (and that below) the background is still visible, but with a very low alpha channel value. If you don't need it, the background value could be ignored, but, for consistency it's better to explicitly declare it: background: transparent;.
That said, consider that QMainWindow has its own private layout, which could add unwanted margins on some systems (and trying to override them might not work at all).
Unless you really need any of the QMainWindow features (menu bar, status bar, tool bars and dock widgets), you should use a basic QWidget instead, which will give you direct control over the layout:
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
borderWidget = QWidget(objectName='borderWidget')
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(borderWidget)
bgd = self.palette().color(QPalette.Window)
bgd.setAlphaF(.01)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: {bgd};
}}
'''.format(bgd=bgd.name(bgd.HexArgb)))
self.showFullScreen()
Remember that setting the basic background/border properties of a QWidget stylesheet only works for actual QWidget instances: Qt subclasses implement it on their own way, and if you are going to create the child widget as a custom QWidget subclass the above will NOT work (see this question and this note in the documentation).
[1] This depends on the implementation of the platform and how Qt deals with it through its QPlatformPlugin. For instance, using older versions of xcompmgr I can get the border just by unsetting the WA_NoSystemBackground attribute (which, as the documentation reports, is automatically set when WA_TranslucentBackground is). While properly implementing specific-platform issues would be better, it's almost impossible as their behavior is often inconsistent across different versions, and the combinations between the window and composition managers are almost infinite. The proposed solution should work in all the situations, and with a minimal, possible, overhead.
I have a Main window, and I want to apply some QEvent in that window mouse events like move press... etc., My problem is after I created the app and main window I resized it, but when I call the rect() function which is from QWidget it actually gives me the default (0, 0, 100, 30), I want it to give me the size of the window.
class TestTools(unittest.TestCase):
app = QApplication([])
main_window = QMainWindow()
main_window.resize(958, 584)
scene = QGraphicsScene()
canvas = Canvas(scene, main_window)
print(canvas.rect()) # It Print (0, 0, 100, 30) Whereas it should be (0, 0, 958, 584)
def test():
pass # There is many functions am using but I don't think it is important to share it
And This is Canvas Class
class Canvas(QGraphicsView):
def __init__(self, scene, centralwidget):
super().__init__(scene, centralwidget)
self.scene = scene
background_color = Qt.white
self.pixmap_item: QGraphicsItem = self.scene.addPixmap(QPixmap(780, 580))
self.pixmap_item.setTransformationMode(Qt.FastTransformation)
So Where is the problem.
You're making an assumption based on wrong premises.
First of all, you're just creating a widget (the QGraphicsView) with a parent. Doing this will only make the widget a child of that parent, but without any size constraints: the widget is just "floating" inside its parent, so it will have a default size that usually is 100x30 for new child widgets (and 640x480 for widgets without parent set in the constructor)).
If you want to adjust the child to the parent it must be added to a layout. QMainWindow has its own (private) layout manager that uses a central widget to show the "main" content of the window, so if that QGraphicsView is going to be the only widget, you can just set that using main_window.setCentralWidget(canvas).
Then, a widget doesn't usually receive a resizeEvent until it's mapped the first time, unless it's manually resized. When it's shown the first time, any non previously layed out item receives the resize event. Since you're not showing the main window, no resize event is emitted, but even in that case, you resized the window before adding the child, so you would still see the same default size.
In order to ensure that the layout is properly computed even without showing the widget in which it is set, activate() must be explicitly called.
So, summing it up:
widgets must be added to a layout in order to adapt their size to their parent;
whenever a QMainWindow is used, setCentralWidget() must be used on the "main widget";
manual resizing should be done after adding children to the layout in order to ensure that their geometries adapt to the parent (if you do the opposite, some size policies could result in the parent resizing itself based on the children);
if the widget is not going to be shown, the layout must be manually activated;
class TestTools(unittest.TestCase):
app = QApplication([])
main_window = QMainWindow()
scene = QGraphicsScene()
canvas = Canvas(scene, main_window)
main_window.setCentralWidget(canvas)
main_window.resize(958, 584)
main_window.layout().activate()
print(canvas.rect())
When creating a PyQt5 tab widget the background colour is whiter than the background of a normal widget. I am looking for a way to get the tab exact background colour.
Some good examples that relate to this question are:
Get the background color of a widget - really
How to make the background color the same as the form background?
The most common suggestions to get the background colour is to use:
widget.palette().color(QtGui.QPalette.Background)
# alternatively with QtGui.QPalette.Base
None of which gets the exact colour. The first being too dark and the later too white.
Whether this works on not also depends on the style and system you are using. On my case it is on a Linux Mint setup but the app is also intended for windows.
===== Using this to set the background of a matplotlib figure =====
The use case for this question is to keep the facecolor of a matplotlib figure consistent with the widget it is embed on.
Here is my example:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt
import matplotlib as mpl
class Window(QtWidgets.QMainWindow):
""" Class to use the data pattern widget in a separate window"""
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
# ===== Setup the window =====
self.setWindowTitle("background")
self.resize(600, 400)
self.maintabs = QtWidgets.QTabWidget(self)
self.setCentralWidget(self.maintabs)
self.page = QtWidgets.QWidget(self)
self.mpl_layout = QtWidgets.QHBoxLayout(self.page)
self.maintabs.addTab(self.page, 'Demo tab')
# ===== Set up matplotlib canvas =====
self.mpl_canvas = None
# get background color from widget and convert it to RBG
pyqt_bkg = self.maintabs.palette().color(QtGui.QPalette.Background).getRgbF()
mpl_bkg = mpl.colors.rgb2hex(pyqt_bkg)
self.pltfig = mpl.figure.Figure()
self.pltfig.set_facecolor(mpl_bkg) # uses the background of mainwindow and not tab
self.plot_ax = self.pltfig.add_subplot(111)
self.addmpl(self.pltfig)
def addmpl(self, fig):
self.mpl_canvas = FigureCanvas(fig)
self.mpl_layout.addWidget(self.mpl_canvas)
self.mpl_canvas.draw()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec())
Resulting in,
The colors of a palette are only a reference for how the style will actually paint widgets. Some widgets use gradients to show their contents (consider buttons, which usually "glow" the color given for the palette Button role). Some styles even paint some widgets using roles that seem unrelated to that type of widget.
QTabWidget behaves in a similar way (depending on the style), so even if you get its background, it will probably painted with a slightly different color.
A possible solution is to set the background of the widget using stylesheet, while still keeping the palette reference:
self.maintabs.setStyleSheet('background: palette(window);')
Note that this will make all children widgets of maintabs use that plain background color.
you can use stylesheet to set the background-color, like these:
self.maintabs.setStyleSheet("background-color: rgb(0,0,59)")
I am trying to rebuild a screen record PyQt App, and the ScreenToGIF is a very good demo for me, it creates an interface which only has the border and record contents in the "Central Widgets", like this:
with key functions of:
The border exists and can be drag and resize by mouse
the inner content is transparent
the mouse click can penetrate through the app, and interact with other app beneath it.
However, it is implemented in C# (link:https://github.com/NickeManarin/ScreenToGif), I am wondering whether it possible to make a similar PyQt App without learning to be expertise about C#?
Changing the background image of QMainWidgets to the desktop area been overlayed doesn't make sense, because mouse operation on desktop (such as double click to open files) should be recorded. Mouse event can penetrate the app (like Qt.WindowTransparentForInput applied for inner contents?)
What you want to achieve requires setting a mask, allowing you to have a widget that has a specific "shape" that doesn't have to be a rectangle.
The main difficulty is to understand how window geometries work, which can be tricky.
You have to ensure that the window "frame" (which includes its margins and titlebar - if any) has been computed, then find out the inner rectangle and create a mask accordingly. Note that on Linux this happens "some time" after show() has been called; I think you're on Windows, but I've implemented it in a way that should work fine for both Linux, MacOS and Windows. There's a comment about that, if you're sure that your program will run on Windows only.
Finally, I've only been able to run this on Linux, Wine and a virtualized WinXP environment. It should work fine on any system, but, from my experience, there's a specific "cosmetic" bug: the title bar is not painted according to the current Windows theme. I think that this is due to the fact that whenever a mask is applied, the underlying windows system doesn't draw its "styled" window frame as it usually would. If this happens in newer systems also, there could be a workaround, but it's not easy, and I cannot guarantee that it would solve this issue.
NB: remember that this approach will never allow you to draw anything inside the "grab rectangle" (no shade, nor semi-transparent color mask); the reason for this is that you obviously need to achieve mouse interaction with what is "beneath" the widget, and painting over it would require altering the overlaying mask.
from PyQt5 import QtCore, QtGui, QtWidgets
class VLine(QtWidgets.QFrame):
# a simple VLine, like the one you get from designer
def __init__(self):
super(VLine, self).__init__()
self.setFrameShape(self.VLine|self.Sunken)
class Grabber(QtWidgets.QWidget):
dirty = True
def __init__(self):
super(Grabber, self).__init__()
self.setWindowTitle('Screen grabber')
# ensure that the widget always stays on top, no matter what
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
# limit widget AND layout margins
layout.setContentsMargins(0, 0, 0, 0)
self.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# create a "placeholder" widget for the screen grab geometry
self.grabWidget = QtWidgets.QWidget()
self.grabWidget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
layout.addWidget(self.grabWidget)
# let's add a configuration panel
self.panel = QtWidgets.QWidget()
layout.addWidget(self.panel)
panelLayout = QtWidgets.QHBoxLayout()
self.panel.setLayout(panelLayout)
panelLayout.setContentsMargins(0, 0, 0, 0)
self.setContentsMargins(1, 1, 1, 1)
self.configButton = QtWidgets.QPushButton(self.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon), '')
self.configButton.setFlat(True)
panelLayout.addWidget(self.configButton)
panelLayout.addWidget(VLine())
self.fpsSpinBox = QtWidgets.QSpinBox()
panelLayout.addWidget(self.fpsSpinBox)
self.fpsSpinBox.setRange(1, 50)
self.fpsSpinBox.setValue(15)
panelLayout.addWidget(QtWidgets.QLabel('fps'))
panelLayout.addWidget(VLine())
self.widthLabel = QtWidgets.QLabel()
panelLayout.addWidget(self.widthLabel)
self.widthLabel.setFrameShape(QtWidgets.QLabel.StyledPanel|QtWidgets.QLabel.Sunken)
panelLayout.addWidget(QtWidgets.QLabel('x'))
self.heightLabel = QtWidgets.QLabel()
panelLayout.addWidget(self.heightLabel)
self.heightLabel.setFrameShape(QtWidgets.QLabel.StyledPanel|QtWidgets.QLabel.Sunken)
panelLayout.addWidget(QtWidgets.QLabel('px'))
panelLayout.addWidget(VLine())
self.recButton = QtWidgets.QPushButton('rec')
panelLayout.addWidget(self.recButton)
self.playButton = QtWidgets.QPushButton('play')
panelLayout.addWidget(self.playButton)
panelLayout.addStretch(1000)
def updateMask(self):
# get the *whole* window geometry, including its titlebar and borders
frameRect = self.frameGeometry()
# get the grabWidget geometry and remap it to global coordinates
grabGeometry = self.grabWidget.geometry()
grabGeometry.moveTopLeft(self.grabWidget.mapToGlobal(QtCore.QPoint(0, 0)))
# get the actual margins between the grabWidget and the window margins
left = frameRect.left() - grabGeometry.left()
top = frameRect.top() - grabGeometry.top()
right = frameRect.right() - grabGeometry.right()
bottom = frameRect.bottom() - grabGeometry.bottom()
# reset the geometries to get "0-point" rectangles for the mask
frameRect.moveTopLeft(QtCore.QPoint(0, 0))
grabGeometry.moveTopLeft(QtCore.QPoint(0, 0))
# create the base mask region, adjusted to the margins between the
# grabWidget and the window as computed above
region = QtGui.QRegion(frameRect.adjusted(left, top, right, bottom))
# "subtract" the grabWidget rectangle to get a mask that only contains
# the window titlebar, margins and panel
region -= QtGui.QRegion(grabGeometry)
self.setMask(region)
# update the grab size according to grabWidget geometry
self.widthLabel.setText(str(self.grabWidget.width()))
self.heightLabel.setText(str(self.grabWidget.height()))
def resizeEvent(self, event):
super(Grabber, self).resizeEvent(event)
# the first resizeEvent is called *before* any first-time showEvent and
# paintEvent, there's no need to update the mask until then; see below
if not self.dirty:
self.updateMask()
def paintEvent(self, event):
super(Grabber, self).paintEvent(event)
# on Linux the frameGeometry is actually updated "sometime" after show()
# is called; on Windows and MacOS it *should* happen as soon as the first
# non-spontaneous showEvent is called (programmatically called: showEvent
# is also called whenever a window is restored after it has been
# minimized); we can assume that all that has already happened as soon as
# the first paintEvent is called; before then the window is flagged as
# "dirty", meaning that there's no need to update its mask yet.
# Once paintEvent has been called the first time, the geometries should
# have been already updated, we can mark the geometries "clean" and then
# actually apply the mask.
if self.dirty:
self.updateMask()
self.dirty = False
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
grabber = Grabber()
grabber.show()
sys.exit(app.exec_())
please try this
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import Qt
import sys
class MainWindowExample(QMainWindow):
def __init__(self, parent=None):
try:
QMainWindow.__init__(self, parent)
self.setWindowFlags(Qt.CustomizeWindowHint | Qt.FramelessWindowHint)
self.setStyleSheet("border: 1px solid rgba(0, 0, 0, 0.15);")
except Exception as e:
print(e)
if __name__ == '__main__':
app = QApplication(sys.argv)
main_widow = MainWindowExample()
main_widow.show()
sys.exit(app.exec_())