5

I am trying to create a simple gui that displays the (memory) layout of some components of a device, but I am having a really hard time enforcing the policy I want to the displayed area.
Let me first show what I have so far (my code became quite large, but I changed/narrowed the code down to the minimal required for anyone to be able to run it):

#!/usr/local/bin/python3
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys

class Register(QGraphicsRectItem):

    RegisterSize = 125

    NameColor = QColor(Qt.blue)
    ValueColor = QColor(0, 154, 205)

    def __init__(self, name, value, pos, parent = None):
        super(Register, self).__init__(parent)
        self.setPos(pos)

        self.width = Register.RegisterSize
        self.height = 0

        self.set_register_name(name)
        self.set_register_value(value)

        self.setRect(0, 0, self.width, self.height)

    def set_register_name(self, name):
        self.text_item = QGraphicsTextItem(name, self)
        self.text_item.setDefaultTextColor(Register.NameColor)
        self.height += self.text_item.boundingRect().height()

    def set_register_value(self, value):
        self.value_item = QGraphicsTextItem(str(value), self)
        self.value_item.setDefaultTextColor(Register.ValueColor)
        self.value_item.setPos(self.text_item.boundingRect().bottomLeft())
        self.height += self.value_item.boundingRect().height()

class Title(QGraphicsTextItem):

    TitleFont = 'Times New Roman'
    TitleFontSize = 18
    TitleColor = QColor(Qt.red)

    def __init__(self, title, parent = None):
        super(Title, self).__init__(title, parent)

        self.setFont(QFont(Title.TitleFont, Title.TitleFontSize))
        self.setDefaultTextColor(Title.TitleColor)

class Component(QGraphicsItem):

    def __init__(self, parent = None):
        super(Component, self).__init__(parent)

        self.width = Register.RegisterSize * 4
        self.height = 0

        self.add_title()
        self.add_registers()

        self.rect = QRectF(0, 0, self.width, self.height)

    def add_title(self):
        self.title = Title('Component Layout', self)
        self.title.setPos((self.width - self.title.boundingRect().width()) / 2, 0)
        self.height += self.title.boundingRect().height()

    def add_registers(self):
        y_coor = self.height
        x_coor = 0
        for i in range(64):
            register = Register('register {0:d}'.format(i), i, QPointF(x_coor, y_coor), self)
            x_coor = ((i + 1) % 4) * Register.RegisterSize
            if (i + 1) % 4 == 0:
                y_coor += register.rect().height()

        self.height = y_coor

    def boundingRect(self):
        return self.rect.adjusted(-1, -1, 1, 1)

    def paint(self, painter, option, widget):
        pen = QPen(Qt.blue)
        painter.setPen(pen)
        painter.drawRect(self.rect)

class Device(QGraphicsItem):

    LeftMargin = 50
    RightMargin = 50

    TopMargin = 20
    BottomMargin = 20

    def __init__(self, parent = None):
        super(Device, self).__init__(parent)

        self.width = Device.LeftMargin + Device.RightMargin
        self.height = Device.TopMargin + Device.BottomMargin

        component = Component(self)
        component.setPos(QPointF(Device.LeftMargin, Device.TopMargin))
        self.width += component.boundingRect().width() 
        self.height += component.boundingRect().height()

        self.rect = QRectF(0, 0, self.width, self.height)

    def paint(self, painter, option, widget):
        pass

    def boundingRect(self):
        return self.rect.adjusted(-1, -1, 1, 1)

class MainForm(QDialog):

    def __init__(self, parent = None):
        super(MainForm, self).__init__(parent)

        self.scene = QGraphicsScene(parent)
        self.view = QGraphicsView(self)


        self.view.setScene(self.scene)
        self.scene.addItem(Device())
        self.resize(700, 900)


def run_app():
    app = QApplication(sys.argv)
    form = MainForm()
    form.show()
    app.exec_()

if __name__ == '__main__':
    run_app()

This code, when launched, displays the following:

enter image description here

I don't mind the vertical scrollbar, since I intend to add more Components to the Device, and they won't all fit, what bothers me is the horizontal scrollbar.
Why does it appear without me explicitly asking?
It's not like there's no room in the window for the QGraphicsView to display the content.

Moreover, I noticed that the horizontal (and vertical) scrollbars do not appear, when only the Component is added to the QGraphicsView:

self.scene.addItem(Component()) # << previously was self.scene.addItem(Device())

Now the scrollbars do not appear:
enter image description here

Also; when I instead change the following lines:

LeftMargin = 0  # previously was 50
RightMargin = 0  # previously was 50

TopMargin = 0 # previously was 20
BottomMargin = 0 # previously was 20

Scrollbars do not appear. (I probably crossed some boundary with these margins added?)
I know I can control the scrollbars policy with the QGraphicsView.setHorizontalScrollBarPolicy() to make the horizontal scrollbar always off, but that raises another problem: When there's no way to scroll right, the vertical scrollbar "steals" some of the pixels from the display, making Device.RightMargin != Device.LeftMargin. Also, I am curious about what's the size boundary above which the horizontal/vertical scrollbars appear.

So, this is the policy I want to enforce:

  • I want the displayed area to always have a minimum height of X pixels (regardless of Device()'s height), and for vertical scrollbar to appear only if the Device() height passes these X pixels boundary (I'll determine Device()'s height by summing all Component()s heights)

  • I want QGraphicsView to never show horizontal scrollbar (the width Device()'s width is fixed and independent of the number of Component()s).

  • Whenever vertical scrollbar is needed, I don't want it to take up pixels from my display area.
  • I want to know what is the boundary (in pixels) above which scrollbars will appear (when I don't specify scrollbar policy).

EDIT:
After playing with it a bit, I figured something:

  • The unwanted horizontal scroll bar appears only because the vertical one appears and steals some of the display space.

  • According to the doc, the default policy for the horizontal scroll bar is Qt::ScrollBarAsNeeded, which means: "shows a scroll bar when the content is too large to fit and not otherwise.", but it doesn't state what is considered "too large".
    When I played around with the margins (Device.TopMargin/Device.BottomMargin), I discovered that the vertical scroll bar appears (and consequently the horizontal one) when Device.boundingRect().height() crosses the 786 pixels boundary.
    I couldn't figure out where did this number came from or how to control it.

so.very.tired
  • 2,958
  • 4
  • 41
  • 69
  • 786 could stem from 900 - 100 (title bar) - 2*7 (window borders), just a guess though. Shouldn't your requirement "Whenever horizontal scrollbar is needed, I don't want it to take up pixels from my display area" be "Whenever the _vertical_ scrollbar..." (the presence of a horizontal bar was excluded by the previous bullet point)? Basically that boils down to checking for the presence of a vertical scroll bar on every redraw in order to increase the window width if necessary. It might by easier to use asymmetric left/right margins and make the vertical scrollbar _always_ display... – Tobias Kienzler Sep 20 '17 at 06:56
  • Yes, thanks, that's a typo (horizontal instead of vertical). Regarding your calculation - how do you know these measurements? (title bar, window borders, etc...) – so.very.tired Sep 20 '17 at 17:22
  • Mere guesses, but I'm pretty certain title bar height and window borders add up to the 114 pixels missing from your 900 pixels window height – Tobias Kienzler Sep 20 '17 at 18:01
  • @so.very.tired. So have you completely solved this problem now, or are there still some outstanding issues? – ekhumoro Sep 24 '17 at 19:03
  • @ekhumoro, see my comment to igrinis below – so.very.tired Sep 25 '17 at 16:23

1 Answers1

4

I believe you are looking for setFixedWidth() and setFixedHeight()

class MainForm(QDialog):

    def __init__(self, parent = None):
        super(MainForm, self).__init__(parent)

        self.scene = QGraphicsScene(parent)
        self.view = QGraphicsView(self)

        self.view.setScene(self.scene)
        self.scene.addItem(Device())
        self.resize(700, 900)
        self.view.setFixedWidth(650)   # <-
        self.view.setFixedHeight(500)  # <- these two lines will set Device dimensions 
        self.setFixedWidth(700)        # <- this will fix window width

When you set fixed width to view it must be greater than its content (left margin + Device + right margin), otherwise horizontal scroll bar will be displayed. This is why you did not get the horizontal scroll bar when margins were zero.

Generally, the scrollbar will appear when your current view can't display the content.

The vertical scroll bar will take some space from inside the window, and I believe that you do not have control over that, so you should reserve some place for that, too. The behavior of vertical scroll bar depends on your windows system, e.g. on Mac it hovers over and disappear when unneeded, so it does not takes space at all.

I recommend to do the layout in QT Designer. I find it much easier to do it visually, testing it immediately and only introduce small changes in the generated code.

igrinis
  • 12,398
  • 20
  • 45
  • Thanks. I ended up overriding `sizeHint()` to `return QSize(self.sceneRect().width() + 16, self.sceneRect().height())`. and setting the horizontal size policy to 'Fixed'. (any number below 16 made the vertical scroll bar to appear for some reason), and while I hate using such 'magic numbers' in my code, I convince myself that 16 is exactly the width of the vertical scroll bar (14) + 1 pixels margin from each side. – so.very.tired Sep 22 '17 at 14:52
  • @so.very.tired. To get the width of a vertical scroll-bar, or the height of a horizontal scroll-bar, try: `qApp.style().pixelMetric(QStyle.PM_ScrollBarExtent)`. – ekhumoro Sep 24 '17 at 19:22