I've been trying to create a basic clock using PyQt5. The clock part works and the background image behind the clock loads but I am unable to get the background of the text to be transparent.
I have tried both the way listed below and also with self.lbl1.setStyleSheet("background-color: rgba(255, 255, 255, 10);") but I get the same result in both cases.
If I set the layout.setCurrentIndex(0) I see that the background jpeg is there so that is loading just fine.
Any thoughts on what I would need to do to have layered widgets with transparency?
To try to clear it up a bit
Here is what I get
here is what I want
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
layout = QStackedLayout()
#Background area test
self.background = QLabel(self)
self.background.setPixmap(QPixmap("image.jpeg"))
self.background.show()
#self.background.setAutoFillBackground(True)
#Setup text area for clock
newfont = QFont("Consolas",120, QFont.Bold)
self.lbl1 = QLabel()
self.lbl1.setAlignment(Qt.AlignCenter)
self.lbl1.setFont(newfont)
self.lbl1.setWindowFlags(Qt.FramelessWindowHint)
self.lbl1.setAttribute(Qt.WA_TranslucentBackground)
#Timer to refresh clock
timer = QTimer(self)
timer.timeout.connect(self.showTime)
timer.start(1000)
self.showTime()
#layout area for widgets
layout.addWidget(self.background)
layout.addWidget(self.lbl1)
layout.setCurrentIndex(1)
self.setLayout(layout)
self.setGeometry(300,300,250,150)
self.show()
def showTime(self):
time = QTime.currentTime()
text = time.toString('hh:mm')
if (time.second() % 2) == 0:
text = text[:2] + ' ' + text[3:]
self.lbl1.setText(text)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
The problem is using QStackedWidget, according to the documentation:
The QStackedLayout class provides a stack of widgets where only one
widget is visible at a time.
In it tells us that only a widget is visible. That is why the image is not shown, to force make them visible we use:
layout.setStackingMode(QStackedLayout.StackAll)
Obtaining the following:
As mentioned in the documentation QStackedlayout is used to display one widget at a time. In your case I think it is unnecessary, you could only use a label and any layout, you can use styleSheet to place the background image, the following code shows what I say.
class Example(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent=parent)
self.initUI()
def initUI(self):
self.setGeometry(300,300,250,150)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
newfont = QFont("Consolas",120, QFont.Bold)
self.lbl1 = QLabel(self)
self.lbl1.setStyleSheet("border-image:url(image.jpeg);")
self.lbl1.setAlignment(Qt.AlignCenter)
self.lbl1.setFont(newfont)
layout.addWidget(self.lbl1)
#Timer to refresh clock
timer = QTimer(self)
timer.timeout.connect(self.showTime)
timer.start(1000)
self.showTime()
self.show()
def showTime(self):
time = QTime.currentTime()
text = time.toString('hh:mm')
if (time.second() % 2) == 0:
text = text[:2] + ' ' + text[3:]
self.lbl1.setText(text)
The following image shows the new output:
Related
I am trying the whole morning already to fix that.
So I have a PyQt Main Window where I want to display two widgets.
In the first widget there are articles listed (which works so far).
When I click on them until now a QMessageBox is opening, but I want that
a second widget is opening where I can read the RSS Feed.
But this is not working. See Code below:
class ArticleWidgets(QWidget):
def __init__(self, *args):
super().__init__(*args)
self.setGeometry(610, 610, 600, 600)
self.initUi()
def initUi(self):
self.box = QHBoxLayout(self)
def show(self, feed=None):
self.title = QLabel()
self.summary = QLabel()
self.link = QLabel()
if feed:
self.title.setText(feed[0])
self.summary.setText(feed[1])
self.link.setText(feed[2])
self.box.addWidget(self.title)
self.box.addWidget(self.summary)
self.box.addWidget(self.link)
self.setLayout(self.box)
class TitleWidgets(QWidget):
def __init__(self, *args):
super().__init__(*args)
self.setGeometry(10, 10, 600, 600)
self.initUi()
def initUi(self):
vbox = QHBoxLayout(self)
self.titleList = QListWidget()
self.titleList.itemDoubleClicked.connect(self.onClicked)
self.titleList.setGeometry(0, 0, 400, 400)
self.news = ANFFeed()
for item in self.news.all_feeds:
self.titleList.addItem(item[0])
vbox.addWidget(self.titleList)
def onClicked(self, item):
feeds = self.news.all_feeds
id = 0
for elem in range(len(feeds)):
if feeds[elem][0] == item.text():
id = elem
summary = feeds[id][1] + '\n\n'
link = feeds[id][2]
if feeds and id:
#ANFApp(self).show_articles(feeds[id])
show = ANFApp()
show.show_articles(feed=feeds[id])
QMessageBox.information(self, 'Details', summary + link)
class ANFApp(QMainWindow):
def __init__(self, *args):
super().__init__(*args)
self.setWindowState(Qt.WindowMaximized)
self.setWindowIcon(QIcon('anf.png'))
self.setAutoFillBackground(True)
self.anfInit()
self.show()
def anfInit(self):
self.setWindowTitle('ANF RSS Reader')
TitleWidgets(self)
#article_box = ArticleWidgets(self)
exitBtn = QPushButton(self)
exitBtn.setGeometry(600, 600, 100, 50)
exitBtn.setText('Exit')
exitBtn.setStyleSheet("background-color: red")
exitBtn.clicked.connect(self.exit)
def show_articles(self, feed=None):
present = ArticleWidgets()
present.show(feed)
def exit(self):
QCoreApplication.instance().quit()
Solution using Pyqtgraph's Docks and QTextBrowser
Here is a code trying to reproduce your sketch. I used the Pyqtgraph module (Documentation here: Pyqtgraph's Documentation and Pyqtgraph's Web Page) because its Dock widget is easier to use and implement from my perspective.
You must install the pyqtgraph module before trying this code:
import sys
from PyQt5 import QtGui, QtCore
from pyqtgraph.dockarea import *
class DockArea(DockArea):
## This is to prevent the Dock from being resized to te point of disappear
def makeContainer(self, typ):
new = super(DockArea, self).makeContainer(typ)
new.setChildrenCollapsible(False)
return new
class MyApp(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
central_widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
label = QtGui.QLabel('This is a label, The widgets will be below')
label.setMaximumHeight(15)
## The DockArea as its name says, is the are where we place the Docks
dock_area = DockArea(self)
## Create the Docks and change some esthetic of them
self.dock1 = Dock('Widget 1', size=(300, 500))
self.dock2 = Dock('Widget 2', size=(400, 500))
self.dock1.hideTitleBar()
self.dock2.hideTitleBar()
self.dock1.nStyle = """
Dock > QWidget {
border: 0px solid #000;
border-radius: 0px;
}"""
self.dock2.nStyle = """
Dock > QWidget {
border: 0px solid #000;
border-radius: 0px;
}"""
self.button = QtGui.QPushButton('Exit')
self.widget_one = WidgetOne()
self.widget_two = WidgetTwo()
## Place the Docks inside the DockArea
dock_area.addDock(self.dock1)
dock_area.addDock(self.dock2, 'right', self.dock1)
## The statment above means that dock2 will be placed at the right of dock 1
layout.addWidget(label)
layout.addWidget(dock_area)
layout.addWidget(self.button)
## Add the Widgets inside each dock
self.dock1.addWidget(self.widget_one)
self.dock2.addWidget(self.widget_two)
## This is for set the initial size and posotion of the main window
self.setGeometry(100, 100, 600, 400)
## Connect the actions to functions, there is a default function called close()
self.widget_one.TitleClicked.connect(self.dob_click)
self.button.clicked.connect(self.close)
def dob_click(self, feed):
self.widget_two.text_box.clear()
## May look messy but wat i am doing is somethin like this:
## 'Title : ' + feed[0] + '\n\n' + 'Summary : ' + feed[1]
self.widget_two.text_box.setText(
'Title : ' + feed[0]\
+ '\n\n' +\
'Summary : ' + feed[1]
)
class WidgetOne(QtGui.QWidget):
## This signal is created to pass a "list" when it (the signal) is emited
TitleClicked = QtCore.pyqtSignal([list])
def __init__(self):
QtGui.QWidget.__init__(self)
self.layout = QtGui.QVBoxLayout()
self.setLayout(self.layout)
self.titleList = QtGui.QListWidget()
self.label = QtGui.QLabel('Here is my list:')
self.layout.addWidget(self.label)
self.layout.addWidget(self.titleList)
self.titleList.addItem(QtGui.QListWidgetItem('Title 1'))
self.titleList.addItem(QtGui.QListWidgetItem('Title 2'))
self.titleList.itemDoubleClicked.connect(self.onClicked)
def onClicked(self, item):
## Just test values
title = item.text()
summary = "Here you will put the summary of {}. ".format(title)*50
## Pass the values as a list in the signal. You can pass as much values
## as you want, remember that all of them have to be inside one list
self.TitleClicked.emit([title, summary])
class WidgetTwo(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.layout = QtGui.QVBoxLayout()
self.setLayout(self.layout)
self.label2 = QtGui.QLabel('Here we show results?:')
self.text_box = QtGui.QTextBrowser()
self.layout.addWidget(self.label2)
self.layout.addWidget(self.text_box)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
Again, there are comments inside the code to help you understand what I did.
Here is how it looks:
If you pass the mouse between the two widgets you will see the mouse icon will change, with that you can readjust on the run the size of both widgets.
Final Words
This is another approach, more "interactive" and more esthetic than my previous answer. As you said, using a QSplitter works too.
Problems
The way you are building your GUI is, in my opinion, messy and it may lead to errors. I suggest the use of Layouts for a more organized GUI.
The other problem is that each widget is an independent class so if you want to connect an action in one widget to do something in the other widget through the Main Window, you must use Signals.
Edit : Another suggestion, use other name for the close function instead of exit and try using self.close() instead of QCoreApplication.instance().quit()
Solution
Trying to emulate what you want to do I made this GUI:
import sys
from PyQt5 import QtGui, QtCore
class MyWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
## Generate the structure parts of the MainWindow
self.central_widget = QtGui.QWidget() # A QWidget to work as Central Widget
self.layout1 = QtGui.QVBoxLayout() # Vertical Layout
self.layout2 = QtGui.QHBoxLayout() # Horizontal Layout
self.widget_one = WidgetOne()
self.widget_two = WidgetTwo()
self.exitBtn = QtGui.QPushButton('Exit')
## Build the structure
# Insert a QWidget as a central widget for the MainWindow
self.setCentralWidget(self.central_widget)
# Add a principal layout for the widgets/layouts you want to add
self.central_widget.setLayout(self.layout1)
# Add widgets/layuts, as many as you want, remember they are in a Vertical
# layout: they will be added one below of the other
self.layout1.addLayout(self.layout2)
self.layout1.addWidget(self.exitBtn)
# Here we add the widgets to the horizontal layout: one next to the other
self.layout2.addWidget(self.widget_one)
self.layout2.addWidget(self.widget_two)
## Connect the signal
self.widget_one.TitleClicked.connect(self.dob_click)
def dob_click(self, feed):
## Change the properties of the elements in the second widget
self.widget_two.title.setText('Title : '+feed[0])
self.widget_two.summary.setText('Summary : '+feed[1])
## Build your widgets same as the Main Window, with the excepton that here you don't
## need a central widget, because it is already a widget.
class WidgetOne(QtGui.QWidget):
TitleClicked = QtCore.pyqtSignal([list]) # Signal Created
def __init__(self):
QtGui.QWidget.__init__(self)
##
self.layout = QtGui.QVBoxLayout() # Vertical Layout
self.setLayout(self.layout)
self.titleList = QtGui.QListWidget()
self.label = QtGui.QLabel('Here is my list:')
self.layout.addWidget(self.label)
self.layout.addWidget(self.titleList)
self.titleList.addItem(QtGui.QListWidgetItem('Title 1'))
self.titleList.addItem(QtGui.QListWidgetItem('Title 2'))
self.titleList.itemDoubleClicked.connect(self.onClicked)
def onClicked(self, item):
## Just test parameters and signal emited
self.TitleClicked.emit([item.text(), item.text()+item.text()])
class WidgetTwo(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.layout = QtGui.QVBoxLayout()
self.setLayout(self.layout)
self.title = QtGui.QLabel('Title : ---')
self.summary = QtGui.QLabel('Summary : ---')
self.link = QtGui.QLabel('Link : ---')
self.layout.addWidget(self.title)
self.layout.addWidget(self.summary)
self.layout.addWidget(self.link)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
Inside the code, there are comments to help you understand why I did to build an organized GUI. There is also an example of a Signal being used to connect the action of itemDoubleClicked from the first widget to the second one. Here is how the MainWindow looks:
It is not very clear how the layouts work just from seeing the result, so I did a little paint over to a better understanding:
The blue box is the vertical layout (QVBoxLayout) and the red one is the horizontal layout (QHBoxLayout). Inside the blue layout, are located the red layout (above) and the exit button (below); and inside the red layout, are located the widget_1 (left) and the widget_2 (right).
Other Solution
An "easier" solution will be building the widgets inside the MainWindow instead of creating separate classes. With this you will avoid the use of signals, but the code will become a little more confusing because all the code will be cramped in one class.
I want to display a loading screen every time a user presses a button (a process that takes a few seconds runs).
I want something like this
QSplashScreen does not help me because that is only used before opening the application and a QDialog is not useful for me because I want that by dragging the window the application will move along with the message Loading...
What do I have to use?
The only (safe) way to achieve this is to add a child widget without adding it to any layout manager.
The only things you have to care about is that the widget is always raised as soon as it's shown, and that the geometry is always updated to the parent widget (or, better, the top level window).
This is a slightly more advanced example, but it has the benefit that you can just subclass any widget adding the LoadingWidget class to the base classes in order to implement a loading mechanism.
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class Loader(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.gradient = QtGui.QConicalGradient(.5, .5, 0)
self.gradient.setCoordinateMode(self.gradient.ObjectBoundingMode)
self.gradient.setColorAt(.25, QtCore.Qt.transparent)
self.gradient.setColorAt(.75, QtCore.Qt.transparent)
self.animation = QtCore.QVariantAnimation(
startValue=0., endValue=1.,
duration=1000, loopCount=-1,
valueChanged=self.updateGradient
)
self.stopTimer = QtCore.QTimer(singleShot=True, timeout=self.stop)
self.focusWidget = None
self.hide()
parent.installEventFilter(self)
def start(self, timeout=None):
self.show()
self.raise_()
self.focusWidget = QtWidgets.QApplication.focusWidget()
self.setFocus()
if timeout:
self.stopTimer.start(timeout)
else:
self.stopTimer.setInterval(0)
def stop(self):
self.hide()
self.stopTimer.stop()
if self.focusWidget:
self.focusWidget.setFocus()
self.focusWidget = None
def updateGradient(self, value):
self.gradient.setAngle(-value * 360)
self.update()
def eventFilter(self, source, event):
# ensure that we always cover the whole parent area
if event.type() == QtCore.QEvent.Resize:
self.setGeometry(source.rect())
return super().eventFilter(source, event)
def showEvent(self, event):
self.setGeometry(self.parent().rect())
self.animation.start()
def hideEvent(self, event):
# stop the animation when hidden, just for performance
self.animation.stop()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
color = self.palette().window().color()
color.setAlpha(max(color.alpha() * .5, 128))
qp.fillRect(self.rect(), color)
text = 'Loading...'
interval = self.stopTimer.interval()
if interval:
remaining = int(max(0, interval - self.stopTimer.remainingTime()) / interval * 100)
textWidth = self.fontMetrics().width(text + ' 000%')
text += ' {}%'.format(remaining)
else:
textWidth = self.fontMetrics().width(text)
textHeight = self.fontMetrics().height()
# ensure that there's enough space for the text
if textWidth > self.width() or textHeight * 3 > self.height():
drawText = False
size = max(0, min(self.width(), self.height()) - textHeight * 2)
else:
size = size = min(self.height() / 3, max(textWidth, textHeight))
drawText = True
circleRect = QtCore.QRect(0, 0, size, size)
circleRect.moveCenter(self.rect().center())
if drawText:
# text is going to be drawn, move the circle rect higher
circleRect.moveTop(circleRect.top() - textHeight)
middle = circleRect.center().x()
qp.drawText(
middle - textWidth / 2, circleRect.bottom() + textHeight,
textWidth, textHeight,
QtCore.Qt.AlignCenter, text)
self.gradient.setColorAt(.5, self.palette().windowText().color())
qp.setPen(QtGui.QPen(self.gradient, textHeight))
qp.drawEllipse(circleRect)
class LoadingExtension(object):
# a base class to extend any QWidget subclass's top level window with a loader
def startLoading(self, timeout=0):
window = self.window()
if not hasattr(window, '_loader'):
window._loader = Loader(window)
window._loader.start(timeout)
# this is just for testing purposes
if not timeout:
QtCore.QTimer.singleShot(randrange(1000, 5000), window._loader.stop)
def loadingFinished(self):
if hasattr(self.window(), '_loader'):
self.window()._loader.stop()
class Test(QtWidgets.QWidget, LoadingExtension):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
# just a test widget
textEdit = QtWidgets.QTextEdit()
layout.addWidget(textEdit, 0, 0, 1, 2)
textEdit.setMinimumHeight(20)
layout.addWidget(QtWidgets.QLabel('Timeout:'))
self.timeoutSpin = QtWidgets.QSpinBox(maximum=5000, singleStep=250, specialValueText='Random')
layout.addWidget(self.timeoutSpin, 1, 1)
self.timeoutSpin.setValue(2000)
btn = QtWidgets.QPushButton('Start loading...')
layout.addWidget(btn, 2, 0, 1, 2)
btn.clicked.connect(lambda: self.startLoading(self.timeoutSpin.value()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
Please check Qt::WindowFlags. The Qt::SplashScreen flag will give you splash screen experience without usage QSplashScreen (you can use it with all widget as show) or, better, use QDialog with this flag.
For moving, probably fine solution is not available but you can just use parent moveEvent to emmit signal. For example:
Main window:
moveEvent -> signal moved
Dialog:
signal move -> re-center window.
Its look as not hard.
By the way, I think block all GUI during application run is not the best solution. You you think use QProgressBar?
You can use this slot: void QWidget::raise().
Raises this widget to the top of the parent widget's stack.
After this call the widget will be visually in front of any overlapping sibling widgets.
I've already looked heavily to see how to properly layer my ui and haven't found out how to layer my windows so it comes off looking somewhat like this:
I want to have my background layer which I have set as a label with an image and then have a qt widget with login centered in the middle of it almost popping out at the user however when I do this it comes out with the widget behind my main window, it doesn't align properly and it also doesnt "follow" the window when I move it around image provided:
import PyQt5.QtWidgets
import sys
class LoginPanel(PyQt5.QtWidgets.QWidget):
def __init__(self):
PyQt5.QtWidgets.QWidget.__init__(self)
self.setFixedSize(600,400)
self.setWindowFlags(PyQt5.QtCore.Qt.FramelessWindowHint | PyQt5.QtCore.Qt.WindowStaysOnTopHint)
self.setStyleSheet("""
QWidget {
background-color: #CBCAB7;
border-radius: 50px;
}
""")
self.show()
class Auth(PyQt5.QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Login")
self.setFixedSize(1200,800)
self.setWindowFlags(PyQt5.QtCore.Qt.WindowCloseButtonHint | PyQt5.QtCore.Qt.WindowMinimizeButtonHint)
self.setWindowIcon(PyQt5.QtGui.QIcon("assets\\login.ico"))
self.background = PyQt5.QtWidgets.QLabel("", self)
self.layout = PyQt5.QtWidgets.QGridLayout()
self.layout.addWidget(LoginPanel(), 0, 1)
self.set_background()
self.show()
#self.layout.setAlignment(PyQt5.QtCore.Qt.AlignCenter)
def set_background(self):
img = PyQt5.QtGui.QPixmap("assets\\background.png")
pixmap = img.scaled(self.width(), self.height())
self.background.setPixmap(img)
self.background.resize(self.width(), self.height())
if __name__ == "__main__":
app = PyQt5.QtWidgets.QApplication(sys.argv)
a = Auth()
sys.exit(app.exec())
Here is my current code, I just wanted some help sense while looking online I was unable to find any great examples or references.
My recommendation is not to create a new window but to set it as a child of the window, and to raise it above any other child, you must use raise_() method, also add a QGraphicsDropShadowEffect to establish the floating window effect:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class LoginPanel(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(600, 400)
self.container = QtWidgets.QWidget(self)
self.container.setStyleSheet(
"""
background-color: #CBCAB7;
border-radius: 50px;
"""
)
offset = 30
self.container.setGeometry(
self.rect().adjusted(offset, offset, -offset, -offset)
)
effect = QtWidgets.QGraphicsDropShadowEffect(
blurRadius=50, offset=QtCore.QPointF(0, 0)
)
self.container.setGraphicsEffect(effect)
lay = QtWidgets.QFormLayout(self)
lay.setContentsMargins(2 * offset, 2 * offset, 2 * offset, 2 * offset)
lay.addRow("Username:", QtWidgets.QLineEdit())
lay.addRow("Email:", QtWidgets.QLineEdit())
class Auth(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Login")
self.setFixedSize(1200, 800)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint
)
self.setWindowIcon(QtGui.QIcon("assets\\login.ico"))
self.background = QtWidgets.QLabel(self)
self.set_background()
self.panel = LoginPanel(self)
self.center_panel()
def set_background(self):
img = QtGui.QPixmap("assets\\background.png")
pixmap = img.scaled(self.size())
self.background.setPixmap(pixmap)
self.background.resize(self.size())
def resizeEvent(self, event):
super().resizeEvent(event)
self.center_panel()
def center_panel(self):
g = self.panel.geometry()
g.moveCenter(self.rect().center())
self.panel.setGeometry(g)
self.panel.raise_()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
a = Auth()
a.show()
sys.exit(app.exec())
I am trying to make a GUI that will display (and eventually let the user build) circuits. Below is a rough sketch of what the application is supposed to look like.
The bottom panel (currently a simple QToolBar) should be of constant height but span the width of the application and the side panels (IOPanels in the below code) should have a constant width and span the height of the application.
The main part of the application (Canvas, which is currently a QWidget with an overriden paintEvent method, but might eventually become a QGraphicsScene with a QGraphicsView or at least something scrollable) should then fill the remaining space.
This is my current code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt, QSize
class MainWindow(QMainWindow):
def __init__(self, *args):
super().__init__(*args)
self._wire_ys = None
self._init_ui()
self.update_wire_ys()
def update_wire_ys(self):
self._wire_ys = [(i + 0.5) * self.panel.height() / 4 for i in range(4)]
self.input.update_field_positions()
self.output.update_field_positions()
def wire_ys(self):
return self._wire_ys
def _init_ui(self):
self.panel = QWidget(self)
self.canvas = Canvas(self, self.panel)
self.input = IOPanel(self, self.panel)
self.output = IOPanel(self, self.panel)
hbox = QHBoxLayout(self.panel)
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
hbox.addWidget(self.input, 0, Qt.AlignLeft)
hbox.addWidget(self.output, 0, Qt.AlignRight)
self.setCentralWidget(self.panel)
self.addToolBar(Qt.BottomToolBarArea, self._create_run_panel())
self.reset_placement()
def _create_run_panel(self):
# some other code to create the toolbar
return QToolBar(self)
def reset_placement(self):
g = QDesktopWidget().availableGeometry()
self.resize(0.4 * g.width(), 0.4 * g.height())
self.move(g.center().x() - self.width() / 2, g.center().y() - self.height() / 2)
def resizeEvent(self, *args, **kwargs):
super().resizeEvent(*args, **kwargs)
self.update_wire_ys()
class IOPanel(QWidget):
def __init__(self, main_window, *args):
super().__init__(*args)
self.main = main_window
self.io = [Field(self) for _ in range(4)]
def update_field_positions(self):
wire_ys = self.main.wire_ys()
for i in range(len(wire_ys)):
field = self.io[i]
field.move(self.width() - field.width() - 10, wire_ys[i] - field.height() / 2)
def sizeHint(self):
return QSize(40, self.main.height())
class Field(QLabel):
def __init__(self, *args):
super().__init__(*args)
self.setAlignment(Qt.AlignCenter)
self.setText(str(0))
self.resize(20, 20)
# This class is actually defined in another module and imported
class Canvas(QWidget):
def __init__(self, main_window, *args):
super().__init__(*args)
self.main = main_window
def paintEvent(self, e):
print("ASFD")
qp = QPainter()
qp.begin(self)
self._draw(qp)
qp.end()
def _draw(self, qp):
# Draw stuff
qp.drawLine(0, 0, 1, 1)
# __main__.py
def main():
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Running that code gives me the following:
Here I have coloured the components to better see them using code like this in their construction:
p = self.palette()
p.setColor(self.backgroundRole(), Qt.blue)
self.setPalette(p)
self.setAutoFillBackground(True)
Green is the central panel (MainWindow.panel), blue are the IOPanels, the Fields are supposed to be red, and the Canvas is supposed to be white.
Ignore the bottom toolbar, it's some extra code I didn't include above (to keep it as minimal and relevant as possible), but it does no resizing of anything and no layout management except for its own child QWidget. In fact, including the painting code in my above minimal example gave a similar result with thinner bottom toolbar without the Run button. I'm just including the toolbar here to show its expected behaviour (as the toolbar is working correctly) in the general layout.
This result has several problems.
Problem 1
The Fields do not show up, initially. However, they do show up (and are appropriately placed within their respective panels) once I resize the main window. Why is this? The only thing the main window's resizeEvent does is update_wire_ys and update_field_positions, and those are performed by the main window's __init__ as well.
Problem 2
The IOPanels are not properly aligned. The first one should be on the left side of the central panel. Changing the order of adding them fixes this, as so:
hbox.addWidget(self.input, 0, Qt.AlignLeft)
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
hbox.addWidget(self.output, 0, Qt.AlignRight)
However, shouldn't the Qt.AlignX already do this, regardless of the order they're added in? What if I later on wanted to add another panel to the left side, would I have to remove all the components, add the new panel and then re-add them?
Problem 3
The IOPanels are not properly sized. They need to span the entire height of the central panel and touch the left/right edge of the central panel. I'm not sure if this is an issue with the layout or my colouring of the panels. What am I doing wrong?
Problem 4
The Canvas does not show up at all and in fact its paintEvent is never called ("ASFD" never gets printed to the console). I have not overridden its sizeHint, because I want the central panel's layout to appropriately size the Canvas by itself. I was hoping the stretch factor of 1 when adding the component would accomplish that.
hbox.addWidget(self.canvas, 1, Qt.AlignCenter)
How do I get the canvas to actually show up and fill all the remaining space on the central panel?
This is the typical spaghetti code, where many elements are tangled, which is usually difficult to test, I have found many problems such as sizeEvent is only called when the layout containing the widget is called, another example is when you use the Function update_field_positions and update_wire_ys that handle each other object.
In this answer I will propose a simpler implementation:
IOPanel clas must contain a QVBoxLayout that handles the changes of image size.
In the MainWindow class we will use the layouts with the alignments but you must add them in order.
lay.addWidget(self.input, 0, Qt.AlignLeft)
lay.addWidget(self.canvas, 0, Qt.AlignCenter)
lay.addWidget(self.output, 0, Qt.AlignRight)
To place a minimum width for IOPanel we use QSizePolicy() and setMinimumSize()
Complete code:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Field(QLabel):
def __init__(self, text="0", parent=None):
super(Field, self).__init__(parent=parent)
self.setAlignment(Qt.AlignCenter)
self.setText(text)
class IOPanel(QWidget):
numbers_of_fields = 4
def __init__(self, parent=None):
super(IOPanel, self).__init__(parent=None)
lay = QVBoxLayout(self)
for _ in range(self.numbers_of_fields):
w = Field()
lay.addWidget(w)
self.setMinimumSize(QSize(40, 0))
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
self.setSizePolicy(sizePolicy)
class Panel(QWidget):
def __init__(self, parent=None):
super(Panel, self).__init__(parent=None)
lay = QHBoxLayout(self)
self.input = IOPanel()
self.output = IOPanel()
self.canvas = QWidget()
lay.addWidget(self.input, 0, Qt.AlignLeft)
lay.addWidget(self.canvas, 0, Qt.AlignCenter)
lay.addWidget(self.output, 0, Qt.AlignRight)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent=parent)
self.initUi()
self.reset_placement()
def initUi(self):
panel = Panel(self)
self.setCentralWidget(panel)
self.addToolBar(Qt.BottomToolBarArea, QToolBar(self))
def reset_placement(self):
g = QDesktopWidget().availableGeometry()
self.resize(0.4 * g.width(), 0.4 * g.height())
self.move(g.center().x() - self.width() / 2, g.center().y() - self.height() / 2)
def main():
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Screenshot:
I am having trouble getting this to work the way I want. I want to draw a number of lines below each other and display this in a window that has scrollbars.
So far I can draw the lines and display them, but my scrollbars wont work. What is the correct way to do this?
The file y.list contains simple pairs of numbers as start and end point of my lines. Like:
1 100
4 64
72 98
Here is my code so far:
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Example(QWidget):
def __init__(self):
super(Example, self).__init__()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawLines(qp)
qp.end()
def drawLines(self, qp):
wTotal = 4*2*117
pen = QPen(QColor(100, 200, 0), 5, Qt.SolidLine)
qp.setPen(pen)
qp.drawLine(24, 20, 24+wTotal, 20)
pen = QPen(QColor(0, 0, 255), 2, Qt.SolidLine)
qp.setPen(pen)
qp.drawLine(24+wTotal/2,18,24+wTotal/2,22)
pen = QPen(QColor(0, 50, 255), 2, Qt.SolidLine)
qp.setPen(pen)
with open("y.list", 'r') as points:
linecount = 0
for line in points:
linecount += 1
splitLine = line.split()
start = int(splitLine[0])*4
end = int(splitLine[1])*4
qp.drawLine(20+start, 20+5*linecount, 20+end, 20+5*linecount)
class Widget(QWidget):
def __init__(self, parent= None):
super(Widget, self).__init__()
self.setGeometry(200,100,1100,800)
#Container Widget
widget = QWidget()
#Layout of Container Widget
layout = QVBoxLayout(self)
lines = Example()
layout.addWidget(lines)
widget.setLayout(layout)
#Scroll Area Properties
scroll = QScrollArea()
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scroll.setWidgetResizable(True)
scroll.setWidget(widget)
#Scroll Area Layer add
vLayout = QVBoxLayout(self)
vLayout.addWidget(scroll)
self.setLayout(vLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
dialog = Widget()
dialog.show()
app.exec_()
Currently, you have made the widget resizable, so the widget will be automatically resized to fit the available space, and the scrollbars will never change (because they're not needed).
To change this, you need give the widget a specific size, and don't automatically resize it:
scroll.setWidgetResizable(False)
scroll.setWidget(widget)
widget.resize(2000, 2000)
NB: don't attempt to programmatically resize the widget during the paint-event, because resizing can itself cause a re-paint.