How to make one tab bar in QTabWidget expendable? - pyqt

I need to customize my QTabWidget so that one of its tab bars (lets say there are 4 tabs overall) has expanding property and will fill the remaining space between other tabs. Any ideas?

you can subclass QTabBar, set it to the width of your tabwidget (the height depends on the fontsize) and overwrite tabSizeHint():
class tabBar(QTabBar):
def __init__(self, width, height, parent=None):
QTabBar.__init__(self, parent)
self.setFixedSize(width, height)
def tabSizeHint(self, i):
f = 3 # Tab3 shall be f times wider then the other tabs
tw = int(self.width()/(self.count() + f -1)) # width per Tab
if i == 2: # Tab3
# return QSize(tw*f, self.height()) edited -> rounding error possible
return QSize(self.width() - (self.count() - 1)*tw, self.height())
return QSize(tw, self.height()) # all other tabs
and set this tabBar to your tabwidget:
tb = tabBar(tabWidget.width(), 34) # tabBars height depends on fontSize
tabwidget..setTabBar(tb)
looks like this:
edit:
if the tabWidget is resized, a resizeEvent() occurs. In this moment the tabWidget already has its new size and is repainted immediatedly after the resizeEvent(),
see QT-Doc QTabWidget.resizeEvent
So if the width() of the tabBar is adapted in resizeEvent(), the tabBar will always have the same width as the tabwidget. Because the tabSizeHint() depends on the width, all tabs will have the correct width too. So You can subclass QTabWidget() and overwrite resizeEvent() for a dynamical solution:
class tabWidget(QTabWidget):
def __init__(self, parent=None):
QTabWidget.__init__(self, parent)
def resizeEvent(self, event):
self.tabBar().setFixedWidth(self.width())
QTabWidget.resizeEvent(self, event)

To do this correctly, it's necessary to work backwards from the existing sizes of the tabs. This is because the tab sizes are affected by the current style, and by other features such as tab close buttons. It's also important to set a minimum size for the tab which is exandable (otherwise it could be resized to nothing).
Here is a simple demo that does all that:
from PyQt4 import QtCore, QtGui
class TabBar(QtGui.QTabBar):
def __init__(self, expanded=-1, parent=None):
super(TabBar, self).__init__(parent)
self._expanded = expanded
def tabSizeHint(self, index):
size = super(TabBar, self).tabSizeHint(index)
if index == self._expanded:
offset = self.width()
for index in range(self.count()):
offset -= super(TabBar, self).tabSizeHint(index).width()
size.setWidth(max(size.width(), size.width() + offset))
return size
class TabWidget(QtGui.QTabWidget):
def __init__(self, expanded=-1, parent=None):
super(TabWidget, self).__init__(parent)
self.setTabBar(TabBar(expanded, self))
def resizeEvent(self, event):
self.tabBar().setMinimumWidth(self.width())
super(TabWidget, self).resizeEvent(event)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.tabs = TabWidget(2, self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.tabs)
for text in 'One Two Three Four'.split():
self.tabs.addTab(QtGui.QWidget(self), text)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 400, 200)
window.show()
sys.exit(app.exec_())

Related

How to correctly position the QDockWidgets, with a size hint

How to correctly position a different Dockwidgets inside a QMainwindow based on the size hint given to each of the dockwidgets. I tried to position based on addDockWidget and setAllowedAreas, but seems I am missing up the concept. And turn off the tabified when moving the QDockwidgets (prevent them from group in tabs).
from PyQt5.QtWidgets import *
from PyQt5 import QtCore, QtGui
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class Dock_1(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.2*self.width(), .7*self.height())
class Dock_2(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.2*self.width(), .3*self.height())
class Dock_3(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.6*self.width(), .7*self.height())
class Dock_4(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.6*self.width(), .3*self.height())
class Dock_5(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
def sizeHint(self):
return QSize(.1*self.width(), self.height())
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(200, 200, 800, 800)
self.UiComponents()
self.show()
def UiComponents(self):
dock1 = QDockWidget("Dock_1", self)
dock2 = QDockWidget("Dock_2", self)
dock3 = QDockWidget("Dock_3", self)
dock4 = QDockWidget("Dock_4", self)
dock5 = QDockWidget("Dock_5", self)
dock1.setAllowedAreas(Qt.LeftDockWidgetArea)
dock2.setAllowedAreas(Qt.BottomDockWidgetArea)
dock3.setAllowedAreas(Qt.TopDockWidgetArea)
dock4.setAllowedAreas(Qt.BottomDockWidgetArea)
dock5.setAllowedAreas(Qt.RightDockWidgetArea)
w_1 = Dock_1(self)
w_2 = Dock_2(self)
w_3 = Dock_3(self)
w_4 = Dock_4(self)
w_5 = Dock_5(self)
dock1.setWidget(w_1)
dock2.setWidget(w_2)
dock3.setWidget(w_3)
dock4.setWidget(w_4)
dock5.setWidget(w_5)
self.addDockWidget(Qt.LeftDockWidgetArea, dock1)
self.addDockWidget(Qt.LeftDockWidgetArea, dock2)
self.addDockWidget(Qt.RightDockWidgetArea, dock3)
self.addDockWidget(Qt.RightDockWidgetArea, dock4)
self.addDockWidget(Qt.RightDockWidgetArea, dock5)
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
Size hints are almost useless for dock widgets, since precedence is given to the central widget and the main window layout, but your approach is in any case wrong for two reasons:
the size hint is called before resizing, and returning a hint based on the current size wouldn't be valid since it could cause some sort of recursion (this doesn't normally happen as sizeHint() is generally called only when the layout structure is changed and then cached, but that's not the point);
when any widget is initialized it has a default size (100x30 if the parent is in the constructor, otherwise 640x480), so the results from your implementation would be invalid anyway;
Since what you want completely depends on the size of the main window, the only possibility is to resize the docks in the resizeEvent().
Also note that:
in order to have dock widgets side by side you must use splitDockWidget();
to have dock 3 and 4 vertically aligned with 1 and 2 (or 5) you can only put them in (and allow) the left or right dock area;
the allowed areas should match the area in which the dock is added;
creating the "inner" widget of a dock with the main window as a parent is pointless, since the dock will take ownership of that widget;
Considering the above, you should remove all sizeHint() overrides, set the proper allowed areas and then lay out the docks as required.
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(200, 200, 800, 800)
self.UiComponents()
self.show()
def UiComponents(self):
dock1 = QDockWidget("Dock_1", self)
dock2 = QDockWidget("Dock_2", self)
dock3 = QDockWidget("Dock_3", self)
dock4 = QDockWidget("Dock_4", self)
dock5 = QDockWidget("Dock_5", self)
dock1.setAllowedAreas(Qt.LeftDockWidgetArea)
dock2.setAllowedAreas(Qt.LeftDockWidgetArea)
dock3.setAllowedAreas(Qt.RightDockWidgetArea)
dock4.setAllowedAreas(Qt.RightDockWidgetArea)
dock5.setAllowedAreas(Qt.RightDockWidgetArea)
w_1 = Dock_1()
w_2 = Dock_2()
w_3 = Dock_3()
w_4 = Dock_4()
w_5 = Dock_5()
dock1.setWidget(w_1)
dock2.setWidget(w_2)
dock3.setWidget(w_3)
dock4.setWidget(w_4)
dock5.setWidget(w_5)
self.addDockWidget(Qt.LeftDockWidgetArea, dock1)
self.addDockWidget(Qt.LeftDockWidgetArea, dock2)
self.addDockWidget(Qt.RightDockWidgetArea, dock3)
self.addDockWidget(Qt.RightDockWidgetArea, dock4)
self.addDockWidget(Qt.RightDockWidgetArea, dock5)
self.splitDockWidget(dock1, dock2, Qt.Vertical)
self.splitDockWidget(dock3, dock5, Qt.Horizontal)
self.splitDockWidget(dock3, dock4, Qt.Vertical)
self.docks = dock1, dock2, dock3, dock4, dock5
def resizeEvent(self, event):
super().resizeEvent(event)
side = self.width() // 5 # 2 / 10
center = side * 3 # 6 / 10
widths = side, side, center, center, side
self.resizeDocks(self.docks, widths, Qt.Horizontal)
vUnit = self.height() // 10
top = vUnit * 7
bottom = vUnit * 3
heights = top, bottom, top, bottom, top + bottom
self.resizeDocks(self.docks, heights, Qt.Vertical)

How to resize a window from the edges after adding the property QtCore.Qt.FramelessWindowHint

Good night.
I have seen some programs with new borderless designs and still you can make use of resizing.
At the moment I know that to remove the borders of a pyqt program we use:
QtCore.Qt.FramelessWindowHint
And that to change the size of a window use QSizeGrip.
But how can we resize a window without borders?
This is the code that I use to remove the border of a window but after that I have not found information on how to do it in pyqt5.
I hope you can help me with an example of how to solve this problem
from PyQt5.QtWidgets import QMainWindow,QApplication
from PyQt5 import QtCore
class Main(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
app = QApplication([])
m = Main()
m.show()
m.resize(800,600)
app.exec_()
If you use a QMainWindow you can add a QStatusBar (which automatically adds a QSizeGrip) just by calling statusBar():
This function creates and returns an empty status bar if the status bar does not exist.
Otherwise, you can manually add grips, and their interaction is done automatically based on their position. In the following example I'm adding 4 grips, one for each corner, and then I move them each time the window is resized.
class Main(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.gripSize = 16
self.grips = []
for i in range(4):
grip = QSizeGrip(self)
grip.resize(self.gripSize, self.gripSize)
self.grips.append(grip)
def resizeEvent(self, event):
QMainWindow.resizeEvent(self, event)
rect = self.rect()
# top left grip doesn't need to be moved...
# top right
self.grips[1].move(rect.right() - self.gripSize, 0)
# bottom right
self.grips[2].move(
rect.right() - self.gripSize, rect.bottom() - self.gripSize)
# bottom left
self.grips[3].move(0, rect.bottom() - self.gripSize)
UPDATE
Based on comments, also side-resizing is required. To do so a good solution is to create a custom widget that behaves similarly to QSizeGrip, but for vertical/horizontal resizing only.
For better implementation I changed the code above, used a gripSize to construct an "inner" rectangle and, based on it, change the geometry of all widgets, for both corners and sides.
Here you can see the "outer" rectangle and the "inner" rectangle used for geometry computations:
Then you can create all geometries, for QSizeGrip widgets (in light blue):
And for custom side widgets:
from PyQt5 import QtCore, QtGui, QtWidgets
class SideGrip(QtWidgets.QWidget):
def __init__(self, parent, edge):
QtWidgets.QWidget.__init__(self, parent)
if edge == QtCore.Qt.LeftEdge:
self.setCursor(QtCore.Qt.SizeHorCursor)
self.resizeFunc = self.resizeLeft
elif edge == QtCore.Qt.TopEdge:
self.setCursor(QtCore.Qt.SizeVerCursor)
self.resizeFunc = self.resizeTop
elif edge == QtCore.Qt.RightEdge:
self.setCursor(QtCore.Qt.SizeHorCursor)
self.resizeFunc = self.resizeRight
else:
self.setCursor(QtCore.Qt.SizeVerCursor)
self.resizeFunc = self.resizeBottom
self.mousePos = None
def resizeLeft(self, delta):
window = self.window()
width = max(window.minimumWidth(), window.width() - delta.x())
geo = window.geometry()
geo.setLeft(geo.right() - width)
window.setGeometry(geo)
def resizeTop(self, delta):
window = self.window()
height = max(window.minimumHeight(), window.height() - delta.y())
geo = window.geometry()
geo.setTop(geo.bottom() - height)
window.setGeometry(geo)
def resizeRight(self, delta):
window = self.window()
width = max(window.minimumWidth(), window.width() + delta.x())
window.resize(width, window.height())
def resizeBottom(self, delta):
window = self.window()
height = max(window.minimumHeight(), window.height() + delta.y())
window.resize(window.width(), height)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.mousePos = event.pos()
def mouseMoveEvent(self, event):
if self.mousePos is not None:
delta = event.pos() - self.mousePos
self.resizeFunc(delta)
def mouseReleaseEvent(self, event):
self.mousePos = None
class Main(QtWidgets.QMainWindow):
_gripSize = 8
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
self.sideGrips = [
SideGrip(self, QtCore.Qt.LeftEdge),
SideGrip(self, QtCore.Qt.TopEdge),
SideGrip(self, QtCore.Qt.RightEdge),
SideGrip(self, QtCore.Qt.BottomEdge),
]
# corner grips should be "on top" of everything, otherwise the side grips
# will take precedence on mouse events, so we are adding them *after*;
# alternatively, widget.raise_() can be used
self.cornerGrips = [QtWidgets.QSizeGrip(self) for i in range(4)]
#property
def gripSize(self):
return self._gripSize
def setGripSize(self, size):
if size == self._gripSize:
return
self._gripSize = max(2, size)
self.updateGrips()
def updateGrips(self):
self.setContentsMargins(*[self.gripSize] * 4)
outRect = self.rect()
# an "inner" rect used for reference to set the geometries of size grips
inRect = outRect.adjusted(self.gripSize, self.gripSize,
-self.gripSize, -self.gripSize)
# top left
self.cornerGrips[0].setGeometry(
QtCore.QRect(outRect.topLeft(), inRect.topLeft()))
# top right
self.cornerGrips[1].setGeometry(
QtCore.QRect(outRect.topRight(), inRect.topRight()).normalized())
# bottom right
self.cornerGrips[2].setGeometry(
QtCore.QRect(inRect.bottomRight(), outRect.bottomRight()))
# bottom left
self.cornerGrips[3].setGeometry(
QtCore.QRect(outRect.bottomLeft(), inRect.bottomLeft()).normalized())
# left edge
self.sideGrips[0].setGeometry(
0, inRect.top(), self.gripSize, inRect.height())
# top edge
self.sideGrips[1].setGeometry(
inRect.left(), 0, inRect.width(), self.gripSize)
# right edge
self.sideGrips[2].setGeometry(
inRect.left() + inRect.width(),
inRect.top(), self.gripSize, inRect.height())
# bottom edge
self.sideGrips[3].setGeometry(
self.gripSize, inRect.top() + inRect.height(),
inRect.width(), self.gripSize)
def resizeEvent(self, event):
QtWidgets.QMainWindow.resizeEvent(self, event)
self.updateGrips()
app = QtWidgets.QApplication([])
m = Main()
m.show()
m.resize(240, 160)
app.exec_()
to hide the QSizeGrip on the corners where they shouldn't be showing, you can just change the background color of the QSizeGrip to camouflage them to the background. add this to each of the corners of musicamante's answer:
self.cornerGrips[0].setStyleSheet("""
background-color: transparent;
""")

scaledToHeight() does not scale to height - PyQt5

I'm trying to render my image, but scaled to the height of the current window and keeping with aspect ratio.
The following does just that, but does not actually scale the image:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Img(QWidget):
def __init__(self, parent=None):
super(Img, self).__init__(parent)
self.setAttribute(Qt.WA_StyledBackground, True)
self.setStyleSheet('background-color: orange')
window = QDesktopWidget().screenGeometry(0)
height = window.height()
layout = QVBoxLayout(self)
pixmap = QPixmap("test.png")
pixmap = pixmap.scaledToHeight(height)
label = QLabel()
label.setPixmap(pixmap)
label.setScaledContents(True)
layout.addWidget(label)
print(height)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
img = Img()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(img)
layout.setAlignment(Qt.AlignCenter)
content = QWidget()
content.setLayout(layout)
w,h = (1024,670)
self.resize(w,h)
self.setCentralWidget(content)
self.show()
if __name__ == '__main__':
app = QApplication([])
window = MainWindow()
app.exec_()
Although it prints out height is 1050, what I see is a huge image and the window expands down past my monitor. Can someone explain the issue and what needs to happen for scaledToHeight()?
Here is an example of what I see:
And here is the actual image:
The image gets cut off because it gets extended past the monitor screen, and is also stretched oddly.

How can I align a right-click context menu properly in PyQt?

With the sample code below (heavily influenced from here) the right-click context menu is not really aligned properly.
As can be seen in the screenshot, the resulting menu is above the mouse cursor quite a bit. I would expect the menu's top left corner to be exactly aligned with the mouse pointer.
Is there any way to adjust for this?
import re
import operator
import os
import sys
import sqlite3
import cookies
from PyQt4.QtCore import *
from PyQt4.QtGui import *
def main():
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.tabledata = [('apple', 'red', 'small'),
('apple', 'red', 'medium'),
('apple', 'green', 'small'),
('banana', 'yellow', 'large')]
self.header = ['fruit', 'color', 'size']
# create table
self.createTable()
# layout
layout = QVBoxLayout()
layout.addWidget(self.tv)
self.setLayout(layout)
def popup(self, pos):
for i in self.tv.selectionModel().selection().indexes():
print i.row(), i.column()
menu = QMenu()
quitAction = menu.addAction("Quit")
action = menu.exec_(self.mapToGlobal(pos))
if action == quitAction:
qApp.quit()
def createTable(self):
# create the view
self.tv = QTableView()
self.tv.setStyleSheet("gridline-color: rgb(191, 191, 191)")
self.tv.setContextMenuPolicy(Qt.CustomContextMenu)
self.tv.customContextMenuRequested.connect(self.popup)
# set the table model
tm = MyTableModel(self.tabledata, self.header, self)
self.tv.setModel(tm)
# set the minimum size
self.tv.setMinimumSize(400, 300)
# hide grid
self.tv.setShowGrid(True)
# set the font
font = QFont("Calibri (Body)", 12)
self.tv.setFont(font)
# hide vertical header
vh = self.tv.verticalHeader()
vh.setVisible(False)
# set horizontal header properties
hh = self.tv.horizontalHeader()
hh.setStretchLastSection(True)
# set column width to fit contents
self.tv.resizeColumnsToContents()
# set row height
nrows = len(self.tabledata)
for row in xrange(nrows):
self.tv.setRowHeight(row, 18)
# enable sorting
self.tv.setSortingEnabled(True)
return self.tv
class MyTableModel(QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None, *args):
""" datain: a list of lists
headerdata: a list of strings
"""
QAbstractTableModel.__init__(self, parent, *args)
self.arraydata = datain
self.headerdata = headerdata
def rowCount(self, parent):
return len(self.arraydata)
def columnCount(self, parent):
return len(self.arraydata[0])
def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.headerdata[col])
return QVariant()
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
if order == Qt.DescendingOrder:
self.arraydata.reverse()
self.emit(SIGNAL("layoutChanged()"))
if __name__ == "__main__":
main()
the position is in viewport coordinate, so if you are using
self.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
so you don't have event passed to popup, you can do the following
action = menu.exec_(self.tableView.viewport().mapToGlobal(pos))
instead.
This was a bit tricky, but following the subclassing example in this wiki example and replacing
15 action = menu.exec_(self.mapToGlobal(event.pos()))
with
15 action = menu.exec_(event.globalPos())
will make the popup menu's top left corner match the mouse click exactly.
This will work for maximized/minified windows.
Menu will be generated at right-bottom position of mouse.
menu.exec_(self.mapToGlobal(self.mapFromGlobal(QtGui.QCursor.pos())))

Pyqt: How-to set item always in the center of a QGraphicsView when scrolling is possible?

I have a QGraphicsView. To that I added a QGraphicsScene and to that a added an QPixmap(*.jpeg) and QGraphicsEllipseItem(a circle). The QPixmap is much more bigger than the QGraphicsView, so scrolling is enabled. The problem is that both QPixmap and QGraphicsEllipseItem are moving. But I want a fixed position for the QGraphicsEllipseItem in QGraphicsView. It should always be in the center of the QGraphicsView.
How can I do that? Hope someone can help.
Add a signal handler for the scroll signal of the scroll bars (use QAbstractSlider::sliderMoved()).
Then you can query the view for it's left/top offset and size and position the circle accordingly. See the explanation for QAbstractScrollArea to get you started.
If you subclass QGraphicsView, you can override the scrollContentsBy method to set the position of the ellipse whenever the scroll area changes. Here's some really minimal code:
import sys
from PyQt4 import QtGui
class MyView(QtGui.QGraphicsView):
def __init__(self, scene, parent = None):
super(MyView, self).__init__(parent)
self.scene = scene
self.setScene(scene)
self.ellipse = QtGui.QGraphicsEllipseItem(0, 0, 30, 30, scene = self.scene)
self.scene.addItem(self.ellipse)
def scrollContentsBy(self, x, y):
super(MyView, self).scrollContentsBy(x, y)
self.ellipse.setPos(self.mapToScene(28, 28))
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent = None):
super(MainWindow, self).__init__(parent)
pixmap = QtGui.QPixmap()
pixmap.load('imagefile.jpg')
scene = QtGui.QGraphicsScene(self)
scene.setSceneRect(0, 0, pixmap.width(), pixmap.height())
item = QtGui.QGraphicsPixmapItem(pixmap)
scene.addItem(item)
self.view = MyView(scene, self)
self.view.setMinimumSize(100, 100)
def main():
app = QtGui.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
app.exec_()
if __name__ == '__main__':
main()

Resources