In QDialog, resize window to contain all columns of QTableView
Asked Answered
G

1

2

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://mcmap.net/q/1072739/-qt-fit-width-of-tableview-to-width-of-content
        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()
Gonfalon answered 16/11, 2014 at 17:39 Comment(0)
E
2

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.

Earlearla answered 16/11, 2014 at 18:25 Comment(6)
Great! Did two things to cobble into solution. Will spread over two comments. First, to get the width right I did three things: a) Remove the factor of 2 from frameWidth, so we have frameWidth = self.view.frameWidth(). b) Use your trick: decoratorWidth = self.frameSize().width()-self.width(), and c) Include both widths in resizeWindowToColumns. (I'm not sure if the factor of 2 should be changed in previous post (perhaps it works just because decoratorWidth == frameWidth in previous examples?).Gonfalon
[comment continued] Second, to get it to work within init instead of having to do it in main, after calling show within init, first call QtGui.QApplication.processEvents() to push everything through the queue. Only then call the resize method (resizeWindowToColumns). Voila! It seems to work perfectly in my limited test cases.Gonfalon
while the above works, it still feels a little like cargo cult programming, in that I don't really understand what I'm doing. That 'Windows Geometry' page in the docs that people always refer to? How helpful is it, really? I find it mostly a frustrating tease. The attributes it invokes don't even appear in the code above. The docs say that to get values with the window frame included, use .x and .y. we don't use that at all in the above. It seems that whole section could use a major upgrade with examples that exhaustively explore what's going on in different scenarios. You can do it, ek :)Gonfalon
@neuronet. Sorry, but I don't see much wrong with that page. The x() and y() aren't relevant here, because they relate to the position of the window, as opposed to its dimensions. My code uses width() (which is mentioned) and frameSize() which is just a minor variant of frameGeometry() (also mentioned). Having said that, I'm still not entirely happy with my current calculation, so I may revisit it tomorrow.Earlearla
fair enough on x, y, my bad there. I do wish the documentation were more thorough on this. I always feel I'm finding some new attribute that should be on that page.Gonfalon
interesting....seems great...looks great in my test cases...Good catch with the scrollbar/#rows issue.Gonfalon

© 2022 - 2024 — McMap. All rights reserved.