Should I use QGraphicsView to display an image and some decorated text side by side? - qt

I want to create a "details" view for books I have downloaded.
With the attached image as an example, imagine the red block to the left is the book's cover page, and metadata related to it is displayed to the right.
With the way I have it done right now:
from PySide6 import QtWidgets as qtw
from PySide6 import QtGui as qtg
from PySide6 import QtCore as qtc
class Details:
def __init__(self):
self.location = "/home/user/Desktop/Untitled.png"
self.title = "Some title"
self.subtitle = "Sub title"
self.id = 123124
def to_html(self):
return """
<p>
<b>Author =</b> author<br/>
<b>Published Date =</b> 2000-1-1<br/>
<b>Pages =</b> 500<br/>
</p>
"""
class DetailsWidget(qtw.QWidget):
_title_font = qtg.QFont()
_title_font.setBold(True)
_title_font.setPixelSize(24)
_subtitle_font = qtg.QFont()
_subtitle_font.setBold(True)
_subtitle_font.setPixelSize(19)
_id_font = qtg.QFont()
_id_font.setBold(True)
_id_font.setPixelSize(15)
_redacted_details_font = qtg.QFont()
_redacted_details_font.setPixelSize(12)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.setFixedSize(1000, 500)
self.setWindowFlag(qtc.Qt.WindowType.Dialog, True)
self.setLayout(qtw.QGridLayout())
self.layout().setContentsMargins(0, 0, 0, 0)
self._details: Details = Details()
self._thumbnail_image = qtg.QImage(self._details.location)
self._thumbnail_image = self._thumbnail_image.scaled(
500,
500,
qtc.Qt.AspectRatioMode.KeepAspectRatio,
qtc.Qt.TransformationMode.SmoothTransformation,
)
self._details_rect = qtc.QRect(
self._get_actual_geometry().left() + self._thumbnail_image.width() + 10,
self._get_actual_geometry().top(),
self._get_actual_geometry().width() - self._thumbnail_image.width() - 20,
self._get_actual_geometry().height(),
)
height = 0
self._title_rects = []
font_metrics_rect = qtg.QFontMetrics(self._title_font).boundingRect(
self._details_rect, qtc.Qt.TextFlag.TextWordWrap, self._details.title, 0
)
drawing_rect = qtc.QRect(self._details_rect)
self._title_rects.append(drawing_rect)
height += font_metrics_rect.height() + 10
drawing_rect = qtc.QRect(self._details_rect)
drawing_rect.moveTop(height)
self._title_rects.append(drawing_rect)
font_metrics_rect = qtg.QFontMetrics(self._title_font).boundingRect(
self._details_rect, qtc.Qt.TextFlag.TextWordWrap, self._details.subtitle, 0
)
drawing_rect = qtc.QRect(self._details_rect)
height += font_metrics_rect.height() - 3
drawing_rect.moveTop(height)
self._title_rects.append(drawing_rect)
font_metrics_rect = qtg.QFontMetrics(self._title_font).boundingRect(
self._details_rect,
qtc.Qt.TextFlag.TextWordWrap,
str(self._details.id),
0,
)
self._title_rects.append(drawing_rect)
height += font_metrics_rect.height() + 10
self._details_rect.moveTop(height)
self._redacted_details_text_document = qtg.QTextDocument()
self._redacted_details_text_document.setHtml(self._details.to_html())
# First set the width,
self._redacted_details_text_document.setTextWidth(self._details_rect.width())
# then get the height of the QTextDocument based on the given width and set
# that + the titles heights + bottom padding as the total height.
if (total_height:=height + self._redacted_details_text_document.size().height() + 10) > self.height():
self.setFixedHeight(total_height)
def _get_actual_geometry(self) -> qtc.QRect:
# Probably not needed for normal desktop environments with window
# managers but I'm an epik i3 user so self.geometry() does not work as
# intended when full screening the window with $mod + F. Or I'm just
# retarded and this is not even a problem.
geometry = self.geometry()
geometry.setTopLeft(qtc.QPoint(0, 0))
return geometry
def paintEvent(self, event: qtg.QPaintEvent) -> None:
total_height = 0
painter = qtg.QPainter(self)
painter.setRenderHint(qtg.QPainter.RenderHint.TextAntialiasing)
painter.drawImage(0, 0, self._thumbnail_image)
painter.save()
painter.setFont(self._title_font)
painter.drawText(
self._title_rects[0], qtc.Qt.TextFlag.TextWordWrap, self._details.title
)
painter.setFont(self._subtitle_font)
painter.drawText(
self._title_rects[1], qtc.Qt.TextFlag.TextWordWrap, self._details.subtitle
)
painter.setFont(self._id_font)
painter.drawText(
self._title_rects[2],
qtc.Qt.TextFlag.TextWordWrap,
str(self._details.id),
)
painter.translate(self._details_rect.topLeft())
painter.setFont(self._redacted_details_font)
self._redacted_details_text_document.drawContents(painter)
painter.restore()
app = qtw.QApplication()
widget = DetailsWidget()
widget.show()
app.exec()
I can display the text and the image next to each other just fine, but the text is not selectable. Looking around for a way to do so, I stumbled upon QGraphicsTextItem. Should I re-do the whole thing in a QGraphicsView instead of using the paintEvent on a QWidget? The reason I'm hesitant to do so is because I don't know of the cons of using a QGraphicsView, maybe it's a lot more resource heavy and not the best for this use case?

You're complicating things unnecessarily.
Just use a basic QHBoxLayout and two QLabels, with the one on the left for the image, and the one on the right for the details.
If you want to allow text selection, use QLabel.setTextInteractionFlags(Qt.TextSelectableByMouse).
An even better solution would be to use a QGraphicsView with a QGraphicsPixmapItem for the image (using fitInView() in the resizeEvent to always show it as large as possible) and a QTextEdit for the details, set in read only mode.
Note that your usage of _get_actual_geometry is wrong in principle (besides the fact that you're calling 4 times in a row, while you could just use a local variable instead), because when a widget has not been shown yet it always has a default size (100x30 for widgets created with a parent, otherwise 640x480), so not only you'll be getting a wrong geometry, but you're also changing it, since setTopLeft() will only move the corner, not translate the rectangle: if you want the basic rectangle of the widget, just use rect(). Obviously, if you properly use layouts as suggested above, this won't be necessary in the first place.

Related

Lock resize direction of QSizeGrip to Vertical/Horizontal [duplicate]

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;
""")

QDockWidget with QPixmap - how to prevent QPixmap limiting the downsizing of parent widget while maintaining aspect ration?

I haven't worked with images in labels for a long time so I'm stuck with an issue - once resized a QPixmap (loaded inside a QLabel or similar widget) cannot return to a smaller (downsized) version of itself. This is particularly annoying when working with docked widgets in a QMainWindow or similar setting:
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from random import seed
from random import random
class CentralWidget(QWidget):
def __init__(self):
QWidget.__init__(self)
vb_layout = QVBoxLayout()
self.label = QLabel('Central Widget')
self.label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
vb_layout.addWidget(self.label)
self.setLayout(vb_layout)
class DockedWidget(QDockWidget):
class Widget(QWidget):
def __init__(self):
QWidget.__init__(self)
vb_layout = QVBoxLayout()
self.label = QLabel()
# Enable scaled contents, otherwise enjoy artifacts and visual glitches
self.label.setScaledContents(True)
self.rimg = QImage(self.width(),self.height(), QImage.Format_Grayscale8)
self.rimg.fill(Qt.black)
print(self.rimg.width(), self.rimg.height())
for j in range(self.height()):
for i in range(self.width()):
r = round(random()* 255)
if r % 2 == 0:
self.rimg.setPixel(i, j, qRgb(255, 0, 0))
self.label.setPixmap(QPixmap.fromImage(self.rimg))
vb_layout.addWidget(self.label)
self.setLayout(vb_layout)
def resizeEvent(self, e: QResizeEvent) -> None:
super().resizeEvent(e)
preview = self.label.pixmap()
# FIXME Trying to figure out a way to scale image inside label up and down
self.label.setPixmap(preview.scaled(self.label.width(),self.label.height(),Qt.KeepAspectRatio))
def __init__(self):
QDockWidget.__init__(self)
self.setWindowTitle('Docked Widget')
self.widget = DockedWidget.Widget()
self.setWidget(self.widget)
class MyWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setGeometry(300, 100, 270, 100)
self.setWindowTitle('Test')
dockedwidget = DockedWidget()
self.addDockWidget(Qt.LeftDockWidgetArea, dockedwidget)
widget = CentralWidget()
self.setCentralWidget(widget)
seed(1)
app = QApplication([])
win = MyWindow()
win.show()
app.exec_()
I've tried to link the pixmap's scaling to the parent label, which in terms should be controlled by the behaviour of the docked widget. Initially I was facing the issue that the image would stretch and create weird artifacts:
I figured out I had to enable scaled contents (QLabel.setScaledContents()) but I'm still facing the issue that I cannot go below the initial size of the image:
Minimum size restricts resizing beyond the initially set image size
Increasing the size is not a problem
I need to make the image capable of downsizing properly, otherwise it compromises the rest of the components in the layout in my actual setup. I'm thinking that the solution lies somewhere between the resize event and the size policy.
QLabel does not provide support for aspect ratio, and it normally only allows to scale it to sizes bigger than the image size (but this can be worked around using setMinimumSize(1, 1)).
While you could create a subclass that actually paints the proper aspect ratio no matter of the widget size, it's often not necessary, as you could use a QGraphicsView and a basic pixmap item.
class ImageWidget(QGraphicsView):
def __init__(self):
super().__init__()
self.setFrameShape(0)
self.setStyleSheet('''
QGraphicsView {
background: transparent;
}
''')
rimg = QImage(640, 480, QImage.Format_Grayscale8)
rimg.fill(Qt.black)
for j in range(480):
for i in range(640):
r = round(random()* 255)
if r % 2 == 0:
rimg.setPixel(i, j, qRgb(255, 0, 0))
scene = QGraphicsScene()
self.setScene(scene)
self.pixmapItem = scene.addPixmap(QPixmap.fromImage(rimg))
def resizeEvent(self, event):
super().resizeEvent(event)
self.fitInView(self.pixmapItem, Qt.KeepAspectRatio)
class DockedWidget(QDockWidget):
def __init__(self):
QDockWidget.__init__(self)
self.setWindowTitle('Docked Widget')
self.widget = ImageWidget()
self.setWidget(self.widget)
Note: 1. use nested classes only when actually necessary, otherwise they only make the code cumbersome and difficult to read; 2. all widgets have a default size when created (640x480, or 100x30 for widgets created with a parent), so using self.width() and self.height() is pointless.

DIV tag equivalent in Qt Graphics Framework

I am working on a simple desktop application where I have to show a tree structure of folders and files along with other diagrams. For this I chose Qt and python (PySide). I need a structure like below (Forgive me for the bad drawing. But you get the idea):
The folders can be double clicked to expand/shrink. When a folder expands, new child elements need to take more space, and the folders below the current folder must move down. Similarly when the folder is shrunk, the folders below the current folder must come up; just like a standard folder system.
Hence I am in search of a <div> equivalent element in Qt where I can place each directory and all of its children inside that div and the div can expand and shrink. This way I don't have to write code for a re-draw every time the folder is opened/closed. Currently I have to calculate each item's position and place the child items respective to that position. That is a lot of calculation and no of items are > 1000. With a div, I will just re-calculate positions of child items and resize the div. Other divs can then automatically re-draw themselves.
I am not using QTreeView because as I said earlier, I have to draw other diagrams and connect these folders with them. QTreeView will live in its own space (with scroll bar and stuff), and I won't be able to draw lines to connect items in QTreeView and QGraphicsScene.
You can view my current work here in github. Here is the file that has my work.
I'm not sure what you're thinking of "<div>". It's just the most simple HTML container, and it seems to have nothing to do with your goal.
You can use graphics layouts to align items in the scene automatically. Here's how it can be implemented:
from PySide import QtGui, QtCore
class Leaf(QtGui.QGraphicsProxyWidget):
def __init__(self, path, folder = None):
QtGui.QGraphicsProxyWidget.__init__(self)
self.folder = folder
label = QtGui.QLabel()
label.setText(QtCore.QFileInfo(path).fileName())
self.setWidget(label)
self.setToolTip(path)
self.setAcceptedMouseButtons(QtCore.Qt.LeftButton)
def mousePressEvent(self, event):
if self.folder:
self.folder.toggleChildren()
class Folder(QtGui.QGraphicsWidget):
def __init__(self, path, isTopLevel = False):
QtGui.QGraphicsWidget.__init__(self)
self.offset = 32
childrenLayout = QtGui.QGraphicsLinearLayout(QtCore.Qt.Vertical)
childrenLayout.setContentsMargins(self.offset, 0, 0, 0)
flags = QtCore.QDir.AllEntries | QtCore.QDir.NoDotAndDotDot
for info in QtCore.QDir(path).entryInfoList(flags):
if info.isDir():
childrenLayout.addItem(Folder(info.filePath()))
else:
childrenLayout.addItem(Leaf(info.filePath()))
self.childrenWidget = QtGui.QGraphicsWidget()
self.childrenWidget.setLayout(childrenLayout)
mainLayout = QtGui.QGraphicsLinearLayout(QtCore.Qt.Vertical)
mainLayout.setContentsMargins(0, 0, 0, 0)
self.leaf = Leaf(path, self)
mainLayout.addItem(self.leaf)
mainLayout.addItem(self.childrenWidget)
if isTopLevel:
mainLayout.addStretch()
self.setLayout(mainLayout)
def paint(self, painter, option, widget):
QtGui.QGraphicsWidget.paint(self, painter, option, widget)
if self.childrenWidget.isVisible() and self.childrenWidget.layout().count() > 0:
lastChild = self.childrenWidget.layout().itemAt(self.childrenWidget.layout().count() - 1)
lastChildY = self.childrenWidget.geometry().top() + \
lastChild.geometry().top() + self.leaf.geometry().height() / 2;
painter.drawLine(self.offset / 2, self.leaf.geometry().bottom(), self.offset / 2, lastChildY)
for i in range(0, self.childrenWidget.layout().count()):
child = self.childrenWidget.layout().itemAt(i)
childY = self.childrenWidget.geometry().top() + \
child.geometry().top() + self.leaf.geometry().height() / 2
painter.drawLine(self.offset / 2, childY, self.offset, childY)
def toggleChildren(self):
if self.childrenWidget.isVisible():
self.layout().removeItem(self.childrenWidget)
self.childrenWidget.hide()
self.leaf.widget().setStyleSheet("QLabel { color : blue; }")
print "hide"
else:
self.childrenWidget.show()
self.layout().insertItem(1, self.childrenWidget)
self.leaf.widget().setStyleSheet("")
self.update()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
scene = QtGui.QGraphicsScene()
view = QtGui.QGraphicsView(scene)
# put your root path here
scene.addItem(Folder("/usr/share/alsa", True))
view.show()
view.resize(400, 400)
sys.exit(app.exec_())

In QDialog, resize window to contain all columns of QTableView

I am writing a simple program to display the contents of a SQL database table in a QDialog (PySide). The goal is to have a method that expands the window to show all of the columns, so the user doesn't have to resize to see everything. This problem was addressed in a slightly different context:
Fit width of TableView to width of content
Based on that, I wrote the following method:
def resizeWindowToColumns(self):
frameWidth = self.view.frameWidth() * 2
vertHeaderWidth = self.view.verticalHeader().width()
horizHeaderWidth =self.view.horizontalHeader().length()
vertScrollWidth = self.view.style().pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
fudgeFactor = 6 #not sure why this is needed
newWidth = frameWidth + vertHeaderWidth + horizHeaderWidth + vertScrollWidth + fudgeFactor
It works great. But notice that I have had to add a fudgeFactor. With fudge, it works perfectly. But it suggests I have lost track of six pixels, and am very curious where they are coming from. It doesn't seem to matter how many columns are displayed, or their individual widths: the fudgeFactor 6 always seems to work.
System details
Python 2.7 (Spyder/Anaconda). PySide version 1.2.2, Qt version 4.8.5. Windows 7 laptop with a touch screen (touch screens sometimes screw things up in PySide).
Full working example
# -*- coding: utf-8 -*-
import os
import sys
from PySide import QtGui, QtCore, QtSql
class DatabaseInspector(QtGui.QDialog):
def __init__(self, tableName, parent = None):
QtGui.QDialog.__init__(self, parent)
#define model
self.model = QtSql.QSqlTableModel(self)
self.model.setTable(tableName)
self.model.select()
#View of model
self.view = QtGui.QTableView()
self.view.setModel(self.model)
#Sizing
self.view.resizeColumnsToContents() #Resize columns to fit content
self.resizeWindowToColumns() #resize window to fit columns
#Quit button
self.quitButton = QtGui.QPushButton("Quit");
self.quitButton.clicked.connect(self.reject)
#Layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.view) #table view
layout.addWidget(self.quitButton) #pushbutton
self.setLayout(layout)
self.show()
def resizeEvent(self, event):
#This is just to see what's going on
print "Size set to ({0}, {1})".format(event.size().width(), event.size().height())
def resizeWindowToColumns(self):
#Based on: https://stackoverflow.com/a/20807145/1886357
frameWidth = self.view.frameWidth() * 2
vertHeaderWidth = self.view.verticalHeader().width()
horizHeaderWidth =self.view.horizontalHeader().length()
vertScrollWidth = self.view.style().pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
fudgeFactor = 6 #not sure why this is needed
newWidth = frameWidth + vertHeaderWidth + horizHeaderWidth + vertScrollWidth + fudgeFactor
if newWidth <= 500:
self.resize(newWidth, self.height())
else:
self.resize(500, self.height())
def populateDatabase():
print "Populating table in database..."
query = QtSql.QSqlQuery()
if not query.exec_("""CREATE TABLE favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
category VARCHAR(40) NOT NULL,
number INTEGER NOT NULL,
shortdesc VARCHAR(20) NOT NULL,
longdesc VARCHAR(80))"""):
print "Failed to create table"
return False
categories = ("Apples", "Chocolate chip cookies", "Favra beans")
numbers = (1, 2, 3)
shortDescs = ("Crispy", "Yummy", "Clarice?")
longDescs = ("Healthy and tasty", "Never not good...", "Awkward beans for you!")
query.prepare("""INSERT INTO favorites (category, number, shortdesc, longdesc)
VALUES (:category, :number, :shortdesc, :longdesc)""")
for category, number, shortDesc, longDesc in zip(categories, numbers, shortDescs, longDescs):
query.bindValue(":category", category)
query.bindValue(":number", number)
query.bindValue(":shortdesc", shortDesc)
query.bindValue(":longdesc", longDesc)
if not query.exec_():
print "Failed to populate table"
return False
return True
def main():
import site
app = QtGui.QApplication(sys.argv)
#Connect to/initialize database
dbName = "food.db"
tableName = "favorites"
site_pack_path = site.getsitepackages()[1]
QtGui.QApplication.addLibraryPath('{0}\\PySide\\plugins'.format(site_pack_path))
db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
fullFilePath = os.path.join(os.path.dirname(__file__), dbName) #;print fullFilePath
dbExists = QtCore.QFile.exists(fullFilePath) #does it already exist in directory?
db.setDatabaseName(fullFilePath)
db.open()
if not dbExists:
populateDatabase()
#Display database
dataTable = DatabaseInspector(tableName)
sys.exit(app.exec_())
#Close and delete database (not sure this is needed)
db.close()
del db
if __name__ == "__main__":
main()
The difference between the linked question and your example, is that the former is resizing a widget within a layout, whereas the latter is resizing a top-level window.
A top-level window is usually decorated with a frame. On your system, the width of this frame seems to be three pixels on each side, making six pixels in all.
You can calculate this value programmatically with:
self.frameSize().width() - self.width()
where self is the top-level window.
However, there may be an extra issue to deal with, and that is in choosing when to calculate this value. On my Linux system, the frame doesn't get drawn until the window is fully shown - so calculating during __init__ doesn't work.
I worked around that problem like this:
dataTable = DatabaseInspector(tableName)
dataTable.show()
QtCore.QTimer.singleShot(10, dataTable.resizeWindowToColumns)
but I'm not sure whether that's portable (or even necessarily the best way to do it).
PS:
It seems that the latter issue may be specific to X11 - see the Window Geometry section in the Qt docs.
UPDATE:
The above explanation and calculation is not correct!
The window decoration is only relevant when postioning windows. The resize() and setGeometry() functions always exclude the window frame, so it doesn't need to be factored in when calculating the total width.
The difference between resizing a widget within a layout versus resizing a top-level window, is that the latter needs to take account of the layout margin.
So the correct calculation is this:
margins = self.layout().contentsMargins()
self.resize((
margins.left() + margins.right() +
self.view.frameWidth() * 2 +
self.view.verticalHeader().width() +
self.view.horizontalHeader().length() +
self.view.style().pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
), self.height())
But note that this always allows room for a vertical scrollbar.
The example script doesn't add enough rows to show the vertical scrollbar, so it is misleading in that respect - if more rows are added, the total width is exactly right.

wxPython Solitaire GUI

I'm writing a Solitaire GUI using wxPython, and I'm on Windows 7. I've only written one GUI before (in Java Swing), so I'm not as familiar as I could be with all the different types of widgets and controls. I'm faced with the challenge of having resizable, cascading piles of cards in the Tableaux of the Solitaire board. To me, using BitmapButtons for each card (or at least for face-up cards) and having a panel contain a pile of cards seemed natural, since it is legal to move sub-piles of cards in the Tableau from pile to pile in Solitaire. I'm sure there is a better way to do this, but for now I've been fiddling with a smaller GUI (not my main GUI) to try and achieve this. I've attached the code for the test GUI below.
Note: My main GUI uses a GridBagSizer with 14 cells. I haven't tried using the following panel/buttons in the GridBagSizer, or even know if a GridBagSizer is the best way to go about this.
import wx
class MyFrame(wx.Frame):
def __init__(self, parent, id_, title):
wx.Frame.__init__(self, parent, id_, title, size=(810, 580))
self.panel = wx.Panel(self, size=(72, 320), pos=(20,155))
self.buttons = []
self.init_buttons()
def init_buttons(self):
for i in range(6):
face_down = wx.Image('img/cardback.png', wx.BITMAP_TYPE_PNG).ConvertToBitmap()
wid = face_down.GetWidth()
hgt = face_down.GetHeight()
bmpbtn = wx.BitmapButton(self.panel, -1, bitmap=face_down, pos=(20,155+7*i), size=(wid, hgt))
bmpbtn.Bind(wx.EVT_ENTER_WINDOW, self.onMouseOver)
self.buttons.append(bmpbtn)
for i in range(1,14):
rank = 14 - i
if i % 2 == 0:
filename = 'img/%sC.png' % rank
else:
filename = 'img/%sH.png' % rank
img = wx.Image(filename, wx.BITMAP_TYPE_PNG).ConvertToBitmap()
wid = img.GetWidth()
hgt = img.GetHeight()
bmpbtn = wx.BitmapButton(self.panel, -1, bitmap=img, pos=(20, 177+20*i), size=(wid, hgt))
bmpbtn.Bind(wx.EVT_ENTER_WINDOW, self.onMouseOver)
self.buttons.append(bmpbtn)
def onMouseOver(self, event):
#event.Skip()
pass
class MyApp(wx.App):
def OnInit(self):
wx.InitAllImageHandlers()
self.frame = MyFrame(None, -1, "Solitaire")
self.frame.Show(True)
self.SetTopWindow(self.frame)
return True
app = MyApp(0)
app.MainLoop()
This is what results from running:
http://oi44.tinypic.com/1zv4swj.jpg
Which I was satisfied with, until I moved my mouse over some of the buttons:
http://oi44.tinypic.com/2rdupmq.jpg
This must have to do with the EVT_ENTER_WINDOW event. I attempted to write an event handler, but realized I didn't really know how to achieve what I need. According to the docs, a BitmapButton has different bitmaps for each of its states - hover, focus, selected, inactive, etc. However, I do not want to change the Bitmap on a mouseover event. I simply want the button to stay put, and to not display itself on top of other buttons.
Any help would be greatly appreciated. Incidentally, if anybody has advice for a better way (than GridBagSizer and these panels of buttons) to implement this GUI, I would love that!
I would recommend against using actual window controls for each of the cards. I would instead have a single canvas upon which you render the card bitmaps in their appropriate locations. You'll have to do a little extra math to determine what cards are being clicked on, but this is definitely the way to go.
Use a wx.Panel with a EVT_PAINT handler to do your drawing.
Here's a starting point that is written to use double-buffering to avoid flickering.
P.S. You can use bitmap = wx.Bitmap(path) to load an image, instead of bothering with wx.Image and converting it to a bitmap object.
import wx
class Panel(wx.Panel):
def __init__(self, parent):
super(Panel, self).__init__(parent)
self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
def on_left_down(self, event):
print 'on_left_down', event.GetPosition()
def on_left_up(self, event):
print 'on_left_up', event.GetPosition()
def on_paint(self, event):
dc = wx.AutoBufferedPaintDC(self)
# Use dc.DrawBitmap(bitmap, x, y) to draw the cards here
class Frame(wx.Frame):
def __init__(self):
super(Frame, self).__init__(None)
self.SetTitle('My Title')
Panel(self)
def main():
app = wx.App()
frame = Frame()
frame.Center()
frame.Show()
app.MainLoop()
if __name__ == '__main__':
main()

Resources